activecypher 0.0.0 → 0.2.0

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_cypher/associations/collection_proxy.rb +144 -0
  3. data/lib/active_cypher/associations.rb +537 -0
  4. data/lib/active_cypher/base.rb +47 -0
  5. data/lib/active_cypher/bolt/connection.rb +525 -0
  6. data/lib/active_cypher/bolt/driver.rb +144 -0
  7. data/lib/active_cypher/bolt/handlers.rb +10 -0
  8. data/lib/active_cypher/bolt/message_reader.rb +100 -0
  9. data/lib/active_cypher/bolt/message_writer.rb +53 -0
  10. data/lib/active_cypher/bolt/messaging.rb +307 -0
  11. data/lib/active_cypher/bolt/packstream.rb +319 -0
  12. data/lib/active_cypher/bolt/result.rb +82 -0
  13. data/lib/active_cypher/bolt/session.rb +201 -0
  14. data/lib/active_cypher/bolt/transaction.rb +211 -0
  15. data/lib/active_cypher/bolt/version_encoding.rb +41 -0
  16. data/lib/active_cypher/bolt.rb +7 -0
  17. data/lib/active_cypher/connection_adapters/abstract_adapter.rb +75 -0
  18. data/lib/active_cypher/connection_adapters/abstract_bolt_adapter.rb +178 -0
  19. data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +44 -0
  20. data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +58 -0
  21. data/lib/active_cypher/connection_factory.rb +130 -0
  22. data/lib/active_cypher/connection_handler.rb +9 -0
  23. data/lib/active_cypher/connection_pool.rb +123 -0
  24. data/lib/active_cypher/connection_url_resolver.rb +137 -0
  25. data/lib/active_cypher/cypher_config.rb +50 -0
  26. data/lib/active_cypher/generators/install_generator.rb +23 -0
  27. data/lib/active_cypher/generators/node_generator.rb +32 -0
  28. data/lib/active_cypher/generators/relationship_generator.rb +33 -0
  29. data/lib/active_cypher/generators/templates/application_graph_node.rb +5 -0
  30. data/lib/active_cypher/generators/templates/application_graph_relationship.rb +5 -0
  31. data/lib/active_cypher/generators/templates/cypher_databases.yml +28 -0
  32. data/lib/active_cypher/generators/templates/node.rb.erb +10 -0
  33. data/lib/active_cypher/generators/templates/relationship.rb.erb +11 -0
  34. data/lib/active_cypher/logging.rb +44 -0
  35. data/lib/active_cypher/model/abstract.rb +87 -0
  36. data/lib/active_cypher/model/attributes.rb +24 -0
  37. data/lib/active_cypher/model/callbacks.rb +44 -0
  38. data/lib/active_cypher/model/connection_handling.rb +76 -0
  39. data/lib/active_cypher/model/connection_owner.rb +50 -0
  40. data/lib/active_cypher/model/core.rb +45 -0
  41. data/lib/active_cypher/model/countable.rb +30 -0
  42. data/lib/active_cypher/model/destruction.rb +49 -0
  43. data/lib/active_cypher/model/inspectable.rb +28 -0
  44. data/lib/active_cypher/model/persistence.rb +182 -0
  45. data/lib/active_cypher/model/querying.rb +67 -0
  46. data/lib/active_cypher/railtie.rb +34 -0
  47. data/lib/active_cypher/relation.rb +190 -0
  48. data/lib/active_cypher/relationship.rb +233 -0
  49. data/lib/active_cypher/runtime_registry.rb +8 -0
  50. data/lib/active_cypher/scoping.rb +97 -0
  51. data/lib/active_cypher/utils/logger.rb +100 -0
  52. data/lib/active_cypher/version.rb +5 -0
  53. data/lib/activecypher.rb +108 -0
  54. data/lib/cyrel/call_procedure.rb +29 -0
  55. data/lib/cyrel/clause/call.rb +46 -0
  56. data/lib/cyrel/clause/call_subquery.rb +40 -0
  57. data/lib/cyrel/clause/create.rb +33 -0
  58. data/lib/cyrel/clause/delete.rb +41 -0
  59. data/lib/cyrel/clause/limit.rb +33 -0
  60. data/lib/cyrel/clause/match.rb +40 -0
  61. data/lib/cyrel/clause/merge.rb +34 -0
  62. data/lib/cyrel/clause/order_by.rb +78 -0
  63. data/lib/cyrel/clause/remove.rb +75 -0
  64. data/lib/cyrel/clause/return.rb +90 -0
  65. data/lib/cyrel/clause/set.rb +97 -0
  66. data/lib/cyrel/clause/skip.rb +34 -0
  67. data/lib/cyrel/clause/where.rb +42 -0
  68. data/lib/cyrel/clause/with.rb +94 -0
  69. data/lib/cyrel/clause.rb +25 -0
  70. data/lib/cyrel/direction.rb +18 -0
  71. data/lib/cyrel/expression/alias.rb +27 -0
  72. data/lib/cyrel/expression/base.rb +101 -0
  73. data/lib/cyrel/expression/case.rb +45 -0
  74. data/lib/cyrel/expression/comparison.rb +60 -0
  75. data/lib/cyrel/expression/exists.rb +42 -0
  76. data/lib/cyrel/expression/function_call.rb +57 -0
  77. data/lib/cyrel/expression/literal.rb +33 -0
  78. data/lib/cyrel/expression/logical.rb +38 -0
  79. data/lib/cyrel/expression/operator.rb +27 -0
  80. data/lib/cyrel/expression/pattern_comprehension.rb +44 -0
  81. data/lib/cyrel/expression/property_access.rb +25 -0
  82. data/lib/cyrel/expression.rb +56 -0
  83. data/lib/cyrel/functions.rb +116 -0
  84. data/lib/cyrel/node.rb +397 -0
  85. data/lib/cyrel/parameterizable.rb +20 -0
  86. data/lib/cyrel/pattern/node.rb +66 -0
  87. data/lib/cyrel/pattern/path.rb +41 -0
  88. data/lib/cyrel/pattern/relationship.rb +74 -0
  89. data/lib/cyrel/pattern.rb +8 -0
  90. data/lib/cyrel/query.rb +497 -0
  91. data/lib/cyrel/return_only.rb +26 -0
  92. data/lib/cyrel/types/hash_type.rb +22 -0
  93. data/lib/cyrel/types/symbol_type.rb +13 -0
  94. data/lib/cyrel.rb +72 -0
  95. data/lib/tasks/active_cypher_tasks.rake +6 -0
  96. data/sig/activecypher.rbs +4 -0
  97. metadata +173 -10
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrel
4
+ module Expression
5
+ # Represents a function call in Cypher (e.g., id(n), count(*), coalesce(a, b)).
6
+ class FunctionCall < Base
7
+ attr_reader :function_name, :arguments, :distinct
8
+
9
+ # @param function_name [Symbol, String] The name of the Cypher function.
10
+ # @param arguments [Array<Cyrel::Expression::Base, Object>] The arguments to the function.
11
+ # @param distinct [Boolean] Whether to use the DISTINCT keyword (e.g., count(DISTINCT n)).
12
+ def initialize(function_name, arguments = [], distinct: false)
13
+ @function_name = function_name.to_s # Store as string for consistency
14
+ @arguments = Array(arguments).map do |arg|
15
+ # Don't coerce ASTERISK or existing Expressions
16
+ if arg == Functions::ASTERISK || arg.is_a?(Expression::Base)
17
+ arg
18
+ else
19
+ Expression.coerce(arg) # Coerce only non-expression literals
20
+ end
21
+ end
22
+ @distinct = distinct
23
+ end
24
+
25
+ # Renders the function call expression.
26
+ # @param query [Cyrel::Query] The query object for rendering arguments.
27
+ # @return [String] The Cypher string fragment (e.g., "id(n)", "count(DISTINCT n.prop)").
28
+ def render(query)
29
+ rendered_args = @arguments.map do |arg|
30
+ case arg
31
+ when Functions::ASTERISK
32
+ '*'
33
+ # Special handling for RawIdentifier when used as argument
34
+ when Clause::Return::RawIdentifier
35
+ arg.identifier # Render the raw identifier directly
36
+ when Expression::Base, ->(a) { a.respond_to?(:render) } # Check if it's an Expression or renderable
37
+ arg.render(query) # Render other expressions normally
38
+ else
39
+ # Parameterize other literal values
40
+ param_key = query.register_parameter(arg)
41
+ "$#{param_key}"
42
+ end
43
+ end.join(', ')
44
+ distinct_str = @distinct ? 'DISTINCT ' : ''
45
+ "#{@function_name}(#{distinct_str}#{rendered_args})"
46
+ end
47
+
48
+ # Creates an aliased version of this function call expression.
49
+ # Duplicates method from Base for robustness.
50
+ # @param alias_name [Symbol, String] The alias to assign.
51
+ # @return [Cyrel::Expression::Alias]
52
+ def as(alias_name)
53
+ Alias.new(self, alias_name)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrel
4
+ module Expression
5
+ # Represents a literal value (String, Number, Boolean, Nil, Array, Map) in a Cypher query.
6
+ # Literals are typically converted into parameters.
7
+ class Literal < Base
8
+ attr_reader :value
9
+
10
+ def initialize(value)
11
+ # We don't validate the type here extensively, assuming Neo4j driver
12
+ # or the database itself will handle type compatibility.
13
+ # We could add checks for common unsupported types if needed.
14
+ @value = value
15
+ end
16
+
17
+ # Renders the literal by registering it as a parameter.
18
+ # @param query [Cyrel::Query] The query object for parameter registration.
19
+ # @return [String] The parameter placeholder string (e.g., "$p1").
20
+ def render(query)
21
+ # Special handling for NULL as it doesn't use a parameter
22
+ return 'NULL' if @value.nil?
23
+
24
+ param_key = query.register_parameter(@value)
25
+ "$#{param_key}"
26
+ end
27
+
28
+ # Override comparison methods for direct literal comparison if needed,
29
+ # although the Base class methods creating Comparison objects are generally preferred.
30
+ # Example: def ==(other) ... end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrel
4
+ module Expression
5
+ # Represents a logical operation (AND, OR, XOR, NOT).
6
+ class Logical < Base
7
+ attr_reader :left, :operator, :right
8
+
9
+ # @param left [Cyrel::Expression::Base, Object] The left operand (or the single operand for NOT).
10
+ # @param operator [Symbol] The logical operator symbol (:AND, :OR, :XOR, :NOT).
11
+ # @param right [Cyrel::Expression::Base, Object, nil] The right operand (nil for NOT).
12
+ def initialize(left, operator, right = nil)
13
+ @operator = operator.to_s.upcase.to_sym # Ensure uppercase symbol
14
+ raise ArgumentError, "Invalid logical operator: #{@operator}" unless %i[AND OR XOR NOT].include?(@operator)
15
+
16
+ @left = Expression.coerce(left)
17
+ @right = @operator == :NOT ? nil : Expression.coerce(right)
18
+
19
+ raise ArgumentError, "Operator #{@operator} requires two operands." if @operator != :NOT && @right.nil?
20
+ return unless @operator == :NOT && !right.nil?
21
+
22
+ raise ArgumentError, 'Operator NOT requires only one operand.'
23
+ end
24
+
25
+ # Renders the logical expression.
26
+ # @param query [Cyrel::Query] The query object for rendering operands.
27
+ # @return [String] The Cypher string fragment (e.g., "((n.age > $p1) AND (n.status = $p2))").
28
+ def render(query)
29
+ if @operator == :NOT
30
+ "(#{@operator} #{@left.render(query)})"
31
+ else
32
+ # Parentheses ensure correct precedence
33
+ "(#{@left.render(query)} #{@operator} #{@right.render(query)})"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrel
4
+ module Expression
5
+ # Represents a binary arithmetic operation (e.g., +, -, *, /, %, ^).
6
+ class Operator < Base
7
+ attr_reader :left, :operator, :right
8
+
9
+ # @param left [Cyrel::Expression::Base, Object] The left operand.
10
+ # @param operator [Symbol] The arithmetic operator symbol (e.g., :+, :*).
11
+ # @param right [Cyrel::Expression::Base, Object] The right operand.
12
+ def initialize(left, operator, right)
13
+ @left = Expression.coerce(left) # Ensure operands are Expression objects
14
+ @operator = operator
15
+ @right = Expression.coerce(right)
16
+ end
17
+
18
+ # Renders the operator expression.
19
+ # @param query [Cyrel::Query] The query object for rendering operands.
20
+ # @return [String] The Cypher string fragment (e.g., "(n.age + $p1)").
21
+ def render(query)
22
+ # Parentheses ensure correct precedence
23
+ "(#{@left.render(query)} #{@operator} #{@right.render(query)})"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../expression'
4
+ require_relative '../pattern' # Need Path
5
+
6
+ module Cyrel
7
+ module Expression
8
+ # Represents a Pattern Comprehension in Cypher.
9
+ # Syntax: [ pattern WHERE condition | expression ]
10
+ # Simplified version for now: [ pattern | expression ]
11
+ class PatternComprehension < Base
12
+ attr_reader :pattern, :projection_expression # TODO: Add where_condition
13
+
14
+ # @param pattern [Cyrel::Pattern::Path, Cyrel::Pattern::Node, Cyrel::Pattern::Relationship]
15
+ # The pattern to iterate over.
16
+ # @param projection_expression [Cyrel::Expression::Base, Object]
17
+ # The expression evaluated for each match of the pattern.
18
+ def initialize(pattern, projection_expression)
19
+ unless pattern.is_a?(Cyrel::Pattern::Path) || pattern.is_a?(Cyrel::Pattern::Node) || pattern.is_a?(Cyrel::Pattern::Relationship)
20
+ raise ArgumentError,
21
+ "Pattern Comprehension pattern must be a Path, Node, or Relationship, got #{pattern.class}"
22
+ end
23
+
24
+ @pattern = pattern
25
+ @projection_expression = Expression.coerce(projection_expression)
26
+ # @where_condition = where_condition ? Expression.coerce(where_condition) : nil
27
+ end
28
+
29
+ # Renders the pattern comprehension expression.
30
+ # @param query [Cyrel::Query] The query object for rendering pattern and expression.
31
+ # @return [String] The Cypher string fragment.
32
+ def render(query)
33
+ pattern_str = @pattern.render(query)
34
+ # where_str = @where_condition ? " WHERE #{@where_condition.render(query)}" : ""
35
+ projection_str = @projection_expression.render(query)
36
+
37
+ "[#{pattern_str} | #{projection_str}]" # Simplified: missing WHERE support
38
+ end
39
+ end
40
+
41
+ # Helper function? Might be complex due to pattern definition.
42
+ # def self.comprehend(pattern, projection, where: nil) ... end
43
+ end
44
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrel
4
+ module Expression
5
+ # Represents accessing a property on a variable (node or relationship alias).
6
+ # Example: n.name, r.since
7
+ class PropertyAccess < Base
8
+ attr_reader :variable, :property_name
9
+
10
+ # @param variable [Symbol, String] The alias of the node/relationship.
11
+ # @param property_name [Symbol, String] The name of the property to access.
12
+ def initialize(variable, property_name)
13
+ @variable = variable.to_sym
14
+ @property_name = property_name.to_sym
15
+ end
16
+
17
+ # Renders the property access expression.
18
+ # @param _query [Cyrel::Query] The query object (unused for simple property access).
19
+ # @return [String] The Cypher string fragment (e.g., "n.name").
20
+ def render(_query)
21
+ "#{@variable}.#{@property_name}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Subclasses are autoloaded by Zeitwerk based on constant usage.
4
+ # Explicit requires removed.
5
+
6
+ module Cyrel
7
+ # Namespace for classes representing expressions in Cypher queries.
8
+ # Expressions are parts of a query that evaluate to a value or condition.
9
+ # Examples: property access (n.name), literals ('string', 123),
10
+ # function calls (id(n)), operators (a + b), comparisons (a > b),
11
+ # logical combinations (a AND b).
12
+ module Expression
13
+ # Base class/module for all expression types.
14
+ # Defines the common interface, primarily the `render` method.
15
+ # Base class is defined in lib/cyrel/expression/base.rb and autoloaded.
16
+
17
+ # Forces values into Expression objects like a parent shoving their kid into piano lessons—
18
+ # not because it’s fun, but because one day AI will take all our jobs
19
+ # and at least they'll have music to cry to.
20
+ # @param value [Object] The value to coerce.
21
+ # @return [Cyrel::Expression::Base] An Expression object.
22
+ def self.coerce(value)
23
+ # Assumes Base and Literal are loaded (via Zeitwerk or explicit require)
24
+ value.is_a?(Base) ? value : Literal.new(value)
25
+ end
26
+
27
+ module_function
28
+
29
+ # Accesses a property on a node or relationship.
30
+ # This is the Cypher equivalent of saying "hey buddy" and hoping the database just knows.
31
+ # @param variable [Symbol, String] The alias of the node/relationship.
32
+ # @param property_name [Symbol, String] The name of the property to access.
33
+ # @return [Cyrel::Expression::PropertyAccess]
34
+ def prop(variable, property_name)
35
+ # Assumes PropertyAccess is loaded (via Zeitwerk or explicit require)
36
+ PropertyAccess.new(variable, property_name)
37
+ end
38
+
39
+ # Helper function for creating Exists instances
40
+ # @param pattern [Cyrel::Pattern::Path, Cyrel::Pattern::Node, Cyrel::Pattern::Relationship]
41
+ # @return [Cyrel::Expression::Exists]
42
+ def exists(pattern)
43
+ # Assumes Exists is loaded (via Zeitwerk or explicit require)
44
+ Exists.new(pattern)
45
+ end
46
+
47
+ # Wraps an expression in a Cypher NOT.
48
+ # Useful when your query — and your life — needs a little more denial.
49
+ # @param expression [Cyrel::Expression::Base, Object] The expression to negate.
50
+ # @return [Cyrel::Expression::Logical]
51
+ def not(expression)
52
+ # Assumes Logical is loaded (via Zeitwerk or explicit require)
53
+ Logical.new(expression, :NOT)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrel
4
+ # Provides helper methods for creating Cypher function call expressions.
5
+ module Functions
6
+ # Represents the Cypher '*' literal, often used in count(*).
7
+ # We use a specific object to differentiate it from a string literal "*".
8
+ ASTERISK = Object.new
9
+ # Special render for asterisk
10
+ def ASTERISK.render(_query) = '*'
11
+ # Act like an expression
12
+ def ASTERISK.is_a?(klass) = klass == Cyrel::Expression::Base || super
13
+
14
+ ASTERISK.freeze
15
+
16
+ module_function
17
+
18
+ # --- Common Cypher Functions ---
19
+
20
+ # Use elementId() instead of deprecated id()
21
+ def element_id(node_variable)
22
+ Expression::FunctionCall.new(:elementId, Clause::Return::RawIdentifier.new(node_variable.to_s))
23
+ end
24
+
25
+ alias id element_id
26
+
27
+ # Because apparently, COUNT(*) isn’t obvious enough.
28
+ # Handles the 'give me everything and make it snappy' use case.
29
+ def count(expression, distinct: false)
30
+ # Handle count(*) specifically
31
+ expr_arg = case expression
32
+ when :* then ASTERISK
33
+ when Symbol, String then Clause::Return::RawIdentifier.new(expression.to_s) # Convert symbol/string to RawIdentifier
34
+ else expression # Assume it's already an Expression object (like PropertyAccess)
35
+ end
36
+ Expression::FunctionCall.new(:count, expr_arg, distinct: distinct)
37
+ end
38
+
39
+ # Retrieves labels from a node. You could also just... ask the node. But noooo.
40
+ # Instead, we call a function and pretend we're not slowly dying inside.
41
+ def labels(node_variable)
42
+ Expression::FunctionCall.new(:labels, node_variable)
43
+ end
44
+
45
+ def type(relationship_variable)
46
+ Expression::FunctionCall.new(:type, relationship_variable)
47
+ end
48
+
49
+ def properties(variable)
50
+ Expression::FunctionCall.new(:properties, variable)
51
+ end
52
+
53
+ # Returns the first non-null expression.
54
+ # The Cypher equivalent of settling.
55
+ def coalesce(*expressions)
56
+ Expression::FunctionCall.new(:coalesce, expressions)
57
+ end
58
+
59
+ def timestamp
60
+ Expression::FunctionCall.new(:timestamp)
61
+ end
62
+
63
+ def to_string(expression)
64
+ Expression::FunctionCall.new(:toString, expression)
65
+ end
66
+
67
+ # Converts a thing into an integer.
68
+ # Great for when your data is having an identity crisis and just wants to be whole again.
69
+ def to_integer(expression)
70
+ Expression::FunctionCall.new(:toInteger, expression)
71
+ end
72
+
73
+ def to_float(expression)
74
+ Expression::FunctionCall.new(:toFloat, expression)
75
+ end
76
+
77
+ def to_boolean(expression)
78
+ Expression::FunctionCall.new(:toBoolean, expression)
79
+ end
80
+
81
+ # --- Aggregation Functions ---
82
+
83
+ def sum(expression, distinct: false)
84
+ Expression::FunctionCall.new(:sum, expression, distinct: distinct)
85
+ end
86
+
87
+ def avg(expression, distinct: false)
88
+ Expression::FunctionCall.new(:avg, expression, distinct: distinct)
89
+ end
90
+
91
+ def min(expression)
92
+ Expression::FunctionCall.new(:min, expression)
93
+ end
94
+
95
+ def max(expression)
96
+ Expression::FunctionCall.new(:max, expression)
97
+ end
98
+
99
+ def collect(expression, distinct: false)
100
+ Expression::FunctionCall.new(:collect, expression, distinct: distinct)
101
+ end
102
+
103
+ # --- List Functions ---
104
+ # Add common list functions like size(), keys(), range(), etc. as needed
105
+
106
+ def size(expression)
107
+ Expression::FunctionCall.new(:size, expression)
108
+ end
109
+
110
+ # --- String Functions ---
111
+ # Add common string functions like substring(), replace(), toLower(), etc. as needed
112
+
113
+ # --- Spatial/Temporal/etc. Functions ---
114
+ # Add other function categories as required by ActiveCypher
115
+ end
116
+ end