Skip to content

Commit dcf3c1d

Browse files
committed
refactor(stack): remove obsolete v3 freeTextSearch tuner
In v3 the concrete type defines capabilities, so the vestigial v2-style chainable .freeTextSearch(opts) tuner on EncryptedTextSearchColumn is obsolete. Remove the method, its matchOpts field, and the build() override that only cloned tuner opts; default match building is unchanged (the base build() already emits the default match config for the TextSearch type). The 'freeTextSearch' query-type/capability is untouched. Drops the now-obsolete tuner tests.
1 parent 6b47444 commit dcf3c1d

3 files changed

Lines changed: 17 additions & 127 deletions

File tree

packages/stack/__tests__/schema-v3.test.ts

Lines changed: 8 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -22,65 +22,19 @@ describe('eql_v3 text_search column', () => {
2222
expect(v3).toStrictEqual(v2)
2323
})
2424

25-
it('.freeTextSearch(opts) overrides each provided key and keeps the rest as defaults', () => {
26-
const built = types
27-
.TextSearch('email')
28-
.freeTextSearch({
29-
tokenizer: { kind: 'ngram', token_length: 4 },
30-
k: 8,
31-
m: 4096,
32-
include_original: false,
33-
})
34-
.build()
25+
it('default build() emits the unique, ore, and match indexes', () => {
26+
const built = types.TextSearch('email').build()
27+
expect(built.indexes.unique).toEqual({ token_filters: [] })
28+
expect(built.indexes.ore).toEqual({})
3529
expect(built.indexes.match).toEqual({
36-
tokenizer: { kind: 'ngram', token_length: 4 },
37-
// omitted -> default downcase filter retained
30+
tokenizer: { kind: 'ngram', token_length: 3 },
3831
token_filters: [{ kind: 'downcase' }],
39-
k: 8,
40-
m: 4096,
41-
include_original: false,
32+
k: 6,
33+
m: 2048,
34+
include_original: true,
4235
})
4336
})
4437

45-
it('.freeTextSearch({ token_filters: [] }) overrides the downcase default with an empty array', () => {
46-
// LOAD-BEARING: `[] ?? default` evaluates to `[]` (an empty array is not
47-
// nullish), so an explicit empty array must OVERRIDE the downcase default,
48-
// not fall back to it. Mirrors v2 (schema-builders.test.ts).
49-
const built = types
50-
.TextSearch('email')
51-
.freeTextSearch({ token_filters: [] })
52-
.build()
53-
expect(built.indexes.match.token_filters).toEqual([])
54-
})
55-
56-
it('repeated .freeTextSearch() calls are last-call-wins-fully (each re-merges against defaults, not prior state)', () => {
57-
// Each call re-merges against a fresh defaultMatchOpts(), not the
58-
// accumulated matchOpts — so the second call resets k back to its default
59-
// of 6. This is intentional: it mirrors v2 exactly. Pinned here so a future
60-
// "merge against current state" change can't silently slip in.
61-
const built = types
62-
.TextSearch('email')
63-
.freeTextSearch({ k: 8 })
64-
.freeTextSearch({ m: 4096 })
65-
.build()
66-
expect(built.indexes.match.k).toBe(6)
67-
expect(built.indexes.match.m).toBe(4096)
68-
})
69-
70-
it('.freeTextSearch() with no argument is a no-op: build() equals the default build()', () => {
71-
// Pins the opts === undefined branch: every `opts?.x ?? default` falls
72-
// through, so a bare call must emit exactly the default match block.
73-
expect(types.TextSearch('email').freeTextSearch().build()).toStrictEqual(
74-
types.TextSearch('email').build(),
75-
)
76-
})
77-
78-
it('.freeTextSearch() is tuning-only: unique and ore indexes stay present', () => {
79-
const built = types.TextSearch('email').freeTextSearch({ k: 8 }).build()
80-
expect(built.indexes.unique).toEqual({ token_filters: [] })
81-
expect(built.indexes.ore).toEqual({})
82-
})
83-
8438
it('built columns share no mutable state: mutating one build() output does not affect another', () => {
8539
// Guards against the shared-defaults aliasing bug: defaults come from a
8640
// per-instance factory and build() deep-clones the match block.
@@ -104,29 +58,6 @@ describe('eql_v3 text_search column', () => {
10458
expect(c.indexes.match.k).toBe(6)
10559
expect(c.indexes.match.token_filters).toEqual([{ kind: 'downcase' }])
10660
})
107-
108-
it('clones caller opts on freeTextSearch(): mutating them before build() does not leak', () => {
109-
// build() deep-clones at build time, but if freeTextSearch stored the
110-
// caller's nested tokenizer / token_filters by reference, a caller mutating
111-
// their own opts object between freeTextSearch(opts) and build() would leak
112-
// the mutation into the emitted config. freeTextSearch must clone on write.
113-
const opts = {
114-
tokenizer: { kind: 'ngram' as const, token_length: 3 },
115-
token_filters: [{ kind: 'downcase' as const }],
116-
}
117-
const col = types.TextSearch('email').freeTextSearch(opts)
118-
119-
// Mutate the caller's own opts AFTER freeTextSearch but BEFORE build().
120-
opts.tokenizer.token_length = 999
121-
opts.token_filters.push({ kind: 'downcase' as const })
122-
123-
const built = col.build()
124-
expect(built.indexes.match.tokenizer).toEqual({
125-
kind: 'ngram',
126-
token_length: 3,
127-
})
128-
expect(built.indexes.match.token_filters).toEqual([{ kind: 'downcase' }])
129-
})
13061
})
13162

13263
describe('eql_v3 text_match column', () => {

packages/stack/src/eql/v3/columns.ts

Lines changed: 5 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
1-
import type { ColumnSchema, MatchIndexOpts } from '@/schema'
2-
import {
3-
type BuiltMatchIndexOpts,
4-
cloneMatchOpts,
5-
defaultMatchOpts,
6-
resolveMatchOpts,
7-
} from '@/schema/match-defaults'
1+
import type { ColumnSchema } from '@/schema'
2+
import { defaultMatchOpts } from '@/schema/match-defaults'
83

94
/**
105
* The query capabilities a v3 concrete domain exposes. These are SDK-facing
@@ -155,7 +150,7 @@ export const SMALLINT_ORD = {
155150
// bigint (int8) domains. Plaintext is a JS `bigint` (always decrypts to
156151
// `bigint`); bounds are the full i64 range, enforced at the protect-ffi
157152
// boundary, which round-trips a native bigint losslessly. (`*_ord_ope`
158-
// variants are out of scope — CIP-3403.)
153+
// variants are out of scope for this change.)
159154
export const BIGINT = {
160155
eqlType: 'public.bigint',
161156
castAs: 'bigint',
@@ -424,8 +419,8 @@ const TEXT_SEARCH_DOMAIN = {
424419
* Builder for a `public.text_search` column.
425420
*
426421
* The concrete type inherently enables equality + order/range + free-text
427-
* match — there are no capability-enabling methods. `.freeTextSearch(opts?)`
428-
* tunes the match index only.
422+
* match — there are no capability-enabling or tuning methods. The match index
423+
* is always emitted with the default configuration.
429424
*
430425
* NOTE — querying: a `text_search` column emits all three indexes (`unique`,
431426
* `ore`, `match`), and the shared index-inference picks them by fixed priority
@@ -444,45 +439,8 @@ const TEXT_SEARCH_DOMAIN = {
444439
export class EncryptedTextSearchColumn extends EncryptedV3Column<
445440
typeof TEXT_SEARCH_DOMAIN
446441
> {
447-
private matchOpts: BuiltMatchIndexOpts
448-
449442
constructor(columnName: string) {
450443
super(columnName, TEXT_SEARCH_DOMAIN)
451-
this.matchOpts = defaultMatchOpts()
452-
}
453-
454-
/**
455-
* Tune the match index. Each provided key replaces its default; omitted
456-
* keys keep the default. This NEVER enables a capability — match is always
457-
* on for this type. Merge semantics mirror v2's `opts?.x ?? default`.
458-
*/
459-
freeTextSearch(opts?: MatchIndexOpts): this {
460-
// Shared merge+clone (schema/match-defaults) — one source of truth with the
461-
// v2 `freeTextSearch()` builder. `resolveMatchOpts` merges each key over the
462-
// per-call defaults and deep-clones, so a caller mutating their own opts
463-
// object between `freeTextSearch(opts)` and `build()` cannot leak into the
464-
// emitted config (clone-on-write).
465-
this.matchOpts = resolveMatchOpts(opts)
466-
return this
467-
}
468-
469-
/** Emit the encrypt-config column. Byte-identical to a v2 equality+order+match column. */
470-
override build(): ColumnSchema {
471-
// Derive `cast_as` + the `unique`/`ore` blocks from the shared
472-
// capability→index mapping (this domain's TEXT_SEARCH capabilities produce
473-
// exactly `{ unique, ore, match }`), then override ONLY `match` with this
474-
// builder's tuned opts. Hand-writing the unique/ore shape here would let it
475-
// silently drift from `indexesForCapabilities` — the exact divergence class
476-
// behind the text_ord `hm`-index bug. Deep-clone the match block so the
477-
// returned config never aliases this builder's internal `matchOpts`.
478-
const base = super.build()
479-
return {
480-
...base,
481-
indexes: {
482-
...base.indexes,
483-
match: cloneMatchOpts(this.matchOpts),
484-
},
485-
}
486444
}
487445
}
488446

packages/stack/src/eql/v3/types.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,10 @@ import {
103103
* })
104104
* ```
105105
*
106-
* `types.TextSearch` keeps the chainable `.freeTextSearch(opts)` tuner (the
107-
* only capability-bearing chain — every other domain is fully described by its
108-
* type).
106+
* `types.TextSearch` is fully described by its type — like every other domain,
107+
* it has no capability-bearing or tuning chain. Its match index is always
108+
* emitted with the default configuration; run a free-text query by passing
109+
* `queryType: 'freeTextSearch'` to `encryptQuery`.
109110
*/
110111
export const types = {
111112
// integer

0 commit comments

Comments
 (0)