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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: aae0b1907cab6d377de663093abb2aca70b7925b10dafc1596a50ccfc6f8a97f
|
|
4
|
+
data.tar.gz: 21ad9bce707aabfb247196a75bdfb7de97ffa215e53b210aaa2179dcf0a7b313
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 69811594f30155665847bc68fc4c66471de5e0f8ae1b1a83e39d08fd6d8426720e4206aa260f20358c395a87ba2ff2a530eafe518f7621235267d97831a05754
|
|
7
|
+
data.tar.gz: bc7823802abd14746151e12ae00050be8070be48b0fbed3ef593ef6654bdd0e6613c5c8ebdd78089d3ffbb7303830f06560cb637bb402d893ec279c57f111e43
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0.alpha.1] - 2026-10-31
|
|
11
|
+
|
|
12
|
+
First pre-release, reserving the gem name. A Ruby reimplementation of the design of
|
|
13
|
+
[Neo4j's Cypher-DSL for Java](https://github.com/neo4j/cypher-dsl), built test-first from
|
|
14
|
+
specs translated from its test suite (CypherIT, ProcedureCalls, Subqueries, parameter
|
|
15
|
+
collection, IssueRelatedIT, FunctionsIT).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Immutable statement builder with MATCH/OPTIONAL MATCH, WHERE, WITH, RETURN, ORDER BY,
|
|
20
|
+
SKIP/LIMIT, UNWIND, CREATE, MERGE (ON CREATE/ON MATCH), SET, REMOVE, DELETE, FOREACH,
|
|
21
|
+
CALL procedures (YIELD, YIELD *), CALL subqueries (imports, IN TRANSACTIONS), LOAD CSV,
|
|
22
|
+
USE, UNION/UNION ALL, FINISH, EXPLAIN/PROFILE.
|
|
23
|
+
- Patterns: nodes, relationships, chains, named paths, quantified path patterns,
|
|
24
|
+
path selectors, label expressions.
|
|
25
|
+
- Expressions: literals, parameters, maps, lists, map projections, list and pattern
|
|
26
|
+
comprehensions, list predicates, CASE, reduce(), EXISTS/COUNT/COLLECT subquery
|
|
27
|
+
expressions, the built-in function catalogue.
|
|
28
|
+
- Renderer with dialects (Neo4j 4, 5, 5.23+), pretty printing, identifier escaping,
|
|
29
|
+
parameter collection (`Statement#catalog`).
|
|
30
|
+
|
|
31
|
+
[Unreleased]: https://github.com/neo4jrb/cypher-dsl/compare/v0.1.0.alpha.1...HEAD
|
|
32
|
+
[0.1.0.alpha.1]: https://github.com/neo4jrb/cypher-dsl/releases/tag/v0.1.0.alpha.1
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Heinrich Klobuczek and contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/NOTICE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
cypher-dsl (Ruby)
|
|
2
|
+
Copyright (c) 2026 Heinrich Klobuczek and contributors
|
|
3
|
+
|
|
4
|
+
This project is a Ruby implementation of the design of Neo4j's Cypher-DSL for
|
|
5
|
+
Java (https://github.com/neo4j/cypher-dsl), Copyright (c) 2019-2026 "Neo4j,"
|
|
6
|
+
Neo4j Sweden AB, licensed under the Apache License, Version 2.0.
|
|
7
|
+
|
|
8
|
+
The specifications under spec/ are translated from the Cypher-DSL test suite and
|
|
9
|
+
are therefore a derivative work of that Apache-2.0 licensed code. A copy of the
|
|
10
|
+
Apache License, Version 2.0 is available at
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0.
|
|
12
|
+
|
|
13
|
+
The Ruby source under lib/ is original work licensed under the MIT License (see
|
|
14
|
+
LICENSE.txt).
|
data/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# cypher-dsl
|
|
2
|
+
|
|
3
|
+
A Ruby DSL (builder) for the [Cypher](https://neo4j.com/docs/cypher-manual/current/)
|
|
4
|
+
query language, modelled on the design of Neo4j's
|
|
5
|
+
[Cypher-DSL for Java](https://github.com/neo4j/cypher-dsl).
|
|
6
|
+
|
|
7
|
+
**Status: early development.** The API is being built test-first against
|
|
8
|
+
specifications translated from the Java project's test suite, so rendered
|
|
9
|
+
output is meant to match Cypher-DSL byte for byte.
|
|
10
|
+
|
|
11
|
+
## Design
|
|
12
|
+
|
|
13
|
+
- **Immutable AST.** Every builder call returns a new object; a built
|
|
14
|
+
`Statement` is a frozen tree that can be rendered, inspected or transformed.
|
|
15
|
+
- **Part-aware builder.** Cypher is a linear sequence of parts chained by
|
|
16
|
+
`WITH`, and clauses keep the order you wrote them. The builder's states
|
|
17
|
+
mirror which clause may follow which, exactly as the Java `StatementBuilder`
|
|
18
|
+
does.
|
|
19
|
+
- **Rendering is separate from the tree.** A `Renderer` turns a `Statement` into
|
|
20
|
+
Cypher text and collects parameters; options such as identifier escaping and
|
|
21
|
+
pretty printing live in a `Configuration`.
|
|
22
|
+
- **Names follow the Java API in snake_case**, so examples and tests from the
|
|
23
|
+
Java project translate mechanically:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
include CypherDSL
|
|
27
|
+
|
|
28
|
+
statement = Cypher.match(Cypher.node('S1').named(:n))
|
|
29
|
+
.where(Cypher.property(:n, :a).is_equal_to(Cypher.literal_of('A')))
|
|
30
|
+
.with(:n)
|
|
31
|
+
.match(Cypher.any_node(:n).relationship_to(Cypher.node('S2').named(:m), 'SOMEHOW_RELATED'))
|
|
32
|
+
.returning(:n, :m)
|
|
33
|
+
.build
|
|
34
|
+
|
|
35
|
+
statement.cypher
|
|
36
|
+
# => "MATCH (n:`S1`) WHERE n.a = 'A' WITH n MATCH (n)-[:`SOMEHOW_RELATED`]->(m:`S2`) RETURN n, m"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- **No ActiveSupport.** The only runtime dependency is Zeitwerk.
|
|
40
|
+
|
|
41
|
+
## Parameters
|
|
42
|
+
|
|
43
|
+
Parameters render as `$name`; give them a value and the statement hands back
|
|
44
|
+
the map a driver needs:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
statement = Cypher.match(Cypher.node('User').named(:u))
|
|
48
|
+
.where(Cypher.property(:u, :name).is_equal_to(Cypher.parameter(:name, 'Neo')))
|
|
49
|
+
.returning(:u)
|
|
50
|
+
.build
|
|
51
|
+
|
|
52
|
+
statement.cypher # => "MATCH (u:`User`) WHERE u.name = $name RETURN u"
|
|
53
|
+
statement.parameters # => {"name" => "Neo"}
|
|
54
|
+
statement.parameter_names # => ["name"]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`Cypher.anon_parameter(value)` lets the renderer pick the name (`$pcdsl01`, …).
|
|
58
|
+
Binding one name to two different values raises `ConflictingParametersException`.
|
|
59
|
+
|
|
60
|
+
## Rendering options
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
config = Renderer::Configuration.new_config
|
|
64
|
+
.with_dialect(Renderer::Dialect::NEO4J_4) # default: NEO4J_5_23
|
|
65
|
+
.always_escape_names(false) # default: true, false when pretty printing
|
|
66
|
+
.with_pretty_print(true) # one clause per line, indented blocks
|
|
67
|
+
.with_indent_style(:tab) # or .with_indent_size(4)
|
|
68
|
+
.build
|
|
69
|
+
Renderer.get_renderer(config).render(statement)
|
|
70
|
+
Renderer.get_renderer(Renderer::Configuration.pretty_printing).render(statement)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The dialect decides how `CALL { }` subqueries import variables (`CALL (u) { … }`
|
|
74
|
+
from Neo4j 5.23 on, `CALL { WITH u … }` before) and whether `elementId()` exists.
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
gem 'cypher-dsl'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Requires Ruby 3.4 or newer.
|
|
83
|
+
|
|
84
|
+
## Licence
|
|
85
|
+
|
|
86
|
+
The Ruby source is MIT licensed (see `LICENSE.txt`). The specifications are
|
|
87
|
+
translated from Cypher-DSL's Apache-2.0 test suite; see `NOTICE`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# `expression AS alias`.
|
|
5
|
+
class AliasedExpression
|
|
6
|
+
include Expression
|
|
7
|
+
|
|
8
|
+
attr_reader :delegate, :alias_name
|
|
9
|
+
|
|
10
|
+
def initialize(delegate, alias_name)
|
|
11
|
+
@delegate = delegate
|
|
12
|
+
@alias_name = SymbolicName.of(alias_name)
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# `CASE [subject] WHEN condition THEN value … [ELSE default] END`.
|
|
5
|
+
# Built with `Cypher.case_expression`, `.when(…).then(…)` pairs and `else_default`.
|
|
6
|
+
class CaseExpression
|
|
7
|
+
include Expression
|
|
8
|
+
|
|
9
|
+
attr_reader :subject, :whens, :default
|
|
10
|
+
|
|
11
|
+
def initialize(subject: nil, whens: [], default: nil)
|
|
12
|
+
@subject = subject
|
|
13
|
+
@whens = whens.freeze
|
|
14
|
+
@default = default
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def when(condition) = OngoingWhen.new(self, Expression.coerce(condition))
|
|
19
|
+
|
|
20
|
+
def else_default(default_value) = with(default: Expression.coerce(default_value))
|
|
21
|
+
|
|
22
|
+
def with(**changes) = CaseExpression.new(subject:, whens:, default:, **changes)
|
|
23
|
+
|
|
24
|
+
# A `WHEN` waiting for its `THEN`.
|
|
25
|
+
class OngoingWhen
|
|
26
|
+
def initialize(case_expression, condition)
|
|
27
|
+
@case_expression = case_expression
|
|
28
|
+
@condition = condition
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def then(value)
|
|
33
|
+
@case_expression.with(whens: @case_expression.whens + [[@condition, Expression.coerce(value)]])
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
module Clauses
|
|
5
|
+
# `CALL (imports) { body } [IN TRANSACTIONS [OF n ROWS]]`; the body is a
|
|
6
|
+
# statement or a raw Cypher fragment, `imports` a list of names or the asterisk.
|
|
7
|
+
CallSubquery = Data.define(:body, :imports, :in_transactions, :rows)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# Factory methods for clause objects, mirroring the Java `Clauses` class. Most
|
|
5
|
+
# code goes through the {StatementBuilder}; these exist for building clause
|
|
6
|
+
# lists by hand, e.g. the body of a FOREACH.
|
|
7
|
+
module Clauses
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def for_each(variable, list, clauses)
|
|
11
|
+
Foreach.new(variable: SymbolicName.of(variable), list: Expression.coerce(list), clauses: clauses.freeze)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def merge(pattern_elements, actions = [])
|
|
15
|
+
Merge.new(pattern_elements: pattern_elements.freeze, actions: actions.freeze)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def set(operations) = Set.new(operations: operations.freeze)
|
|
19
|
+
def remove(items) = Remove.new(items: items.freeze)
|
|
20
|
+
|
|
21
|
+
def delete(detach, expressions)
|
|
22
|
+
Delete.new(detach:, expressions: expressions.map { |e| Expression.coerce(e) }.freeze)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# `COLLECT { [WITH imports] statement }`: the statement must end with a RETURN of one column.
|
|
5
|
+
class CollectExpression
|
|
6
|
+
include Expression
|
|
7
|
+
|
|
8
|
+
MESSAGE = 'The final RETURN clause in a subquery used with COLLECT is mandatory and ' \
|
|
9
|
+
'the RETURN clause must return exactly one column.'
|
|
10
|
+
|
|
11
|
+
attr_reader :statement, :imports
|
|
12
|
+
|
|
13
|
+
def initialize(statement, imports = [])
|
|
14
|
+
raise ArgumentError, MESSAGE unless CollectExpression.single_column?(statement)
|
|
15
|
+
|
|
16
|
+
@statement = statement
|
|
17
|
+
@imports = imports.freeze
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.single_column?(statement)
|
|
22
|
+
return statement.statements.all? { |inner| single_column?(inner) } if statement.is_a?(UnionQuery)
|
|
23
|
+
|
|
24
|
+
last = statement.clauses.last
|
|
25
|
+
last.is_a?(Clauses::Return) && last.items.size == 1
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# A binary comparison (`a = b`, `a =~ b`, …) or a unary one (`a IS NULL`).
|
|
5
|
+
class Comparison < Condition
|
|
6
|
+
attr_reader :left, :operator, :right
|
|
7
|
+
|
|
8
|
+
def initialize(left, operator, right)
|
|
9
|
+
super()
|
|
10
|
+
@left = left
|
|
11
|
+
@operator = operator
|
|
12
|
+
@right = right
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def unary? = right.nil?
|
|
17
|
+
|
|
18
|
+
# Negating a null check flips it instead of wrapping it in NOT.
|
|
19
|
+
def not
|
|
20
|
+
case operator
|
|
21
|
+
when 'IS NULL' then Comparison.new(left, 'IS NOT NULL', nil)
|
|
22
|
+
when 'IS NOT NULL' then Comparison.new(left, 'IS NULL', nil)
|
|
23
|
+
else super
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# Conditions joined by AND / OR / XOR, or negated with NOT. Rendered in
|
|
5
|
+
# parentheses; chaining the same operator flattens into one group and empty
|
|
6
|
+
# conditions drop out.
|
|
7
|
+
class CompoundCondition < Condition
|
|
8
|
+
attr_reader :operator, :conditions
|
|
9
|
+
|
|
10
|
+
def self.create(operator, left, right)
|
|
11
|
+
conditions = [left, right].flat_map do |condition|
|
|
12
|
+
case condition
|
|
13
|
+
when NoCondition then []
|
|
14
|
+
when CompoundCondition then condition.operator == operator ? condition.conditions : [condition]
|
|
15
|
+
else [condition]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
case conditions.size
|
|
19
|
+
when 0 then NoCondition::INSTANCE
|
|
20
|
+
when 1 then conditions.first
|
|
21
|
+
else new(operator, conditions)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(operator, conditions)
|
|
26
|
+
super()
|
|
27
|
+
@operator = operator
|
|
28
|
+
@conditions = conditions.freeze
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def negation? = operator == 'NOT'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# Base class of boolean expressions; supports chaining with `and`, `or`, `xor` and `not`.
|
|
5
|
+
# Relationship patterns are accepted wherever a condition is expected.
|
|
6
|
+
class Condition
|
|
7
|
+
include Expression
|
|
8
|
+
|
|
9
|
+
def self.coerce(value)
|
|
10
|
+
case value
|
|
11
|
+
when Condition then value
|
|
12
|
+
when Relationship, RelationshipChain then RelationshipPatternCondition.new(value)
|
|
13
|
+
when FunctionInvocation then ExpressionCondition.new(value) # boolean functions such as isEmpty()
|
|
14
|
+
else raise ArgumentError, "#{value.inspect} is not a condition"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def and(other) = combine('AND', other)
|
|
19
|
+
def or(other) = combine('OR', other)
|
|
20
|
+
def xor(other) = combine('XOR', other)
|
|
21
|
+
def not = CompoundCondition.new('NOT', [self])
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Chaining conditions flattens same-operator chains; chaining a relationship
|
|
26
|
+
# pattern (the Java overload) wraps without flattening, as upstream does.
|
|
27
|
+
def combine(operator, other)
|
|
28
|
+
return CompoundCondition.new(operator, [self, Condition.coerce(other)]) if pattern?(other)
|
|
29
|
+
|
|
30
|
+
CompoundCondition.create(operator, self, Condition.coerce(other))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def pattern?(value) = value.is_a?(Relationship) || value.is_a?(RelationshipChain)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# Raised when one parameter name is bound to different values within a
|
|
5
|
+
# statement, or bound in one place and left unbound in another.
|
|
6
|
+
class ConflictingParametersException < StandardError
|
|
7
|
+
# `name => Set` of the conflicting values; an unbound use is {Parameter::NO_VALUE}.
|
|
8
|
+
attr_reader :erroneous_parameters
|
|
9
|
+
|
|
10
|
+
def initialize(erroneous_parameters)
|
|
11
|
+
@erroneous_parameters = erroneous_parameters.freeze
|
|
12
|
+
super("Conflicting parameter values for #{erroneous_parameters.keys.join(', ')}")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# The constant conditions `true` and `false`.
|
|
5
|
+
class ConstantCondition < Condition
|
|
6
|
+
attr_reader :value
|
|
7
|
+
|
|
8
|
+
def initialize(value)
|
|
9
|
+
super()
|
|
10
|
+
@value = value
|
|
11
|
+
freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
TRUE = new(true)
|
|
15
|
+
FALSE = new(false)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypherDSL
|
|
4
|
+
# `COUNT { pattern }` / `COUNT { statement }`.
|
|
5
|
+
class CountExpression
|
|
6
|
+
include Expression
|
|
7
|
+
|
|
8
|
+
attr_reader :body, :imports
|
|
9
|
+
|
|
10
|
+
def initialize(body, imports = [])
|
|
11
|
+
@body = body
|
|
12
|
+
@imports = imports.freeze
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|