Skip to content

feat(column-usage): expose WHERE-clause function wrappers on ColumnUsage#56

Merged
eitamring merged 3 commits into
mainfrom
feature/eitam/issue-51-where-function-wrappers
May 6, 2026
Merged

feat(column-usage): expose WHERE-clause function wrappers on ColumnUsage#56
eitamring merged 3 commits into
mainfrom
feature/eitam/issue-51-where-function-wrappers

Conversation

@eitamring

@eitamring eitamring commented May 5, 2026

Copy link
Copy Markdown
Contributor

Adds ColumnUsage.Function (*FunctionWrapper) carrying typed metadata about an allowlisted function call wrapping a bare column reference in a WHERE-clause predicate. The eight allowlisted wrappers — length, lower, upper, coalesce, extract, date_trunc, char_length, octet_length — are the structural-semantic boundary for "function calls that wrap a single column in a predicate position and change the comparison's semantics". Any wider set would dilute the contract for downstream consumers (ORMs, linters, query rewriters, AI-assisted SQL generators) that key off it.

FunctionWrapper carries:

  • Name: canonical lowercase, unqualified.
  • Schema: "" for unqualified or pg_catalog (canonicalised); any other schema rejects the wrapper outright.
  • Args: literal arguments other than the column itself (extract field, date_trunc unit, coalesce defaults). Each FunctionArg has a *string Literal for recoverable literals (NULL handled separately via IsNull), nil for non-literal expressions.
  • IsNested: outermost-only attribution; true when the wrapped column is reached through more allowlisted-function wrappers.
  • Cast: outermost cast target type (length(col)::int, length(col)::int::bigint, CAST(length(col) AS bigint)). Empty when there is no cast.

Scope is enforced by threading wantWrappers bool through findAndRecordComparisons. WHERE call sites (SELECT, UPDATE, DELETE, set-op branches, subqueries, CTE bodies) pass true; HAVING passes false. JOIN ON, ORDER BY, GROUP BY, window PARTITION/ORDER, RETURNING and SELECT projection use other extraction paths and never produce a wrapper by construction. The legacy ColumnUsage.Functions []string is unchanged on WHERE entries (stays empty there); document the asymmetry.

Functions []string continues to apply to projection/order/group/etc. contexts unchanged.

Mirrored in analysis as SQLFunctionWrapper / SQLFunctionArg type aliases plus a deep-clone in convertColumnUsage so analysis-side mutation cannot affect the parser IR.

Test matrix covers: all 8 functions, all comparison operators (=, <>, <,

, <=, >=, LIKE, ILIKE, NOT LIKE, IN, NOT IN, BETWEEN, IS NULL), both LHS
and RHS positions, case insensitivity, pg_catalog canonicalisation, three nesting depths, casts on every operator type, casts via :: and CAST(... AS ...), DML UPDATE/DELETE WHERE, subquery WHERE, CTE body WHERE, every branch of UNION/UNION ALL/INTERSECT/EXCEPT, quoted columns with parens, non-literal date_trunc unit, placeholder extract field, NULL coalesce default. Negative cases: HAVING, INNER/LEFT/RIGHT/FULL/CROSS/USING joins, ORDER BY, GROUP BY, window PARTITION/ORDER, RETURNING, projection, foreign schemas, non-allowlisted functions, expressions inside wrappers, casts on bare columns and on expressions.

Closes #51

Summary

Layer

  • Core parser (ir.go, ddl.go, select.go, dml_*.go, merge.go, setops.go, entry.go)
  • Analysis layer (analysis/)
  • Docs / examples
  • CI / tooling

SQL examples

-- example

Test plan

  • New unit tests added
  • Existing tests updated
  • make test passes (race detector + coverage)
  • make vet passes
  • Manual verification with examples/

Related issues

Checklist

  • No changes to gen/ (auto-generated ANTLR code)
  • New IR fields documented in docs/parsed-query.md (if applicable)
  • Public API additions are backward compatible

Adds `ColumnUsage.Function` (`*FunctionWrapper`) carrying typed metadata
about an allowlisted function call wrapping a bare column reference in a
WHERE-clause predicate. The eight allowlisted wrappers — length, lower,
upper, coalesce, extract, date_trunc, char_length, octet_length — are
the structural-semantic boundary for "function calls that wrap a single
column in a predicate position and change the comparison's semantics".
Any wider set would dilute the contract for downstream consumers (ORMs,
linters, query rewriters, AI-assisted SQL generators) that key off it.

`FunctionWrapper` carries:
- Name: canonical lowercase, unqualified.
- Schema: "" for unqualified or pg_catalog (canonicalised); any other
  schema rejects the wrapper outright.
- Args: literal arguments other than the column itself (extract field,
  date_trunc unit, coalesce defaults). Each FunctionArg has a *string
  Literal for recoverable literals (NULL handled separately via IsNull),
  nil for non-literal expressions.
- IsNested: outermost-only attribution; true when the wrapped column is
  reached through more allowlisted-function wrappers.
- Cast: outermost cast target type (length(col)::int, length(col)::int::bigint,
  CAST(length(col) AS bigint)). Empty when there is no cast.

Scope is enforced by threading `wantWrappers bool` through
findAndRecordComparisons. WHERE call sites (SELECT, UPDATE, DELETE, set-op
branches, subqueries, CTE bodies) pass true; HAVING passes false. JOIN ON,
ORDER BY, GROUP BY, window PARTITION/ORDER, RETURNING and SELECT
projection use other extraction paths and never produce a wrapper by
construction. The legacy ColumnUsage.Functions []string is unchanged on
WHERE entries (stays empty there); document the asymmetry.

`Functions []string` continues to apply to projection/order/group/etc.
contexts unchanged.

Mirrored in analysis as SQLFunctionWrapper / SQLFunctionArg type aliases
plus a deep-clone in convertColumnUsage so analysis-side mutation cannot
affect the parser IR.

Test matrix covers: all 8 functions, all comparison operators (=, <>, <,
>, <=, >=, LIKE, ILIKE, NOT LIKE, IN, NOT IN, BETWEEN, IS NULL), both LHS
and RHS positions, case insensitivity, pg_catalog canonicalisation, three
nesting depths, casts on every operator type, casts via :: and CAST(...
AS ...), DML UPDATE/DELETE WHERE, subquery WHERE, CTE body WHERE, every
branch of UNION/UNION ALL/INTERSECT/EXCEPT, quoted columns with parens,
non-literal date_trunc unit, placeholder extract field, NULL coalesce
default. Negative cases: HAVING, INNER/LEFT/RIGHT/FULL/CROSS/USING joins,
ORDER BY, GROUP BY, window PARTITION/ORDER, RETURNING, projection,
foreign schemas, non-allowlisted functions, expressions inside wrappers,
casts on bare columns and on expressions.

Closes #51
@eitamring
eitamring marked this pull request as draft May 5, 2026 22:12
@eitamring

Copy link
Copy Markdown
Contributor Author

In testing

eitamring added 2 commits May 7, 2026 01:08
Add a Column Usage subsection that documents the new Function field on
ColumnUsage, the FunctionWrapper struct, the FunctionArg struct, the
8-name allowlist, the WHERE-equivalent attribution scope, schema
canonicalisation, outermost-only nesting attribution, cast capture, and
the rule for expression-wrapped columns. Add a usage snippet under
Practical Guidance showing how downstream consumers walk filter usages
and switch on Function.Name.
Move the contents of parser_ir_function_wrapper_test.go into the existing
parser_ir_advanced_test.go and delete the standalone file. This continues
the trend of merging narrowly-scoped test files back into the broader
advanced-IR suite rather than growing the flat top-level file count.

The strPtr / findUsageWithFunction / flattenColumnUsage helpers live with
the function-wrapper tests now and are only referenced from those tests,
so consolidation is a no-op for compile + go test.
@eitamring
eitamring marked this pull request as ready for review May 6, 2026 22:16
@eitamring
eitamring merged commit 71e58f8 into main May 6, 2026
2 checks passed
@eitamring
eitamring deleted the feature/eitam/issue-51-where-function-wrappers branch May 7, 2026 07:31
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.

ColumnUsage drops function wrappers (length/lower/coalesce/extract/date_trunc) when a column is wrapped in a WHERE predicate

1 participant