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,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# The main entry point of the DSL. Mirrors the static factory methods of the
|
|
5
|
+
# Java `org.neo4j.cypherdsl.core.Cypher` class in snake_case.
|
|
6
|
+
module Cypher
|
|
7
|
+
extend Functions
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# --- pattern elements ------------------------------------------------
|
|
12
|
+
|
|
13
|
+
# A node with one primary label and optional additional labels; a trailing
|
|
14
|
+
# map (`Cypher.node('Test', Cypher.map_of(…))`) sets its properties.
|
|
15
|
+
def node(primary_label, *additional_labels)
|
|
16
|
+
properties = additional_labels.pop if [MapExpression, Hash].any? { |type| additional_labels.last.is_a?(type) }
|
|
17
|
+
labels = [primary_label, *additional_labels].map { |label| label.is_a?(LabelExpression) ? label : label.to_s }
|
|
18
|
+
node = Node.new(labels:)
|
|
19
|
+
properties ? node.with_properties(properties) : node
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# A node without labels, optionally named.
|
|
23
|
+
def any_node(name = nil)
|
|
24
|
+
node = Node.new(labels: [])
|
|
25
|
+
name ? node.named(name) : node
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# --- names, properties, parameters -----------------------------------
|
|
29
|
+
|
|
30
|
+
def name(value) = SymbolicName.of(value)
|
|
31
|
+
|
|
32
|
+
# `Cypher.property(:n, :name)` → `n.name`; the container may also be an expression,
|
|
33
|
+
# and a name may be an expression for a dynamic lookup (`n[$prop]`).
|
|
34
|
+
def property(container, *names)
|
|
35
|
+
container = SymbolicName.of(container) unless container.is_a?(Expression)
|
|
36
|
+
Property.new(container, names.flatten)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# `$name`, optionally with its value; see {Parameter}.
|
|
40
|
+
def parameter(name, value = Parameter::NO_VALUE) = Parameter.new(name, value)
|
|
41
|
+
|
|
42
|
+
# A parameter named by the renderer (`$pcdsl01`, …), carrying its value.
|
|
43
|
+
def anon_parameter(value) = Parameter.anonymous(value)
|
|
44
|
+
|
|
45
|
+
# --- literals --------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
# Wraps a Ruby value as a literal; expressions pass through unchanged.
|
|
48
|
+
def literal_of(value) = Literal.of(value)
|
|
49
|
+
|
|
50
|
+
def literal_true = Literal::TRUE
|
|
51
|
+
def literal_false = Literal::FALSE
|
|
52
|
+
def literal_null = Literal::NULL
|
|
53
|
+
|
|
54
|
+
def map_of(*keys_and_values) = MapExpression.of(*keys_and_values)
|
|
55
|
+
def list_of(*expressions) = ListExpression.of(*expressions)
|
|
56
|
+
|
|
57
|
+
def asterisk = Asterisk::INSTANCE
|
|
58
|
+
|
|
59
|
+
# --- conditions ------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def is_true = ConstantCondition::TRUE
|
|
62
|
+
def is_false = ConstantCondition::FALSE
|
|
63
|
+
def no_condition = NoCondition::INSTANCE
|
|
64
|
+
def not(condition_or_pattern) = Condition.coerce(condition_or_pattern).not
|
|
65
|
+
def includes_all(list_expression, list) = Expression.coerce(list_expression).includes_all(list)
|
|
66
|
+
def includes_any(list_expression, list) = Expression.coerce(list_expression).includes_any(list)
|
|
67
|
+
|
|
68
|
+
# `[pattern WHERE … | …]`; continue with `where` and `returning`.
|
|
69
|
+
def list_based_on(pattern) = PatternComprehension.new(pattern)
|
|
70
|
+
|
|
71
|
+
# `[variable IN list WHERE … | …]`; continue with `in`, `where` and `returning`.
|
|
72
|
+
def list_with(variable) = ListComprehension.new(variable)
|
|
73
|
+
|
|
74
|
+
# `Cypher.path(:p).defined_by(pattern)` for `MATCH p = pattern`; `.get` for the bare name.
|
|
75
|
+
def path(name) = NamedPath::Ongoing.new(SymbolicName.of(name))
|
|
76
|
+
|
|
77
|
+
# Path selectors, `Cypher.any_shortest.named(:p).defined_by(pattern)` → `p = ANY pattern`.
|
|
78
|
+
def any_shortest = NamedPath::Selector.new('ANY')
|
|
79
|
+
def all_shortest = NamedPath::Selector.new('ALL SHORTEST')
|
|
80
|
+
def shortest_k(count) = NamedPath::Selector.shortest_k(count)
|
|
81
|
+
def shortest_k_groups(count) = NamedPath::Selector.shortest_k(count, groups: true)
|
|
82
|
+
|
|
83
|
+
# `EXISTS { pattern }` / `EXISTS { pattern, pattern }` / `EXISTS { [WITH imports] statement }`.
|
|
84
|
+
# A property gives the condition `n.prop IS NOT NULL`, any other plain expression the
|
|
85
|
+
# legacy `exists(expr)` function.
|
|
86
|
+
def exists(body, *imports)
|
|
87
|
+
return body.is_not_null if body.is_a?(Property)
|
|
88
|
+
return function('exists', body) if body.is_a?(Expression) && !body.is_a?(Statement) && !pattern?(body)
|
|
89
|
+
|
|
90
|
+
ExistsCondition.new(body, imports.flatten.map { |import| Expression.coerce(import) })
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
PATTERN_TYPES = [Relationship, RelationshipChain, QuantifiedPathPattern, NamedPath].freeze
|
|
94
|
+
private_constant :PATTERN_TYPES
|
|
95
|
+
|
|
96
|
+
def pattern?(value) = PATTERN_TYPES.any? { |type| value.is_a?(type) }
|
|
97
|
+
|
|
98
|
+
# List predicates: `Cypher.all(:x).in(list).where(condition)`.
|
|
99
|
+
def all(variable) = ListPredicate::Ongoing.new('all', variable)
|
|
100
|
+
def any(variable) = ListPredicate::Ongoing.new('any', variable)
|
|
101
|
+
def none(variable) = ListPredicate::Ongoing.new('none', variable)
|
|
102
|
+
def single(variable) = ListPredicate::Ongoing.new('single', variable)
|
|
103
|
+
|
|
104
|
+
def sub_list_until(list, end_index) = ListOperator.sublist(Expression.coerce(list), nil, end_index)
|
|
105
|
+
def sub_list_from(list, start_index) = ListOperator.sublist(Expression.coerce(list), start_index, nil)
|
|
106
|
+
def sub_list(list, start_index = nil, end_index = nil) = sublist(list, start_index, end_index)
|
|
107
|
+
|
|
108
|
+
def range(start_value, end_value, step = nil) = function('range', *[start_value, end_value, step].compact)
|
|
109
|
+
|
|
110
|
+
# --- operations ------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def set(target, value) = Operation.new(target, '=', Expression.coerce(value))
|
|
113
|
+
def set_label(node, *labels) = LabelOperation.new(node, labels)
|
|
114
|
+
def remove_label(node, *labels) = LabelOperation.new(node, labels, remove: true)
|
|
115
|
+
def mutate(target, expression) = Operation.new(target, '+=', Expression.coerce(expression))
|
|
116
|
+
def minus(expression) = UnaryOperation.new('-', Expression.coerce(expression))
|
|
117
|
+
def plus(expression) = UnaryOperation.new('+', Expression.coerce(expression))
|
|
118
|
+
def value_at(list, index) = ListOperator.value_at(Expression.coerce(list), index)
|
|
119
|
+
|
|
120
|
+
def sublist(list, start_index = nil, end_index = nil)
|
|
121
|
+
ListOperator.sublist(Expression.coerce(list), start_index, end_index)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# `CASE … END`; `case_expression(subject)` for the simple form, no argument for the generic one.
|
|
125
|
+
def case_expression(subject = nil) = CaseExpression.new(subject: subject && Expression.coerce(subject))
|
|
126
|
+
|
|
127
|
+
# --- sorting and functions --------------------------------------------
|
|
128
|
+
# Plain functions live in {Functions}; these overloads also accept patterns and statements.
|
|
129
|
+
|
|
130
|
+
# `collect(expression)`, or `COLLECT { statement }` when given a statement.
|
|
131
|
+
def collect(expression_or_statement)
|
|
132
|
+
return CollectExpression.new(expression_or_statement) if expression_or_statement.is_a?(Statement)
|
|
133
|
+
|
|
134
|
+
function('collect', expression_or_statement)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# A map projection on a name: `Cypher.create_projection(:n, 'title')` → `n{.title}`.
|
|
138
|
+
def create_projection(name, *entries) = MapProjection.new(SymbolicName.of(name), entries.flatten)
|
|
139
|
+
|
|
140
|
+
def sort(expression) = SortItem.new(Expression.coerce(expression), nil)
|
|
141
|
+
|
|
142
|
+
# `size(list)` or `size(pattern)`.
|
|
143
|
+
def size(expression_or_pattern)
|
|
144
|
+
argument = pattern?(expression_or_pattern) ? PatternExpression.new(expression_or_pattern) : expression_or_pattern
|
|
145
|
+
function('size', argument)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# `count(expression)`, or `COUNT { pattern }` / `COUNT { statement }` for patterns and statements.
|
|
149
|
+
def count(expression_or_pattern)
|
|
150
|
+
subquery = pattern?(expression_or_pattern) || expression_or_pattern.is_a?(Statement)
|
|
151
|
+
return CountExpression.new(expression_or_pattern) if subquery
|
|
152
|
+
|
|
153
|
+
FunctionInvocation.new('count', [Expression.coerce(expression_or_pattern)])
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# `Cypher.call('db.labels')` / `Cypher.call('db', 'labels')`: a procedure call (see {StandaloneCall});
|
|
157
|
+
# `Cypher.call(statement, imports…)`: a `CALL { subquery }`.
|
|
158
|
+
def call(*arguments)
|
|
159
|
+
return StatementBuilder.new.call(*arguments) if arguments.first.is_a?(Statement)
|
|
160
|
+
|
|
161
|
+
StandaloneCall.new(arguments)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# `Cypher.use('graph', statement)` / `Cypher.use(Cypher.graph_by_name(name), statement)`:
|
|
165
|
+
# the statement prefixed with `USE graph`.
|
|
166
|
+
def use(graph, statement)
|
|
167
|
+
graph = graph.to_s unless graph.is_a?(Expression)
|
|
168
|
+
Statement.new([Clauses::Use.new(graph:)] + statement.clauses)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# `Cypher.load_csv('file:///x.csv').as(:line)`.
|
|
172
|
+
def load_csv(uri, with_headers: false) = StatementBuilder.new.load_csv(uri, with_headers:)
|
|
173
|
+
|
|
174
|
+
# Imports for an EXISTS / COLLECT subquery: `Cypher.subquery_with(x).collect(statement)`.
|
|
175
|
+
def subquery_with(*imports) = SubqueryWith.new(imports)
|
|
176
|
+
|
|
177
|
+
# `CALL () { raw cypher }`; continues as a statement builder.
|
|
178
|
+
def call_raw_cypher(cypher)
|
|
179
|
+
clause = Clauses::CallSubquery.new(body: RawExpression.new(cypher), imports: [],
|
|
180
|
+
in_transactions: false, rows: nil)
|
|
181
|
+
StatementBuilder.new.send(:append, clause, nil)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Verbatim Cypher, the escape hatch.
|
|
185
|
+
def raw(cypher) = RawExpression.new(cypher)
|
|
186
|
+
|
|
187
|
+
# Static comparison forms, `Cypher.gt(a, b)` etc.
|
|
188
|
+
def eq(left, right) = Expression.coerce(left).is_equal_to(right)
|
|
189
|
+
def ne(left, right) = Expression.coerce(left).is_not_equal_to(right)
|
|
190
|
+
def gt(left, right) = Expression.coerce(left).gt(right)
|
|
191
|
+
def gte(left, right) = Expression.coerce(left).gte(right)
|
|
192
|
+
def lt(left, right) = Expression.coerce(left).lt(right)
|
|
193
|
+
def lte(left, right) = Expression.coerce(left).lte(right)
|
|
194
|
+
|
|
195
|
+
# --- statement builders -----------------------------------------------
|
|
196
|
+
|
|
197
|
+
def match(*pattern_elements) = StatementBuilder.new.match(*pattern_elements)
|
|
198
|
+
def optional_match(*pattern_elements) = StatementBuilder.new.optional_match(*pattern_elements)
|
|
199
|
+
def create(*pattern_elements) = StatementBuilder.new.create(*pattern_elements)
|
|
200
|
+
def merge(*pattern_elements) = StatementBuilder.new.merge(*pattern_elements)
|
|
201
|
+
def with(*expressions) = StatementBuilder.new.with(*expressions)
|
|
202
|
+
def unwind(*expressions) = StatementBuilder.new.unwind(*expressions)
|
|
203
|
+
def returning(*expressions) = StatementBuilder.new.returning(*expressions)
|
|
204
|
+
def returning_distinct(*expressions) = StatementBuilder.new.returning_distinct(*expressions)
|
|
205
|
+
def returning_raw(*expressions) = StatementBuilder.new.returning_raw(*expressions)
|
|
206
|
+
|
|
207
|
+
def union(*statements) = UnionQuery.create(statements, all: false)
|
|
208
|
+
def union_all(*statements) = UnionQuery.create(statements, all: true)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# `EXISTS { pattern }` / `EXISTS { [WITH imports] statement }`.
|
|
5
|
+
class ExistsCondition < Condition
|
|
6
|
+
attr_reader :body, :imports
|
|
7
|
+
|
|
8
|
+
def initialize(body, imports = [])
|
|
9
|
+
super()
|
|
10
|
+
@body = body
|
|
11
|
+
@imports = imports.freeze
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# Mixed into everything that can appear where Cypher expects an expression.
|
|
5
|
+
# Provides the fluent condition, alias and sort helpers of the Java API.
|
|
6
|
+
module Expression
|
|
7
|
+
# Coerces a plain Ruby value into an expression; expressions pass through.
|
|
8
|
+
def self.coerce(value)
|
|
9
|
+
value.is_a?(Expression) ? value : Literal.of(value)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def as(alias_name) = AliasedExpression.new(self, alias_name)
|
|
13
|
+
|
|
14
|
+
# A property of this expression's value, e.g. `$param.key` or `datetime().epochSeconds`.
|
|
15
|
+
def property(*names) = Property.new(self, names)
|
|
16
|
+
|
|
17
|
+
# This expression as a condition, e.g. a CASE expression in WHERE.
|
|
18
|
+
def as_condition = ExpressionCondition.new(self)
|
|
19
|
+
|
|
20
|
+
def ascending = SortItem.new(self, :asc)
|
|
21
|
+
def descending = SortItem.new(self, :desc)
|
|
22
|
+
|
|
23
|
+
def is_equal_to(other) = Comparison.new(self, '=', Expression.coerce(other))
|
|
24
|
+
def is_not_equal_to(other) = Comparison.new(self, '<>', Expression.coerce(other))
|
|
25
|
+
def lt(other) = Comparison.new(self, '<', Expression.coerce(other))
|
|
26
|
+
def lte(other) = Comparison.new(self, '<=', Expression.coerce(other))
|
|
27
|
+
def gt(other) = Comparison.new(self, '>', Expression.coerce(other))
|
|
28
|
+
def gte(other) = Comparison.new(self, '>=', Expression.coerce(other))
|
|
29
|
+
def matches(other) = Comparison.new(self, '=~', Expression.coerce(other))
|
|
30
|
+
def starts_with(other) = Comparison.new(self, 'STARTS WITH', Expression.coerce(other))
|
|
31
|
+
def ends_with(other) = Comparison.new(self, 'ENDS WITH', Expression.coerce(other))
|
|
32
|
+
def contains(other) = Comparison.new(self, 'CONTAINS', Expression.coerce(other))
|
|
33
|
+
def is_null = Comparison.new(self, 'IS NULL', nil)
|
|
34
|
+
def is_not_null = Comparison.new(self, 'IS NOT NULL', nil)
|
|
35
|
+
def is_true = Comparison.new(self, '=', Literal::TRUE)
|
|
36
|
+
def is_false = Comparison.new(self, '=', Literal::FALSE)
|
|
37
|
+
def in(haystack) = Comparison.new(self, 'IN', Expression.coerce(haystack))
|
|
38
|
+
|
|
39
|
+
# `size(self)`, `size(self) = n` and `size(self) = 0`.
|
|
40
|
+
def size = FunctionInvocation.new('size', [self])
|
|
41
|
+
def has_size(expected) = size.is_equal_to(expected)
|
|
42
|
+
def is_empty = size.is_equal_to(Literal.new(0))
|
|
43
|
+
|
|
44
|
+
# `all(x IN list WHERE x IN self)` / `any(…)`: does this list contain all / any of the elements?
|
|
45
|
+
def includes_all(list) = ListPredicate.new('all', :x, Expression.coerce(list), SymbolicName.of(:x).in(self))
|
|
46
|
+
def includes_any(list) = ListPredicate.new('any', :x, Expression.coerce(list), SymbolicName.of(:x).in(self))
|
|
47
|
+
|
|
48
|
+
# Arithmetic; rendered in parentheses, e.g. `(a + b)`.
|
|
49
|
+
def add(other) = Operation.new(self, '+', Expression.coerce(other))
|
|
50
|
+
def subtract(other) = Operation.new(self, '-', Expression.coerce(other))
|
|
51
|
+
def multiply(other) = Operation.new(self, '*', Expression.coerce(other))
|
|
52
|
+
def divide(other) = Operation.new(self, '/', Expression.coerce(other))
|
|
53
|
+
def remainder(other) = Operation.new(self, '%', Expression.coerce(other))
|
|
54
|
+
def pow(other) = Operation.new(self, '^', Expression.coerce(other))
|
|
55
|
+
def concat(other) = Operation.new(self, '+', Expression.coerce(other))
|
|
56
|
+
|
|
57
|
+
# Java short forms.
|
|
58
|
+
alias eq is_equal_to
|
|
59
|
+
alias ne is_not_equal_to
|
|
60
|
+
|
|
61
|
+
# Arel spellings, each mapping to exactly one of the above.
|
|
62
|
+
alias not_eq is_not_equal_to
|
|
63
|
+
alias gteq gte
|
|
64
|
+
alias lteq lte
|
|
65
|
+
alias asc ascending
|
|
66
|
+
alias desc descending
|
|
67
|
+
def not_in(haystack) = self.in(haystack).not
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# Any expression used where a condition is expected, e.g. a CASE expression in WHERE.
|
|
5
|
+
class ExpressionCondition < Condition
|
|
6
|
+
attr_reader :expression
|
|
7
|
+
|
|
8
|
+
def initialize(expression)
|
|
9
|
+
super()
|
|
10
|
+
@expression = expression
|
|
11
|
+
freeze
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# A function call such as `size((a)-->())` or `count(n)`.
|
|
5
|
+
class FunctionInvocation
|
|
6
|
+
include Expression
|
|
7
|
+
|
|
8
|
+
attr_reader :function_name, :arguments, :distinct
|
|
9
|
+
|
|
10
|
+
def initialize(function_name, arguments, distinct: false)
|
|
11
|
+
@function_name = function_name
|
|
12
|
+
@arguments = arguments.freeze
|
|
13
|
+
@distinct = distinct
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# Factory methods for Cypher's built-in functions, mixed into {Cypher}
|
|
5
|
+
# (`Cypher.to_upper(name)`, `Cypher.date(year: …)`). Names follow the Java
|
|
6
|
+
# `Functions` class in snake_case; `*_distinct` variants render `DISTINCT`
|
|
7
|
+
# inside the call. Arguments are coerced, so plain strings and numbers become
|
|
8
|
+
# literals and `nil` becomes `NULL`.
|
|
9
|
+
module Functions
|
|
10
|
+
# A call of any function with the given arguments, coerced to expressions.
|
|
11
|
+
def function(name, *arguments) = FunctionInvocation.new(name, arguments.map { |a| Expression.coerce(a) })
|
|
12
|
+
|
|
13
|
+
# --- strings ---------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
def left(expression, length) = string_side('left', expression, length)
|
|
16
|
+
def right(expression, length) = string_side('right', expression, length)
|
|
17
|
+
def ltrim(expression) = function('ltrim', expression)
|
|
18
|
+
def rtrim(expression) = function('rtrim', expression)
|
|
19
|
+
def trim(expression) = function('trim', expression)
|
|
20
|
+
def to_lower(expression) = function('toLower', expression)
|
|
21
|
+
def to_upper(expression) = function('toUpper', expression)
|
|
22
|
+
def reverse(expression) = function('reverse', expression)
|
|
23
|
+
def replace(original, search, replacement) = function('replace', original, search, replacement)
|
|
24
|
+
def split(expression, delimiter) = function('split', expression, delimiter)
|
|
25
|
+
def to_string_or_null(expression) = function('toStringOrNull', expression)
|
|
26
|
+
|
|
27
|
+
def substring(original, start, length = nil)
|
|
28
|
+
raise ArgumentError, 'start is required' if start.nil?
|
|
29
|
+
|
|
30
|
+
function('substring', original, start, *[length].compact)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# --- aggregates ------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
def avg(expression) = function('avg', expression)
|
|
36
|
+
def avg_distinct(expression) = distinct_function('avg', expression)
|
|
37
|
+
def collect(expression) = function('collect', expression)
|
|
38
|
+
def collect_distinct(expression) = distinct_function('collect', expression)
|
|
39
|
+
def count(expression) = function('count', expression)
|
|
40
|
+
def count_distinct(expression) = distinct_function('count', expression)
|
|
41
|
+
def max(expression) = function('max', expression)
|
|
42
|
+
def max_distinct(expression) = distinct_function('max', expression)
|
|
43
|
+
def min(expression) = function('min', expression)
|
|
44
|
+
def min_distinct(expression) = distinct_function('min', expression)
|
|
45
|
+
def sum(expression) = function('sum', expression)
|
|
46
|
+
def sum_distinct(expression) = distinct_function('sum', expression)
|
|
47
|
+
def percentile_cont(expression, percentile) = function('percentileCont', expression, percentile)
|
|
48
|
+
def percentile_cont_distinct(expression, percentile) = distinct_function('percentileCont', expression, percentile)
|
|
49
|
+
def percentile_disc(expression, percentile) = function('percentileDisc', expression, percentile)
|
|
50
|
+
def percentile_disc_distinct(expression, percentile) = distinct_function('percentileDisc', expression, percentile)
|
|
51
|
+
def st_dev(expression) = function('stDev', expression)
|
|
52
|
+
def st_dev_distinct(expression) = distinct_function('stDev', expression)
|
|
53
|
+
def st_dev_p(expression) = function('stDevP', expression)
|
|
54
|
+
def st_dev_p_distinct(expression) = distinct_function('stDevP', expression)
|
|
55
|
+
|
|
56
|
+
# --- lists, paths and entities ---------------------------------------
|
|
57
|
+
|
|
58
|
+
def head(expression) = function('head', expression)
|
|
59
|
+
def last(expression) = function('last', expression)
|
|
60
|
+
def tail(expression) = function('tail', expression)
|
|
61
|
+
def keys(expression) = function('keys', expression)
|
|
62
|
+
def properties(expression) = function('properties', expression)
|
|
63
|
+
def coalesce(*expressions) = function('coalesce', *expressions)
|
|
64
|
+
def is_empty(expression) = function('isEmpty', expression)
|
|
65
|
+
def range(start_value, end_value, step = nil) = function('range', start_value, end_value, *[step].compact)
|
|
66
|
+
def nodes(path) = function('nodes', path.required_symbolic_name)
|
|
67
|
+
def relationships(path) = function('relationships', path.required_symbolic_name)
|
|
68
|
+
def length(path) = function('length', path.required_symbolic_name)
|
|
69
|
+
def labels(node) = function('labels', node.required_symbolic_name)
|
|
70
|
+
def type(relationship) = function('type', relationship.required_symbolic_name)
|
|
71
|
+
def start_node(relationship) = function('startNode', relationship.required_symbolic_name)
|
|
72
|
+
def end_node(relationship) = function('endNode', relationship.required_symbolic_name)
|
|
73
|
+
def element_id(element) = function('elementId', element.required_symbolic_name)
|
|
74
|
+
|
|
75
|
+
# `id(n)`; deprecated in Neo4j 5 in favour of {#element_id}, kept for parity.
|
|
76
|
+
def id(element) = function('id', element.required_symbolic_name)
|
|
77
|
+
|
|
78
|
+
# `reduce(acc = initial, x IN list | mapper)`, built as
|
|
79
|
+
# `reduce(:x).in(list).map(mapper).accumulate_on(acc).with_initial_value_of(0)`.
|
|
80
|
+
def reduce(variable) = Reduction.of(variable)
|
|
81
|
+
|
|
82
|
+
# --- scalars and conversions -----------------------------------------
|
|
83
|
+
|
|
84
|
+
def to_integer(expression) = function('toInteger', expression)
|
|
85
|
+
def to_float(expression) = function('toFloat', expression)
|
|
86
|
+
def to_string(expression) = function('toString', expression)
|
|
87
|
+
def to_boolean(expression) = function('toBoolean', expression)
|
|
88
|
+
def random_uuid = function('randomUUID')
|
|
89
|
+
|
|
90
|
+
# --- mathematical ----------------------------------------------------
|
|
91
|
+
|
|
92
|
+
def abs(expression) = function('abs', expression)
|
|
93
|
+
def ceil(expression) = function('ceil', expression)
|
|
94
|
+
def floor(expression) = function('floor', expression)
|
|
95
|
+
def sign(expression) = function('sign', expression)
|
|
96
|
+
def exp(expression) = function('exp', expression)
|
|
97
|
+
def log(expression) = function('log', expression)
|
|
98
|
+
def log10(expression) = function('log10', expression)
|
|
99
|
+
def sqrt(expression) = function('sqrt', expression)
|
|
100
|
+
def acos(expression) = function('acos', expression)
|
|
101
|
+
def asin(expression) = function('asin', expression)
|
|
102
|
+
def atan(expression) = function('atan', expression)
|
|
103
|
+
def atan2(y, x) = function('atan2', y, x)
|
|
104
|
+
def cos(expression) = function('cos', expression)
|
|
105
|
+
def cot(expression) = function('cot', expression)
|
|
106
|
+
def degrees(expression) = function('degrees', expression)
|
|
107
|
+
def haversin(expression) = function('haversin', expression)
|
|
108
|
+
def radians(expression) = function('radians', expression)
|
|
109
|
+
def sin(expression) = function('sin', expression)
|
|
110
|
+
def tan(expression) = function('tan', expression)
|
|
111
|
+
def e = function('e')
|
|
112
|
+
def pi = function('pi')
|
|
113
|
+
def rand = function('rand')
|
|
114
|
+
|
|
115
|
+
# `round(value)`, `round(value, precision)` or `round(value, precision, mode)`.
|
|
116
|
+
def round(value, *arguments)
|
|
117
|
+
raise ArgumentError, 'round() takes a value, an optional precision and an optional mode' if arguments.size > 2
|
|
118
|
+
|
|
119
|
+
function('round', value, *arguments)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# --- spatial ---------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
def point(expression) = function('point', expression)
|
|
125
|
+
def cartesian(x, y) = point(MapExpression.of('x', x, 'y', y))
|
|
126
|
+
def coordinate(longitude, latitude) = point(MapExpression.of('longitude', longitude, 'latitude', latitude))
|
|
127
|
+
def within_bbox(point, lower_left, upper_right) = function('point.withinBBox', point, lower_left, upper_right)
|
|
128
|
+
|
|
129
|
+
# `distance(a, b)`; rendered as `point.distance(a, b)` on Neo4j 5.
|
|
130
|
+
def distance(point1, point2) = function('distance', point1, point2)
|
|
131
|
+
|
|
132
|
+
# --- temporal --------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
# `date()`, `date('2020-09-15')`, `date({year: 2020})`, `date($p)` or `date(timezone: 'Europe/Berlin')`;
|
|
135
|
+
# the same for datetime, localdatetime, localtime and time.
|
|
136
|
+
def date(value = nil, timezone: nil) = temporal('date', value, timezone)
|
|
137
|
+
def datetime(value = nil, timezone: nil) = temporal('datetime', value, timezone)
|
|
138
|
+
def localdatetime(value = nil, timezone: nil) = temporal('localdatetime', value, timezone)
|
|
139
|
+
def localtime(value = nil, timezone: nil) = temporal('localtime', value, timezone)
|
|
140
|
+
def time(value = nil, timezone: nil) = temporal('time', value, timezone)
|
|
141
|
+
def duration(value) = function('duration', value)
|
|
142
|
+
|
|
143
|
+
def calendar_date(year, month = nil, day = nil) = date(date_components(year:, month:, day:))
|
|
144
|
+
def week_date(year, week = nil, day_of_week = nil) = date(date_components(year:, week:, dayOfWeek: day_of_week))
|
|
145
|
+
def ordinal_date(year, ordinal_day = nil) = date(date_components(year:, ordinalDay: ordinal_day))
|
|
146
|
+
|
|
147
|
+
def quarter_date(year, quarter = nil, day_of_quarter = nil)
|
|
148
|
+
date(date_components(year:, quarter:, dayOfQuarter: day_of_quarter))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# --- graphs (composite databases) ------------------------------------
|
|
152
|
+
|
|
153
|
+
def graph_names = function('graph.names')
|
|
154
|
+
def graph_properties_by_name(name) = function('graph.propertiesByName', name)
|
|
155
|
+
def graph_by_name(name) = function('graph.byName', name)
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def distinct_function(name, *arguments)
|
|
160
|
+
FunctionInvocation.new(name, arguments.map { |a| Expression.coerce(a) }, distinct: true)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def string_side(name, expression, length)
|
|
164
|
+
raise ArgumentError, 'length might not be null when the expression is not null' if !expression.nil? && length.nil?
|
|
165
|
+
|
|
166
|
+
function(name, expression, length)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def temporal(name, value, timezone)
|
|
170
|
+
raise ArgumentError, "#{name}() takes either a value or a timezone, not both" if value && timezone
|
|
171
|
+
return function(name, MapExpression.of('timezone', timezone)) if timezone
|
|
172
|
+
|
|
173
|
+
value.nil? ? function(name) : function(name, value)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def date_components(**components)
|
|
177
|
+
raise ArgumentError, 'year is required' if components[:year].nil?
|
|
178
|
+
|
|
179
|
+
MapExpression.of(components.compact.transform_keys(&:to_s))
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# The label predicate `n:Label:Other`.
|
|
5
|
+
class HasLabelCondition < Condition
|
|
6
|
+
attr_reader :node, :labels
|
|
7
|
+
|
|
8
|
+
def initialize(node, labels)
|
|
9
|
+
super()
|
|
10
|
+
@node = node
|
|
11
|
+
@labels = labels.map(&:to_s).freeze
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# A label expression as in `(n:A|B)`, `(n:A&!B)` or `(n:(A|B)&C)`; built from
|
|
5
|
+
# {Labels.exactly} and combined with {#or}, {#and} and {#negate}.
|
|
6
|
+
class LabelExpression
|
|
7
|
+
TYPES = %i[leaf conjunction disjunction negation].freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :type, :value, :operands
|
|
10
|
+
|
|
11
|
+
def self.exactly(name) = new(:leaf, name.to_s, [])
|
|
12
|
+
|
|
13
|
+
def initialize(type, value, operands)
|
|
14
|
+
raise ArgumentError, "Unknown label expression type #{type.inspect}" unless TYPES.include?(type)
|
|
15
|
+
|
|
16
|
+
@type = type
|
|
17
|
+
@value = value
|
|
18
|
+
@operands = operands.freeze
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def or(other) = LabelExpression.new(:disjunction, nil, [self, coerce(other)])
|
|
23
|
+
def and(other) = LabelExpression.new(:conjunction, nil, [self, coerce(other)])
|
|
24
|
+
def negate = LabelExpression.new(:negation, nil, [self])
|
|
25
|
+
alias not negate
|
|
26
|
+
|
|
27
|
+
def leaf? = type == :leaf
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def coerce(other) = other.is_a?(LabelExpression) ? other : LabelExpression.exactly(other)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Factory for label expressions: `Labels.exactly('A').or('B')`.
|
|
35
|
+
module Labels
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
def exactly(name) = LabelExpression.exactly(name)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# `n:Label:Other` as the target of SET or REMOVE; `remove` marks an operation
|
|
5
|
+
# built for REMOVE, which a SET clause rejects.
|
|
6
|
+
class LabelOperation
|
|
7
|
+
attr_reader :node, :labels, :remove
|
|
8
|
+
|
|
9
|
+
def initialize(node, labels, remove: false)
|
|
10
|
+
@node = node
|
|
11
|
+
@labels = labels.map(&:to_s).freeze
|
|
12
|
+
@remove = remove
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def remove? = remove
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# `[variable IN list WHERE condition | expression]`. Built with
|
|
5
|
+
# `Cypher.list_with(name).in(list)`, refined with `where`, completed with `returning`.
|
|
6
|
+
class ListComprehension
|
|
7
|
+
include Expression
|
|
8
|
+
|
|
9
|
+
attr_reader :variable, :list, :where_condition, :projection
|
|
10
|
+
|
|
11
|
+
def initialize(variable, list: nil, where_condition: nil, projection: nil)
|
|
12
|
+
@variable = SymbolicName.of(variable)
|
|
13
|
+
@list = list
|
|
14
|
+
@where_condition = where_condition
|
|
15
|
+
@projection = projection
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def in(list) = with(list: Expression.coerce(list))
|
|
20
|
+
|
|
21
|
+
def where(condition) = with(where_condition: Condition.coerce(condition))
|
|
22
|
+
|
|
23
|
+
# Without an argument the comprehension yields the variable itself; several
|
|
24
|
+
# expressions yield a list.
|
|
25
|
+
def returning(*expressions)
|
|
26
|
+
projection = case expressions.size
|
|
27
|
+
when 0 then nil
|
|
28
|
+
when 1 then Expression.coerce(expressions.first)
|
|
29
|
+
else ListExpression.of(expressions)
|
|
30
|
+
end
|
|
31
|
+
with(projection:)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def with(**changes)
|
|
37
|
+
ListComprehension.new(variable, list:, where_condition:, projection:, **changes)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# A list literal `[a, b, c]` of expressions.
|
|
5
|
+
class ListExpression
|
|
6
|
+
include Expression
|
|
7
|
+
|
|
8
|
+
attr_reader :elements
|
|
9
|
+
|
|
10
|
+
def self.of(*elements) = new(elements.flatten.map { |element| Expression.coerce(element) })
|
|
11
|
+
|
|
12
|
+
def initialize(elements)
|
|
13
|
+
@elements = elements.freeze
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# Index and slice access on lists: `list[0]`, `list[1..3]`, `list[..2]`.
|
|
5
|
+
class ListOperator
|
|
6
|
+
include Expression
|
|
7
|
+
|
|
8
|
+
attr_reader :list, :start_index, :end_index, :slice
|
|
9
|
+
|
|
10
|
+
def self.value_at(list, index) = new(list, Expression.coerce(index), nil, false)
|
|
11
|
+
|
|
12
|
+
def self.sublist(list, start_index = nil, end_index = nil)
|
|
13
|
+
new(list, start_index && Expression.coerce(start_index), end_index && Expression.coerce(end_index), true)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(list, start_index, end_index, slice)
|
|
17
|
+
@list = list
|
|
18
|
+
@start_index = start_index
|
|
19
|
+
@end_index = end_index
|
|
20
|
+
@slice = slice
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|