From 54a039a4c202282854e6e16d0734a74f0f7ee26b Mon Sep 17 00:00:00 2001 From: Christophe Pettus Date: Sun, 21 Jun 2026 09:29:48 -0700 Subject: [PATCH] docs: document param-ordering, agg-validation, and Attr dataclass invariants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comment-only pass; no behavior change. - compiler.py: ADD a third hard invariant to the module header — params are appended in the same left-to-right order their placeholders appear in the SQL text (required by positional paramstyles like qmark/format), with Division called out as the subtle case. - aggregates.py: EXTEND output_domain to record the cross-file invariant that Grouping.__post_init__ pre-validates aggregate attrs, so the schema lookup cannot raise in normal flow. - predicates.py: EXTEND the Attr.__hash__ note explaining why a hand-written __eq__/__hash__ coexists with @dataclass(frozen=True), and how the relation nodes take the opposite eq=False route. --- coddpiece/aggregates.py | 5 +++++ coddpiece/compiler.py | 8 ++++++++ coddpiece/predicates.py | 5 +++++ 3 files changed, 18 insertions(+) diff --git a/coddpiece/aggregates.py b/coddpiece/aggregates.py index 5a919d9..c3ad76c 100644 --- a/coddpiece/aggregates.py +++ b/coddpiece/aggregates.py @@ -33,6 +33,11 @@ def output_domain(self, schema: Schema) -> type: # attr == "*" only makes sense for COUNT, but guard defensively. if self.attr == "*": return int + # Cross-file invariant: the only caller, Grouping._schema(), runs + # after Grouping.__post_init__ has already verified each non-"*" + # aggregate attr exists on the child schema, so this lookup cannot + # raise here in normal flow. (Calling output_domain directly on an + # AggSpec with a bogus attr would still raise AttributeError_.) return schema[self.attr].domain def algebra(self) -> str: diff --git a/coddpiece/compiler.py b/coddpiece/compiler.py index 1f2e9cf..b961833 100644 --- a/coddpiece/compiler.py +++ b/coddpiece/compiler.py @@ -17,6 +17,14 @@ * Each expression node is visited exactly once per compile, with one documented exception: Division, which needs the dividend subtree twice to build its correlated subquery. + * Parameters are appended to self.params in the SAME left-to-right + order their placeholders appear in the emitted SQL text. Positional + paramstyles (qmark "?", format "%s") bind the Nth value to the Nth + placeholder, so any compile method that emits multiple sources must + visit them in textual order. Division relies on this most subtly: + the outer dividend FROM is rendered first (via _as_source at the + top), then the divisor, then the re-visited inner dividend — which + is exactly the order those placeholders appear in the final string. """ from __future__ import annotations diff --git a/coddpiece/predicates.py b/coddpiece/predicates.py index 7ba482b..bec19fa 100644 --- a/coddpiece/predicates.py +++ b/coddpiece/predicates.py @@ -87,6 +87,11 @@ def __repr__(self) -> str: # only hash-equal if they reference the exact same relation instance. This # is correct for the expression tree where identity, not structural equality, # determines equivalence. + # Non-obvious mechanic: @dataclass(frozen=True) would normally synthesize + # both __eq__ and __hash__, but dataclass only fills in dunders NOT already + # defined in the class body — so these hand-written __eq__/__hash__ win and + # coexist with frozen. (The expression-tree nodes in relation.py take the + # opposite route, passing eq=False to suppress the synthesized pair.) def __hash__(self) -> int: return hash((id(self.source), self.name))