active-orient 0.42 → 0.79

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.
Files changed (60) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/Gemfile +13 -5
  4. data/Guardfile +12 -4
  5. data/README.md +67 -280
  6. data/VERSION +1 -1
  7. data/active-orient.gemspec +6 -5
  8. data/bin/active-orient-0.6.gem +0 -0
  9. data/bin/active-orient-console +85 -0
  10. data/config/boot.rb +72 -1
  11. data/config/config.yml +10 -0
  12. data/config/connect.yml +9 -4
  13. data/examples/books.rb +92 -40
  14. data/examples/streets.rb +89 -85
  15. data/examples/test_commands.rb +97 -0
  16. data/examples/test_commands_2.rb +59 -0
  17. data/examples/test_commands_3.rb +55 -0
  18. data/examples/test_commands_4.rb +33 -0
  19. data/examples/time_graph.md +162 -0
  20. data/lib/active-orient.rb +75 -9
  21. data/lib/base.rb +238 -169
  22. data/lib/base_properties.rb +68 -60
  23. data/lib/class_utils.rb +226 -0
  24. data/lib/database_utils.rb +98 -0
  25. data/lib/init.rb +79 -0
  26. data/lib/java-api.rb +442 -0
  27. data/lib/jdbc.rb +211 -0
  28. data/lib/model/custom.rb +26 -0
  29. data/lib/model/edge.rb +70 -0
  30. data/lib/model/model.rb +134 -0
  31. data/lib/model/the_class.rb +607 -0
  32. data/lib/model/the_record.rb +266 -0
  33. data/lib/model/vertex.rb +236 -0
  34. data/lib/orientdb_private.rb +48 -0
  35. data/lib/other.rb +371 -0
  36. data/lib/railtie.rb +68 -0
  37. data/lib/rest/change.rb +147 -0
  38. data/lib/rest/create.rb +279 -0
  39. data/lib/rest/delete.rb +134 -0
  40. data/lib/rest/operations.rb +211 -0
  41. data/lib/rest/read.rb +171 -0
  42. data/lib/rest/rest.rb +112 -0
  43. data/lib/rest_disabled.rb +24 -0
  44. data/lib/support/logging.rb +38 -0
  45. data/lib/support/orient.rb +196 -0
  46. data/lib/support/orientquery.rb +469 -0
  47. data/rails.md +154 -0
  48. data/rails/activeorient.rb +32 -0
  49. data/rails/config.yml +10 -0
  50. data/rails/connect.yml +17 -0
  51. metadata +65 -24
  52. data/active-orient-0.4.gem +0 -0
  53. data/active-orient-0.41.gem +0 -0
  54. data/lib/model.rb +0 -468
  55. data/lib/orient.rb +0 -98
  56. data/lib/query.rb +0 -88
  57. data/lib/rest.rb +0 -1059
  58. data/lib/support.rb +0 -372
  59. data/test.rb +0 -4
  60. data/usecase.md +0 -91
@@ -0,0 +1,211 @@
1
+ require 'orientdb'
2
+ require_relative "database_utils.rb" #common methods without rest.specific content
3
+ require_relative "class_utils.rb" #common methods without rest.specific content
4
+ require_relative "orientdb_private.rb"
5
+
6
+
7
+ module ActiveOrient
8
+
9
+
10
+ class API
11
+ include OrientSupport::Support
12
+ include DatabaseUtils
13
+ include ClassUtils
14
+ include OrientDbPrivate
15
+ include OrientDB
16
+
17
+ mattr_accessor :logger # borrowed from active_support
18
+ mattr_accessor :default_server
19
+ attr_reader :database # Used to read the working database
20
+
21
+ #### INITIALIZATION ####
22
+
23
+
24
+ def initialize database: nil, connect: true, preallocate: true
25
+ self.logger = Logger.new('/dev/stdout') unless logger.present?
26
+ self.default_server = {
27
+ :server => 'localhost',
28
+ :port => 2480,
29
+ :protocol => 'http',
30
+ :user => 'root',
31
+ :password => 'root',
32
+ :database => 'temp'
33
+ }.merge default_server.presence || {}
34
+ @database = database || default_server[:database]
35
+ @all_classes=[]
36
+ #puts ["remote:#{default_server[:server]}/#{@database}",
37
+ # default_server[:user], default_server[:password] ]
38
+ connect() if connect
39
+ # @db = DocumentDatabase.connect("remote:#{default_server[:server]}/#{@database}",
40
+ # default_server[:user], default_server[:password] )
41
+ ActiveOrient::Model.api = self
42
+ preallocate_classes if preallocate
43
+
44
+ end
45
+
46
+ def db
47
+ @db
48
+ end
49
+
50
+ # Used for the connection on the server
51
+ #
52
+
53
+ # Used to connect to the database
54
+
55
+ def connect
56
+
57
+ @db = DocumentDatabase.connect("remote:#{default_server[:server]}/#{@database}",
58
+ default_server[:user], default_server[:password] )
59
+ @classes = get_database_classes
60
+ end
61
+
62
+
63
+
64
+ def get_classes *attributes
65
+ classes= @db.metadata.schema.classes.map{|x| { 'name' => x.name , 'superClass' => x.get_super_class.nil? ? '': x.get_super_class.name } }
66
+ unless attributes.empty?
67
+ classes.map{|y| y.select{|v,_| attributes.include?(v)}}
68
+ else
69
+ classes
70
+ end
71
+
72
+ end
73
+
74
+
75
+ def create_classes classes , &b
76
+ consts = allocate_classes_in_ruby( classes , &b )
77
+
78
+ all_classes = consts.is_a?( Array) ? consts.flatten : [consts]
79
+ get_database_classes(requery: true)
80
+ selected_classes = all_classes.map do | this_class |
81
+ this_class unless get_database_classes(requery: false).include?( this_class.ref_name ) rescue nil
82
+ end.compact.uniq
83
+ command= selected_classes.map do | database_class |
84
+ ## improper initialized ActiveOrient::Model-classes lack a ref_name class-variable
85
+ next if database_class.ref_name.blank?
86
+ c = if database_class.superclass == ActiveOrient::Model || database_class.superclass.ref_name.blank?
87
+ puts "CREATE CLASS #{database_class.ref_name} "
88
+ OClassImpl.create @db, database_class.ref_name
89
+ else
90
+ puts "CREATE CLASS #{database_class.ref_name} EXTENDS #{database_class.superclass.ref_name}"
91
+ OClassImpl.create @db, superClass: database_class.ref_name
92
+ end
93
+ end
94
+
95
+ # update the internal class hierarchy
96
+ get_database_classes requery: true
97
+ # return all allocated classes, no matter whether they had to be created in the DB or not.
98
+ # keep the format of the input-parameter
99
+ consts.shift if block_given? && consts.is_a?( Array) # remove the first element
100
+ # remove traces of superclass-allocations
101
+ if classes.is_a? Hash
102
+ consts = Hash[ consts ]
103
+ consts.each_key{ |x| consts[x].delete_if{|y| y == x} if consts[x].is_a? Array }
104
+ end
105
+ consts
106
+ end
107
+
108
+
109
+
110
+ def delete_class o_class
111
+ @db.schema.drop_class classname(o_class)
112
+ get_database_classes requery: true
113
+ end
114
+ =begin
115
+ Creates properties and optional an associated index as defined in the provided block
116
+ create_properties(classname or class, properties as hash){index}
117
+
118
+ The default-case
119
+ create_properties(:my_high_sophisticated_database_class,
120
+ con_id: {type: :integer},
121
+ details: {type: :link, linked_class: 'Contracts'}) do
122
+ contract_idx: :notunique
123
+ end
124
+
125
+ A composite index
126
+ create_properties(:my_high_sophisticated_database_class,
127
+ con_id: {type: :integer},
128
+ symbol: {type: :string}) do
129
+ {name: 'indexname',
130
+ on: [:con_id, :details] # default: all specified properties
131
+ type: :notunique # default: :unique
132
+ }
133
+ end
134
+ =end
135
+
136
+ def create_properties o_class, **all_properties, &b
137
+ logger.progname = 'JavaApi#CreateProperties'
138
+ ap = all_properties
139
+ created_properties = ap.map do |property, specification |
140
+ puts "specification: #{specification.inspect}"
141
+ field_type = ( specification.is_a?( Hash) ? specification[:type] : specification ).downcase.to_sym rescue :string
142
+ the_other_class = specification.is_a?(Hash) ? specification[:other_class] : nil
143
+ other_class = if the_other_class.present?
144
+ @db.get_class( the_other_class)
145
+ end
146
+ index = ap.is_a?(Hash) ? ap[:index] : nil
147
+ if other_class.present?
148
+ @db.get_class(classname(o_class)).add property,[ field_type, other_class ], { :index => index }
149
+ else
150
+ @db.get_class(classname(o_class)).add property, field_type, { :index => index }
151
+ end
152
+ end
153
+ if block_given?
154
+ attr = yield
155
+ index_parameters = case attr
156
+ when String, Symbol
157
+ { name: attr }
158
+ when Hash
159
+ { name: attr.keys.first , type: attr.values.first, on: all_properties.keys.map(&:to_s) }
160
+ else
161
+ nil
162
+ end
163
+ create_index o_class, **index_parameters unless index_parameters.blank?
164
+ end
165
+ created_properties.size # return_value
166
+
167
+ end
168
+
169
+ def get_properties o_class
170
+ @db.get_class(classname(o_class)).propertiesMap
171
+ end
172
+
173
+
174
+
175
+ def create_index o_class, name:, on: :automatic, type: :unique
176
+ logger.progname = 'JavaApi#CreateIndex'
177
+ begin
178
+ c = @db.get_class( classname( o_class ))
179
+ index = if on == :automatic
180
+ nil # not implemented
181
+ elsif on.is_a? Array
182
+ c.createIndex name.to_s, INDEX_TYPES[type], *on
183
+ else
184
+ c.createIndex name.to_s, INDEX_TYPES[type], on
185
+ end
186
+ end
187
+ end
188
+
189
+ def create_record o_class, attributes: {}
190
+ logger.progname = 'HavaApi#CreateRecord'
191
+ attributes = yield if attributes.empty? && block_given?
192
+ new_record = insert_document( o_class, attributes.to_orient )
193
+
194
+
195
+ end
196
+ alias create_document create_record
197
+
198
+ def insert_document o_class, attributes
199
+ d = Document.create @db, classname(o_class), **attributes
200
+ d.save
201
+ ActiveOrient::Model.get_model_class(classname(o_class)).new attributes.merge( { "@rid" => d.rid,
202
+ "@version" => d.version,
203
+ "@type" => 'd',
204
+ "@class" => classname(o_class) } )
205
+
206
+
207
+
208
+ end
209
+ end
210
+ end
211
+
@@ -0,0 +1,26 @@
1
+ module CustomClass
2
+ =begin
3
+ The REST-Interface does not work with "select from SomeClass where a_property like 'pattern%' "
4
+
5
+ This is rewritten as
6
+
7
+ SomeClass.like "name = D*", order: 'asc'
8
+
9
+ The order-argument is optional, "asc" is the default.
10
+ This Primitiv-Version only accepts the wildcards "*" and "%" at the end of the seach-string.
11
+ The Wildcard can be omitted.
12
+
13
+ The method does not accept further arguments.
14
+ =end
15
+ def like operation, order: 'asc'
16
+ # remove all spaces and split the resulting word
17
+ p, s = operation.gsub(/\s+/, "").split("=")
18
+ if ["%","*"].include?(s[-1])
19
+ s.chop!
20
+ end
21
+
22
+ q = OrientSupport::OrientQuery.new where: { "#{p}.left(#{s.length})" => s } ,order: { p => order }
23
+ query_database q
24
+
25
+ end
26
+ end
@@ -0,0 +1,70 @@
1
+ class E < ActiveOrient::Model
2
+ ## class methods
3
+ class << self
4
+ =begin
5
+ Establish constrains on Edges
6
+
7
+ After applying this method Edges are uniq!
8
+
9
+ Creates individual indices for child-classes if applied to the class itself.
10
+ =end
11
+ def uniq_index
12
+ create_property :in, type: :link, linked_class: :V
13
+ create_property :out, type: :link, linked_class: :V
14
+ create_index "#{ref_name}_idx", on: [ :in, :out ]
15
+ end
16
+
17
+ =begin
18
+ Instantiate a new Edge between two Vertices
19
+
20
+ =end
21
+ def create from:, to: , attributes: {}, transaction: false
22
+ return nil if from.blank? || to.blank?
23
+ statement = "CREATE EDGE #{ref_name} from #{from.to_or} to #{to.to_or}"
24
+ transaction = true if [:fire, :complete, :run].include?(transaction)
25
+ ir= db.execute( transaction: transaction ){ statement }
26
+ from.reload! # get last version
27
+ to.is_a?(Array)? to.each( &:reload! ) : to.reload!
28
+ ir
29
+
30
+ rescue ArgumentError => e
31
+ logger.error{ "wrong parameters #{keyword_arguments} \n\t\t required: from: , to: , attributes:\n\t\t Edge is NOT created"}
32
+ end
33
+
34
+ =begin
35
+ Fires a "delete edge" command to the database.
36
+
37
+ The where statement can be empty ( "" or {}"), then all edges are removed
38
+
39
+ The rid-cache is resetted
40
+
41
+ :call-seq:
42
+ delete where:
43
+ =end
44
+ def delete where:
45
+
46
+ db.execute { "delete edge #{ref_name} #{db.compose_where(where)}" }
47
+ reset_rid_store
48
+
49
+ end
50
+
51
+ end # class methods
52
+
53
+ ### instance methods ###
54
+
55
+ =begin
56
+ Removes the actual ActiveOrient::Model-Edge-Object
57
+
58
+ This method overloads the unspecified ActiveOrient::Model#remove-Method
59
+ =end
60
+ def remove
61
+ # remove works on record-level
62
+ db.delete_edge self
63
+ end
64
+
65
+ def to_human
66
+ displayed_attributes = content_attributes.reject{|k,_| [:in, :out].include?(k) }
67
+ "<#{self.class.to_s.demodulize}[#{rrid}] -i-> ##{ attributes[:in].rid} #{displayed_attributes.to_human} -o-> #{out.rrid}>"
68
+ end
69
+
70
+ end
@@ -0,0 +1,134 @@
1
+ require_relative "the_class.rb"
2
+ require_relative "the_record.rb"
3
+ require_relative "custom.rb"
4
+ module ActiveOrient
5
+ class Model < ActiveOrient::Base
6
+
7
+ include BaseProperties
8
+ include ModelRecord # For objects (file: lib/record.rb)
9
+ extend CustomClass # for customized class-methods aka like
10
+ extend ModelClass # For classes
11
+
12
+ =begin
13
+ Either retrieves the object from the rid_store or loads it from the DB.
14
+
15
+ Example:
16
+ ActiveOrient::Model.autoload_object "#00:00"
17
+
18
+
19
+ The rid_store is updated!
20
+
21
+ _Todo:_ fetch for version in the db and load the object only if a change is detected
22
+
23
+ _Note:_ This function is not located in ModelClass since it needs to use @@rid_store
24
+ =end
25
+
26
+ def self.autoload_object rid
27
+ rid = rid[1..-1] if rid[0]=='#'
28
+ if rid.rid?
29
+ if @@rid_store[rid].present?
30
+ @@rid_store[rid] # return_value
31
+ else
32
+ get(rid)
33
+ end
34
+ else
35
+ logger.progname = "ActiveOrient::Model#AutoloadObject"
36
+ logger.info{"#{rid} is not a valid rid."}
37
+ end
38
+ end
39
+
40
+ ## used for active-model-compatibility
41
+ def persisted?
42
+ true
43
+ end
44
+
45
+
46
+
47
+ =begin
48
+ Based on the parameter rid (as "#{a}:{b}" or "{a}:{b}") the cached value is used if found.
49
+ Otherwise the provided Block is executed, which is responsible for the allocation of a new dataset
50
+
51
+ i.e.
52
+ ActiveOrient::Model.use_or_allocated my_rid do
53
+ ActiveOrient::Model.orientdb_class(name: raw_data['@class']).new raw_data
54
+ end
55
+
56
+ =end
57
+
58
+ def self.use_or_allocate rid
59
+ cached_obj = get_rid( rid )
60
+ if cached_obj.present?
61
+ cached_obj
62
+ else
63
+ yield
64
+ end
65
+ end
66
+
67
+
68
+ def self._to_partial_path #:nodoc:
69
+ @_to_partial_path ||= begin
70
+ element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name))
71
+ collection = ActiveSupport::Inflector.tableize(name)
72
+ "#{collection}/#{element}".freeze
73
+ end
74
+ end
75
+ ## to prevent errors when calling to_a
76
+ def to_ary # :nodoc:
77
+ attributes.to_a
78
+ end
79
+
80
+ def document # :nodoc:
81
+ @d
82
+ end
83
+
84
+ =begin
85
+ Deletes the database class and removes the ruby-class
86
+ =end
87
+ def self.delete_class what= :all
88
+ orientdb.delete_class( self ) if what == :all # remove the database-class
89
+ ## namespace is defined in config/boot
90
+ ns = namespace.to_s == 'Object' ? "" : namespace.to_s
91
+ ns_found = -> ( a_class ) do
92
+ to_compare = a_class.to_s.split(':')
93
+ if ns == "" && to_compare.size == 1
94
+ true
95
+ elsif to_compare.first == ns
96
+ true
97
+ else
98
+ false
99
+ end
100
+ end
101
+ self.allocated_classes.delete_if{|x,y| x == self.ref_name && ns_found[y]} if allocated_classes.is_a?(Hash)
102
+ namespace.send(:remove_const, naming_convention.to_sym) if namespace &.send( :const_defined?, naming_convention)
103
+ end
104
+
105
+ # provides an unique accessor on the Class
106
+ # works with a class-variable, its unique through all Subclasses
107
+ mattr_accessor :orientdb # points to the instance of the REST-DB-Client used for Administration
108
+ # i.e. creation and deleting of classes and databases
109
+ mattr_accessor :db # points to the instance of the Client used for Database-Queries
110
+ mattr_accessor :api
111
+ # mattr_accessor :logger ... already inherented from ::Base
112
+ mattr_accessor :namespace # Namespace in which Model records are initialized, a constant ( defined in config.yml )
113
+ mattr_accessor :model_dir # path to model-files
114
+ mattr_accessor :keep_models_without_file
115
+ mattr_accessor :allocated_classes
116
+
117
+ # mattr_accessor :ref_name
118
+ # Used to read the metadata
119
+ attr_reader :metadata
120
+
121
+ # provides an accessor at class level
122
+ # (unique on all instances)
123
+ class << self
124
+ attr_accessor :ref_name
125
+ attr_accessor :abstract
126
+
127
+ def to_or
128
+ ref_name.to_or
129
+ end
130
+
131
+
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,607 @@
1
+ module ModelClass
2
+ require 'stringio'
3
+ include OrientSupport::Support
4
+
5
+
6
+ ########### CLASS FUNCTIONS ######### SELF ####
7
+
8
+
9
+ ######## INITIALIZE A RECORD FROM A CLASS ########
10
+
11
+
12
+ =begin
13
+ NamingConvention provides a translation from database-names to class-names.
14
+
15
+ It can be overwritten to provide different conventions for different classes, eg. Vertexes or edges
16
+ and to introduce distinct naming-conventions in differrent namespaces
17
+
18
+ To overwrite use
19
+ class Model # < ActiveOrient::Model[:: ...]
20
+ def self.naming_convention
21
+ ( conversion code )
22
+ end
23
+ end
24
+ =end
25
+ def naming_convention name=nil
26
+ nc = name.present?? name.to_s : ref_name
27
+ if namespace_prefix.present?
28
+ nc.split(namespace_prefix).last.camelize
29
+ else
30
+ nc.camelize
31
+ end
32
+ rescue
33
+ nil
34
+ end
35
+
36
+
37
+ =begin
38
+ Set the namespace_prefix for database-classes.
39
+
40
+ If a namespace is set by
41
+ ActiveOrient::Init.define_namespace { ModuleName }
42
+ ActiveOrient translates this to
43
+ ModuleName::CamelizedClassName
44
+ The database-class becomes
45
+ modulename_class_name
46
+
47
+ If the namespace is set to a class (Object, ActiveOrient::Model ) namespace_prefix returns an empty string.
48
+
49
+ Override to change its behavior
50
+ =end
51
+ def namespace_prefix
52
+ namespace.is_a?(Class )? '' : namespace.to_s.downcase+'_'
53
+ end
54
+ =begin
55
+ orientdb_class is used to refer a ActiveOrient:Model-Object providing its name
56
+
57
+ Parameter: name: string or symbol
58
+ =end
59
+
60
+ def orientdb_class name:, superclass: nil # :nodoc: # public method: autoload_class
61
+
62
+ ActiveOrient.database_classes[name].presence || ActiveOrient::Model
63
+ rescue NoMethodError => e
64
+ logger.error { "Error in orientdb_class: is ActiveOrient.database_classes initialized ? \n\n\n" }
65
+ logger.error{ e.backtrace.map {|l| " #{l}\n"}.join }
66
+ Kernel.exit
67
+ end
68
+
69
+
70
+ =begin
71
+ setter method to initialise a dummy ActiveOrient::Model class to enable multi-level
72
+ access to links and linklists
73
+ =end
74
+
75
+ def link_list *property
76
+ property.each do |p|
77
+
78
+ the_dummy_class = orientdb.allocate_class_in_ruby("dummy_"+p.to_s)
79
+ the_dummy_class.ref_name = ref_name + "." + p.to_s
80
+ singleton_class.send :define_method, p do
81
+ the_dummy_class
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ =begin
88
+ requires the file specified in the model-dir
89
+
90
+ In fact, the model-files are loaded instead of required.
91
+ Thus, even after recreation of a class (Class.delete_class, ORD.create_class classname)
92
+ custom methods declared in the model files are present.
93
+
94
+ Required modelfiles are gone, if the class is destroyed.
95
+
96
+ The directory specified is expanded by the namespace. The parameter itself is the base-dir.
97
+
98
+ Example:
99
+ Namespace: HC
100
+ model_dir : 'lib/model'
101
+ searched directory: 'lib/model/hc'
102
+
103
+ =end
104
+ def require_model_file the_directory = nil
105
+ logger.progname = 'ModelClass#RequireModelFile'
106
+ the_directory = Pathname( the_directory.presence || ActiveOrient::Model.model_dir ) rescue nil # the_directory is a Pathname
107
+ return nil if the_directory.nil?
108
+ if File.exists?( the_directory )
109
+ model= self.to_s.underscore + ".rb"
110
+ filename = the_directory + model
111
+ if File.exists?(filename )
112
+ if load filename
113
+ logger.info{ "#{filename} sucessfully loaded" }
114
+ self #return_value
115
+ else
116
+ logger.error{ "#{filename} load error" }
117
+ nil #return_value
118
+ end
119
+ else
120
+ logger.info{ "model-file not present: #{filename}" }
121
+ nil #return_value
122
+ end
123
+ else
124
+ logger.info{ "Directory #{ the_directory } not present " }
125
+ nil #return_value
126
+ end
127
+ rescue TypeError => e
128
+ puts "TypeError: #{e.message}"
129
+ puts "Working on #{self.to_s} -> #{self.superclass}"
130
+ puts "Class_hierarchy: #{orientdb.class_hierarchy.inspect}."
131
+ print e.backtrace.join("\n")
132
+ raise
133
+ #
134
+ end
135
+
136
+ ########## CREATE ############
137
+
138
+ =begin
139
+ Universal method to create a new record.
140
+ It's overloaded to create specific kinds, eg. edge and vertex and is called only for abstract classes
141
+
142
+ Example:
143
+ ORD.create_class :test
144
+ Test.create string_attribute: 'a string', symbol_attribute: :a_symbol, array_attribute: [34,45,67]
145
+ Test.create link_attribute: Test.create( :a_new_attribute => 'new' )
146
+
147
+ =end
148
+ def create **attributes
149
+ attributes.merge :created_at => DateTime.new
150
+ result = db.create_record self, attributes: attributes
151
+ if result.nil
152
+ logger.error('Model::Class'){ "Table #{refname}: create failed: #{attributes.inspect}" }
153
+ elsif block_given?
154
+ yield result
155
+ else
156
+ result # return value
157
+ end
158
+ end
159
+
160
+ =begin
161
+ Creates or updates a record.
162
+ Parameter:
163
+ - set: A hash of attributes to insert or update unconditionally
164
+ - where: A string or hash as condition which should return just one record.
165
+
166
+ The where-part should be covered with an unique-index.
167
+
168
+ returns the affected record
169
+ =end
170
+ def upsert set: nil, where:
171
+ set = where if set.nil?
172
+ db.upsert self, set: set, where: where
173
+ end
174
+ =begin
175
+ Sets a value to certain attributes, overwrites existing entries, creates new attributes if nessesary
176
+
177
+ IB::Account.update_all connected: false
178
+ IB::Account.update_all where: "account containsText 'F'", set:{ connected: false }
179
+
180
+ **note: By calling UpdateAll, all records of the Class previously stored in the rid-cache are removed from the cache. Thus autoload gets the updated records.
181
+ =end
182
+
183
+ def update_all where: {} , set: {}, **arg
184
+ if where.empty?
185
+ set.merge! arg
186
+ end
187
+ db.update_records self, set: set, where: where
188
+
189
+ end
190
+ #
191
+ # removes a property from the collection (where given) or the entire class
192
+ def remove attribute, where:{}
193
+ db.update_records self, remove: attribute, where: where
194
+ end
195
+
196
+ =begin
197
+ Create a Property in the Schema of the Class and optionaly create an automatic index
198
+
199
+ Examples:
200
+
201
+ create_property :customer_id, type: integer, index: :unique
202
+ create_property( :name, type: :string ) { :unique }
203
+ create_property :in, type: :link, linked_class: V (used by edges)
204
+
205
+ :call-seq: create_property(field (required),
206
+ type: :a_supported_type',
207
+ linked_class: nil
208
+
209
+ supported types:
210
+ :bool :double :datetime :float :decimal
211
+ :embedded_list = :list :embedded_map = :map :embedded_set = :set
212
+ :int :integer :link_list :link_map :link_set
213
+
214
+ If `:list`, `:map`, `:set`, `:link`, `:link_list`, `:link_map` or `:link_set` is specified
215
+ a `linked_class:` parameter can be specified. Argument is the OrientDB-Class-Constant
216
+ =end
217
+ def create_property field, type: :integer, index: nil, **args
218
+ arguments = args.values.map do |y|
219
+ if y.is_a?(Class) && ActiveOrient.database_classes.values.include?(y)
220
+ y.ref_name
221
+ elsif ActiveOrient.database_classes.keys.include?(y.to_s)
222
+ y
223
+ else
224
+ puts ActiveOrient.database_classes.inspect
225
+ puts "YY : #{y.to_s} #{y.class}"
226
+ raise ArgumentError , "database class #{y.to_s} not allocated"
227
+ end
228
+ end.compact.join(',')
229
+
230
+ supported_types = {
231
+ :bool => "BOOLEAN",
232
+ :double => "BYTE",
233
+ :datetime => "DATE",
234
+ :float => "FLOAT",
235
+ :decimal => "DECIMAL",
236
+ :embedded_list => "EMBEDDEDLIST",
237
+ :list => "EMBEDDEDLIST",
238
+ :embedded_map => "EMBEDDEDMAP",
239
+ :map => "EMBEDDEDMAP",
240
+ :embedded_set => "EMBEDDEDSET",
241
+ :set => "EMBEDDEDSET",
242
+ :string => "STRING",
243
+ :int => "INTEGER",
244
+ :integer => "INTEGER",
245
+ :link => "LINK",
246
+ :link_list => "LINKLIST",
247
+ :link_map => "LINKMAP",
248
+ :link_set => "LINKSET",
249
+ }
250
+
251
+ ## if the »type« argument is a string, it is used unchanged
252
+ type = supported_types[type] if type.is_a?(Symbol)
253
+
254
+ raise ArgumentError , "unsupported type" if type.nil?
255
+ s= " CREATE PROPERTY #{ref_name}.#{field} #{type} #{arguments}"
256
+ puts s
257
+ db.execute { s }
258
+
259
+ i = block_given? ? yield : index
260
+ ## supported format of block: index: { name: 'something' , on: :automatic, type: :unique }
261
+ ## or { name: 'something' , on: :automatic, type: :unique } #
262
+ ## or { some_name: :unique } # manual index
263
+ ## or { :unique } # automatic index
264
+ if i.is_a? Hash
265
+ att= i.key( :index ) ? i.values.first : i
266
+ name, on, type = if att.size == 1 && att[:type].nil?
267
+ [att.keys.first, field, att.values.first ]
268
+ else
269
+ [ att[:name] || field , att[:on] || field , att[:type] || :unique ]
270
+ end
271
+ create_index( name , on: on, type: type)
272
+ elsif i.is_a?(Symbol) || i.is_a?(String)
273
+ create_index field, type: i
274
+ end
275
+
276
+ # orientdb.create_property self, field, **keyword_arguments, &b
277
+ end
278
+
279
+ # Create more Properties in the Schema of the Class
280
+
281
+ def create_properties argument_hash, &b
282
+ orientdb.create_properties self, argument_hash, &b
283
+ end
284
+
285
+
286
+ # Add an Index
287
+ def create_index name, **attributes
288
+ orientdb.create_index self, name: name, **attributes
289
+ end
290
+
291
+ # list all Indexes
292
+ def indexes
293
+ properties[:indexes]
294
+ end
295
+ ########## GET ###############
296
+
297
+ def classname # :nodoc: #
298
+ ref_name
299
+ end
300
+
301
+ # get elements by rid
302
+
303
+ def get rid
304
+ if @excluded.blank?
305
+ db.get_record(rid)
306
+ else
307
+ db.execute{ "select expand( @this.exclude( #{@excluded.map(&:to_or).join(",")})) from #{rid} "}
308
+ end
309
+ end
310
+
311
+ # get all the elements of the class
312
+
313
+ def all
314
+ db.get_records from: self
315
+ end
316
+
317
+ # get the first element of the class
318
+
319
+ def first where: {}
320
+ db.get_records(from: self, where: where, limit: 1).pop
321
+ end
322
+
323
+ # get the last element of the class
324
+
325
+ def last where: {}
326
+ db.get_records(from: self, where: where, order: {"@rid" => 'desc'}, limit: 1).pop
327
+ end
328
+ # Used to count of the elements in the class
329
+
330
+ def count **args
331
+ orientdb.count from: self, **args
332
+ end
333
+
334
+ # Get the properties of the class
335
+
336
+ def properties
337
+ object = orientdb.get_class_properties self
338
+ #HashWithIndifferentAccess.new :properties => object['properties'], :indexes => object['indexes']
339
+ {:properties => object['properties'], :indexes => object['indexes']}
340
+ end
341
+ alias get_class_properties properties
342
+
343
+ # Print the properties of the class
344
+
345
+ def print_properties
346
+ orientdb.print_class_properties self
347
+ end
348
+
349
+ =begin
350
+ »GetRecords« uses the REST-Interface to query the database. The alternative »QueryDatabase« submits
351
+ the query via Batch.
352
+
353
+ Both methods rely on OrientSupport::OrientQuery and its capacity to support complex query-builds.
354
+ The method requires a hash of arguments. The following keys are supported:
355
+
356
+ *projection:*
357
+
358
+ SQL-Queries use »select« to specify a projection (ie. `select sum(a), b+5 as z from class where ...`)
359
+
360
+ In ruby »select« is a method of enumeration. To specify anything etween »select« and »from« in the query-string
361
+ we use »projection«, which acceps different arguments
362
+
363
+ projection: a_string --> inserts the sting as it appears
364
+ projection: an OrientSupport::OrientQuery-Object --> performs a sub-query and uses the result for further querying though the given parameters.
365
+ projection: [a, b, c] --> "a, b, c" (inserts a comma-separated list)
366
+ projection: {a: b, "sum(x)" => f} --> "a as b, sum(x) as f" (renames properties and uses functions)
367
+
368
+ *distinct:*
369
+
370
+ Constructs a query like »select distinct(property) [as property] from ...«
371
+
372
+ distinct: :property --> the result is mapped to the property »distinct«.
373
+ distinct: [:property] --> the result replaces the property
374
+ distinct: {property: :some_name} --> the result is mapped to ModelInstance.some_name
375
+
376
+ *order:*
377
+
378
+ Sorts the result-set. If new properties were introduced via select:, distinct: etc. Sorting takes place on these properties
379
+
380
+ order: :property {property: asc, property: desc}[property, property, .. ](orderdirection is 'asc')
381
+
382
+
383
+ Further supported Parameter:
384
+
385
+ group_by
386
+ skip
387
+ limit
388
+ unwind
389
+
390
+ see orientdb- documentation (https://orientdb.com/docs/last/SQL-Query.html)
391
+
392
+ *query:*
393
+
394
+ Instead of providing the parameter to »get_records«, a OrientSupport::OrientQuery can build and
395
+ tested prior to the method-call. The OrientQuery-Object is then provided with the query-parameter. I.e.
396
+
397
+ q = OrientSupport::OrientQuery.new
398
+ ORD.create_class :test_model
399
+ q.from TestModel
400
+ q.where {name: 'Thomas'}
401
+ count = TestModel.count query: q
402
+ q.limit 10
403
+ 0.step(count,10) do |x|
404
+ q.skip = x
405
+ puts TestModel.get_documents(query: q).map{|x| x.adress }.join('\t')
406
+ end
407
+ prints a Table with 10 columns.
408
+ =end
409
+
410
+ def get_records **args
411
+ db.get_records(from: self, **args){self}
412
+ end
413
+ alias get_documents get_records
414
+
415
+
416
+ def custom_where search_string
417
+ q = OrientSupport::OrientQuery.new from: self, where: search_string
418
+ #puts q.compose
419
+ query_database q
420
+ end
421
+ =begin
422
+ Performs a query on the Class and returns an Array of ActiveOrient:Model-Records.
423
+
424
+ Example:
425
+ Log.where priority: 'high'
426
+ --> submited database-request: query/hc_database/sql/select from Log where priority = 'high'/-1
427
+ => [ #<Log:0x0000000480f7d8 @metadata={ ... }, ...
428
+
429
+ Multible arguments are joined via "and" eg
430
+ Aktie.where symbol: 'TSL, exchange: 'ASX'
431
+ ---> select from aktie where symbol = 'TLS' and exchange = 'ASX'
432
+
433
+
434
+ Where performs a »match-Query« that returns only links to the queries records.
435
+ These are autoloaded (and reused from the cache). If changed database-records should be obtained,
436
+ custom_query should be used. It performs a "select form class where ... " query which returns records
437
+ instead of links.
438
+
439
+ Property.custom_where( "'Hamburg' in exchanges.label")
440
+
441
+ =end
442
+
443
+ def where *attributes
444
+ query= OrientSupport::OrientQuery.new kind: :match, start:{ class: self.classname }
445
+ query.match_statements[0].where = attributes unless attributes.empty?
446
+ # the block contains a result-record :
447
+ #<ActiveOrient::Model:0x0000000003972e00
448
+ # @metadata={:type=>"d", :class=>nil, :version=>0, :fieldTypes=>"test_models=x"}, @d=nil,
449
+ # @attributes={:test_models=>"#29:3", :created_at=>Thu, 28 Mar 2019 10:43:51 +0000}>]
450
+ # ^...........° -> classname.pluralize
451
+ query_database( query, set_from: false){| record | record.is_a?(ActiveOrient::Model) ? record : record.send( self.classnamepluralize.to_sym ) }
452
+ end
453
+ =begin
454
+ Performs a Match-Query
455
+
456
+ The Query starts at the given ActiveOrient::Model-Class. The where-cause narrows the sample to certain
457
+ records. In the simplest version this can be returned:
458
+
459
+ Industry.match where:{ name: "Communications" }
460
+ => #<ActiveOrient::Model::Query:0x00000004309608 @metadata={"type"=>"d", "class"=>nil, "version"=>0, "fieldTypes"=>"Industries=x"}, @attributes={"Industries"=>"#21:1", (...)}>
461
+
462
+ The attributes are the return-Values of the Match-Query. Unless otherwise noted, the pluralized Model-Classname is used as attribute in the result-set.
463
+
464
+ I.match( where: { name: 'Communications' }).first.Industries
465
+
466
+ is the same then
467
+ Industry.where name: "Communications"
468
+
469
+
470
+ The Match-Query uses this result-set as start for subsequent queries on connected records.
471
+ These connections are defined in the Block
472
+
473
+ var = Industry.match do | query |
474
+ query.connect :in, count: 2, as: 'Subcategories'
475
+ puts query.to_s # print the query before sending it to the database
476
+ query # important: block has to return the query
477
+ end
478
+ => MATCH {class: Industry, as: Industries} <-- {} <-- { as: Subcategories } RETURN Industries, Subcategories
479
+
480
+ The result-set has two attributes: Industries and Subcategories, pointing to the filtered datasets.
481
+
482
+ By using subsequent »connect« and »statement« method-calls even complex Match-Queries can be clearly constructed.
483
+
484
+ =end
485
+
486
+ def match where: {}
487
+ query= OrientSupport::OrientQuery.new kind: :match, start:{ class: self.classname }
488
+ query.match_statements[0].where = where unless where.empty?
489
+ if block_given?
490
+ query_database yield(query), set_from: false
491
+ else
492
+ send :where, where
493
+ end
494
+
495
+ end
496
+
497
+
498
+ =begin
499
+ QueryDatabase sends the Query directly to the database.
500
+
501
+ The result is not nessessary an Object of the Class.
502
+
503
+ The result can be modified further by passing a block.
504
+ This is helpful, if a match-statement is used and the records should be autoloaded:
505
+
506
+ result = query_database(query, set_from: false){| record | record[ self.classname.pluralize ] }
507
+
508
+ This autoloads (fetches from the cache/ or database) the attribute self.classname.pluralize (taken from method: where )
509
+
510
+
511
+ query_database is used on model-level and submits
512
+ select (...) from class
513
+
514
+ #query performs queries on the instance-level and submits
515
+ select (...) from #{a}:{b}
516
+
517
+ =end
518
+
519
+ def query_database query, set_from: true
520
+ query.from self if set_from && query.is_a?(OrientSupport::OrientQuery) && query.from.nil?
521
+ sql_cmd = -> (command) {{ type: "cmd", language: "sql", command: command }}
522
+ result = db.execute do
523
+ query.to_s # sql_cmd[query.to_s]
524
+ end
525
+ if block_given?
526
+ result.is_a?(Array)? result.map{|x| yield x } : yield(result)
527
+ else
528
+ result
529
+ end
530
+ if result.is_a? Array
531
+ OrientSupport::Array.new work_on: self, work_with: result
532
+ else
533
+ result
534
+ end # return value
535
+ end
536
+
537
+ ########### DELETE ###############
538
+
539
+ # Delete a property from the class
540
+
541
+ def delete_property field
542
+ orientdb.delete_property self, field
543
+ end
544
+
545
+ # Delete record(s) specified by their rid's
546
+
547
+ def delete_record *rid
548
+ db.delete_record rid
549
+ end
550
+ alias delete_document delete_record
551
+
552
+ # Query the database and delete the records of the resultset
553
+
554
+ def delete_records where: {}
555
+ orientdb.delete_records self, where: where
556
+ end
557
+ alias delete_documents delete_records
558
+
559
+
560
+
561
+ ##################### EXPERIMENT #################
562
+
563
+ =begin
564
+ Suppose that you created a graph where vertexes month is connected with
565
+ the vertexes day by the edge TIMEOF.
566
+ Suppose we want to find all the days in the first month and in the third month..
567
+
568
+ Usually we can do in the following way.
569
+
570
+ ORD.create_class :month
571
+ (.. put some records into Month ... )
572
+ firstmonth = Month.first
573
+ thirdmonth = Month.all[2]
574
+ days_firstmonth = firstmonth.out_TIMEOF.map{|x| x.in}
575
+ days_thirdmonth = thirdmonth.out_TIMEOF.map{|x| x.in}
576
+
577
+ However we can obtain the same result with the following command
578
+
579
+ Month.add_edge_link name: "days", direction: "out", edge: TIME_OF
580
+ firstmonth = month.first
581
+ thirdmonth = month.all[2]
582
+ days_firstmonth = firstmonth.days
583
+ days_thirdmonth = thirdmonth.days
584
+
585
+ To get their value you can do:
586
+ thirdmonth.days.value
587
+ =end
588
+
589
+
590
+ def add_edge_link name:, direction: :out, edge:
591
+ dir = direction.to_s == "out" ? :out : :in
592
+ define_method(name.to_sym) do
593
+ return self["#{dir}_#{edge.classname}"].map{|x| x["in"]}
594
+ end
595
+ end
596
+
597
+ =begin
598
+ See http://orientdb.com/docs/2.1/SQL-Alter-Property.html
599
+ =end
600
+
601
+ def alter_property property:, attribute: "DEFAULT", alteration: # :nodoc:
602
+ orientdb.alter_property self, property: property, attribute: attribute, alteration: alteration
603
+ end
604
+
605
+
606
+
607
+ end