2828from __future__ import annotations
2929
3030import re
31- from typing import Any
3231
33- from flag_engine .context .types import EnvironmentContext , SegmentContext
32+ from flag_engine .context .types import (
33+ EnvironmentContext ,
34+ SegmentCondition ,
35+ SegmentContext ,
36+ SegmentRule ,
37+ )
3438
3539from flagsmith_sql_flag_engine .dialect import Dialect
3640from flagsmith_sql_flag_engine .dialects .snowflake import SnowflakeDialect
@@ -213,7 +217,11 @@ def _semver_sort_key_expr(ctx: TranslateContext, value_sql: str) -> str:
213217
214218
215219def _comparison (
216- ctx : TranslateContext , op : str , expr : str , value : object , is_jsonpath : bool = False
220+ ctx : TranslateContext ,
221+ op : str ,
222+ expr : str ,
223+ value : object ,
224+ is_jsonpath : bool = False ,
217225) -> str | None :
218226 """Emit a SQL fragment comparing `expr` against `value` per `op`.
219227
@@ -237,21 +245,32 @@ def _comparison(
237245 if op == "NOT_CONTAINS" :
238246 return f"({ expr } IS NOT NULL AND NOT ({ d .position (lit , str_expr )} ))"
239247 if op in {"GREATER_THAN" , "LESS_THAN" , "GREATER_THAN_INCLUSIVE" , "LESS_THAN_INCLUSIVE" }:
248+ # Validate as numeric BEFORE interpolating — segment values come from
249+ # `MANAGE_SEGMENTS`-permissioned users, who shouldn't be able to inject
250+ # SQL via a comparator value. ValueError → untranslatable, caller
251+ # falls back to the engine UDF or surfaces an error upstream.
252+ try :
253+ numeric_value = float (str (value ))
254+ except (TypeError , ValueError ):
255+ return None
240256 sql_op = {
241257 "GREATER_THAN" : ">" ,
242258 "LESS_THAN" : "<" ,
243259 "GREATER_THAN_INCLUSIVE" : ">=" ,
244260 "LESS_THAN_INCLUSIVE" : "<=" ,
245261 }[op ]
246- return f"({ expr } IS NOT NULL AND { d .cast_float (expr )} { sql_op } { value } )"
262+ return f"({ expr } IS NOT NULL AND { d .cast_float (expr )} { sql_op } { numeric_value } )"
247263 if op == "MODULO" :
264+ # Same validation discipline: divisor/remainder must parse as numbers.
248265 try :
249- divisor , remainder = str (value ).split ("|" )
250- float (divisor )
251- float (remainder )
266+ divisor_str , remainder_str = str (value ).split ("|" )
267+ divisor = float (divisor_str )
268+ remainder = float (remainder_str )
252269 except (ValueError , AttributeError ):
253270 return None
254- return f"({ expr } IS NOT NULL AND ({ d .mod (d .cast_number (expr ), divisor )} ) = { remainder } )"
271+ return (
272+ f"({ expr } IS NOT NULL AND ({ d .mod (d .cast_number (expr ), str (divisor ))} ) = { remainder } )"
273+ )
255274 if op == "REGEX" :
256275 if not _regex_safe_for_re2 (str (value )):
257276 return None
@@ -264,7 +283,7 @@ def _comparison(
264283# ---------------------------------------------------------------------------
265284
266285
267- def translate_condition (cond : dict [ str , Any ] , ctx : TranslateContext ) -> str | None :
286+ def translate_condition (cond : SegmentCondition , ctx : TranslateContext ) -> str | None :
268287 op = cond ["operator" ]
269288 if op not in TRANSLATABLE_OPERATORS :
270289 return None
@@ -276,7 +295,10 @@ def translate_condition(cond: dict[str, Any], ctx: TranslateContext) -> str | No
276295 if op == "PERCENTAGE_SPLIT" :
277296 if not ctx .segment_key :
278297 return None
279- threshold = float (val ) if val is not None else 0.0
298+ try :
299+ threshold = float (str (val )) if val is not None else 0.0
300+ except (TypeError , ValueError ):
301+ return None
280302 if not prop :
281303 value_expr = ctx .dialect .cast_string (ctx .identity_key_col )
282304 elif prop .startswith ("$." ):
@@ -345,7 +367,7 @@ def translate_condition(cond: dict[str, Any], ctx: TranslateContext) -> str | No
345367# ---------------------------------------------------------------------------
346368
347369
348- def translate_rule (rule : dict [ str , Any ] , ctx : TranslateContext ) -> str | None :
370+ def translate_rule (rule : SegmentRule , ctx : TranslateContext ) -> str | None :
349371 children : list [str ] = []
350372 for cond in rule .get ("conditions" ) or []:
351373 sql = translate_condition (cond , ctx )
@@ -378,14 +400,14 @@ def translate_segment(segment: SegmentContext, ctx: TranslateContext) -> str | N
378400 The expression goes after `WHERE i.environment_id = '<env-key>' AND ...`.
379401 The caller composes the surrounding `SELECT ... FROM IDENTITIES i`.
380402 """
381- if ctx .segment_key is None and "key" in segment :
382- ctx = ctx .with_segment_key (str ( segment ["key" ]) )
403+ if ctx .segment_key is None :
404+ ctx = ctx .with_segment_key (segment ["key" ])
383405 rules = segment .get ("rules" ) or []
384406 if not rules :
385407 return "FALSE"
386408 rule_sql : list [str ] = []
387409 for r in rules :
388- sql = translate_rule (dict ( r ) , ctx )
410+ sql = translate_rule (r , ctx )
389411 if sql is None :
390412 return None
391413 rule_sql .append (f"({ sql } )" )
0 commit comments