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>