flagsmith-sql-flag-engine 0.1.2__py3-none-any.whl → 0.2.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.
- flagsmith_sql_flag_engine/__init__.py +15 -0
- flagsmith_sql_flag_engine/binder.py +50 -0
- flagsmith_sql_flag_engine/dialect.py +30 -6
- flagsmith_sql_flag_engine/dialects/clickhouse.py +29 -7
- flagsmith_sql_flag_engine/translator.py +44 -22
- flagsmith_sql_flag_engine/utils.py +15 -0
- {flagsmith_sql_flag_engine-0.1.2.dist-info → flagsmith_sql_flag_engine-0.2.0.dist-info}/METADATA +31 -1
- flagsmith_sql_flag_engine-0.2.0.dist-info/RECORD +11 -0
- flagsmith_sql_flag_engine-0.1.2.dist-info/RECORD +0 -10
- {flagsmith_sql_flag_engine-0.1.2.dist-info → flagsmith_sql_flag_engine-0.2.0.dist-info}/WHEEL +0 -0
|
@@ -4,11 +4,22 @@ Public API:
|
|
|
4
4
|
translate_segment(segment, ctx) -> str | None
|
|
5
5
|
TranslateContext
|
|
6
6
|
|
|
7
|
+
By default the translator inlines each segment value as an escaped SQL
|
|
8
|
+
string literal. Pass a `Binder` on the `TranslateContext` to bind
|
|
9
|
+
value-bearing literals as query parameters instead — read its params off
|
|
10
|
+
`Binder.params` after translation. See `flagsmith_sql_flag_engine.binder`.
|
|
11
|
+
|
|
7
12
|
See README.md for usage. The translator is dialect-aware via the `Dialect`
|
|
8
13
|
protocol; `flagsmith_sql_flag_engine.dialects.clickhouse.ClickHouseDialect`
|
|
9
14
|
is the only implementation today.
|
|
10
15
|
"""
|
|
11
16
|
|
|
17
|
+
from flagsmith_sql_flag_engine.binder import (
|
|
18
|
+
Binder,
|
|
19
|
+
ClickHouseServerParamStyle,
|
|
20
|
+
ParamStyle,
|
|
21
|
+
PyformatParamStyle,
|
|
22
|
+
)
|
|
12
23
|
from flagsmith_sql_flag_engine.dialect import Dialect
|
|
13
24
|
from flagsmith_sql_flag_engine.translator import (
|
|
14
25
|
TRANSLATABLE_OPERATORS,
|
|
@@ -20,7 +31,11 @@ from flagsmith_sql_flag_engine.translator import (
|
|
|
20
31
|
|
|
21
32
|
__all__ = [
|
|
22
33
|
"TRANSLATABLE_OPERATORS",
|
|
34
|
+
"Binder",
|
|
35
|
+
"ClickHouseServerParamStyle",
|
|
23
36
|
"Dialect",
|
|
37
|
+
"ParamStyle",
|
|
38
|
+
"PyformatParamStyle",
|
|
24
39
|
"TranslateContext",
|
|
25
40
|
"translate_condition",
|
|
26
41
|
"translate_rule",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ParamStyle(Protocol):
|
|
5
|
+
"""A driver's placeholder syntax for a named bound parameter."""
|
|
6
|
+
|
|
7
|
+
def placeholder(self, name: str) -> str:
|
|
8
|
+
"""The placeholder token referencing bound parameter `name`."""
|
|
9
|
+
...
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PyformatParamStyle:
|
|
13
|
+
"""`%(name)s`
|
|
14
|
+
|
|
15
|
+
Used by `clickhouse-driver` which substitutes parameters
|
|
16
|
+
client-side via `query % params`."""
|
|
17
|
+
|
|
18
|
+
def placeholder(self, name: str) -> str:
|
|
19
|
+
return f"%({name})s"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClickHouseServerParamStyle:
|
|
23
|
+
"""`{name:String}`
|
|
24
|
+
|
|
25
|
+
ClickHouse's native server-side parameter syntax,
|
|
26
|
+
used by `clickhouse-connect`."""
|
|
27
|
+
|
|
28
|
+
def placeholder(self, name: str) -> str:
|
|
29
|
+
return "{" + name + ":String}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Binder:
|
|
33
|
+
"""Collects bound parameter values and mints their placeholders.
|
|
34
|
+
|
|
35
|
+
Not thread-safe; use one `Binder` per predicate translation.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, style: ParamStyle, prefix: str = "") -> None:
|
|
39
|
+
self.params: dict[str, str] = {}
|
|
40
|
+
self._style = style
|
|
41
|
+
self._prefix = prefix
|
|
42
|
+
self._count = 0
|
|
43
|
+
|
|
44
|
+
def add(self, value: str) -> str:
|
|
45
|
+
"""Record `value` under a fresh namespaced name and return its
|
|
46
|
+
placeholder token for the active paramstyle."""
|
|
47
|
+
name = f"{self._prefix}p{self._count}"
|
|
48
|
+
self._count += 1
|
|
49
|
+
self.params[name] = value
|
|
50
|
+
return self._style.placeholder(name)
|
|
@@ -3,6 +3,8 @@ regex, padded-version comparison, type-aware trait predicates, regex flavour."""
|
|
|
3
3
|
|
|
4
4
|
from typing import Protocol
|
|
5
5
|
|
|
6
|
+
from flagsmith_sql_flag_engine.binder import Binder
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
class Dialect(Protocol):
|
|
8
10
|
"""Per-dialect SQL fragments.
|
|
@@ -10,6 +12,10 @@ class Dialect(Protocol):
|
|
|
10
12
|
Methods return SQL string fragments. Inputs are already-formatted SQL
|
|
11
13
|
strings (column refs, string literals); the dialect only chooses the
|
|
12
14
|
right syntax for the operation.
|
|
15
|
+
|
|
16
|
+
Methods that embed a segment- or context-derived value take an
|
|
17
|
+
optional `binder`: when provided, the value is emitted as a bound
|
|
18
|
+
query parameter rather than an inline literal.
|
|
13
19
|
"""
|
|
14
20
|
|
|
15
21
|
name: str # human-readable, used in test ids and error messages
|
|
@@ -35,7 +41,14 @@ class Dialect(Protocol):
|
|
|
35
41
|
"""
|
|
36
42
|
...
|
|
37
43
|
|
|
38
|
-
def trait_eq(
|
|
44
|
+
def trait_eq(
|
|
45
|
+
self,
|
|
46
|
+
alias: str,
|
|
47
|
+
trait_key: str,
|
|
48
|
+
value: object,
|
|
49
|
+
negate: bool,
|
|
50
|
+
binder: Binder | None = None,
|
|
51
|
+
) -> str:
|
|
39
52
|
"""Type-aware EQUAL / NOT_EQUAL predicate on a trait, mirroring
|
|
40
53
|
`flag_engine`'s per-type coercion: the segment value is cast to
|
|
41
54
|
the trait's runtime type before compare, and a cast failure
|
|
@@ -45,7 +58,13 @@ class Dialect(Protocol):
|
|
|
45
58
|
"""
|
|
46
59
|
...
|
|
47
60
|
|
|
48
|
-
def trait_in(
|
|
61
|
+
def trait_in(
|
|
62
|
+
self,
|
|
63
|
+
alias: str,
|
|
64
|
+
trait_key: str,
|
|
65
|
+
items: list[str],
|
|
66
|
+
binder: Binder | None = None,
|
|
67
|
+
) -> str:
|
|
49
68
|
"""Type-aware IN predicate on a trait, mirroring engine semantics:
|
|
50
69
|
string trait does direct lookup; integer trait stringifies and
|
|
51
70
|
looks up; other trait types never match. `items` is the parsed
|
|
@@ -77,14 +96,19 @@ class Dialect(Protocol):
|
|
|
77
96
|
to `flag_engine`."""
|
|
78
97
|
...
|
|
79
98
|
|
|
80
|
-
def regexp_anchored_match(
|
|
99
|
+
def regexp_anchored_match(
|
|
100
|
+
self,
|
|
101
|
+
value_expr: str,
|
|
102
|
+
pattern: str,
|
|
103
|
+
binder: Binder | None = None,
|
|
104
|
+
) -> str:
|
|
81
105
|
"""Boolean: equivalent to Python `re.match(pattern, value)` —
|
|
82
106
|
anchored at position 0, may be a prefix of the value, not a
|
|
83
107
|
full-match.
|
|
84
108
|
|
|
85
|
-
`pattern` is the raw Python regex string
|
|
86
|
-
its own escaping into a SQL literal, since regex
|
|
87
|
-
differ in how backslashes are treated."""
|
|
109
|
+
`pattern` is the raw Python regex string. With no `binder`, the
|
|
110
|
+
dialect handles its own escaping into a SQL literal, since regex
|
|
111
|
+
flavours differ in how backslashes are treated."""
|
|
88
112
|
...
|
|
89
113
|
|
|
90
114
|
def regexp_nth_digit_run(self, value_expr: str, n: int) -> str:
|
|
@@ -76,7 +76,8 @@ typical Flagsmith trait vocabularies; we should monitor.
|
|
|
76
76
|
ClickHouse Cloud as of 25.12 (no longer experimental on OSS 25.x).
|
|
77
77
|
Callers should apply this setting at session creation."""
|
|
78
78
|
|
|
79
|
-
from flagsmith_sql_flag_engine.
|
|
79
|
+
from flagsmith_sql_flag_engine.binder import Binder
|
|
80
|
+
from flagsmith_sql_flag_engine.utils import bind_or_inline, re2_safe
|
|
80
81
|
|
|
81
82
|
SCHEMA_DDL = """\
|
|
82
83
|
CREATE TABLE IF NOT EXISTS IDENTITIES (
|
|
@@ -154,10 +155,17 @@ class ClickHouseDialect:
|
|
|
154
155
|
sub = self._sub(alias, trait_key)
|
|
155
156
|
return f"if({sub} IS NULL, NULL, toString({sub}))"
|
|
156
157
|
|
|
157
|
-
def trait_eq(
|
|
158
|
+
def trait_eq(
|
|
159
|
+
self,
|
|
160
|
+
alias: str,
|
|
161
|
+
trait_key: str,
|
|
162
|
+
value: object,
|
|
163
|
+
negate: bool,
|
|
164
|
+
binder: Binder | None = None,
|
|
165
|
+
) -> str:
|
|
158
166
|
sub = self._sub(alias, trait_key)
|
|
159
167
|
str_value = str(value)
|
|
160
|
-
str_lit =
|
|
168
|
+
str_lit = bind_or_inline(binder, str_value)
|
|
161
169
|
# Engine bool cast: `v not in ("False", "false")`. A JSON true matches
|
|
162
170
|
# every segment value except literal "False" / "false"; those two coerce
|
|
163
171
|
# to False and match a JSON false.
|
|
@@ -223,7 +231,13 @@ class ClickHouseDialect:
|
|
|
223
231
|
f"(({str_sub} IS NOT NULL AND {str_sub} <> {str_lit}) OR {bool_branch} OR {num_branch})"
|
|
224
232
|
)
|
|
225
233
|
|
|
226
|
-
def trait_in(
|
|
234
|
+
def trait_in(
|
|
235
|
+
self,
|
|
236
|
+
alias: str,
|
|
237
|
+
trait_key: str,
|
|
238
|
+
items: list[str],
|
|
239
|
+
binder: Binder | None = None,
|
|
240
|
+
) -> str:
|
|
227
241
|
# `toString(<sub>)` returns the canonical string form for any JSON
|
|
228
242
|
# value type in a single subcolumn read. Engine semantics only
|
|
229
243
|
# match String and integer trait types — bool / float / array
|
|
@@ -235,7 +249,7 @@ class ClickHouseDialect:
|
|
|
235
249
|
bool_sub = f"{sub}.:Bool"
|
|
236
250
|
float_sub = f"{sub}.:Float64"
|
|
237
251
|
str_path = f"toString({sub})"
|
|
238
|
-
item_lits = ",".join(
|
|
252
|
+
item_lits = ",".join(bind_or_inline(binder, v) for v in items)
|
|
239
253
|
return f"({bool_sub} IS NULL AND {float_sub} IS NULL AND {str_path} IN ({item_lits}))"
|
|
240
254
|
|
|
241
255
|
# ----- string operations -----
|
|
@@ -267,13 +281,21 @@ class ClickHouseDialect:
|
|
|
267
281
|
doubled = pattern.replace("\\", "\\\\").replace("'", "''")
|
|
268
282
|
return f"'{doubled}'"
|
|
269
283
|
|
|
270
|
-
def regexp_anchored_match(
|
|
284
|
+
def regexp_anchored_match(
|
|
285
|
+
self, value_expr: str, pattern: str, binder: Binder | None = None
|
|
286
|
+
) -> str:
|
|
271
287
|
# `match` is RE2 but unanchored — equivalent to `re.search`. Prepend
|
|
272
288
|
# `^` to get `re.match` semantics (start-anchored, prefix-allowed).
|
|
273
289
|
# Wrapping in `(...)` keeps the user's top-level alternation from
|
|
274
290
|
# binding tighter than the anchor.
|
|
275
291
|
anchored = "^(" + pattern + ")"
|
|
276
|
-
|
|
292
|
+
# Bind the raw pattern when a binder is active: the driver escapes
|
|
293
|
+
# it, and — crucially — no `%` from a character class like
|
|
294
|
+
# `[a-z%]` lands in the query text to trip a `%`-substituting
|
|
295
|
+
# driver. Inline, `_regex_literal` doubles backslashes so RE2 sees
|
|
296
|
+
# the pattern the segment author wrote.
|
|
297
|
+
pattern_lit = binder.add(anchored) if binder is not None else self._regex_literal(anchored)
|
|
298
|
+
return f"match({_non_null(value_expr)}, {pattern_lit})"
|
|
277
299
|
|
|
278
300
|
def regexp_nth_digit_run(self, value_expr: str, n: int) -> str:
|
|
279
301
|
# `extractAll` returns the matches array; subscript is 1-indexed
|
|
@@ -22,12 +22,12 @@ from flag_engine.context.types import (
|
|
|
22
22
|
from flag_engine.segments.evaluator import is_context_in_segment
|
|
23
23
|
from flag_engine.segments.types import ConditionOperator
|
|
24
24
|
|
|
25
|
+
from flagsmith_sql_flag_engine.binder import Binder
|
|
25
26
|
from flagsmith_sql_flag_engine.dialect import Dialect
|
|
26
27
|
from flagsmith_sql_flag_engine.utils import (
|
|
27
|
-
|
|
28
|
+
bind_or_inline,
|
|
28
29
|
modulo_literal,
|
|
29
30
|
numeric_literal,
|
|
30
|
-
string_literal,
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
TRANSLATABLE_OPERATORS: frozenset[ConditionOperator] = frozenset(
|
|
@@ -75,7 +75,10 @@ class TranslateContext:
|
|
|
75
75
|
being configured here. `identities_alias` is the table alias for
|
|
76
76
|
`IDENTITIES` in the surrounding query — defaults to `i`.
|
|
77
77
|
`segment_key` salts `PERCENTAGE_SPLIT` and is auto-injected from
|
|
78
|
-
the segment's `key` field by `translate_segment`.
|
|
78
|
+
the segment's `key` field by `translate_segment`. `binder`, when
|
|
79
|
+
provided, promotes value-bearing literals to bound query parameters
|
|
80
|
+
instead of inlining them — see `flagsmith_sql_flag_engine.binder`;
|
|
81
|
+
the default `None` inlines them as escaped SQL string literals.
|
|
79
82
|
"""
|
|
80
83
|
|
|
81
84
|
def __init__(
|
|
@@ -84,11 +87,13 @@ class TranslateContext:
|
|
|
84
87
|
dialect: Dialect,
|
|
85
88
|
identities_alias: str = "i",
|
|
86
89
|
segment_key: str | None = None,
|
|
90
|
+
binder: Binder | None = None,
|
|
87
91
|
) -> None:
|
|
88
92
|
self.evaluation_context = evaluation_context
|
|
89
93
|
self.dialect = dialect
|
|
90
94
|
self.identities_alias = identities_alias
|
|
91
95
|
self.segment_key = segment_key
|
|
96
|
+
self.binder = binder
|
|
92
97
|
|
|
93
98
|
@property
|
|
94
99
|
def identity_key_expr(self) -> str:
|
|
@@ -114,6 +119,7 @@ class TranslateContext:
|
|
|
114
119
|
dialect=self.dialect,
|
|
115
120
|
identities_alias=self.identities_alias,
|
|
116
121
|
segment_key=key,
|
|
122
|
+
binder=self.binder,
|
|
117
123
|
)
|
|
118
124
|
|
|
119
125
|
|
|
@@ -133,7 +139,7 @@ def _percentage_split_expr(
|
|
|
133
139
|
the engine recurses with doubled input; we don't.
|
|
134
140
|
"""
|
|
135
141
|
d = ctx.dialect
|
|
136
|
-
seg_lit =
|
|
142
|
+
seg_lit = bind_or_inline(ctx.binder, seg_key)
|
|
137
143
|
hash_subject = f"{seg_lit} || ',' || ({ctx_value_sql})"
|
|
138
144
|
h = d.md5_hex(hash_subject)
|
|
139
145
|
s1 = d.parse_hex_chunk(h, 1)
|
|
@@ -289,18 +295,19 @@ def _comparison(
|
|
|
289
295
|
if value is None:
|
|
290
296
|
return "FALSE"
|
|
291
297
|
d = ctx.dialect
|
|
292
|
-
lit = string_literal(str(value))
|
|
293
298
|
str_expr = expr if is_jsonpath else d.cast_string(expr)
|
|
299
|
+
# Bind the operand lazily
|
|
294
300
|
if op == "EQUAL":
|
|
295
|
-
return f"{str_expr} = {
|
|
301
|
+
return f"{str_expr} = {bind_or_inline(ctx.binder, str(value))}"
|
|
296
302
|
if op == "NOT_EQUAL":
|
|
297
|
-
return f"{str_expr} <> {
|
|
303
|
+
return f"{str_expr} <> {bind_or_inline(ctx.binder, str(value))}"
|
|
298
304
|
if op == "IN":
|
|
299
|
-
items = "
|
|
300
|
-
return f"{str_expr} IN (
|
|
305
|
+
items = ",".join(bind_or_inline(ctx.binder, v.strip()) for v in str(value).split(","))
|
|
306
|
+
return f"{str_expr} IN ({items})"
|
|
301
307
|
if op == "CONTAINS":
|
|
302
|
-
return d.position(
|
|
308
|
+
return d.position(bind_or_inline(ctx.binder, str(value)), str_expr)
|
|
303
309
|
if op == "NOT_CONTAINS":
|
|
310
|
+
lit = bind_or_inline(ctx.binder, str(value))
|
|
304
311
|
return f"({expr} IS NOT NULL AND NOT ({d.position(lit, str_expr)}))"
|
|
305
312
|
if op in {"GREATER_THAN", "LESS_THAN", "GREATER_THAN_INCLUSIVE", "LESS_THAN_INCLUSIVE"}:
|
|
306
313
|
numeric_lit = numeric_literal(value)
|
|
@@ -327,7 +334,7 @@ def _comparison(
|
|
|
327
334
|
pattern = str(value)
|
|
328
335
|
if not d.regex_supports(pattern):
|
|
329
336
|
return None
|
|
330
|
-
return f"({expr} IS NOT NULL AND {d.regexp_anchored_match(str_expr, pattern)})"
|
|
337
|
+
return f"({expr} IS NOT NULL AND {d.regexp_anchored_match(str_expr, pattern, ctx.binder)})"
|
|
331
338
|
raise AssertionError( # pragma: no cover - all TRANSLATABLE_OPERATORS handled above
|
|
332
339
|
f"unhandled translatable operator in _comparison: {op}"
|
|
333
340
|
)
|
|
@@ -369,7 +376,7 @@ def _translate_trait_op(
|
|
|
369
376
|
# content, which is what the fall-through handlers already do.
|
|
370
377
|
if isinstance(val, str) and val.endswith(":semver") and op in _SEMVER_OPS:
|
|
371
378
|
bare = val[:-7]
|
|
372
|
-
bare_lit =
|
|
379
|
+
bare_lit = bind_or_inline(ctx.binder, bare)
|
|
373
380
|
col_str = ctx.dialect.cast_string(path)
|
|
374
381
|
return (
|
|
375
382
|
f"({path} IS NOT NULL AND "
|
|
@@ -382,7 +389,13 @@ def _translate_trait_op(
|
|
|
382
389
|
# casts, and short-circuit pitfalls are all engine-specific.
|
|
383
390
|
if op in {"EQUAL", "NOT_EQUAL"} and val is not None:
|
|
384
391
|
negate = op == "NOT_EQUAL"
|
|
385
|
-
eq_pred = ctx.dialect.trait_eq(
|
|
392
|
+
eq_pred = ctx.dialect.trait_eq(
|
|
393
|
+
ctx.identities_alias,
|
|
394
|
+
trait_key,
|
|
395
|
+
val,
|
|
396
|
+
negate=negate,
|
|
397
|
+
binder=ctx.binder,
|
|
398
|
+
)
|
|
386
399
|
return f"({path} IS NOT NULL AND {eq_pred})"
|
|
387
400
|
if op == "IN":
|
|
388
401
|
items = _engine_in_values(val)
|
|
@@ -390,7 +403,12 @@ def _translate_trait_op(
|
|
|
390
403
|
# Bad IN value — neither a string nor a list. Engine returns
|
|
391
404
|
# False.
|
|
392
405
|
return "FALSE"
|
|
393
|
-
in_pred = ctx.dialect.trait_in(
|
|
406
|
+
in_pred = ctx.dialect.trait_in(
|
|
407
|
+
ctx.identities_alias,
|
|
408
|
+
trait_key,
|
|
409
|
+
items,
|
|
410
|
+
binder=ctx.binder,
|
|
411
|
+
)
|
|
394
412
|
return f"({path} IS NOT NULL AND {in_pred})"
|
|
395
413
|
|
|
396
414
|
return _comparison(ctx, op, path, val, is_jsonpath=False)
|
|
@@ -432,7 +450,6 @@ def translate_condition(cond: SegmentCondition, ctx: TranslateContext) -> str |
|
|
|
432
450
|
# Engine: float() on the threshold raises → returns False.
|
|
433
451
|
return "FALSE"
|
|
434
452
|
threshold = float(threshold_lit)
|
|
435
|
-
identity: dict[str, object] = ctx.evaluation_context.get("identity") or {} # type: ignore[assignment]
|
|
436
453
|
kind = classification.kind
|
|
437
454
|
if not prop:
|
|
438
455
|
# In traditional engine implementations, this branch implies
|
|
@@ -457,13 +474,18 @@ def translate_condition(cond: SegmentCondition, ctx: TranslateContext) -> str |
|
|
|
457
474
|
# future work and let the caller fall back to the engine.
|
|
458
475
|
return None
|
|
459
476
|
else:
|
|
460
|
-
# Plain trait key, or `$.identity.traits.<X>` rewritten to
|
|
461
|
-
#
|
|
462
|
-
#
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
477
|
+
# Plain trait key, or `$.identity.traits.<X>` rewritten to the
|
|
478
|
+
# bare key. Hash the per-row trait value from `i.traits:"<key>"`.
|
|
479
|
+
# A row missing the trait has no value to bucket, so it cannot
|
|
480
|
+
# match — mirroring the engine's `context_value is None -> False`.
|
|
481
|
+
trait_sql = ctx.trait_path(prop)
|
|
482
|
+
split = _percentage_split_expr(
|
|
483
|
+
ctx,
|
|
484
|
+
ctx.segment_key,
|
|
485
|
+
ctx.dialect.cast_string(trait_sql),
|
|
486
|
+
threshold,
|
|
487
|
+
)
|
|
488
|
+
return f"({trait_sql} IS NOT NULL AND {split})"
|
|
467
489
|
return _percentage_split_expr(ctx, ctx.segment_key, value_expr, threshold)
|
|
468
490
|
|
|
469
491
|
if not prop:
|
|
@@ -21,6 +21,21 @@ on the `Dialect` protocol instead.
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
import re
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from flagsmith_sql_flag_engine.binder import Binder
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def bind_or_inline(binder: "Binder | None", value: str) -> str:
|
|
31
|
+
"""Return a bound-parameter placeholder for `value` when a `binder` is
|
|
32
|
+
active, else a single-quoted SQL string literal.
|
|
33
|
+
|
|
34
|
+
The single seam that lets a value-bearing literal either be inlined
|
|
35
|
+
(the default) or promoted to a query parameter, chosen per call site
|
|
36
|
+
by whether the caller threaded a `Binder` onto the context.
|
|
37
|
+
"""
|
|
38
|
+
return binder.add(value) if binder is not None else string_literal(value)
|
|
24
39
|
|
|
25
40
|
|
|
26
41
|
def escape_string(value: str) -> str:
|
{flagsmith_sql_flag_engine-0.1.2.dist-info → flagsmith_sql_flag_engine-0.2.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flagsmith-sql-flag-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: SQL translator for Flagsmith segment predicates.
|
|
5
5
|
Author: Flagsmith
|
|
6
6
|
Author-email: Flagsmith <engineering@flagsmith.com>
|
|
@@ -66,6 +66,36 @@ translator can't handle — typically a REGEX pattern the active dialect's
|
|
|
66
66
|
regex flavour can't compile. Callers should fall back to
|
|
67
67
|
`flag_engine.is_context_in_segment` for those segments.
|
|
68
68
|
|
|
69
|
+
## Bound parameters
|
|
70
|
+
|
|
71
|
+
By default the translator inlines each segment value as an escaped SQL string literal. Pass a `Binder` on the `TranslateContext` to bind value-bearing literals as query parameters instead.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from flagsmith_sql_flag_engine import (
|
|
75
|
+
Binder,
|
|
76
|
+
PyformatParamStyle,
|
|
77
|
+
TranslateContext,
|
|
78
|
+
translate_segment,
|
|
79
|
+
)
|
|
80
|
+
from flagsmith_sql_flag_engine.dialects import ClickHouseDialect
|
|
81
|
+
|
|
82
|
+
binder = Binder(PyformatParamStyle())
|
|
83
|
+
ctx = TranslateContext(
|
|
84
|
+
evaluation_context=eval_context,
|
|
85
|
+
dialect=ClickHouseDialect(),
|
|
86
|
+
binder=binder,
|
|
87
|
+
)
|
|
88
|
+
where_expr = translate_segment(segment, ctx)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Hand both to the driver:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
cursor.execute(f"... WHERE ({where_expr})", binder.params)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Currently, `%`-prefixed style `PyformatParamStyle` and ClickHouse-specific `ClickHouseServerParamStyle` are supported.
|
|
98
|
+
|
|
69
99
|
## Schema
|
|
70
100
|
|
|
71
101
|
Each dialect publishes the table layout it expects via a `schema_ddl`
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
flagsmith_sql_flag_engine/__init__.py,sha256=gsyNqBdTyELjV-xdfMYfVTAIDcS9LE1jiv1Yt4JWFFw,1223
|
|
2
|
+
flagsmith_sql_flag_engine/binder.py,sha256=eSb_OiHIN_ZmZcz7I0cGurm1vn2XmMeljB6w6yNBrEM,1386
|
|
3
|
+
flagsmith_sql_flag_engine/dialect.py,sha256=xofskXKrVMFzv73_vVzNsygUH9uvfKRq2VnZ1KTVZ9Y,5005
|
|
4
|
+
flagsmith_sql_flag_engine/dialects/__init__.py,sha256=4pkblw-Jr04CLnSbpMud5Vyj5alBgB6WhSm0Cz9OWSI,141
|
|
5
|
+
flagsmith_sql_flag_engine/dialects/clickhouse.py,sha256=dcrfyOQ4XNlmsFLTWOjp47GtOmoV_u4zXQ2LFAYSoYg,16218
|
|
6
|
+
flagsmith_sql_flag_engine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
flagsmith_sql_flag_engine/translator.py,sha256=_oTYHAeaxNiWC9ew0FjI_cg5tGTGe-4A7vVXn3bx6zE,23833
|
|
8
|
+
flagsmith_sql_flag_engine/utils.py,sha256=FEh_W5_2BLCdJaucgIm3s4YaCS_H4JU7Q19Fj4c7VVk,3800
|
|
9
|
+
flagsmith_sql_flag_engine-0.2.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
10
|
+
flagsmith_sql_flag_engine-0.2.0.dist-info/METADATA,sha256=TSMcscM4_yC8XPbkOf76EDxnxVntEp91lZJphZLcqAk,6974
|
|
11
|
+
flagsmith_sql_flag_engine-0.2.0.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
flagsmith_sql_flag_engine/__init__.py,sha256=DgiUBg8KnfdisZiMNsWpzKYtDqJoLvxnKctXQGESdZg,710
|
|
2
|
-
flagsmith_sql_flag_engine/dialect.py,sha256=G4rzXszXUVM8iV-r0w_YJgFAWtqWx1hfJI48P9zuTmE,4514
|
|
3
|
-
flagsmith_sql_flag_engine/dialects/__init__.py,sha256=4pkblw-Jr04CLnSbpMud5Vyj5alBgB6WhSm0Cz9OWSI,141
|
|
4
|
-
flagsmith_sql_flag_engine/dialects/clickhouse.py,sha256=Flwy2QNEv-A53rvZoaWv8Q0wytuO8NY4IaRogGr0Ztg,15523
|
|
5
|
-
flagsmith_sql_flag_engine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
flagsmith_sql_flag_engine/translator.py,sha256=UfqttdhK3ew7mV2PbLIiKEjzL2YAUOBYjhH9wzCl2oQ,22999
|
|
7
|
-
flagsmith_sql_flag_engine/utils.py,sha256=ygP8cijp1jQjszFj87odK95jdfV3DwaT02eXrQdikAA,3202
|
|
8
|
-
flagsmith_sql_flag_engine-0.1.2.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
9
|
-
flagsmith_sql_flag_engine-0.1.2.dist-info/METADATA,sha256=xcP1ByC00k1Y0pHxlQbCoKttWvcFua-5v2kNdhGPAF8,6137
|
|
10
|
-
flagsmith_sql_flag_engine-0.1.2.dist-info/RECORD,,
|
{flagsmith_sql_flag_engine-0.1.2.dist-info → flagsmith_sql_flag_engine-0.2.0.dist-info}/WHEEL
RENAMED
|
File without changes
|