fix(sql_base): iter_rows detects the end of a table; count_rows works on DB-API - #9
Merged
Merged
Conversation
…-API - iter_rows decided whether a page had rows from the cursor's rowcount, which is -1 (truthy) for a SELECT on sqlite3, so without a limit it requested empty pages until limit (default 1e12) ran out. It now stops on the first page shorter than requested. It also no longer yields more than limit rows when limit spans several pages (the running counter restarted at the last index), and a page never asks for more rows than remain under limit. A batch_size < 1 raises a ValueError naming it. - SqlTableRowsCollection.count_rows used .first(), a SQLAlchemy result method a DB-API cursor lacks; it now uses fetchone(), which both have. Tests: sqldol/tests/test_legacy_raw_sql_rows.py (sqlite3, with a query-counting connection so the old code fails instead of hanging). The workaround note in test_sql_injection.py is dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…count_rows note - iter_rows validates batch_size/offset/limit when called (not on first next()); a negative offset duplicated rows on sqlite and a negative limit silently gave [], both now ValueError. - SqlTableRowsCollection[a:0] returned every row (`if stop:`). - count_rows comment no longer claims SQLAlchemy 2 connections work: the raw-SQL strings here only execute on DB-API connections. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the two pre-existing legacy-path bugs noted in the bodies of #7 and #8.
What was wrong
sql_base.iter_rowsdecided whether a page had rows fromrowcount. For a SELECT on sqlite3 (and other DB-API drivers that don't pre-buffer),rowcountis-1, which is truthy. So without a smalllimitit kept requesting empty pages untillimit(default1e12) ran out.SqlTableRowsCollection.__iter__goes through it, so iterating a collection on a sqlite3 connection never ended.iter_rows: the running counter restarted each page at the previous page's last index. Whenlimitspanned several pages it yielded extra rows. For example,batch_size=2, limit=5on a 7-row table gave 6 rows.SqlTableRowsCollection.count_rowscalled.first(), a SQLAlchemy result method. On a DB-API connection,executereturns a cursor, which has nofirst, solen(collection)raisedAttributeError.Change
iter_rowsstops on the first page shorter than it asked for, and never asks for more rows than remain underlimit. Offset/limit act like a slicerows[offset:offset+limit]. Abatch_size < 1raises aValueErrorthat names it (it was a barerange()error before). Doctests added.count_rowsusesfetchone()[0], which a DB-API cursor has. (The raw-SQL strings in this module only execute on DB-API connections anyway: SQLAlchemy 2 rejects a bare string. That is unchanged and out of scope here.)iter_rowsis called, not on the firstnext(). A negativeoffset(sqlite reads it as 0 and duplicated rows) and a negativelimit(silently[]) now raiseValueError.SqlTableRowsCollection[a:0]returned every row (if stop:); it is now empty.test_sql_injection.pyare dropped, since the unbounded call now terminates.Tests
sqldol/tests/test_legacy_raw_sql_rows.py, on in-memory sqlite3, uses a connection wrapper that counts queries and fails past a cap. That way the old code fails instead of hanging. It covers end detection for several batch sizes (with an exact query count), the empty table, 32 offset/limit/batch combinations checked against list slicing,count_rows/len/iteration/slicing on the collection, andSqlTableRowsSequence. Against master, 12 of the 41 fail. On this branch the full suite with doctests passes: 149 passed (py3.10).Dependents
fleet_dependentslists raglab_app. It imports onlysqldol.storesandsqldol.base, neversql_base, so no call site changes. Its own test suite cannot run here for reasons unrelated to sqldol:test_app.pyimports a moved name, and the other tests call a LangChain method that no longer exists.sqldol.storesimports fine from its environment with this branch installed.Review
An independent refute-review agent found no blockers. It swept old vs new
iter_rowsover batch_size × offset × limit and confirmed that the only differences on valid arguments are the fixed over-yield cases. I applied its should-fix items in the second commit: eager validation, rejecting negative offset/limit, and an accuratecount_rowsnote. I also applied itsrows[:0]nit. Not changed: paging withoutORDER BYcan be inconsistent on non-sqlite backends or on a table that changes mid-iteration. That was already true before this PR.🤖 Generated with Claude Code