Skip to content

Commit fd2c9c7

Browse files
committed
feat: SQL translator with Dialect protocol and ClickHouse dialect
beep boop
1 parent 060eacb commit fd2c9c7

9 files changed

Lines changed: 2239 additions & 1 deletion

File tree

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,28 @@
1-
"""SQL translator for Flagsmith segment predicates."""
1+
"""SQL translator for Flagsmith segment predicates.
2+
3+
Public API:
4+
translate_segment(segment, ctx) -> str | None
5+
TranslateContext
6+
7+
See README.md for usage. The translator is dialect-aware via the `Dialect`
8+
protocol; `flagsmith_sql_flag_engine.dialects.clickhouse.ClickHouseDialect`
9+
is the only implementation today.
10+
"""
11+
12+
from flagsmith_sql_flag_engine.dialect import Dialect
13+
from flagsmith_sql_flag_engine.translator import (
14+
TRANSLATABLE_OPERATORS,
15+
TranslateContext,
16+
translate_condition,
17+
translate_rule,
18+
translate_segment,
19+
)
20+
21+
__all__ = [
22+
"TRANSLATABLE_OPERATORS",
23+
"Dialect",
24+
"TranslateContext",
25+
"translate_condition",
26+
"translate_rule",
27+
"translate_segment",
28+
]
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""Per-dialect SQL fragments — MD5 hex, hex-to-int parsing, prefix-anchored
2+
regex, padded-version comparison, type-aware trait predicates, regex flavour."""
3+
4+
from typing import Protocol
5+
6+
7+
class Dialect(Protocol):
8+
"""Per-dialect SQL fragments.
9+
10+
Methods return SQL string fragments. Inputs are already-formatted SQL
11+
strings (column refs, string literals); the dialect only chooses the
12+
right syntax for the operation.
13+
"""
14+
15+
name: str # human-readable, used in test ids and error messages
16+
17+
# --- IDENTITIES schema access ---
18+
#
19+
# The dialect owns the canonical IDENTITIES schema, see `schema_ddl`,
20+
# so it also owns the SQL expression for each logical column. The
21+
# translator just hands over an alias.
22+
23+
def identifier_expr(self, alias: str) -> str:
24+
"""SQL expression for `$.identity.identifier`."""
25+
...
26+
27+
def identity_key_expr(self, alias: str) -> str:
28+
"""SQL expression for `$.identity.key`."""
29+
...
30+
31+
def trait_path(self, alias: str, trait_key: str) -> str:
32+
"""Path-extract a trait value from the IDENTITIES traits container.
33+
34+
The path syntax varies by SQL engine.
35+
"""
36+
...
37+
38+
def trait_eq(self, alias: str, trait_key: str, value: object, negate: bool) -> str:
39+
"""Type-aware EQUAL / NOT_EQUAL predicate on a trait, mirroring
40+
`flag_engine`'s per-type coercion: the segment value is cast to
41+
the trait's runtime type before compare, and a cast failure
42+
means no match for both ops. Implementation is dialect-specific
43+
because trait-type discrimination and runtime type-coercion
44+
casts both vary by engine.
45+
"""
46+
...
47+
48+
def trait_in(self, alias: str, trait_key: str, items: list[str]) -> str:
49+
"""Type-aware IN predicate on a trait, mirroring engine semantics:
50+
string trait does direct lookup; integer trait stringifies and
51+
looks up; other trait types never match. `items` is the parsed
52+
candidate list per `flag_engine`'s `_get_in_values`.
53+
"""
54+
...
55+
56+
# --- string operations ---
57+
58+
def position(self, needle_lit: str, haystack_expr: str) -> str:
59+
"""Boolean: does the string literal `needle_lit` appear in
60+
`haystack_expr`? Used for CONTAINS / NOT_CONTAINS."""
61+
...
62+
63+
def lpad(self, expr: str, width: int, pad_lit: str) -> str:
64+
"""Left-pad `expr` to `width` using `pad_lit`."""
65+
...
66+
67+
def coalesce(self, *exprs: str) -> str:
68+
"""COALESCE/NVL-style: first non-null."""
69+
...
70+
71+
# --- regex ---
72+
73+
def regex_supports(self, pattern: str) -> bool:
74+
"""Return True if this dialect's regex engine can compile
75+
`pattern`. The translator falls back to `None` for any REGEX
76+
condition where this returns False, letting the caller defer
77+
to `flag_engine`."""
78+
...
79+
80+
def regexp_anchored_match(self, value_expr: str, pattern: str) -> str:
81+
"""Boolean: equivalent to Python `re.match(pattern, value)` —
82+
anchored at position 0, may be a prefix of the value, not a
83+
full-match.
84+
85+
`pattern` is the raw Python regex string; the dialect handles
86+
its own escaping into a SQL literal, since regex flavours
87+
differ in how backslashes are treated."""
88+
...
89+
90+
def regexp_nth_digit_run(self, value_expr: str, n: int) -> str:
91+
"""Extract the n-th sequence of digits from `value_expr`. Returns NULL
92+
if there are fewer than n digit runs. Used for semver."""
93+
...
94+
95+
# --- hashing primitives for PERCENTAGE_SPLIT ---
96+
97+
def md5_hex(self, expr: str) -> str:
98+
"""SQL fragment producing the lowercase 32-char hex MD5 digest."""
99+
...
100+
101+
def parse_hex_chunk(self, hex_expr: str, start: int, length: int = 8) -> str:
102+
"""Parse `length` hex characters of `hex_expr` starting at 1-indexed
103+
`start` into a non-negative integer."""
104+
...
105+
106+
# --- type casts ---
107+
108+
def cast_string(self, expr: str) -> str:
109+
"""Cast `expr` to STRING / VARCHAR."""
110+
...
111+
112+
def cast_float(self, expr: str) -> str:
113+
"""Cast `expr` to a 64-bit float / DOUBLE."""
114+
...
115+
116+
def cast_number(self, expr: str) -> str:
117+
"""Cast `expr` to a NUMBER / BIGINT — the engine-side numeric
118+
type used for modulo arithmetic."""
119+
...
120+
121+
# --- composition ---
122+
123+
def mod(self, dividend: str, divisor: str) -> str:
124+
"""`dividend MOD divisor` returning a numeric value."""
125+
...
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Dialect implementations."""
2+
3+
from flagsmith_sql_flag_engine.dialects.clickhouse import ClickHouseDialect
4+
5+
__all__ = ["ClickHouseDialect"]

0 commit comments

Comments
 (0)