Skip to content

Commit ef3447c

Browse files
authored
fix contains (#564)
1 parent 7d34e65 commit ef3447c

File tree

2 files changed

+82
-41
lines changed

2 files changed

+82
-41
lines changed

src/memos/graph_dbs/neo4j.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,17 +1441,24 @@ def build_filter_condition(condition_dict: dict, param_counter: list) -> tuple[s
14411441
f"{node_alias}.{key} {cypher_op} ${param_name}"
14421442
)
14431443
elif op == "contains":
1444-
# Handle contains operator (for array fields like tags, sources)
1445-
param_name = f"filter_{key}_{op}_{param_counter[0]}"
1446-
param_counter[0] += 1
1447-
params[param_name] = op_value
1448-
1449-
# For array fields, check if element is in array
1450-
if key in ("tags", "sources"):
1451-
condition_parts.append(f"${param_name} IN {node_alias}.{key}")
1452-
else:
1453-
# For non-array fields, contains might not be applicable, but we'll treat it as IN for consistency
1454-
condition_parts.append(f"${param_name} IN {node_alias}.{key}")
1444+
# Handle contains operator (for array fields)
1445+
# Only supports array format: {"field": {"contains": ["value1", "value2"]}}
1446+
# Single string values are not supported, use array format instead: {"field": {"contains": ["value"]}}
1447+
if not isinstance(op_value, list):
1448+
raise ValueError(
1449+
f"contains operator only supports array format. "
1450+
f"Use {{'{key}': {{'contains': ['{op_value}']}}}} instead of {{'{key}': {{'contains': '{op_value}'}}}}"
1451+
)
1452+
# Handle array of values: generate AND conditions for each value (all must be present)
1453+
and_conditions = []
1454+
for item in op_value:
1455+
param_name = f"filter_{key}_{op}_{param_counter[0]}"
1456+
param_counter[0] += 1
1457+
params[param_name] = item
1458+
# For array fields, check if element is in array
1459+
and_conditions.append(f"${param_name} IN {node_alias}.{key}")
1460+
if and_conditions:
1461+
condition_parts.append(f"({' AND '.join(and_conditions)})")
14551462
elif op == "like":
14561463
# Handle like operator (for fuzzy matching, similar to SQL LIKE '%value%')
14571464
# Neo4j uses CONTAINS for string matching

src/memos/graph_dbs/polardb.py

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3443,23 +3443,40 @@ def build_cypher_filter_condition(condition_dict: dict) -> str:
34433443
condition_parts.append(f"n.{key} = {op_value}")
34443444
elif op == "contains":
34453445
# Handle contains operator (for array fields)
3446+
# Only supports array format: {"field": {"contains": ["value1", "value2"]}}
3447+
# Single string values are not supported, use array format instead: {"field": {"contains": ["value"]}}
3448+
if not isinstance(op_value, list):
3449+
raise ValueError(
3450+
f"contains operator only supports array format. "
3451+
f"Use {{'{key}': {{'contains': ['{op_value}']}}}} instead of {{'{key}': {{'contains': '{op_value}'}}}}"
3452+
)
34463453
# Check if key starts with "info." prefix
34473454
if key.startswith("info."):
34483455
info_field = key[5:] # Remove "info." prefix
3449-
if isinstance(op_value, str):
3450-
escaped_value = escape_cypher_string(op_value)
3451-
condition_parts.append(
3452-
f"'{escaped_value}' IN n.info.{info_field}"
3453-
)
3454-
else:
3455-
condition_parts.append(f"{op_value} IN n.info.{info_field}")
3456+
# Handle array of values: generate AND conditions for each value (all must be present)
3457+
and_conditions = []
3458+
for item in op_value:
3459+
if isinstance(item, str):
3460+
escaped_value = escape_cypher_string(item)
3461+
and_conditions.append(
3462+
f"'{escaped_value}' IN n.info.{info_field}"
3463+
)
3464+
else:
3465+
and_conditions.append(f"{item} IN n.info.{info_field}")
3466+
if and_conditions:
3467+
condition_parts.append(f"({' AND '.join(and_conditions)})")
34563468
else:
34573469
# Direct property access
3458-
if isinstance(op_value, str):
3459-
escaped_value = escape_cypher_string(op_value)
3460-
condition_parts.append(f"'{escaped_value}' IN n.{key}")
3461-
else:
3462-
condition_parts.append(f"{op_value} IN n.{key}")
3470+
# Handle array of values: generate AND conditions for each value (all must be present)
3471+
and_conditions = []
3472+
for item in op_value:
3473+
if isinstance(item, str):
3474+
escaped_value = escape_cypher_string(item)
3475+
and_conditions.append(f"'{escaped_value}' IN n.{key}")
3476+
else:
3477+
and_conditions.append(f"{item} IN n.{key}")
3478+
if and_conditions:
3479+
condition_parts.append(f"({' AND '.join(and_conditions)})")
34633480
elif op == "like":
34643481
# Handle like operator (for fuzzy matching, similar to SQL LIKE '%value%')
34653482
# Check if key starts with "info." prefix
@@ -3668,29 +3685,46 @@ def build_filter_condition(condition_dict: dict) -> str:
36683685
)
36693686
elif op == "contains":
36703687
# Handle contains operator (for array fields) - use @> operator
3688+
# Only supports array format: {"field": {"contains": ["value1", "value2"]}}
3689+
# Single string values are not supported, use array format instead: {"field": {"contains": ["value"]}}
3690+
if not isinstance(op_value, list):
3691+
raise ValueError(
3692+
f"contains operator only supports array format. "
3693+
f"Use {{'{key}': {{'contains': ['{op_value}']}}}} instead of {{'{key}': {{'contains': '{op_value}'}}}}"
3694+
)
36713695
# Check if key starts with "info." prefix
36723696
if key.startswith("info."):
36733697
info_field = key[5:] # Remove "info." prefix
3674-
if isinstance(op_value, str):
3675-
escaped_value = escape_sql_string(op_value)
3676-
condition_parts.append(
3677-
f"ag_catalog.agtype_access_operator(VARIADIC ARRAY[properties, '\"info\"'::ag_catalog.agtype, '\"{info_field}\"'::ag_catalog.agtype]) @> '\"{escaped_value}\"'::agtype"
3678-
)
3679-
else:
3680-
condition_parts.append(
3681-
f"ag_catalog.agtype_access_operator(VARIADIC ARRAY[properties, '\"info\"'::ag_catalog.agtype, '\"{info_field}\"'::ag_catalog.agtype]) @> {op_value}::agtype"
3682-
)
3698+
# Handle array of values: generate AND conditions for each value (all must be present)
3699+
and_conditions = []
3700+
for item in op_value:
3701+
if isinstance(item, str):
3702+
escaped_value = escape_sql_string(item)
3703+
and_conditions.append(
3704+
f"ag_catalog.agtype_access_operator(VARIADIC ARRAY[properties, '\"info\"'::ag_catalog.agtype, '\"{info_field}\"'::ag_catalog.agtype]) @> '\"{escaped_value}\"'::agtype"
3705+
)
3706+
else:
3707+
and_conditions.append(
3708+
f"ag_catalog.agtype_access_operator(VARIADIC ARRAY[properties, '\"info\"'::ag_catalog.agtype, '\"{info_field}\"'::ag_catalog.agtype]) @> {item}::agtype"
3709+
)
3710+
if and_conditions:
3711+
condition_parts.append(f"({' AND '.join(and_conditions)})")
36833712
else:
36843713
# Direct property access
3685-
if isinstance(op_value, str):
3686-
escaped_value = escape_sql_string(op_value)
3687-
condition_parts.append(
3688-
f"ag_catalog.agtype_access_operator(properties, '\"{key}\"'::agtype) @> '\"{escaped_value}\"'::agtype"
3689-
)
3690-
else:
3691-
condition_parts.append(
3692-
f"ag_catalog.agtype_access_operator(properties, '\"{key}\"'::agtype) @> {op_value}::agtype"
3693-
)
3714+
# Handle array of values: generate AND conditions for each value (all must be present)
3715+
and_conditions = []
3716+
for item in op_value:
3717+
if isinstance(item, str):
3718+
escaped_value = escape_sql_string(item)
3719+
and_conditions.append(
3720+
f"ag_catalog.agtype_access_operator(properties, '\"{key}\"'::agtype) @> '\"{escaped_value}\"'::agtype"
3721+
)
3722+
else:
3723+
and_conditions.append(
3724+
f"ag_catalog.agtype_access_operator(properties, '\"{key}\"'::agtype) @> {item}::agtype"
3725+
)
3726+
if and_conditions:
3727+
condition_parts.append(f"({' AND '.join(and_conditions)})")
36943728
elif op == "like":
36953729
# Handle like operator (for fuzzy matching, similar to SQL LIKE '%value%')
36963730
# Check if key starts with "info." prefix

0 commit comments

Comments
 (0)