Use double quotes not braces for tables and columns (#678)

Closes #677
This commit is contained in:
Simon Willison 2025-11-23 20:43:26 -08:00 • committed by GitHub
commit fb93452ea8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 910 additions and 849 deletions

View file

@ -547,13 +547,13 @@ To see the in-memory database schema that would be used for a file or for multip
.. code-block:: output .. code-block:: output
CREATE TABLE [dogs] ( CREATE TABLE "dogs" (
[id] INTEGER, "id" INTEGER,
[age] INTEGER, "age" INTEGER,
[name] TEXT "name" TEXT
); );
CREATE VIEW t1 AS select * from [dogs]; CREATE VIEW "t1" AS select * from "dogs";
CREATE VIEW t AS select * from [dogs]; CREATE VIEW "t" AS select * from "dogs";
You can run the equivalent of the :ref:`analyze-tables <cli_analyze_tables>` command using ``--analyze``: You can run the equivalent of the :ref:`analyze-tables <cli_analyze_tables>` command using ``--analyze``:
@ -596,15 +596,15 @@ You can output SQL that will both create the tables and insert the full data use
.. code-block:: output .. code-block:: output
BEGIN TRANSACTION; BEGIN TRANSACTION;
CREATE TABLE [dogs] ( CREATE TABLE "dogs" (
[id] INTEGER, "id" INTEGER,
[age] INTEGER, "age" INTEGER,
[name] TEXT "name" TEXT
); );
INSERT INTO "dogs" VALUES('1','4','Cleo'); INSERT INTO "dogs" VALUES('1','4','Cleo');
INSERT INTO "dogs" VALUES('2','2','Pancakes'); INSERT INTO "dogs" VALUES('2','2','Pancakes');
CREATE VIEW t1 AS select * from [dogs]; CREATE VIEW "t1" AS select * from "dogs";
CREATE VIEW t AS select * from [dogs]; CREATE VIEW "t" AS select * from "dogs";
COMMIT; COMMIT;
Passing ``--save other.db`` will instead use that SQL to populate a new database file: Passing ``--save other.db`` will instead use that SQL to populate a new database file:
@ -746,10 +746,10 @@ Use ``--schema`` to include the schema of each table:
------- ----------------------------------------------- ------- -----------------------------------------------
Gosh CREATE TABLE Gosh (c1 text, c2 text, c3 text) Gosh CREATE TABLE Gosh (c1 text, c2 text, c3 text)
Gosh2 CREATE TABLE Gosh2 (c1 text, c2 text, c3 text) Gosh2 CREATE TABLE Gosh2 (c1 text, c2 text, c3 text)
dogs CREATE TABLE [dogs] ( dogs CREATE TABLE "dogs" (
[id] INTEGER, "id" INTEGER,
[age] INTEGER, "age" INTEGER,
[name] TEXT) "name" TEXT)
The ``--nl``, ``--csv``, ``--tsv``, ``--table`` and ``--fmt`` options are also available. The ``--nl``, ``--csv``, ``--tsv``, ``--table`` and ``--fmt`` options are also available.
@ -839,13 +839,13 @@ The ``triggers`` command shows any triggers configured for the database:
name table sql name table sql
--------------- --------- ----------------------------------------------------------------- --------------- --------- -----------------------------------------------------------------
plants_insert plants CREATE TRIGGER [plants_insert] AFTER INSERT ON [plants] plants_insert plants CREATE TRIGGER "plants_insert" AFTER INSERT ON "plants"
BEGIN BEGIN
INSERT OR REPLACE INTO [_counts] INSERT OR REPLACE INTO "_counts"
VALUES ( VALUES (
'plants', 'plants',
COALESCE( COALESCE(
(SELECT count FROM [_counts] WHERE [table] = 'plants'), (SELECT count FROM "_counts" WHERE "table" = 'plants'),
0 0
) + 1 ) + 1
); );
@ -876,8 +876,8 @@ The ``sqlite-utils schema`` command shows the full SQL schema for the database:
.. code-block:: output .. code-block:: output
CREATE TABLE "dogs" ( CREATE TABLE "dogs" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[name] TEXT "name" TEXT
); );
This will show the schema for every table and index in the database. To view the schema just for a specified subset of tables pass those as additional arguments: This will show the schema for every table and index in the database. To view the schema just for a specified subset of tables pass those as additional arguments:
@ -983,16 +983,16 @@ The ``_analyze_tables_`` table has the following schema:
.. code-block:: sql .. code-block:: sql
CREATE TABLE [_analyze_tables_] ( CREATE TABLE "_analyze_tables_" (
[table] TEXT, "table" TEXT,
[column] TEXT, "column" TEXT,
[total_rows] INTEGER, "total_rows" INTEGER,
[num_null] INTEGER, "num_null" INTEGER,
[num_blank] INTEGER, "num_blank" INTEGER,
[num_distinct] INTEGER, "num_distinct" INTEGER,
[most_common] TEXT, "most_common" TEXT,
[least_common] TEXT, "least_common" TEXT,
PRIMARY KEY ([table], [column]) PRIMARY KEY ("table", "column")
); );
The ``most_common`` and ``least_common`` columns will contain nested JSON arrays of the most common and least common values that look like this: The ``most_common`` and ``least_common`` columns will contain nested JSON arrays of the most common and least common values that look like this:
@ -1197,23 +1197,23 @@ Inserting this into a table using ``sqlite-utils insert logs.db logs log.json``
.. code-block:: sql .. code-block:: sql
CREATE TABLE [logs] ( CREATE TABLE "logs" (
[httpRequest] TEXT, "httpRequest" TEXT,
[insertId] TEXT, "insertId" TEXT,
[labels] TEXT "labels" TEXT
); );
With the ``--flatten`` option columns will be created using ``topkey_nextkey`` column names - so running ``sqlite-utils insert logs.db logs log.json --flatten`` will create the following schema instead: With the ``--flatten`` option columns will be created using ``topkey_nextkey`` column names - so running ``sqlite-utils insert logs.db logs log.json --flatten`` will create the following schema instead:
.. code-block:: sql .. code-block:: sql
CREATE TABLE [logs] ( CREATE TABLE "logs" (
[httpRequest_latency] TEXT, "httpRequest_latency" TEXT,
[httpRequest_requestMethod] TEXT, "httpRequest_requestMethod" TEXT,
[httpRequest_requestSize] TEXT, "httpRequest_requestSize" TEXT,
[httpRequest_status] INTEGER, "httpRequest_status" INTEGER,
[insertId] TEXT, "insertId" TEXT,
[labels_service] TEXT "labels_service" TEXT
); );
.. _cli_insert_csv_tsv: .. _cli_insert_csv_tsv:
@ -1272,9 +1272,9 @@ Will produce this schema:
.. code-block:: output .. code-block:: output
CREATE TABLE "creatures" ( CREATE TABLE "creatures" (
[name] TEXT, "name" TEXT,
[age] INTEGER, "age" INTEGER,
[weight] FLOAT "weight" FLOAT
); );
You can set the ``SQLITE_UTILS_DETECT_TYPES`` environment variable if you want ``--detect-types`` to be the default behavior: You can set the ``SQLITE_UTILS_DETECT_TYPES`` environment variable if you want ``--detect-types`` to be the default behavior:
@ -1354,8 +1354,8 @@ This will produce the following schema:
.. code-block:: sql .. code-block:: sql
CREATE TABLE [loglines] ( CREATE TABLE "loglines" (
[line] TEXT "line" TEXT
); );
You can also insert the entire contents of the file into a single column called ``text`` using ``--text``: You can also insert the entire contents of the file into a single column called ``text`` using ``--text``:
@ -1368,8 +1368,8 @@ The schema here will be:
.. code-block:: sql .. code-block:: sql
CREATE TABLE [content] ( CREATE TABLE "content" (
[text] TEXT "text" TEXT
); );
.. _cli_insert_convert: .. _cli_insert_convert:
@ -1462,8 +1462,8 @@ The result looks like this:
.. code-block:: output .. code-block:: output
BEGIN TRANSACTION; BEGIN TRANSACTION;
CREATE TABLE [words] ( CREATE TABLE "words" (
[word] TEXT "word" TEXT
); );
INSERT INTO "words" VALUES('A'); INSERT INTO "words" VALUES('A');
INSERT INTO "words" VALUES('bunch'); INSERT INTO "words" VALUES('bunch');
@ -1568,10 +1568,10 @@ By default this command will create a table with the following schema:
.. code-block:: sql .. code-block:: sql
CREATE TABLE [images] ( CREATE TABLE "images" (
[path] TEXT PRIMARY KEY, "path" TEXT PRIMARY KEY,
[content] BLOB, "content" BLOB,
[size] INTEGER "size" INTEGER
); );
Content will be treated as binary by default and stored in a ``BLOB`` column. You can use the ``--text`` option to store that content in a ``TEXT`` column instead. Content will be treated as binary by default and stored in a ``BLOB`` column. You can use the ``--text`` option to store that content in a ``TEXT`` column instead.
@ -1586,10 +1586,10 @@ This will result in the following schema:
.. code-block:: sql .. code-block:: sql
CREATE TABLE [images] ( CREATE TABLE "images" (
[path] TEXT PRIMARY KEY, "path" TEXT PRIMARY KEY,
[md5] TEXT, "md5" TEXT,
[mtime] FLOAT "mtime" FLOAT
); );
Note that there's no ``content`` column here at all - if you specify custom columns using ``-c`` you need to include ``-c content`` to create that column. Note that there's no ``content`` column here at all - if you specify custom columns using ``-c`` you need to include ``-c content`` to create that column.
@ -1886,10 +1886,10 @@ The type of the returned values will be taken into account when creating the new
.. code-block:: sql .. code-block:: sql
CREATE TABLE [places] ( CREATE TABLE "places" (
[location] TEXT, "location" TEXT,
[latitude] FLOAT, "latitude" FLOAT,
[longitude] FLOAT "longitude" FLOAT
); );
The code function can also return ``None``, in which case its output will be ignored. You can drop the original column at the end of the operation by adding ``--drop``. The code function can also return ``None``, in which case its output will be ignored. You can drop the original column at the end of the operation by adding ``--drop``.
@ -1933,11 +1933,11 @@ You can specify columns that should be NOT NULL using ``--not-null colname``. Yo
table schema table schema
------- -------------------------------- ------- --------------------------------
mytable CREATE TABLE [mytable] ( mytable CREATE TABLE "mytable" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[name] TEXT NOT NULL, "name" TEXT NOT NULL,
[age] INTEGER NOT NULL, "age" INTEGER NOT NULL,
[is_good] INTEGER DEFAULT '1' "is_good" INTEGER DEFAULT '1'
) )
You can specify foreign key relationships between the tables you are creating using ``--fk colname othertable othercolumn``: You can specify foreign key relationships between the tables you are creating using ``--fk colname othertable othercolumn``:
@ -1964,14 +1964,14 @@ You can specify foreign key relationships between the tables you are creating us
table schema table schema
------- ------------------------------------------------- ------- -------------------------------------------------
authors CREATE TABLE [authors] ( authors CREATE TABLE "authors" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[name] TEXT "name" TEXT
) )
books CREATE TABLE [books] ( books CREATE TABLE "books" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[title] TEXT, "title" TEXT,
[author_id] INTEGER REFERENCES [authors]([id]) "author_id" INTEGER REFERENCES "authors"("id")
) )
You can create a table in `SQLite STRICT mode <https://www.sqlite.org/stricttables.html>`__ using ``--strict``: You can create a table in `SQLite STRICT mode <https://www.sqlite.org/stricttables.html>`__ using ``--strict``:
@ -1988,9 +1988,9 @@ You can create a table in `SQLite STRICT mode <https://www.sqlite.org/stricttabl
table schema table schema
------- ------------------------ ------- ------------------------
mytable CREATE TABLE [mytable] ( mytable CREATE TABLE "mytable" (
[id] INTEGER, "id" INTEGER,
[name] TEXT "name" TEXT
) STRICT ) STRICT
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``. 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``.
@ -2100,16 +2100,16 @@ If you want to see the SQL that will be executed to make the change without actu
.. code-block:: output .. code-block:: output
CREATE TABLE [roadside_attractions_new_4033a60276b9] ( CREATE TABLE "roadside_attractions_new_4033a60276b9" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[longitude] FLOAT, "longitude" FLOAT,
[latitude] FLOAT, "latitude" FLOAT,
[name] TEXT DEFAULT 'Untitled' "name" TEXT DEFAULT 'Untitled'
); );
INSERT INTO [roadside_attractions_new_4033a60276b9] ([longitude], [latitude], [id], [name]) INSERT INTO "roadside_attractions_new_4033a60276b9" ("longitude", "latitude", "id", "name")
SELECT [longitude], [latitude], [pk], [name] FROM [roadside_attractions]; SELECT "longitude", "latitude", "pk", "name" FROM "roadside_attractions";
DROP TABLE [roadside_attractions]; DROP TABLE "roadside_attractions";
ALTER TABLE [roadside_attractions_new_4033a60276b9] RENAME TO [roadside_attractions]; ALTER TABLE "roadside_attractions_new_4033a60276b9" RENAME TO "roadside_attractions";
.. _cli_transform_table_add_primary_key_to_rowid: .. _cli_transform_table_add_primary_key_to_rowid:
@ -2126,8 +2126,8 @@ SQLite tables that are created without an explicit primary key are created as `r
.. code-block:: output .. code-block:: output
CREATE TABLE [chickens] ( CREATE TABLE "chickens" (
[name] TEXT "name" TEXT
); );
.. code-block:: bash .. code-block:: bash
@ -2161,8 +2161,8 @@ You can use ``sqlite-utils transform ... --pk id`` to add a primary key column c
.. code-block:: output .. code-block:: output
CREATE TABLE "chickens" ( CREATE TABLE "chickens" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[name] TEXT "name" TEXT
); );
.. code-block:: bash .. code-block:: bash
@ -2208,17 +2208,17 @@ This would produce the following schema:
.. code-block:: sql .. code-block:: sql
CREATE TABLE "trees" ( CREATE TABLE "trees" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[TreeAddress] TEXT, "TreeAddress" TEXT,
[species_id] INTEGER, "species_id" INTEGER,
FOREIGN KEY(species_id) REFERENCES species(id) FOREIGN KEY(species_id) REFERENCES species(id)
); );
CREATE TABLE [species] ( CREATE TABLE "species" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[species] TEXT "species" TEXT
); );
CREATE UNIQUE INDEX [idx_species_species] CREATE UNIQUE INDEX "idx_species_species"
ON [species] ([species]); ON "species" ("species");
The command takes the following options: The command takes the following options:
@ -2251,40 +2251,40 @@ After running the above, the command ``sqlite-utils schema global.db`` reveals t
.. code-block:: sql .. code-block:: sql
CREATE TABLE [countries] ( CREATE TABLE "countries" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[country] TEXT, "country" TEXT,
[name] TEXT "name" TEXT
); );
CREATE TABLE "power_plants" ( CREATE TABLE "power_plants" (
[country_id] INTEGER, "country_id" INTEGER,
[name] TEXT, "name" TEXT,
[gppd_idnr] TEXT, "gppd_idnr" TEXT,
[capacity_mw] TEXT, "capacity_mw" TEXT,
[latitude] TEXT, "latitude" TEXT,
[longitude] TEXT, "longitude" TEXT,
[primary_fuel] TEXT, "primary_fuel" TEXT,
[other_fuel1] TEXT, "other_fuel1" TEXT,
[other_fuel2] TEXT, "other_fuel2" TEXT,
[other_fuel3] TEXT, "other_fuel3" TEXT,
[commissioning_year] TEXT, "commissioning_year" TEXT,
[owner] TEXT, "owner" TEXT,
[source] TEXT, "source" TEXT,
[url] TEXT, "url" TEXT,
[geolocation_source] TEXT, "geolocation_source" TEXT,
[wepp_id] TEXT, "wepp_id" TEXT,
[year_of_capacity_data] TEXT, "year_of_capacity_data" TEXT,
[generation_gwh_2013] TEXT, "generation_gwh_2013" TEXT,
[generation_gwh_2014] TEXT, "generation_gwh_2014" TEXT,
[generation_gwh_2015] TEXT, "generation_gwh_2015" TEXT,
[generation_gwh_2016] TEXT, "generation_gwh_2016" TEXT,
[generation_gwh_2017] TEXT, "generation_gwh_2017" TEXT,
[generation_data_source] TEXT, "generation_data_source" TEXT,
[estimated_generation_gwh] TEXT, "estimated_generation_gwh" TEXT,
FOREIGN KEY([country_id]) REFERENCES [countries]([id]) FOREIGN KEY("country_id") REFERENCES "countries"("id")
); );
CREATE UNIQUE INDEX [idx_countries_country_name] CREATE UNIQUE INDEX "idx_countries_country_name"
ON [countries] ([country], [name]); ON "countries" ("country", "name");
.. _cli_create_view: .. _cli_create_view:
@ -2567,17 +2567,17 @@ Use the ``--sql`` option to output the SQL that would be executed, rather than r
select select
rowid, rowid,
* *
from [documents] from "documents"
) )
select select
[original].* "original".*
from from
[original] "original"
join [documents_fts] on [original].rowid = [documents_fts].rowid join "documents_fts" on "original".rowid = "documents_fts".rowid
where where
[documents_fts] match :query "documents_fts" match :query
order by order by
[documents_fts].rank "documents_fts".rank
.. _cli_enable_counts: .. _cli_enable_counts:

View file

@ -459,8 +459,8 @@ The ``db.schema`` property returns the full SQL schema for the database as a str
>>> db = sqlite_utils.Database("dogs.db") >>> db = sqlite_utils.Database("dogs.db")
>>> print(db.schema) >>> print(db.schema)
CREATE TABLE "dogs" ( CREATE TABLE "dogs" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[name] TEXT "name" TEXT
); );
.. _python_api_creating_tables: .. _python_api_creating_tables:
@ -544,11 +544,11 @@ This will create a table with the following schema:
.. code-block:: sql .. code-block:: sql
CREATE TABLE [dogs] ( CREATE TABLE "dogs" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[name] TEXT, "name" TEXT,
[age] INTEGER, "age" INTEGER,
[weight] FLOAT "weight" FLOAT
) )
.. _python_api_explicit_create: .. _python_api_explicit_create:
@ -729,10 +729,10 @@ Here's an example that uses these features:
# {'id': 3, 'name': 'Dharma', 'score': 1}] # {'id': 3, 'name': 'Dharma', 'score': 1}]
print(db.table("authors").schema) print(db.table("authors").schema)
# Outputs: # Outputs:
# CREATE TABLE [authors] ( # CREATE TABLE "authors" (
# [id] INTEGER PRIMARY KEY, # "id" INTEGER PRIMARY KEY,
# [name] TEXT NOT NULL, # "name" TEXT NOT NULL,
# [score] INTEGER NOT NULL DEFAULT 1 # "score" INTEGER NOT NULL DEFAULT 1
# ) # )
@ -1196,10 +1196,10 @@ You can inspect the database to see the results like this::
>>> list(db.table("characteristics_dogs").rows) >>> list(db.table("characteristics_dogs").rows)
[{'characteristics_id': 1, 'dogs_id': 1}, {'characteristics_id': 2, 'dogs_id': 1}] [{'characteristics_id': 1, 'dogs_id': 1}, {'characteristics_id': 2, 'dogs_id': 1}]
>>> print(db.table("characteristics_dogs").schema) >>> print(db.table("characteristics_dogs").schema)
CREATE TABLE [characteristics_dogs] ( CREATE TABLE "characteristics_dogs" (
[characteristics_id] INTEGER REFERENCES [characteristics]([id]), "characteristics_id" INTEGER REFERENCES "characteristics"("id"),
[dogs_id] INTEGER REFERENCES [dogs]([id]), "dogs_id" INTEGER REFERENCES "dogs"("id"),
PRIMARY KEY ([characteristics_id], [dogs_id]) PRIMARY KEY ("characteristics_id", "dogs_id")
) )
.. _python_api_analyze_column: .. _python_api_analyze_column:
@ -1648,10 +1648,10 @@ The schema of the above table is:
.. code-block:: sql .. code-block:: sql
CREATE TABLE [Trees] ( CREATE TABLE "Trees" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[TreeAddress] TEXT, "TreeAddress" TEXT,
[Species] TEXT "Species" TEXT
) )
Here's how to extract the ``Species`` column using ``.extract()``: Here's how to extract the ``Species`` column using ``.extract()``:
@ -1665,9 +1665,9 @@ After running this code the table schema now looks like this:
.. code-block:: sql .. code-block:: sql
CREATE TABLE "Trees" ( CREATE TABLE "Trees" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[TreeAddress] TEXT, "TreeAddress" TEXT,
[Species_id] INTEGER, "Species_id" INTEGER,
FOREIGN KEY(Species_id) REFERENCES Species(id) FOREIGN KEY(Species_id) REFERENCES Species(id)
) )
@ -1675,9 +1675,9 @@ A new ``Species`` table will have been created with the following schema:
.. code-block:: sql .. code-block:: sql
CREATE TABLE [Species] ( CREATE TABLE "Species" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[Species] TEXT "Species" TEXT
) )
The ``.extract()`` method defaults to creating a table with the same name as the column that was extracted, and adding a foreign key column called ``tablename_id``. The ``.extract()`` method defaults to creating a table with the same name as the column that was extracted, and adding a foreign key column called ``tablename_id``.
@ -1693,15 +1693,15 @@ The resulting schema looks like this:
.. code-block:: sql .. code-block:: sql
CREATE TABLE "Trees" ( CREATE TABLE "Trees" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[TreeAddress] TEXT, "TreeAddress" TEXT,
[tree_species_id] INTEGER, "tree_species_id" INTEGER,
FOREIGN KEY(tree_species_id) REFERENCES tree_species(id) FOREIGN KEY(tree_species_id) REFERENCES tree_species(id)
) )
CREATE TABLE [tree_species] ( CREATE TABLE "tree_species" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[Species] TEXT "Species" TEXT
) )
You can also extract multiple columns into the same external table. Say for example you have a table like this: You can also extract multiple columns into the same external table. Say for example you have a table like this:
@ -1726,15 +1726,15 @@ This produces the following schema:
.. code-block:: sql .. code-block:: sql
CREATE TABLE "Trees" ( CREATE TABLE "Trees" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[TreeAddress] TEXT, "TreeAddress" TEXT,
[CommonName_LatinName_id] INTEGER, "CommonName_LatinName_id" INTEGER,
FOREIGN KEY(CommonName_LatinName_id) REFERENCES CommonName_LatinName(id) FOREIGN KEY(CommonName_LatinName_id) REFERENCES CommonName_LatinName(id)
) )
CREATE TABLE [CommonName_LatinName] ( CREATE TABLE "CommonName_LatinName" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[CommonName] TEXT, "CommonName" TEXT,
[LatinName] TEXT "LatinName" TEXT
) )
The table name ``CommonName_LatinName`` is derived from the extract columns. You can use ``table=`` and ``fk_column=`` to specify custom names like this: The table name ``CommonName_LatinName`` is derived from the extract columns. You can use ``table=`` and ``fk_column=`` to specify custom names like this:
@ -1748,15 +1748,15 @@ This produces the following schema:
.. code-block:: sql .. code-block:: sql
CREATE TABLE "Trees" ( CREATE TABLE "Trees" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[TreeAddress] TEXT, "TreeAddress" TEXT,
[species_id] INTEGER, "species_id" INTEGER,
FOREIGN KEY(species_id) REFERENCES Species(id) FOREIGN KEY(species_id) REFERENCES Species(id)
) )
CREATE TABLE [Species] ( CREATE TABLE "Species" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[CommonName] TEXT, "CommonName" TEXT,
[LatinName] TEXT "LatinName" TEXT
) )
You can use the ``rename=`` argument to rename columns in the lookup table. To create a ``Species`` table with columns called ``name`` and ``latin`` you can do this: You can use the ``rename=`` argument to rename columns in the lookup table. To create a ``Species`` table with columns called ``name`` and ``latin`` you can do this:
@ -1774,10 +1774,10 @@ This produces a lookup table like so:
.. code-block:: sql .. code-block:: sql
CREATE TABLE [Species] ( CREATE TABLE "Species" (
[id] INTEGER PRIMARY KEY, "id" INTEGER PRIMARY KEY,
[name] TEXT, "name" TEXT,
[latin] TEXT "latin" TEXT
) )
.. _python_api_hash: .. _python_api_hash:
@ -2100,12 +2100,12 @@ The ``.schema`` property outputs the table's schema as a SQL string::
"Longitude" REAL, "Longitude" REAL,
"Location" TEXT "Location" TEXT
, ,
FOREIGN KEY ("PlantType") REFERENCES [PlantType](id), FOREIGN KEY ("PlantType") REFERENCES "PlantType"(id),
FOREIGN KEY ("qCaretaker") REFERENCES [qCaretaker](id), FOREIGN KEY ("qCaretaker") REFERENCES "qCaretaker"(id),
FOREIGN KEY ("qSpecies") REFERENCES [qSpecies](id), FOREIGN KEY ("qSpecies") REFERENCES "qSpecies"(id),
FOREIGN KEY ("qSiteInfo") REFERENCES [qSiteInfo](id), FOREIGN KEY ("qSiteInfo") REFERENCES "qSiteInfo"(id),
FOREIGN KEY ("qCareAssistant") REFERENCES [qCareAssistant](id), FOREIGN KEY ("qCareAssistant") REFERENCES "qCareAssistant"(id),
FOREIGN KEY ("qLegalStatus") REFERENCES [qLegalStatus](id)) FOREIGN KEY ("qLegalStatus") REFERENCES "qLegalStatus"(id))
.. _python_api_introspection_strict: .. _python_api_introspection_strict:
@ -2507,9 +2507,9 @@ This will create the ``_counts`` table if it does not already exist, with the fo
.. code-block:: sql .. code-block:: sql
CREATE TABLE [_counts] ( CREATE TABLE "_counts" (
[table] TEXT PRIMARY KEY, "table" TEXT PRIMARY KEY,
[count] INTEGER DEFAULT 0 "count" INTEGER DEFAULT 0
) )
You can enable cached counts for every table in a database (except for virtual tables and the ``_counts`` table itself) using the database ``enable_counts()`` method: You can enable cached counts for every table in a database (except for virtual tables and the ``_counts`` table itself) using the database ``enable_counts()`` method:
@ -2719,11 +2719,11 @@ For example:
# The table schema looks like this: # The table schema looks like this:
# print(db.table("cats").schema) # print(db.table("cats").schema)
# CREATE TABLE [cats] ( # CREATE TABLE "cats" (
# [id] INTEGER PRIMARY KEY, # "id" INTEGER PRIMARY KEY,
# [name] TEXT, # "name" TEXT,
# [age] INTEGER, # "age" INTEGER,
# [thumbnail] BLOB # "thumbnail" BLOB
# ) # )
.. _python_api_register_function: .. _python_api_register_function:
@ -2887,9 +2887,9 @@ If we insert this data directly into a table we will get a schema that is entire
db.table("creatures").insert_all(rows) db.table("creatures").insert_all(rows)
print(db.schema) print(db.schema)
# Outputs: # Outputs:
# CREATE TABLE [creatures] ( # CREATE TABLE "creatures" (
# [id] TEXT, # "id" TEXT,
# [name] TEXT # "name" TEXT
# ); # );
We can detect the best column types using a ``TypeTracker`` instance: We can detect the best column types using a ``TypeTracker`` instance:
@ -2910,9 +2910,9 @@ We can then apply those types to our new table using the :ref:`table.transform()
db.table("creatures2").transform(types=tracker.types) db.table("creatures2").transform(types=tracker.types)
print(db.table("creatures2").schema) print(db.table("creatures2").schema)
# Outputs: # Outputs:
# CREATE TABLE [creatures2] ( # CREATE TABLE "creatures2" (
# [id] INTEGER, # "id" INTEGER,
# [name] TEXT # "name" TEXT
# ); # );
.. _python_api_gis: .. _python_api_gis:

View file

@ -180,10 +180,10 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"CREATE TABLE [creatures] (\n", "CREATE TABLE \"creatures\" (\n",
" [name] TEXT,\n", " \"name\" TEXT,\n",
" [species] TEXT,\n", " \"species\" TEXT,\n",
" [age] FLOAT\n", " \"age\" FLOAT\n",
")\n" ")\n"
] ]
} }
@ -534,11 +534,11 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"CREATE TABLE [creatures] (\n", "CREATE TABLE \"creatures\" (\n",
" [id] INTEGER PRIMARY KEY,\n", " \"id\" INTEGER PRIMARY KEY,\n",
" [name] TEXT,\n", " \"name\" TEXT,\n",
" [species] TEXT,\n", " \"species\" TEXT,\n",
" [age] FLOAT\n", " \"age\" FLOAT\n",
")\n" ")\n"
] ]
} }
@ -929,11 +929,11 @@
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"CREATE TABLE \"creatures\" (\n", "CREATE TABLE \"creatures\" (\n",
" [id] INTEGER PRIMARY KEY,\n", " \"id\" INTEGER PRIMARY KEY,\n",
" [name] TEXT,\n", " \"name\" TEXT,\n",
" [species_id] INTEGER,\n", " \"species_id\" INTEGER,\n",
" [age] FLOAT,\n", " \"age\" FLOAT,\n",
" FOREIGN KEY([species_id]) REFERENCES [species]([id])\n", " FOREIGN KEY(\"species_id\") REFERENCES \"species\"(\"id\")\n",
")\n", ")\n",
"[{'id': 1, 'name': 'Cleo', 'species_id': 1, 'age': 6.0}, {'id': 2, 'name': 'Lila', 'species_id': 2, 'age': 0.8}, {'id': 3, 'name': 'Bants', 'species_id': 2, 'age': 0.8}, {'id': 4, 'name': 'Azi', 'species_id': 2, 'age': 0.8}, {'id': 5, 'name': 'Snowy', 'species_id': 2, 'age': 0.9}, {'id': 6, 'name': 'Blue', 'species_id': 2, 'age': 0.9}]\n" "[{'id': 1, 'name': 'Cleo', 'species_id': 1, 'age': 6.0}, {'id': 2, 'name': 'Lila', 'species_id': 2, 'age': 0.8}, {'id': 3, 'name': 'Bants', 'species_id': 2, 'age': 0.8}, {'id': 4, 'name': 'Azi', 'species_id': 2, 'age': 0.8}, {'id': 5, 'name': 'Snowy', 'species_id': 2, 'age': 0.9}, {'id': 6, 'name': 'Blue', 'species_id': 2, 'age': 0.9}]\n"
] ]
@ -962,9 +962,9 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"CREATE TABLE [species] (\n", "CREATE TABLE \"species\" (\n",
" [id] INTEGER PRIMARY KEY,\n", " \"id\" INTEGER PRIMARY KEY,\n",
" [species] TEXT\n", " \"species\" TEXT\n",
")\n", ")\n",
"[{'id': 1, 'species': 'dog'}, {'id': 2, 'species': 'chicken'}]\n" "[{'id': 1, 'species': 'dog'}, {'id': 2, 'species': 'chicken'}]\n"
] ]
@ -1048,4 +1048,4 @@
}, },
"nbformat": 4, "nbformat": 4,
"nbformat_minor": 5 "nbformat_minor": 5
} }

View file

@ -6,7 +6,13 @@ import hashlib
import pathlib import pathlib
from runpy import run_module from runpy import run_module
import sqlite_utils import sqlite_utils
from sqlite_utils.db import AlterError, BadMultiValues, DescIndex, NoTable from sqlite_utils.db import (
AlterError,
BadMultiValues,
DescIndex,
NoTable,
quote_identifier,
)
from sqlite_utils.plugins import pm, get_plugins from sqlite_utils.plugins import pm, get_plugins
from sqlite_utils.utils import maximize_csv_field_size_limit from sqlite_utils.utils import maximize_csv_field_size_limit
from sqlite_utils import recipes from sqlite_utils import recipes
@ -1967,7 +1973,9 @@ def memory(
view_names.append("t") view_names.append("t")
for view_name in view_names: for view_name in view_names:
if not db[view_name].exists(): if not db[view_name].exists():
db.create_view(view_name, "select * from [{}]".format(file_table)) db.create_view(
view_name, "select * from {}".format(quote_identifier(file_table))
)
if fp: if fp:
fp.close() fp.close()
@ -2230,8 +2238,8 @@ def rows(
""" """
columns = "*" columns = "*"
if column: if column:
columns = ", ".join("[{}]".format(c) for c in column) columns = ", ".join(quote_identifier(c) for c in column)
sql = "select {} from [{}]".format(columns, dbtable) sql = "select {} from {}".format(columns, quote_identifier(dbtable))
if where: if where:
sql += " where " + where sql += " where " + where
if order: if order:
@ -2288,10 +2296,10 @@ def triggers(
\b \b
sqlite-utils triggers trees.db sqlite-utils triggers trees.db
""" """
sql = "select name, tbl_name as [table], sql from sqlite_master where type = 'trigger'" sql = "select name, tbl_name as \"table\", sql from sqlite_master where type = 'trigger'"
if tables: if tables:
quote = sqlite_utils.Database(memory=True).quote quote = sqlite_utils.Database(memory=True).quote
sql += " and [table] in ({})".format( sql += ' and "table" in ({})'.format(
", ".join(quote(table) for table in tables) ", ".join(quote(table) for table in tables)
) )
ctx.invoke( ctx.invoke(

View file

@ -70,6 +70,16 @@ USING\s+(?P<using>\w+) # for example USING FTS5
re.VERBOSE | re.IGNORECASE, re.VERBOSE | re.IGNORECASE,
) )
def quote_identifier(identifier: str) -> str:
"""
Quote an identifier (table name, column name, etc.) using double quotes.
Double quotes inside the identifier are escaped by doubling them.
"""
return '"{}"'.format(identifier.replace('"', '""'))
try: try:
import pandas as pd # type: ignore import pandas as pd # type: ignore
except ImportError: except ImportError:
@ -280,8 +290,8 @@ class BadMultiValues(Exception):
_COUNTS_TABLE_CREATE_SQL = """ _COUNTS_TABLE_CREATE_SQL = """
CREATE TABLE IF NOT EXISTS [{}]( CREATE TABLE IF NOT EXISTS "{}"(
[table] TEXT PRIMARY KEY, "table" TEXT PRIMARY KEY,
count INTEGER DEFAULT 0 count INTEGER DEFAULT 0
); );
""".strip() """.strip()
@ -500,9 +510,9 @@ class Database:
:param filepath: Path to SQLite database file on disk :param filepath: Path to SQLite database file on disk
""" """
attach_sql = """ attach_sql = """
ATTACH DATABASE '{}' AS [{}]; ATTACH DATABASE '{}' AS {};
""".format( """.format(
str(pathlib.Path(filepath).resolve()), alias str(pathlib.Path(filepath).resolve()), quote_identifier(alias)
).strip() ).strip()
self.execute(attach_sql) self.execute(attach_sql)
@ -786,9 +796,9 @@ class Database:
:param tables: Subset list of tables to return counts for. :param tables: Subset list of tables to return counts for.
""" """
sql = "select [table], count from {}".format(self._counts_table_name) sql = 'select "table", count from {}'.format(self._counts_table_name)
if tables: if tables:
sql += " where [table] in ({})".format(", ".join("?" for table in tables)) sql += ' where "table" in ({})'.format(", ".join("?" for table in tables))
try: try:
return {r[0]: r[1] for r in self.execute(sql, tables).fetchall()} return {r[0]: r[1] for r in self.execute(sql, tables).fetchall()}
except OperationalError: except OperationalError:
@ -933,7 +943,6 @@ class Database:
), "defaults set {} includes items not in columns {}".format( ), "defaults set {} includes items not in columns {}".format(
repr(set(defaults)), repr(set(columns.keys())) repr(set(defaults)), repr(set(columns.keys()))
) )
validate_column_names(columns.keys())
column_items = list(columns.items()) column_items = list(columns.items())
if column_order is not None: if column_order is not None:
@ -976,9 +985,13 @@ class Database:
) )
if column_name in foreign_keys_by_column: if column_name in foreign_keys_by_column:
column_extras.append( column_extras.append(
"REFERENCES [{other_table}]([{other_column}])".format( "REFERENCES {}({})".format(
other_table=foreign_keys_by_column[column_name].other_table, quote_identifier(
other_column=foreign_keys_by_column[column_name].other_column, foreign_keys_by_column[column_name].other_table
),
quote_identifier(
foreign_keys_by_column[column_name].other_column
),
) )
) )
column_type_str = COLUMN_TYPE_MAPPING[column_type] column_type_str = COLUMN_TYPE_MAPPING[column_type]
@ -987,8 +1000,8 @@ class Database:
if strict and column_type_str == "FLOAT": if strict and column_type_str == "FLOAT":
column_type_str = "REAL" column_type_str = "REAL"
column_defs.append( column_defs.append(
" [{column_name}] {column_type}{column_extras}".format( " {} {column_type}{column_extras}".format(
column_name=column_name, quote_identifier(column_name),
column_type=column_type_str, column_type=column_type_str,
column_extras=( column_extras=(
(" " + " ".join(column_extras)) if column_extras else "" (" " + " ".join(column_extras)) if column_extras else ""
@ -998,15 +1011,15 @@ class Database:
extra_pk = "" extra_pk = ""
if single_pk is None and pk and len(pk) > 1: if single_pk is None and pk and len(pk) > 1:
extra_pk = ",\n PRIMARY KEY ({pks})".format( extra_pk = ",\n PRIMARY KEY ({pks})".format(
pks=", ".join(["[{}]".format(p) for p in pk]) pks=", ".join([quote_identifier(p) for p in pk])
) )
columns_sql = ",\n".join(column_defs) columns_sql = ",\n".join(column_defs)
sql = """CREATE TABLE {if_not_exists}[{table}] ( sql = """CREATE TABLE {if_not_exists}{table} (
{columns_sql}{extra_pk} {columns_sql}{extra_pk}
){strict}; ){strict};
""".format( """.format(
if_not_exists="IF NOT EXISTS " if if_not_exists else "", if_not_exists="IF NOT EXISTS " if if_not_exists else "",
table=name, table=quote_identifier(name),
columns_sql=columns_sql, columns_sql=columns_sql,
extra_pk=extra_pk, extra_pk=extra_pk,
strict=" STRICT" if strict and self.supports_strict else "", strict=" STRICT" if strict and self.supports_strict else "",
@ -1144,8 +1157,8 @@ class Database:
:param new_name: Name to rename it to :param new_name: Name to rename it to
""" """
self.execute( self.execute(
"ALTER TABLE [{name}] RENAME TO [{new_name}]".format( "ALTER TABLE {} RENAME TO {}".format(
name=name, new_name=new_name quote_identifier(name), quote_identifier(new_name)
) )
) )
@ -1163,7 +1176,9 @@ class Database:
assert not ( assert not (
ignore and replace ignore and replace
), "Use one or the other of ignore/replace, not both" ), "Use one or the other of ignore/replace, not both"
create_sql = "CREATE VIEW {name} AS {sql}".format(name=name, sql=sql) create_sql = "CREATE VIEW {name} AS {sql}".format(
name=quote_identifier(name), sql=sql
)
if ignore or replace: if ignore or replace:
# Does view exist already? # Does view exist already?
if name in self.view_names(): if name in self.view_names():
@ -1270,7 +1285,7 @@ class Database:
""" """
sql = "ANALYZE" sql = "ANALYZE"
if name is not None: if name is not None:
sql += " [{}]".format(name) sql += " {}".format(quote_identifier(name))
self.execute(sql) self.execute(sql)
def iterdump(self) -> Generator[str, None, None]: def iterdump(self) -> Generator[str, None, None]:
@ -1348,7 +1363,7 @@ class Queryable:
:param where_args: Parameters to use with that fragment - an iterable for ``id > ?`` :param where_args: Parameters to use with that fragment - an iterable for ``id > ?``
parameters, or a dictionary for ``id > :id`` parameters, or a dictionary for ``id > :id``
""" """
sql = "select count(*) from [{}]".format(self.name) sql = "select count(*) from {}".format(quote_identifier(self.name))
if where is not None: if where is not None:
sql += " where " + where sql += " where " + where
return self.db.execute(sql, where_args or []).fetchone()[0] return self.db.execute(sql, where_args or []).fetchone()[0]
@ -1391,7 +1406,7 @@ class Queryable:
""" """
if not self.exists(): if not self.exists():
return return
sql = "select {} from [{}]".format(select, self.name) sql = "select {} from {}".format(select, quote_identifier(self.name))
if where is not None: if where is not None:
sql += " where " + where sql += " where " + where
if order_by is not None: if order_by is not None:
@ -1429,7 +1444,7 @@ class Queryable:
if not pks: if not pks:
column_names.insert(0, "rowid") column_names.insert(0, "rowid")
pks = ["rowid"] pks = ["rowid"]
select = ",".join("[{}]".format(column_name) for column_name in column_names) select = ",".join(quote_identifier(column_name) for column_name in column_names)
for row in self.rows_where( for row in self.rows_where(
select=select, select=select,
where=where, where=where,
@ -1448,7 +1463,9 @@ class Queryable:
"List of :ref:`Columns <reference_db_other_column>` representing the columns in this table or view." "List of :ref:`Columns <reference_db_other_column>` representing the columns in this table or view."
if not self.exists(): if not self.exists():
return [] return []
rows = self.db.execute("PRAGMA table_info([{}])".format(self.name)).fetchall() rows = self.db.execute(
"PRAGMA table_info({})".format(quote_identifier(self.name))
).fetchall()
return [Column(*row) for row in rows] return [Column(*row) for row in rows]
@property @property
@ -1588,7 +1605,7 @@ class Table(Queryable):
) )
) )
wheres = ["[{}] = ?".format(pk_name) for pk_name in pks] wheres = ["{} = ?".format(quote_identifier(pk_name)) for pk_name in pks]
rows = self.rows_where(" and ".join(wheres), pk_values) rows = self.rows_where(" and ".join(wheres), pk_values)
try: try:
row = list(rows)[0] row = list(rows)[0]
@ -1602,7 +1619,7 @@ class Table(Queryable):
"List of foreign keys defined on this table." "List of foreign keys defined on this table."
fks = [] fks = []
for row in self.db.execute( for row in self.db.execute(
"PRAGMA foreign_key_list([{}])".format(self.name) "PRAGMA foreign_key_list({})".format(quote_identifier(self.name))
).fetchall(): ).fetchall():
if row is not None: if row is not None:
id, seq, table_name, from_, to_, on_update, on_delete, match = row id, seq, table_name, from_, to_, on_update, on_delete, match = row
@ -1799,9 +1816,9 @@ class Table(Queryable):
if not self.exists(): if not self.exists():
raise NoTable(f"Table {self.name} does not exist") raise NoTable(f"Table {self.name} does not exist")
with self.db.conn: with self.db.conn:
sql = "CREATE TABLE [{new_table}] AS SELECT * FROM [{table}];".format( sql = "CREATE TABLE {} AS SELECT * FROM {};".format(
new_table=new_name, quote_identifier(new_name),
table=self.name, quote_identifier(self.name),
) )
self.db.execute(sql) self.db.execute(sql)
return self.db[new_name] return self.db[new_name]
@ -2034,23 +2051,27 @@ class Table(Queryable):
if "rowid" not in new_cols: if "rowid" not in new_cols:
new_cols.insert(0, "rowid") new_cols.insert(0, "rowid")
old_cols.insert(0, "rowid") old_cols.insert(0, "rowid")
copy_sql = "INSERT INTO [{new_table}] ({new_cols})\n SELECT {old_cols} FROM [{old_table}];".format( copy_sql = "INSERT INTO {} ({new_cols})\n SELECT {old_cols} FROM {};".format(
new_table=new_table_name, quote_identifier(new_table_name),
old_table=self.name, quote_identifier(self.name),
old_cols=", ".join("[{}]".format(col) for col in old_cols), old_cols=", ".join(quote_identifier(col) for col in old_cols),
new_cols=", ".join("[{}]".format(col) for col in new_cols), new_cols=", ".join(quote_identifier(col) for col in new_cols),
) )
sqls.append(copy_sql) sqls.append(copy_sql)
# Drop (or keep) the old table # Drop (or keep) the old table
if keep_table: if keep_table:
sqls.append( sqls.append(
"ALTER TABLE [{}] RENAME TO [{}];".format(self.name, keep_table) "ALTER TABLE {} RENAME TO {};".format(
quote_identifier(self.name), quote_identifier(keep_table)
)
) )
else: else:
sqls.append("DROP TABLE [{}];".format(self.name)) sqls.append("DROP TABLE {};".format(quote_identifier(self.name)))
# Rename the new one # Rename the new one
sqls.append( sqls.append(
"ALTER TABLE [{}] RENAME TO [{}];".format(new_table_name, self.name) "ALTER TABLE {} RENAME TO {};".format(
quote_identifier(new_table_name), quote_identifier(self.name)
)
) )
# Re-add existing indexes # Re-add existing indexes
for index in self.indexes: for index in self.indexes:
@ -2066,7 +2087,7 @@ class Table(Queryable):
"transformation and manually recreate the new index after running this transformation." "transformation and manually recreate the new index after running this transformation."
) )
if keep_table: if keep_table:
sqls.append(f"DROP INDEX IF EXISTS [{index.name}];") sqls.append(f"DROP INDEX IF EXISTS {quote_identifier(index.name)};")
for col in index.columns: for col in index.columns:
if col in rename.keys() or col in drop: if col in rename.keys() or col in drop:
raise TransformError( raise TransformError(
@ -2137,11 +2158,11 @@ class Table(Queryable):
lookup_columns = [(rename.get(col) or col) for col in columns] lookup_columns = [(rename.get(col) or col) for col in columns]
lookup_table.create_index(lookup_columns, unique=True, if_not_exists=True) lookup_table.create_index(lookup_columns, unique=True, if_not_exists=True)
self.db.execute( self.db.execute(
"INSERT OR IGNORE INTO [{lookup_table}] ({lookup_columns}) SELECT DISTINCT {table_cols} FROM [{table}]".format( "INSERT OR IGNORE INTO {} ({lookup_columns}) SELECT DISTINCT {table_cols} FROM {}".format(
lookup_table=table, quote_identifier(table),
lookup_columns=", ".join("[{}]".format(c) for c in lookup_columns), quote_identifier(self.name),
table_cols=", ".join("[{}]".format(c) for c in columns), lookup_columns=", ".join(quote_identifier(c) for c in lookup_columns),
table=self.name, table_cols=", ".join(quote_identifier(c) for c in columns),
) )
) )
@ -2150,16 +2171,16 @@ class Table(Queryable):
# And populate it # And populate it
self.db.execute( self.db.execute(
"UPDATE [{table}] SET [{magic_lookup_column}] = (SELECT id FROM [{lookup_table}] WHERE {where})".format( "UPDATE {} SET {} = (SELECT id FROM {} WHERE {where})".format(
table=self.name, quote_identifier(self.name),
magic_lookup_column=magic_lookup_column, quote_identifier(magic_lookup_column),
lookup_table=table, quote_identifier(table),
where=" AND ".join( where=" AND ".join(
"[{table}].[{column}] IS [{lookup_table}].[{lookup_column}]".format( "{}.{} IS {}.{}".format(
table=self.name, quote_identifier(self.name),
lookup_table=table, quote_identifier(column),
column=column, quote_identifier(table),
lookup_column=rename.get(column) or column, quote_identifier(rename.get(column) or column),
) )
for column in columns for column in columns
), ),
@ -2216,10 +2237,9 @@ class Table(Queryable):
columns_sql = [] columns_sql = []
for column in columns: for column in columns:
if isinstance(column, DescIndex): if isinstance(column, DescIndex):
fmt = "[{}] desc" columns_sql.append("{} desc".format(quote_identifier(column)))
else: else:
fmt = "[{}]" columns_sql.append(quote_identifier(column))
columns_sql.append(fmt.format(column))
suffix = None suffix = None
created_index_name = None created_index_name = None
@ -2230,14 +2250,14 @@ class Table(Queryable):
sql = ( sql = (
textwrap.dedent( textwrap.dedent(
""" """
CREATE {unique}INDEX {if_not_exists}[{index_name}] CREATE {unique}INDEX {if_not_exists}{index_name}
ON [{table_name}] ({columns}); ON {table_name} ({columns});
""" """
) )
.strip() .strip()
.format( .format(
index_name=created_index_name, index_name=quote_identifier(created_index_name),
table_name=self.name, table_name=quote_identifier(self.name),
columns=", ".join(columns_sql), columns=", ".join(columns_sql),
unique="UNIQUE " if unique else "", unique="UNIQUE " if unique else "",
if_not_exists="IF NOT EXISTS " if if_not_exists else "", if_not_exists="IF NOT EXISTS " if if_not_exists else "",
@ -2307,9 +2327,9 @@ class Table(Queryable):
not_null_sql = "NOT NULL DEFAULT {}".format( not_null_sql = "NOT NULL DEFAULT {}".format(
self.db.quote_default_value(not_null_default) self.db.quote_default_value(not_null_default)
) )
sql = "ALTER TABLE [{table}] ADD COLUMN [{col_name}] {col_type}{not_null_default};".format( sql = "ALTER TABLE {} ADD COLUMN {} {col_type}{not_null_default};".format(
table=self.name, quote_identifier(self.name),
col_name=col_name, quote_identifier(col_name),
col_type=fk_col_type or COLUMN_TYPE_MAPPING[col_type], col_type=fk_col_type or COLUMN_TYPE_MAPPING[col_type],
not_null_default=(" " + not_null_sql) if not_null_sql else "", not_null_default=(" " + not_null_sql) if not_null_sql else "",
) )
@ -2325,7 +2345,7 @@ class Table(Queryable):
:param ignore: Set to ``True`` to ignore the error if the table does not exist :param ignore: Set to ``True`` to ignore the error if the table does not exist
""" """
try: try:
self.db.execute("DROP TABLE [{}]".format(self.name)) self.db.execute("DROP TABLE {}".format(quote_identifier(self.name)))
except sqlite3.OperationalError: except sqlite3.OperationalError:
if not ignore: if not ignore:
raise raise
@ -2431,29 +2451,29 @@ class Table(Queryable):
textwrap.dedent( textwrap.dedent(
""" """
{create_counts_table} {create_counts_table}
CREATE TRIGGER IF NOT EXISTS [{table}{counts_table}_insert] AFTER INSERT ON [{table}] CREATE TRIGGER IF NOT EXISTS {trigger_insert} AFTER INSERT ON {table}
BEGIN BEGIN
INSERT OR REPLACE INTO [{counts_table}] INSERT OR REPLACE INTO {counts_table}
VALUES ( VALUES (
{table_quoted}, {table_quoted},
COALESCE( COALESCE(
(SELECT count FROM [{counts_table}] WHERE [table] = {table_quoted}), (SELECT count FROM {counts_table} WHERE "table" = {table_quoted}),
0 0
) + 1 ) + 1
); );
END; END;
CREATE TRIGGER IF NOT EXISTS [{table}{counts_table}_delete] AFTER DELETE ON [{table}] CREATE TRIGGER IF NOT EXISTS {trigger_delete} AFTER DELETE ON {table}
BEGIN BEGIN
INSERT OR REPLACE INTO [{counts_table}] INSERT OR REPLACE INTO {counts_table}
VALUES ( VALUES (
{table_quoted}, {table_quoted},
COALESCE( COALESCE(
(SELECT count FROM [{counts_table}] WHERE [table] = {table_quoted}), (SELECT count FROM {counts_table} WHERE "table" = {table_quoted}),
0 0
) - 1 ) - 1
); );
END; END;
INSERT OR REPLACE INTO _counts VALUES ({table_quoted}, (select count(*) from [{table}])); INSERT OR REPLACE INTO _counts VALUES ({table_quoted}, (select count(*) from {table}));
""" """
) )
.strip() .strip()
@ -2461,9 +2481,15 @@ class Table(Queryable):
create_counts_table=_COUNTS_TABLE_CREATE_SQL.format( create_counts_table=_COUNTS_TABLE_CREATE_SQL.format(
self.db._counts_table_name self.db._counts_table_name
), ),
counts_table=self.db._counts_table_name, counts_table=quote_identifier(self.db._counts_table_name),
table=self.name, table=quote_identifier(self.name),
table_quoted=self.db.quote(self.name), table_quoted=self.db.quote(self.name),
trigger_insert=quote_identifier(
self.name + self.db._counts_table_name + "_insert"
),
trigger_delete=quote_identifier(
self.name + self.db._counts_table_name + "_delete"
),
) )
) )
with self.db.conn: with self.db.conn:
@ -2503,16 +2529,17 @@ class Table(Queryable):
create_fts_sql = ( create_fts_sql = (
textwrap.dedent( textwrap.dedent(
""" """
CREATE VIRTUAL TABLE [{table}_fts] USING {fts_version} ( CREATE VIRTUAL TABLE {table_fts} USING {fts_version} (
{columns},{tokenize} {columns},{tokenize}
content=[{table}] content={table}
) )
""" """
) )
.strip() .strip()
.format( .format(
table=self.name, table=quote_identifier(self.name),
columns=", ".join("[{}]".format(c) for c in columns), table_fts=quote_identifier(self.name + "_fts"),
columns=", ".join(quote_identifier(c) for c in columns),
fts_version=fts_version, fts_version=fts_version,
tokenize="\n tokenize='{}',".format(tokenize) if tokenize else "", tokenize="\n tokenize='{}',".format(tokenize) if tokenize else "",
) )
@ -2539,27 +2566,34 @@ class Table(Queryable):
self.populate_fts(columns) self.populate_fts(columns)
if create_triggers: if create_triggers:
old_cols = ", ".join("old.[{}]".format(c) for c in columns) old_cols = ", ".join("old.{}".format(quote_identifier(c)) for c in columns)
new_cols = ", ".join("new.[{}]".format(c) for c in columns) new_cols = ", ".join("new.{}".format(quote_identifier(c)) for c in columns)
columns_quoted = ", ".join(quote_identifier(c) for c in columns)
table = quote_identifier(self.name)
table_fts = quote_identifier(self.name + "_fts")
triggers = ( triggers = (
textwrap.dedent( textwrap.dedent(
""" """
CREATE TRIGGER [{table}_ai] AFTER INSERT ON [{table}] BEGIN CREATE TRIGGER {table_ai} AFTER INSERT ON {table} BEGIN
INSERT INTO [{table}_fts] (rowid, {columns}) VALUES (new.rowid, {new_cols}); INSERT INTO {table_fts} (rowid, {columns}) VALUES (new.rowid, {new_cols});
END; END;
CREATE TRIGGER [{table}_ad] AFTER DELETE ON [{table}] BEGIN CREATE TRIGGER {table_ad} AFTER DELETE ON {table} BEGIN
INSERT INTO [{table}_fts] ([{table}_fts], rowid, {columns}) VALUES('delete', old.rowid, {old_cols}); INSERT INTO {table_fts} ({table_fts}, rowid, {columns}) VALUES('delete', old.rowid, {old_cols});
END; END;
CREATE TRIGGER [{table}_au] AFTER UPDATE ON [{table}] BEGIN CREATE TRIGGER {table_au} AFTER UPDATE ON {table} BEGIN
INSERT INTO [{table}_fts] ([{table}_fts], rowid, {columns}) VALUES('delete', old.rowid, {old_cols}); INSERT INTO {table_fts} ({table_fts}, rowid, {columns}) VALUES('delete', old.rowid, {old_cols});
INSERT INTO [{table}_fts] (rowid, {columns}) VALUES (new.rowid, {new_cols}); INSERT INTO {table_fts} (rowid, {columns}) VALUES (new.rowid, {new_cols});
END; END;
""" """
) )
.strip() .strip()
.format( .format(
table=self.name, table=table,
columns=", ".join("[{}]".format(c) for c in columns), table_fts=table_fts,
table_ai=quote_identifier(self.name + "_ai"),
table_ad=quote_identifier(self.name + "_ad"),
table_au=quote_identifier(self.name + "_au"),
columns=columns_quoted,
old_cols=old_cols, old_cols=old_cols,
new_cols=new_cols, new_cols=new_cols,
) )
@ -2574,16 +2608,19 @@ class Table(Queryable):
:param columns: Columns to populate the data for :param columns: Columns to populate the data for
""" """
columns_quoted = ", ".join(quote_identifier(c) for c in columns)
sql = ( sql = (
textwrap.dedent( textwrap.dedent(
""" """
INSERT INTO [{table}_fts] (rowid, {columns}) INSERT INTO {table_fts} (rowid, {columns})
SELECT rowid, {columns} FROM [{table}]; SELECT rowid, {columns} FROM {table};
""" """
) )
.strip() .strip()
.format( .format(
table=self.name, columns=", ".join("[{}]".format(c) for c in columns) table=quote_identifier(self.name),
table_fts=quote_identifier(self.name + "_fts"),
columns=columns_quoted,
) )
) )
self.db.executescript(sql) self.db.executescript(sql)
@ -2600,18 +2637,20 @@ class Table(Queryable):
""" """
SELECT name FROM sqlite_master SELECT name FROM sqlite_master
WHERE type = 'trigger' WHERE type = 'trigger'
AND sql LIKE '% INSERT INTO [{}]%' AND (sql LIKE '% INSERT INTO [{}]%' OR sql LIKE '% INSERT INTO "{}"%')
""" """
) )
.strip() .strip()
.format(fts_table) .format(fts_table, fts_table)
) )
trigger_names = [] trigger_names = []
for row in self.db.execute(sql).fetchall(): for row in self.db.execute(sql).fetchall():
trigger_names.append(row[0]) trigger_names.append(row[0])
with self.db.conn: with self.db.conn:
for trigger_name in trigger_names: for trigger_name in trigger_names:
self.db.execute("DROP TRIGGER IF EXISTS [{}]".format(trigger_name)) self.db.execute(
"DROP TRIGGER IF EXISTS {}".format(quote_identifier(trigger_name))
)
return self return self
def rebuild_fts(self): def rebuild_fts(self):
@ -2621,8 +2660,8 @@ class Table(Queryable):
# Assume this is itself an FTS table # Assume this is itself an FTS table
fts_table = self.name fts_table = self.name
self.db.execute( self.db.execute(
"INSERT INTO [{table}]([{table}]) VALUES('rebuild');".format( "INSERT INTO {table}({table}) VALUES('rebuild');".format(
table=fts_table table=quote_identifier(fts_table)
) )
) )
return self return self
@ -2644,7 +2683,7 @@ class Table(Queryable):
""" """
).strip() ).strip()
args = { args = {
"like": "%VIRTUAL TABLE%USING FTS%content=[{}]%".format(self.name), "like": '%VIRTUAL TABLE%USING FTS%content="{}"%'.format(self.name),
"like2": '%VIRTUAL TABLE%USING FTS%content="{}"%'.format(self.name), "like2": '%VIRTUAL TABLE%USING FTS%content="{}"%'.format(self.name),
"table": self.name, "table": self.name,
} }
@ -2660,9 +2699,9 @@ class Table(Queryable):
if fts_table is not None: if fts_table is not None:
self.db.execute( self.db.execute(
""" """
INSERT INTO [{table}] ([{table}]) VALUES ("optimize"); INSERT INTO {table} ({table}) VALUES ("optimize");
""".strip().format( """.strip().format(
table=fts_table table=quote_identifier(fts_table)
) )
) )
return self return self
@ -2688,17 +2727,19 @@ class Table(Queryable):
""" """
# Pick names for table and rank column that don't clash # Pick names for table and rank column that don't clash
original = "original_" if self.name == "original" else "original" original = "original_" if self.name == "original" else "original"
original_quoted = quote_identifier(original)
columns_sql = "*" columns_sql = "*"
columns_with_prefix_sql = "[{}].*".format(original) columns_with_prefix_sql = "{}.*".format(original_quoted)
if columns: if columns:
columns_sql = ",\n ".join("[{}]".format(c) for c in columns) columns_sql = ",\n ".join(quote_identifier(c) for c in columns)
columns_with_prefix_sql = ",\n ".join( columns_with_prefix_sql = ",\n ".join(
"[{}].[{}]".format(original, c) for c in columns "{}.{}".format(original_quoted, quote_identifier(c)) for c in columns
) )
fts_table = self.detect_fts() fts_table = self.detect_fts()
assert fts_table, "Full-text search is not configured for table '{}'".format( assert fts_table, "Full-text search is not configured for table '{}'".format(
self.name self.name
) )
fts_table_quoted = quote_identifier(fts_table)
virtual_table_using = self.db[fts_table].virtual_table_using virtual_table_using = self.db[fts_table].virtual_table_using
sql = textwrap.dedent( sql = textwrap.dedent(
""" """
@ -2706,26 +2747,26 @@ class Table(Queryable):
select select
rowid, rowid,
{columns} {columns}
from [{dbtable}]{where_clause} from {dbtable}{where_clause}
) )
select select
{columns_with_prefix} {columns_with_prefix}
from from
[{original}] {original}
join [{fts_table}] on [{original}].rowid = [{fts_table}].rowid join {fts_table} on {original}.rowid = {fts_table}.rowid
where where
[{fts_table}] match :query {fts_table} match :query
order by order by
{order_by} {order_by}
{limit_offset} {limit_offset}
""" """
).strip() ).strip()
if virtual_table_using == "FTS5": if virtual_table_using == "FTS5":
rank_implementation = "[{}].rank".format(fts_table) rank_implementation = "{}.rank".format(fts_table_quoted)
else: else:
self.db.register_fts4_bm25() self.db.register_fts4_bm25()
rank_implementation = "rank_bm25(matchinfo([{}], 'pcnalx'))".format( rank_implementation = "rank_bm25(matchinfo({}, 'pcnalx'))".format(
fts_table fts_table_quoted
) )
if include_rank: if include_rank:
columns_with_prefix_sql += ",\n " + rank_implementation + " rank" columns_with_prefix_sql += ",\n " + rank_implementation + " rank"
@ -2735,12 +2776,12 @@ class Table(Queryable):
if offset is not None: if offset is not None:
limit_offset += " offset {}".format(offset) limit_offset += " offset {}".format(offset)
return sql.format( return sql.format(
dbtable=self.name, dbtable=quote_identifier(self.name),
where_clause="\n where {}".format(where) if where else "", where_clause="\n where {}".format(where) if where else "",
original=original, original=original_quoted,
columns=columns_sql, columns=columns_sql,
columns_with_prefix=columns_with_prefix_sql, columns_with_prefix=columns_with_prefix_sql,
fts_table=fts_table, fts_table=fts_table_quoted,
order_by=order_by or rank_implementation, order_by=order_by or rank_implementation,
limit_offset=limit_offset.strip(), limit_offset=limit_offset.strip(),
).strip() ).strip()
@ -2808,9 +2849,9 @@ class Table(Queryable):
if not isinstance(pk_values, (list, tuple)): if not isinstance(pk_values, (list, tuple)):
pk_values = [pk_values] pk_values = [pk_values]
self.get(pk_values) self.get(pk_values)
wheres = ["[{}] = ?".format(pk_name) for pk_name in self.pks] wheres = ["{} = ?".format(quote_identifier(pk_name)) for pk_name in self.pks]
sql = "delete from [{table}] where {wheres}".format( sql = "delete from {} where {wheres}".format(
table=self.name, wheres=" and ".join(wheres) quote_identifier(self.name), wheres=" and ".join(wheres)
) )
with self.db.conn: with self.db.conn:
self.db.execute(sql, pk_values) self.db.execute(sql, pk_values)
@ -2834,7 +2875,7 @@ class Table(Queryable):
""" """
if not self.exists(): if not self.exists():
return self return self
sql = "delete from [{}]".format(self.name) sql = "delete from {}".format(quote_identifier(self.name))
if where is not None: if where is not None:
sql += " where " + where sql += " where " + where
self.db.execute(sql, where_args or []) self.db.execute(sql, where_args or [])
@ -2873,14 +2914,17 @@ class Table(Queryable):
sets = [] sets = []
wheres = [] wheres = []
pks = self.pks pks = self.pks
validate_column_names(updates.keys())
for key, value in updates.items(): for key, value in updates.items():
sets.append("[{}] = {}".format(key, conversions.get(key, "?"))) sets.append(
"{} = {}".format(quote_identifier(key), conversions.get(key, "?"))
)
args.append(jsonify_if_needed(value)) args.append(jsonify_if_needed(value))
wheres = ["[{}] = ?".format(pk_name) for pk_name in pks] wheres = ["{} = ?".format(quote_identifier(pk_name)) for pk_name in pks]
args.extend(pk_values) args.extend(pk_values)
sql = "update [{table}] set {sets} where {wheres}".format( sql = "update {} set {sets} where {wheres}".format(
table=self.name, sets=", ".join(sets), wheres=" and ".join(wheres) quote_identifier(self.name),
sets=", ".join(sets),
wheres=" and ".join(wheres),
) )
with self.db.conn: with self.db.conn:
try: try:
@ -2957,14 +3001,14 @@ class Table(Queryable):
if fn_name == "<lambda>": if fn_name == "<lambda>":
fn_name = f"lambda_{abs(hash(fn))}" fn_name = f"lambda_{abs(hash(fn))}"
self.db.register_function(convert_value, name=fn_name) self.db.register_function(convert_value, name=fn_name)
sql = "update [{table}] set {sets}{where};".format( sql = "update {} set {sets}{where};".format(
table=self.name, quote_identifier(self.name),
sets=", ".join( sets=", ".join(
[ [
"[{output_column}] = {fn_name}([{column}])".format( "{} = {}({})".format(
output_column=output or column, quote_identifier(output or column),
column=column, fn_name,
fn_name=fn_name, quote_identifier(column),
) )
for column in columns for column in columns
] ]
@ -2992,7 +3036,7 @@ class Table(Queryable):
) as bar: ) as bar:
for row in self.rows_where( for row in self.rows_where(
select=", ".join( select=", ".join(
"[{}]".format(column_name) for column_name in (pks + [column]) quote_identifier(column_name) for column_name in (pks + [column])
), ),
where=where, where=where,
where_args=where_args, where_args=where_args,
@ -3097,7 +3141,7 @@ class Table(Queryable):
record_values.append(value) record_values.append(value)
values.append(record_values) values.append(record_values)
columns_sql = ", ".join(f"[{c}]" for c in all_columns) columns_sql = ", ".join(quote_identifier(c) for c in all_columns)
placeholder_expr = ", ".join(conversions.get(c, "?") for c in all_columns) placeholder_expr = ", ".join(conversions.get(c, "?") for c in all_columns)
row_placeholders_sql = ", ".join(f"({placeholder_expr})" for _ in values) row_placeholders_sql = ", ".join(f"({placeholder_expr})" for _ in values)
flat_params = list(itertools.chain.from_iterable(values)) flat_params = list(itertools.chain.from_iterable(values))
@ -3105,7 +3149,7 @@ class Table(Queryable):
# replace=True mean INSERT OR REPLACE INTO # replace=True mean INSERT OR REPLACE INTO
if replace: if replace:
sql = ( sql = (
f"INSERT OR REPLACE INTO [{self.name}] " f"INSERT OR REPLACE INTO {quote_identifier(self.name)} "
f"({columns_sql}) VALUES {row_placeholders_sql}" f"({columns_sql}) VALUES {row_placeholders_sql}"
) )
return [(sql, flat_params)] return [(sql, flat_params)]
@ -3116,7 +3160,7 @@ class Table(Queryable):
if ignore: if ignore:
or_ignore = " OR IGNORE" or_ignore = " OR IGNORE"
sql = ( sql = (
f"INSERT{or_ignore} INTO [{self.name}] " f"INSERT{or_ignore} INTO {quote_identifier(self.name)} "
f"({columns_sql}) VALUES {row_placeholders_sql}" f"({columns_sql}) VALUES {row_placeholders_sql}"
) )
return [(sql, flat_params)] return [(sql, flat_params)]
@ -3124,26 +3168,27 @@ class Table(Queryable):
# Everything from here on is for upsert=True # Everything from here on is for upsert=True
pk_cols = [pk] if isinstance(pk, str) else list(pk) pk_cols = [pk] if isinstance(pk, str) else list(pk)
non_pk_cols = [c for c in all_columns if c not in pk_cols] non_pk_cols = [c for c in all_columns if c not in pk_cols]
conflict_sql = ", ".join(f"[{c}]" for c in pk_cols) conflict_sql = ", ".join(quote_identifier(c) for c in pk_cols)
if self.db.supports_on_conflict and not self.db.use_old_upsert: if self.db.supports_on_conflict and not self.db.use_old_upsert:
if non_pk_cols: if non_pk_cols:
# DO UPDATE # DO UPDATE
assignments = [] assignments = []
for c in non_pk_cols: for c in non_pk_cols:
c_quoted = quote_identifier(c)
if c in conversions: if c in conversions:
assignments.append( assignments.append(
f"[{c}] = {conversions[c].replace('?', f'excluded.[{c}]')}" f"{c_quoted} = {conversions[c].replace('?', f'excluded.{c_quoted}')}"
) )
else: else:
assignments.append(f"[{c}] = excluded.[{c}]") assignments.append(f"{c_quoted} = excluded.{c_quoted}")
do_clause = "DO UPDATE SET " + ", ".join(assignments) do_clause = "DO UPDATE SET " + ", ".join(assignments)
else: else:
# All columns are in the PK – nothing to update. # All columns are in the PK – nothing to update.
do_clause = "DO NOTHING" do_clause = "DO NOTHING"
sql = ( sql = (
f"INSERT INTO [{self.name}] ({columns_sql}) " f"INSERT INTO {quote_identifier(self.name)} ({columns_sql}) "
f"VALUES {row_placeholders_sql} " f"VALUES {row_placeholders_sql} "
f"ON CONFLICT({conflict_sql}) {do_clause}" f"ON CONFLICT({conflict_sql}) {do_clause}"
) )
@ -3164,24 +3209,30 @@ class Table(Queryable):
# them since it ignores the resulting integrity errors # them since it ignores the resulting integrity errors
if not_null: if not_null:
placeholders.extend(not_null) placeholders.extend(not_null)
sql = "INSERT OR IGNORE INTO [{table}]({cols}) VALUES({placeholders});".format( sql = (
table=self.name, "INSERT OR IGNORE INTO {table}({cols}) VALUES({placeholders});".format(
cols=", ".join(["[{}]".format(p) for p in placeholders]), table=quote_identifier(self.name),
placeholders=", ".join(["?" for p in placeholders]), cols=", ".join([quote_identifier(p) for p in placeholders]),
placeholders=", ".join(["?" for p in placeholders]),
)
) )
queries_and_params.append( queries_and_params.append(
(sql, [record[col] for col in pks] + ["" for _ in (not_null or [])]) (sql, [record[col] for col in pks] + ["" for _ in (not_null or [])])
) )
# UPDATE [book] SET [name] = 'Programming' WHERE [id] = 1001; # UPDATE "book" SET "name" = 'Programming' WHERE "id" = 1001;
set_cols = [col for col in all_columns if col not in pks] set_cols = [col for col in all_columns if col not in pks]
if set_cols: if set_cols:
sql2 = "UPDATE [{table}] SET {pairs} WHERE {wheres}".format( sql2 = "UPDATE {} SET {pairs} WHERE {wheres}".format(
table=self.name, quote_identifier(self.name),
pairs=", ".join( pairs=", ".join(
"[{}] = {}".format(col, conversions.get(col, "?")) "{} = {}".format(
quote_identifier(col), conversions.get(col, "?")
)
for col in set_cols for col in set_cols
), ),
wheres=" AND ".join("[{}] = ?".format(pk) for pk in pks), wheres=" AND ".join(
"{} = ?".format(quote_identifier(pk)) for pk in pks
),
) )
queries_and_params.append( queries_and_params.append(
( (
@ -3452,9 +3503,6 @@ class Table(Queryable):
else: else:
# Dict mode: traditional behavior # Dict mode: traditional behavior
records_iter = itertools.chain([first_record], records_iter) records_iter = itertools.chain([first_record], records_iter)
records_iter = fix_square_braces(
cast(Iterable[Dict[str, Any]], records_iter)
)
try: try:
first_record = next(records_iter) first_record = next(records_iter)
except StopIteration: except StopIteration:
@ -3469,7 +3517,7 @@ class Table(Queryable):
self.last_rowid = None self.last_rowid = None
self.last_pk = None self.last_pk = None
if truncate and self.exists(): if truncate and self.exists():
self.db.execute("DELETE FROM [{}];".format(self.name)) self.db.execute("DELETE FROM {};".format(quote_identifier(self.name)))
result = None result = None
for chunk in chunks(itertools.chain([first_record], records_iter), batch_size): for chunk in chunks(itertools.chain([first_record], records_iter), batch_size):
chunk = list(chunk) chunk = list(chunk)
@ -3724,7 +3772,9 @@ class Table(Queryable):
unique_column_sets = [set(i.columns) for i in self.indexes] unique_column_sets = [set(i.columns) for i in self.indexes]
if set(lookup_values.keys()) not in unique_column_sets: if set(lookup_values.keys()) not in unique_column_sets:
self.create_index(lookup_values.keys(), unique=True) self.create_index(lookup_values.keys(), unique=True)
wheres = ["[{}] = ?".format(column) for column in lookup_values] wheres = [
"{} = ?".format(quote_identifier(column)) for column in lookup_values
]
rows = list( rows = list(
self.rows_where( self.rows_where(
" and ".join(wheres), [value for _, value in lookup_values.items()] " and ".join(wheres), [value for _, value in lookup_values.items()]
@ -3887,20 +3937,24 @@ class Table(Queryable):
value = value[:value_truncate] + "..." value = value[:value_truncate] + "..."
return value return value
table_quoted = quote_identifier(table)
column_quoted = quote_identifier(column)
num_null = db.execute( num_null = db.execute(
"select count(*) from [{}] where [{}] is null".format(table, column) "select count(*) from {} where {} is null".format(
table_quoted, column_quoted
)
).fetchone()[0] ).fetchone()[0]
num_blank = db.execute( num_blank = db.execute(
"select count(*) from [{}] where [{}] = ''".format(table, column) "select count(*) from {} where {} = ''".format(table_quoted, column_quoted)
).fetchone()[0] ).fetchone()[0]
num_distinct = db.execute( num_distinct = db.execute(
"select count(distinct [{}]) from [{}]".format(column, table) "select count(distinct {}) from {}".format(column_quoted, table_quoted)
).fetchone()[0] ).fetchone()[0]
most_common_results = None most_common_results = None
least_common_results = None least_common_results = None
if num_distinct == 1: if num_distinct == 1:
value = db.execute( value = db.execute(
"select [{}] from [{}] limit 1".format(column, table) "select {} from {} limit 1".format(column_quoted, table_quoted)
).fetchone()[0] ).fetchone()[0]
most_common_results = [(truncate(value), total_rows)] most_common_results = [(truncate(value), total_rows)]
elif num_distinct != total_rows: elif num_distinct != total_rows:
@ -3912,8 +3966,12 @@ class Table(Queryable):
most_common_results = [ most_common_results = [
(truncate(r[0]), r[1]) (truncate(r[0]), r[1])
for r in db.execute( for r in db.execute(
"select [{}], count(*) from [{}] group by [{}] order by count(*) desc, [{}] limit {}".format( "select {}, count(*) from {} group by {} order by count(*) desc, {} limit {}".format(
column, table, column, column, common_limit column_quoted,
table_quoted,
column_quoted,
column_quoted,
common_limit,
) )
).fetchall() ).fetchall()
] ]
@ -3926,8 +3984,12 @@ class Table(Queryable):
least_common_results = [ least_common_results = [
(truncate(r[0]), r[1]) (truncate(r[0]), r[1])
for r in db.execute( for r in db.execute(
"select [{}], count(*) from [{}] group by [{}] order by count(*), [{}] desc limit {}".format( "select {}, count(*) from {} group by {} order by count(*), {} desc limit {}".format(
column, table, column, column, common_limit column_quoted,
table_quoted,
column_quoted,
column_quoted,
common_limit,
) )
).fetchall() ).fetchall()
] ]
@ -4045,7 +4107,7 @@ class View(Queryable):
""" """
try: try:
self.db.execute("DROP VIEW [{}]".format(self.name)) self.db.execute("DROP VIEW {}".format(quote_identifier(self.name)))
except sqlite3.OperationalError: except sqlite3.OperationalError:
if not ignore: if not ignore:
raise raise
@ -4082,25 +4144,6 @@ def resolve_extracts(
return extracts return extracts
def validate_column_names(columns):
# Validate no columns contain '[' or ']' - #86
for column in columns:
assert (
"[" not in column and "]" not in column
), "'[' and ']' cannot be used in column names"
def fix_square_braces(records: Iterable[Dict[str, Any]]):
for record in records:
if any("[" in key or "]" in key for key in record.keys()):
yield {
key.replace("[", "_").replace("]", "_"): value
for key, value in record.items()
}
else:
yield record
def _decode_default_value(value): def _decode_default_value(value):
if value.startswith("'") and value.endswith("'"): if value.startswith("'") and value.endswith("'"):
# It's a string # It's a string

View file

@ -62,8 +62,8 @@ def test_views(db_path):
result = CliRunner().invoke(cli.cli, ["views", db_path, "--table", "--schema"]) result = CliRunner().invoke(cli.cli, ["views", db_path, "--table", "--schema"])
assert ( assert (
"view schema\n" "view schema\n"
"------ --------------------------------------------\n" "------ ----------------------------------------------\n"
"hello CREATE VIEW hello AS select sqlite_version()" 'hello CREATE VIEW "hello" AS select sqlite_version()'
) == result.output.strip() ) == result.output.strip()
@ -132,7 +132,7 @@ def test_tables_schema(db_path):
assert ( assert (
'[{"table": "Gosh", "schema": "CREATE TABLE Gosh (c1 text, c2 text, c3 text)"},\n' '[{"table": "Gosh", "schema": "CREATE TABLE Gosh (c1 text, c2 text, c3 text)"},\n'
' {"table": "Gosh2", "schema": "CREATE TABLE Gosh2 (c1 text, c2 text, c3 text)"},\n' ' {"table": "Gosh2", "schema": "CREATE TABLE Gosh2 (c1 text, c2 text, c3 text)"},\n'
' {"table": "lots", "schema": "CREATE TABLE [lots] (\\n [id] INTEGER,\\n [age] INTEGER\\n)"}]' ' {"table": "lots", "schema": "CREATE TABLE \\"lots\\" (\\n \\"id\\" INTEGER,\\n \\"age\\" INTEGER\\n)"}]'
) == result.output.strip() ) == result.output.strip()
@ -264,38 +264,38 @@ def test_create_index_desc(db_path):
assert result.exit_code == 0 assert result.exit_code == 0
assert ( assert (
db.execute("select sql from sqlite_master where type='index'").fetchone()[0] db.execute("select sql from sqlite_master where type='index'").fetchone()[0]
== "CREATE INDEX [idx_Gosh_c1]\n ON [Gosh] ([c1] desc)" == 'CREATE INDEX "idx_Gosh_c1"\n ON "Gosh" ("c1" desc)'
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
"col_name,col_type,expected_schema", "col_name,col_type,expected_schema",
( (
("text", "TEXT", "CREATE TABLE [dogs] (\n [name] TEXT\n, [text] TEXT)"), ("text", "TEXT", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "text" TEXT)'),
("text", "str", "CREATE TABLE [dogs] (\n [name] TEXT\n, [text] TEXT)"), ("text", "str", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "text" TEXT)'),
("text", "STR", "CREATE TABLE [dogs] (\n [name] TEXT\n, [text] TEXT)"), ("text", "STR", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "text" TEXT)'),
( (
"integer", "integer",
"INTEGER", "INTEGER",
"CREATE TABLE [dogs] (\n [name] TEXT\n, [integer] INTEGER)", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "integer" INTEGER)',
), ),
( (
"integer", "integer",
"int", "int",
"CREATE TABLE [dogs] (\n [name] TEXT\n, [integer] INTEGER)", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "integer" INTEGER)',
), ),
("float", "FLOAT", "CREATE TABLE [dogs] (\n [name] TEXT\n, [float] FLOAT)"), ("float", "FLOAT", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "float" FLOAT)'),
("blob", "blob", "CREATE TABLE [dogs] (\n [name] TEXT\n, [blob] BLOB)"), ("blob", "blob", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "blob" BLOB)'),
("blob", "BLOB", "CREATE TABLE [dogs] (\n [name] TEXT\n, [blob] BLOB)"), ("blob", "BLOB", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "blob" BLOB)'),
("blob", "bytes", "CREATE TABLE [dogs] (\n [name] TEXT\n, [blob] BLOB)"), ("blob", "bytes", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "blob" BLOB)'),
("blob", "BYTES", "CREATE TABLE [dogs] (\n [name] TEXT\n, [blob] BLOB)"), ("blob", "BYTES", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "blob" BLOB)'),
("default", None, "CREATE TABLE [dogs] (\n [name] TEXT\n, [default] TEXT)"), ("default", None, 'CREATE TABLE "dogs" (\n "name" TEXT\n, "default" TEXT)'),
), ),
) )
def test_add_column(db_path, col_name, col_type, expected_schema): def test_add_column(db_path, col_name, col_type, expected_schema):
db = Database(db_path) db = Database(db_path)
db.create_table("dogs", {"name": str}) db.create_table("dogs", {"name": str})
assert db["dogs"].schema == "CREATE TABLE [dogs] (\n [name] TEXT\n)" assert db["dogs"].schema == 'CREATE TABLE "dogs" (\n "name" TEXT\n)'
args = ["add-column", db_path, "dogs", col_name] args = ["add-column", db_path, "dogs", col_name]
if col_type is not None: if col_type is not None:
args.append(col_type) args.append(col_type)
@ -319,7 +319,7 @@ def test_add_column_ignore(db_path, ignore):
def test_add_column_not_null_default(db_path): def test_add_column_not_null_default(db_path):
db = Database(db_path) db = Database(db_path)
db.create_table("dogs", {"name": str}) db.create_table("dogs", {"name": str})
assert db["dogs"].schema == "CREATE TABLE [dogs] (\n [name] TEXT\n)" assert db["dogs"].schema == 'CREATE TABLE "dogs" (\n "name" TEXT\n)'
args = [ args = [
"add-column", "add-column",
db_path, db_path,
@ -330,9 +330,9 @@ def test_add_column_not_null_default(db_path):
] ]
assert CliRunner().invoke(cli.cli, args).exit_code == 0 assert CliRunner().invoke(cli.cli, args).exit_code == 0
assert db["dogs"].schema == ( assert db["dogs"].schema == (
"CREATE TABLE [dogs] (\n" 'CREATE TABLE "dogs" (\n'
" [name] TEXT\n" ' "name" TEXT\n'
", [nickname] TEXT NOT NULL DEFAULT 'dogs''dawg')" ", \"nickname\" TEXT NOT NULL DEFAULT 'dogs''dawg')"
) )
@ -403,8 +403,8 @@ def test_add_column_foreign_key(db_path):
assert result.exit_code == 0, result.output assert result.exit_code == 0, result.output
assert db["books"].schema == ( assert db["books"].schema == (
'CREATE TABLE "books" (\n' 'CREATE TABLE "books" (\n'
" [title] TEXT,\n" ' "title" TEXT,\n'
" [author_id] INTEGER REFERENCES [authors]([id])\n" ' "author_id" INTEGER REFERENCES "authors"("id")\n'
")" ")"
) )
# Try it again with a custom --fk-col # Try it again with a custom --fk-col
@ -424,9 +424,9 @@ def test_add_column_foreign_key(db_path):
assert result.exit_code == 0, result.output assert result.exit_code == 0, result.output
assert db["books"].schema == ( assert db["books"].schema == (
'CREATE TABLE "books" (\n' 'CREATE TABLE "books" (\n'
" [title] TEXT,\n" ' "title" TEXT,\n'
" [author_id] INTEGER REFERENCES [authors]([id]),\n" ' "author_id" INTEGER REFERENCES "authors"("id"),\n'
" [author_name_ref] TEXT REFERENCES [authors]([name])\n" ' "author_name_ref" TEXT REFERENCES "authors"("name")\n'
")" ")"
) )
# Throw an error if the --fk table does not exist # Throw an error if the --fk table does not exist
@ -492,10 +492,10 @@ def test_enable_fts(db_path):
assert "http://example.com_fts" == db["http://example.com"].detect_fts() assert "http://example.com_fts" == db["http://example.com"].detect_fts()
# Check tokenize was set to porter # Check tokenize was set to porter
assert ( assert (
"CREATE VIRTUAL TABLE [http://example.com_fts] USING FTS4 (\n" 'CREATE VIRTUAL TABLE "http://example.com_fts" USING FTS4 (\n'
" [c1],\n" ' "c1",\n'
" tokenize='porter',\n" " tokenize='porter',\n"
" content=[http://example.com]" ' content="http://example.com"'
"\n)" "\n)"
) == db["http://example.com_fts"].schema ) == db["http://example.com_fts"].schema
db["http://example.com"].drop() db["http://example.com"].drop()
@ -516,7 +516,7 @@ def test_enable_fts_replace(db_path):
cli.cli, ["enable-fts", db_path, "Gosh", "c1", "--fts4"] cli.cli, ["enable-fts", db_path, "Gosh", "c1", "--fts4"]
) )
assert result2.exit_code == 1 assert result2.exit_code == 1
assert result2.output == "Error: table [Gosh_fts] already exists\n" assert result2.output == 'Error: table "Gosh_fts" already exists\n'
# This should work # This should work
result3 = CliRunner().invoke( result3 = CliRunner().invoke(
@ -1139,7 +1139,7 @@ def test_upsert_alter(db_path, tmpdir):
"age", "age",
"integer", "integer",
], ],
("CREATE TABLE [t] (\n [name] TEXT,\n [age] INTEGER\n)"), ('CREATE TABLE "t" (\n "name" TEXT,\n "age" INTEGER\n)'),
), ),
# All types: # All types:
( (
@ -1158,31 +1158,31 @@ def test_upsert_alter(db_path, tmpdir):
"id", "id",
], ],
( (
"CREATE TABLE [t] (\n" 'CREATE TABLE "t" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [age] INTEGER,\n" ' "age" INTEGER,\n'
" [weight] FLOAT,\n" ' "weight" FLOAT,\n'
" [thumbnail] BLOB\n" ' "thumbnail" BLOB\n'
")" ")"
), ),
), ),
# Not null: # Not null:
( (
["name", "text", "--not-null", "name"], ["name", "text", "--not-null", "name"],
("CREATE TABLE [t] (\n" " [name] TEXT NOT NULL\n" ")"), ('CREATE TABLE "t" (\n' ' "name" TEXT NOT NULL\n' ")"),
), ),
# Default: # Default:
( (
["age", "integer", "--default", "age", "3"], ["age", "integer", "--default", "age", "3"],
("CREATE TABLE [t] (\n" " [age] INTEGER DEFAULT '3'\n" ")"), ('CREATE TABLE "t" (\n' " \"age\" INTEGER DEFAULT '3'\n" ")"),
), ),
# Compound primary key # Compound primary key
( (
["category", "text", "name", "text", "--pk", "category", "--pk", "name"], ["category", "text", "name", "text", "--pk", "category", "--pk", "name"],
( (
"CREATE TABLE [t] (\n [category] TEXT,\n [name] TEXT,\n" 'CREATE TABLE "t" (\n "category" TEXT,\n "name" TEXT,\n'
" PRIMARY KEY ([category], [name])\n)" ' PRIMARY KEY ("category", "name")\n)'
), ),
), ),
], ],
@ -1233,16 +1233,16 @@ def test_create_table_foreign_key():
assert result.exit_code == 0 assert result.exit_code == 0
db = Database("books.db") db = Database("books.db")
assert ( assert (
"CREATE TABLE [authors] (\n" 'CREATE TABLE "authors" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [name] TEXT\n" ' "name" TEXT\n'
")" ")"
) == db["authors"].schema ) == db["authors"].schema
assert ( assert (
"CREATE TABLE [books] (\n" 'CREATE TABLE "books" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [title] TEXT,\n" ' "title" TEXT,\n'
" [author_id] INTEGER REFERENCES [authors]([id])\n" ' "author_id" INTEGER REFERENCES "authors"("id")\n'
")" ")"
) == db["books"].schema ) == db["books"].schema
@ -1271,7 +1271,7 @@ def test_create_table_ignore():
cli.cli, ["create-table", "test.db", "dogs", "id", "integer", "--ignore"] cli.cli, ["create-table", "test.db", "dogs", "id", "integer", "--ignore"]
) )
assert result.exit_code == 0 assert result.exit_code == 0
assert "CREATE TABLE [dogs] (\n [name] TEXT\n)" == db["dogs"].schema assert 'CREATE TABLE "dogs" (\n "name" TEXT\n)' == db["dogs"].schema
def test_create_table_replace(): def test_create_table_replace():
@ -1283,7 +1283,7 @@ def test_create_table_replace():
cli.cli, ["create-table", "test.db", "dogs", "id", "integer", "--replace"] cli.cli, ["create-table", "test.db", "dogs", "id", "integer", "--replace"]
) )
assert result.exit_code == 0 assert result.exit_code == 0
assert "CREATE TABLE [dogs] (\n [id] INTEGER\n)" == db["dogs"].schema assert 'CREATE TABLE "dogs" (\n "id" INTEGER\n)' == db["dogs"].schema
def test_create_view(): def test_create_view():
@ -1294,7 +1294,9 @@ def test_create_view():
cli.cli, ["create-view", "test.db", "version", "select sqlite_version()"] cli.cli, ["create-view", "test.db", "version", "select sqlite_version()"]
) )
assert result.exit_code == 0 assert result.exit_code == 0
assert "CREATE VIEW version AS select sqlite_version()" == db["version"].schema assert (
'CREATE VIEW "version" AS select sqlite_version()' == db["version"].schema
)
def test_create_view_error_if_view_exists(): def test_create_view_error_if_view_exists():
@ -1329,7 +1331,8 @@ def test_create_view_ignore():
) )
assert result.exit_code == 0 assert result.exit_code == 0
assert ( assert (
"CREATE VIEW version AS select sqlite_version() + 1" == db["version"].schema 'CREATE VIEW "version" AS select sqlite_version() + 1'
== db["version"].schema
) )
@ -1349,7 +1352,9 @@ def test_create_view_replace():
], ],
) )
assert result.exit_code == 0 assert result.exit_code == 0
assert "CREATE VIEW version AS select sqlite_version()" == db["version"].schema assert (
'CREATE VIEW "version" AS select sqlite_version()' == db["version"].schema
)
def test_drop_table(): def test_drop_table():
@ -1537,9 +1542,9 @@ def test_add_foreign_keys(db_path):
[], [],
( (
'CREATE TABLE "dogs" (\n' 'CREATE TABLE "dogs" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [age] INTEGER NOT NULL DEFAULT '1',\n" " \"age\" INTEGER NOT NULL DEFAULT '1',\n"
" [name] TEXT\n" ' "name" TEXT\n'
")" ")"
), ),
), ),
@ -1547,9 +1552,9 @@ def test_add_foreign_keys(db_path):
["--type", "age", "text"], ["--type", "age", "text"],
( (
'CREATE TABLE "dogs" (\n' 'CREATE TABLE "dogs" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [age] TEXT NOT NULL DEFAULT '1',\n" " \"age\" TEXT NOT NULL DEFAULT '1',\n"
" [name] TEXT\n" ' "name" TEXT\n'
")" ")"
), ),
), ),
@ -1557,8 +1562,8 @@ def test_add_foreign_keys(db_path):
["--drop", "age"], ["--drop", "age"],
( (
'CREATE TABLE "dogs" (\n' 'CREATE TABLE "dogs" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [name] TEXT\n" ' "name" TEXT\n'
")" ")"
), ),
), ),
@ -1566,9 +1571,9 @@ def test_add_foreign_keys(db_path):
["--rename", "age", "age2", "--rename", "id", "pk"], ["--rename", "age", "age2", "--rename", "id", "pk"],
( (
'CREATE TABLE "dogs" (\n' 'CREATE TABLE "dogs" (\n'
" [pk] INTEGER PRIMARY KEY,\n" ' "pk" INTEGER PRIMARY KEY,\n'
" [age2] INTEGER NOT NULL DEFAULT '1',\n" " \"age2\" INTEGER NOT NULL DEFAULT '1',\n"
" [name] TEXT\n" ' "name" TEXT\n'
")" ")"
), ),
), ),
@ -1576,9 +1581,9 @@ def test_add_foreign_keys(db_path):
["--not-null", "name"], ["--not-null", "name"],
( (
'CREATE TABLE "dogs" (\n' 'CREATE TABLE "dogs" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [age] INTEGER NOT NULL DEFAULT '1',\n" " \"age\" INTEGER NOT NULL DEFAULT '1',\n"
" [name] TEXT NOT NULL\n" ' "name" TEXT NOT NULL\n'
")" ")"
), ),
), ),
@ -1586,9 +1591,9 @@ def test_add_foreign_keys(db_path):
["--not-null-false", "age"], ["--not-null-false", "age"],
( (
'CREATE TABLE "dogs" (\n' 'CREATE TABLE "dogs" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [age] INTEGER DEFAULT '1',\n" " \"age\" INTEGER DEFAULT '1',\n"
" [name] TEXT\n" ' "name" TEXT\n'
")" ")"
), ),
), ),
@ -1596,9 +1601,9 @@ def test_add_foreign_keys(db_path):
["--pk", "name"], ["--pk", "name"],
( (
'CREATE TABLE "dogs" (\n' 'CREATE TABLE "dogs" (\n'
" [id] INTEGER,\n" ' "id" INTEGER,\n'
" [age] INTEGER NOT NULL DEFAULT '1',\n" " \"age\" INTEGER NOT NULL DEFAULT '1',\n"
" [name] TEXT PRIMARY KEY\n" ' "name" TEXT PRIMARY KEY\n'
")" ")"
), ),
), ),
@ -1606,9 +1611,9 @@ def test_add_foreign_keys(db_path):
["--pk-none"], ["--pk-none"],
( (
'CREATE TABLE "dogs" (\n' 'CREATE TABLE "dogs" (\n'
" [id] INTEGER,\n" ' "id" INTEGER,\n'
" [age] INTEGER NOT NULL DEFAULT '1',\n" " \"age\" INTEGER NOT NULL DEFAULT '1',\n"
" [name] TEXT\n" ' "name" TEXT\n'
")" ")"
), ),
), ),
@ -1616,9 +1621,9 @@ def test_add_foreign_keys(db_path):
["--default", "name", "Turnip"], ["--default", "name", "Turnip"],
( (
'CREATE TABLE "dogs" (\n' 'CREATE TABLE "dogs" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [age] INTEGER NOT NULL DEFAULT '1',\n" " \"age\" INTEGER NOT NULL DEFAULT '1',\n"
" [name] TEXT DEFAULT 'Turnip'\n" " \"name\" TEXT DEFAULT 'Turnip'\n"
")" ")"
), ),
), ),
@ -1626,9 +1631,9 @@ def test_add_foreign_keys(db_path):
["--default-none", "age"], ["--default-none", "age"],
( (
'CREATE TABLE "dogs" (\n' 'CREATE TABLE "dogs" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [age] INTEGER NOT NULL,\n" ' "age" INTEGER NOT NULL,\n'
" [name] TEXT\n" ' "name" TEXT\n'
")" ")"
), ),
), ),
@ -1636,9 +1641,9 @@ def test_add_foreign_keys(db_path):
["-o", "name", "--column-order", "age", "-o", "id"], ["-o", "name", "--column-order", "age", "-o", "id"],
( (
'CREATE TABLE "dogs" (\n' 'CREATE TABLE "dogs" (\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [age] INTEGER NOT NULL DEFAULT '1',\n" " \"age\" INTEGER NOT NULL DEFAULT '1',\n"
" [id] INTEGER PRIMARY KEY\n" ' "id" INTEGER PRIMARY KEY\n'
")" ")"
), ),
), ),
@ -1667,11 +1672,11 @@ def test_transform(db_path, args, expected_schema):
["--drop-foreign-key", "country"], ["--drop-foreign-key", "country"],
( (
'CREATE TABLE "places" (\n' 'CREATE TABLE "places" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [country] INTEGER,\n" ' "country" INTEGER,\n'
" [city] INTEGER REFERENCES [city]([id]),\n" ' "city" INTEGER REFERENCES "city"("id"),\n'
" [continent] INTEGER\n" ' "continent" INTEGER\n'
")" ")"
), ),
), ),
@ -1679,11 +1684,11 @@ def test_transform(db_path, args, expected_schema):
["--drop-foreign-key", "country", "--drop-foreign-key", "city"], ["--drop-foreign-key", "country", "--drop-foreign-key", "city"],
( (
'CREATE TABLE "places" (\n' 'CREATE TABLE "places" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [country] INTEGER,\n" ' "country" INTEGER,\n'
" [city] INTEGER,\n" ' "city" INTEGER,\n'
" [continent] INTEGER\n" ' "continent" INTEGER\n'
")" ")"
), ),
), ),
@ -1691,11 +1696,11 @@ def test_transform(db_path, args, expected_schema):
["--add-foreign-key", "continent", "continent", "id"], ["--add-foreign-key", "continent", "continent", "id"],
( (
'CREATE TABLE "places" (\n' 'CREATE TABLE "places" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [country] INTEGER REFERENCES [country]([id]),\n" ' "country" INTEGER REFERENCES "country"("id"),\n'
" [city] INTEGER REFERENCES [city]([id]),\n" ' "city" INTEGER REFERENCES "city"("id"),\n'
" [continent] INTEGER REFERENCES [continent]([id])\n" ' "continent" INTEGER REFERENCES "continent"("id")\n'
")" ")"
), ),
), ),
@ -1734,7 +1739,7 @@ def test_transform_add_or_drop_foreign_key(db_path, extra_args, expected_schema)
_common_other_schema = ( _common_other_schema = (
"CREATE TABLE [species] (\n [id] INTEGER PRIMARY KEY,\n [species] TEXT\n)" 'CREATE TABLE "species" (\n "id" INTEGER PRIMARY KEY,\n "species" TEXT\n)'
) )
@ -1745,9 +1750,9 @@ _common_other_schema = (
[], [],
( (
'CREATE TABLE "trees" (\n' 'CREATE TABLE "trees" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [address] TEXT,\n" ' "address" TEXT,\n'
" [species_id] INTEGER REFERENCES [species]([id])\n" ' "species_id" INTEGER REFERENCES "species"("id")\n'
")" ")"
), ),
_common_other_schema, _common_other_schema,
@ -1756,20 +1761,20 @@ _common_other_schema = (
["--table", "custom_table"], ["--table", "custom_table"],
( (
'CREATE TABLE "trees" (\n' 'CREATE TABLE "trees" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [address] TEXT,\n" ' "address" TEXT,\n'
" [custom_table_id] INTEGER REFERENCES [custom_table]([id])\n" ' "custom_table_id" INTEGER REFERENCES "custom_table"("id")\n'
")" ")"
), ),
"CREATE TABLE [custom_table] (\n [id] INTEGER PRIMARY KEY,\n [species] TEXT\n)", 'CREATE TABLE "custom_table" (\n "id" INTEGER PRIMARY KEY,\n "species" TEXT\n)',
), ),
( (
["--fk-column", "custom_fk"], ["--fk-column", "custom_fk"],
( (
'CREATE TABLE "trees" (\n' 'CREATE TABLE "trees" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [address] TEXT,\n" ' "address" TEXT,\n'
" [custom_fk] INTEGER REFERENCES [species]([id])\n" ' "custom_fk" INTEGER REFERENCES "species"("id")\n'
")" ")"
), ),
_common_other_schema, _common_other_schema,
@ -1777,11 +1782,11 @@ _common_other_schema = (
( (
["--rename", "name", "name2"], ["--rename", "name", "name2"],
'CREATE TABLE "trees" (\n' 'CREATE TABLE "trees" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [address] TEXT,\n" ' "address" TEXT,\n'
" [species_id] INTEGER REFERENCES [species]([id])\n" ' "species_id" INTEGER REFERENCES "species"("id")\n'
")", ")",
"CREATE TABLE [species] (\n [id] INTEGER PRIMARY KEY,\n [species] TEXT\n)", 'CREATE TABLE "species" (\n "id" INTEGER PRIMARY KEY,\n "species" TEXT\n)',
), ),
], ],
) )
@ -2036,34 +2041,34 @@ def test_triggers(tmpdir, extra_args, expected):
( (
[], [],
( (
"CREATE TABLE [dogs] (\n" 'CREATE TABLE "dogs" (\n'
" [id] INTEGER,\n" ' "id" INTEGER,\n'
" [name] TEXT\n" ' "name" TEXT\n'
");\n" ");\n"
"CREATE TABLE [chickens] (\n" 'CREATE TABLE "chickens" (\n'
" [id] INTEGER,\n" ' "id" INTEGER,\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [breed] TEXT\n" ' "breed" TEXT\n'
");\n" ");\n"
"CREATE INDEX [idx_chickens_breed]\n" 'CREATE INDEX "idx_chickens_breed"\n'
" ON [chickens] ([breed]);\n" ' ON "chickens" ("breed");\n'
), ),
), ),
( (
["dogs"], ["dogs"],
("CREATE TABLE [dogs] (\n" " [id] INTEGER,\n" " [name] TEXT\n" ")\n"), ('CREATE TABLE "dogs" (\n' ' "id" INTEGER,\n' ' "name" TEXT\n' ")\n"),
), ),
( (
["chickens", "dogs"], ["chickens", "dogs"],
( (
"CREATE TABLE [chickens] (\n" 'CREATE TABLE "chickens" (\n'
" [id] INTEGER,\n" ' "id" INTEGER,\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [breed] TEXT\n" ' "breed" TEXT\n'
")\n" ")\n"
"CREATE TABLE [dogs] (\n" 'CREATE TABLE "dogs" (\n'
" [id] INTEGER,\n" ' "id" INTEGER,\n'
" [name] TEXT\n" ' "name" TEXT\n'
")\n" ")\n"
), ),
), ),
@ -2127,10 +2132,10 @@ def test_import_no_headers(tmpdir, args, tsv):
db = Database(db_path) db = Database(db_path)
schema = db["creatures"].schema schema = db["creatures"].schema
assert schema == ( assert schema == (
"CREATE TABLE [creatures] (\n" 'CREATE TABLE "creatures" (\n'
" [untitled_1] TEXT,\n" ' "untitled_1" TEXT,\n'
" [untitled_2] TEXT,\n" ' "untitled_2" TEXT,\n'
" [untitled_3] TEXT\n" ' "untitled_3" TEXT\n'
")" ")"
) )
rows = list(db["creatures"].rows) rows = list(db["creatures"].rows)
@ -2182,8 +2187,8 @@ def test_csv_insert_bom(tmpdir):
db = Database(db_path) db = Database(db_path)
tables = db.execute("select name, sql from sqlite_master").fetchall() tables = db.execute("select name, sql from sqlite_master").fetchall()
assert tables == [ assert tables == [
("broken", "CREATE TABLE [broken] (\n [\ufeffname] TEXT,\n [age] TEXT\n)"), ("broken", 'CREATE TABLE "broken" (\n "\ufeffname" TEXT,\n "age" TEXT\n)'),
("fixed", "CREATE TABLE [fixed] (\n [name] TEXT,\n [age] TEXT\n)"), ("fixed", 'CREATE TABLE "fixed" (\n "name" TEXT,\n "age" TEXT\n)'),
] ]
@ -2245,7 +2250,7 @@ def test_integer_overflow_error(tmpdir):
assert result.exit_code == 1 assert result.exit_code == 1
assert result.output == ( assert result.output == (
"Error: Python int too large to convert to SQLite INTEGER\n\n" "Error: Python int too large to convert to SQLite INTEGER\n\n"
"sql = INSERT INTO [items] ([bignumber]) VALUES (?)\n" 'sql = INSERT INTO "items" ("bignumber") VALUES (?)\n'
"parameters = [34223049823094832094802398430298048240]\n" "parameters = [34223049823094832094802398430298048240]\n"
) )

View file

@ -406,9 +406,9 @@ def test_convert_multi_complex_column_types(fresh_db_and_path):
{"id": 4, "is_str": None, "is_float": None, "is_int": None, "is_bytes": None}, {"id": 4, "is_str": None, "is_float": None, "is_int": None, "is_bytes": None},
] ]
assert db["rows"].schema == ( assert db["rows"].schema == (
"CREATE TABLE [rows] (\n" 'CREATE TABLE "rows" (\n'
" [id] INTEGER PRIMARY KEY\n" ' "id" INTEGER PRIMARY KEY\n'
", [is_str] TEXT, [is_float] FLOAT, [is_int] INTEGER, [is_bytes] BLOB)" ', "is_str" TEXT, "is_float" FLOAT, "is_int" INTEGER, "is_bytes" BLOB)'
) )

View file

@ -127,12 +127,12 @@ def test_insert_multiple_with_compound_primary_key(db_path, tmpdir):
assert dogs == list(db.query("select * from dogs order by breed, id")) assert dogs == list(db.query("select * from dogs order by breed, id"))
assert {"breed", "id"} == set(db["dogs"].pks) assert {"breed", "id"} == set(db["dogs"].pks)
assert ( assert (
"CREATE TABLE [dogs] (\n" 'CREATE TABLE "dogs" (\n'
" [breed] TEXT,\n" ' "breed" TEXT,\n'
" [id] INTEGER,\n" ' "id" INTEGER,\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [age] INTEGER,\n" ' "age" INTEGER,\n'
" PRIMARY KEY ([id], [breed])\n" ' PRIMARY KEY ("id", "breed")\n'
")" ")"
) == db["dogs"].schema ) == db["dogs"].schema
@ -154,11 +154,11 @@ def test_insert_not_null_default(db_path, tmpdir):
assert result.exit_code == 0 assert result.exit_code == 0
db = Database(db_path) db = Database(db_path)
assert ( assert (
"CREATE TABLE [dogs] (\n" 'CREATE TABLE "dogs" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [name] TEXT NOT NULL,\n" ' "name" TEXT NOT NULL,\n'
" [age] INTEGER NOT NULL DEFAULT '1',\n" " \"age\" INTEGER NOT NULL DEFAULT '1',\n"
" [score] INTEGER DEFAULT '5'\n)" " \"score\" INTEGER DEFAULT '5'\n)"
) == db["dogs"].schema ) == db["dogs"].schema
@ -466,7 +466,7 @@ def test_insert_convert_text(db_path):
) )
assert result.exit_code == 0, result.output assert result.exit_code == 0, result.output
db = Database(db_path) db = Database(db_path)
rows = list(db.query("select [text] from [text]")) rows = list(db.query('select "text" from "text"'))
assert rows == [{"text": "THIS IS TEXT\nWILL BE UPPER NOW"}] assert rows == [{"text": "THIS IS TEXT\nWILL BE UPPER NOW"}]
@ -486,7 +486,7 @@ def test_insert_convert_text_returning_iterator(db_path):
) )
assert result.exit_code == 0, result.output assert result.exit_code == 0, result.output
db = Database(db_path) db = Database(db_path)
rows = list(db.query("select [word] from [text]")) rows = list(db.query('select "word" from "text"'))
assert rows == [{"word": "A"}, {"word": "bunch"}, {"word": "of"}, {"word": "words"}] assert rows == [{"word": "A"}, {"word": "bunch"}, {"word": "of"}, {"word": "words"}]
@ -506,7 +506,7 @@ def test_insert_convert_lines(db_path):
) )
assert result.exit_code == 0, result.output assert result.exit_code == 0, result.output
db = Database(db_path) db = Database(db_path)
rows = list(db.query("select [line] from [all]")) rows = list(db.query('select "line" from "all"'))
assert rows == [{"line": "THIS IS TEXT"}, {"line": "WILL BE UPPER NOW"}] assert rows == [{"line": "THIS IS TEXT"}, {"line": "WILL BE UPPER NOW"}]

View file

@ -167,13 +167,13 @@ def test_memory_dump(extra_args):
expected = ( expected = (
"BEGIN TRANSACTION;\n" "BEGIN TRANSACTION;\n"
'CREATE TABLE IF NOT EXISTS "stdin" (\n' 'CREATE TABLE IF NOT EXISTS "stdin" (\n'
" [id] INTEGER,\n" ' "id" INTEGER,\n'
" [name] TEXT\n" ' "name" TEXT\n'
");\n" ");\n"
"INSERT INTO \"stdin\" VALUES(1,'Cleo');\n" "INSERT INTO \"stdin\" VALUES(1,'Cleo');\n"
"INSERT INTO \"stdin\" VALUES(2,'Bants');\n" "INSERT INTO \"stdin\" VALUES(2,'Bants');\n"
"CREATE VIEW t1 AS select * from [stdin];\n" 'CREATE VIEW "t1" AS select * from "stdin";\n'
"CREATE VIEW t AS select * from [stdin];\n" 'CREATE VIEW "t" AS select * from "stdin";\n'
"COMMIT;" "COMMIT;"
) )
# Using sqlite-dump it won't have IF NOT EXISTS # Using sqlite-dump it won't have IF NOT EXISTS
@ -191,11 +191,11 @@ def test_memory_schema(extra_args):
assert result.exit_code == 0 assert result.exit_code == 0
assert result.output.strip() == ( assert result.output.strip() == (
'CREATE TABLE "stdin" (\n' 'CREATE TABLE "stdin" (\n'
" [id] INTEGER,\n" ' "id" INTEGER,\n'
" [name] TEXT\n" ' "name" TEXT\n'
");\n" ");\n"
"CREATE VIEW t1 AS select * from [stdin];\n" 'CREATE VIEW "t1" AS select * from "stdin";\n'
"CREATE VIEW t AS select * from [stdin];" 'CREATE VIEW "t" AS select * from "stdin";'
) )
@ -285,16 +285,16 @@ def test_memory_two_files_with_same_stem(tmpdir):
assert result.exit_code == 0 assert result.exit_code == 0
assert result.output == ( assert result.output == (
'CREATE TABLE "data" (\n' 'CREATE TABLE "data" (\n'
" [id] INTEGER,\n" ' "id" INTEGER,\n'
" [name] TEXT\n" ' "name" TEXT\n'
");\n" ");\n"
"CREATE VIEW t1 AS select * from [data];\n" 'CREATE VIEW "t1" AS select * from "data";\n'
"CREATE VIEW t AS select * from [data];\n" 'CREATE VIEW "t" AS select * from "data";\n'
'CREATE TABLE "data_2" (\n' 'CREATE TABLE "data_2" (\n'
" [id] INTEGER,\n" ' "id" INTEGER,\n'
" [name] TEXT\n" ' "name" TEXT\n'
");\n" ");\n"
"CREATE VIEW t2 AS select * from [data_2];\n" 'CREATE VIEW "t2" AS select * from "data_2";\n'
) )

View file

@ -50,13 +50,13 @@ def test_create_table(fresh_db):
{"name": "datetime_col", "type": "TEXT"}, {"name": "datetime_col", "type": "TEXT"},
] == [{"name": col.name, "type": col.type} for col in table.columns] ] == [{"name": col.name, "type": col.type} for col in table.columns]
assert ( assert (
"CREATE TABLE [test_table] (\n" 'CREATE TABLE "test_table" (\n'
" [text_col] TEXT,\n" ' "text_col" TEXT,\n'
" [float_col] FLOAT,\n" ' "float_col" FLOAT,\n'
" [int_col] INTEGER,\n" ' "int_col" INTEGER,\n'
" [bool_col] INTEGER,\n" ' "bool_col" INTEGER,\n'
" [bytes_col] BLOB,\n" ' "bytes_col" BLOB,\n'
" [datetime_col] TEXT\n" ' "datetime_col" TEXT\n'
")" ")"
) == table.schema ) == table.schema
@ -66,11 +66,11 @@ def test_create_table_compound_primary_key(fresh_db):
"test_table", {"id1": str, "id2": str, "value": int}, pk=("id1", "id2") "test_table", {"id1": str, "id2": str, "value": int}, pk=("id1", "id2")
) )
assert ( assert (
"CREATE TABLE [test_table] (\n" 'CREATE TABLE "test_table" (\n'
" [id1] TEXT,\n" ' "id1" TEXT,\n'
" [id2] TEXT,\n" ' "id2" TEXT,\n'
" [value] INTEGER,\n" ' "value" INTEGER,\n'
" PRIMARY KEY ([id1], [id2])\n" ' PRIMARY KEY ("id1", "id2")\n'
")" ")"
) == table.schema ) == table.schema
assert ["id1", "id2"] == table.pks assert ["id1", "id2"] == table.pks
@ -80,13 +80,17 @@ def test_create_table_compound_primary_key(fresh_db):
def test_create_table_with_single_primary_key(fresh_db, pk): def test_create_table_with_single_primary_key(fresh_db, pk):
fresh_db["foo"].insert({"id": 1}, pk=pk) fresh_db["foo"].insert({"id": 1}, pk=pk)
assert ( assert (
fresh_db["foo"].schema == "CREATE TABLE [foo] (\n [id] INTEGER PRIMARY KEY\n)" fresh_db["foo"].schema == 'CREATE TABLE "foo" (\n "id" INTEGER PRIMARY KEY\n)'
) )
def test_create_table_with_invalid_column_characters(fresh_db): def test_create_table_with_special_column_characters(fresh_db):
with pytest.raises(AssertionError): # With double-quote escaping, columns with special characters are now valid
fresh_db.create_table("players", {"name[foo]": str}) table = fresh_db.create_table("players", {"name[foo]": str})
assert ["players"] == fresh_db.table_names()
assert [{"name": "name[foo]", "type": "TEXT"}] == [
{"name": col.name, "type": col.type} for col in table.columns
]
def test_create_table_with_defaults(fresh_db): def test_create_table_with_defaults(fresh_db):
@ -100,7 +104,7 @@ def test_create_table_with_defaults(fresh_db):
{"name": col.name, "type": col.type} for col in table.columns {"name": col.name, "type": col.type} for col in table.columns
] ]
assert ( assert (
"CREATE TABLE [players] (\n [name] TEXT DEFAULT 'bob''''bob',\n [score] INTEGER DEFAULT 1\n)" "CREATE TABLE \"players\" (\n \"name\" TEXT DEFAULT 'bob''''bob',\n \"score\" INTEGER DEFAULT 1\n)"
) == table.schema ) == table.schema
@ -123,7 +127,7 @@ def test_create_table_with_not_null(fresh_db):
{"name": col.name, "type": col.type} for col in table.columns {"name": col.name, "type": col.type} for col in table.columns
] ]
assert ( assert (
"CREATE TABLE [players] (\n [name] TEXT NOT NULL,\n [score] INTEGER NOT NULL DEFAULT 3\n)" 'CREATE TABLE "players" (\n "name" TEXT NOT NULL,\n "score" INTEGER NOT NULL DEFAULT 3\n)'
) == table.schema ) == table.schema
@ -145,7 +149,7 @@ def test_create_table_with_not_null(fresh_db):
[{"name": "memoryview", "type": "BLOB"}], [{"name": "memoryview", "type": "BLOB"}],
), ),
({"uuid": uuid.uuid4()}, [{"name": "uuid", "type": "TEXT"}]), ({"uuid": uuid.uuid4()}, [{"name": "uuid", "type": "TEXT"}]),
({"foo[bar]": 1}, [{"name": "foo_bar_", "type": "INTEGER"}]), ({"foo[bar]": 1}, [{"name": "foo[bar]", "type": "INTEGER"}]),
( (
{"timedelta": datetime.timedelta(hours=1)}, {"timedelta": datetime.timedelta(hours=1)},
[{"name": "timedelta", "type": "TEXT"}], [{"name": "timedelta", "type": "TEXT"}],
@ -307,9 +311,9 @@ def test_self_referential_foreign_key(fresh_db):
foreign_keys=(("ref", "test_table", "id"),), foreign_keys=(("ref", "test_table", "id"),),
) )
assert ( assert (
"CREATE TABLE [test_table] (\n" 'CREATE TABLE "test_table" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [ref] INTEGER REFERENCES [test_table]([id])\n" ' "ref" INTEGER REFERENCES "test_table"("id")\n'
")" ")"
) == table.schema ) == table.schema
@ -340,58 +344,58 @@ def test_create_error_if_invalid_self_referential_foreign_keys(fresh_db):
"nickname", "nickname",
str, str,
None, None,
"CREATE TABLE [dogs] (\n [name] TEXT\n, [nickname] TEXT)", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "nickname" TEXT)',
), ),
( (
"dob", "dob",
datetime.date, datetime.date,
None, None,
"CREATE TABLE [dogs] (\n [name] TEXT\n, [dob] TEXT)", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "dob" TEXT)',
), ),
("age", int, None, "CREATE TABLE [dogs] (\n [name] TEXT\n, [age] INTEGER)"), ("age", int, None, 'CREATE TABLE "dogs" (\n "name" TEXT\n, "age" INTEGER)'),
( (
"weight", "weight",
float, float,
None, None,
"CREATE TABLE [dogs] (\n [name] TEXT\n, [weight] FLOAT)", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "weight" FLOAT)',
), ),
("text", "TEXT", None, "CREATE TABLE [dogs] (\n [name] TEXT\n, [text] TEXT)"), ("text", "TEXT", None, 'CREATE TABLE "dogs" (\n "name" TEXT\n, "text" TEXT)'),
( (
"integer", "integer",
"INTEGER", "INTEGER",
None, None,
"CREATE TABLE [dogs] (\n [name] TEXT\n, [integer] INTEGER)", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "integer" INTEGER)',
), ),
( (
"float", "float",
"FLOAT", "FLOAT",
None, None,
"CREATE TABLE [dogs] (\n [name] TEXT\n, [float] FLOAT)", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "float" FLOAT)',
), ),
("blob", "blob", None, "CREATE TABLE [dogs] (\n [name] TEXT\n, [blob] BLOB)"), ("blob", "blob", None, 'CREATE TABLE "dogs" (\n "name" TEXT\n, "blob" BLOB)'),
( (
"default_str", "default_str",
None, None,
None, None,
"CREATE TABLE [dogs] (\n [name] TEXT\n, [default_str] TEXT)", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "default_str" TEXT)',
), ),
( (
"nickname", "nickname",
str, str,
"", "",
"CREATE TABLE [dogs] (\n [name] TEXT\n, [nickname] TEXT NOT NULL DEFAULT '')", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "nickname" TEXT NOT NULL DEFAULT \'\')',
), ),
( (
"nickname", "nickname",
str, str,
"dawg's dawg", "dawg's dawg",
"CREATE TABLE [dogs] (\n [name] TEXT\n, [nickname] TEXT NOT NULL DEFAULT 'dawg''s dawg')", 'CREATE TABLE "dogs" (\n "name" TEXT\n, "nickname" TEXT NOT NULL DEFAULT \'dawg\'\'s dawg\')',
), ),
), ),
) )
def test_add_column(fresh_db, col_name, col_type, not_null_default, expected_schema): def test_add_column(fresh_db, col_name, col_type, not_null_default, expected_schema):
fresh_db.create_table("dogs", {"name": str}) fresh_db.create_table("dogs", {"name": str})
assert fresh_db["dogs"].schema == "CREATE TABLE [dogs] (\n [name] TEXT\n)" assert fresh_db["dogs"].schema == 'CREATE TABLE "dogs" (\n "name" TEXT\n)'
fresh_db["dogs"].add_column(col_name, col_type, not_null_default=not_null_default) fresh_db["dogs"].add_column(col_name, col_type, not_null_default=not_null_default)
assert fresh_db["dogs"].schema == expected_schema assert fresh_db["dogs"].schema == expected_schema
@ -496,8 +500,8 @@ def test_add_column_foreign_key(fresh_db):
fresh_db["dogs"].add_column("breed_id", fk="breeds") fresh_db["dogs"].add_column("breed_id", fk="breeds")
assert fresh_db["dogs"].schema == ( assert fresh_db["dogs"].schema == (
'CREATE TABLE "dogs" (\n' 'CREATE TABLE "dogs" (\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [breed_id] INTEGER REFERENCES [breeds]([rowid])\n" ' "breed_id" INTEGER REFERENCES "breeds"("rowid")\n'
")" ")"
) )
# And again with an explicit primary key column # And again with an explicit primary key column
@ -505,9 +509,9 @@ def test_add_column_foreign_key(fresh_db):
fresh_db["dogs"].add_column("subbreed_id", fk="subbreeds") fresh_db["dogs"].add_column("subbreed_id", fk="subbreeds")
assert fresh_db["dogs"].schema == ( assert fresh_db["dogs"].schema == (
'CREATE TABLE "dogs" (\n' 'CREATE TABLE "dogs" (\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [breed_id] INTEGER REFERENCES [breeds]([rowid]),\n" ' "breed_id" INTEGER REFERENCES "breeds"("rowid"),\n'
" [subbreed_id] TEXT REFERENCES [subbreeds]([primkey])\n" ' "subbreed_id" TEXT REFERENCES "subbreeds"("primkey")\n'
")" ")"
) )
@ -519,8 +523,8 @@ def test_add_foreign_key_guess_table(fresh_db):
fresh_db["dogs"].add_foreign_key("breed_id") fresh_db["dogs"].add_foreign_key("breed_id")
assert fresh_db["dogs"].schema == ( assert fresh_db["dogs"].schema == (
'CREATE TABLE "dogs" (\n' 'CREATE TABLE "dogs" (\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [breed_id] INTEGER REFERENCES [breeds]([id])\n" ' "breed_id" INTEGER REFERENCES "breeds"("id")\n'
")" ")"
) )
@ -591,7 +595,7 @@ def test_add_missing_columns_case_insensitive(fresh_db):
table.add_missing_columns([{"Name": ".", "age": 4}]) table.add_missing_columns([{"Name": ".", "age": 4}])
assert ( assert (
table.schema table.schema
== "CREATE TABLE [foo] (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT\n, [age] INTEGER)" == 'CREATE TABLE "foo" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT\n, "age" INTEGER)'
) )
@ -814,7 +818,7 @@ def test_create_index_desc(fresh_db):
"select sql from sqlite_master where name='idx_dogs_age_name'" "select sql from sqlite_master where name='idx_dogs_age_name'"
).fetchone()[0] ).fetchone()[0]
assert sql == ( assert sql == (
"CREATE INDEX [idx_dogs_age_name]\n" " ON [dogs] ([age] desc, [name])" 'CREATE INDEX "idx_dogs_age_name"\n' ' ON "dogs" ("age" desc, "name")'
) )
@ -1154,19 +1158,19 @@ def test_quote(fresh_db, input, expected):
( (
( (
{"id": int}, {"id": int},
"[id] INTEGER", '"id" INTEGER',
), ),
( (
{"col": dict}, {"col": dict},
"[col] TEXT", '"col" TEXT',
), ),
( (
{"col": tuple}, {"col": tuple},
"[col] TEXT", '"col" TEXT',
), ),
( (
{"col": list}, {"col": list},
"[col] TEXT", '"col" TEXT',
), ),
), ),
) )
@ -1191,12 +1195,12 @@ def test_create(fresh_db):
defaults={"integer": 0}, defaults={"integer": 0},
) )
assert fresh_db["t"].schema == ( assert fresh_db["t"].schema == (
"CREATE TABLE [t] (\n" 'CREATE TABLE "t" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [float] FLOAT NOT NULL,\n" ' "float" FLOAT NOT NULL,\n'
" [text] TEXT,\n" ' "text" TEXT,\n'
" [integer] INTEGER NOT NULL DEFAULT 0,\n" ' "integer" INTEGER NOT NULL DEFAULT 0,\n'
" [bytes] BLOB\n" ' "bytes" BLOB\n'
")" ")"
) )
@ -1232,7 +1236,7 @@ def test_create_replace(fresh_db):
fresh_db["t"].create({"id": int}) fresh_db["t"].create({"id": int})
# This should not # This should not
fresh_db["t"].create({"name": str}, replace=True) fresh_db["t"].create({"name": str}, replace=True)
assert fresh_db["t"].schema == ("CREATE TABLE [t] (\n" " [name] TEXT\n" ")") assert fresh_db["t"].schema == ('CREATE TABLE "t" (\n' ' "name" TEXT\n' ")")
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -1242,58 +1246,58 @@ def test_create_replace(fresh_db):
( (
{"id": int, "name": str}, {"id": int, "name": str},
{"pk": "id"}, {"pk": "id"},
"CREATE TABLE [demo] (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT\n)", 'CREATE TABLE "demo" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT\n)',
False, False,
), ),
# Drop name column, remove primary key # Drop name column, remove primary key
({"id": int}, {}, 'CREATE TABLE "demo" (\n [id] INTEGER\n)', True), ({"id": int}, {}, 'CREATE TABLE "demo" (\n "id" INTEGER\n)', True),
# Add a new column # Add a new column
( (
{"id": int, "name": str, "age": int}, {"id": int, "name": str, "age": int},
{"pk": "id"}, {"pk": "id"},
'CREATE TABLE "demo" (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT,\n [age] INTEGER\n)', 'CREATE TABLE "demo" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT,\n "age" INTEGER\n)',
True, True,
), ),
# Change a column type # Change a column type
( (
{"id": int, "name": bytes}, {"id": int, "name": bytes},
{"pk": "id"}, {"pk": "id"},
'CREATE TABLE "demo" (\n [id] INTEGER PRIMARY KEY,\n [name] BLOB\n)', 'CREATE TABLE "demo" (\n "id" INTEGER PRIMARY KEY,\n "name" BLOB\n)',
True, True,
), ),
# Change the primary key # Change the primary key
( (
{"id": int, "name": str}, {"id": int, "name": str},
{"pk": "name"}, {"pk": "name"},
'CREATE TABLE "demo" (\n [id] INTEGER,\n [name] TEXT PRIMARY KEY\n)', 'CREATE TABLE "demo" (\n "id" INTEGER,\n "name" TEXT PRIMARY KEY\n)',
True, True,
), ),
# Change in column order # Change in column order
( (
{"id": int, "name": str}, {"id": int, "name": str},
{"pk": "id", "column_order": ["name"]}, {"pk": "id", "column_order": ["name"]},
'CREATE TABLE "demo" (\n [name] TEXT,\n [id] INTEGER PRIMARY KEY\n)', 'CREATE TABLE "demo" (\n "name" TEXT,\n "id" INTEGER PRIMARY KEY\n)',
True, True,
), ),
# Same column order is ignored # Same column order is ignored
( (
{"id": int, "name": str}, {"id": int, "name": str},
{"pk": "id", "column_order": ["id", "name"]}, {"pk": "id", "column_order": ["id", "name"]},
"CREATE TABLE [demo] (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT\n)", 'CREATE TABLE "demo" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT\n)',
False, False,
), ),
# Change not null # Change not null
( (
{"id": int, "name": str}, {"id": int, "name": str},
{"pk": "id", "not_null": {"name"}}, {"pk": "id", "not_null": {"name"}},
'CREATE TABLE "demo" (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT NOT NULL\n)', 'CREATE TABLE "demo" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT NOT NULL\n)',
True, True,
), ),
# Change default values # Change default values
( (
{"id": int, "name": str}, {"id": int, "name": str},
{"pk": "id", "defaults": {"id": 0, "name": "Bob"}}, {"pk": "id", "defaults": {"id": 0, "name": "Bob"}},
"CREATE TABLE \"demo\" (\n [id] INTEGER PRIMARY KEY DEFAULT 0,\n [name] TEXT DEFAULT 'Bob'\n)", 'CREATE TABLE "demo" (\n "id" INTEGER PRIMARY KEY DEFAULT 0,\n "name" TEXT DEFAULT \'Bob\'\n)',
True, True,
), ),
), ),
@ -1356,11 +1360,11 @@ def test_insert_upsert_strict(fresh_db, method_name, strict):
def test_create_table_strict(fresh_db, strict): def test_create_table_strict(fresh_db, strict):
table = fresh_db.create_table("t", {"id": int, "f": float}, strict=strict) table = fresh_db.create_table("t", {"id": int, "f": float}, strict=strict)
assert table.strict == strict or not fresh_db.supports_strict assert table.strict == strict or not fresh_db.supports_strict
expected_schema = "CREATE TABLE [t] (\n" " [id] INTEGER,\n" " [f] FLOAT\n" ")" expected_schema = 'CREATE TABLE "t" (\n' ' "id" INTEGER,\n' ' "f" FLOAT\n' ")"
if strict and not fresh_db.supports_strict: if strict and not fresh_db.supports_strict:
return return
if strict: if strict:
expected_schema = "CREATE TABLE [t] (\n [id] INTEGER,\n [f] REAL\n) STRICT" expected_schema = 'CREATE TABLE "t" (\n "id" INTEGER,\n "f" REAL\n) STRICT'
assert table.schema == expected_schema assert table.schema == expected_schema

View file

@ -6,12 +6,12 @@ import pytest
def test_duplicate(fresh_db): def test_duplicate(fresh_db):
# Create table using native Sqlite statement: # Create table using native Sqlite statement:
fresh_db.execute( fresh_db.execute(
"""CREATE TABLE [table1] ( """CREATE TABLE "table1" (
[text_col] TEXT, "text_col" TEXT,
[real_col] REAL, "real_col" REAL,
[int_col] INTEGER, "int_col" INTEGER,
[bool_col] INTEGER, "bool_col" INTEGER,
[datetime_col] TEXT)""" "datetime_col" TEXT)"""
) )
# Insert one row of mock data: # Insert one row of mock data:
dt = datetime.datetime.now() dt = datetime.datetime.now()

View file

@ -15,25 +15,25 @@ def test_enable_counts_specific_table(fresh_db):
foo.enable_counts() foo.enable_counts()
assert foo.triggers_dict == { assert foo.triggers_dict == {
"foo_counts_insert": ( "foo_counts_insert": (
"CREATE TRIGGER [foo_counts_insert] AFTER INSERT ON [foo]\n" 'CREATE TRIGGER "foo_counts_insert" AFTER INSERT ON "foo"\n'
"BEGIN\n" "BEGIN\n"
" INSERT OR REPLACE INTO [_counts]\n" ' INSERT OR REPLACE INTO "_counts"\n'
" VALUES (\n 'foo',\n" " VALUES (\n 'foo',\n"
" COALESCE(\n" " COALESCE(\n"
" (SELECT count FROM [_counts] WHERE [table] = 'foo'),\n" ' (SELECT count FROM "_counts" WHERE "table" = \'foo\'),\n'
" 0\n" " 0\n"
" ) + 1\n" " ) + 1\n"
" );\n" " );\n"
"END" "END"
), ),
"foo_counts_delete": ( "foo_counts_delete": (
"CREATE TRIGGER [foo_counts_delete] AFTER DELETE ON [foo]\n" 'CREATE TRIGGER "foo_counts_delete" AFTER DELETE ON "foo"\n'
"BEGIN\n" "BEGIN\n"
" INSERT OR REPLACE INTO [_counts]\n" ' INSERT OR REPLACE INTO "_counts"\n'
" VALUES (\n" " VALUES (\n"
" 'foo',\n" " 'foo',\n"
" COALESCE(\n" " COALESCE(\n"
" (SELECT count FROM [_counts] WHERE [table] = 'foo'),\n" ' (SELECT count FROM "_counts" WHERE "table" = \'foo\'),\n'
" 0\n" " 0\n"
" ) - 1\n" " ) - 1\n"
" );\n" " );\n"
@ -132,7 +132,7 @@ def test_uses_counts_after_enable_counts(counts_db_path):
assert db.table("foo").count == 1 assert db.table("foo").count == 1
assert logged == [ assert logged == [
("select name from sqlite_master where type = 'view'", None), ("select name from sqlite_master where type = 'view'", None),
("select count(*) from [foo]", []), ('select count(*) from "foo"', []),
] ]
logged.clear() logged.clear()
assert not db.use_counts_table assert not db.use_counts_table
@ -141,7 +141,7 @@ def test_uses_counts_after_enable_counts(counts_db_path):
assert db.table("foo").count == 1 assert db.table("foo").count == 1
assert logged == [ assert logged == [
( (
"CREATE TABLE IF NOT EXISTS [_counts](\n [table] TEXT PRIMARY KEY,\n count INTEGER DEFAULT 0\n);", 'CREATE TABLE IF NOT EXISTS "_counts"(\n "table" TEXT PRIMARY KEY,\n count INTEGER DEFAULT 0\n);',
None, None,
), ),
("select name from sqlite_master where type = 'table'", None), ("select name from sqlite_master where type = 'table'", None),
@ -157,7 +157,7 @@ def test_uses_counts_after_enable_counts(counts_db_path):
("SELECT quote(:value)", {"value": "baz"}), ("SELECT quote(:value)", {"value": "baz"}),
("select sql from sqlite_master where name = ?", ("_counts",)), ("select sql from sqlite_master where name = ?", ("_counts",)),
("select name from sqlite_master where type = 'view'", None), ("select name from sqlite_master where type = 'view'", None),
("select [table], count from _counts where [table] in (?)", ["foo"]), ('select "table", count from _counts where "table" in (?)', ["foo"]),
] ]

View file

@ -24,16 +24,16 @@ def test_extract_single_column(fresh_db, table, fk_column):
fresh_db["tree"].extract("species", table=table, fk_column=fk_column) fresh_db["tree"].extract("species", table=table, fk_column=fk_column)
assert fresh_db["tree"].schema == ( assert fresh_db["tree"].schema == (
'CREATE TABLE "tree" (\n' 'CREATE TABLE "tree" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [{}] INTEGER REFERENCES [{}]([id]),\n".format(expected_fk, expected_table) ' "{}" INTEGER REFERENCES "{}"("id"),\n'.format(expected_fk, expected_table)
+ " [end] INTEGER\n" + ' "end" INTEGER\n'
+ ")" + ")"
) )
assert fresh_db[expected_table].schema == ( assert fresh_db[expected_table].schema == (
"CREATE TABLE [{}] (\n".format(expected_table) 'CREATE TABLE "{}" (\n'.format(expected_table)
+ " [id] INTEGER PRIMARY KEY,\n" + ' "id" INTEGER PRIMARY KEY,\n'
" [species] TEXT\n" ' "species" TEXT\n'
")" ")"
) )
assert list(fresh_db[expected_table].rows) == [ assert list(fresh_db[expected_table].rows) == [
@ -71,16 +71,16 @@ def test_extract_multiple_columns_with_rename(fresh_db):
) )
assert fresh_db["tree"].schema == ( assert fresh_db["tree"].schema == (
'CREATE TABLE "tree" (\n' 'CREATE TABLE "tree" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [common_name_latin_name_id] INTEGER REFERENCES [common_name_latin_name]([id])\n" ' "common_name_latin_name_id" INTEGER REFERENCES "common_name_latin_name"("id")\n'
")" ")"
) )
assert fresh_db["common_name_latin_name"].schema == ( assert fresh_db["common_name_latin_name"].schema == (
"CREATE TABLE [common_name_latin_name] (\n" 'CREATE TABLE "common_name_latin_name" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [latin_name] TEXT\n" ' "latin_name" TEXT\n'
")" ")"
) )
assert list(fresh_db["common_name_latin_name"].rows) == [ assert list(fresh_db["common_name_latin_name"].rows) == [
@ -122,8 +122,8 @@ def test_extract_rowid_table(fresh_db):
fresh_db["tree"].extract(["common_name", "latin_name"]) fresh_db["tree"].extract(["common_name", "latin_name"])
assert fresh_db["tree"].schema == ( assert fresh_db["tree"].schema == (
'CREATE TABLE "tree" (\n' 'CREATE TABLE "tree" (\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [common_name_latin_name_id] INTEGER REFERENCES [common_name_latin_name]([id])\n" ' "common_name_latin_name_id" INTEGER REFERENCES "common_name_latin_name"("id")\n'
")" ")"
) )
assert ( assert (
@ -152,15 +152,15 @@ def test_reuse_lookup_table(fresh_db):
fresh_db["individuals"].extract("species", rename={"species": "name"}) fresh_db["individuals"].extract("species", rename={"species": "name"})
assert fresh_db["sightings"].schema == ( assert fresh_db["sightings"].schema == (
'CREATE TABLE "sightings" (\n' 'CREATE TABLE "sightings" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [species_id] INTEGER REFERENCES [species]([id])\n" ' "species_id" INTEGER REFERENCES "species"("id")\n'
")" ")"
) )
assert fresh_db["individuals"].schema == ( assert fresh_db["individuals"].schema == (
'CREATE TABLE "individuals" (\n' 'CREATE TABLE "individuals" (\n'
" [id] INTEGER PRIMARY KEY,\n" ' "id" INTEGER PRIMARY KEY,\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [species_id] INTEGER REFERENCES [species]([id])\n" ' "species_id" INTEGER REFERENCES "species"("id")\n'
")" ")"
) )
assert list(fresh_db["species"].rows) == [ assert list(fresh_db["species"].rows) == [

View file

@ -30,13 +30,13 @@ def test_extracts(fresh_db, kwargs, expected_table, use_table_factory):
# Should now have two tables: Trees and Species # Should now have two tables: Trees and Species
assert {expected_table, "Trees"} == set(fresh_db.table_names()) assert {expected_table, "Trees"} == set(fresh_db.table_names())
assert ( assert (
"CREATE TABLE [{}] (\n [id] INTEGER PRIMARY KEY,\n [value] TEXT\n)".format( 'CREATE TABLE "{}" (\n "id" INTEGER PRIMARY KEY,\n "value" TEXT\n)'.format(
expected_table expected_table
) )
== fresh_db[expected_table].schema == fresh_db[expected_table].schema
) )
assert ( assert (
"CREATE TABLE [Trees] (\n [id] INTEGER,\n [species_id] INTEGER REFERENCES [{}]([id])\n)".format( 'CREATE TABLE "Trees" (\n "id" INTEGER,\n "species_id" INTEGER REFERENCES "{}"("id")\n)'.format(
expected_table expected_table
) )
== fresh_db["Trees"].schema == fresh_db["Trees"].schema

View file

@ -435,40 +435,40 @@ def test_enable_fts_error_message_on_views():
{}, {},
"FTS5", "FTS5",
( (
"with original as (\n" 'with "original" as (\n'
" select\n" " select\n"
" rowid,\n" " rowid,\n"
" *\n" " *\n"
" from [books]\n" ' from "books"\n'
")\n" ")\n"
"select\n" "select\n"
" [original].*\n" ' "original".*\n'
"from\n" "from\n"
" [original]\n" ' "original"\n'
" join [books_fts] on [original].rowid = [books_fts].rowid\n" ' join "books_fts" on "original".rowid = "books_fts".rowid\n'
"where\n" "where\n"
" [books_fts] match :query\n" ' "books_fts" match :query\n'
"order by\n" "order by\n"
" [books_fts].rank" ' "books_fts".rank'
), ),
), ),
( (
{"columns": ["title"], "order_by": "rowid", "limit": 10}, {"columns": ["title"], "order_by": "rowid", "limit": 10},
"FTS5", "FTS5",
( (
"with original as (\n" 'with "original" as (\n'
" select\n" " select\n"
" rowid,\n" " rowid,\n"
" [title]\n" ' "title"\n'
" from [books]\n" ' from "books"\n'
")\n" ")\n"
"select\n" "select\n"
" [original].[title]\n" ' "original"."title"\n'
"from\n" "from\n"
" [original]\n" ' "original"\n'
" join [books_fts] on [original].rowid = [books_fts].rowid\n" ' join "books_fts" on "original".rowid = "books_fts".rowid\n'
"where\n" "where\n"
" [books_fts] match :query\n" ' "books_fts" match :query\n'
"order by\n" "order by\n"
" rowid\n" " rowid\n"
"limit 10" "limit 10"
@ -478,64 +478,64 @@ def test_enable_fts_error_message_on_views():
{"where": "author = :author"}, {"where": "author = :author"},
"FTS5", "FTS5",
( (
"with original as (\n" 'with "original" as (\n'
" select\n" " select\n"
" rowid,\n" " rowid,\n"
" *\n" " *\n"
" from [books]\n" ' from "books"\n'
" where author = :author\n" " where author = :author\n"
")\n" ")\n"
"select\n" "select\n"
" [original].*\n" ' "original".*\n'
"from\n" "from\n"
" [original]\n" ' "original"\n'
" join [books_fts] on [original].rowid = [books_fts].rowid\n" ' join "books_fts" on "original".rowid = "books_fts".rowid\n'
"where\n" "where\n"
" [books_fts] match :query\n" ' "books_fts" match :query\n'
"order by\n" "order by\n"
" [books_fts].rank" ' "books_fts".rank'
), ),
), ),
( (
{"columns": ["title"]}, {"columns": ["title"]},
"FTS4", "FTS4",
( (
"with original as (\n" 'with "original" as (\n'
" select\n" " select\n"
" rowid,\n" " rowid,\n"
" [title]\n" ' "title"\n'
" from [books]\n" ' from "books"\n'
")\n" ")\n"
"select\n" "select\n"
" [original].[title]\n" ' "original"."title"\n'
"from\n" "from\n"
" [original]\n" ' "original"\n'
" join [books_fts] on [original].rowid = [books_fts].rowid\n" ' join "books_fts" on "original".rowid = "books_fts".rowid\n'
"where\n" "where\n"
" [books_fts] match :query\n" ' "books_fts" match :query\n'
"order by\n" "order by\n"
" rank_bm25(matchinfo([books_fts], 'pcnalx'))" " rank_bm25(matchinfo(\"books_fts\", 'pcnalx'))"
), ),
), ),
( (
{"offset": 1, "limit": 1}, {"offset": 1, "limit": 1},
"FTS4", "FTS4",
( (
"with original as (\n" 'with "original" as (\n'
" select\n" " select\n"
" rowid,\n" " rowid,\n"
" *\n" " *\n"
" from [books]\n" ' from "books"\n'
")\n" ")\n"
"select\n" "select\n"
" [original].*\n" ' "original".*\n'
"from\n" "from\n"
" [original]\n" ' "original"\n'
" join [books_fts] on [original].rowid = [books_fts].rowid\n" ' join "books_fts" on "original".rowid = "books_fts".rowid\n'
"where\n" "where\n"
" [books_fts] match :query\n" ' "books_fts" match :query\n'
"order by\n" "order by\n"
" rank_bm25(matchinfo([books_fts], 'pcnalx'))\n" " rank_bm25(matchinfo(\"books_fts\", 'pcnalx'))\n"
"limit 1 offset 1" "limit 1 offset 1"
), ),
), ),
@ -543,21 +543,21 @@ def test_enable_fts_error_message_on_views():
{"limit": 2}, {"limit": 2},
"FTS4", "FTS4",
( (
"with original as (\n" 'with "original" as (\n'
" select\n" " select\n"
" rowid,\n" " rowid,\n"
" *\n" " *\n"
" from [books]\n" ' from "books"\n'
")\n" ")\n"
"select\n" "select\n"
" [original].*\n" ' "original".*\n'
"from\n" "from\n"
" [original]\n" ' "original"\n'
" join [books_fts] on [original].rowid = [books_fts].rowid\n" ' join "books_fts" on "original".rowid = "books_fts".rowid\n'
"where\n" "where\n"
" [books_fts] match :query\n" ' "books_fts" match :query\n'
"order by\n" "order by\n"
" rank_bm25(matchinfo([books_fts], 'pcnalx'))\n" " rank_bm25(matchinfo(\"books_fts\", 'pcnalx'))\n"
"limit 2" "limit 2"
), ),
), ),
@ -565,66 +565,66 @@ def test_enable_fts_error_message_on_views():
{"where": "author = :author"}, {"where": "author = :author"},
"FTS4", "FTS4",
( (
"with original as (\n" 'with "original" as (\n'
" select\n" " select\n"
" rowid,\n" " rowid,\n"
" *\n" " *\n"
" from [books]\n" ' from "books"\n'
" where author = :author\n" " where author = :author\n"
")\n" ")\n"
"select\n" "select\n"
" [original].*\n" ' "original".*\n'
"from\n" "from\n"
" [original]\n" ' "original"\n'
" join [books_fts] on [original].rowid = [books_fts].rowid\n" ' join "books_fts" on "original".rowid = "books_fts".rowid\n'
"where\n" "where\n"
" [books_fts] match :query\n" ' "books_fts" match :query\n'
"order by\n" "order by\n"
" rank_bm25(matchinfo([books_fts], 'pcnalx'))" " rank_bm25(matchinfo(\"books_fts\", 'pcnalx'))"
), ),
), ),
( (
{"include_rank": True}, {"include_rank": True},
"FTS5", "FTS5",
( (
"with original as (\n" 'with "original" as (\n'
" select\n" " select\n"
" rowid,\n" " rowid,\n"
" *\n" " *\n"
" from [books]\n" ' from "books"\n'
")\n" ")\n"
"select\n" "select\n"
" [original].*,\n" ' "original".*,\n'
" [books_fts].rank rank\n" ' "books_fts".rank rank\n'
"from\n" "from\n"
" [original]\n" ' "original"\n'
" join [books_fts] on [original].rowid = [books_fts].rowid\n" ' join "books_fts" on "original".rowid = "books_fts".rowid\n'
"where\n" "where\n"
" [books_fts] match :query\n" ' "books_fts" match :query\n'
"order by\n" "order by\n"
" [books_fts].rank" ' "books_fts".rank'
), ),
), ),
( (
{"include_rank": True}, {"include_rank": True},
"FTS4", "FTS4",
( (
"with original as (\n" 'with "original" as (\n'
" select\n" " select\n"
" rowid,\n" " rowid,\n"
" *\n" " *\n"
" from [books]\n" ' from "books"\n'
")\n" ")\n"
"select\n" "select\n"
" [original].*,\n" ' "original".*,\n'
" rank_bm25(matchinfo([books_fts], 'pcnalx')) rank\n" " rank_bm25(matchinfo(\"books_fts\", 'pcnalx')) rank\n"
"from\n" "from\n"
" [original]\n" ' "original"\n'
" join [books_fts] on [original].rowid = [books_fts].rowid\n" ' join "books_fts" on "original".rowid = "books_fts".rowid\n'
"where\n" "where\n"
" [books_fts] match :query\n" ' "books_fts" match :query\n'
"order by\n" "order by\n"
" rank_bm25(matchinfo([books_fts], 'pcnalx'))" " rank_bm25(matchinfo(\"books_fts\", 'pcnalx'))"
), ),
), ),
], ],

View file

@ -200,19 +200,19 @@ def test_triggers_and_triggers_dict(fresh_db):
} }
expected_triggers = { expected_triggers = {
"authors_ai": ( "authors_ai": (
"CREATE TRIGGER [authors_ai] AFTER INSERT ON [authors] BEGIN\n" 'CREATE TRIGGER "authors_ai" AFTER INSERT ON "authors" BEGIN\n'
" INSERT INTO [authors_fts] (rowid, [name], [famous_works]) VALUES (new.rowid, new.[name], new.[famous_works]);\n" ' INSERT INTO "authors_fts" (rowid, "name", "famous_works") VALUES (new.rowid, new."name", new."famous_works");\n'
"END" "END"
), ),
"authors_ad": ( "authors_ad": (
"CREATE TRIGGER [authors_ad] AFTER DELETE ON [authors] BEGIN\n" 'CREATE TRIGGER "authors_ad" AFTER DELETE ON "authors" BEGIN\n'
" INSERT INTO [authors_fts] ([authors_fts], rowid, [name], [famous_works]) VALUES('delete', old.rowid, old.[name], old.[famous_works]);\n" ' INSERT INTO "authors_fts" ("authors_fts", rowid, "name", "famous_works") VALUES(\'delete\', old.rowid, old."name", old."famous_works");\n'
"END" "END"
), ),
"authors_au": ( "authors_au": (
"CREATE TRIGGER [authors_au] AFTER UPDATE ON [authors] BEGIN\n" 'CREATE TRIGGER "authors_au" AFTER UPDATE ON "authors" BEGIN\n'
" INSERT INTO [authors_fts] ([authors_fts], rowid, [name], [famous_works]) VALUES('delete', old.rowid, old.[name], old.[famous_works]);\n" ' INSERT INTO "authors_fts" ("authors_fts", rowid, "name", "famous_works") VALUES(\'delete\', old.rowid, old."name", old."famous_works");\n'
" INSERT INTO [authors_fts] (rowid, [name], [famous_works]) VALUES (new.rowid, new.[name], new.[famous_works]);\nEND" ' INSERT INTO "authors_fts" (rowid, "name", "famous_works") VALUES (new.rowid, new."name", new."famous_works");\nEND'
), ),
} }
assert authors.triggers_dict == expected_triggers assert authors.triggers_dict == expected_triggers

View file

@ -114,18 +114,18 @@ def test_lookup_with_extra_insert_parameters(fresh_db):
columns={"make_this_integer": int}, columns={"make_this_integer": int},
) )
assert species.schema == ( assert species.schema == (
"CREATE TABLE [species] (\n" 'CREATE TABLE "species" (\n'
" [renamed_id] INTEGER PRIMARY KEY,\n" ' "renamed_id" INTEGER PRIMARY KEY,\n'
" [this_at_front] INTEGER,\n" ' "this_at_front" INTEGER,\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [type] TEXT,\n" ' "type" TEXT,\n'
" [first_seen] TEXT,\n" ' "first_seen" TEXT,\n'
" [make_not_null] INTEGER NOT NULL,\n" ' "make_not_null" INTEGER NOT NULL,\n'
" [fk_to_other] INTEGER REFERENCES [other_table]([id]),\n" ' "fk_to_other" INTEGER REFERENCES "other_table"("id"),\n'
" [default_is_dog] TEXT DEFAULT 'dog',\n" " \"default_is_dog\" TEXT DEFAULT 'dog',\n"
" [extract_this] INTEGER REFERENCES [extract_this]([id]),\n" ' "extract_this" INTEGER REFERENCES "extract_this"("id"),\n'
" [convert_to_upper] TEXT,\n" ' "convert_to_upper" TEXT,\n'
" [make_this_integer] INTEGER\n" ' "make_this_integer" INTEGER\n'
")" ")"
) )
assert species.get(id) == { assert species.get(id) == {

View file

@ -18,15 +18,15 @@ def test_tracer():
("select name from sqlite_master where type = 'view'", None), ("select name from sqlite_master where type = 'view'", None),
("select name from sqlite_master where type = 'table'", None), ("select name from sqlite_master where type = 'table'", None),
("select name from sqlite_master where type = 'view'", None), ("select name from sqlite_master where type = 'view'", None),
("CREATE TABLE [dogs] (\n [name] TEXT\n);\n ", None), ('CREATE TABLE "dogs" (\n "name" TEXT\n);\n ', None),
("select name from sqlite_master where type = 'view'", None), ("select name from sqlite_master where type = 'view'", None),
("INSERT INTO [dogs] ([name]) VALUES (?)", ["Cleopaws"]), ('INSERT INTO "dogs" ("name") VALUES (?)', ["Cleopaws"]),
( (
"CREATE VIRTUAL TABLE [dogs_fts] USING FTS5 (\n [name],\n content=[dogs]\n)", 'CREATE VIRTUAL TABLE "dogs_fts" USING FTS5 (\n "name",\n content="dogs"\n)',
None, None,
), ),
( (
"INSERT INTO [dogs_fts] (rowid, [name])\n SELECT rowid, [name] FROM [dogs];", 'INSERT INTO "dogs_fts" (rowid, "name")\n SELECT rowid, "name" FROM "dogs";',
None, None,
), ),
] ]
@ -64,7 +64,7 @@ def test_with_tracer():
" )\n" " )\n"
" )", " )",
{ {
"like": "%VIRTUAL TABLE%USING FTS%content=[dogs]%", "like": '%VIRTUAL TABLE%USING FTS%content="dogs"%',
"like2": '%VIRTUAL TABLE%USING FTS%content="dogs"%', "like2": '%VIRTUAL TABLE%USING FTS%content="dogs"%',
"table": "dogs", "table": "dogs",
}, },
@ -73,21 +73,21 @@ def test_with_tracer():
("select name from sqlite_master where type = 'view'", None), ("select name from sqlite_master where type = 'view'", None),
("select sql from sqlite_master where name = ?", ("dogs_fts",)), ("select sql from sqlite_master where name = ?", ("dogs_fts",)),
( (
"with original as (\n" 'with "original" as (\n'
" select\n" " select\n"
" rowid,\n" " rowid,\n"
" *\n" " *\n"
" from [dogs]\n" ' from "dogs"\n'
")\n" ")\n"
"select\n" "select\n"
" [original].*\n" ' "original".*\n'
"from\n" "from\n"
" [original]\n" ' "original"\n'
" join [dogs_fts] on [original].rowid = [dogs_fts].rowid\n" ' join "dogs_fts" on "original".rowid = "dogs_fts".rowid\n'
"where\n" "where\n"
" [dogs_fts] match :query\n" ' "dogs_fts" match :query\n'
"order by\n" "order by\n"
" [dogs_fts].rank", ' "dogs_fts".rank',
{"query": "Cleopaws"}, {"query": "Cleopaws"},
), ),
] ]

View file

@ -10,90 +10,90 @@ import pytest
( (
{}, {},
[ [
"CREATE TABLE [dogs_new_suffix] (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT,\n [age] TEXT\n);", 'CREATE TABLE "dogs_new_suffix" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT,\n "age" TEXT\n);',
"INSERT INTO [dogs_new_suffix] ([rowid], [id], [name], [age])\n SELECT [rowid], [id], [name], [age] FROM [dogs];", 'INSERT INTO "dogs_new_suffix" ("rowid", "id", "name", "age")\n SELECT "rowid", "id", "name", "age" FROM "dogs";',
"DROP TABLE [dogs];", 'DROP TABLE "dogs";',
"ALTER TABLE [dogs_new_suffix] RENAME TO [dogs];", 'ALTER TABLE "dogs_new_suffix" RENAME TO "dogs";',
], ],
), ),
# Change column type # Change column type
( (
{"types": {"age": int}}, {"types": {"age": int}},
[ [
"CREATE TABLE [dogs_new_suffix] (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT,\n [age] INTEGER\n);", 'CREATE TABLE "dogs_new_suffix" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT,\n "age" INTEGER\n);',
"INSERT INTO [dogs_new_suffix] ([rowid], [id], [name], [age])\n SELECT [rowid], [id], [name], [age] FROM [dogs];", 'INSERT INTO "dogs_new_suffix" ("rowid", "id", "name", "age")\n SELECT "rowid", "id", "name", "age" FROM "dogs";',
"DROP TABLE [dogs];", 'DROP TABLE "dogs";',
"ALTER TABLE [dogs_new_suffix] RENAME TO [dogs];", 'ALTER TABLE "dogs_new_suffix" RENAME TO "dogs";',
], ],
), ),
# Rename a column # Rename a column
( (
{"rename": {"age": "dog_age"}}, {"rename": {"age": "dog_age"}},
[ [
"CREATE TABLE [dogs_new_suffix] (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT,\n [dog_age] TEXT\n);", 'CREATE TABLE "dogs_new_suffix" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT,\n "dog_age" TEXT\n);',
"INSERT INTO [dogs_new_suffix] ([rowid], [id], [name], [dog_age])\n SELECT [rowid], [id], [name], [age] FROM [dogs];", 'INSERT INTO "dogs_new_suffix" ("rowid", "id", "name", "dog_age")\n SELECT "rowid", "id", "name", "age" FROM "dogs";',
"DROP TABLE [dogs];", 'DROP TABLE "dogs";',
"ALTER TABLE [dogs_new_suffix] RENAME TO [dogs];", 'ALTER TABLE "dogs_new_suffix" RENAME TO "dogs";',
], ],
), ),
# Drop a column # Drop a column
( (
{"drop": ["age"]}, {"drop": ["age"]},
[ [
"CREATE TABLE [dogs_new_suffix] (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT\n);", 'CREATE TABLE "dogs_new_suffix" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT\n);',
"INSERT INTO [dogs_new_suffix] ([rowid], [id], [name])\n SELECT [rowid], [id], [name] FROM [dogs];", 'INSERT INTO "dogs_new_suffix" ("rowid", "id", "name")\n SELECT "rowid", "id", "name" FROM "dogs";',
"DROP TABLE [dogs];", 'DROP TABLE "dogs";',
"ALTER TABLE [dogs_new_suffix] RENAME TO [dogs];", 'ALTER TABLE "dogs_new_suffix" RENAME TO "dogs";',
], ],
), ),
# Convert type AND rename column # Convert type AND rename column
( (
{"types": {"age": int}, "rename": {"age": "dog_age"}}, {"types": {"age": int}, "rename": {"age": "dog_age"}},
[ [
"CREATE TABLE [dogs_new_suffix] (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT,\n [dog_age] INTEGER\n);", 'CREATE TABLE "dogs_new_suffix" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT,\n "dog_age" INTEGER\n);',
"INSERT INTO [dogs_new_suffix] ([rowid], [id], [name], [dog_age])\n SELECT [rowid], [id], [name], [age] FROM [dogs];", 'INSERT INTO "dogs_new_suffix" ("rowid", "id", "name", "dog_age")\n SELECT "rowid", "id", "name", "age" FROM "dogs";',
"DROP TABLE [dogs];", 'DROP TABLE "dogs";',
"ALTER TABLE [dogs_new_suffix] RENAME TO [dogs];", 'ALTER TABLE "dogs_new_suffix" RENAME TO "dogs";',
], ],
), ),
# Change primary key # Change primary key
( (
{"pk": "age"}, {"pk": "age"},
[ [
"CREATE TABLE [dogs_new_suffix] (\n [id] INTEGER,\n [name] TEXT,\n [age] TEXT PRIMARY KEY\n);", 'CREATE TABLE "dogs_new_suffix" (\n "id" INTEGER,\n "name" TEXT,\n "age" TEXT PRIMARY KEY\n);',
"INSERT INTO [dogs_new_suffix] ([rowid], [id], [name], [age])\n SELECT [rowid], [id], [name], [age] FROM [dogs];", 'INSERT INTO "dogs_new_suffix" ("rowid", "id", "name", "age")\n SELECT "rowid", "id", "name", "age" FROM "dogs";',
"DROP TABLE [dogs];", 'DROP TABLE "dogs";',
"ALTER TABLE [dogs_new_suffix] RENAME TO [dogs];", 'ALTER TABLE "dogs_new_suffix" RENAME TO "dogs";',
], ],
), ),
# Change primary key to a compound pk # Change primary key to a compound pk
( (
{"pk": ("age", "name")}, {"pk": ("age", "name")},
[ [
"CREATE TABLE [dogs_new_suffix] (\n [id] INTEGER,\n [name] TEXT,\n [age] TEXT,\n PRIMARY KEY ([age], [name])\n);", 'CREATE TABLE "dogs_new_suffix" (\n "id" INTEGER,\n "name" TEXT,\n "age" TEXT,\n PRIMARY KEY ("age", "name")\n);',
"INSERT INTO [dogs_new_suffix] ([rowid], [id], [name], [age])\n SELECT [rowid], [id], [name], [age] FROM [dogs];", 'INSERT INTO "dogs_new_suffix" ("rowid", "id", "name", "age")\n SELECT "rowid", "id", "name", "age" FROM "dogs";',
"DROP TABLE [dogs];", 'DROP TABLE "dogs";',
"ALTER TABLE [dogs_new_suffix] RENAME TO [dogs];", 'ALTER TABLE "dogs_new_suffix" RENAME TO "dogs";',
], ],
), ),
# Remove primary key, creating a rowid table # Remove primary key, creating a rowid table
( (
{"pk": None}, {"pk": None},
[ [
"CREATE TABLE [dogs_new_suffix] (\n [id] INTEGER,\n [name] TEXT,\n [age] TEXT\n);", 'CREATE TABLE "dogs_new_suffix" (\n "id" INTEGER,\n "name" TEXT,\n "age" TEXT\n);',
"INSERT INTO [dogs_new_suffix] ([rowid], [id], [name], [age])\n SELECT [rowid], [id], [name], [age] FROM [dogs];", 'INSERT INTO "dogs_new_suffix" ("rowid", "id", "name", "age")\n SELECT "rowid", "id", "name", "age" FROM "dogs";',
"DROP TABLE [dogs];", 'DROP TABLE "dogs";',
"ALTER TABLE [dogs_new_suffix] RENAME TO [dogs];", 'ALTER TABLE "dogs_new_suffix" RENAME TO "dogs";',
], ],
), ),
# Keeping the table # Keeping the table
( (
{"drop": ["age"], "keep_table": "kept_table"}, {"drop": ["age"], "keep_table": "kept_table"},
[ [
"CREATE TABLE [dogs_new_suffix] (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT\n);", 'CREATE TABLE "dogs_new_suffix" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT\n);',
"INSERT INTO [dogs_new_suffix] ([rowid], [id], [name])\n SELECT [rowid], [id], [name] FROM [dogs];", 'INSERT INTO "dogs_new_suffix" ("rowid", "id", "name")\n SELECT "rowid", "id", "name" FROM "dogs";',
"ALTER TABLE [dogs] RENAME TO [kept_table];", 'ALTER TABLE "dogs" RENAME TO "kept_table";',
"ALTER TABLE [dogs_new_suffix] RENAME TO [dogs];", 'ALTER TABLE "dogs_new_suffix" RENAME TO "dogs";',
], ],
), ),
], ],
@ -133,40 +133,40 @@ def test_transform_sql_table_with_primary_key(
( (
{}, {},
[ [
"CREATE TABLE [dogs_new_suffix] (\n [id] INTEGER,\n [name] TEXT,\n [age] TEXT\n);", 'CREATE TABLE "dogs_new_suffix" (\n "id" INTEGER,\n "name" TEXT,\n "age" TEXT\n);',
"INSERT INTO [dogs_new_suffix] ([rowid], [id], [name], [age])\n SELECT [rowid], [id], [name], [age] FROM [dogs];", 'INSERT INTO "dogs_new_suffix" ("rowid", "id", "name", "age")\n SELECT "rowid", "id", "name", "age" FROM "dogs";',
"DROP TABLE [dogs];", 'DROP TABLE "dogs";',
"ALTER TABLE [dogs_new_suffix] RENAME TO [dogs];", 'ALTER TABLE "dogs_new_suffix" RENAME TO "dogs";',
], ],
), ),
# Change column type # Change column type
( (
{"types": {"age": int}}, {"types": {"age": int}},
[ [
"CREATE TABLE [dogs_new_suffix] (\n [id] INTEGER,\n [name] TEXT,\n [age] INTEGER\n);", 'CREATE TABLE "dogs_new_suffix" (\n "id" INTEGER,\n "name" TEXT,\n "age" INTEGER\n);',
"INSERT INTO [dogs_new_suffix] ([rowid], [id], [name], [age])\n SELECT [rowid], [id], [name], [age] FROM [dogs];", 'INSERT INTO "dogs_new_suffix" ("rowid", "id", "name", "age")\n SELECT "rowid", "id", "name", "age" FROM "dogs";',
"DROP TABLE [dogs];", 'DROP TABLE "dogs";',
"ALTER TABLE [dogs_new_suffix] RENAME TO [dogs];", 'ALTER TABLE "dogs_new_suffix" RENAME TO "dogs";',
], ],
), ),
# Rename a column # Rename a column
( (
{"rename": {"age": "dog_age"}}, {"rename": {"age": "dog_age"}},
[ [
"CREATE TABLE [dogs_new_suffix] (\n [id] INTEGER,\n [name] TEXT,\n [dog_age] TEXT\n);", 'CREATE TABLE "dogs_new_suffix" (\n "id" INTEGER,\n "name" TEXT,\n "dog_age" TEXT\n);',
"INSERT INTO [dogs_new_suffix] ([rowid], [id], [name], [dog_age])\n SELECT [rowid], [id], [name], [age] FROM [dogs];", 'INSERT INTO "dogs_new_suffix" ("rowid", "id", "name", "dog_age")\n SELECT "rowid", "id", "name", "age" FROM "dogs";',
"DROP TABLE [dogs];", 'DROP TABLE "dogs";',
"ALTER TABLE [dogs_new_suffix] RENAME TO [dogs];", 'ALTER TABLE "dogs_new_suffix" RENAME TO "dogs";',
], ],
), ),
# Make ID a primary key # Make ID a primary key
( (
{"pk": "id"}, {"pk": "id"},
[ [
"CREATE TABLE [dogs_new_suffix] (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT,\n [age] TEXT\n);", 'CREATE TABLE "dogs_new_suffix" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT,\n "age" TEXT\n);',
"INSERT INTO [dogs_new_suffix] ([rowid], [id], [name], [age])\n SELECT [rowid], [id], [name], [age] FROM [dogs];", 'INSERT INTO "dogs_new_suffix" ("rowid", "id", "name", "age")\n SELECT "rowid", "id", "name", "age" FROM "dogs";',
"DROP TABLE [dogs];", 'DROP TABLE "dogs";',
"ALTER TABLE [dogs_new_suffix] RENAME TO [dogs];", 'ALTER TABLE "dogs_new_suffix" RENAME TO "dogs";',
], ],
), ),
], ],
@ -204,13 +204,13 @@ def test_transform_sql_with_no_primary_key_to_primary_key_of_id(fresh_db):
dogs.insert({"id": 1, "name": "Cleo", "age": "5"}) dogs.insert({"id": 1, "name": "Cleo", "age": "5"})
assert ( assert (
dogs.schema dogs.schema
== "CREATE TABLE [dogs] (\n [id] INTEGER,\n [name] TEXT,\n [age] TEXT\n)" == 'CREATE TABLE "dogs" (\n "id" INTEGER,\n "name" TEXT,\n "age" TEXT\n)'
) )
dogs.transform(pk="id") dogs.transform(pk="id")
# Slight oddity: [dogs] becomes "dogs" during the rename: # Slight oddity: [dogs] becomes "dogs" during the rename:
assert ( assert (
dogs.schema dogs.schema
== 'CREATE TABLE "dogs" (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT,\n [age] TEXT\n)' == 'CREATE TABLE "dogs" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT,\n "age" TEXT\n)'
) )
@ -220,7 +220,7 @@ def test_transform_rename_pk(fresh_db):
dogs.transform(rename={"id": "pk"}) dogs.transform(rename={"id": "pk"})
assert ( assert (
dogs.schema dogs.schema
== 'CREATE TABLE "dogs" (\n [pk] INTEGER PRIMARY KEY,\n [name] TEXT,\n [age] TEXT\n)' == 'CREATE TABLE "dogs" (\n "pk" INTEGER PRIMARY KEY,\n "name" TEXT,\n "age" TEXT\n)'
) )
@ -230,7 +230,7 @@ def test_transform_not_null(fresh_db):
dogs.transform(not_null={"name"}) dogs.transform(not_null={"name"})
assert ( assert (
dogs.schema dogs.schema
== 'CREATE TABLE "dogs" (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT NOT NULL,\n [age] TEXT\n)' == 'CREATE TABLE "dogs" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT NOT NULL,\n "age" TEXT\n)'
) )
@ -240,7 +240,7 @@ def test_transform_remove_a_not_null(fresh_db):
dogs.transform(not_null={"name": True, "age": False}) dogs.transform(not_null={"name": True, "age": False})
assert ( assert (
dogs.schema dogs.schema
== 'CREATE TABLE "dogs" (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT NOT NULL,\n [age] TEXT\n)' == 'CREATE TABLE "dogs" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT NOT NULL,\n "age" TEXT\n)'
) )
@ -251,7 +251,7 @@ def test_transform_add_not_null_with_rename(fresh_db, not_null):
dogs.transform(not_null=not_null, rename={"age": "dog_age"}) dogs.transform(not_null=not_null, rename={"age": "dog_age"})
assert ( assert (
dogs.schema dogs.schema
== 'CREATE TABLE "dogs" (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT,\n [dog_age] TEXT NOT NULL\n)' == 'CREATE TABLE "dogs" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT,\n "dog_age" TEXT NOT NULL\n)'
) )
@ -261,7 +261,7 @@ def test_transform_defaults(fresh_db):
dogs.transform(defaults={"age": 1}) dogs.transform(defaults={"age": 1})
assert ( assert (
dogs.schema dogs.schema
== 'CREATE TABLE "dogs" (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT,\n [age] INTEGER DEFAULT 1\n)' == 'CREATE TABLE "dogs" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT,\n "age" INTEGER DEFAULT 1\n)'
) )
@ -271,7 +271,7 @@ def test_transform_defaults_and_rename_column(fresh_db):
dogs.transform(rename={"age": "dog_age"}, defaults={"age": 1}) dogs.transform(rename={"age": "dog_age"}, defaults={"age": 1})
assert ( assert (
dogs.schema dogs.schema
== 'CREATE TABLE "dogs" (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT,\n [dog_age] INTEGER DEFAULT 1\n)' == 'CREATE TABLE "dogs" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT,\n "dog_age" INTEGER DEFAULT 1\n)'
) )
@ -281,7 +281,7 @@ def test_remove_defaults(fresh_db):
dogs.transform(defaults={"age": None}) dogs.transform(defaults={"age": None})
assert ( assert (
dogs.schema dogs.schema
== 'CREATE TABLE "dogs" (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT,\n [age] INTEGER\n)' == 'CREATE TABLE "dogs" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT,\n "age" INTEGER\n)'
) )
@ -391,7 +391,7 @@ def test_transform_verify_foreign_keys(fresh_db):
# This should have rolled us back # This should have rolled us back
assert ( assert (
fresh_db["authors"].schema fresh_db["authors"].schema
== "CREATE TABLE [authors] (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT\n)" == 'CREATE TABLE "authors" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT\n)'
) )
assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0] assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0]
@ -420,11 +420,11 @@ def test_transform_add_foreign_keys_from_scratch(fresh_db):
] ]
assert fresh_db["places"].schema == ( assert fresh_db["places"].schema == (
'CREATE TABLE "places" (\n' 'CREATE TABLE "places" (\n'
" [id] INTEGER,\n" ' "id" INTEGER,\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [country] INTEGER REFERENCES [country]([id]),\n" ' "country" INTEGER REFERENCES "country"("id"),\n'
" [continent] INTEGER REFERENCES [continent]([id]),\n" ' "continent" INTEGER REFERENCES "continent"("id"),\n'
" [city] INTEGER REFERENCES [city]([id])\n" ' "city" INTEGER REFERENCES "city"("id")\n'
")" ")"
) )
@ -491,11 +491,11 @@ def test_transform_replace_foreign_keys(fresh_db, foreign_keys):
fresh_db["places"].transform(foreign_keys=foreign_keys) fresh_db["places"].transform(foreign_keys=foreign_keys)
assert fresh_db["places"].schema == ( assert fresh_db["places"].schema == (
'CREATE TABLE "places" (\n' 'CREATE TABLE "places" (\n'
" [id] INTEGER,\n" ' "id" INTEGER,\n'
" [name] TEXT,\n" ' "name" TEXT,\n'
" [country] INTEGER REFERENCES [country]([id]),\n" ' "country" INTEGER REFERENCES "country"("id"),\n'
" [continent] INTEGER REFERENCES [continent]([id]),\n" ' "continent" INTEGER REFERENCES "continent"("id"),\n'
" [city] INTEGER\n" ' "city" INTEGER\n'
")" ")"
) )

View file

@ -70,11 +70,12 @@ def test_update_alter(fresh_db):
] == list(table.rows) ] == list(table.rows)
def test_update_alter_with_invalid_column_characters(fresh_db): def test_update_alter_with_special_column_characters(fresh_db):
# With double-quote escaping, columns with special characters are now valid
table = fresh_db["table"] table = fresh_db["table"]
rowid = table.insert({"foo": "bar"}).last_pk rowid = table.insert({"foo": "bar"}).last_pk
with pytest.raises(AssertionError): table.update(rowid, {"new_col[abc]": 1.2}, alter=True)
table.update(rowid, {"new_col[abc]": 1.2}, alter=True) assert list(table.rows) == [{"foo": "bar", "new_col[abc]": 1.2}]
def test_update_with_no_values_sets_last_pk(fresh_db): def test_update_with_no_values_sets_last_pk(fresh_db):