sqrbl 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/LICENSE.txt +621 -0
- data/README.txt +85 -0
- data/Rakefile +32 -0
- data/TODO.txt +11 -0
- data/bin/sqrbl +8 -0
- data/lib/core_exts/object.rb +44 -0
- data/lib/core_exts/symbol.rb +16 -0
- data/lib/sqrbl/base_migration_writer.rb +52 -0
- data/lib/sqrbl/call_stack.rb +21 -0
- data/lib/sqrbl/group.rb +42 -0
- data/lib/sqrbl/individual_migration_writer.rb +48 -0
- data/lib/sqrbl/migration.rb +62 -0
- data/lib/sqrbl/mixins/expects_block_with_new.rb +27 -0
- data/lib/sqrbl/mixins/has_todos.rb +65 -0
- data/lib/sqrbl/mixins/method_missing_delegation.rb +83 -0
- data/lib/sqrbl/step.rb +192 -0
- data/lib/sqrbl/step_pair.rb +56 -0
- data/lib/sqrbl/unified_migration_writer.rb +34 -0
- data/lib/sqrbl.rb +83 -0
- data/spec/README.txt +4 -0
- data/spec/functional/base_migration_writer_spec.rb +12 -0
- data/spec/functional/individual_migration_writer_spec.rb +172 -0
- data/spec/functional/unified_migration_writer_spec.rb +97 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/unit/group_spec.rb +85 -0
- data/spec/unit/migration_spec.rb +68 -0
- data/spec/unit/sqrbl_spec.rb +28 -0
- data/spec/unit/step_pair_spec.rb +110 -0
- data/spec/unit/step_spec.rb +154 -0
- data/tasks/ann.rake +80 -0
- data/tasks/bones.rake +20 -0
- data/tasks/gem.rake +201 -0
- data/tasks/git.rake +40 -0
- data/tasks/notes.rake +27 -0
- data/tasks/post_load.rake +34 -0
- data/tasks/rdoc.rake +51 -0
- data/tasks/rubyforge.rake +55 -0
- data/tasks/setup.rb +292 -0
- data/tasks/spec.rake +54 -0
- data/tasks/svn.rake +47 -0
- data/tasks/test.rake +40 -0
- data/tasks/zentest.rake +36 -0
- metadata +115 -0
data/README.txt
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
== SQrbL: Making database migrations suck less since July 2009!
|
2
|
+
|
3
|
+
Copyright (c) 2009 raSANTIAGO + Associates LLC (http://www.rasantiago.com)
|
4
|
+
|
5
|
+
The source code for SQrbL can be found on GitHub at:
|
6
|
+
1. http://github.com/rasantiago/sqrbl/
|
7
|
+
2. http://github.com/geeksam/sqrbl/
|
8
|
+
|
9
|
+
SQrbL's creator and primary author is Sam Livingston-Gray.
|
10
|
+
|
11
|
+
== DESCRIPTION:
|
12
|
+
|
13
|
+
SQrbL was created to help manage an extremely specific problem: managing SQL-based database migrations.
|
14
|
+
|
15
|
+
In essence, SQrbL is a tool for managing multiple SQL queries using Ruby. SQrbL borrows some terminology and ideas from ActiveRecord's schema migrations, but where ActiveRecord manages changes to your database schema over time, SQrbL was written to manage the process of transforming your data from one schema to another. (Of course, you could use SQrbL for the former case as well -- just use it to write DDL queries -- but ActiveRecord has better tools for figuring out which migrations have already been applied.)
|
16
|
+
|
17
|
+
===How It Works:
|
18
|
+
|
19
|
+
You describe the steps in your migration in a SQrbL file. Each step can produce as much or as little SQL as you like. Each step has an "up" and a "down" part -- so you can do and undo each step as many times as you need to, until you get it just right. When you run your SQrbL file, it creates a tree of *.sql files containing the output from your migration steps, one file per step. It also creates the combined files "all_up.sql" and "all_down.sql"; these contain all of your steps combined into one giant file so that, when Cutover Day arrives, you can copy/paste the whole thing into your SQL client and run it all at once.
|
20
|
+
|
21
|
+
===Why It Exists:
|
22
|
+
|
23
|
+
SQL is a fantastic DSL for describing queries. It's not bad at doing transformations, either. Unfortunately, SQL is, um... rather verbose. And, it lacks tools for reducing duplication -- if you have to run five similar queries that differ only in their parameters, you have to figure out how to use a prepared statement, but if you have to run five similar queries that differ in the field names they use, that's a bit more work. SQrbL lets you use SQL for the things SQL is good at, and Ruby for the other stuff.
|
24
|
+
|
25
|
+
===About the Name:
|
26
|
+
|
27
|
+
SQL.rb seemed a bit too pretentious, so I went with SQrbL -- as in, "You got Ruby in my SQL." I pronounce it "squirble" (rhymes with "squirrel"). The proper capitalization can be annoying to type, so I've aliased the SQrbL class as Sqrbl.
|
28
|
+
|
29
|
+
== SYNOPSIS:
|
30
|
+
|
31
|
+
<i>(Note that, in the following code sample, I'm using the convention that do/end blocks are used primarily for their side effects, and curly-brace blocks are used primarily for their return value.)</i>
|
32
|
+
|
33
|
+
Sqrbl.migration "Convert from old widgets to new widgets" do
|
34
|
+
set_output_directory '/path/to/generated/sql'
|
35
|
+
|
36
|
+
group "Widgets" do
|
37
|
+
step "Create widgets" do
|
38
|
+
up do
|
39
|
+
action "Migrate old_widgets" {
|
40
|
+
<<-SQL
|
41
|
+
#{
|
42
|
+
insert_into("new_widgets", {
|
43
|
+
:name => 'widget_name',
|
44
|
+
:part_num => 'CONCAT("X_", part_number)',
|
45
|
+
:note => '"Imported from old_widgets"',
|
46
|
+
})
|
47
|
+
}
|
48
|
+
FROM old_widgets
|
49
|
+
SQL
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
down do
|
54
|
+
action "Drop imported organizational contacts" {
|
55
|
+
'DELETE FROM new_widgets WHERE note LIKE "Imported from old_widgets"'
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
The above code sample describes a migration with one step: migrating the data in an `old_widgets` table to a `new_widgets` table. When run, this will generate a set of *.sql files in the directory /path/to/generated/sql.
|
63
|
+
|
64
|
+
== REQUIREMENTS:
|
65
|
+
|
66
|
+
* Ruby. (SQrbL was written using MRI Ruby 1.8.6, and has not been tested using other versions.)
|
67
|
+
|
68
|
+
== INSTALL:
|
69
|
+
|
70
|
+
sudo gem install rasantiago-sqrbl --source http://gems.github.com
|
71
|
+
|
72
|
+
== LICENSE:
|
73
|
+
|
74
|
+
SQrbL is free software: you can redistribute it and/or modify
|
75
|
+
it under the terms of the GNU General Public License as published by
|
76
|
+
the Free Software Foundation, either version 3 of the License, or
|
77
|
+
(at your option) any later version.
|
78
|
+
|
79
|
+
This program is distributed in the hope that it will be useful,
|
80
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
81
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
82
|
+
GNU General Public License for more details.
|
83
|
+
|
84
|
+
You should have received a copy of the GNU General Public License
|
85
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# Look in the tasks/setup.rb file for the various options that can be
|
2
|
+
# configured in this Rakefile. The .rake files in the tasks directory
|
3
|
+
# are where the options are used.
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'bones'
|
7
|
+
Bones.setup
|
8
|
+
rescue LoadError
|
9
|
+
begin
|
10
|
+
load 'tasks/setup.rb'
|
11
|
+
rescue LoadError
|
12
|
+
raise RuntimeError, '### please install the "bones" gem ###'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
ensure_in_path 'lib'
|
17
|
+
require 'sqrbl'
|
18
|
+
|
19
|
+
task :default => 'spec:run'
|
20
|
+
|
21
|
+
PROJ.rubyforge.name = 'sqrbl'
|
22
|
+
PROJ.name = 'sqrbl'
|
23
|
+
PROJ.version = Sqrbl::VERSION
|
24
|
+
PROJ.authors = 'Sam Livingston-Gray'
|
25
|
+
PROJ.email = 'geeksam@gmail.com'
|
26
|
+
PROJ.url = 'http://sqrbl.rubyforge.org'
|
27
|
+
PROJ.ignore_file = '.gitignore'
|
28
|
+
|
29
|
+
|
30
|
+
PROJ.spec.opts << '--color'
|
31
|
+
|
32
|
+
# EOF
|
data/TODO.txt
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
- bin/sqrbl should create an empty SQrbL file with the following simple template:
|
2
|
+
require 'rubygems'
|
3
|
+
require 'sqrbl'
|
4
|
+
Sqrbl.migration do
|
5
|
+
# (copy the rest from README.txt)
|
6
|
+
end
|
7
|
+
|
8
|
+
- Writers should create some sort of output to let you know that something happened.
|
9
|
+
|
10
|
+
- Define #helper(&block) as a simple wrapper to instance_eval on the same object. (Purpose: allow you to define helpers in a block that can be code-folded.)
|
11
|
+
|
data/bin/sqrbl
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# Swiped from Rails' ActiveSupport 2.3.2
|
2
|
+
|
3
|
+
class Object
|
4
|
+
# Returns +value+ after yielding +value+ to the block. This simplifies the
|
5
|
+
# process of constructing an object, performing work on the object, and then
|
6
|
+
# returning the object from a method. It is a Ruby-ized realization of the K
|
7
|
+
# combinator, courtesy of Mikael Brockman.
|
8
|
+
#
|
9
|
+
# ==== Examples
|
10
|
+
#
|
11
|
+
# # Without returning
|
12
|
+
# def foo
|
13
|
+
# values = []
|
14
|
+
# values << "bar"
|
15
|
+
# values << "baz"
|
16
|
+
# return values
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# foo # => ['bar', 'baz']
|
20
|
+
#
|
21
|
+
# # returning with a local variable
|
22
|
+
# def foo
|
23
|
+
# returning values = [] do
|
24
|
+
# values << 'bar'
|
25
|
+
# values << 'baz'
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# foo # => ['bar', 'baz']
|
30
|
+
#
|
31
|
+
# # returning with a block argument
|
32
|
+
# def foo
|
33
|
+
# returning [] do |values|
|
34
|
+
# values << 'bar'
|
35
|
+
# values << 'baz'
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# foo # => ['bar', 'baz']
|
40
|
+
def returning(value)
|
41
|
+
yield(value)
|
42
|
+
value
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Swiped from Rails' ActiveSupport 2.3.2
|
2
|
+
|
3
|
+
unless :to_proc.respond_to?(:to_proc)
|
4
|
+
class Symbol
|
5
|
+
# Turns the symbol into a simple proc, which is especially useful for enumerations. Examples:
|
6
|
+
#
|
7
|
+
# # The same as people.collect { |p| p.name }
|
8
|
+
# people.collect(&:name)
|
9
|
+
#
|
10
|
+
# # The same as people.select { |p| p.manager? }.collect { |p| p.salary }
|
11
|
+
# people.select(&:manager?).collect(&:salary)
|
12
|
+
def to_proc
|
13
|
+
Proc.new { |*args| args.shift.__send__(self, *args) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Sqrbl
|
4
|
+
# Base class for other migration writers.
|
5
|
+
class BaseMigrationWriter
|
6
|
+
attr_accessor :migration, :output_directory
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# List all classes that inherit from this one
|
10
|
+
def subclasses
|
11
|
+
@@subclasses ||= []
|
12
|
+
end
|
13
|
+
|
14
|
+
def inherited(subclass) #:nodoc:
|
15
|
+
subclasses << subclass
|
16
|
+
end
|
17
|
+
|
18
|
+
# Convenience method: create a new instance and invoke <tt>write!</tt> on it.
|
19
|
+
def write_migration!(migration)
|
20
|
+
new(migration).write!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(migration) #:nodoc:
|
25
|
+
@migration = migration
|
26
|
+
set_default_output_dir!
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
# Set the output_directory attribute based on the output_directory or creating_file
|
31
|
+
# attributes of the given migration, in that order
|
32
|
+
def set_default_output_dir!
|
33
|
+
raise "No migration given!" unless migration
|
34
|
+
self.output_directory ||= migration.output_directory
|
35
|
+
self.output_directory ||= migration.creating_file && File.join(File.expand_path(File.dirname(migration.creating_file)), 'sql')
|
36
|
+
raise "Unable to determine output directory!" unless self.output_directory
|
37
|
+
end
|
38
|
+
|
39
|
+
# Make sure that the given directory exists
|
40
|
+
def ensure_dir_exists(dirname)
|
41
|
+
raise ArgumentError.new("dirname cannot be nil!") if dirname.nil?
|
42
|
+
dirname = File.expand_path(dirname)
|
43
|
+
return if File.directory?(dirname)
|
44
|
+
FileUtils.makedirs(dirname)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Open +filename+ and write +contents+ to it
|
48
|
+
def write_file(filename, contents)
|
49
|
+
File.open(filename, 'w') { |f| f << contents }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Sqrbl
|
2
|
+
# A small set of tools for working with the call stack.
|
3
|
+
module CallStack
|
4
|
+
module_function
|
5
|
+
|
6
|
+
# Find the first item on the call stack that isn't in the Sqrbl library path
|
7
|
+
def first_non_sqrbl_caller(call_stack = caller)
|
8
|
+
call_stack.detect { |call| ! call.include?(Sqrbl::LIBPATH) }
|
9
|
+
end
|
10
|
+
|
11
|
+
# For a given entry in the call stack, get the file
|
12
|
+
def caller_file(caller_item)
|
13
|
+
caller_item.split(':')[0]
|
14
|
+
end
|
15
|
+
|
16
|
+
# For a given entry in the call stack, get the line number
|
17
|
+
def caller_line(caller_item)
|
18
|
+
caller_item.split(':')[1]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/sqrbl/group.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# This file is part of SQrbL, which is free software licensed under v3 of the GNU GPL.
|
2
|
+
# For more information, please see README.txt and LICENSE.txt in the project's root directory.
|
3
|
+
|
4
|
+
|
5
|
+
module Sqrbl
|
6
|
+
# Like the Migration class, Group doesn't do much on its own.
|
7
|
+
# It's basically a container for a list of StepPair objects, which are created using #step.
|
8
|
+
#
|
9
|
+
# Group delegates +method_missing+ calls to its +migration+ object.
|
10
|
+
# For more information, see MethodMissingDelegation.
|
11
|
+
class Group
|
12
|
+
attr_reader :migration, :description, :block, :steps
|
13
|
+
|
14
|
+
include Sqrbl::ExpectsBlockWithNew
|
15
|
+
include Sqrbl::MethodMissingDelegation
|
16
|
+
delegate_method_missing_to :migration
|
17
|
+
include HasTodos
|
18
|
+
|
19
|
+
def initialize(migration, description, options = {}, &block)
|
20
|
+
@migration = migration
|
21
|
+
@description = description
|
22
|
+
@block = lambda(&block)
|
23
|
+
@steps = []
|
24
|
+
|
25
|
+
eval_block_on_initialize(options)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Creates a StepPair object, passing it the step_description and block arguments.
|
29
|
+
def step(step_description, &block)
|
30
|
+
steps << StepPair.new(self, step_description, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
# A Group is valid if it contains at least one StepPair object, and all of those objects are themselves valid.
|
34
|
+
def valid?
|
35
|
+
!steps.empty? && steps.all? { |step| step.kind_of?(StepPair) && step.valid? }
|
36
|
+
end
|
37
|
+
|
38
|
+
def unix_name
|
39
|
+
Sqrbl.calculate_unix_name(description)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Sqrbl
|
2
|
+
# Writes one [numbered] file per migration step, in two main directories: sql/up/ and sql/down/.
|
3
|
+
# Migration steps are contained in a [numbered] subfolder that corresponds to the group.
|
4
|
+
class IndividualMigrationWriter < BaseMigrationWriter
|
5
|
+
# Prepare the up and down directories and populate them with individual files.
|
6
|
+
#
|
7
|
+
# WARNING: <b>RECURSIVELY DELETES ALL FILES</b> in output_directory/up and output_directory/down unless passed :safe_mode => true in the options hash!
|
8
|
+
def write!(options = {})
|
9
|
+
ensure_dir_exists(output_directory)
|
10
|
+
clear_dirs!(options)
|
11
|
+
write_individual_files!
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
# Recursively delete all files in output_directory/up and output_directory/down unless passed :safe_mode => true in the options hash.
|
16
|
+
def clear_dirs!(options)
|
17
|
+
return if options[:safe_mode]
|
18
|
+
base_dir = File.expand_path(output_directory)
|
19
|
+
FileUtils.rm_rf(File.join(base_dir, 'up'))
|
20
|
+
FileUtils.rm_rf(File.join(base_dir, 'down'))
|
21
|
+
end
|
22
|
+
|
23
|
+
# Write out the contents of the individual files, each in its group's subfolder
|
24
|
+
def write_individual_files!
|
25
|
+
migration.groups.each_with_index do |group, idx|
|
26
|
+
group_dir = "%0#{group_num_width}d_%s" % [idx + 1, group.unix_name]
|
27
|
+
up_subdir = File.join(output_directory, 'up', group_dir)
|
28
|
+
down_subdir = File.join(output_directory, 'down', group_dir)
|
29
|
+
ensure_dir_exists(up_subdir)
|
30
|
+
ensure_dir_exists(down_subdir)
|
31
|
+
|
32
|
+
group.steps.each_with_index do |step, idx|
|
33
|
+
step_filename = "%0#{step_num_width(step)}d_%s.sql" % [idx + 1, step.unix_name]
|
34
|
+
write_file(File.join(up_subdir, step_filename), step.up_step.output )
|
35
|
+
write_file(File.join(down_subdir, step_filename), step.down_step.output)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def group_num_width # :nodoc:
|
41
|
+
migration.groups.length.to_s.length
|
42
|
+
end
|
43
|
+
|
44
|
+
def step_num_width(step) # :nodoc:
|
45
|
+
step.group.steps.length.to_s.length
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# This file is part of SQrbL, which is free software licensed under v3 of the GNU GPL.
|
2
|
+
# For more information, please see README.txt and LICENSE.txt in the project's root directory.
|
3
|
+
|
4
|
+
|
5
|
+
module Sqrbl
|
6
|
+
|
7
|
+
# The Migration class doesn't do much on its own; it's basically just a
|
8
|
+
# container for a list of Group objects, which are created using #group.
|
9
|
+
#
|
10
|
+
# Note also that because Group includes the MethodMissingDelegation mixin,
|
11
|
+
# instance methods defined in the block given to Migration.build will become
|
12
|
+
# available to all groups (and their related objects) that belong to a
|
13
|
+
# Migration instance. For more information, see MethodMissingDelegation.
|
14
|
+
class Migration
|
15
|
+
attr_reader :groups, :creating_file, :output_directory
|
16
|
+
|
17
|
+
include HasTodos
|
18
|
+
|
19
|
+
# Creates a new Migration object and evaluates the block in its binding.
|
20
|
+
# As a result, any methods called within the block will affect the state
|
21
|
+
# of that object (and that object only).
|
22
|
+
#
|
23
|
+
# <i>(Eventually, this will also pass the migration to a helper object that
|
24
|
+
# will create the tree of output *.sql files.)</i>
|
25
|
+
def self.build(&block)
|
26
|
+
returning(self.new) do |migration|
|
27
|
+
migration.instance_eval(&block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize # :nodoc:
|
32
|
+
@groups = []
|
33
|
+
creating_caller = CallStack.first_non_sqrbl_caller
|
34
|
+
@creating_file = File.expand_path(CallStack.caller_file(creating_caller))
|
35
|
+
end
|
36
|
+
|
37
|
+
# Convenience method: set the +output_directory+ attribute
|
38
|
+
def set_output_directory(dirname)
|
39
|
+
self.output_directory = File.expand_path(dirname)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Creates a Group object, passing it the name and block arguments.
|
43
|
+
def group(name, &block)
|
44
|
+
groups << Group.new(self, name, &block)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Convenience method: iterate all StepPair objects in the migration
|
48
|
+
def step_pairs
|
49
|
+
groups.map(&:steps).flatten
|
50
|
+
end
|
51
|
+
|
52
|
+
# Convenience method: iterate all 'up' steps in the migration
|
53
|
+
def up_steps
|
54
|
+
step_pairs.map(&:up_step)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Convenience method: iterate all 'down' steps in the migration
|
58
|
+
def down_steps
|
59
|
+
step_pairs.map(&:down_step)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# This file is part of SQrbL, which is free software licensed under v3 of the GNU GPL.
|
2
|
+
# For more information, please see README.txt and LICENSE.txt in the project's root directory.
|
3
|
+
|
4
|
+
|
5
|
+
module Sqrbl
|
6
|
+
# This module primarily exists to help with testing.
|
7
|
+
# It encapsulates the common pattern of a method on one object
|
8
|
+
# that takes a block, creates a new object, and passes the block
|
9
|
+
# to the new object.
|
10
|
+
#
|
11
|
+
# In normal use, the new object should then immediately instance_eval
|
12
|
+
# the block; however, for testing, it is sometimes useful to delay
|
13
|
+
# block evaluation until later so we can set up mocking beforehand.
|
14
|
+
#
|
15
|
+
# (Search the /spec directory for "skip_block_evaluation" for examples.)
|
16
|
+
module ExpectsBlockWithNew
|
17
|
+
protected
|
18
|
+
|
19
|
+
def eval_block_on_initialize(options = {})
|
20
|
+
evaluate_block! unless options[:skip_block_evaluation]
|
21
|
+
end
|
22
|
+
|
23
|
+
def evaluate_block!
|
24
|
+
instance_eval(&block) if block
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# This file is part of SQrbL, which is free software licensed under v3 of the GNU GPL.
|
2
|
+
# For more information, please see README.txt and LICENSE.txt in the project's root directory.
|
3
|
+
|
4
|
+
|
5
|
+
module Sqrbl
|
6
|
+
|
7
|
+
module HasTodos
|
8
|
+
class Todo
|
9
|
+
attr_accessor :message, :call_stack, :type
|
10
|
+
def initialize(message, call_stack, type)
|
11
|
+
@message = message
|
12
|
+
@call_stack = call_stack
|
13
|
+
@type = type
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the first item from the call stack that isn't inside the Sqrbl library.
|
17
|
+
# This lets us output a pointer back to the place where this instance was created.
|
18
|
+
def location
|
19
|
+
@location ||= call_stack.detect { |call| ! call.include?(Sqrbl::LIBPATH) }
|
20
|
+
end
|
21
|
+
|
22
|
+
# Return just the line number from +location+.
|
23
|
+
def calling_line
|
24
|
+
CallStack.caller_line(location)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Return just the filename from +location+.
|
28
|
+
def creating_file
|
29
|
+
CallStack.caller_file(location)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Is this Todo of type <tt>:todo</tt>?
|
33
|
+
def todo?
|
34
|
+
type == :todo
|
35
|
+
end
|
36
|
+
|
37
|
+
# Is this Todo of type <tt>:warning</tt>?
|
38
|
+
def warning?
|
39
|
+
type == :warning
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Return the list of Todo items.
|
44
|
+
def todos
|
45
|
+
@todos ||= []
|
46
|
+
end
|
47
|
+
|
48
|
+
# Create a new Todo item (of type <tt>:todo</tt>) and add it to +todos+.
|
49
|
+
def todo(message)
|
50
|
+
add_todo(message, caller, :todo)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Create a new Todo item (of type <tt>:warning</tt>) and add it to +todos+.
|
54
|
+
def warning(message)
|
55
|
+
add_todo(message, caller, :warning)
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
def add_todo(message, caller, type)
|
60
|
+
returning Todo.new(message, caller, type) do |new_todo|
|
61
|
+
todos << new_todo
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# This file is part of SQrbL, which is free software licensed under v3 of the GNU GPL.
|
2
|
+
# For more information, please see README.txt and LICENSE.txt in the project's root directory.
|
3
|
+
|
4
|
+
|
5
|
+
module Sqrbl
|
6
|
+
# When used as directed, Sqrbl will create a graph of objects with bidirectional
|
7
|
+
# references to one another. The graph will have the following general shape
|
8
|
+
# (where --> indicates a relationship that is generally one-to-many):
|
9
|
+
#
|
10
|
+
# Migration --> Group --> StepPair --> Step
|
11
|
+
#
|
12
|
+
# Also, because these various objects are created with blocks that are
|
13
|
+
# instance_eval'd, those blocks can define helper methods that will be
|
14
|
+
# added to the appropriate objects in the graph.
|
15
|
+
#
|
16
|
+
# Taken together, these two features (back-references and instance_eval)
|
17
|
+
# let us provide a useful facility for defining helper methods inside a
|
18
|
+
# block, and accessing them using scoping rules that work in an intuitive
|
19
|
+
# manner. For example, if a Migration defines a method #foo, which is
|
20
|
+
# later called from inside a Step's block, the Step can catch that call
|
21
|
+
# using method_missing and delegate it to its StepPair, which in turn
|
22
|
+
# will delegate to its Group, which in turn will delegate to its Migration.
|
23
|
+
#
|
24
|
+
# Confused yet? Here's a slightly modified version of the example in README.txt:
|
25
|
+
#
|
26
|
+
# Sqrbl.migration "Convert from old widgets to new widgets" do
|
27
|
+
# def new_widget_insert()
|
28
|
+
# insert_into("new_widgets", {
|
29
|
+
# :name => 'widget_name',
|
30
|
+
# :part_num => 'CONCAT("X_", part_number)',
|
31
|
+
# :note => '"Imported from old_widgets"',
|
32
|
+
# })
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# group "Widgets" do
|
36
|
+
# step "Create widgets" do
|
37
|
+
# up do
|
38
|
+
# action "Migrate old_widgets" {
|
39
|
+
# "#{new_widget_insert()} FROM old_widgets"
|
40
|
+
# }
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# down do
|
44
|
+
# action "Drop imported organizational contacts" {
|
45
|
+
# 'DELETE FROM new_widgets WHERE note LIKE "Imported from old_widgets"'
|
46
|
+
# }
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# Note that the call to new_widget_insert occurs several layers of
|
53
|
+
# nesting down from its definition. By using this method_missing
|
54
|
+
# delegation strategy, we can effectively hide Sqrbl's object model
|
55
|
+
# from the user and provide something that "just works."
|
56
|
+
#
|
57
|
+
# (Without this strategy, the above example would need to define
|
58
|
+
# new_widget_insert as a lambda function that gets invoked using
|
59
|
+
# either #call or the square-bracket syntax, but I find that more
|
60
|
+
# awkward -- hence this wee bit of metaprogramming.)
|
61
|
+
module MethodMissingDelegation
|
62
|
+
def self.included(receiver)
|
63
|
+
receiver.class_eval(<<-EOF, __FILE__, __LINE__)
|
64
|
+
@@mm_delegate_accessor = nil
|
65
|
+
|
66
|
+
# Defines the accessor method that instances of this class should use
|
67
|
+
# when delegating +method_missing+ calls.
|
68
|
+
def self.delegate_method_missing_to(accessor)
|
69
|
+
@@mm_delegate_accessor = accessor
|
70
|
+
end
|
71
|
+
|
72
|
+
# If +delegate_method_missing_to+ was called on the class,
|
73
|
+
# use the accessor defined there to find a delegate and
|
74
|
+
# pass the unknown method to it.
|
75
|
+
def method_missing(method, *args, &block)
|
76
|
+
return super unless defined?(@@mm_delegate_accessor) && !@@mm_delegate_accessor.nil?
|
77
|
+
delegate = self.send(@@mm_delegate_accessor)
|
78
|
+
delegate.send(method, *args, &block)
|
79
|
+
end
|
80
|
+
EOF
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|