Skip to content

Fix transform() corrupting TRUE/FALSE/NULL column defaults into strings#764

Open
gaoflow wants to merge 1 commit into
simonw:mainfrom
gaoflow:fix-transform-keyword-literal-defaults
Open

Fix transform() corrupting TRUE/FALSE/NULL column defaults into strings#764
gaoflow wants to merge 1 commit into
simonw:mainfrom
gaoflow:fix-transform-keyword-literal-defaults

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 30, 2026

Copy link
Copy Markdown

The bug

table.transform() corrupts TRUE / FALSE / NULL keyword-literal column defaults into string literals, silently changing the value future rows receive.

import sqlite_utils
db = sqlite_utils.Database(memory=True)
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, is_active INTEGER DEFAULT TRUE)")
db["t"].insert({"id": 1})
db["t"].transform(rename={"id": "pk"})          # any structure-only rebuild
print(db["t"].schema)                            # ... "is_active" INTEGER DEFAULT 'TRUE'
db["t"].insert({})
print(db.execute("SELECT is_active FROM t").fetchall())   # [(1,), ('TRUE',)]  <-- corrupted

Before the transform a default insert stored the integer 1; afterwards it stores the text 'TRUE'. Same for FALSE (→ 'FALSE' instead of 0) and NULL (→ 'NULL' instead of null). On an INTEGER column the quoted 'TRUE' isn't numeric, so it's stored as TEXT — a genuine type and value change.

Cause

transform() rebuilds the table by re-emitting each column's PRAGMA table_info default through quote_default_value(). That method returns the value verbatim when it is already quoted, is a CURRENT_TIME/DATE/TIMESTAMP literal, or ends with ); otherwise it calls self.quote(). The keyword literals TRUE, FALSE and NULL match none of those guards, so they get string-quoted.

The fix

Return TRUE / FALSE / NULL unquoted, mirroring the existing CURRENT_* handling. A populated/numeric/expression default is unaffected.

Tests

Added TRUE / FALSE / true / NULL cases to the quote_default_value EXAMPLES table, and a functional test_transform_preserves_keyword_literal_defaults that creates DEFAULT TRUE/FALSE/NULL, rebuilds via transform(), and asserts the schema stays unquoted and a fresh default insert still yields 1 / 0 / NULL. All new cases fail on main and pass with the fix; the full test_default_value.py + test_transform.py suites stay green (78 passed); black, flake8 and mypy clean.


📚 Documentation preview 📚: https://sqlite-utils--764.org.readthedocs.build/en/764/

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant