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.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +32 -0
  3. data/LICENSE.txt +21 -0
  4. data/NOTICE +14 -0
  5. data/README.md +87 -0
  6. data/lib/cypher_dsl/aliased_expression.rb +16 -0
  7. data/lib/cypher_dsl/asterisk.rb +10 -0
  8. data/lib/cypher_dsl/case_expression.rb +37 -0
  9. data/lib/cypher_dsl/clauses/call.rb +8 -0
  10. data/lib/cypher_dsl/clauses/call_subquery.rb +9 -0
  11. data/lib/cypher_dsl/clauses/create.rb +8 -0
  12. data/lib/cypher_dsl/clauses/delete.rb +8 -0
  13. data/lib/cypher_dsl/clauses/finish.rb +8 -0
  14. data/lib/cypher_dsl/clauses/foreach.rb +8 -0
  15. data/lib/cypher_dsl/clauses/load_csv.rb +8 -0
  16. data/lib/cypher_dsl/clauses/match.rb +8 -0
  17. data/lib/cypher_dsl/clauses/merge.rb +8 -0
  18. data/lib/cypher_dsl/clauses/order.rb +8 -0
  19. data/lib/cypher_dsl/clauses/raw_return.rb +8 -0
  20. data/lib/cypher_dsl/clauses/remove.rb +8 -0
  21. data/lib/cypher_dsl/clauses/return.rb +8 -0
  22. data/lib/cypher_dsl/clauses/set.rb +8 -0
  23. data/lib/cypher_dsl/clauses/unwind.rb +8 -0
  24. data/lib/cypher_dsl/clauses/use.rb +8 -0
  25. data/lib/cypher_dsl/clauses/with.rb +8 -0
  26. data/lib/cypher_dsl/clauses.rb +25 -0
  27. data/lib/cypher_dsl/collect_expression.rb +28 -0
  28. data/lib/cypher_dsl/comparison.rb +27 -0
  29. data/lib/cypher_dsl/compound_condition.rb +34 -0
  30. data/lib/cypher_dsl/condition.rb +35 -0
  31. data/lib/cypher_dsl/conflicting_parameters_exception.rb +15 -0
  32. data/lib/cypher_dsl/constant_condition.rb +17 -0
  33. data/lib/cypher_dsl/count_expression.rb +16 -0
  34. data/lib/cypher_dsl/cypher.rb +210 -0
  35. data/lib/cypher_dsl/exists_condition.rb +15 -0
  36. data/lib/cypher_dsl/expression.rb +69 -0
  37. data/lib/cypher_dsl/expression_condition.rb +14 -0
  38. data/lib/cypher_dsl/function_invocation.rb +17 -0
  39. data/lib/cypher_dsl/functions.rb +182 -0
  40. data/lib/cypher_dsl/has_label_condition.rb +15 -0
  41. data/lib/cypher_dsl/label_expression.rb +40 -0
  42. data/lib/cypher_dsl/label_operation.rb +18 -0
  43. data/lib/cypher_dsl/list_comprehension.rb +40 -0
  44. data/lib/cypher_dsl/list_expression.rb +17 -0
  45. data/lib/cypher_dsl/list_operator.rb +24 -0
  46. data/lib/cypher_dsl/list_predicate.rb +32 -0
  47. data/lib/cypher_dsl/literal.rb +29 -0
  48. data/lib/cypher_dsl/map_expression.rb +32 -0
  49. data/lib/cypher_dsl/map_projection.rb +58 -0
  50. data/lib/cypher_dsl/merge_action.rb +11 -0
  51. data/lib/cypher_dsl/named_path.rb +59 -0
  52. data/lib/cypher_dsl/no_condition.rb +14 -0
  53. data/lib/cypher_dsl/node.rb +83 -0
  54. data/lib/cypher_dsl/operation.rb +22 -0
  55. data/lib/cypher_dsl/parameter.rb +40 -0
  56. data/lib/cypher_dsl/pattern_comprehension.rb +31 -0
  57. data/lib/cypher_dsl/pattern_expression.rb +16 -0
  58. data/lib/cypher_dsl/property.rb +24 -0
  59. data/lib/cypher_dsl/quantified_path_pattern.rb +37 -0
  60. data/lib/cypher_dsl/raw_expression.rb +15 -0
  61. data/lib/cypher_dsl/reduction.rb +33 -0
  62. data/lib/cypher_dsl/relationship.rb +97 -0
  63. data/lib/cypher_dsl/relationship_chain.rb +44 -0
  64. data/lib/cypher_dsl/relationship_pattern_condition.rb +14 -0
  65. data/lib/cypher_dsl/renderer/configuration.rb +66 -0
  66. data/lib/cypher_dsl/renderer/dialect.rb +20 -0
  67. data/lib/cypher_dsl/renderer/escaper.rb +31 -0
  68. data/lib/cypher_dsl/renderer/visitor/expressions.rb +274 -0
  69. data/lib/cypher_dsl/renderer/visitor/patterns.rb +125 -0
  70. data/lib/cypher_dsl/renderer/visitor.rb +298 -0
  71. data/lib/cypher_dsl/renderer.rb +28 -0
  72. data/lib/cypher_dsl/sort_item.rb +17 -0
  73. data/lib/cypher_dsl/standalone_call.rb +61 -0
  74. data/lib/cypher_dsl/statement.rb +56 -0
  75. data/lib/cypher_dsl/statement_builder.rb +294 -0
  76. data/lib/cypher_dsl/statement_catalog.rb +25 -0
  77. data/lib/cypher_dsl/subquery_with.rb +15 -0
  78. data/lib/cypher_dsl/symbolic_name.rb +48 -0
  79. data/lib/cypher_dsl/unary_operation.rb +16 -0
  80. data/lib/cypher_dsl/union_query.rb +29 -0
  81. data/lib/cypher_dsl/version.rb +5 -0
  82. data/lib/cypher_dsl.rb +13 -0
  83. 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