Repository: ploomber/jupysql (python)

Issue:
Change the url for the docs
We need to switch the docs to serve from our domain:
`https://jupysql.readthedocs.io/en/latest/quick-start.html` to  `https://jupysql.ploomber.io/en/latest/quick-start.html`

Pull request:
changed base urls
## Describe your changes
Changes base URL references found in readme to updated link

## Issue ticket number and link
Closes #96 

## Checklist before requesting a review

- [x] I have performed a self-review of my code
- [x] I have added thorough [tests](https://github.com/ploomber/contributing/blob/main/CONTRIBUTING.md#testing) (when necessary).
- [x] I have added the right 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--103.org.readthedocs.build/en/103/

<!-- readthedocs-preview jupysql end -->

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 8dc9734..9cb80c5 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -56,3 +56,33 @@ jobs:
       - name: Check project
         run: |
           pkgmt check
+
+  release:
+    runs-on: ubuntu-latest
+    if: github.ref_name == 'master' && startsWith(github.event.head_commit.message, '[release]')
+    needs: [test, check]
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: '3.10'
+
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install 'pkgmt[check]' twine wheel
+      - name: Configure git
+        run: |
+          git config user.email "github-actions@ploomber.io"
+          git config user.name "github-actions"
+      - name: Bump up tag version 
+        run: |
+          pkgmt version --yes
+      - name: Publish to PyPI
+        env:
+          TWINE_USERNAME: "__token__"
+          TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
+        run: |
+          TAG=$(git describe --abbrev=0)
+          echo yes | pkgmt release $TAG --production
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 037bfb2..d2648c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,15 @@
 # CHANGELOG
 
-## 0.5.3dev
-* [Fix] Adds telemetry collect for `%sqlplot` invocation ([#89](https://github.com/ploomber/jupysql/pull/89))
+## 0.5.4dev
+
+## 0.5.3 (2023-01-31)
+
+* [Feature] Adds `%sqlcmd tables` ([#76](https://github.com/ploomber/jupysql/issues/76))
+* [Feature] Adds `%sqlcmd columns` ([#76](https://github.com/ploomber/jupysql/issues/76))
 * [Fix] `setup.py` fix due to change in setuptools 67.0.0
 
 ## 0.5.2 (2023-01-03)
+
 * Adds example for connecting to a SQLite database with spaces ([#35](https://github.com/ploomber/jupysql/issues/35))
 * Documents how to securely pass credentials ([#40](https://github.com/ploomber/jupysql/issues/40))
 * Adds `-a/--alias` option to name connections for easier management ([#59](https://github.com/ploomber/jupysql/issues/59))
@@ -14,34 +19,42 @@
 * Removes `six` as dependency (drops Python 2 support)
 
 ## 0.5.1 (2022-12-26)
+
 * Allow to connect to databases with an existing `sqlalchemy.engine.Engine` object
 
 ## 0.5 (2022-12-24)
+
 * `ResultSet.plot()`, `ResultSet.bar()`, and `ResultSet.pie()` return `matplotlib.Axes` objects
 
 ## 0.4.7 (2022-12-23)
+
 * Assigns a variable without displaying an output message ([#13](https://github.com/ploomber/jupysql/issues/13))
 
 ## 0.4.6 (2022-08-30)
+
 * Updates telemetry key
 
 ## 0.4.5 (2022-08-13)
+
 * Adds anonymous telemetry
 
 ## 0.4.4 (2022-08-06)
+
 * Adds `plot` module (boxplot and histogram)
 
 ## 0.4.3 (2022-08-04)
+
 * Adds `--save`, `--with`, and `%sqlrender` for SQL composition ([#1](https://github.com/ploomber/jupysql/issues/1))
 
 ## 0.4.2 (2022-07-26)
+
 *First version release by Ploomber*
 
 * Adds `--no-index` option to `--persist` data frames without the index
 
 ## 0.4.1
 
-* Fixed .rst file location in MANIFEST.in 
+* Fixed .rst file location in MANIFEST.in
 * Parse SQL comments in first line
 * Bugfixes for DSN, `--close`, others
 
@@ -53,7 +66,6 @@
 * Turn off echo of connection information with `displaycon` in config
 * Consistent support for {} variables (thanks Lucas)
 
-
 ## 0.3.9
 
 * Restored Python 2 compatibility (thanks tokenmathguy)
@@ -64,25 +76,20 @@
 * pgspecial installation optional (thanks jstoebel and arjoe)
 * conceal passwords in connection strings (thanks jstoebel)
 
-
 ## 0.3.8
 
 * Stop warnings for deprecated use of IPython 3 traitlets in IPython 4 (thanks graphaelli; also stonebig, aebrahim, mccahill)
 * README update for keeping connection info private, from eshilts
 
-
 ## 0.3.7.1
 
 * Avoid "connection busy" error for SQL Server (thanks Andrés Celis)
 
-
-
 ## 0.3.7
 
 * New `column_local_vars` config option submitted by darikg
 * Avoid contaminating user namespace from locals (thanks alope107)
 
-
 ## 0.3.6
 
 * Fixed issue number 30, commit failures for sqlite (thanks stonebig, jandot)
@@ -92,27 +99,22 @@
 * Indentations visible in HTML cells
 * COMMIT each SQL statement immediately - prevent locks
 
-
-
 ## 0.3.4
 
 * PERSIST pseudo-SQL command added
 
-
 ## 0.3.3
 
 * Python 3 compatibility restored
 * DSN access supported (thanks Berton Earnshaw)
 
-
 ## 0.3.2
 
-* ``.csv(filename=None)`` method added to result sets
-
+* `.csv(filename=None)` method added to result sets
 
 ## 0.3.1
 
-* Reporting of number of rows affected configurable with ``feedback``
+* Reporting of number of rows affected configurable with `feedback`
 
 * Local variables usable as SQL bind variables
 
@@ -135,6 +137,7 @@
 
   - Proper handling of configuration
 
+
 * Added .DataFrame(), .pie(), .plot(), and .bar() methods to
   result sets
 
@@ -158,14 +161,12 @@ Converted from an IPython Plugin to an Extension for 1.0 compatibility
 
 * Bugfix - issue 4 (remember existing connections by case)
 
-
 ## 0.2.0
 
 *Release date: 30-May-2013*
 
 * Accept bind variables (Thanks Mike Wilson!)
 
-
 ## 0.1.2
 
 *Release date: 29-Mar-2013*
@@ -176,7 +177,6 @@ Converted from an IPython Plugin to an Extension for 1.0 compatibility
 
 * allow multiple SQL per cell
 
-
 ## 0.1.1
 
 *Release date: 29-Mar-2013*
@@ -189,11 +189,8 @@ Converted from an IPython Plugin to an Extension for 1.0 compatibility
 
 * set autolimit and text wrap in configuration
 
-
-
 ## 0.1
 
 *Release date: 21-Mar-2013*
 
 * Initial release
-
diff --git a/README.md b/README.md
index d362e6a..6d34725 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
   |
   <a href="mailto:contact@ploomber.io">Contact us</a>
   |
-  <a href="https://jupysql.readthedocs.io/">Docs</a>
+  <a href="https://jupysql.ploomber.io/">Docs</a>
   |
   <a href="https://ploomber.io/">Blog</a>
   |
@@ -23,10 +23,10 @@ Run SQL in Jupyter/IPython via a `%sql` and `%%sql` magics.
 
 ## Features
 
-- [Pandas integration](https://jupysql.readthedocs.io/en/latest/integrations/pandas.html)
-- [SQL composition (no more hard-to-debug CTEs!)](https://jupysql.readthedocs.io/en/latest/compose.html)
-- [Plot massive datasets without blowing up memory](https://jupysql.readthedocs.io/en/latest/plot.html)
-- [DuckDB integration](https://jupysql.readthedocs.io/en/latest/integrations/duckdb.html)
+- [Pandas integration](https://jupysql.ploomber.io/en/latest/integrations/pandas.html)
+- [SQL composition (no more hard-to-debug CTEs!)](https://jupysql.ploomber.io/en/latest/compose.html)
+- [Plot massive datasets without blowing up memory](https://jupysql.ploomber.io/en/latest/plot.html)
+- [DuckDB integration](https://jupysql.ploomber.io/en/latest/integrations/duckdb.html)
 
 ## Installation
 
@@ -36,7 +36,7 @@ pip install jupysql
 
 ## Documentation
 
-[Click here to see the documentation.](https://jupysql.readthedocs.io)
+[Click here to see the documentation.](https://jupysql.ploomber.io)
 
 
 ## Credits
diff --git a/doc/_toc.yml b/doc/_toc.yml
index 701c49f..425d74c 100644
--- a/doc/_toc.yml
+++ b/doc/_toc.yml
@@ -9,10 +9,9 @@ parts:
     - file: intro
     - file: connecting
     - file: plot
-    - file: duckdb
-    - file: pandas
     - file: csv
     - file: compose
+    - file: user-guide/tables-columns
     - file: plot-legacy
 
   - caption: Integrations
@@ -37,6 +36,7 @@ parts:
 
   - caption: Community
     chapters:
+    - file: community/coc
+    - file: community/support
     - file: community/projects
-    - file: community/community
     - file: community/credits
\ No newline at end of file
diff --git a/doc/community/coc.md b/doc/community/coc.md
new file mode 100644
index 0000000..f25a76e
--- /dev/null
+++ b/doc/community/coc.md
@@ -0,0 +1,5 @@
+# Code of Conduct
+
+Ploomber has been committed to build the product by making it easy for community users to facilitate the day-to-day work.
+
+For more details, see [here](https://docs.ploomber.io/en/latest/community/coc.html)
\ No newline at end of file
diff --git a/doc/community/community.md b/doc/community/community.md
deleted file mode 100644
index 6a0baca..0000000
--- a/doc/community/community.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# Community
-
-## Code of Conduct
-
-[See here](https://docs.ploomber.io/en/latest/community/coc.html)
-
-## Telemetry
-
-[See here](https://docs.ploomber.io/en/latest/community/user-stats.html)
\ No newline at end of file
diff --git a/doc/community/support.md b/doc/community/support.md
new file mode 100644
index 0000000..f4b997a
--- /dev/null
+++ b/doc/community/support.md
@@ -0,0 +1,7 @@
+# Support
+
+For support, feature requests, and product updates: [join our community](https://ploomber.io/community) or follow us on [Twitter](https://twitter.com/ploomber)/[LinkedIn](https://www.linkedin.com/company/ploomber/).
+
+# Telemetry
+
+We collect (optional) anonymous statistics to understand and improve usage. For more details of what we collect and how to opt-out the telemetry collection, [see here](https://docs.ploomber.io/en/latest/community/user-stats.html).
\ No newline at end of file
diff --git a/doc/howto/postgres-connect.ipynb b/doc/howto/postgres-connect.ipynb
index 78cc03b..143cf6f 100644
--- a/doc/howto/postgres-connect.ipynb
+++ b/doc/howto/postgres-connect.ipynb
@@ -22,7 +22,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": 5,
    "id": "3df653d7",
    "metadata": {},
    "outputs": [
@@ -35,7 +35,7 @@
     }
    ],
    "source": [
-    "%pip install jupysql pandas --quiet"
+    "%pip install jupysql pandas pyarrow --quiet"
    ]
   },
   {
@@ -48,7 +48,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 2,
+   "execution_count": 1,
    "id": "ae033470",
    "metadata": {},
    "outputs": [
@@ -65,7 +65,6 @@
    ]
   },
   {
-   "attachments": {},
    "cell_type": "markdown",
    "id": "b2745e92",
    "metadata": {},
@@ -97,7 +96,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "9863544b380c019bf8b6a6cbe1ad15d12cd5ff2c1b4dd16f72067ca35f70d471\n"
+      "94cd3bdd41d175f7f0b3868b391676c260f91fc74acfb9a427a59517c70fe49f\n"
      ]
     }
    ],
@@ -129,7 +128,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 6,
    "id": "16b1bfed",
    "metadata": {},
    "outputs": [
@@ -139,7 +138,7 @@
        "(1369769, 19)"
       ]
      },
-     "execution_count": 4,
+     "execution_count": 6,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -161,7 +160,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 7,
    "id": "a3402cdf",
    "metadata": {},
    "outputs": [],
@@ -185,7 +184,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 1,
    "id": "0c312912",
    "metadata": {},
    "outputs": [],
@@ -195,7 +194,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 2,
    "id": "7d17f582",
    "metadata": {},
    "outputs": [],
@@ -213,9 +212,284 @@
     "```"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "id": "fa5b7942-e2ef-4bd6-8748-faad0d8fd7e2",
+   "metadata": {},
+   "source": [
+    "List the tables in the database:"
+   ]
+  },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 3,
+   "id": "4c5780d8-9234-40a8-a29d-799567498910",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<table>\n",
+       "    <tr>\n",
+       "        <th>Name</th>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>taxi</td>\n",
+       "    </tr>\n",
+       "</table>"
+      ],
+      "text/plain": [
+       "+------+\n",
+       "| Name |\n",
+       "+------+\n",
+       "| taxi |\n",
+       "+------+"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "%sqlcmd tables"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "af93dd84-98f8-44bc-b1f9-0c644a99b089",
+   "metadata": {},
+   "source": [
+    "List columns in the taxi table:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "ff92514c-456e-4abf-8539-f1455ea5de49",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<table>\n",
+       "    <tr>\n",
+       "        <th>name</th>\n",
+       "        <th>type</th>\n",
+       "        <th>nullable</th>\n",
+       "        <th>default</th>\n",
+       "        <th>autoincrement</th>\n",
+       "        <th>comment</th>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>index</td>\n",
+       "        <td>BIGINT</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>VendorID</td>\n",
+       "        <td>BIGINT</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>tpep_pickup_datetime</td>\n",
+       "        <td>TIMESTAMP</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>tpep_dropoff_datetime</td>\n",
+       "        <td>TIMESTAMP</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>passenger_count</td>\n",
+       "        <td>DOUBLE_PRECISION</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>trip_distance</td>\n",
+       "        <td>DOUBLE_PRECISION</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>RatecodeID</td>\n",
+       "        <td>DOUBLE_PRECISION</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>store_and_fwd_flag</td>\n",
+       "        <td>TEXT</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>PULocationID</td>\n",
+       "        <td>BIGINT</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>DOLocationID</td>\n",
+       "        <td>BIGINT</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>payment_type</td>\n",
+       "        <td>BIGINT</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>fare_amount</td>\n",
+       "        <td>DOUBLE_PRECISION</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>extra</td>\n",
+       "        <td>DOUBLE_PRECISION</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>mta_tax</td>\n",
+       "        <td>DOUBLE_PRECISION</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>tip_amount</td>\n",
+       "        <td>DOUBLE_PRECISION</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>tolls_amount</td>\n",
+       "        <td>DOUBLE_PRECISION</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>improvement_surcharge</td>\n",
+       "        <td>DOUBLE_PRECISION</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>total_amount</td>\n",
+       "        <td>DOUBLE_PRECISION</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>congestion_surcharge</td>\n",
+       "        <td>DOUBLE_PRECISION</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "        <td>airport_fee</td>\n",
+       "        <td>DOUBLE_PRECISION</td>\n",
+       "        <td>True</td>\n",
+       "        <td>None</td>\n",
+       "        <td>False</td>\n",
+       "        <td>None</td>\n",
+       "    </tr>\n",
+       "</table>"
+      ],
+      "text/plain": [
+       "+-----------------------+------------------+----------+---------+---------------+---------+\n",
+       "|          name         |       type       | nullable | default | autoincrement | comment |\n",
+       "+-----------------------+------------------+----------+---------+---------------+---------+\n",
+       "|         index         |      BIGINT      |   True   |   None  |     False     |   None  |\n",
+       "|        VendorID       |      BIGINT      |   True   |   None  |     False     |   None  |\n",
+       "|  tpep_pickup_datetime |    TIMESTAMP     |   True   |   None  |     False     |   None  |\n",
+       "| tpep_dropoff_datetime |    TIMESTAMP     |   True   |   None  |     False     |   None  |\n",
+       "|    passenger_count    | DOUBLE_PRECISION |   True   |   None  |     False     |   None  |\n",
+       "|     trip_distance     | DOUBLE_PRECISION |   True   |   None  |     False     |   None  |\n",
+       "|       RatecodeID      | DOUBLE_PRECISION |   True   |   None  |     False     |   None  |\n",
+       "|   store_and_fwd_flag  |       TEXT       |   True   |   None  |     False     |   None  |\n",
+       "|      PULocationID     |      BIGINT      |   True   |   None  |     False     |   None  |\n",
+       "|      DOLocationID     |      BIGINT      |   True   |   None  |     False     |   None  |\n",
+       "|      payment_type     |      BIGINT      |   True   |   None  |     False     |   None  |\n",
+       "|      fare_amount      | DOUBLE_PRECISION |   True   |   None  |     False     |   None  |\n",
+       "|         extra         | DOUBLE_PRECISION |   True   |   None  |     False     |   None  |\n",
+       "|        mta_tax        | DOUBLE_PRECISION |   True   |   None  |     False     |   None  |\n",
+       "|       tip_amount      | DOUBLE_PRECISION |   True   |   None  |     False     |   None  |\n",
+       "|      tolls_amount     | DOUBLE_PRECISION |   True   |   None  |     False     |   None  |\n",
+       "| improvement_surcharge | DOUBLE_PRECISION |   True   |   None  |     False     |   None  |\n",
+       "|      total_amount     | DOUBLE_PRECISION |   True   |   None  |     False     |   None  |\n",
+       "|  congestion_surcharge | DOUBLE_PRECISION |   True   |   None  |     False     |   None  |\n",
+       "|      airport_fee      | DOUBLE_PRECISION |   True   |   None  |     False     |   None  |\n",
+       "+-----------------------+------------------+----------+---------+---------------+---------+"
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "%sqlcmd columns --table taxi"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "c6f07f37-f572-4285-9c16-9c01e566afb1",
+   "metadata": {},
+   "source": [
+    "Query our data:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
    "id": "84902d46",
    "metadata": {},
    "outputs": [
@@ -243,7 +517,7 @@
        "[(1369769,)]"
       ]
      },
-     "execution_count": 8,
+     "execution_count": 10,
      "metadata": {},
      "output_type": "execute_result"
     }
diff --git a/doc/user-guide/tables-columns.md b/doc/user-guide/tables-columns.md
new file mode 100644
index 0000000..273ed7a
--- /dev/null
+++ b/doc/user-guide/tables-columns.md
@@ -0,0 +1,99 @@
+---
+jupytext:
+  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
+---
+
+# List tables and columns
+
+```{note}
+This example uses `SQLite` but the same commands work for other databases.
+```
+
+With JupySQL, you can quickly explore what tables are available in your database and which columns each table has.
+
++++
+
+## Setup
+
+```{code-cell} ipython3
+%load_ext sql
+%sql sqlite://
+```
+
+Let's create some sample tables in the default schema:
+
+```{code-cell} ipython3
+:tags: [hide-output]
+
+%%sql
+CREATE TABLE coordinates (x INT, y INT)
+```
+
+```{code-cell} ipython3
+:tags: [hide-output]
+
+%%sql
+CREATE TABLE people (name TEXT, birth_year INT)
+```
+
+## List tables
+
++++
+
+Use `%sqlcmd tables` to print the tables for the current connection:
+
+```{code-cell} ipython3
+%sqlcmd tables
+```
+
+Pass `--schema/-s` to get tables in a different schema:
+
+```python
+%sqlcmd tables --schema schema
+```
+
++++
+
+## List columns
+
+Use `%sqlcmd columns --table/-t` to get the columns for the given table.
+
+```{code-cell} ipython3
+%sqlcmd columns --table coordinates
+```
+
+```{code-cell} ipython3
+%sqlcmd columns -t people
+```
+
+If the table isn't in the defautl schema, pass `--schema/-s`.  Let's create a new table in a new schema:
+
+```{code-cell} ipython3
+:tags: [hide-output]
+
+import sqlite3
+
+with sqlite3.connect("my.db") as conn:
+    conn.execute("CREATE TABLE numbers (n FLOAT)")
+```
+
+```{code-cell} ipython3
+:tags: [hide-output]
+
+%%sql
+ATTACH DATABASE 'my.db' AS some_schema
+```
+
+Get the columns for the table in the newly created schema:
+
+```{code-cell} ipython3
+%sqlcmd columns --table numbers --schema some_schema
+```
diff --git a/setup.py b/setup.py
index dd64b69..5ff8358 100644
--- a/setup.py
+++ b/setup.py
@@ -27,6 +27,7 @@ install_requires = [
 ]
 
 DEV = [
+    "flake8",
     "pytest",
     "pandas",
     "invoke",
@@ -37,6 +38,7 @@ DEV = [
     "duckdb-engine",
     # sql.plot module tests
     "matplotlib",
+    "black",
 ]
 
 setup(
diff --git a/src/sql/__init__.py b/src/sql/__init__.py
index 8121792..b116e3e 100644
--- a/src/sql/__init__.py
+++ b/src/sql/__init__.py
@@ -1,6 +1,6 @@
 from .magic import RenderMagic, SqlMagic, load_ipython_extension
 
-__version__ = "0.5.3dev"
+__version__ = "0.5.4dev"
 
 
 __all__ = [
diff --git a/src/sql/command.py b/src/sql/command.py
index 34435d4..319f0b6 100644
--- a/src/sql/command.py
+++ b/src/sql/command.py
@@ -1,3 +1,5 @@
+from IPython.core.magic_arguments import parse_argstring
+
 from sqlalchemy.engine import Engine
 
 from sql import parse
@@ -7,7 +9,7 @@ from sql.connection import Connection
 
 class SQLPlotCommand:
     def __init__(self, magic, line) -> None:
-        self.args = parse.magic_args(magic.execute, line)
+        self.args = parse_argstring(magic.execute, line)
 
 
 class SQLCommand:
diff --git a/src/sql/inspect.py b/src/sql/inspect.py
new file mode 100644
index 0000000..16b3ab1
--- /dev/null
+++ b/src/sql/inspect.py
@@ -0,0 +1,84 @@
+from sqlalchemy import inspect
+from prettytable import PrettyTable
+
+
+from sql.connection import Connection
+from sql.telemetry import telemetry
+
+
+def _get_inspector(conn):
+    if conn:
+        return inspect(conn)
+
+    if not Connection.current:
+        raise RuntimeError("No active connection")
+    else:
+        return inspect(Connection.current.session)
+
+
+class DatabaseInspection:
+    def __repr__(self) -> str:
+        return self._table_txt
+
+    def _repr_html_(self) -> str:
+        return self._table_html
+
+
+class Tables(DatabaseInspection):
+    """
+    Displays the tables in a database
+    """
+
+    def __init__(self, schema=None, conn=None) -> None:
+        inspector = _get_inspector(conn)
+
+        self._table = PrettyTable()
+        self._table.field_names = ["Name"]
+
+        for row in inspector.get_table_names(schema=schema):
+            self._table.add_row([row])
+
+        self._table_html = self._table.get_html_string()
+        self._table_txt = self._table.get_string()
+
+
+class Columns(DatabaseInspection):
+    """
+    Represents the columns in a database table
+    """
+
+    def __init__(self, name, schema, conn=None) -> None:
+        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())
+
+        for row in columns:
+            self._table.add_row(list(row.values()))
+
+        self._table_html = self._table.get_html_string()
+        self._table_txt = self._table.get_string()
+
+
+@telemetry.log_call()
+def get_table_names(schema=None):
+    """Get table names for a given connection"""
+    return Tables(schema)
+
+
+@telemetry.log_call()
+def get_columns(name, schema=None):
+    """Get column names for a given connection"""
+    return Columns(name, schema)
diff --git a/src/sql/magic.py b/src/sql/magic.py
index 18318c2..64e797d 100644
--- a/src/sql/magic.py
+++ b/src/sql/magic.py
@@ -17,6 +17,7 @@ import sql.run
 from sql.store import store
 from sql.command import SQLCommand
 from sql.magic_plot import SqlPlotMagic
+from sql.magic_cmd import SqlCmdMagic
 
 try:
     from traitlets.config.configurable import Configurable
@@ -382,3 +383,4 @@ def load_ipython_extension(ip):
     ip.register_magics(SqlMagic)
     ip.register_magics(RenderMagic)
     ip.register_magics(SqlPlotMagic)
+    ip.register_magics(SqlCmdMagic)
diff --git a/src/sql/magic_cmd.py b/src/sql/magic_cmd.py
new file mode 100644
index 0000000..aff07f0
--- /dev/null
+++ b/src/sql/magic_cmd.py
@@ -0,0 +1,72 @@
+import sys
+import argparse
+
+from IPython.utils.process import arg_split
+from IPython.core.magic import (
+    Magics,
+    line_magic,
+    magics_class,
+)
+from IPython.core.magic_arguments import argument, magic_arguments
+from IPython.core.error import UsageError
+
+
+try:
+    from traitlets.config.configurable import Configurable
+except ImportError:
+    from IPython.config.configurable import Configurable
+
+
+from sql import inspect
+
+
+class CmdParser(argparse.ArgumentParser):
+    def exit(self, status=0, message=None):
+        if message:
+            self._print_message(message, sys.stderr)
+
+    def error(self, message):
+        raise UsageError(message)
+
+
+@magics_class
+class SqlCmdMagic(Magics, Configurable):
+    """%sqlcmd magic"""
+
+    @line_magic("sqlcmd")
+    @magic_arguments()
+    @argument("line", default="", type=str, help="Command name")
+    def execute(self, line="", cell="", local_ns=None):
+        """
+        Command
+        """
+        split = arg_split(line)
+        cmd_name, others = split[0].strip(), split[1:]
+
+        if cmd_name == "tables":
+            parser = CmdParser()
+
+            parser.add_argument(
+                "-s", "--schema", type=str, help="Schema name", required=False
+            )
+
+            args = parser.parse_args(others)
+
+            return inspect.get_table_names(schema=args.schema)
+        elif cmd_name == "columns":
+            parser = CmdParser()
+
+            parser.add_argument(
+                "-t", "--table", type=str, help="Table name", required=True
+            )
+            parser.add_argument(
+                "-s", "--schema", type=str, help="Schema name", required=False
+            )
+
+            args = parser.parse_args(others)
+            return inspect.get_columns(name=args.table, schema=args.schema)
+        else:
+            raise UsageError(
+                f"%sqlcmd has no command: {cmd_name!r}. "
+                "Valid commands are: 'tables', 'columns'"
+            )


Test patch:
diff --git a/src/tests/conftest.py b/src/tests/conftest.py
index b9e52a6..4ee30a4 100644
--- a/src/tests/conftest.py
+++ b/src/tests/conftest.py
@@ -7,6 +7,7 @@ from IPython.core.interactiveshell import InteractiveShell
 
 from sql.magic import SqlMagic, RenderMagic
 from sql.magic_plot import SqlPlotMagic
+from sql.magic_cmd import SqlCmdMagic
 
 PATH_TO_TESTS = Path(__file__).absolute().parent
 PATH_TO_TMP_ASSETS = PATH_TO_TESTS / "tmp"
@@ -46,6 +47,7 @@ def ip():
     ip_session.register_magics(SqlMagic)
     ip_session.register_magics(RenderMagic)
     ip_session.register_magics(SqlPlotMagic)
+    ip_session.register_magics(SqlCmdMagic)
 
     # runsql creates an inmemory sqlitedatabase
     runsql(
diff --git a/src/tests/test_inspect.py b/src/tests/test_inspect.py
new file mode 100644
index 0000000..92538db
--- /dev/null
+++ b/src/tests/test_inspect.py
@@ -0,0 +1,110 @@
+from inspect import getsource
+import sqlite3
+import pytest
+from functools import partial
+
+
+from sql import inspect, connection
+
+
+@pytest.fixture
+def sample_db(tmp_empty):
+    conn = connection.Connection.from_connect_str("sqlite://")
+
+    conn.session.execute("CREATE TABLE one (x INT, y TEXT)")
+    conn.session.execute("CREATE TABLE another (i INT, j TEXT)")
+
+    conn_mydb = sqlite3.connect("my.db")
+    conn_mydb.execute("CREATE TABLE uno (x INT, y TEXT)")
+    conn_mydb.execute("CREATE TABLE dos (i INT, j TEXT)")
+    conn_mydb.close()
+
+    conn.session.execute("ATTACH DATABASE 'my.db' AS schema")
+
+
+@pytest.mark.parametrize(
+    "function",
+    [
+        inspect.get_table_names,
+        partial(inspect.get_columns, name="some_name"),
+    ],
+)
+def test_no_active_session(function, monkeypatch):
+    monkeypatch.setattr(connection.Connection, "current", None)
+    with pytest.raises(RuntimeError, match="No active connection"):
+        function()
+
+
+@pytest.mark.parametrize(
+    "first, second, schema",
+    [
+        ["one", "another", None],
+        ["uno", "dos", "schema"],
+    ],
+)
+def test_tables(sample_db, first, second, schema):
+    tables = inspect.get_table_names(schema=schema)
+
+    assert "Name" in repr(tables)
+    assert first in repr(tables)
+    assert second in repr(tables)
+
+    assert "<table>" in tables._repr_html_()
+    assert "Name" in tables._repr_html_()
+    assert first in tables._repr_html_()
+    assert second in tables._repr_html_()
+
+
+@pytest.mark.parametrize(
+    "name, first, second, schema",
+    [
+        ["one", "x", "y", None],
+        ["another", "i", "j", None],
+        ["uno", "x", "y", "schema"],
+        ["dos", "i", "j", "schema"],
+    ],
+)
+def test_get_column(sample_db, name, first, second, schema):
+    columns = inspect.get_columns(name, schema=schema)
+
+    assert "name" in repr(columns)
+    assert first in repr(columns)
+    assert second in repr(columns)
+
+    assert "<table>" in columns._repr_html_()
+    assert "name" in columns._repr_html_()
+    assert first in columns._repr_html_()
+    assert second in columns._repr_html_()
+
+
+@pytest.mark.parametrize(
+    "name, schema, error",
+    [
+        [
+            "some_table",
+            "schema",
+            "There is no table with name 'some_table' in schema 'schema'",
+        ],
+        [
+            "name",
+            None,
+            "There is no table with name 'name' in the default schema",
+        ],
+    ],
+)
+def test_nonexistent_table(name, schema, error):
+    with pytest.raises(ValueError) as excinfo:
+        inspect.get_columns(name, schema)
+
+    assert str(excinfo.value) == error
+
+
+@pytest.mark.parametrize(
+    "function",
+    [
+        inspect.get_table_names,
+        inspect.get_columns,
+    ],
+)
+def test_telemetry(function):
+    assert "@telemetry.log_call" in getsource(function)
diff --git a/src/tests/test_magic_cmd.py b/src/tests/test_magic_cmd.py
new file mode 100644
index 0000000..7fc189d
--- /dev/null
+++ b/src/tests/test_magic_cmd.py
@@ -0,0 +1,72 @@
+import sqlite3
+
+import pytest
+from IPython.core.error import UsageError
+
+
+@pytest.mark.parametrize(
+    "cell, error_type, error_message",
+    [
+        [
+            "%sqlcmd stuff",
+            UsageError,
+            "%sqlcmd has no command: 'stuff'. Valid commands are: 'tables', 'columns'",
+        ],
+        [
+            "%sqlcmd columns",
+            UsageError,
+            "the following arguments are required: -t/--table",
+        ],
+    ],
+)
+def test_error(tmp_empty, ip, cell, error_type, error_message):
+    out = ip.run_cell(cell)
+
+    assert isinstance(out.error_in_exec, error_type)
+    assert str(out.error_in_exec) == error_message
+
+
+def test_tables(ip):
+    out = ip.run_cell("%sqlcmd tables").result._repr_html_()
+    assert "author" in out
+    assert "empty_table" in out
+    assert "test" in out
+
+
+def test_tables_with_schema(ip, tmp_empty):
+    with sqlite3.connect("my.db") as conn:
+        conn.execute("CREATE TABLE numbers (some_number FLOAT)")
+
+    ip.run_cell(
+        """%%sql
+ATTACH DATABASE 'my.db' AS some_schema
+"""
+    )
+
+    out = ip.run_cell("%sqlcmd tables --schema some_schema").result._repr_html_()
+
+    assert "numbers" in out
+
+
+def test_columns(ip):
+    out = ip.run_cell("%sqlcmd columns -t author").result._repr_html_()
+    assert "first_name" in out
+    assert "last_name" in out
+    assert "year_of_death" in out
+
+
+def test_columns_with_schema(ip, tmp_empty):
+    with sqlite3.connect("my.db") as conn:
+        conn.execute("CREATE TABLE numbers (some_number FLOAT)")
+
+    ip.run_cell(
+        """%%sql
+ATTACH DATABASE 'my.db' AS some_schema
+"""
+    )
+
+    out = ip.run_cell(
+        "%sqlcmd columns --table numbers --schema some_schema"
+    ).result._repr_html_()
+
+    assert "some_number" in out