Repository: ploomber/jupysql
Pull request #833 (2023-08-25 04:58:31)

clearer error messages if a connection fails to initialize
## Describe your changes

- improved the error messages when a connection fails to initialize
- also updated `_testing.py` to make it simpler to start a single container (instead of all of them)

## Issue number

Closes #X

## Checklist before requesting a review

- [ ] Performed a self-review of my code
- [ ] Formatted my code with [`pkgmt format`](https://ploomber-contributing.readthedocs.io/en/latest/contributing/pr.html#linting-formatting)
- [ ] Added [tests](https://ploomber-contributing.readthedocs.io/en/latest/contributing/pr.html#testing) (when necessary).
- [ ] Added [docstring](https://ploomber-contributing.readthedocs.io/en/latest/contributing/pr.html#documenting-changes-and-new-features) documentation and update the [changelog](https://ploomber-contributing.readthedocs.io/en/latest/contributing/pr.html#changelog) (when needed)



<!-- readthedocs-preview jupysql start -->
----
:books: Documentation preview :books:: https://jupysql--833.org.readthedocs.build/en/833/

<!-- readthedocs-preview jupysql end -->

neelasha23: Is this expected behaviour? I have uninstalled `duckdb-engine` but I think the error message should come up only if I'm trying to access DuckDB? 
<img width="900" alt="Screenshot 2023-08-25 at 5 52 36 PM" src="https://github.com/ploomber/jupysql/assets/29096135/4b9412ba-bccd-4e06-98ce-36c6ee703f12">

edublancas: > Is this expected behaviour? I have uninstalled `duckdb-engine` but I think the error message should come up only if I'm trying to access DuckDB? <img alt="Screenshot 2023-08-25 at 5 52 36 PM" width="900" src="https://user-images.githubusercontent.com/29096135/263267661-4b9412ba-bccd-4e06-98ce-36c6ee703f12.png">

looks like you have a default connection in your ini file. that's why it's trying to connect to it. I fixed that in https://github.com/ploomber/jupysql/commit/82be1d276977fa7901334e809de194cc5f5aacc9 so the error is clearer
neelasha23: CI is failing @edublancas 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30bd8e6..3867b46 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@
 
 * [Feature] Automatically connect if the `dsn_filename` (defaults to `~/.jupysql/connections.ini`) contains a `default` section
 * [Feature] Add `%sqlcmd connect` to see existing connections and create new ones (#632)
+* [Fix] Clearer error messages when failing to initialize a connection
+* [Doc] Added section on installing database drivers
 * [Fix] Improve error when passing a non-identifier to start a connection (#764)
 * [Fix] Display a warning (instead of raising an error) if the `default` connection in the `.ini` file cannot start
 * [Fix] Display a message instead of an error when `toml` isn't installed and `pyproject.toml` is found (#825)
diff --git a/doc/_toc.yml b/doc/_toc.yml
index 9038e8e..fc0c0f1 100644
--- a/doc/_toc.yml
+++ b/doc/_toc.yml
@@ -37,11 +37,11 @@ parts:
       - file: integrations/mariadb
       - file: integrations/clickhouse
       - file: integrations/mindsdb
-      - file: integrations/compatibility
       - file: integrations/questdb
       - file: integrations/oracle
       - file: integrations/trinodb
       - file: integrations/duckdb-native
+      - file: integrations/compatibility
 
   - caption: API Reference
     chapters:
@@ -66,6 +66,7 @@ parts:
     - file: howto/py-scripts
     - file: howto/interactive
     - file: howto/testing-columns
+    - file: howto/db-drivers
 
   - caption: Tutorials
     chapters:
diff --git a/doc/howto/db-drivers.md b/doc/howto/db-drivers.md
new file mode 100644
index 0000000..52a0d3e
--- /dev/null
+++ b/doc/howto/db-drivers.md
@@ -0,0 +1,28 @@
+# Install database drivers
+
+## DuckDB
+
+To connect to a DuckDB database, install `duckdb-engine`:
+
+```sh
+%pip install duckdb-engine --quiet
+```
+
+## PostgreSQL
+
+We recommend using `psycopg2` to connect to a PostgreSQL database. The most reliable
+way to install it is via `conda`:
+
+```sh
+# run this in your notebook
+%conda install psycopg2 -c conda-forge --yes --quiet
+```
+
+If you don't have conda, you can install it with `pip`:
+
+```sh
+# run this in your notebook
+%pip install psycopg2-binary --quiet
+```
+
+Once installed, restart the kernel.
\ No newline at end of file
diff --git a/src/sql/connection/connection.py b/src/sql/connection/connection.py
index e0831af..3b0e95f 100644
--- a/src/sql/connection/connection.py
+++ b/src/sql/connection/connection.py
@@ -32,14 +32,18 @@ from sql.parse import (
 )
 from sql.warnings import JupySQLQuotedNamedParametersWarning, JupySQLRollbackPerformed
 from sql import _current
+from sql.connection import error_handling
 
+BASE_DOC_URL = "https://jupysql.ploomber.io/en/latest"
+
+
+PLOOMBER_DOCS_LINK_STR = f"{BASE_DOC_URL}/connecting.html"
 
-PLOOMBER_DOCS_LINK_STR = (
-    "Documentation: https://jupysql.ploomber.io/en/latest/connecting.html"
-)
 IS_SQLALCHEMY_ONE = int(sqlalchemy.__version__.split(".")[0]) == 1
 
 # Check Full List: https://docs.sqlalchemy.org/en/20/dialects
+
+
 MISSING_PACKAGE_LIST_EXCEPT_MATCHERS = {
     # SQLite
     "sqlite": "sqlite",
@@ -67,21 +71,28 @@ MISSING_PACKAGE_LIST_EXCEPT_MATCHERS = {
     # MSSQL
     "pyodbc": "pyodbc",
     "pymssql": "pymssql",
+    # snowflake
+    "snowflake": "snowflake-sqlalchemy",
+}
+
+BASE_DRIVERS_URL = f"{BASE_DOC_URL}/howto/db-drivers.html"
+
+DBNAME_2_DOC_LINK = {
+    "psycopg2": f"{BASE_DRIVERS_URL}#postgresql",
+    "duckdb": f"{BASE_DRIVERS_URL}#duckdb",
 }
 
 DIALECT_NAME_SQLALCHEMY_TO_SQLGLOT_MAPPING = {"postgresql": "postgres", "mssql": "tsql"}
 
 # All the DBs and their respective documentation links
 DB_DOCS_LINKS = {
-    "duckdb": "https://jupysql.ploomber.io/en/latest/integrations/duckdb.html",
-    "mysql": "https://jupysql.ploomber.io/en/latest/integrations/mysql.html",
-    "mssql": "https://jupysql.ploomber.io/en/latest/integrations/mssql.html",
-    "mariadb": "https://jupysql.ploomber.io/en/latest/integrations/mariadb.html",
-    "clickhouse": "https://jupysql.ploomber.io/en/latest/integrations/clickhouse.html",
-    "postgresql": (
-        "https://jupysql.ploomber.io/en/latest/integrations/postgres-connect.html"
-    ),
-    "questdb": "https://jupysql.ploomber.io/en/latest/integrations/questdb.html",
+    "duckdb": f"{BASE_DOC_URL}/integrations/duckdb.html",
+    "mysql": f"{BASE_DOC_URL}/integrations/mysql.html",
+    "mssql": f"{BASE_DOC_URL}/integrations/mssql.html",
+    "mariadb": f"{BASE_DOC_URL}/integrations/mariadb.html",
+    "clickhouse": f"{BASE_DOC_URL}/integrations/clickhouse.html",
+    "postgresql": f"{BASE_DOC_URL}/integrations/postgres-connect.html",
+    "questdb": f"{BASE_DOC_URL}/integrations/questdb.html",
 }
 
 
@@ -125,30 +136,53 @@ class ResultSetCollection:
 
 
 def get_missing_package_suggestion_str(e):
+    """Provide a better error when a user tries to connect to a database but they're
+    missing the database driver
+    """
     suggestion_prefix = "To fix it, "
+
     module_name = None
+
     if isinstance(e, ModuleNotFoundError):
         module_name = extract_module_name_from_ModuleNotFoundError(e)
     elif isinstance(e, NoSuchModuleError):
         module_name = extract_module_name_from_NoSuchModuleError(e)
 
     module_name = module_name.lower()
-    # Excepted
-    for matcher, suggested_package in MISSING_PACKAGE_LIST_EXCEPT_MATCHERS.items():
-        if matcher == module_name:
-            return suggestion_prefix + "try to install package: " + suggested_package
-    # Closely matched
-    close_matches = difflib.get_close_matches(
-        module_name, MISSING_PACKAGE_LIST_EXCEPT_MATCHERS.keys()
-    )
-    if close_matches:
-        return f'Perhaps you meant to use driver the dialect: "{close_matches[0]}"'
-    # Not found
-    return (
+
+    error_message = (
         suggestion_prefix + "make sure you are using correct driver name:\n"
         "Ref: https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls"
     )
 
+    # Exact match
+    suggested_package = MISSING_PACKAGE_LIST_EXCEPT_MATCHERS.get(module_name)
+
+    if suggested_package:
+        error_message = (
+            suggestion_prefix
+            + "run this in your notebook: "
+            + error_handling.install_command(suggested_package)
+        )
+    else:
+        # Closely matched
+        close_matches = difflib.get_close_matches(
+            module_name, MISSING_PACKAGE_LIST_EXCEPT_MATCHERS.keys()
+        )
+
+        if close_matches:
+            error_message = (
+                f'Perhaps you meant to use driver the dialect: "{close_matches[0]}"'
+            )
+
+    error_suffix = (
+        DBNAME_2_DOC_LINK.get(module_name)
+        if DBNAME_2_DOC_LINK.get(module_name)
+        else PLOOMBER_DOCS_LINK_STR
+    )
+
+    return error_message + "\n\nFor more details, see: " + error_suffix
+
 
 def rough_dict_get(dct, sought, default=None):
     """
@@ -385,13 +419,7 @@ class ConnectionManager:
         except (ModuleNotFoundError, NoSuchModuleError) as e:
             suggestion_str = get_missing_package_suggestion_str(e)
             raise exceptions.MissingPackageError(
-                "\n\n".join(
-                    [
-                        str(e),
-                        suggestion_str,
-                        PLOOMBER_DOCS_LINK_STR,
-                    ]
-                )
+                "\n\n".join([str(e), suggestion_str])
             ) from e
         except Exception as e:
             raise _error_invalid_connection_info(e, connect_str) from e
@@ -1099,7 +1127,7 @@ def _suggest_fix(env_var, connect_str=None):
     if len(options) >= 3:
         options.insert(-1, "OR")
 
-    options.append(PLOOMBER_DOCS_LINK_STR)
+    options.append(f"For more details, see: {PLOOMBER_DOCS_LINK_STR}")
 
     return "\n\n".join(options)
 
diff --git a/src/sql/connection/error_handling.py b/src/sql/connection/error_handling.py
new file mode 100644
index 0000000..1150fff
--- /dev/null
+++ b/src/sql/connection/error_handling.py
@@ -0,0 +1,18 @@
+import shutil
+
+
+_CONDA_INSTALLED = shutil.which("conda") is not None
+_PREFER_CONDA = {"psycopg2"}
+
+
+def install_command(package):
+    # special case for psycopg2
+    if package == "psycopg2" and not _CONDA_INSTALLED:
+        package = "psycopg2-binary"
+
+    if _CONDA_INSTALLED and package in _PREFER_CONDA:
+        template = "%conda install {package} -c conda-forge --yes --quiet"
+    else:
+        template = "%pip install {package} --quiet"
+
+    return template.format(package=package)


Test patch:
diff --git a/src/sql/_testing.py b/src/sql/_testing.py
index 297745b..3163a77 100644
--- a/src/sql/_testing.py
+++ b/src/sql/_testing.py
@@ -1,3 +1,4 @@
+import argparse
 import os
 from contextlib import contextmanager
 import sys
@@ -287,11 +288,16 @@ def get_docker_client():
 
 @contextmanager
 @requires(["docker", "dockerctx"])
-def postgres(is_bypass_init=False):
+def postgres(is_bypass_init=False, print_credentials=False):
     if is_bypass_init:
         yield None
         return
+
     db_config = DatabaseConfigHelper.get_database_config("postgreSQL")
+
+    if print_credentials:
+        print(db_config)
+
     try:
         client = get_docker_client()
         container = client.containers.get(db_config["docker_ct"]["name"])
@@ -320,11 +326,16 @@ def postgres(is_bypass_init=False):
 
 @contextmanager
 @requires(["docker", "dockerctx"])
-def mysql(is_bypass_init=False):
+def mysql(is_bypass_init=False, print_credentials=False):
     if is_bypass_init:
         yield None
         return
+
     db_config = DatabaseConfigHelper.get_database_config("mySQL")
+
+    if print_credentials:
+        print(db_config)
+
     try:
         client = get_docker_client()
         container = client.containers.get(db_config["docker_ct"]["name"])
@@ -361,11 +372,16 @@ def mysql(is_bypass_init=False):
 
 @contextmanager
 @requires(["docker", "dockerctx"])
-def mariadb(is_bypass_init=False):
+def mariadb(is_bypass_init=False, print_credentials=False):
     if is_bypass_init:
         yield None
         return
+
     db_config = DatabaseConfigHelper.get_database_config("mariaDB")
+
+    if print_credentials:
+        print(db_config)
+
     try:
         client = get_docker_client()
         curr = client.containers.get(db_config["docker_ct"]["name"])
@@ -402,11 +418,16 @@ def mariadb(is_bypass_init=False):
 
 @contextmanager
 @requires(["docker", "dockerctx"])
-def mssql(is_bypass_init=False):
+def mssql(is_bypass_init=False, print_credentials=False):
     if is_bypass_init:
         yield None
         return
+
     db_config = DatabaseConfigHelper.get_database_config("MSSQL")
+
+    if print_credentials:
+        print(db_config)
+
     try:
         client = get_docker_client()
         curr = client.containers.get(db_config["docker_ct"]["name"])
@@ -436,11 +457,16 @@ def mssql(is_bypass_init=False):
 
 @contextmanager
 @requires(["docker", "dockerctx"])
-def oracle(is_bypass_init=False):
+def oracle(is_bypass_init=False, print_credentials=False):
     if is_bypass_init:
         yield None
         return
+
     db_config = DatabaseConfigHelper.get_database_config("oracle")
+
+    if print_credentials:
+        print(db_config)
+
     try:
         client = get_docker_client()
         curr = client.containers.get(db_config["docker_ct"]["name"])
@@ -464,11 +490,16 @@ def oracle(is_bypass_init=False):
 
 @contextmanager
 @requires(["docker", "dockerctx"])
-def clickhouse(is_bypass_init=False):
+def clickhouse(is_bypass_init=False, print_credentials=False):
     if is_bypass_init:
         yield None
         return
+
     db_config = DatabaseConfigHelper.get_database_config("clickhouse")
+
+    if print_credentials:
+        print(db_config)
+
     try:
         client = get_docker_client()
         curr = client.containers.get(db_config["docker_ct"]["name"])
@@ -490,9 +521,28 @@ def clickhouse(is_bypass_init=False):
 
 
 def main():
-    print("Starting test containers...")
-    with postgres(), mysql(), mariadb(), mssql(), oracle(), clickhouse():
+    available_databases = [
+        "postgres",
+        "mysql",
+        "mariadb",
+        "mssql",
+        "oracle",
+        "clickhouse",
+    ]
+
+    parser = argparse.ArgumentParser(description="Start database containers")
+    parser.add_argument(
+        "database",
+        choices=available_databases,
+        help="database to start",
+    )
+
+    args = parser.parse_args()
+    fn = globals()[args.database]
+
+    with fn(print_credentials=True):
         print("Press CTRL+C to exit")
+
         try:
             while True:
                 time.sleep(5)
diff --git a/src/tests/test_connection.py b/src/tests/test_connection.py
index 3dd170d..3dccfe1 100644
--- a/src/tests/test_connection.py
+++ b/src/tests/test_connection.py
@@ -13,6 +13,7 @@ from sqlalchemy import create_engine
 from sqlalchemy.engine import Engine
 from sqlalchemy import exc
 
+from sql.connection import connection as connection_module
 import sql.connection
 from sql.connection import (
     SQLAlchemyConnection,
@@ -23,6 +24,7 @@ from sql.connection import (
     ResultSetCollection,
 )
 from sql.warnings import JupySQLRollbackPerformed
+from sql.connection import error_handling
 
 
 @pytest.fixture
@@ -241,41 +243,139 @@ def test_missing_duckdb_dependencies(cleanup, monkeypatch):
 
 
 @pytest.mark.parametrize(
-    "missing_pkg, except_missing_pkg_suggestion, connect_str",
+    "connect_str, pkg_missing, pkg_in_install_command",
     [
         # MySQL + MariaDB
-        ["pymysql", "pymysql", "mysql+pymysql://"],
-        ["mysqlclient", "mysqlclient", "mysql+mysqldb://"],
-        ["mariadb", "mariadb", "mariadb+mariadbconnector://"],
-        ["mysql-connector-python", "mysql-connector-python", "mysql+mysqlconnector://"],
-        ["asyncmy", "asyncmy", "mysql+asyncmy://"],
-        ["aiomysql", "aiomysql", "mysql+aiomysql://"],
-        ["cymysql", "cymysql", "mysql+cymysql://"],
-        ["pyodbc", "pyodbc", "mysql+pyodbc://"],
+        ("mysql+pymysql://", "pymysql", "pymysql"),
+        ("mysql+mysqldb://", "mysqlclient", "mysqlclient"),
+        ("mariadb+mariadbconnector://", "mariadb", "mariadb"),
+        ("mysql+mysqlconnector://", "mysql-connector-python", "mysql-connector-python"),
+        ("mysql+asyncmy://", "asyncmy", "asyncmy"),
+        ("mysql+aiomysql://", "aiomysql", "aiomysql"),
+        ("mysql+cymysql://", "cymysql", "cymysql"),
+        ("mysql+pyodbc://", "pyodbc", "pyodbc"),
         # PostgreSQL
-        ["psycopg2", "psycopg2", "postgresql+psycopg2://"],
-        ["psycopg", "psycopg", "postgresql+psycopg://"],
-        ["pg8000", "pg8000", "postgresql+pg8000://"],
-        ["asyncpg", "asyncpg", "postgresql+asyncpg://"],
-        ["psycopg2cffi", "psycopg2cffi", "postgresql+psycopg2cffi://"],
+        ("postgresql://", "psycopg2", "psycopg2"),
+        ("postgresql+psycopg2://", "psycopg2", "psycopg2"),
+        ("postgresql+psycopg://", "psycopg", "psycopg"),
+        ("postgresql+pg8000://", "pg8000", "pg8000"),
+        ("postgresql+asyncpg://", "asyncpg", "asyncpg"),
+        ("postgresql+psycopg2cffi://", "psycopg2cffi", "psycopg2cffi"),
         # Oracle
-        ["cx_oracle", "cx_oracle", "oracle+cx_oracle://"],
-        ["oracledb", "oracledb", "oracle+oracledb://"],
+        ("oracle+cx_oracle://", "cx_oracle", "cx_oracle"),
+        ("oracle+oracledb://", "oracledb", "oracledb"),
         # MSSQL
-        ["pyodbc", "pyodbc", "mssql+pyodbc://"],
-        ["pymssql", "pymssql", "mssql+pymssql://"],
+        ("mssql+pyodbc://", "pyodbc", "pyodbc"),
+        ("mssql+pymssql://", "pymssql", "pymssql"),
+    ],
+)
+def test_error_when_missing_driver(
+    connect_str, pkg_missing, pkg_in_install_command, monkeypatch
+):
+    # psycopg2 returns %conda install if conda is installed
+    monkeypatch.setattr(error_handling, "_CONDA_INSTALLED", False)
+
+    with patch.dict(sys.modules):
+        sys.modules[pkg_missing] = None
+
+        with pytest.raises(UsageError) as excinfo:
+            ConnectionManager.from_connect_str(connect_str)
+
+    assert excinfo.value.error_type == "MissingPackageError"
+    expected = f"run this in your notebook: %pip install {pkg_in_install_command}"
+    assert expected in str(excinfo.value)
+
+
+@pytest.mark.parametrize(
+    "connect_str, dialect, pkg_in_install_command",
+    [
+        ("duckdb://", "duckdb", "duckdb-engine"),
+        ("snowflake://", "snowflake", "snowflake-sqlalchemy"),
+    ],
+)
+def test_error_when_cannot_load_plugin(
+    connect_str, dialect, pkg_in_install_command, monkeypatch
+):
+    mock = Mock(
+        side_effect=exc.NoSuchModuleError(
+            f"Can't load plugin: sqlalchemy.dialects:{dialect}"
+        )
+    )
+    monkeypatch.setattr(connection_module.sqlalchemy, "create_engine", mock)
+
+    with pytest.raises(UsageError) as excinfo:
+        ConnectionManager.from_connect_str(connect_str)
+
+    assert excinfo.value.error_type == "MissingPackageError"
+    expected = f"run this in your notebook: %pip install {pkg_in_install_command}"
+    assert expected in str(excinfo.value)
+
+
+@pytest.mark.parametrize(
+    "missing_pkg, except_missing_pkg_suggestion, connect_str",
+    [
+        ("psycopg2", "psycopg2", "postgresql+psycopg2://"),
     ],
 )
-def test_missing_driver(
-    missing_pkg, except_missing_pkg_suggestion, connect_str, monkeypatch
+def test_error_when_missing_driver_with_conda(
+    monkeypatch, missing_pkg, except_missing_pkg_suggestion, connect_str
 ):
+    # psycopg2 returns %conda install if conda is installed
+    monkeypatch.setattr(error_handling, "_CONDA_INSTALLED", True)
+
     with patch.dict(sys.modules):
         sys.modules[missing_pkg] = None
+
         with pytest.raises(UsageError) as excinfo:
             ConnectionManager.from_connect_str(connect_str)
 
         assert excinfo.value.error_type == "MissingPackageError"
-        assert "try to install package: " + missing_pkg in str(excinfo.value)
+        expected = f"run this in your notebook: %conda install {missing_pkg}"
+        assert expected in str(excinfo.value)
+
+
+@pytest.mark.parametrize(
+    "missing_pkg, section_name, connect_str",
+    [
+        ("psycopg2", "postgresql", "postgresql+psycopg2://"),
+    ],
+)
+def test_error_shows_link_to_installation_instructions_when_missing_package(
+    monkeypatch, missing_pkg, section_name, connect_str
+):
+    with patch.dict(sys.modules):
+        sys.modules[missing_pkg] = None
+
+        with pytest.raises(UsageError) as excinfo:
+            ConnectionManager.from_connect_str(connect_str)
+
+        assert excinfo.value.error_type == "MissingPackageError"
+        expected = f"howto/db-drivers.html#{section_name}"
+        assert expected in str(excinfo.value)
+
+
+@pytest.mark.parametrize(
+    "missing_pkg, dialect, connect_str",
+    [
+        ("duckdb_engine", "duckdb", "duckdb://"),
+    ],
+)
+def test_error_shows_link_to_installation_instructions_when_missing_dialect(
+    monkeypatch, missing_pkg, dialect, connect_str
+):
+    mock = Mock(
+        side_effect=exc.NoSuchModuleError(
+            f"Can't load plugin: sqlalchemy.dialects:{dialect}"
+        )
+    )
+    monkeypatch.setattr(connection_module.sqlalchemy, "create_engine", mock)
+
+    with pytest.raises(UsageError) as excinfo:
+        ConnectionManager.from_connect_str(connect_str)
+
+    assert excinfo.value.error_type == "MissingPackageError"
+    expected = f"howto/db-drivers.html#{dialect}"
+    assert expected in str(excinfo.value)
 
 
 def test_get_connections():
diff --git a/src/tests/test_magic.py b/src/tests/test_magic.py
index c65ebe5..5c15914 100644
--- a/src/tests/test_magic.py
+++ b/src/tests/test_magic.py
@@ -1125,7 +1125,7 @@ OR
 
 Set the environment variable $DATABASE_URL
 
-{PLOOMBER_DOCS_LINK_STR}
+For more details, see: {PLOOMBER_DOCS_LINK_STR}
 {COMMUNITY}
 """
 
@@ -1143,7 +1143,7 @@ Can't load plugin: sqlalchemy.dialects:something
 To fix it, make sure you are using correct driver name:
 Ref: https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls
 
-{PLOOMBER_DOCS_LINK_STR}
+For more details, see: {PLOOMBER_DOCS_LINK_STR}
 {COMMUNITY}
 """  # noqa
 
@@ -1169,7 +1169,7 @@ Can't load plugin: sqlalchemy.dialects:sqlit
 
 Perhaps you meant to use driver the dialect: "sqlite"
 
-{PLOOMBER_DOCS_LINK_STR}
+For more details, see: {PLOOMBER_DOCS_LINK_STR}
 {COMMUNITY}
 """  # noqa
 
@@ -1197,7 +1197,7 @@ To fix it:
 Pass a valid connection string:
     Example: %sql postgresql://username:password@hostname/dbname
 
-{PLOOMBER_DOCS_LINK_STR}
+For more details, see: {PLOOMBER_DOCS_LINK_STR}
 {COMMUNITY}
 """  # noqa