arcadedb 0.4 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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