From a7662a856185e47cbeda9a1a3e4982327b193a1c Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Fri, 10 Jul 2026 07:57:41 +0200 Subject: [PATCH] =?UTF-8?q?feat(query):=20sort=5Fmodel=5Fto=5Fsql=20+=20s?= =?UTF-8?q?=C3=A9rialisation=20AST=20(#41)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/utils/query_ast.py | 51 ++++++++++++++++++++++++++++++++++++++++- tests/test_query_ast.py | 39 ++++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/utils/query_ast.py b/src/utils/query_ast.py index d81e8f1..f36a4aa 100644 --- a/src/utils/query_ast.py +++ b/src/utils/query_ast.py @@ -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 diff --git a/tests/test_query_ast.py b/tests/test_query_ast.py index fee6f2c..60f4a4e 100644 --- a/tests/test_query_ast.py +++ b/tests/test_query_ast.py @@ -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