From 6480636a5ac0889bd9ae9008d5454fcfbefc0099 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Tue, 30 Jun 2026 16:23:39 +1000 Subject: [PATCH 1/3] docs(dev): prune obsolete process docs; consolidate SQL doc guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the completed-effort reference-sync cluster (documentation-blockers, documentation-questions, reference-sync-notes, reference-sync-rules) — empty stubs and their rules doc, self-referential with no live consumer. Merge sql-documentation-standards + sql-documentation-templates into a single docs/development/sql-documentation.md, cross-linked to CLAUDE.md as the source of truth, and drop the dead Operator Class / Composite Type templates that contradict the v3 jsonb-domain footgun rules. Update the stale reference-sync example in docs_v3_grep.sh's comment. --- docs/development/documentation-blockers.md | 13 - docs/development/documentation-questions.md | 13 - docs/development/reference-sync-notes.md | 12 - docs/development/reference-sync-rules.md | 109 --------- .../sql-documentation-standards.md | 120 ---------- .../sql-documentation-templates.md | 133 ----------- docs/development/sql-documentation.md | 226 ++++++++++++++++++ tasks/test/docs_v3_grep.sh | 2 +- 8 files changed, 227 insertions(+), 401 deletions(-) delete mode 100644 docs/development/documentation-blockers.md delete mode 100644 docs/development/documentation-questions.md delete mode 100644 docs/development/reference-sync-notes.md delete mode 100644 docs/development/reference-sync-rules.md delete mode 100644 docs/development/sql-documentation-standards.md delete mode 100644 docs/development/sql-documentation-templates.md create mode 100644 docs/development/sql-documentation.md diff --git a/docs/development/documentation-blockers.md b/docs/development/documentation-blockers.md deleted file mode 100644 index 4b15dcdee..000000000 --- a/docs/development/documentation-blockers.md +++ /dev/null @@ -1,13 +0,0 @@ -# Documentation Blockers - -Bugs found during documentation process: - -## Format -- [ ] BUG FOUND: function_name - Issue: [description] - GitHub Issue: #XXX (if created) - Action: [what was done] - Blocking documentation: Yes/No - ---- - diff --git a/docs/development/documentation-questions.md b/docs/development/documentation-questions.md deleted file mode 100644 index d6a84f14b..000000000 --- a/docs/development/documentation-questions.md +++ /dev/null @@ -1,13 +0,0 @@ -# Documentation Questions for Review - -Discrepancies requiring principal engineer + team review: - -## Format -- [ ] DISCREPANCY: function_name - SQL code: [what code does] - Reference docs: [what docs say] - Question: [specific question] - For review by: Principal Engineer - ---- - diff --git a/docs/development/reference-sync-notes.md b/docs/development/reference-sync-notes.md deleted file mode 100644 index 9e328180d..000000000 --- a/docs/development/reference-sync-notes.md +++ /dev/null @@ -1,12 +0,0 @@ -# Reference Documentation Sync Notes - -Items to update in docs/reference/ after documentation complete: - -## Format -- [ ] docs/reference/file.md:section - Issue: [what needs updating] - SQL shows: [actual behavior] - Docs show: [current docs content] - ---- - diff --git a/docs/development/reference-sync-rules.md b/docs/development/reference-sync-rules.md deleted file mode 100644 index 90c070d25..000000000 --- a/docs/development/reference-sync-rules.md +++ /dev/null @@ -1,109 +0,0 @@ -# Reference Documentation Sync Rules - -## CRITICAL PRINCIPLE -**SQL code implementation is the source of truth.** - -During documentation, you will encounter discrepancies between: -- SQL code behavior -- Existing SQL comments (if any) -- Reference documentation in `docs/reference/` - -**NEVER modify SQL code to match documentation.** -**ALWAYS document what the code actually does.** - -## Decision Tree for Discrepancies - -### Scenario 1: SQL code is more detailed/accurate than reference docs -**Action:** -- Document the SQL code behavior accurately -- Mark reference doc for update in tracking file -- Continue with documentation - -**Add to:** `docs/development/reference-sync-notes.md` -**Format:** -``` -- [ ] docs/reference/eql-functions.md:add_column - SQL implementation has additional parameter validation not documented - SQL shows: validates table exists before adding config - Docs show: minimal description -``` - -### Scenario 2: Reference docs describe different behavior than SQL implements -**Action:** -- Document what the SQL code ACTUALLY does -- Flag discrepancy for principal engineer review -- DO NOT change SQL code -- DO NOT invent behavior to match docs - -**Add to:** `docs/development/documentation-questions.md` -**Format:** -``` -- [ ] DISCREPANCY: eql_v3.eq_term behavior - SQL code: raises exception when the payload is missing its equality term - Reference docs: suggest it returns NULL for incomplete payloads - Question: Is SQL correct or does it need fixing? - For review by: Principal Engineer -``` - -### Scenario 3: SQL code appears to have a bug -**Action:** -- Document the actual behavior (including the bug) -- Create GitHub issue for bug investigation -- Add `@note` tag mentioning the issue number -- DO NOT fix the bug in this plan - -**Example:** -```sql ---! @brief Extract ciphertext from encrypted value ---! @param val JSONB Raw encrypted value ---! @return Text Extracted ciphertext ---! @note Issue #XXX: Returns null for malformed input instead of raising error -CREATE FUNCTION eql_v3.ciphertext(val jsonb) ... -``` - -**Add to:** `docs/development/documentation-blockers.md` -**Format:** -``` -- [ ] BUG FOUND: eql_v3.ciphertext - Issue: Returns null for malformed input instead of raising error - GitHub Issue: #XXX - Action: Documented actual behavior, flagged for fix - Blocking documentation: No (documented as-is) -``` - -### Scenario 4: Unclear what code does (complex logic) -**Action:** -- Study the test files in `src/**/*_test.sql` -- Examine test cases to understand intended behavior -- Document based on test coverage -- If still unclear, read the code carefully and document what you observe -- Flag for principal engineer review if high-impact function - -**Add to:** `docs/development/documentation-questions.md` - -### Scenario 5: Reference docs conflict with each other -**Action:** -- SQL code is tiebreaker -- Document what code does -- Note conflicting docs in sync notes - -## Review Process - -**Principal Engineer + Team Code Review** will handle: -- Discrepancies flagged in `documentation-questions.md` -- Bugs flagged in `documentation-blockers.md` -- Reference doc updates listed in `reference-sync-notes.md` - -**Timeline:** -- Flag issues during documentation (Phases 1-4) -- Review session after Phase 5 (QA) -- Address critical issues before final PR -- Schedule reference doc updates as follow-up work - -## Tracking Files - -Create these files in `docs/development/`: - -**reference-sync-notes.md** - Reference docs needing updates -**documentation-questions.md** - Discrepancies needing review -**documentation-blockers.md** - Bugs found during documentation diff --git a/docs/development/sql-documentation-standards.md b/docs/development/sql-documentation-standards.md deleted file mode 100644 index 990840772..000000000 --- a/docs/development/sql-documentation-standards.md +++ /dev/null @@ -1,120 +0,0 @@ -# SQL Documentation Standards - -## Required Doxygen Tags - -### Mandatory -- `@brief` - One sentence description -- `@param` - For each parameter (with type and description) -- `@return` - Return value description (include structure for JSONB) - -### Encouraged -- `@example` - Usage examples (SQL code blocks) -- `@throws` - Exception conditions (when RAISE is used) -- `@internal` - Mark private functions (prefix with `_`) - -### Optional -- `@see` - Cross-references -- `@note` - Additional warnings/notes -- `@deprecated` - Migration path for deprecated functions - -## Format Examples - -### Public Function -```sql ---! @brief Extract the equality (hm) index term from an encrypted value ---! ---! Returns the HMAC equality term used by `=` / `<>` and by a functional ---! hash index. Inlinable, so a functional index on this extractor engages ---! bare-form queries. ---! ---! @param a eql_v3.int4_eq Encrypted value carrying an `hm` term ---! @return eql_v3.hmac_256 The equality index term ---! ---! @example ---! CREATE INDEX ON users USING hash (eql_v3.eq_term(salary_eq)); ---! ---! @see eql_v3.ord_term -CREATE FUNCTION eql_v3.eq_term(a eql_v3.int4_eq) - RETURNS eql_v3.hmac_256 -AS $$ ... $$; -``` - -### Private Function -```sql ---! @brief Internal helper for encrypted-payload validation ---! @internal ---! @param val JSONB Encrypted payload to validate ---! @return Boolean True if the payload is well-formed -CREATE FUNCTION eql_v3._validate_payload(val jsonb) - RETURNS boolean -AS $$ ... $$; -``` - -### Operator -```sql ---! @brief Equality comparison for an encrypted-domain value ---! ---! Implements the `=` operator for an `eql_v3` domain variant. Reduces to a ---! comparison on the extracted equality term — no decryption. ---! ---! @param a eql_v3.int4_eq Left operand ---! @param b eql_v3.int4_eq Right operand ---! @return Boolean True if the equality terms match ---! ---! @example ---! -- Using operator syntax: ---! SELECT * FROM users WHERE encrypted_email = $1; ---! ---! @see eql_v3.eq_term -CREATE FUNCTION eql_v3.eq(a eql_v3.int4_eq, b eql_v3.int4_eq) - RETURNS boolean -AS $$ ... $$; - -CREATE OPERATOR = ( - FUNCTION=eql_v3.eq, - LEFTARG=eql_v3.int4_eq, - RIGHTARG=eql_v3.int4_eq -); -``` - -### Type -```sql ---! @brief Encrypted-domain type for an equality-searchable int4 column ---! ---! A `jsonb`-backed domain in the `eql_v3` schema. The `CHECK` requires the ---! envelope keys (`v`, `i`, `c`), the equality term (`hm`), and pins the ---! payload version (`VALUE->>'v' = '2'`). ---! ---! @see eql_v3.eq_term -CREATE DOMAIN eql_v3.int4_eq AS jsonb - CHECK ( ... ); -``` - -### Aggregate -```sql ---! @brief State transition function for the MIN aggregate ---! @internal ---! @param $1 eql_v3.int4_ord Accumulated state ---! @param $2 eql_v3.int4_ord New value ---! @return eql_v3.int4_ord Updated state -CREATE FUNCTION eql_v3.min_sfunc(eql_v3.int4_ord, eql_v3.int4_ord) - RETURNS eql_v3.int4_ord -AS $$ ... $$; - ---! @brief Minimum encrypted value in a group ---! ---! Aggregate over an ordered encrypted-domain column. Comparison routes ---! through the variant's `<` operator (the ORE block term) — no decryption. ---! ---! @param input eql_v3.int4_ord Encrypted values to aggregate ---! @return eql_v3.int4_ord The minimum value ---! ---! @example ---! SELECT eql_v3.min(price_encrypted) FROM products; ---! ---! @see eql_v3.min_sfunc -CREATE AGGREGATE eql_v3.min(eql_v3.int4_ord) ( - SFUNC = eql_v3.min_sfunc, - STYPE = eql_v3.int4_ord -); -``` diff --git a/docs/development/sql-documentation-templates.md b/docs/development/sql-documentation-templates.md deleted file mode 100644 index 8e52a9d98..000000000 --- a/docs/development/sql-documentation-templates.md +++ /dev/null @@ -1,133 +0,0 @@ -# SQL Documentation Templates - -## Template: Public Function - -```sql ---! @brief [One sentence description] ---! ---! [Detailed description paragraph explaining purpose, ---! behavior, and any important context] ---! ---! @param param_name [Type] [Description] ---! @param param_name [Type] [Description with default: DEFAULT value] ---! @return [Return type] [Description of return value structure] ---! @throws [Condition that triggers exception] ---! ---! @example ---! -- [Example description] ---! SELECT eql_v3.function_name('value1', 'value2'); ---! ---! @see eql_v3.related_function -CREATE FUNCTION eql_v3.function_name(...) -``` - -## Template: Private/Internal Function - -```sql ---! @brief [One sentence description] ---! @internal ---! @param param_name [Type] [Description] ---! @return [Return type] [Description] -CREATE FUNCTION eql_v3._internal_function(...) -``` - -## Template: Operator Implementation - -```sql ---! @brief [Operator symbol] operator for encrypted values ---! ---! Implements the [operator] operator using [index type] for ---! [operation description] without decryption. ---! ---! @param a eql_v3.[domain_type] Left operand ---! @param b eql_v3.[domain_type] Right operand ---! @return Boolean [Result description] ---! ---! @example ---! -- [Specific example showing operator usage] ---! SELECT * FROM table WHERE encrypted_col [operator] value; ---! ---! @see eql_v3.[related_function] -CREATE FUNCTION eql_v3."[operator]"(...) -``` - -## Template: Domain Type - -```sql ---! @brief [Type name] index term type ---! ---! Domain type representing [description of what this type represents]. ---! Used for [use case] during searchable-encryption queries (e.g. equality via ---! `eq_term`, ordering via `ord_term`). ---! ---! @see eql_v3.eq_term ---! @note This is a transient type used only during query execution ---! @note Encrypted-domain types are jsonb-backed — always `AS jsonb`, never domain-over-domain -CREATE DOMAIN eql_v3.[type_name] AS jsonb; -``` - -## Template: Composite Type - -```sql ---! @brief [Brief description of composite type] ---! ---! [Detailed description including structure/fields] ---! ---! @see [related functions] -CREATE TYPE eql_v3.[type_name] AS ( - field_name field_type -); -``` - -## Template: Aggregate Function - -```sql ---! @brief [State function description] ---! @internal ---! @param $1 [State type] [State description] ---! @param $2 [Input type] [Input description] ---! @return [State type] [Updated state description] -CREATE FUNCTION eql_v3._state_function(...) - ---! @brief [Aggregate behavior description] ---! ---! [Detailed description of what aggregate computes] ---! ---! @param input [Input type] [Input description] ---! @return [Return type] [Return description] ---! ---! @example ---! -- [Example query using aggregate] ---! ---! @see eql_v3._state_function -CREATE AGGREGATE eql_v3.aggregate_name(...) (...) -``` - -## Template: Operator Class - -```sql ---! @brief [Operator class purpose description] ---! ---! Defines the operator class required for creating [index type] indexes ---! on encrypted columns. Enables [capabilities description]. ---! ---! @example ---! -- Create index using this operator class: ---! CREATE INDEX ON table USING [index_method] (column [opclass_name]); ---! ---! @see CREATE OPERATOR CLASS in PostgreSQL documentation -CREATE OPERATOR CLASS [opclass_name] ... -``` - -## Template: Constraint Function - -```sql ---! @brief [Constraint check description] ---! ---! [What the constraint validates] ---! ---! @param value [Type] [Value being checked] ---! @return Boolean True if constraint satisfied ---! @throws Exception if [constraint violation condition] -CREATE FUNCTION eql_v3.[constraint_function](...) -``` diff --git a/docs/development/sql-documentation.md b/docs/development/sql-documentation.md new file mode 100644 index 000000000..4f18cde5c --- /dev/null +++ b/docs/development/sql-documentation.md @@ -0,0 +1,226 @@ +# SQL Documentation (Doxygen) — Contributor Reference + +> The authoritative rules live in `CLAUDE.md` ("Documentation Standards"). This file is +> the longer-form contributor reference: the tag requirements, worked `eql_v3` examples, +> and copy-paste templates. When the two disagree, `CLAUDE.md` wins. + +All SQL functions and types are documented with Doxygen-style comments using the `--!` +prefix (not plain `--`). Coverage and required tags are checked by `mise run docs:validate`. + +## Required / Encouraged / Optional tags + +### Mandatory +- `@brief` - One sentence description +- `@param` - For each parameter (with type and description) +- `@return` - Return value description (include structure for JSONB) + +### Encouraged +- `@example` - Usage examples (SQL code blocks) +- `@throws` - Exception conditions (when RAISE is used) +- `@internal` - Mark private functions (prefix with `_`) + +### Optional +- `@see` - Cross-references +- `@note` - Additional warnings/notes +- `@deprecated` - Migration path for deprecated functions + +## Worked examples (`eql_v3`) + +### Public Function +```sql +--! @brief Extract the equality (hm) index term from an encrypted value +--! +--! Returns the HMAC equality term used by `=` / `<>` and by a functional +--! hash index. Inlinable, so a functional index on this extractor engages +--! bare-form queries. +--! +--! @param a eql_v3.int4_eq Encrypted value carrying an `hm` term +--! @return eql_v3.hmac_256 The equality index term +--! +--! @example +--! CREATE INDEX ON users USING hash (eql_v3.eq_term(salary_eq)); +--! +--! @see eql_v3.ord_term +CREATE FUNCTION eql_v3.eq_term(a eql_v3.int4_eq) + RETURNS eql_v3.hmac_256 +AS $$ ... $$; +``` + +### Private Function +```sql +--! @brief Internal helper for encrypted-payload validation +--! @internal +--! @param val JSONB Encrypted payload to validate +--! @return Boolean True if the payload is well-formed +CREATE FUNCTION eql_v3._validate_payload(val jsonb) + RETURNS boolean +AS $$ ... $$; +``` + +### Operator +```sql +--! @brief Equality comparison for an encrypted-domain value +--! +--! Implements the `=` operator for an `eql_v3` domain variant. Reduces to a +--! comparison on the extracted equality term — no decryption. +--! +--! @param a eql_v3.int4_eq Left operand +--! @param b eql_v3.int4_eq Right operand +--! @return Boolean True if the equality terms match +--! +--! @example +--! -- Using operator syntax: +--! SELECT * FROM users WHERE encrypted_email = $1; +--! +--! @see eql_v3.eq_term +CREATE FUNCTION eql_v3.eq(a eql_v3.int4_eq, b eql_v3.int4_eq) + RETURNS boolean +AS $$ ... $$; + +CREATE OPERATOR = ( + FUNCTION=eql_v3.eq, + LEFTARG=eql_v3.int4_eq, + RIGHTARG=eql_v3.int4_eq +); +``` + +### Domain Type +```sql +--! @brief Encrypted-domain type for an equality-searchable int4 column +--! +--! A `jsonb`-backed domain in the `eql_v3` schema. The `CHECK` requires the +--! envelope keys (`v`, `i`, `c`), the equality term (`hm`), and pins the +--! payload version (`VALUE->>'v' = '2'`). +--! +--! @see eql_v3.eq_term +CREATE DOMAIN eql_v3.int4_eq AS jsonb + CHECK ( ... ); +``` + +### Aggregate +```sql +--! @brief State transition function for the MIN aggregate +--! @internal +--! @param $1 eql_v3.int4_ord Accumulated state +--! @param $2 eql_v3.int4_ord New value +--! @return eql_v3.int4_ord Updated state +CREATE FUNCTION eql_v3.min_sfunc(eql_v3.int4_ord, eql_v3.int4_ord) + RETURNS eql_v3.int4_ord +AS $$ ... $$; + +--! @brief Minimum encrypted value in a group +--! +--! Aggregate over an ordered encrypted-domain column. Comparison routes +--! through the variant's `<` operator (the ORE block term) — no decryption. +--! +--! @param input eql_v3.int4_ord Encrypted values to aggregate +--! @return eql_v3.int4_ord The minimum value +--! +--! @example +--! SELECT eql_v3.min(price_encrypted) FROM products; +--! +--! @see eql_v3.min_sfunc +CREATE AGGREGATE eql_v3.min(eql_v3.int4_ord) ( + SFUNC = eql_v3.min_sfunc, + STYPE = eql_v3.int4_ord +); +``` + +## Copy-paste templates + +### Template: Public Function +```sql +--! @brief [One sentence description] +--! +--! [Detailed description paragraph explaining purpose, +--! behavior, and any important context] +--! +--! @param param_name [Type] [Description] +--! @param param_name [Type] [Description with default: DEFAULT value] +--! @return [Return type] [Description of return value structure] +--! @throws [Condition that triggers exception] +--! +--! @example +--! -- [Example description] +--! SELECT eql_v3.function_name('value1', 'value2'); +--! +--! @see eql_v3.related_function +CREATE FUNCTION eql_v3.function_name(...) +``` + +### Template: Private/Internal Function +```sql +--! @brief [One sentence description] +--! @internal +--! @param param_name [Type] [Description] +--! @return [Return type] [Description] +CREATE FUNCTION eql_v3._internal_function(...) +``` + +### Template: Operator Implementation +```sql +--! @brief [Operator symbol] operator for encrypted values +--! +--! Implements the [operator] operator using [index type] for +--! [operation description] without decryption. +--! +--! @param a eql_v3.[domain_type] Left operand +--! @param b eql_v3.[domain_type] Right operand +--! @return Boolean [Result description] +--! +--! @example +--! -- [Specific example showing operator usage] +--! SELECT * FROM table WHERE encrypted_col [operator] value; +--! +--! @see eql_v3.[related_function] +CREATE FUNCTION eql_v3."[operator]"(...) +``` + +### Template: Domain Type +```sql +--! @brief [Type name] index term type +--! +--! Domain type representing [description of what this type represents]. +--! Used for [use case] during searchable-encryption queries (e.g. equality via +--! `eq_term`, ordering via `ord_term`). +--! +--! @see eql_v3.eq_term +--! @note This is a transient type used only during query execution +--! @note Encrypted-domain types are jsonb-backed — always `AS jsonb`, never domain-over-domain +CREATE DOMAIN eql_v3.[type_name] AS jsonb; +``` + +### Template: Aggregate Function +```sql +--! @brief [State function description] +--! @internal +--! @param $1 [State type] [State description] +--! @param $2 [Input type] [Input description] +--! @return [State type] [Updated state description] +CREATE FUNCTION eql_v3._state_function(...) + +--! @brief [Aggregate behavior description] +--! +--! [Detailed description of what aggregate computes] +--! +--! @param input [Input type] [Input description] +--! @return [Return type] [Return description] +--! +--! @example +--! -- [Example query using aggregate] +--! +--! @see eql_v3._state_function +CREATE AGGREGATE eql_v3.aggregate_name(...) (...) +``` + +### Template: Constraint Function +```sql +--! @brief [Constraint check description] +--! +--! [What the constraint validates] +--! +--! @param value [Type] [Value being checked] +--! @return Boolean True if constraint satisfied +--! @throws Exception if [constraint violation condition] +CREATE FUNCTION eql_v3.[constraint_function](...) +``` diff --git a/tasks/test/docs_v3_grep.sh b/tasks/test/docs_v3_grep.sh index 1a5e6145d..8eeff97a3 100755 --- a/tasks/test/docs_v3_grep.sh +++ b/tasks/test/docs_v3_grep.sh @@ -27,7 +27,7 @@ cd "$REPO_ROOT" # Out of scope (NOT scanned) — these legitimately retain eql_v2 and are excluded # by path, not by silent omission: # docs/upgrading/ historical upgrade guides for the v2.x line -# docs/development/ internal contributor/process docs (reference-sync, etc.) +# docs/development/ internal contributor/process docs (release runbook, etc.) # CHANGELOG.md the permanent release record (documents the v2 removal) # CLAUDE.md project/dev instructions (describe the removal + provenance) # DEVELOPMENT.md contributor guide; its eql_v2 mentions are the removal From 5742873860b1d9c81bad761cc0201822940d5615 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Tue, 30 Jun 2026 16:23:45 +1000 Subject: [PATCH 2/3] docs: correct inaccuracies in scalar encrypted-domain reference Verification pass over docs/reference/adding-a-scalar-encrypted-domain-type.md found several stale or wrong claims; fix them all: - Drop the stale 'codegen gates the PostgreSQL test matrix' claim (sections 4 and 6); the gate was removed (test job needs only changes/setup/build-archive). - Fix filename tasks/pin_search_path.sql -> pin_search_path_v3.sql (5 refs). - Fix non-compiling example int_values!(.., INT4) -> INT4_FIXTURES. - Rewrite section 6 stage-4 to match generate_type: ensure_generated_paths_writable -> write_generated_file -> remove_generated_orphans (no clean_generated_files in that path). - Correct text domain count (five -> six) and 'operators' vs 'CREATE OPERATOR statements' wording; intro reference range 5-7 -> 5-8. - Misc precision nits: text pivots (only mid_pivot overridden), SignedScalar also for timestamptz, binding output paths, int_values! location, splinter constructors/comparators label, self-containment gate pointer. --- .../adding-a-scalar-encrypted-domain-type.md | 202 +++++++++++------- 1 file changed, 119 insertions(+), 83 deletions(-) diff --git a/docs/reference/adding-a-scalar-encrypted-domain-type.md b/docs/reference/adding-a-scalar-encrypted-domain-type.md index 247a97837..3b8fb4d0d 100644 --- a/docs/reference/adding-a-scalar-encrypted-domain-type.md +++ b/docs/reference/adding-a-scalar-encrypted-domain-type.md @@ -2,7 +2,7 @@ The one reference for adding a scalar encrypted-domain type (`int4`, `int2`, and future ordered numeric scalars). The **top half** (§§1–4) is the path you -follow to add a type; the **reference half** (§§5–7) is the detail behind it — +follow to add a type; the **reference half** (§§5–8) is the detail behind it — the generated surface, its invariants, and how the generator itself works. Read top-down to ship a type; drop into the reference half when something breaks or you need the *why*. @@ -15,7 +15,7 @@ aggregates also live in `eql_v3`; the searchable-encrypted-metadata (SEM) index-term types they return (`eql_v3.hmac_256`, `eql_v3.ore_block_256`) are **also `eql_v3`** — hand-written under `src/v3/sem/`. The whole v3 surface is self-contained: it owns every type it -needs and is fully self-contained (CI gates this — see §6). +needs and is fully self-contained (CI gates this via `mise run test:self_contained_v3`). The whole SQL surface is **generated** from a single Rust source of truth: the `CATALOG` const in [`crates/eql-domains/src/lib.rs`](../../crates/eql-domains/src/lib.rs), @@ -34,10 +34,11 @@ materializer (see §7). To add a scalar type `` (e.g. `int8`), with Rust type `` (e.g. `i64`): -1. **Add a `DomainFamily` row to `eql_domains::CATALOG`** — `name`, `kind`, - `domains`, `fixtures` (§2). If the type needs a new scalar width, add a - `ScalarKind` variant first; if it needs new term behaviour, that goes in the - `Term` enum's `impl`, never in catalog data. +1. **Add a `DomainFamily` row to `eql_domains::CATALOG`** — just `name` + + `domains` — plus a matching `TypeFixtures` record (carrying the `kind` and the + plaintext fixture `values`) in the `fixtures` module (§2). If the type needs a + new scalar width, add a `ScalarKind` variant first; if it needs new term + behaviour, that goes in the `Term` enum's `impl`, never in catalog data. 2. **Materialise the value list** — `int_values!(_VALUES, , );` next to `CATALOG`, pinned by a `values_tests` assertion (§2). This is the single source the SQLx matrix reads; there is no generated `_values.rs`. @@ -55,7 +56,7 @@ To add a scalar type `` (e.g. `int8`), with Rust type `` (e.g. `i64`): impl and its `inventory.rs`/`all()` entry (a two-line catalog-derived struct doc — a summary line plus an operators/required-keys detail line, no field docs), and the `ts-rs`/`schemars` derives then emit the committed - `bindings/v3/*.ts` + `schema/v3/*.json`. Unlike the SQL these `.rs` ARE + `crates/eql-bindings/bindings/v3/*.ts` + `crates/eql-bindings/schema/v3/*.json`. Unlike the SQL these `.rs` ARE committed (`// @generated`), so run `mise run types:generate` and commit the result; `mise run types:check` is the drift gate. For a new *domain* in an existing family `mod.rs` is untouched, but a new *family* needs a @@ -74,7 +75,7 @@ Things you do **not** do: `*_operators.sql` / `*_aggregates.sql` are gitignored; the catalog plus the renderers are the source of truth. Change the catalog and rebuild — never hand-edit generated SQL. -- **Don't edit `mise.toml`, the CI workflow, `pin_search_path.sql`, or +- **Don't edit `mise.toml`, the CI workflow, `pin_search_path_v3.sql`, or `splinter.sh`** for an ordinary type — they recognise the generated surface intrinsically (§5, §6). The exception is a brand-new *term* whose extractor has a new name (§5). @@ -88,50 +89,62 @@ Hand-written SQL beyond the fixed surface goes in ## 2. The catalog row (`DomainFamily`) A scalar type is one `DomainFamily` row in -[`crates/eql-domains/src/lib.rs`](../../crates/eql-domains/src/lib.rs): +[`crates/eql-domains/src/lib.rs`](../../crates/eql-domains/src/lib.rs), paired +with a `TypeFixtures` record in +[`crates/eql-domains/src/fixtures/record.rs`](../../crates/eql-domains/src/fixtures/record.rs): ```rust -DomainFamily { +// The structural catalog row — name + domains only: +const INT4: DomainFamily = DomainFamily { name: "int4", + domains: ORDERED_INT_DOMAINS, // storage, _eq (hm), _ord_ore (ore), _ord (ore) +}; + +// The fixture-layer record — kind + plaintext values — joined back to the +// catalog row by `family.name`: +pub const INT4_FIXTURES: TypeFixtures = TypeFixtures { + family: &crate::INT4, kind: ScalarKind::I32, - domains: &[ - Domain { name: "", terms: &[] }, - Domain { name: "eq", terms: &[Term::Hm] }, - Domain { name: "ord_ore", terms: &[Term::Ore] }, - Domain { name: "ord", terms: &[Term::Ore] }, - ], - fixtures: INT4_FIXTURES, -} + values: fixtures!(int i32; + Min, N(-100), N(-1), Zero, N(1), N(2), N(5), N(10), N(17), N(25), + N(42), N(50), N(100), N(250), N(1000), N(9999), Max), +}; ``` -The fields, all enforced by the type system and the catalog `#[test]`s rather -than a runtime validator: - -- **`name`** — the type name (`int4`); supplies `` everywhere. Each domain's - full name is the family `name` + `_` + the domain `name` - (`DomainFamily::domain_name`); codegen owns the `_` join (`Domain::full_name`), - and an empty domain `name` yields the bare family name. Pinned by - `every_domain_name_starts_with_its_family_name`. -- **`kind`** — a `ScalarKind` (`I16` / `I32` / `I64` / `Numeric` / `Text` / - `Jsonb` / `Date` / `Timestamptz`), carrying the Rust type name. Only the +`DomainFamily` carries only **`name`** and **`domains`**; the **`kind`** and +plaintext **`values`** are a fixture-layer concern that lives on the paired +`TypeFixtures` record, not on `DomainFamily`. A compile-time `const _` parity +block in `record.rs` enforces the 1:1 — every `CATALOG` row has exactly one +`TypeFixtures` (same order) carrying the right `kind`. All are otherwise enforced +by the type system and the catalog `#[test]`s rather than a runtime validator: + +- **`name`** (on `DomainFamily`) — the type name (`int4`); supplies `` + everywhere. Each domain's full name is the family `name` + `_` + the domain + `name` (`DomainFamily::domain_name`); codegen owns the `_` join + (`Domain::full_name`), and an empty domain `name` yields the bare family name. + Pinned by `every_domain_name_starts_with_its_family_name`. +- **`domains`** (on `DomainFamily`) — a non-empty `&[Domain]` (pinned by + `every_type_has_at_least_one_domain`), each a bare `name` + the fixed `&[Term]` it + carries. The storage domain is `name: ""` with no terms; `eq => [Term::Hm]`; + `ord` and `ord_ore => [Term::Ore]`. A `Domain` declares nothing else — no + extractor names, no operator lists, no REQUIRE edges. Every behavioural fact + comes from the `Term` enum. +- **`kind`** (on the `TypeFixtures` record) — a `ScalarKind` (`I16` / `I32` / + `I64` / `Numeric` / `Text` / `Jsonb` / `Date` / `Timestamptz` / `Bool` / `F32` / + `F64`), carrying the Rust type name. Only the integer kinds have an i128 range with `Min`/`Max`/`Zero` sentinels: those bounded accessors (`min_symbol`/`max_symbol`/`zero_symbol`/`min_value`/`max_value`) live on the total `BoundedIntKind` sub-enum, reached via `ScalarKind::as_bounded_int() -> Option`. Non-integer kinds - (`Numeric`/`Text`/`Jsonb`/`Date`/`Timestamptz`) + (`Numeric`/`Text`/`Jsonb`/`Bool`/`F32`/`F64`/`Date`/`Timestamptz`) return `None` and simply have no bounded accessor — misuse is a compile error, not a runtime panic. **If `` needs a new fixed-width integer, add a `BoundedIntKind` variant** (rust-type name, `MIN`/`MAX`/zero symbols, bounds) plus its `ScalarKind` variant and `as_bounded_int` arm, with unit tests over the `impl` methods. -- **`domains`** — a non-empty `&[Domain]` (pinned by - `every_type_has_at_least_one_domain`), each a bare `name` + the fixed `&[Term]` it - carries. The storage domain is `name: ""` with no terms; `eq => [Term::Hm]`; - `ord` and `ord_ore => [Term::Ore]`. A `Domain` declares nothing else — no - extractor names, no operator lists, no REQUIRE edges. Every behavioural fact - comes from the `Term` enum. -- **`fixtures`** — the type's plaintext fixture list (see below). +- **`values`** (on the `TypeFixtures` record) — the type's plaintext fixture + list (see below). **Terms** are fixed by the `Term` enum (`crates/eql-domains/src/lib.rs`). The `json_key` / `extractor` / `ctor` values are the cross-schema SQL contract (the @@ -156,8 +169,10 @@ emits an extra `CHECK` requiring `ob` to be a non-empty array (`jsonb_array_length(VALUE -> 'ob') > 0`). An empty ORE term (`ob: []`) is only ever produced by encrypting the empty string into an ordered column, and is rejected at the boundary rather than ordered (issue #262). This is emitted from -the catalog by the codegen renderer (`DomainBlock::ore_check` in -`crates/eql-codegen/src/context.rs`, gated on `Term::provides_ordering`), not +the catalog by the codegen renderer (the `nonempty_array_keys` field on +`DomainBlock` in `crates/eql-codegen/src/context.rs`, populated from +`Term::nonempty_array_keys`, which filters on the per-term +`Term::nonempty_array_key()` — `Some("ob")` only for `Term::Ore`), not hand-added — a new ordered scalar gets it for free. **Twins.** `int4_ord` and `int4_ord_ore` both carry `&[Term::Ore]`. The @@ -173,19 +188,22 @@ the way you want the output to read. ### Fixtures — single-sourcing the value list -The `fixtures` field is an ordered `&[Fixture]` — the single source of truth +The `TypeFixtures` record's `values` field is an ordered `&[Fixture]` — the +single source of truth for the type's plaintext list, consumed by both the SQLx fixture generator and the matrix oracle. A `Fixture` is value-kind tagged: `Min` / `Max` / `Zero` (the integer matrix pivots, resolved per-kind), `Int(i128)` (an integer literal), and -`Numeric` / `Text` / `Jsonb` / `Date` / `Timestamptz` string variants. The +`Numeric` / `Text` / `Jsonb` / `Date` / `Timestamptz` / `Float` string variants +(plus a `Bool` variant for the storage-only `bool` scalar). The `fixtures!` macro range-checks each `Int` literal against the kind at compile time (`N(-40000)` for an `i16` kind does not compile): ```rust -const INT4_FIXTURES: &[Fixture] = fixtures!(int i32; +// the `values:` expression of INT4_FIXTURES (a `TypeFixtures`): +values: fixtures!(int i32; Min, N(-100), N(-1), Zero, N(1), N(2), N(5), N(10), N(17), N(25), - N(42), N(50), N(100), N(250), N(1000), N(9999), Max); + N(42), N(50), N(100), N(250), N(1000), N(9999), Max), ``` Catalog `#[test]`s enforce a **distinct-plaintext contract** plus the @@ -206,11 +224,11 @@ Beyond the pivots, choose values so range operators produce distinguishable result counts, include useful boundaries, and cover omitted-term negative cases. The plaintext list is **not** rendered to a generated file. The `int_values!` -macro (next to `CATALOG`) materialises a `Fixture` list into a typed `pub const +macro (in `crates/eql-domains/src/fixtures/values.rs`) materialises a `Fixture` list into a typed `pub const _VALUES: &[]` at compile time (`INT4_VALUES`, `INT2_VALUES`): ```rust -int_values!(INT4_VALUES, i32, INT4); +int_values!(INT4_VALUES, i32, INT4_FIXTURES); ``` Both consumers reference that single symbol — the fixture generator @@ -298,7 +316,7 @@ integer kinds: |------|-----| | `tests/sqlx/src/scalar_types.rs` | One ` => ` line in the `scalar_types!` list (e.g. `int8 => i64,`). This single line drives the `impl ScalarType` **(integer kinds only)**, the `eql_v3_` fixture module, the `scalar_matrix!` suite, and the `generate_for_token` arm — all generated by the `eql-tests-macros` proc-macros. | | `tests/sqlx/src/fixtures/eql_plaintext.rs` | A sealed `EqlPlaintext` impl for ``: `impl Sealed for {}` and `impl EqlPlaintext for ` carrying just `const KIND: ScalarKind` plus the value-typed `to_plaintext` → the right `Plaintext` variant. `CAST` and `PLAINTEXT_SQL_TYPE` are **derived** from `KIND` via the `cast_for_kind` / `plaintext_sql_type_for_kind` `const fn` defaults, so a brand-new kind needs an arm in those two helpers — not a per-type const (see §3.1 for a non-integer kind's full wiring). Keep the three `#[test]`s (cast / sql-type / to_plaintext) mirroring the existing ones. | -| `tests/sqlx/src/scalar_domains.rs` **(non-integer only)** | The `impl ScalarType` the proc-macro skips for non-integer kinds. For a **chrono-backed** kind (`date`, `timestamptz`) this is a `temporal_values!` invocation that materialises the catalog ISO/RFC3339 strings into a `LazyLock>` and emits `impl ScalarType` + `OrderedScalar` (+ `SignedScalar` for `date`). For **`text`** it is a hand-written `impl ScalarType` / `OrderedScalar` block (lexicographic `min`/`max`/`mid` pivots, `to_sql_literal` override) — `String` has no numeric origin, so it is deliberately **not** `SignedScalar`. | +| `tests/sqlx/src/scalar_domains.rs` **(non-integer only)** | The `impl ScalarType` the proc-macro skips for non-integer kinds. For a **chrono-backed** kind (`date`, `timestamptz`) this is a `temporal_values!` invocation that materialises the catalog ISO/RFC3339 strings into a `LazyLock>` and emits `impl ScalarType` + `OrderedScalar` (+ `SignedScalar` for `date` and `timestamptz`). For **`text`** it is a hand-written `impl ScalarType` / `OrderedScalar` block (an overridden lexicographic-median `mid_pivot()` — `min`/`max` inherit the fixture-derived defaults — plus a `to_sql_literal` override) — `String` has no numeric origin, so it is deliberately **not** `SignedScalar`. | The single ` => ` line in `scalar_types.rs` is the harness source of truth. The four code-generators (`emit_scalar_type_impls`, @@ -339,7 +357,7 @@ no live catalog type today) vs `ORDERED_INT_DOMAINS` (→ `[eq, ord]`). (`EQ_ONL is currently unused — `timestamptz` was promoted to the ordered shape once the ORE comparator generalized to N blocks.) The pivot *sweep* is uniform across every ordered type (one canonical snapshot); the signed-only sign-boundary -test (`SignedScalar`, `int2`/`int4`/`int8`/`date`/`timestamptz`) lives outside `scalars::` in +test (`SignedScalar`, `int2`/`int4`/`int8`/`date`/`timestamptz`/`float4`/`float8`) lives outside `scalars::` in `encrypted_domain/signed.rs`, so a `text` instantiation of it is a compile error and it never enters the inventory snapshot. The `matrix.rs` module header is the canonical, @@ -421,24 +439,33 @@ such a domain you must, in addition to the catalog row: ### Matrix coverage inventory snapshot -The *set of test names* the matrix emits is guarded by **one** committed, -token-normalized snapshot at `tests/sqlx/snapshots/matrix_tests.txt` — the +The *set of test names* the matrix emits is guarded by **four** committed, +token-normalized **shape** snapshots under `tests/sqlx/snapshots/` — each the sorted inventory of every `scalars::::*` test name with the type token -replaced by the literal ``. (The per-type `_matrix_tests.txt` files are -gone: they were byte-identical modulo the token, so one canonical set plus a -per-type normalize-and-compare carries the same signal at a fraction of the -committed surface.) This is the guard that catches a silently dropped, renamed, +replaced by the literal ``. The canonical baseline is `matrix_tests.txt` (the +ordered `caps = [eq, ord]` shape); alongside it are `matrix_tests_eq_only.txt` +(the eq-only shape, *derived* from the baseline minus the `_ord`/`order_by`/ +`routes_through_ob` lines), `matrix_tests_text.txt` (the text shape, a *superset* +of the baseline adding the `_search`/`_eqidx`/`_match` arms), and +`matrix_tests_storage_only.txt` (the storage-only shape, e.g. `bool` — see §8). +(The per-type `_matrix_tests.txt` files are +gone: they were byte-identical modulo the token, so the shape snapshots plus a +per-type normalize-and-compare carry the same signal at a fraction of the +committed surface.) These are the guard that catches a silently dropped, renamed, or `#[cfg]`-gated matrix test — a behaviour the SQLx assertions cannot see (a -deleted test just stops running). The snapshot is a committed test baseline, +deleted test just stops running). The snapshots are committed test baselines, **not** gitignored generated SQL. `mise run test:matrix:inventory` discovers the present scalar types from the -`encrypted_domain` binary's `--list`, normalizes each type's token to ``, -asserts every type's set equals the canonical snapshot, and cross-checks the -discovered type set against `cargo run -p eql-codegen -- list-types` (the -catalog is the single source). You do **not** edit a per-type snapshot or touch -`mise.toml` / the CI workflow — you only regenerate the one `matrix_tests.txt` -when the macro's emitted name set itself changes. A catalog type missing its +`encrypted_domain` binary's `--list`, normalizes each type's token to ``, and +matches each discovered type against whichever shape applies — the canonical +ordered baseline, the derived eq-only subset, the text superset, or the +storage-only set — then cross-checks the discovered type set against +`cargo run -p eql-codegen -- list-types` (the catalog is the single source). You +do **not** edit a per-type snapshot or touch `mise.toml` / the CI workflow — you +regenerate the affected shape snapshots (all four atomically via +`mise run test:matrix:snapshots:regen`) only when the macro's emitted name set +itself changes. A catalog type missing its matrix wiring fails the cross-check. The CI `matrix-coverage` job gates it. **`tests/sqlx/snapshots/README.md` is the source of truth** for the mechanics (pinned feature set, the catalog cross-check, the CI diff, and when to @@ -468,8 +495,10 @@ Run, in order: - `mise run --output prefix test:splinter --postgres 17` after a PostgreSQL 17 install has built EQL -The CI codegen job is a prerequisite of the PostgreSQL test matrix, so -generated-SQL drift is caught before database tests run. +The CI `codegen` job runs `mise run codegen:parity` as an independent required +check (feeding the final `ci-required` gate); it no longer blocks the PostgreSQL +test matrix from starting — the shards run after `build-archive` in parallel with +codegen. **Commit a per-type reference baseline.** Every catalog type **must** have a committed `tests/codegen/reference//` baseline, generated once and checked in @@ -649,7 +678,7 @@ extends). Use it for cross-domain casts, helper functions, or type-specific constraints. Unlike the generated siblings, **`_extensions.sql` IS committed.** (Neither `int4` nor `int2` ships one today.) -`tasks/pin_search_path.sql` describes the fallback marker for inline-critical +`tasks/pin_search_path_v3.sql` describes the fallback marker for inline-critical extension functions that take no domain argument and so escape the structural skip: @@ -693,14 +722,14 @@ unreachable. Invariants encoded in the renderers / templates and guarded by Two pieces of build tooling recognise the generated output without per-type edits: -- **`tasks/pin_search_path.sql`** — structural skip identifies encrypted-domain +- **`tasks/pin_search_path_v3.sql`** — structural skip identifies encrypted-domain functions by language (`sql`), volatility (`IMMUTABLE`), and a jsonb-backed `DOMAIN` argument in the `eql_v3` schema. New scalar types need no edit. - **`tasks/test/splinter.sh`** — name-based allowlist. The converged wrapper / extractor names (`eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `eq_term`, `ord_term`, the `Bloom` term's `match_term` extractor and its `contains` / `contained_by` containment wrappers) plus the generated `min` / `max` aggregates and the SEM - `hmac_256` / `ore_block_256` / `bloom_filter` constructors are already + `hmac_256` / `ore_block_256` / `bloom_filter` types' constructors and comparators are already covered by `eql_v3`-schema entries. A new scalar type inherits coverage; **a new term needs splinter entries for each new name it introduces — both its extractor and its comparison wrappers** (adding `Bloom` required `match_term`, @@ -718,14 +747,14 @@ adding a type. A single scalar type emits several hundred SQL declarations. For `int4`: eleven files, four domains, three extractors, dozens of wrappers and blockers, 176 `CREATE OPERATOR` statements (44 per domain), and MIN/MAX aggregates per ordered -domain. (The per-domain figure is fixed — 44 operators per domain, the `1 + 2D + -A` file formula below — so a type with more domains, e.g. `text`'s five, scales +domain. (The per-domain figure is fixed — 44 `CREATE OPERATOR` statements per domain, the `1 + 2D + +A` file formula below — so a type with more domains, e.g. `text`'s six, scales those totals up.) The shape is mechanical and the invariants are unforgiving — a `STRICT` blocker silently bypasses its exception; a pinned `search_path` reverts queries to seq scans. The generator exists so each new type adds one `CATALOG` row rather than ninety hand-written declarations that must agree with each other and with -`pin_search_path.sql`, `tasks/test/splinter.sh`, and +`pin_search_path_v3.sql`, `tasks/test/splinter.sh`, and `src/v3/scalars/functions.sql`. ### Pipeline @@ -734,10 +763,15 @@ ninety hand-written declarations that must agree with each other and with runs as `cargo run -p eql-codegen` (no subcommand), which calls `generate::generate_all` (`crates/eql-codegen/src/generate.rs`) over every row of `eql_domains::CATALOG`, writing each type's SQL into -`src/v3/scalars//`. A second subcommand, `cargo run -p eql-codegen --- list-types`, prints the catalog tokens one per line (consumed by the fixture -and matrix-inventory enumeration). `main` (`crates/eql-codegen/src/main.rs`) -recognises exactly these two forms; any other argument is a usage error. +`src/v3/scalars//`. Three subcommands round out the surface: +`-- list-types` prints the catalog tokens one per line (consumed by the fixture +and matrix-inventory enumeration); `-- dump-catalog` prints the catalog surface +(types → domains → supported operators) as JSON (consumed by the +catalog-coverage / log-verification gates); and `-- bindings` regenerates the +committed `eql-bindings` Rust payload types (the first step of `mise run +types:generate`). `main` (`crates/eql-codegen/src/main.rs`) recognises exactly +these four forms (no-arg generate-all, `list-types`, `dump-catalog`, +`bindings`); any other argument is a usage error. The generator targets the `eql_v3` schema throughout: `SCHEMA = "eql_v3"` (`crates/eql-codegen/src/consts.rs`) qualifies both the domain families and the @@ -770,14 +804,15 @@ Stages, in order (`generate_all` → `generate_type`): **minijinja** templates (`crates/eql-codegen/templates/*.j2`, compiled in via `include_str!` — no runtime file IO). The structural shape of each declaration is split between the context builders (Rust) and the templates (Jinja). -4. **Write.** `clean_generated_files` first deletes every generated `.sql` in the - target directory (recognised by the header marker) so an abandoned domain - disappears on the next regeneration; `ensure_generated_paths_writable` then - refuses to proceed if any target path is a hand-written file lacking the - marker; `write_generated_file` writes each rendered body verbatim - (`crates/eql-codegen/src/writer.rs`). The template emits the `-- AUTOMATICALLY - GENERATED FILE.` marker as its own first line, so the writer does not prepend - a header — it only uses the marker to recognise files it owns. +4. **Write.** `ensure_generated_paths_writable` runs first and refuses to proceed + if any target path is a hand-written file lacking the marker; `write_generated_file` + then writes each rendered body verbatim; finally `remove_generated_orphans` + prunes — marker-aware — any previously-generated `.sql` no longer produced, so an + abandoned domain disappears on the next regeneration (a hand-written file with no + marker always survives) (`crates/eql-codegen/src/writer.rs`). The template emits + the `-- AUTOMATICALLY GENERATED FILE.` marker as its own first line, so the + writer does not prepend a header — it only uses the marker to recognise files it + owns. There is no caching layer and no incremental mode. Each run regenerates every output for every catalog type from scratch. @@ -840,8 +875,9 @@ CI runs these in three jobs in `.github/workflows/test-eql.yml`: `rust-crates` (`Rust workspace crates`, runs `mise run test:crates`), `codegen` (`Encrypted-domain codegen`, runs `mise run codegen:parity`), and `matrix-coverage` (`Matrix coverage inventory`, runs `mise run -test:matrix:inventory`). The codegen job is a prerequisite of the PostgreSQL -test matrix. +test:matrix:inventory`). These run as independent required checks feeding the +final `ci-required` gate; the `codegen` job no longer blocks the PostgreSQL test +matrix from starting (the gate was removed — shards start after `build-archive`). Adding a new **term** is a bigger move than adding a type: edit the `Term` enum's `impl` methods, add `#[test]`s, add a `splinter.sh` entry for **each new name the @@ -898,7 +934,7 @@ What makes it storage-only: - **Generator: no changes needed.** The SQL generator already handles a zero-term, single-domain type — it emits exactly three files (`bool_types.sql`, `bool_functions.sql`, `bool_operators.sql`; no `_aggregates.sql`, since no - ordered domain). All 44 functions are `plpgsql` blockers, all 44 operators back + ordered domain). All 44 functions are `plpgsql` blockers, all 44 `CREATE OPERATOR` statements back onto them: every comparison/containment/path operator reachable through domain fallback raises. The domain `CHECK` still pins `{v,i,c}` + `VALUE->>'v' = '2'`. - **Kind, not term.** Add a `ScalarKind` variant (`Bool`) with @@ -933,5 +969,5 @@ What makes it storage-only: Everything else is the standard path: one catalog row, regenerate, commit the `tests/codegen/reference/bool/` baseline (3 files), no edits to -`pin_search_path.sql` or `splinter.sh` (a storage-only type emits only blockers +`pin_search_path_v3.sql` or `splinter.sh` (a storage-only type emits only blockers — no extractors/wrappers/aggregates, so no new inline-critical names). From 7599bdb4191f0ae38c1999bb8150206217ee2c0f Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Tue, 30 Jun 2026 17:22:16 +1000 Subject: [PATCH 3/3] docs: hyphenate one-sentence compound modifier in @brief description --- docs/development/sql-documentation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/sql-documentation.md b/docs/development/sql-documentation.md index 4f18cde5c..04dcf7e83 100644 --- a/docs/development/sql-documentation.md +++ b/docs/development/sql-documentation.md @@ -10,7 +10,7 @@ prefix (not plain `--`). Coverage and required tags are checked by `mise run doc ## Required / Encouraged / Optional tags ### Mandatory -- `@brief` - One sentence description +- `@brief` - one-sentence description - `@param` - For each parameter (with type and description) - `@return` - Return value description (include structure for JSONB)