Skip to content

Updates - #9

Merged
jgarzik merged 64 commits into
mainfrom
updates
Oct 12, 2026
Merged

jgarzik merged 64 commits into
mainfrom
updates

Conversation

@jgarzik

@jgarzik jgarzik commented Oct 11, 2026

Copy link
Copy Markdown
Owner

No description provided.

jgarzik and others added 9 commits October 11, 2026 16:23
Add tests/defects/v1_review_2026_10.rs: 113 #[ignore]d tests encoding
correct behaviour for every finding of the v1-readiness review
(name resolution, precedence, comparison rules, deep expressions,
column naming, REAL rendering and summation, verbatim writeback, load
validation, table specs, writeback edge cases, file-type rules, CLI
flags, error format, REPL behaviour, scalar functions, transactions),
plus 5 passing guard tests for behaviour the fixes must keep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every statement now goes through vm::parse::parse_sql:

- SqawkDialect (src/vm/dialect.rs) is Hive's dialect, reporting Hive's
  TypeId so the parser's Hive checks still apply, with SQLite operator
  precedence: = IS IN LIKE BETWEEN share one level; < <= > >= bind
  tighter; then bitwise, + -, * / %, ||, and unary - + ~ tightest. IS is
  parsed SQLite style: the right side of IS [NOT] [DISTINCT FROM] is
  parsed at the = level, and x IS y / x IS NOT y are NULL-safe equality.
- Compound SELECTs are rebuilt left-deep in source order (SQLite gives
  UNION/UNION ALL/INTERSECT/EXCEPT equal precedence, left to right);
  parenthesized operands stay intact.
- An iterative walk enforces SQLite's limits before anything recurses
  over the tree: expression depth 1000 (parentheses add none), 500
  compound terms.
- Derived tables execute their parsed Query directly instead of
  rendering it to SQL and parsing it again.
- sqawk runs on a 256 MiB stack via stacker::grow on the main thread (a
  second thread would make glibc malloc lock on every call), so a
  999-deep expression works in debug builds.
- O(n^2) compile work removed: || no longer clones its operands, and
  aggregate-binding lookup renders a node only when its variant matches
  a bound expression.

Red list: 12 tests un-ignored (precedence, compound order, deep
expressions).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… sums

Comparison: every comparison (=, <, IS, IN list, IN subquery, CASE, NULLIF,
hash join, ORDER BY, MIN/MAX, windows) uses one rule. Values that both have
a numeric reading (numbers, or text numeric_text accepts) compare as
numbers, text against text included ('10' > '9'); numbers sort before other
text; other text compares by bytes. compare_values is the matching total
order (number before equal-valued text, equal texts by spelling), so
GROUP BY / DISTINCT still keep '1' and 1 apart. IN (SELECT ...) sets and
hash-join buckets key values by an exact numeric key (cmp_key), so they
agree with = ; the hash join's Wild class is gone (booleans are 0/1).

Reals: Display and text_of render a REAL as SQLite's CAST(x AS TEXT)
(%!.15g, half-up digits, .0 on whole values, Inf/-Inf). c_general moved to
arith and shared with printf %g. Load inference: a field is REAL only if it
equals that rendering, else verbatim TEXT.

SUM/TOTAL/AVG: Kahan-Babuska-Neumaier summation as SQLite (integer
overflow reported at finalize unless a real was added after it).
ROUND(x, -N) treats N as 0. INTERSECT keeps the left side's values.

Un-ignores 14 red tests (A3 comparison, A7 reals/ROUND/sums); updates 24
test expectations for integral-real ".0" / 15-digit rendering / ROUND.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ly aggregates rejected

- NameCtx::lookup / resolve_qualified lose the single-source fallback that
  let any qualifier (zz.k, or a table's real name after it was aliased)
  resolve to the one table in FROM; t.* likewise matches only the name a
  source is known under. Unknown qualifiers fail as in SQLite:
  'no such column: zz.k', 'no such table: zz'.
- UPDATE / DELETE scope the target under its alias (UPDATE d AS x ...
  WHERE x.k), so an unknown qualifier there fails before anything is
  written under --write.
- A derived table reading an outer-query column now fails cleanly
  ('no such column: a.aid') instead of binding to an inner column.
- Schema qualifiers: main.t (case-insensitive) is accepted in every table
  position and main.t.c / main.t.* in expressions; any other schema is
  'no such table: foo.t' / 'no such column: foo.t.c'.
- An aggregate whose arguments read only outer-query columns is rejected
  (SQLite would move it to the outer query); one that also reads an inner
  column aggregates in the subquery as before.
- ColumnNotFound / TableNotFound render as SQLite's 'no such column: X' /
  'no such table: X'. Five tests matched the old 'not found' text and now
  match the new message; no existing test relied on the removed fallback.

Red tests un-ignored: unknown_qualifier_is_error,
unknown_qualifier_in_update_is_error, unknown_qualifier_star_is_error,
correlated_derived_table_is_error, unknown_schema_is_error,
unknown_schema_in_update_is_error, outer_only_aggregate_in_subquery_is_error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unaliased result columns are named as SQLite 3.45 names them:
- an alias wins;
- a column reference, qualified or not, is named by the column as the
  table declares it (u.name -> name, NAME -> name), and * / t.* expand to
  bare column names, duplicates included (id,name,id,uid);
- any other expression is named by its SQL text exactly as written
  (COUNT(*), n  +  1, upper(name)||1, a IS 5 -- formerly COUNT, expr,
  a IS NOT DISTINCT FROM 5).

The text comes from the source: parse::SqlSource tokenizes the statement
with byte offsets, and each SELECT's result list is located from its
SELECT token and split at top-level commas (sqlparser's expression spans
do not cover -x, CAST(...), (a), ...). Without a locatable source the name
falls back to the expression's Display form. The source is threaded from
execute_vm through execute_statements and derived-table materialization
into the compiler.

Derived tables and CREATE TABLE ... AS make their column names unique as
SQLite does (id, id:1, id:2); in a derived table a column reference is
named as written. DDL instruction specs escape ':' / '|' / '%' in names
and paths, since column names may now contain them (a||b).

Differential check: 236 queries (columns, qualified refs, aliases, *,
functions, aggregates, windows, CASE, CAST, subqueries, literals,
IS / IS [NOT] DISTINCT FROM, comments and newlines inside items,
compounds, derived tables, CTAS) against SQLite cursor.description /
PRAGMA table_info: 234 match, 0 mismatch (1 both-error, 1 unsupported
function).

Test expectations: 140 header-only changes across tests/golden,
tests/defects, join, join_on, aggregate, distinct, comparison,
math_functions, stdin, window and tsq/tests -- every failing test was
checked to differ from its expectation in header lines only.

Red tests un-ignored: qualified_column_header_is_bare_name,
star_over_join_header_is_bare_names, expression_header_is_sql_text,
count_star_header_is_sql_text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- File types: .tsv/.tab are tab-separated, every other file and stdin
  comma, unless -F. The first line is always the header unless
  --no-header/--tabledef; the headerless guess is gone. '#' lines are data
  unless the new --comments flag (any file type), which skips them on load
  and keeps them in place on writeback.
- Verbatim writeback: a loaded table keeps the file's bytes (one buffer
  plus per-row offsets, RawRows in table.rs). Rows no statement changed
  are written back exactly as read (quoting, ragged rows, invalid UTF-8,
  mixed line endings); UPDATE keeps a row's bytes when every value is
  identical, DELETE drops them with the row, INSERT adds rows without,
  ALTER ADD COLUMN and TRUNCATE discard them. The BOM, blank lines and the
  header are kept; a row written after an unterminated last line gets the
  file's line ending first.
- Load errors: an unclosed quote ("unterminated quoted field starting at
  line N"); empty or duplicate (case-insensitive) header names.
- Column names: one case-insensitive lookup (Table::column_index) for
  SELECT, INSERT column lists and UPDATE SET; ALTER TABLE ADD COLUMN of an
  existing column and CREATE TABLE with a repeated column are errors.
- Table specs: one parser (parse_table_spec); NAME=PATH only when NAME is
  identifier-like; empty table names are errors. Tables store canonical
  absolute paths, so REPL .cd + .save writes the loaded file and the same
  file cannot be loaded twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lags

Every error is one line on stderr, 'sqawk: <message>', exit status 1
(clap usage errors keep exit 2). SqawkError messages are self-contained
and no variant exposes source(), so causes are never printed twice; no
SQL echo or 'Caused by' chain. File errors name the user's file
('cannot open', 'cannot write'), never the writeback temp file; load
errors read 'FILE: line N: MESSAGE'. Unknown functions report 'no such
function: F', aggregates outside an aggregate context 'misuse of
aggregate function F()' (COUNT(*) included), ambiguous columns
'ambiguous column name: c'. The REPL prints errors in the same form.

All stderr diagnostics (-v, errors) and REPL stdout lines go through
diag!/outln!, which ignore write errors: a closed stream never panics.
-v is documented as debugging output whose format is not stable.

New flags: -f/--file FILE (repeatable, '-' for stdin, ordered with -s),
--field-separator (long -F), --output-delimiter (default: -F, else
comma), --no-output-header. Separator values are validated by clap.
The anyhow dependency is gone from sqawk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Transactions: BEGIN [DEFERRED|IMMEDIATE|EXCLUSIVE] [TRANSACTION], COMMIT /
END, ROLLBACK, handled in vm/mod.rs against Database. The undo state is
copy-on-write (a table is copied on its first change inside the
transaction, moved there when dropped, recorded absent when created), and
the modified-table set now lives in Database, so ROLLBACK restores both.
SQLite's errors for nested BEGIN and COMMIT/ROLLBACK without one. A
transaction open at the end of the run or REPL session is rolled back
before writeback (-v notes it). In the REPL, .write on saves only outside
a transaction; .save and .load are refused inside one. SAVEPOINT, RELEASE,
ROLLBACK TO, transaction modes, AND CHAIN, START TRANSACTION and BEGIN
blocks are rejected. The unused TransactionState and Begin/Commit/
Rollback/SavePoint/Release opcodes are removed.

Writeback: every target is checked before any file is written; a
read-only file (mode bits, or refused open for append) is
"cannot write 'x.csv': file is read-only", and the directory must take a
temp file. Scratch tables (no LOCATION, CTAS) are skipped; a dropped
table leaves the modified set. Hard links are split by the atomic rename;
documented. Docs no longer claim only INSERT/UPDATE/DELETE write back.

Silent ignores: check_create_table destructures CreateTable exhaustively;
STORED AS other than TEXTFILE, WITH options other than DELIMITER, other
option forms, comments, TEMP with LOCATION, and LOCATION/STORED AS/WITH or
a column list on CTAS are errors. --tabledef without ':', with an empty
table or column name, a duplicate column, a table defined twice, or a
table no input file loads is an error.

Red list: 16 tests un-ignored (transactions, savepoints, read-only
refusal, scratch skip, DROP of a modified table, STORED AS, tabledef).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jgarzik
jgarzik requested a balanced review from Copilot October 11, 2026 19:22
@jgarzik jgarzik self-assigned this Oct 11, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It is a large v1-readiness change spanning SQL parser precedence, transactions, float formatting, and error/file-handling refactors—high-correctness-risk areas that warrant final human review.

0 open findings

What changed in this PR

This "Updates" PR advances sqawk toward v1 readiness by aligning its SQL semantics more closely with SQLite 3.45 and adding transaction support. The changes shown here are mostly test updates that encode the new behavior, plus a substantial new end-to-end test file for transactions and writeback rules. They accompany broader source changes (a custom SqawkDialect with SQLite operator precedence, BEGIN/COMMIT/ROLLBACK with copy-on-write tables, SQLite-compatible real-number formatting, compensated summation, and error/file-handling refactors).

Changes:

  • SQLite-compatible output: real numbers now render with a decimal point (e.g. 1 → 1.0, 0.285714285714286), and auto-generated column names use the written expression text and are unqualified for joins (s.name → name, COUNT → COUNT(*)).
  • New tests/transaction/mod.rs exercises BEGIN/COMMIT/ROLLBACK over in-memory tables, --write interaction, REPL behavior, and writeback rules (scratch tables, DROP, read-only files, hard links, --tabledef validation).
  • Reworded unsupported-feature errors ("Unsupported" → "not supported") and added unsupported cases for CREATE TABLE options and transaction sub-statements.
File Description
tsq/​tests/​tsq.rs Updates expected header COUNT → COUNT(*) for new auto-naming.
tests/​window/​mod.rs Adjusts expected output for real formatting and unqualified join column names.
tests/​unsupported/​mod.rs Adds create_table_options and transaction_clauses rejection tests.
tests/​transaction/​mod.rs New suite covering transactions, --write, REPL, and writeback rules.
tests/​subquery/​exact.rs Updates expected error text to "not supported".
tests/​stdin/​mod.rs Updates expected headers (COUNT(*), unqualified name).
tests/​mod.rs Registers new file_io and transaction modules; clarifies headerless comment.
tests/​math_functions/​mod.rs Updates ROUND/real expectations and negative-precision doc to match SQLite.
tests/​join/​planner.rs Updates expected error text to "not supported".
tests/​join/​mod.rs Join test expectation updates (truncated in diff).

Verification notes: the reworded error assertions match source strings (src/vm/compiler_check.rs:26, src/vm/compiler_from.rs:674, src/vm/compiler_subquery.rs:147); the real-formatting and column-naming test updates are consistent with Value::field_str/Display and arith::real_text; transaction/writeback behavior is backed by the copy-on-write model in database.rs and REPL guards in repl.rs. I did not find concrete, reportable defects in the shown diffs.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

jgarzik and others added 17 commits October 11, 2026 19:28
The test crate failed to compile on Windows: helpers called
std::os::unix::fs::PermissionsExt::set_mode and std::os::unix::fs::symlink
unconditionally. Gate those helpers (perms_enforced, chmod) and the tests
that use them with #[cfg(unix)].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Piped (non-tty) stdin: read line by line; stdout carries only command
  output (no banner, prompts, CTRL-C/D echoes or status messages; status
  goes to stderr under -v). "Query returned N rows" is gone in both modes:
  results print exactly as with -s.
- .exit N sets the exit status (mod 256); a script with any SQL or
  dot-command error keeps running and exits 1. Dot-command failures
  (unknown command, .schema of a missing table, failed .cd, bad .stats
  option, bad .exit code) are now errors.
- A final statement without ';' runs at end of input; statement end and
  blank/comment-only input are found outside quotes and comments.
- -i with -s/-f is a usage error (exit 2).
- History only for a tty: $XDG_STATE_HOME/sqawk/history, else
  ~/.sqawk_history, else none; never the current directory.
- .tables PATTERN uses the LIKE matcher; drop the regex dependency.
- Tab completion: dot-commands, table-name arguments, SQL keywords,
  function names, table and column names, table.column.
- Ctrl-C abandons the current entry instead of leaving the REPL.
- Remove the REPL-only ResultSet/execute_sql/rows_as_strings path.
- Un-ignore the REPL red-list tests; user guide REPL section updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Verified by running the whole suite as x86_64-pc-windows-gnu binaries
under Wine (all pass) as well as natively on Linux.

- error.rs tests used raw OS error 13, which is EACCES on Unix but
  ERROR_INVALID_DATA on Windows; use the platform's access-denied code
  and read the OS's own wording instead of spelling it out.
- repl.rs history_location used "/state", which is relative on Windows;
  use platform-absolute paths.
- REPL tests spawned `cargo run` from inside the test run (nested cargo,
  build-lock contention, no cargo under Wine); spawn the built binary via
  CARGO_BIN_EXE_sqawk.
- transaction::run wrote stdin to a process that may already have exited
  after rejecting its arguments, failing intermittently with BrokenPipe;
  a BrokenPipe on that write is not a test failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A record with too many fields at line 4 of a CRLF file is reported at
line 3. Windows CI exposed it: its checkout converts tests/data to CRLF,
so too_many_fields_is_an_error saw line 5 instead of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- printf.rs: a port of SQLite 3.45's sqlite3_str_vappendf as the SQL
  printf drives it: every conversion (d i u f e E g G x X o c s z q Q w
  p r n %), flags - + space 0 # , !, * width/precision, l/ll, real
  decoding to 16 (26 with !) digits half up, NULL for an empty result.
  Widths/precisions past the length limit stay an error.
- QUOTE(x).
- datetime.rs: a port of date.c: negative years, whitespace rules,
  subsec, auto, julianday, +-HH:MM, +-YYYY-MM-DD[ HH:MM...], every 3.45
  strftime conversion (%e %F %I %k %l %p %P %R %T %u ...), %J via
  %.16g, TIMEDIFF, and localtime/utc through the C library (libc).
- Hex integer literals 0x1F / 0X1F (64-bit two's complement, "hex
  literal too big", unrecognized token for 0x alone); X'..' is the
  error "blob literals are not supported". `~~` splits into two `~`.
- Bitwise & | << >> ~ with SQLite semantics; << and >> now parse.
- Infinite real literals (1e999) no longer become 0.0.
- Red list: the 13 remaining v1-review tests pass and are un-ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
csv-core ends a CRLF record at the CR and counts its LF only when the
next record is read, so every line number taken from its record
positions was one too low in CRLF files (too many fields, bad header).

read_table now numbers lines from the bytes: the line of a record is one
plus the line breaks before the record's first byte, where a break is
LF, CRLF, or a CR not followed by LF (the terminators the csv crate
accepts). The count resumes from the previous record, so it stays
linear. The unterminated-quote error uses the same counter, so CR-only
files number lines correctly there too. Skipped blank and # lines, the
BOM and quoted line breaks all fall out of counting bytes.

Tests: the red CRLF test is no longer ignored; unit cases cover mixed
endings, CR-only, quoted LF/CRLF, blank lines, BOM, --comments lines,
unclosed quotes and bad headers; an integration test runs the binary on
LF and CRLF twins of malformed files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHub's Windows runner checks out with core.autocrlf=true, which turned
tests/data/* into CRLF files, so the fixtures the tests read were not the
bytes in the repo. Mark the fixtures -text: git never converts their line
endings, so they are byte-identical on every checkout.

Scope: tests/data/** (every fixture the tests read) and tests/*.csv
(the other CSV data). Sources are left alone. A
CRLF-converted tree was simulated (native and under wine with
--target x86_64-pc-windows-gnu), and with every file, Rust sources
included, converted to CRLF the suite passes. Rustc normalizes CRLF in
source, so multi-line string literals are unaffected. Forcing
'* text=auto eol=lf' would buy nothing and would override contributors'
own autocrlf settings. Every file in the index is already LF, so
renormalization changes no content.

Before the previous commit, the one test that depended on fixture line
endings was csv_handler::tests::too_many_fields_is_an_error (line 5 vs
6, the CRLF line-number bug). That bug is fixed in the loader. This file
makes sure fixtures cannot drift by platform in future.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Delete the unused index-based result types in table.rs, the IfNeg, Sort
and Sequence opcodes the compiler never emits (and the cursor sort-key /
sequence state only they used), the unreachable catch-all opcode arm,
and every #[allow(dead_code)] that hid them. SqlCompiler::compile is
test-only and now says so with #[cfg(test)].

Comments state current fact: no roadmap notes, removed-method notes or
development phase labels. Doc footers that described future versions
are gone.

The empty test_complex_where_expressions is now seven exact-output
checks of AND/OR/NOT precedence, parentheses and NULL truth tables,
with expected output computed by SQLite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Version 0.9.0 (tsq stays 0.1.0).
- Drop the parser-implementations category: sqawk is a command-line
  tool, not a parser crate.
- [profile.release]: codegen-units = 1 and strip = true alongside
  lto. The release binary goes from 9,161,800 to 5,433,808 bytes on
  x86_64 Linux; best-of-5 timings on 200k rows are unchanged or a hair
  faster (GROUP BY 0.21s -> 0.20s, hash join 0.28s -> 0.27s, ORDER BY
  LIMIT 0.48s -> 0.47s). The profiling profile keeps its symbols with
  strip = false.
- README: install with `cargo install --locked sqawk`.

MSRV 1.88 re-verified: cargo +1.88.0 check --locked --workspace
--all-targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every example command in README.md and doc/user_guide.md and every value
claim in doc/sql_reference.md was run against fixtures; fixed what did
not match:

- user guide: the header rule (first line always the header), an INSERT
  example that needed a three-column table, a DISTINCT example naming a
  column the documented file lacks, CREATE TABLE types (accepted, not
  enforced), the TOC, and install with --locked.
- Memory: a table takes roughly six to seven times its file size
  (83 MB / 1M rows -> 530 MB peak for COUNT(*); SELECT * 1.4 GB), plus
  the result set. Documented in the user guide, database.md and README;
  "select fewer columns to save memory" advice removed (the whole table
  is loaded regardless).
- SQL reference: TRIM takes one argument (TRIM(s, chars) does not parse;
  LTRIM/RTRIM take two); the precedence table gains the bitwise level and
  unary ~; the parser's nesting limit (47 levels of parentheses or
  function calls, about half that for subqueries: "recursion limit
  exceeded") is stated; REGEXP is rejected rather than unparsed.
- database.md: TOC, column lookup, derived-table registration, output
  delimiter, result sets are materialized, memory figures.
- CLAUDE.md: test directory list.

CHANGELOG.md (Keep a Changelog) records 0.9.0: breaking changes (file
types and headers, # lines, NULL and real output, column names, the
comparison rule, integer arithmetic, LIKE, precedence, ROUND, errors and
exit status, unknown qualifiers and schemas, REPL output, exit codes and
history, -i with -s, packaging), additions and fixes since 0.8.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…zers

harness.py now loads each cell into SQLite as the value sqawk infers
(empty -> NULL, canonical integer, real only if it equals SQLite's
CAST(x AS TEXT), else verbatim text) into columns with no affinity, and
renders SQLite's results as sqawk prints them (NULL empty, reals via
CAST(x AS TEXT), RFC 4180 quoting compared byte for byte). Every result
set of a multi-statement test is compared; a both_err counter is added;
data goes to a temp dir; SQAWK overrides the binary. NULL and '' both
print empty, which the header documents.

New, all self-contained (repo-relative, temp dirs, SUMMARY line):
- diffh.py + hand1-3.sql: adversarial tables, hand corpora and a seeded
  random query generator.
- wb.py: --write round-trip fuzzer (untouched records byte-identical).
- csvfuzz.py: malformed and huge inputs.
- fuzz.py: garbage SQL, limit values, deep nesting.

Results: harness.py pass 522 / DIFF 3 / sqawk_err 14 / sqlite_err 1 /
both_err 7; diffh hand 359 / 13 / 15 / both 36; random seeds 1-3 pass
676 / 684 / 682 of 700; wb.py 300/300 ok; csvfuzz crash 0,
writeback_bad 0; fuzz findings 0. Every remaining DIFF and sqawk_err is
a documented difference or unsupported feature; sql_reference.md gains
the ones that were missing (SUM/AVG over text, bare columns in aggregate
queries, integer ORDER BY / GROUP BY terms, x IN (), COLLATE, the
RIGHT/FULL JOIN limits, INTERSECT/EXCEPT ALL). CLAUDE.md says how to run
each tester and what its SUMMARY must show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`ORDER BY +1` returned rows unsorted (SQLite: position 1); `ORDER BY -1`
was accepted; `GROUP BY -1` gave a misleading error; integers beyond 32
bits and real literals errored instead of being constant terms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The deep-expression red tests sent 20-70 KB statements with -s, past
Windows' 32,767-character command line limit ("The filename or extension
is too long", os error 206), so the process never started on the Windows
runner. run_one now feeds the SQL through `-f -` on stdin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A term is a result-column position exactly when SQLite's
sqlite3ExprIsInteger says so: an integer literal fitting in 32 bits, under
any parentheses and unary +/- (`+1`, `(1)`, `- -1`, `0x2`). It must lie in
1..=N, else "Nth ORDER BY term out of range - should be between 1 and N"
(GROUP BY likewise). Any other term (`1.0`, `4294967297`, `1+0`, `'1'`,
`-2147483648`) is an ordinary expression. In a compound such a term must
name a result column ("Nth ORDER BY term does not match any column in the
result set"). Window ORDER BY integers stay constants.

Un-ignores the four red tests; adds exact-output tests for simple, grouped,
compound, DISTINCT, subquery and window cases (SQLite 3.45 expectations).
Drops the "Differences from SQLite" entry in favour of the SQLite rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also drop the links to it from README.md and doc/user_guide.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…types

CREATE TABLE ... LOCATION 'x.tsv' writes comma-separated text that does not
load back as written, and declared column types other than the handful sqawk
recognizes are recorded as TEXT. These #[ignore]d tests encode the correct
behaviour, with controls for an explicit WITH (DELIMITER=...), .csv files,
and declared types not being enforced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CREATE TABLE ... LOCATION without WITH (DELIMITER=...) now writes a .tsv or
.tab file tab-separated, as such a file is loaded, so it reads back as
written; an explicit delimiter still wins.

A declared column type (CREATE TABLE, ALTER TABLE ADD COLUMN) is recorded as
written and shown so by .schema, instead of collapsing every name but
INT/REAL/FLOAT/DOUBLE/BOOL to TEXT. Its affinity follows SQLite's rules
(INT -> INTEGER; CHAR/CLOB/TEXT -> TEXT; BLOB -> BLOB; REAL/FLOA/DOUB ->
REAL; else NUMERIC) and is what CREATE TABLE ... AS reports for a column of
a declared table. Types are still not enforced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jgarzik and others added 28 commits October 12, 2026 01:03
A FIFO queue is breadth-first: it holds one generation of rows, each
followed by its children in order. When every recursive arm maps each
working row independently (no DISTINCT, no wildcard), the whole generation
is now the working table at once, each row tagged with its position; the
children are put back in queue order by (position, arm) and UNION dedupes
in that order -- the same rows in the same order as one run per queue row,
which an ordered queue and the other arms still use.

A 10,000-node tree walk drops from 7.2 s to 0.03 s (each run used to scan
the whole parent-child table for a single row). Two tests pin the
boundary: DISTINCT in an arm stays per row, and children follow their
parent in queue order across a generation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sql_reference.md: WITH syntax, scoping, column lists, alias qualification,
statement forms, errors; recursive CTEs (setup/recursive arms, FIFO or
ORDER BY queue, UNION dedupe, LIMIT/OFFSET, no recursion limit, errors,
performance); correlated CTE bodies listed under Differences from SQLite.
user_guide.md: examples (run), README feature line, database.md: the
pre-pass architecture and the unresolved-CTE guard, CLAUDE.md: vm/mod.rs,
prepass.rs and parse.rs notes. queries2.sql gains 17 CTE queries (plain,
DML, recursive counters, tree walks, depth-first), all matching SQLite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Foundation of the tsq rewrite. The old generator drew from libc
srand/rand through `unsafe`, divided rand() (at most 2^31-1) by
u32::MAX so every float landed in [0, 0.5) -- every customer active,
nullable columns never NULL, every order to the top fifth of
customers, value ranges halved -- truncated --seed to 32 bits, had
modulo bias and Windows' RAND_MAX of 32767, and iterated HashMaps
into its output files.

- rng.rs: ChaCha8 seeded from the full 64-bit seed, unbiased ranges,
  floats uniform in [0, 1), independent per-item streams. The same seed
  gives the same bytes on every OS. No libc, `#![forbid(unsafe_code)]`.
- gen.rs (`tsq gen`): customers (now with a referred_by hierarchy),
  products, orders, order_items, reviews with correct distributions and
  NULL rates; money in sqawk's canonical real spelling; `--edge-rate`
  salts non-key cells with edge values (i64 limits and just past them,
  007, 1.50, 1e3, " 5", +5, -0, Unicode with combining marks, emoji and
  RTL, embedded LF/CR/quotes/delimiters, long strings, numeric-looking
  and NULL/true literal text, quoted empty fields) from its own stream.
- data.rs: lossless writer for csv/tsv, --crlf, --bom, --no-header,
  --ragged-rate (rows leaving off trailing empty fields).
- metadata.json records seed, options and row counts.
- The 25 static query files and verify/run_verification.sh, which
  checked almost nothing, are gone; `tsq diff` replaces them.

Tests: every output file identical across two runs (seed above 2^32),
different seeds differ, foreign keys hold (sqawk anti-joins), and sqawk
loads every layout with exact row counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`tsq diff` generates random statement sequences, runs each through
sqawk (SQL on stdin, per-test time limit) and through SQLite 3.45
(rusqlite's bundled build) holding the same values, and compares every
result set byte for byte, headers included.

- Oracle (diff/oracle.rs): tables loaded into untyped columns with
  sqawk's per-cell inference (empty NULL, canonical integer, real only
  when CAST AS TEXT gives the text back, else verbatim text); each
  sequence runs on a fresh copy of the database, so transactions work;
  results rendered as sqawk prints them (NULL empty, reals via CAST AS
  TEXT, RFC 4180 quoting). .cargo/config.toml builds the bundled SQLite
  with its math functions and with LONGDOUBLE_TYPE=double, so the
  oracle's real formatting is the same on every OS (MSVC has no 80-bit
  long double; mingw under the Windows x87 control word got it wrong).
- Generator (diff/sqlgen.rs, on shrinkable trees in diff/ast.rs): a
  port of diffh.py's Gen -- typed expressions, functions, CASE / CAST /
  COALESCE / IIF / NULLIF, LIKE / BETWEEN / IN, scalar / IN / EXISTS
  subqueries with correlation, every join kind including USING and
  NATURAL, GROUP BY / HAVING / FILTER / DISTINCT aggregates, windows with
  frames, compounds, derived tables -- plus CTEs (chained, column lists,
  recursive counters, Fibonacci, depth-bounded tree walks), DML
  sequences, transactions, CREATE TABLE AS, ALTER TABLE ADD COLUMN,
  printf and date functions. Column properties from the loaded data
  (diff/schema.rs) keep every comparison, ordering and MIN/MAX where the
  two engines' rules agree, so documented differences are avoided by
  construction; a result set compares in order only under an ORDER BY
  over every output column.
- Classification: pass / diff / sqawk_err / sqlite_err / both_err /
  crash (panic, signal, abnormal exit, time limit); diff/allow.rs has one
  rule per documented difference (doc/sql_reference.md); SQLite running
  out of time is counted, not reported. Exit 1 on any finding.
- --shrink greedily cuts the SQL tree (clauses, joins, list items,
  sub-expressions, literals), the tables (unnamed tables, row chunks,
  unnamed columns) and writes query.sql, the CSVs, both outputs and the
  replay command.
- --tables gen|edge|harness, --scale small|large|ROWS, --edge-rate,
  --only N, --print, --jobs, --timeout, global --sqawk.

Smoke test in cargo test: 300 tests on generated tables and 150 on the
adversarial edge set (about 3 s on two cores).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`tsq corpus FILE...` runs hand-written query files with the oracle and
renderer of `tsq diff`, in harness.py's line format: one test per line,
`--` and blank lines skipped, `O:` for an ordered comparison,
`;`-separated statements with every result set compared (headers
included). A `-- tsq-tables: NAME` line picks the built-in table set:
`harness` (harness.py's TABLES, the default) or `edge` (diffh.py's
build_tables(7), transcribed cell for cell).

queries.sql, queries2.sql and hand1-3.sql move from tests/sqlite_diff/
to tsq/corpus/ (git mv; the hand files gain their tsq-tables line).
Parity with the Python tools on the same files:

  harness.py queries + queries2: pass 544, DIFF 3, sqawk_err 9,
    sqlite_err 1, both_err 7
  tsq corpus:                    the same, every non-pass covered by a
    documented-difference rule
  diffh.py --only-hand hand1-3:  pass 359, diff 13, sqawk_err 15,
    both_err 36
  tsq corpus:                    the same (tsq also compares headers and
    every result set where diffh.py compared only the last; no case
    changes category)

harness.py / diffh.py point at the new paths and stay until their
writeback and CLI testers are ported. README, CLAUDE.md and the user
guide describe the subcommands; a test pins both corpus summaries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Eight #[ignore]d exact-output tests in tests/defects/tsq_2026_10.rs,
each naming the tsq seed and test that found it (`tsq diff --seed S
--only N --shrink` replays it). Expected answers are SQLite's; where
SQLite builds disagree (x87 long double vs double-double, 3.45 vs 3.50)
only values every build agrees on are asserted.

Wrong answers:
- CONCAT_WS writes a separator for an empty-string argument; SQLite
  3.45/3.46 skip it (3.50 changed upstream -- fix or document).
- Real to text: 443.85 / 5867 prints ...594, SQLite ...595 (SQLite
  rounds an ~18-digit expansion half up, not the exact binary value).
- QUOTE(real) fallback (`%!.20e`) drops digits: 6.444918504005517e-04
  for SQLite's 6.44491850400551701e-04.
- `%` with a real operand rounds the integer operand through a double:
  9007199254740993 % 2.0 is 0.0, SQLite 1.0 (doc: integers beyond 2^53
  stay exact).
- ROUND(1058.05, 1) is 1058.1; the stored value is below the half, and
  sqawk's own doc says halves are judged on the stored value (SQLite
  1058.0).

Performance cliffs (right answers, quadratic time; tsq diff --scale
large reports them as crashes at its 10 s limit), each bounded at 5 s
for work SQLite does in under one:
- EXISTS whose only tie to the outer row is an outer-only predicate.
- Window frames ending at UNBOUNDED FOLLOWING with a moving start.
- An IN (SELECT ...) WHERE term on a join's first table applied only
  after the whole join.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comment now names the evidence: SQLite 3.50 dropped x87 long double
and MSVC never had it, and with it CAST(100.0/476.42 AS TEXT) ends in
...535 instead of ...536.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rustc 1.88 cannot infer `&str` through `Vec::push(r.pick(..))` the way
current stable does; dereference explicitly. Generated data is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each performance test now uses data on which the quadratic code needs
over a minute in a debug build (EXISTS 94 s, window 77 s, IN-subquery
join 93 s measured) while linear code needs well under a second, and
allows 20 s, so slow runners cannot make it flaky and the quadratic code
cannot pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Real-to-text conversion now ports SQLite 3.50's sqlite3FpDecode, the
portable path SQLite 3.45 also takes when built without long double (as
MSVC builds and tsq's oracle are): the magnitude is scaled near 1e18 in
Dekker double-double arithmetic, read as an 18- or 19-digit integer, and
rounded half up. CAST(x AS TEXT), ||, output, writeback, QUOTE and every
printf real conversion use it, so output is identical on every platform
and equal to the oracle's: 443.85 / 5867 is 0.0756519515936595 (sqawk
rounded the exact binary value, ...594), QUOTE(1 / 1551.61) keeps all
of SQLite's digits, %!.20f of 0.1 is 0.1000000000000000055.

Text-to-real conversion ports SQLite 3.50's sqlite3AtoF the same way
(up to 19 significant digits, scaled in double-double arithmetic): SQL
literals, CAST, arithmetic on text, date parsing and file loading read
the double SQLite reads, which for extreme input can be a neighbour of
the nearest one (1e-300). QUOTE's round-trip test uses it too, so
QUOTE(1e-300) is 1.0e-300 as in SQLite. A real literal is now read from
its own text rather than from Rust's shortest spelling of it. Load-time
inference is unchanged in rule (a field is REAL only when its text is
the rendering of its reading), now over the new conversions.

Differential (scratch harness, 18997 doubles incl. random bit patterns,
subnormals, 15/16/17-digit ties, huge/tiny; values passed bit-exactly):
0 mismatches against tsq's oracle (SQLite 3.45.0, LONGDOUBLE_TYPE=double)
over CAST AS TEXT, QUOTE, ten printf forms and text-to-real reading; it
agrees with SQLite 3.50.2 except where 3.50 changed printf (%.0f rounds,
QUOTE of Inf). Load inference: 56991 fields, 0 mismatches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
% with a real operand now reads each operand as SQLite's
sqlite3VdbeIntValue does -- integers exactly, reals truncated and
clamped, text by its integer prefix -- and returns the integer
remainder as a real. The integer operand used to pass through a double:
9007199254740993 % 2.0 was 0.0 (SQLite 1.0), and '1e3' % 7.0 was 6.0
(SQLite 1.0).

ROUND ports SQLite's roundFunc: a magnitude above 2^52 is unchanged; to
0 places, x + 0.5 (- 0.5) truncated; otherwise %!.*f of x, with
SQLite's decimal expansion, read back as a real. The digit-string
rounding it replaces decided halves on Rust's 17-digit expansion, so
ROUND(1058.05, 1) was 1058.1 although 1058.05 is stored below the half
(SQLite 1058.0). d above 30 is now 30, as in SQLite.

Checked bit-exactly against tsq's oracle over 18997 doubles (ROUND at
0/1/2/5/14/30 places, x % 7, 9007199254740993 % x, x % 2.5): 0
mismatches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sqawk writes the separator for an empty-string argument (a,,b), as
SQLite 3.50 and PostgreSQL do; SQLite 3.45 skips it like NULL. Keep
sqawk's behaviour: list it under Differences from SQLite, add the
matching tsq allow rule (rows must agree once the CONCAT_WS separators
are removed from both sides), and drop the red test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt ones first

A WHERE conjunct containing a subquery was always evaluated at the
innermost loop, after every join: expr_sources cannot see into a
subquery. Such a conjunct (and any other that expr_sources cannot
analyze) is now compiled in a scratch compiler and placed by the
cursors its Column reads name, a correlated subquery's references
included. t1.pid IN (SELECT pid FROM p WHERE q = 7) filters at t1's
loop: the red test's join (40 million rows unfiltered) drops from 93 s
to 0.4 s in a debug build.

A WHERE conjunct that reads no table of the FROM -- a constant, an
uncorrelated subquery, or in a subquery a test of the enclosing row
only -- is tested once before the loop nest instead of once per row of
its first table, as SQLite codes constant terms before the loop. A
correlated EXISTS (SELECT 1 FROM r t3 WHERE t3.a < 19 AND t1.b < t1.c)
no longer scans t3 for an outer row that fails t1.b < t1.c: 94 s to
0.04 s for the red test's 12000 rows (debug). EXISTS already stops at
its first row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An aggregate whose frame does not keep a fixed start and a growing end
was recomputed from scratch for every row: O(n^2) for frames ending at
UNBOUNDED FOLLOWING with a moving start (the red test's 60000 rows took
77 s in a debug build; now 0.2 s), O(n*w) for sliding frames.

The window pass now runs SQLite's sqlite3WindowCodeStep schedule: three
cursors (next row to step into the frame, next to return, next to remove)
advanced in SQLite's order for each frame shape, with its countdowns, by
rows for ROWS and by peer groups for RANGE. Every row is stepped and
removed at most once, so every frame shape is O(n). Removal is SQLite's
inverse: COUNT decrements; SUM / AVG / TOTAL subtract (integers exactly,
reals in the compensated sum, and a SUM that has seen a real stays a
real); MIN / MAX keep the frame's candidate values in a monotonic queue
(SQLite's ordered index, newest wins among equals); GROUP_CONCAT ports
groupConcatStep / groupConcatInverse (drop the leaving value and the
separator after it; an emptied string is NULL and starts over).

So values now match SQLite where recomputation did not: SUM(x) OVER
(ROWS 1 PRECEDING) over 1.5, 2, 3 is 1.5, 3.5, 5.0 (was ..., 5), inverse
rounding residuals, emptied GROUP_CONCAT. Scratch differential against
tsq's oracle, 62 window queries x 12 aggregates x every supported frame
shape, seeds 1-8: 0 mismatching queries (the previous build: about 20
per seed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A CONCAT_WS result with a separator containing a comma is quoted by the
CSV writer; removing the separator left the quotes on one side only, so
tsq diff --seed 2 --only 14411 stayed a finding. Undo the quoting and
compare field by field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…yway

tsq diff --seed 4 --only 3486 takes 91 s in sqawk, 0.02 s in SQLite: the
CTE is read only by a compound arm whose WHERE is constant false, so SQLite
never computes it, while sqawk materializes every referenced CTE before the
statement runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A CTE or derived table was run in the pre-pass, before the statement:
every referenced one cost its full evaluation even when no code that reads
it ever ran. tsq diff --seed 4 --only 3486 reads a CTE (whose body
self-joins another into 400 million rows) only from a compound arm whose
WHERE is constant false; SQLite never runs it (0.02 s), sqawk took 91 s.

The pre-pass now only compiles such a query and registers a lazy table
(Database::add_lazy_table): its columns from the program's result schema,
its rows from running the program when an OpenRead first opens it
(VmEngine::fill_lazy), as SQLite runs a FROM subquery behind OP_Once.
Recursive CTEs are still computed in the pre-pass.

The FROM planner tests WHERE terms that read no table before it opens any
cursor or builds a hash index (it tested them after), so a false constant
WHERE opens nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uted

Also covers, passing since 7429d16, a derived table read only under a false
WHERE and a CTE read only by a CASE branch not taken or by the SELECT list
of a query with no rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t reaches it

Every table of a join was opened, and every hash index built, before the
loop nest. A CTE or derived table is now computed when first opened
(7429d16), so a join whose outer loop has no rows still computed its inner
CTE: 12 s for a 64-million-row one where SQLite takes 0.0001 s. An inner
level's OpenRead and hash build now sit behind a Once at the level's entry,
where SQLite builds an automatic index. A FULL JOIN, whose unmatched-row
pass reads its tables after the nest, and a RIGHT JOIN group's inner
levels, which can be NULL-extended unreached, keep opening up front.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe the lazy registration in prepass.rs and merge the duplicated
mod.rs entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tsq wb fuzzes --write: random files (delimiters, LF/CRLF/mixed line
endings, BOM, ragged rows, blank and comment lines, quoted, multi-line and
invalid-UTF-8 cells, gen's edge values, CREATE TABLE ... LOCATION files)
and random DML / ALTER / TRUNCATE / transaction sequences, run statement by
statement on the SQLite oracle as well. Each file must equal the oracle's
final table rendered by the documented writeback rules (untouched rows are
their source bytes, tracked by rowid); read-only targets must refuse with
nothing written; SELECT output and a re-read must match; nothing stray may
be left. On Unix, SIGKILL (through kill(1)) during a large write must leave
the old or the new file.

tsq cli fuzzes the command line and the REPL: flag and operand
combinations, piped REPL scripts, malformed files, garbage, limit-value and
huge SQL, closed stdout. Every run must exit 0/1/2 (or a .exit status)
in time, report errors as one sqawk: line (clap's format for usage
errors), never panic, and create nothing in cwd, HOME or XDG_STATE_HOME.

tsq stress times 18 workloads over generated data (1M to 10M rows) and,
on Linux, the peak RSS of sqawk (VmHWM polled from /proc); --json saves
the results and --baseline exits 1 on a regression beyond --tolerance.

run.rs gains one spawn helper (stdin, cwd, env, stdout kept / drained /
closed early, peak RSS) that run_sqawk now uses. cargo test runs
wb --iterations 50 and cli --iterations 100 next to the diff and corpus
smoke runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…esters

The Python testers under tests/sqlite_diff/ were deleted with the previous
commit (554dd4c): harness.py and diffh.py were superseded by tsq corpus /
tsq diff, wb.py by tsq wb, csvfuzz.py and fuzz.py by tsq cli. CLAUDE.md,
the README and the user guide now describe every tsq subcommand and the
cargo test smoke budgets; tsq's comments name the Python tools as retired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efix

On Windows a --write error on a file outside the working directory printed
the verbatim path fs::canonicalize returns (`cannot write
'\\?\C:\...\ro.csv': file is read-only`). Found by tsq cli under wine; the
test is Windows-only and ignored until fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in linear time

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Table::column_count copied every column name to count them, and the
Column opcode asks for the count on each read, so a row of n columns
cost O(n^2) (SELECT * over 1000 columns x 1000 rows: 12.8 s). It is the
length of the column list. Table::column_index looks a name up in a map
built on first use for a table of more than 16 columns, and the header
check finds repeated names with a hash set instead of rescanning the
names before each one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…constant time

A NATURAL JOIN of wide tables and a window over wide rows compiled in
time quadratic in the column count: every reference scanned the list of
merged USING columns or of the bindings of the materialized row, and the
window stage's register remap scanned its register list per register.
USING columns are indexed by name and by joined column (UsingCols), row
bindings by column (ColBindings), the hidden-column set is a HashSet,
and the window stage dedups and remaps registers through hash maps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…refix

fs::canonicalize returns the verbatim form on Windows, so a write error
on a file outside the working directory read `cannot write
'\\?\C:\...\ro.csv'`. Paths in open and write errors and in the -v
writeback diagnostic go through display_path, which drops `\\?\` before
a drive letter and turns `\\?\UNC\` into `\\`; the canonical path stays
the table's identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Wide tables now cost linear time per row, so the column-count cap that
kept the quadratic code under the time limit is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jgarzik
jgarzik merged commit 39ee49a into main Oct 12, 2026
6 checks passed
@jgarzik
jgarzik deleted the updates branch October 12, 2026 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants