lib/og/store/kirby.rb in og-0.29.0 vs lib/og/store/kirby.rb in og-0.30.0

- old
+ new

@@ -8,340 +8,483 @@ require 'fileutils' require 'og/store/sql' module Og - -# A Store that persists objects into a KirbyBase database. -# To read documentation about the methods, consult the documentation -# for SqlStore and Store. - -class KirbyStore < SqlStore - - # Override if needed. - - def self.base_dir(options) - options[:base_dir] || './kirbydb' + + #Fix for schema inheritance. Instead of inferring the property on each + #access of the database we add it explicitly when the marker module + #is imported. Currently this fix is applied for this store only. The + #other stores should be similarly altered. + module SchemaInheritanceBase + property :ogtype, String end + + # A Store that persists objects into a KirbyBase database. + # To read documentation about the methods, consult the documentation + # for SqlStore and Store. + + class KirbyStore < SqlStore + + Text = :Text - def self.destroy(options) - begin - FileUtils.rm_rf(base_dir(options)) + # Override if needed. + + def self.base_dir(options) + options[:base_dir] || './kirbydb' + end + + def self.destroy(options) + begin + FileUtils.rm_rf(base_dir(options)) + super + rescue Object + Logger.info 'Cannot drop database!' + end + end + + def initialize(options) super - rescue Object - Logger.info 'Cannot drop database!' + + @typemap = { + Integer => :Integer, + Fixnum => :Integer, + Float => :Float, + String => :String, + Time => :Time, + Date => :Date, + TrueClass => :Boolean, + FalseClass => :Boolean, + Object => :Object, + Array => :String, + Hash => :String + } + + mode = options[:mode] || :local + + if mode == :client + # Use a client/server configuration. + @conn = KirbyBase.new(:client, options[:address], options[:port]) + else + # Use an in-process configuration. + base_dir = self.class.base_dir(options) + FileUtils.mkdir_p(base_dir) unless File.exist?(base_dir) + @conn = KirbyBase.new( + :local, + nil, nil, + base_dir, + options[:ext] || '.tbl' + ) + end end - end - - def initialize(options) - super - @typemap = { - :Fixnum => :Integer, - :TrueClass => :Boolean - } + def close + # Nothing to do. + super + end - mode = options[:mode] || :local + def enchant(klass, manager) + klass.send(:attr_accessor, :oid) + klass.send(:alias_method, :recno, :oid) + klass.send(:alias_method, :recno=, :oid=) + + super + end - if mode == :client - # Use a client/server configuration. - @conn = KirbyBase.new(:client, options[:address], options[:port]) - else - # Use an in-process configuration. - base_dir = self.class.base_dir(options) - FileUtils.mkdir_p(base_dir) unless File.exist?(base_dir) - @conn = KirbyBase.new( - :local, - nil, nil, - base_dir, - options[:ext] || '.tbl' - ) + def get_table(klass) + @conn.get_table(klass.table.to_sym) end - end + + # :section: Lifecycle methods. + + def load(pk, klass) + convert_to_instance(klass, get_table(klass)[pk.to_i]) + end + + #convert the given record into an instance of the given class. + #Supports schema_inheritance. + def convert_to_instance(klass, record) + return nil unless record + + if(klass.schema_inheritance?) + obj = eval("#{record.ogtype}.allocate") + elsif + obj = klass.allocate + end - def close - # Nothing to do. - super - end + record.each_pair do |k, v| + assign_sym = (k.to_s + '=').to_sym + if (v && obj.respond_to?(assign_sym)) + obj.method(assign_sym).call(v) + end + end + obj + end + + alias_method :exist?, :load + + def reload(obj, pk) + raise 'Cannot reload unmanaged object' unless obj.saved? + new_obj = load(pk, obj.class) + #TODO: replace obj with new_obj + end + + def find(options) + #FIXME: this currently overrides options with scope; this should + #work the other way around + if scope = options[:class].get_scope + scope = scope.dup + options.update(scope) + end + query(options) + end - def enchant(klass, manager) - klass.send :attr_accessor, :oid - klass.send :alias_method, :recno, :oid - klass.send :alias_method, :recno=, :oid= + def find_one(options) + query(options).first + end + + #-- + # FIXME: optimize me! + #++ + + def count(options) + find(options).size + end + + #-- + # FIXME: optimize me! + # + #++ + + def aggregate(term = 'COUNT(*)', options = {}) + case term.upcase + when /COUNT/ + count(options) + when /MIN/ + term =~ /MIN\((\w*)\)/ + find_column_values($1, options){|result| result.min} + when /MAX/ + term =~ /MAX\((\w*)\)/ + find_column_values($1, options) {|result| result.max} + when /AVG/ + term =~ /AVG\((\w*)\)/ + values = find_column_values($1, options) do |results| + results.inject(0.0){|sum, result| sum + result} / results.size + end + when /SUM/ + term =~ /SUM\((\w*)\)/ + values = find_column_values($1, options) do |results| + results.inject(0.0){|sum, result| sum + result} + end + end + end + alias_method :calculate, :aggregate + + #find an array of values for the given column; or a hash of arrays + #if there is a group by expression. Takes an optional block + #that operates on each value in the array. + + def find_column_values(column_name, options) + options[:column] = column_name + results = find(options) + if (block_given?) + if (options[:group]) #hash of arrays + results.values.collect { |group| yield group} + else + yield results + end + end + end - symbols = klass.properties.keys - - klass.module_eval %{ - def self.kb_create(recno, #{symbols.join(', ')}) - obj = self.allocate - obj.recno = recno - #{ symbols.map { |s| "obj.#{s} = #{s}; "} } - return obj + #needs refactoring, badly + def query(options) + Logger.debug "Querying with #{options.inspect}." if $DBG + + klass = options[:class] + + table = get_table(klass) + result_set = [] + if (klass.schema_inheritance_child?) + type_condition = 'ogtype = \'' + klass.name + '\'' + condition = options[:condition] + + if (condition) + #if there is already an ogtype condition do nothing + if (condition !~ /ogtype/) + options[:condition] = condition + " and #{type_condition}" + end + else + options[:condition] = type_condition + end end - } + + #FIXME: add support for select statements with mathematics (tc_select) + #as well as 'from table' statements + if condition = options[:condition] || options[:where] + condition = condition.is_a?(Array) ? prepare_statement(condition) : condition + condition.gsub!(/ogtc_resolveoptions\w*\./, '') + condition.gsub!(/([^><])=/, '\1==') + condition.gsub!(/ OR /, ' or ') + condition.gsub!(/ AND /, ' and ') + condition.gsub!(/LIKE '(.*?)'/, '=~ /\1/') + condition.gsub!(/\%/, '') + condition.gsub!(/(\w*?)\s?([=><])(.)/, 'o.\1 \2\3') + condition.gsub!(/o.oid/, 'o.recno') + if(condition =~ /LIMIT (\d+)/) + condition.gsub!(/LIMIT (\d+)/, '') + options[:limit] = $1.to_i + end + result_set = eval("table.select do |o| #{condition} end") + else + result_set = table.select + end + + if (order = options[:order]) + order = order.to_s + desc = (order =~ /DESC/) + order = order.gsub(/DESC/, '').gsub(/ASC/, '') + eval("result_set.sort { |x, y| x.#{order} <=> y.#{order} }") + result_set.reverse! if desc + end + + if (options[:limit]) + limit = options[:limit] + if (result_set.size > limit) + result_set = result_set[0, limit] + end + end + + if (column = options[:column]) + if (!result_set.methods.include?(column)) + return [] + end + begin + #if there is a group by expression on a valid column + #gather arrays of values + if ((group = options[:group]) && result_set.method(options[:group])) + #store an empty array under the key if there is no value + groups = Hash.new {|groups, key| groups[key] = []} + result_set.each do |object| + groups[object.method(group).call] << object.method(column).call + end + return groups + end + rescue NameError => error + Logger.warn 'Attempt to group by missing column:' + Logger.warn 'Column \'' + options[:group].to_s + '\' does not exist on table \'' + klass.name + '\'' + raise error + end + + #when called on a resultset a column method returns an array of that + #column's values + return result_set.method(column).call() + end - super - end - - def get_table(klass) - @conn.get_table(klass.table.to_sym) - end - - # :section: Lifecycle methods. - - def load(pk, klass) - get_table(klass)[pk.to_i] - end - alias_method :exist?, :load - - def reload(obj, pk) - raise 'Cannot reload unmanaged object' unless obj.saved? - new_obj = load(pk, obj.class) - obj.clone(new_obj) - end - - def find(options) - query(options) - end - - def find_one(options) - query(options).first - end - - #-- - # FIXME: optimize me! - #++ - - def count(options) - find(options).size - end - - def query(options) - Logger.debug "Querying with #{options.inspect}." if $DBG - - klass = options[:class] - table = get_table(klass) - - objects = [] - - if condition = options[:condition] || options[:where] - condition.gsub!(/=/, '==') - condition.gsub!(/LIKE '(.*?)'/, '=~ /\1/') - condition.gsub!(/\%/, '') - condition.gsub!(/(\w*?)\s?=(.)/, 'o.\1 =\2') - objects = eval("table.select { |o| #{condition} }") - else - objects = table.select + result_set.map { |object| convert_to_instance(klass, object) } end - if order = options[:order] - order = order.to_s - desc = (order =~ /DESC/) - order = order.gsub(/DESC/, '').gsub(/ASC/, '') - eval "objects.sort { |x, y| x.#{order} <=> y.#{order} }" - objects.reverse! if desc + def start + # nop end + + # Commit a transaction. + + def commit + # nop, not supported? + end + + # Rollback a transaction. + + def rollback + # nop, not supported? + end + + def sql_update(sql) + # nop, not supported. + end + + #FIXME: this method relies on naming conventions and does not account + #for more than one set of '::' marks. + + def join(obj1, obj2, table, options = nil) + first, second = join_object_ordering(obj1, obj2) + + options[get_key(first)] = first.pk + options[get_key(second)] = second.pk + + @conn.get_table(table.to_sym).insert(options) + end + + #retrieve the join table key name for the object + + def get_key(obj) + obj.class.name =~ /::(\w*)/ + ($1.downcase + '_oid').to_sym + end + + def unjoin(obj1, obj2, table) + first, second = join_object_ordering(obj1, obj2) + + @conn.get_table(table.to_sym).delete do |r| + r.send(get_key(first)) == first.pk and + r.send(get_key(second)) == second.pk + end + end + + private - return objects - end - - def start - # nop - end - - # Commit a transaction. - - def commit - # nop, not supported? - end - - # Rollback a transaction. - - def rollback - # nop, not supported? - end - - def sql_update(sql) - # nop, not supported. - end - - def join(obj1, obj2, table, options = nil) - first, second = join_object_ordering(obj1, obj2) - @conn.get_table(table.to_sym).insert(first.pk, second.pk) - end - - def unjoin(obj1, obj2, table) - first, second = join_object_ordering(obj1, obj2) - - @conn.get_table(table.to_sym).delete do |r| - require 'dev-utils/debug' - breakpoint - r.send(:first_key) == first.pk and - r.send(:second_key) == second.pk + def typemap(key) + @typemap[key] end - end -private - - def typemap(key) - @typemap[key] || key - end - - def create_table(klass) - if @conn.table_exists?(klass.table.to_sym) - get_table(klass).pack # Kirby specific method of database cleanup. - - field_names = field_names_for_class(klass) - actual_fields = get_table(klass).field_names - - field_names.each do |needed_field| - next if actual_fields.include?(needed_field) - - if @options[:evolve_schema] == true - Logger.debug "Adding field '#{needed_field}' to '#{klass.table}'" - field_type = typemap(klass.properties[needed_field].klass.name.to_sym) - if get_table(klass).respond_to?(:add_column) - get_table(klass).add_column(needed_field, field_type) + def create_table(klass) + if @conn.table_exists?(klass.table.to_sym) + get_table(klass).pack # Kirby specific method of database cleanup. + + field_names = field_names_for_class(klass) + + actual_fields = get_table(klass).field_names + + field_names.each do |needed_field| + next if actual_fields.include?(needed_field) + + if @options[:evolve_schema] == true + Logger.debug "Adding field '#{needed_field}' to '#{klass.table}'" if $DBG + field_type = typemap(klass.properties[needed_field].klass) + if get_table(klass).respond_to?(:add_column) + get_table(klass).add_column(needed_field, field_type) + else + @conn.add_table_column(klass.table, needed_field, field_type) + end else - @conn.add_table_column(klass.table, needed_field, field_type) + Logger.warn "Table '#{klass.table}' is missing field '#{needed_field}' and :evolve_schema is not set to true!" end - else - Logger.warn "Table '#{klass.table}' is missing field '#{needed_field}' and :evolve_schema is not set to true!" end - end - - actual_fields.each do |obsolete_field| - next if field_names.include?(obsolete_field) - if @options[:evolve_schema] == true and @options[:evolve_schema_cautious] == false - Logger.debug "Removing obsolete field '#{obsolete_field}' from '#{klass.table}'" - if get_table(klass).respond_to?(:drop_column) - get_table(klass).drop_column(obsolete_field) + actual_fields.each do |obsolete_field| + next if field_names.include?(obsolete_field) || obsolete_field == :recno + if @options[:evolve_schema] == true and @options[:evolve_schema_cautious] == false + Logger.debug "Removing obsolete field '#{obsolete_field}' from '#{klass.table}'" if $DBG + if get_table(klass).respond_to?(:drop_column) + get_table(klass).drop_column(obsolete_field) + else + @conn.drop_table_column(klass.table, obsolete_field) + end else - @conn.drop_table_column(klass.table, obsolete_field) + Logger.warn "You have an obsolete field '#{obsolete_field}' on table '#{klass.table}' and :evolve_schema is not set or is in cautious mode!" end - else - Logger.warn "You have an obsolete field '#{obsolete_field}' on table '#{klass.table}' and :evolve_schema is not set or is in cautious mode!" end + else + Logger.debug "Creating table '#{klass.table}'" if $DBG + fields = fields_for_class(klass) + table = @conn.create_table(klass.table.to_sym, *fields) end - else - Logger.debug "Creating table '#{klass.table}'" - fields = fields_for_class(klass) - table = @conn.create_table(klass.table.to_sym, *fields) do |t| - t.record_class = klass - end - end =begin # Create join tables if needed. Join tables are used in # 'many_to_many' relations. if join_tables = klass.ann.self[:join_tables] for info in join_tables unless @conn.table_exists?(info[:table].to_sym) @conn.create_table(info[:table].to_sym, *create_join_table_sql(info)) - Logger.debug "Created jointable '#{info[:table]}'." + Logger.debug "Created jointable '#{info[:table]}'." if $DBG end end end =end - end + end + + def create_join_table_sql(join_info) + [join_info[:first_key].to_sym, :Integer, join_info[:second_key].to_sym, :Integer] + end - - def create_join_table_sql(join_info) - [join_info[:first_key].to_sym, :Integer, join_info[:second_key].to_sym, :Integer] - end - - def drop_table(klass) - @conn.drop_table(klass.table) if @conn.table_exists?(klass.table) - end - - def field_names_for_class(klass) - klass.properties.values.map {|p| p.symbol } - end - - def fields_for_class(klass) - fields = [] - - klass.properties.values.each do |p| - klass.index(p.symbol) if p.index - - fields << p.symbol - - type = typemap(p.klass.name.to_sym) - - fields << type + def drop_table(klass) + @conn.drop_table(klass.table) if @conn.table_exists?(klass.table) end - return fields - end - - def eval_og_insert(klass) - pk = klass.primary_key.symbol - props = klass.properties.values.dup - values = props.collect { |p| write_prop(p) }.join(',') - - if klass.schema_inheritance? - props << Property.new(:symbol => :ogtype, :klass => String) - values << ", '#{klass}'" + def field_names_for_class(klass) + properties = get_properties_for_class(klass) + properties.values.map {|p| p.symbol } end - klass.class_eval %{ - def og_insert(store) - #{Glue::Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)} - Logger.debug "Inserting \#{self}." if $DBG - @#{pk} = store.get_table(#{klass}).insert(self) - #{Glue::Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)} + def fields_for_class(klass) + fields = [] + + get_properties_for_class(klass).values.each do |p| + klass.index(p.symbol) if p.index + + fields << p.symbol + + type = typemap(p.klass) + + fields << type end - } - end + + fields + end + + def eval_og_insert(klass) + pk = klass.primary_key.symbol + + fields = get_properties_for_class(klass).keys + fields = fields.map { |f| ":#{f} => (self.respond_to?(:#{f}) ? self.#{f} : nil)"} + field_string = fields.join(', ') + klass.class_eval %{ + def og_insert(store) + @ogtype = #{klass} + #{::Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)} + Logger.debug "Inserting \#{self}." if $DBG + @#{pk} = store.get_table(#{klass}).insert({#{field_string}}) + #{::Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)} + end + } + end - # Compile the og_update method for the class. + # Compile the og_update method for the class. - def eval_og_update(klass) - pk = klass.pk_symbol - updates = klass.properties.keys.collect { |p| ":#{p} => @#{p}" } + def eval_og_update(klass) + pk = klass.pk_symbol + updates = klass.properties.keys.collect { |p| ":#{p} => @#{p}" } - klass.module_eval %{ - def og_update(store, options = nil) - #{Glue::Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)} - store.get_table(#{klass}).update { |r| r.recno == #{pk} }.set(#{updates.join(', ')}) - #{Glue::Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)} - end - } - end - - def eval_og_read(klass) - klass.module_eval %{ - def og_read(res, row = 0, offset = 0) - #{Glue::Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)} - #{Glue::Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)} - end - } - end - - def eval_og_delete(klass) - klass.module_eval %{ - def og_delete(store, pk, cascade = true) - pk ||= self.pk - pk = pk.to_i - #{Glue::Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)} - table = store.get_table(self.class) - transaction do |tx| - table.delete { |r| r.recno == pk } - if cascade and #{klass}.ann.self[:descendants] - #{klass}.ann.self.descendants.each do |dclass, foreign_key| - dtable = store.get_table(dclass) - dtable.delete { |r| foreign_key == pk } + klass.module_eval %{ + def og_update(store, options = nil) + #{::Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)} + store.get_table(#{klass}).update { |r| r.recno == #{pk} }.set(#{updates.join(', ')}) + #{::Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)} + end + } + end + + def eval_og_read(klass) + klass.module_eval %{ + def og_read(res, row = 0, offset = 0) + #{::Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)} + #{::Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)} + end + } + end + + def eval_og_delete(klass) + klass.module_eval %{ + def og_delete(store, pk, cascade = true) + pk ||= self.pk + pk = pk.to_i + #{::Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)} + table = store.get_table(self.class) + transaction do |tx| + table.delete { |r| r.recno == pk } + if cascade and #{klass}.ann.self[:descendants] + #{klass}.ann.self.descendants.each do |dclass, foreign_key| + dtable = store.get_table(dclass) + dtable.delete { |r| foreign_key == pk } + end end end + #{::Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)} end - #{Glue::Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)} - end - } + } + end end - -end - end # * George Moschovitis <gm@navel.gr>