arcadedb 0.5.0 → 0.5.4

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.
@@ -0,0 +1,211 @@
1
+ module Arcade
2
+ # Abstraction of ArcadeDB's native TimeSeries types.
3
+ #
4
+ # A TimeSeries type is not a vertex/document: it has no rid and carries no edges.
5
+ # Membership is expressed through tags, values through typed fields.
6
+ #
7
+ # class Ohlcv < Arcade::Timeseries
8
+ # timestamp :ts, precision: :second
9
+ # tags symbol: String, currency: String
10
+ # fields open: Float, high: Float, low: Float, close: Float, volume: Integer
11
+ # end
12
+ #
13
+ # Ohlcv.create_type
14
+ # Ohlcv.write symbol: 'SAPX:GER', ts: Date.today, open: 1, high: 2, low: 0.5, close: 1.5, volume: 100
15
+ # Ohlcv.range from: '2026-01-01', tags: { symbol: 'SAPX:GER' }
16
+ # Ohlcv.latest tags: { symbol: 'SAPX:GER' }
17
+ # Ohlcv.bucket '7d', from: '2026-01-01', aggregate: { close: :last, volume: :sum }
18
+ #
19
+ # Conventions:
20
+ # * Writes go through the InfluxDB Line Protocol endpoint (fastest remote path);
21
+ # reads go through SQL (range pushdown, ts.* functions).
22
+ # * Timestamps are accepted as Time, DateTime, Date, ISO-String or epoch Numeric
23
+ # and normalised to the declared precision — never pass raw numbers of a
24
+ # different unit than declared, the engine will not complain but read wrong.
25
+ # * The server's DELETE on timeseries types reports a count but leaves the rows
26
+ # (ArcadeDB 26.10.1) — there is deliberately no window-replace helper here.
27
+ # Re-writing a timestamp appends a second sample; deduplicate on read if needed.
28
+ class Timeseries < Base
29
+
30
+ TYPES = { String => 'STRING', Float => 'DOUBLE', Integer => 'LONG',
31
+ :bool => 'BOOLEAN', :datetime => 'DATETIME' }.freeze
32
+
33
+ # SQL keyword and nanosecond-multiplier per supported precision
34
+ PRECISIONS = { second: ['SECOND', 1],
35
+ millisecond: ['MILLISECOND', 1_000],
36
+ microsecond: ['MICROSECOND', 1_000_000],
37
+ nanosecond: ['NANOSECOND', 1_000_000_000] }.freeze
38
+
39
+ LP_PRECISION = { second: 's', millisecond: 'ms', microsecond: 'us', nanosecond: 'ns' }.freeze
40
+
41
+ # ts.<fn>(field, ts) where the engine needs the timestamp, plain SQL otherwise
42
+ AGGREGATES = { last: ->(f, ts){ "ts.last(#{ f }, #{ ts })" },
43
+ first: ->(f, ts){ "ts.first(#{ f }, #{ ts })" },
44
+ avg: ->(f, _){ "avg(#{ f })" },
45
+ sum: ->(f, _){ "sum(#{ f })" },
46
+ min: ->(f, _){ "min(#{ f })" },
47
+ max: ->(f, _){ "max(#{ f })" },
48
+ count: ->(f, _){ "count(#{ f })" } }.freeze
49
+
50
+ class << self
51
+
52
+ # ---- schema declaration -----------------------------------------------------------
53
+
54
+ def timestamp name = nil, precision: nil, shards: nil, retention: nil
55
+ if name.nil? && precision.nil? # getter use: ts_name via timestamp()
56
+ @ts_name ||= :ts
57
+ @ts_precision ||= :second
58
+ return @ts_name
59
+ end
60
+ @ts_name, @ts_precision = name, (precision || :second)
61
+ @ts_shards, @ts_retention = shards, retention
62
+ end
63
+
64
+ def tags spec = nil
65
+ @ts_tags = spec if spec
66
+ @ts_tags ||= {}
67
+ end
68
+
69
+ def fields spec = nil
70
+ @ts_fields = spec if spec
71
+ @ts_fields ||= {}
72
+ end
73
+
74
+ def ts_name = timestamp
75
+ def ts_precision = @ts_precision ||= :second
76
+ def type_name = database_name
77
+
78
+ # ---- type lifecycle ---------------------------------------------------------------
79
+
80
+ def create_type
81
+ return true if type_exists?
82
+ tag_cols = tags.map { |k, t| "#{ k } #{ sql_type t }" }.join(', ')
83
+ field_cols = fields.map { |k, t| "#{ k } #{ sql_type t }" }.join(', ')
84
+ cmd = "CREATE TIMESERIES TYPE #{ type_name } " \
85
+ "TIMESTAMP #{ ts_name } PRECISION #{ PRECISIONS[ts_precision].first } " \
86
+ "TAGS (#{ tag_cols }) FIELDS (#{ field_cols })"
87
+ cmd += " SHARDS #{ @ts_shards }" if @ts_shards
88
+ cmd += " RETENTION #{ @ts_retention }" if @ts_retention
89
+ result = db.transmit { cmd }
90
+ # the engine registers the type asynchronously — wait until readable
91
+ # (writes immediately after create otherwise fail with unknownTypes)
92
+ 10.times { break if type_exists?; sleep 0.2 }
93
+ result
94
+ end
95
+
96
+ # NB: schema:types does NOT list timeseries types on 26.10 —
97
+ # probe the ts/query endpoint instead (400 unknownTypes = absent).
98
+ def type_exists?
99
+ Api.post_json "ts/#{ db.database }/query", { type: type_name.to_s }
100
+ true
101
+ rescue Arcade::QueryError
102
+ false
103
+ end
104
+
105
+ def drop_type
106
+ db.transmit { "DROP TIMESERIES TYPE #{ type_name }" }
107
+ end
108
+
109
+ # ---- writing ----------------------------------------------------------------------
110
+
111
+ # one sample: Ohlcv.write symbol: 'SAP', ts: Date.today, close: 1.5
112
+ def write **sample
113
+ write_many [sample]
114
+ end
115
+
116
+ # batch: array of hashes, one per sample (Line Protocol, precision param matches class)
117
+ def write_many samples
118
+ return 0 if samples.empty?
119
+ lines = samples.map { |s| line_for s }
120
+ Api.post_text "ts/#{ db.database }/write?precision=#{ LP_PRECISION[ts_precision] }", lines.join("\n")
121
+ end
122
+
123
+ # ---- reading ----------------------------------------------------------------------
124
+
125
+ # raw samples within [from, to] (both optional), filtered by tag equality
126
+ def range from: nil, to: nil, tags: {}, columns: nil, limit: nil
127
+ projections = columns ? ([ts_name] + Array(columns)).join(', ') : '*'
128
+ conditions = []
129
+ conditions << "#{ ts_name } >= #{ sql_time from }" if from
130
+ conditions << "#{ ts_name } <= #{ sql_time to }" if to
131
+ tags.each { |k, v| conditions << "#{ k } = '#{ v }'" }
132
+ sql = "SELECT #{ projections } FROM #{ type_name }"
133
+ sql += " WHERE #{ conditions.join ' AND ' }" unless conditions.empty?
134
+ sql += " ORDER BY #{ ts_name }"
135
+ sql += " LIMIT #{ limit }" if limit
136
+ db.transmit { sql }
137
+ end
138
+
139
+ # the most recent sample, via the dedicated latest-endpoint
140
+ # returns a Hash of column => value (nil when the series is empty)
141
+ def latest tags: {}
142
+ query = "ts/#{ db.database }/latest?type=#{ type_name }"
143
+ query += "&tag=#{ tags.first.join ':' }" unless tags.empty?
144
+ answer = Api.get_json query
145
+ return nil if answer[:latest].nil?
146
+ answer[:columns].map.with_index { |col, i| [ col.to_sym, answer[:latest][i] ] }.to_h
147
+ end
148
+
149
+ # time-bucketed aggregation, e.g. weekly candles:
150
+ # Ohlcv.bucket '7d', aggregate: { close: :last, high: :max, low: :min, volume: :sum }
151
+ def bucket interval, from: nil, to: nil, tags: {}, aggregate: {}
152
+ measures = aggregate.map do |field, fn|
153
+ "#{ AGGREGATES.fetch(fn).call(field, ts_name) } as #{ field }_#{ fn }"
154
+ end.join(', ')
155
+ conditions = []
156
+ conditions << "#{ ts_name } >= #{ sql_time from }" if from
157
+ conditions << "#{ ts_name } <= #{ sql_time to }" if to
158
+ tags.each { |k, v| conditions << "#{ k } = '#{ v }'" }
159
+ sql = "SELECT ts.timeBucket('#{ interval }', #{ ts_name }) as `bucket`"
160
+ sql += ", #{ measures }" unless measures.empty?
161
+ sql += " FROM #{ type_name }"
162
+ sql += " WHERE #{ conditions.join ' AND ' }" unless conditions.empty?
163
+ sql += " GROUP BY `bucket` ORDER BY `bucket`"
164
+ db.transmit { sql }
165
+ end
166
+
167
+ private
168
+
169
+ # ---- normalisation ------------------------------------------------------------------
170
+
171
+ def sql_type type
172
+ TYPES.fetch(type) { type.to_s.upcase }
173
+ end
174
+
175
+ # sample hash -> InfluxDB line, epoch normalised to the declared precision
176
+ def line_for sample
177
+ stamp = to_epoch sample[ts_name] || sample[ts_name.to_s]
178
+ raise ArgumentError, "#{ type_name }: missing timestamp '#{ ts_name }'" if stamp.nil?
179
+ tag_part = tags.keys.map { |t| "#{ t }=#{ escape_tag sample[t] }" }.join(',')
180
+ field_part = fields.keys.map { |f| "#{ f }=#{ sample[f] }" }.join(',')
181
+ "#{ type_name },#{ tag_part } #{ field_part } #{ stamp }"
182
+ end
183
+
184
+ def to_epoch value
185
+ seconds = case value
186
+ when Time then value.to_i
187
+ when Numeric then value.to_i # already epoch seconds
188
+ when Date then Time.utc(value.year, value.month, value.day).to_i
189
+ when String then Time.parse(value).to_i
190
+ else nil end
191
+ return nil if seconds.nil?
192
+ seconds * PRECISIONS[ts_precision].last
193
+ end
194
+
195
+ def sql_time value
196
+ t = case value
197
+ when Time then value
198
+ when Date then Time.utc(value.year, value.month, value.day)
199
+ when String then Time.parse(value, in: 'UTC')
200
+ when Numeric then Time.at(value)
201
+ else raise ArgumentError, "unsupported time: #{ value.inspect }" end
202
+ "'#{ t.utc.strftime('%Y-%m-%d %H:%M:%S') }'"
203
+ end
204
+
205
+ def escape_tag value
206
+ value.to_s.gsub(/([,= ])/) { '\\' + $1 }
207
+ end
208
+
209
+ end
210
+ end
211
+ end
@@ -1,3 +1,3 @@
1
1
  module Arcade
2
- VERSION = "0.5.0"
2
+ VERSION = "0.5.4"
3
3
  end
data/lib/arcade.rb CHANGED
@@ -9,13 +9,13 @@ require "dry/core/class_builder"
9
9
  require "dry/core/class_attributes"
10
10
  require 'json'
11
11
 
12
- module Types
13
- include Dry.Types()
14
- end
12
+ require_relative '../lib/types'
13
+ #module Types
14
+ # include Dry.Types()
15
+ #end
15
16
  require 'yaml'
16
17
  require 'securerandom'
17
18
  require 'httpx'
18
- require 'arcade/errors'
19
19
  require_relative '../lib/arcade/support/object'
20
20
  require_relative '../lib/arcade/support/string'
21
21
  require_relative '../lib/arcade/support/class'
@@ -24,13 +24,16 @@ require_relative '../lib/arcade/support/model'
24
24
  require_relative '../lib/arcade/support/conversions'
25
25
  require_relative '../lib/arcade/api/primitives'
26
26
  require_relative '../lib/arcade/api/operations'
27
+ require_relative '../lib/arcade/errors'
27
28
  require_relative '../lib/arcade/base'
28
29
  require_relative '../lib/arcade/logging'
29
30
  require_relative '../lib/arcade/config'
30
31
  require_relative '../lib/arcade/database'
31
32
  require_relative '../lib/arcade/match'
32
33
  require_relative '../lib/arcade/query'
34
+ require_relative '../lib/arcade/timeseries'
33
35
  require_relative '../lib/arcade/init'
36
+ require_relative '../lib/arcade/rag_schema'
34
37
  require_relative "../lib/models"
35
38
  require_relative '../lib/railtie' if defined? Rails::Railtie
36
39
 
@@ -11,7 +11,7 @@ module Arcade
11
11
  end
12
12
 
13
13
  =begin
14
- Document.delete fires a "delete vertex" command to the database.
14
+ Document.delete fires a "delete" command to the database.
15
15
  To remove all records use »all: true« as argument
16
16
 
17
17
  The "where" parameter is optional
data/lib/model/edge.rb CHANGED
@@ -27,6 +27,23 @@ module Arcade
27
27
 
28
28
  end
29
29
 
30
+
31
+ ## class-method: Delete Edge between two vertices
32
+ ##
33
+ ## returns the count of deleted edges
34
+ ##
35
+ ## todo: add conditions
36
+ def self.delete from:, to:
37
+ return 0 if from.nil? || to.nil?
38
+ raise "parameter ( from: ) must be a String or a Vertex" unless from.is_a?(String) || from.is_a?( Arcade::Vertex)
39
+ raise "parameter ( to: ) must be a String or a Vertex" unless to.is_a?(String) || to.is_a?( Arcade::Vertex)
40
+ raise "parameters (from: + to:) must respond to `.rid`" unless from.respond_to?( :rid) && to.respond_to?(:rid)
41
+
42
+ db.execute{ "delete edge from #{from.rid} to #{to.rid} " } &.select_result &.first
43
+ end
44
+
45
+
46
+ ## instance method: Delete specified edge
30
47
  def delete
31
48
  db.execute{ "delete edge #{ rid }" }.select_result
32
49
  end
@@ -0,0 +1,41 @@
1
+ module Arcade
2
+ class Revision < Arcade::Vertex
3
+ attribute :protocol?, Types::Nominal::Array
4
+
5
+ @@u='root' # default user
6
+ @@i = 'Record initiated'
7
+
8
+ def self.set_user u
9
+ @@u = u
10
+ end
11
+
12
+ def self.set_initial_message m
13
+ @@i = m
14
+ end
15
+
16
+ def self.insert **attr
17
+
18
+ action = block_given? ? yield : @@i
19
+ i= super **attr
20
+ i.update_list :protocol,
21
+ Arcade::RevisionRecord.create( user: @@u, action: action ),
22
+ modus: :first
23
+ i.refresh
24
+
25
+ end
26
+
27
+
28
+ def update **attr
29
+ hist_state = attr.keys.map do |k|
30
+ [ k, send( k ) ]
31
+ end.to_h
32
+ super **attr
33
+ revision = if block_given?
34
+ Arcade::RevisionRecord.create( user: @@u, action: yield, fields: hist_state )
35
+ else
36
+ Arcade::RevisionRecord.create( fields: hist_state )
37
+ end
38
+ update_list :protocol, revision, modus: :append
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ module Arcade
2
+ class RevisionRecord < Document
3
+ attribute :user, Types::Nominal::String.default( 'root'.freeze )
4
+ attribute :action, Types::Nominal::String.default( "Property changed".freeze )
5
+ attribute :date, Types::Nominal::Date.default( Date.today.freeze )
6
+ attribute :fields?, Types::Nominal::Hash
7
+
8
+ # Revision Records are always embedded.
9
+ # create and insert methods return an new ruby record
10
+ def self.create **attr
11
+ new **( { rid: '#0:0', date: Date.today}.merge attr )
12
+ end
13
+ def self.insert **attr
14
+ new **( { rid: '#0:0', date: Date.today}.merge **attr )
15
+ end
16
+
17
+ =begin
18
+ Its not allowed to delete records.
19
+ =end
20
+ def self.delete where: {} , **args
21
+ raise ArcadeQueryError, "Its not possible to delete revision records"
22
+ end
23
+
24
+ def delete
25
+ raise ArcadeQueryError, "Its not possible to delete revision records"
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ module Arcade
2
+ class Timeseries < Arcade::Base
3
+ # base class for user model classes; behaviour lives in Arcade::Timeseries (lib/arcade/timeseries.rb)
4
+ # the constant Arcade::Timeseries IS this class — the arcade-side file reopens it with full behaviour.
5
+ end
6
+ end
data/lib/model/vertex.rb CHANGED
@@ -213,6 +213,50 @@ or
213
213
  nil
214
214
  end
215
215
 
216
+ =begin
217
+ ----------------------------------- Match ------------------------------------------
218
+ (ClassMethod)
219
+
220
+ Creates a Match-Statement based on the vertex type
221
+
222
+ ```
223
+ m = Watchlist.match( symbol: 'iBit', as: :ibit )
224
+ .out( HasUnderlying)
225
+ .node( as: :u )
226
+
227
+ m.to_s
228
+ => "MATCH { type: watchlist, where: ( symbol='iBit' ), as: ibit }
229
+ .out('has_underlying'){ as: u }
230
+ RETURN ibit,u "
231
+ ```
232
+ =end
233
+ def self.match **args
234
+ as = args.delete( :as) || :a
235
+ args = args.delete( :where ) if args.key?( :where )
236
+ Arcade::Match.new type: self, where: args, as: as
237
+ end
238
+
239
+ =begin
240
+ ----------------------------------- Match ------------------------------------------
241
+ (InstanceMethod)
242
+
243
+ Creates a Match-Statement based on the current vertex ( i.e as response to a website-request )
244
+
245
+ ```
246
+ params[:rid] => "#49:0"
247
+
248
+ m= params[:rid].load_rid.match.out( HasUnterlying ).node( as: :u )
249
+ m.to_s
250
+ => "MATCH { type: strategie, rid: #49:0 }.out('has_underlying'){ as: u } RETURN u "
251
+ ```
252
+ =end
253
+ def match as: nil
254
+ if as.nil?
255
+ Arcade::Match.new vertex: self
256
+ else
257
+ Arcade::Match.new vertex: self, as: as
258
+ end
259
+ end
216
260
 
217
261
  def remove
218
262
  db.execute{ "delete vertex #{rid}" }
@@ -224,7 +268,7 @@ Format: < Classname: Edges, Attributes >
224
268
  =end
225
269
  def to_human
226
270
 
227
- in_and_out = "{#{inE.count}->}{->#{outE.count }}, "
271
+ in_and_out = coe.then{| i,o| "{#{i}->}{->#{o}}, "}
228
272
 
229
273
  #Default presentation of Arcade::Base::Model-Objects
230
274
 
@@ -244,6 +288,13 @@ Format: < Classname: Edges, Attributes >
244
288
  end
245
289
 
246
290
 
291
+ # Count Of Edges (coe)
292
+ # returns an Array: [ count-of-in-edges, count-of-out-edges ]
293
+ def coe
294
+ db.query( "select in().size() as ie, out().size() as oe from #{rid}" ) &.first.values
295
+ end
296
+
297
+
247
298
  def refresh
248
299
  # force reloading of edges and nodes
249
300
  # edges are not cached (now)
data/lib/models.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  require_relative "../lib/model/document.rb"
2
+ require_relative "../lib/model/revision_record.rb"
2
3
  require_relative "../lib/model/vertex.rb"
3
4
  require_relative "../lib/model/edge.rb"
5
+ require_relative "../lib/model/revision.rb"
4
6
  require_relative "../lib/model/basicvertex.rb"
5
7
  require_relative "../lib/model/basicdocument.rb"
6
8
  require_relative "../lib/model/basicedge.rb"
9
+ require_relative "../lib/model/timeseries.rb"
data/lib/types.rb ADDED
@@ -0,0 +1,35 @@
1
+ module Types
2
+ include Dry.Types()
3
+
4
+ # include in attribute definitions
5
+ Rid = String.constrained( format: /\A[#]{1}[0-9]{1,}:[0-9]{1,}\z/ )
6
+ Blockchain = String.constrained( format: /^(algo|eth|btc)$/ ) # add other blockchain symbols here
7
+ Email = String.constrained( format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i )
8
+
9
+
10
+ # Define a type for your DateTime with timezone. Crucially, use
11
+ # the `.constructor` to handle the string parsing and potential errors. (source: Gemini)
12
+ Types::DateTime = Types::Any.constructor do |value|
13
+ begin
14
+ case value
15
+ when String
16
+ ::DateTime.parse(value)
17
+ when Integer
18
+ ::Time.at(value).to_datetime
19
+ else
20
+ ::DateTime.now
21
+ end
22
+ rescue ArgumentError
23
+ # Handle invalid date strings. Options:
24
+ # 1. Raise an exception (recommended for API responses)
25
+ raise Dry::Types::CoercionError.new(
26
+ value, DateTime, "Invalid DateTime format: #{value}"
27
+ )
28
+ # 2. Return nil (less strict, but might lead to unexpected behavior)
29
+ # nil
30
+ # 3. Use a default value (be careful with this, as it might mask errors)
31
+ # DateTime.now
32
+ end
33
+ end
34
+
35
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arcadedb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hartmut Bischoff
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-12-12 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bundler
@@ -134,6 +133,7 @@ files:
134
133
  - Gemfile.lock
135
134
  - Guardfile
136
135
  - LICENSE
136
+ - RAG_SCHEMA.md
137
137
  - README.md
138
138
  - Rakefile
139
139
  - arcade.yml
@@ -141,6 +141,8 @@ files:
141
141
  - bin/+
142
142
  - bin/console
143
143
  - examples/books.rb
144
+ - examples/rag_example.rb
145
+ - examples/rag_usecase_demo.rb
144
146
  - examples/relation_1__1.rb
145
147
  - examples/relation_1__n.rb
146
148
  - examples/relation_n_n.rb
@@ -158,20 +160,26 @@ files:
158
160
  - lib/arcade/logging.rb
159
161
  - lib/arcade/match.rb
160
162
  - lib/arcade/query.rb
163
+ - lib/arcade/rag_schema.rb
161
164
  - lib/arcade/support/class.rb
162
165
  - lib/arcade/support/conversions.rb
163
166
  - lib/arcade/support/model.rb
164
167
  - lib/arcade/support/object.rb
165
168
  - lib/arcade/support/sql.rb
166
169
  - lib/arcade/support/string.rb
170
+ - lib/arcade/timeseries.rb
167
171
  - lib/arcade/version.rb
168
172
  - lib/model/basicdocument.rb
169
173
  - lib/model/basicedge.rb
170
174
  - lib/model/basicvertex.rb
171
175
  - lib/model/document.rb
172
176
  - lib/model/edge.rb
177
+ - lib/model/revision.rb
178
+ - lib/model/revision_record.rb
179
+ - lib/model/timeseries.rb
173
180
  - lib/model/vertex.rb
174
181
  - lib/models.rb
182
+ - lib/types.rb
175
183
  - rails.md
176
184
  - rails/arcade.rb
177
185
  - rails/config.yml
@@ -180,7 +188,6 @@ homepage: https://github.com/topofocus/arcadedb
180
188
  licenses:
181
189
  - MIT
182
190
  metadata: {}
183
- post_install_message:
184
191
  rdoc_options: []
185
192
  require_paths:
186
193
  - lib
@@ -195,8 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
195
202
  - !ruby/object:Gem::Version
196
203
  version: '0'
197
204
  requirements: []
198
- rubygems_version: 3.4.6
199
- signing_key:
205
+ rubygems_version: 4.0.8
200
206
  specification_version: 4
201
207
  summary: Ruby Interface to ArcadeDB
202
208
  test_files: []