Commit 67d840f
committed
fix(stack): count codepoints in the free-text needle floor
Review of #607 found the needle guard it added was itself incomplete, and two
claims it made were false. Three agents verified each finding against live
Postgres before anything changed here.
The guard counted UTF-16 code units; the ngram tokenizer counts CODEPOINTS. A
needle of 1-2 astral-plane characters cleared the floor, produced no trigram,
and so built an EMPTY bloom filter -- `stored_bf @> '{}'` matches every row.
Measured live against three seeded rows:
"👍👍" utf16=4 cp=2 -> 3/3 rows <- passed the guard
"👍👍👍" utf16=6 cp=3 -> 0/3 rows
"joh" utf16=3 cp=3 -> 1/3 rows
Codepoints, not graphemes: NFD "e+combining-acute" twice is 4 codepoints but 2
grapheme clusters, and does build a non-empty filter (0/3 rows live). A grapheme
floor would wrongly reject it; a byte floor would wrongly accept the emoji. The
three measurements are consistent only with codepoint counting.
Also rejects the empty needle under ANY tokenizer. `matchNeedleMinLength`
returns undefined for the `standard` tokenizer, so the guard short-circuited and
`contains(col, '')` tokenized to nothing and matched every row. Unreachable via
the v3 `types` namespace today, which hardcodes ngram, but the schema permits it.
Tests, written first and watched fail:
- `__tests__/match-needle-guard.test.ts` pins the unit. The astral and
empty-needle cases failed before the fix; the NFD case passed before and
after, proving the fix does not over-correct into rejecting usable needles.
Unicode is built from explicit escapes -- the precomposed NFC form is a
different, 2-codepoint string, so a bare literal would silently test the
opposite case depending on the file's on-disk normalisation.
- The live `contains` rejection now asserts the guard's own message, not just
`EncryptionOperatorError`. `operandFailure` throws the same class, and that is
not hypothetical: `text_search` carries an `ore` index and cannot encrypt any
non-ASCII operand ("Can only order strings that are pure ASCII"), so the
astral case was passing there for the wrong reason. That constraint now has
its own test, and the codepoint complement runs only on match-only columns.
Two false claims corrected rather than papered over:
- The changeset and the doc comment both said the floor was "shared with the v2
adapter". It is not -- `matchNeedleError` has exactly one caller, in v3. v2's
`like`/`ilike` reduces to `eql_v2.bloom_filter(a) @> eql_v2.bloom_filter(b)`
(cipherstash-encrypt.sql), native array containment, so v2 has the same
fail-open and is NOT fixed here. Wiring it in needs its own measurement first:
v2 needles carry SQL wildcards, so the floor may belong on the unwrapped term.
- `decryptDenied` returned a bare boolean and treated any throw as denial, so a
ZeroKMS outage read as "the identity boundary held". It now returns the
message and the call sites assert /^Failed to retrieve key/, matching the
assertion this PR already made in `lock-context.test.ts`.
The `as SQL` cast in `inArrayOp` was reviewed and kept: drizzle returns
undefined only for an empty filtered list, and the empty-list guard throws
first. Restoring the old `?? sql`false`` fallback would be worse -- it would
mask a `notInArray` that silently matches everything.1 parent 0ebf57e commit 67d840f
5 files changed
Lines changed: 250 additions & 41 deletions
File tree
- .changeset
- packages/stack
- __tests__
- drizzle-v3
- src/schema
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
15 | 21 | | |
16 | 22 | | |
17 | 23 | | |
| |||
23 | 29 | | |
24 | 30 | | |
25 | 31 | | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
Lines changed: 67 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
549 | 549 | | |
550 | 550 | | |
551 | 551 | | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
552 | 556 | | |
553 | | - | |
554 | | - | |
555 | | - | |
556 | | - | |
557 | | - | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
558 | 620 | | |
559 | 621 | | |
560 | 622 | | |
| |||
Lines changed: 40 additions & 19 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
91 | 97 | | |
92 | | - | |
| 98 | + | |
93 | 99 | | |
94 | | - | |
| 100 | + | |
95 | 101 | | |
96 | 102 | | |
97 | | - | |
98 | | - | |
99 | | - | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
100 | 109 | | |
101 | 110 | | |
102 | 111 | | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
103 | 119 | | |
104 | 120 | | |
105 | 121 | | |
| |||
230 | 246 | | |
231 | 247 | | |
232 | 248 | | |
233 | | - | |
234 | | - | |
235 | | - | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
236 | 253 | | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
237 | 257 | | |
238 | 258 | | |
239 | 259 | | |
| |||
244 | 264 | | |
245 | 265 | | |
246 | 266 | | |
247 | | - | |
248 | | - | |
249 | | - | |
250 | | - | |
251 | | - | |
252 | | - | |
253 | | - | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
254 | 275 | | |
255 | 276 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
98 | | - | |
99 | | - | |
100 | | - | |
101 | | - | |
102 | | - | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
103 | 103 | | |
104 | | - | |
105 | | - | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
106 | 121 | | |
107 | 122 | | |
108 | 123 | | |
109 | 124 | | |
110 | 125 | | |
111 | 126 | | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
112 | 130 | | |
113 | | - | |
114 | | - | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
115 | 137 | | |
0 commit comments