├── .gitignore ├── .rspec ├── .rubocop.yml ├── .simplecov ├── .travis.yml ├── Appraisals ├── CHANGES.md ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── active_record-postgres-constraints.gemspec ├── gemfiles ├── rails_5.0.gemfile ├── rails_5.0.gemfile.lock ├── rails_5.1.gemfile ├── rails_5.1.gemfile.lock ├── rails_5.2.gemfile ├── rails_5.2.gemfile.lock ├── rails_6.0.gemfile └── rails_6.0.gemfile.lock ├── lib └── active_record │ └── postgres │ ├── constraints.rb │ └── constraints │ ├── command_recorder.rb │ ├── postgresql_adapter.rb │ ├── railtie.rb │ ├── schema_creation.rb │ ├── schema_dumper.rb │ ├── table_definition.rb │ ├── types │ ├── check.rb │ └── exclude.rb │ └── version.rb └── spec ├── active_record └── postgres │ ├── constraints │ └── types │ │ ├── check_spec.rb │ │ └── exclude_spec.rb │ └── constraints_spec.rb ├── dummy ├── Rakefile ├── app │ └── models │ │ └── application_record.rb ├── bin │ └── rake ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ └── test.rb │ └── secrets.yml ├── db │ ├── migrate │ │ └── .keep │ └── schema.rb └── log │ └── .keep ├── rails_helper.rb ├── spec_helper.rb └── support ├── constraint_support.rb └── shared_migration_methods.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/examples.txt 9 | coverage 10 | .rake_tasks~ 11 | Gemfile.lock 12 | vendor 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: osm-rubocop 2 | 3 | AllCops: 4 | DisplayCopNames: true 5 | DisplayStyleGuide: true 6 | ExtraDetails: true 7 | TargetRubyVersion: 2.3 8 | Exclude: 9 | - spec/dummy/db/schema.rb 10 | - spec/dummy/db/migrate*/**/*.rb 11 | - gemfiles/*.gemfile 12 | - gemfiles/vendor/**/* 13 | - vendor/**/* 14 | 15 | # Just in case we end up supporting Rails 4.2, 16 | # we don't want to force a Ruby upgrade. 17 | Style/ExpandPathArguments: 18 | Enabled: false 19 | 20 | Style/ImplicitRuntimeError: 21 | Enabled: false 22 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | SimpleCov.start do 2 | load_profile "test_frameworks" 3 | 4 | track_files "lib/**/*.rb" 5 | end 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | # Based on https://github.com/grosser/parallel_tests/issues/649#issuecomment-487100470 3 | - POSTGRESQL_VERSION=9.6 4 | BUNDLE_DISABLE_EXEC_LOAD=true 5 | - POSTGRESQL_VERSION=10 6 | BUNDLE_DISABLE_EXEC_LOAD=true 7 | - POSTGRESQL_VERSION=11 8 | BUNDLE_DISABLE_EXEC_LOAD=true 9 | - POSTGRESQL_VERSION=12 10 | BUNDLE_DISABLE_EXEC_LOAD=true 11 | rvm: 12 | - 2.3.0 13 | - 2.4.0 14 | - 2.5.0 15 | - 2.6.0 16 | gemfile: 17 | - gemfiles/rails_5.0.gemfile 18 | - gemfiles/rails_5.1.gemfile 19 | - gemfiles/rails_5.2.gemfile 20 | - gemfiles/rails_6.0.gemfile 21 | matrix: 22 | exclude: 23 | - rvm: 2.3.0 24 | gemfile: gemfiles/rails_6.0.gemfile 25 | - rvm: 2.4.0 26 | gemfile: gemfiles/rails_6.0.gemfile 27 | before_install: 28 | - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - 29 | - echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list 30 | - sudo apt update 31 | - sudo apt install -yq --no-install-suggests --no-install-recommends postgresql-${POSTGRESQL_VERSION} 32 | - sudo sed -ri 's/^port.*/port = 5432/' /etc/postgresql/${POSTGRESQL_VERSION}/main/postgresql.conf 33 | - sudo sed -ri 's/peer/trust/' /etc/postgresql/${POSTGRESQL_VERSION}/main/pg_hba.conf 34 | - sudo /etc/init.d/postgresql stop 35 | - sudo systemctl start postgresql@${POSTGRESQL_VERSION}-main.service || (systemctl status postgresql.service; sudo journalctl -xe; exit 1) 36 | - pg_lsclusters -h 37 | - /usr/lib/postgresql/${POSTGRESQL_VERSION}/bin/psql -U postgres -c 'SHOW server_version;' 38 | - /usr/lib/postgresql/${POSTGRESQL_VERSION}/bin/psql -U postgres -c 'SHOW server_version;' | grep -P "(?<"'!'"[0-9.])${POSTGRES_VERSION}" 39 | - /usr/lib/postgresql/${POSTGRESQL_VERSION}/bin/psql -U postgres -c "SELECT * FROM pg_catalog.pg_roles where rolname = 'travis'" | grep travis || /usr/lib/postgresql/${POSTGRESQL_VERSION}/bin/psql -U postgres -c 'CREATE ROLE travis SUPERUSER LOGIN CREATEDB' 40 | - gem install --force bundler -v 1.17.3 41 | script: 42 | - bundle _1.17.3_ exec rubocop 43 | - sh -c 'cd spec/dummy && bundle _1.17.3_ exec bin/rake db:reset' 44 | - sh -c 'cd spec/dummy && bundle _1.17.3_ exec bin/rake parallel:create[3]' 45 | - bundle _1.17.3_ exec rspec 46 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | appraise 'rails-5.0' do 4 | gem 'rails', '~> 5.0.7' 5 | end 6 | 7 | appraise 'rails-5.1' do 8 | gem 'rails', '~> 5.1.6' 9 | end 10 | 11 | appraise 'rails-5.2' do 12 | gem 'rails', '~> 5.2.2' 13 | end 14 | 15 | appraise 'rails-6.0' do 16 | gem 'rails', '~> 6.0.0.rc2' 17 | end 18 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # v0.2.0 (Aug 8, 2019) 2 | * Support check constraints ([#1](https://github.com/on-site/active_record-postgres-constraints/pull/1) and [#15](https://github.com/on-site/active_record-postgres-constraints/pull/15), thanks [@ArthurWD](https://github.com/ArthurWD) for doing 99% of the work!) 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Declare your gem's dependencies in active_record-postgres-constraints.gemspec. 6 | # Bundler will treat runtime dependencies like base dependencies, and 7 | # development dependencies will be added by default to the :development group. 8 | gemspec 9 | 10 | # Declare any dependencies that are still in development here instead of in 11 | # your gemspec. These might include edge Rails or gems from your path or 12 | # Git. Remember to move these dependencies to your gemspec before releasing 13 | # your gem to rubygems.org. 14 | 15 | # To use a debugger 16 | gem 'byebug', '< 11.1.0', group: [:development, :test] # 11.1.0+ requires Ruby 2.4.0+ 17 | 18 | gem 'parallel_tests' 19 | gem 'pg', '< 1.0' # Higher versions are not compatible with Rails 5.1 20 | 21 | group :test do 22 | gem 'appraisal' 23 | gem 'simplecov', require: false 24 | end 25 | 26 | gem 'simplecov-html', '< 0.11', require: false # To remain compatible with Ruby < 2.4 27 | gem 'sprockets', '< 4' # To remain compatible with Ruby < 2.5 28 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Isaac Betesh 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 | # ActiveRecord::Postgres::Constraints 2 | 3 | This gem is no longer supported. [Here's why](https://github.com/betesh/active_record-postgres-constraints/issues/37#issuecomment-824539916) 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require 'bundler/setup' 5 | rescue LoadError 6 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 7 | end 8 | 9 | require 'bundler/gem_tasks' 10 | require 'parallel_tests/tasks' 11 | task default: :spec 12 | -------------------------------------------------------------------------------- /active_record-postgres-constraints.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.push File.expand_path('../lib', __FILE__) 4 | 5 | # Maintain your gem's version: 6 | require 'active_record/postgres/constraints/version' 7 | 8 | # Describe your gem and declare its dependencies: 9 | Gem::Specification.new do |s| 10 | s.name = 'active_record-postgres-constraints' 11 | s.version = ActiveRecord::Postgres::Constraints::VERSION 12 | s.authors = ['Isaac Betesh'] 13 | s.email = ['ibetesh@on-site.com'] 14 | s.homepage = 'https://github.com/on-site/active_record-postgres-constraints' 15 | s.summary = 'Store your constraints in db/schema.rb' 16 | s.description = %( 17 | From http://edgeguides.rubyonrails.org/active_record_migrations.html#types-of-schema-dumps: 18 | 19 | There is however a trade-off: db/schema.rb cannot express database 20 | specific items such as triggers, stored procedures or check constraints. 21 | While in a migration you can execute custom SQL statements, the schema 22 | dumper cannot reconstitute those statements from the database. If you are 23 | using features like this, then you should set the schema format to :sql. 24 | 25 | No longer is this the case. You can now use the default schema format 26 | (:ruby) and still preserve your check constraints. 27 | ) 28 | 29 | s.license = 'MIT' 30 | 31 | s.files = Dir[ 32 | '{app,config,db,lib}/**/*', 'MIT-LICENSE', 'Rakefile', 'README.md' 33 | ] 34 | 35 | s.add_dependency 'pg' 36 | s.add_dependency 'rails', '>= 5.0', '<= 7.0' 37 | 38 | s.add_development_dependency 'osm-rubocop', '= 0.1.16' 39 | s.add_development_dependency 'rspec', '~> 3.8' 40 | s.add_development_dependency 'rspec-rails' 41 | end 42 | -------------------------------------------------------------------------------- /gemfiles/rails_5.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "byebug", "< 11.1.0", group: [:development, :test] 6 | gem "parallel_tests" 7 | gem "pg", "< 1.0" 8 | gem "simplecov-html", "< 0.11", require: false 9 | gem "sprockets", "< 4" 10 | gem "rails", "~> 5.0.7" 11 | 12 | group :test do 13 | gem "appraisal" 14 | gem "simplecov", require: false 15 | end 16 | 17 | gemspec path: "../" 18 | -------------------------------------------------------------------------------- /gemfiles/rails_5.0.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | active_record-postgres-constraints (0.2.2) 5 | pg 6 | rails (>= 5.0, <= 7.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actioncable (5.0.7.2) 12 | actionpack (= 5.0.7.2) 13 | nio4r (>= 1.2, < 3.0) 14 | websocket-driver (~> 0.6.1) 15 | actionmailer (5.0.7.2) 16 | actionpack (= 5.0.7.2) 17 | actionview (= 5.0.7.2) 18 | activejob (= 5.0.7.2) 19 | mail (~> 2.5, >= 2.5.4) 20 | rails-dom-testing (~> 2.0) 21 | actionpack (5.0.7.2) 22 | actionview (= 5.0.7.2) 23 | activesupport (= 5.0.7.2) 24 | rack (~> 2.0) 25 | rack-test (~> 0.6.3) 26 | rails-dom-testing (~> 2.0) 27 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 28 | actionview (5.0.7.2) 29 | activesupport (= 5.0.7.2) 30 | builder (~> 3.1) 31 | erubis (~> 2.7.0) 32 | rails-dom-testing (~> 2.0) 33 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 34 | activejob (5.0.7.2) 35 | activesupport (= 5.0.7.2) 36 | globalid (>= 0.3.6) 37 | activemodel (5.0.7.2) 38 | activesupport (= 5.0.7.2) 39 | activerecord (5.0.7.2) 40 | activemodel (= 5.0.7.2) 41 | activesupport (= 5.0.7.2) 42 | arel (~> 7.0) 43 | activesupport (5.0.7.2) 44 | concurrent-ruby (~> 1.0, >= 1.0.2) 45 | i18n (>= 0.7, < 2) 46 | minitest (~> 5.1) 47 | tzinfo (~> 1.1) 48 | appraisal (2.2.0) 49 | bundler 50 | rake 51 | thor (>= 0.14.0) 52 | arel (7.1.4) 53 | ast (2.4.0) 54 | builder (3.2.4) 55 | byebug (11.0.1) 56 | concurrent-ruby (1.1.6) 57 | crass (1.0.6) 58 | diff-lcs (1.3) 59 | docile (1.3.2) 60 | erubis (2.7.0) 61 | globalid (0.4.2) 62 | activesupport (>= 4.2.0) 63 | i18n (1.8.2) 64 | concurrent-ruby (~> 1.0) 65 | jaro_winkler (1.5.4) 66 | json (2.3.0) 67 | loofah (2.4.0) 68 | crass (~> 1.0.2) 69 | nokogiri (>= 1.5.9) 70 | mail (2.7.1) 71 | mini_mime (>= 0.1.1) 72 | method_source (1.0.0) 73 | mini_mime (1.0.2) 74 | mini_portile2 (2.4.0) 75 | minitest (5.14.0) 76 | nio4r (2.5.2) 77 | nokogiri (1.10.9) 78 | mini_portile2 (~> 2.4.0) 79 | osm-rubocop (0.1.16) 80 | rubocop (= 0.79.0) 81 | rubocop-performance (= 1.5.2) 82 | rubocop-rails (= 2.4.2) 83 | rubocop-rspec (= 1.37.1) 84 | parallel (1.19.1) 85 | parallel_tests (2.32.0) 86 | parallel 87 | parser (2.7.0.5) 88 | ast (~> 2.4.0) 89 | pg (0.21.0) 90 | rack (2.2.2) 91 | rack-test (0.6.3) 92 | rack (>= 1.0) 93 | rails (5.0.7.2) 94 | actioncable (= 5.0.7.2) 95 | actionmailer (= 5.0.7.2) 96 | actionpack (= 5.0.7.2) 97 | actionview (= 5.0.7.2) 98 | activejob (= 5.0.7.2) 99 | activemodel (= 5.0.7.2) 100 | activerecord (= 5.0.7.2) 101 | activesupport (= 5.0.7.2) 102 | bundler (>= 1.3.0) 103 | railties (= 5.0.7.2) 104 | sprockets-rails (>= 2.0.0) 105 | rails-dom-testing (2.0.3) 106 | activesupport (>= 4.2.0) 107 | nokogiri (>= 1.6) 108 | rails-html-sanitizer (1.3.0) 109 | loofah (~> 2.3) 110 | railties (5.0.7.2) 111 | actionpack (= 5.0.7.2) 112 | activesupport (= 5.0.7.2) 113 | method_source 114 | rake (>= 0.8.7) 115 | thor (>= 0.18.1, < 2.0) 116 | rainbow (3.0.0) 117 | rake (13.0.1) 118 | rspec (3.9.0) 119 | rspec-core (~> 3.9.0) 120 | rspec-expectations (~> 3.9.0) 121 | rspec-mocks (~> 3.9.0) 122 | rspec-core (3.9.1) 123 | rspec-support (~> 3.9.1) 124 | rspec-expectations (3.9.1) 125 | diff-lcs (>= 1.2.0, < 2.0) 126 | rspec-support (~> 3.9.0) 127 | rspec-mocks (3.9.1) 128 | diff-lcs (>= 1.2.0, < 2.0) 129 | rspec-support (~> 3.9.0) 130 | rspec-rails (4.0.0) 131 | actionpack (>= 4.2) 132 | activesupport (>= 4.2) 133 | railties (>= 4.2) 134 | rspec-core (~> 3.9) 135 | rspec-expectations (~> 3.9) 136 | rspec-mocks (~> 3.9) 137 | rspec-support (~> 3.9) 138 | rspec-support (3.9.2) 139 | rubocop (0.79.0) 140 | jaro_winkler (~> 1.5.1) 141 | parallel (~> 1.10) 142 | parser (>= 2.7.0.1) 143 | rainbow (>= 2.2.2, < 4.0) 144 | ruby-progressbar (~> 1.7) 145 | unicode-display_width (>= 1.4.0, < 1.7) 146 | rubocop-performance (1.5.2) 147 | rubocop (>= 0.71.0) 148 | rubocop-rails (2.4.2) 149 | rack (>= 1.1) 150 | rubocop (>= 0.72.0) 151 | rubocop-rspec (1.37.1) 152 | rubocop (>= 0.68.1) 153 | ruby-progressbar (1.10.1) 154 | simplecov (0.17.1) 155 | docile (~> 1.1) 156 | json (>= 1.8, < 3) 157 | simplecov-html (~> 0.10.0) 158 | simplecov-html (0.10.2) 159 | sprockets (3.7.2) 160 | concurrent-ruby (~> 1.0) 161 | rack (> 1, < 3) 162 | sprockets-rails (3.2.1) 163 | actionpack (>= 4.0) 164 | activesupport (>= 4.0) 165 | sprockets (>= 3.0.0) 166 | thor (1.0.1) 167 | thread_safe (0.3.6) 168 | tzinfo (1.2.6) 169 | thread_safe (~> 0.1) 170 | unicode-display_width (1.6.1) 171 | websocket-driver (0.6.5) 172 | websocket-extensions (>= 0.1.0) 173 | websocket-extensions (0.1.4) 174 | 175 | PLATFORMS 176 | ruby 177 | 178 | DEPENDENCIES 179 | active_record-postgres-constraints! 180 | appraisal 181 | byebug (< 11.1.0) 182 | osm-rubocop (= 0.1.16) 183 | parallel_tests 184 | pg (< 1.0) 185 | rails (~> 5.0.7) 186 | rspec (~> 3.8) 187 | rspec-rails 188 | simplecov 189 | simplecov-html (< 0.11) 190 | sprockets (< 4) 191 | 192 | BUNDLED WITH 193 | 1.17.3 194 | -------------------------------------------------------------------------------- /gemfiles/rails_5.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "byebug", "< 11.1.0", group: [:development, :test] 6 | gem "parallel_tests" 7 | gem "pg", "< 1.0" 8 | gem "simplecov-html", "< 0.11", require: false 9 | gem "sprockets", "< 4" 10 | gem "rails", "~> 5.1.6" 11 | 12 | group :test do 13 | gem "appraisal" 14 | gem "simplecov", require: false 15 | end 16 | 17 | gemspec path: "../" 18 | -------------------------------------------------------------------------------- /gemfiles/rails_5.1.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | active_record-postgres-constraints (0.2.2) 5 | pg 6 | rails (>= 5.0, <= 7.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actioncable (5.1.7) 12 | actionpack (= 5.1.7) 13 | nio4r (~> 2.0) 14 | websocket-driver (~> 0.6.1) 15 | actionmailer (5.1.7) 16 | actionpack (= 5.1.7) 17 | actionview (= 5.1.7) 18 | activejob (= 5.1.7) 19 | mail (~> 2.5, >= 2.5.4) 20 | rails-dom-testing (~> 2.0) 21 | actionpack (5.1.7) 22 | actionview (= 5.1.7) 23 | activesupport (= 5.1.7) 24 | rack (~> 2.0) 25 | rack-test (>= 0.6.3) 26 | rails-dom-testing (~> 2.0) 27 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 28 | actionview (5.1.7) 29 | activesupport (= 5.1.7) 30 | builder (~> 3.1) 31 | erubi (~> 1.4) 32 | rails-dom-testing (~> 2.0) 33 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 34 | activejob (5.1.7) 35 | activesupport (= 5.1.7) 36 | globalid (>= 0.3.6) 37 | activemodel (5.1.7) 38 | activesupport (= 5.1.7) 39 | activerecord (5.1.7) 40 | activemodel (= 5.1.7) 41 | activesupport (= 5.1.7) 42 | arel (~> 8.0) 43 | activesupport (5.1.7) 44 | concurrent-ruby (~> 1.0, >= 1.0.2) 45 | i18n (>= 0.7, < 2) 46 | minitest (~> 5.1) 47 | tzinfo (~> 1.1) 48 | appraisal (2.2.0) 49 | bundler 50 | rake 51 | thor (>= 0.14.0) 52 | arel (8.0.0) 53 | ast (2.4.0) 54 | builder (3.2.4) 55 | byebug (11.0.1) 56 | concurrent-ruby (1.1.6) 57 | crass (1.0.6) 58 | diff-lcs (1.3) 59 | docile (1.3.2) 60 | erubi (1.9.0) 61 | globalid (0.4.2) 62 | activesupport (>= 4.2.0) 63 | i18n (1.8.2) 64 | concurrent-ruby (~> 1.0) 65 | jaro_winkler (1.5.4) 66 | json (2.3.0) 67 | loofah (2.4.0) 68 | crass (~> 1.0.2) 69 | nokogiri (>= 1.5.9) 70 | mail (2.7.1) 71 | mini_mime (>= 0.1.1) 72 | method_source (1.0.0) 73 | mini_mime (1.0.2) 74 | mini_portile2 (2.4.0) 75 | minitest (5.14.0) 76 | nio4r (2.5.2) 77 | nokogiri (1.10.9) 78 | mini_portile2 (~> 2.4.0) 79 | osm-rubocop (0.1.16) 80 | rubocop (= 0.79.0) 81 | rubocop-performance (= 1.5.2) 82 | rubocop-rails (= 2.4.2) 83 | rubocop-rspec (= 1.37.1) 84 | parallel (1.19.1) 85 | parallel_tests (2.32.0) 86 | parallel 87 | parser (2.7.0.5) 88 | ast (~> 2.4.0) 89 | pg (0.21.0) 90 | rack (2.2.2) 91 | rack-test (1.1.0) 92 | rack (>= 1.0, < 3) 93 | rails (5.1.7) 94 | actioncable (= 5.1.7) 95 | actionmailer (= 5.1.7) 96 | actionpack (= 5.1.7) 97 | actionview (= 5.1.7) 98 | activejob (= 5.1.7) 99 | activemodel (= 5.1.7) 100 | activerecord (= 5.1.7) 101 | activesupport (= 5.1.7) 102 | bundler (>= 1.3.0) 103 | railties (= 5.1.7) 104 | sprockets-rails (>= 2.0.0) 105 | rails-dom-testing (2.0.3) 106 | activesupport (>= 4.2.0) 107 | nokogiri (>= 1.6) 108 | rails-html-sanitizer (1.3.0) 109 | loofah (~> 2.3) 110 | railties (5.1.7) 111 | actionpack (= 5.1.7) 112 | activesupport (= 5.1.7) 113 | method_source 114 | rake (>= 0.8.7) 115 | thor (>= 0.18.1, < 2.0) 116 | rainbow (3.0.0) 117 | rake (13.0.1) 118 | rspec (3.9.0) 119 | rspec-core (~> 3.9.0) 120 | rspec-expectations (~> 3.9.0) 121 | rspec-mocks (~> 3.9.0) 122 | rspec-core (3.9.1) 123 | rspec-support (~> 3.9.1) 124 | rspec-expectations (3.9.1) 125 | diff-lcs (>= 1.2.0, < 2.0) 126 | rspec-support (~> 3.9.0) 127 | rspec-mocks (3.9.1) 128 | diff-lcs (>= 1.2.0, < 2.0) 129 | rspec-support (~> 3.9.0) 130 | rspec-rails (4.0.0) 131 | actionpack (>= 4.2) 132 | activesupport (>= 4.2) 133 | railties (>= 4.2) 134 | rspec-core (~> 3.9) 135 | rspec-expectations (~> 3.9) 136 | rspec-mocks (~> 3.9) 137 | rspec-support (~> 3.9) 138 | rspec-support (3.9.2) 139 | rubocop (0.79.0) 140 | jaro_winkler (~> 1.5.1) 141 | parallel (~> 1.10) 142 | parser (>= 2.7.0.1) 143 | rainbow (>= 2.2.2, < 4.0) 144 | ruby-progressbar (~> 1.7) 145 | unicode-display_width (>= 1.4.0, < 1.7) 146 | rubocop-performance (1.5.2) 147 | rubocop (>= 0.71.0) 148 | rubocop-rails (2.4.2) 149 | rack (>= 1.1) 150 | rubocop (>= 0.72.0) 151 | rubocop-rspec (1.37.1) 152 | rubocop (>= 0.68.1) 153 | ruby-progressbar (1.10.1) 154 | simplecov (0.17.1) 155 | docile (~> 1.1) 156 | json (>= 1.8, < 3) 157 | simplecov-html (~> 0.10.0) 158 | simplecov-html (0.10.2) 159 | sprockets (3.7.2) 160 | concurrent-ruby (~> 1.0) 161 | rack (> 1, < 3) 162 | sprockets-rails (3.2.1) 163 | actionpack (>= 4.0) 164 | activesupport (>= 4.0) 165 | sprockets (>= 3.0.0) 166 | thor (1.0.1) 167 | thread_safe (0.3.6) 168 | tzinfo (1.2.6) 169 | thread_safe (~> 0.1) 170 | unicode-display_width (1.6.1) 171 | websocket-driver (0.6.5) 172 | websocket-extensions (>= 0.1.0) 173 | websocket-extensions (0.1.4) 174 | 175 | PLATFORMS 176 | ruby 177 | 178 | DEPENDENCIES 179 | active_record-postgres-constraints! 180 | appraisal 181 | byebug (< 11.1.0) 182 | osm-rubocop (= 0.1.16) 183 | parallel_tests 184 | pg (< 1.0) 185 | rails (~> 5.1.6) 186 | rspec (~> 3.8) 187 | rspec-rails 188 | simplecov 189 | simplecov-html (< 0.11) 190 | sprockets (< 4) 191 | 192 | BUNDLED WITH 193 | 1.17.3 194 | -------------------------------------------------------------------------------- /gemfiles/rails_5.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "byebug", "< 11.1.0", group: [:development, :test] 6 | gem "parallel_tests" 7 | gem "pg", "< 1.0" 8 | gem "simplecov-html", "< 0.11", require: false 9 | gem "sprockets", "< 4" 10 | gem "rails", "~> 5.2.2" 11 | 12 | group :test do 13 | gem "appraisal" 14 | gem "simplecov", require: false 15 | end 16 | 17 | gemspec path: "../" 18 | -------------------------------------------------------------------------------- /gemfiles/rails_5.2.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | active_record-postgres-constraints (0.2.2) 5 | pg 6 | rails (>= 5.0, <= 7.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actioncable (5.2.3) 12 | actionpack (= 5.2.3) 13 | nio4r (~> 2.0) 14 | websocket-driver (>= 0.6.1) 15 | actionmailer (5.2.3) 16 | actionpack (= 5.2.3) 17 | actionview (= 5.2.3) 18 | activejob (= 5.2.3) 19 | mail (~> 2.5, >= 2.5.4) 20 | rails-dom-testing (~> 2.0) 21 | actionpack (5.2.3) 22 | actionview (= 5.2.3) 23 | activesupport (= 5.2.3) 24 | rack (~> 2.0) 25 | rack-test (>= 0.6.3) 26 | rails-dom-testing (~> 2.0) 27 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 28 | actionview (5.2.3) 29 | activesupport (= 5.2.3) 30 | builder (~> 3.1) 31 | erubi (~> 1.4) 32 | rails-dom-testing (~> 2.0) 33 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 34 | activejob (5.2.3) 35 | activesupport (= 5.2.3) 36 | globalid (>= 0.3.6) 37 | activemodel (5.2.3) 38 | activesupport (= 5.2.3) 39 | activerecord (5.2.3) 40 | activemodel (= 5.2.3) 41 | activesupport (= 5.2.3) 42 | arel (>= 9.0) 43 | activestorage (5.2.3) 44 | actionpack (= 5.2.3) 45 | activerecord (= 5.2.3) 46 | marcel (~> 0.3.1) 47 | activesupport (5.2.3) 48 | concurrent-ruby (~> 1.0, >= 1.0.2) 49 | i18n (>= 0.7, < 2) 50 | minitest (~> 5.1) 51 | tzinfo (~> 1.1) 52 | appraisal (2.2.0) 53 | bundler 54 | rake 55 | thor (>= 0.14.0) 56 | arel (9.0.0) 57 | ast (2.4.0) 58 | builder (3.2.4) 59 | byebug (11.0.1) 60 | concurrent-ruby (1.1.6) 61 | crass (1.0.6) 62 | diff-lcs (1.3) 63 | docile (1.3.2) 64 | erubi (1.9.0) 65 | globalid (0.4.2) 66 | activesupport (>= 4.2.0) 67 | i18n (1.8.2) 68 | concurrent-ruby (~> 1.0) 69 | jaro_winkler (1.5.4) 70 | json (2.3.0) 71 | loofah (2.4.0) 72 | crass (~> 1.0.2) 73 | nokogiri (>= 1.5.9) 74 | mail (2.7.1) 75 | mini_mime (>= 0.1.1) 76 | marcel (0.3.3) 77 | mimemagic (~> 0.3.2) 78 | method_source (1.0.0) 79 | mimemagic (0.3.4) 80 | mini_mime (1.0.2) 81 | mini_portile2 (2.4.0) 82 | minitest (5.14.0) 83 | nio4r (2.5.2) 84 | nokogiri (1.10.9) 85 | mini_portile2 (~> 2.4.0) 86 | osm-rubocop (0.1.16) 87 | rubocop (= 0.79.0) 88 | rubocop-performance (= 1.5.2) 89 | rubocop-rails (= 2.4.2) 90 | rubocop-rspec (= 1.37.1) 91 | parallel (1.19.1) 92 | parallel_tests (2.32.0) 93 | parallel 94 | parser (2.7.0.5) 95 | ast (~> 2.4.0) 96 | pg (0.21.0) 97 | rack (2.2.2) 98 | rack-test (1.1.0) 99 | rack (>= 1.0, < 3) 100 | rails (5.2.3) 101 | actioncable (= 5.2.3) 102 | actionmailer (= 5.2.3) 103 | actionpack (= 5.2.3) 104 | actionview (= 5.2.3) 105 | activejob (= 5.2.3) 106 | activemodel (= 5.2.3) 107 | activerecord (= 5.2.3) 108 | activestorage (= 5.2.3) 109 | activesupport (= 5.2.3) 110 | bundler (>= 1.3.0) 111 | railties (= 5.2.3) 112 | sprockets-rails (>= 2.0.0) 113 | rails-dom-testing (2.0.3) 114 | activesupport (>= 4.2.0) 115 | nokogiri (>= 1.6) 116 | rails-html-sanitizer (1.3.0) 117 | loofah (~> 2.3) 118 | railties (5.2.3) 119 | actionpack (= 5.2.3) 120 | activesupport (= 5.2.3) 121 | method_source 122 | rake (>= 0.8.7) 123 | thor (>= 0.19.0, < 2.0) 124 | rainbow (3.0.0) 125 | rake (13.0.1) 126 | rspec (3.9.0) 127 | rspec-core (~> 3.9.0) 128 | rspec-expectations (~> 3.9.0) 129 | rspec-mocks (~> 3.9.0) 130 | rspec-core (3.9.1) 131 | rspec-support (~> 3.9.1) 132 | rspec-expectations (3.9.1) 133 | diff-lcs (>= 1.2.0, < 2.0) 134 | rspec-support (~> 3.9.0) 135 | rspec-mocks (3.9.1) 136 | diff-lcs (>= 1.2.0, < 2.0) 137 | rspec-support (~> 3.9.0) 138 | rspec-rails (4.0.0) 139 | actionpack (>= 4.2) 140 | activesupport (>= 4.2) 141 | railties (>= 4.2) 142 | rspec-core (~> 3.9) 143 | rspec-expectations (~> 3.9) 144 | rspec-mocks (~> 3.9) 145 | rspec-support (~> 3.9) 146 | rspec-support (3.9.2) 147 | rubocop (0.79.0) 148 | jaro_winkler (~> 1.5.1) 149 | parallel (~> 1.10) 150 | parser (>= 2.7.0.1) 151 | rainbow (>= 2.2.2, < 4.0) 152 | ruby-progressbar (~> 1.7) 153 | unicode-display_width (>= 1.4.0, < 1.7) 154 | rubocop-performance (1.5.2) 155 | rubocop (>= 0.71.0) 156 | rubocop-rails (2.4.2) 157 | rack (>= 1.1) 158 | rubocop (>= 0.72.0) 159 | rubocop-rspec (1.37.1) 160 | rubocop (>= 0.68.1) 161 | ruby-progressbar (1.10.1) 162 | simplecov (0.17.1) 163 | docile (~> 1.1) 164 | json (>= 1.8, < 3) 165 | simplecov-html (~> 0.10.0) 166 | simplecov-html (0.10.2) 167 | sprockets (3.7.2) 168 | concurrent-ruby (~> 1.0) 169 | rack (> 1, < 3) 170 | sprockets-rails (3.2.1) 171 | actionpack (>= 4.0) 172 | activesupport (>= 4.0) 173 | sprockets (>= 3.0.0) 174 | thor (1.0.1) 175 | thread_safe (0.3.6) 176 | tzinfo (1.2.6) 177 | thread_safe (~> 0.1) 178 | unicode-display_width (1.6.1) 179 | websocket-driver (0.7.1) 180 | websocket-extensions (>= 0.1.0) 181 | websocket-extensions (0.1.4) 182 | 183 | PLATFORMS 184 | ruby 185 | 186 | DEPENDENCIES 187 | active_record-postgres-constraints! 188 | appraisal 189 | byebug (< 11.1.0) 190 | osm-rubocop (= 0.1.16) 191 | parallel_tests 192 | pg (< 1.0) 193 | rails (~> 5.2.2) 194 | rspec (~> 3.8) 195 | rspec-rails 196 | simplecov 197 | simplecov-html (< 0.11) 198 | sprockets (< 4) 199 | 200 | BUNDLED WITH 201 | 1.17.3 202 | -------------------------------------------------------------------------------- /gemfiles/rails_6.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "byebug", "< 11.1.0", group: [:development, :test] 6 | gem "parallel_tests" 7 | gem "pg", "< 1.0" 8 | gem "simplecov-html", "< 0.11", require: false 9 | gem "sprockets", "< 4" 10 | gem "rails", "~> 6.0.0.rc2" 11 | 12 | group :test do 13 | gem "appraisal" 14 | gem "simplecov", require: false 15 | end 16 | 17 | gemspec path: "../" 18 | -------------------------------------------------------------------------------- /gemfiles/rails_6.0.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | active_record-postgres-constraints (0.2.2) 5 | pg 6 | rails (>= 5.0, <= 7.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actioncable (6.0.2.2) 12 | actionpack (= 6.0.2.2) 13 | nio4r (~> 2.0) 14 | websocket-driver (>= 0.6.1) 15 | actionmailbox (6.0.2.2) 16 | actionpack (= 6.0.2.2) 17 | activejob (= 6.0.2.2) 18 | activerecord (= 6.0.2.2) 19 | activestorage (= 6.0.2.2) 20 | activesupport (= 6.0.2.2) 21 | mail (>= 2.7.1) 22 | actionmailer (6.0.2.2) 23 | actionpack (= 6.0.2.2) 24 | actionview (= 6.0.2.2) 25 | activejob (= 6.0.2.2) 26 | mail (~> 2.5, >= 2.5.4) 27 | rails-dom-testing (~> 2.0) 28 | actionpack (6.0.2.2) 29 | actionview (= 6.0.2.2) 30 | activesupport (= 6.0.2.2) 31 | rack (~> 2.0, >= 2.0.8) 32 | rack-test (>= 0.6.3) 33 | rails-dom-testing (~> 2.0) 34 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 35 | actiontext (6.0.2.2) 36 | actionpack (= 6.0.2.2) 37 | activerecord (= 6.0.2.2) 38 | activestorage (= 6.0.2.2) 39 | activesupport (= 6.0.2.2) 40 | nokogiri (>= 1.8.5) 41 | actionview (6.0.2.2) 42 | activesupport (= 6.0.2.2) 43 | builder (~> 3.1) 44 | erubi (~> 1.4) 45 | rails-dom-testing (~> 2.0) 46 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 47 | activejob (6.0.2.2) 48 | activesupport (= 6.0.2.2) 49 | globalid (>= 0.3.6) 50 | activemodel (6.0.2.2) 51 | activesupport (= 6.0.2.2) 52 | activerecord (6.0.2.2) 53 | activemodel (= 6.0.2.2) 54 | activesupport (= 6.0.2.2) 55 | activestorage (6.0.2.2) 56 | actionpack (= 6.0.2.2) 57 | activejob (= 6.0.2.2) 58 | activerecord (= 6.0.2.2) 59 | marcel (~> 0.3.1) 60 | activesupport (6.0.2.2) 61 | concurrent-ruby (~> 1.0, >= 1.0.2) 62 | i18n (>= 0.7, < 2) 63 | minitest (~> 5.1) 64 | tzinfo (~> 1.1) 65 | zeitwerk (~> 2.2) 66 | appraisal (2.2.0) 67 | bundler 68 | rake 69 | thor (>= 0.14.0) 70 | ast (2.4.0) 71 | builder (3.2.4) 72 | byebug (11.0.1) 73 | concurrent-ruby (1.1.6) 74 | crass (1.0.6) 75 | diff-lcs (1.3) 76 | docile (1.3.2) 77 | erubi (1.9.0) 78 | globalid (0.4.2) 79 | activesupport (>= 4.2.0) 80 | i18n (1.8.2) 81 | concurrent-ruby (~> 1.0) 82 | jaro_winkler (1.5.4) 83 | json (2.3.0) 84 | loofah (2.4.0) 85 | crass (~> 1.0.2) 86 | nokogiri (>= 1.5.9) 87 | mail (2.7.1) 88 | mini_mime (>= 0.1.1) 89 | marcel (0.3.3) 90 | mimemagic (~> 0.3.2) 91 | method_source (1.0.0) 92 | mimemagic (0.3.4) 93 | mini_mime (1.0.2) 94 | mini_portile2 (2.4.0) 95 | minitest (5.14.0) 96 | nio4r (2.5.2) 97 | nokogiri (1.10.9) 98 | mini_portile2 (~> 2.4.0) 99 | osm-rubocop (0.1.16) 100 | rubocop (= 0.79.0) 101 | rubocop-performance (= 1.5.2) 102 | rubocop-rails (= 2.4.2) 103 | rubocop-rspec (= 1.37.1) 104 | parallel (1.19.1) 105 | parallel_tests (2.32.0) 106 | parallel 107 | parser (2.7.0.5) 108 | ast (~> 2.4.0) 109 | pg (0.21.0) 110 | rack (2.2.2) 111 | rack-test (1.1.0) 112 | rack (>= 1.0, < 3) 113 | rails (6.0.2.2) 114 | actioncable (= 6.0.2.2) 115 | actionmailbox (= 6.0.2.2) 116 | actionmailer (= 6.0.2.2) 117 | actionpack (= 6.0.2.2) 118 | actiontext (= 6.0.2.2) 119 | actionview (= 6.0.2.2) 120 | activejob (= 6.0.2.2) 121 | activemodel (= 6.0.2.2) 122 | activerecord (= 6.0.2.2) 123 | activestorage (= 6.0.2.2) 124 | activesupport (= 6.0.2.2) 125 | bundler (>= 1.3.0) 126 | railties (= 6.0.2.2) 127 | sprockets-rails (>= 2.0.0) 128 | rails-dom-testing (2.0.3) 129 | activesupport (>= 4.2.0) 130 | nokogiri (>= 1.6) 131 | rails-html-sanitizer (1.3.0) 132 | loofah (~> 2.3) 133 | railties (6.0.2.2) 134 | actionpack (= 6.0.2.2) 135 | activesupport (= 6.0.2.2) 136 | method_source 137 | rake (>= 0.8.7) 138 | thor (>= 0.20.3, < 2.0) 139 | rainbow (3.0.0) 140 | rake (13.0.1) 141 | rspec (3.9.0) 142 | rspec-core (~> 3.9.0) 143 | rspec-expectations (~> 3.9.0) 144 | rspec-mocks (~> 3.9.0) 145 | rspec-core (3.9.1) 146 | rspec-support (~> 3.9.1) 147 | rspec-expectations (3.9.1) 148 | diff-lcs (>= 1.2.0, < 2.0) 149 | rspec-support (~> 3.9.0) 150 | rspec-mocks (3.9.1) 151 | diff-lcs (>= 1.2.0, < 2.0) 152 | rspec-support (~> 3.9.0) 153 | rspec-rails (4.0.0) 154 | actionpack (>= 4.2) 155 | activesupport (>= 4.2) 156 | railties (>= 4.2) 157 | rspec-core (~> 3.9) 158 | rspec-expectations (~> 3.9) 159 | rspec-mocks (~> 3.9) 160 | rspec-support (~> 3.9) 161 | rspec-support (3.9.2) 162 | rubocop (0.79.0) 163 | jaro_winkler (~> 1.5.1) 164 | parallel (~> 1.10) 165 | parser (>= 2.7.0.1) 166 | rainbow (>= 2.2.2, < 4.0) 167 | ruby-progressbar (~> 1.7) 168 | unicode-display_width (>= 1.4.0, < 1.7) 169 | rubocop-performance (1.5.2) 170 | rubocop (>= 0.71.0) 171 | rubocop-rails (2.4.2) 172 | rack (>= 1.1) 173 | rubocop (>= 0.72.0) 174 | rubocop-rspec (1.37.1) 175 | rubocop (>= 0.68.1) 176 | ruby-progressbar (1.10.1) 177 | simplecov (0.17.1) 178 | docile (~> 1.1) 179 | json (>= 1.8, < 3) 180 | simplecov-html (~> 0.10.0) 181 | simplecov-html (0.10.2) 182 | sprockets (3.7.2) 183 | concurrent-ruby (~> 1.0) 184 | rack (> 1, < 3) 185 | sprockets-rails (3.2.1) 186 | actionpack (>= 4.0) 187 | activesupport (>= 4.0) 188 | sprockets (>= 3.0.0) 189 | thor (1.0.1) 190 | thread_safe (0.3.6) 191 | tzinfo (1.2.6) 192 | thread_safe (~> 0.1) 193 | unicode-display_width (1.6.1) 194 | websocket-driver (0.7.1) 195 | websocket-extensions (>= 0.1.0) 196 | websocket-extensions (0.1.4) 197 | zeitwerk (2.3.0) 198 | 199 | PLATFORMS 200 | ruby 201 | 202 | DEPENDENCIES 203 | active_record-postgres-constraints! 204 | appraisal 205 | byebug (< 11.1.0) 206 | osm-rubocop (= 0.1.16) 207 | parallel_tests 208 | pg (< 1.0) 209 | rails (~> 6.0.0.rc2) 210 | rspec (~> 3.8) 211 | rspec-rails 212 | simplecov 213 | simplecov-html (< 0.11) 214 | sprockets (< 4) 215 | 216 | BUNDLED WITH 217 | 1.17.3 218 | -------------------------------------------------------------------------------- /lib/active_record/postgres/constraints.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Postgres 5 | module Constraints 6 | CONSTRAINT_TYPES = { 7 | check: 'c', 8 | exclude: 'x', 9 | }.freeze 10 | 11 | def self.class_for_constraint_type(type) 12 | 'ActiveRecord::Postgres::Constraints::Types::'\ 13 | "#{type.to_s.classify}".constantize 14 | end 15 | 16 | def self.normalize_name_and_conditions(table, name_or_conditions, conditions) 17 | return [name_or_conditions, conditions] if conditions 18 | 19 | ["#{table}_#{Time.zone.now.nsec}", name_or_conditions] 20 | end 21 | end 22 | end 23 | end 24 | 25 | require_relative 'constraints/command_recorder' 26 | require_relative 'constraints/postgresql_adapter' 27 | require_relative 'constraints/railtie' 28 | require_relative 'constraints/schema_creation' 29 | require_relative 'constraints/schema_dumper' 30 | require_relative 'constraints/table_definition' 31 | require_relative 'constraints/types/check' 32 | require_relative 'constraints/types/exclude' 33 | require_relative 'constraints/version' 34 | -------------------------------------------------------------------------------- /lib/active_record/postgres/constraints/command_recorder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Postgres 5 | module Constraints 6 | module CommandRecorder 7 | CONSTRAINT_TYPES.keys.each do |type| 8 | define_method("add_#{type}_constraint") do |*args, &block| 9 | record(:"add_#{type}_constraint", args, &block) 10 | end 11 | 12 | define_method("invert_add_#{type}_constraint") do |args, &block| 13 | [:"remove_#{type}_constraint", args, block] 14 | end 15 | 16 | define_method("remove_#{type}_constraint") do |*args, &block| 17 | if args.length < 3 18 | example_constraint = ActiveRecord::Postgres::Constraints. 19 | class_for_constraint_type(type). 20 | example_constraint 21 | 22 | raise ActiveRecord::IrreversibleMigration, 23 | 'To make this migration reversible, pass the constraint to '\ 24 | "remove_#{type}_constraint, i.e. `remove_#{type}_constraint "\ 25 | "#{args[0].inspect}, #{args[1].inspect}, #{example_constraint}`" 26 | end 27 | record(:"remove_#{type}_constraint", args, &block) 28 | end 29 | 30 | define_method("invert_remove_#{type}_constraint") do |args, &block| 31 | [:"add_#{type}_constraint", args, block] 32 | end 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/active_record/postgres/constraints/postgresql_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Postgres 5 | module Constraints 6 | module PostgreSQLAdapter 7 | CONSTRAINT_TYPES.keys.each do |type| 8 | define_method "add_#{type}_constraint" do |table, name_or_conditions, conditions = nil| 9 | add_constraint(type, table, name_or_conditions, conditions) 10 | end 11 | 12 | define_method "remove_#{type}_constraint" do |table, name, conditions = nil| 13 | remove_constraint(type, table, name, conditions) 14 | end 15 | end 16 | 17 | def add_constraint(type, table, name_or_conditions, conditions) 18 | constraint = 19 | ActiveRecord::Postgres::Constraints. 20 | class_for_constraint_type(type). 21 | to_sql(table, name_or_conditions, conditions) 22 | execute("ALTER TABLE #{table} ADD #{constraint}") 23 | end 24 | 25 | def remove_constraint(_type, table, name, _conditions) 26 | execute("ALTER TABLE #{table} DROP CONSTRAINT #{name}") 27 | end 28 | 29 | def constraints(table) 30 | types = CONSTRAINT_TYPES.values.map { |v| "'#{v}'" }.join(', ') 31 | sql = "SELECT conname, contype, 32 | pg_get_constraintdef(pg_constraint.oid) AS definition 33 | FROM pg_constraint 34 | JOIN pg_class ON pg_constraint.conrelid = pg_class.oid 35 | WHERE 36 | pg_constraint.contype IN (#{types}) 37 | AND 38 | pg_class.relname = '#{table}'".tr("\n", ' ').squeeze(' ') 39 | execute sql 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/active_record/postgres/constraints/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined?(::Rails::Railtie) 4 | module ActiveRecord 5 | module Postgres 6 | module Constraints 7 | class Railtie < ::Rails::Railtie 8 | initializer 'active_record.postgres.constraints.patch_active_record' do |*_args| 9 | engine = self 10 | ActiveSupport.on_load(:active_record) do 11 | AR_CAS = ::ActiveRecord::ConnectionAdapters 12 | 13 | engine.apply_patch! if engine.pg? 14 | end 15 | end 16 | 17 | def apply_patch! 18 | Rails.logger.info do 19 | 'Applying Postgres Constraints patches to ActiveRecord' 20 | end 21 | AR_CAS::TableDefinition.include TableDefinition 22 | AR_CAS::PostgreSQLAdapter.include PostgreSQLAdapter 23 | AR_CAS::AbstractAdapter::SchemaCreation.prepend SchemaCreation 24 | 25 | ::ActiveRecord::Migration::CommandRecorder.include CommandRecorder 26 | ::ActiveRecord::SchemaDumper.prepend SchemaDumper 27 | end 28 | 29 | def pg? 30 | config = ActiveRecord::Base.connection_config 31 | return true if config && config[:adapter].in?(%w[postgresql postgis]) 32 | 33 | Rails.logger.warn do 34 | 'Not applying Postgres Constraints patches to ActiveRecord ' \ 35 | 'since the database is not postgres' 36 | end 37 | false 38 | end 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/active_record/postgres/constraints/schema_creation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Postgres 5 | module Constraints 6 | module SchemaCreation 7 | # rubocop:disable Naming/MethodName 8 | def visit_TableDefinition(table_definition) 9 | # rubocop:enable Naming/MethodName 10 | result = super 11 | return result unless table_definition.constraints 12 | 13 | nesting = 0 14 | # Find the closing paren of the "CREATE TABLE ( ... )" clause 15 | index = result.length.times do |i| 16 | token = result[i] 17 | nesting, should_break = adjust_nesting(nesting, token) 18 | break i if should_break 19 | end 20 | result[index] = ", #{table_definition.constraints.join(', ')})" 21 | result 22 | end 23 | 24 | def adjust_nesting(nesting, token) 25 | nesting_was = nesting 26 | nesting += 1 if '(' == token 27 | nesting -= 1 if ')' == token 28 | [nesting, (1 == nesting_was && nesting.zero?)] 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/active_record/postgres/constraints/schema_dumper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Postgres 5 | module Constraints 6 | module SchemaDumper 7 | def indexes_in_create(table, stream) 8 | constraints = @connection.constraints(table) 9 | indexes = @connection.indexes(table).reject do |index| 10 | constraints.pluck('conname').include?(index_name(index)) 11 | end 12 | dump_indexes(indexes, stream) 13 | dump_constraints(constraints, stream) 14 | end 15 | 16 | private 17 | 18 | def dump_indexes(indexes, stream) 19 | return unless indexes.any? 20 | 21 | index_statements = indexes.map do |index| 22 | " t.index #{index_parts(index).join(', ')}" 23 | end 24 | stream.puts index_statements.sort.join("\n") 25 | end 26 | 27 | def dump_constraints(constraints, stream) 28 | return unless constraints.any? 29 | 30 | constraint_statements = constraints.map do |constraint| 31 | type = CONSTRAINT_TYPES.key(constraint['contype']) 32 | ActiveRecord::Postgres::Constraints. 33 | class_for_constraint_type(type). 34 | to_schema_dump(constraint) 35 | end 36 | stream.puts constraint_statements.sort.join("\n") 37 | end 38 | 39 | def index_name(index) 40 | if index.is_a?(ActiveRecord::ConnectionAdapters::IndexDefinition) 41 | index.name 42 | else 43 | index['name'] 44 | end 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/active_record/postgres/constraints/table_definition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Postgres 5 | module Constraints 6 | module TableDefinition 7 | attr_reader :constraints 8 | 9 | CONSTRAINT_TYPES.keys.each do |type| 10 | define_method "#{type}_constraint" do |name_or_conditions, conditions = nil| 11 | add_constraint(type, name_or_conditions, conditions) 12 | end 13 | end 14 | 15 | def add_constraint(type, name_or_conditions, conditions) 16 | @constraints ||= [] 17 | constraint = 18 | ActiveRecord::Postgres::Constraints. 19 | class_for_constraint_type(type). 20 | to_sql(name, name_or_conditions, conditions) 21 | constraints << constraint 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/active_record/postgres/constraints/types/check.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Postgres 5 | module Constraints 6 | module Types 7 | module Check 8 | class << self 9 | def to_sql(table, name_or_conditions, conditions = nil) 10 | name, conditions = ActiveRecord::Postgres::Constraints. 11 | normalize_name_and_conditions(table, name_or_conditions, conditions) 12 | "CONSTRAINT #{name} CHECK (#{normalize_conditions(conditions)})" 13 | end 14 | 15 | def to_schema_dump(constraint) 16 | name = constraint['conname'] 17 | conditions = constraint['definition'].gsub(/^CHECK\s*\((.*)\)\s*$/, '\\1') 18 | " t.check_constraint :#{name}, #{conditions.inspect}" 19 | end 20 | 21 | def example_constraint 22 | "'price > 999'" 23 | end 24 | 25 | private 26 | 27 | def normalize_conditions(conditions) 28 | conditions = [conditions] unless conditions.is_a?(Array) 29 | conditions = conditions.map do |condition| 30 | if condition.is_a?(Hash) 31 | normalize_conditions_hash(condition) 32 | else 33 | condition 34 | end 35 | end 36 | 37 | return conditions.first if 1 == conditions.length 38 | 39 | "(#{conditions.join(') AND (')})" 40 | end 41 | 42 | def normalize_conditions_hash(hash) 43 | hash = hash.reduce([]) do |array, (column, predicate)| 44 | predicate = predicate.join("', '") if predicate.is_a?(Array) 45 | array << "#{column} IN ('#{predicate}')" 46 | end 47 | "(#{hash.join(') AND (')})" 48 | end 49 | end 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/active_record/postgres/constraints/types/exclude.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Postgres 5 | module Constraints 6 | module Types 7 | module Exclude 8 | OPERATOR_SYMBOLS = { 9 | equals: '=', 10 | overlaps: '&&', 11 | }.freeze 12 | 13 | class << self 14 | def to_sql(table, name_or_conditions, conditions = nil) 15 | name, conditions = ActiveRecord::Postgres::Constraints. 16 | normalize_name_and_conditions(table, name_or_conditions, conditions) 17 | 18 | using = conditions.delete(:using) 19 | using = " USING #{using}" if using 20 | 21 | where = conditions.delete(:where) 22 | where = " WHERE (#{where})" if where 23 | 24 | conditions = normalize_conditions(conditions).join(', ') 25 | 26 | "CONSTRAINT #{name} EXCLUDE#{using} (#{conditions})#{where}" 27 | end 28 | 29 | def to_schema_dump(constraint) 30 | name = constraint['conname'] 31 | definition = constraint['definition'] 32 | 33 | using = definition.match(/USING (\w*)/).try(:[], 1) 34 | using = "using: :#{using}, " if using 35 | 36 | where = definition.match(/WHERE \((.*)\)/).try(:[], 1) 37 | where = "where: '#{where}'" if where 38 | 39 | exclusions = definition_to_exclusions(definition).join(', ') 40 | conditions = "#{using}#{exclusions}#{", #{where}" if where}" 41 | 42 | " t.exclude_constraint :#{name}, #{conditions}" 43 | end 44 | 45 | def example_constraint 46 | %(using: :gist, 'tsrange("from", "to")' => :overlaps, project_id: :equals) 47 | end 48 | 49 | private 50 | 51 | def definition_to_exclusions(definition) 52 | definition. 53 | split(' WHERE')[0]. 54 | match(/\((.*)/)[1]. 55 | chomp(')'). 56 | scan(/((?:[^,(]+|(\((?>[^()]+|\g<-1>)*)\))+)/). 57 | map!(&:first). 58 | map!(&:strip). 59 | flatten. 60 | map! { |exclusion| element_and_operator(exclusion) } 61 | end 62 | 63 | def element_and_operator(exclusion) 64 | element, operator = exclusion.strip.split(' WITH ') 65 | "#{normalize_element(element)} #{normalize_operator(operator)}" 66 | end 67 | 68 | def normalize_conditions(conditions) 69 | conditions.map do |element, operator| 70 | "#{element} WITH #{OPERATOR_SYMBOLS[operator.to_sym]}" 71 | end 72 | end 73 | 74 | def normalize_element(element) 75 | element.include?('(') ? "'#{element}' =>" : "#{element}:" 76 | end 77 | 78 | def normalize_operator(operator) 79 | ":#{OPERATOR_SYMBOLS.invert[operator]}" 80 | end 81 | end 82 | end 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/active_record/postgres/constraints/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Postgres 5 | module Constraints 6 | VERSION = '0.2.2' 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/active_record/postgres/constraints/types/check_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe ActiveRecord::Postgres::Constraints::Types::Check, :constraint do 6 | context 'when a migration adds a check constraint' do 7 | shared_examples_for 'adds a constraint' do 8 | let(:expected_schema) do 9 | <<-MIGRATION 10 | #{create_table_line_of_schema_file(:prices)} 11 | t\.integer "price" 12 | t\.check_constraint :test_constraint, #{expected_constraint_string} 13 | end 14 | MIGRATION 15 | end 16 | 17 | it { should include_the_constraint_in_the_schema_file } 18 | 19 | it 'enforces the constraint' do 20 | expect { Price.create! price: 999 }.to raise_error( 21 | ActiveRecord::StatementInvalid, expected_error_regex 22 | ) 23 | end 24 | end 25 | let(:model_class) { 'Price' } 26 | let(:expected_constraint_error_message) do 27 | 'PG::CheckViolation: ERROR: new row for relation "prices" violates '\ 28 | 'check constraint "test_constraint"' 29 | end 30 | 31 | let(:expected_error_regex) { /\A#{expected_constraint_error_message}/ } 32 | 33 | context 'when using `t.check_constraint`' do 34 | let(:content_of_change_method) do 35 | <<-MIGRATION 36 | create_table :prices do |t| 37 | t.integer :price 38 | t.check_constraint :test_constraint, #{constraint} 39 | end 40 | MIGRATION 41 | end 42 | 43 | context 'when the constraint is a String' do 44 | let(:constraint) { "'price > 1000'" } 45 | let(:expected_constraint_string) { '"\(price > 1000\)"' } 46 | 47 | it_behaves_like 'adds a constraint' 48 | 49 | context 'when the constraint is anonymous' do 50 | let(:content_of_change_method) do 51 | <<-MIGRATION 52 | create_table :prices do |t| 53 | t.integer :price 54 | t.check_constraint #{constraint} 55 | end 56 | MIGRATION 57 | end 58 | 59 | let(:expected_constraint_error_message) do 60 | 'PG::CheckViolation: ERROR: new row for relation "prices" '\ 61 | 'violates check constraint "prices_[0-9]{7,9}"' 62 | end 63 | 64 | it_behaves_like 'adds a constraint' do 65 | let(:expected_schema) do 66 | <<-MIGRATION 67 | #{create_table_line_of_schema_file(:prices)} 68 | t\.integer "price" 69 | t\.check_constraint :prices_[0-9]{7,9}, #{expected_constraint_string} 70 | end 71 | MIGRATION 72 | end 73 | end 74 | end 75 | end 76 | 77 | context 'when the constraint is a Hash' do 78 | let(:constraint) { { price: [10, 20, 30] } } 79 | let(:expected_constraint_string) do 80 | '"\(price = ANY \(ARRAY\[10, 20, 30\]\)\)"' 81 | end 82 | 83 | it_behaves_like 'adds a constraint' 84 | end 85 | 86 | context 'when the constraint is an Array' do 87 | let(:constraint) { ['price > 50', { price: [90, 100] }] } 88 | let(:expected_constraint_string) do 89 | '"\(\(price > 50\) AND \(price = ANY \(ARRAY\[90, 100\]\)\)\)"' 90 | end 91 | 92 | it_behaves_like 'adds a constraint' 93 | end 94 | end 95 | 96 | context 'when using add_check_constraint' do 97 | let(:constraint) { "'price > 1000'" } 98 | let(:expected_constraint_string) { '"\(price > 1000\)"' } 99 | let(:content_of_change_method) do 100 | <<-MIGRATION 101 | create_table :prices do |t| 102 | t.integer :price 103 | end 104 | add_check_constraint :prices, :test_constraint, #{constraint} 105 | MIGRATION 106 | end 107 | 108 | it_behaves_like 'adds a constraint' 109 | 110 | context 'when the constraint is removed in a later migration' do 111 | let(:content_of_change_method_for_removing_migration) do 112 | "remove_check_constraint :prices, :test_constraint, #{constraint}" 113 | end 114 | 115 | before do 116 | generate_migration('20170201120000', Random.rand(1001..2000)) do 117 | content_of_change_method_for_removing_migration 118 | end 119 | 120 | run_migrations 121 | end 122 | 123 | it 'removes the check_constraint from the schema file' do 124 | schema = File.read(schema_file) 125 | expect(schema).not_to match(/check_constraint/) 126 | end 127 | 128 | it 'does not enforce the constraint until we rollback the second migration' do 129 | create_price = -> { Price.create! price: 999 } 130 | expect(create_price).not_to raise_error 131 | 132 | # Ensure that we can safely roll back the migration that removed the 133 | # check constraint 134 | Price.destroy_all 135 | 136 | rollback 137 | 138 | expect(create_price).to raise_error(ActiveRecord::StatementInvalid, expected_error_regex) 139 | end 140 | 141 | context 'when remove_check_constraint is irreversible' do 142 | let(:content_of_change_method_for_removing_migration) do 143 | 'remove_check_constraint :prices, :test_constraint' 144 | end 145 | 146 | let(:expected_irreversible_migration_error_message) do 147 | 'To make this migration reversible, pass the constraint to '\ 148 | 'remove_check_constraint, i\.e\. `remove_check_constraint '\ 149 | ":prices, :test_constraint, 'price > 999'`" 150 | end 151 | 152 | it 'removes the check_constraint from the schema file' do 153 | schema = File.read(schema_file) 154 | expect(schema).not_to match(/check_constraint/) 155 | end 156 | 157 | it 'does not enforce the constraint' do 158 | expect { Price.create! price: 999 }.not_to raise_error 159 | 160 | # Ensure that we can safely roll back the migration that removed the 161 | # check constraint 162 | Price.destroy_all 163 | end 164 | 165 | def rollback 166 | expect { super }.to raise_error StandardError, 167 | /#{expected_irreversible_migration_error_message}/m 168 | end 169 | end 170 | end 171 | end 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /spec/active_record/postgres/constraints/types/exclude_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe ActiveRecord::Postgres::Constraints::Types::Exclude, :constraint do 6 | context 'when a migration adds an exclude constraint' do 7 | shared_examples_for 'adds a constraint' do 8 | let(:expected_schema) do 9 | <<-MIGRATION 10 | 11 | #{create_table_line_of_schema_file(:phases)} 12 | t\.integer {1,2}"project_id" 13 | t\.datetime "from" 14 | t\.datetime "to" 15 | t\.exclude_constraint :test_constraint, #{expected_constraint_string} 16 | end 17 | MIGRATION 18 | end 19 | 20 | it { should include_the_constraint_in_the_schema_file } 21 | 22 | it 'enforces the constraint for a different project only if there is no WHERE clause' do 23 | create_phase(1.day.ago, 1.day.from_now) 24 | create_phase(2.days.from_now, nil) 25 | create_excluded_phase_with_project_id_of_1 26 | expect { create_excluded_phase_with_project_id_of_1 }. 27 | public_send(*(where_clause ? [:not_to, raise_error] : [:to, raise_statement_invalid])) 28 | end 29 | 30 | it 'enforces the constraint for the same project and an overlapping time range' do 31 | create_phase(1.day.ago, 1.day.from_now) 32 | expect { create_phase(Time.current, 2.days.from_now) }.to(raise_statement_invalid) 33 | end 34 | end 35 | 36 | def create_phase(from, to, project_id = 999) 37 | Phase.create!(project_id: project_id, from: from, to: to) 38 | end 39 | 40 | def create_excluded_phase_with_project_id_of_1 41 | create_phase(Time.current, 2.days.from_now, 1) 42 | end 43 | 44 | def raise_statement_invalid 45 | raise_error(ActiveRecord::StatementInvalid, expected_error_regex) 46 | end 47 | 48 | let(:model_class) { 'Phase' } 49 | 50 | let(:where_clause) { false } 51 | 52 | let(:expected_constraint_error_message) do 53 | 'PG::ExclusionViolation: ERROR: conflicting key value violates '\ 54 | 'exclusion constraint "test_constraint"' 55 | end 56 | 57 | let(:expected_error_regex) { /\A#{expected_constraint_error_message}/ } 58 | 59 | context 'when using `t.exclude_constraint`' do 60 | let(:content_of_change_method) do 61 | <<-MIGRATION 62 | enable_extension "btree_gist" 63 | create_table :phases do |t| 64 | t.integer :project_id 65 | t.datetime :from 66 | t.datetime :to 67 | t.exclude_constraint :test_constraint, #{constraint} 68 | end 69 | MIGRATION 70 | end 71 | 72 | let(:expected_constraint_string) do 73 | 'using: :gist, \'tsrange\("from", "to"\)\' => :overlaps, project_id: :equals' 74 | end 75 | 76 | context 'when the constraint is a String' do 77 | let(:constraint) do 78 | 'using: :gist, \'tsrange("from", "to")\' => :overlaps, :project_id => :equals' 79 | end 80 | 81 | it_behaves_like 'adds a constraint' 82 | 83 | context 'when a where clause is present' do 84 | let(:where_clause) { true } 85 | let(:constraint) { "#{super()}, where: 'project_id <> 1'" } 86 | let(:expected_constraint_string) { "#{super()}, where: '\\(project_id <> 1\\)'" } 87 | 88 | it_behaves_like 'adds a constraint' 89 | end 90 | 91 | context 'when the migration contains additional indexes' do 92 | let(:content_of_change_method) do 93 | <<-MIGRATION 94 | enable_extension "btree_gist" 95 | #{create_table_line_of_schema_file(:phases)} 96 | t.integer :project_id 97 | t.datetime :from 98 | t.datetime :to 99 | t.index [:project_id, :from] 100 | t.exclude_constraint :test_constraint, #{constraint} 101 | end 102 | MIGRATION 103 | end 104 | 105 | it_behaves_like 'adds a constraint' do 106 | let(:expected_schema) do 107 | using_btree = ', using: :btree' unless rails_gte_5_1_0? 108 | <<-MIGRATION 109 | 110 | #{create_table_line_of_schema_file(:phases)} 111 | t\.integer {1,2}"project_id" 112 | t\.datetime "from" 113 | t\.datetime "to" 114 | t\.index \\["project_id", "from"\\], name: "index_phases_on_project_id_and_from"#{using_btree} 115 | t\.exclude_constraint :test_constraint, #{expected_constraint_string} 116 | end 117 | MIGRATION 118 | end 119 | end 120 | end 121 | 122 | context 'when the constraint is anonymous' do 123 | let(:content_of_change_method) do 124 | <<-MIGRATION 125 | enable_extension "btree_gist" 126 | create_table :phases do |t| 127 | t.integer :project_id 128 | t.datetime :from 129 | t.datetime :to 130 | t.exclude_constraint #{constraint} 131 | end 132 | MIGRATION 133 | end 134 | 135 | let(:expected_constraint_error_message) do 136 | 'PG::ExclusionViolation: ERROR: conflicting key value violates '\ 137 | 'exclusion constraint "phases_[0-9]{7,9}"' 138 | end 139 | 140 | it_behaves_like 'adds a constraint' do 141 | let(:expected_schema) do 142 | <<-MIGRATION 143 | 144 | #{create_table_line_of_schema_file(:phases)} 145 | t\.integer {1,2}"project_id" 146 | t\.datetime "from" 147 | t\.datetime "to" 148 | t\.exclude_constraint :phases_[0-9]{7,9}, #{expected_constraint_string} 149 | end 150 | MIGRATION 151 | end 152 | end 153 | end 154 | end 155 | 156 | context 'when the constraint is a Hash' do 157 | let(:constraint) do 158 | { using: :gist, 'tsrange("from", "to")' => :overlaps, project_id: :equals } 159 | end 160 | 161 | it_behaves_like 'adds a constraint' 162 | 163 | context 'when a where clause is present' do 164 | let(:where_clause) { true } 165 | let(:constraint) { super().merge(where: 'project_id <> 1') } 166 | let(:expected_constraint_string) { "#{super()}, where: '\\(project_id <> 1\\)'" } 167 | 168 | it_behaves_like 'adds a constraint' 169 | end 170 | 171 | context 'when a field is cast to another type' do 172 | let(:constraint) do 173 | { 174 | using: :gist, 175 | 'tsrange("from", "to")' => :overlaps, 176 | 'cast("project_id" AS text)' => :equals, 177 | } 178 | end 179 | 180 | let(:expected_constraint_string) do 181 | 'using: :gist, \'tsrange\("from", "to"\)\' => :overlaps, ' \ 182 | '\'\(\(project_id\)::text\)\' => :equals' 183 | end 184 | 185 | it_behaves_like 'adds a constraint' 186 | end 187 | end 188 | end 189 | 190 | context 'when using add_exclude_constraint' do 191 | let(:constraint) do 192 | { using: :gist, 'tsrange("from", "to")' => :overlaps, project_id: :equals } 193 | end 194 | let(:expected_constraint_string) do 195 | 'using: :gist, \'tsrange\("from", "to"\)\' => :overlaps, project_id: :equals' 196 | end 197 | let(:content_of_change_method) do 198 | <<-MIGRATION 199 | enable_extension "btree_gist" 200 | create_table :phases do |t| 201 | t.integer :project_id 202 | t.datetime :from 203 | t.datetime :to 204 | end 205 | add_exclude_constraint :phases, :test_constraint, #{constraint} 206 | MIGRATION 207 | end 208 | 209 | it_behaves_like 'adds a constraint' 210 | 211 | context 'when a where clause is present' do 212 | let(:where_clause) { true } 213 | let(:constraint) do 214 | 'using: :gist, \'tsrange("from", "to")\' => :overlaps, '\ 215 | ':project_id => :equals, where: \'project_id <> 1\'' 216 | end 217 | let(:expected_constraint_string) do 218 | "#{super()}, where: '\\(project_id <> 1\\)'" 219 | end 220 | 221 | it_behaves_like 'adds a constraint' 222 | end 223 | 224 | context 'when the constraint is removed in a later migration' do 225 | let(:content_of_change_method_for_removing_migration) do 226 | "remove_exclude_constraint :phases, :test_constraint, #{constraint}" 227 | end 228 | 229 | before do 230 | generate_migration('20170201120000', Random.rand(1001..2000)) do 231 | content_of_change_method_for_removing_migration 232 | end 233 | 234 | run_migrations 235 | end 236 | 237 | it 'removes the constraint from the schema file' do 238 | schema = File.read(schema_file) 239 | expect(schema).not_to match(/exclude_constraint/) 240 | end 241 | 242 | def create_overlapping_phase 243 | create_phase(Time.current, 2.days.from_now) 244 | create_phase(1.day.ago, 1.day.from_now) 245 | end 246 | 247 | it 'does not enforce the constraint until we rollback the second migration' do 248 | expect { create_overlapping_phase }.not_to(raise_error) 249 | 250 | # Ensure that we can safely roll back the migration that removed the exclude constraint 251 | Phase.destroy_all 252 | rollback 253 | 254 | expect { create_overlapping_phase }.to(raise_statement_invalid) 255 | end 256 | 257 | context 'when remove_exclude_constraint is irreversible' do 258 | let(:content_of_change_method_for_removing_migration) do 259 | 'remove_exclude_constraint :phases, :test_constraint' 260 | end 261 | 262 | let(:expected_irreversible_migration_error_message) do 263 | 'To make this migration reversible, pass the constraint to '\ 264 | 'remove_exclude_constraint, i\.e\. `remove_exclude_constraint '\ 265 | ':phases, :test_constraint, using: :gist, \'tsrange\("from", "to"\)\''\ 266 | ' => :overlaps, project_id: :equals`' 267 | end 268 | 269 | it 'removes the exclude_constraint from the schema file' do 270 | schema = File.read(schema_file) 271 | expect(schema).not_to match(/exclude_constraint/) 272 | end 273 | 274 | it 'does not enforce the constraint' do 275 | create_phase(1.day.ago, 2.days.from_now) 276 | expect { create_phase(2.days.ago, 1.day.from_now) }.not_to(raise_error) 277 | 278 | # Ensure that we can safely roll back the migration that removed the 279 | # exclude constraint 280 | Phase.destroy_all 281 | end 282 | 283 | def rollback 284 | expect { super }.to raise_error StandardError, 285 | /#{expected_irreversible_migration_error_message}/m 286 | end 287 | end 288 | end 289 | end 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /spec/active_record/postgres/constraints_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'active_record/postgres/constraints' 5 | 6 | RSpec.describe ActiveRecord::Postgres::Constraints do 7 | it 'has a version number' do 8 | expect(described_class::VERSION).not_to be nil 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be 5 | # available to Rake. 6 | 7 | require_relative 'config/application' 8 | 9 | Rails.application.load_tasks 10 | require 'parallel_tests/tasks' 11 | -------------------------------------------------------------------------------- /spec/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require_relative '../config/boot' 5 | require 'rake' 6 | Rake.application.run 7 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require_relative 'config/environment' 6 | 7 | run Rails.application 8 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'boot' 4 | 5 | require 'active_model/railtie' 6 | require 'active_record/railtie' 7 | 8 | Bundler.require(*Rails.groups) 9 | require 'active_record/postgres/constraints' 10 | 11 | module Dummy 12 | class Application < Rails::Application 13 | # Settings in config/environments/* take precedence over those specified 14 | # here. 15 | # Application configuration should go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded. 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) 5 | 6 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 7 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) 8 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | pool: 5 4 | timeout: 5000 5 | 6 | development: 7 | <<: *default 8 | database: active_record-postgres-constraints-development 9 | 10 | test: 11 | <<: *default 12 | database: active_record-postgres-constraints-test<%= ENV['TEST_ENV_NUMBER'] %> 13 | migrations_paths: 14 | - spec/dummy/db/migrate<%= ENV['TEST_ENV_NUMBER'] %> 15 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the Rails application. 4 | require_relative 'application' 5 | 6 | # Initialize the Rails application. 7 | Rails.application.initialize! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Do not eager load code on boot. 5 | config.eager_load = false 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Do not eager load code on boot. This avoids loading your whole application 5 | # just for the purpose of running a single test. If you are using a tool that 6 | # preloads Rails for running tests, you may have to set it to true. 7 | config.eager_load = false 8 | end 9 | -------------------------------------------------------------------------------- /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 `rails 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: 3899b6263cd83590b27f25cc85932d9b0054ec9bc3ee3cdd9bf66265ef74a01a06799af7f21ec9a5af4c488b854680145e8fb067187f5871e6babf5c0301cdc3 15 | 16 | test: 17 | secret_key_base: b0615010a3f8a5886a7105abc8b70fcd6be745f43f2393c5399d4ad020313f98261396afdac968265b37ed5c7b9341e4e2f8579bff45fa72e6d7c17ddbff1834 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/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betesh/active_record-postgres-constraints/2bb795cf3cdbd82070b74512cd32be3ce7dd3d90/spec/dummy/db/migrate/.keep -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 0) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | 18 | end 19 | -------------------------------------------------------------------------------- /spec/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betesh/active_record-postgres-constraints/2bb795cf3cdbd82070b74512cd32be3ce7dd3d90/spec/dummy/log/.keep -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is copied to spec/ when you run 'rails generate rspec:install' 4 | ENV['RAILS_ENV'] ||= 'test' 5 | 6 | # Suppress the patch at 7 | # https://github.com/rails/rails/blob/v5.1.7/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L7 8 | # to ensure that all pg statements run synchronously during tests 9 | require 'pg' 10 | class ::PG::Connection 11 | # We don't want sync activities leaking from one test to another 12 | if PG::Connection.respond_to?('async_api=') 13 | PG::Connection.async_api = false 14 | else 15 | def async_exec_params 16 | raise 'This should never happen' 17 | end 18 | 19 | alias async_exec exec_params 20 | end 21 | end 22 | 23 | require File.expand_path('../dummy/config/environment', __FILE__) 24 | # Prevent database truncation if the environment is production 25 | abort('The Rails environment is running in production mode!') if Rails.env.production? 26 | require 'spec_helper' 27 | require 'rspec/rails' 28 | # Add additional requires below this line. Rails is not loaded until this point! 29 | 30 | # Requires supporting ruby files with custom matchers and macros, etc, in 31 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 32 | # run as spec files by default. This means that files in spec/support that end 33 | # in _spec.rb will both be required and run as specs, causing the specs to be 34 | # run twice. It is recommended that you do not name files matching this glob to 35 | # end with _spec.rb. You can configure this pattern with the --pattern 36 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 37 | Dir[File.join(File.expand_path('../support/**/*.rb', __FILE__))].sort.each { |f| require f } 38 | 39 | # Checks for pending migration and applies them before tests are run. 40 | # If you are not using ActiveRecord, you can remove this line. 41 | ActiveRecord::Migration.maintain_test_schema! 42 | 43 | RSpec.configure do |config| 44 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 45 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 46 | 47 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 48 | # examples within a transaction, remove the following line or assign false 49 | # instead of true. 50 | config.use_transactional_fixtures = false 51 | 52 | # RSpec Rails can automatically mix in different behaviours to your tests 53 | # based on their file location, for example enabling you to call `get` and 54 | # `post` in specs under `spec/controllers`. 55 | # 56 | # You can disable this behaviour by removing the line below, and instead 57 | # explicitly tag your specs with their type, e.g.: 58 | # 59 | # RSpec.describe UsersController, :type => :controller do 60 | # # ... 61 | # end 62 | # 63 | # The different available types are documented in the features, such as in 64 | # https://relishapp.com/rspec/rspec-rails/docs 65 | config.infer_spec_type_from_file_location! 66 | 67 | # Filter lines from Rails gems in backtraces. 68 | config.filter_rails_from_backtrace! 69 | # arbitrary gems may also be filtered via: 70 | # config.filter_gems_from_backtrace("gem name") 71 | end 72 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by the `rails generate rspec:install` command. 4 | # Conventionally, all specs live under a `spec` directory, which RSpec adds to 5 | # the `$LOAD_PATH`. 6 | # The generated `.rspec` file contains `--require spec_helper` which will cause 7 | # this file to always be loaded, without a need to explicitly require it in any 8 | # files. 9 | # 10 | # Given that it is always loaded, you are encouraged to keep this file as 11 | # light-weight as possible. Requiring heavyweight dependencies from this file 12 | # will add to the boot time of your test suite on EVERY test run, even for an 13 | # individual file that may not need all of that loaded. Instead, consider making 14 | # a separate helper file that requires the additional dependencies and performs 15 | # the additional setup, and require it from the spec files that actually need 16 | # it. 17 | # 18 | # The `.rspec` file also contains a few flags that are not defaults but that 19 | # users commonly want. 20 | # 21 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 22 | require 'simplecov' 23 | 24 | RSpec.configure do |config| 25 | # rspec-expectations config goes here. You can use an alternate 26 | # assertion/expectation library such as wrong or the stdlib/minitest 27 | # assertions if you prefer. 28 | config.expect_with :rspec do |expectations| 29 | # This option will default to `true` in RSpec 4. It makes the `description` 30 | # and `failure_message` of custom matchers include text for helper methods 31 | # defined using `chain`, e.g.: 32 | # be_bigger_than(2).and_smaller_than(4).description 33 | # # => "be bigger than 2 and smaller than 4" 34 | # ...rather than: 35 | # # => "be bigger than 2" 36 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 37 | expectations.max_formatted_output_length = nil 38 | end 39 | 40 | # rspec-mocks config goes here. You can use an alternate test double 41 | # library (such as bogus or mocha) by changing the `mock_with` option here. 42 | config.mock_with :rspec do |mocks| 43 | # Prevents you from mocking or stubbing a method that does not exist on 44 | # a real object. This is generally recommended, and will default to 45 | # `true` in RSpec 4. 46 | mocks.verify_partial_doubles = true 47 | end 48 | 49 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 50 | # have no way to turn it off -- the option exists only for backwards 51 | # compatibility in RSpec 3). It causes shared context metadata to be 52 | # inherited by the metadata hash of host groups and examples, rather than 53 | # triggering implicit auto-inclusion in groups with matching metadata. 54 | config.shared_context_metadata_behavior = :apply_to_host_groups 55 | 56 | # This allows you to limit a spec run to individual examples or groups 57 | # you care about by tagging them with `:focus` metadata. When nothing 58 | # is tagged with `:focus`, all examples get run. RSpec also provides 59 | # aliases for `it`, `describe`, and `context` that include `:focus` 60 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 61 | config.filter_run_when_matching :focus 62 | 63 | # Allows RSpec to persist some state between runs in order to support 64 | # the `--only-failures` and `--next-failure` CLI options. We recommend 65 | # you configure your source control system to ignore this file. 66 | config.example_status_persistence_file_path = 'spec/examples.txt' 67 | 68 | # Limits the available syntax to the non-monkey patched syntax that is 69 | # recommended. For more details, see: 70 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 71 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 72 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 73 | config.disable_monkey_patching! 74 | 75 | # Many RSpec users commonly either run the entire suite or an individual 76 | # file, and it's useful to allow more verbose output when running an 77 | # individual spec file. 78 | if config.files_to_run.one? 79 | # Use the documentation formatter for detailed output, 80 | # unless a formatter has already been configured 81 | # (e.g. via a command-line flag). 82 | config.default_formatter = 'doc' 83 | end 84 | 85 | # Print the 10 slowest examples and example groups at the 86 | # end of the spec run, to help surface which specs are running 87 | # particularly slow. 88 | config.profile_examples = 10 89 | 90 | # Run specs in random order to surface order dependencies. If you find an 91 | # order dependency and want to debug it, you can fix the order by providing 92 | # the seed, which is printed after each run. 93 | # --seed 1234 94 | config.order = :random 95 | 96 | # Seed global randomization in this process using the `--seed` CLI option. 97 | # Setting this allows you to use `--seed` to deterministically reproduce 98 | # test failures related to randomization by passing the same `--seed` value 99 | # as the one that triggered the failure. 100 | Kernel.srand config.seed 101 | end 102 | -------------------------------------------------------------------------------- /spec/support/constraint_support.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'shared_migration_methods' 4 | 5 | module ConstraintSupport 6 | include SharedMigrationMethods 7 | 8 | extend RSpec::SharedContext 9 | extend RSpec::Matchers::DSL 10 | 11 | before(:all) do 12 | FileUtils.mkdir_p(migration_dir) 13 | 14 | unless defined?(ActiveRecord::Base.connection.migration_context) 15 | ActiveRecord::Tasks::DatabaseTasks.migrations_paths = migration_dir 16 | end 17 | end 18 | 19 | before do 20 | stub_const(model_class, Class.new(ApplicationRecord)) 21 | 22 | cleanup_database 23 | 24 | generate_migration('20170101120000', Random.rand(1..1000)) do 25 | content_of_change_method 26 | end 27 | 28 | run_migrations 29 | end 30 | 31 | after do 32 | rollback 33 | delete_all_migration_files 34 | end 35 | 36 | after(:all) do 37 | cleanup_database 38 | dump_schema 39 | end 40 | 41 | matcher :include_the_constraint_in_the_schema_file do 42 | def schema_file_offset 43 | 17 44 | end 45 | 46 | def schema 47 | @schema ||= File.read(schema_file).split("\n") 48 | end 49 | 50 | def expected_lines 51 | @expected_lines ||= expected_schema.strip_heredoc.indent(2).split("\n") 52 | end 53 | 54 | match do 55 | expected_lines.each_with_index do |line, i| 56 | expect(schema[i + schema_file_offset]).to match(/\A#{line}\z/) 57 | end 58 | end 59 | 60 | failure_message do 61 | i = -1 62 | line_not_found = expected_lines.find do |line| 63 | i += 1 64 | !schema[i + schema_file_offset].match(/\A#{line}\z/) 65 | end 66 | schema_file_line = schema_file_offset + i 67 | "Expected line #{schema_file_line} of schema.rb to match:\n" \ 68 | "#{line_not_found}\nbut it was:\n#{schema[schema_file_line]}" 69 | end 70 | end 71 | 72 | def rails_gte_5_1_0? 73 | Gem::Version.new(ActiveRecord.gem_version) >= Gem::Version.new('5.1.0') 74 | end 75 | 76 | def create_table_line_of_schema_file(table_name) 77 | "create_table \"#{table_name}\", #{'id: :serial, ' if rails_gte_5_1_0?}force: :cascade do \|t\|" 78 | end 79 | end 80 | 81 | RSpec.configure do |config| 82 | config.include ConstraintSupport, constraint: true 83 | end 84 | -------------------------------------------------------------------------------- /spec/support/shared_migration_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SharedMigrationMethods 4 | def dummy_dir 5 | File.expand_path('../../dummy', __FILE__) 6 | end 7 | 8 | def migration_dir 9 | "#{dummy_dir}/db/migrate#{ENV['TEST_ENV_NUMBER']}" 10 | end 11 | 12 | def delete_all_migration_files 13 | `rm -rf #{migration_dir}/*.rb` 14 | end 15 | 16 | def cleanup_database 17 | delete_all_migration_files 18 | ActiveRecord::Tasks::DatabaseTasks.drop_current 19 | ActiveRecord::Tasks::DatabaseTasks.create_current 20 | end 21 | 22 | def migration_file(migration_number, suffix) 23 | migration_file_name = "#{migration_number}_migration_#{suffix}" 24 | "#{migration_dir}/#{migration_file_name}.rb" 25 | end 26 | 27 | def migration_content(migration_name_suffix) 28 | <<-MIGRATION_CLASS.strip_heredoc 29 | class Migration#{migration_name_suffix} < ActiveRecord::Migration[5.0] 30 | self.verbose = false 31 | def change\n#{yield.strip_heredoc.indent(10).rstrip} 32 | end 33 | end 34 | MIGRATION_CLASS 35 | end 36 | 37 | def generate_migration(migration_number, suffix, &block) 38 | File.open(migration_file(migration_number, suffix), 'w') do |f| 39 | f.puts migration_content(suffix, &block) 40 | end 41 | end 42 | 43 | def schema_file 44 | "#{dummy_dir}/db/schema.rb" 45 | end 46 | 47 | def dump_schema 48 | require 'active_record/schema_dumper' 49 | File.open(schema_file, 'w:utf-8') do |file| 50 | ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) 51 | end 52 | end 53 | 54 | def run_migrations 55 | if defined?(ActiveRecord::Base.connection.migration_context) 56 | ActiveRecord::Base.connection.migration_context.migrate 57 | else 58 | ActiveRecord::Tasks::DatabaseTasks.migrate 59 | end 60 | dump_schema 61 | end 62 | 63 | def rollback 64 | if defined?(ActiveRecord::MigrationContext) 65 | ActiveRecord::Base.connection.migration_context.rollback(1) 66 | else 67 | ActiveRecord::Migrator.rollback( 68 | ActiveRecord::Tasks::DatabaseTasks.migrations_paths, 1 69 | ) 70 | end 71 | dump_schema 72 | end 73 | end 74 | --------------------------------------------------------------------------------