From 6d3f7f17aa66a7ab4a32af831bb761d8033b6ebf Mon Sep 17 00:00:00 2001 From: Sean Johnson Date: Wed, 11 Feb 2026 17:03:34 -0600 Subject: [PATCH] fix(cypher): use @> for kind matching instead of && --- cypher/models/pgsql/operators.go | 73 ++++++++++++------------ cypher/models/pgsql/pgtypes.go | 6 +- cypher/models/pgsql/pytypes_test.go | 14 +++++ cypher/models/pgsql/translate/hinting.go | 2 +- cypher/models/pgsql/translate/kind.go | 2 +- cypher/models/pgsql/translate/node.go | 2 +- 6 files changed, 54 insertions(+), 45 deletions(-) diff --git a/cypher/models/pgsql/operators.go b/cypher/models/pgsql/operators.go index 6c90a24..1497039 100644 --- a/cypher/models/pgsql/operators.go +++ b/cypher/models/pgsql/operators.go @@ -1,5 +1,7 @@ package pgsql +import "slices" + type Operator string func (s Operator) IsIn(others ...Operator) bool { @@ -25,13 +27,7 @@ func (s Operator) NodeType() string { } func OperatorIsIn(operator Expression, matchers ...Expression) bool { - for _, matcher := range matchers { - if operator == matcher { - return true - } - } - - return false + return slices.Contains(matchers, operator) } func OperatorIsBoolean(operator Expression) bool { @@ -59,40 +55,41 @@ func OperatorIsComparator(operator Expression) bool { return OperatorIsIn(operator, OperatorEquals, OperatorNotEquals, OperatorGreaterThan, OperatorGreaterThanOrEqualTo, OperatorLessThan, OperatorLessThanOrEqualTo, OperatorArrayOverlap, OperatorLike, OperatorILike, OperatorPGArrayOverlap, - OperatorRegexMatch, OperatorSimilarTo) + OperatorRegexMatch, OperatorSimilarTo, OperatorPGArrayLHSContainsRHS) } const ( - UnsetOperator Operator = "" - OperatorUnion Operator = "union" - OperatorConcatenate Operator = "||" - OperatorArrayOverlap Operator = "&&" - OperatorEquals Operator = "=" - OperatorNotEquals Operator = "!=" - OperatorGreaterThan Operator = ">" - OperatorGreaterThanOrEqualTo Operator = ">=" - OperatorLessThan Operator = "<" - OperatorLessThanOrEqualTo Operator = "<=" - OperatorLike Operator = "like" - OperatorILike Operator = "ilike" - OperatorPGArrayOverlap Operator = "operator (pg_catalog.&&)" - OperatorAnd Operator = "and" - OperatorOr Operator = "or" - OperatorNot Operator = "not" - OperatorJSONBFieldExists Operator = "?" - OperatorJSONField Operator = "->" - OperatorJSONTextField Operator = "->>" - OperatorAdd Operator = "+" - OperatorSubtract Operator = "-" - OperatorMultiply Operator = "*" - OperatorDivide Operator = "/" - OperatorIn Operator = "in" - OperatorIs Operator = "is" - OperatorIsNot Operator = "is not" - OperatorSimilarTo Operator = "similar to" - OperatorRegexMatch Operator = "~" - OperatorAssignment Operator = "=" - OperatorAdditionAssignment Operator = "+=" + UnsetOperator Operator = "" + OperatorUnion Operator = "union" + OperatorConcatenate Operator = "||" + OperatorArrayOverlap Operator = "&&" + OperatorEquals Operator = "=" + OperatorNotEquals Operator = "!=" + OperatorGreaterThan Operator = ">" + OperatorGreaterThanOrEqualTo Operator = ">=" + OperatorLessThan Operator = "<" + OperatorLessThanOrEqualTo Operator = "<=" + OperatorLike Operator = "like" + OperatorILike Operator = "ilike" + OperatorPGArrayOverlap Operator = "operator (pg_catalog.&&)" + OperatorPGArrayLHSContainsRHS Operator = "operator (pg_catalog.@>)" + OperatorAnd Operator = "and" + OperatorOr Operator = "or" + OperatorNot Operator = "not" + OperatorJSONBFieldExists Operator = "?" + OperatorJSONField Operator = "->" + OperatorJSONTextField Operator = "->>" + OperatorAdd Operator = "+" + OperatorSubtract Operator = "-" + OperatorMultiply Operator = "*" + OperatorDivide Operator = "/" + OperatorIn Operator = "in" + OperatorIs Operator = "is" + OperatorIsNot Operator = "is not" + OperatorSimilarTo Operator = "similar to" + OperatorRegexMatch Operator = "~" + OperatorAssignment Operator = "=" + OperatorAdditionAssignment Operator = "+=" OperatorCypherRegexMatch Operator = "=~" OperatorCypherStartsWith Operator = "starts with" diff --git a/cypher/models/pgsql/pgtypes.go b/cypher/models/pgsql/pgtypes.go index dd76933..70748c3 100644 --- a/cypher/models/pgsql/pgtypes.go +++ b/cypher/models/pgsql/pgtypes.go @@ -10,9 +10,7 @@ import ( "github.com/specterops/dawgs/graph" ) -var ( - ErrNoAvailableArrayDataType = errors.New("data type has no direct array representation") -) +var ErrNoAvailableArrayDataType = errors.New("data type has no direct array representation") const ( StringLiteralNull = "null" @@ -118,7 +116,7 @@ func (s DataType) IsKnown() bool { func (s DataType) IsComparable(other DataType, operator Operator) bool { switch operator { - case OperatorPGArrayOverlap, OperatorArrayOverlap: + case OperatorPGArrayOverlap, OperatorArrayOverlap, OperatorPGArrayLHSContainsRHS: if !s.IsArrayType() || !other.IsArrayType() { return false } diff --git a/cypher/models/pgsql/pytypes_test.go b/cypher/models/pgsql/pytypes_test.go index 1de4ee7..0223d83 100644 --- a/cypher/models/pgsql/pytypes_test.go +++ b/cypher/models/pgsql/pytypes_test.go @@ -201,6 +201,20 @@ func TestDataType_Comparable(t *testing.T) { Expected: false, }, + // Array types may use the "LHS contains RHS" operator but only if their base types match + { + LeftTypes: []DataType{IntArray}, + Operators: []Operator{OperatorPGArrayLHSContainsRHS}, + RightTypes: []DataType{IntArray}, + Expected: true, + }, + { + LeftTypes: []DataType{IntArray}, + Operators: []Operator{OperatorPGArrayLHSContainsRHS}, + RightTypes: []DataType{Int}, + Expected: false, + }, + // Catch all for any unsupported operator { LeftTypes: []DataType{Int}, diff --git a/cypher/models/pgsql/translate/hinting.go b/cypher/models/pgsql/translate/hinting.go index 7a6bf72..74f8bb7 100644 --- a/cypher/models/pgsql/translate/hinting.go +++ b/cypher/models/pgsql/translate/hinting.go @@ -137,7 +137,7 @@ func InferExpressionType(expression pgsql.Expression) (pgsql.DataType, error) { case pgsql.OperatorAnd, pgsql.OperatorOr, pgsql.OperatorEquals, pgsql.OperatorGreaterThan, pgsql.OperatorGreaterThanOrEqualTo, pgsql.OperatorLessThan, pgsql.OperatorLessThanOrEqualTo, pgsql.OperatorIn, pgsql.OperatorJSONBFieldExists, - pgsql.OperatorLike, pgsql.OperatorILike, pgsql.OperatorPGArrayOverlap: + pgsql.OperatorLike, pgsql.OperatorILike, pgsql.OperatorPGArrayOverlap, pgsql.OperatorPGArrayLHSContainsRHS: return pgsql.Boolean, nil default: diff --git a/cypher/models/pgsql/translate/kind.go b/cypher/models/pgsql/translate/kind.go index cb2bf72..71ae610 100644 --- a/cypher/models/pgsql/translate/kind.go +++ b/cypher/models/pgsql/translate/kind.go @@ -17,7 +17,7 @@ func newPGKindIDMatcher(scope *Scope, treeTranslator *ExpressionTreeTranslator, treeTranslator.PushOperand(pgd.Column(binding.Identifier, pgsql.ColumnKindIDs)) treeTranslator.PushOperand(kindIDsLiteral) - return treeTranslator.CompleteBinaryExpression(scope, pgsql.OperatorPGArrayOverlap) + return treeTranslator.CompleteBinaryExpression(scope, pgsql.OperatorPGArrayLHSContainsRHS) case pgsql.EdgeComposite, pgsql.ExpansionEdge: treeTranslator.PushOperand(pgsql.CompoundIdentifier{binding.Identifier, pgsql.ColumnKindID}) diff --git a/cypher/models/pgsql/translate/node.go b/cypher/models/pgsql/translate/node.go index b585d2d..fcf3a20 100644 --- a/cypher/models/pgsql/translate/node.go +++ b/cypher/models/pgsql/translate/node.go @@ -54,7 +54,7 @@ func (s *Translator) translateNodePatternToStep(nodePattern *cypher.NodePattern, return err } else if err := s.treeTranslator.AddTranslationConstraint(pgsql.NewIdentifierSet().Add(bindingResult.Binding.Identifier), pgsql.NewBinaryExpression( pgsql.CompoundIdentifier{bindingResult.Binding.Identifier, pgsql.ColumnKindIDs}, - pgsql.OperatorPGArrayOverlap, + pgsql.OperatorPGArrayLHSContainsRHS, kindIDsLiteral, )); err != nil { return err