Skip to content

Commit d8d581c

Browse files
wustzdyCaralHsi
andauthored
Dev zdy delete 120002 (#580)
* add contains for polardb.py * add contains for neo4j.py * add contains for neo4j.py --------- Co-authored-by: CaralHsi <[email protected]>
1 parent 0b86431 commit d8d581c

File tree

2 files changed

+114
-72
lines changed

2 files changed

+114
-72
lines changed

src/memos/graph_dbs/neo4j.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,9 +1415,9 @@ def build_filter_condition(condition_dict: dict, param_counter: list) -> tuple[s
14151415
params = {}
14161416

14171417
for key, value in condition_dict.items():
1418-
# Check if value is a dict with comparison operators (gt, lt, gte, lte)
1418+
# Check if value is a dict with comparison operators (gt, lt, gte, lte, contains, in, like)
14191419
if isinstance(value, dict):
1420-
# Handle comparison operators: gt (greater than), lt (less than), gte (greater than or equal), lte (less than or equal)
1420+
# Handle comparison operators: gt, lt, gte, lte, contains, in, like
14211421
for op, op_value in value.items():
14221422
if op in ("gt", "lt", "gte", "lte"):
14231423
# Map operator to Cypher operator
@@ -1440,24 +1440,28 @@ def build_filter_condition(condition_dict: dict, param_counter: list) -> tuple[s
14401440
f"{node_alias}.{key} {cypher_op} ${param_name}"
14411441
)
14421442
elif op == "contains":
1443-
# Handle contains operator (for array fields)
1444-
# Only supports array format: {"field": {"contains": ["value1", "value2"]}}
1445-
# Single string values are not supported, use array format instead: {"field": {"contains": ["value"]}}
1443+
# Handle contains operator
1444+
# For arrays: use IN to check if array contains value (value IN array_field)
1445+
# For strings: also use IN syntax to check if string value is in array field
1446+
# Note: In Neo4j, for array fields, we use "value IN field" syntax
1447+
param_name = f"filter_{key}_{op}_{param_counter[0]}"
1448+
param_counter[0] += 1
1449+
params[param_name] = op_value
1450+
# Use IN syntax: value IN array_field (works for both string and array values)
1451+
condition_parts.append(f"${param_name} IN {node_alias}.{key}")
1452+
elif op == "in":
1453+
# Handle in operator (for checking if field value is in a list)
1454+
# Supports array format: {"field": {"in": ["value1", "value2"]}}
14461455
if not isinstance(op_value, list):
14471456
raise ValueError(
1448-
f"contains operator only supports array format. "
1449-
f"Use {{'{key}': {{'contains': ['{op_value}']}}}} instead of {{'{key}': {{'contains': '{op_value}'}}}}"
1457+
f"in operator only supports array format. "
1458+
f"Use {{'{key}': {{'in': ['{op_value}']}}}} instead of {{'{key}': {{'in': '{op_value}'}}}}"
14501459
)
1451-
# Handle array of values: generate AND conditions for each value (all must be present)
1452-
and_conditions = []
1453-
for item in op_value:
1454-
param_name = f"filter_{key}_{op}_{param_counter[0]}"
1455-
param_counter[0] += 1
1456-
params[param_name] = item
1457-
# For array fields, check if element is in array
1458-
and_conditions.append(f"${param_name} IN {node_alias}.{key}")
1459-
if and_conditions:
1460-
condition_parts.append(f"({' AND '.join(and_conditions)})")
1460+
# Build IN clause
1461+
param_name = f"filter_{key}_{op}_{param_counter[0]}"
1462+
param_counter[0] += 1
1463+
params[param_name] = op_value
1464+
condition_parts.append(f"{node_alias}.{key} IN ${param_name}")
14611465
elif op == "like":
14621466
# Handle like operator (for fuzzy matching, similar to SQL LIKE '%value%')
14631467
# Neo4j uses CONTAINS for string matching

src/memos/graph_dbs/polardb.py

Lines changed: 93 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ def clean_properties(props):
9090
return {k: v for k, v in props.items() if k not in vector_keys}
9191

9292

93+
def escape_sql_string(value: str) -> str:
94+
"""Escape single quotes in SQL string."""
95+
return value.replace("'", "''")
96+
97+
9398
class PolarDBGraphDB(BaseGraphDB):
9499
"""PolarDB-based implementation using Apache AGE graph database extension."""
95100

@@ -3438,9 +3443,11 @@ def build_cypher_filter_condition(condition_dict: dict) -> str:
34383443
"""Build a Cypher WHERE condition for a single filter item."""
34393444
condition_parts = []
34403445
for key, value in condition_dict.items():
3441-
# Check if value is a dict with comparison operators (gt, lt, gte, lte, =, contains)
3446+
# Check if value is a dict with comparison operators (gt, lt, gte, lte, =, contains, in, like)
34423447
if isinstance(value, dict):
3443-
# Handle comparison operators: gt, lt, gte, lte, =, contains
3448+
# Handle comparison operators: gt, lt, gte, lte, =, contains, in, like
3449+
# Supports multiple operators for the same field, e.g.:
3450+
# will generate: n.created_at >= '2025-09-19' AND n.created_at <= '2025-12-31'
34443451
for op, op_value in value.items():
34453452
if op in ("gt", "lt", "gte", "lte"):
34463453
# Map operator to Cypher operator
@@ -3540,40 +3547,90 @@ def build_cypher_filter_condition(condition_dict: dict) -> str:
35403547
condition_parts.append(f"n.{key} = {op_value}")
35413548
elif op == "contains":
35423549
# Handle contains operator (for array fields)
3543-
# Only supports array format: {"field": {"contains": ["value1", "value2"]}}
3544-
# Single string values are not supported, use array format instead: {"field": {"contains": ["value"]}}
3550+
# Check if key starts with "info." prefix
3551+
if key.startswith("info."):
3552+
info_field = key[5:] # Remove "info." prefix
3553+
if isinstance(op_value, str):
3554+
escaped_value = escape_cypher_string(op_value)
3555+
condition_parts.append(
3556+
f"'{escaped_value}' IN n.info.{info_field}"
3557+
)
3558+
else:
3559+
condition_parts.append(f"{op_value} IN n.info.{info_field}")
3560+
else:
3561+
# Direct property access
3562+
if isinstance(op_value, str):
3563+
escaped_value = escape_cypher_string(op_value)
3564+
condition_parts.append(f"'{escaped_value}' IN n.{key}")
3565+
else:
3566+
condition_parts.append(f"{op_value} IN n.{key}")
3567+
elif op == "in":
3568+
# Handle in operator (for checking if field value is in a list)
3569+
# Supports array format: {"field": {"in": ["value1", "value2"]}}
3570+
# Generates: n.field IN ['value1', 'value2'] or (n.field = 'value1' OR n.field = 'value2')
35453571
if not isinstance(op_value, list):
35463572
raise ValueError(
3547-
f"contains operator only supports array format. "
3548-
f"Use {{'{key}': {{'contains': ['{op_value}']}}}} instead of {{'{key}': {{'contains': '{op_value}'}}}}"
3573+
f"in operator only supports array format. "
3574+
f"Use {{'{key}': {{'in': ['{op_value}']}}}} instead of {{'{key}': {{'in': '{op_value}'}}}}"
35493575
)
35503576
# Check if key starts with "info." prefix
35513577
if key.startswith("info."):
35523578
info_field = key[5:] # Remove "info." prefix
3553-
# Handle array of values: generate AND conditions for each value (all must be present)
3554-
and_conditions = []
3555-
for item in op_value:
3579+
# Build OR conditions for nested properties (Apache AGE compatibility)
3580+
if len(op_value) == 0:
3581+
# Empty list means no match
3582+
condition_parts.append("false")
3583+
elif len(op_value) == 1:
3584+
# Single value, use equality
3585+
item = op_value[0]
35563586
if isinstance(item, str):
35573587
escaped_value = escape_cypher_string(item)
3558-
and_conditions.append(
3559-
f"'{escaped_value}' IN n.info.{info_field}"
3588+
condition_parts.append(
3589+
f"n.info.{info_field} = '{escaped_value}'"
35603590
)
35613591
else:
3562-
and_conditions.append(f"{item} IN n.info.{info_field}")
3563-
if and_conditions:
3564-
condition_parts.append(f"({' AND '.join(and_conditions)})")
3592+
condition_parts.append(f"n.info.{info_field} = {item}")
3593+
else:
3594+
# Multiple values, use OR conditions instead of IN (Apache AGE compatibility)
3595+
or_conditions = []
3596+
for item in op_value:
3597+
if isinstance(item, str):
3598+
escaped_value = escape_cypher_string(item)
3599+
or_conditions.append(
3600+
f"n.info.{info_field} = '{escaped_value}'"
3601+
)
3602+
else:
3603+
or_conditions.append(
3604+
f"n.info.{info_field} = {item}"
3605+
)
3606+
if or_conditions:
3607+
condition_parts.append(
3608+
f"({' OR '.join(or_conditions)})"
3609+
)
35653610
else:
35663611
# Direct property access
3567-
# Handle array of values: generate AND conditions for each value (all must be present)
3568-
and_conditions = []
3569-
for item in op_value:
3612+
# Build array for IN clause or OR conditions
3613+
if len(op_value) == 0:
3614+
# Empty list means no match
3615+
condition_parts.append("false")
3616+
elif len(op_value) == 1:
3617+
# Single value, use equality
3618+
item = op_value[0]
35703619
if isinstance(item, str):
35713620
escaped_value = escape_cypher_string(item)
3572-
and_conditions.append(f"'{escaped_value}' IN n.{key}")
3621+
condition_parts.append(f"n.{key} = '{escaped_value}'")
35733622
else:
3574-
and_conditions.append(f"{item} IN n.{key}")
3575-
if and_conditions:
3576-
condition_parts.append(f"({' AND '.join(and_conditions)})")
3623+
condition_parts.append(f"n.{key} = {item}")
3624+
else:
3625+
# Multiple values, use IN clause
3626+
escaped_items = [
3627+
f"'{escape_cypher_string(str(item))}'"
3628+
if isinstance(item, str)
3629+
else str(item)
3630+
for item in op_value
3631+
]
3632+
array_str = "[" + ", ".join(escaped_items) + "]"
3633+
condition_parts.append(f"n.{key} IN {array_str}")
35773634
elif op == "like":
35783635
# Handle like operator (for fuzzy matching, similar to SQL LIKE '%value%')
35793636
# Check if key starts with "info." prefix
@@ -3781,47 +3838,28 @@ def build_filter_condition(condition_dict: dict) -> str:
37813838
f"ag_catalog.agtype_access_operator(properties, '\"{key}\"'::agtype) = {op_value}::agtype"
37823839
)
37833840
elif op == "contains":
3784-
# Handle contains operator (for array fields) - use @> operator
3785-
# Only supports array format: {"field": {"contains": ["value1", "value2"]}}
3786-
# Single string values are not supported, use array format instead: {"field": {"contains": ["value"]}}
3787-
if not isinstance(op_value, list):
3841+
# Handle contains operator (for string fields only)
3842+
# Check if agtype contains value (using @> operator)
3843+
if not isinstance(op_value, str):
37883844
raise ValueError(
3789-
f"contains operator only supports array format. "
3790-
f"Use {{'{key}': {{'contains': ['{op_value}']}}}} instead of {{'{key}': {{'contains': '{op_value}'}}}}"
3845+
f"contains operator only supports string format. "
3846+
f"Use {{'{key}': {{'contains': '{op_value}'}}}} instead of {{'{key}': {{'contains': {op_value}}}}}"
37913847
)
37923848
# Check if key starts with "info." prefix
37933849
if key.startswith("info."):
37943850
info_field = key[5:] # Remove "info." prefix
3795-
# Handle array of values: generate AND conditions for each value (all must be present)
3796-
and_conditions = []
3797-
for item in op_value:
3798-
if isinstance(item, str):
3799-
escaped_value = escape_sql_string(item)
3800-
and_conditions.append(
3801-
f"ag_catalog.agtype_access_operator(VARIADIC ARRAY[properties, '\"info\"'::ag_catalog.agtype, '\"{info_field}\"'::ag_catalog.agtype]) @> '\"{escaped_value}\"'::agtype"
3802-
)
3803-
else:
3804-
and_conditions.append(
3805-
f"ag_catalog.agtype_access_operator(VARIADIC ARRAY[properties, '\"info\"'::ag_catalog.agtype, '\"{info_field}\"'::ag_catalog.agtype]) @> {item}::agtype"
3806-
)
3807-
if and_conditions:
3808-
condition_parts.append(f"({' AND '.join(and_conditions)})")
3851+
# String contains: use @> operator for agtype contains
3852+
escaped_value = escape_sql_string(op_value)
3853+
condition_parts.append(
3854+
f"ag_catalog.agtype_access_operator(VARIADIC ARRAY[properties, '\"info\"'::ag_catalog.agtype, '\"{info_field}\"'::ag_catalog.agtype]) @> '\"{escaped_value}\"'::agtype"
3855+
)
38093856
else:
38103857
# Direct property access
3811-
# Handle array of values: generate AND conditions for each value (all must be present)
3812-
and_conditions = []
3813-
for item in op_value:
3814-
if isinstance(item, str):
3815-
escaped_value = escape_sql_string(item)
3816-
and_conditions.append(
3817-
f"ag_catalog.agtype_access_operator(properties, '\"{key}\"'::agtype) @> '\"{escaped_value}\"'::agtype"
3818-
)
3819-
else:
3820-
and_conditions.append(
3821-
f"ag_catalog.agtype_access_operator(properties, '\"{key}\"'::agtype) @> {item}::agtype"
3822-
)
3823-
if and_conditions:
3824-
condition_parts.append(f"({' AND '.join(and_conditions)})")
3858+
# String contains: use @> operator for agtype contains
3859+
escaped_value = escape_sql_string(op_value)
3860+
condition_parts.append(
3861+
f"ag_catalog.agtype_access_operator(properties, '\"{key}\"'::agtype) @> '\"{escaped_value}\"'::agtype"
3862+
)
38253863
elif op == "like":
38263864
# Handle like operator (for fuzzy matching, similar to SQL LIKE '%value%')
38273865
# Check if key starts with "info." prefix

0 commit comments

Comments
 (0)