[Bug]: SQLite variable limit silently disables grid preload, cached-thumbnail hydrate, and prune at ~33k+ images #191
Labels
No labels
Kind/Bug
Kind/Feature
Priority/High
Priority/Medium
Reviewed/Confirmed
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
ai-collab/bulk-image-organizer#191
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Affected area
Directory open —
ui/main_window.py::open_directory()preload block (lines 4692-4720),db/working_db.py(get_tags_for_images,prune_images_not_in, and five sibling methods).Summary
Every
WorkingDBmethod that expands one SQL parameter per caller-supplied id/path breaks once the workspace exceeds SQLite'sSQLITE_LIMIT_VARIABLE_NUMBER. On builds where that limit is the SQLite compile-time default of 32766 (which the official CPython-for-Windows amalgamation ships), a workspace above ~33k images silently loses:All three fail to a
logger.warning, with no user-visible error.This is not reproducible in CI or on Debian/Ubuntu dev machines: those ship a patched SQLite with the limit raised to 250,000, so 125k parameters succeed. Verified on this sandbox:
sqlite3.connect(':memory:').getlimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER)→250000.Root cause
open_directory()'s preload block wraps the tag fetch and the hydrate-worker start in onetry:image_dicts_with_tags()callsWorkingDB.get_tags_for_images(ids), which buildsWHERE it.image_id IN (?, ?, ...)with one parameter per image.get_all_images(include_thumbnails=False)selectsid(see_IMAGE_COLUMNS_NO_BLOB), so all 125,000 ids are bound. Above the limit this raisessqlite3.OperationalError: too many SQL variables, and the singleexcepttakes the hydrate worker down with it.Separately,
prune_images_not_in()buildsDELETE FROM images WHERE relative_path NOT IN (?, ?, ...)with one parameter per surviving path, so prune raises too — caught inscan_metadata_ordered_and_upsert()aserrors.append(f"prune failed: {exc}").Same unbounded expansion, triggered whenever their argument set is large:
get_images_by_paths,get_cache_metadata_for_paths,get_images_with_thumbnails_for_paths,get_image_dicts_for_dupe_tags,delete_images_by_paths.Reproduction (mechanism proven against the real code)
Lowering the limit on every connection and running the actual
WorkingDB/TagManagerpath, with 250 images against a simulated limit of 100:Against a real (unpatched-default) limit of 32766,
INwith 32767+ parameters raises; 32766 succeeds.Relationship to #190
#190 (cached-thumbnail hydrate paginating with unindexed
OFFSET) is a genuine, separately-measured O(n²) bug and its fix stands. But on a build whose variable limit is below the image count, the hydrate worker never starts, so #190's fix cannot affect the reported symptom on that platform. This issue is the more direct explanation of "125,000-image workspace shows placeholders with zero thumbnails": the placeholders come from the discovery scan'simages_added, and the cached thumbnails never load because the worker was never launched.Confirmation needed on the reporting platform
One line in a Python console on the affected Windows build:
32766(or999) confirms;250000would rule it out for that machine.Suggested fix direction
conn.getlimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER)(Python 3.11+), falling back to a conservative 999, and chunk every parameter-expanding query to that budget, merging results.prune_images_not_in()around a temp table (NOT INcannot be fixed by splitting the list — each chunk would delete nearly everything): insert the keep-set withexecutemany, then select/delete viaNOT IN (SELECT ... FROM temp). This also allows streaming pruned paths back to the UI in batches.open_directory()preloadtryso a preload failure cannot prevent the cached-thumbnail hydrate worker from starting.Acceptance criteria
WorkingDBmethod binds an unbounded number of parameters.Confirmed on the reporting platform
The reporter ran the check on the affected Windows 11 build:
Result:
32766— SQLite's unpatched compile-time default.That makes the diagnosis definitive rather than inferred. At 125,000 images every one of the affected queries was raising
OperationalError: too many SQL variableson that machine, so the workspace opened with:TagManager.image_dicts_with_tags→get_tags_for_images(125000 ids)),_start_cache_hydrate_worker()shared the failingtry,prune_images_not_inwith 125kNOT INparameters).The breakage threshold is ~32,766 images
This also retroactively explains the A/B comparison in #184: the reporter's ~3,900-image directory is below the limit and behaved normally, while the ~125,000-image directory on the same share did not. Any workspace above ~33k images on a build with the stock limit is affected; below it, nothing is.
Follow-up: dupe-path queries chunked too
Three further call sites scale with duplicate-group count rather than image count and were chunked in the same pass, since a 125k workspace can accumulate more than 32,766 dupe-group tags:
remove_dupe_tags_with_single_member()— highest risk, runs after every scanget_image_dicts_for_dupe_tags()get_dupe_group_representatives()The remaining unbounded expansions are genuinely bounded by column count or by the user's active tag-filter selection, and are left alone.
Bug found in the fix itself
The first cut of
_resolve_sql_variable_limit()clamped withmax(64, min(limit - RESERVED, 20000)). That floor can return a chunk size larger than the reported limit when the limit is small, defeating the reserved headroom for the fixed parameters some queries bind alongside the chunked list (get_dupe_group_representativesbinds theDELETEtag name plus the chunk). Caught by the new dupe test at a simulated limit of 64; corrected tomin(max(1, limit - RESERVED), 20000).Status
Fixed on
0.9.4/issue-179-184-dupe-pip-thumb-perf(dd9032e, plus the dupe-path chunking). 12 regression tests force a small variable limit on every connection so this project's Debian-patched 250,000 cannot mask a regression.