Repository navigation
feat(data-index): per-upload data tables, safe re-upload, delete clean-up - #298
Merged
Merged
Conversation
Table names were built from the resource name and dataset title, and Postgres keeps only the first 63 bytes of a name, so two resources with long similar names shared one table and a re-upload of one replaced the other's data. Re-uploads also dropped the old table before the new one existed, so readers saw empty or half-written data and a failed re-upload lost it. Each upload now gets a table named rdt_<row id>. index_resource_data builds the new table in one data-database transaction, then switches the resource over in one main-database commit (resource row locked, stored file re-checked so the newest file wins, column rows rebuilt with errors escaping) and confirms the new row exists; anything short of that drops this build's table and keeps the old data. A post_delete receiver on ResourceDataTable drops the old table after commit, which also frees storage on resource, dataset and admin deletes; it leaves legacy tables two rows still share. update_dataindex no longer wraps each call in a transaction, and the flow refuses to run inside one. Tests run against a real Postgres when TEST_DATA_DB_URL is set; CI adds a postgres service and fails instead of skipping them. Plan: plans/claude/data-index-table-names.md (Phase 1)
Existing tables still carry title-based names, and two resources can share one (Postgres cut both names to the same 63 bytes), so one of them serves the other's data. This one-off command moves them to rdt_<row id> names. Short old names can't have collided and are renamed in place, holding the resource row and waiting at most 2 s for readers. Long ones are rebuilt from each resource's own file through the normal upload flow, oldest first and with no read-time limit; the shared table is dropped once its last row has moved. A long table whose file is missing or can no longer become a table is renamed and marked "unverified" in its comment if no other row uses it, and every later report lists it until the resource is re-uploaded. A rerun after a crash finishes half-done renames. It is a dry run by default. The dry run checks each stored file, so its report shows what --apply will do, which tables are shared and which rows need a person. Tables no row uses are listed with size and comment. They are dropped only with --apply --drop-orphans --from-report <reviewed report>, and only if still unused at that moment. The command refuses to run when the data database looks like the main one. Every row's outcome is logged as it happens, and an in-progress report is written before any change. The Layer 5 journey (re-upload while polling, racing re-uploads, delete) is added for on-demand runs. Plan: plans/claude/data-index-table-names.md (Phase 2)
Covers per-upload table naming, the build -> switch -> drop-after-commit flow, the delete receiver and its shared-table guard, the readers, the migrate_data_table_names command with its runbook and old naming schemes, the Postgres test setup, the failure model, and known limitations. SDK: no. The data API doc links to it. Plan: plans/claude/data-index-table-names.md (Phase 4)
Final QC found that stopping a command (Ctrl+C) while the old table's after-commit drop waited for a reader made the flow's clean-up drop the new table, though the resource already pointed at it. The clean-up now asks the database first: it drops the build's table only if its row isn't saved, and leaves the table for the orphan audit when it can't tell. That also covers a commit that went through but reported an error. The runbook now says a row that fails twice the same way needs a person. Plan: plans/claude/data-index-table-names.md (final QC)
Two headers that differ only after Postgres's 63-byte cut become one column, so the file never gets a table. This bug predates per-upload tables and is noted, not fixed, by user decision.
A re-upload whose rows can't be loaded still reports success, so the download serves the new file while previews, charts and the data API serve the old one. Marked pending a product decision: enforce the database's limits at upload and flag problem columns in the column-description UI.
ubuntu-latest is now Ubuntu 26.04, where setup-python has no Python 3.9 build. The 3.9 job failed at setup and fail-fast cancelled 3.10 and 3.11 (seen on #295). The SDK still declares 3.9 support, so pin the runner instead of dropping it.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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.
Pull Request
Description
Every tabular file is copied into a table in
data_db, which previews, charts and the data API read. Three problems came from how that was done:What changes:
rdt_<32 hex>, 36 characters. Names can't collide and don't change when something is renamed.migrate_data_table_names, moves existing tables:api/utils/data_indexing_architecture.md.No schema change and no migration file: only the generated table name changes. Old and new names work side by side, and rolling back the code is safe.
Type of Change
Changes Made
api/utils/data_indexing.py:index_resource_datanow builds the table, switches in one commit (resource row locked, stored file re-checked), confirms the switch and cleans up. It refuses to run inside an outer transaction. The oldcreate_table_for_resourceis removed.api/utils/data_tables.py,api/utils/data_table_names.py: the helpers for each step, and the naming rule.api/models/Resource.py: a newResourceDataTablerow is namedrdt_<own id>.api/signals/data_table_signals.py, registered inapi/signals/__init__.py: drops a row's table after its delete commits. It keeps an old table another row still shares.api/management/commands/update_dataindex.py: no longer wraps each resource in a transaction. One failing column used to silently roll the whole switch back.api/management/commands/migrate_data_table_names.py+api/utils/data_table_migration.py: the migration command.tests/data_index/: tests run against a real Postgres whenTEST_DATA_DB_URLis set (tests/test_settings.py,pytest.inimarkerdata_db)..github/workflows/django-tests.yml: apostgres:16service, plusREQUIRE_DATA_DB_TESTS=1, so CI fails instead of quietly skipping those tests..github/workflows/sdk-tests.yml: pinned toubuntu-24.04.ubuntu-latest(26.04) has no Python 3.9, which failed the SDK job on fix(data-api): read tables whose column names contain a percent sign #295.api/utils/data_indexing_architecture.md(new), with a link fromdocs/dataset_data_api.md.Testing
tests/data_index,tests/schema/test_resource_permissions.pyandtests/test_data_indexing_filters.pygive 133 passed. Every new test was seen failing first for the expected reason.tests/journeys/data_index/reupload-and-delete.pyis written but not run yet. It needs a running backend and a token.Test Instructions
pytest tests/data_index. Without the URL these tests are skipped; withREQUIRE_DATA_DB_TESTS=1and no URL the run fails.GET /api/resources/<id>/data/; every read is the complete old or the complete new file;python manage.py migrate_data_table_names --report dry.json.Checklist
blackandisort --profile blackon the changed files. mypy (with the Django plugin) has no errors in the changedapi/files.Additional Notes
Known and left for later (marked
TODOin the code and the architecture doc):Rollout: deploy, then run the migration command off-peak as described in the runbook: dry run, review,
--applyuntil it prints "nothing to do", and orphans dropped only from a reviewed report.🤖 Generated with Claude Code