active-orient 0.2

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,98 @@
1
+ module OrientSupport
2
+ =begin
3
+ This Module fences specialized ruby objects
4
+ =end
5
+
6
+ class Array < Array
7
+ include Support
8
+ =begin
9
+ Initialisation method stores the modelinstance in @orient.
10
+
11
+ Further a list of array-elements is expected, which are forwarded (as Array) to Array
12
+
13
+
14
+ =end
15
+ def initialize modelinstance, *args
16
+ @orient = modelinstance
17
+ super args
18
+ @name = modelinstance.attributes.key(self)
19
+
20
+ end
21
+
22
+ def << arg
23
+ @orient.add_item_to_property( @name, arg ) if @name.present?
24
+ super
25
+ end
26
+
27
+ =begin
28
+ Updating of single items
29
+
30
+ this only works if the hole embedded Array is previosly loaded into the ruby-array.
31
+ =end
32
+
33
+ def []= key, value
34
+ super
35
+ @orient.update set: { @name => self } if @name.present?
36
+ end
37
+
38
+ def [] *arg
39
+ # puts "[] ARG: #{arg.inspect}"
40
+ # arg.each{|u| puts "[] #{u.inspect} : #{u.class} " }
41
+ super
42
+
43
+ end
44
+
45
+ def delete_at *pos
46
+ if @name.present?
47
+ delete self[*pos]
48
+ else
49
+ super
50
+ end
51
+ # old version: works only if the hole array is loaded into memory
52
+ # self[*pos]=nil
53
+ # @orient.update set:{ @name => self.compact }
54
+ end
55
+
56
+ def delete_if
57
+ if @name.present?
58
+ delete *self.map{|y| y if yield(y) }.compact # if the block returns true then delete the item
59
+ else
60
+ super
61
+ end
62
+ end
63
+
64
+ def delete *item
65
+ @orient.remove_item_from_property( @name ) { item } if @name.present?
66
+ end
67
+
68
+ def where *item
69
+ where_string = item.map{|m| where_string = compose_where m }.join( ' and ' )
70
+ query = "select from ( select expand( #{@name} ) from #{@orient.classname}) #{where_string} "
71
+ puts query
72
+ @orient.query query
73
+
74
+ end
75
+
76
+ def method_missing *args
77
+ map{|x| x.send args.first }
78
+ end
79
+ end
80
+ class LinkMap < OrientSupport::Array
81
+
82
+ def []= arg
83
+ end
84
+ end
85
+
86
+ #
87
+ # class Hash < Hash_with_indifferent_access
88
+ # # additional and overlayed methods for Hash-Objects in OrientDB
89
+ # def initialize modelinstance, *args
90
+ # @orient = modelinstance
91
+ # super args
92
+ # @name = modelinstance.attributes.key(self)
93
+ #
94
+ # end
95
+ #
96
+ # end
97
+
98
+ end
@@ -0,0 +1,88 @@
1
+
2
+ module ActiveOrient
3
+ class Query < ActiveOrient::Model
4
+
5
+ has_many :records
6
+ has_many :queries
7
+
8
+ def reset_records
9
+ self.records= []
10
+ end
11
+ alias reset_results reset_records
12
+
13
+ def reset_queries
14
+ self.queries = []
15
+ end
16
+ =begin
17
+ calls ActiveOrient::ActiveOrient#GetDocuments
18
+ stores the query in the query-stack and saves the result in the record-Array
19
+
20
+ returns the count of assigned records
21
+ =end
22
+
23
+ def get_documents o_class , **args
24
+
25
+ query = OrientSupport::OrientQuery.new class_name(o_class), args
26
+ self.queries << query.compose
27
+ count= 0
28
+ orientdb.get_documents( o_class , query: query.compose ).each{|c| records << c; count+=1 }
29
+ count
30
+ end
31
+
32
+ =begin
33
+ All predefined queries are send to the database.
34
+ The result is stored in the records.
35
+ Unknown Records are of Type ActiveOrient::Model::Myquery, uses ActiveOrient::Orientdb.execute which tries to autosuggest the ActiveOrient::Model::{Class}
36
+
37
+ example: Multible Records
38
+ ach = ActiveOrient::Query.new
39
+ ach.queries << 'create class Contracts ABSTRACT'
40
+ ach.queries << 'create property Contracts.details link'
41
+ ach.queries << 'create class Stocks extends Contracts'
42
+ result = ach.execute_queries transaction: false
43
+
44
+ example: Batch
45
+ q = ActiveOrient::Query.new
46
+ q.queries << [
47
+ "select expand( contracts ) from Openinterest"
48
+ "let con = select expand( contracts ) from Openinterest; ",
49
+ "let sub = select from Subcategories where contracts in $con;",
50
+ "let cat = select from Categories where subcategories in $sub;",
51
+ "let ind = select from Industries where categories in $cat;",
52
+ "SELECT expand(unionall) FROM (SELECT unionall( $con, $cat))"
53
+ ]
54
+ q.execute_queries.each{|x| puts "X #{x.inspect}" }
55
+
56
+ =end
57
+ def execute_queries reset: true, transaction: true
58
+ reset_records if reset
59
+ begin
60
+ orientdb.execute( transaction: transaction ) do
61
+ result = queries.map do |q|
62
+ # command: words are seperated by one space only, thus squeeze multible spaces
63
+ sql_cmd = -> (command) { { type: "cmd", language: "sql", command: command.squeeze(' ') } }
64
+ batch_cmd = ->( command_array ){ {type: "script", language: "sql", script: command_array } }
65
+ case q
66
+ when String
67
+ sql_cmd[ q ]
68
+ when Hash
69
+ q
70
+ when Array
71
+ batch_cmd[ q ]
72
+ else
73
+ nil
74
+ end # case
75
+ end.compact
76
+ # save the result in records
77
+ result.each{|y| records << y }
78
+
79
+ end # block
80
+ rescue RestClient::InternalServerError => e
81
+ puts e.inspect
82
+ end
83
+
84
+ end # def execute_queries
85
+
86
+ end # class
87
+
88
+ end # module
@@ -0,0 +1,942 @@
1
+ module ActiveOrient
2
+ require 'cgi'
3
+ require 'rest-client'
4
+ require 'active_support/core_ext/string' # provides blank?, present?, presence etc
5
+ #require 'model'
6
+ =begin
7
+ OrientDB performs queries to a OrientDB-Database
8
+
9
+ The communication is based on the ActiveOrient-API.
10
+
11
+ The OrientDB-Server is specified in config/connect.yml
12
+
13
+ A Sample:
14
+ :orientdb:
15
+ :server: localhost
16
+ :port: 2480
17
+ :database: working-database
18
+ :admin:
19
+ :user: admin-user
20
+ :pass: admin-password
21
+
22
+
23
+ =end
24
+
25
+ class OrientDB
26
+ mattr_accessor :logger ## borrowed from active_support
27
+ include OrientSupport::Support
28
+
29
+ =begin
30
+ Contructor: OrientDB is conventionally initialized.
31
+ Thus several instances pointing to the same or different databases can coexist
32
+
33
+ A simple
34
+ xyz = ActiveOrient::OrientDB.new
35
+ uses the database specified in the yaml-file »config/connect.yml« and connects
36
+ xyz = ActiveOrient::OrientDB.new database: my_fency_database
37
+ accesses the database »my_fency_database«. The database is created if its not existing.
38
+
39
+ *USECASE*
40
+ xyz = ActiveOrient::Model.orientdb = ActiveOrient::OrientDB.new
41
+ initialises the Database-Connection and publishes the Instance to any ActiveOrient::Model-Object
42
+ =end
43
+
44
+ def initialize database: nil, connect: true
45
+ @res = get_ressource
46
+ @database = database.presence || YAML::load_file( File.expand_path('../../config/connect.yml',__FILE__))[:orientdb][:database]
47
+ # @database=@database#.camelcase
48
+ connect() if connect
49
+ # save existing classes
50
+ @classes = []
51
+ ActiveOrient::Model.orientdb = self
52
+ end
53
+
54
+ ## included for development , should be removed in production release
55
+ def ressource
56
+ @res
57
+ end
58
+ ##
59
+
60
+ def connect
61
+ i = 0
62
+ begin
63
+ logger.progname = 'OrientDB#Connect'
64
+ r= @res["/connect/#{ @database }" ].get
65
+ if r.code == 204
66
+ logger.info{ "Connected to database #{@database} " }
67
+ true
68
+ else
69
+ logger.error{ "Connection to database #{@database} could NOT be established" }
70
+ nil
71
+ end
72
+ rescue RestClient::Unauthorized => e
73
+ if i.zero?
74
+ logger.info{ "Database #{@database} NOT present --> creating" }
75
+ i=i+1
76
+ create_database
77
+ retry
78
+ else
79
+ Kernel.exit
80
+ end
81
+ end
82
+
83
+ end
84
+
85
+ ## -----------------------------------------------------------------------------------------
86
+ ##
87
+ ## Database stuff
88
+ ##
89
+ ## get_databases
90
+ ## create_database
91
+ ## change_database
92
+ ## delete_database
93
+ ## -----------------------------------------------------------------------------------------
94
+
95
+ =begin
96
+ returns an Array with available Database-Names as Elements
97
+ =end
98
+ def get_databases
99
+ JSON.parse( @res["/listDatabases"].get.body)['databases']
100
+ end
101
+
102
+ =begin
103
+ Creates a database with the given name and switches to this database as working-database
104
+ Types are either 'plocal' or 'memory'
105
+
106
+ returns the name of the working-database
107
+ =end
108
+ def create_database type: 'plocal' , database: @database
109
+ logger.progname = 'OrientDB#CreateDatabase'
110
+ old_d = @database
111
+ @classes = []
112
+ @database = database
113
+ begin
114
+ response = @res[database_uri{ type }].post ""
115
+ if response.code == 200
116
+ logger.info{ "Database #{@database} successfully created and stored as working database"}
117
+ else
118
+ @database = old_d
119
+ logger.error{ "Database #{name} was NOT created. Working Database is still #{@database} "}
120
+ end
121
+ rescue RestClient::InternalServerError => e
122
+ @database = old_d
123
+ logger.error{ "Database #{name} was NOT created. Working Database is still #{@database} "}
124
+ end
125
+ @database
126
+
127
+ end
128
+ =begin
129
+ changes the working-database to {name}
130
+ =end
131
+ def change_database name
132
+ @classes = []
133
+ @database = name
134
+
135
+ end
136
+ =begin
137
+ deletes the database and returns true on success
138
+
139
+ after the removal of the database, the working-database might be empty
140
+ =end
141
+ def delete_database database:
142
+ @classes = []
143
+ logger.progname = 'OrientDB#DropDatabase'
144
+ old_ds = @database
145
+ change_database database
146
+ begin
147
+ response = @res[database_uri].delete
148
+ if database == old_ds
149
+ change_database ""
150
+ logger.info{ "Working database deleted" }
151
+ else
152
+ change_database old_ds
153
+ logger.info{ "Database #{database} deleted, working database is still #{@database} "}
154
+ end
155
+ rescue RestClient::InternalServerError => e
156
+ logger.info{ "Database #{database} NOT deleted" }
157
+ change_database old_ds
158
+ end
159
+ !response.nil? && response.code == 204 ? true : false
160
+
161
+ end
162
+
163
+ ## -----------------------------------------------------------------------------------------
164
+ ##
165
+ ## Inspect, Create and Delete Classes
166
+ ##
167
+ ## inspect_classes
168
+ ## create_class
169
+ ## delete_class
170
+ ##
171
+ ## -----------------------------------------------------------------------------------------
172
+
173
+ =begin
174
+ returns an Array with (unmodified) Class-attribute-hash-Elements
175
+
176
+ get_classes 'name', 'superClass'
177
+ returns
178
+ [ {"name"=>"E", "superClass"=>""},
179
+ {"name"=>"OFunction", "superClass"=>""},
180
+ {"name"=>"ORole", "superClass"=>"OIdentity"}
181
+ (...)
182
+ ]
183
+ =end
184
+
185
+ def get_classes *attributes
186
+ i = 0
187
+ begin
188
+ response = @res[database_uri].get
189
+ if response.code == 200
190
+ classes= JSON.parse( response.body )['classes' ]
191
+ unless attributes.empty?
192
+ classes.map{|y| y.select{| v,_| attributes.include?(v) } }
193
+ else
194
+ classes
195
+ end
196
+
197
+ #.map{ |y| y.name }
198
+ else
199
+ []
200
+ end
201
+ rescue JSON::ParserError
202
+ if i.zero?
203
+ i = i + 1
204
+ retry
205
+ else
206
+ raise
207
+ end
208
+ end
209
+
210
+ end
211
+ =begin
212
+ returns the class_hierachie
213
+
214
+ to fetch all Vertices
215
+ class_hiearchie( base_class: 'V').flatten
216
+ to fetch all Edges
217
+ class_hierachie( base_class: 'E').flatten
218
+
219
+ Notice: base_class has to be noted as String! There is no implicit conversion from Symbol or Class
220
+ =end
221
+
222
+ def class_hierachie base_class: '', requery: false
223
+ @all_classes = get_classes( 'name', 'superClass') if requery || @all_classes.blank?
224
+ def fv s # :nodoc:
225
+ @all_classes.find_all{ |x| x[ 'superClass' ]== s }.map{| v| v[ 'name' ]} #.camelize }
226
+ end
227
+
228
+ def fx v # :nodoc:
229
+ fv(v).map{ |x| ar = fx( x ) ; ar.empty? ? x : [ x , ar ] }
230
+ end
231
+
232
+ fx base_class
233
+ end
234
+ =begin
235
+ returns an array with all names of the classes of the database
236
+ caches the result.
237
+
238
+ parameter: include_system_classes: false|true, requery: false|true
239
+ =end
240
+ def database_classes include_system_classes: false, requery: false
241
+ requery = true if @classes.empty?
242
+ if requery
243
+ class_hierachie requery: true
244
+ system_classes = ["OFunction", "OIdentity", "ORIDs", "ORestricted", "ORole", "OSchedule", "OTriggered", "OUser", "_studio"]
245
+ all_classes = get_classes( 'name' ).map( &:values).flatten
246
+ @classes = include_system_classes ? all_classes : all_classes - system_classes
247
+ end
248
+ @classes
249
+ end
250
+
251
+ alias inspect_classes database_classes
252
+
253
+ =begin
254
+ Creates classes and class-hierachies in OrientDB and in Ruby.
255
+
256
+
257
+ Takes an Array or a Hash as argument and returns an Array of
258
+ successfull allocated Ruby-Classes
259
+
260
+ If the argument is an array, Basic-Classes are build.
261
+
262
+ Otherwise key/value pairs are assumend to follow this terminology
263
+ { SuperClass => [ class, class, ...], SuperClass => [] , ... }
264
+ =end
265
+
266
+ def create_classes classes
267
+ # rebuild cashed classes-array
268
+ # first the classes-string is camelized (this is used to allocate the ruby-class)
269
+ # Then the database is queried for this string or the underscored string-variant
270
+ # the name-building process is independend from the method »class_name«
271
+ database_classes requery: true
272
+ consts = Array.new
273
+ execute transaction: false do
274
+ class_cmd = ->(s,n) do
275
+ n = n.to_s.camelize
276
+ consts << ActiveOrient::Model.orientdb_class( name: n)
277
+ unless database_classes.include?(n) || database_classes.include?(n.underscore)
278
+ { type: "cmd", language: 'sql', command: "create class #{n} extends #{s}" }
279
+
280
+ end
281
+ end ## class_cmd
282
+
283
+ if classes.is_a?(Array)
284
+ classes.map do | n |
285
+ n = n.to_s.camelize # capitalize
286
+ consts << ActiveOrient::Model.orientdb_class( name: n)
287
+ unless database_classes.include?( n ) || database_classes.include?(n.underscore)
288
+ { type: "cmd", language: 'sql', command: "create class #{n} " }
289
+ end
290
+ end
291
+ elsif classes.is_a?(Hash)
292
+ classes.keys.map do | superclass |
293
+ items = Array.new
294
+ superClass = superclass.to_s.camelize
295
+ items << { type: "cmd", language: 'sql', command: "create class #{superClass} abstract" } unless database_classes.flatten.include?( superClass ) || database_classes.flatten.include?( superClass.underscore )
296
+ items << if classes[superclass].is_a?( String ) || classes[superclass].is_a?( Symbol )
297
+ class_cmd[superClass, classes[superclass] ]
298
+ elsif classes[superclass].is_a?( Array )
299
+ classes[superclass].map{|n| class_cmd[superClass, n] }
300
+ end
301
+ #puts items.flatten.map{|x| x[:command]}
302
+ items # returnvalue
303
+ end.flatten
304
+ end.compact # erase nil-entries, in case the class is already allocated
305
+ end
306
+ # refresh cached class-informations
307
+ database_classes requery: true
308
+ # returns an array of allocated Constants/Classes
309
+ consts
310
+ end
311
+
312
+ =begin
313
+ creates a class and returns the a ActiveOrient::Model:{Newclass}-Class- (Constant)
314
+ which is designed to take any documents stored in this class
315
+
316
+ Predefined attributes: version, cluster, record
317
+
318
+ Other attributes are assigned dynamically upon reading documents
319
+
320
+ The classname is Camelized by default, eg: classnames are always Capitalized, underscores ('_') indicate
321
+ that the following letter is capitalized, too.
322
+
323
+ Other class-names can be used if a "$" is placed at the begining of the name-string. However, most of the
324
+ model-based methods will not work in this case.
325
+ =end
326
+ def create_class newclass
327
+ create_classes( [ newclass ] ).first
328
+ end
329
+
330
+ alias open_class create_class
331
+
332
+ def create_vertex_class name , superclass: 'V'
333
+ create_classes( { superclass => name } ).first
334
+ end
335
+
336
+ def create_edge_class name , superclass: 'E'
337
+ create_classes( { superclass => name } ).first
338
+ end
339
+
340
+ =begin
341
+ nexus_edge connects two documents/vertexes
342
+ The parameter o_class can be either a class or a string
343
+ =end
344
+
345
+ def nexus_edge o_class , attributes: {}, from:, to:, unique: false
346
+ logger.progname = "ActiveOrient::OrientDB#NexusEdge"
347
+ if unique
348
+ wwhere = { out: from.to_orient, in: to.to_orient }.merge(attributes.to_orient)
349
+ existing_edge = get_documents( from: o_class, where: wwhere )
350
+ if existing_edge.first.is_a?( ActiveOrient::Model )
351
+ logger.debug { "reusing edge #{class_name(o_class)} from #{from.to_orient} to #{to.to_orient} " }
352
+ return existing_edge.first
353
+ end
354
+ end
355
+ logger.debug { "creating edge #{class_name(o_class)} from #{from.to_orient} to #{to.to_orient} " }
356
+ response= execute( o_class, transaction: false) do
357
+ #[ { type: "cmd", language: 'sql', command: CGI.escapeHTML("create edge #{class_name(o_class)} from #{translate_to_rid[m]} to #{to.to_roient}; ")} ]
358
+ attr_string = attributes.blank? ? "" : "set #{ generate_sql_list attributes.to_orient }"
359
+ [ { type: "cmd", language: 'sql',
360
+ command: "create edge #{class_name(o_class)} from #{from.to_orient} to #{to.to_orient} #{attr_string}"} ]
361
+ end
362
+ if response.is_a?(Array) && response.size == 1
363
+ response.pop # RETURN_VALUE
364
+ else
365
+ response
366
+ end
367
+ end
368
+
369
+ =begin
370
+ Deletes a single edge when providing a single rid-link (#00:00)
371
+ Deletes multible edges when providing a list of rid-links
372
+ Todo: implement delete_edges after querying the database in one statement
373
+
374
+ =end
375
+ def delete_edge *rid
376
+ rid = rid.map do |mm|
377
+ if mm.is_a?(String)
378
+ if mm.rid?
379
+ mm
380
+ elsif mm.is_a?(Rest::Model)
381
+ mm.rid
382
+ else
383
+ nil
384
+ end
385
+ end
386
+ end.compact
387
+ response= execute transaction: false do
388
+ [ { type: "cmd", language: 'sql', command: CGI.escapeHTML("delete edge #{rid.join(',') }")} ]
389
+ end
390
+ if response.is_a?( Array ) && response.size == 1
391
+ response.pop # RETURN_VALUE
392
+ else
393
+ response
394
+ end
395
+ end
396
+ =begin
397
+ deletes the specified class and returns true on success
398
+
399
+
400
+ todo: remove all instances of the class
401
+ =end
402
+ def delete_class o_class
403
+ cl= class_name(o_class)
404
+ logger.progname = 'OrientDB#DeleteClass'
405
+
406
+ if database_classes.include? cl
407
+ begin
408
+ response = @res[class_uri{ cl } ].delete
409
+ if response.code == 204
410
+ # return_value: sussess of the removal
411
+ !database_classes( requery: true ).include?(cl)
412
+ # don't delete the ruby-class
413
+ # ActiveOrient::Model.send :remove_const, cl.to_sym if o_class.is_a?(Class)
414
+ end
415
+ rescue RestClient::InternalServerError => e
416
+ if database_classes( requery: true).include?( cl )
417
+ logger.error{ "Class #{cl} still present" }
418
+ logger.error{ e.inspect }
419
+ false
420
+ else
421
+ true
422
+ end
423
+ end
424
+ else
425
+ logger.info { "Class #{cl} not present. "}
426
+ end
427
+
428
+ end
429
+
430
+ def create_property o_class, field, type: 'string', linked_class: nil
431
+ logger.progname= 'OrientDB#CreateProperty'
432
+ js= if linked_class.nil?
433
+ { field => { propertyType: type.upcase } }
434
+ else
435
+ { field => { propertyType: type.upcase, linkedClass: class_name( linked_class ) } }
436
+ end
437
+ create_properties( o_class ){ js }
438
+
439
+ end
440
+ ## -----------------------------------------------------------------------------------------
441
+ ##
442
+ ## Properties
443
+ ##
444
+ ## create_properties
445
+ ## get_class_properties
446
+ ## delete_properties
447
+ ##
448
+ ## -----------------------------------------------------------------------------------------
449
+ =begin
450
+
451
+ creates properties which are defined as json in the provided block as
452
+ create_properties( classname or class ) do
453
+ { symbol: { propertyType: 'STRING' },
454
+ con_id: { propertyType: 'INTEGER' } ,
455
+ details: { propertyType: 'LINK', linkedClass: 'Contracts' }
456
+ }
457
+
458
+ =end
459
+ def create_properties o_class
460
+
461
+ begin
462
+ all_properties_in_a_hash = yield
463
+ if all_properties_in_a_hash.is_a? Hash
464
+ response = @res[ property_uri(class_name(o_class)) ].post all_properties_in_a_hash.to_json
465
+ if response.code == 201
466
+ response.body.to_i
467
+ else
468
+ 0
469
+ end
470
+ end
471
+ rescue RestClient::InternalServerError => e
472
+ response = JSON.parse( e.response)['errors'].pop
473
+ error_message = response['content'].split(':').last
474
+ # if error_message ~= /Missing linked class/
475
+ logger.progname= 'OrientDB#CreateProperty'
476
+ logger.error { "Properties in #{class_name(o_class)} were NOT created" }
477
+ logger.error { "Error-code #{response['code']} --> #{response['content'].split(':').last }" }
478
+ nil
479
+ end
480
+
481
+ end
482
+
483
+ def delete_property o_class, field
484
+ logger.progname = 'OrientDB#DeleteProperty'
485
+ begin
486
+ response = @res[property_uri( class_name(o_class)){ field } ].delete
487
+ true if response.code == 204
488
+ rescue RestClient::InternalServerError => e
489
+ logger.error{ "Property #{ field } in class #{ class_name(o_class) } NOT deleted" }
490
+ false
491
+ end
492
+
493
+ end
494
+
495
+ def get_class_properties o_class # :nodoc:
496
+ JSON.parse( @res[ class_uri{ class_name(o_class) } ].get )
497
+ end
498
+
499
+ def print_class_properties o_class
500
+ puts "Detected Properties for class #{class_name(o_class)}"
501
+
502
+ rp = get_class_properties o_class
503
+ n= rp['name']
504
+ puts rp['properties'].map{|x| [ n+'.'+x['name'], x['type'],x['linkedClass'] ].compact.join(' -> ' )}.join("\n")
505
+
506
+ end
507
+ #
508
+ ## -----------------------------------------------------------------------------------------
509
+ ##
510
+ ## Documents
511
+ ##
512
+ ## create_document get_document delete_document
513
+ ## update_or_create_document patch_document
514
+ ##
515
+ ##
516
+ ## get_documents
517
+ ## update_documents
518
+ ## delete_documents
519
+ ## update_or_create_documents
520
+ ## -----------------------------------------------------------------------------------------
521
+
522
+ =begin #nodoc#
523
+ If properties are allocated on class-level, they can be preinitialized using
524
+ this method.
525
+ This is disabled for now, because it does not seem nessesary
526
+
527
+ =end
528
+ def preallocate_class_properties o_class
529
+ p= get_class_properties( o_class )['properties']
530
+ unless p.nil? || p.blank?
531
+ predefined_attributes = p.map do | property |
532
+ [ property['name'] ,
533
+ case property['type']
534
+ when 'LINKMAP'
535
+ Array.new
536
+ when 'STRING'
537
+ ''
538
+ else
539
+ nil
540
+ end ]
541
+ end.to_h
542
+ else
543
+ {}
544
+ end
545
+ end
546
+
547
+ =begin
548
+ Creates an Object in the Database and returns this as ActuveOrient::Model-Instance
549
+ =end
550
+
551
+ def create_document o_class, attributes: {}
552
+ attributes = yield if attributes.empty? && block_given?
553
+ # preallocated_attributes = preallocate_class_properties o_class
554
+ # puts "preallocated_attributes: #{o_class} -->#{preallocated_attributes.inspect }"
555
+ post_argument = { '@class' => class_name(o_class) }.merge(attributes).to_orient
556
+ # puts "post_argument: #{post_argument.inspect}"
557
+
558
+ response = @res[ document_uri ].post post_argument.to_json
559
+ data= JSON.parse( response.body )
560
+ # data = preallocated_attributes.merge data
561
+ ActiveOrient::Model.orientdb_class( name: data['@class']).new data
562
+ # o_class.new JSON.parse( response.body)
563
+ end
564
+
565
+
566
+ def delete_document record_id
567
+ logger.progname = 'OrientDB#DeleteDocument'
568
+ begin
569
+ response = @res[document_uri{ record_id } ].delete
570
+ true if response.code == 204
571
+ rescue RestClient::InternalServerError => e
572
+ logger.error{ "Document #{ record_id } NOT deleted" }
573
+ false
574
+ end
575
+
576
+ end
577
+ =begin
578
+ retrieves documents from a query
579
+
580
+ If raw is specified, the JSON-Array is returned, eg
581
+ { "@type"=>"d",
582
+ "@rid"=>"#15:1",
583
+ "@version"=>1,
584
+ "@class"=>"DocumebntKlasse10",
585
+ "con_id"=>343,
586
+ "symbol"=>"EWTZ"
587
+ }
588
+ otherwise a ActiveModel-Instance of o_class is created and returned
589
+ =end
590
+ def get_documents limit: -1, raw: false, query: nil, **args
591
+ query = OrientSupport::OrientQuery.new( args ) if query.nil?
592
+ i=0
593
+ begin
594
+ url = query_sql_uri << query.compose << "/#{limit}"
595
+ response = @res[URI.encode(url) ].get
596
+ r=JSON.parse( response.body )['result'].map do |document |
597
+ if raw
598
+ document
599
+ else
600
+ ActiveOrient::Model.orientdb_class( name: document['@class']).new document
601
+ end
602
+ end
603
+ rescue RestClient::InternalServerError => e
604
+ response = JSON.parse( e.response)['errors'].pop
605
+ logger.error { response['content'].split(':').last }
606
+ i=i+1
607
+ if i > 1
608
+ raise
609
+ else
610
+ query.dataset_name = query.datatset_name.underscore
611
+ logger.info { "trying to query using #{o_class}" }
612
+ retry
613
+ end
614
+ end
615
+
616
+ end
617
+
618
+ def count_documents **args
619
+ logger.progname = 'OrientDB#count_documents'
620
+
621
+ query = OrientSupport::OrientQuery.new args
622
+ query.projection 'COUNT (*)'
623
+ result = get_documents raw: true, query: query
624
+
625
+ result.first['COUNT']
626
+
627
+ end
628
+
629
+ =begin
630
+ Inserts a Document with the attributes provided in the attributes-hash
631
+ eg
632
+ create_document class_name: @classname, attributes: { con_id: 343, symbol: 'EWTZ' }
633
+
634
+ untested: for hybrid and schema-less documents the following syntax is supported
635
+ create_document class_name: "Account",
636
+ attributes: { date: 1350426789, amount: 100.34,
637
+ "@fieldTypes" => "date=t,amount=c" }
638
+
639
+ The supported special types are:
640
+ n
641
+ 'f' for float
642
+ 'c' for decimal
643
+ 'l' for long
644
+ 'd' for double
645
+ 'b' for byte and binary
646
+ 'a' for date
647
+ 't' for datetime
648
+ 's' for short
649
+ 'e' for Set, because arrays and List are serialized as arrays like [3,4,5]
650
+ 'x' for links
651
+ 'n' for linksets
652
+ 'z' for linklist
653
+ 'm' for linkmap
654
+ 'g' for linkbag
655
+
656
+ =end
657
+ =begin
658
+ Creating a new Database-Entry ( where is omitted )
659
+
660
+ otherwise updating the Database-Entry (if present)
661
+
662
+ The optional Block should provide a hash with attributes(properties). These are used if
663
+ a new dataset is created.
664
+ =end
665
+ def create_or_update_document o_class , **args, &b
666
+ logger.progname = 'Rest#CreateOrUpdateDocument'
667
+ r= update_or_create_documents o_class, **args, &b
668
+ if r.size > 1
669
+ logger.error { "multible documents updated by #{ generate_sql_list( where )}" }
670
+ end
671
+ r.first # return_value
672
+ end
673
+ =begin
674
+ Based on the query specified in :where records are updated according to :set
675
+
676
+ Returns an Array of updated documents
677
+
678
+ The optional Block should provide a hash with attributes(properties). These are used if
679
+ a new dataset is created.
680
+
681
+ ### das ist noch nicht rund.
682
+ #
683
+ =end
684
+ def update_or_create_documents o_class , set: {}, where: {} , **args , &b
685
+ logger.progname = 'Rest#UpdateOrCreateDocuments'
686
+ if where.blank?
687
+ [ create_document( o_class, attributes: set ) ]
688
+ else
689
+ set.extract!( where.keys ) # removes any keys from where in set
690
+ possible_documents = get_documents from: class_name( o_class ), where: where, **args
691
+ if possible_documents.empty?
692
+ if block_given?
693
+ more_where = yield # do Preparations prior to the creation of the dataset
694
+ # if the block returns a Hash , it is merged into the insert_query.
695
+ where.merge! more_where if more_where.is_a?(Hash)
696
+ end
697
+ [ create_document( o_class, attributes: set.merge(where) ) ]
698
+ else
699
+ possible_documents.map{| doc | doc.update( set: set ) }
700
+ end
701
+ end
702
+ end
703
+ =begin
704
+ Deletes documents.
705
+ They are defined by a query. All records which match the attributes are deleted.
706
+ An Array with freed index-values is returned
707
+ =end
708
+ def delete_documents o_class, where: {}
709
+ get_documents( from: o_class, where: where).map do |doc|
710
+ if doc['@type']=='d' # document
711
+ index = doc['@rid'][1,doc['@rid'].size] # omit the first character ('#')
712
+ r=@res[ document_uri{ index }].delete
713
+ index if r.code==204 && r.body.empty? # return_value
714
+ end
715
+
716
+ end
717
+
718
+ end
719
+ =begin
720
+ Retrieves a Document from the Database as ActiveOrient::Model::{class}
721
+ The argument can either be a rid (#[x}:{y}) or a link({x}:{y})
722
+ If no Document is found, nil is returned
723
+
724
+ In the optional block, a subset of properties can be defined (as array of names)
725
+ =end
726
+ def get_document rid
727
+
728
+ rid = rid[1 .. rid.length] if rid[0]=='#'
729
+
730
+ response = @res[ document_uri { rid } ].get
731
+
732
+ raw_data = JSON.parse( response.body) #.merge( "#no_links" => "#no_links" )
733
+ ActiveOrient::Model.orientdb_class( name: raw_data['@class']).new raw_data
734
+
735
+ rescue RestClient::InternalServerError => e
736
+ if e.http_body.split(':').last =~ /was not found|does not exist in database/
737
+ nil
738
+ else
739
+ logger.progname 'OrientDB#GetDocument'
740
+ logger.error "something went wrong"
741
+ logger.error e.http_body.inspect
742
+ raise
743
+ end
744
+ end
745
+ =begin
746
+ Lazy Updating of the given Document.
747
+ =end
748
+ def patch_document rid
749
+ logger.progname = 'Rest#PatchDocument'
750
+ content = yield
751
+ if content.is_a? Hash
752
+ content.each do | key, value |
753
+ # puts "content: #{key}, #{value.class}"
754
+ # content[key]= value.to_orient #if value.is_a? ActiveOrient::Model
755
+ end
756
+ @res[ document_uri { rid } ].patch content.to_orient.to_json
757
+ else
758
+ logger.error { "FAILED: The Block must provide an Hash with properties to be updated"}
759
+ end
760
+ end
761
+ =begin
762
+ update_documents classname,
763
+ set: { :symbol => 'TWR' },
764
+ where: { con_id: 340 }
765
+
766
+ replaces the symbol to TWR in each record where the con_id is 340
767
+ Both set and where take multible attributes
768
+ returns the JSON-Response.
769
+
770
+ =end
771
+
772
+
773
+ def update_documents o_class, set: , where: {}
774
+ url = "update #{class_name(o_class)} set "<< generate_sql_list(set) << compose_where(where)
775
+ response = @res[ URI.encode( command_sql_uri << url) ].post '' #url.to_json
776
+ end
777
+
778
+ ## -----------------------------------------------------------------------------------------
779
+ ##
780
+ ## Functions and Batch
781
+ ##
782
+ ##
783
+ ## -----------------------------------------------------------------------------------------
784
+
785
+ =begin
786
+ Execute a predefined Function
787
+ =end
788
+ def call_function *args
789
+ #puts "uri:#{function_uri { args.join('/') } }"
790
+ @res[ function_uri { args.join('/') } ].post ''
791
+ rescue RestClient::InternalServerError => e
792
+ puts JSON.parse(e.http_body)
793
+ end
794
+
795
+
796
+ =begin
797
+ Executes a list of commands and returns the result-array (if present)
798
+
799
+ structure of the provided block:
800
+ [{ type: "cmd",
801
+ language: "sql",
802
+ command: "create class Person extends V"
803
+ },
804
+ (...)
805
+ ]
806
+
807
+ It's used by ActiveOrient::Query.execute_queries
808
+
809
+ =end
810
+ def execute classname = 'Myquery', transaction: true
811
+ batch = { transaction: transaction, operations: yield }
812
+ unless batch[:operations].blank?
813
+ # puts "batch_uri: #{@res[batch_uri]}"
814
+ # puts "post: #{batch.to_json}"
815
+ response = @res[ batch_uri ].post batch.to_json
816
+ if response.code == 200
817
+ if response.body['result'].present?
818
+ result= JSON.parse(response.body)['result']
819
+ result.map do |x|
820
+ if x.is_a? Hash
821
+ if x.has_key?('@class')
822
+ ActiveOrient::Model.orientdb_class( name: x['@class']).new x
823
+ elsif x.has_key?( 'value' )
824
+ x['value']
825
+ else
826
+ # puts "ActiveOrient::Execute"
827
+ # puts "o_class: #{o_class.inspect}"
828
+ ActiveOrient::Model.orientdb_class( name: classname).new x
829
+ end
830
+ end
831
+ end.compact # return_value
832
+
833
+ else
834
+ response.body
835
+ end
836
+ else
837
+ nil
838
+ end
839
+ end
840
+ rescue RestClient::InternalServerError => e
841
+ raise
842
+
843
+ end
844
+ =begin
845
+ Converts a given name to the camelized database-classname
846
+
847
+ Converts a given class-constant to the corresponding database-classname
848
+
849
+ returns a valid database-class name, nil if the class not exists
850
+ =end
851
+ def class_name name_or_class
852
+ name= if name_or_class.is_a? Class
853
+ name_or_class.to_s.split('::').last
854
+ elsif name_or_class.is_a? ActiveOrient::Model
855
+ name_or_class.classname
856
+ else
857
+ name_or_class.to_s.camelize
858
+ end
859
+ if database_classes.include?(name)
860
+ name
861
+ elsif database_classes.include?(name.underscore)
862
+ name.underscore
863
+ else
864
+ logger.progname = 'OrientDB#ClassName'
865
+ logger.info{ "Classname #{name} not present in active Database" }
866
+ nil
867
+ end
868
+ end
869
+ # def compose_where arg
870
+ # if arg.blank?
871
+ # ""
872
+ # elsif arg.is_a? String
873
+ # if arg =~ /[w|W]here/
874
+ # arg
875
+ # else
876
+ # "where "+arg
877
+ # end
878
+ # elsif arg.is_a? Hash
879
+ # " where " + generate_sql_list(arg)
880
+ # end
881
+ # end
882
+ private
883
+
884
+ #def generate_sql_list attributes={}
885
+ # attributes.map do | key, value |
886
+ # case value
887
+ # when Numeric
888
+ # key.to_s << " = " << value.to_s
889
+ # else # String, Symbol
890
+ # key.to_s << ' = ' << "\'#{ value }\'"
891
+ # # else
892
+ # # puts "ERROR, value-> #{value}, class -> #{value.class}"
893
+ # end
894
+ # end.join( ' and ' )
895
+ #
896
+ #end
897
+ #
898
+ def property_uri(this_class_name)
899
+ if block_given?
900
+ "property/#{ @database }/#{this_class_name}/" << yield
901
+ else
902
+ "property/#{ @database }/#{this_class_name}"
903
+ end
904
+ end
905
+ # called in the beginning or after a 404-Error
906
+ def get_ressource
907
+ read_yml = ->( key ){ YAML::load_file( File.expand_path('../../config/connect.yml',__FILE__))[:orientdb][key] }
908
+ login = read_yml[ :admin ].values
909
+ server_adress = read_yml[ :server ] + ":" + read_yml[ :port ].to_s
910
+ RestClient::Resource.new('http://' << server_adress, *login )
911
+ end
912
+
913
+ def self.simple_uri *names
914
+ names.each do | name |
915
+ m_name = (name.to_s << '_uri').to_sym
916
+ define_method( m_name ) do | &b |
917
+ if b
918
+ "#{name.to_s}/"<< @database << "/" << b.call
919
+ else
920
+ "#{name.to_s}/"<< @database
921
+ end # branch
922
+ end # def
923
+ end
924
+ end
925
+
926
+ def self.sql_uri *names
927
+ names.each do | name |
928
+ define_method ((name.to_s << '_sql_uri').to_sym) do
929
+ "#{name.to_s}/" << @database << "/sql/"
930
+ end
931
+ end
932
+ end
933
+
934
+ simple_uri :database, :document, :class, :batch, :function
935
+ sql_uri :command , :query
936
+
937
+
938
+
939
+ end # class
940
+ end # module
941
+ __END__
942
+