arcadedb 0.4 → 0.5.2

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.
@@ -17,12 +17,12 @@ module Arcade
17
17
  defines :environment
18
18
 
19
19
  def initialize environment=:development
20
- self.class.configure_logger( Config.logger )
21
- @connection = connect environment
20
+ self.class.configure_logger( Config.logger )
22
21
  if self.class.environment.nil? # class attribute is set on the first call
23
22
  # further instances of Database share the same environment
24
23
  self.class.environment environment
25
24
  end
25
+ @session_id = nil # declare session_id
26
26
  self.class.namespace Object.const_get( Config.namespace )
27
27
  end
28
28
 
@@ -39,20 +39,19 @@ module Arcade
39
39
  #
40
40
  def types refresh=false
41
41
  # uses API
42
- if $types.nil? || refresh
43
- $types = Api.query(database, "select from schema:types" )
44
- .map{ |x| x.transform_keys &:to_sym } # symbolize keys
45
- .map{ |y| y.delete_if{|_,b,| b.empty? } } # eliminate empty entries
42
+ if @types.nil? || refresh
43
+ @types = Api.query(database, "select from schema:types" )
44
+ .map{ |y| y.delete_if{|_,b,| b.blank? } } # eliminate empty entries
46
45
  end
47
- $types
48
- ## upom startup, this is the first access to the database-server
46
+ @types
47
+ ## upon startup, this is the first access to the database-server
49
48
  rescue NoMethodError => e
50
49
  logger.fatal "Could not read Database Types. \n Is the database running?"
51
50
  Kernel.exit
52
51
  end
53
52
 
54
- def indexes
55
- DB.types.find{|x| x.key? :indexes }[:indexes]
53
+ def indexes refresh=false
54
+ types(refresh).find_all{|x| x.key? :indexes }.map{|y| y[:indexes]}.flatten
56
55
  end
57
56
 
58
57
  # ------------ hierarchy -------------
@@ -108,29 +107,57 @@ module Arcade
108
107
  "create edge type #{type} "
109
108
  end.concat( args.map{|x,y| "#{x} #{y} "}.join)
110
109
  end
111
- db= Api.execute database, &exe
110
+ dbe= Api.execute database, &exe
112
111
  types( true ) # update cached schema
113
- db
114
-
115
- rescue HTTPX::HTTPError => e
116
- # puts "ERROR: #{e.message.to_s}"
117
- if e.status == 500 && e.message.to_s =~ /already exists/
118
- Arcade::Database.logger.warn "Database type #{type} already present"
119
- else
120
- raise
121
- end
112
+ dbe
113
+
114
+ rescue Arcade::QueryError => e
115
+ if e.message =~/Type\s+.+\salready\s+exists/
116
+ Arcade::Database.logger.debug "Database type #{type} already present"
117
+ else
118
+ raise
119
+ end
122
120
  end
123
121
 
124
122
  alias create_class create_type
125
123
 
126
124
  # ------------ drop type -----------
127
125
  # delete any record prior to the attempt to drop a type.
128
- # The `unsafe` option is nit implemented.
126
+ # The `unsafe` option is not implemented.
129
127
  def drop_type type
130
128
  Api.execute database, "drop type #{type} if exists"
131
129
  end
132
130
 
133
- # ------------ create -----------
131
+ # ------------------------------ transaction ----------------------------------------------------- #
132
+ # Encapsulates simple transactions
133
+ #
134
+ # nested transactions are not supported.
135
+ # * use the low-leve api.begin_tranaction for that purpose
136
+ # * reuses an existing transaction
137
+ #
138
+ def begin_transaction
139
+ @session_id ||= Api.begin_transaction database
140
+ end
141
+ # ------------------------------ commit ----------------------------------------------------- #
142
+ def commit
143
+ r = Api.commit( database, session_id: session)
144
+ @session_id = nil
145
+ true if r == 204
146
+ end
147
+
148
+ # ------------------------------ rollback ----------------------------------------------------- #
149
+ #
150
+ def rollback
151
+ r = Api.rollback( database, session_id: session)
152
+ @session_id = nil
153
+ true if r == 500
154
+ rescue HTTPX::HTTPError => e
155
+ raise
156
+ end
157
+
158
+
159
+
160
+ # ------------ create ----------- ## not supported anymore by the api
134
161
  # returns an rid of the successfully created vertex or document
135
162
  #
136
163
  # Parameter: name of the vertex or document type
@@ -141,24 +168,38 @@ module Arcade
141
168
  #
142
169
  def create type, **params
143
170
  # uses API
144
- Api.create_document database, type, **params
171
+ Api.create_document database, type, session_id: session, **params
145
172
  end
146
173
 
174
+ # ------------------------------ insert ------------------------------------------------------ #
175
+ #
176
+ # translates the given parameters to
177
+ # INSERT INTO [TYPE:]<type>|BUCKET:<bucket>|INDEX:<index>
178
+ # [(<field>[,]*) VALUES (<expression>[,]*)[,]*]|
179
+ # [CONTENT {<JSON>}|[{<JSON>}[,]*]]
180
+ #
181
+ # :from and :return are not supported
182
+ #
183
+ # If a transaction is active, the insert is executed in that context.
184
+ # Nested transactions are not supported
147
185
  def insert **params
148
186
 
149
- content_params = params.except( :type, :bucket, :index, :from, :return )
187
+ content_params = params.except( :type, :bucket, :index, :from, :return, :session_id )
150
188
  target_params = params.slice( :type, :bucket, :index )
189
+ # session_id = params[:session_id] # extraxt session_id --> future-use?
151
190
  if target_params.empty?
152
- logger.error "Could not insert: target mising (type:, bucket:, index:)"
191
+ raise "Could not insert: target missing (type:, bucket:, index:)"
153
192
  elsif content_params.empty?
154
193
  logger.error "Nothing to Insert"
155
194
  else
156
- content = "CONTENT #{ content_params.to_json }"
195
+ content = "CONTENT #{ content_params.to_json }"
157
196
  target = target_params.map{|y,z| y==:type ? z : "#{y.to_s} #{ z } "}.join
158
- Api.execute( database, "INSERT INTO #{target} #{content} ") &.first.allocate_model(false)
197
+ result = Api.execute( database, session_id: session ){ "INSERT INTO #{target} #{content} "}
198
+ result &.first.allocate_model(false)
159
199
  end
160
200
  end
161
201
 
202
+
162
203
  # ------------------------------ get ------------------------------------------------------ #
163
204
  # Get fetches the record associated with the rid given as parameter.
164
205
  #
@@ -178,103 +219,61 @@ module Arcade
178
219
  rid = rid.join(':')
179
220
  rid = rid[1..-1] if rid[0]=="#"
180
221
  if rid.rid?
181
- Api.query( database, "select from #{rid}" ).first &.allocate_model(autocomplete)
222
+ Api.query( database, "select from #{rid}", session_id: session ).first &.allocate_model(autocomplete)
182
223
  else
183
224
  raise Arcade::QueryError "Get requires a rid input", caller
184
225
  end
185
226
  end
186
227
 
187
- # ------------------------------ property ------------------------------------------------- #
188
- # Adds properties to the type
228
+ # ------------------------------ delete ------------------------------------------------------ #
189
229
  #
190
- # call via
191
- # Api.property <database>, <type>, name1: a_format , name2: a_format
230
+ # Delete the specified rid
192
231
  #
193
- # Format is one of
194
- # Boolean, Integer, Short, Long, Float, Double, String
195
- # Datetime, Binary, Byte, Decimal, Link
196
- # Embedded, EmbeddedList, EmbeddedMap
232
+ def delete rid
233
+ r = Api.execute( database, session_id: session ){ "delete from #{rid}" }
234
+ success = r == [{ :count => 1 }]
235
+ end
236
+
237
+ # ------------------------------ transmit ------------------------------------------------------ #
238
+ # transmits a command which potentially modifies the database
197
239
  #
198
- # In case of an Error, anything is rolled back and nil is returned
240
+ # Uses the given session_id for transaction-based operations
199
241
  #
200
- def self.property database, type, **args
201
-
202
- begin_transaction database
203
- success = args.map do | name, format |
204
- r= execute(database) {" create property #{type.to_s}.#{name.to_s} #{format.to_s} " } &.first
205
- if r.nil?
206
- false
207
- else
208
- r.keys == [ :propertyName, :typeName, :operation ] && r[:operation] == 'create property'
209
- end
210
- end.uniq
211
- if success == [true]
212
- commit database
213
- true
242
+ # Otherwise just performs the operation
243
+
244
+ def transmit &block
245
+ response = Api.execute database, session_id: session, &block
246
+ if response.is_a? Hash
247
+ _allocate_model res
214
248
  else
215
- rollback database
249
+ response
216
250
  end
217
-
218
-
219
- end
220
-
221
- # ------------------------------ index ------------------------------------------------- #
222
- def self.index database, type, name , *properties
223
- properties = properties.map( &:to_s )
224
- unique_requested = "unique" if properties.delete("unique")
225
- unique_requested = "notunique" if properties.delete("notunique" )
226
- automatic = true if
227
- properties << name if properties.empty?
228
- end
229
-
230
- def delete rid
231
- r = Api.execute( database ){ "delete from #{rid}" }
232
- success = r == [{ :count => 1 }]
233
251
  end
234
-
252
+ # ------------------------------ execute ------------------------------------------------------ #
235
253
  # execute a command which modifies the database
236
254
  #
237
255
  # The operation is performed via Transaction/Commit
238
256
  # If an Error occurs, its rolled back
239
257
  #
258
+ # If a transaction is already active, a nested transation is initiated
259
+ #
240
260
  def execute &block
241
- s = Api.begin_transaction database
242
- # begin
243
- response = Api.execute database, nil, s, &block
244
- # rescue HTTPX::HTTPError => e
245
- # raise e.message
246
- # puts e.methods
247
- # puts e.status
248
- # puts e.response
249
- # puts e.message
250
- # puts e.exception
251
- # puts e.cause
252
- # end
253
- # puts response.inspect # debugging
261
+ # initiate a new transaction
262
+ s= Api.begin_transaction database
263
+ response = Api.execute database, session_id: s, &block
254
264
  r= if response.is_a? Hash
255
- _allocate_model res
256
- # elsif response.is_a? Array
257
- # remove empty results
258
- # response.delete_if{|y| y.empty?}
259
- # response.map do | res |
260
- # if res.key? :"@rid"
261
- # allocate_model res
262
- # else
263
- # res
264
- # end
265
- # end
265
+ _allocate_model response
266
266
  else
267
267
  response
268
268
  end
269
- if Api.commit( database, s) == 204
269
+ if Api.commit( database, session_id: s) == 204
270
270
  r # return associated array of Arcade::Base-objects
271
271
  else
272
272
  []
273
273
  end
274
- rescue Dry::Struct::Error, HTTPX::HTTPError, Arcade::QueryError => e
275
- Api.rollback database, s
276
- logger.error "Execution FAILED --> Status #{e.status}"
277
- # logger.error "Execution FAILED --> #{e.exception.message}"
274
+ rescue Dry::Struct::Error, Arcade::QueryError => e
275
+ Api.rollback database, session_id: s, log: false
276
+ logger.fatal "Execution FAILED --> Status #{e}"
278
277
  [] # return empty result
279
278
  end
280
279
 
@@ -283,7 +282,7 @@ module Arcade
283
282
  # detects database-records and allocates them as model-objects
284
283
  #
285
284
  def query query_object
286
- Api.query database, query_object.to_s
285
+ Api.query database, query_object.to_s, session_id: session
287
286
  end
288
287
 
289
288
  # returns an array of rid's (same logic as create)
@@ -292,20 +291,12 @@ module Arcade
292
291
  content = attributes.empty? ? "" : "CONTENT #{attributes.to_json}"
293
292
  cr = ->( f, t ) do
294
293
  begin
295
- edges = Api.execute( database, "create edge #{edge_class} from #{f.rid} to #{t.rid} #{content}").allocate_model(false)
296
- rescue HTTPX::HTTPError => e
297
- # if e.status == 503
298
- # puts e.status
299
- # puts e.message
300
- # puts e.message.class
301
- # end
294
+ cmd = -> (){ "create edge #{edge_class} from #{f.rid} to #{t.rid} #{content}" }
295
+ edges = transmit( &cmd ).allocate_model(false)
296
+ rescue Arcade::QueryError => e
302
297
  raise unless e.message =~ /Found duplicate key/
303
- puts "#"+e.message.split("#").last[0..-3]
298
+ puts "#"+e.detail.split("#").last[0..-3]
304
299
  end
305
- #else
306
- # logger.error "Could not create Edge #{edge_class} from #{f} to #{t}"
307
- ## logger.error edges.to_s
308
- #end
309
300
  end
310
301
  from = [from] unless from.is_a? Array
311
302
  to = [to] unless to.is_a? Array
@@ -316,52 +307,12 @@ module Arcade
316
307
 
317
308
  end
318
309
 
319
-
320
- # query all: select @rid, * from {database}
321
-
322
- # not used
323
- # def get_schema
324
- # query( "select from schema:types" ).map do |a|
325
- # puts "a: #{a}"
326
- # class_name = a["name"]
327
- # inherent_class = a["parentTypes"].empty? ? [Object,nil] : a["parentTypes"].map(&:camelcase_and_namespace)
328
- # namespace, type_name = a["type"].camelcase_and_namespace
329
- # namespace= Arcade if namespace.nil?
330
- # klass= Dry::Core::ClassBuilder.new( name: type_name,
331
- # parent: nil,
332
- # namespace: namespace).call
333
- # end
334
- # rescue NameError
335
- # logger.error "Dataset type #{e} not defined."
336
- # raise
337
- # end
338
- # Postgres is not implemented
339
- # connects to the database and initialises @connection
340
- def connection
341
- @connection
310
+ def session
311
+ @session_id
342
312
  end
343
313
 
344
- def connect environment=:development # environments: production devel test
345
- if [:production, :development, :test].include? environment
346
-
347
- # connect through the ruby postgres driver
348
- # c= PG::Connection.new dbname: Config.database[environment],
349
- # user: Config.username[environment],
350
- # password: Config.password[environment],
351
- # host: Config.pg[:host],
352
- # port: Config.pg[:port]
353
- #
354
- end
355
- rescue PG::ConnectionBad => e
356
- if e.to_s =~ /Credentials/
357
- logger.error "NOT CONNECTED ! Either Database is not present or credentials (#{ Config.username[environment]} / #{Config.password[environment]}) are wrong"
358
- nil
359
- else
360
- raise
361
- end
362
- end # def
363
-
364
-
365
-
314
+ def session?
315
+ !session.nil?
316
+ end
366
317
  end # class
367
318
  end # module
data/lib/arcade/errors.rb CHANGED
@@ -22,6 +22,14 @@ module Arcade
22
22
  end
23
23
 
24
24
  class QueryError < RuntimeError
25
+ attr_reader :error, :args
26
+ def initialize error: "", detail: "", exception: "", **args
27
+ @error = error
28
+ # @detail = detail
29
+ @args = args
30
+ @exception = exception
31
+ super detail
32
+ end
25
33
  end
26
34
 
27
35
  # used by Dry::Validation, not covered by "error"
@@ -14,7 +14,8 @@ module Arcade
14
14
  #
15
15
  class Init
16
16
  extend Dry::Core::ClassAttributes
17
- defines :db # database handle
17
+ defines :db # database handle
18
+ defines :models # static array of database name for arcade-model classes
18
19
 
19
20
  def self.connect e= :development
20
21
 
@@ -25,10 +26,12 @@ module Arcade
25
26
  else
26
27
  :development
27
28
  end
28
- # set the class attribute
29
29
 
30
+ # set the class attributes
31
+ models Base.descendants.map{|x| x.to_s.split("Arcade::").last }
30
32
  db Database.new(env)
31
33
  end
34
+
32
35
  end
33
36
 
34
37
  # Provides method `db` to every Model class
@@ -0,0 +1,210 @@
1
+ module Arcade
2
+ class Match
3
+
4
+ include Arcade::Support::Sql
5
+
6
+ =begin
7
+ This is a wrapper for the match statement
8
+
9
+ Initialize: a= Arcade::Match.new type: Arcade::DatabaseType, where: { property: 'value' }, as: :alias
10
+ Complete: b = a.out( Arcade::EdgeType ).node( while: true, as: item )[ .in.node ... ]
11
+ Inspect b.to_s
12
+ Query DB b.execute [.allocate_model]
13
+ [.analyse_result]
14
+ Customize the return values:
15
+ b.to_s{ "customized return statement" }
16
+ b.execute{ "customized return statment" }
17
+
18
+ Conditions on edges:
19
+ m = Match.new type: Arcade::DatabaseType
20
+ m.inE via: Arcade::Edge || via: [Arcade::Edge1, Arcade::Edge2], where: { var: 3..6 }
21
+ ---> inE('edge(1)'){ where: (var between 3 and 6 )}.outV('edge(2)')
22
+
23
+ Address a single database record:
24
+ m = Match.new( vertex: { Arcade::Vertex-Instance } )
25
+ .....
26
+ .node( vertex: { Arcade::Vertex-Instance } ) # instead of where statement
27
+
28
+ Example
29
+
30
+ symbol_or_title = symbol.present? ? { :symbol => symbol } : { :title => title }
31
+ where = { :right => right }
32
+
33
+ a= Arcade::Match.new( type: self.class, where: symbol_or_title )
34
+ .out( Arcade::HasContract )
35
+ .node( as: :c, where: where )
36
+ a.execute do "c.last_trading_day as expiry, # c is returned form the query [as: :c]
37
+ count(c) as contracts,
38
+ min(c.strike) as s_min,
39
+ max(c.strike) as s_max
40
+ group by c.last_trading_day order by c.last_trading_day"
41
+ end
42
+
43
+
44
+ =end
45
+
46
+ def initialize **args
47
+ type = args.delete :type
48
+ vertex = args.delete :vertex
49
+ rid = args.delete :rid
50
+ raise "MATCH#new:: parameter rid is not supported. Use `vertex: Arcade::Vertex.instance` instead." if rid.present?
51
+ @args = args
52
+ @as = []
53
+
54
+ @stack = if vertex.is_a?( Arcade::Vertex ) && args.empty?
55
+ [ "MATCH { type: #{vertex.class.database_name }, rid: #{vertex.rid} }" ]
56
+ elsif vertex.is_a?( Arcade::Vertex )
57
+ [ "MATCH { type: #{vertex.class.database_name }, rid: #{vertex.rid}, #{ assigned_parameters } }" ]
58
+ elsif type.is_a?( Class) && type.ancestors.include?(Arcade::Vertex) && args.empty?
59
+ [ "MATCH { type: #{type.database_name} }" ]
60
+ elsif type.is_a?( Class) && type.ancestors.include?(Arcade::Vertex)
61
+ [ "MATCH { type: #{type.database_name}, #{ assigned_parameters } }" ]
62
+ else
63
+ raise "Match:: Either type (Arcade::Vertex-Class) or vertex (Arcade::Vertex-Object) is required as parameter"
64
+ end
65
+
66
+ return self
67
+ end
68
+
69
+ # Inspect the generated match statement
70
+ def to_s &b
71
+ r = ""
72
+ r = "DISTINCT " if @distinct
73
+ r << @as.join(",")
74
+ r= yield(r) if block_given?
75
+ @stack.join("") + " RETURN #{r} "
76
+ end
77
+
78
+
79
+ # Execute the @stack
80
+ # generally followed by `select_result` to convert json-hashes to arcade objects
81
+ #
82
+ # The optional block modifies the result-statement
83
+ # i.e
84
+ # TG::TimeGraph.grid( 2023, 2..9 ).out(HasPosition)
85
+ # .node( as: :contract )
86
+ # .execute { "contract.symbol" }
87
+ # .select_result
88
+ # gets all associated contracts connected to the month-grid
89
+ def execute &b
90
+ Arcade::Init.db.query( to_s( &b ) )
91
+ end
92
+
93
+
94
+
95
+ def self.define_base_edge *direction
96
+ direction.each do | e_d |
97
+ define_method e_d do | edge = "" |
98
+ raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
99
+ @stack << ".#{ e_d }(#{edge.is_a?(Class) ? edge.database_name.to_or : ''})"
100
+ self
101
+ end
102
+ end
103
+ end
104
+
105
+
106
+ # add conditions on edges to the match statement
107
+ def self.define_inner_edge start_dir, final_dir
108
+ define_method start_dir do | edge = "", **a |
109
+ raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
110
+
111
+ print_edges = -> (e){ e.is_a?(Class) ? e.database_name.to_or : ''}
112
+ edges = [ a.delete(:via) ].flatten
113
+ edges << edge unless edge == ''
114
+ n = if a.empty?
115
+ ""
116
+ else
117
+ @args = a
118
+ "{ #{ assigned_parameters } }"
119
+ end
120
+ @stack << ".#{ start_dir }(#{print_edges.call(edges.first)})#{ n }.#{ final_dir }(#{print_edges.call(edges.last)})"
121
+ return self
122
+ end
123
+
124
+ end
125
+
126
+ define_base_edge :out, :in, :both
127
+ define_inner_edge :inE, :outV
128
+ define_inner_edge :outE, :inV
129
+
130
+
131
+
132
+ # general declation of a node (ie. vertex)
133
+ def node **args
134
+ vertex = args.delete :vertex
135
+ rid = args.delete :rid
136
+ raise "MATCH#node:: parameter rid is not supported. Use `vertex: Arcade::Vertex.instance` instead." if rid.present?
137
+ @args = args
138
+ @stack << if args.empty?
139
+ vertex.present? ? "{ type: #{vertex.class.database_name }, rid: #{vertex.rid} }" : "{}"
140
+ else
141
+ vertex.present? ? "{ type: #{vertex.class.database_name }, rid: #{vertex.rid}, #{assigned_parameters} }" : "{ #{ assigned_parameters } }"
142
+ end
143
+ return self
144
+ end
145
+
146
+
147
+
148
+ ### ---------------- end of public api ---------------------------------------------------------------- ###
149
+ private
150
+
151
+
152
+ def assigned_parameters
153
+ @args.map do | k, v |
154
+ # unless k == :while # mask ruby keyword
155
+ send k, v
156
+ # else
157
+ # the_while v
158
+ # end
159
+ end.compact.join(', ')
160
+
161
+ end
162
+
163
+ ## Metastatement ---------- last --------------------
164
+ ##
165
+ ## generates a node declaration for fetching the last element of a traversal on the previous edge
166
+ ##
167
+ ## ie: Arcade::Match.new( type: self.class, where: { symbol: symbol } ).out( Arcade::HasContract )
168
+ # .node( as: :c, where: where )
169
+ # .in( Arcade::IsOrder )
170
+ # .node( last: true, as: :o )
171
+ #
172
+ # --> ... }.in('is_order'){ while: (in('is_order').size() > 0), where: (in('is_order').size() == 0), as: o }
173
+ def last arg
174
+ in_or_out = @stack[-1][1..-1] # use the last statement
175
+ "while: ( #{in_or_out}.size() > 0 ), where: (#{in_or_out}.size() == 0)"
176
+ end
177
+
178
+ def distinct arg
179
+ @distinct = true
180
+ end
181
+
182
+
183
+ def where arg
184
+ "where: ( #{ generate_sql_list( arg ) } )" unless arg.empty?
185
+ end
186
+
187
+ def while arg
188
+ if arg.is_a? TrueClass
189
+ "while: ( true )"
190
+ else
191
+ "while: ( #{ generate_sql_list( arg ) } )"
192
+ end
193
+ end
194
+
195
+ def maxdepth arg
196
+ "maxDepth: #{arg}"
197
+ end
198
+
199
+ def as arg
200
+ @as << arg
201
+ "as: " + arg.to_s
202
+ end
203
+
204
+ def type klassname
205
+ raise "type must be a Database-class" unless klassname.is_a? Class
206
+ "type: #{klassname.database_name}"
207
+ end
208
+
209
+ end
210
+ end