arcadedb 0.3.1 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +57 -0
- data/CHANGELOG.md +19 -0
- data/Gemfile +25 -0
- data/Gemfile.lock +186 -0
- data/Guardfile +30 -0
- data/LICENSE +21 -0
- data/README.md +242 -0
- data/Rakefile +11 -0
- data/arcade.yml +23 -0
- data/arcadedb.gemspec +32 -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 +257 -0
- data/lib/arcade/api/primitives.rb +98 -0
- data/lib/arcade/base.rb +454 -0
- data/lib/arcade/database.rb +367 -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 +36 -0
- data/lib/config.rb +72 -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 +87 -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 +64 -5
data/lib/arcade/base.rb
ADDED
@@ -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
|