Skip to content

Commit a4acc39

Browse files
authored
Fix transform() corrupting TRUE/FALSE/NULL column defaults into strings
* Fix transform() corrupting TRUE/FALSE/NULL column defaults quote_default_value() passed keyword-literal defaults (TRUE, FALSE, NULL) through self.quote(), wrapping them in quotes. So transform() rebuilt a column declared "INTEGER DEFAULT TRUE" as "INTEGER DEFAULT 'TRUE'", and a later default insert stored the text 'TRUE' instead of the integer 1 (likewise 'FALSE' for 0 and 'NULL' for null) - silent value corruption. Return these keyword literals unquoted, as already done for the CURRENT_TIME/DATE/TIMESTAMP literals. PR #764
1 parent 77d2419 commit a4acc39

4 files changed

Lines changed: 48 additions & 0 deletions

File tree

docs/changelog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ Column names passed to Python API methods are now matched against the table sche
4242
- Foreign key columns are validated and recorded using the casing of the actual schema columns, in ``foreign_keys=`` when creating tables, ``db.add_foreign_keys()``, ``table.add_foreign_key()`` and ``table.add_column(fk_col=...)``. Duplicate foreign key detection is also case-insensitive.
4343
- ``table.create()`` with ``pk=``, ``not_null=``, ``defaults=`` or ``column_order=`` referencing columns using different casing no longer creates an unwanted extra primary key column or raises a ``ValueError``.
4444

45+
Everything else:
46+
47+
- Fixed a bug where ``table.transform()`` could convert ``DEFAULT TRUE``, ``DEFAULT FALSE`` and ``DEFAULT NULL`` column defaults into quoted string defaults when rebuilding a table. Thanks, `Vincent Gao <https://github.com/gaoflow>`__. (`#764 <https://github.com/simonw/sqlite-utils/pull/764>`__)
48+
4549
.. _v4_0rc2:
4650

4751
4.0rc2 (2026-07-04)

sqlite_utils/db.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,11 @@ def quote_default_value(self, value: str) -> str:
983983
if str(value).upper() in ("CURRENT_TIME", "CURRENT_DATE", "CURRENT_TIMESTAMP"):
984984
return value
985985

986+
if isinstance(value, str) and value.upper() in ("TRUE", "FALSE", "NULL"):
987+
# Keyword literals must stay unquoted; quoting them would turn the
988+
# default into a string ('TRUE' instead of 1, 'NULL' instead of null).
989+
return value
990+
986991
if str(value).endswith(")"):
987992
# Expr
988993
return "({})".format(value)

tests/test_default_value.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
# Strings
2222
("TEXT DEFAULT 'CURRENT_TIMESTAMP'", "'CURRENT_TIMESTAMP'", "'CURRENT_TIMESTAMP'"),
2323
('TEXT DEFAULT "CURRENT_TIMESTAMP"', '"CURRENT_TIMESTAMP"', '"CURRENT_TIMESTAMP"'),
24+
# Boolean and null keyword literals must stay unquoted
25+
("INTEGER DEFAULT TRUE", "TRUE", "TRUE"),
26+
("INTEGER DEFAULT FALSE", "FALSE", "FALSE"),
27+
("INTEGER DEFAULT true", "true", "true"),
28+
("TEXT DEFAULT NULL", "NULL", "NULL"),
2429
]
2530

2631

tests/test_transform.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,40 @@ def test_transform_rename_pk(fresh_db):
224224
)
225225

226226

227+
def test_transform_preserves_keyword_literal_defaults(fresh_db):
228+
# transform() used to requote keyword-literal defaults (DEFAULT TRUE became
229+
# DEFAULT 'TRUE'), so a default insert stored the text 'TRUE' instead of the
230+
# integer 1 -- silent value corruption on every rebuilt table.
231+
fresh_db.execute(
232+
"CREATE TABLE t ("
233+
" id INTEGER PRIMARY KEY,"
234+
" is_active INTEGER DEFAULT TRUE,"
235+
" flag INTEGER DEFAULT FALSE,"
236+
" note TEXT DEFAULT NULL"
237+
")"
238+
)
239+
table = fresh_db["t"]
240+
table.insert({"id": 1})
241+
before = fresh_db.execute("SELECT is_active, flag, note FROM t").fetchone()
242+
assert before == (1, 0, None)
243+
244+
# Rebuild the table via an unrelated change.
245+
table.transform(rename={"note": "note2"})
246+
247+
# The keyword literals stay unquoted in the schema ...
248+
assert "DEFAULT TRUE" in table.schema
249+
assert "DEFAULT FALSE" in table.schema
250+
assert "DEFAULT NULL" in table.schema
251+
assert "'TRUE'" not in table.schema
252+
253+
# ... and a fresh default insert still yields 1 / 0 / NULL, not strings.
254+
table.insert({"id": 2})
255+
after = fresh_db.execute(
256+
"SELECT is_active, flag, note2 FROM t WHERE id = 2"
257+
).fetchone()
258+
assert after == (1, 0, None)
259+
260+
227261
def test_transform_not_null(fresh_db):
228262
dogs = fresh_db["dogs"]
229263
dogs.insert({"id": 1, "name": "Cleo", "age": "5"}, pk="id")

0 commit comments

Comments
 (0)