├── .gitignore ├── .rspec ├── .travis.yml ├── Appraisals ├── Gemfile ├── MIT-LICENSE ├── README.md ├── README.rdoc ├── Rakefile ├── gemfiles ├── rails_3.1.gemfile ├── rails_3.2.gemfile ├── rails_4.0.gemfile └── rails_4.1.gemfile ├── lib ├── polymorphic_constraints.rb └── polymorphic_constraints │ ├── adapter.rb │ ├── connection_adapters │ ├── abstract │ │ └── schema_statements.rb │ ├── base_adapter.rb │ ├── common_adapter.rb │ ├── mysql2_adapter.rb │ ├── postgresql_adapter.rb │ └── sqlite3_adapter.rb │ ├── migration │ └── command_recorder.rb │ ├── railtie.rb │ ├── utils │ ├── polymorphic_error_handler.rb │ ├── polymorphic_model_finder.rb │ └── sql_string.rb │ └── version.rb ├── polymorphic_constraints.gemspec └── spec ├── dummy ├── Rakefile ├── app │ ├── assets │ │ ├── images │ │ │ └── .keep │ │ ├── javascripts │ │ │ └── application.js │ │ └── stylesheets │ │ │ └── application.css │ ├── controllers │ │ ├── application_controller.rb │ │ └── concerns │ │ │ └── .keep │ ├── helpers │ │ └── application_helper.rb │ ├── mailers │ │ └── .keep │ ├── models │ │ ├── .keep │ │ ├── concerns │ │ │ └── .keep │ │ ├── employee.rb │ │ ├── picture.rb │ │ └── product.rb │ └── views │ │ └── layouts │ │ └── application.html.erb ├── bin │ ├── bundle │ ├── rails │ └── rake ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.mysql.yml │ ├── database.postgresql.yml │ ├── database.sqlite.yml │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── secret_token.rb │ │ ├── session_store.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── routes.rb │ └── secrets.yml ├── db │ └── migrate │ │ └── 20141002195532_polymorphic_tables.rb ├── lib │ └── assets │ │ └── .keep ├── log │ └── .keep └── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ └── favicon.ico ├── integration └── active_record_integration_spec.rb ├── lib └── polymorphic_constraints │ ├── connection_adapters │ ├── mysql2_adapter_spec.rb │ ├── postgresql_adapter_spec.rb │ └── sqlite3_adapter_spec.rb │ └── utils │ └── polymorphic_error_handler_spec.rb ├── spec_helper.rb └── support └── adapter_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | spec/dummy/db/*.sqlite3 5 | spec/dummy/db/*.sqlite3-journal 6 | spec/dummy/log/*.log 7 | spec/dummy/tmp/ 8 | spec/dummy/.sass-cache 9 | Gemfile.lock 10 | gemfiles/*.lock 11 | coverage/ 12 | /.idea 13 | .ruby-version 14 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 2.0 5 | - 2.2.3 6 | - ruby-head 7 | 8 | gemfile: 9 | - gemfiles/rails_4.1.gemfile 10 | - gemfiles/rails_4.0.gemfile 11 | - gemfiles/rails_3.2.gemfile 12 | - gemfiles/rails_3.1.gemfile 13 | 14 | matrix: 15 | allow_failures: 16 | - rvm: ruby-head 17 | exclude: 18 | - rvm: 2.2.3 19 | gemfile: gemfiles/rails_3.1.gemfile 20 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise 'rails-3.1' do 2 | gem 'rails', '~> 3.1.12' 3 | gem 'mysql2', '~> 0.3.16' 4 | end 5 | 6 | appraise 'rails-3.2' do 7 | gem 'rails', '~> 3.2.22' 8 | gem 'mysql2', '~> 0.3.16' 9 | gem 'test-unit', '~> 3.1.8' 10 | end 11 | 12 | appraise 'rails-4.0' do 13 | gem 'rails', '~> 4.0.10' 14 | gem 'mysql2', '~> 0.3.16' 15 | end 16 | 17 | appraise 'rails-4.1' do 18 | gem 'rails', '~> 4.1.6' 19 | gem 'mysql2', '~> 0.3.16' 20 | end 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Declare your gem's dependencies in polymorphic_constraints.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use debugger 14 | # gem 'debugger' 15 | gem 'appraisal' 16 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 YOURNAME 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. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Polymorphic Constraints 2 | 3 | [![Gem Version](https://badge.fury.io/rb/polymorphic_constraints.svg)](http://badge.fury.io/rb/polymorphic_constraints) 4 | [![Build Status](https://travis-ci.org/musaffa/polymorphic_constraints.svg)](https://travis-ci.org/musaffa/polymorphic_constraints) 5 | [![Dependency Status](https://gemnasium.com/musaffa/polymorphic_constraints.svg)](https://gemnasium.com/musaffa/polymorphic_constraints) 6 | [![Coverage Status](https://coveralls.io/repos/musaffa/polymorphic_constraints/badge.png)](https://coveralls.io/r/musaffa/polymorphic_constraints) 7 | [![Code Climate](https://codeclimate.com/github/musaffa/polymorphic_constraints/badges/gpa.svg)](https://codeclimate.com/github/musaffa/polymorphic_constraints) 8 | 9 | Polymorphic Constraints gem introduces some methods to your migrations to help to maintain the referential integrity for your Rails polymorphic associations. 10 | 11 | It uses triggers to enforce the constraints. It enforces constraints on `insert`, `update` and `delete`. `update` and `delete` constraints works like the `:restrict` option of foreign key. 12 | 13 | ## Support 14 | 15 | It supports the following adapters: 16 | 17 | * sqlite3 18 | * postgresql 19 | * mysql2 20 | 21 | Supported platforms: 22 | 23 | * Rails versions - 3 and 4. 24 | 25 | ## Installation 26 | 27 | Add the following to your Gemfile: 28 | 29 | ```ruby 30 | gem 'polymorphic_constraints' 31 | ``` 32 | 33 | ## API Examples 34 | 35 | This gem adds the following methods to your migrations: 36 | 37 | * add_polymorphic_constraints(relation, associated_table, options) 38 | * update_polymorphic_constraints(relation, associated_table, options) 39 | * remove_polymorphic_constraints(relation) 40 | 41 | From [Rails Guide](http://guides.rubyonrails.org/association_basics.html#polymorphic-associations) 42 | take these examples: 43 | 44 | ```ruby 45 | class Picture < ActiveRecord::Base 46 | belongs_to :imageable, polymorphic: true 47 | end 48 | 49 | class Employee < ActiveRecord::Base 50 | has_many :pictures, as: :imageable, dependent: :destroy 51 | end 52 | 53 | class Product < ActiveRecord::Base 54 | has_many :pictures, as: :imageable 55 | end 56 | ``` 57 | 58 | Add a new migration: 59 | 60 | ```ruby 61 | class AddPolymorphicConstraints < ActiveRecord::Migration 62 | def change 63 | add_polymorphic_constraints :imageable, :pictures 64 | end 65 | end 66 | ``` 67 | Or you can add it to pictures migration: 68 | 69 | ```ruby 70 | class CreateComments < ActiveRecord::Migration 71 | create_table :pictures do |t| 72 | t.references :imageable, polymorphic: true 73 | t.timestamps 74 | end 75 | 76 | add_polymorphic_constraints :imageable, :pictures 77 | end 78 | ``` 79 | For the second method to work properly, the polymorphic tables `employees` and `products` have to be in the database first i.e `pictures` migration should come after the migrations of `employees` and `products`. 80 | 81 | run: `rake db:migrate` 82 | 83 | This migration will create the necessary triggers to apply insert, update and delete constraints on `imageable` polymorphic relation. 84 | 85 | ```ruby 86 | # insert 87 | >> picture = Picture.new 88 | >> picture.imageable_id = 1 89 | >> picture.imageable_type = 'Product' 90 | >> picture.save # raises ActiveRecord::RecordNotFound exception. there's no product with id 1 91 | 92 | >> product = Product.create 93 | 94 | >> picture.imageable_id = product.id 95 | >> picture.imageable_type = 'World' 96 | >> picture.save # raises ActiveRecord::RecordNotFound exception. there's no imageable model named 'World'. 97 | 98 | >> picture.imageable_type = product.class.to_s # 'Product' 99 | >> picture.save # saves successfully 100 | 101 | # update 102 | >> picture.imageable_type = 'Hello' 103 | >> picture.save # raises ActiveRecord::RecordNotFound exception. there's no imageable model named 'Hello'. 104 | 105 | >> employee = Employee.create 106 | 107 | >> picture.imageable_id = employee.id 108 | >> picture.imageable_type = employee.class.to_s # 'Employee' 109 | >> picture.save # update completes successfully 110 | 111 | # delete/destroy 112 | >> employee.delete # raises ActiveRecord::ReferenceViolation exeption. cannot delete because the picture still refers to the employee as the imageable. 113 | >> employee.destroy # destroys successfully. unlike product, employee implements dependent destroy on imageable. so it destroys the picture first, then it destroys itself. 114 | >> Employee.count # 0 115 | >> Picture.count # 0 116 | 117 | >> picture = Picture.new 118 | >> picture.imageable_id = product.id 119 | >> picture.imageable_type = product.class.to_s # 'Product' 120 | >> picture.save 121 | 122 | >> product.delete # raises ActiveRecord::ReferenceViolation exeption. cannot delete because the picture still refers to the product as the imageable. 123 | >> product.destroy # raises ActiveRecord::ReferenceViolation exeption. works the same as delete because product model hasn't implemented dependent destroy on imageable. 124 | 125 | >> another_product = Product.create 126 | >> another_product.delete # deletes successfully as no picture refers to this product. 127 | ``` 128 | 129 | ## Exceptions 130 | 131 | * `ActiveRecord::RecordNotFound` - It is raised when insert/update is attempted on a table with a record that refers to a non-existent polymorphic record in another table. 132 | * `ActiveRecord::ReferenceViolation` - It is raised when a delete is attempted from a polymorphic table that is referred to by a record in the associated table. 133 | 134 | _naming convention:_ in the above example `Product` and `Employee` are polymorphic tables. `Picture` is an associated table. 135 | 136 | ## Model Search Strategy: 137 | 138 | When you add polymorphic constraints like this: 139 | 140 | ```ruby 141 | add_polymorphic_constraints :imageable, :pictures 142 | ``` 143 | the gem will search for models acting as imageable using `ActiveRecord::Base.descendants`. This will search all the models including your gems, models directory etc. 144 | 145 | You can also explicitly specify the models with which you want to create polymorphic constraints. 146 | 147 | ```ruby 148 | add_polymorphic_constraints :imageable, :pictures, polymorphic_models: [:employee] 149 | ``` 150 | This will create polymorphic constraints only between `pictures` and `employees`. `:polymorphic_models` will supersede `ActiveRecord::Base.descendants` search_strategy. 151 | 152 | **Note:** `:polymorphic_models` option requires an array. The models specified in the array should be in singular form. Make sure the models indeed have the polymorphic relationship (in this example, `:employee` acting as `:imageable` with `:pictures`). 153 | 154 | ## Update Constraints 155 | 156 | This gem creates triggers using the existing state of the application. If you add any model later or add new polymorphic relationships in the existing model, it wont have any polymorphic constraint applied to it. For example, if you add a member class later in the application life cycle: 157 | 158 | ```ruby 159 | class Member < ActiveRecord::Base 160 | has_many :pictures, as: :imageable, dependent: :destroy 161 | end 162 | ``` 163 | There will be no polymorphic constraints between `pictures` and `members`. You have to renew the `imageable` constraints by adding another migration: 164 | 165 | ```ruby 166 | class AgainUpdatePolymorphicConstraints < ActiveRecord::Migration 167 | def change 168 | update_polymorphic_constraints :imageable, :pictures 169 | end 170 | end 171 | ``` 172 | This will delete all the existing `:imageable` constraints and create new ones. You can also specify `:polymorphic_models` options with `update_polymorphic_constraints` method. See [Model Search Strategy](#model-search-strategy) 173 | 174 | **Note:** `update_polymorphic_constraints` is simply an alias to `add_polymorphic_constraints`. 175 | 176 | ## Schema Dump 177 | 178 | The gem doesn't support `ruby` schema dump yet. You have to dump `sql` instead of schema.rb. To do this, change the application config settings: 179 | 180 | ```ruby 181 | # app/config/application.rb 182 | config.active_record.schema_format = :sql 183 | ``` 184 | 185 | ```ruby 186 | rake db:structure:dump 187 | ``` 188 | 189 | ## Migration Rollback 190 | 191 | `add_polymorphic_constraints` and `update_polymorphic_constraints` are both reversible. So you don't need to worry about rollback. 192 | 193 | ```ruby 194 | class AddPolymorphicConstraints < ActiveRecord::Migration 195 | def change 196 | add_polymorphic_constraints :imageable, :pictures 197 | end 198 | end 199 | ``` 200 | 201 | You can also use `up` and `down` like this: 202 | 203 | ```ruby 204 | class AddPolymorphicConstraints < ActiveRecord::Migration 205 | def self.up 206 | add_polymorphic_constraints :imageable, :pictures 207 | end 208 | 209 | def self.down 210 | remove_polymorphic_constraints :imageable 211 | end 212 | end 213 | ``` 214 | 215 | This `remove_polymorphic_constraints` will delete all the existing `:imageable` constraints during rollback. 216 | 217 | **Caution:** After migration, always test if rollback works properly. 218 | 219 | ## Tests 220 | 221 | ```ruby 222 | $ rake 223 | $ rake test:unit 224 | $ rake test:integration:all 225 | $ rake test:integration:sqlite 226 | $ rake test:integration:postgresql 227 | $ rake test:integration:mysql 228 | 229 | # test different active model versions 230 | $ appraisal install 231 | $ appraisal rake 232 | ``` 233 | 234 | ## Problems 235 | 236 | Please use GitHub's [issue tracker](http://github.com/musaffa/polymorphic_constraints/issues). 237 | 238 | ## TODO 239 | 1. Ruby schema dump 240 | 2. Supporting `on_delete`, `on_update` options with `:nullify`, `:restrict` and `:cascade`. 241 | 242 | ## Contributing 243 | 244 | 1. Fork it 245 | 2. Create your feature branch (`git checkout -b my-new-feature`) 246 | 3. Commit your changes (`git commit -am 'Added some feature'`) 247 | 4. Push to the branch (`git push origin my-new-feature`) 248 | 5. Create a new Pull Request 249 | 250 | ## License 251 | 252 | This project rocks and uses MIT-LICENSE. 253 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = PolymorphicConstraints 2 | 3 | This project rocks and uses MIT-LICENSE. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rspec/core/rake_task' 8 | require 'fileutils' 9 | # require 'rdoc/task' 10 | 11 | namespace :test do 12 | task :prepare_database do 13 | FileUtils.copy 'spec/dummy/config/database.sqlite.yml', 'spec/dummy/config/database.yml' 14 | end 15 | 16 | RSpec::Core::RakeTask.new(:unit_specs) do |t| 17 | t.pattern = ['spec/lib/**/*_spec.rb'] 18 | end 19 | 20 | task :unit => [:prepare_database, :unit_specs] 21 | 22 | namespace :integration do 23 | rule '.yml' do |file| 24 | FileUtils.copy ('spec/dummy/config/' + file.name), 'spec/dummy/config/database.yml' 25 | end 26 | 27 | task :sqlite => 'database.sqlite.yml' do 28 | RSpec::Core::RakeTask.new(:sqlite_integration) do |t| 29 | t.pattern = 'spec/integration/active_record_integration_spec.rb' 30 | end 31 | Rake::Task['sqlite_integration'].execute 32 | end 33 | task :postgresql => 'database.postgresql.yml' do 34 | RSpec::Core::RakeTask.new(:postgresql_integration) do |t| 35 | t.pattern = 'spec/integration/active_record_integration_spec.rb' 36 | end 37 | Rake::Task['postgresql_integration'].execute 38 | end 39 | task :mysql => 'database.mysql.yml' do 40 | RSpec::Core::RakeTask.new(:mysql_integration) do |t| 41 | t.pattern = 'spec/integration/active_record_integration_spec.rb' 42 | end 43 | Rake::Task['mysql_integration'].execute 44 | end 45 | 46 | task :all => [:sqlite, :postgresql, :mysql] 47 | end 48 | end 49 | 50 | task :default => ['test:unit', 'test:integration:all'] 51 | 52 | # RDoc::Task.new(:rdoc) do |rdoc| 53 | # rdoc.rdoc_dir = 'rdoc' 54 | # rdoc.title = 'PolymorphicConstraints' 55 | # rdoc.options << '--line-numbers' 56 | # rdoc.rdoc_files.include('README.rdoc') 57 | # rdoc.rdoc_files.include('lib/**/*.rb') 58 | # end 59 | 60 | Bundler::GemHelper.install_tasks 61 | -------------------------------------------------------------------------------- /gemfiles/rails_3.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "rails", "~> 3.1.12" 7 | gem "mysql2", "~> 0.3.16" 8 | 9 | gemspec :path => "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rails_3.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "rails", "~> 3.2.22" 7 | gem "mysql2", "~> 0.3.16" 8 | gem "test-unit", "~> 3.1.8" 9 | 10 | gemspec :path => "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_4.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "rails", "~> 4.0.10" 7 | gem "mysql2", "~> 0.3.16" 8 | 9 | gemspec :path => "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rails_4.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "rails", "~> 4.1.6" 7 | gem "mysql2", "~> 0.3.16" 8 | 9 | gemspec :path => "../" 10 | -------------------------------------------------------------------------------- /lib/polymorphic_constraints.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/all' 2 | 3 | module PolymorphicConstraints 4 | extend ActiveSupport::Autoload 5 | autoload :Adapter 6 | 7 | module ConnectionAdapters 8 | extend ActiveSupport::Autoload 9 | 10 | autoload :BaseAdapter 11 | autoload :CommonAdapter 12 | 13 | autoload_under 'abstract' do 14 | autoload :SchemaStatements 15 | end 16 | end 17 | 18 | module Utils 19 | extend ActiveSupport::Autoload 20 | 21 | autoload :SqlString 22 | autoload :PolymorphicModelFinder 23 | end 24 | 25 | module Migration 26 | autoload :CommandRecorder, 'polymorphic_constraints/migration/command_recorder' 27 | end 28 | end 29 | 30 | PolymorphicConstraints::Adapter.register 'sqlite3', 'polymorphic_constraints/connection_adapters/sqlite3_adapter' 31 | PolymorphicConstraints::Adapter.register 'postgresql', 'polymorphic_constraints/connection_adapters/postgresql_adapter' 32 | PolymorphicConstraints::Adapter.register 'mysql2', 'polymorphic_constraints/connection_adapters/mysql2_adapter' 33 | 34 | require 'polymorphic_constraints/railtie' if defined?(Rails) 35 | -------------------------------------------------------------------------------- /lib/polymorphic_constraints/adapter.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicConstraints 2 | class Adapter 3 | class_attribute :registered 4 | self.registered = {} 5 | 6 | class << self 7 | def register(adapter_name, file_name) 8 | registered[adapter_name] = file_name 9 | end 10 | 11 | def load! 12 | if registered.key?(configured_name) 13 | require registered[configured_name] 14 | else 15 | p "Database adapter #{configured_name} not supported. Use:\n" + 16 | "PolymorphicConstraints::Adapter.register '#{configured_name}', 'path/to/adapter'" 17 | end 18 | end 19 | 20 | def configured_name 21 | @configured_name ||= ActiveRecord::Base.connection_pool.spec.config[:adapter] 22 | end 23 | 24 | def safe_include(adapter_class_name, adapter_ext) 25 | ActiveRecord::ConnectionAdapters.const_get(adapter_class_name).class_eval do 26 | unless ancestors.include? adapter_ext 27 | include adapter_ext 28 | end 29 | end 30 | rescue 31 | end 32 | end 33 | end 34 | end -------------------------------------------------------------------------------- /lib/polymorphic_constraints/connection_adapters/abstract/schema_statements.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicConstraints 2 | module ConnectionAdapters 3 | module SchemaStatements 4 | def self.included(base) 5 | base::AbstractAdapter.class_eval do 6 | include PolymorphicConstraints::ConnectionAdapters::AbstractAdapter 7 | end 8 | end 9 | end 10 | 11 | module AbstractAdapter 12 | def supports_polymorphic_constraints? 13 | false 14 | end 15 | 16 | def add_polymorphic_constraints(relation, associated_model, options = {}) 17 | end 18 | 19 | def remove_polymorphic_constraints(relation) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/polymorphic_constraints/connection_adapters/base_adapter.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicConstraints 2 | module ConnectionAdapters 3 | module BaseAdapter 4 | include PolymorphicConstraints::Utils::SqlString 5 | include PolymorphicConstraints::Utils::PolymorphicModelFinder 6 | 7 | def supports_polymorphic_constraints? 8 | true 9 | end 10 | 11 | def add_polymorphic_constraints(relation, associated_table, options = {}) 12 | polymorphic_models = options.fetch(:polymorphic_models) { get_polymorphic_models(relation) } 13 | 14 | statements = [] 15 | statements << drop_constraints(relation) 16 | statements << generate_upsert_constraints(relation, associated_table, polymorphic_models) 17 | statements << generate_delete_constraints(relation, associated_table, polymorphic_models) 18 | 19 | statements.flatten.each { |statement| execute statement } 20 | end 21 | 22 | def remove_polymorphic_constraints(relation) 23 | statements = [] 24 | statements << drop_constraints(relation) 25 | statements.flatten.each { |statement| execute statement } 26 | end 27 | 28 | alias_method :update_polymorphic_constraints, :add_polymorphic_constraints 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/polymorphic_constraints/connection_adapters/common_adapter.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/inflector' 2 | 3 | module PolymorphicConstraints 4 | module ConnectionAdapters 5 | module CommonAdapter 6 | include BaseAdapter 7 | 8 | private 9 | 10 | def drop_constraints(relation) 11 | polymorphic_models = get_polymorphic_models(relation) 12 | 13 | statements = [] 14 | statements << drop_trigger(relation, 'insert') 15 | statements << drop_trigger(relation, 'update') 16 | 17 | polymorphic_models.each do |polymorphic_model| 18 | statements << drop_delete_trigger(relation, polymorphic_model) 19 | end 20 | 21 | statements 22 | end 23 | 24 | def drop_trigger(relation, action) 25 | sql = <<-SQL 26 | DROP TRIGGER IF EXISTS check_#{relation}_#{action}_integrity; 27 | SQL 28 | 29 | strip_non_essential_spaces(sql) 30 | end 31 | 32 | def drop_delete_trigger(relation, polymorphic_model) 33 | table_name = polymorphic_model.to_s.classify.constantize.table_name 34 | 35 | sql = <<-SQL 36 | DROP TRIGGER IF EXISTS check_#{relation}_#{table_name}_delete_integrity; 37 | SQL 38 | 39 | strip_non_essential_spaces(sql) 40 | end 41 | 42 | def generate_upsert_constraints(relation, associated_table, polymorphic_models) 43 | statements = [] 44 | statements << generate_insert_constraints(relation, associated_table, polymorphic_models) 45 | statements << generate_update_constraints(relation, associated_table, polymorphic_models) 46 | statements 47 | end 48 | 49 | def generate_insert_constraints(relation, associated_table, polymorphic_models) 50 | associated_table = associated_table.to_s 51 | 52 | sql = <<-SQL 53 | CREATE TRIGGER check_#{relation}_insert_integrity 54 | BEFORE INSERT ON #{associated_table} 55 | SQL 56 | 57 | sql << common_upsert_sql(relation, polymorphic_models) 58 | 59 | strip_non_essential_spaces(sql) 60 | end 61 | 62 | def generate_update_constraints(relation, associated_table, polymorphic_models) 63 | associated_table = associated_table.to_s 64 | polymorphic_models = polymorphic_models.map(&:to_s) 65 | 66 | sql = <<-SQL 67 | CREATE TRIGGER check_#{relation}_update_integrity 68 | BEFORE UPDATE ON #{associated_table} 69 | SQL 70 | 71 | sql << common_upsert_sql(relation, polymorphic_models) 72 | 73 | strip_non_essential_spaces(sql) 74 | end 75 | 76 | def generate_delete_constraints(relation, associated_table, polymorphic_models) 77 | statements = [] 78 | 79 | polymorphic_models.each do |polymorphic_model| 80 | statements << delete_statement(relation, associated_table, polymorphic_model) 81 | end 82 | 83 | statements 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/polymorphic_constraints/connection_adapters/mysql2_adapter.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicConstraints 2 | module ConnectionAdapters 3 | module Mysql2Adapter 4 | include CommonAdapter 5 | 6 | private 7 | 8 | def delete_statement(relation, associated_table, polymorphic_model) 9 | associated_table = associated_table.to_s 10 | polymorphic_model = polymorphic_model.to_s 11 | 12 | sql = <<-SQL 13 | CREATE TRIGGER check_#{relation}_#{polymorphic_model.classify.constantize.table_name}_delete_integrity 14 | BEFORE DELETE ON #{polymorphic_model.classify.constantize.table_name} 15 | FOR EACH ROW 16 | BEGIN 17 | IF EXISTS (SELECT id FROM #{associated_table} 18 | WHERE #{relation}_type = '#{polymorphic_model.classify}' 19 | AND #{relation}_id = OLD.id) THEN 20 | SIGNAL SQLSTATE '45000' 21 | SET MESSAGE_TEXT = 'Polymorphic reference exists. 22 | There are records in the #{associated_table} table that refer to the 23 | table #{polymorphic_model.classify.constantize.table_name}. 24 | You must delete those records of table #{associated_table} first.'; 25 | END IF; 26 | END; 27 | SQL 28 | 29 | strip_non_essential_spaces(sql) 30 | end 31 | 32 | def common_upsert_sql(relation, polymorphic_models) 33 | polymorphic_models = polymorphic_models.map(&:to_s) 34 | 35 | sql = <<-SQL 36 | FOR EACH ROW 37 | BEGIN 38 | IF 39 | SQL 40 | 41 | polymorphic_models.each do |polymorphic_model| 42 | sql << <<-SQL 43 | NEW.#{relation}_type != '#{polymorphic_model.classify}' 44 | SQL 45 | 46 | unless polymorphic_model == polymorphic_models.last 47 | sql << <<-SQL 48 | AND 49 | SQL 50 | end 51 | end 52 | 53 | sql << <<-SQL 54 | THEN SIGNAL SQLSTATE '45000' 55 | SET MESSAGE_TEXT = 'Polymorphic record not found. No model by that name.'; 56 | SQL 57 | 58 | 59 | polymorphic_models.each do |polymorphic_model| 60 | sql << <<-SQL 61 | ELSEIF NEW.#{relation}_type = '#{polymorphic_model.classify}' AND 62 | NOT EXISTS (SELECT id FROM #{polymorphic_model.classify.constantize.table_name} 63 | WHERE id = NEW.#{relation}_id) THEN 64 | 65 | SIGNAL SQLSTATE '45000' 66 | SET MESSAGE_TEXT = 'Polymorphic record not found. No #{polymorphic_model.classify} with that id.'; 67 | SQL 68 | end 69 | 70 | sql << <<-SQL 71 | END IF; 72 | END; 73 | SQL 74 | end 75 | end 76 | end 77 | end 78 | 79 | PolymorphicConstraints::Adapter.safe_include :Mysql2Adapter, PolymorphicConstraints::ConnectionAdapters::Mysql2Adapter 80 | -------------------------------------------------------------------------------- /lib/polymorphic_constraints/connection_adapters/postgresql_adapter.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/inflector' 2 | 3 | module PolymorphicConstraints 4 | module ConnectionAdapters 5 | module PostgreSQLAdapter 6 | include BaseAdapter 7 | 8 | private 9 | 10 | def generate_upsert_constraints(relation, associated_table, polymorphic_models) 11 | associated_table = associated_table.to_s 12 | polymorphic_models = polymorphic_models.map(&:to_s) 13 | 14 | sql = <<-SQL 15 | CREATE FUNCTION check_#{relation}_upsert_integrity() 16 | RETURNS TRIGGER AS ' 17 | BEGIN 18 | IF NEW.#{relation}_type = ''#{polymorphic_models[0].classify}'' AND 19 | EXISTS (SELECT id FROM #{polymorphic_models[0].classify.constantize.table_name} 20 | WHERE id = NEW.#{relation}_id) THEN 21 | 22 | RETURN NEW; 23 | SQL 24 | 25 | polymorphic_models[1..-1].each do |polymorphic_model| 26 | sql << <<-SQL 27 | ELSEIF NEW.#{relation}_type = ''#{polymorphic_model.classify}'' AND 28 | EXISTS (SELECT id FROM #{polymorphic_model.classify.constantize.table_name} 29 | WHERE id = NEW.#{relation}_id) THEN 30 | 31 | RETURN NEW; 32 | SQL 33 | end 34 | 35 | sql << <<-SQL 36 | ELSE 37 | RAISE EXCEPTION ''Polymorphic record not found. 38 | No % model with id %.'', NEW.#{relation}_type, NEW.#{relation}_id; 39 | RETURN NULL; 40 | END IF; 41 | END' 42 | LANGUAGE plpgsql; 43 | 44 | CREATE TRIGGER check_#{relation}_upsert_integrity_trigger 45 | BEFORE INSERT OR UPDATE ON #{associated_table} 46 | FOR EACH ROW 47 | EXECUTE PROCEDURE check_#{relation}_upsert_integrity(); 48 | SQL 49 | 50 | strip_non_essential_spaces(sql) 51 | end 52 | 53 | def generate_delete_constraints(relation, associated_table, polymorphic_models) 54 | associated_table = associated_table.to_s 55 | polymorphic_models = polymorphic_models.map(&:to_s) 56 | 57 | sql = <<-SQL 58 | CREATE FUNCTION check_#{relation}_delete_integrity() 59 | RETURNS TRIGGER AS ' 60 | BEGIN 61 | IF TG_TABLE_NAME = ''#{polymorphic_models[0].classify.constantize.table_name}'' AND 62 | EXISTS (SELECT id FROM #{associated_table} 63 | WHERE #{relation}_type = ''#{polymorphic_models[0].classify}'' 64 | AND #{relation}_id = OLD.id) THEN 65 | 66 | RAISE EXCEPTION ''Polymorphic reference exists. 67 | There are records in #{associated_table} that refer to the table % with id %. 68 | You must delete those records of table #{associated_table} first.'', TG_TABLE_NAME, OLD.id; 69 | RETURN NULL; 70 | SQL 71 | 72 | polymorphic_models[1..-1].each do |polymorphic_model| 73 | sql << <<-SQL 74 | ELSEIF TG_TABLE_NAME = ''#{polymorphic_model.classify.constantize.table_name}'' AND 75 | EXISTS (SELECT id FROM #{associated_table} 76 | WHERE #{relation}_type = ''#{polymorphic_model.classify}'' 77 | AND #{relation}_id = OLD.id) THEN 78 | 79 | RAISE EXCEPTION ''Polymorphic reference exists. 80 | There are records in #{associated_table} that refer to the table % with id %. 81 | You must delete those records of table #{associated_table} first.'', TG_TABLE_NAME, OLD.id; 82 | RETURN NULL; 83 | SQL 84 | end 85 | 86 | sql << <<-SQL 87 | ELSE 88 | RETURN OLD; 89 | END IF; 90 | END' 91 | LANGUAGE plpgsql; 92 | SQL 93 | 94 | polymorphic_models.each do |polymorphic_model| 95 | table_name = polymorphic_model.classify.constantize.table_name 96 | 97 | sql << <<-SQL 98 | CREATE TRIGGER check_#{relation}_#{table_name}_delete_integrity_trigger 99 | BEFORE DELETE ON #{table_name} 100 | FOR EACH ROW 101 | EXECUTE PROCEDURE check_#{relation}_delete_integrity(); 102 | SQL 103 | end 104 | 105 | strip_non_essential_spaces(sql) 106 | end 107 | 108 | def drop_constraints(relation) 109 | sql = <<-SQL 110 | DROP FUNCTION IF EXISTS check_#{relation}_upsert_integrity() 111 | CASCADE; 112 | DROP FUNCTION IF EXISTS check_#{relation}_delete_integrity() 113 | CASCADE; 114 | SQL 115 | 116 | strip_non_essential_spaces(sql) 117 | end 118 | end 119 | end 120 | end 121 | 122 | PolymorphicConstraints::Adapter.safe_include :PostgreSQLAdapter, PolymorphicConstraints::ConnectionAdapters::PostgreSQLAdapter 123 | -------------------------------------------------------------------------------- /lib/polymorphic_constraints/connection_adapters/sqlite3_adapter.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicConstraints 2 | module ConnectionAdapters 3 | module SQLite3Adapter 4 | include CommonAdapter 5 | 6 | private 7 | 8 | def delete_statement(relation, associated_table, polymorphic_model) 9 | associated_table = associated_table.to_s 10 | polymorphic_model = polymorphic_model.to_s 11 | 12 | sql = <<-SQL 13 | CREATE TRIGGER check_#{relation}_#{polymorphic_model.classify.constantize.table_name}_delete_integrity 14 | BEFORE DELETE ON #{polymorphic_model.classify.constantize.table_name} 15 | BEGIN 16 | SELECT CASE 17 | WHEN EXISTS (SELECT id FROM #{associated_table} 18 | WHERE #{relation}_type = '#{polymorphic_model.classify}' 19 | AND #{relation}_id = OLD.id) THEN 20 | RAISE(ABORT, 'Polymorphic reference exists. 21 | There are records in the #{associated_table} table that refer to the 22 | table #{polymorphic_model.classify.constantize.table_name}. 23 | You must delete those records of table #{associated_table} first.') 24 | END; 25 | END; 26 | SQL 27 | 28 | strip_non_essential_spaces(sql) 29 | end 30 | 31 | def common_upsert_sql(relation, polymorphic_models) 32 | polymorphic_models = polymorphic_models.map(&:to_s) 33 | 34 | sql = <<-SQL 35 | BEGIN 36 | SELECT CASE 37 | SQL 38 | 39 | sql << <<-SQL 40 | WHEN ( 41 | SQL 42 | 43 | polymorphic_models.each do |polymorphic_model| 44 | sql << <<-SQL 45 | NEW.#{relation}_type != '#{polymorphic_model.classify}' 46 | SQL 47 | 48 | unless polymorphic_model == polymorphic_models.last 49 | sql << <<-SQL 50 | AND 51 | SQL 52 | end 53 | end 54 | 55 | sql << <<-SQL 56 | ) THEN RAISE(ABORT, 'Polymorphic record not found. No model by that name.') 57 | SQL 58 | 59 | polymorphic_models.each do |polymorphic_model| 60 | sql << <<-SQL 61 | WHEN ((NEW.#{relation}_type = '#{polymorphic_model.classify}') AND 62 | NOT EXISTS (SELECT id FROM #{polymorphic_model.classify.constantize.table_name} 63 | WHERE id = NEW.#{relation}_id)) THEN 64 | RAISE(ABORT, 'Polymorphic record not found. No #{polymorphic_model.classify} with that id.') 65 | SQL 66 | end 67 | 68 | sql << <<-SQL 69 | END; 70 | END; 71 | SQL 72 | end 73 | end 74 | end 75 | end 76 | 77 | PolymorphicConstraints::Adapter.safe_include :SQLite3Adapter, PolymorphicConstraints::ConnectionAdapters::SQLite3Adapter 78 | -------------------------------------------------------------------------------- /lib/polymorphic_constraints/migration/command_recorder.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicConstraints 2 | module Migration 3 | module CommandRecorder 4 | def add_polymorphic_constraints(*args) 5 | record(:add_polymorphic_constraints, args) 6 | end 7 | 8 | def remove_polymorphic_constraints(*args) 9 | record(:remove_polymorphic_constraints, args) 10 | end 11 | 12 | def invert_add_polymorphic_constraints(args) 13 | relation, associated_model, options = *args 14 | [:remove_polymorphic_constraints, relation] 15 | end 16 | 17 | alias_method :update_polymorphic_constraints, :add_polymorphic_constraints 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /lib/polymorphic_constraints/railtie.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicConstraints 2 | class Railtie < Rails::Railtie 3 | initializer 'polymorphic_constraints.load_migration' do 4 | ActiveSupport.on_load :active_record do 5 | class ActiveRecord::ReferenceViolation < ActiveRecord::ActiveRecordError 6 | end 7 | 8 | ActiveRecord::ConnectionAdapters.module_eval do 9 | include PolymorphicConstraints::ConnectionAdapters::SchemaStatements 10 | end 11 | 12 | if defined?(ActiveRecord::Migration::CommandRecorder) 13 | ActiveRecord::Migration::CommandRecorder.class_eval do 14 | include PolymorphicConstraints::Migration::CommandRecorder 15 | end 16 | end 17 | 18 | PolymorphicConstraints::Adapter.load! 19 | end 20 | 21 | ActiveSupport.on_load :action_controller do 22 | require 'polymorphic_constraints/utils/polymorphic_error_handler' 23 | include PolymorphicConstraints::Utils::PolymorphicErrorHandler 24 | end 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /lib/polymorphic_constraints/utils/polymorphic_error_handler.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicConstraints 2 | module Utils 3 | module PolymorphicErrorHandler 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | rescue_from ActiveRecord::StatementInvalid do |exception| 8 | if exception.message =~ /Polymorphic record not found./ 9 | raise ActiveRecord::RecordNotFound, exception.message 10 | elsif exception.message =~ /Polymorphic reference exists./ 11 | raise ActiveRecord::ReferenceViolation, exception.message 12 | else 13 | raise exception 14 | end 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/polymorphic_constraints/utils/polymorphic_model_finder.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicConstraints 2 | module Utils 3 | module PolymorphicModelFinder 4 | def get_polymorphic_models(relation) 5 | Rails.application.eager_load! 6 | ActiveRecord::Base.descendants.select do |klass| 7 | contains_polymorphic_relation?(klass, relation) 8 | end 9 | end 10 | 11 | private 12 | 13 | def contains_polymorphic_relation?(model_class, relation) 14 | associations = model_class.reflect_on_all_associations 15 | associations.map{ |r| r.options[:as] }.include?(relation.to_sym) 16 | end 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /lib/polymorphic_constraints/utils/sql_string.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicConstraints 2 | module Utils 3 | module SqlString 4 | def strip_non_essential_spaces(string_to_strip) 5 | string_to_strip.gsub(/\s{2,}|\\n/, " ").strip 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/polymorphic_constraints/version.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicConstraints 2 | VERSION = "1.0.0" 3 | end 4 | -------------------------------------------------------------------------------- /polymorphic_constraints.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path('../lib', __FILE__) 2 | 3 | require 'polymorphic_constraints/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'polymorphic_constraints' 7 | s.version = PolymorphicConstraints::VERSION 8 | s.authors = ['Ahmad Musaffa'] 9 | s.email = ['musaffa_csemm@yahoo.com'] 10 | s.homepage = 'https://github.com/musaffa/polymorphic_constraints' 11 | s.summary = 'Database agnostic referential integrity enforcer for Rails polymorphic associations using triggers.' 12 | s.description = 'Helps to maintain referential integrity for Rails polymorphic associations.' 13 | s.license = 'MIT' 14 | 15 | s.files = `git ls-files`.split($/) 16 | s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | s.test_files = s.files.grep(%r{^spec/}) 18 | s.require_paths = ['lib'] 19 | 20 | s.required_ruby_version = '>= 1.9.3' 21 | s.add_dependency 'rails' 22 | 23 | s.add_development_dependency 'rake' 24 | s.add_development_dependency 'sqlite3' 25 | s.add_development_dependency 'pg' 26 | s.add_development_dependency 'mysql2' 27 | s.add_development_dependency 'rspec-rails', '~> 3.4.2' 28 | s.add_development_dependency 'coveralls' 29 | end 30 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musaffa/polymorphic_constraints/893af5d8e319816a8a85dc4e62af56bdff87dce2/spec/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musaffa/polymorphic_constraints/893af5d8e319816a8a85dc4e62af56bdff87dce2/spec/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musaffa/polymorphic_constraints/893af5d8e319816a8a85dc4e62af56bdff87dce2/spec/dummy/app/mailers/.keep -------------------------------------------------------------------------------- /spec/dummy/app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musaffa/polymorphic_constraints/893af5d8e319816a8a85dc4e62af56bdff87dce2/spec/dummy/app/models/.keep -------------------------------------------------------------------------------- /spec/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musaffa/polymorphic_constraints/893af5d8e319816a8a85dc4e62af56bdff87dce2/spec/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy/app/models/employee.rb: -------------------------------------------------------------------------------- 1 | class Employee < ActiveRecord::Base 2 | has_many :pictures, as: :imageable, dependent: :destroy 3 | end -------------------------------------------------------------------------------- /spec/dummy/app/models/picture.rb: -------------------------------------------------------------------------------- 1 | class Picture < ActiveRecord::Base 2 | belongs_to :imageable, polymorphic: true 3 | end -------------------------------------------------------------------------------- /spec/dummy/app/models/product.rb: -------------------------------------------------------------------------------- 1 | class Product < ActiveRecord::Base 2 | has_many :pictures, as: :imageable 3 | end -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | # Pick the frameworks you want: 4 | require "active_record/railtie" 5 | require "action_controller/railtie" 6 | require "action_mailer/railtie" 7 | require "action_view/railtie" 8 | require "sprockets/railtie" 9 | # require "rails/test_unit/railtie" 10 | 11 | Bundler.require(*Rails.groups) 12 | require "polymorphic_constraints" 13 | 14 | module Dummy 15 | class Application < Rails::Application 16 | # Settings in config/environments/* take precedence over those specified here. 17 | # Application configuration should go into files in config/initializers 18 | # -- all .rb files in that directory are automatically loaded. 19 | 20 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 21 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 22 | # config.time_zone = 'Central Time (US & Canada)' 23 | 24 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 25 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 26 | # config.i18n.default_locale = :de 27 | config.i18n.enforce_available_locales = false 28 | config.active_record.schema_format = :sql 29 | end 30 | end 31 | 32 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /spec/dummy/config/database.mysql.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: mysql2 3 | encoding: utf8 4 | username: root 5 | database: polymorphic_constraints 6 | host: localhost -------------------------------------------------------------------------------- /spec/dummy/config/database.postgresql.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: postgresql 3 | encoding: utf8 4 | username: postgres 5 | database: polymorphic_constraints 6 | host: localhost -------------------------------------------------------------------------------- /spec/dummy/config/database.sqlite.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: sqlite3 3 | database: db/test.sqlite3 4 | pool: 5 5 | timeout: 5000 -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: sqlite3 3 | database: db/test.sqlite3 4 | pool: 5 5 | timeout: 5000 -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Adds additional error checking when serving assets at runtime. 31 | # Checks for improperly declared sprockets dependencies. 32 | # Raises helpful error messages. 33 | config.assets.raise_runtime_errors = true 34 | 35 | # Raises error for missing translations 36 | # config.action_view.raise_on_missing_translations = true 37 | end 38 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | config.serve_static_assets = false 24 | 25 | # Compress JavaScripts and CSS. 26 | config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = false 31 | 32 | # Generate digests for assets URLs. 33 | config.assets.digest = true 34 | 35 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 36 | 37 | # Specifies the header that your server uses for sending files. 38 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 39 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 40 | 41 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 42 | # config.force_ssl = true 43 | 44 | # Set to :debug to see everything in the log. 45 | config.log_level = :info 46 | 47 | # Prepend all log lines with the following tags. 48 | # config.log_tags = [ :subdomain, :uuid ] 49 | 50 | # Use a different logger for distributed setups. 51 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 52 | 53 | # Use a different cache store in production. 54 | # config.cache_store = :mem_cache_store 55 | 56 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 57 | # config.action_controller.asset_host = "http://assets.example.com" 58 | 59 | # Ignore bad email addresses and do not raise email delivery errors. 60 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 61 | # config.action_mailer.raise_delivery_errors = false 62 | 63 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 64 | # the I18n.default_locale when a translation cannot be found). 65 | config.i18n.fallbacks = true 66 | 67 | # Send deprecation notices to registered listeners. 68 | config.active_support.deprecation = :notify 69 | 70 | # Disable automatic flushing of the log to improve performance. 71 | # config.autoflush_log = false 72 | 73 | # Use default logging formatter so that PID and timestamp are not suppressed. 74 | config.log_formatter = ::Logger::Formatter.new 75 | 76 | # Do not dump schema after migrations. 77 | config.active_record.dump_schema_after_migration = false 78 | end 79 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_assets = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | 37 | # Raises error for missing translations 38 | # config.action_view.raise_on_missing_translations = true 39 | end 40 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Precompile additional assets. 7 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 8 | # Rails.application.config.assets.precompile += %w( search.js ) 9 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.config.secret_token = 'existing secret token' 2 | Dummy::Application.config.secret_key_base = 'new secret key base' -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # The priority is based upon order of creation: first created -> highest priority. 3 | # See how all your routes lay out with "rake routes". 4 | 5 | # You can have the root of your site routed with "root" 6 | # root 'welcome#index' 7 | 8 | # Example of regular route: 9 | # get 'products/:id' => 'catalog#view' 10 | 11 | # Example of named route that can be invoked with purchase_url(id: product.id) 12 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase 13 | 14 | # Example resource route (maps HTTP verbs to controller actions automatically): 15 | # resources :products 16 | 17 | # Example resource route with options: 18 | # resources :products do 19 | # member do 20 | # get 'short' 21 | # post 'toggle' 22 | # end 23 | # 24 | # collection do 25 | # get 'sold' 26 | # end 27 | # end 28 | 29 | # Example resource route with sub-resources: 30 | # resources :products do 31 | # resources :comments, :sales 32 | # resource :seller 33 | # end 34 | 35 | # Example resource route with more complex sub-resources: 36 | # resources :products do 37 | # resources :comments 38 | # resources :sales do 39 | # get 'recent', on: :collection 40 | # end 41 | # end 42 | 43 | # Example resource route with concerns: 44 | # concern :toggleable do 45 | # post 'toggle' 46 | # end 47 | # resources :posts, concerns: :toggleable 48 | # resources :photos, concerns: :toggleable 49 | 50 | # Example resource route within a namespace: 51 | # namespace :admin do 52 | # # Directs /admin/products/* to Admin::ProductsController 53 | # # (app/controllers/admin/products_controller.rb) 54 | # resources :products 55 | # end 56 | end 57 | -------------------------------------------------------------------------------- /spec/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: fd0f940047505a0b36c884160d6982de40a9cd427d666a5d051a735f950e1bcaffa84def7e04da334d5690bc81973df3264b83b457558bd3007a23e16a3a6d05 15 | 16 | test: 17 | secret_key_base: b8f29540469654aa75afc9bb19298152bbf2ddf32ec0741f41df411226be3d92f7d65d0aca12229767039fa39fb81704e7da5d8b0535af9741254399fc2c1af1 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20141002195532_polymorphic_tables.rb: -------------------------------------------------------------------------------- 1 | class PolymorphicTables < ActiveRecord::Migration 2 | def change 3 | create_table :employees do |t| 4 | t.timestamps null: false 5 | end 6 | 7 | create_table :products do |t| 8 | t.timestamps null: false 9 | end 10 | 11 | create_table :pictures do |t| 12 | t.references :imageable, polymorphic: true 13 | t.timestamps null: false 14 | end 15 | 16 | add_polymorphic_constraints :imageable, :pictures 17 | end 18 | end -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musaffa/polymorphic_constraints/893af5d8e319816a8a85dc4e62af56bdff87dce2/spec/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /spec/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musaffa/polymorphic_constraints/893af5d8e319816a8a85dc4e62af56bdff87dce2/spec/dummy/log/.keep -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musaffa/polymorphic_constraints/893af5d8e319816a8a85dc4e62af56bdff87dce2/spec/dummy/public/favicon.ico -------------------------------------------------------------------------------- /spec/integration/active_record_integration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Active Record Integration' do 4 | before :all do 5 | adapter = ActiveRecord::Base.connection_config[:adapter] 6 | send("setup_#{adapter}") 7 | end 8 | 9 | context 'insertion' do 10 | it 'raises an exception creating a polymorphic relation without a corresponding record' do 11 | picture = Picture.new 12 | picture.imageable_id = 1 13 | picture.imageable_type = 'Product' 14 | expect { picture.save }.to raise_error(ActiveRecord::StatementInvalid) 15 | end 16 | 17 | it 'does not allow an insert of a model type that wasn\'t specified in the polymorphic triggers' do 18 | product = Product.new 19 | product.save 20 | product.reload 21 | 22 | picture = Picture.new 23 | picture.imageable_id = product.id 24 | picture.imageable_type = 'World' 25 | 26 | expect { picture.save }.to raise_error(ActiveRecord::StatementInvalid) 27 | end 28 | 29 | it 'allows an insert of a model type specified in the polymorphic triggers' do 30 | product = Product.new 31 | product.save 32 | product.reload 33 | 34 | picture = Picture.new 35 | picture.imageable_id = product.id 36 | picture.imageable_type = product.class.to_s 37 | 38 | expect { picture.save }.to change(Picture, :count).by(1) 39 | end 40 | end 41 | 42 | context 'update' do 43 | it "does not allow an update to a model type that wasn't specified in the polymorphic triggers" do 44 | product = Product.new 45 | product.save 46 | product.reload 47 | 48 | picture = Picture.new 49 | picture.imageable_id = product.id 50 | picture.imageable_type = product.class.to_s 51 | picture.save 52 | picture.reload 53 | 54 | picture.imageable_type = 'Hello' 55 | 56 | expect { picture.save }.to raise_error(ActiveRecord::StatementInvalid) 57 | end 58 | end 59 | 60 | context 'deletion' do 61 | it 'raises an exception deleting a record that is still referenced by the polymorphic table' do 62 | product = Product.new 63 | product.save 64 | product.reload 65 | 66 | picture = Picture.new 67 | picture.imageable_id = product.id 68 | picture.imageable_type = product.class.to_s 69 | picture.save 70 | 71 | expect { product.delete }.to raise_error(ActiveRecord::StatementInvalid) 72 | expect { product.destroy }.to raise_error(ActiveRecord::StatementInvalid) 73 | end 74 | 75 | it 'doesn\'t get in the way of dependent: :destroy' do 76 | employee = Employee.new 77 | employee.save 78 | employee.reload 79 | 80 | picture = Picture.new 81 | picture.imageable_id = employee.id 82 | picture.imageable_type = employee.class.to_s 83 | picture.save 84 | 85 | expect { employee.destroy }.to change(Employee, :count).by(-1) 86 | end 87 | 88 | it 'does enforce dependency behaviour if delete is used instead of destroy' do 89 | employee = Employee.new 90 | employee.save 91 | employee.reload 92 | 93 | picture = Picture.new 94 | picture.imageable_id = employee.id 95 | picture.imageable_type = employee.class.to_s 96 | picture.save 97 | 98 | expect { employee.delete }.to raise_error(ActiveRecord::StatementInvalid) 99 | end 100 | 101 | it 'allows a delete of a record NOT referenced by the polymorphic table' do 102 | employee = Employee.new 103 | employee.save 104 | employee.reload 105 | 106 | expect { employee.delete }.to change(Employee, :count).by(-1) 107 | end 108 | 109 | it 'allows a destroy of a record NOT referenced by the polymorphic table' do 110 | employee = Employee.new 111 | employee.save 112 | employee.reload 113 | 114 | expect { employee.destroy }.to change(Employee, :count).by(-1) 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /spec/lib/polymorphic_constraints/connection_adapters/mysql2_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'polymorphic_constraints/connection_adapters/mysql2_adapter' 3 | 4 | describe PolymorphicConstraints::ConnectionAdapters::Mysql2Adapter do 5 | 6 | class MysqlTestAdapter 7 | include Support::AdapterHelper 8 | include PolymorphicConstraints::ConnectionAdapters::Mysql2Adapter 9 | end 10 | 11 | subject { MysqlTestAdapter.new } 12 | 13 | it { is_expected.to respond_to(:supports_polymorphic_constraints?) } 14 | it { is_expected.to respond_to(:add_polymorphic_constraints) } 15 | it { is_expected.to respond_to(:remove_polymorphic_constraints) } 16 | 17 | describe 'add constraints' do 18 | it 'defaults to active_record_descendants search strategy' do 19 | expect(subject.add_polymorphic_constraints(:imageable, :pictures)).to eql([drop_create_trigger_sql, 20 | drop_update_trigger_sql, 21 | drop_employees_delete_trigger_sql, 22 | drop_products_delete_trigger_sql, 23 | create_trigger_sql, 24 | update_trigger_sql, 25 | employees_delete_trigger_sql, 26 | products_delete_trigger_sql]) 27 | end 28 | 29 | it 'returns expected add constraints sql with polymorphic model options' do 30 | expect(subject.add_polymorphic_constraints(:imageable, :pictures, 31 | polymorphic_models: [:employee])).to eql([drop_create_trigger_sql, 32 | drop_update_trigger_sql, 33 | drop_employees_delete_trigger_sql, 34 | drop_products_delete_trigger_sql, 35 | create_trigger_sql_only_employee, 36 | update_trigger_sql_only_employee, 37 | employees_delete_trigger_sql]) 38 | end 39 | end 40 | 41 | describe 'remove constraints' do 42 | it 'defaults to active_record_descendants search strategy' do 43 | expect(subject.remove_polymorphic_constraints(:imageable)).to eql([drop_create_trigger_sql, 44 | drop_update_trigger_sql, 45 | drop_employees_delete_trigger_sql, 46 | drop_products_delete_trigger_sql]) 47 | end 48 | end 49 | 50 | let(:drop_create_trigger_sql) { 'DROP TRIGGER IF EXISTS check_imageable_insert_integrity;' } 51 | let(:drop_update_trigger_sql) { 'DROP TRIGGER IF EXISTS check_imageable_update_integrity;' } 52 | 53 | let(:drop_employees_delete_trigger_sql) { 'DROP TRIGGER IF EXISTS check_imageable_employees_delete_integrity;' } 54 | 55 | let(:employees_delete_trigger_sql) do 56 | subject.strip_non_essential_spaces(%{ 57 | CREATE TRIGGER check_imageable_employees_delete_integrity 58 | BEFORE DELETE ON employees 59 | FOR EACH ROW 60 | BEGIN 61 | IF EXISTS (SELECT id FROM pictures 62 | WHERE imageable_type = 'Employee' 63 | AND imageable_id = OLD.id) THEN 64 | SIGNAL SQLSTATE '45000' 65 | SET MESSAGE_TEXT = 'Polymorphic reference exists. 66 | There are records in the pictures table that refer to the table employees. 67 | You must delete those records of table pictures first.'; 68 | END IF; 69 | END; 70 | }) 71 | end 72 | 73 | let(:drop_products_delete_trigger_sql) { 'DROP TRIGGER IF EXISTS check_imageable_products_delete_integrity;' } 74 | 75 | let(:products_delete_trigger_sql) do 76 | subject.strip_non_essential_spaces(%{ 77 | CREATE TRIGGER check_imageable_products_delete_integrity 78 | BEFORE DELETE ON products 79 | FOR EACH ROW 80 | BEGIN 81 | IF EXISTS (SELECT id FROM pictures 82 | WHERE imageable_type = 'Product' 83 | AND imageable_id = OLD.id) THEN 84 | SIGNAL SQLSTATE '45000' 85 | SET MESSAGE_TEXT = 'Polymorphic reference exists. 86 | There are records in the pictures table that refer to the table products. 87 | You must delete those records of table pictures first.'; 88 | END IF; 89 | END; 90 | }) 91 | end 92 | 93 | let(:create_trigger_sql) do 94 | subject.strip_non_essential_spaces(%{ 95 | CREATE TRIGGER check_imageable_insert_integrity 96 | BEFORE INSERT ON pictures 97 | FOR EACH ROW 98 | BEGIN 99 | IF NEW.imageable_type != 'Employee' AND NEW.imageable_type != 'Product' THEN 100 | SIGNAL SQLSTATE '45000' 101 | SET MESSAGE_TEXT = 'Polymorphic record not found. No model by that name.'; 102 | ELSEIF NEW.imageable_type = 'Employee' AND NOT EXISTS (SELECT id FROM employees WHERE id = NEW.imageable_id) THEN 103 | SIGNAL SQLSTATE '45000' 104 | SET MESSAGE_TEXT = 'Polymorphic record not found. No Employee with that id.'; 105 | ELSEIF NEW.imageable_type = 'Product' AND NOT EXISTS (SELECT id FROM products WHERE id = NEW.imageable_id) THEN 106 | SIGNAL SQLSTATE '45000' 107 | SET MESSAGE_TEXT = 'Polymorphic record not found. No Product with that id.'; 108 | END IF; 109 | END; 110 | }) 111 | end 112 | 113 | let(:update_trigger_sql) do 114 | subject.strip_non_essential_spaces(%{ 115 | CREATE TRIGGER check_imageable_update_integrity 116 | BEFORE UPDATE ON pictures 117 | FOR EACH ROW 118 | BEGIN 119 | IF NEW.imageable_type != 'Employee' AND NEW.imageable_type != 'Product' THEN 120 | SIGNAL SQLSTATE '45000' 121 | SET MESSAGE_TEXT = 'Polymorphic record not found. No model by that name.'; 122 | ELSEIF NEW.imageable_type = 'Employee' AND NOT EXISTS (SELECT id FROM employees WHERE id = NEW.imageable_id) THEN 123 | SIGNAL SQLSTATE '45000' 124 | SET MESSAGE_TEXT = 'Polymorphic record not found. No Employee with that id.'; 125 | ELSEIF NEW.imageable_type = 'Product' AND NOT EXISTS (SELECT id FROM products WHERE id = NEW.imageable_id) THEN 126 | SIGNAL SQLSTATE '45000' 127 | SET MESSAGE_TEXT = 'Polymorphic record not found. No Product with that id.'; 128 | END IF; 129 | END; 130 | }) 131 | end 132 | 133 | let(:create_trigger_sql_only_employee) do 134 | subject.strip_non_essential_spaces(%{ 135 | CREATE TRIGGER check_imageable_insert_integrity 136 | BEFORE INSERT ON pictures 137 | FOR EACH ROW 138 | BEGIN 139 | IF NEW.imageable_type != 'Employee' THEN 140 | SIGNAL SQLSTATE '45000' 141 | SET MESSAGE_TEXT = 'Polymorphic record not found. No model by that name.'; 142 | ELSEIF NEW.imageable_type = 'Employee' AND NOT EXISTS (SELECT id FROM employees WHERE id = NEW.imageable_id) THEN 143 | SIGNAL SQLSTATE '45000' 144 | SET MESSAGE_TEXT = 'Polymorphic record not found. No Employee with that id.'; 145 | END IF; 146 | END; 147 | }) 148 | end 149 | 150 | let(:update_trigger_sql_only_employee) do 151 | subject.strip_non_essential_spaces(%{ 152 | CREATE TRIGGER check_imageable_update_integrity 153 | BEFORE UPDATE ON pictures 154 | FOR EACH ROW 155 | BEGIN 156 | IF NEW.imageable_type != 'Employee' THEN 157 | SIGNAL SQLSTATE '45000' 158 | SET MESSAGE_TEXT = 'Polymorphic record not found. No model by that name.'; 159 | ELSEIF NEW.imageable_type = 'Employee' AND NOT EXISTS (SELECT id FROM employees WHERE id = NEW.imageable_id) THEN 160 | SIGNAL SQLSTATE '45000' 161 | SET MESSAGE_TEXT = 'Polymorphic record not found. No Employee with that id.'; 162 | END IF; 163 | END; 164 | }) 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /spec/lib/polymorphic_constraints/connection_adapters/postgresql_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'polymorphic_constraints/connection_adapters/postgresql_adapter' 3 | 4 | describe PolymorphicConstraints::ConnectionAdapters::PostgreSQLAdapter do 5 | 6 | class PostgresqlTestAdapter 7 | include Support::AdapterHelper 8 | include PolymorphicConstraints::ConnectionAdapters::PostgreSQLAdapter 9 | end 10 | 11 | subject { PostgresqlTestAdapter.new } 12 | 13 | it { is_expected.to respond_to(:supports_polymorphic_constraints?) } 14 | it { is_expected.to respond_to(:add_polymorphic_constraints) } 15 | it { is_expected.to respond_to(:remove_polymorphic_constraints) } 16 | 17 | describe 'add constraints' do 18 | it 'defaults to active_record_descendants search strategy' do 19 | expect(subject.add_polymorphic_constraints(:imageable, :pictures)).to eql([drop_triggers_sql, 20 | upsert_triggers_sql, 21 | delete_triggers_sql]) 22 | end 23 | 24 | it 'returns expected add constraints sql with polymorphic model options' do 25 | expect(subject.add_polymorphic_constraints(:imageable, :pictures, 26 | polymorphic_models: [:employee])).to eql([drop_triggers_sql, 27 | upsert_triggers_sql_only_employee, 28 | delete_triggers_sql_only_employee]) 29 | end 30 | end 31 | 32 | describe 'remove constraints' do 33 | it 'returns expected drop trigger sql' do 34 | expect(subject.remove_polymorphic_constraints(:imageable)).to eql([drop_triggers_sql]) 35 | end 36 | end 37 | 38 | let(:upsert_triggers_sql) do 39 | subject.strip_non_essential_spaces(%{ 40 | CREATE FUNCTION check_imageable_upsert_integrity() RETURNS TRIGGER AS ' 41 | BEGIN 42 | IF NEW.imageable_type = ''Employee'' AND 43 | EXISTS (SELECT id FROM employees WHERE id = NEW.imageable_id) THEN 44 | RETURN NEW; 45 | 46 | ELSEIF NEW.imageable_type = ''Product'' AND 47 | EXISTS (SELECT id FROM products WHERE id = NEW.imageable_id) THEN 48 | RETURN NEW; 49 | 50 | ELSE 51 | RAISE EXCEPTION ''Polymorphic record not found. 52 | No % model with id %.'', NEW.imageable_type, NEW.imageable_id; 53 | RETURN NULL; 54 | END IF; 55 | END' 56 | LANGUAGE plpgsql; 57 | 58 | CREATE TRIGGER check_imageable_upsert_integrity_trigger 59 | BEFORE INSERT OR UPDATE ON pictures 60 | FOR EACH ROW EXECUTE PROCEDURE check_imageable_upsert_integrity(); 61 | }) 62 | end 63 | 64 | let(:delete_triggers_sql) do 65 | subject.strip_non_essential_spaces(%{ 66 | CREATE FUNCTION check_imageable_delete_integrity() 67 | RETURNS TRIGGER AS ' 68 | BEGIN 69 | IF TG_TABLE_NAME = ''employees'' AND 70 | EXISTS (SELECT id FROM pictures 71 | WHERE imageable_type = ''Employee'' AND imageable_id = OLD.id) THEN 72 | 73 | RAISE EXCEPTION ''Polymorphic reference exists. 74 | There are records in pictures that refer to the table % with id %. 75 | You must delete those records of table pictures first.'', TG_TABLE_NAME, OLD.id; 76 | RETURN NULL; 77 | 78 | ELSEIF TG_TABLE_NAME = ''products'' AND 79 | EXISTS (SELECT id FROM pictures 80 | WHERE imageable_type = ''Product'' AND imageable_id = OLD.id) THEN 81 | 82 | RAISE EXCEPTION ''Polymorphic reference exists. 83 | There are records in pictures that refer to the table % with id %. 84 | You must delete those records of table pictures first.'', TG_TABLE_NAME, OLD.id; 85 | RETURN NULL; 86 | 87 | ELSE 88 | RETURN OLD; 89 | END IF; 90 | END' 91 | LANGUAGE plpgsql; 92 | 93 | CREATE TRIGGER check_imageable_employees_delete_integrity_trigger 94 | BEFORE DELETE ON employees 95 | FOR EACH ROW EXECUTE PROCEDURE check_imageable_delete_integrity(); 96 | 97 | CREATE TRIGGER check_imageable_products_delete_integrity_trigger 98 | BEFORE DELETE ON products 99 | FOR EACH ROW EXECUTE PROCEDURE check_imageable_delete_integrity(); 100 | }) 101 | end 102 | 103 | let(:upsert_triggers_sql_only_employee) do 104 | subject.strip_non_essential_spaces(%{ 105 | CREATE FUNCTION check_imageable_upsert_integrity() RETURNS TRIGGER AS ' 106 | BEGIN 107 | IF NEW.imageable_type = ''Employee'' AND 108 | EXISTS (SELECT id FROM employees WHERE id = NEW.imageable_id) THEN 109 | RETURN NEW; 110 | 111 | ELSE 112 | RAISE EXCEPTION ''Polymorphic record not found. 113 | No % model with id %.'', NEW.imageable_type, NEW.imageable_id; 114 | RETURN NULL; 115 | END IF; 116 | END' 117 | LANGUAGE plpgsql; 118 | 119 | CREATE TRIGGER check_imageable_upsert_integrity_trigger 120 | BEFORE INSERT OR UPDATE ON pictures 121 | FOR EACH ROW EXECUTE PROCEDURE check_imageable_upsert_integrity(); 122 | }) 123 | end 124 | 125 | let(:delete_triggers_sql_only_employee) do 126 | subject.strip_non_essential_spaces(%{ 127 | CREATE FUNCTION check_imageable_delete_integrity() 128 | RETURNS TRIGGER AS ' 129 | BEGIN 130 | IF TG_TABLE_NAME = ''employees'' AND 131 | EXISTS (SELECT id FROM pictures 132 | WHERE imageable_type = ''Employee'' AND imageable_id = OLD.id) THEN 133 | 134 | RAISE EXCEPTION ''Polymorphic reference exists. 135 | There are records in pictures that refer to the table % with id %. 136 | You must delete those records of table pictures first.'', TG_TABLE_NAME, OLD.id; 137 | RETURN NULL; 138 | 139 | ELSE 140 | RETURN OLD; 141 | END IF; 142 | END' 143 | LANGUAGE plpgsql; 144 | 145 | CREATE TRIGGER check_imageable_employees_delete_integrity_trigger 146 | BEFORE DELETE ON employees 147 | FOR EACH ROW EXECUTE PROCEDURE check_imageable_delete_integrity(); 148 | }) 149 | end 150 | 151 | let(:drop_triggers_sql) do 152 | subject.strip_non_essential_spaces(%{ 153 | DROP FUNCTION IF EXISTS check_imageable_upsert_integrity() 154 | CASCADE; 155 | DROP FUNCTION IF EXISTS check_imageable_delete_integrity() 156 | CASCADE; 157 | }) 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /spec/lib/polymorphic_constraints/connection_adapters/sqlite3_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'polymorphic_constraints/connection_adapters/sqlite3_adapter' 3 | 4 | describe PolymorphicConstraints::ConnectionAdapters::SQLite3Adapter do 5 | 6 | class SqliteTestAdapter 7 | include Support::AdapterHelper 8 | include PolymorphicConstraints::ConnectionAdapters::SQLite3Adapter 9 | end 10 | 11 | subject { SqliteTestAdapter.new } 12 | 13 | it { is_expected.to respond_to(:supports_polymorphic_constraints?) } 14 | it { is_expected.to respond_to(:add_polymorphic_constraints) } 15 | it { is_expected.to respond_to(:remove_polymorphic_constraints) } 16 | 17 | describe 'add constraints' do 18 | it 'defaults to active_record_descendants search strategy' do 19 | expect(subject.add_polymorphic_constraints(:imageable, :pictures)).to eql([drop_create_trigger_sql, 20 | drop_update_trigger_sql, 21 | drop_employees_delete_trigger_sql, 22 | drop_products_delete_trigger_sql, 23 | create_trigger_sql, 24 | update_trigger_sql, 25 | employees_delete_trigger_sql, 26 | products_delete_trigger_sql]) 27 | end 28 | 29 | it 'returns expected add constraints sql with polymorphic model options' do 30 | expect(subject.add_polymorphic_constraints(:imageable, :pictures, 31 | polymorphic_models: [:employee])).to eql([drop_create_trigger_sql, 32 | drop_update_trigger_sql, 33 | drop_employees_delete_trigger_sql, 34 | drop_products_delete_trigger_sql, 35 | create_trigger_sql_only_employee, 36 | update_trigger_sql_only_employee, 37 | employees_delete_trigger_sql]) 38 | end 39 | end 40 | 41 | describe 'remove constraints' do 42 | it 'defaults to active_record_descendants search strategy' do 43 | expect(subject.remove_polymorphic_constraints(:imageable)).to eql([drop_create_trigger_sql, 44 | drop_update_trigger_sql, 45 | drop_employees_delete_trigger_sql, 46 | drop_products_delete_trigger_sql]) 47 | end 48 | end 49 | 50 | let(:drop_create_trigger_sql) { 'DROP TRIGGER IF EXISTS check_imageable_insert_integrity;' } 51 | let(:drop_update_trigger_sql) { 'DROP TRIGGER IF EXISTS check_imageable_update_integrity;' } 52 | 53 | let(:drop_employees_delete_trigger_sql) { 'DROP TRIGGER IF EXISTS check_imageable_employees_delete_integrity;' } 54 | 55 | let(:employees_delete_trigger_sql) do 56 | subject.strip_non_essential_spaces(%{ 57 | CREATE TRIGGER check_imageable_employees_delete_integrity 58 | BEFORE DELETE ON employees 59 | BEGIN 60 | SELECT CASE 61 | WHEN EXISTS (SELECT id FROM pictures WHERE imageable_type = 'Employee' AND imageable_id = OLD.id) THEN 62 | RAISE(ABORT, 'Polymorphic reference exists. 63 | There are records in the pictures table that refer to the table employees. 64 | You must delete those records of table pictures first.') 65 | END; 66 | END; 67 | }) 68 | end 69 | 70 | let(:drop_products_delete_trigger_sql) { 'DROP TRIGGER IF EXISTS check_imageable_products_delete_integrity;' } 71 | 72 | let(:products_delete_trigger_sql) do 73 | subject.strip_non_essential_spaces(%{ 74 | CREATE TRIGGER check_imageable_products_delete_integrity 75 | BEFORE DELETE ON products 76 | BEGIN 77 | SELECT CASE 78 | WHEN EXISTS (SELECT id FROM pictures WHERE imageable_type = 'Product' AND imageable_id = OLD.id) THEN 79 | RAISE(ABORT, 'Polymorphic reference exists. 80 | There are records in the pictures table that refer to the table products. 81 | You must delete those records of table pictures first.') 82 | END; 83 | END; 84 | }) 85 | end 86 | 87 | let(:create_trigger_sql) do 88 | subject.strip_non_essential_spaces(%{ 89 | CREATE TRIGGER check_imageable_insert_integrity 90 | BEFORE INSERT ON pictures 91 | BEGIN 92 | SELECT CASE 93 | WHEN ( NEW.imageable_type != 'Employee' AND NEW.imageable_type != 'Product' ) THEN 94 | RAISE(ABORT, 'Polymorphic record not found. No model by that name.') 95 | WHEN ((NEW.imageable_type = 'Employee') AND NOT EXISTS (SELECT id FROM employees WHERE id = NEW.imageable_id)) THEN 96 | RAISE(ABORT, 'Polymorphic record not found. No Employee with that id.') 97 | WHEN ((NEW.imageable_type = 'Product') AND NOT EXISTS (SELECT id FROM products WHERE id = NEW.imageable_id)) THEN 98 | RAISE(ABORT, 'Polymorphic record not found. No Product with that id.') 99 | END; 100 | END; 101 | }) 102 | end 103 | 104 | let(:update_trigger_sql) do 105 | subject.strip_non_essential_spaces(%{ 106 | CREATE TRIGGER check_imageable_update_integrity 107 | BEFORE UPDATE ON pictures 108 | BEGIN 109 | SELECT CASE 110 | WHEN ( NEW.imageable_type != 'Employee' AND NEW.imageable_type != 'Product' ) THEN 111 | RAISE(ABORT, 'Polymorphic record not found. No model by that name.') 112 | WHEN ((NEW.imageable_type = 'Employee') AND NOT EXISTS (SELECT id FROM employees WHERE id = NEW.imageable_id)) THEN 113 | RAISE(ABORT, 'Polymorphic record not found. No Employee with that id.') 114 | WHEN ((NEW.imageable_type = 'Product') AND NOT EXISTS (SELECT id FROM products WHERE id = NEW.imageable_id)) THEN 115 | RAISE(ABORT, 'Polymorphic record not found. No Product with that id.') 116 | END; 117 | END; 118 | }) 119 | end 120 | 121 | let(:create_trigger_sql_only_employee) do 122 | subject.strip_non_essential_spaces(%{ 123 | CREATE TRIGGER check_imageable_insert_integrity 124 | BEFORE INSERT ON pictures 125 | BEGIN 126 | SELECT CASE 127 | WHEN ( NEW.imageable_type != 'Employee' ) THEN 128 | RAISE(ABORT, 'Polymorphic record not found. No model by that name.') 129 | WHEN ((NEW.imageable_type = 'Employee') AND NOT EXISTS (SELECT id FROM employees WHERE id = NEW.imageable_id)) THEN 130 | RAISE(ABORT, 'Polymorphic record not found. No Employee with that id.') 131 | END; 132 | END; 133 | }) 134 | end 135 | 136 | let(:update_trigger_sql_only_employee) do 137 | subject.strip_non_essential_spaces(%{ 138 | CREATE TRIGGER check_imageable_update_integrity 139 | BEFORE UPDATE ON pictures 140 | BEGIN 141 | SELECT CASE 142 | WHEN ( NEW.imageable_type != 'Employee' ) THEN 143 | RAISE(ABORT, 'Polymorphic record not found. No model by that name.') 144 | WHEN ((NEW.imageable_type = 'Employee') AND NOT EXISTS (SELECT id FROM employees WHERE id = NEW.imageable_id)) THEN 145 | RAISE(ABORT, 'Polymorphic record not found. No Employee with that id.') 146 | END; 147 | END; 148 | }) 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /spec/lib/polymorphic_constraints/utils/polymorphic_error_handler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'polymorphic error reraise' do 4 | class DummyController < ApplicationController 5 | def polymorphic_record_not_found 6 | raise ActiveRecord::StatementInvalid, 'Polymorphic record not found.' 7 | end 8 | 9 | def polymorphic_reference_exists 10 | raise ActiveRecord::StatementInvalid, 'Polymorphic reference exists.' 11 | end 12 | 13 | def not_a_polymorphic_error 14 | raise ActiveRecord::StatementInvalid, 'Not a Polymorphic Constraints error.' 15 | end 16 | end 17 | 18 | Rails.application.routes.draw do 19 | # The priority is based upon order of creation: first created -> highest priority. 20 | # See how all your routes lay out with "rake routes". 21 | get '/polymorphic_record_not_found', to: 'dummy#polymorphic_record_not_found' 22 | get '/polymorphic_reference_exists', to: 'dummy#polymorphic_reference_exists' 23 | get '/not_a_polymorphic_error', to: 'dummy#not_a_polymorphic_error' 24 | end 25 | 26 | describe DummyController, type: :controller do 27 | context 'polymorphic record not found' do 28 | it 're-raises ActiveRecord::RecordNotFound properly' do 29 | expect { get :polymorphic_record_not_found }.to raise_error(ActiveRecord::RecordNotFound) 30 | end 31 | end 32 | 33 | context 'polymorphic reference exists' do 34 | it 're-raises ActiveRecord::ReferenceViolation properly' do 35 | expect { get :polymorphic_reference_exists }.to raise_error(ActiveRecord::ReferenceViolation) 36 | end 37 | end 38 | 39 | context 'not a polymorphic constraints error' do 40 | it 're-raises ActiveRecord::StatementInvalid properly' do 41 | expect { get :not_a_polymorphic_error }.to raise_error(ActiveRecord::StatementInvalid) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | 3 | require 'dummy/config/environment' 4 | require_relative '../lib/polymorphic_constraints' 5 | require 'rspec/rails' 6 | require 'coveralls' 7 | 8 | Coveralls.wear! 9 | 10 | ActiveRecord::Migration.verbose = true 11 | Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require f } 12 | 13 | RSpec.configure do |config| 14 | config.after(:all) do 15 | drop_tables 16 | end 17 | end 18 | 19 | def setup_sqlite3 20 | connection_config = ActiveRecord::Base.connection_config 21 | ActiveRecord::Base.establish_connection(connection_config) 22 | migrate_db 23 | end 24 | 25 | def setup_postgresql 26 | connect_db 27 | migrate_db 28 | end 29 | 30 | def setup_mysql2 31 | connect_db 32 | migrate_db 33 | end 34 | 35 | private 36 | 37 | def connect_db 38 | connection_config = ActiveRecord::Base.connection_config 39 | ActiveRecord::Base.establish_connection(connection_config.merge(:database => nil)) 40 | ActiveRecord::Base.connection.recreate_database(connection_config[:database]) 41 | ActiveRecord::Base.establish_connection(connection_config) 42 | end 43 | 44 | def migrate_db 45 | ActiveRecord::Migrator.migrate(File.join(Rails.root, 'db/migrate')) 46 | end 47 | 48 | def drop_tables 49 | ActiveRecord::Base.connection.tables.each do |table| 50 | ActiveRecord::Base.connection.drop_table(table) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/support/adapter_helper.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | module AdapterHelper 3 | def execute(sql, name = nil) 4 | sql_statements << sql 5 | sql 6 | end 7 | 8 | def sql_statements 9 | @sql_statements ||= [] 10 | end 11 | 12 | def strip_non_essential_spaces(string_to_strip) 13 | string_to_strip.gsub(/\s{2,}|\\n/, " ").strip 14 | end 15 | end 16 | end 17 | --------------------------------------------------------------------------------