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

@ -59,6 +59,9 @@ def test_rows_where_order_by(where, order_by, expected_ids, fresh_db):
(None, 3, [1, 2, 3]),
(0, 3, [1, 2, 3]),
(3, 3, [4, 5, 6]),
# offset without limit should return every remaining row
(97, None, [98, 99, 100]),
(0, None, list(range(1, 101))),
],
)
def test_rows_where_offset_limit(fresh_db, offset, limit, expected):
@ -70,6 +73,12 @@ def test_rows_where_offset_limit(fresh_db, offset, limit, expected):
]
def test_pks_and_rows_where_offset_without_limit(fresh_db):
table = fresh_db["rows"]
table.insert_all([{"id": id} for id in range(1, 6)], pk="id")
assert [pk for pk, _ in table.pks_and_rows_where(offset=3, order_by="id")] == [4, 5]
def test_pks_and_rows_where_rowid(fresh_db):
table = fresh_db["rowid_table"]
table.insert_all({"number": i + 10} for i in range(3))