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.
- checksums.yaml +4 -4
- data/.gitignore +57 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +168 -0
- data/Guardfile +30 -0
- data/LICENSE +21 -0
- data/README.md +242 -0
- data/Rakefile +11 -0
- data/arcade.yml +27 -0
- data/arcadedb.gemspec +35 -0
- data/bin/+ +106 -0
- data/bin/console +126 -0
- data/examples/books.rb +139 -0
- data/examples/relation_1__1.rb +149 -0
- data/examples/relation_1__n.rb +56 -0
- data/examples/relation_n_n.rb +194 -0
- data/lib/arcade/api/operations.rb +306 -0
- data/lib/arcade/api/version.rb +5 -0
- data/lib/arcade/base.rb +455 -0
- data/lib/arcade/database.rb +338 -0
- data/lib/arcade/errors.rb +71 -0
- data/lib/arcade/logging.rb +38 -0
- data/lib/arcade/version.rb +3 -0
- data/lib/arcade.rb +39 -0
- data/lib/config.rb +70 -0
- data/lib/init.rb +50 -0
- data/lib/match.rb +216 -0
- data/lib/model/basicdocument.rb +7 -0
- data/lib/model/basicedge.rb +6 -0
- data/lib/model/basicvertex.rb +6 -0
- data/lib/model/document.rb +10 -0
- data/lib/model/edge.rb +47 -0
- data/lib/model/vertex.rb +238 -0
- data/lib/models.rb +6 -0
- data/lib/query.rb +384 -0
- data/lib/support/class.rb +13 -0
- data/lib/support/conversions.rb +295 -0
- data/lib/support/model.rb +84 -0
- data/lib/support/object.rb +20 -0
- data/lib/support/sql.rb +74 -0
- data/lib/support/string.rb +116 -0
- data/rails/arcade.rb +20 -0
- data/rails/config.yml +10 -0
- data/rails/connect.yml +17 -0
- data/rails.md +147 -0
- metadata +49 -4
data/lib/arcade/base.rb
ADDED
@@ -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
|