Skip to content

Commit 377aa0e

Browse files
committed
docs: README
beep boop
1 parent 4a33ce3 commit 377aa0e

1 file changed

Lines changed: 135 additions & 1 deletion

File tree

README.md

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,137 @@
11
# flagsmith-sql-flag-engine
22

3-
Placeholder. The initial package scaffold lands via the first pull request.
3+
SQL translator for Flagsmith segment predicates.
4+
5+
Where the Python and Rust `flag_engine` implementations evaluate
6+
`is_context_in_segment` against an in-memory `EvaluationContext`, this
7+
package takes a `SegmentContext` and emits a SQL `WHERE` expression that
8+
evaluates the segment against an entire `IDENTITIES` table — one row per
9+
identity, with the identity's full trait map held in a single column
10+
the translator path-extracts at query time. `PERCENTAGE_SPLIT` and
11+
`:semver`-marked comparators compile to inline pure-SQL.
12+
13+
## Quickstart
14+
15+
```python
16+
from flag_engine.context.types import EvaluationContext, SegmentContext
17+
18+
from flagsmith_sql_flag_engine import TranslateContext, translate_segment
19+
from flagsmith_sql_flag_engine.dialects import ClickHouseDialect
20+
21+
eval_context: EvaluationContext = {
22+
"environment": {"key": "n9fbf9...3ngWhb", "name": "Production"},
23+
}
24+
ctx = TranslateContext(evaluation_context=eval_context, dialect=ClickHouseDialect())
25+
26+
segment: SegmentContext = {
27+
"key": "growth-cohort",
28+
"name": "Growth cohort",
29+
"rules": [
30+
{
31+
"type": "ALL",
32+
"conditions": [
33+
{"operator": "EQUAL", "property": "plan", "value": "growth"},
34+
],
35+
},
36+
],
37+
}
38+
where_expr = translate_segment(segment, ctx)
39+
# where_expr is a SQL string. Drop into:
40+
# SELECT COUNT(*) FROM IDENTITIES i
41+
# WHERE i.environment_id = 'n9fbf9...3ngWhb' AND ({where_expr})
42+
```
43+
44+
`environment_id` in the `IDENTITIES` table is a string column holding
45+
`EnvironmentContext.key` directly — the same identifier the engine uses,
46+
no separate integer PK.
47+
48+
`translate_segment` returns `None` if the segment uses an operator the
49+
translator can't handle — typically a REGEX pattern the active dialect's
50+
regex flavour can't compile. Callers should fall back to
51+
`flag_engine.is_context_in_segment` for those segments.
52+
53+
## Schema
54+
55+
Each dialect publishes the table layout it expects via a `schema_ddl`
56+
constant. For ClickHouse:
57+
58+
```sql
59+
CREATE TABLE IF NOT EXISTS IDENTITIES (
60+
environment_id String,
61+
id UInt64,
62+
identifier String,
63+
identity_key String,
64+
traits JSON
65+
)
66+
ENGINE = MergeTree()
67+
ORDER BY (environment_id, id);
68+
```
69+
70+
Traits live in a single `JSON` column (CH 24+, GA in 25.x). Each key is
71+
stored as a typed subcolumn, so trait reads are direct columnar scans
72+
rather than per-row JSON parses. Trait keys are *data* — new keys appear
73+
without schema changes — and the translator only sees the abstract path
74+
extraction.
75+
76+
ClickHouse Cloud requires `SET allow_experimental_json_type = 1` when
77+
creating a `JSON`-column table (the type is GA on OSS 25.x); the test
78+
harness applies this setting automatically.
79+
80+
Programmatic access:
81+
82+
```python
83+
from flagsmith_sql_flag_engine.dialects.clickhouse import SCHEMA_DDL
84+
```
85+
86+
## Engine parity
87+
88+
Validated against [Flagsmith/engine-test-data](https://github.com/Flagsmith/engine-test-data),
89+
the test suite every engine implementation is checked against. The
90+
engine-parity suite loads each test case's identity into a per-dialect
91+
scratch table, translates the case's segments, runs the generated SQL,
92+
and compares to `flag_engine.is_context_in_segment`.
93+
94+
To run the engine-parity suite locally:
95+
96+
```bash
97+
git submodule update --init # pull engine-test-data
98+
docker compose up --detach --wait clickhouse
99+
uv run pytest tests/test_engine.py
100+
```
101+
102+
Adding a new dialect's parity coverage is one harness module — see
103+
`tests/harnesses/` for the shape.
104+
105+
## Dialects
106+
107+
The translator is dialect-aware: a `Dialect` protocol abstracts the
108+
SQL fragments that differ across SQL engines — MD5 hex, hex-to-int
109+
parsing, prefix-anchored regex, padded-version comparison, type-aware
110+
trait predicates, regex flavour. Today `ClickHouseDialect` is the only
111+
implementation; adding another engine such as Snowflake, DuckDB or
112+
Postgres means writing one class.
113+
114+
## Operator coverage
115+
116+
| Operator | Translatable | Notes |
117+
| -------------------------------------------- | :----------: | -------------------------------------------------------------- |
118+
| `EQUAL`, `NOT_EQUAL`, `IN` | yes | |
119+
| `IS_SET`, `IS_NOT_SET` | yes | trait subcolumn `IS NOT NULL` / `IS NULL` |
120+
| `CONTAINS`, `NOT_CONTAINS` | yes | |
121+
| `GREATER_THAN`, `LESS_THAN` plus `_INCLUSIVE`| yes | |
122+
| `MODULO` | yes | |
123+
| `PERCENTAGE_SPLIT` | yes | inlined MD5-mod-9999; ~0.005% diverge on hash==9998 |
124+
| `REGEX` | partial | dialect-flavour gated; unsupported patterns → caller fallback |
125+
| `:semver`-marked comparators | yes | major.minor.patch only; ignores prerelease |
126+
127+
## Development
128+
129+
```bash
130+
make install # uv sync + pre-commit install
131+
make lint # run pre-commit hooks across the tree
132+
make typecheck # mypy
133+
make test # unit tests
134+
```
135+
136+
Ruff (lint + format) runs as a pre-commit hook on every commit. Mypy
137+
runs as a `make typecheck` hook on staged Python files.

0 commit comments

Comments
 (0)