Skip to content

Release v0.6 - #233

Open
conradbzura wants to merge 12 commits into
masterfrom
release
Open

Release v0.6#233
conradbzura wants to merge 12 commits into
masterfrom
release

Conversation

@conradbzura

Copy link
Copy Markdown
Collaborator

Auto-generated by the cut release workflow.

conradbzura and others added 12 commits July 8, 2026 16:43
Table validated coordinate_system and interval_type in __post_init__ but
was a plain dataclass, so assigning either field afterwards installed a
value the constructor rejects. The value is not inert: it selects the
coordinate translation the emitted SQL performs. Setting it to a third
string produced an ON predicate that is neither the 0-based form nor the
1-based one but a half-shifted hybrid matching no coordinate system, and
transpile raised nothing.

Freezing makes __post_init__ the single way in, so validation holds for
the object's whole lifetime rather than only at construction. Callers
vary a config with dataclasses.replace, which re-runs that validation.
Immutability also restores __hash__, which the generated __eq__ had set
to None.

Separately, _build_tables duck-typed every non-str entry for a .name
attribute. An arbitrary object therefore reached pass 1 and failed there
with an AttributeError naming an internal column attribute, telling the
caller nothing about which argument was wrong. It now rejects the entry
with a TypeError naming the offending type.

Adds tests for the two field validations, which had none.
Target, Capabilities, GenericTarget, DuckDBTarget and DataFusionTarget
are all exported from the package root and autodocumented, yet the only
public function that consumes a target rejected every one of them and
reported the object's repr as though it were a misspelled name.

Selecting a target by name stays the documented default and is the right
seam for the plugin-distribution case, where a package ships a target and
users select it without importing it. What that does not cover is the
one-off: making a bespoke Target selectable meant mutating the
process-global registry, and the registration outlived the call. The
object path removes that side effect and is purely additive.

GenericTarget is now accepted as an instance while the name "generic"
still raises. That asymmetry is deliberate: None remains the one public
spelling for the generic target, but an instance is unambiguous.

Also drops the three transpile overloads. All three returned str and the
widest admitted a bare str, so the set collapsed to the implementation
signature and taught a type checker nothing. Their stated purpose was
editor completion of the built-in dialect names, which the DialectName
literal alias in the signature preserves without three public typing
artifacts.
DuckDB's IE_JOIN is INNER-only, so a LEFT or RIGHT outer join carrying a
column-to-column INTERSECTS fell through to the naive predicate: a hash
join on chrom with the position inequalities as a residual filter, which
is quadratic when the chromosome key has low cardinality.

The query is now rewritten as a UNION ALL of an INNER half for the
matched pairs, a NOT EXISTS half for the preserved side's unmatched rows,
and a third branch for its NULL-chromosome rows. The first two reach the
fast operator; the third is a filtered scan, and it is load-bearing
rather than defensive, because both partitions come from SELECT DISTINCT
chrom where a NULL renders as a NULL literal that string_agg skips. RIGHT
is served by swapping the FROM and joined tables so one LEFT-shaped path
covers both. Shapes the rewrite cannot express decline as one unit.

Two properties of the emission decide whether it is worth taking, and
both are settled by execution rather than by plan inspection, which
reports IE_JOIN either way.

The first is contig cardinality. One UNION ALL branch is emitted per
distinct chromosome, so cost tracks that count while the plain
predicate's does not: measured at 262,144 rows per side, the partitioned
form runs 0.79s against 4.53s naive at 24 contigs and 57.2s against 0.11s
at 3,000. The partition's cardinality is a property of the data, so the
choice is made at execution time by a CASE over the partition's own row
count, above which the same query binds with the chromosome equality
inlined instead. The unmatched half also partitions on the chromosome
INTERSECT rather than the preserved side's distinct chromosomes, carrying
its left-only chromosomes in one non-partitioned branch: those rows
cannot match, so a branch apiece scans both tables to prove an emptiness
the partition already knows.

The second is session state. Each half declares a DuckDB session
variable, and the emitted script cannot release them because the final
statement has to be the SELECT. Names are therefore a digest of the
variable's own rendered value, which bounds a session's variable set by
the number of distinct query shapes rather than the number of calls;
naming them per call retained 26 to 84 MB after 50 to 200 queries.

The naive-predicate fallback resolves through the registry rather than
calling the built-in directly, so a user expander registered on
(GenericTarget, Intersects) reaches the shapes this target declines
instead of applying under dialect=None alone.
The bedtools lane declared its integration marker in conftest.py, where
pytest does not honour pytestmark, leaving all of its tests unmarked. Both
documented selection commands therefore inverted: running with the
integration marker skipped the whole lane, and running without it pulled
in the lane and its bedtools and pybedtools dependencies. CI was
unaffected because it runs the suite with no marker filter, so nothing
surfaced it. Each module now declares the marker itself, matching the
datafusion lane's working convention.

The cross-target oracle asserted only that the three targets agree, which
it would continue to do if duckdb silently stopped decomposing. It now
also pins which plan duckdb took, through a target-to-SQL map the oracle
fixture exposes.

Adds bedtools oracles for the RIGHT outer join, expressed as the left
outer join with the operands swapped, and for duplicate input rows, the
multiplicity axis a UNION ALL rewrite is most likely to break and which
the property lane could not reach because it draws from unique inputs.
The dialect parameter promised that an unqualified projection raises at
transpile time. That holds for the INNER, SEMI and ANTI shapes and not
for the outer joins, which decline silently to the naive predicate
instead, so one stated rule covered two behaviours with no way for a
caller to tell which applied. The same paragraph carried an inline list
of declined shapes that had drifted behind the guide it points at, so the
list is gone and the pointer stays: a second copy of an enumeration only
drifts again.

Returns said the result was a SQL query. Under duckdb an accelerated join
returns a multi-statement script, and a driver that splits statements or
forwards only the last drops the variable the SELECT reads and yields
empty results.

The partition-count ceiling decides at execution time whether a query
takes the per-chromosome form at all, and it is the difference between
winning and losing by orders of magnitude on a scaffold-level assembly.
The performance guide now carries the measured crossover and the
reasoning behind the bound, rather than generalising from a single
low-contig measurement to a claim of large speedups at scale.

Two further claims are restated from measurements rather than intuition.
The NULL-chromosome branch, described as costing a linear scan of the
preserved table, is pruned by null statistics to a quarter of a percent
of runtime on a base table. The per-chromosome LEFT JOIN comparison
asserted an inflection near 1e5 rows that the recorded figures do not
support; it now states the two points that were actually measured.

Also documents what the session-variable token is, now that it addresses
the variable's content rather than being random, and gives
transform_to_sql its own contract in place of a description of its
sibling: the script shape, the ValueError it raises, and that the query
it is handed is never mutated.
…he outer join into INNER pairs plus unmatched rows — Closes #95 (#223)
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.

1 participant