cypher-dsl 0.1.0.alpha.1
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 +7 -0
- data/CHANGELOG.md +32 -0
- data/LICENSE.txt +21 -0
- data/NOTICE +14 -0
- data/README.md +87 -0
- data/lib/cypher_dsl/aliased_expression.rb +16 -0
- data/lib/cypher_dsl/asterisk.rb +10 -0
- data/lib/cypher_dsl/case_expression.rb +37 -0
- data/lib/cypher_dsl/clauses/call.rb +8 -0
- data/lib/cypher_dsl/clauses/call_subquery.rb +9 -0
- data/lib/cypher_dsl/clauses/create.rb +8 -0
- data/lib/cypher_dsl/clauses/delete.rb +8 -0
- data/lib/cypher_dsl/clauses/finish.rb +8 -0
- data/lib/cypher_dsl/clauses/foreach.rb +8 -0
- data/lib/cypher_dsl/clauses/load_csv.rb +8 -0
- data/lib/cypher_dsl/clauses/match.rb +8 -0
- data/lib/cypher_dsl/clauses/merge.rb +8 -0
- data/lib/cypher_dsl/clauses/order.rb +8 -0
- data/lib/cypher_dsl/clauses/raw_return.rb +8 -0
- data/lib/cypher_dsl/clauses/remove.rb +8 -0
- data/lib/cypher_dsl/clauses/return.rb +8 -0
- data/lib/cypher_dsl/clauses/set.rb +8 -0
- data/lib/cypher_dsl/clauses/unwind.rb +8 -0
- data/lib/cypher_dsl/clauses/use.rb +8 -0
- data/lib/cypher_dsl/clauses/with.rb +8 -0
- data/lib/cypher_dsl/clauses.rb +25 -0
- data/lib/cypher_dsl/collect_expression.rb +28 -0
- data/lib/cypher_dsl/comparison.rb +27 -0
- data/lib/cypher_dsl/compound_condition.rb +34 -0
- data/lib/cypher_dsl/condition.rb +35 -0
- data/lib/cypher_dsl/conflicting_parameters_exception.rb +15 -0
- data/lib/cypher_dsl/constant_condition.rb +17 -0
- data/lib/cypher_dsl/count_expression.rb +16 -0
- data/lib/cypher_dsl/cypher.rb +210 -0
- data/lib/cypher_dsl/exists_condition.rb +15 -0
- data/lib/cypher_dsl/expression.rb +69 -0
- data/lib/cypher_dsl/expression_condition.rb +14 -0
- data/lib/cypher_dsl/function_invocation.rb +17 -0
- data/lib/cypher_dsl/functions.rb +182 -0
- data/lib/cypher_dsl/has_label_condition.rb +15 -0
- data/lib/cypher_dsl/label_expression.rb +40 -0
- data/lib/cypher_dsl/label_operation.rb +18 -0
- data/lib/cypher_dsl/list_comprehension.rb +40 -0
- data/lib/cypher_dsl/list_expression.rb +17 -0
- data/lib/cypher_dsl/list_operator.rb +24 -0
- data/lib/cypher_dsl/list_predicate.rb +32 -0
- data/lib/cypher_dsl/literal.rb +29 -0
- data/lib/cypher_dsl/map_expression.rb +32 -0
- data/lib/cypher_dsl/map_projection.rb +58 -0
- data/lib/cypher_dsl/merge_action.rb +11 -0
- data/lib/cypher_dsl/named_path.rb +59 -0
- data/lib/cypher_dsl/no_condition.rb +14 -0
- data/lib/cypher_dsl/node.rb +83 -0
- data/lib/cypher_dsl/operation.rb +22 -0
- data/lib/cypher_dsl/parameter.rb +40 -0
- data/lib/cypher_dsl/pattern_comprehension.rb +31 -0
- data/lib/cypher_dsl/pattern_expression.rb +16 -0
- data/lib/cypher_dsl/property.rb +24 -0
- data/lib/cypher_dsl/quantified_path_pattern.rb +37 -0
- data/lib/cypher_dsl/raw_expression.rb +15 -0
- data/lib/cypher_dsl/reduction.rb +33 -0
- data/lib/cypher_dsl/relationship.rb +97 -0
- data/lib/cypher_dsl/relationship_chain.rb +44 -0
- data/lib/cypher_dsl/relationship_pattern_condition.rb +14 -0
- data/lib/cypher_dsl/renderer/configuration.rb +66 -0
- data/lib/cypher_dsl/renderer/dialect.rb +20 -0
- data/lib/cypher_dsl/renderer/escaper.rb +31 -0
- data/lib/cypher_dsl/renderer/visitor/expressions.rb +274 -0
- data/lib/cypher_dsl/renderer/visitor/patterns.rb +125 -0
- data/lib/cypher_dsl/renderer/visitor.rb +298 -0
- data/lib/cypher_dsl/renderer.rb +28 -0
- data/lib/cypher_dsl/sort_item.rb +17 -0
- data/lib/cypher_dsl/standalone_call.rb +61 -0
- data/lib/cypher_dsl/statement.rb +56 -0
- data/lib/cypher_dsl/statement_builder.rb +294 -0
- data/lib/cypher_dsl/statement_catalog.rb +25 -0
- data/lib/cypher_dsl/subquery_with.rb +15 -0
- data/lib/cypher_dsl/symbolic_name.rb +48 -0
- data/lib/cypher_dsl/unary_operation.rb +16 -0
- data/lib/cypher_dsl/union_query.rb +29 -0
- data/lib/cypher_dsl/version.rb +5 -0
- data/lib/cypher_dsl.rb +13 -0
- metadata +140 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# Builds a {Statement} clause by clause. Every method returns a new builder;
|
|
5
|
+
# the last clause stays open for refinement (WHERE on MATCH/WITH, ORDER BY,
|
|
6
|
+
# SKIP and LIMIT on RETURN/WITH) until the next clause is added.
|
|
7
|
+
#
|
|
8
|
+
# `and` is overloaded as in the Java API: after `where` it extends the
|
|
9
|
+
# condition, after `order_by` it adds another sort item.
|
|
10
|
+
class StatementBuilder
|
|
11
|
+
def initialize(clauses = [], mode = nil)
|
|
12
|
+
@clauses = clauses.freeze
|
|
13
|
+
@mode = mode
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# --- reading ---------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
# `match(pattern, …)` or, as in Java, `match(optional, pattern, …)` with a leading flag.
|
|
20
|
+
def match(*pattern_elements)
|
|
21
|
+
optional = [true, false].include?(pattern_elements.first) ? pattern_elements.shift : false
|
|
22
|
+
append(Clauses::Match.new(optional:, pattern_elements: pattern_elements.flatten.freeze, where: nil), :condition)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def optional_match(*pattern_elements) = match(true, *pattern_elements)
|
|
26
|
+
|
|
27
|
+
def where(condition)
|
|
28
|
+
if @clauses.last.is_a?(Clauses::CallSubquery)
|
|
29
|
+
raise ArgumentError, 'A CALL{} clause requires to WITH before you can add further conditions'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
update_last(:condition) { |clause| clause.with(where: Condition.coerce(condition)) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def and(argument)
|
|
36
|
+
case @mode
|
|
37
|
+
when :order then add_sort_item(argument)
|
|
38
|
+
when :condition then update_last(:condition) { |clause| clause.with(where: clause.where.and(argument)) }
|
|
39
|
+
else raise ArgumentError, '`and` needs a preceding WHERE or ORDER BY.'
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def or(condition) = update_last(:condition) { |clause| clause.with(where: clause.where.or(condition)) }
|
|
44
|
+
def xor(condition) = update_last(:condition) { |clause| clause.with(where: clause.where.xor(condition)) }
|
|
45
|
+
|
|
46
|
+
# --- projecting ------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
def returning(*expressions) = append(return_clause(false, expressions), :projection)
|
|
49
|
+
def returning_distinct(*expressions) = append(return_clause(true, expressions), :projection)
|
|
50
|
+
|
|
51
|
+
# A projection rendered without the RETURN keyword, for raw fragments that carry their own.
|
|
52
|
+
def returning_raw(*expressions) = append(Clauses::RawReturn.new(items: coerce_items(expressions)), :projection)
|
|
53
|
+
|
|
54
|
+
def with(*expressions)
|
|
55
|
+
append(Clauses::With.new(distinct: false, items: coerce_items(expressions), order: nil, skip: nil, limit: nil,
|
|
56
|
+
where: nil), :condition)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def with_distinct(*expressions)
|
|
60
|
+
append(Clauses::With.new(distinct: true, items: coerce_items(expressions), order: nil, skip: nil, limit: nil,
|
|
61
|
+
where: nil), :condition)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def order_by(*sort_items)
|
|
65
|
+
update_last(:order) { |clause| clause.with(order: Clauses::Order.new(items: coerce_sort_items(sort_items))) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ascending = update_last_sort_item(&:ascending)
|
|
69
|
+
def descending = update_last_sort_item(&:descending)
|
|
70
|
+
|
|
71
|
+
def skip(amount) = update_last(@mode) { |clause| clause.with(skip: optional_expression(amount)) }
|
|
72
|
+
def limit(amount) = update_last(@mode) { |clause| clause.with(limit: optional_expression(amount)) }
|
|
73
|
+
alias take limit # Arel spelling
|
|
74
|
+
|
|
75
|
+
# `unwind(list).as(:item)`; `as` completes the clause. Several arguments, or
|
|
76
|
+
# an array, form a list literal.
|
|
77
|
+
def unwind(*expressions)
|
|
78
|
+
single = expressions.size == 1 && !expressions.first.is_a?(Array)
|
|
79
|
+
OngoingUnwind.new(self, single ? coerce_items(expressions).first : ListExpression.of(expressions.flatten))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# --- updating --------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
def create(*pattern_elements) = append(Clauses::Create.new(pattern_elements: pattern_elements.flatten.freeze), nil)
|
|
85
|
+
|
|
86
|
+
def merge(*pattern_elements)
|
|
87
|
+
append(Clauses::Merge.new(pattern_elements: pattern_elements.flatten.freeze, actions: [].freeze), nil)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# `on_match.set(…)` / `on_create.mutate(…)` after a MERGE.
|
|
91
|
+
def on_match = MergeActionBuilder.new(self, :on_match)
|
|
92
|
+
def on_create = MergeActionBuilder.new(self, :on_create)
|
|
93
|
+
|
|
94
|
+
# Accepts operations (`n.p.to(x)`, `n.mutate(map)`), a node followed by
|
|
95
|
+
# labels (`set(n, 'A', 'B')`), or an even list of target/value pairs.
|
|
96
|
+
def set(*arguments) = append(Clauses::Set.new(operations: operations_for_set(arguments)), nil)
|
|
97
|
+
|
|
98
|
+
# `SET target += expression`.
|
|
99
|
+
def mutate(target, expression) = set(Operation.new(target, '+=', Expression.coerce(expression)))
|
|
100
|
+
|
|
101
|
+
# Accepts properties, or a node followed by labels (`remove(n, 'A', 'B')`).
|
|
102
|
+
def remove(*arguments)
|
|
103
|
+
arguments = arguments.flatten
|
|
104
|
+
items = label_operation?(arguments) ? [LabelOperation.new(arguments.first, arguments.drop(1))] : arguments
|
|
105
|
+
append(Clauses::Remove.new(items: items.freeze), nil)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def delete(*expressions) = append(Clauses::Delete.new(detach: false, expressions: coerce_items(expressions)), nil)
|
|
109
|
+
|
|
110
|
+
def detach_delete(*expressions)
|
|
111
|
+
append(Clauses::Delete.new(detach: true, expressions: coerce_items(expressions)), nil)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# `foreach(:i).in(list).apply(clause, …)`.
|
|
115
|
+
def foreach(variable) = OngoingForeach.new(self, SymbolicName.of(variable))
|
|
116
|
+
|
|
117
|
+
# `call('db.labels').with_args(…).yield(…)` for a procedure, or
|
|
118
|
+
# `call(statement, imports…)` for a `CALL { subquery }`.
|
|
119
|
+
def call(*arguments)
|
|
120
|
+
return StandaloneCall.new(arguments, builder: self) unless arguments.first.is_a?(Statement)
|
|
121
|
+
|
|
122
|
+
statement, *imports = arguments
|
|
123
|
+
append(subquery_clause(statement, imports, in_transactions: false, rows: nil), nil)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# `CALL { subquery } IN TRANSACTIONS [OF n ROWS]`; `call_in_transactions(statement, [rows], imports…)`.
|
|
127
|
+
def call_in_transactions(statement, *arguments)
|
|
128
|
+
rows = arguments.first.is_a?(Integer) ? arguments.shift : nil
|
|
129
|
+
append(subquery_clause(statement, arguments, in_transactions: true, rows:), nil)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# `CALL (*) { raw cypher }`.
|
|
133
|
+
def call_raw_cypher(cypher)
|
|
134
|
+
body = RawExpression.new(cypher)
|
|
135
|
+
append(Clauses::CallSubquery.new(body:, imports: [], in_transactions: false, rows: nil), nil)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# `load_csv('file:///x.csv').as(:line)`.
|
|
139
|
+
def load_csv(uri, with_headers: false) = OngoingLoadCsv.new(self, uri.to_s, with_headers)
|
|
140
|
+
|
|
141
|
+
# The statement built so far as an `EXISTS { … }` condition.
|
|
142
|
+
def as_condition = ExistsCondition.new(build)
|
|
143
|
+
|
|
144
|
+
# --- finishing -------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
# Continues with the clauses of another statement, e.g. after a `CALL … YIELD`.
|
|
147
|
+
def and_then(statement) = StatementBuilder.new(@clauses + statement.clauses, nil)
|
|
148
|
+
|
|
149
|
+
def finish = append(Clauses::Finish.new, nil)
|
|
150
|
+
|
|
151
|
+
def build = Statement.new(@clauses)
|
|
152
|
+
def explain = Statement.new(@clauses, prefix: 'EXPLAIN')
|
|
153
|
+
def profile = Statement.new(@clauses, prefix: 'PROFILE')
|
|
154
|
+
|
|
155
|
+
# Intermediate state of `unwind(expression)` waiting for `as(variable)`.
|
|
156
|
+
class OngoingUnwind
|
|
157
|
+
def initialize(builder, expression)
|
|
158
|
+
@builder = builder
|
|
159
|
+
@expression = expression
|
|
160
|
+
freeze
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def as(variable)
|
|
164
|
+
@builder.send(:append, Clauses::Unwind.new(expression: @expression, variable: SymbolicName.of(variable)), nil)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Intermediate states of `foreach(variable)`: `in(list)` then `apply(clauses…)`.
|
|
169
|
+
class OngoingForeach
|
|
170
|
+
def initialize(builder, variable, list = nil)
|
|
171
|
+
@builder = builder
|
|
172
|
+
@variable = variable
|
|
173
|
+
@list = list
|
|
174
|
+
freeze
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def in(list) = OngoingForeach.new(@builder, @variable, Expression.coerce(list))
|
|
178
|
+
|
|
179
|
+
def apply(*clauses)
|
|
180
|
+
foreach = Clauses::Foreach.new(variable: @variable, list: @list, clauses: clauses.flatten.freeze)
|
|
181
|
+
@builder.send(:append, foreach, nil)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Intermediate state of `load_csv(uri)` waiting for `as(alias)`.
|
|
186
|
+
class OngoingLoadCsv
|
|
187
|
+
def initialize(builder, uri, with_headers)
|
|
188
|
+
@builder = builder
|
|
189
|
+
@uri = uri
|
|
190
|
+
@with_headers = with_headers
|
|
191
|
+
freeze
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def as(alias_name)
|
|
195
|
+
clause = Clauses::LoadCsv.new(uri: @uri, with_headers: @with_headers, alias_name: SymbolicName.of(alias_name))
|
|
196
|
+
@builder.send(:append, clause, nil)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Intermediate state after `on_match` / `on_create`, waiting for `set` or `mutate`.
|
|
201
|
+
class MergeActionBuilder
|
|
202
|
+
def initialize(builder, type)
|
|
203
|
+
@builder = builder
|
|
204
|
+
@type = type
|
|
205
|
+
freeze
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def set(*arguments) = @builder.send(:add_merge_action, @type, @builder.send(:operations_for_set, arguments))
|
|
209
|
+
|
|
210
|
+
def mutate(target, expression) = set(Operation.new(target, '+=', Expression.coerce(expression)))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
private
|
|
214
|
+
|
|
215
|
+
attr_reader :clauses
|
|
216
|
+
|
|
217
|
+
def append(clause, mode) = StatementBuilder.new(@clauses + [clause], mode)
|
|
218
|
+
|
|
219
|
+
def subquery_clause(statement, imports, in_transactions:, rows:)
|
|
220
|
+
Clauses::CallSubquery.new(body: statement, imports: coerce_items(imports), in_transactions:, rows:)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def add_merge_action(type, operations)
|
|
224
|
+
update_last(nil) do |clause|
|
|
225
|
+
raise ArgumentError, 'ON CREATE / ON MATCH need a preceding MERGE.' unless clause.is_a?(Clauses::Merge)
|
|
226
|
+
|
|
227
|
+
clause.with(actions: clause.actions + [MergeAction.new(type:, operations:)])
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def operations_for_set(arguments)
|
|
232
|
+
arguments = arguments.flatten
|
|
233
|
+
if arguments.any? { |argument| argument.is_a?(LabelOperation) && argument.remove? }
|
|
234
|
+
raise ArgumentError, 'REMOVE operations are not supported in a SET clause'
|
|
235
|
+
end
|
|
236
|
+
return [LabelOperation.new(arguments.first, arguments.drop(1))].freeze if label_operation?(arguments)
|
|
237
|
+
return arguments.freeze if arguments.all? { |argument| operation?(argument) }
|
|
238
|
+
raise ArgumentError, 'The list of expression to set must be even.' if arguments.size.odd?
|
|
239
|
+
|
|
240
|
+
arguments.each_slice(2).map { |target, value| Operation.new(target, '=', Expression.coerce(value)) }.freeze
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def operation?(argument) = argument.is_a?(Operation) || argument.is_a?(LabelOperation)
|
|
244
|
+
|
|
245
|
+
def label_operation?(arguments)
|
|
246
|
+
arguments.size > 1 && arguments.first.is_a?(Node) &&
|
|
247
|
+
arguments.drop(1).all? { |argument| argument.is_a?(String) || argument.is_a?(Symbol) }
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def optional_expression(value) = value.nil? ? nil : Expression.coerce(value)
|
|
251
|
+
|
|
252
|
+
def update_last(mode)
|
|
253
|
+
raise ArgumentError, 'There is no clause to refine yet.' if @clauses.empty?
|
|
254
|
+
|
|
255
|
+
StatementBuilder.new(@clauses[0...-1] + [yield(@clauses.last)], mode)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def return_clause(distinct, expressions)
|
|
259
|
+
Clauses::Return.new(distinct:, items: coerce_items(expressions), order: nil, skip: nil, limit: nil)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def add_sort_item(expression)
|
|
263
|
+
update_last(:order) do |clause|
|
|
264
|
+
clause.with(order: Clauses::Order.new(items: clause.order.items + coerce_sort_items([expression])))
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def update_last_sort_item
|
|
269
|
+
update_last(:order) do |clause|
|
|
270
|
+
items = clause.order.items
|
|
271
|
+
clause.with(order: Clauses::Order.new(items: items[0...-1] + [yield(items.last)]))
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# RETURN/WITH items: strings and symbols name variables, nodes and
|
|
276
|
+
# relationships stand for their (possibly generated) names, everything else
|
|
277
|
+
# is an expression.
|
|
278
|
+
def coerce_items(expressions)
|
|
279
|
+
expressions.flatten.map do |item|
|
|
280
|
+
case item
|
|
281
|
+
when String, Symbol then SymbolicName.of(item)
|
|
282
|
+
when Node, Relationship then item.required_symbolic_name
|
|
283
|
+
else Expression.coerce(item)
|
|
284
|
+
end
|
|
285
|
+
end.freeze
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def coerce_sort_items(sort_items)
|
|
289
|
+
sort_items.flatten.map do |item|
|
|
290
|
+
item.is_a?(SortItem) ? item : SortItem.new(Expression.coerce(item), nil)
|
|
291
|
+
end.freeze
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# What a statement uses: its parameter names, and the values of the
|
|
5
|
+
# parameters that carry one. Built from a rendering pass.
|
|
6
|
+
class StatementCatalog
|
|
7
|
+
attr_reader :parameters, :parameter_names
|
|
8
|
+
|
|
9
|
+
# `uses` maps each parameter name to every value it was given, in render
|
|
10
|
+
# order, with {Parameter::NO_VALUE} for unbound uses.
|
|
11
|
+
def self.from_uses(uses)
|
|
12
|
+
conflicts = uses.filter_map { |name, values| [name, values.uniq.to_set] if values.uniq.size > 1 }.to_h
|
|
13
|
+
raise ConflictingParametersException, conflicts unless conflicts.empty?
|
|
14
|
+
|
|
15
|
+
bound = uses.filter_map { |name, values| [name, values.first] unless values.first.equal?(Parameter::NO_VALUE) }
|
|
16
|
+
new(parameters: bound.to_h, parameter_names: uses.keys)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(parameters:, parameter_names:)
|
|
20
|
+
@parameters = parameters.freeze
|
|
21
|
+
@parameter_names = parameter_names.freeze
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# `Cypher.subquery_with(imports…)`: imports for an EXISTS or COLLECT subquery,
|
|
5
|
+
# rendered as a leading `WITH` inside the braces.
|
|
6
|
+
class SubqueryWith
|
|
7
|
+
def initialize(imports)
|
|
8
|
+
@imports = imports.flatten.map { |import| Expression.coerce(import) }.freeze
|
|
9
|
+
freeze
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def collect(statement) = CollectExpression.new(statement, @imports)
|
|
13
|
+
def exists(statement_or_pattern) = ExistsCondition.new(statement_or_pattern, @imports)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# A variable name, such as the `n` in `MATCH (n)`.
|
|
5
|
+
#
|
|
6
|
+
# An *unresolved* name has no value yet: it is handed out when an unnamed
|
|
7
|
+
# node or relationship needs a name, and the renderer generates one for it.
|
|
8
|
+
class SymbolicName
|
|
9
|
+
include Expression
|
|
10
|
+
|
|
11
|
+
attr_reader :value, :hint
|
|
12
|
+
|
|
13
|
+
def self.of(value)
|
|
14
|
+
value.is_a?(SymbolicName) ? value : new(value.to_s)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# A name to be generated at render time; `hint` seeds the generated name (letters only).
|
|
18
|
+
def self.unresolved(hint) = new(nil, hint: hint.to_s.gsub(/[^a-zA-Z]/, ''))
|
|
19
|
+
|
|
20
|
+
def initialize(value, hint: nil)
|
|
21
|
+
raise ArgumentError, 'A symbolic name must not be empty.' if value && value.empty?
|
|
22
|
+
|
|
23
|
+
@value = value
|
|
24
|
+
@hint = hint
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def resolved? = !value.nil?
|
|
29
|
+
|
|
30
|
+
# A name is its own name; lets names stand in for named elements (e.g. `nodes(p)`).
|
|
31
|
+
def required_symbolic_name = self
|
|
32
|
+
|
|
33
|
+
# The map projection `name{…}`.
|
|
34
|
+
def project(*entries) = MapProjection.new(self, entries.flatten)
|
|
35
|
+
|
|
36
|
+
def property(*names) = Property.new(self, names)
|
|
37
|
+
|
|
38
|
+
# Resolved names are equal by value; unresolved ones only to themselves.
|
|
39
|
+
def ==(other)
|
|
40
|
+
return equal?(other) unless resolved?
|
|
41
|
+
|
|
42
|
+
other.is_a?(SymbolicName) && other.value == value
|
|
43
|
+
end
|
|
44
|
+
alias eql? ==
|
|
45
|
+
def hash = resolved? ? [self.class, value].hash : object_id.hash
|
|
46
|
+
def to_s = value || 'Unresolved SymbolicName'
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# A prefix operation such as `-x` or `+x`.
|
|
5
|
+
class UnaryOperation
|
|
6
|
+
include Expression
|
|
7
|
+
|
|
8
|
+
attr_reader :operator, :expression
|
|
9
|
+
|
|
10
|
+
def initialize(operator, expression)
|
|
11
|
+
@operator = operator
|
|
12
|
+
@expression = expression
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# Several statements combined with UNION or UNION ALL.
|
|
5
|
+
class UnionQuery < Statement
|
|
6
|
+
attr_reader :statements, :all
|
|
7
|
+
|
|
8
|
+
# Appends to an existing union when the first statement already is one of the same kind.
|
|
9
|
+
def self.create(statements, all:)
|
|
10
|
+
statements = statements.flatten
|
|
11
|
+
raise ArgumentError, 'A union needs at least two statements.' if statements.size < 2
|
|
12
|
+
|
|
13
|
+
first = statements.first
|
|
14
|
+
return new(statements, all:) unless first.is_a?(UnionQuery)
|
|
15
|
+
raise ArgumentError, 'Cannot mix union and union all!' unless first.all == all
|
|
16
|
+
|
|
17
|
+
new(first.statements + statements.drop(1), all:)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(statements, all:)
|
|
21
|
+
super([])
|
|
22
|
+
@statements = statements.freeze
|
|
23
|
+
@all = all
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def result_statement? = true
|
|
27
|
+
def multi_part_query? = false
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/cypher_dsl.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'zeitwerk'
|
|
4
|
+
|
|
5
|
+
loader = Zeitwerk::Loader.for_gem
|
|
6
|
+
loader.inflector.inflect('cypher_dsl' => 'CypherDSL')
|
|
7
|
+
loader.setup
|
|
8
|
+
|
|
9
|
+
# A Ruby DSL for building Cypher statements.
|
|
10
|
+
#
|
|
11
|
+
# The entry point is {CypherDSL::Cypher}; everything else is reached from there.
|
|
12
|
+
module CypherDSL
|
|
13
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cypher-dsl
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.alpha.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Heinrich Klobuczek
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: zeitwerk
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.6'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.6'
|
|
26
|
+
description: An immutable, part-aware builder and renderer for Cypher statements,
|
|
27
|
+
modelled on the design of Neo4j's Cypher-DSL for Java.
|
|
28
|
+
email:
|
|
29
|
+
- heinrich@mail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- CHANGELOG.md
|
|
35
|
+
- LICENSE.txt
|
|
36
|
+
- NOTICE
|
|
37
|
+
- README.md
|
|
38
|
+
- lib/cypher_dsl.rb
|
|
39
|
+
- lib/cypher_dsl/aliased_expression.rb
|
|
40
|
+
- lib/cypher_dsl/asterisk.rb
|
|
41
|
+
- lib/cypher_dsl/case_expression.rb
|
|
42
|
+
- lib/cypher_dsl/clauses.rb
|
|
43
|
+
- lib/cypher_dsl/clauses/call.rb
|
|
44
|
+
- lib/cypher_dsl/clauses/call_subquery.rb
|
|
45
|
+
- lib/cypher_dsl/clauses/create.rb
|
|
46
|
+
- lib/cypher_dsl/clauses/delete.rb
|
|
47
|
+
- lib/cypher_dsl/clauses/finish.rb
|
|
48
|
+
- lib/cypher_dsl/clauses/foreach.rb
|
|
49
|
+
- lib/cypher_dsl/clauses/load_csv.rb
|
|
50
|
+
- lib/cypher_dsl/clauses/match.rb
|
|
51
|
+
- lib/cypher_dsl/clauses/merge.rb
|
|
52
|
+
- lib/cypher_dsl/clauses/order.rb
|
|
53
|
+
- lib/cypher_dsl/clauses/raw_return.rb
|
|
54
|
+
- lib/cypher_dsl/clauses/remove.rb
|
|
55
|
+
- lib/cypher_dsl/clauses/return.rb
|
|
56
|
+
- lib/cypher_dsl/clauses/set.rb
|
|
57
|
+
- lib/cypher_dsl/clauses/unwind.rb
|
|
58
|
+
- lib/cypher_dsl/clauses/use.rb
|
|
59
|
+
- lib/cypher_dsl/clauses/with.rb
|
|
60
|
+
- lib/cypher_dsl/collect_expression.rb
|
|
61
|
+
- lib/cypher_dsl/comparison.rb
|
|
62
|
+
- lib/cypher_dsl/compound_condition.rb
|
|
63
|
+
- lib/cypher_dsl/condition.rb
|
|
64
|
+
- lib/cypher_dsl/conflicting_parameters_exception.rb
|
|
65
|
+
- lib/cypher_dsl/constant_condition.rb
|
|
66
|
+
- lib/cypher_dsl/count_expression.rb
|
|
67
|
+
- lib/cypher_dsl/cypher.rb
|
|
68
|
+
- lib/cypher_dsl/exists_condition.rb
|
|
69
|
+
- lib/cypher_dsl/expression.rb
|
|
70
|
+
- lib/cypher_dsl/expression_condition.rb
|
|
71
|
+
- lib/cypher_dsl/function_invocation.rb
|
|
72
|
+
- lib/cypher_dsl/functions.rb
|
|
73
|
+
- lib/cypher_dsl/has_label_condition.rb
|
|
74
|
+
- lib/cypher_dsl/label_expression.rb
|
|
75
|
+
- lib/cypher_dsl/label_operation.rb
|
|
76
|
+
- lib/cypher_dsl/list_comprehension.rb
|
|
77
|
+
- lib/cypher_dsl/list_expression.rb
|
|
78
|
+
- lib/cypher_dsl/list_operator.rb
|
|
79
|
+
- lib/cypher_dsl/list_predicate.rb
|
|
80
|
+
- lib/cypher_dsl/literal.rb
|
|
81
|
+
- lib/cypher_dsl/map_expression.rb
|
|
82
|
+
- lib/cypher_dsl/map_projection.rb
|
|
83
|
+
- lib/cypher_dsl/merge_action.rb
|
|
84
|
+
- lib/cypher_dsl/named_path.rb
|
|
85
|
+
- lib/cypher_dsl/no_condition.rb
|
|
86
|
+
- lib/cypher_dsl/node.rb
|
|
87
|
+
- lib/cypher_dsl/operation.rb
|
|
88
|
+
- lib/cypher_dsl/parameter.rb
|
|
89
|
+
- lib/cypher_dsl/pattern_comprehension.rb
|
|
90
|
+
- lib/cypher_dsl/pattern_expression.rb
|
|
91
|
+
- lib/cypher_dsl/property.rb
|
|
92
|
+
- lib/cypher_dsl/quantified_path_pattern.rb
|
|
93
|
+
- lib/cypher_dsl/raw_expression.rb
|
|
94
|
+
- lib/cypher_dsl/reduction.rb
|
|
95
|
+
- lib/cypher_dsl/relationship.rb
|
|
96
|
+
- lib/cypher_dsl/relationship_chain.rb
|
|
97
|
+
- lib/cypher_dsl/relationship_pattern_condition.rb
|
|
98
|
+
- lib/cypher_dsl/renderer.rb
|
|
99
|
+
- lib/cypher_dsl/renderer/configuration.rb
|
|
100
|
+
- lib/cypher_dsl/renderer/dialect.rb
|
|
101
|
+
- lib/cypher_dsl/renderer/escaper.rb
|
|
102
|
+
- lib/cypher_dsl/renderer/visitor.rb
|
|
103
|
+
- lib/cypher_dsl/renderer/visitor/expressions.rb
|
|
104
|
+
- lib/cypher_dsl/renderer/visitor/patterns.rb
|
|
105
|
+
- lib/cypher_dsl/sort_item.rb
|
|
106
|
+
- lib/cypher_dsl/standalone_call.rb
|
|
107
|
+
- lib/cypher_dsl/statement.rb
|
|
108
|
+
- lib/cypher_dsl/statement_builder.rb
|
|
109
|
+
- lib/cypher_dsl/statement_catalog.rb
|
|
110
|
+
- lib/cypher_dsl/subquery_with.rb
|
|
111
|
+
- lib/cypher_dsl/symbolic_name.rb
|
|
112
|
+
- lib/cypher_dsl/unary_operation.rb
|
|
113
|
+
- lib/cypher_dsl/union_query.rb
|
|
114
|
+
- lib/cypher_dsl/version.rb
|
|
115
|
+
homepage: https://github.com/neo4jrb/cypher-dsl
|
|
116
|
+
licenses:
|
|
117
|
+
- MIT
|
|
118
|
+
metadata:
|
|
119
|
+
source_code_uri: https://github.com/neo4jrb/cypher-dsl
|
|
120
|
+
changelog_uri: https://github.com/neo4jrb/cypher-dsl/blob/main/CHANGELOG.md
|
|
121
|
+
bug_tracker_uri: https://github.com/neo4jrb/cypher-dsl/issues
|
|
122
|
+
rubygems_mfa_required: 'true'
|
|
123
|
+
rdoc_options: []
|
|
124
|
+
require_paths:
|
|
125
|
+
- lib
|
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '3.4'
|
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
132
|
+
requirements:
|
|
133
|
+
- - ">="
|
|
134
|
+
- !ruby/object:Gem::Version
|
|
135
|
+
version: '0'
|
|
136
|
+
requirements: []
|
|
137
|
+
rubygems_version: 4.0.20
|
|
138
|
+
specification_version: 4
|
|
139
|
+
summary: A Ruby DSL (builder) for the Cypher query language
|
|
140
|
+
test_files: []
|