feat(query): sort_model_to_sql + sérialisation AST (#41)
Ajoute sort_model_to_sql (adapte le sortModel AG Grid vers sort_by_to_sql existant) et ast_to_dict/ast_from_dict pour le round-trip JSON de l'AST, nécessaire aux vues sauvegardées.
This commit is contained in:
+50
-1
@@ -11,7 +11,7 @@ from dataclasses import dataclass
|
||||
import polars as pl
|
||||
|
||||
from src.utils import logger
|
||||
from src.utils.table_sql import tokenize_text_filter
|
||||
from src.utils.table_sql import sort_by_to_sql, tokenize_text_filter
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -212,3 +212,52 @@ def filtermodel_to_ast(filter_model, schema):
|
||||
continue
|
||||
children.append(node)
|
||||
return And(children) if children else None
|
||||
|
||||
|
||||
def sort_model_to_sql(sort_model: list | None, schema: pl.Schema) -> str:
|
||||
"""Traduit un sortModel AG Grid en clause ORDER BY DuckDB (adapte à sort_by_to_sql)."""
|
||||
if not sort_model:
|
||||
return ""
|
||||
sort_by = [
|
||||
{"column_id": s.get("colId"), "direction": s.get("sort")} for s in sort_model
|
||||
]
|
||||
return sort_by_to_sql(sort_by, schema)
|
||||
|
||||
|
||||
def ast_to_dict(node: Node) -> dict | None:
|
||||
"""Sérialise un AST en dict JSON-compatible (pour persistance en base)."""
|
||||
if node is None:
|
||||
return None
|
||||
if isinstance(node, Condition):
|
||||
return {
|
||||
"t": "cond",
|
||||
"column": node.column,
|
||||
"operator": node.operator,
|
||||
"value": node.value,
|
||||
"value2": node.value2,
|
||||
}
|
||||
if isinstance(node, And):
|
||||
return {"t": "and", "children": [ast_to_dict(c) for c in node.children]}
|
||||
if isinstance(node, Or):
|
||||
return {"t": "or", "children": [ast_to_dict(c) for c in node.children]}
|
||||
if isinstance(node, Not):
|
||||
return {"t": "not", "child": ast_to_dict(node.child)}
|
||||
return None
|
||||
|
||||
|
||||
def ast_from_dict(data: dict | None) -> Node:
|
||||
"""Reconstruit un AST à partir d'un dict produit par ast_to_dict."""
|
||||
if data is None:
|
||||
return None
|
||||
t = data.get("t")
|
||||
if t == "cond":
|
||||
return Condition(
|
||||
data["column"], data["operator"], data.get("value"), data.get("value2")
|
||||
)
|
||||
if t == "and":
|
||||
return And([ast_from_dict(c) for c in data["children"]])
|
||||
if t == "or":
|
||||
return Or([ast_from_dict(c) for c in data["children"]])
|
||||
if t == "not":
|
||||
return Not(ast_from_dict(data["child"]))
|
||||
return None
|
||||
|
||||
+38
-1
@@ -1,6 +1,16 @@
|
||||
import polars as pl
|
||||
|
||||
from src.utils.query_ast import And, Condition, Not, Or, ast_to_sql, filtermodel_to_ast
|
||||
from src.utils.query_ast import (
|
||||
And,
|
||||
Condition,
|
||||
Not,
|
||||
Or,
|
||||
ast_from_dict,
|
||||
ast_to_dict,
|
||||
ast_to_sql,
|
||||
filtermodel_to_ast,
|
||||
sort_model_to_sql,
|
||||
)
|
||||
|
||||
SCHEMA = pl.Schema(
|
||||
{
|
||||
@@ -198,3 +208,30 @@ def test_filtermodel_multiple_columns_are_anded():
|
||||
}
|
||||
sql, params = ast_to_sql(filtermodel_to_ast(fm, SCHEMA), SCHEMA)
|
||||
assert " AND " in sql and set(params) == {"%voirie%", 1000.0}
|
||||
|
||||
|
||||
def test_sort_model_to_sql():
|
||||
sm = [{"colId": "montant", "sort": "desc"}, {"colId": "dureeMois", "sort": "asc"}]
|
||||
out = sort_model_to_sql(sm, SCHEMA)
|
||||
assert out == '"montant" DESC NULLS LAST, "dureeMois" ASC NULLS LAST'
|
||||
|
||||
|
||||
def test_sort_model_empty():
|
||||
assert sort_model_to_sql(None, SCHEMA) == ""
|
||||
assert sort_model_to_sql([], SCHEMA) == ""
|
||||
|
||||
|
||||
def test_ast_dict_roundtrip():
|
||||
node = And(
|
||||
[
|
||||
Or([Condition("objet", "contains", "beton")]),
|
||||
Not(Condition("objet", "contains", "x")),
|
||||
]
|
||||
)
|
||||
restored = ast_from_dict(ast_to_dict(node))
|
||||
assert ast_to_sql(restored, SCHEMA) == ast_to_sql(node, SCHEMA)
|
||||
|
||||
|
||||
def test_ast_dict_none():
|
||||
assert ast_to_dict(None) is None
|
||||
assert ast_from_dict(None) is None
|
||||
|
||||
Reference in New Issue
Block a user