arcadedb 0.4 → 0.5.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.
@@ -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,28 +107,56 @@ 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
 
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
+
133
160
  # ------------ create -----------
134
161
  # returns an rid of the successfully created vertex or document
135
162
  #
@@ -141,21 +168,34 @@ 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
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
 
@@ -178,103 +218,61 @@ module Arcade
178
218
  rid = rid.join(':')
179
219
  rid = rid[1..-1] if rid[0]=="#"
180
220
  if rid.rid?
181
- Api.query( database, "select from #{rid}" ).first &.allocate_model(autocomplete)
221
+ Api.query( database, "select from #{rid}", session_id: session ).first &.allocate_model(autocomplete)
182
222
  else
183
223
  raise Arcade::QueryError "Get requires a rid input", caller
184
224
  end
185
225
  end
186
226
 
187
- # ------------------------------ property ------------------------------------------------- #
188
- # Adds properties to the type
227
+ # ------------------------------ get ------------------------------------------------------ #
189
228
  #
190
- # call via
191
- # Api.property <database>, <type>, name1: a_format , name2: a_format
229
+ # Delete the specified rid
192
230
  #
193
- # Format is one of
194
- # Boolean, Integer, Short, Long, Float, Double, String
195
- # Datetime, Binary, Byte, Decimal, Link
196
- # Embedded, EmbeddedList, EmbeddedMap
231
+ def delete rid
232
+ r = Api.execute( database, session_id: session ){ "delete from #{rid}" }
233
+ success = r == [{ :count => 1 }]
234
+ end
235
+
236
+ # ------------------------------ transmit ------------------------------------------------------ #
237
+ # transmits a command which potentially modifies the database
197
238
  #
198
- # In case of an Error, anything is rolled back and nil is returned
239
+ # Uses the given session_id for transaction-based operations
199
240
  #
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
241
+ # Otherwise just performs the operation
242
+
243
+ def transmit &block
244
+ response = Api.execute database, session_id: session, &block
245
+ if response.is_a? Hash
246
+ _allocate_model res
214
247
  else
215
- rollback database
248
+ response
216
249
  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
250
  end
234
-
251
+ # ------------------------------ execute ------------------------------------------------------ #
235
252
  # execute a command which modifies the database
236
253
  #
237
254
  # The operation is performed via Transaction/Commit
238
255
  # If an Error occurs, its rolled back
239
256
  #
257
+ # If a transaction is already active, a nested transation is initiated
258
+ #
240
259
  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
260
+ # initiate a new transaction
261
+ s= Api.begin_transaction database
262
+ response = Api.execute database, session_id: s, &block
254
263
  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
264
+ _allocate_model response
266
265
  else
267
266
  response
268
267
  end
269
- if Api.commit( database, s) == 204
268
+ if Api.commit( database, session_id: s) == 204
270
269
  r # return associated array of Arcade::Base-objects
271
270
  else
272
271
  []
273
272
  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}"
273
+ rescue Dry::Struct::Error, Arcade::QueryError => e
274
+ Api.rollback database, session_id: s, log: false
275
+ logger.info "Execution FAILED --> Status #{e.status}"
278
276
  [] # return empty result
279
277
  end
280
278
 
@@ -283,7 +281,7 @@ module Arcade
283
281
  # detects database-records and allocates them as model-objects
284
282
  #
285
283
  def query query_object
286
- Api.query database, query_object.to_s
284
+ Api.query database, query_object.to_s, session_id: session
287
285
  end
288
286
 
289
287
  # returns an array of rid's (same logic as create)
@@ -292,20 +290,12 @@ module Arcade
292
290
  content = attributes.empty? ? "" : "CONTENT #{attributes.to_json}"
293
291
  cr = ->( f, t ) do
294
292
  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
293
+ cmd = -> (){ "create edge #{edge_class} from #{f.rid} to #{t.rid} #{content}" }
294
+ edges = transmit( &cmd ).allocate_model(false)
295
+ rescue Arcade::QueryError => e
302
296
  raise unless e.message =~ /Found duplicate key/
303
- puts "#"+e.message.split("#").last[0..-3]
297
+ puts "#"+e.detail.split("#").last[0..-3]
304
298
  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
299
  end
310
300
  from = [from] unless from.is_a? Array
311
301
  to = [to] unless to.is_a? Array
@@ -316,52 +306,12 @@ module Arcade
316
306
 
317
307
  end
318
308
 
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
309
+ def session
310
+ @session_id
342
311
  end
343
312
 
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
-
313
+ def session?
314
+ !session.nil?
315
+ end
366
316
  end # class
367
317
  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"
@@ -0,0 +1,162 @@
1
+ module Arcade
2
+ class Match
3
+
4
+ include Arcade::Support::Sql
5
+
6
+ =begin
7
+ This is a very simple 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
+
15
+ =end
16
+
17
+ def initialize type: , **args
18
+
19
+ @args = args
20
+ @as = []
21
+
22
+ @stack = [ "MATCH { type: #{type.database_name}, #{ assigned_parameters } }" ]
23
+
24
+ return self
25
+ end
26
+
27
+ # Inspect the generated match statement
28
+ def to_s &b
29
+ r = ""
30
+ r = "DISTINCT " if @distinct
31
+ r << @as.join(",")
32
+ r= yield(r) if block_given?
33
+ @stack.join("") + " RETURN #{r} "
34
+ end
35
+
36
+
37
+ # Execute the @stack
38
+ # generally followed by `select_result` to convert json-hashes to arcade objects
39
+ #
40
+ # The optional block modifies the result-statement
41
+ # i.e
42
+ # TG::TimeGraph.grid( 2023, 2..9 ).out(HasPosition)
43
+ # .node( as: :contract )
44
+ # .execute { "contract.symbol" }
45
+ # .select_result
46
+ # gets all associated contracts connected to the month-grid
47
+ def execute &b
48
+ Arcade::Init.db.query( to_s( &b ) )
49
+ end
50
+
51
+
52
+ # todo : metaprogramming!
53
+ def out edge=""
54
+ raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
55
+ @stack << ".out(#{edge.is_a?(Class) ? edge.database_name.to_or : ''})"
56
+ return self
57
+ end
58
+ def in edge=""
59
+ raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
60
+ @stack << ".in(#{edge.is_a?(Class) ? edge.database_name.to_or : ''})"
61
+ return self
62
+ end
63
+ def both edge=""
64
+ raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
65
+ @stack << ".both(#{edge.is_a?(Class) ? edge.database_name.to_or : ''})"
66
+ return self
67
+ end
68
+
69
+ # add conditions on edges to the match statement
70
+ def inE edge="", **args
71
+ raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
72
+ @stack << ".inE(#{edge.is_a?(Class) ? edge.database_name.to_or : ''}) #{assigned_parameters}.outV()"
73
+ return self
74
+ end
75
+ def outE edge="", **args
76
+ raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
77
+ @stack << ".outE(#{edge.is_a?(Class) ? edge.database_name.to_or : ''}) #{assigned_parameters}.inV()"
78
+ return self
79
+ end
80
+ def bothE edge="", **args
81
+ raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
82
+ @stack << ".bothE(#{edge.is_a?(Class) ? edge.database_name : ''}) #{assigned_parameters}.bothV()"
83
+ return self
84
+ end
85
+
86
+
87
+ # general declation of a node (ie. vertex)
88
+ def node **args
89
+ @args = args
90
+ @stack << if args.empty?
91
+ "{}"
92
+ else
93
+ "{ #{ assigned_parameters } }"
94
+ end
95
+ return self
96
+ end
97
+
98
+
99
+
100
+ ### ---------------- end of public api ---------------------------------------------------------------- ###
101
+ private
102
+
103
+
104
+ def assigned_parameters
105
+ @args.map do | k, v |
106
+ # unless k == :while # mask ruby keyword
107
+ send k, v
108
+ # else
109
+ # the_while v
110
+ # end
111
+ end.compact.join(', ')
112
+
113
+ end
114
+
115
+ ## Metastatement ---------- last --------------------
116
+ ##
117
+ ## generates a node declaration for fetching the last element of a traversal on the previous edge
118
+ ##
119
+ ## ie: Arcade::Match.new( type: self.class, where: { symbol: symbol } ).out( Arcade::HasContract )
120
+ # .node( as: :c, where: where )
121
+ # .in( Arcade::IsOrder )
122
+ # .node( last: true, as: :o )
123
+ #
124
+ # --> ... }.in('is_order'){ while: (in('is_order').size() > 0), where: (in('is_order').size() == 0), as: o }
125
+ def last arg
126
+ in_or_out = @stack[-1][1..-1] # use the last statement
127
+ "while: ( #{in_or_out}.size() > 0 ), where: (#{in_or_out}.size() == 0)"
128
+ end
129
+
130
+ def distinct arg
131
+ @distinct = true
132
+ end
133
+
134
+
135
+ def where arg
136
+ "where: ( #{ generate_sql_list( arg ) } )" unless arg.empty?
137
+ end
138
+
139
+ def while arg
140
+ if arg.is_a? TrueClass
141
+ "while: ( true )"
142
+ else
143
+ "while: ( #{ generate_sql_list( arg ) } )"
144
+ end
145
+ end
146
+
147
+ def maxdepth arg
148
+ "maxDepth: #{arg}"
149
+ end
150
+
151
+ def as arg
152
+ @as << arg
153
+ "as: " + arg.to_s
154
+ end
155
+
156
+ def type klassname
157
+ raise "type must be a Database-class" unless klassname.is_a? Class
158
+ "type: #{klassname.database_name}"
159
+ end
160
+
161
+ end
162
+ end
@@ -110,12 +110,16 @@ module Arcade
110
110
  ' ( '+ the_argument.compose + ' ) '
111
111
  when Class
112
112
  the_argument.database_name
113
- else
113
+ when Arcade::Match
114
+ '(' + the_argument.to_s + ')'
115
+ when String
114
116
  if the_argument.to_s.rid? # a string with "#ab:cd"
115
117
  the_argument
116
- else # a database-class-name
118
+ else
119
+ '(' + the_argument + ')'
120
+ end
121
+ else # a database-class-name
117
122
  the_argument.to_s
118
- end
119
123
  end
120
124
  else
121
125
  raise "cannot complete until a target is specified"
@@ -311,27 +315,39 @@ end # class << self
311
315
  self
312
316
  end
313
317
 
314
- # connects by adding {in_or_out}('edgeClass')
315
- def connect_with in_or_out, via: nil
316
- argument = " #{in_or_out}(#{via.to_or if via.present?})"
317
- end
318
+ # # connects by adding {in_or_out}('edgeClass')
319
+ # def connect_with in_or_out, via: nil
320
+ # argument = " #{in_or_out}(#{via.to_or if via.present?})"
321
+ # end
322
+
318
323
  # adds a connection
319
- # in_or_out: :out ---> outE('edgeClass').in[where-condition]
320
- # :in ---> inE('edgeClass').out[where-condition]
324
+ # in_or_out: :out ---> out('edgeClass')[where-condition]
325
+ # :in ---> in('edgeClass')[where-condition]
326
+ # :inE ---> inE('edgeClass')[where-condition].outV()
327
+ # :outE ---> outE('edgeClass')[where-condition].inV()
328
+ #
329
+ # via: Edge-Class
330
+ # where: Condition to be applied on the targed vertex (in_or_out = :in, :out, :both)
331
+ # or on the intermitted edge (in_or_out = :inE, :outE, :bothE)
332
+ # Condition is inserted as "in_or_out[ condition ]"
333
+ # Attention: ranges have to be included as array, ie [ 2..4 ]
334
+ #
321
335
 
322
336
  def nodes in_or_out = :out, via: nil, where: nil, expand: false
323
337
 
324
338
  condition = where.present? ? "[ #{generate_sql_list(where)} ]" : ""
325
339
  via = resolve_edge_name(via) unless via.nil?
326
340
 
327
- start = if in_or_out.is_a? Symbol
328
- in_or_out.to_s
329
- elsif in_or_out.is_a? String
330
- in_or_out
331
- else
332
- "both"
333
- end
334
- argument = " #{start}(#{via})#{condition} "
341
+ argument = if in_or_out.to_s[-1] == 'E'
342
+ case in_or_out.to_s[0..-2]
343
+ when 'in'
344
+ "inE(#{via})#{condition}.outV()"
345
+ when 'out'
346
+ "outE(#{via})#{condition}.inV()"
347
+ end
348
+ else
349
+ "#{in_or_out.to_s}(#{via})#{condition}"
350
+ end
335
351
 
336
352
  if expand.present?
337
353
  send :expand, argument
@@ -349,7 +365,7 @@ end # class << self
349
365
  # returns nil if the query was not sucessfully executed
350
366
  def execute(reduce: false, autoload: true )
351
367
  # unless projection.nil? || projection.empty?
352
- result = db.execute { compose }
368
+ result = db.transmit { compose }
353
369
  return nil unless result.is_a?(Array)
354
370
  block_given? ? result.map{|x| yield x } : result
355
371
  # return result.first if reduce && result.size == 1
@@ -63,6 +63,7 @@ module Arcade
63
63
  #
64
64
 
65
65
  def select_result condition=nil
66
+ return [] if self.empty?
66
67
  condition = first.keys.first if condition.nil?
67
68
  map{|x| x[condition.to_sym]}.flatten.allocate_model
68
69
  end