polymorpheus 0.2 → 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +145 -0
- data/lib/polymorpheus.rb +2 -0
- data/lib/polymorpheus/mysql_adapter.rb +107 -51
- data/lib/polymorpheus/railtie.rb +4 -0
- data/lib/polymorpheus/schema_dumper.rb +19 -0
- data/lib/polymorpheus/trigger.rb +29 -0
- data/lib/polymorpheus/version.rb +1 -1
- data/spec/mysql2_adapter_spec.rb +178 -14
- data/spec/schema_dumper_spec.rb +48 -0
- data/spec/sql_logger.rb +16 -3
- data/spec/trigger_spec.rb +46 -0
- metadata +11 -4
data/README.md
CHANGED
@@ -0,0 +1,145 @@
|
|
1
|
+
# Polymorpheus
|
2
|
+
**Polymorphic relationships in Rails that keep your database happy with almost no setup**
|
3
|
+
|
4
|
+
### Background
|
5
|
+
* **What is polymorphism?** [Rails Guides has a great overview of what polymorphic relationships are and how Rails handles them](http://guides.rubyonrails.org/association_basics.html#polymorphic-associations)
|
6
|
+
|
7
|
+
* **If you don't think database constraints are important** then [here is a presentation that might change your mind](http://bostonrb.org/presentations/databases-constraints-polymorphism). If you're still not convinced, this gem won't be relevant to you.
|
8
|
+
|
9
|
+
* **What's wrong with Rails' built-in approach to polymorphism?** Using Rails, polymorphism is implemented in the database using a `type` column and an `id` column, where the `id` column references one of multiple other tables, depending on the `type`. This violates the basic principle that one column in a database should mean to one thing, and it prevents us from setting up any sort of database constraint on the `id` column.
|
10
|
+
|
11
|
+
|
12
|
+
## Basic Use
|
13
|
+
|
14
|
+
We'll outline the use case to mirror the example [outline in the Rails Guides](http://guides.rubyonrails.org/association_basics.html#polymorphic-associations):
|
15
|
+
|
16
|
+
* You have a `Picture` object that can belong to an `Imageable`, where an `Imageable` is a polymorphic representation of either an `Employee` or a `Product`.
|
17
|
+
|
18
|
+
With Polymorpheus, you would define this relationship as follows:
|
19
|
+
|
20
|
+
**Database migration**
|
21
|
+
|
22
|
+
```
|
23
|
+
class SetUpPicturesTable < ActiveRecord::Migration
|
24
|
+
def self.up
|
25
|
+
create_table :pictures do |t|
|
26
|
+
t.integer :employee_id
|
27
|
+
t.integer :product_id
|
28
|
+
end
|
29
|
+
|
30
|
+
add_polymorphic_constraints 'pictures',
|
31
|
+
{ 'employee_id' => 'employees.id',
|
32
|
+
'product_id' => 'products.id' }
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.down
|
36
|
+
remove_polymorphic_constraints 'pictures',
|
37
|
+
{ 'employee_id' => 'employees.id',
|
38
|
+
'product_id' => 'products.id' }
|
39
|
+
|
40
|
+
drop_table :pictures
|
41
|
+
end
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
**ActiveRecord model definitions**
|
46
|
+
|
47
|
+
```
|
48
|
+
class Picture < ActiveRecord::Base
|
49
|
+
belongs_to_polymorphic :employee, :product, :as => :imageable
|
50
|
+
validates_polymorphic :imageable
|
51
|
+
end
|
52
|
+
|
53
|
+
class Employee < ActiveRecord::Base
|
54
|
+
has_many :pictures
|
55
|
+
end
|
56
|
+
|
57
|
+
class Product < ActiveRecord::Base
|
58
|
+
has_many :pictures
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
That's it!
|
63
|
+
|
64
|
+
Now let's review what we've done.
|
65
|
+
|
66
|
+
|
67
|
+
## Database Migration
|
68
|
+
|
69
|
+
* Instead of `imageable_type` and `imageable_id` columns in the pictures table, we've created explicit columns for the `employee_id` and `product_id`
|
70
|
+
* The `add_polymorphic_constraints` call takes care of all of the database constraints you need, without you needing to worry about sql! Specifically it:
|
71
|
+
* Creates foreign key relationships in the database as specified. So in this example, we have specified that the `employee_id` column in the `pictures` table should have a foreign key constraint with the `id` column of the `employees` table.
|
72
|
+
* Creates appropriate triggers in our database that make sure that exactly on or the other of `employee_id` or `poduct_id` are specified for a given record. An exception will be raised if you try to save a database record that contains both or none of them.
|
73
|
+
* **Options for migrations**: There are options to add uniqueness constraints, customize the foreign keys generated by Polymorpheus, and specify the name of generated database indexes. For more info on this, [read the wiki entry](https://github.com/wegowise/polymorpheus/wiki/Migration-options).
|
74
|
+
|
75
|
+
## Model definitions
|
76
|
+
|
77
|
+
* The `belongs_to_polymorphic` declaration in the `Picture` class specifies the polymorphic relationship. It provides all of the same methods that Rails does for it's built-in polymorphic relationships, plus a couple additional features. See the Interface section below.
|
78
|
+
* `validates_polymorph` declaration: checks that exactly one of the possible polymorphic relationships is specified. In this example, either an `employee_id` or `product_id` must be specified -- if both are nil or if both are non-nil a validation error will be added to the object.
|
79
|
+
* The `has_many` declarations are just normal Rails declarations.
|
80
|
+
|
81
|
+
|
82
|
+
## Requirements / Support
|
83
|
+
|
84
|
+
* Currently the gem only supports MySQL. Postgres support is planned; please feel free to fork and submit a (well-tested) pull request if you want to add Postgres support before then.
|
85
|
+
* This gem is tested and has been tested for Rails 2.3.8, 3.0.x, 3.1.x and 3.2.x
|
86
|
+
* For Rails 3.1+, you'll still need to use `up` and `down` methods in your migrations.
|
87
|
+
|
88
|
+
## Interface
|
89
|
+
|
90
|
+
The nice thing about Polymorpheus is that under the hood it builds on top of the Rails conventions you're already used to which means that you can interface with your polymorphic relationships in simple, familiar ways. There are also a number of helpful interface helpers that you can use.
|
91
|
+
|
92
|
+
Let's use the example above to illustrate.
|
93
|
+
|
94
|
+
```
|
95
|
+
sam = Employee.create(name: 'Sam')
|
96
|
+
nintendo = Product.create(name: 'Nintendo')
|
97
|
+
|
98
|
+
pic = Picture.new
|
99
|
+
=> #<Picture id: nil, employee_id: nil, product_id: nil>
|
100
|
+
|
101
|
+
pic.imageable
|
102
|
+
=> nil
|
103
|
+
|
104
|
+
# the following two options are equivalent, just as they are normally with ActiveRecord:
|
105
|
+
# pic.employee = sam
|
106
|
+
# pic.employee_id = sam.id
|
107
|
+
|
108
|
+
# if we specify an employee, the imageable getter method will return that employee:
|
109
|
+
pic.employee = sam;
|
110
|
+
pic.imageable
|
111
|
+
=> #<Employee id: 1, name: "Sam">
|
112
|
+
pic.employee
|
113
|
+
=> #<Employee id: 1, name: "Sam">
|
114
|
+
pic.product
|
115
|
+
=> nil
|
116
|
+
|
117
|
+
# if we specify a product, the imageable getting will return that product:
|
118
|
+
Picture.new(product: nintendo).imageable
|
119
|
+
=> #<Product id: 1, name: "Nintendo">
|
120
|
+
|
121
|
+
# but if we specify an employee and a product, the getter will know this makes no sense and return nil for the imageable:
|
122
|
+
Picture.new(employee: sam, product: nintendo).imageable
|
123
|
+
=> nil
|
124
|
+
|
125
|
+
# There are some useful helper methods:
|
126
|
+
|
127
|
+
pic.imageable_active_key
|
128
|
+
=> "employee_id"
|
129
|
+
|
130
|
+
pic.imageable_query_condition
|
131
|
+
=> {"employee_id"=>"1"}
|
132
|
+
|
133
|
+
pic.imageable_types
|
134
|
+
=> ["employee", "picture"]
|
135
|
+
|
136
|
+
Picture::IMAGEABLE_KEYS
|
137
|
+
=> ["employee_id", "picture_id"]
|
138
|
+
```
|
139
|
+
|
140
|
+
## Credits and License
|
141
|
+
|
142
|
+
* This gem was written by [Barun Singh](https://github.com/barunio)
|
143
|
+
* It uses the [Foreigner gem](https://github.com/matthuhiggins/foreigner) under the hood for a few things
|
144
|
+
|
145
|
+
polymorpheus is Copyright © 2011-2012 Barun Singh and [WegoWise](http://wegowise.com). It is free software, and may be redistributed under the terms specified in the LICENSE file.
|
data/lib/polymorpheus.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
module Polymorpheus
|
2
2
|
autoload :Adapter, 'polymorpheus/adapter'
|
3
3
|
autoload :Interface, 'polymorpheus/interface'
|
4
|
+
autoload :Trigger, 'polymorpheus/trigger'
|
5
|
+
autoload :SchemaDumper, 'polymorpheus/schema_dumper'
|
4
6
|
|
5
7
|
module ConnectionAdapters
|
6
8
|
autoload :SchemaStatements, 'polymorpheus/schema_statements'
|
@@ -5,75 +5,115 @@ module Polymorpheus
|
|
5
5
|
INSERT = 'INSERT'
|
6
6
|
UPDATE = 'UPDATE'
|
7
7
|
|
8
|
-
#
|
9
|
-
# have a polymorphic database constraint such that dog_id references the "id" column of the
|
10
|
-
# dogs table, and kitty_id references the "name" column of the "cats" table. then my inputs
|
11
|
-
# to this method would be:
|
12
|
-
# table: 'pets'
|
13
|
-
# columns: { 'dog_id' => 'dogs.id', 'kitty_id' => 'cats.name' }
|
8
|
+
# See the README for explanations regarding the use of these methods
|
14
9
|
#
|
15
|
-
#
|
10
|
+
# table: a string equal to the name of the db table
|
16
11
|
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
12
|
+
# columns: a hash, with keys equal to the column names in the table we
|
13
|
+
# are operating on, and values indicating the foreign key
|
14
|
+
# association through the form "table.column". so,
|
15
|
+
# { 'employee_id' => 'employees.ssn',
|
16
|
+
# 'product_id' => 'products.id' }
|
17
|
+
# indicates that the `employee_id` column in `table` should have
|
18
|
+
# a foreign key constraint connecting it to the `ssn` column
|
19
|
+
# in the `employees` table, and the `product_id` column should
|
20
|
+
# have a foreign key constraint with the `id` column in the
|
21
|
+
# `products` table
|
22
|
+
#
|
23
|
+
# options: a hash, corrently only accepts one option that allows us to
|
24
|
+
# add an additional uniqueness constraint.
|
25
|
+
# if the columns hash was specified as above, and we supplied
|
26
|
+
# options of
|
27
|
+
# { :unique => true }
|
28
|
+
# then this would create a uniqueness constraint in the database
|
29
|
+
# that would ensure that any given employee_id could only be in
|
30
|
+
# the table once, and that any given product_id could only be in
|
31
|
+
# the table once.
|
32
|
+
#
|
33
|
+
# alternatively, the user can also supply a column name or array
|
34
|
+
# of column names to the :unique option:
|
35
|
+
# { :unique => 'picture_url' }
|
36
|
+
# This would allow an employee_id (or product_id) to appear
|
37
|
+
# multiple times in the table, but no two employee ids would
|
38
|
+
# be able to have the same picture_url.
|
39
|
+
|
40
|
+
def add_polymorphic_constraints(table, columns, options={})
|
41
|
+
column_names = columns.keys.sort
|
42
|
+
add_polymorphic_triggers(table, column_names)
|
25
43
|
options.symbolize_keys!
|
26
|
-
index_suffix = options[:index_suffix]
|
27
44
|
if options[:unique].present?
|
28
|
-
poly_create_indexes(table,
|
45
|
+
poly_create_indexes(table, column_names, Array(options[:unique]))
|
29
46
|
end
|
30
|
-
|
31
|
-
ref_table, ref_col =
|
32
|
-
add_foreign_key table, ref_table,
|
47
|
+
column_names.each do |col_name|
|
48
|
+
ref_table, ref_col = columns[col_name].to_s.split('.')
|
49
|
+
add_foreign_key table, ref_table,
|
50
|
+
:column => col_name,
|
51
|
+
:primary_key => (ref_col || 'id')
|
33
52
|
end
|
34
53
|
end
|
35
54
|
|
36
55
|
def remove_polymorphic_constraints(table, columns, options = {})
|
37
|
-
poly_drop_triggers(table)
|
38
|
-
index_suffix = options[:index_suffix]
|
56
|
+
poly_drop_triggers(table, columns.keys.sort)
|
39
57
|
columns.each do |(col, reference)|
|
40
58
|
ref_table, ref_col = reference.to_s.split('.')
|
41
59
|
remove_foreign_key table, ref_table
|
42
60
|
end
|
43
61
|
if options[:unique].present?
|
44
|
-
poly_remove_indexes(table, columns.keys, Array(options[:unique])
|
62
|
+
poly_remove_indexes(table, columns.keys, Array(options[:unique]))
|
45
63
|
end
|
46
64
|
end
|
47
65
|
|
66
|
+
def triggers
|
67
|
+
execute("show triggers").collect {|t| Trigger.new(t) }
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# DO NOT USE THIS METHOD DIRECTLY
|
72
|
+
#
|
73
|
+
# it will not create the foreign key relationships you want. the only
|
74
|
+
# reason it is here is because it is used by the schema dumper, since
|
75
|
+
# the schema dump will contains separate statements for foreign keys,
|
76
|
+
# and we don't want to duplicate those
|
77
|
+
def add_polymorphic_triggers(table, column_names)
|
78
|
+
column_names.sort!
|
79
|
+
poly_drop_triggers(table, column_names)
|
80
|
+
poly_create_triggers(table, column_names)
|
81
|
+
end
|
82
|
+
|
48
83
|
|
49
84
|
##########################################################################
|
50
85
|
private
|
51
86
|
|
52
|
-
def poly_trigger_name(table, action)
|
53
|
-
"#{
|
87
|
+
def poly_trigger_name(table, action, columns)
|
88
|
+
prefix = "pfk#{action.first}_#{table}_".downcase
|
89
|
+
generate_name prefix, columns.sort
|
54
90
|
end
|
55
91
|
|
56
|
-
def poly_drop_trigger(table, action)
|
57
|
-
|
92
|
+
def poly_drop_trigger(table, action, columns)
|
93
|
+
trigger_name = poly_trigger_name(table, action, columns)
|
94
|
+
execute %{DROP TRIGGER IF EXISTS #{trigger_name}}
|
58
95
|
end
|
59
96
|
|
60
97
|
def poly_create_trigger(table, action, columns)
|
61
|
-
|
62
|
-
"FOR EACH ROW\n" +
|
63
|
-
"BEGIN\n"
|
98
|
+
trigger_name = poly_trigger_name(table, action, columns)
|
64
99
|
colchecks = columns.collect { |col| "IF(NEW.#{col} IS NULL, 0, 1)" }.
|
65
100
|
join(' + ')
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
101
|
+
|
102
|
+
sql = %{
|
103
|
+
CREATE TRIGGER #{trigger_name} BEFORE #{action} ON #{table}
|
104
|
+
FOR EACH ROW
|
105
|
+
BEGIN
|
106
|
+
IF(#{colchecks}) <> 1 THEN
|
107
|
+
SET NEW = 'Error';
|
108
|
+
END IF;
|
109
|
+
END}
|
70
110
|
|
71
111
|
execute sql
|
72
112
|
end
|
73
113
|
|
74
|
-
def poly_drop_triggers(table)
|
75
|
-
poly_drop_trigger(table, 'INSERT')
|
76
|
-
poly_drop_trigger(table, 'UPDATE')
|
114
|
+
def poly_drop_triggers(table, columns)
|
115
|
+
poly_drop_trigger(table, 'INSERT', columns)
|
116
|
+
poly_drop_trigger(table, 'UPDATE', columns)
|
77
117
|
end
|
78
118
|
|
79
119
|
def poly_create_triggers(table, columns)
|
@@ -81,37 +121,53 @@ module Polymorpheus
|
|
81
121
|
poly_create_trigger(table, 'UPDATE', columns)
|
82
122
|
end
|
83
123
|
|
84
|
-
def poly_create_index(table, column, unique_cols
|
85
|
-
unique_cols
|
86
|
-
|
124
|
+
def poly_create_index(table, column, unique_cols)
|
125
|
+
if unique_cols == [true]
|
126
|
+
unique_cols = [column]
|
127
|
+
else
|
128
|
+
unique_cols = [column] + unique_cols
|
129
|
+
end
|
130
|
+
name = poly_index_name(table, unique_cols)
|
87
131
|
execute %{
|
88
|
-
CREATE UNIQUE INDEX #{name} ON #{table} (#{
|
132
|
+
CREATE UNIQUE INDEX #{name} ON #{table} (#{unique_cols.join(', ')})
|
89
133
|
}
|
90
134
|
end
|
91
135
|
|
92
|
-
def poly_remove_index(table, column, unique_cols
|
93
|
-
unique_cols
|
94
|
-
|
136
|
+
def poly_remove_index(table, column, unique_cols)
|
137
|
+
if unique_cols == [true]
|
138
|
+
unique_cols = [column]
|
139
|
+
else
|
140
|
+
unique_cols = [column] + unique_cols
|
141
|
+
end
|
142
|
+
name = poly_index_name(table, unique_cols)
|
95
143
|
execute %{ DROP INDEX #{name} ON #{table} }
|
96
144
|
end
|
97
145
|
|
98
|
-
def poly_index_name(table,
|
99
|
-
|
100
|
-
|
146
|
+
def poly_index_name(table, columns)
|
147
|
+
prefix = "pfk_#{table}_"
|
148
|
+
generate_name prefix, columns
|
101
149
|
end
|
102
150
|
|
103
|
-
def poly_create_indexes(table, columns, unique_cols
|
151
|
+
def poly_create_indexes(table, columns, unique_cols)
|
104
152
|
columns.each do |column|
|
105
|
-
poly_create_index(table, column, unique_cols
|
153
|
+
poly_create_index(table, column, unique_cols)
|
106
154
|
end
|
107
155
|
end
|
108
156
|
|
109
|
-
def poly_remove_indexes(table, columns, unique_cols
|
157
|
+
def poly_remove_indexes(table, columns, unique_cols)
|
110
158
|
columns.each do |column|
|
111
|
-
poly_remove_index(table, column, unique_cols
|
159
|
+
poly_remove_index(table, column, unique_cols)
|
112
160
|
end
|
113
161
|
end
|
114
162
|
|
163
|
+
def generate_name(prefix, columns)
|
164
|
+
# names can be at most 64 characters long
|
165
|
+
col_length = (64 - prefix.length) / columns.length
|
166
|
+
|
167
|
+
prefix +
|
168
|
+
columns.map { |c| c.to_s.gsub('_','').first(col_length-1) }.join('_')
|
169
|
+
end
|
170
|
+
|
115
171
|
end
|
116
172
|
end
|
117
173
|
end
|
data/lib/polymorpheus/railtie.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Polymorpheus
|
2
|
+
module SchemaDumper
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
alias_method_chain :tables, :triggers
|
7
|
+
end
|
8
|
+
|
9
|
+
def tables_with_triggers(stream)
|
10
|
+
tables_without_triggers(stream)
|
11
|
+
|
12
|
+
@connection.triggers.collect(&:schema_statement).each do |statement|
|
13
|
+
stream.puts statement
|
14
|
+
end
|
15
|
+
stream.puts
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Trigger
|
2
|
+
|
3
|
+
attr_accessor :name, :event, :table, :statement, :timing, :created, :sql_mode,
|
4
|
+
:definer, :charset, :collation_connection, :db_collation
|
5
|
+
|
6
|
+
def initialize(arr)
|
7
|
+
raise ArgumentError unless arr.is_a?(Array) && arr.length == 11
|
8
|
+
[:name, :event, :table, :statement, :timing, :created, :sql_mode,
|
9
|
+
:definer, :charset, :collation_connection, :db_collation].
|
10
|
+
each_with_index do |attr, ind|
|
11
|
+
self.send("#{attr}=", arr[ind])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def columns
|
16
|
+
/IF\((.*)\) \<\> 1/.match(self.statement) do |match|
|
17
|
+
match[1].split(' + ').collect do |submatch|
|
18
|
+
/NEW\.([^ ]*)/.match(submatch)[1]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def schema_statement
|
24
|
+
# note that we don't need to worry about unique indices or foreign keys
|
25
|
+
# because separate schema statements will be generated for them
|
26
|
+
"add_polymorphic_triggers(#{table}, #{columns})"
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
data/lib/polymorpheus/version.rb
CHANGED
data/spec/mysql2_adapter_spec.rb
CHANGED
@@ -4,49 +4,213 @@ require 'sql_logger'
|
|
4
4
|
require 'foreigner'
|
5
5
|
require 'foreigner/connection_adapters/mysql2_adapter'
|
6
6
|
require 'polymorpheus'
|
7
|
+
require 'polymorpheus/trigger'
|
7
8
|
|
8
9
|
Polymorpheus::Adapter.load!
|
9
10
|
|
10
|
-
describe
|
11
|
-
|
12
|
-
|
11
|
+
describe Polymorpheus::ConnectionAdapters::MysqlAdapter do
|
12
|
+
|
13
|
+
before(:all) do
|
14
|
+
class << ActiveRecord::Base.connection
|
15
|
+
include Polymorpheus::SqlLogger
|
16
|
+
alias_method :original_execute, :execute
|
17
|
+
alias_method :execute, :log_sql_statements
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
after(:all) do
|
22
|
+
class << ActiveRecord::Base.connection
|
23
|
+
alias_method :execute, :original_execute
|
24
|
+
end
|
13
25
|
end
|
14
26
|
|
15
27
|
let(:connection) { ActiveRecord::Base.connection }
|
16
28
|
let(:sql) { connection.sql_statements }
|
17
29
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
30
|
+
before do
|
31
|
+
connection.clear_sql_history
|
32
|
+
subject
|
33
|
+
end
|
34
|
+
|
35
|
+
shared_examples_for "migration statements" do
|
36
|
+
describe "#add_polymorphic_constraints" do
|
37
|
+
before { connection.add_polymorphic_constraints(table, columns, options) }
|
38
|
+
|
39
|
+
specify do
|
40
|
+
clean_sql(sql.join("\n")).should == clean_sql(full_constraints_sql)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#add_polymorphic_triggers" do
|
45
|
+
before { connection.add_polymorphic_triggers(table, columns.keys) }
|
46
|
+
|
47
|
+
specify do
|
48
|
+
clean_sql(sql.join("\n")).should == clean_sql(trigger_sql)
|
49
|
+
end
|
22
50
|
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "when the table and column names are not too long" do
|
54
|
+
let(:table) { 'pets' }
|
55
|
+
let(:columns) { { 'kitty_id' => 'cats.name', 'dog_id' => 'dogs.id' } }
|
56
|
+
let(:options) { {} }
|
23
57
|
|
24
|
-
|
25
|
-
|
26
|
-
DROP TRIGGER IF EXISTS
|
27
|
-
DROP TRIGGER IF EXISTS
|
28
|
-
CREATE TRIGGER
|
58
|
+
let(:trigger_sql) do
|
59
|
+
%{
|
60
|
+
DROP TRIGGER IF EXISTS pfki_pets_dogid_kittyid
|
61
|
+
DROP TRIGGER IF EXISTS pfku_pets_dogid_kittyid
|
62
|
+
CREATE TRIGGER pfki_pets_dogid_kittyid BEFORE INSERT ON pets
|
29
63
|
FOR EACH ROW
|
30
64
|
BEGIN
|
31
65
|
IF(IF(NEW.dog_id IS NULL, 0, 1) + IF(NEW.kitty_id IS NULL, 0, 1)) <> 1 THEN
|
32
66
|
SET NEW = 'Error';
|
33
67
|
END IF;
|
34
68
|
END
|
35
|
-
CREATE TRIGGER
|
69
|
+
CREATE TRIGGER pfku_pets_dogid_kittyid BEFORE UPDATE ON pets
|
36
70
|
FOR EACH ROW
|
37
71
|
BEGIN
|
38
72
|
IF(IF(NEW.dog_id IS NULL, 0, 1) + IF(NEW.kitty_id IS NULL, 0, 1)) <> 1 THEN
|
39
73
|
SET NEW = 'Error';
|
40
74
|
END IF;
|
41
75
|
END
|
76
|
+
}
|
77
|
+
end
|
42
78
|
|
79
|
+
let(:fkey_sql) do
|
80
|
+
%{
|
43
81
|
ALTER TABLE `pets` ADD CONSTRAINT `pets_dog_id_fk` FOREIGN KEY (`dog_id`) REFERENCES `dogs`(id)
|
44
82
|
ALTER TABLE `pets` ADD CONSTRAINT `pets_kitty_id_fk` FOREIGN KEY (`kitty_id`) REFERENCES `cats`(name)
|
45
|
-
}
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
let(:full_constraints_sql) { trigger_sql + fkey_sql }
|
87
|
+
|
88
|
+
it_behaves_like "migration statements"
|
89
|
+
|
90
|
+
context "and we specify a uniqueness constraint as true" do
|
91
|
+
let(:options) { { :unique => true } }
|
92
|
+
let(:unique_key_sql) do
|
93
|
+
%{
|
94
|
+
CREATE UNIQUE INDEX pfk_pets_dogid ON pets (dog_id)
|
95
|
+
CREATE UNIQUE INDEX pfk_pets_kittyid ON pets (kitty_id)
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
let(:full_constraints_sql) { trigger_sql + unique_key_sql + fkey_sql }
|
100
|
+
|
101
|
+
it_behaves_like "migration statements"
|
102
|
+
end
|
103
|
+
|
104
|
+
context "and we specify a uniqueness constraint as a string" do
|
105
|
+
let(:options) { { :unique => 'field1' } }
|
106
|
+
let(:unique_key_sql) do
|
107
|
+
%{
|
108
|
+
CREATE UNIQUE INDEX pfk_pets_dogid_field1 ON pets (dog_id, field1)
|
109
|
+
CREATE UNIQUE INDEX pfk_pets_kittyid_field1 ON pets (kitty_id, field1)
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
let(:full_constraints_sql) { trigger_sql + unique_key_sql + fkey_sql }
|
114
|
+
|
115
|
+
it_behaves_like "migration statements"
|
116
|
+
end
|
117
|
+
|
118
|
+
context "and we specify a uniqueness constraint as an array" do
|
119
|
+
let(:options) { { :unique => [:foo, :bar] } }
|
120
|
+
let(:unique_key_sql) do
|
121
|
+
%{
|
122
|
+
CREATE UNIQUE INDEX pfk_pets_dogid_foo_bar ON pets (dog_id, foo, bar)
|
123
|
+
CREATE UNIQUE INDEX pfk_pets_kittyid_foo_bar ON pets (kitty_id, foo, bar)
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
let(:full_constraints_sql) { trigger_sql + unique_key_sql + fkey_sql }
|
128
|
+
|
129
|
+
it_behaves_like "migration statements"
|
46
130
|
end
|
47
131
|
|
132
|
+
context "and we specify a uniqueness constraint on fields with really long names" do
|
133
|
+
let(:options) do
|
134
|
+
{ :unique => [:fee_was_a_buddhist_prodigy, :ground_control_to_major_tom] }
|
135
|
+
end
|
136
|
+
let(:unique_key_sql) do
|
137
|
+
%{
|
138
|
+
CREATE UNIQUE INDEX pfk_pets_dogid_feewasabuddhistpr_groundcontroltoma ON pets (dog_id, fee_was_a_buddhist_prodigy, ground_control_to_major_tom)
|
139
|
+
CREATE UNIQUE INDEX pfk_pets_kittyid_feewasabuddhistpr_groundcontroltoma ON pets (kitty_id, fee_was_a_buddhist_prodigy, ground_control_to_major_tom)
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
let(:full_constraints_sql) { trigger_sql + unique_key_sql + fkey_sql }
|
144
|
+
|
145
|
+
it_behaves_like "migration statements"
|
146
|
+
end
|
48
147
|
end
|
49
148
|
|
149
|
+
context "when the table and column names combined are very long" do
|
150
|
+
let(:table) { 'bicycles' }
|
151
|
+
let(:columns) do
|
152
|
+
{ 'im_too_cool_to_vote_and_ill_only_ride_a_fixie' => 'hipster.id',
|
153
|
+
'really_im_not_doping_i_just_practice_a_lot' => 'professional.id' }
|
154
|
+
end
|
155
|
+
let(:options) { {} }
|
156
|
+
|
157
|
+
let(:trigger_sql) do
|
158
|
+
%{
|
159
|
+
DROP TRIGGER IF EXISTS pfki_bicycles_imtoocooltovoteandillonl_reallyimnotdopingijustpr
|
160
|
+
DROP TRIGGER IF EXISTS pfku_bicycles_imtoocooltovoteandillonl_reallyimnotdopingijustpr
|
161
|
+
CREATE TRIGGER pfki_bicycles_imtoocooltovoteandillonl_reallyimnotdopingijustpr BEFORE INSERT ON bicycles
|
162
|
+
FOR EACH ROW
|
163
|
+
BEGIN
|
164
|
+
IF(IF(NEW.im_too_cool_to_vote_and_ill_only_ride_a_fixie IS NULL, 0, 1) + IF(NEW.really_im_not_doping_i_just_practice_a_lot IS NULL, 0, 1)) <> 1 THEN
|
165
|
+
SET NEW = 'Error';
|
166
|
+
END IF;
|
167
|
+
END
|
168
|
+
CREATE TRIGGER pfku_bicycles_imtoocooltovoteandillonl_reallyimnotdopingijustpr BEFORE UPDATE ON bicycles
|
169
|
+
FOR EACH ROW
|
170
|
+
BEGIN
|
171
|
+
IF(IF(NEW.im_too_cool_to_vote_and_ill_only_ride_a_fixie IS NULL, 0, 1) + IF(NEW.really_im_not_doping_i_just_practice_a_lot IS NULL, 0, 1)) <> 1 THEN
|
172
|
+
SET NEW = 'Error';
|
173
|
+
END IF;
|
174
|
+
END
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
178
|
+
let(:fkey_sql) do
|
179
|
+
%{
|
180
|
+
ALTER TABLE `bicycles` ADD CONSTRAINT `bicycles_im_too_cool_to_vote_and_ill_only_ride_a_fixie_fk` FOREIGN KEY (`im_too_cool_to_vote_and_ill_only_ride_a_fixie`) REFERENCES `hipster`(id)
|
181
|
+
ALTER TABLE `bicycles` ADD CONSTRAINT `bicycles_really_im_not_doping_i_just_practice_a_lot_fk` FOREIGN KEY (`really_im_not_doping_i_just_practice_a_lot`) REFERENCES `professional`(id)
|
182
|
+
}
|
183
|
+
end
|
184
|
+
|
185
|
+
let(:unique_key_sql) do
|
186
|
+
%{
|
187
|
+
CREATE UNIQUE INDEX pfk_blah
|
188
|
+
}
|
189
|
+
end
|
190
|
+
|
191
|
+
let(:full_constraints_sql) { trigger_sql + fkey_sql }
|
192
|
+
|
193
|
+
it_behaves_like "migration statements"
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
|
198
|
+
describe "#triggers" do
|
199
|
+
let(:trigger1) { stub(Trigger, :name => '1') }
|
200
|
+
let(:trigger2) { stub(Trigger, :name => '2') }
|
201
|
+
|
202
|
+
before do
|
203
|
+
connection.stub_sql('show triggers', [:trigger1, :trigger2])
|
204
|
+
Trigger.stub(:new).with(:trigger1).and_return(trigger1)
|
205
|
+
Trigger.stub(:new).with(:trigger2).and_return(trigger2)
|
206
|
+
end
|
207
|
+
|
208
|
+
specify do
|
209
|
+
connection.triggers.should == [trigger1, trigger2]
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
|
50
214
|
def clean_sql(sql_string)
|
51
215
|
sql_string.gsub(/^\n\s*/,'').gsub(/\s*\n\s*$/,'').gsub(/\n\s*/,"\n")
|
52
216
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'sql_logger'
|
4
|
+
require 'foreigner'
|
5
|
+
require 'foreigner/connection_adapters/mysql2_adapter'
|
6
|
+
require 'polymorpheus'
|
7
|
+
require 'polymorpheus/trigger'
|
8
|
+
require 'stringio'
|
9
|
+
|
10
|
+
# this is normally done via a Railtie in non-testing situations
|
11
|
+
ActiveRecord::SchemaDumper.class_eval { include Polymorpheus::SchemaDumper }
|
12
|
+
|
13
|
+
describe Polymorpheus::SchemaDumper do
|
14
|
+
|
15
|
+
let(:connection) { ActiveRecord::Base.connection }
|
16
|
+
let(:stream) { StringIO.new }
|
17
|
+
|
18
|
+
before do
|
19
|
+
# pretend like we have a trigger defined
|
20
|
+
connection.stub(:triggers).and_return(
|
21
|
+
[Trigger.new(["trigger_name", "INSERT", "pets",
|
22
|
+
%{BEGIN
|
23
|
+
IF(IF(NEW.dog_id IS NULL, 0, 1) + IF(NEW.kitty_id IS NULL, 0, 1)) <> 1 THEN
|
24
|
+
SET NEW = 'Error';
|
25
|
+
END IF;
|
26
|
+
END},
|
27
|
+
"BEFORE", nil, "", "production@%", "utf8", "utf8_general_ci",
|
28
|
+
"utf8_unicode_ci"])]
|
29
|
+
)
|
30
|
+
|
31
|
+
ActiveRecord::SchemaDumper.dump(connection, stream)
|
32
|
+
end
|
33
|
+
|
34
|
+
subject { stream.string }
|
35
|
+
|
36
|
+
let(:schema_statement) do
|
37
|
+
%{add_polymorphic_triggers(pets, ["dog_id", "kitty_id"])}
|
38
|
+
end
|
39
|
+
|
40
|
+
specify "the schema statement is part of the dump" do
|
41
|
+
subject.index(schema_statement).should be_a(Integer)
|
42
|
+
end
|
43
|
+
|
44
|
+
specify "there is exactly one instance of the schema statement" do
|
45
|
+
subject.index(schema_statement).should == subject.rindex(schema_statement)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
data/spec/sql_logger.rb
CHANGED
@@ -5,11 +5,24 @@ module Polymorpheus
|
|
5
5
|
@sql_statements ||= []
|
6
6
|
end
|
7
7
|
|
8
|
+
def clear_sql_history
|
9
|
+
@sql_statements = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def stub_sql(statement, response)
|
13
|
+
@stubbed ||= {}
|
14
|
+
@stubbed[statement] = response
|
15
|
+
end
|
16
|
+
|
8
17
|
private
|
9
18
|
|
10
|
-
def
|
11
|
-
|
12
|
-
|
19
|
+
def log_sql_statements(sql, name = nil)
|
20
|
+
if @stubbed && @stubbed.has_key?(sql)
|
21
|
+
@stubbed[sql]
|
22
|
+
else
|
23
|
+
sql_statements << sql
|
24
|
+
sql
|
25
|
+
end
|
13
26
|
end
|
14
27
|
|
15
28
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'polymorpheus'
|
2
|
+
require 'polymorpheus/trigger'
|
3
|
+
|
4
|
+
describe Trigger do
|
5
|
+
|
6
|
+
let(:name) { "pets_unique_polyfk_on_INSERT" }
|
7
|
+
let(:event) { "INSERT" }
|
8
|
+
let(:table) { "pets"}
|
9
|
+
let(:statement) do
|
10
|
+
%{BEGIN
|
11
|
+
IF(IF(NEW.dog_id IS NULL, 0, 1) + IF(NEW.kitty_id IS NULL, 0, 1)) <> 1 THEN
|
12
|
+
SET NEW = 'Error';
|
13
|
+
END IF;
|
14
|
+
END}
|
15
|
+
end
|
16
|
+
let(:timing) { "BEFORE" }
|
17
|
+
let(:created) { nil }
|
18
|
+
let(:sql_mode) { "" }
|
19
|
+
let(:definer) { "production@%" }
|
20
|
+
let(:charset) { "utf8" }
|
21
|
+
let(:collation_connection) { "utf8_general_ci" }
|
22
|
+
let(:db_collation) { "utf8_unicode_ci" }
|
23
|
+
|
24
|
+
subject do
|
25
|
+
Trigger.new([name, event, table, statement, timing, created, sql_mode,
|
26
|
+
definer, charset, collation_connection, db_collation])
|
27
|
+
end
|
28
|
+
|
29
|
+
its(:name) { should == name }
|
30
|
+
its(:event) { should == event }
|
31
|
+
its(:table) { should == table }
|
32
|
+
its(:statement) { should == statement }
|
33
|
+
its(:timing) { should == timing }
|
34
|
+
its(:created) { should == created }
|
35
|
+
its(:sql_mode) { should == sql_mode }
|
36
|
+
its(:definer) { should == definer }
|
37
|
+
its(:charset) { should == charset }
|
38
|
+
its(:collation_connection) { should == collation_connection }
|
39
|
+
its(:db_collation) { should == db_collation }
|
40
|
+
|
41
|
+
its(:columns) { should == %w{dog_id kitty_id} }
|
42
|
+
|
43
|
+
its(:schema_statement) do
|
44
|
+
should == %{add_polymorphic_triggers(pets, ["dog_id", "kitty_id"])}
|
45
|
+
end
|
46
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: polymorpheus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0
|
4
|
+
version: '1.0'
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2012-02-21 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: foreigner
|
16
|
-
requirement: &
|
16
|
+
requirement: &2151832820 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,7 +21,7 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *2151832820
|
25
25
|
description: Provides a database-friendly method for polymorphic relationships
|
26
26
|
email: bsingh@wegowise.com
|
27
27
|
executables: []
|
@@ -34,13 +34,17 @@ files:
|
|
34
34
|
- lib/polymorpheus/interface.rb
|
35
35
|
- lib/polymorpheus/mysql_adapter.rb
|
36
36
|
- lib/polymorpheus/railtie.rb
|
37
|
+
- lib/polymorpheus/schema_dumper.rb
|
37
38
|
- lib/polymorpheus/schema_statements.rb
|
39
|
+
- lib/polymorpheus/trigger.rb
|
38
40
|
- lib/polymorpheus/version.rb
|
39
41
|
- lib/polymorpheus.rb
|
40
42
|
- spec/interface_spec.rb
|
41
43
|
- spec/mysql2_adapter_spec.rb
|
44
|
+
- spec/schema_dumper_spec.rb
|
42
45
|
- spec/spec_helper.rb
|
43
46
|
- spec/sql_logger.rb
|
47
|
+
- spec/trigger_spec.rb
|
44
48
|
- LICENSE.txt
|
45
49
|
- README.md
|
46
50
|
- polymorpheus.gemspec
|
@@ -58,6 +62,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
58
62
|
- - ! '>='
|
59
63
|
- !ruby/object:Gem::Version
|
60
64
|
version: '0'
|
65
|
+
segments:
|
66
|
+
- 0
|
67
|
+
hash: 2769054126994398138
|
61
68
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
69
|
none: false
|
63
70
|
requirements:
|