diff --git a/docs/cli.rst b/docs/cli.rst index 3bb2a83..d39d367 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -348,6 +348,19 @@ It defaults to showing triggers for all tables. To see triggers for one or more The command takes the same format options as the ``tables`` and ``views`` commands. +.. _cli_schema: + +Showing the schema +================== + +The ``sqlite-utils schema`` command shows the full SQL schema for the database:: + + $ sqlite-utils schema dogs.db + CREATE TABLE "dogs" ( + [id] INTEGER PRIMARY KEY, + [name] TEXT + ); + .. _cli_analyze_tables: Analyzing tables diff --git a/docs/python-api.rst b/docs/python-api.rst index f629c1d..725445c 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -297,6 +297,21 @@ If the record does not exist a ``NotFoundError`` will be raised: except NotFoundError: print("Dog not found") +.. _python_api_schema: + +Showing the schema +================== + +The ``db.schema`` property returns the full SQL schema for the database as a string:: + + >>> db = sqlite_utils.Database("dogs.db") + >>> print(db.schema) + >>> print(db.schema) + CREATE TABLE "dogs" ( + [id] INTEGER PRIMARY KEY, + [name] TEXT + ); + .. _python_api_creating_tables: Creating tables diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 2e3d08e..6c40454 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -1348,6 +1348,23 @@ def indexes( ) +@cli.command() +@click.argument( + "path", + type=click.Path(file_okay=True, dir_okay=False, allow_dash=False), + required=True, +) +@load_extension_option +def schema( + path, + load_extension, +): + "Show full schema for this database" + db = sqlite_utils.Database(path) + _load_extensions(db, load_extension) + click.echo(db.schema) + + @cli.command() @click.argument( "path", diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index 6781735..53fae10 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -301,6 +301,18 @@ class Database: "Returns {trigger_name: sql} dictionary" return {trigger.name: trigger.sql for trigger in self.triggers} + @property + def schema(self): + sqls = [] + for row in self.execute( + "select sql from sqlite_master where sql is not null" + ).fetchall(): + sql = row[0] + if not sql.strip().endswith(";"): + sql += ";" + sqls.append(sql) + return "\n".join(sqls) + @property def journal_mode(self): return self.execute("PRAGMA journal_mode;").fetchone()[0] diff --git a/tests/test_cli.py b/tests/test_cli.py index 56f1953..a73cdff 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1916,6 +1916,33 @@ def test_triggers(tmpdir, extra_args, expected): assert result.output == expected +def test_schema(tmpdir): + db_path = str(tmpdir / "test.db") + db = Database(db_path) + db["dogs"].create({"id": int, "name": str}) + db["chickens"].create({"id": int, "name": str, "breed": str}) + db["chickens"].create_index(["breed"]) + result = CliRunner().invoke( + cli.cli, + ["schema", db_path], + catch_exceptions=False, + ) + assert result.exit_code == 0 + assert result.output == ( + "CREATE TABLE [dogs] (\n" + " [id] INTEGER,\n" + " [name] TEXT\n" + ");\n" + "CREATE TABLE [chickens] (\n" + " [id] INTEGER,\n" + " [name] TEXT,\n" + " [breed] TEXT\n" + ");\n" + "CREATE INDEX [idx_chickens_breed]\n" + " ON [chickens] ([breed]);\n" + ) + + def test_long_csv_column_value(tmpdir): db_path = str(tmpdir / "test.db") csv_path = str(tmpdir / "test.csv") diff --git a/tests/test_introspect.py b/tests/test_introspect.py index 66dbcff..d54ca88 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -62,8 +62,12 @@ def test_columns(existing_db): ] -def test_schema(existing_db): - assert "CREATE TABLE foo (text TEXT)" == existing_db["foo"].schema +def test_table_schema(existing_db): + assert existing_db["foo"].schema == "CREATE TABLE foo (text TEXT)" + + +def test_database_schema(existing_db): + assert existing_db.schema == "CREATE TABLE foo (text TEXT);" def test_table_repr(fresh_db):