Repository: ploomber/jupysql (python)

Issue:
update .md headers
in the doc folder, we have many .md files and they all contain metadata that looks like this:

```yaml
---
jupytext:
  notebook_metadata_filter: myst
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.14.4
kernelspec:
  display_name: Python 3 (ipykernel)
  language: python
  name: python3
---
```

however, some of them are missing the `notebook_metadata_filter: myst`, which causes jupyter to delete certain parts of the metadata, we need to ensure `notebook_metadata_filter: myst` is in all .md files in the doc folder


Pull request description:
Update md headers
## Describe your changes

## Issue number

Closes #314 

## Checklist before requesting a review

- [ ] Performed a self-review of my code
- [ ] Formatted my code with [`pkgmt format`](https://github.com/ploomber/contributing/blob/main/CONTRIBUTING.md#lintingformatting)
- [ ] Added [tests](https://github.com/ploomber/contributing/blob/main/CONTRIBUTING.md#testing) (when necessary).
- [ ] Added documentation in the [docstring](https://github.com/ploomber/contributing/blob/main/CONTRIBUTING.md#documenting-changes-and-new-features) and [changelog](https://github.com/ploomber/contributing/blob/main/CONTRIBUTING.md#changelog) (when needed)



<!-- readthedocs-preview jupysql start -->
----
:books: Documentation preview :books:: https://jupysql--340.org.readthedocs.build/en/340/

<!-- readthedocs-preview jupysql end -->

Patch:
diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml
new file mode 100644
index 0000000..191b3d2
--- /dev/null
+++ b/.github/workflows/changelog.yml
@@ -0,0 +1,60 @@
+# Workflow that prints a message to the PR if the CHANGELOG.md hasn't been modified
+# if it has been modified, it does nothing.
+name: changelog-edited
+
+on: pull_request
+
+jobs:
+ changelog-edited:
+   runs-on: ubuntu-latest
+
+   steps:
+    - name: Checkout
+      uses: actions/checkout@v3
+
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v2
+      with:
+        python-version: '3.10'
+
+    - name: Check CHANGELOG.md
+      id: check
+      run: |
+        BRANCH_NAME=master
+        git fetch --depth=1 origin $BRANCH_NAME:$BRANCH_NAME
+        CHANGELOG_DIFF=$(git diff "$BRANCH_NAME" CHANGELOG.md)
+
+        # https://stackoverflow.com/a/47498710/709975
+        if [[ -z `echo $CHANGELOG_DIFF` ]]
+        then
+            echo "CHANGELOG.md has **not** been edited..."
+            echo "CHANGELOG_EDITED=0" >> $GITHUB_OUTPUT
+        else
+            echo "CHANGELOG.md has been edited..."
+            echo "CHANGELOG.md diff:"
+            echo -e $CHANGELOG_DIFF
+            echo "CHANGELOG_EDITED=1" >> $GITHUB_OUTPUT
+        fi
+
+    - name: Add comment to PR
+      uses: actions/github-script@v6
+      if: always()
+      with:
+        script: |
+          const changelog_edited = '${{ steps.check.outputs.CHANGELOG_EDITED }}' == '1'
+
+          if (!changelog_edited){
+            const body = `
+            Your PR didn't edit the CHANGELOG.md file. This is ok in some cases, however, ensure that you're following the
+            [CHANGELOG guidelines](https://ploomber-contributing.readthedocs.io/en/latest/contributing/coding.html#changelog)
+            `
+
+            await github.rest.issues.createComment({
+              issue_number: context.issue.number,
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              body: body
+            })
+          } else {
+            console.log("CHANGELOG has been edited...")
+          }
diff --git a/.github/workflows/chatops.yml b/.github/workflows/chatops.yml
new file mode 100644
index 0000000..d7d62cc
--- /dev/null
+++ b/.github/workflows/chatops.yml
@@ -0,0 +1,78 @@
+name: bot-format
+
+on:
+  issue_comment:
+    types: [created]
+
+jobs:
+ pkgmt-format:
+   if: contains(github.event.comment.html_url, '/pull/') && contains(github.event.comment.body, '/format') 
+   runs-on: ubuntu-latest
+
+   steps:
+    - uses: xt0rted/pull-request-comment-branch@v2
+      id: comment-branch
+
+    - name: Set latest commit status as pending
+      uses: myrotvorets/set-commit-status-action@master
+      with:
+        sha: ${{ steps.comment-branch.outputs.head_sha }}
+        token: ${{ secrets.GITHUB_TOKEN }}
+        status: pending
+
+    # there's an alternative way to check out:
+    # https://github.com/actions/checkout/issues/331#issuecomment-925405415
+    - name: Checkout PR branch
+      uses: actions/checkout@v3
+      with:
+        ref: ${{ steps.comment-branch.outputs.head_ref }}
+
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v2
+      with:
+        python-version: '3.10'
+
+    - name: format
+      run: |
+        python -m pip install --upgrade pip pkgmt
+
+        # https://github.com/actions/checkout/discussions/479#discussioncomment-625461
+        git config user.name 'github-actions[bot]'
+        git config user.email 'github-actions[bot]@users.noreply.github.com'
+
+        pkgmt format
+
+        if [[ -z $(git status -s) ]]
+        then
+          echo "No changes to commit..."
+        else
+          echo "Committing changes..."
+          git add --all
+          git commit -m 'formattting'
+          git push
+        fi
+
+    - name: Set latest commit status as ${{ job.status }}
+      uses: myrotvorets/set-commit-status-action@master
+      if: always()
+      with:
+        sha: ${{ steps.comment-branch.outputs.head_sha }}
+        token: ${{ secrets.GITHUB_TOKEN }}
+        status: ${{ job.status }}
+
+    - name: Add comment to PR
+      uses: actions/github-script@v6
+      if: always()
+      with:
+        script: |
+          const name = '${{ github.workflow   }}';
+          const url = '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}';
+          const success = '${{ job.status }}' === 'success';
+          const body = `${name}: ${success ? 'succeeded ✅' : 'failed ❌'}\n${url}`;
+
+          await github.rest.issues.createComment({
+            issue_number: context.issue.number,
+            owner: context.repo.owner,
+            repo: context.repo.repo,
+            body: body
+          })
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b94398..3f0c6a8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 # CHANGELOG
 
 ## 0.7.0dev
+* [Fix] Clearer error when using bad table/schema name with `%sqlcmd` and `%sqlplot` (#155)
 * [Fix] Fix `%sqlcmd` exception handling (#262)
 * [Doc] Tutorial on ETLs via Jupysql and Github actions
 * [Feature] Adds `%sqlcmd profile` (#66)
@@ -13,6 +14,9 @@
 * [Fix] Clearer error when using `--with` with snippets that do not exist (#257)
 * [Fix] Pytds now automatically compatible
 * [Doc] SQL keywords autocompletion
+* [Feature] Adds `%%config SqlMagic.polars_dataframe_kwargs = {...}`
+* [Fix] Jupysql with autopolars crashes when schema cannot be inferred from the first 100 rows ([#312](https://github.com/ploomber/jupysql/issues/312))
+
 ## 0.6.6 (2023-03-16)
 
 * [Fix] Pinning SQLAlchemy 1.x
diff --git a/doc/api/configuration.md b/doc/api/configuration.md
index 6e36733..607cabb 100644
--- a/doc/api/configuration.md
+++ b/doc/api/configuration.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   cell_metadata_filter: -all
   formats: md:myst
   text_representation:
@@ -161,6 +162,34 @@ df = %sql SELECT * FROM languages
 type(df)
 ```
 
+## `polars_dataframe_kwargs`
+
+Default: `{}`
+
+Polars [DataFrame constructor](https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/index.html) keyword arguments (e.g. infer_schema_length, nan_to_null, schema_overrides, etc)
+
+```{code-cell} ipython3
+# By default Polars will only look at the first 100 rows to infer schema
+# Disable this limit by setting infer_schema_length to None
+%config SqlMagic.polars_dataframe_kwargs = { "infer_schema_length": None}
+
+# Create a table with 101 rows, last row has a string which will cause the
+# column type to be inferred as a string (rather than crashing polars)
+%sql CREATE TABLE points (x, y);
+insert_stmt = ""
+for _ in range(100):
+    insert_stmt += "INSERT INTO points VALUES (1, 2);"
+%sql {{insert_stmt}}
+%sql INSERT INTO points VALUES (1, "foo");
+
+
+%sql SELECT * FROM points
+```
+To unset:
+```{code-cell} ipython3
+%config SqlMagic.polars_dataframe_kwargs = {}
+```
+
 ## `feedback`
 
 Default: `True`
diff --git a/doc/api/magic-plot.md b/doc/api/magic-plot.md
index 4747b3b..025ffaa 100644
--- a/doc/api/magic-plot.md
+++ b/doc/api/magic-plot.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   text_representation:
     extension: .md
     format_name: myst
diff --git a/doc/api/magic-render.md b/doc/api/magic-render.md
index 95e949e..cdef9b5 100644
--- a/doc/api/magic-render.md
+++ b/doc/api/magic-render.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   text_representation:
     extension: .md
     format_name: myst
diff --git a/doc/compose.md b/doc/compose.md
index 7f27b32..7b293ea 100644
--- a/doc/compose.md
+++ b/doc/compose.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   text_representation:
     extension: .md
     format_name: myst
diff --git a/doc/connecting.md b/doc/connecting.md
index 61b77a7..159532b 100644
--- a/doc/connecting.md
+++ b/doc/connecting.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   cell_metadata_filter: -all
   formats: md:myst
   text_representation:
diff --git a/doc/howto/autocompletion.md b/doc/howto/autocompletion.md
index 36dd9d1..8789dde 100644
--- a/doc/howto/autocompletion.md
+++ b/doc/howto/autocompletion.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   text_representation:
     extension: .md
     format_name: myst
diff --git a/doc/howto/csv.md b/doc/howto/csv.md
index 9b8c6a6..d240713 100644
--- a/doc/howto/csv.md
+++ b/doc/howto/csv.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   cell_metadata_filter: -all
   formats: md:myst
   text_representation:
diff --git a/doc/howto/json.md b/doc/howto/json.md
index cb3ac06..f1422e8 100644
--- a/doc/howto/json.md
+++ b/doc/howto/json.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   text_representation:
     extension: .md
     format_name: myst
diff --git a/doc/howto/syntax-highlighting.md b/doc/howto/syntax-highlighting.md
index 4fe4f89..40ee257 100644
--- a/doc/howto/syntax-highlighting.md
+++ b/doc/howto/syntax-highlighting.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   text_representation:
     extension: .md
     format_name: myst
diff --git a/doc/integrations/pandas.md b/doc/integrations/pandas.md
index 46140ff..34dffef 100644
--- a/doc/integrations/pandas.md
+++ b/doc/integrations/pandas.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   cell_metadata_filter: -all
   formats: md:myst
   text_representation:
diff --git a/doc/integrations/polars.md b/doc/integrations/polars.md
index 3fcc684..2806d40 100644
--- a/doc/integrations/polars.md
+++ b/doc/integrations/polars.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   cell_metadata_filter: -all
   formats: md:myst
   text_representation:
diff --git a/doc/intro.md b/doc/intro.md
index f14589e..d20409e 100644
--- a/doc/intro.md
+++ b/doc/intro.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   cell_metadata_filter: -all
   formats: md:myst
   text_representation:
diff --git a/doc/plot-legacy.md b/doc/plot-legacy.md
index d59b18c..7dfb3f0 100644
--- a/doc/plot-legacy.md
+++ b/doc/plot-legacy.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   cell_metadata_filter: -all
   formats: md:myst
   text_representation:
diff --git a/doc/plot.md b/doc/plot.md
index 53cf8ce..1aae5bb 100644
--- a/doc/plot.md
+++ b/doc/plot.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   text_representation:
     extension: .md
     format_name: myst
diff --git a/doc/quick-start.md b/doc/quick-start.md
index 5adcd45..bb3ada5 100644
--- a/doc/quick-start.md
+++ b/doc/quick-start.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   cell_metadata_filter: -all
   formats: md:myst
   text_representation:
diff --git a/doc/tutorials/etl.md b/doc/tutorials/etl.md
index 9a31820..80d71cb 100644
--- a/doc/tutorials/etl.md
+++ b/doc/tutorials/etl.md
@@ -1,6 +1,7 @@
 ---
 jupyter:
   jupytext:
+    notebook_metadata_filter: myst
     text_representation:
       extension: .md
       format_name: markdown
diff --git a/doc/user-guide/interactive.md b/doc/user-guide/interactive.md
index d38c03a..063d07f 100644
--- a/doc/user-guide/interactive.md
+++ b/doc/user-guide/interactive.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   text_representation:
     extension: .md
     format_name: myst
diff --git a/doc/user-guide/tables-columns.md b/doc/user-guide/tables-columns.md
index 0a4602b..2f48d55 100644
--- a/doc/user-guide/tables-columns.md
+++ b/doc/user-guide/tables-columns.md
@@ -1,5 +1,6 @@
 ---
 jupytext:
+  notebook_metadata_filter: myst
   text_representation:
     extension: .md
     format_name: myst
diff --git a/src/sql/connection.py b/src/sql/connection.py
index 28b0aee..c2cdecc 100644
--- a/src/sql/connection.py
+++ b/src/sql/connection.py
@@ -376,3 +376,24 @@ class Connection:
             query = sqlglot.parse_one(query).sql(dialect=write_dialect)
         finally:
             return query
+
+    @classmethod
+    def get_curr_identifiers(cls) -> list:
+        """
+        Returns list of identifiers for current connection
+
+        Default identifiers are : ["", '"']
+        """
+        identifiers = ["", '"']
+        try:
+            connection_info = cls._get_curr_connection_info()
+            if connection_info:
+                cur_dialect = connection_info["dialect"]
+                identifiers_ = sqlglot.Dialect.get_or_raise(
+                    cur_dialect).Tokenizer.IDENTIFIERS
+
+                identifiers = [*set(identifiers + identifiers_)]
+        except ValueError:
+            pass
+
+        return identifiers
diff --git a/src/sql/inspect.py b/src/sql/inspect.py
index 3580a14..1a706d6 100644
--- a/src/sql/inspect.py
+++ b/src/sql/inspect.py
@@ -5,7 +5,7 @@ from sql.connection import Connection
 from sql.telemetry import telemetry
 import sql.run
 import math
-from sql.util import convert_to_scientific
+from sql import util
 
 
 def _get_inspector(conn):
@@ -51,20 +51,12 @@ class Columns(DatabaseInspection):
     """
 
     def __init__(self, name, schema, conn=None) -> None:
+        util.is_table_exists(name, schema)
+
         inspector = _get_inspector(conn)
 
         columns = inspector.get_columns(name, schema)
 
-        if not columns:
-            if schema:
-                raise ValueError(
-                    f"There is no table with name {name!r} in schema {schema!r}"
-                )
-            else:
-                raise ValueError(
-                    f"There is no table with name {name!r} in the default schema"
-                )
-
         self._table = PrettyTable()
         self._table.field_names = list(columns[0].keys())
 
@@ -103,6 +95,8 @@ class TableDescription(DatabaseInspection):
     """
 
     def __init__(self, table_name, schema=None) -> None:
+        util.is_table_exists(table_name, schema)
+
         if schema:
             table_name = f"{schema}.{table_name}"
 
@@ -227,7 +221,7 @@ class TableDescription(DatabaseInspection):
                     value = table_stats[column][row]
                 else:
                     value = ""
-                value = convert_to_scientific(value)
+                value = util.convert_to_scientific(value)
                 values.append(value)
 
             self._table.add_row(values)
@@ -257,3 +251,9 @@ def get_table_statistics(name, schema=None):
     and `freq` statistics.
     """
     return TableDescription(name, schema=schema)
+
+
+def get_schema_names(conn=None):
+    """Get list of schema names for a given connection"""
+    inspector = _get_inspector(conn)
+    return inspector.get_schema_names()
diff --git a/src/sql/magic.py b/src/sql/magic.py
index 76250aa..ad1b08c 100644
--- a/src/sql/magic.py
+++ b/src/sql/magic.py
@@ -28,7 +28,7 @@ from sql.magic_cmd import SqlCmdMagic
 from ploomber_core.dependencies import check_installed
 
 from traitlets.config.configurable import Configurable
-from traitlets import Bool, Int, Unicode, observe
+from traitlets import Bool, Int, Unicode, Dict, observe
 
 try:
     from pandas.core.frame import DataFrame, Series
@@ -110,6 +110,14 @@ class SqlMagic(Magics, Configurable):
         config=True,
         help="Return Polars DataFrames instead of regular result sets",
     )
+    polars_dataframe_kwargs = Dict(
+        {},
+        config=True,
+        help=(
+            "Polars DataFrame constructor keyword arguments"
+            "(e.g. infer_schema_length, nan_to_null, schema_overrides, etc)"
+        ),
+    )
     column_local_vars = Bool(
         False, config=True, help="Return data into local variables from column names"
     )
diff --git a/src/sql/magic_plot.py b/src/sql/magic_plot.py
index 183fad2..1eeaca7 100644
--- a/src/sql/magic_plot.py
+++ b/src/sql/magic_plot.py
@@ -14,6 +14,7 @@ except ImportError:
 
 from sql import plot
 from sql.command import SQLPlotCommand
+from sql import util
 
 
 @magics_class
@@ -68,6 +69,8 @@ class SqlPlotMagic(Magics, Configurable):
             )
 
         if cmd.args.line[0] in {"box", "boxplot"}:
+            util.is_table_exists(cmd.args.table, with_=cmd.args.with_)
+
             return plot.boxplot(
                 table=cmd.args.table,
                 column=column,
@@ -76,6 +79,8 @@ class SqlPlotMagic(Magics, Configurable):
                 conn=None,
             )
         elif cmd.args.line[0] in {"hist", "histogram"}:
+            util.is_table_exists(cmd.args.table, with_=cmd.args.with_)
+
             return plot.histogram(
                 table=cmd.args.table,
                 column=column,
diff --git a/src/sql/run.py b/src/sql/run.py
index 3906f56..bc56506 100644
--- a/src/sql/run.py
+++ b/src/sql/run.py
@@ -184,11 +184,13 @@ class ResultSet(list, ColumnGuesserMixin):
         return frame
 
     @telemetry.log_call("polars-data-frame")
-    def PolarsDataFrame(self):
+    def PolarsDataFrame(self, **polars_dataframe_kwargs):
         "Returns a Polars DataFrame instance built from the result set."
         import polars as pl
 
-        frame = pl.DataFrame((tuple(row) for row in self), schema=self.keys)
+        frame = pl.DataFrame(
+            (tuple(row) for row in self), schema=self.keys, **polars_dataframe_kwargs
+        )
         return frame
 
     @telemetry.log_call("pie")
@@ -444,7 +446,7 @@ def select_df_type(resultset, config):
     if config.autopandas:
         return resultset.DataFrame()
     elif config.autopolars:
-        return resultset.PolarsDataFrame()
+        return resultset.PolarsDataFrame(**config.polars_dataframe_kwargs)
     else:
         return resultset
     # returning only last result, intentionally
diff --git a/src/sql/util.py b/src/sql/util.py
index f606391..bb75bb4 100644
--- a/src/sql/util.py
+++ b/src/sql/util.py
@@ -1,3 +1,10 @@
+from sql import inspect
+import difflib
+import sql.run
+from sql.connection import Connection
+from sql.store import store
+
+
 def convert_to_scientific(value):
     """
     Converts value to scientific notation if necessary
@@ -29,3 +36,132 @@ def _is_long_number(num) -> bool:
         if len(split_by_decimal[0]) > 10 or len(split_by_decimal[1]) > 10:
             return True
     return False
+
+
+def is_table_exists(
+    table: str, schema: str = None, ignore_error: bool = False, with_: str = None
+) -> bool:
+    """
+    Checks if a given table exists for a given connection
+
+    Parameters
+    ----------
+    table: str
+        Table name
+
+    schema: str, default None
+        Schema name
+
+    with_: list, default None
+        Temporary table
+
+    ignore_error: bool, default False
+        Avoid raising a ValueError
+    """
+    if table is None:
+        if ignore_error:
+            return False
+        else:
+            raise ValueError("Table cannot be None")
+
+    table = strip_multiple_chars(table, "\"'")
+
+    if schema:
+        table_ = f"{schema}.{table}"
+    else:
+        table_ = table
+
+    _is_exist = _is_table_exists(table_, with_)
+
+    if not _is_exist:
+        if not ignore_error:
+            expected = []
+            existing_schemas = inspect.get_schema_names()
+            if schema and schema not in existing_schemas:
+                expected = existing_schemas
+                invalid_input = schema
+            else:
+                existing_tables = _get_list_of_existing_tables()
+                expected = existing_tables
+                invalid_input = table
+
+            if schema:
+                err_message = (
+                    f"There is no table with name {table!r} in schema {schema!r}"
+                )
+            else:
+                err_message = (
+                    f"There is no table with name {table!r} in the default schema"
+                )
+
+            suggestions = difflib.get_close_matches(invalid_input, expected)
+
+            if len(suggestions) > 0:
+                _suggestions_string = pretty_print(suggestions, last_delimiter="or")
+                suggestions_message = f"\nDid you mean : {_suggestions_string}"
+                err_message = f"{err_message}{suggestions_message}"
+
+            raise ValueError(err_message)
+
+    return _is_exist
+
+
+def _get_list_of_existing_tables() -> list:
+    """
+    Returns a list of table names for a given connection
+    """
+    tables = []
+    tables_rows = inspect.get_table_names()._table
+    for row in tables_rows:
+        table_name = row.get_string(fields=["Name"], border=False, header=False).strip()
+
+        tables.append(table_name)
+    return tables
+
+
+def pretty_print(
+    obj: list, delimiter: str = ",", last_delimiter: str = "and", repr_: bool = False
+) -> str:
+    """
+    Returns a formatted string representation of an array
+    """
+    if repr_:
+        sorted_ = sorted(repr(element) for element in obj)
+    else:
+        sorted_ = sorted(f"'{element}'" for element in obj)
+
+    if len(sorted_) > 1:
+        sorted_[-1] = f"{last_delimiter} {sorted_[-1]}"
+
+    return f"{delimiter} ".join(sorted_)
+
+
+def strip_multiple_chars(string: str, chars: str) -> str:
+    """
+    Trims characters from the start and end of the string
+    """
+    return string.translate(str.maketrans("", "", chars))
+
+
+def _is_table_exists(table: str, with_: str) -> bool:
+    """
+    Runs a SQL query to check if table exists
+    """
+
+    identifiers = Connection.get_curr_identifiers()
+
+    for iden in identifiers:
+        if isinstance(iden, tuple):
+            query = "SELECT * FROM {0}{1}{2} WHERE 1=0".format(iden[0], table, iden[1])
+        else:
+            query = "SELECT * FROM {0}{1}{0} WHERE 1=0".format(iden, table)
+        try:
+            if with_:
+                query = str(store.render(query, with_=with_))
+            query = sql.connection.Connection._transpile_query(query)
+            sql.run.raw_run(Connection.current, query)
+            return True
+        except Exception:
+            pass
+
+    return False


Test patch:
diff --git a/src/tests/test_inspect.py b/src/tests/test_inspect.py
index e34cc09..7e445fb 100644
--- a/src/tests/test_inspect.py
+++ b/src/tests/test_inspect.py
@@ -26,6 +26,7 @@ def sample_db(tmp_empty):
     [
         inspect.get_table_names,
         partial(inspect.get_columns, name="some_name"),
+        inspect.get_schema_names
     ],
 )
 def test_no_active_session(function, monkeypatch):
@@ -107,3 +108,22 @@ def test_nonexistent_table(name, schema, error):
 )
 def test_telemetry(function):
     assert "@telemetry.log_call" in getsource(function)
+
+
+def test_get_schema_names(ip):
+    ip.run_cell(
+        """%%sql sqlite:///my.db
+CREATE TABLE IF NOT EXISTS test_table (id INT)
+"""
+    )
+
+    ip.run_cell(
+        """%%sql
+ATTACH DATABASE 'my.db' AS test_schema
+"""
+    )
+
+    expected_schema_names = ["main", "test_schema"]
+    schema_names = inspect.get_schema_names()
+    for schema in schema_names:
+        assert schema in expected_schema_names
diff --git a/src/tests/test_magic.py b/src/tests/test_magic.py
index 578c1b4..76a5742 100644
--- a/src/tests/test_magic.py
+++ b/src/tests/test_magic.py
@@ -7,6 +7,7 @@ import tempfile
 from textwrap import dedent
 from unittest.mock import patch
 
+import polars as pl
 import pytest
 from sqlalchemy import create_engine
 from IPython.core.error import UsageError
@@ -277,20 +278,47 @@ def test_autopolars(ip):
     ip.run_line_magic("config", "SqlMagic.autopolars = True")
     dframe = runsql(ip, "SELECT * FROM test;")
 
-    import polars as pl
-
     assert type(dframe) == pl.DataFrame
     assert not dframe.is_empty()
     assert len(dframe.shape) == 2
     assert dframe["name"][0] == "foo"
 
 
+def test_autopolars_infer_schema_length(ip):
+    """Test for `SqlMagic.polars_dataframe_kwargs = {"infer_schema_length": None}`
+    Without this config, polars will raise an exception when it cannot infer the
+    correct schema from the first 100 rows.
+    """
+    # Create a table with 100 rows with a NULL value and one row with a non-NULL value
+    ip.run_line_magic("config", "SqlMagic.autopolars = True")
+    sql = ["CREATE TABLE test_autopolars_infer_schema (n INT, name TEXT)"]
+    for i in range(100):
+        sql.append(f"INSERT INTO test_autopolars_infer_schema VALUES ({i}, NULL)")
+    sql.append("INSERT INTO test_autopolars_infer_schema VALUES (100, 'foo')")
+    runsql(ip, sql)
+
+    # By default, this dataset should raise a ComputeError
+    with pytest.raises(pl.exceptions.ComputeError):
+        runsql(ip, "SELECT * FROM test_autopolars_infer_schema;")
+
+    # To avoid this error, pass the `infer_schema_length` argument to polars.DataFrame
+    line_magic = 'SqlMagic.polars_dataframe_kwargs = {"infer_schema_length": None}'
+    ip.run_line_magic("config", line_magic)
+    dframe = runsql(ip, "SELECT * FROM test_autopolars_infer_schema;")
+    assert dframe.schema == {"n": pl.Int64, "name": pl.Utf8}
+
+    # Assert that if we unset the dataframe kwargs, the error is raised again
+    ip.run_line_magic("config", "SqlMagic.polars_dataframe_kwargs = {}")
+    with pytest.raises(pl.exceptions.ComputeError):
+        runsql(ip, "SELECT * FROM test_autopolars_infer_schema;")
+
+    runsql(ip, "DROP TABLE test_autopolars_infer_schema")
+
+
 def test_mutex_autopolars_autopandas(ip):
     dframe = runsql(ip, "SELECT * FROM test;")
     assert type(dframe) == ResultSet
 
-    import polars as pl
-
     ip.run_line_magic("config", "SqlMagic.autopolars = True")
     dframe = runsql(ip, "SELECT * FROM test;")
     assert type(dframe) == pl.DataFrame
diff --git a/src/tests/test_run.py b/src/tests/test_run.py
index 1689486..f1eaf55 100644
--- a/src/tests/test_run.py
+++ b/src/tests/test_run.py
@@ -32,6 +32,7 @@ def mock_config():
         autopolars = None
         autocommit = True
         feedback = True
+        polars_dataframe_kwargs = {}
 
     return Config
 
diff --git a/src/tests/test_util.py b/src/tests/test_util.py
new file mode 100644
index 0000000..57278fa
--- /dev/null
+++ b/src/tests/test_util.py
@@ -0,0 +1,194 @@
+import pytest
+from sql import util
+
+ERROR_MESSAGE = "Table cannot be None"
+EXPECTED_SUGGESTIONS_MESSAGE = "Did you mean :"
+EXPECTED_NO_TABLE_IN_DEFAULT_SCHEMA = (
+    "There is no table with name {0!r} in the default schema"
+)
+EXPECTED_NO_TABLE_IN_SCHEMA = "There is no table with name {0!r} in schema {1!r}"
+
+
+@pytest.mark.parametrize(
+    "table, query, suggestions",
+    [
+        ("tes", "%sqlcmd columns --table {}", ["test"]),
+        ("_table", "%sqlcmd columns --table {}", ["empty_table", "number_table"]),
+        ("no_similar_tables", "%sqlcmd columns --table {}", []),
+        ("tes", "%sqlcmd profile --table {}", ["test"]),
+        ("_table", "%sqlcmd profile --table {}", ["empty_table", "number_table"]),
+        ("no_similar_tables", "%sqlcmd profile --table {}", []),
+        ("tes", "%sqlplot histogram --table {} --column x", ["test"]),
+        ("tes", "%sqlplot boxplot --table {} --column x", ["test"]),
+    ],
+)
+def test_bad_table_error_message(ip, table, query, suggestions):
+    query = query.format(table)
+    out = ip.run_cell(query)
+    expected_error_message = EXPECTED_NO_TABLE_IN_DEFAULT_SCHEMA.format(table)
+
+    error_message = str(out.error_in_exec)
+    assert isinstance(out.error_in_exec, ValueError)
+    assert str(expected_error_message).lower() in error_message.lower()
+
+    error_suggestions_arr = error_message.split(EXPECTED_SUGGESTIONS_MESSAGE)
+
+    if len(suggestions) > 0:
+        assert len(error_suggestions_arr) > 1
+        for suggestion in suggestions:
+            assert suggestion in error_suggestions_arr[1]
+
+
+@pytest.mark.parametrize(
+    "table, schema, query, suggestions",
+    [
+        (
+            "test_table",
+            "invalid_name_no_match",
+            "%sqlcmd columns --table {} --schema {}",
+            [],
+        ),
+        (
+            "test_table",
+            "te_schema",
+            "%sqlcmd columns --table {} --schema {}",
+            ["test_schema"],
+        ),
+        (
+            "invalid_name_no_match",
+            "test_schema",
+            "%sqlcmd columns --table {} --schema {}",
+            [],
+        ),
+        (
+            "test_tabl",
+            "test_schema",
+            "%sqlcmd columns --table {} --schema {}",
+            ["test_table", "test"],
+        ),
+        (
+            "invalid_name_no_match",
+            "invalid_name_no_match",
+            "%sqlcmd columns --table {} --schema {}",
+            [],
+        ),
+        (
+            "_table",
+            "_schema",
+            "%sqlcmd columns --table {} --schema {}",
+            ["test_schema"],
+        ),
+    ],
+)
+def test_bad_table_error_message_with_schema(ip, query, suggestions, table, schema):
+    query = query.format(table, schema)
+
+    expected_error_message = EXPECTED_NO_TABLE_IN_SCHEMA.format(table, schema)
+
+    ip.run_cell(
+        """%%sql sqlite:///my.db
+CREATE TABLE IF NOT EXISTS test_table (id INT)
+"""
+    )
+
+    ip.run_cell(
+        """%%sql
+ATTACH DATABASE 'my.db' AS test_schema
+"""
+    )
+
+    out = ip.run_cell(query)
+
+    error_message = str(out.error_in_exec)
+    assert isinstance(out.error_in_exec, ValueError)
+    assert str(expected_error_message).lower() in error_message.lower()
+
+    error_suggestions_arr = error_message.split(EXPECTED_SUGGESTIONS_MESSAGE)
+
+    if len(suggestions) > 0:
+        assert len(error_suggestions_arr) > 1
+        for suggestion in suggestions:
+            assert suggestion in error_suggestions_arr[1]
+
+
+@pytest.mark.parametrize(
+    "table, expected_result",
+    [
+        ("number_table", True),
+        ("test", True),
+        ("author", True),
+        ("empty_table", True),
+        ("numbers1", False),
+        ("test1", False),
+        ("author1", False),
+        ("empty_table1", False),
+        (None, False),
+    ],
+)
+def test_is_table_exists_ignore_error(ip, table, expected_result):
+    assert expected_result is util.is_table_exists(table, ignore_error=True)
+
+
+@pytest.mark.parametrize(
+    "table, expected_error",
+    [
+        ("number_table", None),
+        ("test", None),
+        ("author", None),
+        ("empty_table", None),
+        ("numbers1", ValueError),
+        ("test1", ValueError),
+        ("author1", ValueError),
+        ("empty_table1", ValueError),
+        (None, ValueError),
+    ],
+)
+def test_is_table_exists(ip, table, expected_error):
+    if expected_error:
+        with pytest.raises(ValueError):
+            util.is_table_exists(table)
+    else:
+        util.is_table_exists(table)
+
+
+@pytest.mark.parametrize(
+    "table, expected_error, expected_suggestions",
+    [
+        ("number_table", None, []),
+        ("number_tale", ValueError, ["number_table"]),
+        ("_table", ValueError, ["number_table", "empty_table"]),
+        (None, ValueError, []),
+    ],
+)
+def test_is_table_exists_with(ip, table, expected_error, expected_suggestions):
+    with_ = ["temp"]
+
+    ip.run_cell(
+        f"""
+        %%sql --save {with_[0]} --no-execute
+        SELECT *
+        FROM {table}
+        WHERE x > 2
+        """
+    )
+    if expected_error:
+        with pytest.raises(expected_error) as error:
+            util.is_table_exists(table, with_=with_)
+
+        error_suggestions_arr = str(error.value).split(EXPECTED_SUGGESTIONS_MESSAGE)
+
+        if len(expected_suggestions) > 0:
+            assert len(error_suggestions_arr) > 1
+            for suggestion in expected_suggestions:
+                assert suggestion in error_suggestions_arr[1]
+        else:
+            assert len(error_suggestions_arr) == 1
+    else:
+        util.is_table_exists(table)
+
+
+def test_get_list_of_existing_tables(ip):
+    expected = ["author", "empty_table", "number_table", "test", "website"]
+    list_of_tables = util._get_list_of_existing_tables()
+    for table in expected:
+        assert table in list_of_tables