From 1a8a309fe3fce4c25de735fa861cf18e8bf17c39 Mon Sep 17 00:00:00 2001 From: russell romney Date: Sat, 23 May 2026 02:25:24 -0400 Subject: [PATCH 1/5] Fix SQLite output for ignore and offset limits --- sqlglot/generators/sqlite.py | 36 +++++++++++++++++++++++++++++++++++ tests/dialects/test_sqlite.py | 31 ++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/sqlglot/generators/sqlite.py b/sqlglot/generators/sqlite.py index b53f9369fe..2b741cebc7 100644 --- a/sqlglot/generators/sqlite.py +++ b/sqlglot/generators/sqlite.py @@ -73,6 +73,33 @@ def _generated_to_auto_increment(expression: exp.Expr) -> exp.Expr: return expression +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 + + +def _offset_to_limit(expression: exp.Expr) -> exp.Expr: + offset = expression.args.get("offset") + + if offset and not expression.args.get("limit"): + expression.set( + "limit", + exp.Limit( + expression=exp.Literal.number(-1), + comments=offset.comments, + ), + ) + + return expression + + class SQLiteGenerator(generator.Generator): SELECT_KINDS: tuple[str, ...] = () TRY_SUPPORTED = False @@ -152,6 +179,8 @@ class SQLiteGenerator(generator.Generator): exp.Rand: rename_func("RANDOM"), exp.Select: transforms.preprocess( [ + _limit_all_to_no_limit, + _offset_to_limit, transforms.eliminate_distinct_on, transforms.eliminate_qualify, transforms.eliminate_semi_and_anti_joins, @@ -182,6 +211,13 @@ class SQLiteGenerator(generator.Generator): LIMIT_FETCH = "LIMIT" + def insert_sql(self, expression: exp.Insert) -> str: + if expression.args.get("ignore"): + expression.set("ignore", False) + expression.set("alternative", "IGNORE") + + return super().insert_sql(expression) + def bitwiseandagg_sql(self, expression: exp.BitwiseAndAgg) -> str: self.unsupported("BITWISE_AND aggregation is not supported in SQLite") return self.function_fallback_sql(expression) diff --git a/tests/dialects/test_sqlite.py b/tests/dialects/test_sqlite.py index 3c090247cb..baeab7adda 100644 --- a/tests/dialects/test_sqlite.py +++ b/tests/dialects/test_sqlite.py @@ -97,6 +97,37 @@ def test_sqlite(self): "postgres": "SELECT JSON_OBJECT_AGG(name, value) FROM t", }, ) + self.validate_all( + "INSERT OR IGNORE INTO foo (x, y) VALUES (1, 2)", + read={ + "mysql": "INSERT IGNORE INTO foo (x, y) VALUES (1, 2)", + "sqlite": "INSERT OR IGNORE INTO foo (x, y) VALUES (1, 2)", + }, + ) + self.validate_all( + "SELECT x FROM y", + read={ + "postgres": "SELECT x FROM y LIMIT ALL", + }, + write={ + "postgres": "SELECT x FROM y", + }, + ) + self.validate_all( + "SELECT x FROM y LIMIT -1 OFFSET 10", + read={ + "postgres": "SELECT x FROM y OFFSET 10", + }, + write={ + "postgres": "SELECT x FROM y LIMIT -1 OFFSET 10", + }, + ) + self.validate_all( + "SELECT x FROM y LIMIT -1 OFFSET 10", + read={ + "postgres": "SELECT x FROM y LIMIT ALL OFFSET 10", + }, + ) self.validate_all( "CURRENT_DATE", read={ From e9fbe5bac13497c367bbef48d3193769ea4045f3 Mon Sep 17 00:00:00 2001 From: russell romney Date: Mon, 25 May 2026 15:50:42 -0400 Subject: [PATCH 2/5] Address SQLite limit review feedback --- sqlglot/generators/sqlite.py | 24 ++++-------------------- sqlglot/parser.py | 3 +++ tests/dialects/test_sqlite.py | 6 ++++++ 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/sqlglot/generators/sqlite.py b/sqlglot/generators/sqlite.py index 2b741cebc7..a0a4cb1667 100644 --- a/sqlglot/generators/sqlite.py +++ b/sqlglot/generators/sqlite.py @@ -73,29 +73,14 @@ def _generated_to_auto_increment(expression: exp.Expr) -> exp.Expr: return expression -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 - - def _offset_to_limit(expression: exp.Expr) -> exp.Expr: + if not isinstance(expression, exp.Select): + return expression + offset = expression.args.get("offset") if offset and not expression.args.get("limit"): - expression.set( - "limit", - exp.Limit( - expression=exp.Literal.number(-1), - comments=offset.comments, - ), - ) + expression.limit(-1, copy=False) return expression @@ -179,7 +164,6 @@ class SQLiteGenerator(generator.Generator): exp.Rand: rename_func("RANDOM"), exp.Select: transforms.preprocess( [ - _limit_all_to_no_limit, _offset_to_limit, transforms.eliminate_distinct_on, transforms.eliminate_qualify, diff --git a/sqlglot/parser.py b/sqlglot/parser.py index 84d84423ae..e07f6c793c 100644 --- a/sqlglot/parser.py +++ b/sqlglot/parser.py @@ -5488,6 +5488,9 @@ def _parse_limit( self._match_r_paren() else: + if self._match(TokenType.ALL): + return this + # Parsing LIMIT x% (i.e x PERCENT) as a term leads to an error, since # we try to build an exp.Mod expr. For that matter, we backtrack and instead # consume the factor plus parse the percentage separately diff --git a/tests/dialects/test_sqlite.py b/tests/dialects/test_sqlite.py index baeab7adda..944a3c4d3d 100644 --- a/tests/dialects/test_sqlite.py +++ b/tests/dialects/test_sqlite.py @@ -128,6 +128,12 @@ def test_sqlite(self): "postgres": "SELECT x FROM y LIMIT ALL OFFSET 10", }, ) + self.validate_all( + 'SELECT x FROM y LIMIT "all"', + read={ + "postgres": 'SELECT x FROM y LIMIT "all"', + }, + ) self.validate_all( "CURRENT_DATE", read={ From 912265052187ad16836d8f13118243df8abd783c Mon Sep 17 00:00:00 2001 From: russell romney Date: Mon, 25 May 2026 16:36:42 -0400 Subject: [PATCH 3/5] Scope LIMIT ALL parsing to supporting dialects --- sqlglot/dialects/dialect.py | 5 +++++ sqlglot/dialects/postgres.py | 1 + sqlglot/parser.py | 2 +- tests/dialects/test_dialect.py | 11 +++++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/sqlglot/dialects/dialect.py b/sqlglot/dialects/dialect.py index 1522414e28..3ab40d673c 100644 --- a/sqlglot/dialects/dialect.py +++ b/sqlglot/dialects/dialect.py @@ -525,6 +525,11 @@ class Dialect(metaclass=_Dialect): Whether ORDER BY ALL is supported (expands to all the selected columns) as in DuckDB, Spark3/Databricks """ + SUPPORTS_LIMIT_ALL = False + """ + Whether LIMIT ALL is supported (equivalent to no limit) as in Postgres. + """ + PROJECTION_ALIASES_SHADOW_SOURCE_NAMES = False """ Whether projection alias names can shadow table/source names in GROUP BY and HAVING clauses. diff --git a/sqlglot/dialects/postgres.py b/sqlglot/dialects/postgres.py index 111f6e393f..c79d256972 100644 --- a/sqlglot/dialects/postgres.py +++ b/sqlglot/dialects/postgres.py @@ -15,6 +15,7 @@ class Postgres(Dialect): CONCAT_COALESCE = True CONCAT_WS_COALESCE = True NULL_ORDERING = "nulls_are_large" + SUPPORTS_LIMIT_ALL = True TIME_FORMAT = "'YYYY-MM-DD HH24:MI:SS'" TABLESAMPLE_SIZE_IS_PERCENT = True TABLES_REFERENCEABLE_AS_COLUMNS = True diff --git a/sqlglot/parser.py b/sqlglot/parser.py index e07f6c793c..84fd50aa04 100644 --- a/sqlglot/parser.py +++ b/sqlglot/parser.py @@ -5488,7 +5488,7 @@ def _parse_limit( self._match_r_paren() else: - if self._match(TokenType.ALL): + if self.dialect.SUPPORTS_LIMIT_ALL and self._match(TokenType.ALL): return this # Parsing LIMIT x% (i.e x PERCENT) as a term leads to an error, since diff --git a/tests/dialects/test_dialect.py b/tests/dialects/test_dialect.py index b4d63a4775..7cccfafb89 100644 --- a/tests/dialects/test_dialect.py +++ b/tests/dialects/test_dialect.py @@ -2747,6 +2747,17 @@ def test_safediv(self): ) def test_limit(self): + limit_all_identifier = "WITH t AS (SELECT 1 AS all) SELECT 1 FROM t LIMIT all" + self.assertEqual(parse_one(limit_all_identifier).sql(), limit_all_identifier) + self.assertEqual( + parse_one(limit_all_identifier, read="postgres").sql("postgres"), + "WITH t AS (SELECT 1 AS all) SELECT 1 FROM t", + ) + self.assertEqual( + parse_one(limit_all_identifier, read="mysql").sql("mysql"), + "WITH t AS (SELECT 1 AS `all`) SELECT 1 FROM t LIMIT `all`", + ) + self.validate_all( "SELECT * FROM data LIMIT 10, 20", write={"sqlite": "SELECT * FROM data LIMIT 20 OFFSET 10"}, From e6bcedb28ab30a050e524cc1a929d890290a0d94 Mon Sep 17 00:00:00 2001 From: russell romney Date: Wed, 27 May 2026 05:33:02 -0400 Subject: [PATCH 4/5] Address LIMIT ALL review feedback --- sqlglot/dialects/duckdb.py | 1 + sqlglot/dialects/presto.py | 1 + sqlglot/dialects/spark.py | 1 + tests/dialects/test_dialect.py | 13 +++++++++---- tests/dialects/test_sqlite.py | 30 ------------------------------ 5 files changed, 12 insertions(+), 34 deletions(-) diff --git a/sqlglot/dialects/duckdb.py b/sqlglot/dialects/duckdb.py index 2d777fa512..bc4ed9bbec 100644 --- a/sqlglot/dialects/duckdb.py +++ b/sqlglot/dialects/duckdb.py @@ -21,6 +21,7 @@ class DuckDB(Dialect): CONCAT_COALESCE = True CONCAT_WS_COALESCE = True SUPPORTS_ORDER_BY_ALL = True + SUPPORTS_LIMIT_ALL = True SUPPORTS_FIXED_SIZE_ARRAYS = True STRICT_JSON_PATH_SYNTAX = False NUMBERS_CAN_BE_UNDERSCORE_SEPARATED = True diff --git a/sqlglot/dialects/presto.py b/sqlglot/dialects/presto.py index f086f3079a..2fea6e7585 100644 --- a/sqlglot/dialects/presto.py +++ b/sqlglot/dialects/presto.py @@ -22,6 +22,7 @@ class Presto(Dialect): TYPED_DIVISION = True TABLESAMPLE_SIZE_IS_PERCENT = True LOG_BASE_FIRST: bool | None = None + SUPPORTS_LIMIT_ALL = True SUPPORTS_VALUES_DEFAULT = False LEAST_GREATEST_IGNORES_NULLS = False UUID_IS_STRING_TYPE = False diff --git a/sqlglot/dialects/spark.py b/sqlglot/dialects/spark.py index 2ff76aa9a1..0cc9ef7d07 100644 --- a/sqlglot/dialects/spark.py +++ b/sqlglot/dialects/spark.py @@ -11,6 +11,7 @@ class Spark(Spark2): SUPPORTS_ORDER_BY_ALL = True + SUPPORTS_LIMIT_ALL = True SUPPORTS_NULL_TYPE = True ARRAY_FUNCS_PROPAGATES_NULLS = True EXPRESSION_METADATA = EXPRESSION_METADATA.copy() diff --git a/tests/dialects/test_dialect.py b/tests/dialects/test_dialect.py index 7cccfafb89..1c45d30ad3 100644 --- a/tests/dialects/test_dialect.py +++ b/tests/dialects/test_dialect.py @@ -2753,10 +2753,15 @@ def test_limit(self): parse_one(limit_all_identifier, read="postgres").sql("postgres"), "WITH t AS (SELECT 1 AS all) SELECT 1 FROM t", ) - self.assertEqual( - parse_one(limit_all_identifier, read="mysql").sql("mysql"), - "WITH t AS (SELECT 1 AS `all`) SELECT 1 FROM t LIMIT `all`", - ) + for dialect, expected in ( + ("duckdb", 'WITH t AS (SELECT 1 AS "all") SELECT 1 FROM t'), + ("presto", "WITH t AS (SELECT 1 AS all) SELECT 1 FROM t"), + ("spark", "WITH t AS (SELECT 1 AS all) SELECT 1 FROM t"), + ): + self.assertEqual( + parse_one(limit_all_identifier, read=dialect).sql(dialect), + expected, + ) self.validate_all( "SELECT * FROM data LIMIT 10, 20", diff --git a/tests/dialects/test_sqlite.py b/tests/dialects/test_sqlite.py index 944a3c4d3d..b0c7ebf6bf 100644 --- a/tests/dialects/test_sqlite.py +++ b/tests/dialects/test_sqlite.py @@ -104,36 +104,6 @@ def test_sqlite(self): "sqlite": "INSERT OR IGNORE INTO foo (x, y) VALUES (1, 2)", }, ) - self.validate_all( - "SELECT x FROM y", - read={ - "postgres": "SELECT x FROM y LIMIT ALL", - }, - write={ - "postgres": "SELECT x FROM y", - }, - ) - self.validate_all( - "SELECT x FROM y LIMIT -1 OFFSET 10", - read={ - "postgres": "SELECT x FROM y OFFSET 10", - }, - write={ - "postgres": "SELECT x FROM y LIMIT -1 OFFSET 10", - }, - ) - self.validate_all( - "SELECT x FROM y LIMIT -1 OFFSET 10", - read={ - "postgres": "SELECT x FROM y LIMIT ALL OFFSET 10", - }, - ) - self.validate_all( - 'SELECT x FROM y LIMIT "all"', - read={ - "postgres": 'SELECT x FROM y LIMIT "all"', - }, - ) self.validate_all( "CURRENT_DATE", read={ From ad4018cc121ca57aa259f1b1f3f3f0501f4da737 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 27 May 2026 16:10:10 +0300 Subject: [PATCH 5/5] Update tests/dialects/test_dialect.py --- tests/dialects/test_dialect.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/tests/dialects/test_dialect.py b/tests/dialects/test_dialect.py index 1c45d30ad3..6a0abcae06 100644 --- a/tests/dialects/test_dialect.py +++ b/tests/dialects/test_dialect.py @@ -2747,21 +2747,17 @@ def test_safediv(self): ) def test_limit(self): - limit_all_identifier = "WITH t AS (SELECT 1 AS all) SELECT 1 FROM t LIMIT all" - self.assertEqual(parse_one(limit_all_identifier).sql(), limit_all_identifier) - self.assertEqual( - parse_one(limit_all_identifier, read="postgres").sql("postgres"), + self.validate_identity("WITH t AS (SELECT 1 AS all) SELECT 1 FROM t LIMIT all") + self.validate_all( "WITH t AS (SELECT 1 AS all) SELECT 1 FROM t", + read={ + "databricks": "WITH t AS (SELECT 1 AS all) SELECT 1 FROM t LIMIT all", + "duckdb": "WITH t AS (SELECT 1 AS all) SELECT 1 FROM t LIMIT all", + "presto": "WITH t AS (SELECT 1 AS all) SELECT 1 FROM t LIMIT all", + "postgres": "WITH t AS (SELECT 1 AS all) SELECT 1 FROM t LIMIT all", + "spark": "WITH t AS (SELECT 1 AS all) SELECT 1 FROM t LIMIT all", + }, ) - for dialect, expected in ( - ("duckdb", 'WITH t AS (SELECT 1 AS "all") SELECT 1 FROM t'), - ("presto", "WITH t AS (SELECT 1 AS all) SELECT 1 FROM t"), - ("spark", "WITH t AS (SELECT 1 AS all) SELECT 1 FROM t"), - ): - self.assertEqual( - parse_one(limit_all_identifier, read=dialect).sql(dialect), - expected, - ) self.validate_all( "SELECT * FROM data LIMIT 10, 20",