module ModelClass require 'stringio' include OrientSupport::Support ########### CLASS FUNCTIONS ######### SELF #### ######## INITIALIZE A RECORD FROM A CLASS ######## =begin NamingConvention provides a translation from database-names to class-names. It can be overwritten to provide different conventions for different classes, eg. Vertexes or edges and to introduce distinct naming-conventions in different namespaces To overwrite use class Model # < ActiveOrient::Model[:: ...] def self.naming_convention ( conversion code ) end end =end def naming_convention name=nil nc = name.present?? name.to_s : ref_name if namespace_prefix.present? nc.split(namespace_prefix).last.camelize else nc.camelize end rescue nil end =begin Set the namespace_prefix for database-classes. If a namespace is set by ActiveOrient::Init.define_namespace { ModuleName } ActiveOrient translates this to ModuleName::CamelizedClassName The database-class becomes modulename_class_name If the namespace is set to a class (Object, ActiveOrient::Model ) namespace_prefix returns an empty string. Override to change its behavior =end def namespace_prefix namespace.is_a?(Class )? '' : namespace.to_s.downcase+'_' end =begin orientdb_class is used to refer a ActiveOrient:Model-Object providing its name Parameter: name: string or symbol =end def orientdb_class name:, superclass: nil # :nodoc: # public method: autoload_class ActiveOrient.database_classes[name.to_s].presence || ActiveOrient::Model rescue NoMethodError => e logger.error { "Error in orientdb_class: is ActiveOrient.database_classes initialized ? \n\n\n" } logger.error{ e.backtrace.map {|l| " #{l}\n"}.join } Kernel.exit end =begin setter method to initialise a dummy ActiveOrient::Model class to enable multi-level access to links and linklists =end def link_list *property property.each do |p| the_dummy_class = orientdb.allocate_class_in_ruby("dummy_"+p.to_s) the_dummy_class.ref_name = ref_name + "." + p.to_s singleton_class.send :define_method, p do the_dummy_class end end end =begin requires the file specified in the model-dir In fact, the model-files are loaded instead of required. Thus, even after recreation of a class (Class.delete_class, ORD.create_class classname) custom methods declared in the model files are present. If a class is destroyed (i.e. the database class is deleted), the ruby-class and its methods vanish, too. The directory specified is expanded by the namespace. The parameter itself is the base-dir. Example: Namespace: HC model_dir : 'lib/model' searched directory: 'lib/model/hc' ActiveOrient::Model.modeldir is aimed to be set to the application dir. It may be a String, Pathname or an array of strings or pathnames. The parameter `dir` is used internally and by gems to ensure that basic methods are loaded first. =end def require_model_file dir = nil logger.progname = 'ModelClass#RequireModelFile' # model-dir can either be a string or an array of string or pathnames default = [ActiveOrient::Model.model_dir].flatten # access the default dir's first the_directories = case dir when String, Pathname default.present? ? [dir] + default : [dir] when Array default.present? ? dir + default : dir else default.present? ? default : [] end.uniq.compact the_directories.uniq.map do |raw_directory| the_directory = Pathname( raw_directory ) if File.exists?( the_directory ) model= self.to_s.underscore + ".rb" filename = the_directory + model if File.exists?(filename ) if load filename logger.debug{ "#{filename} sucessfully loaded" } self #return_value else logger.error{ "#{filename} load error" } nil #return_value end else logger.debug{ "model-file not present: #{filename} --> skipping" } nil #return_value end else logger.error{ "Directory #{ the_directory } not present " } nil #return_value end end.compact.present? # return true only if at least one model-file is present rescue TypeError => e puts "THE CLASS#require_model_file -> TypeError: #{e.message}" puts "Working on #{self.to_s} -> #{self.superclass}" puts "Class_hierarchy: #{orientdb.class_hierarchy.inspect}." print e.backtrace.join("\n") raise # end # creates an inherent class def create_class *c orientdb.create_class( *c ){ self } end ########## CREATE ############ =begin Universal method to create a new record. It's overloaded to create specific kinds, eg. edge and vertex and is called only for abstract classes Example: V.create_class :test Test.create string_attribute: 'a string', symbol_attribute: :a_symbol, array_attribute: [34,45,67] Test.create link_attribute: Test.create( :a_new_attribute => 'new' ) =end def create **attributes attributes.merge :created_at => DateTime.new result = db.create_record self, attributes: attributes if result.nil logger.error('Model::Class'){ "Table #{refname}: create failed: #{attributes.inspect}" } elsif block_given? yield result else result # return value end end # returns a OrientSupport::OrientQuery def query **args OrientSupport::OrientQuery.new( **( {from: self}.merge args)) end =begin Creates or updates records. Parameter: - set: A hash of attributes to insert or update unconditionally - where: A string or hash as condition which should return just one record. The where-part should be covered with an unique-index. returns the affected record, if the where-condition is set properly. Otherwise upsert acts as »update« and returns all updated records (as array). =end def upsert set: nil, where: , **args set = where if set.nil? query( **args.merge( kind: :upsert, set: set, where: where )).execute(reduce: true){|y| y[:$current].reload!} end =begin Sets a value to certain attributes, overwrites existing entries, creates new attributes if necessary returns the count of affected records IB::Account.update connected: false IB::Account.update where: "account containsText 'F'", set:{ connected: false } # or IB::Account.update connected: false, where: "account containsText 'F'" =end def update! where: nil , set: {}, **arg query( kind: :update!, set: set.merge(arg), where: where).execute(reduce: true){|y| y[:count]} end alias update_all update! # same as update!, but returns a list of updated records def update where: , set: {}, **arg # In OrientDB V.3 the database only returns the affected rid's # We have to update the contents manually, this is done in the execute-block query( kind: :update, set: set.merge(arg), where: where).execute{|y| y[:$current].reload!} end =begin Create a Property in the Schema of the Class and optionally create an automatic index Examples: create_property :customer_id, type: :integer, index: :unique create_property( :name, type: :string ) { :unique } create_property( :name, type: :string ) { name: 'some_index', on: :automatic, type: :unique } create_property :in, type: :link, linked_class: V (used by edges) :call-seq: create_property(field (required), type: :a_supported_type', linked_class: nil supported types: :bool :double :datetime = :date :float :decimal :embedded_list = :list :embedded_map = :map :embedded_set = :set :int :integer :link_list :link_map :link_set If `:list`, `:map`, `:set`, `:link`, `:link_list`, `:link_map` or `:link_set` is specified a `linked_class:` parameter can be specified. Argument is the OrientDB-Class-Constant =end def create_property field, type: :integer, index: nil, **args arguments = args.values.map do |y| if y.is_a?(Class) && ActiveOrient.database_classes.values.include?(y) y.ref_name elsif ActiveOrient.database_classes.keys.include?(y.to_s) y else puts ActiveOrient.database_classes.inspect puts "YY : #{y.to_s} #{y.class}" raise ArgumentError , "database class #{y.to_s} not allocated" end end.compact.join(',') supported_types = { :bool => "BOOLEAN", :double => "BYTE", :datetime => "DATE", :date => "DATE", :float => "FLOAT", :decimal => "DECIMAL", :embedded_list => "EMBEDDEDLIST", :list => "EMBEDDEDLIST", :embedded_map => "EMBEDDEDMAP", :map => "EMBEDDEDMAP", :embedded_set => "EMBEDDEDSET", :set => "EMBEDDEDSET", :string => "STRING", :int => "INTEGER", :integer => "INTEGER", :link => "LINK", :link_list => "LINKLIST", :link_map => "LINKMAP", :link_set => "LINKSET", } ## if the »type« argument is a string, it is used unchanged type = supported_types[type] if type.is_a?(Symbol) raise ArgumentError , "unsupported type" if type.nil? s= " CREATE PROPERTY #{ref_name}.#{field} #{type} #{arguments}" puts s db.execute { s } i = block_given? ? yield : index ## supported format of block: index: { name: 'something' , on: :automatic, type: :unique } ## or { name: 'something' , on: :automatic, type: :unique } # ## or { some_name: :unique } # manual index ## or { :unique } # automatic index if i.is_a? Hash att= i.key( :index ) ? i.values.first : i name, on, type = if att.size == 1 && att[:type].nil? [att.keys.first, field, att.values.first ] else [ att[:name] || field , att[:on] || field , att[:type] || :unique ] end create_index( name , on: on, type: type) elsif i.is_a?(Symbol) || i.is_a?(String) create_index field, type: i end # orientdb.create_property self, field, **keyword_arguments, &b end # Create more Properties in the Schema of the Class def create_properties argument_hash, &b orientdb.create_properties self, argument_hash, &b end # Add an Index # # Parameters: # name (string / symbol), # [ on: :automatic / single Column, Array of Columns, # [ type: :unique, :nonunique, :dictionary,:fulltext, {other supported index-types} ]] # # Default: # on: :automatic # type: :unique # # Example # # ORD.create_vertex_class :pagination # Pagination.create_property :col1 , type: :string # Pagination.create_property :col2, type: :integer # Pagination.create_property :col3, type: :string # Pagination.create_property :col4, type: :integer # Pagination.create_index :composite, :on => [:col1, :col2, :col3], type: 'dictionary' def create_index name, **attributes orientdb.create_index self, name: name, **attributes end # list all Indexes def indexes properties[:indexes] end def migrate_property property, to: , linked_class: nil, via: 'tzr983' if linked_class.nil? create_property via, type: to else create_property via, type: to, linked_class: linked_class end # my_count = query.kind(:update!).set( "#{via} = #{property} ").execute(reduce: true){|c| c[:count]} # logger.info{" migrate property: #{count} records prosessed"} all.each{ |r| r.update set:{ via => r[property.to_sym] }} nullify = query.kind(:update!).set( property: nil ).execute(reduce: true){|c| c[:count]} # raise "migrate property: count of erased items( #{nullify} differs from total count (#{my_count}) " if nullify != my_count db.execute{" alter property #{ref_name}.#{via} name '#{property}' "} logger.info{ "successfully migrated #{property} to #{:to} " } end ########## GET ############### def classname # :nodoc: # ref_name end # get elements by rid def get rid if @excluded.blank? db.get_record(rid) else db.execute{ "select expand( @this.exclude( #{@excluded.map(&:to_or).join(",")})) from #{rid} "} end end # get all the elements of the class def all query.execute end # get the first element of the class def first **args query( **( { order: "@rid" , limit: 1 }.merge args)).execute(reduce: true) end # db.get_records(from: self, where: where, limit: 1).pop #end # get the last element of the class def last **args query( **( { order: {"@rid" => 'desc'} , limit: 1 }.merge args)).execute(reduce: true) end # Used to count of the elements in the class # # Examples # TestClass.count where: 'last_access is NULL' # only records where 'last_access' is not set # TestClass.count # all records def count **args query( **( { projection: 'COUNT(*)' }.merge args )).execute(reduce: true){|x| x[:"COUNT(*)"]} end # Get the properties of the class def properties object = orientdb.get_class_properties self {:properties => object['properties'], :indexes => object['indexes']} end alias get_class_properties properties # Print the properties of the class def print_properties orientdb.print_class_properties self end =begin »GetRecords« uses the REST-Interface to query the database. The alternative »QueryDatabase« submits the query via Execute. Both methods rely on OrientSupport::OrientQuery and its capacity to support complex query-builds. The method requires a hash of arguments. The following keys are supported: *projection:* SQL-Queries use »select« to specify a projection (ie. `select sum(a), b+5 as z from class where ...`) In ruby »select« is a method of enumeration. To specify what to »select« from in the query-string we use »projection«, which accepts different arguments projection: a_string --> inserts the sting as it appears projection: an OrientSupport::OrientQuery-Object --> performs a sub-query and uses the result for further querying though the given parameters. projection: [a, b, c] --> "a, b, c" (inserts a comma-separated list) projection: {a: b, "sum(x)" => f} --> "a as b, sum(x) as f" (renames properties and uses functions) *distinct:* Constructs a query like »select distinct(property) [as property] from ...« distinct: :property --> the result is mapped to the property »distinct«. distinct: [:property] --> the result replaces the property distinct: {property: :some_name} --> the result is mapped to ModelInstance.some_name *order:* Sorts the result-set. If new properties were introduced via select:, distinct: etc. Sorting takes place on these properties order: :property {property: asc, property: desc}[property, property, .. ](orderdirection is 'asc') Further supported Parameter: group_by skip limit unwind see orientdb- documentation (https://orientdb.com/docs/last/SQL-Query.html) *query:* Instead of providing the parameter to »get_records«, a OrientSupport::OrientQuery can build and tested prior to the method-call. The OrientQuery-Object is then provided with the query-parameter. I.e. q = OrientSupport::OrientQuery.new ORD.create_class :test_model q.from TestModel q.where {name: 'Thomas'} count = TestModel.count query: q q.limit 10 0.step(count,10) do |x| q.skip = x puts TestModel.get_documents(query: q).map{|x| x.adress }.join('\t') end prints a Table with 10 columns. =end def get_records **args db.get_records(from: self, **args){self} end alias get_documents get_records =begin Performs a query on the Class and returns an Array of ActiveOrient:Model-Records. Fall-back method, is overloaded by Vertex.where Is aliased by »custom_where» Example: Log.where priority: 'high' --> submitted database-request: query/hc_database/sql/select from Log where priority = 'high'/-1 => [ # select from aktie where symbol = 'TLS' and exchange = 'ASX' =end def where *attributes q= OrientSupport::OrientQuery.new where: attributes query_database( q) end alias custom_where where =begin QueryDatabase sends the Query directly to the database. The query returns a hash if a result set is expected select {something} as {result} (...) leads to [ { :{result} => {result of query} } ] It can be modified further by passing a block, ie. q = OrientSupport::OrientQuery.new( from: :base ) .projection( 'first_list[5].second_list[9] as second_list' ) .where( label: 9 ) q.to_s => 'select first_list[5].second_list[9] as second_list from base where label = 9 ' second_list = Base.query_database( q ){|x| x[:second_list]}.first The query returns (a list of) documents of type ActiveOrient::Model if a document is queried i.e. q = OrientSupport::OrientQuery.new from: :base q.projection 'expand( first_list[5].second_list[9])' #note: no 'as' statement result2 = Base.query_database( q ).first => #9, "@class"=>"second_list"}> query_database is used on model-level and submits select (...) from class #query performs queries on the instance-level and submits select (...) from #{a}:{b} =end def query_database query, set_from: true # note: the parameter is not used anymore query.from self if query.is_a?(OrientSupport::OrientQuery) && query.from.nil? result = db.execute{ query.to_s } result = if block_given? result.is_a?(Array) ? result.map{|x| yield(x) } : yield(result) else result end if result.is_a? Array OrientSupport::Array.new work_on: self, work_with: result else result end # return value end ########### DELETE ############### # Delete a property from the class def delete_property field orientdb.delete_property self, field end # Delete record(s) specified by their rid's def delete_record *rid db.delete_record rid end alias delete_document delete_record # Query the database and delete the records of the resultset # # Returns the count of datasets effected def delete_records where: {} , **args if args[:all] == true where = {} else where.merge!(args) if where.is_a?(Hash) return 0 if where.empty? end orientdb.delete_records( self, where: where ).count end alias delete delete_records ##################### EXPERIMENT ################# =begin Suppose that you created a graph where vertexes month is connected with the vertexes day by the edge TIMEOF. Suppose we want to find all the days in the first month and in the third month.. Usually we can do in the following way. ORD.create_class :month (.. put some records into Month ... ) firstmonth = Month.first thirdmonth = Month.all[2] days_firstmonth = firstmonth.out_TIMEOF.map{|x| x.in} days_thirdmonth = thirdmonth.out_TIMEOF.map{|x| x.in} However we can obtain the same result with the following command Month.add_edge_link name: "days", direction: "out", edge: TIME_OF firstmonth = month.first thirdmonth = month.all[2] days_firstmonth = firstmonth.days days_thirdmonth = thirdmonth.days To get their value you can do: thirdmonth.days.value =end def add_edge_link name:, direction: :out, edge: dir = direction.to_s == "out" ? :out : :in define_method(name.to_sym) do return self["#{dir}_#{edge.classname}"].map{|x| x["in"]} end end =begin See http://orientdb.com/docs/2.1/SQL-Alter-Property.html =end def alter_property property, attribute: "DEFAULT", alteration: # :nodoc: orientdb.alter_property self, property: property, attribute: attribute, alteration: alteration end end