You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Build a lint tool — modelled on Supabase's Splinter — that flags EQL functions and operators which the Postgres planner cannot inline. Non-inlinable functions in operator implementations silently degrade encrypted-query performance: the documented functional indexes (hmac_256, bloom_filter, ste_vec) never engage, and customers seq-scan at scale without a clear diagnostic.
Motivation
Three real issues we've shipped or hit recently are all detectable by a static lint:
Today we discover these by running EXPLAIN on a representative bench fixture and noticing a Seq Scan where there shouldn't be one. That's slow, manual, easy to miss in code review, and only catches what the bench happens to exercise. A lint runs at PR time over the full operator surface and catches the issue at definition time.
Postgres SQL function inlining rules
The lint is straightforward because the rules are deterministic and queryable from pg_catalog:
Property
pg_proc column
Required for inlining
Language
prolang::regtype
must be sql
Volatility
provolatile
i (immutable) or s (stable)
Body shape
prosrc
single SELECT statement
SET clauses
proconfig
must be NULL
Security
prosecdef
must be false
Any operator implementation function that fails one of these is non-inlinable. The lint can produce a row per violation, machine-readable for CI gating + human-readable for developer diagnosis.
Initial scope (v1)
eql_v2.lints() — a single SQL function returning (severity text, category text, object_name text, message text).
Direct check on operator implementations — for every operator on eql_v2_encrypted (and the supporting types like eql_v2.bloom_filter, eql_v2.ore_block_*), confirm the implementation function is inlinable.
Transitive check — when an inlinable SQL function calls a non-inlinable function, the chain breaks. Walk one level via pg_depend (or string-match in prosrc as a fallback).
Test fixture — assert the lint reports zero violations on a clean install (post-Phase 1).
Operator coverage — every documented operator pair on encrypted types is registered with a commutator/negator where applicable.
STRICT extractors — every _sem-style extractor (per the predicate/extractor RFC) is declared STRICT.
Build-time static analyzer — parse the .sql source files in this repo before install. Easier to integrate with the doxygen / comment-annotation work already in flight.
Exemption mechanism
Some functions are intentionally non-inlinable (e.g. eql_v2.compare is a multi-method dispatcher by design). The lint should support an opt-out, ideally via a Doxygen-style comment annotation (consistent with the doc-generation work already underway): --! @lint-allow non-inlinable: dispatcher by design. Annotation lives next to the code it exempts.
Delivery
Phase 1 (this issue): install-time SQL function eql_v2.lints(). Runs against any installed EQL. Splinter-style.
Phase 2 (follow-up): integrate into EQL CI as a gate — every PR runs SELECT * FROM eql_v2.lints() WHERE severity = 'error' and fails the build on non-empty.
Phase 3 (follow-up): build-time static analyzer for definition-level checks that don't need a running DB.
Validation plan
The first PR for the lint stacks ahead of #193 (Phase 1 operator inlining). The lint identifies what #193 needs to fix; #193 fixes it; the lint is then clean. That's the regression-test loop in microcosm — the lint catches the problem the fix addresses.
Summary
Build a lint tool — modelled on Supabase's Splinter — that flags EQL functions and operators which the Postgres planner cannot inline. Non-inlinable functions in operator implementations silently degrade encrypted-query performance: the documented functional indexes (hmac_256, bloom_filter, ste_vec) never engage, and customers seq-scan at scale without a clear diagnostic.
Motivation
Three real issues we've shipped or hit recently are all detectable by a static lint:
eql_v2.like/eql_v2.ilikewereLANGUAGE sql VOLATILE. Volatile blocks inlining;bloom_filterindex never engaged.=operator dispatching to a plpgsql function that can't inline. Same root cause.=/LIKEqueries seq-scan on Supabase because the implementation function isn't inlinable. Currently in flight as a fix in this repo (Phase 1 of the predicate/extractor RFC).Today we discover these by running EXPLAIN on a representative bench fixture and noticing a
Seq Scanwhere there shouldn't be one. That's slow, manual, easy to miss in code review, and only catches what the bench happens to exercise. A lint runs at PR time over the full operator surface and catches the issue at definition time.Postgres SQL function inlining rules
The lint is straightforward because the rules are deterministic and queryable from
pg_catalog:pg_proccolumnprolang::regtypesqlprovolatilei(immutable) ors(stable)prosrcSETclausesproconfigprosecdefAny operator implementation function that fails one of these is non-inlinable. The lint can produce a row per violation, machine-readable for CI gating + human-readable for developer diagnosis.
Initial scope (v1)
eql_v2.lints()— a single SQL function returning(severity text, category text, object_name text, message text).eql_v2_encrypted(and the supporting types likeeql_v2.bloom_filter,eql_v2.ore_block_*), confirm the implementation function is inlinable.pg_depend(or string-match inprosrcas a fallback).Future scope (out of v1)
f(col), estimate the row size fromf's return type and flag if it could exceed the 2704-byte btree limit. Would have caught thebench_text_eql_idxfailure in perf(bench): add @cipherstash/bench for index-engagement validation stack#424._sem-style extractor (per the predicate/extractor RFC) is declaredSTRICT..sqlsource files in this repo before install. Easier to integrate with the doxygen / comment-annotation work already in flight.Exemption mechanism
Some functions are intentionally non-inlinable (e.g.
eql_v2.compareis a multi-method dispatcher by design). The lint should support an opt-out, ideally via a Doxygen-style comment annotation (consistent with the doc-generation work already underway):--! @lint-allow non-inlinable: dispatcher by design. Annotation lives next to the code it exempts.Delivery
eql_v2.lints(). Runs against any installed EQL. Splinter-style.SELECT * FROM eql_v2.lints() WHERE severity = 'error'and fails the build on non-empty.Validation plan
The first PR for the lint stacks ahead of #193 (Phase 1 operator inlining). The lint identifies what #193 needs to fix; #193 fixes it; the lint is then clean. That's the regression-test loop in microcosm — the lint catches the problem the fix addresses.
Related
like/ilikevolatility flip (closed; would have been caught by this lint)docs/plans/uniform-predicate-extractor-pairs-rfc.md— broader predicate/extractor RFC (this lint is complementary tooling)