legacy_migrations 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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