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 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
- # Suppose I have a table named "pets" with columns dog_id and kitty_id, and I want them to
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
- # UNIQUENESS CONSTRAINTS:
10
+ # table: a string equal to the name of the db table
16
11
  #
17
- # Suppose the pets table also has a 'person_id' column, and we want to impose a uniqueness
18
- # constraint such that a given cat or dog can only be associated with the person one time
19
- # We can specify this in the options as follows:
20
- # options: :unique => 'person_id'
21
-
22
- def add_polymorphic_constraints(table, columns, options = {})
23
- poly_drop_triggers(table)
24
- poly_create_triggers(table, columns.keys)
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, columns.keys, Array(options[:unique]), index_suffix)
45
+ poly_create_indexes(table, column_names, Array(options[:unique]))
29
46
  end
30
- columns.each do |(col, reference)|
31
- ref_table, ref_col = reference.to_s.split('.')
32
- add_foreign_key table, ref_table, :column => col, :primary_key => (ref_col || 'id')
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]), index_suffix)
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
- "#{table}_unique_polyfk_on_#{action}"
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
- execute %{DROP TRIGGER IF EXISTS #{poly_trigger_name(table, action)}}
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
- sql = "CREATE TRIGGER #{poly_trigger_name(table, action)} BEFORE #{action} ON #{table}\n" +
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
- sql += "IF(#{colchecks}) <> 1 THEN\n" +
67
- "SET NEW = 'Error';\n" +
68
- "END IF;\n" +
69
- "END"
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, index_suffix)
85
- unique_cols = unique_cols.collect(&:to_s)
86
- name = poly_index_name(table, column, unique_cols, index_suffix)
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} (#{column},#{unique_cols.join(',')})
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, index_suffix)
93
- unique_cols = unique_cols.collect(&:to_s)
94
- name = poly_index_name(table, column, unique_cols, index_suffix)
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, column, unique_cols, index_suffix)
99
- index_suffix ||= unique_cols.join('_and_')
100
- "index_#{table}_on_#{column}_and_#{index_suffix}"
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, index_suffix)
151
+ def poly_create_indexes(table, columns, unique_cols)
104
152
  columns.each do |column|
105
- poly_create_index(table, column, unique_cols, index_suffix)
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, index_suffix)
157
+ def poly_remove_indexes(table, columns, unique_cols)
110
158
  columns.each do |column|
111
- poly_remove_index(table, column, unique_cols, index_suffix)
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
@@ -11,6 +11,10 @@ module Polymorpheus
11
11
  include Polymorpheus::ConnectionAdapters::SchemaStatements
12
12
  end
13
13
 
14
+ ActiveRecord::SchemaDumper.class_eval do
15
+ include Polymorpheus::SchemaDumper
16
+ end
17
+
14
18
  Polymorpheus::Adapter.load!
15
19
  end
16
20
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Polymorpheus
2
- VERSION = '0.2'
2
+ VERSION = '1.0'
3
3
  end
@@ -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 "Polymorpheus" do
11
- class << ActiveRecord::Base.connection
12
- include Polymorpheus::SqlLogger
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
- describe "add_polymorphic_constraints" do
19
- before do
20
- connection.add_polymorphic_constraints 'pets',
21
- { 'dog_id' => 'dogs.id', 'kitty_id' => 'cats.name' }
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
- it "executes the correct sql statements" do
25
- clean_sql(sql.join("\n")).should == clean_sql(%{
26
- DROP TRIGGER IF EXISTS pets_unique_polyfk_on_INSERT
27
- DROP TRIGGER IF EXISTS pets_unique_polyfk_on_UPDATE
28
- CREATE TRIGGER pets_unique_polyfk_on_INSERT BEFORE INSERT ON pets
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 pets_unique_polyfk_on_UPDATE BEFORE UPDATE ON pets
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 execute(sql, name = nil)
11
- sql_statements << sql
12
- sql
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.2'
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: 2011-12-23 00:00:00.000000000Z
12
+ date: 2012-02-21 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: foreigner
16
- requirement: &70363112837300 !ruby/object:Gem::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: *70363112837300
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: