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.
This commit is contained in:
Vincent Gao 2026-07-01 01:03:46 +02:00
commit 5aa61c2f7f
No known key found for this signature in database
GPG key ID: 4EB74F921300AF3B
3 changed files with 44 additions and 0 deletions

View file

@ -710,6 +710,11 @@ class Database:
if str(value).upper() in ("CURRENT_TIME", "CURRENT_DATE", "CURRENT_TIMESTAMP"):
return value
if str(value).upper() in ("TRUE", "FALSE", "NULL"):
# Keyword literals must stay unquoted; quoting them would turn the
# default into a string ('TRUE' instead of 1, 'NULL' instead of null).
return value
if str(value).endswith(")"):
# Expr
return "({})".format(value)

View file

@ -21,6 +21,11 @@ EXAMPLES = [
# Strings
("TEXT DEFAULT 'CURRENT_TIMESTAMP'", "'CURRENT_TIMESTAMP'", "'CURRENT_TIMESTAMP'"),
('TEXT DEFAULT "CURRENT_TIMESTAMP"', '"CURRENT_TIMESTAMP"', '"CURRENT_TIMESTAMP"'),
# Boolean and null keyword literals must stay unquoted
("INTEGER DEFAULT TRUE", "TRUE", "TRUE"),
("INTEGER DEFAULT FALSE", "FALSE", "FALSE"),
("INTEGER DEFAULT true", "true", "true"),
("TEXT DEFAULT NULL", "NULL", "NULL"),
]

View file

@ -224,6 +224,40 @@ def test_transform_rename_pk(fresh_db):
)
def test_transform_preserves_keyword_literal_defaults(fresh_db):
# transform() used to requote keyword-literal defaults (DEFAULT TRUE became
# DEFAULT 'TRUE'), so a default insert stored the text 'TRUE' instead of the
# integer 1 -- silent value corruption on every rebuilt table.
fresh_db.execute(
"CREATE TABLE t ("
" id INTEGER PRIMARY KEY,"
" is_active INTEGER DEFAULT TRUE,"
" flag INTEGER DEFAULT FALSE,"
" note TEXT DEFAULT NULL"
")"
)
table = fresh_db["t"]
table.insert({"id": 1})
before = fresh_db.execute("SELECT is_active, flag, note FROM t").fetchone()
assert before == (1, 0, None)
# Rebuild the table via an unrelated change.
table.transform(rename={"note": "note2"})
# The keyword literals stay unquoted in the schema ...
assert "DEFAULT TRUE" in table.schema
assert "DEFAULT FALSE" in table.schema
assert "DEFAULT NULL" in table.schema
assert "'TRUE'" not in table.schema
# ... and a fresh default insert still yields 1 / 0 / NULL, not strings.
table.insert({"id": 2})
after = fresh_db.execute(
"SELECT is_active, flag, note2 FROM t WHERE id = 2"
).fetchone()
assert after == (1, 0, None)
def test_transform_not_null(fresh_db):
dogs = fresh_db["dogs"]
dogs.insert({"id": 1, "name": "Cleo", "age": "5"}, pk="id")