mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-07-23 01:14:31 +02:00
insert/upsert --csv no longer rewrites column types of existing tables
Type detection is the 4.0 default for CSV/TSV data, and the detected-type
transform ran even when the target table already existed - inserting a
CSV into a table with a TEXT zip column converted the column to INTEGER,
corrupting values with leading zeros ('01234' became 1234) with no
warning. Detected types now only apply to tables the command created.
Refs https://github.com/simonw/sqlite-utils/issues/769#issuecomment-4900034150
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1ed95e4ad2
commit
884574685f
4 changed files with 62 additions and 1 deletions
|
|
@ -19,6 +19,7 @@ Unreleased
|
|||
- Fixed a bug where a failed write statement executed with ``db.execute()`` left the driver's implicit transaction open. Every subsequent write then joined that phantom transaction, which nothing committed, so their work was silently rolled back when the connection was closed. The implicit transaction opened by a failed statement is now rolled back before the exception is raised. A failed write inside a transaction opened with ``db.begin()`` or ``db.atomic()`` leaves that transaction open and untouched, as before.
|
||||
- Fixed a bug where transaction-control statements prefixed with an empty statement - ``db.query("; COMMIT")`` - or a UTF-8 byte order mark slipped past the check that rejects them, committing the caller's open transaction before raising a confusing ``OperationalError``. The keyword scanner used by ``db.query()`` and ``db.execute()`` now skips leading ``;`` and byte order marks, matching what the ``sqlite3`` driver tolerates before the first token, so these statements are rejected with a ``ValueError`` without being executed. The same fix means ``db.execute("; BEGIN")`` no longer auto-commits the transaction it just opened.
|
||||
- Documented a limitation of ``db.query()``: a ``PRAGMA`` statement that returns no rows raises a ``ValueError`` but still takes effect, because PRAGMA statements run outside the savepoint guard used to roll back other rejected statements. Use ``db.execute()`` for row-less PRAGMA statements.
|
||||
- Fixed a bug where inserting CSV or TSV data into an existing table rewrote that table's column types to match the incoming file. Type detection is the default in 4.0, so ``sqlite-utils insert data.db places places.csv --csv`` against a table with a ``TEXT`` zip code column would convert the column to ``INTEGER`` and corrupt values with leading zeros - ``"01234"`` became ``1234``. Detected types are now only applied when the ``insert`` or ``upsert`` command creates the table.
|
||||
- Fixed ``pks_and_rows_where()`` raising ``AttributeError`` when called on a view, and no longer double-quotes the synthesized ``rowid`` column in its generated SQL - SQLite turns a double-quoted identifier that does not resolve into a string literal, which on a view produced a confusing ``KeyError`` instead of the ``OperationalError`` raised in 3.x. Compound primary keys returned by this method now follow ``PRIMARY KEY`` declaration order.
|
||||
- The ``foreign_keys=`` argument to ``create()`` and ``insert()`` accepts a mixed list of ``ForeignKey`` objects, tuples and column name strings again. In 4.0 pre-releases mixing ``ForeignKey`` objects with tuples raised a ``ValueError`` - a regression from 3.x, where ``ForeignKey`` was a ``namedtuple`` and passed the tuple checks.
|
||||
- ``ForeignKey`` objects are hashable again. The 4.0 change from ``namedtuple`` to dataclass accidentally made them unhashable, breaking patterns like ``set(table.foreign_keys)`` that worked in 3.x. ``ForeignKey`` is now a frozen dataclass - immutable and hashable, like the namedtuple was.
|
||||
|
|
|
|||
|
|
@ -1326,6 +1326,8 @@ A progress bar is displayed when inserting data from a file. You can hide the pr
|
|||
|
||||
By default, column types are automatically detected for CSV or TSV files - resulting in a mix of ``TEXT``, ``INTEGER`` and ``REAL`` columns. To disable type detection and treat all columns as ``TEXT``, use the ``--no-detect-types`` option.
|
||||
|
||||
Detected types are only applied when the table is created by the command. Inserting CSV or TSV data into a table that already exists leaves the existing column types unchanged - values are inserted using the table's existing schema.
|
||||
|
||||
For example, given a ``creatures.csv`` file containing this:
|
||||
|
||||
.. code-block::
|
||||
|
|
|
|||
|
|
@ -1165,6 +1165,9 @@ def insert_upsert_implementation(
|
|||
db.conn.cursor().executemany(bulk_sql, doc_chunk)
|
||||
return
|
||||
|
||||
# table_names() rather than db.table(), which raises NoTable for
|
||||
# views before the error handling below can deal with them
|
||||
table_existed_before_insert = table in db.table_names()
|
||||
try:
|
||||
db.table(table).insert_all(
|
||||
docs, pk=pk, batch_size=batch_size, alter=alter, **extra_kwargs
|
||||
|
|
@ -1194,7 +1197,14 @@ def insert_upsert_implementation(
|
|||
)
|
||||
else:
|
||||
raise
|
||||
if tracker is not None and db.table(table).exists():
|
||||
# Apply detected types only to a table this command created -
|
||||
# transforming a pre-existing table would rewrite its column types
|
||||
# and corrupt values such as TEXT zip codes with leading zeros
|
||||
if (
|
||||
tracker is not None
|
||||
and not table_existed_before_insert
|
||||
and db.table(table).exists()
|
||||
):
|
||||
db.table(table).transform(types=tracker.types)
|
||||
|
||||
# Clean up open file-like objects
|
||||
|
|
|
|||
|
|
@ -628,3 +628,51 @@ def test_insert_into_view_errors(tmpdir):
|
|||
)
|
||||
assert result.exit_code == 1
|
||||
assert result.output.strip() == "Error: Table v is actually a view"
|
||||
|
||||
|
||||
def test_insert_csv_detect_types_leaves_existing_table_alone(db_path):
|
||||
# Type detection is the default for CSV/TSV inserts, but it must only
|
||||
# apply to tables created by this command - transforming a pre-existing
|
||||
# table would rewrite its column types and corrupt data such as
|
||||
# TEXT zip codes with leading zeros
|
||||
db = Database(db_path)
|
||||
db["places"].insert({"name": "Boston", "zip": "01234"})
|
||||
result = CliRunner().invoke(
|
||||
cli.cli,
|
||||
["insert", db_path, "places", "-", "--csv"],
|
||||
catch_exceptions=False,
|
||||
input="name,zip\nSF,94107",
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert db["places"].columns_dict["zip"] is str
|
||||
assert list(db["places"].rows) == [
|
||||
{"name": "Boston", "zip": "01234"},
|
||||
{"name": "SF", "zip": "94107"},
|
||||
]
|
||||
|
||||
|
||||
def test_insert_csv_detect_types_new_table(db_path):
|
||||
# A table created by the insert still gets detected types
|
||||
result = CliRunner().invoke(
|
||||
cli.cli,
|
||||
["insert", db_path, "data", "-", "--csv"],
|
||||
catch_exceptions=False,
|
||||
input="name,age,weight\nCleo,5,12.5",
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
db = Database(db_path)
|
||||
assert db["data"].columns_dict == {"name": str, "age": int, "weight": float}
|
||||
|
||||
|
||||
def test_upsert_csv_detect_types_leaves_existing_table_alone(db_path):
|
||||
db = Database(db_path)
|
||||
db["places"].insert({"id": 1, "name": "Boston", "zip": "01234"}, pk="id")
|
||||
result = CliRunner().invoke(
|
||||
cli.cli,
|
||||
["upsert", db_path, "places", "-", "--csv", "--pk", "id"],
|
||||
catch_exceptions=False,
|
||||
input="id,name,zip\n2,SF,94107",
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert db["places"].columns_dict["zip"] is str
|
||||
assert db["places"].get(1)["zip"] == "01234"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue