diff --git a/docs/cli-reference.rst b/docs/cli-reference.rst index 7d73e12..b1a652e 100644 --- a/docs/cli-reference.rst +++ b/docs/cli-reference.rst @@ -1176,6 +1176,20 @@ reset-counts -h, --help Show this message and exit. +duplicate +========= + +:: + + Usage: sqlite-utils duplicate [OPTIONS] PATH TABLE NEW_TABLE + + Create a duplicate of this table, copying across the schema and all row data. + + Options: + --load-extension TEXT SQLite extensions to load + -h, --help Show this message and exit. + + drop-table ========== diff --git a/docs/cli.rst b/docs/cli.rst index 9169fba..e126881 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -1476,6 +1476,15 @@ You can specify foreign key relationships between the tables you are creating us If a table with the same name already exists, you will get an error. You can choose to silently ignore this error with ``--ignore``, or you can replace the existing table with a new, empty table using ``--replace``. +.. _cli_duplicate_table: + +Duplicating tables +================== + +The ``duplicate`` command duplicates a table - creating a new table with the same schema and a copy of all of the rows:: + + $ sqlite-utils duplicate books.db authors authors_copy + .. _cli_drop_table: Dropping tables diff --git a/docs/python-api.rst b/docs/python-api.rst index 4d6676a..464afb2 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -691,6 +691,8 @@ The ``table.duplicate()`` method creates a copy of the table, copying both the t The new ``authors_copy`` table will now contain a duplicate copy of the data from ``authors``. +This method raises ``sqlite_utils.db.NoTable`` if the table does not exist. + .. _python_api_bulk_inserts: Bulk inserts diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 86eddfb..0af947d 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -5,7 +5,7 @@ from datetime import datetime import hashlib import pathlib import sqlite_utils -from sqlite_utils.db import AlterError, BadMultiValues, DescIndex +from sqlite_utils.db import AlterError, BadMultiValues, DescIndex, NoTable from sqlite_utils.utils import maximize_csv_field_size_limit from sqlite_utils import recipes import textwrap @@ -1465,6 +1465,27 @@ def create_table( ) +@cli.command(name="duplicate") +@click.argument( + "path", + type=click.Path(file_okay=True, dir_okay=False, allow_dash=False), + required=True, +) +@click.argument("table") +@click.argument("new_table") +@load_extension_option +def create_table(path, table, new_table, load_extension): + """ + Create a duplicate of this table, copying across the schema and all row data. + """ + db = sqlite_utils.Database(path) + _load_extensions(db, load_extension) + try: + db[table].duplicate(new_table) + except NoTable: + raise click.ClickException('Table "{}" does not exist'.format(table)) + + @cli.command(name="drop-table") @click.argument( "path", diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index 3a27cbe..4755ea2 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -1,3 +1,4 @@ +from ast import Not from .utils import ( chunks, hash_record, @@ -224,6 +225,11 @@ class NoObviousTable(Exception): pass +class NoTable(Exception): + "Specified table does not exist" + pass + + class BadPrimaryKey(Exception): "Table does not have a single obvious primary key" pass @@ -1482,7 +1488,8 @@ class Table(Queryable): :param new_name: Name of the new table """ - assert self.exists() + if not self.exists(): + raise NoTable(f"Table {self.name} does not exist") with self.db.conn: sql = "CREATE TABLE [{new_table}] AS SELECT * FROM [{table}];".format( new_table=new_name, diff --git a/tests/test_cli.py b/tests/test_cli.py index e2e4aa2..a2ce739 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2129,3 +2129,26 @@ def test_analyze(tmpdir, options, expected): result = CliRunner().invoke(cli.cli, ["analyze", db_path] + options) assert result.exit_code == 0 assert list(db["sqlite_stat1"].rows) == expected + + +def test_duplicate_table(tmpdir): + db_path = str(tmpdir / "test.db") + db = Database(db_path) + db["one"].insert({"id": 1, "name": "Cleo"}, pk="id") + # First try a non-existent table + result_error = CliRunner().invoke( + cli.cli, + ["duplicate", db_path, "missing", "two"], + catch_exceptions=False, + ) + assert result_error.exit_code == 1 + assert result_error.output == 'Error: Table "missing" does not exist\n' + # Now try for a table that exists + result = CliRunner().invoke( + cli.cli, + ["duplicate", db_path, "one", "two"], + catch_exceptions=False, + ) + assert result.exit_code == 0 + assert db["one"].columns_dict == db["two"].columns_dict + assert list(db["one"].rows) == list(db["two"].rows) diff --git a/tests/test_duplicate.py b/tests/test_duplicate.py index f56556d..87996ef 100644 --- a/tests/test_duplicate.py +++ b/tests/test_duplicate.py @@ -1,3 +1,4 @@ +from sqlite_utils.db import NoTable import datetime import pytest @@ -38,5 +39,5 @@ def test_duplicate(fresh_db): def test_duplicate_fails_if_table_does_not_exist(fresh_db): - with pytest.raises(AssertionError): + with pytest.raises(NoTable): fresh_db["not_a_table"].duplicate("duplicated")