Skip to content

Commit

Permalink
Released v1.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
binarylogic committed Sep 24, 2008
1 parent 10d1eea commit 6a8e5f7
Show file tree
Hide file tree
Showing 26 changed files with 442 additions and 486 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.rdoc
@@ -1,9 +1,11 @@
== 1.1.4 released 2008-09-24
== 1.2.0 released 2008-09-24

* Added searchgasm_params and searchgasm_url helper to use outside of the control type helpers.
* Added dup and clone methods that work properly for searchgasm objects
* Fixed bug to remove nil scope values, HABTM likes to add :limit => nil
* Removed unnecessary build_search methods for associations
* Removed support for searching with conditions only. This just made things much more complicated when you can accomplish the same thing by starting a new search and only setting conditions.
* Fixed bug when searching with *any* conditions to use left outer joins instead of inner joins.

== 1.1.3 released 2008-09-23

Expand Down
19 changes: 2 additions & 17 deletions README.rdoc
Expand Up @@ -177,19 +177,6 @@ Any of the options used in the above example can be used in these, but for the s
search.conditions.first_name_contains = "Ben"
search.per_page = 20
search.all

== Search with conditions only

Don't need pagination, ordering, or any of the other options? Search with conditions only.

conditions = User.new_conditions(:age_gt => 18)
conditions.first_name_contains = "Ben"
conditions.all
# ... all operations above are available

Pass a conditions object right into ActiveRecord:

User.all(:conditions => conditions)

== Match ANY or ALL of the conditions

Expand Down Expand Up @@ -250,24 +237,22 @@ or

== Always use protection...against SQL injections

If there is one thing we all know, it's to always use protection against SQL injections. That's why searchgasm protects you by default. The new\_search and new\_conditions methods protect mass assignments by default (instantiation and search.options = {}). This means that various checks are done to ensure it is not possible to perform any type of SQL injection during mass assignments. But this also limits how you can search, meaning you can't write raw SQL. If you want to be daring and search without protection, all that you have to do is add ! to the end of the methods: new\_search! and new\_conditions!.
If there is one thing we all know, it's to always use protection against SQL injections. That's why searchgasm protects you by default. The new\_search methods protect mass assignments by default (instantiation and search.options = {}). This means that various checks are done to ensure it is not possible to perform any type of SQL injection during mass assignments. But this also limits how you can search, meaning you can't write raw SQL. If you want to be daring and search without protection, all that you have to do is add ! to the end of the method: new\_search!.

=== Protected from SQL injections

search = Account.new_search(params[:search])
conditions = Account.new_conditions(params[:conditions])

=== *NOT* protected from SQL injections

accounts = Account.find(params[:search])
accounts = Account.all(params[:search])
account = Account.first(params[:search])
search = Account.new_search!(params[:search])
conditions = Account.new_conditions!(params[:conditions])

I'm sure you already knew this, but it's tempting to do this when you can pass the params hash right into these methods.

Lesson learned: use new\_search and new\_conditions when passing in params as *ANY* of the options.
Lesson learned: use new\_search when passing in params as *ANY* of the options.

== Available Conditions

Expand Down
3 changes: 2 additions & 1 deletion lib/searchgasm.rb
Expand Up @@ -8,7 +8,6 @@

# Shared
require "searchgasm/shared/utilities"
require "searchgasm/shared/searching"
require "searchgasm/shared/virtual_classes"

# Base classes
Expand All @@ -23,6 +22,7 @@
require "searchgasm/search/ordering"
require "searchgasm/search/pagination"
require "searchgasm/search/conditions"
require "searchgasm/search/searching"
require "searchgasm/search/base"
require "searchgasm/search/protection"

Expand Down Expand Up @@ -68,6 +68,7 @@ class Base
include Ordering
include Protection
include Pagination
include Searching
end
end

Expand Down
55 changes: 11 additions & 44 deletions lib/searchgasm/active_record/base.rb
Expand Up @@ -11,6 +11,7 @@ module Base
def calculate_with_searchgasm(*args)
options = args.extract_options!
options = filter_options_with_searchgasm(options, false)
args[1] = primary_key if options[:distinct] && [nil, :all].include?(args[1]) # quick fix for adding a column name if distinct is true and no specific column is provided
args << options
calculate_without_searchgasm(*args)
end
Expand Down Expand Up @@ -47,30 +48,6 @@ def with_scope_with_searchgasm(method_scoping = {}, action = :merge, &block)
with_scope_without_searchgasm(method_scoping, action, &block)
end

# This is a special method that Searchgasm adds in. It returns a new conditions object on the model. So you can search by conditions *only*.
#
# <b>This method is "protected". Meaning it checks the passed options for SQL injections. So trying to write raw SQL in *any* of the option will result in a raised exception. It's safe to pass a params object when instantiating.</b>
#
# === Examples
#
# conditions = User.new_conditions
# conditions.first_name_contains = "Ben"
# conditions.all # can call any search method: first, find(:all), find(:first), sum("id"), etc...
def build_conditions(values = {}, &block)
conditions = searchgasm_conditions
conditions.protect = true
conditions.conditions = values
yield conditions if block_given?
conditions
end

# See build_conditions. This is the same method but *without* protection. Do *NOT* pass in a params object to this method.
def build_conditions!(values = {}, &block)
conditions = searchgasm_conditions(values)
yield conditions if block_given?
conditions
end

# This is a special method that Searchgasm adds in. It returns a new search object on the model. So you can search via an object.
#
# <b>This method is "protected". Meaning it checks the passed options for SQL injections. So trying to write raw SQL in *any* of the option will result in a raised exception. It's safe to pass a params object when instantiating.</b>
Expand All @@ -86,7 +63,7 @@ def build_conditions!(values = {}, &block)
# search.order_by = {:user_group => :name}
# search.all # can call any search method: first, find(:all), find(:first), sum("id"), etc...
def build_search(options = {}, &block)
search = searchgasm_searcher
search = searchgasm_search
search.protect = true
search.options = options
yield search if block_given?
Expand All @@ -97,7 +74,7 @@ def build_search(options = {}, &block)
#
# This also has an alias "new_search!"
def build_search!(options = {}, &block)
search = searchgasm_searcher(options)
search = searchgasm_search(options)
yield search if block_given?
search
end
Expand All @@ -120,7 +97,7 @@ def protected_conditions # :nodoc:

# This is the reverse of conditions_protected. You can specify conditions here and *only* these conditions will be allowed in mass assignment. Any condition not specified here will be blocked.
def conditions_accessible(*conditions)
write_inheritable_attribute(:conditions_accessible, Set.new(conditions.map(&:to_s)) + (protected_conditions || []))
write_inheritable_attribute(:conditions_accessible, Set.new(conditions.map(&:to_s)) + (accessible_conditions || []))
end

def accessible_conditions # :nodoc:
Expand All @@ -130,7 +107,7 @@ def accessible_conditions # :nodoc:
private
def filter_options_with_searchgasm(options = {}, searching = true)
return options unless Searchgasm::Search::Base.needed?(self, options)
search = Searchgasm::Search::Base.create_virtual_class(self).new # call explicitly to avoid merging the scopes into the searcher
search = Searchgasm::Search::Base.create_virtual_class(self).new # call explicitly to avoid merging the scopes into the search
search.acting_as_filter = true
conditions = options.delete(:conditions) || options.delete("conditions") || {}
if conditions
Expand All @@ -145,15 +122,7 @@ def filter_options_with_searchgasm(options = {}, searching = true)
search.sanitize(searching)
end

def searchgasm_conditions(options = {})
searcher = Searchgasm::Conditions::Base.create_virtual_class(self).new
conditions = scope(:find) && scope(:find)[:conditions]
searcher.scope = {:conditions => conditions} if conditions
searcher.conditions = options
searcher
end

def searchgasm_searcher(options = {})
def searchgasm_search(options = {})
scope = {}
current_scope = scope(:find) && scope(:find).deep_dup
if current_scope
Expand All @@ -168,11 +137,11 @@ def searchgasm_searcher(options = {})
current_scope.each { |k, v| new_scope[k] = v unless v.nil? }
current_scope = new_scope
end
searcher = Searchgasm::Search::Base.create_virtual_class(self).new
searcher.scope = scope
searcher.options = current_scope
searcher.options = options
searcher
search = Searchgasm::Search::Base.create_virtual_class(self).new
search.scope = scope
search.options = current_scope
search.options = options
search
end
end
end
Expand All @@ -186,8 +155,6 @@ class << self
alias_method_chain :calculate, :searchgasm
alias_method_chain :find, :searchgasm
alias_method_chain :with_scope, :searchgasm
alias_method :new_conditions, :build_conditions
alias_method :new_conditions!, :build_conditions!
alias_method :new_search, :build_search
alias_method :new_search!, :build_search!

Expand Down
9 changes: 5 additions & 4 deletions lib/searchgasm/conditions/base.rb
Expand Up @@ -6,7 +6,6 @@ module Conditions # :nodoc:
# Each condition has its own file and class and the source for each condition is pretty self explanatory.
class Base
include Shared::Utilities
include Shared::Searching
include Shared::VirtualClasses

attr_accessor :any, :relationship_name
Expand Down Expand Up @@ -65,6 +64,8 @@ def condition_names
end

def needed?(model_class, conditions) # :nodoc:
return false if conditions.blank?

if conditions.is_a?(Hash)
return true if conditions[:any]
column_names = model_class.column_names
Expand Down Expand Up @@ -105,11 +106,11 @@ def any?
end

# A list of joins to use when searching, includes relationships
def joins
def auto_joins
j = []
associations.each do |association|
next if association.conditions.blank?
association_joins = association.joins
association_joins = association.auto_joins
j << (association_joins.blank? ? association.relationship_name.to_sym : {association.relationship_name.to_sym => association_joins})
end
j.blank? ? nil : (j.size == 1 ? j.first : j)
Expand Down Expand Up @@ -262,7 +263,7 @@ def objects
end

def reset_objects!
objects.each { |object| eval("@#{object.name} = nil") }
objects.each { |object| object.class < ::Searchgasm::Conditions::Base ? eval("@#{object.relationship_name} = nil") : eval("@#{object.name} = nil") }
objects.clear
end

Expand Down
16 changes: 5 additions & 11 deletions lib/searchgasm/search/base.rb
Expand Up @@ -3,10 +3,8 @@ module Search #:nodoc:
# = Searchgasm
#
# Please refer the README.rdoc for usage, examples, and installation.

class Base
include Searchgasm::Shared::Utilities
include Searchgasm::Shared::Searching
include Searchgasm::Shared::VirtualClasses

# Options ActiveRecord allows when searching
Expand All @@ -24,11 +22,14 @@ class Base
OPTIONS = SPECIAL_FIND_OPTIONS + AR_OPTIONS # the order is very important, these options get set in this order

attr_accessor *AR_OPTIONS
attr_reader :auto_joins

class << self
# Used in the ActiveRecord methods to determine if Searchgasm should get involved or not.
# This keeps Searchgasm out of the way unless it is needed.
def needed?(model_class, options)
return false if options.blank?

SPECIAL_FIND_OPTIONS.each do |option|
return true if options.symbolize_keys.keys.include?(option)
end
Expand Down Expand Up @@ -78,13 +79,8 @@ def inspect
"#<#{klass}Search #{current_find_options.inspect}>"
end

def joins=(value)
@memoized_joins = nil
@joins = value
end

def joins
@memoized_joins ||= @joins
merge_joins(@joins, auto_joins)
end

def limit=(value)
Expand Down Expand Up @@ -128,9 +124,7 @@ def sanitize(searching = true)
if searching
find_options[:group] ||= "#{quote_table_name(klass.table_name)}.#{quote_column_name(klass.primary_key)}"
else
# If we are calculating use includes because they use joins that grab uniq records. When calculating, includes don't have the
# performance hit that they have when searching. Plus it's cleaner.
find_options[:include] = merge_joins(find_options[:include], find_options.delete(:joins))
find_options[:distinct] = true
end
end

Expand Down
17 changes: 14 additions & 3 deletions lib/searchgasm/search/conditions.rb
Expand Up @@ -10,6 +10,7 @@ def self.included(klass)
alias_method_chain :initialize, :conditions
alias_method_chain :conditions=, :conditions
alias_method_chain :conditions, :conditions
alias_method_chain :auto_joins, :conditions
alias_method_chain :joins, :conditions
alias_method_chain :sanitize, :conditions
end
Expand All @@ -34,7 +35,7 @@ def initialize_with_conditions(init_options = {})
# now you can create the rest of your search and your "scope" will get merged into your final SQL.
# What this does is determine if the value a hash or a conditions object, if not it sets it up as a scope.
def conditions_with_conditions=(values)
@memoized_joins = nil
@memoized_auto_joins = nil
case values
when Searchgasm::Conditions::Base
@conditions = values
Expand All @@ -44,16 +45,26 @@ def conditions_with_conditions=(values)
end

def conditions_with_conditions
@memoized_joins = nil # have to assume they are calling a condition on a relationship
@memoized_auto_joins = nil # have to assume they are calling a condition on a relationship
conditions_without_conditions
end

# Tells searchgasm what relationships to join during the search. This is based on what conditions you set.
#
# <b>Be careful!</b>
# ActiveRecord associations can be an SQL train wreck. Make sure you think about what you are searching and that you aren't joining a table with a million records.
def auto_joins_with_conditions
@memoized_auto_joins ||= merge_joins(auto_joins_without_conditions, conditions.auto_joins)
end

# Changes joins to use left outer joins if conditions.any == true.
def joins_with_conditions
@memoized_joins ||= merge_joins(joins_without_conditions, conditions.joins)
if conditions.any?
join_dependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(klass, joins_without_conditions, nil)
join_dependency.join_associations.collect { |assoc| assoc.association_join }.join
else
joins_without_conditions
end
end

def sanitize_with_conditions(searching = true) # :nodoc:
Expand Down

0 comments on commit 6a8e5f7

Please sign in to comment.