From 7fd221da85a36227ae465f166174e3c72683842f Mon Sep 17 00:00:00 2001
From: David Douard <david.douard@sdfa3.org>
Date: Tue, 5 Nov 2024 10:49:32 +0100
Subject: [PATCH] cli/db: show the actual error that occurred during sql script
 execution, if any

When executing a `swh db init` command, the executed sql files for the
package may fail, but the 'swh db' tool would eat the error reported by
sqlsh. Make these errors shown as output of the process if such an error
occurred.

e.g.:

  $ swh db init test:fail

  Error during database setup
  Command '['psql', '--quiet', '--no-psqlrc', '-v', 'ON_ERROR_STOP=1', '-d', 'postgresql://postgres@127.0.0.1:24370/tests', '-f', '/venv/swh-environment/swh-core/swh/core/db/tests/data/test/fail/sql/40-funcs.sql']' returned non-zero exit status 3.
  Process output:
  psql:/venv/swh-environment/swh-core/swh/core/db/tests/data/test/fail/sql/40-funcs.sql:6: ERROR:  function public.digest(text, unknown) does not exist
  LINE 5:     select encode(public.digest($1, 'sha1'), 'hex')
                          ^
  HINT:  No function matches the given name and argument types. You might need to add explicit type casts.

  Aborted!
---
 swh/core/cli/db.py                            | 15 +++++++--
 swh/core/db/tests/data/README                 |  3 ++
 swh/core/db/tests/data/test/fail/__init__.py  |  0
 .../db/tests/data/test/fail/sql/30-schema.sql |  6 ++++
 .../db/tests/data/test/fail/sql/40-funcs.sql  |  6 ++++
 swh/core/db/tests/test_cli.py                 | 33 +++++++++++++++----
 6 files changed, 53 insertions(+), 10 deletions(-)
 mode change 100755 => 100644 swh/core/cli/db.py
 create mode 100644 swh/core/db/tests/data/test/fail/__init__.py
 create mode 100644 swh/core/db/tests/data/test/fail/sql/30-schema.sql
 create mode 100644 swh/core/db/tests/data/test/fail/sql/40-funcs.sql

diff --git a/swh/core/cli/db.py b/swh/core/cli/db.py
old mode 100755
new mode 100644
index 16c7e822..7b9d5ce1
--- a/swh/core/cli/db.py
+++ b/swh/core/cli/db.py
@@ -6,6 +6,7 @@
 
 import logging
 from os import environ
+from subprocess import CalledProcessError
 from typing import Any, Dict, List, Optional, Tuple
 import warnings
 
@@ -340,9 +341,17 @@ def initialize_one(package, module, backend_class, flavor, dbname, cfg):
 
     logger.debug("db_init %s flavor=%s dbname=%s", module, flavor, dbname)
     dbmodule = f"{package}:{cfg['cls']}"
-    initialized, dbversion, dbflavor = populate_database_for_package(
-        dbmodule, dbname, flavor
-    )
+    try:
+        initialized, dbversion, dbflavor = populate_database_for_package(
+            dbmodule, dbname, flavor
+        )
+    except CalledProcessError as exc:
+        click.secho("Error during database setup", fg="red", bold=True)
+        click.echo(str(exc))
+        click.echo("Process output:")
+        click.echo(exc.stderr)
+        raise click.Abort()
+
     if dbversion is not None:
         click.secho(
             "ERROR: the database version has been populated by sql init scripts. "
diff --git a/swh/core/db/tests/data/README b/swh/core/db/tests/data/README
index 27ce3810..1694a355 100644
--- a/swh/core/db/tests/data/README
+++ b/swh/core/db/tests/data/README
@@ -23,3 +23,6 @@ test:
 
 will define 2 'swh.core.db' powered backends initialized from (resp.)
 './postgresql/sql' and './cli2/sql'.
+
+The 'test:fail' backend is expected to fail when trying to initialize the
+database (missing the pgcrypto extension, so the psql execution should fail).
diff --git a/swh/core/db/tests/data/test/fail/__init__.py b/swh/core/db/tests/data/test/fail/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/swh/core/db/tests/data/test/fail/sql/30-schema.sql b/swh/core/db/tests/data/test/fail/sql/30-schema.sql
new file mode 100644
index 00000000..64289f78
--- /dev/null
+++ b/swh/core/db/tests/data/test/fail/sql/30-schema.sql
@@ -0,0 +1,6 @@
+-- origin table
+create table if not exists origin (
+  id       bigserial not null,
+  url      text not null,
+  hash     text not null
+);
diff --git a/swh/core/db/tests/data/test/fail/sql/40-funcs.sql b/swh/core/db/tests/data/test/fail/sql/40-funcs.sql
new file mode 100644
index 00000000..d4dd410b
--- /dev/null
+++ b/swh/core/db/tests/data/test/fail/sql/40-funcs.sql
@@ -0,0 +1,6 @@
+create or replace function hash_sha1(text)
+    returns text
+    language sql strict immutable
+as $$
+    select encode(public.digest($1, 'sha1'), 'hex')
+$$;
diff --git a/swh/core/db/tests/test_cli.py b/swh/core/db/tests/test_cli.py
index b08ddbdb..4cb8ce43 100644
--- a/swh/core/db/tests/test_cli.py
+++ b/swh/core/db/tests/test_cli.py
@@ -3,8 +3,6 @@
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
-from subprocess import CalledProcessError
-
 import pytest
 from pytest_postgresql import factories
 import yaml
@@ -117,8 +115,7 @@ def test_cli_swh_db_initialization_fail_without_creation_first(
     result = cli_runner.invoke(swhdb, ["init", module_name, "--dbname", conninfo])
     # Fails because we cannot connect to an inexisting db
     assert result.exit_code == 1, f"Unexpected output: {result.output}"
-    assert isinstance(result.exception, CalledProcessError)
-    assert b'FATAL:  database "inexisting-db" does not exist' in result.exception.stderr
+    assert 'FATAL:  database "inexisting-db" does not exist' in result.output
 
 
 def test_cli_swh_db_initialization_fail_without_extension(
@@ -136,10 +133,8 @@ def test_cli_swh_db_initialization_fail_without_extension(
     # Fails as the function `public.digest` is not installed, init-admin calls is needed
     # first (the next tests show such behavior)
     assert result.exit_code == 1, f"Unexpected output: {result.output}"
-    assert isinstance(result.exception, CalledProcessError)
     assert (
-        b"ERROR:  function public.digest(text, unknown) does not exist"
-        in result.exception.stderr
+        "ERROR:  function public.digest(text, unknown) does not exist" in result.output
     )
 
 
@@ -263,6 +258,30 @@ def test_cli_swh_db_create_and_init_db_new_api(
         assert len(origins) == 1
 
 
+def test_cli_swh_db_init_report_sqlsh_error(
+    cli_runner,
+    postgresql,
+    mock_get_entry_points,
+    mocker,
+    tmp_path,
+):
+    """Create a db then initializing it should be ok for a "new style" datastore"""
+    module_name = "test:fail"
+
+    conninfo = craft_conninfo(postgresql)
+
+    # This initializes the schema and data
+    result = cli_runner.invoke(swhdb, ["init-admin", module_name, "--dbname", conninfo])
+    assert_result(result)
+
+    result = cli_runner.invoke(swhdb, ["init", module_name, "--dbname", conninfo])
+    assert result.exit_code == 1
+    assert (
+        "test/fail/sql/40-funcs.sql:6: "
+        "ERROR:  function public.digest(text, unknown) does not exist"
+    ) in result.output
+
+
 @pytest.mark.init_version(version=2)
 def test_cli_swh_db_upgrade_new_api(
     request,
-- 
GitLab