cypher-dsl 0.1.0.alpha.2 → 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 +15 -1
- data/README.md +14 -1
- data/lib/cypher_dsl/clauses/raw.rb +9 -0
- data/lib/cypher_dsl/condition.rb +1 -1
- data/lib/cypher_dsl/renderer/visitor/conditions.rb +74 -0
- data/lib/cypher_dsl/renderer/visitor/expressions.rb +0 -37
- data/lib/cypher_dsl/renderer/visitor/patterns.rb +1 -0
- data/lib/cypher_dsl/renderer/visitor.rb +7 -2
- data/lib/cypher_dsl/statement_builder.rb +14 -1
- data/lib/cypher_dsl/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a83e4601b0dc6294144bb3e5a87463cf322861e94927fc5ccfa7c14ab5389e0c
|
|
4
|
+
data.tar.gz: f7475774c957b6ad901caf376774ef2d09d97731874f3cd1b569aec141219089
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef77aec97883fc7445c7ecf759d4d6438c614818bb27cbd6c85fe239d9757a0508cf9f0e2348871a2b291f101756a453ccc5930e76eecb6ecb61db6120e4f8d9
|
|
7
|
+
data.tar.gz: 0a37e339fa998ae143fc5691a4c50b4d18f8aa322e21ca9f66065ee1d1005304603928cfbe9884bb16905cb55f870eb8bd6facc5cd19f6df2c82c10f48639340
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.0.alpha.3] - 2026-11-01
|
|
11
|
+
|
|
12
|
+
The release activegraph's `ActiveGraph::Core::Query` builds on.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Raw fragments for host DSLs built on strings (Ruby-only, no Java counterpart): `Cypher.raw`
|
|
17
|
+
is accepted as a pattern element in MATCH/CREATE/MERGE, as a condition (also as operand of
|
|
18
|
+
`and`/`or`/`xor`/`not`), as a complete SET / ON CREATE SET / ON MATCH SET operation, and
|
|
19
|
+
`StatementBuilder#raw_clause(format, *args)` renders a whole clause verbatim.
|
|
20
|
+
- Raw operands of a compound condition are parenthesised unless they already form one group,
|
|
21
|
+
so `Cypher.raw('a OR b').as_condition.and(c)` renders `((a OR b) AND c)`.
|
|
22
|
+
|
|
10
23
|
## [0.1.0.alpha.2] - 2026-11-01
|
|
11
24
|
|
|
12
25
|
Second pre-release. The whole upstream core test suite that applies to Ruby is now
|
|
@@ -79,6 +92,7 @@ collection, IssueRelatedIT, FunctionsIT).
|
|
|
79
92
|
- Renderer with dialects (Neo4j 4, 5, 5.23+), pretty printing, identifier escaping,
|
|
80
93
|
parameter collection (`Statement#catalog`).
|
|
81
94
|
|
|
82
|
-
[Unreleased]: https://github.com/neo4jrb/cypher-dsl/compare/v0.1.0.alpha.
|
|
95
|
+
[Unreleased]: https://github.com/neo4jrb/cypher-dsl/compare/v0.1.0.alpha.3...HEAD
|
|
96
|
+
[0.1.0.alpha.3]: https://github.com/neo4jrb/cypher-dsl/compare/v0.1.0.alpha.2...v0.1.0.alpha.3
|
|
83
97
|
[0.1.0.alpha.2]: https://github.com/neo4jrb/cypher-dsl/compare/v0.1.0.alpha.1...v0.1.0.alpha.2
|
|
84
98
|
[0.1.0.alpha.1]: https://github.com/neo4jrb/cypher-dsl/releases/tag/v0.1.0.alpha.1
|
data/README.md
CHANGED
|
@@ -75,6 +75,19 @@ Cypher.returning(Cypher.raw('($E + $E) * $factor', Cypher.parameter(:a), 2, Cyph
|
|
|
75
75
|
.build.cypher # => "RETURN ($a + 2) * $factor AS r"
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
A raw fragment is also accepted as a pattern element, as a condition, as a `SET`
|
|
79
|
+
operation, and `raw_clause` renders a whole clause verbatim. These are escape hatches
|
|
80
|
+
for DSLs that have to pass user-written Cypher through; a raw condition combined with
|
|
81
|
+
others is parenthesised so its precedence survives:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
Cypher.match(Cypher.raw('(n)-[:KNOWS]->(m)'))
|
|
85
|
+
.where(Cypher.property(:n, :age).gt(30).and(Cypher.raw('m.name STARTS WITH "A" OR m.vip')))
|
|
86
|
+
.raw_clause('CALL db.labels() YIELD label')
|
|
87
|
+
.returning(:n, :label).build.cypher
|
|
88
|
+
# => "MATCH (n)-[:KNOWS]->(m) WHERE (n.age > 30 AND (m.name STARTS WITH \"A\" OR m.vip)) CALL db.labels() YIELD label RETURN n, label"
|
|
89
|
+
```
|
|
90
|
+
|
|
78
91
|
Every expression and pattern answers `to_s` with its plain Cypher, e.g.
|
|
79
92
|
`Cypher.node('Person').named(:n).to_s # => "(n:Person)"`.
|
|
80
93
|
|
|
@@ -125,7 +138,7 @@ The flags are `entity_names`, `parameter_names`, `all_aliases`, `internal_aliase
|
|
|
125
138
|
Pre-releases are not picked up by a bare requirement, so name the version:
|
|
126
139
|
|
|
127
140
|
```ruby
|
|
128
|
-
gem 'cypher-dsl', '0.1.0.alpha.
|
|
141
|
+
gem 'cypher-dsl', '0.1.0.alpha.3'
|
|
129
142
|
```
|
|
130
143
|
|
|
131
144
|
Requires Ruby 3.4 or newer.
|
data/lib/cypher_dsl/condition.rb
CHANGED
|
@@ -10,7 +10,7 @@ module CypherDSL
|
|
|
10
10
|
case value
|
|
11
11
|
when Condition then value
|
|
12
12
|
when Relationship, RelationshipChain then RelationshipPatternCondition.new(value)
|
|
13
|
-
when FunctionInvocation then ExpressionCondition.new(value) #
|
|
13
|
+
when FunctionInvocation, RawExpression then ExpressionCondition.new(value) # isEmpty(), raw text
|
|
14
14
|
else raise ArgumentError, "#{value.inspect} is not a condition"
|
|
15
15
|
end
|
|
16
16
|
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
|
|
@@ -250,43 +250,6 @@ module CypherDSL
|
|
|
250
250
|
"#{opening}#{break_line(@level + 1)}#{entries.join(",#{break_line(@level + 1)}")}#{break_line}#{closing}"
|
|
251
251
|
end
|
|
252
252
|
|
|
253
|
-
def render_comparison(comparison)
|
|
254
|
-
left = render_operand(comparison.left)
|
|
255
|
-
return "#{left} #{comparison.operator}" if comparison.unary?
|
|
256
|
-
|
|
257
|
-
"#{left} #{comparison.operator} #{render_operand(comparison.right)}"
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
# A comparison nested inside another comparison is parenthesised: `(1 > 2) = true`.
|
|
261
|
-
def render_operand(expression)
|
|
262
|
-
rendered = render_expression(expression)
|
|
263
|
-
expression.is_a?(Comparison) ? "(#{rendered})" : rendered
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
# Pretty printed, each further operand starts an indented line: `(a\n AND b)`.
|
|
267
|
-
# `exists(n.prop)` on Neo4j 4, `n.prop IS NOT NULL` afterwards.
|
|
268
|
-
def render_property_exists(condition, negated: false)
|
|
269
|
-
property = render_expression(condition.property)
|
|
270
|
-
return "exists(#{property})" if @configuration.legacy_functions?
|
|
271
|
-
|
|
272
|
-
"#{property} IS #{'NOT ' unless negated}NULL"
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def render_compound_condition(condition)
|
|
276
|
-
if condition.negation?
|
|
277
|
-
negated = condition.conditions.first
|
|
278
|
-
if negated.is_a?(PropertyExistsCondition) && !@configuration.legacy_functions?
|
|
279
|
-
return render_property_exists(negated, negated: true)
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
rendered = render_expression(negated)
|
|
283
|
-
return negated.is_a?(RelationshipPatternCondition) ? "NOT #{rendered}" : "NOT (#{rendered})"
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
operands = nested { condition.conditions.map { |inner| render_expression(inner) } }
|
|
287
|
-
"(#{operands.join("#{break_line(@level + 1)}#{condition.operator} ")})"
|
|
288
|
-
end
|
|
289
|
-
|
|
290
253
|
def render_function_invocation(invocation)
|
|
291
254
|
arguments = invocation.arguments.map do |argument|
|
|
292
255
|
rendered = render_expression(argument)
|
|
@@ -15,6 +15,7 @@ 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
|
|
@@ -8,6 +8,7 @@ module CypherDSL
|
|
|
8
8
|
class Visitor
|
|
9
9
|
include Patterns
|
|
10
10
|
include Expressions
|
|
11
|
+
include Conditions
|
|
11
12
|
include LabelRendering
|
|
12
13
|
|
|
13
14
|
def initialize(configuration)
|
|
@@ -117,18 +118,22 @@ module CypherDSL
|
|
|
117
118
|
when Clauses::Set then "SET #{render_operations(clause.operations)}"
|
|
118
119
|
when Clauses::Remove then "REMOVE #{render_operations(clause.items)}"
|
|
119
120
|
when Clauses::Delete then render_delete(clause)
|
|
120
|
-
when Clauses::Unwind
|
|
121
|
-
"UNWIND #{render_expression(clause.expression)} AS #{render_expression(clause.variable)}"
|
|
121
|
+
when Clauses::Unwind then render_unwind(clause)
|
|
122
122
|
when Clauses::Finish then 'FINISH'
|
|
123
123
|
when Clauses::Foreach then render_foreach(clause)
|
|
124
124
|
when Clauses::Call then render_call(clause)
|
|
125
125
|
when Clauses::CallSubquery then render_call_subquery(clause)
|
|
126
126
|
when Clauses::LoadCsv then render_load_csv(clause)
|
|
127
127
|
when Clauses::Use then "USE #{clause.graph.is_a?(String) ? clause.graph : render_expression(clause.graph)}"
|
|
128
|
+
when Clauses::Raw then render_raw(clause.expression)
|
|
128
129
|
else raise ArgumentError, "Cannot render clause #{clause.inspect}"
|
|
129
130
|
end
|
|
130
131
|
end
|
|
131
132
|
|
|
133
|
+
def render_unwind(clause)
|
|
134
|
+
"UNWIND #{render_expression(clause.expression)} AS #{render_expression(clause.variable)}"
|
|
135
|
+
end
|
|
136
|
+
|
|
132
137
|
def render_load_csv(clause)
|
|
133
138
|
parts = []
|
|
134
139
|
if clause.periodic_commit
|
|
@@ -177,6 +177,18 @@ module CypherDSL
|
|
|
177
177
|
# The statement built so far as an `EXISTS { … }` condition.
|
|
178
178
|
def as_condition = ExistsCondition.new(build)
|
|
179
179
|
|
|
180
|
+
# --- escape hatch ----------------------------------------------------
|
|
181
|
+
|
|
182
|
+
# A clause rendered verbatim, for Cypher the builder does not model, e.g.
|
|
183
|
+
# `raw_clause('CALL db.labels() YIELD label')`. Takes the arguments of
|
|
184
|
+
# `Cypher.raw` (a format with `$E` placeholders) or a ready {RawExpression}.
|
|
185
|
+
# The next clause starts afresh: a raw clause cannot be refined with WHERE,
|
|
186
|
+
# ORDER BY, SKIP or LIMIT.
|
|
187
|
+
def raw_clause(format, *)
|
|
188
|
+
expression = format.is_a?(RawExpression) ? format : RawExpression.create(format, *)
|
|
189
|
+
append(Clauses::Raw.new(expression:), nil)
|
|
190
|
+
end
|
|
191
|
+
|
|
180
192
|
# --- finishing -------------------------------------------------------
|
|
181
193
|
|
|
182
194
|
# Continues with the clauses of another statement, e.g. after a `CALL … YIELD`.
|
|
@@ -283,7 +295,8 @@ module CypherDSL
|
|
|
283
295
|
arguments.each_slice(2).map { |target, value| Operation.new(target, '=', Expression.coerce(value)) }.freeze
|
|
284
296
|
end
|
|
285
297
|
|
|
286
|
-
|
|
298
|
+
# A raw fragment such as `Cypher.raw("n.name = 'x'")` counts as a complete operation.
|
|
299
|
+
def operation?(argument) = [Operation, LabelOperation, RawExpression].any? { |type| argument.is_a?(type) }
|
|
287
300
|
|
|
288
301
|
def label_operation?(arguments)
|
|
289
302
|
arguments.size > 1 && arguments.first.is_a?(Node) &&
|
data/lib/cypher_dsl/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cypher-dsl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.alpha.
|
|
4
|
+
version: 0.1.0.alpha.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Heinrich Klobuczek
|
|
@@ -50,6 +50,7 @@ files:
|
|
|
50
50
|
- lib/cypher_dsl/clauses/match.rb
|
|
51
51
|
- lib/cypher_dsl/clauses/merge.rb
|
|
52
52
|
- lib/cypher_dsl/clauses/order.rb
|
|
53
|
+
- lib/cypher_dsl/clauses/raw.rb
|
|
53
54
|
- lib/cypher_dsl/clauses/raw_return.rb
|
|
54
55
|
- lib/cypher_dsl/clauses/remove.rb
|
|
55
56
|
- lib/cypher_dsl/clauses/return.rb
|
|
@@ -104,6 +105,7 @@ files:
|
|
|
104
105
|
- lib/cypher_dsl/renderer/generated_name_resolver.rb
|
|
105
106
|
- lib/cypher_dsl/renderer/name_resolver.rb
|
|
106
107
|
- lib/cypher_dsl/renderer/visitor.rb
|
|
108
|
+
- lib/cypher_dsl/renderer/visitor/conditions.rb
|
|
107
109
|
- lib/cypher_dsl/renderer/visitor/expressions.rb
|
|
108
110
|
- lib/cypher_dsl/renderer/visitor/label_rendering.rb
|
|
109
111
|
- lib/cypher_dsl/renderer/visitor/patterns.rb
|