waxsql 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
waxsql/config.py ADDED
@@ -0,0 +1,477 @@
1
+ """Complexity-dial configuration for query generation.
2
+
3
+ Parallel to `schema.SchemaConfig` / `schema_config_for_complexity`,
4
+ but for the query generator rather than the schema generator. The
5
+ philosophy is the same: a 0..10 dial maps onto a fully-specified
6
+ config, but the config can also be hand-built when the preset doesn't
7
+ match the use case.
8
+
9
+ Two separate knobs:
10
+
11
+ * `feature_flags` GATE which features are *available*. Lower
12
+ complexity unlocks fewer features (e.g. LEFT JOIN appears only
13
+ at c >= 4). The generator checks `feature in cfg.feature_flags`
14
+ before considering the feature at all.
15
+
16
+ * Probability constants flavor *how often* available features
17
+ fire. These don't slide with complexity — keeping them constant
18
+ means dialing complexity changes the *shape* of generated queries
19
+ (more joins, more select items, deeper exprs) rather than just
20
+ the rate at which fixed features show up.
21
+
22
+ This split keeps "what does c=5 look like?" predictable: the dial
23
+ unlocks feature buckets; the buckets fire at fixed rates.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass
28
+
29
+
30
+ # Recognized feature-flag names. Exposed as a constant so the
31
+ # generator's `if 'left_join' in flags:` checks are typo-resistant
32
+ # at the test level.
33
+ #
34
+ # Using string literals (not an Enum) keeps the feature_flags set
35
+ # trivial to dump/diff in test failures: a frozenset of plain
36
+ # strings prints readably under pytest -v. The cost is the
37
+ # named-constant discipline below — but mypy + the __all__ export
38
+ # keep typos from compiling.
39
+ FEATURE_INNER_JOIN = "inner_join"
40
+ FEATURE_LEFT_JOIN = "left_join"
41
+ FEATURE_WHERE = "where"
42
+ FEATURE_ORDER_BY = "order_by"
43
+ FEATURE_LIMIT = "limit"
44
+ FEATURE_AGGREGATE = "aggregate"
45
+ FEATURE_HAVING = "having"
46
+ FEATURE_SCALAR_SUBQUERY = "scalar_subquery"
47
+ FEATURE_EXISTS = "exists"
48
+ FEATURE_IN_SUBQUERY = "in_subquery"
49
+ FEATURE_DERIVED_TABLE = "derived_table"
50
+ FEATURE_LATERAL = "lateral"
51
+ FEATURE_CTE = "cte"
52
+ FEATURE_WINDOW_FUNCTION = "window_function"
53
+ FEATURE_SET_OP = "set_op"
54
+ FEATURE_RECURSIVE_CTE = "recursive_cte"
55
+ FEATURE_GROUPING_SET = "grouping_set"
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class ComplexityConfig:
60
+ """Tunables for the query generator.
61
+
62
+ Frozen for the same reason the rest of the model is frozen:
63
+ immutability prevents one branch of generation from accidentally
64
+ mutating a config field in a way that affects siblings, and the
65
+ config is hashable for any future caching.
66
+ """
67
+
68
+ # ---- Structural caps -------------------------------------------------
69
+ max_expr_depth: int
70
+ """Maximum expression-tree depth. The expression generator
71
+ decrements `depth_remaining` on each recursive call; at zero,
72
+ only leaf productions (column ref, literal) are allowed."""
73
+
74
+ max_from_items: int
75
+ """Maximum number of tables in the FROM clause. The generator
76
+ picks 1..max inclusive; with N > 1 items, joins (or comma cross
77
+ products) connect them."""
78
+
79
+ max_select_items: int
80
+ """Maximum number of expressions in the SELECT list."""
81
+
82
+ max_group_by_items: int
83
+ """Maximum number of expressions in the GROUP BY clause for an
84
+ aggregating query. Only consulted when the query is chosen to
85
+ aggregate; for non-aggregating queries this field is ignored."""
86
+
87
+ max_subquery_depth: int
88
+ """Maximum nesting depth of subqueries (scalar / EXISTS / IN). The
89
+ outermost query is depth 0; a subquery inside it is depth 1; a
90
+ subquery inside that is depth 2. Counted separately from
91
+ `max_expr_depth` because subquery nesting is a different
92
+ rationing dimension — a deep `a + b * c + ...` expression
93
+ shouldn't share a budget with `(SELECT ... WHERE x IN (SELECT ...))`.
94
+ Set to 0 to disable subqueries even when the feature flag is set.
95
+
96
+ Also covers CTE inner-SELECT nesting: each CTE definition's body
97
+ consumes one level of subquery budget."""
98
+
99
+ max_ctes_per_with: int
100
+ """Maximum number of CTE definitions in a single WITH clause.
101
+ Capped low (1..3 across the dial) so the WITH list stays readable
102
+ in eyeballed output. The combinatorial blow-up in nested-query
103
+ complexity comes from max_subquery_depth, not from CTE count."""
104
+
105
+ # ---- Feature gates ---------------------------------------------------
106
+ feature_flags: frozenset[str]
107
+ """Set of FEATURE_* identifiers that are unlocked at this dial
108
+ setting. The generator silently skips features whose flag is
109
+ absent (no error — just doesn't generate them)."""
110
+
111
+ # ---- Per-clause firing probabilities --------------------------------
112
+ p_where: float
113
+ """P(emit WHERE), conditional on FEATURE_WHERE being set."""
114
+
115
+ p_order_by: float
116
+ """P(emit ORDER BY), conditional on FEATURE_ORDER_BY being set."""
117
+
118
+ p_limit: float
119
+ """P(emit LIMIT), conditional on FEATURE_LIMIT being set."""
120
+
121
+ p_explicit_join: float
122
+ """P(use INNER/LEFT JOIN syntax over comma cross product) when
123
+ multiple FROM items exist. Comma joins remain valid PG syntax,
124
+ but explicit joins read more like real SQL."""
125
+
126
+ p_left_join_when_explicit: float
127
+ """When emitting an explicit join AND FEATURE_LEFT_JOIN is set,
128
+ P(LEFT JOIN over INNER JOIN). At 0.0, every explicit join is
129
+ INNER even if LEFT is unlocked."""
130
+
131
+ p_aggregate_query: float
132
+ """P(this query aggregates), conditional on FEATURE_AGGREGATE
133
+ being set. Kept under 0.5 so non-aggregate queries remain the
134
+ common case — both paths get exercised by the headline parse
135
+ sweep."""
136
+
137
+ p_having: float
138
+ """P(emit HAVING), conditional on the query already aggregating
139
+ AND FEATURE_HAVING being set. HAVING without aggregation is a
140
+ PG syntax error; the gate inside gen_select enforces this."""
141
+
142
+ p_derived_table_in_from: float
143
+ """P(a given FROM item becomes a derived table), conditional on
144
+ FEATURE_DERIVED_TABLE being set. Kept under 0.5 so most FROM
145
+ items remain base tables — derived tables are a flavor, not the
146
+ dominant shape, of real SQL."""
147
+
148
+ p_lateral_when_derived: float
149
+ """P(LATERAL prefix on a derived table), conditional on
150
+ FEATURE_LATERAL being set AND the derived table being past the
151
+ first FROM position (LATERAL on the first FROM item is a no-op
152
+ — nothing precedes it to reference)."""
153
+
154
+ p_with_clause: float
155
+ """P(emit a WITH clause), conditional on FEATURE_CTE being set.
156
+ Kept under 0.5 so non-CTE queries remain the common case —
157
+ both paths get exercised by the headline parse sweep."""
158
+
159
+ p_cte_in_from: float
160
+ """P(a given FROM item becomes a CteRef rather than a base/derived
161
+ table), conditional on at least one CTE being visible in scope.
162
+ The flag check happens AFTER has_visible_ctes; useless without
163
+ visible CTEs."""
164
+
165
+ p_partition_by: float
166
+ """P(a window spec includes a PARTITION BY clause). Most real-
167
+ world window calls partition; the default is biased high."""
168
+
169
+ p_order_by_in_window: float
170
+ """P(a window spec includes an ORDER BY). Some functions (lag,
171
+ lead, first_value, last_value, row_number) are typically used
172
+ with ORDER BY; the default is biased high."""
173
+
174
+ max_partition_by_items: int
175
+ """Cap on the number of expressions in PARTITION BY. Kept low
176
+ (1..2) so window specs stay readable."""
177
+
178
+ max_order_by_in_window_items: int
179
+ """Cap on the number of items in a window's ORDER BY. Same
180
+ rationale as max_partition_by_items."""
181
+
182
+ p_window_frame: float
183
+ """P(a window spec includes an explicit frame clause like
184
+ `ROWS BETWEEN N PRECEDING AND CURRENT ROW`). Independent of
185
+ p_partition_by / p_order_by_in_window — frames can appear
186
+ even on `OVER ()`. Real SQL most often omits the frame
187
+ (PG's default RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT
188
+ ROW is usually fine), so this is biased moderate-low."""
189
+
190
+ max_derived_table_columns: int
191
+ """Cap on the number of columns in a derived-table subquery
192
+ (`(SELECT c1, c2, c3 FROM ...) AS dt`). Capped low so most
193
+ derived tables stay single-column (the most common real-world
194
+ shape) but multi-column shapes get exercised regularly."""
195
+
196
+ max_cte_columns: int
197
+ """Cap on the number of columns in a non-recursive CTE
198
+ (`WITH cte AS (SELECT c1, c2, c3 FROM ...)`). Same rationale
199
+ as max_derived_table_columns. Recursive CTEs stay single-
200
+ column for now — the multi-column recursive case requires both
201
+ arms to agree on each column type, more bookkeeping than the
202
+ polish item warrants."""
203
+
204
+ max_set_op_arms: int
205
+ """Cap on the number of arms in a UNION/INTERSECT/EXCEPT.
206
+ Capped low (2..3) so output stays readable; nested set ops
207
+ are deferred to milestone 8+."""
208
+
209
+ p_set_op_query: float
210
+ """P(this query is a SetOp rather than a plain Select), gated
211
+ on FEATURE_SET_OP. Kept under 0.5 so plain SELECTs remain the
212
+ common case."""
213
+
214
+ p_nested_set_op_arm: float
215
+ """P(an arm of a SetOp is itself a SetOp). When non-zero,
216
+ produces shapes like `A UNION (B INTERSECT C)` with the
217
+ parens mandated by PG's set-op precedence (INTERSECT binds
218
+ tighter than UNION/EXCEPT). Each nested SetOp consumes another
219
+ `subquery_depth_remaining`, so arbitrary nesting is impossible."""
220
+
221
+ p_grouping_set: float
222
+ """P(an aggregate query's GROUP BY uses a ROLLUP/CUBE/GROUPING
223
+ SETS extension instead of a flat column list), gated on
224
+ FEATURE_GROUPING_SET. When firing, the grouped columns are
225
+ wrapped in one of the three constructs uniformly. Plain
226
+ column-list GROUP BY remains the common case so output stays
227
+ readable."""
228
+
229
+ p_set_op_all: float
230
+ """When emitting a set op, P(use the ALL variant — `UNION ALL`
231
+ etc.). The non-ALL form is the deduplicating variant."""
232
+
233
+ p_recursive_when_cte: float
234
+ """P(a CTE definition is recursive), conditional on
235
+ FEATURE_RECURSIVE_CTE being set. Recursive CTEs are advanced;
236
+ keeping this modest avoids drowning normal-CTE coverage."""
237
+
238
+ # ---- Expression-generator weights -----------------------------------
239
+ leaf_bias: float
240
+ """Exponent applied to `depth_remaining / max_expr_depth` when
241
+ weighting recursive productions. With leaf_bias=1.0 the bias
242
+ decays linearly with depth; >1.0 favors leaves more aggressively
243
+ (shorter expression trees on average); <1.0 favors recursion
244
+ (taller but spiker trees)."""
245
+
246
+ func_call_weight: float
247
+ binary_op_weight: float
248
+ column_ref_weight: float
249
+ literal_weight: float
250
+ aggregate_call_weight: float
251
+ """Weight for an aggregate function call when it's a candidate
252
+ in the expression generator (only when allow_aggregates is True
253
+ and we're not already inside an aggregate). Tuned modestly so
254
+ aggregates appear regularly in HAVING and inside expression
255
+ arguments without dominating the candidate pool."""
256
+
257
+ window_call_weight: float
258
+ """Weight for a window-style function call (`func(args) OVER (...)`)
259
+ when it's a candidate in the expression generator. Only eligible
260
+ when allow_window=True (set by gen_select for SELECT-list expr
261
+ generation) and not in_window (no nested windows)."""
262
+
263
+ scalar_subquery_weight: float
264
+ """Weight for a scalar subquery `(SELECT col FROM ...)` candidate
265
+ in the expression generator. Eligible at any target type when
266
+ the feature is unlocked and there's subquery-depth budget."""
267
+
268
+ exists_weight: float
269
+ """Weight for an `[NOT ]EXISTS (...)` candidate. Only eligible
270
+ when the target type is BOOL and FEATURE_EXISTS is set."""
271
+
272
+ in_subquery_weight: float
273
+ """Weight for an `<expr> [NOT ]IN (...)` candidate. Only eligible
274
+ when the target type is BOOL and FEATURE_IN_SUBQUERY is set."""
275
+
276
+
277
+ def query_config_for_complexity(complexity: int) -> ComplexityConfig:
278
+ """Map a 0..10 complexity dial onto a ComplexityConfig.
279
+
280
+ The dial unlocks features in stages:
281
+ c == 0: trivial SELECT col FROM t1, no joins, no clauses
282
+ c >= 1: WHERE, INNER JOIN unlock; max_from_items grows past 1
283
+ c >= 2: ORDER BY, LIMIT unlock
284
+ c >= 3: AGGREGATE unlocks (queries can do GROUP BY)
285
+ c >= 4: LEFT JOIN, SCALAR SUBQUERY unlock
286
+ c >= 5: HAVING, EXISTS, IN-SUBQUERY, DERIVED TABLE unlock
287
+ c >= 6: LATERAL unlocks (only meaningful with derived tables)
288
+ c >= 7: CTE unlocks (WITH clauses)
289
+ c >= 8: WINDOW FUNCTION unlocks (`func() OVER (...)`)
290
+ c >= 9: SET OP unlocks (UNION/INTERSECT/EXCEPT [ALL])
291
+ c >= 10: RECURSIVE CTE unlocks (`WITH RECURSIVE ...`)
292
+
293
+ Why complexity 0 emits flat `SELECT col FROM t`: the dial is
294
+ intentionally a strict superset — every shape at level N also
295
+ appears at every level > N. That property lets a user "lower
296
+ the dial until the bug reproduces" without changing the qualitative
297
+ character of what's generated. If c=0 produced something different
298
+ in spirit from c=1, the dial would become a discontinuous
299
+ classifier rather than a continuous knob.
300
+
301
+ Why structural caps double-ish: each step roughly doubles the
302
+ surface area the generator can hit at that level (`c // 2`,
303
+ `c // 3`, etc.). Linear scaling would leave the top of the dial
304
+ underpowered relative to the unlocked features; exponential would
305
+ blow up parse-tree size and slow the test suite. Halving steps
306
+ are a deliberate sweet spot for a <1s test suite.
307
+ """
308
+ # `c` is clamped — callers occasionally pass complexity from CLI
309
+ # input or fuzz seeds and we'd rather degrade smoothly than raise.
310
+ c = max(0, min(10, complexity))
311
+
312
+ # The notch ordering below is load-bearing: features land in the
313
+ # order they're typically built up in real SQL — predicates first
314
+ # (WHERE, joins), then result shaping (ORDER BY, LIMIT), then
315
+ # aggregation, then the more advanced clausal features (HAVING,
316
+ # subqueries), then CTEs, then windows, then set ops. Reordering
317
+ # changes which fixtures fire at each c-level and breaks
318
+ # determinism guarantees that callers may depend on for golden
319
+ # output. Add new features at the END of an existing notch when
320
+ # possible; only introduce new notches when a feature is genuinely
321
+ # gated on something earlier being unlocked first.
322
+ flags: set[str] = set()
323
+ if c >= 1:
324
+ flags.add(FEATURE_WHERE)
325
+ flags.add(FEATURE_INNER_JOIN)
326
+ if c >= 2:
327
+ flags.add(FEATURE_ORDER_BY)
328
+ flags.add(FEATURE_LIMIT)
329
+ if c >= 3:
330
+ flags.add(FEATURE_AGGREGATE)
331
+ if c >= 4:
332
+ flags.add(FEATURE_LEFT_JOIN)
333
+ flags.add(FEATURE_SCALAR_SUBQUERY)
334
+ if c >= 5:
335
+ # HAVING is grouped with the subquery batch because both rely
336
+ # on the c=3 AGGREGATE unlock to produce useful output: HAVING
337
+ # only fires on aggregating queries, and subqueries gain a lot
338
+ # of expressive power once the aggregate path is live.
339
+ flags.add(FEATURE_HAVING)
340
+ flags.add(FEATURE_EXISTS)
341
+ flags.add(FEATURE_IN_SUBQUERY)
342
+ flags.add(FEATURE_DERIVED_TABLE)
343
+ if c >= 6:
344
+ # LATERAL is a no-op without DERIVED_TABLE; the c=6 placement
345
+ # ensures derived tables exist by the time LATERAL unlocks.
346
+ flags.add(FEATURE_LATERAL)
347
+ if c >= 7:
348
+ flags.add(FEATURE_CTE)
349
+ if c >= 8:
350
+ flags.add(FEATURE_WINDOW_FUNCTION)
351
+ if c >= 9:
352
+ flags.add(FEATURE_SET_OP)
353
+ if c >= 10:
354
+ # Top-of-dial features: recursive CTEs require the plain CTE
355
+ # machinery (c=7) to already work, and grouping sets require
356
+ # the AGGREGATE machinery (c=3). Both are kept off until the
357
+ # full dial is in use because they exercise narrow PG paths
358
+ # that swamp other coverage if they fire too readily.
359
+ flags.add(FEATURE_RECURSIVE_CTE)
360
+ flags.add(FEATURE_GROUPING_SET)
361
+
362
+ return ComplexityConfig(
363
+ # Structural caps grow with complexity. // 2 / // 3 / // 4
364
+ # keep the numbers small so debug output stays human-readable
365
+ # even at the high end.
366
+ max_expr_depth=1 + c // 2, # 1 .. 6
367
+ max_from_items=1 + c // 3, # 1 .. 4
368
+ max_select_items=1 + c // 2, # 1 .. 6
369
+ max_group_by_items=1 + c // 4, # 1 .. 3
370
+ # Subquery depth: 0 below the unlock notch (subqueries gated
371
+ # by feature flag so this doesn't fire anyway), then grows
372
+ # slowly. At c=10 we allow 3 levels of nesting — deeper than
373
+ # most hand-written SQL. The (c - 4) // 3 step is intentionally
374
+ # gentler than max_expr_depth's c // 2: subquery nesting
375
+ # combinatorially explodes parse-tree size in a way that flat
376
+ # expression depth does not, so the budget grows in coarser
377
+ # steps to keep generated SQL human-readable at the top of dial.
378
+ max_subquery_depth=max(0, 1 + (c - 4) // 3) if c >= 4 else 0,
379
+ # CTEs are capped low (1..3) so the WITH list stays readable
380
+ # — depth complexity comes from max_subquery_depth, not count.
381
+ max_ctes_per_with=max(1, 1 + (c - 7) // 2) if c >= 7 else 1,
382
+ feature_flags=frozenset(flags),
383
+ # Constant probabilities — see module docstring on why.
384
+ p_where=0.7,
385
+ p_order_by=0.5,
386
+ p_limit=0.3,
387
+ p_explicit_join=0.85,
388
+ p_left_join_when_explicit=0.3,
389
+ # Aggregating is intentionally a minority outcome (~35%) so
390
+ # the non-aggregate path keeps getting exercised by every
391
+ # parametrized parse sweep. HAVING fires roughly half the
392
+ # time when the query is already aggregating.
393
+ p_aggregate_query=0.35,
394
+ p_having=0.4,
395
+ # Derived tables: same minority-outcome reasoning. ~25% of
396
+ # FROM items become derived; LATERAL fires ~half the time
397
+ # when a derived table is past the first FROM position.
398
+ p_derived_table_in_from=0.25,
399
+ p_lateral_when_derived=0.5,
400
+ # CTEs: ~30% of queries get a WITH clause when the feature
401
+ # is unlocked; once a CTE exists in scope, a given FROM item
402
+ # has a ~25% chance of being a CteRef (vs base/derived).
403
+ p_with_clause=0.3,
404
+ p_cte_in_from=0.25,
405
+ # Window specs: most window calls use PARTITION BY and
406
+ # ORDER BY in real SQL. Capped low (1..2) for readability.
407
+ p_partition_by=0.7,
408
+ p_order_by_in_window=0.7,
409
+ max_partition_by_items=2,
410
+ max_order_by_in_window_items=2,
411
+ # Frames: ~30% of windows get an explicit frame. Most real
412
+ # SQL omits frames (PG default is fine), but generating them
413
+ # exercises the printer's frame path and PG's grammar
414
+ # validation more thoroughly.
415
+ p_window_frame=0.3,
416
+ # Derived tables and non-recursive CTEs: 1..3 columns. Cap
417
+ # is low enough that single-column remains the most common
418
+ # output (the plurality, not the dominant majority); the
419
+ # multi-column shape gets exercised in maybe a third of
420
+ # derived/CTE emissions.
421
+ max_derived_table_columns=3,
422
+ max_cte_columns=3,
423
+ # Set ops: 2 arms at unlock notch, growing to 3 at c=10.
424
+ # Probability ~0.25 keeps SetOps a minority outcome so
425
+ # plain SELECTs stay the common case.
426
+ max_set_op_arms=2 if c < 10 else 3,
427
+ p_set_op_query=0.25,
428
+ p_set_op_all=0.5,
429
+ # Nested set ops: low probability so most SetOps remain
430
+ # the simple flat shape. When it does fire, exercises the
431
+ # printer's mandatory-paren path for sub-SetOp arms.
432
+ p_nested_set_op_arm=0.2,
433
+ # Grouping sets: moderate probability. When firing, the
434
+ # whole GROUP BY uses one of ROLLUP/CUBE/GROUPING SETS
435
+ # (uniformly chosen), exercising the multi-grouping path
436
+ # that PG's planner has its own handling for.
437
+ p_grouping_set=0.3,
438
+ # Recursive CTEs: ~30% of CTEs become recursive when the
439
+ # feature is unlocked. Both forms (recursive and plain) get
440
+ # exercised by every CTE-eligible parse sweep.
441
+ p_recursive_when_cte=0.3,
442
+ # Expression shape. Weights are relative — only ratios matter.
443
+ # Column refs are weighted heavily because they're what makes
444
+ # the query reference the schema at all; without enough column
445
+ # refs, output is just literal arithmetic.
446
+ leaf_bias=1.0,
447
+ func_call_weight=2.0,
448
+ binary_op_weight=2.0,
449
+ column_ref_weight=4.0,
450
+ literal_weight=1.0,
451
+ aggregate_call_weight=2.0,
452
+ # Subqueries are recursive productions on the same scale as
453
+ # function calls. Modest weights so they appear regularly
454
+ # without dominating — output should still mostly be ordinary
455
+ # column refs and operators.
456
+ scalar_subquery_weight=1.0,
457
+ exists_weight=1.0,
458
+ in_subquery_weight=1.0,
459
+ # Window calls are heavyweight (the OVER clause adds visible
460
+ # text); modest weight keeps them flavorful, not dominant.
461
+ window_call_weight=1.5,
462
+ )
463
+
464
+
465
+ __all__ = [
466
+ "ComplexityConfig",
467
+ "query_config_for_complexity",
468
+ "FEATURE_INNER_JOIN", "FEATURE_LEFT_JOIN",
469
+ "FEATURE_WHERE", "FEATURE_ORDER_BY", "FEATURE_LIMIT",
470
+ "FEATURE_AGGREGATE", "FEATURE_HAVING", "FEATURE_GROUPING_SET",
471
+ "FEATURE_SCALAR_SUBQUERY", "FEATURE_EXISTS", "FEATURE_IN_SUBQUERY",
472
+ "FEATURE_DERIVED_TABLE", "FEATURE_LATERAL",
473
+ "FEATURE_CTE",
474
+ "FEATURE_WINDOW_FUNCTION",
475
+ "FEATURE_SET_OP",
476
+ "FEATURE_RECURSIVE_CTE",
477
+ ]