Skip to content

fix: Cap query nesting depth and phrase expansion to prevent backend … - #37

Merged
elemdisc merged 1 commit into
mainfrom
dsl-parser-improvements
Jul 4, 2026
Merged

fix: Cap query nesting depth and phrase expansion to prevent backend …#37
elemdisc merged 1 commit into
mainfrom
dsl-parser-improvements

Conversation

@elemdisc

@elemdisc elemdisc commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

…crashes

  • Add MAX_DEPTH (1024): nested (/! plus chained within/pre/not-within operators deepen the parser's own recursion with no lexical nesting, so an unbounded chain overflows the stack and crashes the whole Postgres backend rather than erroring.
  • Add MAX_PHRASE_EXPANSION (16384): <->/<N> distribute an OR operand into every combination, so alternatives multiply along a chain and a couple hundred bytes of (a|b) <-> (c|d) <-> … can expand into gigabytes at parse time.
  • Mirror both caps in the pure SQL port (_prox_p_depth_check, the cross-product size check in phrase combination) so the two implementations reject the same queries.
  • Add Rust boundary tests for both caps and corresponding capn*/capx* cases in tests/parity_cases.md.

…crashes

- Add `MAX_DEPTH` (1024): nested `(`/`!` plus chained within/pre/not-within operators
  deepen the parser's own recursion with no lexical nesting, so an unbounded chain
  overflows the stack and crashes the whole Postgres backend rather than erroring.
- Add `MAX_PHRASE_EXPANSION` (16384): `<->`/`<N>` distribute an OR operand into every
  combination, so alternatives multiply along a chain and a couple hundred bytes of
  `(a|b) <-> (c|d) <-> …` can expand into gigabytes at parse time.
- Mirror both caps in the pure SQL port (`_prox_p_depth_check`, the cross-product size
  check in phrase combination) so the two implementations reject the same queries.
- Add Rust boundary tests for both caps and corresponding `capn*`/`capx*` cases in
  `tests/parity_cases.md`.
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

proxquery — pure-SQL port vs native extension

Generated 2026-07-04 02:02:06Z · commit 3da5f5b

Context

key value
host Linux 6.17.0-1018-azure x86_64
cpu Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz (4 cores)
memory 16 GiB
load avg (1/5/15m) 1.00 0.84 0.45
postgres 17.10
proxquery extension 0.5.1
main corpus 64 MiB · 160 generated queries · 1 iters
tokenizer corpus 10000 docs × 40 tokens · 1 iters
scaling sweep 2000 docs × {32,128,512,2048} tokens · 1 iters
total wall time 387s

Results — by query shape

Average server-side ms/query over 1 run(s), no warmup (corpus already
cache-warm) — qualitative, over a
deterministic COCA-frequency corpus and a generated query mix — so each shape is
exercised across a real spread of term frequencies and selectivities (avg_cand
= rows the @@ skeleton selects, avg_match = rows that actually match).
Timings are load-sensitive — check the load average in Context before comparing runs.

shape n avg_cand avg_match ext_op_ms ext_seq_ms index_speedup ext_search_ms pure_search_ms slowdown
hardwild 7 719 242 436.07 2751.51 6.3 437.27 2419.08 5.5
wildcard 9 3726 3726 158.23 354.82 2.2 158.62 158.87 1.0
or2 11 4111 4111 130.23 355.88 2.7 130.70 131.81 1.0
notwithin 6 935 693 77.78 479.61 6.2 78.35 3575.82 45.6
wildprox 17 335 144 33.76 485.06 14.4 34.57 32.36 0.9
within 22 324 233 29.43 472.58 16.1 30.14 26.79 0.9
ordered 8 294 152 27.44 472.90 17.2 28.05 24.88 0.9
phraseprox 9 22 13 20.24 531.29 26.3 19.61 143.21 7.3
phrase 10 11 11 14.51 354.49 24.4 14.96 15.69 1.0
chain3 13 118 50 13.62 525.05 38.5 14.20 706.09 49.7
chain4 8 59 10 8.28 577.79 69.8 8.87 556.21 62.7
single 8 1520 1520 0.75 354.45 471.0 1.28 1.47 1.1
and2 20 629 629 0.56 355.64 630.6 1.02 2.22 2.2
and3 12 269 269 0.51 354.69 697.7 0.93 2.36 2.5
  • ext_op_ms — extension single operator tsv @~@ q (GIN-index-served)
  • ext_seq_ms — the SAME @~@ query with the index disabled (recheck over every
    row, a full seq scan) — the brute-force baseline; identical rows, just unaccelerated.
    One un-warmed run per query (qualitative — an order-of-magnitude index speedup, not a
    precise metric); the corpus is already cache-warm from the indexed measurements
  • index_speedupext_seq_ms / ext_op_ms (what the GIN index buys)
  • ext_search_ms — extension via the consolidated ts_prox_search(tsv, q)
  • pure_search_ms — pure-SQL port via the same ts_prox_search
  • slowdownpure_search_ms / ext_search_ms
Per-query breakdown — all 160 queries (counts + timings)
id shape query candidates matches ext_op_ms ext_search_ms pure_search_ms slowdown
1 notwithin christmas <!~2> college 690 518 57.93 58.76 2684.64 45.7
2 chain4 conservative <~3> file <~12> instead <~7> predict 74 6 10.37 10.96 709.57 64.7
3 and2 fruit & argument 108 108 0.33 0.74 2.21 3.0
4 ordered suspect <-4> flash 58 15 6.50 7.04 6.75 1.0
5 single yeah 3640 3640 1.27 1.89 1.99 1.1
6 chain3 review <~5> wolf <~12> experiment 75 18 8.72 9.32 445.13 47.8
7 chain4 fashion <~3> rush <~7> simple <~2> promote 66 1 9.63 10.21 668.27 65.5
8 or2 tree | big 1993 1993 1.11 1.68 2.83 1.7
9 or2 extend | quite 1381 1381 0.88 1.53 2.60 1.7
10 single next 1785 1785 0.93 1.49 1.68 1.1
11 ordered stretch <-13> waste 246 90 23.84 24.47 22.38 0.9
12 phraseprox "spring dream" <~12> code 20 11 22.21 21.06 150.80 7.2
13 and2 address & companion 53 53 0.27 0.67 1.99 3.0
14 and3 account & all & city 398 398 0.74 1.22 2.65 2.2
15 notwithin color <!~8> father 1069 417 88.04 88.74 3967.65 44.7
16 wildprox fourth <~2> implemen* 172 44 18.10 18.71 17.08 0.9
17 and3 fan & capital & me 163 163 0.52 0.95 2.38 2.5
18 phraseprox "recall senate" <~15> book 14 12 22.54 20.81 107.29 5.2
19 phraseprox "claim ha" <~15> bad 12 12 19.28 17.91 82.57 4.6
20 wildprox career <~14> believ* 617 576 58.10 58.87 54.80 0.9
21 or2 murder | extremely 1157 1157 0.82 1.39 2.49 1.8
22 wildprox claim <~2> ha* 1024 142 107.67 109.42 87.22 0.8
23 single although 1100 1100 0.79 1.45 1.49 1.0
24 chain3 fill <~4> value <~14> june 183 55 21.00 21.65 1114.44 51.5
25 chain3 instrument <~6> bill <~15> breath 113 33 13.10 13.59 665.16 48.9
26 wildprox marketing <~15> univer* 257 56 29.53 30.27 32.47 1.1
27 wildprox rape <~13> devi* 209 122 21.65 22.41 22.73 1.0
28 wildcard all* 7206 7206 354.55 355.48 354.96 1.0
29 hardwild ##capac.*## <~6> formula 306 43 370.17 371.64 1049.08 2.8
30 wildcard fair* 921 921 0.91 1.44 1.66 1.2
31 wildprox glad <~10> ordin* 95 62 10.84 11.41 11.48 1.0
32 ordered factor <-5> turn 679 382 59.68 60.15 52.05 0.9
33 phraseprox "provider neither" <~2> county 4 2 10.69 10.02 30.66 3.1
34 and2 quality & kind 329 329 0.51 0.97 2.16 2.2
35 within demonstrate <~3> expense 94 31 10.40 11.00 10.20 0.9
36 and3 assistance & village & modern 24 24 0.29 0.67 2.13 3.2
37 hardwild ##eas.*## <~7> bother 469 114 569.52 570.48 1607.85 2.8
38 and2 indian & wing 91 91 0.30 0.69 1.90 2.8
39 within collection <~13> doctor 353 278 34.30 34.76 31.09 0.9
40 within similar <~4> tax 790 563 68.29 69.88 60.70 0.9
41 phrase "cut battery" 24 24 19.07 20.00 20.56 1.0
42 and2 decide & few 432 432 0.59 1.07 2.20 2.1
43 chain3 collection <~10> tension <~5> criticism 34 7 4.89 5.48 249.83 45.6
44 and2 ensure & wife 173 173 0.36 0.80 2.00 2.5
45 notwithin activity <!~3> uncle 1003 943 84.17 84.72 3893.49 46.0
46 ordered symbol <-15> association 176 84 17.22 17.70 16.34 0.9
47 and2 your & or 5074 5074 1.89 2.56 3.54 1.4
48 wildcard get* 7155 7155 354.29 354.49 355.65 1.0
49 wildprox successful <~7> roo* 422 116 43.82 44.80 42.74 1.0
50 hardwild ba*r <~4> return 1119 360 108.19 110.84 3633.61 32.8
51 phrase "personal compete" 19 19 17.39 17.91 18.29 1.0
52 wildcard princi* 944 944 0.93 1.46 1.68 1.2
53 or2 or | me 8639 8639 355.90 355.71 357.56 1.0
54 within hold <~14> breathe 266 237 24.55 24.88 22.35 0.9
55 wildcard eve* 6141 6141 355.49 354.90 355.37 1.0
56 notwithin tea <!~9> edge 381 298 32.83 33.51 1533.39 45.8
57 within violent <~12> toss 93 59 9.82 10.15 9.59 0.9
58 chain4 physician <~6> library <~14> act <~11> marriage 75 9 10.18 10.94 690.97 63.2
59 within describe <~12> finding 320 250 30.75 31.25 28.12 0.9
60 hardwild *ment <~3> identity 500 158 875.16 875.14 2584.82 3.0
61 within ignore <~7> approval 129 71 13.26 13.74 12.87 0.9
62 single publicly 255 255 0.32 0.73 1.02 1.4
63 chain3 hill <~3> prisoner <~14> pain 56 7 7.19 7.72 374.71 48.5
64 wildprox liberal <~11> succee* 118 79 12.39 12.98 13.09 1.0
65 wildprox editor <~15> tig* 218 119 22.82 23.71 24.51 1.0
66 and2 lady & perspective 153 153 0.36 0.76 1.98 2.6
67 phrase "channel religious" 17 17 20.80 21.29 21.95 1.0
68 or2 meaningful | want 5446 5446 1.61 2.28 3.18 1.4
69 and2 about & around 2703 2703 1.41 1.99 3.09 1.6
70 and2 mean & famous 449 449 0.59 1.07 2.24 2.1
71 or2 end | exactly 2204 2204 1.09 1.92 2.68 1.4
72 or2 devote | twist 476 476 0.50 0.94 2.06 2.2
73 and3 later & forward & provide 188 188 0.47 0.91 2.36 2.6
74 wildprox combine <~15> locatio* 240 169 24.14 24.72 24.61 1.0
75 chain3 visitor <~3> both <~12> possible 157 75 17.34 17.99 901.38 50.1
76 and2 uh & there 901 901 0.92 1.44 2.56 1.8
77 wildprox court <~5> lif* 1094 337 97.51 98.34 91.34 0.9
78 within republic <~5> order 134 73 13.70 14.14 12.94 0.9
79 ordered night <-9> strong 769 511 68.77 69.36 60.49 0.9
80 and2 different & evil 244 244 0.41 0.88 2.05 2.3
81 and3 one & store & far 391 391 0.72 1.20 2.59 2.2
82 and2 chicken & relation 82 82 0.30 0.68 1.92 2.8
83 notwithin tax <!~12> politics 1328 960 107.92 108.18 4913.16 45.4
84 or2 constant | who 6351 6351 356.27 355.80 358.69 1.0
85 within thin <~9> absolutely 224 153 20.98 21.50 19.60 0.9
86 and2 client & around 424 424 0.57 1.09 2.24 2.1
87 phrase "comment somewhat" 10 10 16.24 16.63 17.18 1.0
88 single should 3909 3909 1.30 1.89 2.03 1.1
89 chain4 majority <~9> inch <~15> thick <~7> genetic 21 4 3.35 3.93 198.06 50.4
90 wildprox deliver <~8> cous* 144 86 14.40 15.04 14.72 1.0
91 and2 language & always 390 390 0.56 1.04 2.23 2.1
92 phraseprox "apparently foot" <~8> between 18 17 27.76 24.83 102.77 4.1
93 and3 yeah & first & give 1846 1846 1.47 1.99 3.33 1.7
94 within letter <~2> record 692 328 60.96 61.88 54.45 0.9
95 chain3 act <~9> marriage <~11> married 259 151 27.33 28.14 1413.97 50.2
96 chain4 graduate <~6> coverage <~4> wide <~10> participation 14 2 2.62 3.16 153.89 48.7
97 wildcard div* 1663 1663 1.22 1.71 1.90 1.1
98 or2 telescope | like 6373 6373 356.68 356.59 357.46 1.0
99 wildcard if* 6878 6878 354.55 354.90 355.07 1.0
100 chain4 maybe <~15> disease <~12> type <~7> sink 93 28 12.77 13.50 888.43 65.8
101 wildcard democ* 1651 1651 1.21 1.72 1.91 1.1
102 and2 seven & oral 50 50 0.26 0.64 1.86 2.9
103 phraseprox "poor tomorrow" <~12> cake 12 8 15.21 14.53 74.68 5.1
104 and2 try & bird 496 496 0.62 1.07 2.24 2.1
105 chain3 honest <~13> happy <~4> till 82 25 9.79 10.38 508.98 49.0
106 wildprox meal <~6> bibl* 97 47 10.58 11.12 10.94 1.0
107 or2 off | session 3932 3932 1.36 2.11 3.05 1.4
108 phrase "detect forth" 3 3 9.54 8.99 9.62 1.1
109 chain3 involve <~15> cross <~8> rather 323 218 35.25 35.88 1844.05 51.4
110 phrase "police exhibit genetic" 0 0 4.77 5.30 6.40 1.2
111 notwithin hit <!~10> contemporary 1139 1024 95.78 96.20 4462.61 46.4
112 single peace 690 690 0.58 1.02 1.26 1.2
113 phrase "faculty bet hey" 1 1 9.52 10.22 11.14 1.1
114 and3 radio & pal & author 13 13 0.27 0.66 2.10 3.2
115 within alcohol <~13> media 238 167 23.46 24.00 21.81 0.9
116 single hurricane 303 303 0.38 0.81 1.10 1.4
117 chain4 church <~8> plenty <~10> scene <~14> retirement 32 4 4.79 5.34 305.67 57.2
118 within mama <~9> quarter 91 54 9.56 10.04 9.49 0.9
119 wildcard igno* 979 979 0.95 1.45 1.66 1.1
120 phraseprox "spot series" <~12> arise 18 8 12.92 13.53 134.37 9.9
121 ordered cable <-13> bar 154 62 16.27 17.18 15.39 0.9
122 wildprox book <~10> pil* 554 301 53.92 54.96 53.37 1.0
123 wildprox clip <~6> qui* 182 30 20.28 21.45 20.26 0.9
124 chain3 beautiful <~3> guest <~15> ultimately 70 16 8.49 9.19 442.27 48.1
125 within hero <~15> hand 401 375 33.51 34.05 29.65 0.9
126 and3 throw & discrimination & wife 48 48 0.33 0.71 2.16 3.0
127 ordered increased <-7> cell 164 48 16.06 16.77 15.04 0.9
128 hardwild ##mos.*## <~12> trouble 795 253 964.52 963.00 2738.83 2.8
129 and3 judge & found & wind 36 36 0.30 0.70 2.13 3.0
130 phrase "item fuel" 29 29 21.71 22.07 22.95 1.0
131 and3 possession & corridor & mom 4 4 0.25 0.62 2.12 3.4
132 and2 disability & find 255 255 0.42 0.84 2.07 2.5
133 chain3 forever <~6> avenue <~6> another 59 19 7.40 7.96 372.76 46.8
134 within prayer <~5> politician 77 41 7.98 8.45 8.06 1.0
135 phraseprox "far wife" <~5> forward 92 44 39.32 42.32 561.68 13.3
136 chain4 hole <~13> restore <~12> complete <~15> along 97 22 12.55 12.94 834.78 64.5
137 phrase "diversity parent seat" 1 1 10.03 10.62 11.67 1.1
138 and2 ethics & already 110 110 0.33 0.74 1.95 2.6
139 single decline 481 481 0.45 0.93 1.16 1.2
140 within information <~7> option 787 687 63.31 64.10 55.74 0.9
141 within attribute <~2> vehicle 174 37 18.48 18.88 17.00 0.9
142 within pain <~10> scared 154 105 15.94 18.34 15.67 0.9
143 wildprox capable <~9> conventi* 119 68 13.69 14.40 14.19 1.0
144 within liberty <~10> string 74 40 8.46 9.03 8.73 1.0
145 phraseprox "twice massive" <~2> press 5 2 12.20 11.47 44.11 3.8
146 within master <~12> analysis 438 343 40.70 42.03 37.43 0.9
147 within against <~15> project 922 890 76.56 76.79 67.01 0.9
148 within lord <~4> training 212 92 21.79 22.35 20.04 0.9
149 within street <~3> emergency 472 261 40.79 41.82 36.73 0.9
150 chain3 enemy <~6> plastic <~11> assistance 48 15 6.32 6.68 308.72 46.2
151 chain3 pack <~13> quit <~12> candidate 79 11 10.26 10.66 537.79 50.4
152 wildprox source <~6> lovel* 136 91 14.41 15.03 14.54 1.0
153 hardwild pr*s <~3> repair 314 41 37.07 40.71 1027.18 25.2
154 and3 productive & by & might 97 97 0.46 0.86 2.30 2.7
155 and2 democrat & congressman 64 64 0.28 0.67 1.90 2.8
156 hardwild ti*e <~8> pretty 1532 724 127.84 129.07 4292.16 33.3
157 or2 suite | by 7268 7268 356.34 357.74 357.27 1.0
158 and3 rapid & engineer & according 19 19 0.28 0.65 2.12 3.3
159 ordered lane <-4> refuse 108 26 11.17 11.75 10.62 0.9
160 phrase "manner responsibility" 9 9 15.98 16.61 17.10 1.0

Parity (extension vs pure match counts on the same corpus — mismatches must be 0;
it is also gated independently, so a nonzero count fails this job):

queries_checked mismatches
160 0

Tokenizer vs simple (overlap overhead)

Custom Unicode tokenizer (proxquery_to_tsvector, which superimposes accent /
hyphen / email lexemes) vs to_tsvector('simple', …) on one overlap-heavy corpus.
term/AND rows have identical selectivity (clean per-op cost ratio); proximity rows
match more on prox (superimposition packs forms onto one position). ratio =
prox_ms / simple_ms.

docs simple_lex prox_lex lex_ratio simple_gin prox_gin
10000 29 33 1.15 432 kB 496 kB
query simple_matches prox_matches simple_ms prox_ms ratio
alpha 5407 5407 48.6 47.3 0.97
alpha & beta 2869 2869 37.3 37.1 0.99
alpha <~3> beta 700 715 37.1 36.7 0.99
confidential <!~5> email 4807 4784 74.7 72.2 0.97

Scaling by text length

One chained query (a <~3> b <~3> c) rechecked over every doc, swept over four
document lengths (32–2048 tokens). The query is chained so it stays non-native and
both ports run their positional recheck. Each length's tsvectors are loaded into memory
once with the TOAST detoast excluded, so the timed loop is just the recheck: the pure
port reads positions with unnest(tsvector), the extension binary-searches the sorted
lexemes. slowdown = pure_ms / ext_ms; disagree is the per-length recheck parity
check over all docs (must be 0).

tokens_per_doc lexemes_per_doc docs matches ext_ms pure_ms slowdown disagree
32 32 2000 2000 24.5 2542.0 103.8 0
128 128 2000 2000 42.0 2919.8 69.5 0
512 506 2000 2000 114.4 4435.3 38.8 0
2048 1947 2000 2000 356.4 10237.5 28.7 0

Each column normalized to its shortest-length value, so the per-column growth rate is
explicit.

tokens_per_doc ext_growth_x pure_growth_x
32 1.0 1.0
128 1.7 1.1
512 4.7 1.7
2048 14.5 4.0
Corpus & vocabulary
vocab_n real_words synth_words topics bg_rate terms_reachable pct_occ_real
54364 4364 50000 300 0.5752 54358 90.0
docs avg_lexemes table_size index_size total_size
12500 304 79 MB 16 MB 96 MB
Plan — @~@ operator on a within shape (index-served via the a&b skeleton, not a seq scan)
                                       QUERY PLAN                                        
-----------------------------------------------------------------------------------------
 Aggregate
   ->  Bitmap Heap Scan on corpus
         Filter: (body_tsv @~@ 'demonstrate <~3> expense'::text)
         ->  Bitmap Index Scan on corpus_gin
               Index Cond: (body_tsv @@ ts_prox_query('demonstrate <~3> expense'::text))
(5 rows)
Plan — ts_prox_search on a boolean query (recheck folded away, Bitmap Index Scan)
                                 QUERY PLAN                                  
-----------------------------------------------------------------------------
 Aggregate
   ->  Bitmap Heap Scan on corpus
         Recheck Cond: (body_tsv @@ '''fruit'' & ''argument'''::tsquery)
         ->  Bitmap Index Scan on corpus_gin
               Index Cond: (body_tsv @@ '''fruit'' & ''argument'''::tsquery)
(5 rows)

@elemdisc
elemdisc merged commit 38265d1 into main Jul 4, 2026
10 checks passed
@elemental-deployer elemental-deployer Bot mentioned this pull request Jul 4, 2026
@elemdisc
elemdisc deleted the dsl-parser-improvements branch July 4, 2026 02:17
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.

1 participant