lib/active_groonga/base.rb in activegroonga-0.0.2 vs lib/active_groonga/base.rb in activegroonga-0.0.6

- old
+ new

@@ -478,12 +478,13 @@ # B descends from A, then B.base_class will return B. def abstract_class? defined?(@abstract_class) && @abstract_class == true end - def find(*args) + def find(*args, &block) options = args.extract_options! + options = options.merge(:expression => block) if block validate_find_options(options) set_readonly_option!(options) case args.first when :first @@ -526,15 +527,15 @@ def table context[groonga_table_name] end def groonga_table_name(name=nil) - "<table:#{name || table_name}>" + (name || table_name).to_s end def groonga_metadata_table_name(name) - "<metadata:#{name}>" + "meta-#{name}" end # Defines an "attribute" method (like +inheritance_column+ or # +table_name+). A new (class) method will be created with the # given name. If a value is specified, the new method will @@ -586,15 +587,16 @@ Groonga::Context.default = nil Groonga::Context.default_options = {:encoding => spec[:encoding]} unless File.exist?(database_directory) FileUtils.mkdir_p(database_directory) end + database_directory = File.expand_path(database_directory) database_file = File.join(database_directory, "database.groonga") if File.exist?(database_file) - @@database = Groonga::Database.new(database_file) + Groonga::Database.new(database_file) else - @@database = Groonga::Database.create(:path => database_file) + Groonga::Database.create(:path => database_file) end self.database_directory = database_directory end end @@ -608,78 +610,73 @@ directory = File.join(tables_directory, table_name.to_s, "columns") FileUtils.mkdir_p(directory) unless File.exist?(directory) directory end + def index_columns_directory(table_name, target_table_name) + directory = File.join(columns_directory(table_name), target_table_name) + FileUtils.mkdir_p(directory) unless File.exist?(directory) + directory + end + def metadata_directory directory = File.join(database_directory, "metadata") FileUtils.mkdir_p(directory) unless File.exist?(directory) directory end - def count - table.size + def count(expression=nil) + if expression + table.select do |record| + expression.call(DynamicRecordExpressionBuilder.new(record)) + end.size + else + table.size + end end private def find_initial(options) options.update(:limit => 1) find_every(options).first end def find_every(options) - limit = options[:limit] ||= 0 - conditions = (options[:conditions] || {}).stringify_keys + expression = options[:expression] include_associations = merge_includes(scope(:find, :include), options[:include]) if include_associations.any? && references_eager_loaded_tables?(options) records = find_with_associations(options) else - records = [] - target_records = [] - original_table = table - index_records = nil - Schema.indexes(table_name).each do |index_definition| - if conditions.has_key?(index_definition.column) - index_column_name = - "#{index_definition.table}/#{index_definition.column}" - index = Schema.index_table.column(index_column_name) - key = conditions.delete(index_definition.column) - index_records = index.search(key, :result => index_records) + if expression + records = table.select do |record| + expression.call(DynamicRecordExpressionBuilder.new(record)) end - end - if index_records - sorted_records = index_records.sort([ - :key => ".:score", - :order => :descending, - ], - :limit => limit) - limit = sorted_records.size - target_records = sorted_records.records(:order => :ascending).collect do |record| - index_record_id = record.value.unpack("i")[0] - index_record = Groonga::Record.new(index_records, index_record_id) - target_record = index_record.key - target_record.instance_variable_set("@score", index_record.score) - def target_record.score - @score - end - target_record - end else - target_records = original_table.records - limit = target_records.size if limit.zero? + records = table.select end - target_records.each_with_index do |record, i| - break if records.size >= limit - unless conditions.all? do |name, value| - record[name] == value or - (record.reference_column?(name) and record[name].id == value) - end - next + sort_options = {} + limit = options[:limit] + offset = options[:offset] + offset = Integer(offset) unless offset.nil? + if limit and offset.nil? + sort_options[:limit] = limit + end + records = records.sort([:key => ".:score", :order => :descending], + sort_options) + if offset + in_target = false + _records, records = records, [] + _records.each_with_index do |record, i| + break if limit and limit <= records.size + in_target = i >= offset unless in_target + records << record if in_target end - records << instantiate(record) end + records = records.collect do |record| + instantiate(record, record.key.id, record.table.domain) + end if include_associations.any? preload_associations(records, include_associations) end end @@ -749,11 +746,11 @@ else [o] end end - VALID_FIND_OPTIONS = [:conditions, :readonly, :limit] + VALID_FIND_OPTIONS = [:expression, :readonly, :limit, :offset] def validate_find_options(options) options.assert_valid_keys(VALID_FIND_OPTIONS) end def set_readonly_option!(options) #:nodoc: @@ -776,45 +773,39 @@ end # Finder methods must instantiate through this method to work with the # single-table inheritance model that makes it possible to create # objects of different types from the same table. - def instantiate(record) - object = - if subclass_name = record[inheritance_column] - # No type given. - if subclass_name.empty? - allocate + def instantiate(record, id=nil, table=nil) + id ||= record.id + table ||= record.table - else - # Ignore type if no column is present since it was probably - # pulled in from a sloppy join. - unless columns_hash.include?(inheritance_column) - allocate + subclass_name = nil + if record.have_column?(inheritance_column) + subclass_name = record[inheritance_column] + end - else - begin - compute_type(subclass_name).allocate - rescue NameError - raise SubclassNotFound, - "The single-table inheritance mechanism failed to locate the subclass: '#{record[inheritance_column]}'. " + - "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + - "Please rename this column if you didn't intend it to be used for storing the inheritance class " + - "or overwrite #{self.to_s}.inheritance_column to use another column for that information." - end - end - end - else - allocate + if subclass_name.blank? or !columns_hash.include?(inheritance_column) + object = allocate + else + begin + object = compute_type(subclass_name).allocate + rescue NameError + raise SubclassNotFound, + "The single-table inheritance mechanism failed to locate the subclass: '#{record[inheritance_column]}'. " + + "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + + "Please rename this column if you didn't intend it to be used for storing the inheritance class " + + "or overwrite #{self.to_s}.inheritance_column to use another column for that information." end + end - object.instance_variable_set("@id", record.id) + object.instance_variable_set("@id", id) object.instance_variable_set("@score", record.score) attributes = {} - record.table.columns.each do |column| - _, column_name = column.name.split(/\A#{record.table.name}\./, 2) - attributes[column_name] = column[record.id] + table.columns.each do |column| + column_name = column.local_name + attributes[column_name] = record[".#{column_name}"] end object.instance_variable_set("@attributes", attributes) object.instance_variable_set("@attributes_cache", Hash.new) if object.respond_to_without_attributes?(:after_find) @@ -827,19 +818,22 @@ object end # Enables dynamic finders like <tt>find_by_user_name(user_name)</tt> and <tt>find_by_user_name_and_password(user_name, password)</tt> - # that are turned into <tt>find(:first, :conditions => ["user_name = ?", user_name])</tt> and - # <tt>find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password])</tt> respectively. Also works for - # <tt>find(:all)</tt> by using <tt>find_all_by_amount(50)</tt> that is turned into <tt>find(:all, :conditions => ["amount = ?", 50])</tt>. + # that are turned into <tt>find(:first) {|record| record["user_name"] == user_name}</tt> and + # <tt>find(:first) {|record| (record["user_name"] == + # user_name) & (record["password"] == password)}</tt> respectively. Also works for + # <tt>find(:all)</tt> by using + # <tt>find_all_by_amount(50)</tt> that is turned into + # <tt>find(:all) {|record| record["amount"] == 50}</tt>. # # It's even possible to use all the additional parameters to +find+. For example, the full interface for +find_all_by_amount+ # is actually <tt>find_all_by_amount(amount, options)</tt>. # # Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that - # are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password]) + # are turned into scoped(:expression => Proc.new {|record| record["user_name"] == user_name}) and scoped(:expression => Proc.new {|record| (record["user_name"] == user_name) & (record["password"] == password)}) # respectively. # # Each dynamic finder, scope or initializer/creator is also defined in the class after it is first invoked, so that future # attempts to use it do not run through method_missing. def method_missing(method_id, *arguments, &block) @@ -849,38 +843,38 @@ if match.finder? finder = match.finder bang = match.bang? # def self.find_by_login_and_activated(*args) # options = args.extract_options! - # attributes = construct_attributes_from_arguments( + # expression = construct_expression_from_arguments( # [:login,:activated], # args # ) - # finder_options = { :conditions => attributes } + # finder_options = { :expression => expression } # validate_find_options(options) # set_readonly_option!(options) # - # if options[:conditions] + # if options[:expression] # with_scope(:find => finder_options) do # find(:first, options) # end # else # find(:first, options.merge(finder_options)) # end # end self.class_eval <<-EOC, __FILE__, __LINE__ def self.#{method_id}(*args) options = args.extract_options! - attributes = construct_attributes_from_arguments( + expression = construct_expression_from_arguments( [:#{attribute_names.join(',:')}], args ) - finder_options = { :conditions => attributes } + finder_options = {:expression => expression} validate_find_options(options) set_readonly_option!(options) - #{'result = ' if bang}if options[:conditions] + #{'result = ' if bang}if options[:expression] with_scope(:find => finder_options) do find(:#{finder}, options) end else find(:#{finder}, options.merge(finder_options)) @@ -895,16 +889,17 @@ # guard_protected_attributes = false # # if args[0].is_a?(Hash) # guard_protected_attributes = true # attributes = args[0].with_indifferent_access - # find_attributes = attributes.slice(*[:user_id]) + # find_expression = attributes.slice(*[:user_id]) # else - # find_attributes = attributes = construct_attributes_from_arguments([:user_id], args) + # attributes = construct_attributes_from_arguments([:user_id], args) + # find_expression = construct_expression_from_arguments([:user_id], args) # end # - # options = { :conditions => find_attributes } + # options = { :expression => find_expression } # set_readonly_option!(options) # # record = find(:first, options) # # if record.nil? @@ -926,11 +921,12 @@ find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}]) else find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args) end - options = { :conditions => find_attributes } + find_expression = construct_expression_from_attributes(find_attributes) + options = { :expression => find_expression } set_readonly_option!(options) record = find(:first, options) if record.nil? @@ -950,15 +946,15 @@ super unless all_attributes_exists?(attribute_names) if match.scope? self.class_eval <<-EOC, __FILE__, __LINE__ def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) options = args.extract_options! # options = args.extract_options! - attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments( + expression = construct_expression_from_arguments( # expression = construct_expression_from_arguments( [:#{attribute_names.join(',:')}], args # [:user_name, :password], args ) # ) # - scoped(:conditions => attributes) # scoped(:conditions => attributes) + scoped(:expression => expression) # scoped(:expression => expression) end # end EOC send(method_id, *arguments) end else @@ -966,15 +962,47 @@ end end def construct_attributes_from_arguments(attribute_names, arguments) attributes = {} - attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] } + attribute_names.each_with_index do |name, i| + attributes[name] = arguments[i] + end attributes end - # Similar in purpose to +expand_hash_conditions_for_aggregates+. + def construct_expression_from_attributes(attributes) + Proc.new do |record| + builder = nil + attributes.each do |name, value| + expression = (record[name] == value) + if builder + builder = builder & expression + else + builder = expression + end + end + builder + end + end + + def construct_expression_from_arguments(attribute_names, arguments) + Proc.new do |record| + builder = nil + attribute_names.each_with_index do |name, i| + expression = (record[name] == arguments[i]) + if builder + builder = builder & expression + else + builder = expression + end + end + builder + end + end + + # Similar in purpose to +expand_hash_expression_for_aggregates+. def expand_attribute_names_for_aggregates(attribute_names) expanded_attribute_names = [] attribute_names.each do |attribute_name| unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil? aggregate_mapping(aggregation).each do |field_attr, aggregate_attr| @@ -1379,11 +1407,10 @@ # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. def update(attribute_names=@attributes.keys) attribute_names = remove_readonly_attributes(attribute_names) table = self.class.table - indexes = Schema.indexes(table) quoted_attributes = attributes_with_quotes(false, attribute_names) quoted_attributes.each do |name, value| column = table.column(name) next if column.nil? column[id] = value @@ -1393,10 +1420,9 @@ # Creates a record with values matching those of the instance attributes # and returns its id. def create table = self.class.table record = table.add - indexes = Schema.indexes(table) quoted_attributes = attributes_with_quotes quoted_attributes.each do |name, value| record[name] = value end self.id = record.id