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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5430e442ecb75b449cefb44e0b4d1e06ccdc80ca8a4dc10a7f3da2366a8cb049
4
- data.tar.gz: 393b61210b500da83e53ea17706bc8e8e691ed3f813f69c1557c85a6be86e182
3
+ metadata.gz: a83e4601b0dc6294144bb3e5a87463cf322861e94927fc5ccfa7c14ab5389e0c
4
+ data.tar.gz: f7475774c957b6ad901caf376774ef2d09d97731874f3cd1b569aec141219089
5
5
  SHA512:
6
- metadata.gz: 15d7b9bedab4f4d8072928d4d8dd207b3ab3fa4e7b4dc8c98422af314806cc5b00441a478b672640f626e4e2b0320202323990c710f0f6a1eac3c9abb52c8c8f
7
- data.tar.gz: d0634d9fc01f6375253637d9bdd4e94a1357dce52ab0be19188c8aed5fcecadd56932e0d88a60ce65a6b7e5f721be3161ce0e7c91b9f4ce7d3b529bfa6a94ea7
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.2...HEAD
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.2'
141
+ gem 'cypher-dsl', '0.1.0.alpha.3'
129
142
  ```
130
143
 
131
144
  Requires Ruby 3.4 or newer.
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CypherDSL
4
+ module Clauses
5
+ # A clause rendered verbatim from a {RawExpression}: the escape hatch for
6
+ # Cypher the builder does not model (`raw_clause('CALL db.labels()')`).
7
+ Raw = Data.define(:expression)
8
+ end
9
+ end
@@ -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) # boolean functions such as isEmpty()
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
- def operation?(argument) = argument.is_a?(Operation) || argument.is_a?(LabelOperation)
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) &&
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CypherDSL
4
- VERSION = '0.1.0.alpha.2'
4
+ VERSION = '0.1.0.alpha.3'
5
5
  end
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.2
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