Repository navigation
Declared tables read exactly what is on disk at query time - #1573
Closed
thekevinbot wants to merge 12 commits into
Closed
thekevinbot wants to merge 12 commits into
thekevinbot wants to merge 12 commits into
Conversation
Hammers, a fresh-instance oracle and each named edge case, in Rust, Python and TypeScript; adds a macOS Rust test job.
…st read Each query walks the table anchors, fingerprints every match, and re-reads the files whose fingerprint moved or whose last read may have overlapped a write. Rows it changes reach the next poll or watch batch once.
Moves the adopt decision into freshness so a unit test pins it, and exempts the Windows and fallback fingerprint bodies the Linux mutation runner compiles out.
Collaborator
Author
|
Restarted red-first as #1577 |
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 #1572
Guarantee
"A read must guarantee that it is accurate to the filesystem." A query on a long-lived Rust
DirSQL(SDK, server, REPL; Python through the core) now reads what is on disk when it starts. Every freshness test asserts at t+0, the moment the write returns, against aDirSQLbuilt fresh over the same tree. No sleep, retry, poll or timeout precedes an assertion.Scope after the split: Rust tests + core fix, Linux and Windows. macOS CI is #1574. The Python binding tests ship in the next PR; the TypeScript binding tests and the JS-thread apply step in the one after (until then a TS query reads what the watcher applied, as before).
Mechanism
Before a query on a declared table, under one lock and one transaction:
.gitignorere-read on every walk).Errand records nothing.Read-your-writes without a marker file. A read is settled iff the file's ctime is more than 2s before the read started.
utimecannot set ctime, and any write after the read began bumps ctime to at least that time, so a settled fingerprint that is unchanged proves the content is what was read. An unsettled file is re-read on every query until it settles. No content hash: a hash cannot see an A→B→A change made mid-parse. Files the build indexed are adopted without a re-read iff their ctime is more than 2s before the build started.Windows. ctime is NTFS ChangeTime (
GetFileInformationByHandleEx(FileBasicInfo)); device/inode are volume serial and file index. The file is opened withFILE_READ_ATTRIBUTESonly. A ChangeTime of 0 counts as never settled.Events. Row events a query's re-reads produce go to an outbox once a watcher is wanted (
start_watching,poll_events,watch). The next watch batch or poll drains the outbox first, under the same lock, so events stay totally ordered. When the watcher later reports the same change, re-applying identical content yields no event, so each change arrives exactly once.Unchanged surfaces. The CLI one-shot (
dirsql query) queries straight after its own build, so fresh reads are off there. The TS binding is off too: itson_fileruns only on the JS thread.Foreign keys. If a file on disk breaks a foreign key, the query fails with the
ForeignKeyViolationa build over that tree returns. Serving the pre-violation rows would be a stale read.Each way a change could be missed, and the test that closes it
fresh_declared_hammer.rsevery_*_is_read_by_the_next_query(2000 rounds each)every_atomic_save_…,an_atomic_save_over_the_original_is_readevery_same_size_edit_…,a_same_size_edit_within_one_mtime_tick_is_readutimeevery_same_size_edit_with_its_times_restored_…,an_edit_whose_times_are_put_back_is_reada_file_deleted_and_recreated_under_the_same_name_is_reada_new_subdirectory_is_read,a_deleted_subdirectory_is_dropped.gitignoreediteda_gitignore_change_changes_which_files_are_reada_retargeted_symlink_is_read,an_edit_to_a_symlink_target_is_reada_file_rewritten_while_queries_run_never_reads_torn_and_ends_exact,a_file_written_in_place_while_queries_run_ends_exact; unita_settled_file_is_read_once_then_not_againa_built_file_unchanged_since_before_the_build_is_not_read_again(adopt only if settled); oracle long-lived runevery_edit_to_a_table_hook_file_…; unita_per_table_hook_reruns_when_one_of_its_files_changes_and_when_one_goesa_file_whose_hook_fails_at_query_time_keeps_no_rows_and_reports_the_errorquery_ordered/query_eachskipping the checkquery_ordered_and_query_each_read_fresh_rows_toofresh_declared_oracle.rs(seeds 1–4, 150 ops each; long-lived instance)fresh_declared_events.rs; unitsa_query_hands_its_row_events_to_the_next_poll_once,a_query_after_start_watching_hands_its_row_events_to_the_consumer,a_change_a_query_read_reaches_the_next_poll_once_when_the_watcher_reports_it_tooa_query_over_an_orphaned_child_fails_as_the_build_wouldThe four freshness suites ran 20 times locally with no flake.
Residual gaps (stat cannot see these)
mmap: the kernel updates ctime lazily.SetFileInformationByHandle.The shared-mmap, NFS and clock-step limits are shared by every stat-based tool and are documented in
docs/reference/sdk.md.Windows open-handle writes: not a gap
tests/fresh_declared_open_handle.rstests open-handle writes. Each of its four tests opens 300 files for writing, waits past the 2s settle window, and queries once so every file's read is trusted on its stat alone. Then, with the handles still open, it writes each file and queries at t+0, asserting the new rows. The four tests cover a same-size rewrite and a resize, each with and withoutsync_all(FlushFileBuffers). That is 1,200 rounds with no sleep before any assert.writereturned.must_readstops comparing fingerprints for settled files, all four tests fail locally.Cost of the settle rule: a file changed within the last 2s is re-parsed on every query until it settles.
Changes to existing tests
populated_db,fk_db), the FK hold test, and the two move-in watch tests turn fresh reads off, so they still test what the watcher applies.persist_fans_out_and_composite_key_round_tripschecks "extracted once" right after the build; the just-written file is unsettled, so the queries after it re-parse it by design.Mutation exemption
testing-conventions.tomlexempts thecfg(windows)and non-unixfingerprintbodies insrc/freshness.rsfrom mutation. The Linux mutation runner compiles them out, so no Linux test can reach them. The Windows job runstests/fresh_declared_*.rs, whose t+0 asserts fail if the Windows fingerprint misses a change.Red run
Commit
test: declared tables read exactly what is on disk at t+0went red in CI (run); every failure was a staleness assertion.fresh_declared_edges, all 13 failing.Perf
Release builds, a 20,000-file JSON tree, per-table
jqhook, measured with hyperfine against the branch's merge-base.dirsql query 'SELECT count(*) FROM records' -c …(60 runs)'./**/*.json'(per query)The one-shot is unchanged (fresh reads are off there). A declared-table query now costs a walk and a stat per match, slightly below the equivalent path-table query.
cargo bench -p dirsql(db, differ, matcher, scanner) covers modules this PR does not touch and ran clean.Downstream acceptance repros (Python, by hand)
I built the worktree with
uv run maturin develop --releaseat 97eff27, then ran both scripts withuv run python.dq-watch.py. Two cases:watch()running, a file written afterready()appears in the next query:no watch [turns/0, turns/1].watch()consumed,turns/2appears in the query and arrives as oneinsertevent.turns/1sends no event, because it was written and queried beforewatch()started.dq-race.py. It consumeswatch(), writes a file, then queries straight away. Over 6 runs of 20 writes:immediate misses 0 of 20in every run, and max lag 0.001 to 0.002 s, which is the query's own cost.E2E Verification
cargo test --workspace --features cli, all 84 test binaries green)packages/python/e2e-attestations/bot-1572-fresh-declared-1103.jsonreceipt writtenpackages/ts/e2e-attestations/bot-1572-fresh-declared-1103.jsonreceipt writtentesting-conventions e2e attest 'uv run python -m pytest tests/e2e/{declared_table_name,fanout,on_file_query,query_contract,watch_speed,server_speed,repl_interactive,sigint_interrupt,use_case_dossier,use_case_join,use_case_notebook,use_case_transcripts,frontmatter_parse_speed,jsonl_parse_speed}_test.py -q'(the e2e tests that touch declared tables, watch, server or REPL; the path-table glob speed tests are unaffected);just e2e-attest-ts;cargo test --workspace --features cli/tmprepros (fresh, fresh2 with and without watch, fresh_path) all read the new state at t+0.Changelog / Migrations
packages/rust/changelog.d/andpackages/ts/changelog.d/packages/rust/migrations.d/andpackages/ts/migrations.d/