Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/cli-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,7 @@ See :ref:`cli_create_table`.

::

Usage: sqlite-utils create-table [OPTIONS] PATH TABLE COLUMNS...
Usage: sqlite-utils create-table [OPTIONS] PATH TABLE [COLUMNS]...

Add a table with the specified columns. Columns should be specified using
name, type pairs, for example:
Expand All @@ -965,7 +965,13 @@ See :ref:`cli_create_table`.

Valid column types are text, integer, real, float, blob and any.

Use --sql to create the table from the results of a SQL query instead:

sqlite-utils create-table my.db tall_people \
--sql "select * from people where height > 180"

Options:
--sql TEXT Create the table using the results of this SQL query
--pk TEXT Column to use as primary key
--not-null TEXT Columns that should be created as NOT NULL
--default <TEXT TEXT>... Default value that should be set for a column
Expand Down
9 changes: 9 additions & 0 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2167,6 +2167,15 @@ Use the ``any`` type for a strict column that should accept integers, floating p
"name" TEXT
) STRICT

Instead of listing columns you can populate a new table from the results of a SQL query using ``--sql``. This is a shortcut for ``CREATE TABLE name AS ...``:

.. code-block:: bash

sqlite-utils create-table mydb.db tall_people \
--sql "select * from people where height > 180"

The new table takes its columns from the query, so ``--sql`` cannot be combined with column definitions or with the ``--pk``, ``--not-null``, ``--default``, ``--fk``, ``--transform`` and ``--strict`` options. It does work with ``--ignore`` and ``--replace``.

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``.

You can also pass ``--transform`` to transform the existing table to match the new schema. See :ref:`python_api_explicit_create` in the Python library documentation for details of how this option works.
Expand Down
49 changes: 48 additions & 1 deletion sqlite_utils/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1703,7 +1703,11 @@ def create_database(path, enable_wal, init_spatialite, load_extension):
required=True,
)
@click.argument("table")
@click.argument("columns", nargs=-1, required=True)
@click.argument("columns", nargs=-1)
@click.option(
"--sql",
help="Create the table using the results of this SQL query",
)
@click.option("pks", "--pk", help="Column to use as primary key", multiple=True)
@click.option(
"--not-null",
Expand Down Expand Up @@ -1747,6 +1751,7 @@ def create_table(
path,
table,
columns,
sql,
pks,
not_null,
default,
Expand All @@ -1769,10 +1774,52 @@ def create_table(
photo blob --pk id

Valid column types are text, integer, real, float, blob and any.

Use --sql to create the table from the results of a SQL query instead:

\b
sqlite-utils create-table my.db tall_people \\
--sql "select * from people where height > 180"
"""
db = sqlite_utils.Database(path)
_register_db_for_cleanup(db)
_load_extensions(db, load_extension)
if sql is not None:
if columns:
raise click.ClickException("Cannot use columns with --sql")
incompatible = [
option
for option, value in (
("--pk", pks),
("--not-null", not_null),
("--default", default),
("--fk", fk),
("--transform", transform),
("--strict", strict),
)
if value
]
if incompatible:
raise click.ClickException(
"Cannot use {} with --sql".format(", ".join(incompatible))
)
if table in db.table_names():
if ignore:
return
elif replace:
db[table].drop()
else:
raise click.ClickException(
f'Table "{table}" already exists. Use --replace to delete and replace it.'
)
db.execute(
"CREATE TABLE {table} AS {sql}".format(
table=quote_identifier(table), sql=sql
)
)
return
if not columns:
raise click.ClickException("Provide columns or use --sql")
if len(columns) % 2 == 1:
raise click.ClickException(
"columns must be an even number of 'name' 'type' pairs"
Expand Down
107 changes: 107 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1509,6 +1509,113 @@ def test_create_table_replace():
assert 'CREATE TABLE "dogs" (\n "id" INTEGER\n)' == db.table("dogs").schema


def test_create_table_sql():
runner = CliRunner()
with runner.isolated_filesystem():
db = Database("test.db")
db["people"].insert_all(
[
{"id": 1, "name": "Ann", "height": 190},
{"id": 2, "name": "Bob", "height": 150},
],
pk="id",
)
result = runner.invoke(
cli.cli,
[
"create-table",
"test.db",
"tall",
"--sql",
"select id, name from people where height > 180",
],
catch_exceptions=False,
)
assert result.exit_code == 0
assert "CREATE TABLE tall(id INT,name TEXT)" == db["tall"].schema
assert [{"id": 1, "name": "Ann"}] == list(db["tall"].rows)


def test_create_table_sql_error_if_table_exists():
runner = CliRunner()
with runner.isolated_filesystem():
db = Database("test.db")
db["dogs"].insert({"name": "Cleo"})
result = runner.invoke(
cli.cli, ["create-table", "test.db", "dogs", "--sql", "select 1"]
)
assert result.exit_code == 1
assert (
'Error: Table "dogs" already exists. Use --replace to delete and replace it.'
== result.output.strip()
)


def test_create_table_sql_ignore():
runner = CliRunner()
with runner.isolated_filesystem():
db = Database("test.db")
db["dogs"].insert({"name": "Cleo"})
result = runner.invoke(
cli.cli,
["create-table", "test.db", "dogs", "--sql", "select 1 as id", "--ignore"],
)
assert result.exit_code == 0
assert 'CREATE TABLE "dogs" (\n "name" TEXT\n)' == db["dogs"].schema


def test_create_table_sql_replace():
runner = CliRunner()
with runner.isolated_filesystem():
db = Database("test.db")
db["dogs"].insert({"name": "Cleo"})
result = runner.invoke(
cli.cli,
["create-table", "test.db", "dogs", "--sql", "select 1 as id", "--replace"],
)
assert result.exit_code == 0
assert "CREATE TABLE dogs(id)" == db["dogs"].schema


def test_create_table_sql_requires_columns_or_sql():
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(cli.cli, ["create-table", "test.db", "t"])
assert result.exit_code == 1
assert "Error: Provide columns or use --sql" == result.output.strip()


def test_create_table_sql_conflicts_with_columns():
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(
cli.cli,
["create-table", "test.db", "t", "id", "integer", "--sql", "select 1"],
)
assert result.exit_code == 1
assert "Error: Cannot use columns with --sql" == result.output.strip()


@pytest.mark.parametrize(
"extra_args,expected",
[
(["--pk", "id"], "Error: Cannot use --pk with --sql"),
(["--not-null", "id"], "Error: Cannot use --not-null with --sql"),
(["--default", "id", "1"], "Error: Cannot use --default with --sql"),
(["--strict"], "Error: Cannot use --strict with --sql"),
],
)
def test_create_table_sql_conflicts_with_options(extra_args, expected):
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(
cli.cli,
["create-table", "test.db", "t", "--sql", "select 1 as id"] + extra_args,
)
assert result.exit_code == 1
assert expected == result.output.strip()


def test_create_view():
runner = CliRunner()
with runner.isolated_filesystem():
Expand Down
Loading