Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Partial commit - trying to get relationships working in DataMapper.

  • Loading branch information...
commit a136b31365a7a0bfbd7b298974deab4d28578d31 1 parent 791878a
@pat pat authored
View
3  features/support/models/book.rb
@@ -8,9 +8,12 @@ class Book
property :author, String
property :delta, Boolean
+ has n, :reviews
+
define_index do
indexes title
indexes author
+ indexes reviews.content, :as => :reviews
set_property :delta => true
end
View
11 features/support/models/review.rb
@@ -0,0 +1,11 @@
+class Review
+ include DataMapper::Resource
+ include ThinkingSphinx::Base
+ include ThinkingSphinx::DataMapper
+
+ property :id, Serial
+ property :title, String
+ property :content, Text
+ property :rating, Integer
+ property :book_id, Integer
+end
View
1  lib/thinking_sphinx.rb
@@ -189,7 +189,6 @@ def self.mysql?
require 'thinking_sphinx/base'
require 'thinking_sphinx/delta'
require 'thinking_sphinx/property'
-require 'thinking_sphinx/association'
require 'thinking_sphinx/attribute'
require 'thinking_sphinx/configuration'
require 'thinking_sphinx/context'
View
11 lib/thinking_sphinx/active_record.rb
@@ -1,10 +1,5 @@
require 'after_commit'
-require 'thinking_sphinx/active_record/ext'
-require 'thinking_sphinx/active_record/attribute_updates'
-require 'thinking_sphinx/active_record/has_many_association'
-require 'thinking_sphinx/active_record/tailor'
-
# Core additions to ActiveRecord models - define_index for creating indexes
# for models. If you want to interrogate the index objects created for the
# model, you can use the class-level accessor :sphinx_indexes.
@@ -103,6 +98,12 @@ def new_record_for_sphinx?
end
end
+require 'thinking_sphinx/active_record/ext'
+require 'thinking_sphinx/active_record/association'
+require 'thinking_sphinx/active_record/attribute_updates'
+require 'thinking_sphinx/active_record/has_many_association'
+require 'thinking_sphinx/active_record/tailor'
+
ActiveRecord::Base.class_eval do
include ThinkingSphinx::Base
include ThinkingSphinx::ActiveRecord
View
162 lib/thinking_sphinx/active_record/association.rb
@@ -0,0 +1,162 @@
+# Association tracks a specific reflection and join to reference data that
+# isn't in the base model. Very much an internal class for Thinking Sphinx -
+# perhaps because I feel it's not as strong (or simple) as most of the rest.
+#
+class ThinkingSphinx::ActiveRecord::Association
+ attr_accessor :parent, :reflection, :join
+
+ # Create a new association by passing in the parent association, and the
+ # corresponding reflection instance. If there is no parent, pass in nil.
+ #
+ # top = Association.new nil, top_reflection
+ # child = Association.new top, child_reflection
+ #
+ def initialize(parent, reflection)
+ @parent, @reflection = parent, reflection
+ @children = {}
+ end
+
+ # Get the children associations for a given association name. The only time
+ # that there'll actually be more than one association is when the
+ # relationship is polymorphic. To keep things simple though, it will always
+ # be an Array that gets returned (an empty one if no matches).
+ #
+ # # where pages is an association on the class tied to the reflection.
+ # association.children(:pages)
+ #
+ def children(assoc)
+ @children[assoc] ||= ThinkingSphinx::ActiveRecord::Association.children(@reflection.klass, assoc, self)
+ end
+
+ # Get the children associations for a given class, association name and
+ # parent association. Much like the instance method of the same name, it
+ # will return an empty array if no associations have the name, and only
+ # have multiple association instances if the underlying relationship is
+ # polymorphic.
+ #
+ # Association.children(User, :pages, user_association)
+ #
+ def self.children(klass, assoc, parent=nil)
+ ref = klass.reflect_on_association(assoc)
+
+ return [] if ref.nil?
+ return [ThinkingSphinx::ActiveRecord::Association.new(parent, ref)] unless ref.options[:polymorphic]
+
+ # association is polymorphic - create associations for each
+ # non-polymorphic reflection.
+ polymorphic_classes(ref).collect { |klass|
+ ThinkingSphinx::ActiveRecord::Association.new parent, ::ActiveRecord::Reflection::AssociationReflection.new(
+ ref.macro,
+ "#{ref.name}_#{klass.name}".to_sym,
+ casted_options(klass, ref),
+ ref.active_record
+ )
+ }
+ end
+
+ # Link up the join for this model from a base join - and set parent
+ # associations' joins recursively.
+ #
+ def join_to(base_join)
+ parent.join_to(base_join) if parent && parent.join.nil?
+
+ @join ||= ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation.new(
+ @reflection, base_join, parent ? parent.join : base_join.joins.first
+ )
+ end
+
+ # Returns the association's join SQL statements - and it replaces
+ # ::ts_join_alias:: with the aliased table name so the generated reflection
+ # join conditions avoid column name collisions.
+ #
+ def to_sql
+ @join.association_join.gsub(/::ts_join_alias::/,
+ "#{@reflection.klass.connection.quote_table_name(@join.parent.aliased_table_name)}"
+ )
+ end
+
+ # Returns true if the association - or a parent - is a has_many or
+ # has_and_belongs_to_many.
+ #
+ def is_many?
+ case @reflection.macro
+ when :has_many, :has_and_belongs_to_many
+ true
+ else
+ @parent ? @parent.is_many? : false
+ end
+ end
+
+ # Returns an array of all the associations that lead to this one - starting
+ # with the top level all the way to the current association object.
+ #
+ def ancestors
+ (parent ? parent.ancestors : []) << self
+ end
+
+ def has_column?(column)
+ @reflection.klass.column_names.include?(column.to_s)
+ end
+
+ def primary_key_from_reflection
+ if @reflection.options[:through]
+ @reflection.source_reflection.options[:foreign_key] ||
+ @reflection.source_reflection.primary_key_name
+ elsif @reflection.macro == :has_and_belongs_to_many
+ @reflection.association_foreign_key
+ else
+ nil
+ end
+ end
+
+ def table
+ if @reflection.options[:through] ||
+ @reflection.macro == :has_and_belongs_to_many
+ @join.aliased_join_table_name
+ else
+ @join.aliased_table_name
+ end
+ end
+
+ private
+
+ # Returns all the objects that could be currently instantiated from a
+ # polymorphic association. This is pretty damn fast if there's an index on
+ # the foreign type column - but if there isn't, it can take a while if you
+ # have a lot of data.
+ #
+ def self.polymorphic_classes(ref)
+ ref.active_record.connection.select_all(
+ "SELECT DISTINCT #{ref.options[:foreign_type]} " +
+ "FROM #{ref.active_record.table_name} " +
+ "WHERE #{ref.options[:foreign_type]} IS NOT NULL"
+ ).collect { |row|
+ row[ref.options[:foreign_type]].constantize
+ }
+ end
+
+ # Returns a new set of options for an association that mimics an existing
+ # polymorphic relationship for a specific class. It adds a condition to
+ # filter by the appropriate object.
+ #
+ def self.casted_options(klass, ref)
+ options = ref.options.clone
+ options[:polymorphic] = nil
+ options[:class_name] = klass.name
+ options[:foreign_key] ||= "#{ref.name}_id"
+
+ quoted_foreign_type = klass.connection.quote_column_name ref.options[:foreign_type]
+ case options[:conditions]
+ when nil
+ options[:conditions] = "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
+ when Array
+ options[:conditions] << "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
+ when Hash
+ options[:conditions].merge!(ref.options[:foreign_type] => klass.name)
+ else
+ options[:conditions] << " AND ::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
+ end
+
+ options
+ end
+end
View
4 lib/thinking_sphinx/adapters/abstract_adapter.rb
@@ -24,6 +24,10 @@ def quote_with_table(column)
"#{@model.quoted_table_name}.#{@model.connection.quote_column_name(column)}"
end
+ def quote(column)
+ @model.connection.quote_column_name(column)
+ end
+
protected
def connection
View
164 lib/thinking_sphinx/association.rb
@@ -1,164 +0,0 @@
-module ThinkingSphinx
- # Association tracks a specific reflection and join to reference data that
- # isn't in the base model. Very much an internal class for Thinking Sphinx -
- # perhaps because I feel it's not as strong (or simple) as most of the rest.
- #
- class Association
- attr_accessor :parent, :reflection, :join
-
- # Create a new association by passing in the parent association, and the
- # corresponding reflection instance. If there is no parent, pass in nil.
- #
- # top = Association.new nil, top_reflection
- # child = Association.new top, child_reflection
- #
- def initialize(parent, reflection)
- @parent, @reflection = parent, reflection
- @children = {}
- end
-
- # Get the children associations for a given association name. The only time
- # that there'll actually be more than one association is when the
- # relationship is polymorphic. To keep things simple though, it will always
- # be an Array that gets returned (an empty one if no matches).
- #
- # # where pages is an association on the class tied to the reflection.
- # association.children(:pages)
- #
- def children(assoc)
- @children[assoc] ||= Association.children(@reflection.klass, assoc, self)
- end
-
- # Get the children associations for a given class, association name and
- # parent association. Much like the instance method of the same name, it
- # will return an empty array if no associations have the name, and only
- # have multiple association instances if the underlying relationship is
- # polymorphic.
- #
- # Association.children(User, :pages, user_association)
- #
- def self.children(klass, assoc, parent=nil)
- ref = klass.reflect_on_association(assoc)
-
- return [] if ref.nil?
- return [Association.new(parent, ref)] unless ref.options[:polymorphic]
-
- # association is polymorphic - create associations for each
- # non-polymorphic reflection.
- polymorphic_classes(ref).collect { |klass|
- Association.new parent, ::ActiveRecord::Reflection::AssociationReflection.new(
- ref.macro,
- "#{ref.name}_#{klass.name}".to_sym,
- casted_options(klass, ref),
- ref.active_record
- )
- }
- end
-
- # Link up the join for this model from a base join - and set parent
- # associations' joins recursively.
- #
- def join_to(base_join)
- parent.join_to(base_join) if parent && parent.join.nil?
-
- @join ||= ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation.new(
- @reflection, base_join, parent ? parent.join : base_join.joins.first
- )
- end
-
- # Returns the association's join SQL statements - and it replaces
- # ::ts_join_alias:: with the aliased table name so the generated reflection
- # join conditions avoid column name collisions.
- #
- def to_sql
- @join.association_join.gsub(/::ts_join_alias::/,
- "#{@reflection.klass.connection.quote_table_name(@join.parent.aliased_table_name)}"
- )
- end
-
- # Returns true if the association - or a parent - is a has_many or
- # has_and_belongs_to_many.
- #
- def is_many?
- case @reflection.macro
- when :has_many, :has_and_belongs_to_many
- true
- else
- @parent ? @parent.is_many? : false
- end
- end
-
- # Returns an array of all the associations that lead to this one - starting
- # with the top level all the way to the current association object.
- #
- def ancestors
- (parent ? parent.ancestors : []) << self
- end
-
- def has_column?(column)
- @reflection.klass.column_names.include?(column.to_s)
- end
-
- def primary_key_from_reflection
- if @reflection.options[:through]
- @reflection.source_reflection.options[:foreign_key] ||
- @reflection.source_reflection.primary_key_name
- elsif @reflection.macro == :has_and_belongs_to_many
- @reflection.association_foreign_key
- else
- nil
- end
- end
-
- def table
- if @reflection.options[:through] ||
- @reflection.macro == :has_and_belongs_to_many
- @join.aliased_join_table_name
- else
- @join.aliased_table_name
- end
- end
-
- private
-
- # Returns all the objects that could be currently instantiated from a
- # polymorphic association. This is pretty damn fast if there's an index on
- # the foreign type column - but if there isn't, it can take a while if you
- # have a lot of data.
- #
- def self.polymorphic_classes(ref)
- ref.active_record.connection.select_all(
- "SELECT DISTINCT #{ref.options[:foreign_type]} " +
- "FROM #{ref.active_record.table_name} " +
- "WHERE #{ref.options[:foreign_type]} IS NOT NULL"
- ).collect { |row|
- row[ref.options[:foreign_type]].constantize
- }
- end
-
- # Returns a new set of options for an association that mimics an existing
- # polymorphic relationship for a specific class. It adds a condition to
- # filter by the appropriate object.
- #
- def self.casted_options(klass, ref)
- options = ref.options.clone
- options[:polymorphic] = nil
- options[:class_name] = klass.name
- options[:foreign_key] ||= "#{ref.name}_id"
-
- quoted_foreign_type = klass.connection.quote_column_name ref.options[:foreign_type]
- case options[:conditions]
- when nil
- options[:conditions] = "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
- when Array
- options[:conditions] << "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
- when Hash
- options[:conditions].merge!(ref.options[:foreign_type] => klass.name)
- else
- options[:conditions] << " AND ::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
- end
-
- options
- end
- end
-end
View
3  lib/thinking_sphinx/data_mapper.rb
@@ -1,7 +1,7 @@
module ThinkingSphinx::DataMapper
def self.included(base)
base.class_eval do
- extend ThinkingSphinx::DataMapper::ClassMethods
+ extend ThinkingSphinx::DataMapper::ClassMethods
end
end
@@ -64,4 +64,5 @@ def new_record_for_sphinx?
end
end
+require 'thinking_sphinx/data_mapper/association'
require 'thinking_sphinx/data_mapper/tailor'
View
96 lib/thinking_sphinx/data_mapper/association.rb
@@ -0,0 +1,96 @@
+class ThinkingSphinx::DataMapper::Association
+ attr_accessor :parent, :reflection, :join
+
+ # Create a new association by passing in the parent association, and the
+ # corresponding reflection instance. If there is no parent, pass in nil.
+ #
+ # top = Association.new nil, top_reflection
+ # child = Association.new top, child_reflection
+ #
+ def initialize(parent, reflection)
+ @parent, @reflection = parent, reflection
+ @children = {}
+ end
+
+ # Get the children associations for a given association name. The only time
+ # that there'll actually be more than one association is when the
+ # relationship is polymorphic. To keep things simple though, it will always
+ # be an Array that gets returned (an empty one if no matches).
+ #
+ # # where pages is an association on the class tied to the reflection.
+ # association.children(:pages)
+ #
+ def children(assoc)
+ @children[assoc] ||= ThinkingSphinx::DataMapper::Association.children(@reflection.child_model, assoc, self)
+ end
+
+ # Get the children associations for a given class, association name and
+ # parent association. Much like the instance method of the same name, it
+ # will return an empty array if no associations have the name, and only
+ # have multiple association instances if the underlying relationship is
+ # polymorphic.
+ #
+ # Association.children(User, :pages, user_association)
+ #
+ def self.children(klass, assoc, parent=nil)
+ ref = klass.relationships[assoc.to_s]
+
+ ref.nil? ? [] : [ThinkingSphinx::DataMapper::Association.new(parent, ref)]
+ end
+
+ # Returns the association's join SQL statements - and it replaces
+ # ::ts_join_alias:: with the aliased table name so the generated reflection
+ # join conditions avoid column name collisions.
+ #
+ def to_sql
+ @join.association_join.gsub(/::ts_join_alias::/,
+ "#{@reflection.klass.connection.quote_table_name(@join.parent.aliased_table_name)}"
+ )
+ end
+
+ # Returns true if the association - or a parent - is a has_many or
+ # has_and_belongs_to_many.
+ #
+ def is_many?
+ case @reflection
+ when DataMapper::Associations::OneToMany,
+ DataMapper::Associations::ManyToMany
+ true
+ else
+ @parent ? @parent.is_many? : false
+ end
+ end
+
+ # Returns an array of all the associations that lead to this one - starting
+ # with the top level all the way to the current association object.
+ #
+ def ancestors
+ (parent ? parent.ancestors : []) << self
+ end
+
+ def has_column?(column)
+ @reflection.child_model.properties.any? { |property|
+ property.name == column.to_s
+ }
+ end
+
+ def primary_key_from_reflection
+ if @reflection.options[:through]
+ @reflection.source_reflection.options[:foreign_key] ||
+ @reflection.source_reflection.primary_key_name
+ elsif @reflection.is_a?(DataMapper::Associations::ManyToMany)
+ @reflection.association_foreign_key
+ else
+ nil
+ end
+ end
+
+ def table
+ if @reflection.options[:through] ||
+ @reflection.is_a?(DataMapper::Associations::ManyToMany)
+ @join.aliased_join_table_name
+ else
+ @join.aliased_table_name
+ end
+ end
+end
View
3  lib/thinking_sphinx/source.rb
@@ -1,4 +1,5 @@
require 'thinking_sphinx/source/internal_properties'
+require 'thinking_sphinx/source/query'
require 'thinking_sphinx/source/sql'
module ThinkingSphinx
@@ -62,7 +63,7 @@ def delta?
# Gets the association stack for a specific key.
#
def association(key)
- @associations[key] ||= Association.children(@model, key)
+ @associations[key] ||= ThinkingSphinx::ActiveRecord::Association.children(@model, key)
end
private
View
49 lib/thinking_sphinx/source/query.rb
@@ -0,0 +1,49 @@
+class ThinkingSphinx::Source::Query
+ def initialize(table, adapter)
+ @table = table
+ @adapter = adapter
+ @columns = []
+ @joins = []
+ end
+
+ def add_column(location, column = nil)
+ location, column = table, location if column.nil?
+
+ @columns << [location, column]
+ end
+
+ def add_join(from, from_column, to, to_column, name = nil)
+ @joins << [from, from_column, to, to_column, name]
+ end
+
+ def to_s
+ "SELECT #{columns} FROM #{adapter.quote table} #{joins}".strip
+ end
+
+ private
+
+ def columns
+ return '*' if @columns.empty?
+
+ @columns.collect { |location, column|
+ "#{adapter.quote location}.#{adapter.quote column}"
+ }.join(', ')
+ end
+
+ def joins
+ return '' if @joins.empty?
+
+ @joins.collect { |from, from_column, to, to_column, name|
+ name ||= to
+ "LEFT OUTER JOIN #{adapter.quote to} AS #{adapter.quote name} ON #{adapter.quote from}.#{adapter.quote from_column} = #{adapter.quote name}.#{adapter.quote to_column}"
+ }.join(' ')
+ end
+
+ def adapter
+ @adapter
+ end
+
+ def table
+ @table
+ end
+end
View
0  spec/thinking_sphinx/association_spec.rb → ...thinking_sphinx/active_record/association_spec.rb
File renamed without changes
View
28 spec/thinking_sphinx/source/query_spec.rb
@@ -0,0 +1,28 @@
+require 'spec/spec_helper'
+
+describe ThinkingSphinx::Source::Query do
+ before :each do
+ @adapter = ThinkingSphinx::MysqlAdapter.new(Alpha)
+ @query = ThinkingSphinx::Source::Query.new('alphas', @adapter)
+ end
+
+ describe '#initialize' do
+ it "should select everything from the model's table by default" do
+ @query.to_s.should == "SELECT * FROM `alphas`"
+ end
+ end
+
+ describe '#add_column' do
+ it "should reference the default table if none supplied" do
+ @query.add_column 'name'
+ @query.to_s.should == "SELECT `alphas`.`name` FROM `alphas`"
+ end
+ end
+
+ describe '#add_join' do
+ it "should add a LEFT OUTER JOIN for the given details" do
+ @query.add_join 'alphas', 'id', 'betas', 'alpha_id'
+ @query.to_s.should == "SELECT * FROM `alphas` LEFT OUTER JOIN `betas` AS `betas` ON `alphas`.`id` = `betas`.`alpha_id`"
+ end
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.