legacy_migrations 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *.swp
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,44 @@
1
+ =LegacyMigrations
2
+
3
+ This plugin implements a simple, expressive syntax for migrating data from one data
4
+ structure to another. Here's a simple example:
5
+
6
+ require 'legacy_migrations'
7
+
8
+ transfer_from Person, :to => Animal do
9
+ from :name, :to => :pet_name
10
+ end
11
+
12
+ This transfers data from the people table to the animals table,
13
+ mapping the people's _name_ column to animals' _pet\_name_ column.
14
+
15
+ But that's just the beginning. This plugin also:
16
+
17
+ * Reports invalid data by using your application's own validation errors for easy data cleanup
18
+ (just use ActiveRecord validations and after running the script from
19
+ the command line, the output will give you helpful details about validation errors.)
20
+ * Maps foreign keys between the databases (currently not implemented)
21
+ * Gives you a bunch of options for mapping fields between tables.
22
+
23
+ == Example
24
+
25
+ In some file in your rails app (perhaps db/seeds.rb?)
26
+
27
+ require 'legacy_migrations'
28
+
29
+ transfer_from Person, :to => Animal do
30
+ from :name, :to => :pet_name
31
+ from :sex, :to => :gender do |sex|
32
+ sex == 'm' ? 'male' : 'female'
33
+ end
34
+ end
35
+
36
+
37
+ OR, copy all columns with the same name
38
+
39
+ transfer_from Person, :to => Animal do
40
+ match_same_name_attributes
41
+ end
42
+
43
+
44
+ Copyright (c) 2010 Bernie Telles, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rake/testtask'
3
+ require 'rake'
4
+
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.name = "legacy_migrations"
10
+ gem.summary = "Rails plugin for transferring or updating data between two db structures."
11
+ gem.description = "Rails plugin for transferring or updating data between two db structures."
12
+ gem.email = "bernardo.telles@dms.myflorida.com"
13
+ gem.homepage = "http://github.com/btelles/legacy_migrations"
14
+ gem.authors = ["Bernie Telles"]
15
+ gem.add_development_dependency "rspec", ">= 1.2.9"
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'spec/rake/spectask'
24
+ Spec::Rake::SpecTask.new(:spec) do |spec|
25
+ spec.libs << 'lib' << 'spec'
26
+ spec.spec_files = FileList['spec/**/*_spec.rb']
27
+ end
28
+
29
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
30
+ spec.libs << 'lib' << 'spec'
31
+ spec.pattern = 'spec/**/*_spec.rb'
32
+ spec.rcov = true
33
+ end
34
+
35
+ task :spec => :check_dependencies
36
+
37
+ task :default => :spec
38
+
39
+ require 'rake/rdoctask'
40
+ Rake::RDocTask.new do |rdoc|
41
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
42
+
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = "legacy_migrations #{version}"
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
48
+ desc 'Default: run unit tests.'
49
+ task :default => :test
50
+
51
+ desc 'Test the legacy_migrations plugin.'
52
+ Rake::TestTask.new(:test) do |t|
53
+ t.libs << 'test'
54
+ t.pattern = 'test/**/*_test.rb'
55
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,5 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: spec/db/test.sqlite3
4
+ pool: 5
5
+ timeout: 5000
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,94 @@
1
+ require 'legacy_migrations/transformations'
2
+ require 'legacy_migrations/squirrel'
3
+ module LegacyMigrations
4
+
5
+ # Define a source and destination table to transfer data
6
+ #
7
+ # ==== Options
8
+ #
9
+ # * <tt>:to</tt> - (Required) Class of the destination table
10
+ # * <tt>:limit</tt> - Set a limit to the number of rows to transfer.
11
+ # This is useful when you're trying to find faulty data in the source
12
+ # table, and don't want to run the entire dataset.
13
+ # * <tt>:validate</tt> - Default = true. Use ActiveRecord validations
14
+ # when saving destination data.
15
+ # * <tt>:source_type</tt> - Default = :active_record. Sets the source
16
+ # destination type.
17
+ # Options:
18
+ # _:active_record_: Assumes the From option is a class that inherits
19
+ # from ActiveRecord::Base, then iterates through each record of From
20
+ # table by using *From*.all.each...
21
+ # _:other_: Assumes the From option is an iterable 'collection' whose
22
+ # elements/items can respond to all columns speficied in the given block.
23
+ #
24
+ def transfer_from(from_table, *args, &block)
25
+
26
+ configure_transfer(from_table, *args) { yield }
27
+
28
+ source_iterator(@limit, @type).each do |from_record|
29
+ new_destination_record(from_record)
30
+ end
31
+ end
32
+
33
+ def update_from(from_table, *args, &block)
34
+
35
+ configure_transfer(from_table, *args) { yield }
36
+
37
+ source_iterator(@limit, @type).each do |from_record|
38
+ matching_records = @conditions.call(from_record)
39
+
40
+ #debugger if from_record.name == 'smithers'
41
+ unless matching_records.empty?
42
+ matching_records.each do |to_record|
43
+ @columns.each do |to, from|
44
+ to_record[to]= from.call(from_record)
45
+ end
46
+
47
+ if @options[:validate]
48
+ report_validation_errors(to_record, from_record)
49
+ else
50
+ to_record.save(false)
51
+ end
52
+ end
53
+ else
54
+ new_destination_record(from_record)
55
+ end
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def configure_transfer(from_table, *args, &block)
62
+ @columns = {}
63
+
64
+ @options = {:validate => true}.merge(args.extract_options!)
65
+
66
+ @from_table = from_table
67
+ @to_table = @options[:to]
68
+
69
+ yield
70
+
71
+ @limit = @options[:limit] ? {:limit, @options[:limit]} : {}
72
+ @type = @options[:source_type] ? @options[:source_type] : :active_record
73
+ end
74
+
75
+ def new_destination_record(from_record)
76
+ columns = @columns.inject({}) do |result, attributes|
77
+ result[attributes[0]]= attributes[1].call(from_record)
78
+ result
79
+ end
80
+ new_record = @to_table.new(columns)
81
+
82
+ if @options[:validate]
83
+ report_validation_errors(new_record, from_record)
84
+ else
85
+ new_record.save(false)
86
+ end
87
+ end
88
+ end
89
+ include LegacyMigrations
90
+ include LegacyMigrations::Transformations
91
+ include LegacyMigrations::ValidationHelper
92
+ include LegacyMigrations::SourceIterators
93
+ include LegacyMigrations::RowMatchers
94
+
@@ -0,0 +1,38 @@
1
+ module LegacyMigrations
2
+ module RowMatchers
3
+
4
+ # Use 'based_on' to match a destination record with a
5
+ # source record when running an update.
6
+ #
7
+ # This uses the matching syntax available in Thoughtbot's
8
+ # Squirrel plugin where the left operator is the destination
9
+ # field name, and the right-hand operator is usually
10
+ # the source row and method.
11
+ #
12
+ # === Example
13
+ #
14
+ # based_on do |from|
15
+ # name == from.name
16
+ # age > 17
17
+ # end
18
+ #
19
+ # The above says that if we have a destination row
20
+ # whose name attribute matches a source (from) row's name,
21
+ # and the destination row has an age > 17, then assume the
22
+ # source row and destination row are the same record, and update
23
+ # the destination row with the source row's data
24
+ #
25
+ # See the thoughtbot documentation for more details
26
+ # about the squirrel syntax at:
27
+ # http://github.com/thoughtbot/squirrel/
28
+ def based_on(&blk)
29
+ @blck = blk
30
+ @conditions = Proc.new {|from|
31
+ @from = from
32
+ query = LegacyMigrations::Squirrel::Query.new(@to_table, from, &@blck)
33
+ query.execute(:all)
34
+ }
35
+ end
36
+ end
37
+ end
38
+
@@ -0,0 +1,22 @@
1
+ module LegacyMigrations
2
+ module SourceIterators
3
+ def source_iterator(limit, type, &block)
4
+ if type == :active_record
5
+ @from_table.all(limit)
6
+ elsif type == :csv
7
+ if limit[:limit]
8
+ fewer_rows = []
9
+ rows_processed = 0
10
+ @from_table.each do |row|
11
+ fewer_rows << row
12
+ rows_processed += 1
13
+ break if rows_processed == limit[:limit].to_i
14
+ end
15
+ FasterCSV::Table.new(fewer_rows)
16
+ else
17
+ @from_table
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ require File.dirname(__FILE__) + '/squirrel/squirrel.rb'
2
+ class << ActiveRecord::Base
3
+ include LegacyMigrations::Squirrel::Hook
4
+ end
5
+
6
+ if defined?(ActiveRecord::NamedScope::Scope)
7
+ class ActiveRecord::NamedScope::Scope
8
+ include LegacyMigrations::Squirrel::NamedScopeHook
9
+ end
10
+ end
11
+
12
+ [ ActiveRecord::Associations::HasManyAssociation,
13
+ ActiveRecord::Associations::HasAndBelongsToManyAssociation,
14
+ ActiveRecord::Associations::HasManyThroughAssociation
15
+ ].each do |association_class|
16
+ association_class.send(:include, LegacyMigrations::Squirrel::Hook)
17
+ end
@@ -0,0 +1,36 @@
1
+ class Hash
2
+ def merge_tree other
3
+ self.dup.merge_tree! other
4
+ end
5
+
6
+ def merge_tree! other
7
+ other.each do |key, value|
8
+ if self[key].is_a?(Hash) && value.is_a?(Hash)
9
+ self[key] = self[key].merge_tree(value)
10
+ else
11
+ self[key] = value
12
+ end
13
+ end
14
+ self
15
+ end
16
+ end
17
+
18
+ module ActiveRecord #:nodoc: all
19
+ module Associations
20
+ module ClassMethods
21
+ class JoinDependency
22
+ class JoinAssociation
23
+ def ancestry #:doc
24
+ [ parent.ancestry, reflection.name ].flatten.compact
25
+ end
26
+ end
27
+ class JoinBase
28
+ def ancestry
29
+ nil
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
@@ -0,0 +1,83 @@
1
+ module LegacyMigrations
2
+ module Squirrel
3
+ # The WillPagination module emulates the results that the will_paginate plugin returns
4
+ # from its #paginate methods. When it is used to extend a result set from Squirrel, it
5
+ # automtically pulls the result from the pagination that Squirrel performs. The methods
6
+ # added to that result set make it duck-equivalent to the WillPaginate::Collection
7
+ # class.
8
+ module WillPagination
9
+ def self.extended base
10
+ base.current_page = base.pages.current || 1
11
+ base.per_page = base.pages.per_page
12
+ base.total_entries = base.pages.total_results
13
+ end
14
+
15
+ attr_accessor :current_page, :per_page, :total_entries
16
+
17
+ # Returns the current_page + 1, or nil if there are no more.
18
+ def next_page
19
+ current_page < page_count ? (current_page + 1) : nil
20
+ end
21
+
22
+ # Returns the offset of the current page that is suitable for inserting into SQL.
23
+ def offset
24
+ (current_page - 1) * per_page
25
+ end
26
+
27
+ # Returns true if the current_page is greater than the total number of pages.
28
+ # Useful in case someone manually modifies the URL to put their own page number in.
29
+ def out_of_bounds?
30
+ current_page > page_count
31
+ end
32
+
33
+ # The number of pages in the result set.
34
+ def page_count
35
+ pages.last
36
+ end
37
+
38
+ alias_method :total_pages, :page_count
39
+
40
+ # Returns the current_page - 1, or nil if this is the first page.
41
+ def previous_page
42
+ current_page > 1 ? (current_page - 1) : nil
43
+ end
44
+
45
+ # Sets the number of pages and entries.
46
+ def total_entries= total
47
+ @total_entries = total.to_i
48
+ @total_pages = (@total_entries / per_page.to_f).ceil
49
+ end
50
+ end
51
+
52
+ # The Page class holds information about the current page of results.
53
+ class Page
54
+ attr_reader :offset, :limit, :page, :per_page
55
+ def initialize(offset, limit, page, per_page)
56
+ @offset, @limit, @page, @per_page = offset, limit, page, per_page
57
+ end
58
+ end
59
+
60
+ # A Paginator object is what gets inserted into the result set and is returned by
61
+ # the #pages method of the result set. Contains offets and limits for all pages.
62
+ class Paginator < Array
63
+ attr_reader :total_results, :per_page, :current, :next, :previous, :first, :last, :current_range
64
+ def initialize opts={}
65
+ @total_results = opts[:count].to_i
66
+ @limit = opts[:limit].to_i
67
+ @offset = opts[:offset].to_i
68
+
69
+ @per_page = @limit
70
+ @current = (@offset / @limit) + 1
71
+ @first = 1
72
+ @last = ((@total_results-1) / @limit) + 1
73
+ @next = @current + 1 if @current < @last
74
+ @previous = @current - 1 if @current > 1
75
+ @current_range = ((@offset+1)..([@offset+@limit, @total_results].min))
76
+
77
+ (@first..@last).each do |page|
78
+ self[page-1] = Page.new((page-1) * @per_page, @per_page, page, @per_page)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,532 @@
1
+ require File.dirname(__FILE__) + '/paginator'
2
+ require File.dirname(__FILE__) + '/extensions'
3
+ #
4
+ # Squirrel is a library for making querying the database using ActiveRecord cleaner, easier
5
+ # to read, and less prone to user error. It does this by allowing AR::Base#find to take a block,
6
+ # which is run to build the conditions and includes required to execute the query.
7
+ module LegacyMigrations
8
+ module Squirrel
9
+ # When included in AR::Base, it chains the #find method to allow for block execution.
10
+ module Hook
11
+ def find_with_squirrel *args, &blk
12
+ args ||= [:all]
13
+ if blk || (args.last.is_a?(Hash) && args.last.has_key?(:paginate))
14
+ query = Query.new(self, &blk)
15
+ query.execute(*args)
16
+ else
17
+ find_without_squirrel(*args)
18
+ end
19
+ end
20
+
21
+ def scoped_with_squirrel *args, &blk
22
+ if blk
23
+ query = Query.new(self, &blk)
24
+ self.scoped(query.to_find_parameters)
25
+ else
26
+ scoped_without_squirrel(*args)
27
+ end
28
+ end
29
+
30
+ def self.included base
31
+ if ! base.instance_methods.include?('find_without_squirrel') &&
32
+ base.instance_methods.include?('find')
33
+ base.class_eval do
34
+ alias_method :find_without_squirrel, :find
35
+ alias_method :find, :find_with_squirrel
36
+ end
37
+ end
38
+ if ! base.instance_methods.include?('scoped_without_squirrel') &&
39
+ base.instance_methods.include?('scoped')
40
+ base.class_eval do
41
+ alias_method :scoped_without_squirrel, :scoped
42
+ alias_method :scoped, :scoped_with_squirrel
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ module NamedScopeHook
49
+ def scoped *args, &blk
50
+ args = blk ? [Query.new(self, &blk).to_find_parameters] : args
51
+ scopes[:scoped].call(self, *args)
52
+ end
53
+ end
54
+
55
+ # The Query is what contains the query and is what handles execution and pagination of the
56
+ # result set.
57
+ class Query
58
+ attr_reader :conditions, :model
59
+
60
+ # Creates a Query specific to the given model (which is a class that descends from AR::Base)
61
+ # and a block that will be run to find the conditions for the #find call.
62
+ def initialize model, from_record = nil, &blk
63
+ @model = model
64
+ @joins = nil
65
+ @from = from_record
66
+ @binding = blk && blk.binding
67
+ @conditions = ConditionGroup.new(@model, "AND", @binding, @from, &blk)
68
+ @conditions.assign_joins( join_dependency )
69
+ end
70
+
71
+ # Builds the dependencies needed to find what AR plans to call the tables in the query
72
+ # by finding and sending what would be passed in as the +include+ parameter to #find.
73
+ # This is a necessary step because what AR calls tables deeply nested might not be
74
+ # completely obvious.)
75
+ def join_dependency
76
+ jd = ::ActiveRecord::Associations::ClassMethods::JoinDependency
77
+ @join_dependency ||= jd.new model,
78
+ @conditions.to_find_include,
79
+ nil
80
+ end
81
+
82
+ # Runs the block which builds the conditions. If requested, paginates the result_set.
83
+ # If the first parameter to #find is :query (instead of :all or :first), then the query
84
+ # object itself is returned. Useful for debugging, but not much else.
85
+ def execute *args
86
+ if args.first == :query
87
+ self
88
+ else
89
+ opts = args.last.is_a?(Hash) ? args.last : {}
90
+ results = []
91
+
92
+ pagination = opts.delete(:paginate) || {}
93
+ model.send(:with_scope, :find => opts) do
94
+ @conditions.paginate(pagination) unless pagination.empty?
95
+ results = model.find args[0], to_find_parameters
96
+ if @conditions.paginate?
97
+ paginate_result_set results, to_find_parameters
98
+ end
99
+ end
100
+ results
101
+ end
102
+ end
103
+
104
+ def from
105
+ @from
106
+ end
107
+
108
+ def to_find_parameters
109
+ find_parameters = {}
110
+ find_parameters[:conditions] = to_find_conditions unless to_find_conditions.blank?
111
+ find_parameters[:include ] = to_find_include unless to_find_include.blank?
112
+ find_parameters[:order ] = to_find_order unless to_find_order.blank?
113
+ find_parameters[:limit ] = to_find_limit unless to_find_limit.blank?
114
+ find_parameters[:offset ] = to_find_offset unless to_find_offset.blank?
115
+ find_parameters
116
+ end
117
+
118
+ # Delegates the to_find_conditions call to the root ConditionGroup
119
+ def to_find_conditions
120
+ @conditions.to_find_conditions
121
+ end
122
+
123
+ # Delegates the to_find_include call to the root ConditionGroup
124
+ def to_find_include
125
+ @conditions.to_find_include
126
+ end
127
+
128
+ # Delegates the to_find_order call to the root ConditionGroup
129
+ def to_find_order
130
+ @conditions.to_find_order
131
+ end
132
+
133
+ # Delegates the to_find_limit call to the root ConditionGroup
134
+ def to_find_limit
135
+ @conditions.to_find_limit
136
+ end
137
+
138
+ # Delegates the to_find_offset call to the root ConditionGroup
139
+ def to_find_offset
140
+ @conditions.to_find_offset
141
+ end
142
+
143
+ # Used by #execute to paginates the result set if
144
+ # pagination was requested. In this case, it adds +pages+ and +total_results+ accessors
145
+ # to the result set. See Paginator for more details.
146
+ def paginate_result_set set, conditions
147
+ limit = conditions.delete(:limit)
148
+ offset = conditions.delete(:offset)
149
+
150
+ class << set
151
+ attr_reader :pages
152
+ attr_reader :total_results
153
+ end
154
+
155
+ total_results = model.count(conditions)
156
+ set.instance_variable_set("@pages",
157
+ Paginator.new( :count => total_results,
158
+ :limit => limit,
159
+ :offset => offset) )
160
+ set.instance_variable_set("@total_results", total_results)
161
+ set.extend( Squirrel::WillPagination )
162
+ end
163
+
164
+ # ConditionGroups are groups of Conditions, oddly enough. They most closely map to models
165
+ # in your schema, but they also handle the grouping jobs for the #any and #all blocks.
166
+ class ConditionGroup
167
+ attr_accessor :model, :logical_join, :binding, :reflection, :path
168
+
169
+ # Creates a ConditionGroup by passing in the following arguments:
170
+ # * model: The AR subclass that defines what columns and associations will be accessible
171
+ # in the given block.
172
+ # * logical_join: A string containing the join that will be used when concatenating the
173
+ # conditions together. The root level ConditionGroup created by Query defaults the
174
+ # join to be "AND", but the #any and #all methods will create specific ConditionGroups
175
+ # using "OR" and "AND" as their join, respectively.
176
+ # * binding: The +binding+ of the block passed to the original #find. Will be used to
177
+ # +eval+ what +self+ would be. This is necessary for using methods like +params+ and
178
+ # +session+ in your controllers.
179
+ # * path: The "path" taken through the models to arrive at this model. For example, if
180
+ # your User class has_many Posts which has_many Comments each of which belongs_to User,
181
+ # the path to the second User would be [:posts, :comments, :user]
182
+ # * reflection: The association used to get to this block. If nil, then no new association
183
+ # was traversed, which means we're in an #any or #all grouping block.
184
+ # * blk: The block to be executed.
185
+ #
186
+ # This method defines a number of methods to be available inside the block, one for each
187
+ # of the columns and associations in the specified model. Note that you CANNOT use
188
+ # user-defined methods on your model inside Squirrel queries. They don't have any meaning
189
+ # in the context of a database query.
190
+ def initialize model, logical_join, binding, from_record = nil, path = nil, reflection = nil, &blk
191
+ @model = model
192
+ @from = from_record
193
+ @logical_join = logical_join
194
+ @conditions = []
195
+ @condition_blocks = []
196
+ @reflection = reflection
197
+ @path = [ path, reflection ].compact.flatten
198
+ @binding = binding
199
+ @order = []
200
+ @negative = false
201
+ @paginator = false
202
+ @block = blk
203
+
204
+ existing_methods = self.class.instance_methods(false)
205
+ (model.column_names - existing_methods).each do |col|
206
+ (class << self; self; end).class_eval do
207
+ define_method(col.to_s.intern) do
208
+ column(col)
209
+ end
210
+ end
211
+ end
212
+ (model.reflections.keys - existing_methods).each do |assn|
213
+ (class << self; self; end).class_eval do
214
+ define_method(assn.to_s.intern) do
215
+ association(assn)
216
+ end
217
+ end
218
+ end
219
+
220
+ execute_block
221
+ end
222
+
223
+ def from
224
+ @from
225
+ end
226
+
227
+ # Creates a Condition and queues it for inclusion. When calling a method defined
228
+ # during the creation of the ConditionGroup object is the same as calling column(:column_name).
229
+ # This is useful if you need to access a column that happens to coincide with the name of
230
+ # an already-defined method (e.g. anything returned by instance_methods(false) for the
231
+ # given model).
232
+ def column name
233
+ @conditions << Condition.new(name)
234
+ @conditions.last
235
+ end
236
+
237
+ # Similar to #column, this will create an association even if you can't use the normal
238
+ # method version.
239
+ def association name, &blk
240
+ name = name.to_s.intern
241
+ ref = @model.reflect_on_association(name)
242
+ @condition_blocks << ConditionGroup.new(ref.klass, logical_join, binding, @from, path, ref.name, &blk)
243
+ @condition_blocks.last
244
+ end
245
+
246
+ # Creates a ConditionGroup that has the logical_join set to "OR".
247
+ def any &blk
248
+ @condition_blocks << ConditionGroup.new(model, "OR", binding, @from, path, &blk)
249
+ @condition_blocks.last
250
+ end
251
+
252
+ # Creates a ConditionGroup that has the logical_join set to "AND".
253
+ def all &blk
254
+ @condition_blocks << ConditionGroup.new(model, "AND", binding, @from, path, &blk)
255
+ @condition_blocks.last
256
+ end
257
+
258
+ # Sets the arguments for the :order parameter. Arguments can be columns (i.e. Conditions)
259
+ # or they can be strings (for "RANDOM()", etc.). If a Condition is used, and the column is
260
+ # negated using #not or #desc, then the resulting specification in the ORDER clause will
261
+ # be ordered descending. That is, "order_by name.desc" will become "ORDER name DESC"
262
+ def order_by *columns
263
+ @order += [columns].flatten
264
+ end
265
+
266
+ # Flags the result set to be paginated according to the :page and :per_page parameters
267
+ # to this method.
268
+ def paginate opts = {}
269
+ @paginator = true
270
+ page = (opts[:page] || 1).to_i
271
+ per_page = (opts[:per_page] || 20).to_i
272
+ page = 1 if page < 1
273
+ limit( per_page, ( page - 1 ) * per_page )
274
+ end
275
+
276
+ # Similar to #paginate, but does not flag the result set for pagination. Takes a limit
277
+ # and an offset (by default the offset is 0).
278
+ def limit lim, off = nil
279
+ @limit = ( lim || @limit ).to_i
280
+ @offset = ( off || @offset ).to_i
281
+ end
282
+
283
+ # Returns true if this ConditionGroup or any of its subgroups have been flagged for pagination.
284
+ def paginate?
285
+ @paginator || @condition_blocks.any?(&:paginate?)
286
+ end
287
+
288
+ # Negates the condition. Essentially prefixes the condition with NOT in the final query.
289
+ def -@
290
+ @negative = !@negative
291
+ self
292
+ end
293
+
294
+ alias_method :desc, :-@
295
+
296
+ # Negates the condition. Also works to negate ConditionGroup blocks in a more straightforward
297
+ # manner, like so:
298
+ # any.not do
299
+ # id == 1
300
+ # name == "Joe"
301
+ # end
302
+ #
303
+ # # => "NOT( id = 1 OR name = 'Joe')"
304
+ def not &blk
305
+ @negative = !@negative
306
+ if blk
307
+ @block = blk
308
+ execute_block
309
+ end
310
+ end
311
+
312
+ # Takes the JoinDependency object and filters it down through the ConditionGroups
313
+ # to make sure each one knows the aliases necessary to refer to each table by its
314
+ # correct name.
315
+ def assign_joins join_dependency, ancestries = nil
316
+ ancestries ||= join_dependency.join_associations.map{|ja| ja.ancestry }
317
+ unless @conditions.empty?
318
+ my_association = unless @path.blank?
319
+ join_dependency.join_associations[ancestries.index(@path)]
320
+ else
321
+ join_dependency.join_base
322
+ end
323
+ @conditions.each do |column|
324
+ column.assign_join(my_association)
325
+ end
326
+ end
327
+ @condition_blocks.each do |association|
328
+ association.assign_joins(join_dependency, ancestries)
329
+ end
330
+ end
331
+
332
+ # Generates the parameter for :include for this ConditionGroup and all its subgroups.
333
+ def to_find_include
334
+ @condition_blocks.inject({}) do |inc, cb|
335
+ if cb.reflection.nil?
336
+ inc.merge_tree(cb.to_find_include)
337
+ else
338
+ inc[cb.reflection] ||= {}
339
+ inc[cb.reflection] = inc[cb.reflection].merge_tree(cb.to_find_include)
340
+ inc
341
+ end
342
+ end
343
+ end
344
+
345
+ # Generates the :order parameter for this ConditionGroup. Because this does not reference
346
+ # subgroups it should only be used from the outermost block (which is probably where it makes
347
+ # the most sense to reference it, but it's worth mentioning)
348
+ def to_find_order
349
+ if @order.blank?
350
+ nil
351
+ else
352
+ @order.collect do |col|
353
+ col.respond_to?(:full_name) ? (col.full_name + (col.negative? ? " DESC" : "")) : col
354
+ end.join(", ")
355
+ end
356
+ end
357
+
358
+ # Generates the :conditions parameter for this ConditionGroup and all subgroups. It
359
+ # generates them in ["sql", params] format because of the requirements of LIKE, etc.
360
+ def to_find_conditions
361
+ segments = conditions.collect{|c| c.to_find_conditions }.compact
362
+ return nil if segments.length == 0
363
+ cond = "(" + segments.collect{|s| s.first }.join(" #{logical_join} ") + ")"
364
+ cond = "NOT #{cond}" if negative?
365
+
366
+ values = segments.inject([]){|all, now| all + now[1..-1] }
367
+ [ cond, *values ]
368
+ end
369
+
370
+ # Generates the :limit parameter.
371
+ def to_find_limit
372
+ @limit
373
+ end
374
+
375
+ # Generates the :offset parameter.
376
+ def to_find_offset
377
+ @offset
378
+ end
379
+
380
+ # Returns all the conditions, which is the union of the Conditions and ConditionGroups
381
+ # that belong to this ConditionGroup.
382
+ def conditions
383
+ @conditions + @condition_blocks
384
+ end
385
+
386
+ # Returns true if this block has been negated using #not, #desc, or #-
387
+ def negative?
388
+ @negative
389
+ end
390
+
391
+ # This is a bit of a hack, due to how Squirrel is built. It can be used to fetch
392
+ # instance variables from the location where the call to #find was made. For example,
393
+ # if called from within your model and you happened to have an instance variable called
394
+ # "@foo", you can access it by calling
395
+ # instance "@foo"
396
+ # from within your Squirrel query.
397
+ def instance instance_var
398
+ s = eval("self", binding)
399
+ if s
400
+ s.instance_variable_get(instance_var)
401
+ end
402
+ end
403
+
404
+ private
405
+
406
+ def execute_block #:nodoc:
407
+ instance_eval &@block if @block
408
+ end
409
+
410
+ def method_missing meth, *args #:nodoc:
411
+ m = eval <<-end_eval, binding
412
+ begin
413
+ method(:#{meth})
414
+ rescue NameError
415
+ nil
416
+ end
417
+ end_eval
418
+ if m
419
+ m.call(*args)
420
+ else
421
+ super(meth, *args)
422
+ end
423
+ end
424
+
425
+ end
426
+
427
+ # Handles comparisons in the query. This class is analagous to the columns in the database.
428
+ # When comparing the Condition to a value, the operators are used as follows:
429
+ # * ==, === : Straight-up Equals. Can also be used as the "IN" operator if the operand is an Array.
430
+ # Additionally, when the oprand is +nil+, the comparison is correctly generates as "IS NULL"."
431
+ # * =~ : The LIKE and REGEXP operators. If the operand is a String, it will generate a LIKE
432
+ # comparison. If it is a Regexp, the REGEXP operator will be used. NOTE: MySQL regular expressions
433
+ # are NOT the same as Ruby regular expressions. Also NOTE: No wildcards are inserted into the LIKE
434
+ # comparison, so you may add them where you wish.
435
+ # * <=> : Performs a BETWEEN comparison, as long as the operand responds to both #first and #last,
436
+ # which both Ranges and Arrays do.
437
+ # * > : A simple greater-than comparison.
438
+ # * >= : Greater-than or equal-to.
439
+ # * < : A simple less-than comparison.
440
+ # * <= : Less-than or equal-to.
441
+ # * contains? : Like =~, except automatically surrounds the operand in %s, which =~ does not do.
442
+ # * nil? : Works exactly like "column == nil", but in a nicer syntax, which is what Squirrel is all about.
443
+ class Condition
444
+ attr_reader :name, :operator, :operand
445
+
446
+ # Creates and Condition with the given name.
447
+ def initialize name
448
+ @name = name
449
+ @sql = nil
450
+ @negative = false
451
+ end
452
+
453
+ [ :==, :===, :=~, :<=>, :<=, :<, :>, :>= ].each do |op|
454
+ define_method(op) do |val|
455
+ @operator = op
456
+ @operand = val
457
+ self
458
+ end
459
+ end
460
+
461
+ def contains? val #:nodoc:
462
+ @operator = :contains
463
+ @operand = val
464
+ self
465
+ end
466
+
467
+ def nil? #:nodoc:
468
+ @operator = :==
469
+ @operand = nil
470
+ self
471
+ end
472
+
473
+ def -@ #:nodoc:
474
+ @negative = !@negative
475
+ self
476
+ end
477
+
478
+ alias_method :not, :-@
479
+ alias_method :desc, :-@
480
+
481
+ # Returns true if this Condition has been negated, which means it will be prefixed with "NOT"
482
+ def negative?
483
+ @negative
484
+ end
485
+
486
+ # Gets the name of the table that this Condition refers to by taking it out of the
487
+ # association object.
488
+ def assign_join association = nil
489
+ @table_alias = association ? "#{association.aliased_table_name}." : ""
490
+ end
491
+
492
+ # Returns the full name of the column, including any assigned table alias.
493
+ def full_name
494
+ "#{@table_alias}#{name}"
495
+ end
496
+
497
+ # Generates the :condition parameter for this Condition, in ["sql", args] format.]
498
+ def to_find_conditions(join_association = {})
499
+ return nil if operator.nil?
500
+
501
+ op, arg_format, values = operator, "?", [operand]
502
+ op, arg_format, values = case operator
503
+ when :<=> then [ "BETWEEN", "? AND ?", [ operand.first, operand.last ] ]
504
+ when :=~ then
505
+ case operand
506
+ when String then [ "LIKE", arg_format, values ]
507
+ when Regexp then [ "REGEXP", arg_format, values.map(&:source) ]
508
+ end
509
+ when :==, :=== then
510
+ case operand
511
+ when Array then [ "IN", "(?)", values ]
512
+ when Range then [ "IN", "(?)", values ]
513
+ when Condition then [ "=", operand.full_name, [] ]
514
+ when nil then [ "IS", "NULL", [] ]
515
+ else [ "=", arg_format, values ]
516
+ end
517
+ when :contains then [ "LIKE", arg_format, values.map{|v| "%#{v}%" } ]
518
+ else
519
+ case operand
520
+ when Condition then [ op, oprand.full_name, [] ]
521
+ else [ op, arg_format, values ]
522
+ end
523
+ end
524
+ sql = "#{full_name} #{op} #{arg_format}"
525
+ sql = "NOT (#{sql})" if @negative
526
+ [ sql, *values ]
527
+ end
528
+
529
+ end
530
+ end
531
+ end
532
+ end