cypher-dsl 0.1.0.alpha.1 → 0.1.0.alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -1
- data/README.md +66 -4
- data/lib/cypher_dsl/clauses/load_csv.rb +8 -2
- data/lib/cypher_dsl/clauses/match.rb +2 -2
- data/lib/cypher_dsl/clauses/raw.rb +9 -0
- data/lib/cypher_dsl/clauses.rb +11 -0
- data/lib/cypher_dsl/comparison.rb +6 -0
- data/lib/cypher_dsl/condition.rb +1 -1
- data/lib/cypher_dsl/count_expression.rb +7 -3
- data/lib/cypher_dsl/cypher.rb +80 -19
- data/lib/cypher_dsl/expression.rb +3 -0
- data/lib/cypher_dsl/function_invocation.rb +7 -0
- data/lib/cypher_dsl/functions.rb +11 -1
- data/lib/cypher_dsl/has_label_condition.rb +1 -1
- data/lib/cypher_dsl/hint.rb +71 -0
- data/lib/cypher_dsl/label_operation.rb +11 -3
- data/lib/cypher_dsl/labels.rb +66 -0
- data/lib/cypher_dsl/literal.rb +24 -2
- data/lib/cypher_dsl/named_path.rb +6 -3
- data/lib/cypher_dsl/node.rb +15 -4
- data/lib/cypher_dsl/property.rb +9 -2
- data/lib/cypher_dsl/property_exists_condition.rb +15 -0
- data/lib/cypher_dsl/raw_expression.rb +76 -3
- data/lib/cypher_dsl/relationship.rb +8 -1
- data/lib/cypher_dsl/relationship_chain.rb +3 -0
- data/lib/cypher_dsl/renderer/configuration.rb +61 -4
- data/lib/cypher_dsl/renderer/dialect.rb +24 -7
- data/lib/cypher_dsl/renderer/escaper.rb +10 -7
- data/lib/cypher_dsl/renderer/generated_name_resolver.rb +110 -0
- data/lib/cypher_dsl/renderer/name_resolver.rb +35 -0
- data/lib/cypher_dsl/renderer/visitor/conditions.rb +74 -0
- data/lib/cypher_dsl/renderer/visitor/expressions.rb +40 -40
- data/lib/cypher_dsl/renderer/visitor/label_rendering.rb +84 -0
- data/lib/cypher_dsl/renderer/visitor/patterns.rb +18 -29
- data/lib/cypher_dsl/renderer/visitor.rb +106 -29
- data/lib/cypher_dsl/renderer.rb +11 -5
- data/lib/cypher_dsl/search.rb +91 -0
- data/lib/cypher_dsl/standalone_call.rb +0 -6
- data/lib/cypher_dsl/statement.rb +4 -0
- data/lib/cypher_dsl/statement_builder.rb +81 -15
- data/lib/cypher_dsl/statement_catalog.rb +40 -6
- data/lib/cypher_dsl/subquery_with.rb +1 -0
- data/lib/cypher_dsl/symbolic_name.rb +1 -1
- data/lib/cypher_dsl/using_periodic_commit.rb +11 -0
- data/lib/cypher_dsl/vector_literal.rb +41 -0
- data/lib/cypher_dsl/version.rb +1 -1
- metadata +13 -3
- data/lib/cypher_dsl/label_expression.rb +0 -40
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
class Renderer
|
|
5
|
+
# Resolves symbolic names, aliases and parameter names while rendering.
|
|
6
|
+
# The base strategy keeps the given names and invents `hint001`-style names
|
|
7
|
+
# for elements that were referenced without ever being named.
|
|
8
|
+
class NameResolver
|
|
9
|
+
def initialize
|
|
10
|
+
@context_names = {}.compare_by_identity
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resolve(symbolic_name, in_entity: false) # rubocop:disable Lint/UnusedMethodArgument
|
|
14
|
+
context_name(symbolic_name)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def resolve_alias(aliased, define: false, in_last_return: false) # rubocop:disable Lint/UnusedMethodArgument
|
|
18
|
+
context_name(aliased.alias_name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def resolve_parameter(parameter) = parameter.name
|
|
22
|
+
|
|
23
|
+
def enter_scope(imports); end
|
|
24
|
+
def leave_scope(exports); end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def context_name(symbolic_name)
|
|
29
|
+
return symbolic_name.value if symbolic_name.resolved?
|
|
30
|
+
|
|
31
|
+
@context_names[symbolic_name] ||= "#{symbolic_name.hint}#{format('%03d', @context_names.size + 1)}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
class Renderer
|
|
5
|
+
class Visitor
|
|
6
|
+
# Rendering of comparisons and compound conditions.
|
|
7
|
+
module Conditions
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def render_comparison(comparison)
|
|
11
|
+
left = render_operand(comparison.left)
|
|
12
|
+
return "#{left} #{comparison.operator}" if comparison.unary?
|
|
13
|
+
|
|
14
|
+
"#{left} #{comparison.operator} #{render_operand(comparison.right)}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# A comparison nested inside another comparison is parenthesised: `(1 > 2) = true`.
|
|
18
|
+
def render_operand(expression)
|
|
19
|
+
rendered = render_expression(expression)
|
|
20
|
+
expression.is_a?(Comparison) ? "(#{rendered})" : rendered
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# `exists(n.prop)` on Neo4j 4, `n.prop IS NOT NULL` afterwards.
|
|
24
|
+
def render_property_exists(condition, negated: false)
|
|
25
|
+
property = render_expression(condition.property)
|
|
26
|
+
return "exists(#{property})" if @configuration.legacy_functions?
|
|
27
|
+
|
|
28
|
+
"#{property} IS #{'NOT ' unless negated}NULL"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Pretty printed, each further operand starts an indented line: `(a\n AND b)`.
|
|
32
|
+
def render_compound_condition(condition)
|
|
33
|
+
if condition.negation?
|
|
34
|
+
negated = condition.conditions.first
|
|
35
|
+
if negated.is_a?(PropertyExistsCondition) && !@configuration.legacy_functions?
|
|
36
|
+
return render_property_exists(negated, negated: true)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
rendered = render_expression(negated)
|
|
40
|
+
return negated.is_a?(RelationshipPatternCondition) ? "NOT #{rendered}" : "NOT (#{rendered})"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
operands = nested { condition.conditions.map { |inner| render_compound_operand(inner) } }
|
|
44
|
+
"(#{operands.join("#{break_line(@level + 1)}#{condition.operator} ")})"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A raw operand is parenthesised unless it already is one group, so that
|
|
48
|
+
# `Cypher.raw('a OR b').as_condition.and(c)` keeps its precedence. (Ruby addition; the
|
|
49
|
+
# Java renderer trusts raw text.)
|
|
50
|
+
def render_compound_operand(condition)
|
|
51
|
+
rendered = render_expression(condition)
|
|
52
|
+
raw_condition?(condition) && !parenthesised?(rendered) ? "(#{rendered})" : rendered
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def raw_condition?(condition)
|
|
56
|
+
condition.is_a?(ExpressionCondition) && condition.expression.is_a?(RawExpression)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# True when the text is one parenthesised group: `(a OR b)` but not `(a) OR (b)`.
|
|
60
|
+
def parenthesised?(text)
|
|
61
|
+
return false unless text.start_with?('(') && text.end_with?(')')
|
|
62
|
+
|
|
63
|
+
depth = 0
|
|
64
|
+
text.each_char.with_index do |char, index|
|
|
65
|
+
depth += 1 if char == '('
|
|
66
|
+
depth -= 1 if char == ')'
|
|
67
|
+
return false if depth.zero? && index < text.size - 1
|
|
68
|
+
end
|
|
69
|
+
true
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -17,7 +17,7 @@ module CypherDSL
|
|
|
17
17
|
when Asterisk then '*'
|
|
18
18
|
when AliasedExpression then render_aliased_reference(expression)
|
|
19
19
|
when NamedPath then render_expression(expression.symbolic_name)
|
|
20
|
-
when RawExpression then expression
|
|
20
|
+
when RawExpression then render_raw(expression)
|
|
21
21
|
when CollectExpression
|
|
22
22
|
block('COLLECT {', '}') { render_subquery(expression.statement, expression.imports) }
|
|
23
23
|
when CountExpression then render_count(expression)
|
|
@@ -60,6 +60,7 @@ module CypherDSL
|
|
|
60
60
|
when LabelOperation then render_labels(expression.node, expression.labels)
|
|
61
61
|
when Reduction then render_reduction(expression)
|
|
62
62
|
when PatternExpression then render_pattern_expression(expression.pattern)
|
|
63
|
+
when VectorLiteral then expression.as_string
|
|
63
64
|
else render_condition_or_operation(expression)
|
|
64
65
|
end
|
|
65
66
|
end
|
|
@@ -71,7 +72,7 @@ module CypherDSL
|
|
|
71
72
|
end
|
|
72
73
|
|
|
73
74
|
def render_labels(node, labels)
|
|
74
|
-
"#{render_expression(node)}#{labels.map { |label|
|
|
75
|
+
"#{render_expression(node)}#{labels.map { |label| render_label(label) }.join}"
|
|
75
76
|
end
|
|
76
77
|
|
|
77
78
|
def render_condition_or_operation(expression)
|
|
@@ -81,6 +82,7 @@ module CypherDSL
|
|
|
81
82
|
when ConstantCondition then expression.value.to_s
|
|
82
83
|
when ExpressionCondition then render_expression(expression.expression)
|
|
83
84
|
when ExistsCondition then render_exists(expression)
|
|
85
|
+
when PropertyExistsCondition then render_property_exists(expression)
|
|
84
86
|
when RelationshipPatternCondition then render_pattern_expression(expression.pattern)
|
|
85
87
|
when ListPredicate then render_list_predicate(expression)
|
|
86
88
|
when HasLabelCondition then render_labels(expression.node, expression.labels)
|
|
@@ -109,18 +111,29 @@ module CypherDSL
|
|
|
109
111
|
"exists(#{render_pattern_expression(body)})"
|
|
110
112
|
end
|
|
111
113
|
|
|
114
|
+
# A pattern body renders bare; with imports it needs a MATCH after the WITH.
|
|
112
115
|
def render_count(count)
|
|
113
116
|
body = count.body
|
|
114
|
-
return block('COUNT {', '}') { render_subquery(body, count.imports) }
|
|
117
|
+
return block('COUNT {', '}') { render_subquery(body, count.imports) } unless pattern_body?(body)
|
|
115
118
|
|
|
116
|
-
|
|
119
|
+
rendered = render_in_own_scope do
|
|
120
|
+
imports = count.imports.map { |import| render_projection_item(import) }
|
|
121
|
+
parts = [("WITH #{imports.join(', ')} MATCH" unless imports.empty?), render_pattern_element(body)]
|
|
122
|
+
parts << "WHERE #{render_expression(count.where_condition)}" if count.where_condition
|
|
123
|
+
parts.compact.join(' ')
|
|
124
|
+
end
|
|
125
|
+
"COUNT { #{rendered} }"
|
|
117
126
|
end
|
|
118
127
|
|
|
128
|
+
def pattern_body?(body) = !(body.is_a?(Statement) || body.is_a?(UnionQuery))
|
|
129
|
+
|
|
119
130
|
# Renders with the current scope, discarding names introduced inside.
|
|
120
131
|
def render_in_own_scope
|
|
121
132
|
outer_scope = @visited_names.dup
|
|
133
|
+
@names.enter_scope(:all)
|
|
122
134
|
yield
|
|
123
135
|
ensure
|
|
136
|
+
@names.leave_scope([])
|
|
124
137
|
@visited_names = outer_scope
|
|
125
138
|
end
|
|
126
139
|
|
|
@@ -166,13 +179,13 @@ module CypherDSL
|
|
|
166
179
|
# A pattern comprehension is its own scope: names it introduces do not
|
|
167
180
|
# count as visited outside of it.
|
|
168
181
|
def render_pattern_comprehension(comprehension)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
182
|
+
render_in_own_scope do
|
|
183
|
+
parts = [render_pattern_element(comprehension.pattern)]
|
|
184
|
+
parts << "WHERE #{render_expression(comprehension.where_condition)}" if comprehension.where_condition
|
|
185
|
+
projection = comprehension.projection.map { |expression| render_expression(expression) }
|
|
186
|
+
parts << "| #{projection.size == 1 ? projection.first : "[#{projection.join(', ')}]"}"
|
|
187
|
+
"[#{parts.join(' ')}]"
|
|
188
|
+
end
|
|
176
189
|
end
|
|
177
190
|
|
|
178
191
|
def render_list_comprehension(comprehension)
|
|
@@ -185,7 +198,8 @@ module CypherDSL
|
|
|
185
198
|
# Records every use for the catalog; anonymous parameters get `pcdslNN`,
|
|
186
199
|
# shared between anonymous parameters with equal values.
|
|
187
200
|
def render_parameter(parameter)
|
|
188
|
-
name = parameter.anonymous? ? anonymous_parameter_name(parameter) : parameter
|
|
201
|
+
name = parameter.anonymous? ? anonymous_parameter_name(parameter) : @names.resolve_parameter(parameter)
|
|
202
|
+
@renamed_parameters[parameter.name] = name if !parameter.anonymous? && name != parameter.name
|
|
189
203
|
(@parameter_uses[name] ||= []) << parameter.value
|
|
190
204
|
"$#{Escaper.escape_parameter_name(name)}"
|
|
191
205
|
end
|
|
@@ -206,11 +220,18 @@ module CypherDSL
|
|
|
206
220
|
"#{render_expression(property.container)}#{lookups.join}"
|
|
207
221
|
end
|
|
208
222
|
|
|
223
|
+
# Verbatim text with its embedded expressions rendered (and their parameters collected).
|
|
224
|
+
def render_raw(raw)
|
|
225
|
+
raw.parts.map { |part| part.is_a?(String) ? part : render_expression(part) }.join
|
|
226
|
+
end
|
|
227
|
+
|
|
209
228
|
def render_literal(value)
|
|
210
229
|
case value
|
|
211
|
-
when String then
|
|
230
|
+
when String then Literal.quote(value)
|
|
212
231
|
when nil then 'NULL'
|
|
213
232
|
when true, false, Numeric then value.to_s
|
|
233
|
+
when DateTime, Time then "datetime('#{value.iso8601}')"
|
|
234
|
+
when Date then "date('#{value.iso8601}')"
|
|
214
235
|
else raise ArgumentError, "Cannot render literal #{value.inspect}"
|
|
215
236
|
end
|
|
216
237
|
end
|
|
@@ -229,35 +250,14 @@ module CypherDSL
|
|
|
229
250
|
"#{opening}#{break_line(@level + 1)}#{entries.join(",#{break_line(@level + 1)}")}#{break_line}#{closing}"
|
|
230
251
|
end
|
|
231
252
|
|
|
232
|
-
def render_comparison(comparison)
|
|
233
|
-
left = render_operand(comparison.left)
|
|
234
|
-
return "#{left} #{comparison.operator}" if comparison.unary?
|
|
235
|
-
|
|
236
|
-
"#{left} #{comparison.operator} #{render_operand(comparison.right)}"
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
# A comparison nested inside another comparison is parenthesised: `(1 > 2) = true`.
|
|
240
|
-
def render_operand(expression)
|
|
241
|
-
rendered = render_expression(expression)
|
|
242
|
-
expression.is_a?(Comparison) ? "(#{rendered})" : rendered
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
# Pretty printed, each further operand starts an indented line: `(a\n AND b)`.
|
|
246
|
-
def render_compound_condition(condition)
|
|
247
|
-
if condition.negation?
|
|
248
|
-
negated = condition.conditions.first
|
|
249
|
-
rendered = render_expression(negated)
|
|
250
|
-
return negated.is_a?(RelationshipPatternCondition) ? "NOT #{rendered}" : "NOT (#{rendered})"
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
operands = nested { condition.conditions.map { |inner| render_expression(inner) } }
|
|
254
|
-
"(#{operands.join("#{break_line(@level + 1)}#{condition.operator} ")})"
|
|
255
|
-
end
|
|
256
|
-
|
|
257
253
|
def render_function_invocation(invocation)
|
|
258
|
-
arguments = invocation.arguments.map
|
|
254
|
+
arguments = invocation.arguments.map do |argument|
|
|
255
|
+
rendered = render_expression(argument)
|
|
256
|
+
# Pretty printed, a map argument keeps a space after the parenthesis: `collect( {`.
|
|
257
|
+
pretty? && rendered.start_with?('{') ? " #{rendered}" : rendered
|
|
258
|
+
end
|
|
259
259
|
name = invocation.function_name
|
|
260
|
-
legacy = @configuration.
|
|
260
|
+
legacy = @configuration.legacy_functions?
|
|
261
261
|
# Neo4j 4 has no elementId(); the closest equivalent is the stringified internal id.
|
|
262
262
|
return "toString(id(#{arguments.join(', ')}))" if name == 'elementId' && legacy
|
|
263
263
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
class Renderer
|
|
5
|
+
class Visitor
|
|
6
|
+
# Rendering of node labels: plain names and {Labels} expressions, dynamic
|
|
7
|
+
# (`:$($labels)`) where the dialect allows it, static otherwise.
|
|
8
|
+
module LabelRendering
|
|
9
|
+
STATIC_MESSAGE = 'Cannot render the given Labels in a Cypher pre 5.26 compatible way'
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# One label entry including its leading colon.
|
|
14
|
+
def render_label(label)
|
|
15
|
+
label.is_a?(Labels) ? render_labels_expression(label) : ":#{escape_schema_name(label)}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def render_labels_expression(labels)
|
|
19
|
+
return render_static_labels(labels) if !@configuration.dynamic_labels? && static_renderable?(labels)
|
|
20
|
+
return '' if labels.empty?
|
|
21
|
+
|
|
22
|
+
":#{render_labels_tree(labels, nil)}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def static_renderable?(labels)
|
|
26
|
+
labels.lhs.nil? && labels.rhs.nil? && %i[leaf colon_conjunction].include?(labels.type)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Pre-5.26: the label names are taken from literals and parameter values and rendered as `:A:B`.
|
|
30
|
+
def render_static_labels(labels)
|
|
31
|
+
names = labels.values.flat_map { |value| static_label_names(value.value) }
|
|
32
|
+
names.map { |name| ":#{escape_schema_name(name)}" }.join
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def static_label_names(value)
|
|
36
|
+
case value
|
|
37
|
+
when String then [value]
|
|
38
|
+
when Parameter then value.value? ? static_label_names(value.value) : raise(ArgumentError, STATIC_MESSAGE)
|
|
39
|
+
when ListExpression then value.elements.flat_map { |element| static_label_names(element) }
|
|
40
|
+
when Literal then value.value.is_a?(String) ? [value.value] : raise(ArgumentError, STATIC_MESSAGE)
|
|
41
|
+
when Array then value.flat_map { |item| static_label_names(item) }
|
|
42
|
+
else raise ArgumentError, STATIC_MESSAGE
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Mirrors the Java renderer's bracketing rules for `&`, `|` and `!`.
|
|
47
|
+
def render_labels_tree(labels, parent)
|
|
48
|
+
current = labels.type
|
|
49
|
+
close = false
|
|
50
|
+
unless labels.leaf?
|
|
51
|
+
close = (!parent.nil? || labels.negated?) && current != parent
|
|
52
|
+
close = false if close && !labels.negated? && (current == :conjunction || parent == :disjunction)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
parts = []
|
|
56
|
+
parts << '!' if labels.negated?
|
|
57
|
+
parts << '(' if close
|
|
58
|
+
parts << render_labels_tree(labels.lhs, current) if labels.lhs
|
|
59
|
+
parts << render_label_values(labels, current)
|
|
60
|
+
parts << render_labels_tree(labels.rhs, current) if labels.rhs
|
|
61
|
+
parts << ')' if close
|
|
62
|
+
parts.join
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def render_label_values(labels, current)
|
|
66
|
+
return labels.values.map { |value| render_label_value(value) }.join if labels.leaf?
|
|
67
|
+
if %i[colon_conjunction colon_disjunction].include?(current) && labels.values.any?
|
|
68
|
+
return labels.values.map { |value| render_label_value(value) }.join(':')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
labels.empty? ? '' : Labels::OPERATORS[current]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render_label_value(value)
|
|
75
|
+
case value.modifier
|
|
76
|
+
when :static then escape_schema_name(value.value)
|
|
77
|
+
when :all then "$(#{render_expression(value.value)})"
|
|
78
|
+
when :any then "$any(#{render_expression(value.value)})"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -15,13 +15,21 @@ module CypherDSL
|
|
|
15
15
|
when RelationshipChain then render_chain(element)
|
|
16
16
|
when NamedPath then render_named_path(element)
|
|
17
17
|
when QuantifiedPathPattern then render_quantified_path_pattern(element)
|
|
18
|
+
when RawExpression then render_raw(element) # `Cypher.raw('(n)-->(m)')` as a pattern
|
|
18
19
|
else raise ArgumentError, "Cannot render pattern element #{element.inspect}"
|
|
19
20
|
end
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
# `p = SHORTEST 1 pattern`, or `p = shortestPath(pattern)` on dialects before Neo4j 5.21.
|
|
22
24
|
def render_named_path(path)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
name = render_expression(path.symbolic_name)
|
|
26
|
+
pattern = render_pattern_element(path.pattern)
|
|
27
|
+
selector = path.selector
|
|
28
|
+
if selector&.legacy_function && @configuration.legacy_path_selectors?
|
|
29
|
+
"#{name} = #{selector.legacy_function}(#{pattern})"
|
|
30
|
+
else
|
|
31
|
+
"#{name} = #{"#{selector.keyword} " if selector}#{pattern}"
|
|
32
|
+
end
|
|
25
33
|
end
|
|
26
34
|
|
|
27
35
|
def render_quantified_path_pattern(quantified)
|
|
@@ -41,35 +49,15 @@ module CypherDSL
|
|
|
41
49
|
# Inside a pattern expression (a pattern used as a condition) no new
|
|
42
50
|
# variables may be introduced, so an unknown name is left out.
|
|
43
51
|
def render_node(node)
|
|
44
|
-
name = node.symbolic_name && resolve_name(node.symbolic_name)
|
|
45
|
-
return "(#{name})" if name && @visited_names.include?(name)
|
|
52
|
+
name = node.symbolic_name && resolve_name(node.symbolic_name, in_entity: true)
|
|
53
|
+
return "(#{Escaper.escape_if_necessary(name)})" if name && @visited_names.include?(name)
|
|
46
54
|
|
|
47
55
|
@visited_names << name if name && !@in_pattern_expression
|
|
48
56
|
name = nil if @in_pattern_expression
|
|
49
|
-
labels = node.labels.map { |label|
|
|
57
|
+
labels = node.labels.map { |label| render_label(label) }.join
|
|
50
58
|
properties = node.properties && " #{render_expression(node.properties)}"
|
|
51
59
|
where = node.where_condition && " WHERE #{render_expression(node.where_condition)}"
|
|
52
|
-
"(#{name}#{labels}#{properties}#{where})"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def render_label(label)
|
|
56
|
-
label.is_a?(LabelExpression) ? render_label_expression(label) : escape_schema_name(label)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# `A|B`, `A&B`, `!A`; operands of another kind are parenthesised.
|
|
60
|
-
def render_label_expression(expression)
|
|
61
|
-
case expression.type
|
|
62
|
-
when :leaf then escape_schema_name(expression.value)
|
|
63
|
-
when :negation then "!#{render_label_operand(expression.operands.first, expression)}"
|
|
64
|
-
else
|
|
65
|
-
operator = expression.type == :conjunction ? '&' : '|'
|
|
66
|
-
expression.operands.map { |operand| render_label_operand(operand, expression) }.join(operator)
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def render_label_operand(operand, parent)
|
|
71
|
-
rendered = render_label_expression(operand)
|
|
72
|
-
operand.leaf? || operand.type == parent.type || operand.type == :negation ? rendered : "(#{rendered})"
|
|
60
|
+
"(#{name && Escaper.escape_if_necessary(name)}#{labels}#{properties}#{where})"
|
|
73
61
|
end
|
|
74
62
|
|
|
75
63
|
# A pattern used as a condition: known names render bare, unknown ones are dropped.
|
|
@@ -83,9 +71,9 @@ module CypherDSL
|
|
|
83
71
|
# Like nodes, a named relationship renders in full once and as `[name]` afterwards.
|
|
84
72
|
def render_relationship_details(relationship)
|
|
85
73
|
details = relationship.details
|
|
86
|
-
name = relationship.symbolic_name && resolve_name(relationship.symbolic_name)
|
|
74
|
+
name = relationship.symbolic_name && resolve_name(relationship.symbolic_name, in_entity: true)
|
|
87
75
|
inner = if name && @visited_names.include?(name)
|
|
88
|
-
"[#{name}]"
|
|
76
|
+
"[#{Escaper.escape_if_necessary(name)}]"
|
|
89
77
|
else
|
|
90
78
|
@visited_names << name if name && !@in_pattern_expression
|
|
91
79
|
content = render_details_content(relationship)
|
|
@@ -103,7 +91,8 @@ module CypherDSL
|
|
|
103
91
|
def render_details_content(relationship)
|
|
104
92
|
details = relationship.details
|
|
105
93
|
types = details.types.map { |type| escape_schema_name(type) }.join('|')
|
|
106
|
-
|
|
94
|
+
named = relationship.symbolic_name && !@in_pattern_expression
|
|
95
|
+
name = named ? Escaper.escape_if_necessary(resolve_name(relationship.symbolic_name, in_entity: true)) : nil
|
|
107
96
|
[
|
|
108
97
|
name,
|
|
109
98
|
(":#{types}" unless types.empty?),
|