fix(sqlite): support INSERT IGNORE and offset-only limits [CODEX]#7674
fix(sqlite): support INSERT IGNORE and offset-only limits [CODEX]#7674russellromney wants to merge 3 commits into
Conversation
georgesittas
left a comment
There was a problem hiding this comment.
Thank you for the PR!
| def _limit_all_to_no_limit(expression: exp.Expr) -> exp.Expr: | ||
| limit = expression.args.get("limit") | ||
|
|
||
| if isinstance(limit, exp.Limit) and limit.expression and limit.expression.name.upper() == "ALL": | ||
| if expression.args.get("offset"): | ||
| limit.set("expression", exp.Literal.number(-1)) | ||
| else: | ||
| expression.set("limit", None) | ||
|
|
||
| return expression |
There was a problem hiding this comment.
Let's not do this. Instead, in the base LIMIT parser, if you encounter ALL right after the LIMIT keyword, do an early return, effectively simulating the "no limit" semantics. You don't have to set limit in that case.
SQLGlot does not guarantee input preservation on roundtrip, as long as the semantics are respected, so dropping the LIMIT ALL is not an issue.
The only thing we need to verify for this to be complete is whether LIMIT all may actually mean "takeall columns from the result", where all is a column. There may be a dialect that doesn't match Postgres' semantics.
| return expression | ||
|
|
||
|
|
||
| def _offset_to_limit(expression: exp.Expr) -> exp.Expr: |
There was a problem hiding this comment.
Nit: let's gate any logic in this transformation with an instance check:
if isinstance(expr, exp.Select):
...
return expr| if offset and not expression.args.get("limit"): | ||
| expression.set( | ||
| "limit", | ||
| exp.Limit( | ||
| expression=exp.Literal.number(-1), | ||
| comments=offset.comments, | ||
| ), | ||
| ) |
There was a problem hiding this comment.
Why do we need to replicate the comments in Limit as well?
Btw, I think we should be able to do expression.limit(-1, copy=False), which is simpler.
| if expression.args.get("ignore"): | ||
| expression.set("ignore", False) | ||
| expression.set("alternative", "IGNORE") | ||
|
|
There was a problem hiding this comment.
Hmm... this is fine for now, but looks like a code smell to me. Having two ways to represent the IGNORE semantics doesn't look right. Perhaps something to clean up in the future...
There was a problem hiding this comment.
good catch here. let me think about a better way to do this
There was a problem hiding this comment.
This is fine for now, let's not deal with it in this PR. Just wanted to leave a note for future reference.
| if self._match(TokenType.ALL): | ||
| return this |
There was a problem hiding this comment.
Did you happen to test/check in other dialects whether LIMIT all has different semantics vs postgres? I'd try queries like this:
WITH t AS (SELECT 1 AS all) SELECT 1 FROM t LIMIT allThere was a problem hiding this comment.
Good catch, I've updated it so LIMIT ALL is dialect-gated:
- default behavior preserves
LIMIT all - MySQL preserves it as
LIMIT \all - Postgres drops it as
“no limit”
Summary
Fix SQLite-targeted transpilation for three cases that currently produce SQL SQLite rejects:
INSERT IGNORE ...-> SQLiteINSERT OR IGNORE ...LIMIT ALL-> no SQLiteLIMITclauseOFFSET n-> SQLiteLIMIT -1 OFFSET nThese are represented directly in SQLite syntax rather than unsupported semantics.
Examples
I also checked the combined case:
Why
SQLite uses
INSERT OR IGNOREfor this conflict behavior, not MySQL'sINSERT IGNORE. SQLite's INSERT docs describe theINSERT OR actionform for choosing a constraint conflict algorithm: https://www.sqlite.org/lang_insert.htmlSQLite also requires
OFFSETto appear as part of aLIMITclause;LIMIT -1 OFFSET nis the common SQLite spelling for offset without an upper row bound. Related prior art/background on emulating MySQL conflict behavior in other dialects: https://stackoverflow.com/questions/1009584/how-to-emulate-insert-ignore-and-on-duplicate-key-update-sql-merge-with-poI searched for existing SQLGlot issues/PRs mentioning these exact SQLite-target gaps and did not find an existing report.
Tests
python3 -m unittest tests.dialects.test_sqlite.TestSQLite.test_sqlitepython3 -m unittest tests.dialects.test_dialect.TestDialect.test_limit tests.dialects.test_mysql.TestMySQL.test_mysqlpython3 -m unittest tests.dialects.test_sqlitepython3 -m ruff check sqlglot/generators/sqlite.py tests/dialects/test_sqlite.pygit diff --check