Package io.confluent.kafka.schemaregistry.type.logical.constraint
ConstraintValidationContext
and emits a CEL expression string suitable for evaluation by any cel-go /
cel-java engine. The translator is purely text-emitting — it does not bundle
a CEL evaluator.
Layered architecture
The translator runs three layers in order, designed so each layer catches a distinct class of error:
- Pre-emit hand-coded validators (
ConstraintValidator) — SQL-semantic checks that CEL itself can't see (e.g.col IS NULLon a non-nullable column is dead code,col + 1wherecolis a string is meant as concat in SQL even though CEL acceptsstring+string). These checks enforce SQL intent before emit. - Emit (
ConstraintEmitter,ConstraintFunctions,ConstraintPatterns) — cascade walkers turn the parse tree into CEL fragments. Type resolution lives inConstraintResolverand is shared between validators and emitters. - Post-emit strict cel-java type-check (
ConstraintCelChecker,ConstraintTypeProvider) — every emitted CEL is run through cel-java's strict type-checker with the schema's actual column types declared (notdyn). Any type/overload mismatch the hand-coded validators missed becomes a parse-timeValidationExceptioninstead of a runtime CEL error downstream.
What each layer is responsible for
ConstraintValidator covers checks strict can't subsume:
- Column-ref resolution (typos vs unknown bindings)
- Literal NULL operands to NULL-rejecting funcs (GREATEST/LEAST/ COALESCE/NULLIF) — strict's NULL-branch unification is fuzzier
- BETWEEN with NULL or
IS NULLas a bound — emits valid CEL but flips semantics - Literal index bounds (negative indices, out-of-range)
- Macro arity / lambda var name shape
- Date/time literal format and numeric overflow
- SQL intent on
+/-/*/%(numeric only — strict acceptsstring+stringas concat) - Cross-type equality (CEL accepts
int == stringas always-false; SQL rejects) - Ordering ops on non-orderable types (cel-java has
less_bool, cel-go does not — restored for cross-engine portability) - IN-list element-type homogeneity (CEL accepts
list<dyn>)
ConstraintCelChecker covers everything else — every type and overload error in the emitted CEL. Catches the long tail of bugs that hand-coded validators previously had to enumerate one-by-one (compound type errors through arithmetic, function-result types, has() argument shape, missing CEL overloads, etc.).
Cross-engine concerns
The strict checker uses Google cel-java (dev.cel) but the
emitted CEL may be evaluated by cel-go in production. A few overloads
differ between the two implementations:
- cel-java has
less_bool, cel-go does not — handled by the hand-coded orderable-type check - Format validators (
isEmail,isHostname, etc.) and the cel-go StringsExt methods (upperAscii,substring, etc.) are declared inConstraintCelChecker#commonDeclarations— production cel-go runtimes must register equivalents
Validation context
ConstraintValidationContext
carries the column/field bindings and signals placement (column-level vs
table-level CHECK). Placement determines emit shape:
- Column-level:
thisis the field's value;colin SQL collapses tothisin CEL - Table-level:
thisis the surrounding struct;colin SQL emits asthis.col
The strict checker reads the placement from vctx and declares
this accordingly: a primitive type for column-level, a synthetic
struct type (ConstraintTypeProvider)
for table-level.
Column-level skip-on-null contract
Column-level CHECKs are evaluated under a runtime contract: the CEL is not invoked when the column value is null/missing/unset. This rule applies uniformly across all formats (Avro nullable union, proto3 unset field, JSON missing property), giving column-level constraints format-portable semantics matching SQL CHECK's "TRUE or UNKNOWN passes" spec.
Two consequences for users:
- No null-guarding required. Built-in functions and operators
on the column (
LENGTH(name) > 0,name.matches(...),name + 'x' = ..., etc.) are safe at column level — the runtime won't pass null and the operations can't crash on null inputs. - Null-aware tests on the bare column are dead code.
<col> IS NULL/<col> IS NOT NULLandCOALESCE(<col>, ...)at column level are rejected by the validator. UseNOT NULLat the schema level for nullability declarations, or move the test to a table-level CHECK wherehas(this.x)provides format-aware presence checks.
The rule applies only to the bare column reference at column level.
Nested struct fields (<col>.field IS NULL), array indexing
(<col>[0] IS NULL), and macro iter-vars
(EVERY(<col>, t, t IS NULL)) keep their existing null-awareness
— only the outermost this is guaranteed non-null.
-
ClassesClassDescriptionPublic entry point for translating a parsed SQL CHECK expression (postgres-derived subset) to a CEL expression string.Context passed to
ConstraintToCelTranslatorfor parse-time validation of CHECK constraint expressions.