Emit LIMIT -1 when offset is used without limit (#821)

* Emit LIMIT -1 when offset is used without limit, closes #816

SQLite requires a LIMIT clause to appear before OFFSET, so passing offset
without limit generated invalid SQL such as:

    select * from "t" offset 2

which raised OperationalError: near "2": syntax error.

A negative limit means "no upper bound" in SQLite, so "limit -1 offset N"
returns all rows from position N onwards.

Fixed in three places that build LIMIT/OFFSET SQL:

- Queryable.rows_where() - also covers pks_and_rows_where()
- Table.search_sql() - also covers search()
- the "sqlite-utils rows" CLI command

* Remove duplicate comments

---------

Co-authored-by: ethanhawkes-gif <259455325+ethanhawkes-gif@users.noreply.github.com>
This commit is contained in:
ethanhawkes-gif 2026-08-12 01:52:43 -04:00 committed by GitHub
commit 43d5d3331f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 33 additions and 0 deletions

View file

@ -2478,6 +2478,8 @@ def rows(
if limit:
sql += f" limit {limit}"
if offset:
if not limit:
sql += " limit -1"
sql += f" offset {offset}"
ctx.invoke(
query,

View file

@ -2064,6 +2064,10 @@ class Queryable:
if limit is not None:
sql += f" limit {limit}"
if offset is not None:
# SQLite requires a limit clause before offset - a negative limit
# means "no upper bound", so offset works without an explicit limit
if limit is None:
sql += " limit -1"
sql += f" offset {offset}"
cursor = self.db.execute(sql, where_args or [])
columns = dedupe_keys(c[0] for c in cursor.description)
@ -3732,6 +3736,8 @@ class Table(Queryable):
if limit is not None:
limit_offset += f" limit {limit}"
if offset is not None:
if limit is None:
limit_offset += " limit -1"
limit_offset += f" offset {offset}"
return sql.format(
dbtable=quote_identifier(self.name),