arcadedb 0.3.1 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,455 @@
1
+ module Arcade
2
+ class Base < Dry::Struct
3
+
4
+ extend Arcade::Support::Sql
5
+ # schema schema.strict # -- throws an error if specified keys are missing
6
+ transform_keys{ |x| x[0] == '@' ? x[1..-1].to_sym : x.to_sym }
7
+ # Types::Rid --> only accept #000:000, raises an Error, if rid is not present
8
+ attribute :rid, Types::Rid
9
+ # maybe there are edges ## removed in favour of instance methods
10
+ # attribute :in?, Types::Nominal::Any
11
+ # attribute :out?, Types::Nominal::Any
12
+ # any not defined property goes to values
13
+ attribute :values?, Types::Nominal::Hash
14
+
15
+
16
+ def accepted_methods
17
+ [ :rid, :to_human, :delete ]
18
+ end
19
+ # #
20
+ ## ----------------------------------------- Class Methods------------------------------------ ##
21
+ # #
22
+ class << self
23
+
24
+ # this has to be implemented on class level
25
+ # otherwise it interfere with attributes
26
+ def database_name
27
+ self.name.snake_case
28
+ end
29
+
30
+ def create_type
31
+ the_class = nil # declare as local var
32
+ parent_present = ->(cl){ db.hierarchy.flatten.include? cl }
33
+ e = ancestors.each
34
+ myselfclass = e.next # start with the actual class(self)
35
+ loop do
36
+ superclass = the_class = e.next
37
+ break if the_class.is_a? Class
38
+ end
39
+ begin
40
+ loop do
41
+ if the_class.respond_to?(:demodulize)
42
+ if [ 'Document','Vertex', 'Edge'].include?(the_class.demodulize)
43
+ if the_class == superclass # no inheritance
44
+ ## we have to use demodulize as the_class actually is Arcade::Vertex, ...
45
+ unless parent_present[ to_s.snake_case ]
46
+ db.create_type the_class.demodulize, to_s.snake_case
47
+ else
48
+ db.logger.warn "Type #{ to_s.snake_case } is present, process skipped"
49
+ end
50
+ else
51
+ if superclass.is_a? Class # maybe its a module.
52
+ extended = superclass.to_s.snake_case
53
+ else
54
+ extended = superclass.superclass.to_s.snake_case
55
+ end
56
+ if !parent_present[extended]
57
+ superclass.create_type
58
+ end
59
+ db.create_type the_class.demodulize, to_s.snake_case, extends: extended
60
+ end
61
+ break # stop iteration
62
+ end
63
+ end
64
+ the_class = e.next # iterate through the enumerator
65
+ end
66
+ # todo
67
+ # include `created`` and `updated` properties to the aradedb-database schema if timestamps are set
68
+ # (it works without declaring them explicitly, its thus omitted for now )
69
+ # Integration is easy: just execute two commands
70
+ custom_setup = db_init rescue ""
71
+ custom_setup.each_line do | command |
72
+ the_command = command[0 .. -2] # remove '\n'
73
+ next if the_command == ''
74
+ # db.logger.info "Custom Setup:: #{the_command}"
75
+ db.execute { the_command }
76
+ end unless custom_setup.nil?
77
+
78
+ rescue Arcade::RollbackError => e
79
+ db.logger.warn e
80
+ rescue RuntimeError => e
81
+ db.logger.warn e
82
+ end
83
+ end
84
+
85
+
86
+ def drop_type
87
+ db.drop_type to_s.snake_case
88
+ end
89
+
90
+ def properties
91
+
92
+ end
93
+
94
+
95
+
96
+ # add timestamp attributes to the model
97
+ #
98
+ # updated is optional
99
+ #
100
+ # timestamps are included in create and update statements
101
+ #
102
+ def timestamps set=nil
103
+ if set && @stamps.nil?
104
+ @stamps = true
105
+ attribute :created, Types::JSON::DateTime
106
+ attribute :updated?, Types::JSON::DateTime
107
+ end
108
+ @stamps
109
+ end
110
+
111
+
112
+
113
+ ## ----------------------------------------- insert ---------------------------------- ##
114
+ #
115
+ # Adds a record to the database
116
+ #
117
+ # returns the inserted record
118
+ #
119
+ # Bucket and Index are supported
120
+ #
121
+ # fired Database-command
122
+ # INSERT INTO <type> BUCKET<bucket> INDEX <index> [CONTENT {<attributes>}]
123
+ # (not supported (jet): [RETURN <expression>] [FROM <query>] )
124
+
125
+ def insert **attributes
126
+ db.insert type: database_name, **attributes
127
+ end
128
+
129
+ ## ----------------------------------------- create ---------------------------------- ##
130
+ #
131
+ # Adds a record to the database
132
+ #
133
+ # returns the model dataset
134
+ # ( depreciated )
135
+
136
+ def create **attributes
137
+ Api.begin_transaction db.database
138
+ attributes.merge!( created: DateTime.now ) if timestamps
139
+ record = insert **attributes
140
+ Api.commit db.database
141
+ record
142
+ rescue QueryError => e
143
+ db.logger.error "Dataset NOT created"
144
+ db.logger.error "Provided Attributes: #{ attributes.inspect }"
145
+ # Api.rollback db.database ---> raises "transactgion not begun"
146
+ rescue Dry::Struct::Error => e
147
+ Api.rollback db.database
148
+ db.logger.error "#{ rid } :: Validation failed, record deleted."
149
+ db.logger.error e.message
150
+ end
151
+
152
+ def count **args
153
+ command = "count(*)"
154
+ query( **( { projection: command }.merge args ) ).query.first[command.to_sym] rescue 0
155
+ end
156
+
157
+ # Lists all records of a type
158
+ #
159
+ # Accepts any parameter supported by Arcade::Query
160
+ #
161
+ # Model.all false --> suppresses the autoload mechanism
162
+ #
163
+ # Example
164
+ #
165
+ # My::Names.all order: 'name', autoload: false
166
+ #
167
+ def all a= true, autoload: true, **args
168
+ autoload = false if a != autoload
169
+ query(**args).query.allocate_model( autoload )
170
+ end
171
+
172
+ # Lists the first record of a type or a query
173
+ #
174
+ # Accepts any parameter supported by Arcade::Query
175
+ #
176
+ # Model.first false --> suppresses the autoload mechanism
177
+ #
178
+ # Example
179
+ #
180
+ # My::Names.first where: 'age < 50', autoload: false
181
+ #
182
+ def first a= true, autoload: true, **args
183
+ autoload = false if a != autoload
184
+ query( **( { order: "@rid" , limit: 1 }.merge args ) ).query.allocate_model( autoload ) &.first
185
+ end
186
+
187
+
188
+ # Lists the last record of a type or a query
189
+ #
190
+ # Accepts any parameter supported by Arcade::Query
191
+ #
192
+ # Model.last false --> suppresses the autoload mechanism
193
+ #
194
+ # Example
195
+ #
196
+ # My::Names.last where: 'age > 50', autoload: false
197
+ #
198
+ def last a= true, autoload: true, **args
199
+ autoload = false if a != autoload
200
+ query( **( { order: {"@rid" => 'desc'} , limit: 1 }.merge args ) ).query.allocate_model( autoload )&.first
201
+ end
202
+
203
+ # Selects records of a type or a query
204
+ #
205
+ # Accepts **only** parameters to restrict the query (apart from autoload).
206
+ #
207
+ # Use `Model.query where: args``to use the full range of supported parameters
208
+ #
209
+ # Model.where false --> suppresses the autoload mechanism
210
+ #
211
+ # Example
212
+ #
213
+ # My::Names.last where: 'age > 50', autoload: false
214
+ #
215
+ def where a= true, autoload: true, **args
216
+ autoload = false if a != autoload
217
+ args = a if a.is_a?(String)
218
+ ## the result is always an array
219
+ query( where: args ).query.allocate_model(autoload)
220
+ end
221
+
222
+ # Finds the first matching record providing the parameters of a `where` query
223
+ # Strategie.find symbol: 'Still'
224
+ # is equivalent to
225
+ # Strategie.all.find{|y| y.symbol == 'Still'
226
+ # }
227
+ def find **args
228
+ f= where(**args).first
229
+ f= where( "#{ args.keys.first } like #{ args.values.first.to_or }" ).first if f.nil? || f.empty?
230
+ f
231
+ end
232
+ # update returns a list of updated records
233
+ #
234
+ # It fires a query update <type> set <property> = <new value > upsert return after $current where < condition >
235
+ #
236
+ # which returns a list of modified rid's
237
+ #
238
+ # required parameter: set:
239
+ # where:
240
+ #
241
+ #todo refacture required parameters notification
242
+ #
243
+ def update **args
244
+ if args.keys.include?(:set) && args.keys.include?(:where)
245
+ args.merge!( updated: DateTime.now ) if timestamps
246
+ query( **( { kind: :update }.merge args ) ).execute do |r|
247
+ r[:"$current"] &.allocate_model(false) # do not autoload modelfiles
248
+ end
249
+ else
250
+ raise "at least set: and where: are required to perform this operation"
251
+ end
252
+ end
253
+
254
+ # update! returns the count of affected records
255
+ #
256
+ # required parameter: set:
257
+ # where:
258
+ #
259
+ def update! **args
260
+ if args.keys.include?(:set) && args.keys.include?(:where)
261
+ args.merge!( updated: DateTime.now ) if timestamps
262
+ query( **( { kind: :update! }.merge args ) ).execute{|y| y[:count] } &.first
263
+ else
264
+ raise "at least set: and where: are required to perform this operation"
265
+ end
266
+ end
267
+
268
+
269
+ # returns a list of updated records
270
+ def upsert **args
271
+ set_statement = args.delete :set
272
+ args.merge!( updated: DateTime.now ) if timestamps
273
+ where_statement = args[:where] || args
274
+ statement = if set_statement
275
+ { set: set_statement, where: where_statement }
276
+ else
277
+ { where: where_statement }
278
+ end
279
+ result= query( **( { kind: :upsert }.merge statement ) ).execute do | answer|
280
+ z= answer[:"$current"] &.allocate_model(false) # do not autoload modelfiles
281
+ raise Arcade::LoadError "Upsert failed" unless z.is_a? Arcade::Base
282
+ z # return record
283
+ end
284
+ end
285
+
286
+
287
+ def query **args
288
+ Arcade::Query.new( **{ from: self }.merge(args) )
289
+ end
290
+
291
+ # immutable support
292
+ # to make a database type immutable add
293
+ # `not_permitted :update, :upsert, :delete`
294
+ # to the model-specification
295
+ #
296
+ def not_permitted *m
297
+ m.each do | def_m |
298
+ define_method( def_m ) do | v = nil |
299
+ raise ArcadeImmutableError "operation not permitted", caller
300
+ end
301
+ end
302
+ end
303
+
304
+ end
305
+ # #
306
+ ## ------------------------- Instance Methods ----------------------------------- -##
307
+ # #
308
+
309
+ ## Attributes can be declared in the model file
310
+ ##
311
+ ## Those not covered there are stored in the `values` attribute
312
+ ##
313
+ ## invariant_attributes removes :rid, :in, :out, :created_at, :updated_at and
314
+ # includes :values-attributes to the list of attributes
315
+
316
+
317
+ def invariant_attributes
318
+ result= attributes.except :rid, :in, :out, :values, :created_at, :updated_at
319
+ if attributes.keys.include?(:values)
320
+ result.merge values
321
+ else
322
+ result
323
+ end
324
+ end
325
+
326
+ ## enables to display values keys like methods
327
+ ##
328
+ def method_missing method, *key
329
+ if attributes[:values] &.keys &.include? method
330
+ return values.fetch(method)
331
+ end
332
+ end
333
+
334
+ def query **args
335
+ Arcade::Query.new( **{ from: rid }.merge(args) )
336
+ end
337
+
338
+ # to JSON controlls the serialisation of Arcade::Base Objects for the HTTP-JSON API
339
+ #
340
+ # ensure, that only the rid is transmitted to the database
341
+ #
342
+ def to_json *args
343
+ unless ["#0:0", "#-1:-1"].include? rid # '#-1:-1' is the null-rid
344
+ rid
345
+ else
346
+ invariant_attributes.merge( :'@type' => self.class.database_name ).to_json
347
+ end
348
+ end
349
+ def rid?
350
+ true unless ["#0:0", "#-1:-1"].include? rid
351
+ end
352
+
353
+ # enables usage of Base-Objects in queries
354
+ def to_or
355
+ if rid?
356
+ rid
357
+ else
358
+ to_json
359
+ end
360
+ end
361
+
362
+ def to_human
363
+
364
+
365
+ "<#{ self.class.to_s.snake_case }" + rid? ? "[#{ rid }]: " : " " + invariant_attributes.map do |attr, value|
366
+ v= case value
367
+ when Arcade::Base
368
+ "< #{ self.class.to_s.snake_case }: #{ value.rid } >"
369
+ when Array
370
+ value.map{|x| x.to_s}
371
+ else
372
+ value.from_db
373
+ end
374
+ "%s : %s" % [ attr, v] unless v.nil?
375
+ end.compact.sort.join(', ') + ">".gsub('"' , ' ')
376
+ end
377
+
378
+ alias to_s to_human
379
+
380
+ def to_html # iruby
381
+ _modul, _class = self.class.to_s.split "::"
382
+ the_class = _modul == 'Arcade' ? _class : self.class.to_s
383
+ IRuby.display IRuby.html "<b style=\"color: #50953DFF\"><#{ the_class}</b>"
384
+ + rid? ? "[#{ rid }]: " : " " + invariant_attributes.map do |attr, value|
385
+ v= case value
386
+ when Arcade::Base
387
+ "< #{ self.class.to_s.snake_case }: #{ value.rid } >"
388
+ when Array
389
+ value.map{|x| x.to_s}
390
+ else
391
+ value.from_db
392
+ end
393
+ "%s : %s" % [ attr, v] unless v.nil?
394
+ end.compact.sort.join(', ') + ">".gsub('"' , ' ')
395
+ end
396
+
397
+ def update **args
398
+ Arcade::Query.new( from: rid , kind: :update, set: args).execute
399
+ refresh
400
+ end
401
+
402
+ # inserts or updates a embedded document
403
+ def insert_document name, obj
404
+ value = if obj.is_a? Arcade::Document
405
+ obj.to_json
406
+ else
407
+ obj.to_or
408
+ end
409
+ # if send( name ).nil? || send( name ).empty?
410
+ db.execute { "update #{ rid } set #{ name } = #{ value }" }.first[:count]
411
+ # end
412
+ end
413
+
414
+ # updates a single property in an embedded document
415
+ def update_embedded embedded, embedded_property, value
416
+ db.execute{ " update #{rid} set `#{embedded}`.`#{embedded_property}` = #{value.to_or}" }
417
+ end
418
+
419
+ def update_list list, value
420
+ value = if value.is_a? Arcade::Document
421
+ value.to_json
422
+ else
423
+ value.to_or
424
+ end
425
+ if send( list ).nil? || send( list ).empty?
426
+ db.execute { "update #{ rid } set #{ list } = [#{ value }]" }
427
+ else
428
+ db.execute { "update #{ rid } set #{ list } += #{ value }" }
429
+ end
430
+ refresh
431
+ end
432
+
433
+ # updates a map property , actually adds the key-value pair to the property
434
+ def update_map m, key, value
435
+ if send( m ).nil?
436
+ db.execute { "update #{ rid } set #{ m } = MAP ( #{ key.to_s.to_or } , #{ value.to_or } ) " }
437
+ else
438
+ db.execute { "update #{ rid } set #{ m }.`#{ key.to_s }` = #{ value.to_or }" }
439
+ end
440
+ refresh
441
+ end
442
+ def delete
443
+ response = db.execute { "delete from #{ rid }" }
444
+ true if response == [{ count: 1 }]
445
+ end
446
+ def == arg
447
+ # self.attributes == arg.attributes
448
+ self.rid == arg.rid
449
+ end
450
+
451
+ def refresh
452
+ db.get(rid)
453
+ end
454
+ end
455
+ end