arcadedb 0.3.1 → 0.3.3

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,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