Negated conditions and ILIKE; operators that stream - #10
Merged
Merged
Conversation
These parsed but were refused by the translator. Now:
- `<>` / `!=` is {:cmp, :!=, ...}. NOT IN, NOT BETWEEN, ILIKE and NOT ILIKE
are {:not_in, ...}, {:not_range, ...}, {:ilike, ...} and
{:not_ilike, ...}. They're checked on the rows read (Predicate.pushdown/1
is :filter), since no index can serve them. A NULL field matches none of
them, as in SQL, so a NULL status is neither = nor <> 'paid'.
- NOT is pushed into the condition it negates, where SQL's NULL rules
keep each opposite exact: NOT (a < 1) is a >= 1, NOT (a = 1) is a <> 1,
NOT IN/LIKE/ILIKE/BETWEEN/IS NULL flip, NOT NOT cancels, and
NOT (a OR b) is NOT a AND NOT b. NOT (a AND b) would need OR, so it's
refused with OR's message.
- Rewrite turns a single-value NOT IN into <>.
- A filter-only condition on the primary key '_' now says what '_'
supports (=, <, <=, >, >=, BETWEEN, IN) instead of "cannot be combined
with this query shape".
The README, the TUI help and completion cover the new forms. There are
unit tests for the translation (every NOT case), evaluation (NULL
semantics, case-insensitive and non-ASCII ILIKE) and the rewrite, plus
FoundationDB tests on the seeded users.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PFBZUrerge6bbWVCHpS5Ko
Operators used to take the whole list of rows, and a batched query across tenants concatenated every batch before the first one ran. So memory held every row read, and a LIMIT still read every batch. Efsql.Pipeline runs a plan's operators a chunk at a time (new/1, feed/2, finish/1), and each operator keeps only what it has to: - filter and project keep nothing; - limit counts, and once it has its rows feed/2 says :halt, so the source stops reading; - a sort directly followed by a limit is fused into a running top-n; - a sort alone keeps every row, since it has to; - aggregate keeps a running total per group. Efsql.Aggregate is now new/2, add/2 and finish/1, with run/3 unchanged. The executor feeds every plan through a pipeline. A single read is one chunk. Batches are a stream (Executor.stream_concurrently/3, replacing map_concurrently/3), so each batch is processed as it arrives, and a halt stops the batches not yet read, cancelling any in flight. sum and avg now switch to Decimal at the first Decimal value instead of converting every value when any is one. The results differ only if one field mixes floats with Decimals. A property test checks that for random rows, random planner-shaped operator chains and random chunking, the pipeline's result equals the operators applied to all the rows at once, early halts included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PFBZUrerge6bbWVCHpS5Ko
SQLGen.mangle/2 inserts invalid UTF-8 on purpose (a lone 0xC3). The next mangling round then split the string with String.graphemes/1, which raises on invalid UTF-8 under OTP 28's unicode_util, so the "mangled queries never crash" property test failed on some seeds. The string is now cut into codepoints, with each invalid byte a unit of its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PFBZUrerge6bbWVCHpS5Ko
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.
Two independent changes, one commit each.
1.
NOT,<>,NOT IN,NOT BETWEEN,ILIKE,NOT ILIKEThese already parsed, but the translator refused them. Now they work:
NOTis pushed into the condition it negates, and SQL's NULL rules keep each opposite exact:not (a < 1)isa >= 1(both false whenais NULL), andnot (a = 1)isa <> 1;IN,LIKE,ILIKE,BETWEENandIS NULLflip, andnot notcancels;not (a or b)isnot a and not b;not (a and b)would needOR, so it gets OR's "not supported" message.{:cmp, :!=, …},{:not_in, …},{:not_range, …},{:ilike, …}and{:not_ilike, …}. An index can't serve any of them, so they're checked on the rows read (Predicate.pushdown/1is:filter). The rest of theWHEREstill narrows what's read.statusis neither= 'paid'nor<> 'paid'.NOT INbecomes<>._: a condition an index can't serve, like_ <> 'x', now says what_does support (=,<,<=,>,>=,BETWEEN,IN). Before, it said "cannot be combined with this query shape".The README, TUI help and completion cover the new forms.
2. Operators that process rows as they arrive (
Efsql.Pipeline)Operators used to take the whole list of rows. A batched cross-tenant query (
\set tenant_batch N) concatenated every batch before the first operator ran, so it held every row it read, and aLIMITstill read every batch.Efsql.Pipelineruns a plan's operators a chunk at a time, and each operator keeps only what it needs:LIMITwithoutORDER BY: stops reading once it has its rows. Unread batches are skipped, and ones already running are cancelled.ORDER BY … LIMIT n: keeps only the best n rows so far.GROUP BYand aggregates: keep a running total per group.Efsql.Aggregateis nownew/2,add/2andfinish/1, andrun/3is unchanged.The executor feeds every plan through a pipeline; a normal read is one chunk. Batches become a stream (
Executor.stream_concurrently/3, replacingmap_concurrently/3), so a halt stops fetching.One behaviour change:
sum/avgswitch toDecimalat the firstDecimalvalue, instead of converting every value when any is aDecimal. That only matters if one field mixes floats andDecimals, which an Ecto field can't.Tests
NOTtranslation;ILIKE) and the new rewrite;ILIKEon the seeded users;>=while<>is filtered afterwards;ORDER BY … LIMITandLIMITacross real batches.All 308 tests that don't need FoundationDB pass locally on the combined branch, with no new compiler warnings. This CI run is the first time the FoundationDB tests run on either change.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PFBZUrerge6bbWVCHpS5Ko
Generated by Claude Code