|
8 | 8 | * |
9 | 9 | * - Single-codec entries (`cipherstashEq`, `cipherstashIlike`, |
10 | 10 | * `cipherstashNotIlike`, `cipherstashJsonbPathExists`) declare |
11 | | - * `self: { codecId: '<id>' }`. The framework's `OpMatchesField` |
| 11 | + * `self: { codecId: '<id>' }` (v2 legacy registrations). The framework's `OpMatchesField` |
12 | 12 | * direct-codec-id branch surfaces the method on columns whose |
13 | 13 | * codec id is the literal — no consumer-side `CodecTypes` |
14 | 14 | * augmentation needed. |
@@ -86,15 +86,14 @@ type SearchableJsonTraits = readonly ['cipherstash:searchable-json'] |
86 | 86 | /** |
87 | 87 | * v3 TYPE-LEVEL marker traits (no runtime counterpart) — carried only |
88 | 88 | * by the v3 codec entries in `codec-types.ts` (see the vocabulary |
89 | | - * comment there). Three methods differ by runtime generation: |
90 | | - * `cipherstashEq` / `cipherstashIlike` are codec-id-pinned legacy |
91 | | - * registrations in v2 but trait-dispatched in v3, and |
92 | | - * `cipherstashJsonContains` (v3) / `cipherstashJsonbPathExists` (v2) |
93 | | - * exist in only one generation each. Dispatching those on the marker |
94 | | - * traits keeps type-level visibility exactly aligned with what each |
95 | | - * column's runtime can actually execute. |
| 89 | + * comment there). The v2 and v3 surfaces have DISJOINT method names |
| 90 | + * (`cipherstash*` vs the EQL-derived `eql*`), so every v3 operator |
| 91 | + * dispatches on a v3 marker and every v2 operator on the shared v2 |
| 92 | + * trait (or legacy codec-id pin) — type-level visibility stays exactly |
| 93 | + * aligned with what each column's runtime can actually execute. |
96 | 94 | */ |
97 | 95 | type V3EqualityMarker = readonly ['cipherstash:v3-equality'] |
| 96 | +type V3OrderAndRangeMarker = readonly ['cipherstash:v3-order-and-range'] |
98 | 97 | type V3FreeTextSearchMarker = readonly ['cipherstash:v3-free-text-search'] |
99 | 98 | type V3SearchableJsonMarker = readonly ['cipherstash:v3-searchable-json'] |
100 | 99 |
|
@@ -138,31 +137,22 @@ type AnyExpressionLike = Expression<{ |
138 | 137 | export type QueryOperationTypes<CT extends CodecTypesBase> = |
139 | 138 | CT extends CodecTypesBase |
140 | 139 | ? { |
141 | | - // `self` carries BOTH dispatch forms: `OpMatchesField` tries the |
142 | | - // codec-id branch first (matches the v2 legacy single-codec |
143 | | - // registration on `cipherstash/string@1`) and falls through to |
144 | | - // the traits branch (matches v3 columns via the type-level |
145 | | - // `cipherstash:v3-*` markers). v2 non-string columns match |
146 | | - // neither — their runtime has no `cipherstashEq` to dispatch. |
147 | | - // The comparand is `unknown` (not `pg/text@1`) because v3 |
148 | | - // equality spans every equality-capable domain family (bigint, |
149 | | - // date, numeric, …), and the runtime coerces + encrypts the |
150 | | - // operand per the column's castAs. |
| 140 | + // ------------------------------------------------------------- |
| 141 | + // v2 legacy surface (`cipherstash*`). `cipherstashEq` / |
| 142 | + // `cipherstashIlike` are codec-id-pinned to the v2 string codec |
| 143 | + // (legacy single-codec registrations); the rest dispatch on the |
| 144 | + // shared v2 traits, which v3 codec entries no longer carry — |
| 145 | + // so none of these surface on a v3 column. |
| 146 | + // ------------------------------------------------------------- |
151 | 147 | readonly cipherstashEq: { |
152 | | - readonly self: { |
153 | | - readonly codecId: CipherstashStringCodec |
154 | | - readonly traits: V3EqualityMarker |
155 | | - } |
| 148 | + readonly self: { readonly codecId: CipherstashStringCodec } |
156 | 149 | readonly impl: ( |
157 | 150 | self: AnyExpressionLike, |
158 | 151 | other: unknown, |
159 | 152 | ) => PgBoolReturn |
160 | 153 | } |
161 | 154 | readonly cipherstashIlike: { |
162 | | - readonly self: { |
163 | | - readonly codecId: CipherstashStringCodec |
164 | | - readonly traits: V3FreeTextSearchMarker |
165 | | - } |
| 155 | + readonly self: { readonly codecId: CipherstashStringCodec } |
166 | 156 | readonly impl: ( |
167 | 157 | self: AnyExpressionLike, |
168 | 158 | pattern: CodecExpression<'pg/text@1', boolean, CT>, |
@@ -244,13 +234,102 @@ export type QueryOperationTypes<CT extends CodecTypesBase> = |
244 | 234 | readonly self: { readonly traits: SearchableJsonTraits } |
245 | 235 | readonly impl: (self: AnyExpressionLike, path: string) => PgBoolReturn |
246 | 236 | } |
| 237 | + // ------------------------------------------------------------- |
| 238 | + // v3 surface (`eql*`, EQL-derived vocabulary — PR #655 review). |
| 239 | + // Every entry dispatches on a `cipherstash:v3-*` marker, which |
| 240 | + // only v3 codec entries carry, so nothing here surfaces on a |
| 241 | + // v2 column (whose runtime has no `eql*` method to dispatch) — |
| 242 | + // and, symmetrically, the v2 entries above never surface on v3 |
| 243 | + // columns. The comparand is `unknown` because each operator |
| 244 | + // spans every capable domain family (bigint, date, numeric, …) |
| 245 | + // and the runtime coerces + encrypts per the column's castAs. |
| 246 | + // ------------------------------------------------------------- |
| 247 | + readonly eqlEq: { |
| 248 | + readonly self: { readonly traits: V3EqualityMarker } |
| 249 | + readonly impl: ( |
| 250 | + self: AnyExpressionLike, |
| 251 | + other: unknown, |
| 252 | + ) => PgBoolReturn |
| 253 | + } |
| 254 | + readonly eqlNeq: { |
| 255 | + readonly self: { readonly traits: V3EqualityMarker } |
| 256 | + readonly impl: ( |
| 257 | + self: AnyExpressionLike, |
| 258 | + other: unknown, |
| 259 | + ) => PgBoolReturn |
| 260 | + } |
| 261 | + readonly eqlIn: { |
| 262 | + readonly self: { readonly traits: V3EqualityMarker } |
| 263 | + readonly impl: ( |
| 264 | + self: AnyExpressionLike, |
| 265 | + values: readonly unknown[], |
| 266 | + ) => PgBoolReturn |
| 267 | + } |
| 268 | + readonly eqlNotIn: { |
| 269 | + readonly self: { readonly traits: V3EqualityMarker } |
| 270 | + readonly impl: ( |
| 271 | + self: AnyExpressionLike, |
| 272 | + values: readonly unknown[], |
| 273 | + ) => PgBoolReturn |
| 274 | + } |
| 275 | + readonly eqlGt: { |
| 276 | + readonly self: { readonly traits: V3OrderAndRangeMarker } |
| 277 | + readonly impl: ( |
| 278 | + self: AnyExpressionLike, |
| 279 | + other: unknown, |
| 280 | + ) => PgBoolReturn |
| 281 | + } |
| 282 | + readonly eqlGte: { |
| 283 | + readonly self: { readonly traits: V3OrderAndRangeMarker } |
| 284 | + readonly impl: ( |
| 285 | + self: AnyExpressionLike, |
| 286 | + other: unknown, |
| 287 | + ) => PgBoolReturn |
| 288 | + } |
| 289 | + readonly eqlLt: { |
| 290 | + readonly self: { readonly traits: V3OrderAndRangeMarker } |
| 291 | + readonly impl: ( |
| 292 | + self: AnyExpressionLike, |
| 293 | + other: unknown, |
| 294 | + ) => PgBoolReturn |
| 295 | + } |
| 296 | + readonly eqlLte: { |
| 297 | + readonly self: { readonly traits: V3OrderAndRangeMarker } |
| 298 | + readonly impl: ( |
| 299 | + self: AnyExpressionLike, |
| 300 | + other: unknown, |
| 301 | + ) => PgBoolReturn |
| 302 | + } |
| 303 | + readonly eqlBetween: { |
| 304 | + readonly self: { readonly traits: V3OrderAndRangeMarker } |
| 305 | + readonly impl: ( |
| 306 | + self: AnyExpressionLike, |
| 307 | + low: unknown, |
| 308 | + high: unknown, |
| 309 | + ) => PgBoolReturn |
| 310 | + } |
| 311 | + readonly eqlNotBetween: { |
| 312 | + readonly self: { readonly traits: V3OrderAndRangeMarker } |
| 313 | + readonly impl: ( |
| 314 | + self: AnyExpressionLike, |
| 315 | + low: unknown, |
| 316 | + high: unknown, |
| 317 | + ) => PgBoolReturn |
| 318 | + } |
| 319 | + // Fuzzy free-text token match (`eql_v3.contains`) — NOT SQL |
| 320 | + // pattern matching; needles are guarded at lowering time |
| 321 | + // (wildcards, short needles). No negated form: the bloom test |
| 322 | + // may false-positive, so its negation would false-negative. |
| 323 | + readonly eqlMatch: { |
| 324 | + readonly self: { readonly traits: V3FreeTextSearchMarker } |
| 325 | + readonly impl: ( |
| 326 | + self: AnyExpressionLike, |
| 327 | + needle: unknown, |
| 328 | + ) => PgBoolReturn |
| 329 | + } |
247 | 330 | // v3-only: encrypted jsonb `@>` containment on `eql_v3_json` |
248 | | - // columns. Dispatches on the v3 marker trait (NOT the shared |
249 | | - // `cipherstash:searchable-json`) so it never surfaces on v2 |
250 | | - // JSON columns, whose runtime does not register the method — |
251 | | - // and, symmetrically, `cipherstashJsonbPathExists` above never |
252 | | - // surfaces on v3 columns. |
253 | | - readonly cipherstashJsonContains: { |
| 331 | + // columns. |
| 332 | + readonly eqlJsonContains: { |
254 | 333 | readonly self: { readonly traits: V3SearchableJsonMarker } |
255 | 334 | readonly impl: ( |
256 | 335 | self: AnyExpressionLike, |
|
0 commit comments