Skip to content

feat: add eql_v2.lints() inlinability lint (#194)#195

Merged
coderdan merged 3 commits intomainfrom
dan/lint-inlinability
May 7, 2026
Merged

feat: add eql_v2.lints() inlinability lint (#194)#195
coderdan merged 3 commits intomainfrom
dan/lint-inlinability

Conversation

@coderdan
Copy link
Copy Markdown
Contributor

@coderdan coderdan commented May 6, 2026

Summary

Resolves #194. Adds a Splinter-shaped lint that reports operator implementation functions 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.

What's in this PR

  • src/lint/lints.sql — defines eql_v2.lints(), a LANGUAGE sql STABLE function returning (severity, category, object_name, message) rows. Picked up by the existing mise run build pipeline (-- REQUIRE: src/schema.sql directive routes it through dependency ordering).
  • tests/sqlx/tests/lint_tests.rs — basic Rust tests asserting the lint runs, returns rows, and emits well-known severity/category values.

Categories reported

Category Trigger
inlinability_language Implementation function is not LANGUAGE sql (plpgsql etc.)
inlinability_volatility Implementation function is VOLATILE
inlinability_set_clause Implementation function has SET clauses (e.g. SET search_path = ...)
inlinability_secdef Implementation function is SECURITY DEFINER
inlinability_transitive Implementation function is itself inlinable but invokes a non-inlinable function (depth 1)

Usage

SELECT severity, category, object_name, message
  FROM eql_v2.lints()
 WHERE severity = 'error'
 ORDER BY category, object_name;

CI gating later (separate PR): fail the build if the query returns non-empty.

Validation

Smoke-tested against a fresh install of this branch's EQL build. The lint correctly identifies every operator implementation in scope for #193 (=, <>, ~~, ~~*, @>, <@ on eql_v2_encrypted) plus the comparison operators and JSONB extractors that are out of scope for Phase 1 (#193) but worth flagging.

The Phase 1 PR is stacked on this branch (#193) and uses the lint output to drive its fixes — circular validation: the lint catches what Phase 1 fixes; after Phase 1 lands, the lint reports zero violations on the targeted operators (and continues to flag the out-of-scope ones, which is correct).

Future scope (separate PRs)

  • Build-time static analyzer: parse .sql source files in this repo before install. Easier to integrate with the doxygen / comment-annotation work already in flight.
  • CI gate: integrate eql_v2.lints() into the EQL CI workflow.
  • Index-size budget: would have caught the bench_text_eql_idx failure in perf(bench): add @cipherstash/bench for index-engagement validation stack#424.
  • Operator coverage, STRICT extractors, transitive depth >1: see the issue.
  • Exemption mechanism: doxygen-style annotation --! @lint-allow, see issue.

Summary by CodeRabbit

  • New Features

    • Added a linting system that identifies EQL operator implementation issues, including non-inlinable functions that could impact query performance.
  • Tests

    • Added integration tests to validate the linting system returns proper violation reports with appropriate severity levels and categories.

Adds a Splinter-shaped lint that reports operator implementation functions
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.

Categories reported:
  - inlinability_language    (impl is not LANGUAGE sql)
  - inlinability_volatility  (impl is VOLATILE)
  - inlinability_set_clause  (impl has SET clauses, e.g. SET search_path)
  - inlinability_secdef      (impl is SECURITY DEFINER)
  - inlinability_transitive  (inlinable impl calls a non-inlinable function;
                              the chain blocks at depth 1)

Usage:
  SELECT severity, category, object_name, message
    FROM eql_v2.lints()
   WHERE severity = 'error'
   ORDER BY category, object_name;

Smoke-tested against the current released EQL surface and surfaces
exactly the operators that Phase 1 (#193) needs to fix:

  - = / <> / ~~ / ~~* / -> / ->> / @> / <@  on eql_v2_encrypted
  - All <  <= > >= comparison operators
  - The ore_block_u64_8_256_* operators (VOLATILE)

The Phase 1 PR is stacked on top of this commit; the lint becomes
clean for the operators it touches.

This is v1: operator implementations only. Future scope (separate PRs):
index-size budget, operator coverage, STRICT extractor checks,
build-time static analyzer.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Review Change Stack

Warning

Rate limit exceeded

@coderdan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 50 minutes and 15 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e6affa00-8d6c-429d-b319-a123268747ff

📥 Commits

Reviewing files that changed from the base of the PR and between 034a6f5 and 6bddd47.

📒 Files selected for processing (1)
  • src/lint/lints.sql
📝 Walkthrough

Walkthrough

This PR introduces eql_v2.lints(), a SQL function that detects non-inlinable operator implementations by querying PostgreSQL system catalogs, plus integration tests validating lint output structure and operator coverage.

Changes

Inlinability Lint Function & Tests

Layer / File(s) Summary
Function Declaration
src/lint/lints.sql
Function signature declares eql_v2.lints() returning (severity, category, object_name, message) as LANGUAGE sql STABLE.
Operator Metadata Gathering
src/lint/lints.sql
CTEs join pg_operator, pg_proc, and pg_language to collect operator implementations whose operand types match eql_v2% patterns, extracting language, volatility, config, security, and source body.
Direct Inlinability Rules
src/lint/lints.sql
UNION ALL rules emit error rows for operators whose implementation functions violate inlinability requirements: non-sql language, VOLATILE volatility, non-null proconfig (SET clause), or SECURITY DEFINER.
Transitive Dependency Checks
src/lint/lints.sql
Rule checking via pg_depend detects when otherwise-inlinable outer operator implementations call non-inlinable functions one level deep, emitting error rows with category inlinability_transitive.
Deterministic Output & Documentation
src/lint/lints.sql
ORDER BY ensures deterministic result ordering; COMMENT ON FUNCTION documents CI usage.
Test Infrastructure
tests/sqlx/tests/lint_tests.rs
LintRow struct maps query results via SQLx; fetch_lints helper executes eql_v2.lints() and returns ordered rows.
Test Validation
tests/sqlx/tests/lint_tests.rs
Four test cases validate: non-empty result set, severity enum compliance (error|warning|info), category allowlist compliance (inlinability-related), and smoke test confirming core eql_v2_encrypted operator violations are detected.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Poem

🐰 A lint hops through pg_catalog so deep,
Finding operators that can't inline—no sleep!
Direct and transitive checks catch the laze,
Flagging VOLATILE functions through the haze.
CI gates pass now with cleaner, swifter ways!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a new inlinability lint function eql_v2.lints().
Linked Issues check ✅ Passed The PR implements all primary coding requirements from issue #194: eql_v2.lints() SQL function with required output columns, direct checks for inlinability violations (language, volatility, SET clause, security definer), and one-level transitive checks for function call chains.
Out of Scope Changes check ✅ Passed All changes are within scope: src/lint/lints.sql implements the lint function and tests/sqlx/tests/lint_tests.rs validates it, with no unrelated code modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dan/lint-inlinability

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.

@coderdan coderdan marked this pull request as ready for review May 7, 2026 06:44
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lint/lints.sql`:
- Around line 6-13: The `@brief` lists "Single-statement SELECT body" as an
inlining precondition but the current implementation (the four UNION ALL
branches) never inspects the function body (prosrc), causing false-clean
results; either update the docstring/@brief to mark that this check is not yet
implemented, or implement the check in the lint by examining prosrc (or
pg_get_functiondef) to ensure the function body is a single SELECT — e.g.,
conservative heuristic: flag when prosrc does not begin with "SELECT" (ignoring
whitespace/comments) or contains a semicolon before the last character — and add
this check to the same logic that builds the existing UNION ALL branches so the
lint actually rejects multi-statement/CTE bodies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f55ce2b8-4c00-4bc7-ac1f-45b975ad76dd

📥 Commits

Reviewing files that changed from the base of the PR and between 6806141 and 034a6f5.

📒 Files selected for processing (2)
  • src/lint/lints.sql
  • tests/sqlx/tests/lint_tests.rs

Comment thread src/lint/lints.sql
Copy link
Copy Markdown
Contributor

@freshtonic freshtonic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome. Also if CipherStash fails we can all go consult as leet Postgres internals consultants.

Per CodeRabbit on #195: the docstring listed five inlining preconditions but
the lint only checks four. Add an explicit @note so callers aren't misled
into reading a clean lint result as a guarantee that every `LANGUAGE sql`
function in the EQL surface inlines.

The check itself is left for a follow-up — implementing it cleanly needs
`prosrc` parsing or `pg_get_functiondef` reflection.
@coderdan coderdan merged commit 998a015 into main May 7, 2026
6 checks passed
@coderdan coderdan deleted the dan/lint-inlinability branch May 7, 2026 06:58
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.

tooling: add an inlinability lint for EQL functions and operators

2 participants