Skip to content

fix(splinter): flip pinner to opt-in, add inlinability regression net#200

Open
coderdan wants to merge 7 commits intomainfrom
dan/pinner-opt-in
Open

fix(splinter): flip pinner to opt-in, add inlinability regression net#200
coderdan wants to merge 7 commits intomainfrom
dan/pinner-opt-in

Conversation

@coderdan
Copy link
Copy Markdown
Contributor

@coderdan coderdan commented May 7, 2026

Stacked on top of #197. Closes #199.

Problem

The pinner introduced in #190 (tasks/pin_search_path.sql) is opt-out: it ALTERs every `eql_v2.*` function at install time to add `SET search_path`, except for a hand-maintained deny-list of "GIN-critical" functions. PostgreSQL refuses to inline any SQL function whose `pg_proc.proconfig` is non-NULL (see `inline_function` in `src/backend/optimizer/util/clauses.c`), so the pinner silently disabled inlining for any newly-authored SQL function the deny-list missed.

This already happened twice in the last week:

The principle established in those reviews is performance wins over splinter compliance: when the two conflict on the same `proconfig` column, we keep the function inlinable and add an allowlist entry, never the other way round.

Fix

1. Opt-in pinner (issue #199, suggestion 1)

Authors explicitly tag a function with `@noinline` in `COMMENT ON FUNCTION ...` to request pinning. The pinner only ALTERs marked functions; default is to leave functions alone. Failure mode flips:

Mistake Old (opt-out) New (opt-in)
Author forgets to coordinate with allowlist Silent perf regression in production Splinter `function_search_path_mutable` warning (loud, fixable, no perf impact)
Author intentionally writes inlinable SQL Re-pinned, inlining killed Stays inlinable

The 4 plpgsql functions that were previously relying on the pinner now declare `SET search_path = pg_catalog, extensions, public` inline, matching the convention already used by ~143 other plpgsql functions in the codebase:

  • `eql_v2.compare_ope_cllw_u64_65(eql_v2_encrypted, eql_v2_encrypted)`
  • `eql_v2.compare_ope_cllw_var_8(eql_v2_encrypted, eql_v2_encrypted)`
  • `eql_v2.ope_cllw_u64_65(jsonb)`
  • `eql_v2.ope_cllw_var_8(jsonb)`

The pinner's old `inline_critical_oids` query is gone; its replacement is a single `coalesce(d.description, '') LIKE '%@noinline%'` predicate against `pg_description`. Future authors with a corner case that wants pinning can use the marker without learning the allowlist machinery.

2. EXPLAIN regression net (issue #199, suggestion 4)

New `tests/sqlx/tests/inlinability_regression_tests.rs` runs EXPLAIN against representative queries that should hit functional indexes and asserts the index is in the plan. Failure means inlining broke — for any reason, not just pinner overreach. Six tests:

The four operator-form tests are `#[ignore]`'d with explicit messages pointing at #196. Un-ignore once that lands.

3. Splinter allowlist expansion

Allowlist grows from 7 to 30 entries to cover all the inlinable wrappers the old opt-out pinner was silently pinning. Each entry has a one-line justification referencing the index it needs to match. Splinter still passes: 47 raw findings, all 47 allowlisted, 0 unallowlisted.

The new entries fall into three groups:

Test plan

Future work captured by #199

The "definition of done" on #199 is met by this PR. If we ever decide to also move EQL toward being a real `CREATE EXTENSION` package (#198), the splinter allowlist becomes unnecessary entirely — splinter has a built-in carve-out for functions owned by extensions.

freshtonic and others added 7 commits May 7, 2026 16:29
- Drop dead NULL-handling branches from compare_ope_cllw_* (STRICT
  short-circuits NULL inputs before the body runs).
- Fix ciphertext-length wording in OPE type docs ("8 bytes per plaintext
  byte", not "per plaintext bit", which contradicted the 65-byte result).
- Convert simple OPE helpers (extractors, has_*, order_by_ope) from
  LANGUAGE plpgsql to LANGUAGE sql so the planner can inline them on the
  sort/order_by hot path. Per project guideline.
- Spell the cipher consistently as CLLW (matching ope_cllw_* type names
  and the existing ore_cllw_* docs); CLWW was a typo.
- Clarify sql-support.md MIN/MAX and COUNT(DISTINCT) rows so ope is no
  longer implied to be a ste_vec-extracted node term — it only applies
  to the outer column.
- Loosen brittle exact-list assertion in config_check_rejects_unknown_index
  to mention 'ope' and the offending token.
- Add ore_wins_over_opf_when_both_present to lock in the
  ORE-before-OPE dispatch precedence in eql_v2.compare.
- Add operator-level coverage for <=, >=, =, <> on both opf and opv
  variants (previously only < and > were covered for opf, and opv was
  exercised through compare_* but not via operators).
Adds 18 OPE tests covering behaviors that were exercised for ORE but had
no OPE equivalent. The ORE suite duplicates each test across three
ciphertext variants (block / cllw_u64_8 / cllw_var_8); for OPE we only
need one representative per variant per behavior because both opf and
opv use the same primitive (bytea lex compare).

NULL term in payload (mirrors compare_hmac_with_null_ore_index):
- has_opf_false_when_field_is_json_null
- has_opv_false_when_field_is_json_null
- compare_dispatches_through_null_opf_to_hmac
- compare_dispatches_through_null_opv_to_hmac

NULL operands at the comparator (codifies STRICT short-circuit):
- compare_ope_cllw_u64_65_strict_returns_null_for_null_operand
- compare_ope_cllw_var_8_strict_returns_null_for_null_operand

ORDER BY NULLS FIRST/LAST × ASC/DESC on opf-encoded data:
- order_by_asc_nulls_first_with_opf
- order_by_asc_nulls_last_with_opf
- order_by_desc_nulls_first_with_opf
- order_by_desc_nulls_last_with_opf

sort_compare with mixed NULL + opf rows:
- sort_compare_asc_puts_nulls_first_with_opf
- sort_compare_desc_puts_nulls_last_with_opf

MIN / MAX aggregates over OPE-encoded values:
- eql_v2_min_with_opf_finds_minimum
- eql_v2_max_with_opf_finds_maximum
- eql_v2_min_with_opf_null_only_returns_null
- eql_v2_max_with_opf_null_only_returns_null

BETWEEN range filtering:
- between_with_opf_inclusive_bounds
- between_with_opv_inclusive_bounds

All 47 OPE tests pass; full suite (~430 tests) is green.
The earlier sql-support clarification dropped `ope` from the outer-column
index list for COUNT(DISTINCT col). OPE ciphertexts are deterministic
within a column key, so an outer-column `ope` index supports DISTINCT
semantics just like `unique` and `ore`. Restoring it for parity with the
MIN/MAX row.
Match the bidirectional NULL coverage of the u64_65 sibling test.
Asserting only compare(NULL, x) leaves the right-NULL STRICT path
unverified; both directions must short-circuit.
Two doc-only fixes flagged in PR #197 review:

1. compare.sql @note for both u64_65 and var_8: the term-NULL branches
   (a_term IS NULL / b_term IS NULL) are NOT redundant with STRICT.
   STRICT only short-circuits on NULL function inputs (a, b); the term
   branches handle a non-NULL eql_v2_encrypted whose payload simply
   lacks the opf/opv field. Mirrors the defensive pattern in
   compare_ore_block_u64_8_256.

2. functions.sql @param tags for ope_cllw_u64_65 / ope_cllw_var_8 /
   has_ope_cllw_u64_65 / has_ope_cllw_var_8 now include the parameter
   name "val" before the type, matching valid Doxygen syntax (and
   compare.sql which already names a/b correctly).

No code behavior change; mise run docs:validate stays clean.
Closes #199.

The previous pinner (introduced in #190) was opt-out: it ALTERed every
eql_v2.* function at install time to add `SET search_path`, except for
an explicit deny-list of "GIN-critical" functions. PostgreSQL refuses to
inline any SQL function with `proconfig` set (see `inline_function` in
src/backend/optimizer/util/clauses.c), so the pinner silently broke
inlining for any newly-authored SQL function the deny-list missed.
This bit #196 (operator inlining) and #197 (OPE helpers) the same way.

Two fixes:

**1. Pinner is now opt-in (`tasks/pin_search_path.sql`).** Authors
explicitly tag a function with `@noinline` in `COMMENT ON FUNCTION ...`
to request pinning. Default is to leave functions alone. Failure mode
flips: forgetting to tag → splinter `function_search_path_mutable`
warning (loud, fixable). Forgetting under the old model → silent perf
regression.

The 4 plpgsql functions that previously relied on pinning
(`compare_ope_cllw_u64_65/var_8`, `ope_cllw_u64_65/var_8(jsonb)`) now
declare `SET search_path = pg_catalog, extensions, public` inline,
matching the convention used by ~143 other plpgsql functions across the
codebase.

**2. EXPLAIN-based regression net
(`tests/sqlx/tests/inlinability_regression_tests.rs`).** Six tests run
EXPLAIN against representative queries and assert the relevant
functional index is in the plan: hmac_256-hash and bloom_filter-GIN
expressions, plus operator-form variants for `=` / `<>` / `~~` / `~~*`.
The four operator-form tests are `#[ignore]`'d pending PR #196 which
flips those to inlinable SQL; the two direct-expression tests pass now
and protect against pinner overreach independently of the splinter
allowlist.

Splinter allowlist (`tasks/test/splinter.sh`) grows from 7 to 30
entries to cover all the inlinable wrappers that the previous opt-out
pinner was silently pinning. Splinter still passes: 47 raw findings,
all 47 allowlisted, 0 unallowlisted.

Verified locally: full sqlx test suite passes 403/0/50 against a fresh
install on PG17.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 92ea73ff-bf10-4b36-8aec-9ebc48e0dbb6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dan/pinner-opt-in

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Base automatically changed from fix/ope-index-type-docs to main May 7, 2026 06:50
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.

2 participants