arcadedb 0.3.1 → 0.4

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.
@@ -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