Add the LINQ operators Query.jl is missing - #56
Open
davidanthoff wants to merge 9 commits into
Open
davidanthoff wants to merge 9 commits into
davidanthoff wants to merge 9 commits into
Conversation
Ports Enumerable.LeftJoin, Enumerable.RightJoin and Enumerable.FullJoin, which .NET 11 added to System.Linq. The unmatched side of an outer join needs an element to pair with. That element is built by the new `_default_value`, a generated runtime counterpart of the existing `_default_value_expr`, so absent values are DataValue and never Missing. Matched elements on a nullable side are converted to the same all-null-capable type, which keeps a single element type across matched and unmatched rows — the same trick EnumerableDefaultIfEmpty already uses. full_join emits every outer row in source order first, then the inner rows whose key never appeared on the outer side. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ports Enumerable.Concat/Union/UnionBy/Except/ExceptBy/Intersect/IntersectBy. union, except and intersect shadow their Base counterparts, as map, filter, count, take, unique and join already do here. concat is fully lazy and reports a length when both sources have one. union is lazy over both sources behind a growing set of seen keys; except and intersect materialise the second source's keys on first iterate and then walk the first source lazily. All three de-duplicate their output, matching .NET. One deliberate deviation: Enumerable.ExceptBy and IntersectBy take a bare sequence of keys as their second argument, while UnionBy takes a sequence of elements. Here the key selector is applied to both sequences in all three cases. That keeps the operators consistent with each other, matches the shape of the equivalent SQL, and is what a table-shaped second argument makes natural. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ports Enumerable.Order and OrderDescending (.NET 7), Enumerable.Shuffle (.NET 10) and Enumerable.Index (.NET 9), plus Enumerable.Reverse. order and order_descending are expressed in terms of orderby with an identity key selector, so they produce an EnumerableOrderby and thenby can still follow them. index yields (index, item) NamedTuples with 1-based indices, matching Julia rather than .NET's 0-based Index(). It is lazy; reverse and shuffle have to collect their source first. shuffle takes an optional RNG, which makes it reproducible; without one it uses the default RNG and, as in .NET, is not cryptographically secure. This adds Random to the dependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ports Enumerable.CountBy and AggregateBy (.NET 9) and Enumerable.Chunk (.NET 6). count_by and aggregate_by name their key columns through the existing _key_namedtuple, so they agree with summarize: a scalar key becomes a column called `key`, a NamedTuple key contributes one column per field. A test asserts count_by and groupby-plus-summarize produce identical output. summarize remains the more general and more idiomatic way to aggregate; aggregate_by is here for LINQ parity, and its accumulator takes (accumulated, element) to match .NET's argument order. chunk pulls at most n elements per call, so a chunked source is only walked as far as the batches actually consumed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ports Enumerable.TakeWhile, SkipWhile, TakeLast and SkipLast. The Skip operators are named drop_* to match the existing `drop` rather than LINQ's `Skip`. take_while and drop_while are lazy: take_while stops at the first element that fails the predicate and never examines the rest, and drop_while passes everything through untested once the leading run is gone. take_last holds at most n elements in a CircularBuffer while walking the source. drop_last stays n elements behind the source, emitting an element only once n further ones have been read. A count of zero or less yields nothing for take_last and leaves the source unchanged for drop_last, matching .NET. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ports Enumerable.Append, Prepend and Zip. append and zip shadow their Base counterparts. append and prepend convert the extra element to the source's element type, so appending an Int to a sequence of Float64 works rather than erroring. zip stops at the shorter of its two sources, matching both Base.zip and .NET. Nothing is padded, so zip manufactures no null values — a test asserts the result contains neither missing nor DataValue. Without a result selector it yields Tuples, as Base.zip and Enumerable.Zip both do. All three are lazy, and zip pulls at most one element past the end of the shorter source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ports Enumerable.MinBy and MaxBy (.NET 6), Any, All, Contains, SequenceEqual, Aggregate, First, Last, Single and ElementAt. These return a value rather than another Enumerable, so a query ending in one is executed immediately — `count` was already the sole member of that family here. min_by and max_by return the element, not the key, and keep the first element on ties as .NET does. contains and sequence_equal compare with isequal, so DataValue nulls match each other, consistent with unique and the set operators. element_at is 1-based, matching the new `index` operator and the rest of Julia rather than .NET's 0-based ElementAt. summarize remains the idiomatic way to aggregate a table; aggregate is here for LINQ parity. Defining `any` in this module captured the Base.any call in enumerable_pivot.jl, which is now qualified. A test covers that regression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ports Enumerable.OfType and Cast. of_type keeps the elements that are instances of T and narrows the element type to T, which is useful for sources whose element type is Any or a Union and a no-op filter on a homogeneously typed table. .NET's Cast is a type assertion; the Julia counterpart is a conversion, so cast runs every element through convert and fails the way convert would on an element that cannot be represented as T. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Query.jl's operator set has been frozen roughly at the .NET Framework 3.5
System.Linq.Enumerablesurface. This adds the 38 operators it is missing — both the ones .NET has added since (.NET 6 through 11) and older ones that were never ported.This is the first of four PRs, one per package in the stack.
Query.jl(macros + docs),QueryableBackend.jl(Queryablenodes) andQueryDuckDB.jl(SQL push-down) follow once this is registered.Operators added
left_join,right_join,full_joinconcat,union,union_by,except,except_by,intersect,intersect_by*Byare .NET 6order,order_descending,reverse,shuffle,indexcount_by,aggregate_by,chunktake_while,drop_while,take_last,drop_lastappend,prepend,zipmin_by,max_by,any,all,contains,sequence_equal,aggregate,first,last,single,element_atMinBy/MaxByare .NET 6of_type,castOne commit per group, so the PR can be reviewed group by group.
Sequence,InfiniteSequence,Range,Repeat,Empty,TryGetNonEnumeratedCountand theTo*collectors are deliberately skipped — Julia ranges,Iterators.repeatedand Query's sinks already cover them.DistinctByis alreadyunique(f).Following the convention here, no macros were added to
operators.jl— the existing ones are legacy, consumed only byQuery/src/query_translation.jl's@frompath and this package's own tests, and the three most recent operators (pivot_longer,pivot_wider,summarize) have barefunction … enddeclarations. The user-facing macros will land in the Query.jl PR.Points worth a look
Nullability is
DataValue, neverMissing. The unmatched side of an outer join is built by a new_default_value, a generated runtime counterpart of the existing_default_value_expr. Matched elements on a nullable side are converted to the same all-null-capable type, so a single element type covers matched and unmatched rows — the trickEnumerableDefaultIfEmptyalready uses. Tests assert the result isDataValueand carries nomissing.except_by/intersect_bydeviate from .NET.Enumerable.ExceptByandIntersectBytake a bare sequence of keys as their second argument, whileUnionBytakes a sequence of elements. Here the key selector is applied to both sequences in all three cases. That keeps the three consistent with each other, matches the shape of the equivalent SQL (WHERE k NOT IN (SELECT k FROM b)), and is what a table-shaped second argument makes natural. Happy to switch to strict .NET parity if you'd rather.indexandelement_atare 1-based, matching Julia rather than .NET's 0-basedIndex()/ElementAt.count_by/aggregate_byname their key columns through the existing_key_namedtuple, so they agree withsummarize: a scalar key becomes a column calledkey, a NamedTuple key contributes one column per field. A test assertscount_byandgroupby-plus-summarizeproduce identical output.summarizeremains the idiomatic way to aggregate;aggregate_byis here for LINQ parity.Defining
anyin this module captured theBase.anycall inenumerable_pivot.jl, which is now qualified. A test covers that regression.union,intersect,reverse,zip,append,first,last,any,all,containsshadow Base, asmap,filter,count,take,uniqueandjoinalready do here; that one call site was the only casualty.Randomis a new dependency, forshuffle. It takes an optional RNG, which makes it reproducible.Laziness
concat,union,take_while,drop_while,drop_last,append,prepend,zip,index,of_typeandcastare lazy, with tests asserting sources are not over-read (e.g.zippulls at most one element past the end of the shorter source,chunkonly walks as far as the batches consumed).reverse,shuffle,orderand the keyed aggregations have to collect first.Testing
134 test items pass, 96 of them new. Each operator covers empty sources, empty second sequences, duplicate keys, out-of-range counts,
eltype,IteratorSize/length, and a case applied downstream of@groupbysoGroupingelements are exercised.🤖 Generated with Claude Code