fix: transform() corrupts expression defaults into string literals

PRAGMA reports expression defaults without their parentheses (DEFAULT
(1+2) comes back as '1+2'), and transform() fed that fragment straight
back into CREATE TABLE, where it was re-quoted as a string literal.
Every table rebuilt by transform() then silently stored the text '1+2'
instead of the value 3 (and blob/concat defaults like (X'ff') or
('x'||'y') were corrupted too). Re-emit PRAGMA-derived defaults by
re-wrapping non-literal fragments in parentheses and pass blob literals
through unquoted.
This commit is contained in:
Harsh Thakkar 2026-09-24 08:13:12 +05:30
commit f7c690d90b
2 changed files with 72 additions and 2 deletions

View file

@ -1074,6 +1074,10 @@ class Database:
): ):
return value return value
if str(value).upper().startswith("X'") and str(value).endswith("'"):
# Blob literal, e.g. X'FF'
return value
if str(value).upper() in ("CURRENT_TIME", "CURRENT_DATE", "CURRENT_TIMESTAMP"): if str(value).upper() in ("CURRENT_TIME", "CURRENT_DATE", "CURRENT_TIMESTAMP"):
return value return value
@ -1083,11 +1087,46 @@ class Database:
return value return value
if str(value).endswith(")"): if str(value).endswith(")"):
# Expr # Expr - already parenthesized values pass through unchanged
if str(value).startswith("("):
return value
return f"({value})" return f"({value})"
return self.quote(value) return self.quote(value)
def transform_default_fragment(self, value: str) -> str:
"""Return a re-emittable DEFAULT fragment for a PRAGMA-returned value.
PRAGMA strips the parentheses from an expression default (``DEFAULT
(1+2)`` is reported as ``1+2``), so expressions must be wrapped again
or SQLite would treat them as string literals. Literals and keywords
are passed through unchanged.
"""
s = str(value)
if s.startswith("'") and s.endswith("'"):
# A single quoted string literal
if "''" not in s[1:-1] and "'" not in s[1:-1]:
return s
if s.upper().startswith("X'") and s.endswith("'"):
# Blob literal, e.g. X'FF'
return s
if s.upper() in (
"TRUE",
"FALSE",
"NULL",
"CURRENT_TIME",
"CURRENT_DATE",
"CURRENT_TIMESTAMP",
):
return s
try:
float(s)
return s
except ValueError:
pass
# Anything else was a parenthesized expression, e.g. "1+2" or "'a'||'b'"
return f"({s})"
def table_names(self, fts4: bool = False, fts5: bool = False) -> list[str]: def table_names(self, fts4: bool = False, fts5: bool = False) -> list[str]:
""" """
List of string table names in this database. List of string table names in this database.
@ -3061,7 +3100,9 @@ class Table(Queryable):
) )
# defaults= # defaults=
create_table_defaults = { create_table_defaults = {
(rename.get(c.name) or c.name): c.default_value (rename.get(c.name) or c.name): self.db.transform_default_fragment(
c.default_value
)
for c in self.columns for c in self.columns
if c.default_value is not None and c.name not in drop if c.default_value is not None and c.name not in drop
} }

View file

@ -288,6 +288,35 @@ def test_transform_preserves_keyword_literal_defaults(fresh_db):
assert after == (1, 0, None) assert after == (1, 0, None)
def test_transform_preserves_expression_defaults(fresh_db):
# PRAGMA strips the parentheses from expression defaults, so transform()
# used to re-emit them as string literals: DEFAULT (1+2) became DEFAULT
# '1+2' and every new row silently got the text '1+2' instead of 3.
fresh_db.execute(
"CREATE TABLE t ("
" a TEXT,"
" b INTEGER DEFAULT (1+2),"
" c BLOB DEFAULT (X'ff'),"
" d TEXT DEFAULT ('x' || 'y')"
")"
)
table = fresh_db.table("t")
table.insert({"a": "first"})
# Rebuild via an unrelated change.
table.transform(rename={"a": "aa"})
schema = table.schema
assert "DEFAULT (1+2)" in schema
assert "DEFAULT X'ff'" in schema
assert "DEFAULT ('x' || 'y')" in schema
table.insert({"aa": "second"})
assert fresh_db.execute(
"SELECT b, c, d FROM t WHERE aa = 'second'"
).fetchone() == (3, b"\xff", "xy")
def test_transform_not_null(fresh_db): def test_transform_not_null(fresh_db):
dogs = fresh_db.table("dogs") dogs = fresh_db.table("dogs")
dogs.insert({"id": 1, "name": "Cleo", "age": "5"}, pk="id") dogs.insert({"id": 1, "name": "Cleo", "age": "5"}, pk="id")