Repository navigation
Conversation
Add tests/defects/audit_2026_10.rs: 124 #[ignore]d exact-output tests encoding correct behaviour for defects found by differential testing against SQLite and a code review. Groups: data integrity / IO, silent wrong answers, accepted-but-ignored clauses, SQLite semantics, and missing join/aggregate features. Every test fails today. Add tests/sqlite_diff/, the differential harness (python3 sqlite3 as oracle) that found most of them. Baseline: 367 pass, 95 DIFF, 85 err. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Loading - Delete the hand-written mmap parser (src/storage/, including its unsafe 'static transmute) and the memmap2 dependency. Every input -- .csv, delimited files and stdin -- is now read whole and parsed by the csv crate in one place (csv_handler::read_table): RFC 4180 quoting with "" unescaped, quoted fields may span lines, no trimming, UTF-8 BOM stripped, short rows padded with NULL, over-long rows an error naming the line. Rows are owned Vec<Row>; delim_handler.rs is folded into csv_handler.rs. - Stdin is read directly instead of being spooled to a temp file. - Header policy: a .csv file (and stdin) always has a header row and its '#' lines are data. Other files keep the is_likely_data_row guess and '#' comment skipping. New --no-header flag: first line is data, columns a,b,c... (or --tabledef names). - Custom/--tabledef columns no longer consume the first data row. - A second input resolving to an existing table name is an error; only a genuine --tabledef placeholder is filled in. Type inference (Value::from(&str)) - empty -> NULL; Integer iff canonical (i.to_string() == s); Float iff Display reproduces s; everything else verbatim text. No Boolean inference; 007, 1.50, 1e3, 2.0, NaN stay text. - Numeric text still acts as a number: SUM/AVG coerce it, arithmetic trims and parses it, and ORDER BY/GROUP BY/MIN/MAX order it among numbers by value (spelling breaks ties, so 7 and 007 stay distinct groups). The comparator is a total order. - Value::hash now agrees with Eq for Integer(1) vs Float(1.0). Writeback - NULL is written as an empty field; fields are quoted per RFC 4180 (plus leading/trailing whitespace) with the table's delimiter, for every format. The source layout is recorded on the Table (header present, CRLF, '#' comments) and reproduced, so headerless files gain no header and CRLF files stay CRLF. The temp file gets the original permissions, and a symlink is resolved so its target is rewritten and the link kept. Unchanged cells are now byte-identical; only quotes a field did not need are normalized away. Output - Stdout goes through the same record writer: RFC 4180 quoting, NULL as an empty field (a lone NULL as ""), REPL likewise. print_to_stdout returns io::Result; on EPIPE main stops printing quietly (later statements and --write still run) and exits 0 instead of panicking. - -F is validated: one ASCII byte other than quote/CR/LF, or the escape \t. Empty or multi-byte separators are a clean error, not a panic. - All -v diagnostics go to stderr. - Skip freeing loaded tables at exit (measurable on large inputs). Test expectation changes (decided semantics, not regressions) - NULL now prints as an empty field: golden, coalesce, left/right/full join, window expectations updated; a lone NULL row prints "". - A header containing the delimiter is quoted (math_functions ROUND test). - ddl -v tests read diagnostics from stderr. - headerless: the guess no longer applies to .csv, so those tests use .txt files with -F, (still exercising the guess); added --no-header and headerless-writeback tests. tabledef writeback test now asserts the exact file (no header added). - csv_handler: '#' skipping is opt-in for .csv, so the comment tests use load_with_options(comments: true). test_csv_with_custom_columns expected 3 rows only because custom columns wrongly consumed the first line as a header; with custom columns the file has no header, so it is 4 rows. Red list: the 22 group-1 tests in defects/audit_2026_10.rs pass and are un-ignored, as is inserted_float_literal_is_numeric (fixed by SUM coercing numeric text). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Type inference on load, NULL as an empty field, header rules and --no-header, comment lines only outside .csv, ragged-row handling, -F validation, RFC 4180 output, verbose output on stderr, and byte-exact writeback (layout, permissions, symlinks). Drop the mmap storage docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 2 step 1 of the pipeline unification.
code_expr now compiles every expression form directly against a NameCtx:
IS [NOT] NULL, [NOT] LIKE/ILIKE, [NOT] BETWEEN, [NOT] IN (list), IN /
EXISTS / scalar subqueries, CASE (both forms), CAST, SUBSTRING and unary
+/-. Subqueries keep their current implementations (compile-time
interpreter for uncorrelated, runtime loop for correlated) but take the
caller's NameCtx; correlated conditions are compiled by code_expr in a
scope whose outer sources are the caller's.
Predicates are values under SQL three-valued logic (1 / 0 / NULL):
- new And / Or opcodes (SQLite OP_And / OP_Or truth tables)
- BETWEEN = (x >= lo) AND (x <= hi); NOT applies Not
- IN folds Eq results with Or: 1 on a match, else NULL if x or any item
is NULL, else 0; applies to IN (list) and IN (subquery)
- Like opcode yields NULL for a NULL operand
Filters still test with IfZ, which rejects NULL along with 0.
Deleted: legacy_single_source, compile_operand_legacy,
compile_where_operand, compile_where_condition, compile_binary_comparison,
compile_projection_expr, code_predicate, the duplicate
compile_{scalar,in,exists}_subquery, compile_correlated_{where,operand},
and the no-FROM compile_expr dispatcher. FROM-less SELECT and INSERT
VALUES use code_expr with NameCtx::empty(); INSERT ... SELECT uses the
source table's scope. FROM-less SELECT now honours WHERE.
INSERT VALUES now reserves its row registers (temporaries could alias a
column) and Null is emitted with the register in p2.
compiler.rs: 7287 -> 5150 lines.
Red tests now passing: not_in_list_excludes_null,
not_between_excludes_null, not_of_like_excludes_null,
not_of_or_excludes_null, not_in_subquery_with_null_is_empty,
literal_not_in_list_with_null_is_unknown, null_and_true_is_null,
delete_not_in_keeps_null_row, fromless_select_applies_where,
insert_column_list_maps_by_name,
insert_multi_row_omitted_columns_are_null,
insert_multi_row_omitted_middle_column.
sqlite diff harness: pass 371 -> 403, DIFF 91 -> 69, sqawk_err 85 -> 75.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every SELECT now goes through a single FROM planner (src/vm/compiler_from.rs) in place of six join generators and the single-table scan paths. A FROM clause -- one table, a comma list, or a chain of INNER/LEFT/RIGHT/FULL/CROSS joins with ON, USING or NATURAL -- becomes a left-deep nest of loop levels, one cursor per source: - ON conjuncts are evaluated at their join's level, before its match point; WHERE conjuncts after it, at the innermost level that has their tables, so outer joins NULL-extend before WHERE filters (LEFT JOIN ... WHERE r.k IS NULL is an anti-join, not a no-op). - Outer joins keep a match flag per NULL-extendable group. An unmatched row re-enters the nest once with the group's cursors on a NULL row (NullRow, now SQLite's cursor form). RIGHT JOIN swaps the loop order; FULL JOIN adds a pass over unmatched right rows (MarkRowMatched / IfRowMatched), with WHERE re-applied there. - USING / NATURAL lower to ON equalities plus merged output columns (coalesced, shown once in SELECT *). - An equality between the inner table and outer tables becomes a hash join: HashInsert builds an index once, HashProbe/HashNext walk the matches. Keys are normalized so values Eq calls equal share a bucket; NULL keys never match; the full condition is re-checked on each candidate. The projection, aggregate/GROUP BY and ORDER BY stages consume rows through a body callback, so expressions, functions, CASE, HAVING and aggregates work over any join, and every name goes through code_expr against one NameCtx (ambiguous unqualified names are errors). Aggregation collects aggregates recursively from SELECT and HAVING and binds keys and aggregates by text in a post-aggregation scope, giving GROUP BY expressions/ordinals and HAVING on aggregates not selected. SorterSort can jump when empty, so GROUP BY and ORDER BY over zero rows return the header. The golden test that required an aggregate over a LEFT JOIN to be rejected now asserts SQLite's answer instead. 200k x 1k equi-join: 18.8s -> 0.06s (COUNT), LEFT JOIN projection 27.5s -> 0.16s. Single-table GROUP BY unchanged (0.14s -> 0.13s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Removes #[ignore] from every red-list test the planner turned green (outer join WHERE, CROSS JOIN WHERE/aliases, right-table column shift, USING, NATURAL, two LEFT JOINs, expressions in join SELECT lists, HAVING on joins, comma-join GROUP BY keys and OFFSET, aggregates over outer joins, and the GROUP BY / HAVING / LIMIT / ORDER BY cases the shared stages fixed). Adds tests/join/planner.rs: exact-output tests (expected values from SQLite) for LEFT chains, LEFT then INNER, FULL with WHERE, RIGHT over an inner-joined left side, USING / NATURAL with SELECT *, ambiguity and ON-ordering errors, and hash-join keys: duplicates, NULLs, integer vs float and decimal text, and a LEFT hash join with no matches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every SELECT core is now FROM planner -> WHERE -> [aggregate stage -> HAVING] -> [window stage] -> output stage, in the new src/vm/compiler_select.rs. Output stage: output registers -> ORDER BY (ordinals, aliases, projected expressions, else hidden sort keys incl. aggregates not in SELECT) -> DISTINCT (inline, keyed ephemeral table) -> one LIMIT/OFFSET implementation (any integer expression, MustBeInt; LIMIT 0 skips the scan; negative LIMIT means no limit). Rows go to a SelectDest-style destination. Compound SELECTs follow SQLite's multiSelect: arms write into keyed ephemeral tables (IdxInsert/IdxDelete/Found/NotFound) or a compound sorter. Header from the leftmost SELECT, ORDER BY/LIMIT apply to the compound, per-arm DISTINCT stays local, set results come out sorted, and arms with different column counts are an error. A FROM-less SELECT is a FROM plan with zero sources. Aggregates: HAVING and sort keys may name SELECT aliases; AggFinal gets the real function type so SUM/AVG/MIN/MAX over no rows is NULL; one aggregate-name helper in src/aggregate.rs replaces the duplicated lists. Window rows are staged in an ephemeral table and read back through the output stage. Deleted: compile_select_literal, compile_select_plain, emit_result_post_processing, the SortResults/Distinct/Limit/Intersect/Except post-processing opcodes, the Integer p1=-1 result-count hack, and the unused Phase 4C result-row index tracking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Un-ignored (now passing): union_applies_order_by, union_applies_limit, union_all_applies_order_by, intersect_applies_order_by, except_applies_order_by, union_header_from_left_side, union_all_distinct_applies_to_one_side_only, literal_union_applies_limit, fromless_select_applies_limit, having_with_alias, empty_set_aggregates_are_null. New exact-output tests (expected values from SQLite) cover compound ORDER BY/LIMIT/OFFSET, 3-arm UNION/EXCEPT and INTERSECT chains, ORDER BY alias, hidden sort keys and DISTINCT, ORDER BY an aggregate not in SELECT, FROM-less WHERE/ORDER BY/LIMIT, OFFSET past the end, LIMIT edge cases, and empty-input aggregates with and without GROUP BY. Updated goldens: UNION/EXCEPT output is sorted (as SQLite); FROM-less SELECT headers now come from aliases/expressions like any other query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
It is a broad, high-risk refactor (storage removal, a new join planner, type-inference and NULL-rendering changes, new VM opcodes) whose central engine logic is not included in the reviewable diff, so it needs final human review.
0 open findings
What changed in this PR
This PR ("Fixes") is a large refactor of sqawk that consolidates storage into in-memory Vec<Row> tables (removing the mmap/delim_handler modules and the memmap2 dependency), replaces the join compiler with a new unified FROM planner (compiler_from.rs) supporting hash joins and three-valued AND/OR, and overhauls value rendering so that NULL now prints as an empty field (round-trip/canonical type inference). The accompanying test updates realign expectations with the new NULL-as-empty-field behavior, tighten a writeback assertion, and add a SQLite differential-test corpus.
Changes:
- Render NULL as an empty field on stdout/writeback and update window-function tests accordingly.
- Strengthen the tabledef writeback test to assert exact file contents (verifying no header row is added back).
- Add
tests/sqlite_diff/queries.sqlandqueries2.sqlas differential-test query corpora (joins, aggregates, subqueries, window functions, DML).
| File | Description |
|---|---|
| tests/window/mod.rs | Updates LAG/LEAD expectations from ...,NULL to empty trailing field, matching new NULL rendering. |
| tests/tabledef/mod.rs | Replaces a loose contains("999") check with exact assert_eq! on file contents, confirming headerless writeback. |
| tests/sqlite_diff/queries.sql | New differential-test corpus covering basics, LIKE, CASE/CAST, aggregates, ORDER BY, string fns, subqueries, joins. |
| tests/sqlite_diff/queries2.sql | New differential-test corpus for joins, correlated subqueries, window functions, set ops, and DML round-trips. |
The visible test diffs are correct and internally consistent with the broader behavior change (verified field_str() renders Value::Null as "" at table.rs:285 while Display renders "NULL" at table.rs:156). I found no objective defects in the reviewable diff. Note that the core bytecode VM implementation (src/vm/engine.rs), which carries the most new logic for the hash-join and three-valued opcodes, is not part of this diff and could not be directly commented on.
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
A scalar, EXISTS or IN subquery is now compiled by the same query compiler as any statement (compile_query_to), as a fragment of the enclosing program, SQLite style. Every SELECT feature works inside a subquery (joins, GROUP BY, HAVING, DISTINCT, ORDER BY, LIMIT/OFFSET, compounds), and a subquery works in every expression position, including UPDATE SET, DELETE WHERE and INSERT VALUES. - Name resolution: NameCtx links to the enclosing scope; lookup resolves innermost first, then outward, and reports the depth. Any outward reference marks the enclosing subqueries correlated -- no qualifier-string matching (fixes the unaliased same-name outer table). - Uncorrelated subqueries run once (Once guard); correlated ones rerun per evaluation. A subquery's cursors and hash indexes are set up once, and an equality against an outer column becomes a hash probe at level 0. - Destinations (Sink): scalar -> register, first row (NULL if none; >1 column is "sub-select returns N columns - expected 1"); EXISTS -> flag, stops at the first row; IN -> keyed ephemeral set probed with Found, with SQL three-valued logic. Row-value IN is an Unsupported error. - Subquery FROM sources get their own cursor ids; aggregate accumulators are reset at the start of each run; INSERT VALUES no longer truncates the register count. Deleted: the compile-time subquery interpreter (evaluate_scalar_subquery, evaluate_exists_subquery, evaluate_in_subquery_values, evaluate_where_at_compile_time, evaluate_expr_at_compile_time, compare_values, extract_agg_column_index), the inline correlated compilers and detect_outer_references, current_outer_alias, the aggregate.rs evaluation code, and the never-emitted Exists/NotExists/InitCoroutine/ Yield/EndCoroutine opcodes. Red list: 7 tests un-ignored. Harness 449 -> 451 pass, no regressions. 200k rows: IN subquery 1.8s -> 0.05s, correlated EXISTS 13.4s -> 0.05s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the streaming window pass (one shared state, first OVER spec only, running totals for OVER (), no peers) with a window stage in the SELECT pipeline, after FROM/WHERE/aggregation/HAVING: - Each finished row goes into an ephemeral table with every value the output needs (all FROM columns, or a grouped query's keys and aggregates) plus each window's partition keys, order keys and arguments. - One WindowCompute per distinct PARTITION BY/ORDER BY (any expressions, any number) stably sorts the table and evaluates each function with its own state and frame (src/vm/window.rs). - The output stage reads rows back with each window call bound like an aggregate, so window calls work inside expressions and in ORDER BY. Functions: ROW_NUMBER, RANK, DENSE_RANK, PERCENT_RANK, CUME_DIST, NTILE, LAG/LEAD with offset and default, FIRST_VALUE, LAST_VALUE, NTH_VALUE, and COUNT(*)/COUNT/SUM/AVG/MIN/MAX. Default frame is RANGE UNBOUNDED PRECEDING to CURRENT ROW (peers share values; whole partition without ORDER BY). ROWS frames with any bounds, RANGE with UNBOUNDED/CURRENT ROW bounds, and named windows are supported. Windows work over joins, over grouped rows (SUM(COUNT(*)) OVER ()), in subqueries, and with SELECT *. RANGE offsets, GROUPS, FILTER and DISTINCT on window functions are errors; a window call in WHERE/GROUP BY/HAVING/an aggregate is "misuse of window function". Deleted: WindowAggStep, WindowValue, WindowFinalize opcodes and the old compile path; AggAccumulator now owns step/finalize, shared by AggStep, AggFinal and the window pass. Tests: un-ignore 6 red-list window tests; rewrite ntile/first_value/ last_value/percent_rank "is_error" red tests to assert SQLite's results; the two golden tests that expected frame/wildcard rejection now assert correct output; 21 new exact-output window tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Arithmetic (src/vm/arith.rs): integers stay i64 through + - * / % with
checked ops; an overflowing result is computed as a REAL. Integer /
truncates, % is the integer remainder, x/0 and x%0 are NULL. Text
operands are read by their longest numeric prefix. Integer-vs-real
comparison is exact. Text in a condition is read as a number ('x' is
false). A negated literal is one literal, so -9223372036854775808 is
an integer.
CAST follows SQLite: text to INTEGER takes the integer prefix and
saturates, real to INTEGER truncates and saturates, REAL takes the
numeric prefix, and any type name is mapped by SQLite's affinity rules
(BIGINT, VARCHAR(n), NUMERIC, DECIMAL, ...). CAST AS BOOLEAN yields 1/0.
Scalar functions: one Function opcode (modelled on OP_Function) and one
FuncDef table (src/vm/func.rs) replace StringFunc/MathFunc/DateFunc and
their argument-registers-encoded-in-P4 convention. Arity is checked at
compile time from the table. String functions count characters; `||`
is NULL-propagating while CONCAT treats NULL as ''; SUBSTR follows
SQLite's negative/zero start rules; REPLACE with an empty pattern is a
no-op. New: INSTR, TYPEOF, IFNULL, IIF (lazy), LPAD/RPAD (PostgreSQL
semantics), CONCAT_WS, HEX, CHAR, UNICODE, SIGN, PRINTF/FORMAT,
multi-argument MIN/MAX, two-argument LTRIM/RTRIM and
TRIM([LEADING|TRAILING|BOTH] chars FROM s). ROUND rounds half away from
zero on the stored decimal value; ABS(i64::MIN) is an overflow error.
Scalar calls with FILTER, DISTINCT, WITHIN GROUP, IGNORE NULLS or
parameters are rejected instead of ignored.
LIKE (src/vm/like.rs): a direct matcher replaces the per-row regex
compile. ASCII case-insensitive (ILIKE folds Unicode), `_`/`%` match
any character including newline, ESCAPE is honoured, the pattern may be
any expression, and the compiled pattern is cached per instruction.
Dates (src/vm/datetime.rs): DATE/TIME/DATETIME/JULIANDAY/UNIXEPOCH/
STRFTIME and CURRENT_* with SQLite's time strings and modifiers, using
Hinnant's days_from_civil/civil_from_days. CURRENT_DATE was computed
with a 365-day year and 30-day months.
Red list: 17 tests un-ignored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; NULLS FIRST/LAST Unsupported syntax is an error, never ignored (src/vm/compiler_check.rs). INSERT, UPDATE, DELETE, Query and Select are destructured exhaustively, so a field added by a future sqlparser fails to compile until it is handled. Now rejected instead of silently ignored: INSERT OR <action>, ON CONFLICT, RETURNING (INSERT/UPDATE/DELETE), INSERT OVERWRITE, UPDATE ... FROM, DELETE ... USING, DELETE t FROM t JOIN ..., UPDATE / DELETE with ORDER BY or LIMIT, WITH (CTEs were reported as missing tables), FETCH, FOR UPDATE, LIMIT BY, TOP, QUALIFY, SORT/CLUSTER/ DISTRIBUTE BY, LATERAL VIEW, SELECT INTO, GROUP BY WITH ROLLUP, TABLESAMPLE, table hints, LATERAL subqueries, WITHIN GROUP, IGNORE NULLS, ORDER BY inside an aggregate's arguments, CAST ... FORMAT, LIKE ANY, blob/placeholder literals (X'00' was the string "X'00'"), and CREATE TABLE constraints, IF NOT EXISTS, PARTITIONED BY and ROW FORMAT. INSERT ... SELECT and CREATE TABLE ... AS now compile their query with the SELECT pipeline and write rows through a new Sink::Table. The old hand-rolled paths read only the projection and WHERE: ORDER BY, LIMIT, GROUP BY, joins, DISTINCT and aggregates were dropped, and CTAS skipped non-column expressions, misaligning the columns after them. Aggregates (accumulators moved to src/vm/agg.rs): FILTER (WHERE ...) is implemented in both aggregate-stage paths; GROUP_CONCAT(x[, sep]) / STRING_AGG(x, sep), also as window functions; TOTAL; SUM of integers that overflows is the "integer overflow" error (AVG and TOTAL continue in floating point) instead of a silent wrap. NULLS FIRST / NULLS LAST is implemented for ORDER BY, compound ORDER BY and window ORDER BY via a KeyOrder sort-key type shared by the sorter, ephemeral tables and the window pass. Unnamed derived tables are accepted, as in SQLite. Red list: the remaining 9 tests pass; the FILTER and NULLS tests are rewritten to assert SQLite's results. The red list is empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Table names resolve case-insensitively (`FROM EMP` finds emp.csv). - IS [NOT] DISTINCT FROM (NULL-safe equality). - Derived tables are materialized wherever they appear -- in subqueries, compound arms, INSERT ... SELECT and CREATE TABLE ... AS sources, and UPDATE/DELETE expressions -- not only in the top-level FROM. They are registered under internal names, so an alias may repeat across subqueries or shadow a real table (as in SQL), and an alias is no longer required. Registrations are dropped if materialization fails. - An unsupported statement is named by its keywords instead of dumping its syntax tree. - Harness: detect referenced tables case-insensitively. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
doc/sql_reference.md: arithmetic, CAST affinity rules, LIKE, every new function, aggregates (TOTAL, GROUP_CONCAT, FILTER, overflow), date functions and modifiers, NULLS FIRST/LAST, derived tables anywhere, INSERT ... SELECT, constraints, an "Unsupported syntax" section and a "Differences from SQLite" table. README feature list, user guide "SQL Semantics" section, CLAUDE.md module notes. INSERT ... VALUES rejects ORDER BY / LIMIT on the VALUES list. Red list: tests/defects says the list is empty again (all 2026-10 defects fixed; the tests are regression cover), with the fixed tests' comments in the past tense. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/lib.rs made Cargo build and publish a `sqawk` library exposing every internal module, which docs.rs then presented as an API. Nothing depends on it: main.rs declares its own modules, and the only user was tests/csv_handler, whose five tests move into csv_handler.rs as unit tests. Remove the docs.rs `documentation` link and README badge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tsq is a local development tool, but as a second [[bin]] of the sqawk package `cargo install sqawk` installed it too. It now lives in tsq/ as a workspace member with `publish = false`, so the published crate contains only the sqawk binary. - Root Cargo.toml: [workspace] with default-members = ["."], so plain `cargo build` / `cargo publish` act on sqawk alone; drop the tsq [[bin]], the now-unneeded default-run, and libc (only tsq used it). - tsq/tests/tsq.rs: the tsq tests, finding sqawk in the shared target dir. - CI builds, tests and lints with --workspace so tsq stays covered. - README / user guide / CLAUDE.md: tsq is development-only (`cargo run -p tsq -- ...`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ignored exact-output tests for the defects found by the 2026-10 code review of the updates branch: date modifier overflow and fractional month/year modifiers, printf width/precision overflow and %g exponent choice, SUBSTR i64::MIN length, DISTINCT aggregates keyed on Debug text, GROUP BY merging text with integers, comment lines lost on --write, and unbounded LPAD/RPAD/printf/REPLACE result lengths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Date modifiers: SQLite's per-unit limits reject huge, infinite and NaN
counts (they overflowed the add); months/years accept fractions with
SQLite's algorithm; the 0000-9999 range is checked after the last
modifier, as in SQLite.
- PRINTF: width/precision parse saturating and are capped at the length
limit; precisions past u16::MAX no longer panic inside Rust formatting;
%g picks fixed vs exponent form after rounding; infinity prints Inf.
- SUBSTR: an i64::MIN length no longer overflows on negation.
- DISTINCT aggregates key values as SELECT DISTINCT does (1 = 1.0, text
'1' distinct) instead of by Debug text; GROUP BY now keeps text '1'
apart from integer 1, agreeing with DISTINCT and SQLite.
- LPAD/RPAD, PRINTF, REPLACE, HEX and concatenation refuse results over
1,000,000,000 bytes ("string or blob too big") before allocating.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Non-.csv delimited files are read with comment skipping, and the skipped lines were simply lost, so --write deleted them. The reader now records each comment line verbatim (with its own line ending) and anchors it to the row that follows it, or to the top or the end of the file; writeback puts them back, so a no-op UPDATE is byte-identical. UPDATE keeps rows in place; DELETE leaves a deleted row's comments in front of the next surviving row (or the trailing comments); INSERT appends before the trailing comments; TRUNCATE keeps them all. Also drops the phantom all-NULL row the csv crate produced for a final comment line without a line ending. Red list empty again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main replaced .github/workflows/TestingCI.yml with ci.yml (PR #7) while updates edited TestingCI.yml to add --workspace for the tsq crate: a modify/delete conflict, which GitHub's web editor cannot resolve. Resolution: drop TestingCI.yml in favour of main's ci.yml, and carry the --workspace flag into ci.yml's build, clippy, test, release-test and MSRV check steps so the unpublished tsq/ crate stays built and tested. Refresh two stale comments (src/tsq.rs and src/storage/mmap.rs no longer exist; there is no library target). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No description provided.