@@ -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