arcadedb 0.3.1 → 0.4

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