├── .ruby-version
├── spec
├── dummy
│ ├── log
│ │ └── .gitkeep
│ ├── app
│ │ ├── mailers
│ │ │ └── .gitkeep
│ │ ├── models
│ │ │ ├── .gitkeep
│ │ │ ├── pet.rb
│ │ │ ├── demography
│ │ │ │ ├── citizen.rb
│ │ │ │ └── country.rb
│ │ │ ├── demography.rb
│ │ │ └── user.rb
│ │ ├── helpers
│ │ │ └── application_helper.rb
│ │ ├── controllers
│ │ │ └── application_controller.rb
│ │ ├── views
│ │ │ └── layouts
│ │ │ │ └── application.html.erb
│ │ └── assets
│ │ │ ├── stylesheets
│ │ │ └── application.css
│ │ │ └── javascripts
│ │ │ └── application.js
│ ├── lib
│ │ └── assets
│ │ │ └── .gitkeep
│ ├── public
│ │ ├── favicon.ico
│ │ ├── 422.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── config
│ │ ├── initializers
│ │ │ ├── pg_saurus.rb
│ │ │ ├── mime_types.rb
│ │ │ ├── inflections.rb
│ │ │ ├── backtrace_silencers.rb
│ │ │ ├── session_store.rb
│ │ │ ├── secret_token.rb
│ │ │ └── wrap_parameters.rb
│ │ ├── environment.rb
│ │ ├── locales
│ │ │ └── en.yml
│ │ ├── boot.rb
│ │ ├── database.yml
│ │ ├── environments
│ │ │ ├── development.rb
│ │ │ ├── test.rb
│ │ │ └── production.rb
│ │ ├── routes.rb
│ │ └── application.rb
│ ├── config.ru
│ ├── db
│ │ └── migrate
│ │ │ ├── 20120516222439_add_functional_index.rb
│ │ │ ├── 20120720103117_create_people.rb
│ │ │ ├── 20120720103136_move_people_to_demography_schema.rb
│ │ │ ├── 20190801025445_add_compound_functional_index.rb
│ │ │ ├── 20120301171826_remove_demography_nationalities.rb
│ │ │ ├── 20120517211922_add_partial_functional_index.rb
│ │ │ ├── 20120904113806_add_concurrently_index_to_users_on_email.rb
│ │ │ ├── 20130624154800_create_demography_views.rb
│ │ │ ├── 20120301152819_create_demography_nationalities.rb
│ │ │ ├── 20190320025645_add_functional_index_with_longer_operator_string.rb
│ │ │ ├── 20120904114121_add_concurrently_index_with_foreign_key.rb
│ │ │ ├── 20120207112916_remove_comment_on_pets_table.rb
│ │ │ ├── 20130504233224_remove_index_comments.rb
│ │ │ ├── 20120904114120_remove_foreign_key_from_pets_on_user_id.rb
│ │ │ ├── 20120301153650_demography_population_statistics.rb
│ │ │ ├── 20120209094937_create_cities_table.rb
│ │ │ ├── 20120224204546_add_demography_citizens_active_column.rb
│ │ │ ├── 20120201163544_create_owners_and_breeds.rb
│ │ │ ├── 20120207103858_remove_comments_on_countries_table.rb
│ │ │ ├── 20120208114020_remove_some_comments_on_citizens.rb
│ │ │ ├── 20220709040946_add_function_returning_a_table_type.rb
│ │ │ ├── 20120106163711_create_demography_schema.rb
│ │ │ ├── 20120106163810_create_demography_countries.rb
│ │ │ ├── 20140910125700_create_rock_bands_schema_if_not_exists.rb
│ │ │ ├── 20120105112744_create_pets.rb
│ │ │ ├── 20130504232621_add_index_comments.rb
│ │ │ ├── 20120106163544_create_users.rb
│ │ │ ├── 20120207150844_add_foreign_keys.rb
│ │ │ ├── 20120207163652_remove_foreign_keys.rb
│ │ │ ├── 20150713035548_add_function_count_pets.rb
│ │ │ ├── 20121009170904_create_extension.rb
│ │ │ ├── 20120106163820_create_demography_citizens.rb
│ │ │ ├── 20190213151821_create_books.rb
│ │ │ └── 20150714003209_create_pets_trigger.rb
│ ├── Rakefile
│ └── script
│ │ └── rails
├── lib
│ ├── pg_saurus
│ │ ├── connection_adapters
│ │ │ ├── abstract_adapter
│ │ │ │ ├── foreign_key_methods_spec.rb
│ │ │ │ ├── comment_methods_spec.rb
│ │ │ │ ├── trigger_methods_spec.rb
│ │ │ │ ├── function_methods_spec.rb
│ │ │ │ ├── index_methods_spec.rb
│ │ │ │ └── schema_methods_spec.rb
│ │ │ ├── postgresql_adapter
│ │ │ │ ├── index_methods_spec.rb
│ │ │ │ ├── foreign_key_methods_spec.rb
│ │ │ │ ├── view_methods_spec.rb
│ │ │ │ ├── translate_exception_spec.rb
│ │ │ │ ├── extension_methods_spec.rb
│ │ │ │ ├── trigger_methods_spec.rb
│ │ │ │ ├── schema_methods_spec.rb
│ │ │ │ ├── comment_methods_spec.rb
│ │ │ │ └── function_methods_spec.rb
│ │ │ ├── abstract_adapter_spec.rb
│ │ │ └── table
│ │ │ │ ├── trigger_methods_spec.rb
│ │ │ │ └── comment_methods_spec.rb
│ │ ├── migration
│ │ │ ├── command_recorder
│ │ │ │ ├── view_methods_spec.rb
│ │ │ │ ├── extension_methods_spec.rb
│ │ │ │ └── schema_methods_spec.rb
│ │ │ └── command_recorder_spec.rb
│ │ ├── tools_spec.rb
│ │ └── migration_spec.rb
│ └── core_ext
│ │ └── connection_adapters
│ │ ├── postgresql_adapter_spec.rb
│ │ └── abstract
│ │ └── schema_statements_spec.rb
├── spec_helper.rb
├── schema_methods_spec.rb
├── foreign_keys_spec.rb
├── support
│ └── explorer.rb
├── comment_methods_spec.rb
├── active_record
│ ├── pg_saurus_notes.txt
│ └── schema_dumper_spec.rb
└── indexes_spec.rb
├── .ruby-gemset
├── tmp
└── metric_fu
│ └── _data
│ └── .keep
├── .rspec
├── lib
├── pg_saurus
│ ├── version.rb
│ ├── migration.rb
│ ├── connection_adapters
│ │ ├── abstract_adapter
│ │ │ ├── index_methods.rb
│ │ │ ├── schema_methods.rb
│ │ │ ├── function_methods.rb
│ │ │ ├── trigger_methods.rb
│ │ │ └── comment_methods.rb
│ │ ├── foreign_key_definition.rb
│ │ ├── table.rb
│ │ ├── abstract_adapter.rb
│ │ ├── function_definition.rb
│ │ ├── postgresql_adapter
│ │ │ ├── view_methods.rb
│ │ │ ├── index_methods.rb
│ │ │ ├── translate_exception.rb
│ │ │ ├── schema_methods.rb
│ │ │ ├── comment_methods.rb
│ │ │ ├── foreign_key_methods.rb
│ │ │ ├── trigger_methods.rb
│ │ │ ├── function_methods.rb
│ │ │ └── extension_methods.rb
│ │ ├── trigger_definition.rb
│ │ ├── table
│ │ │ ├── trigger_methods.rb
│ │ │ └── comment_methods.rb
│ │ └── postgresql_adapter.rb
│ ├── config.rb
│ ├── connection_adapters.rb
│ ├── errors.rb
│ ├── migration
│ │ ├── command_recorder.rb
│ │ ├── command_recorder
│ │ │ ├── trigger_methods.rb
│ │ │ ├── function_methods.rb
│ │ │ ├── extension_methods.rb
│ │ │ ├── view_methods.rb
│ │ │ ├── schema_methods.rb
│ │ │ └── comment_methods.rb
│ │ └── set_role_method.rb
│ ├── schema_dumper.rb
│ ├── schema_dumper
│ │ ├── function_methods.rb
│ │ ├── extension_methods.rb
│ │ ├── view_methods.rb
│ │ ├── trigger_methods.rb
│ │ ├── schema_methods.rb
│ │ ├── comment_methods.rb
│ │ └── foreign_key_methods.rb
│ ├── engine.rb
│ ├── tools.rb
│ └── create_index_concurrently.rb
├── tasks
│ └── pg_saurus_tasks.rake
├── generators
│ └── pg_saurus
│ │ └── install
│ │ ├── templates
│ │ └── config
│ │ │ └── initializers
│ │ │ └── pg_saurus.rb
│ │ └── install_generator.rb
├── core_ext
│ └── active_record
│ │ ├── errors.rb
│ │ ├── migration
│ │ └── compatibility.rb
│ │ └── schema_dumper.rb
├── pg_saurus.rb
└── colorized_text.rb
├── docs
└── PgPower Ligtning Talk - WindyCityRails 2012.pdf
├── .metrics
├── script
└── rails
├── .travis.yml
├── .rubocop.yml
├── .gitignore
├── bin
└── rails
├── MIT-LICENSE
├── .simplecov
├── Gemfile
├── Rakefile
└── pg_saurus.gemspec
/.ruby-version:
--------------------------------------------------------------------------------
1 | 3.3.9
2 |
--------------------------------------------------------------------------------
/spec/dummy/log/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.ruby-gemset:
--------------------------------------------------------------------------------
1 | pg_saurus
2 |
--------------------------------------------------------------------------------
/tmp/metric_fu/_data/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/mailers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/lib/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --colour
2 | --format documentation
3 | --profile
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/pet.rb:
--------------------------------------------------------------------------------
1 | class Pet < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/spec/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/lib/pg_saurus/version.rb:
--------------------------------------------------------------------------------
1 | module PgSaurus
2 | # Version of pg_saurus gem.
3 | VERSION = "6.0.0"
4 | end
5 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/abstract_adapter/foreign_key_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/pg_saurus.rb:
--------------------------------------------------------------------------------
1 | PgSaurus.configure do |config|
2 | config.ensure_role_set = false
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/demography/citizen.rb:
--------------------------------------------------------------------------------
1 | class Demography::Citizen < ActiveRecord::Base
2 | belongs_to :user
3 | end
4 |
--------------------------------------------------------------------------------
/lib/tasks/pg_saurus_tasks.rake:
--------------------------------------------------------------------------------
1 | # desc "Explaining what the task does"
2 | # task :pg_saurus do
3 | # # Task goes here
4 | # end
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/demography.rb:
--------------------------------------------------------------------------------
1 | module Demography
2 | def self.table_name_prefix
3 | 'demography_'
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ActiveRecord::Base
2 | has_one :citizen, class_name: 'Demography::Citizen'
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/demography/country.rb:
--------------------------------------------------------------------------------
1 | class Demography::Country < ActiveRecord::Base
2 | has_many :citizens, class_name: 'Demography::Citizen'
3 | end
4 |
--------------------------------------------------------------------------------
/docs/PgPower Ligtning Talk - WindyCityRails 2012.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HornsAndHooves/pg_saurus/HEAD/docs/PgPower Ligtning Talk - WindyCityRails 2012.pdf
--------------------------------------------------------------------------------
/lib/pg_saurus/migration.rb:
--------------------------------------------------------------------------------
1 | module PgSaurus::Migration # :nodoc:
2 | extend ActiveSupport::Autoload
3 |
4 | autoload :CommandRecorder
5 | autoload :SetRoleMethod
6 | end
7 |
--------------------------------------------------------------------------------
/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 Dummy::Application
5 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120516222439_add_functional_index.rb:
--------------------------------------------------------------------------------
1 | class AddFunctionalIndex < ActiveRecord::Migration[5.2]
2 | def change
3 | add_index :pets, ["lower(name)"]
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | Dummy::Application.initialize!
6 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120720103117_create_people.rb:
--------------------------------------------------------------------------------
1 | class CreatePeople < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table :people do |t|
4 | t.string :name
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120720103136_move_people_to_demography_schema.rb:
--------------------------------------------------------------------------------
1 | class MovePeopleToDemographySchema < ActiveRecord::Migration[5.2]
2 | def change
3 | move_table_to_schema :people, :demography
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20190801025445_add_compound_functional_index.rb:
--------------------------------------------------------------------------------
1 | class AddCompoundFunctionalIndex < ActiveRecord::Migration[5.2]
2 | def change
3 | add_index :pets, ["lower(color)", "lower(name)"]
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120301171826_remove_demography_nationalities.rb:
--------------------------------------------------------------------------------
1 | class RemoveDemographyNationalities < ActiveRecord::Migration[5.2]
2 | def change
3 | drop_table 'nationalities', schema: 'demography'
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120517211922_add_partial_functional_index.rb:
--------------------------------------------------------------------------------
1 | class AddPartialFunctionalIndex < ActiveRecord::Migration[5.2]
2 | def change
3 | add_index :pets, ["upper(color)"], where: 'name IS NULL'
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120904113806_add_concurrently_index_to_users_on_email.rb:
--------------------------------------------------------------------------------
1 | class AddConcurrentlyIndexToUsersOnEmail < ActiveRecord::Migration[5.2]
2 | def change
3 | add_index :users, :email, concurrently: true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20130624154800_create_demography_views.rb:
--------------------------------------------------------------------------------
1 | class CreateDemographyViews < ActiveRecord::Migration[5.2]
2 | def change
3 | create_view "demography.citizens_view", "select * from demography.citizens"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/.metrics:
--------------------------------------------------------------------------------
1 | MetricFu::Configuration.run do |config|
2 | config.configure_metric(:cane) do |cane|
3 | cane.line_length = 100
4 | end
5 |
6 | config.configure_metric(:flay) do |flay|
7 | flay.minimum_score = 10
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/generators/pg_saurus/install/templates/config/initializers/pg_saurus.rb:
--------------------------------------------------------------------------------
1 | # Configure PgSaurus behaviour.
2 | #
3 | PgSaurus.configure do |config|
4 | # Set to true if you want to enforce migrations to set role.
5 | config.ensure_role_set = false
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Sample localization file for English. Add more files in this directory for other locales.
2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | en:
5 | hello: "Hello world"
6 |
--------------------------------------------------------------------------------
/lib/core_ext/active_record/errors.rb:
--------------------------------------------------------------------------------
1 | module ActiveRecord
2 | # Raised when an DB operation cannot be carried out because the current
3 | # database user lacks the required privileges.
4 | class InsufficientPrivilege < WrappedDatabaseException
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/abstract_adapter/index_methods.rb:
--------------------------------------------------------------------------------
1 | # Extends ActiveRecord::ConnectionAdapters::AbstractAdapter.
2 | module PgSaurus::ConnectionAdapters::AbstractAdapter::IndexMethods
3 | def supports_partial_index?
4 | false
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/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 | # Mime::Type.register_alias "text/html", :iphone
6 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120301152819_create_demography_nationalities.rb:
--------------------------------------------------------------------------------
1 | class CreateDemographyNationalities < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table 'demography.nationalities' do |t|
4 | t.string :name
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20190320025645_add_functional_index_with_longer_operator_string.rb:
--------------------------------------------------------------------------------
1 | class AddFunctionalIndexWithLongerOperatorString < ActiveRecord::Migration[5.2]
2 | def change
3 | add_index :pets, ["trim(lower(name)) DESC NULLS LAST"]
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/foreign_key_definition.rb:
--------------------------------------------------------------------------------
1 | module PgSaurus::ConnectionAdapters
2 | # Structure to store information about foreign keys related to from_table.
3 | class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options)
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120904114121_add_concurrently_index_with_foreign_key.rb:
--------------------------------------------------------------------------------
1 | class AddConcurrentlyIndexWithForeignKey < ActiveRecord::Migration[5.2]
2 | def change
3 | add_foreign_key :pets, :users, exclude_index: true#, :concurrent_index => true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120207112916_remove_comment_on_pets_table.rb:
--------------------------------------------------------------------------------
1 | class RemoveCommentOnPetsTable < ActiveRecord::Migration[5.2]
2 | def up
3 | remove_table_comment "pets"
4 | end
5 |
6 | def down
7 | set_table_comment "pets", "Pets"
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20130504233224_remove_index_comments.rb:
--------------------------------------------------------------------------------
1 | class RemoveIndexComments < ActiveRecord::Migration[5.2]
2 | def change
3 | remove_index_comment 'demography.index_demography_cities_on_country_id'
4 | remove_index_comment 'index_pets_on_breed_id'
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120904114120_remove_foreign_key_from_pets_on_user_id.rb:
--------------------------------------------------------------------------------
1 | class RemoveForeignKeyFromPetsOnUserId < ActiveRecord::Migration[5.2]
2 | def up
3 | remove_foreign_key :pets, :users
4 | end
5 |
6 | def down
7 | add_foreign_key :pets, :users
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__)
3 |
4 | if File.exist?(gemfile)
5 | ENV['BUNDLE_GEMFILE'] = gemfile
6 | require 'bundler'
7 | Bundler.setup
8 | end
9 |
10 | $:.unshift File.expand_path('../../../../lib', __FILE__)
--------------------------------------------------------------------------------
/spec/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | # Add your own tasks in files placed in lib/tasks ending in .rake,
3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
4 |
5 | require File.expand_path('../config/application', __FILE__)
6 |
7 | Dummy::Application.load_tasks
8 |
--------------------------------------------------------------------------------
/script/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | #!/usr/bin/env ruby
3 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
4 |
5 | ENGINE_PATH = File.expand_path('../..', __FILE__)
6 | load File.expand_path('../../spec/dummy/script/rails', __FILE__)
7 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120301153650_demography_population_statistics.rb:
--------------------------------------------------------------------------------
1 | class DemographyPopulationStatistics < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table "population_statistics", schema: "demography" do |t|
4 | t.integer :year
5 | t.integer :population
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 | <%= stylesheet_link_tag "application" %>
6 | <%= javascript_include_tag "application" %>
7 | <%= csrf_meta_tags %>
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/spec/dummy/script/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3 |
4 | APP_PATH = File.expand_path('../../config/application', __FILE__)
5 | require File.expand_path('../../config/boot', __FILE__)
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/spec/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | development: &default
2 | adapter: postgresql
3 | #adapter: jdbcpostgresql
4 | encoding: unicode
5 | database: pg_saurus_dummy_development
6 | pool: 5
7 | #username: postgres
8 | #password: secret
9 | #host: localhost
10 |
11 | test:
12 | <<: *default
13 | database: pg_saurus_dummy_test
14 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | before_script:
3 | - psql -c "create database pg_saurus_dummy_test;" -U postgres
4 | script: "bundle exec rake spec"
5 | rvm:
6 | - 1.9.3
7 | - 2.0.0
8 | - 2.1.0
9 | env:
10 | - RAILS_VERSION="4.0.2"
11 | - RAILS_VERSION="4.1.0.beta1"
12 | notifications:
13 | email:
14 | - blake131313@gmail.com
15 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120209094937_create_cities_table.rb:
--------------------------------------------------------------------------------
1 | class CreateCitiesTable < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table 'demography.cities' do |t|
4 | t.integer :country_id
5 | t.integer :name
6 | end
7 |
8 | add_foreign_key "demography.cities", "demography.countries"
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120224204546_add_demography_citizens_active_column.rb:
--------------------------------------------------------------------------------
1 | class AddDemographyCitizensActiveColumn < ActiveRecord::Migration[5.2]
2 | def change
3 | add_column 'demography.citizens', :active, :boolean, null: false, default: false
4 |
5 | add_index 'demography.citizens', [:country_id, :user_id], unique: true, where: 'active'
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | TargetRubyVersion: 2.4
3 | # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop
4 | # to ignore them, so only the ones explicitly set in this file are enabled.
5 | DisabledByDefault: true
6 |
7 | # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
8 | Style/HashSyntax:
9 | Enabled: true
10 |
--------------------------------------------------------------------------------
/lib/pg_saurus/config.rb:
--------------------------------------------------------------------------------
1 | module PgSaurus
2 | # Configuration for PgSaurus behaviour.
3 | class Config
4 | # When true, raise exception if migration is executed without a role.
5 | attr_accessor :ensure_role_set
6 |
7 | # Instantiate and set default config settings.
8 | def initialize
9 | @ensure_role_set = false
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120201163544_create_owners_and_breeds.rb:
--------------------------------------------------------------------------------
1 | class CreateOwnersAndBreeds < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table :owners do |t|
4 | t.string :name
5 |
6 | t.timestamps
7 | end
8 |
9 | create_table :breeds do |t|
10 | t.string :name
11 |
12 | t.timestamps
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/table.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend ActiveRecord::ConnectionAdapters::Table
2 | # to support pg_saurus features.
3 | module PgSaurus::ConnectionAdapters::Table
4 | extend ActiveSupport::Autoload
5 |
6 | autoload :CommentMethods
7 | autoload :TriggerMethods
8 |
9 | include CommentMethods
10 | include TriggerMethods
11 |
12 | end
13 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120207103858_remove_comments_on_countries_table.rb:
--------------------------------------------------------------------------------
1 | class RemoveCommentsOnCountriesTable < ActiveRecord::Migration[5.2]
2 | def up
3 | remove_table_comment 'demography.countries'
4 | remove_column_comment 'demography.countries', :continent
5 | end
6 |
7 | def down
8 | set_table_comment 'demography.countries', "Countries"
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120208114020_remove_some_comments_on_citizens.rb:
--------------------------------------------------------------------------------
1 | class RemoveSomeCommentsOnCitizens < ActiveRecord::Migration[5.2]
2 | def up
3 | remove_column_comments 'demography.citizens', :birthday, :bio
4 | end
5 |
6 | def down
7 | set_column_comments 'demography.citizens',
8 | birthday: "Birthday",
9 | bio: "Biography"
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll automatically include all the stylesheets available in this directory
3 | * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
4 | * the top of the compiled file, but it's generally better to create a new file per style scope.
5 | *= require_self
6 | *= require_tree .
7 | */
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20220709040946_add_function_returning_a_table_type.rb:
--------------------------------------------------------------------------------
1 | class AddFunctionReturningATableType < ActiveRecord::Migration[6.1]
2 | def change
3 | create_function 'select_authors()', "TABLE (author_id INTEGER)", <<-FUNCTION.gsub(/^[\s]{6}/, ""), replace: false
4 | BEGIN
5 | RETURN query (
6 | SELECT author_id FROM books
7 | );
8 | END;
9 | FUNCTION
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters.rb:
--------------------------------------------------------------------------------
1 | module PgSaurus::ConnectionAdapters # :nodoc:
2 | extend ActiveSupport::Autoload
3 |
4 | autoload :AbstractAdapter
5 | autoload :PostgreSQLAdapter, 'pg_saurus/connection_adapters/postgresql_adapter'
6 | autoload :Table
7 | autoload :FunctionDefinition, 'pg_saurus/connection_adapters/function_definition'
8 | autoload :TriggerDefinition, 'pg_saurus/connection_adapters/trigger_definition'
9 | end
10 |
--------------------------------------------------------------------------------
/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
4 | # (all these examples are active by default):
5 | # ActiveSupport::Inflector.inflections do |inflect|
6 | # inflect.plural /^(ox)$/i, '\1en'
7 | # inflect.singular /^(ox)en/i, '\1'
8 | # inflect.irregular 'person', 'people'
9 | # inflect.uncountable %w( fish sheep )
10 | # end
11 |
--------------------------------------------------------------------------------
/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/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session'
4 |
5 | # Use the database for sessions instead of the cookie-based default,
6 | # which shouldn't be used to store highly confidential information
7 | # (create the session table with "rails generate session_migration")
8 | # Dummy::Application.config.session_store :active_record_store
9 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/abstract_adapter/comment_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PgSaurus::ConnectionAdapters::AbstractAdapter::CommentMethods do
4 | class AbstractAdapter
5 | include ::PgSaurus::ConnectionAdapters::AbstractAdapter::CommentMethods
6 | end
7 |
8 | let(:adapter_stub) { AbstractAdapter.new }
9 |
10 | it ".supports_comments?" do
11 | expect(adapter_stub.supports_comments?).to be false
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/abstract_adapter/trigger_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe PgSaurus::ConnectionAdapters::AbstractAdapter::TriggerMethods do
4 | class AbstractAdapter
5 | include ::PgSaurus::ConnectionAdapters::AbstractAdapter::TriggerMethods
6 | end
7 |
8 | let(:adapter_stub) { AbstractAdapter.new }
9 |
10 | it ".supports_functions?" do
11 | expect(adapter_stub.supports_triggers?).to be false
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into including all the files listed below.
2 | // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
3 | // be included in the compiled file accessible from http://example.com/assets/application.js
4 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
5 | // the compiled file.
6 | //
7 | //= require_tree .
8 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/abstract_adapter/function_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe PgSaurus::ConnectionAdapters::AbstractAdapter::FunctionMethods do
4 | class AbstractAdapter
5 | include ::PgSaurus::ConnectionAdapters::AbstractAdapter::FunctionMethods
6 | end
7 |
8 | let(:adapter_stub) { AbstractAdapter.new }
9 |
10 | it ".supports_functions?" do
11 | expect(adapter_stub.supports_functions?).to be false
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/abstract_adapter/index_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PgSaurus::ConnectionAdapters::AbstractAdapter::IndexMethods do
4 | class AbstractAdapter
5 | include ::PgSaurus::ConnectionAdapters::AbstractAdapter::IndexMethods
6 | end
7 |
8 | let(:adapter_stub) { AbstractAdapter.new }
9 |
10 | it ".supports_partial_index?" do
11 | expect(adapter_stub.supports_partial_index?).to be false
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120106163711_create_demography_schema.rb:
--------------------------------------------------------------------------------
1 | class CreateDemographySchema < ActiveRecord::Migration[5.2]
2 | def change
3 | # do not change the order of these schema;
4 | # they are ordered this way to increase the likelihood of being dumped out of alphabetical order
5 | # if the sorting code breaks (triggering a test failure). -mike 20120306
6 | create_schema 'latest'
7 | create_schema 'demography'
8 | create_schema 'later'
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/postgresql_adapter/index_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PgSaurus::ConnectionAdapters::PostgreSQLAdapter::IndexMethods do
4 | class PostgreSQLAdapter
5 | include ::PgSaurus::ConnectionAdapters::PostgreSQLAdapter::IndexMethods
6 | end
7 |
8 | let(:adapter_stub) { PostgreSQLAdapter.new }
9 |
10 | it ".supports_partial_index?" do
11 | expect(adapter_stub.supports_partial_index?).to be true
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120106163810_create_demography_countries.rb:
--------------------------------------------------------------------------------
1 | class CreateDemographyCountries < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table 'demography.countries' do |t|
4 | t.string :name
5 | t.string :continent
6 |
7 | t.timestamps
8 | end
9 |
10 | set_table_comment 'demography.countries', "Countries"
11 |
12 | set_column_comments 'demography.countries',
13 | name: "Country name",
14 | continent: "Continent"
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20140910125700_create_rock_bands_schema_if_not_exists.rb:
--------------------------------------------------------------------------------
1 | class CreateRockBandsSchemaIfNotExists < ActiveRecord::Migration[5.2]
2 | def change
3 | create_schema_if_not_exists(:rock_bands)
4 | # Should not raise exception even if the same schema exists
5 | create_schema_if_not_exists(:rock_bands)
6 |
7 | drop_schema_if_exists(:rock_bands)
8 | # Should not raise exception even the schema does not exist
9 | drop_schema_if_exists(:rock_bands)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120105112744_create_pets.rb:
--------------------------------------------------------------------------------
1 | class CreatePets < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table :pets do |t|
4 | t.string :name
5 | t.string :color
6 | t.integer :user_id
7 | t.integer :country_id
8 | t.integer :citizen_id
9 | t.integer :breed_id
10 | t.integer :owner_id
11 | t.boolean :active, default: true
12 | end
13 |
14 | add_index(:pets, :color)
15 |
16 | set_table_comment :pets, "Pets"
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20130504232621_add_index_comments.rb:
--------------------------------------------------------------------------------
1 | class AddIndexComments < ActiveRecord::Migration[5.2]
2 | def change
3 | set_index_comment 'demography.index_demography_citizens_on_country_id_and_user_id', 'Unique index on active citizens'
4 | set_index_comment 'demography.index_demography_cities_on_country_id', 'Index on country id'
5 | set_index_comment 'index_pets_on_breed_id', 'Index on breed_id'
6 | set_index_comment 'index_pets_on_to_tsvector_name_gist', 'Functional index on name'
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/abstract_adapter.rb:
--------------------------------------------------------------------------------
1 | # Extends ActiveRecord::ConnectionAdapters::AbstractAdapter class.
2 | module PgSaurus::ConnectionAdapters::AbstractAdapter
3 | extend ActiveSupport::Autoload
4 |
5 | autoload :CommentMethods
6 | autoload :SchemaMethods
7 | autoload :IndexMethods
8 | autoload :FunctionMethods
9 | autoload :TriggerMethods
10 |
11 | include CommentMethods
12 | include SchemaMethods
13 | include IndexMethods
14 | include FunctionMethods
15 | include TriggerMethods
16 | end
17 |
--------------------------------------------------------------------------------
/spec/lib/core_ext/connection_adapters/postgresql_adapter_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveRecord::ConnectionAdapters::PostgreSQLAdapter do
4 | let(:connection) { ActiveRecord::Base.connection }
5 | subject { connection}
6 |
7 | describe '#tables' do
8 | it 'returns tables from public schema' do
9 | connection.tables.should include "users"
10 | end
11 |
12 | it 'returns tables non public schemas' do
13 | connection.tables.should include "demography.cities"
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/function_definition.rb:
--------------------------------------------------------------------------------
1 | module PgSaurus::ConnectionAdapters
2 | # Struct definition for a DB function.
3 | class FunctionDefinition < Struct.new( :name,
4 | :returning,
5 | :definition,
6 | :function_type,
7 | :language,
8 | :oid,
9 | :volatility )
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 | # Make sure the secret is at least 30 characters and all random,
6 | # no regular words or you'll be exposed to dictionary attacks.
7 | Dummy::Application.config.secret_token = '04052f11364e8a797da778bea597b0f79bca3e8d639f02402a376609e0ff10c0ce79c38a4a5e1234c83a827aefef03831f0763a57bee6b4e3184c839452870f2'
8 |
--------------------------------------------------------------------------------
/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]
9 | end
10 |
11 | # Disable root element in JSON by default.
12 | ActiveSupport.on_load(:active_record) do
13 | self.include_root_in_json = false
14 | end
15 |
--------------------------------------------------------------------------------
/lib/pg_saurus/errors.rb:
--------------------------------------------------------------------------------
1 | module PgSaurus
2 | # Base error for PgSaurus errors.
3 | class Error < StandardError; end
4 |
5 | # Raised when an unexpected index exists
6 | class IndexExistsError < Error; end
7 |
8 | # Raised if config.ensure_role_set = true, but migration have no role set.
9 | class RoleNotSetError < Error; end
10 |
11 | # Raised if set_role used for data change migration.
12 | class UseKeepDefaultRoleError < Error; end
13 |
14 | # Raised if keep_default_role used for structure change migration.
15 | class UseSetRoleError < Error; end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120106163544_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table :users do |t|
4 | t.string :name
5 | t.string :email
6 | t.string :phone_number
7 |
8 | t.timestamps
9 | end
10 |
11 | add_index(:users, :name)
12 |
13 | set_table_comment :users, "Information about users"
14 |
15 | set_column_comment :users, :name, "User name"
16 |
17 | set_column_comments :users,
18 | email: "Email address",
19 | phone_number: "Phone number"
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/generators/pg_saurus/install/install_generator.rb:
--------------------------------------------------------------------------------
1 | module PgSaurus
2 | # :nodoc:
3 | module Generators
4 | # Generates config/initializers/pg_saurus.rb with default settings.
5 | class InstallGenerator < ::Rails::Generators::Base
6 |
7 | # :nodoc:
8 | desc <<-DESC
9 | Description:
10 | Create default PgSaurus configuration
11 | DESC
12 |
13 | source_root File.expand_path('../templates', __FILE__)
14 |
15 | # :nodoc:
16 | def copy_rails_files
17 | template "config/initializers/pg_saurus.rb"
18 | end
19 |
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # we don't want Gemfile.lock in gems
2 | Gemfile.lock
3 |
4 | .bundle/
5 | log/*.log
6 | pkg/
7 | .yardoc/*
8 | doc/*
9 |
10 | coverage/*
11 |
12 | spec/dummy/db/*.sqlite3
13 | spec/dummy/log/*.log
14 | spec/dummy/tmp/
15 |
16 | .idea
17 | .generators
18 | .rakeTasks
19 |
20 | TAGS
21 | ctags
22 | mtags
23 | tags
24 |
25 | # exclude everything in tmp
26 | tmp/*
27 | # except the metric_fu directory
28 | !tmp/metric_fu/
29 | # but exclude everything *in* the metric_fu directory
30 | tmp/metric_fu/*
31 | # except for the _data directory to track metrical outputs
32 | !tmp/metric_fu/_data/
33 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails gems
3 | # installed from the root of your application.
4 |
5 | ENGINE_ROOT = File.expand_path('..', __dir__)
6 | ENGINE_PATH = File.expand_path('../lib/pg_saurus/engine', __dir__)
7 | APP_PATH = File.expand_path('../spec/dummy/config/application', __dir__)
8 |
9 | # Set up gems listed in the Gemfile.
10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
11 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
12 |
13 | require 'rails/all'
14 | require 'rails/engine/commands'
15 |
--------------------------------------------------------------------------------
/lib/pg_saurus/migration/command_recorder.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend ActiveRecord::Migration::CommandRecorder to
2 | # support pg_saurus features.
3 | module PgSaurus::Migration::CommandRecorder
4 | extend ActiveSupport::Autoload
5 |
6 | autoload :ExtensionMethods
7 | autoload :SchemaMethods
8 | autoload :CommentMethods
9 | autoload :ViewMethods
10 | autoload :FunctionMethods
11 | autoload :TriggerMethods
12 |
13 | include ExtensionMethods
14 | include SchemaMethods
15 | include CommentMethods
16 | include ViewMethods
17 | include FunctionMethods
18 | include TriggerMethods
19 | end
20 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120207150844_add_foreign_keys.rb:
--------------------------------------------------------------------------------
1 | class AddForeignKeys < ActiveRecord::Migration[5.2]
2 | def change
3 | # Add foreign keys with indexes
4 | add_foreign_key 'pets', 'users'
5 | add_foreign_key 'pets', 'owners'
6 | add_foreign_key 'pets', 'breeds'
7 | add_foreign_key 'pets', 'demography.countries'
8 | add_foreign_key 'demography.citizens', 'demography.countries' # This foreign key is removed in RemoveForeignKeys migration
9 |
10 | # Add foreign key without an index
11 | add_foreign_key 'demography.citizens', 'users', exclude_index: true
12 |
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/postgresql_adapter/foreign_key_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PgSaurus::ConnectionAdapters::PostgreSQLAdapter::ForeignKeyMethods do
4 | class PostgreSQLAdapter
5 | prepend ::PgSaurus::ConnectionAdapters::PostgreSQLAdapter::ForeignKeyMethods
6 | end
7 |
8 | let(:adapter_stub) { PostgreSQLAdapter.new }
9 |
10 | describe ".drop_table" do
11 | it "disables referential integrity if options :force" do
12 | expect(adapter_stub).to receive(:disable_referential_integrity)
13 | adapter_stub.drop_table(force: true)
14 | end
15 | end
16 |
17 | end
18 |
--------------------------------------------------------------------------------
/lib/pg_saurus/migration/command_recorder/trigger_methods.rb:
--------------------------------------------------------------------------------
1 | # Methods to extend ActiveRecord::Migration::CommandRecorder to
2 | # support database triggers.
3 | module PgSaurus::Migration::CommandRecorder::TriggerMethods
4 |
5 | # :nodoc:
6 | def create_trigger(*args)
7 | record :create_trigger, args
8 | end
9 |
10 | # :nodoc:
11 | def remove_trigger(*args)
12 | record :remove_trigger, args
13 | end
14 |
15 | # :nodoc:
16 | def invert_create_trigger(args)
17 | table_name, proc_name, _, options = *args
18 | options ||= {}
19 |
20 | [:remove_trigger, [table_name, proc_name, options]]
21 | end
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/lib/pg_saurus/migration/command_recorder/function_methods.rb:
--------------------------------------------------------------------------------
1 | # Methods to extend ActiveRecord::Migration::CommandRecorder to
2 | # support database functions.
3 | module PgSaurus::Migration::CommandRecorder::FunctionMethods
4 |
5 | # :nodoc
6 | def create_function(*args)
7 | record :create_function, args
8 | end
9 |
10 | # :nodoc
11 | def drop_function(*args)
12 | record :drop_function, args
13 | end
14 |
15 | # :nodoc
16 | def invert_create_function(args)
17 | function_name = args.first
18 | schema = args.last[:schema]
19 |
20 | [:drop_function, [function_name, { schema: schema }]]
21 | end
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120207163652_remove_foreign_keys.rb:
--------------------------------------------------------------------------------
1 | class RemoveForeignKeys < ActiveRecord::Migration[5.2]
2 | def up
3 | remove_foreign_key 'demography.citizens', column: :country_id, remove_index: true
4 | remove_foreign_key 'pets', 'demography.countries'
5 | #remove_foreign_key 'pets', 'owners'
6 | remove_foreign_key 'pets', column: "owner_id", remove_index: true
7 | remove_foreign_key 'pets', column: "breed_id"
8 | end
9 |
10 | def down
11 | add_foreign_key 'demography.citizens', 'demography.countries'
12 | add_foreign_key 'pets', 'demography.countries'
13 | add_foreign_key 'pets', 'owners'
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/postgresql_adapter/view_methods.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend {ActiveRecord::ConnectionAdapters::PostgreSQLAdapter}
2 | # to support views feature.
3 | module PgSaurus::ConnectionAdapters::PostgreSQLAdapter::ViewMethods
4 | # Creates new view in DB.
5 | # @param [String, Symbol] view_name
6 | # @param [String] view_definition
7 | def create_view(view_name, view_definition)
8 | ::PgSaurus::Tools.create_view(view_name, view_definition)
9 | end
10 |
11 | # Drops view in DB.
12 | # @param [String, Symbol] view_name
13 | def drop_view(view_name)
14 | ::PgSaurus::Tools.drop_view(view_name)
15 | end
16 |
17 | end
18 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20150713035548_add_function_count_pets.rb:
--------------------------------------------------------------------------------
1 | class AddFunctionCountPets < ActiveRecord::Migration[5.2]
2 | def change
3 | create_function 'pets_not_empty()', :boolean, <<-FUNCTION.gsub(/^[\s]{6}/, ""), schema: 'public'
4 | BEGIN
5 | IF (SELECT COUNT(*) FROM pets) > 0
6 | THEN
7 | RETURN true;
8 | ELSE
9 | RETURN false;
10 | END IF;
11 | END;
12 | FUNCTION
13 |
14 | create_function 'public.foo_bar()', :boolean, <<-FUNCTION.gsub(/^[\s]{6}/, ""), replace: false
15 | BEGIN
16 | RETURN true;
17 | END;
18 | FUNCTION
19 |
20 | drop_function 'foo_bar()'
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/pg_saurus.rb:
--------------------------------------------------------------------------------
1 | require "pg_saurus/engine"
2 | require "pg_saurus/errors"
3 | require "pg_saurus/config"
4 |
5 | # Rails engine which allows to use some PostgreSQL features:
6 | # * Schemas.
7 | # * Comments on columns and tables.
8 | # * Foreign keys.
9 | # * Partial indexes.
10 | module PgSaurus
11 | extend ActiveSupport::Autoload
12 |
13 | autoload :Adapter
14 | autoload :SchemaDumper
15 | autoload :Tools
16 | autoload :Migration
17 | autoload :ConnectionAdapters
18 | autoload :CreateIndexConcurrently
19 |
20 | mattr_accessor :config
21 | self.config = PgSaurus::Config.new
22 |
23 | # Configure the engine.
24 | def self.configure
25 | yield(config)
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/trigger_definition.rb:
--------------------------------------------------------------------------------
1 | module PgSaurus::ConnectionAdapters
2 |
3 | # Struct definition for a DB trigger.
4 | class TriggerDefinition < Struct.new( :name,
5 | :proc_name,
6 | :constraint,
7 | :event,
8 | :for_each,
9 | :deferrable,
10 | :initially_deferred,
11 | :condition,
12 | :table,
13 | :schema )
14 |
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/pg_saurus/schema_dumper.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend {ActiveRecord::SchemaDumper} to appropriately
2 | # build schema.rb file with schemas, foreign keys and comments on columns
3 | # and tables.
4 | module PgSaurus::SchemaDumper
5 | extend ActiveSupport::Autoload
6 |
7 | autoload :ExtensionMethods
8 | autoload :CommentMethods
9 | autoload :SchemaMethods
10 | autoload :ForeignKeyMethods
11 | autoload :ViewMethods
12 | autoload :FunctionMethods
13 | autoload :TriggerMethods
14 |
15 | include ExtensionMethods
16 | include CommentMethods
17 | include SchemaMethods
18 | include ForeignKeyMethods
19 | include ViewMethods
20 | include FunctionMethods
21 | include TriggerMethods
22 | end
23 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20121009170904_create_extension.rb:
--------------------------------------------------------------------------------
1 | class CreateExtension < ActiveRecord::Migration[5.2]
2 | def change
3 | create_extension "fuzzystrmatch"
4 | drop_extension "fuzzystrmatch", mode: :cascade
5 | create_extension "fuzzystrmatch"
6 |
7 | # Test names with dashes are escaped correctly.
8 | # https://github.com/TMXCredit/pg_power/pull/40
9 | create_extension "uuid-ossp"
10 | drop_extension "uuid-ossp"
11 |
12 | create_extension "btree_gist", schema_name: 'demography'
13 | add_index :pets, :user_id, using: :gist, name: 'index_pets_on_user_id_gist'
14 | add_index :pets, "to_tsvector('english', name)", using: :gist, name: 'index_pets_on_to_tsvector_name_gist'
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/abstract_adapter_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PgSaurus::ConnectionAdapters::AbstractAdapter do
4 | class AbstractAdapterStub
5 | include ::PgSaurus::ConnectionAdapters::AbstractAdapter
6 | end
7 |
8 | let(:adapter_stub){ AbstractAdapterStub.new }
9 |
10 | it 'should define method stubs for comment methods' do
11 | [ :set_table_comment,
12 | :set_column_comment,
13 | :set_column_comments,
14 | :remove_table_comment,
15 | :remove_column_comment,
16 | :remove_column_comments,
17 | :set_index_comment,
18 | :remove_index_comment
19 | ].each { |method_name| adapter_stub.respond_to?(method_name).should be true }
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/colorized_text.rb:
--------------------------------------------------------------------------------
1 | # Colorizes text with ASCII colors.
2 | # == Usage:
3 | # include ColorizedText
4 | #
5 | # puts green "OK" # => green output
6 | # puts bold "Running... # => bold output
7 | # puts bold green "OK!!!" # => bold green output
8 | module ColorizedText
9 | # Colorize text using ASCII color code
10 | def colorize(text, code)
11 | "\033[#{code}m#{text}\033[0m"
12 | end
13 |
14 | # :nodoc:
15 | def yellow(text)
16 | colorize(text, 33)
17 | end
18 |
19 | # :nodoc:
20 | def green(text)
21 | colorize(text, 32)
22 | end
23 |
24 | # :nodoc:
25 | def red(text)
26 | colorize(text, 31)
27 | end
28 |
29 | # :nodoc:
30 | def bold(text)
31 | colorize(text, 1)
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/pg_saurus/migration/command_recorder/extension_methods.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend ActiveRecord::Migration::CommandRecorder to
2 | # support extensions feature.
3 | module PgSaurus::Migration::CommandRecorder::ExtensionMethods
4 | # :nodoc:
5 | def create_extension(*args)
6 | record(:create_extension, args)
7 | end
8 |
9 | # :nodoc:
10 | def drop_extension(*args)
11 | record(:drop_extension, args)
12 | end
13 |
14 | # :nodoc:
15 | def invert_create_extension(args)
16 | extension_name = args.first
17 | [:drop_extension, [extension_name]]
18 | end
19 |
20 | # :nodoc:
21 | def invert_drop_extension(args)
22 | extension_name = args.first
23 | [:create_extension, [extension_name]]
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/migration/command_recorder/view_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PgSaurus::Migration::CommandRecorder::ViewMethods do
4 | class CommandRecorderStub
5 | include ::PgSaurus::Migration::CommandRecorder::ViewMethods
6 | end
7 |
8 | let(:command_recorder_stub) { CommandRecorderStub.new }
9 |
10 | [:create_view, :drop_view].each do |method_name|
11 | it ".#{method_name}" do
12 | expect(command_recorder_stub).to receive(:record).with(method_name, [:foo, :bar])
13 | command_recorder_stub.send(method_name, :foo, :bar)
14 | end
15 | end
16 |
17 | it ".invert_create_view" do
18 | expect(command_recorder_stub.invert_create_view([:foo, :bar])).
19 | to eq([:drop_view, [:foo]])
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20120106163820_create_demography_citizens.rb:
--------------------------------------------------------------------------------
1 | class CreateDemographyCitizens < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table 'demography.citizens' do |t|
4 | t.integer :country_id
5 | t.integer :user_id
6 | t.string :first_name
7 | t.string :last_name
8 | t.date :birthday
9 | t.text :bio
10 |
11 | t.timestamps
12 | end
13 |
14 | set_table_comment 'demography.citizens', "Citizens Info"
15 |
16 | set_column_comment 'demography.citizens', :country_id, 'Country key'
17 |
18 | set_column_comments 'demography.citizens',
19 | first_name: "First name",
20 | last_name: "Last name",
21 | birthday: "Birthday",
22 | bio: "Biography"
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/core_ext/active_record/migration/compatibility.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ActiveRecord
4 | class Migration
5 | module Compatibility
6 | # PgSaurus has been properly creating functional index names since Rails 4, so we don't want the old logic
7 | class V7_0
8 | module TableDefinition
9 | # Override https://github.com/rails/rails/blob/v7.2.2.2/activerecord/lib/active_record/migration/compatibility.rb#L80
10 | def index(...)
11 | super
12 | end
13 | end
14 | # Override https://github.com/rails/rails/blob/v7.2.2.2/activerecord/lib/active_record/migration/compatibility.rb#L102
15 | def add_index(...)
16 | super
17 | end
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] ||= 'test'
2 | # We probably don't want to use simplecov in Travis:CI, and we want to confirm
3 | # simplecov exists (it won't in ruby 1.8.X)
4 | if !ENV['TRAVIS']
5 | begin
6 | require 'simplecov'
7 | rescue LoadError
8 | end
9 | end
10 |
11 | require File.expand_path("../dummy/config/environment", __FILE__)
12 | require 'rspec/rails'
13 |
14 |
15 | Dir["#{File.expand_path('../', __FILE__)}/support/**/*.rb"].each {|f| require f}
16 |
17 | RSpec.configure do |config|
18 | config.mock_with :rspec
19 | # config.fixture_path = "#{::Rails.root}/spec/fixtures"
20 | config.use_transactional_fixtures = true
21 | config.infer_base_class_for_anonymous_controllers = false
22 |
23 | config.expect_with :rspec do |c|
24 | c.syntax = [:should, :expect]
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/postgresql_adapter/view_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PgSaurus::ConnectionAdapters::PostgreSQLAdapter::ViewMethods do
4 | class PostgreSQLAdapter
5 | include ::PgSaurus::ConnectionAdapters::PostgreSQLAdapter::ViewMethods
6 | end
7 |
8 | let(:adapter_stub) { PostgreSQLAdapter.new }
9 |
10 | describe ".create_view" do
11 | it "refers to tools create_view" do
12 | expect(::PgSaurus::Tools).to receive(:create_view).with("someview", "")
13 | adapter_stub.create_view("someview", "")
14 | end
15 | end
16 |
17 | describe ".drop_view" do
18 | it "refers to tools drop_view" do
19 | expect(::PgSaurus::Tools).to receive(:drop_view).with("someview")
20 | adapter_stub.drop_view("someview")
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/dummy/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The change you wanted was rejected.
23 |
Maybe you tried to change something you didn't have access to.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20190213151821_create_books.rb:
--------------------------------------------------------------------------------
1 | class CreateBooks < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table :books do |t|
4 | t.integer :author_id
5 | t.integer :publisher_id
6 | t.string :title
7 | t.json :tags
8 |
9 | t.timestamps
10 | end
11 |
12 | add_index :books, ["author_id", "publisher_id"],
13 | name: "books_author_id_and_publisher_id",
14 | order: { author_id: "DESC NULLS FIRST", publisher_id: "DESC NULLS LAST" }
15 |
16 | add_index :books, "title varchar_pattern_ops"
17 |
18 | add_index :books, "((tags->'attrs'->>'edition')::int)", name: "books_tags_json_index", skip_column_quoting: true
19 |
20 | set_table_comment :books, "Information about books"
21 |
22 | set_column_comment :books, :title, "Book title"
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The page you were looking for doesn't exist.
23 |
You may have mistyped the address or the page may have moved.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/spec/dummy/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
We're sorry, but something went wrong.
23 |
We've been notified about this issue and we'll take a look at it shortly.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/lib/pg_saurus/migration/command_recorder/view_methods.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend ActiveRecord::Migration::CommandRecorder to
2 | # support view feature.
3 | module PgSaurus::Migration::CommandRecorder::ViewMethods
4 | # Create a PostgreSQL view.
5 | #
6 | # @param args [Array] view_name and view_definition
7 | #
8 | # @return [view]
9 | def create_view(*args)
10 | record(:create_view, args)
11 | end
12 |
13 | # Drop a view in the DB.
14 | #
15 | # @param args [Array] first argument is view_name
16 | #
17 | # @return [void]
18 | def drop_view(*args)
19 | record(:drop_view, args)
20 | end
21 |
22 | # Invert the creation of a view in the DB.
23 | #
24 | # @param args [Array] first argument is supposed to be name of view
25 | #
26 | # @return [void]
27 | def invert_create_view(args)
28 | [:drop_view, [args.first]]
29 | end
30 |
31 | end
32 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/table/trigger_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe PgSaurus::ConnectionAdapters::Table::TriggerMethods do
4 | class AbstractTable
5 | include ::PgSaurus::ConnectionAdapters::Table::TriggerMethods
6 |
7 | def initialize
8 | @base = Object.new
9 | @name = "sometable"
10 | end
11 |
12 | end
13 |
14 | let(:table_stub) { AbstractTable.new }
15 | let(:base) { table_stub.instance_variable_get(:@base) }
16 |
17 | specify ".create_trigger" do
18 | expect(base).to receive(:create_trigger).with("sometable", "proc_name", "event", {})
19 |
20 | table_stub.create_trigger "proc_name", "event"
21 | end
22 |
23 | specify ".remove_trigger" do
24 | expect(base).to receive(:remove_trigger).with("sometable", "proc_name", {})
25 |
26 | table_stub.remove_trigger "proc_name"
27 | end
28 |
29 | end
30 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/postgresql_adapter/index_methods.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend {ActiveRecord::ConnectionAdapters::SchemaStatements}
2 | # to support index features.
3 | module PgSaurus::ConnectionAdapters::PostgreSQLAdapter::IndexMethods
4 | def supports_partial_index?
5 | true
6 | end
7 |
8 | # Overrides ActiveRecord::ConnectionAdapters::SchemaStatements.index_name
9 | # to support schema notation. Converts dots in index name to underscores.
10 | #
11 | # === Example
12 | # add_index 'demography.citizens', :country_id
13 | # # produces
14 | # CREATE INDEX "index_demography_citizens_on_country_id" ON "demography"."citizens" ("country_id")
15 | # # instead of
16 | # CREATE INDEX "index_demography.citizens_on_country_id" ON "demography"."citizens" ("country_id")
17 | #
18 | def index_name(table_name, options) #:nodoc:
19 | super.gsub('.','_')
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/pg_saurus/schema_dumper/function_methods.rb:
--------------------------------------------------------------------------------
1 | # Support for dumping database functions.
2 | module PgSaurus::SchemaDumper::FunctionMethods
3 |
4 | # :nodoc
5 | def tables(stream)
6 | # Functions must be dumped before tables.
7 | # Some indexes may use defined functions.
8 | dump_functions stream
9 |
10 | super(stream)
11 |
12 | stream
13 | end
14 |
15 | # Writes out a command to create each detected function.
16 | def dump_functions(stream)
17 | @connection.functions.each do |function|
18 | statement = " create_function '#{function.name}', '#{function.returning}', <<-FUNCTION_DEFINITION.gsub(/^[\s]{4}/, ''), volatility: :#{function.volatility}"
19 | statement << "\n#{function.definition.split("\n").map{|line| " #{line}" }.join("\n")}"
20 | statement << "\n FUNCTION_DEFINITION\n\n"
21 |
22 | stream.puts statement
23 | end
24 | end
25 |
26 | end
27 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/postgresql_adapter/translate_exception_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PgSaurus::ConnectionAdapters::PostgreSQLAdapter::TranslateException do
4 | let(:connection) { ActiveRecord::Base.connection }
5 |
6 | describe "#translate_exception" do
7 | it "intercepts insufficient privilege PG::Error" do
8 | exception = double("PG::Error").as_null_object.tap do |error|
9 | allow(error).to receive(:result) do
10 | double("PGResult").as_null_object.tap do |result|
11 | allow(result).
12 | to receive(:error_field).
13 | and_return(described_class::INSUFFICIENT_PRIVILEGE)
14 | end
15 | end
16 | end
17 |
18 | translated = connection.send(:translate_exception, exception, message: "", sql: "", binds: [])
19 | expect(translated).to be_an_instance_of(ActiveRecord::InsufficientPrivilege)
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/schema_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Schema methods' do
4 | describe '#create_table' do
5 | context 'with :schema option' do
6 | it 'creates table in passed schema' do
7 | PgSaurus::Explorer.table_exists?('demography.population_statistics').should == true
8 | end
9 | end
10 | end
11 |
12 | describe '#drop_table' do
13 | context 'with :schema option' do
14 | # NOTE: this test makes sense only if create_table works as expected.
15 | it 'removes table in passed schema' do
16 | PgSaurus::Explorer.table_exists?('demography.nationalities').should == false
17 | end
18 | end
19 | end
20 |
21 | describe '#move_table_to_schema' do
22 | it 'moves table to another schema' do
23 | PgSaurus::Explorer.table_exists?('public.people') .should == false
24 | PgSaurus::Explorer.table_exists?('demography.people').should == true
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/migration/command_recorder/extension_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PgSaurus::Migration::CommandRecorder::ExtensionMethods do
4 | class CommandRecorderStub
5 | include ::PgSaurus::Migration::CommandRecorder::ExtensionMethods
6 | end
7 |
8 | let(:command_recorder_stub) { CommandRecorderStub.new }
9 |
10 | [:create_extension, :drop_extension].each do |method_name|
11 | it ".#{method_name}" do
12 | expect(command_recorder_stub).to receive(:record).with(method_name, [:foo, :bar])
13 | command_recorder_stub.send(method_name, :foo, :bar)
14 | end
15 | end
16 |
17 | it ".invert_create_extension" do
18 | expect(command_recorder_stub.invert_create_extension([:foo, :bar])).
19 | to eq([:drop_extension, [:foo]])
20 | end
21 |
22 | it ".invert_drop_extension" do
23 | expect(command_recorder_stub.invert_drop_extension([:foo, :bar])).
24 | to eq([:create_extension, [:foo]])
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/pg_saurus/schema_dumper/extension_methods.rb:
--------------------------------------------------------------------------------
1 | # Extends ActiveRecord::SchemaDumper class to dump comments on tables and columns.
2 | module PgSaurus::SchemaDumper::ExtensionMethods
3 | # Overrides https://github.com/rails/rails/blob/v7.2.2.2/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb#L8
4 | #
5 | # Dump current database extensions recreation commands to the given stream.
6 | #
7 | # @param [#puts] stream Stream to write to
8 | def extensions(stream)
9 | extensions = @connection.pg_extensions
10 | commands = extensions.map do |extension_name, options|
11 | result = [%Q|create_extension "#{extension_name}"|]
12 | result << %Q|schema_name: "#{options[:schema_name]}"| unless options[:schema_name] == 'public'
13 | result << %Q|version: "#{options[:version]}"|
14 | result.join(', ')
15 | end
16 |
17 | commands.each do |command|
18 | stream.puts(" #{command}")
19 | end
20 |
21 | stream.puts
22 |
23 | super
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/abstract_adapter/schema_methods.rb:
--------------------------------------------------------------------------------
1 | # Extends ActiveRecord::ConnectionAdapters::AbstractAdapter
2 | # with methods for multi-schema support.
3 | module PgSaurus::ConnectionAdapters::AbstractAdapter::SchemaMethods
4 |
5 | # Provide :schema option to +create_table+ method.
6 | def create_table(table_name, options = {}, &block)
7 | table_name, options = extract_table_options(table_name, options)
8 | super(table_name, **options, &block)
9 | end
10 |
11 | # Provide :schema option to +drop_table+ method.
12 | def drop_table(table_name, options = {})
13 | table_name, options = extract_table_options(table_name, options)
14 | super(table_name, **options)
15 | end
16 |
17 | # Extract the table-specific options for the given table name from the options.
18 | def extract_table_options(table_name, options)
19 | options = options.dup
20 | schema_name = options.delete(:schema)
21 | table_name = "#{schema_name}.#{table_name}" if schema_name
22 | [table_name, options]
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/postgresql_adapter/translate_exception.rb:
--------------------------------------------------------------------------------
1 | # Extend ActiveRecord::ConnectionAdapter::PostgreSQLAdapter logic
2 | # to wrap more pg-specific errors into specific exception classes
3 | module PgSaurus::ConnectionAdapters::PostgreSQLAdapter::TranslateException
4 | # # See http://www.postgresql.org/docs/9.1/static/errcodes-appendix.html
5 | INSUFFICIENT_PRIVILEGE = "42501"
6 |
7 | # Intercept insufficient privilege PG::Error and raise active_record wrapped database exception
8 | def translate_exception(exception, message:, sql:, binds:)
9 | return exception unless exception.respond_to?(:result)
10 | exception_result = exception.result
11 |
12 | case exception_result.try(:error_field, PG::Result::PG_DIAG_SQLSTATE)
13 | when INSUFFICIENT_PRIVILEGE
14 | exc_message = exception_result.try(:error_field, PG::Result::PG_DIAG_MESSAGE_PRIMARY)
15 | exc_message ||= message
16 | ::ActiveRecord::InsufficientPrivilege.new(exc_message, sql: sql, binds: binds)
17 | else
18 | super
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/pg_saurus/schema_dumper/view_methods.rb:
--------------------------------------------------------------------------------
1 | # Extends ActiveRecord::SchemaDumper class to dump views
2 | module PgSaurus::SchemaDumper::ViewMethods
3 | # Dump create view statements
4 | def tables(stream)
5 | super(stream)
6 | views(stream)
7 | stream
8 | end
9 |
10 | # Generates code to create views.
11 | def views(stream)
12 | # Don't create "system" views.
13 | view_names = PgSaurus::Tools.views
14 | view_names.each do |options|
15 | write_view_definition(stream,
16 | options["table_schema"],
17 | options["table_name"],
18 | options["view_definition"])
19 | end
20 | stream << "\n"
21 | end
22 | private :views
23 |
24 | # Generates code to create view.
25 | def write_view_definition(stream, table_schema, table_name, view_definition)
26 | stream << " create_view \"#{table_schema}.#{table_name}\", <<-SQL\n" \
27 | " #{view_definition}\n" \
28 | " SQL\n"
29 | end
30 | private :write_view_definition
31 |
32 | end
33 |
--------------------------------------------------------------------------------
/lib/pg_saurus/schema_dumper/trigger_methods.rb:
--------------------------------------------------------------------------------
1 | # Support for dumping database triggers.
2 | module PgSaurus::SchemaDumper::TriggerMethods
3 |
4 | # :nodoc
5 | def tables(stream)
6 | super(stream)
7 |
8 | dump_triggers(stream)
9 | stream.puts
10 |
11 | stream
12 | end
13 |
14 | # Write out a command to create each detected trigger.
15 | def dump_triggers(stream)
16 | @connection.triggers.each do |trigger|
17 | statement = " create_trigger '#{trigger.table}', '#{trigger.proc_name}', '#{trigger.event}', " \
18 | "name: '#{trigger.name}', " \
19 | "constraint: #{trigger.constraint ? :true : :false}, " \
20 | "for_each: :#{trigger.for_each}, " \
21 | "deferrable: #{trigger.deferrable ? :true : :false}, " \
22 | "initially_deferred: #{trigger.initially_deferred ? :true : :false}, " \
23 | "schema: '#{trigger.schema}'"
24 |
25 | if trigger.condition
26 | statement << %Q{, condition: '#{trigger.condition.gsub("'", %q(\\\'))}'}
27 | end
28 |
29 | stream.puts "#{statement}\n"
30 | end
31 | end
32 |
33 | end
34 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/table/trigger_methods.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend ActiveRecord::ConnectionAdapters::Table
2 | # to support database triggers.
3 | module PgSaurus::ConnectionAdapters::Table::TriggerMethods
4 |
5 | # Creates a trigger.
6 | #
7 | # Example:
8 | #
9 | # change_table :pets do |t|
10 | # t.create_trigger :pets_not_empty_trigger_proc,
11 | # 'AFTER INSERT',
12 | # for_each: 'ROW',
13 | # schema: 'public',
14 | # constraint: true,
15 | # deferrable: true,
16 | # initially_deferred: true
17 | # end
18 | def create_trigger(proc_name, event, options = {})
19 | @base.create_trigger(@name, proc_name, event, options)
20 | end
21 |
22 | # Removes a trigger.
23 | #
24 | # Example:
25 | #
26 | # change_table :pets do |t|
27 | # t.remove_trigger :pets_not_empty_trigger_proc
28 | # end
29 | def remove_trigger(proc_name, options = {})
30 | @base.remove_trigger(@name, proc_name, options)
31 | end
32 |
33 | end
34 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20150714003209_create_pets_trigger.rb:
--------------------------------------------------------------------------------
1 | class CreatePetsTrigger < ActiveRecord::Migration[5.2]
2 | def change
3 | create_function "pets_not_empty_trigger_proc()",
4 | :trigger,
5 | <<-FUNCTION.gsub(/^[\s]{6}/, ""), schema: "public", volatility: :immutable
6 | BEGIN
7 | RETURN null;
8 | END;
9 | FUNCTION
10 |
11 | create_trigger :pets,
12 | :pets_not_empty_trigger_proc,
13 | "AFTER INSERT",
14 | for_each: "ROW",
15 | schema: "public",
16 | constraint: true,
17 | deferrable: true,
18 | initially_deferred: true,
19 | condition: "new.name = 'fluffy'"
20 |
21 |
22 | change_table :pets do |t|
23 | t.create_trigger :pets_not_empty_trigger_proc,
24 | "AFTER INSERT OR UPDATE",
25 | name: "trigger_foo"
26 |
27 | t.remove_trigger nil, name: "trigger_foo"
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/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 | # Show full error reports and disable caching
10 | config.consider_all_requests_local = true
11 | #config.action_controller.perform_caching = false
12 |
13 | # Don't care if the mailer can't send
14 | #config.action_mailer.raise_delivery_errors = false
15 |
16 | # Print deprecation notices to the Rails logger
17 | config.active_support.deprecation = :log
18 |
19 | # Only use best-standards-support built into browsers
20 | #config.action_dispatch.best_standards_support = :builtin
21 |
22 | # Do not compress assets
23 | #config.assets.compress = false
24 |
25 | # Expands the lines which load the assets
26 | #config.assets.debug = true
27 |
28 | config.eager_load = true
29 | end
30 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/migration/command_recorder/schema_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PgSaurus::Migration::CommandRecorder::SchemaMethods do
4 | class CommandRecorderStub
5 | include ::PgSaurus::Migration::CommandRecorder::SchemaMethods
6 | end
7 |
8 | let(:command_recorder_stub) { CommandRecorderStub.new }
9 |
10 | [:create_schema, :drop_schema, :move_table_to_schema].each do |method_name|
11 | it ".#{method_name}" do
12 | expect(command_recorder_stub).to receive(:record).with(method_name, [:foo, :bar])
13 | command_recorder_stub.send(method_name, :foo, :bar)
14 | end
15 | end
16 |
17 | it ".invert_create_schema" do
18 | expect(command_recorder_stub.invert_create_schema([:foo, :bar])).
19 | to eq([:drop_schema, [:foo]])
20 | end
21 |
22 | it ".invert_drop_schema" do
23 | expect(command_recorder_stub.invert_drop_schema([:foo, :bar])).
24 | to eq([:create_schema, [:foo]])
25 | end
26 |
27 | it ".invert_move_table_to_schema" do
28 | expect(command_recorder_stub.invert_move_table_to_schema(["sometable", "someschema"])).
29 | to eq([:move_table_to_schema, ["someschema.sometable", "public"]])
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/abstract_adapter/function_methods.rb:
--------------------------------------------------------------------------------
1 | # Adapter definitions for DB functions.
2 | module PgSaurus::ConnectionAdapters::AbstractAdapter::FunctionMethods
3 |
4 | # :nodoc
5 | def supports_functions?
6 | false
7 | end
8 |
9 | # Create a database function.
10 | #
11 | # Example:
12 | #
13 | # # Arguments are: function_name, return_type, function_definition, options (currently, only :schema)
14 | # create_function 'pets_not_empty()', :boolean, <<-FUNCTION, schema: 'public'
15 | # BEGIN
16 | # IF (SELECT COUNT(*) FROM pets) > 0
17 | # THEN
18 | # RETURN true;
19 | # ELSE
20 | # RETURN false;
21 | # END IF;
22 | # END;
23 | # FUNCTION
24 | #
25 | # The schema is optional.
26 | def create_function(function_name, returning, definition, options = {})
27 |
28 | end
29 |
30 | # Delete the database function.
31 | #
32 | # Example:
33 | #
34 | # drop_function 'pets_not_empty()', schema: 'public'
35 | #
36 | # The schema is optional.
37 | def drop_function(function_name, options)
38 |
39 | end
40 |
41 | # Return the listing of currently defined DB functions.
42 | def functions
43 |
44 | end
45 |
46 | end
47 |
--------------------------------------------------------------------------------
/lib/pg_saurus/schema_dumper/schema_methods.rb:
--------------------------------------------------------------------------------
1 | # Extends ActiveRecord::SchemaDumper class to dump schemas other than "public"
2 | # and tables from those schemas.
3 | module PgSaurus::SchemaDumper::SchemaMethods
4 |
5 | # Overrides https://github.com/rails/rails/blob/v7.2.2.2/activerecord/lib/active_record/schema_dumper.rb#L95
6 | def header(stream)
7 | super
8 | dump_schemas(stream)
9 | end
10 |
11 | # Overrides https://github.com/rails/rails/blob/v7.2.2.2/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb#L31
12 | #
13 | # We are already dumping the schemas through #header
14 | def schemas(...)
15 | end
16 |
17 | # Generates code to create schemas.
18 | def dump_schemas(stream)
19 | # Don't create "public" schema since it exists by default.
20 | schema_names = PgSaurus::Tools.schemas - ["public", "information_schema"]
21 | schema_names.each do |schema_name|
22 | dump_schema(schema_name, stream)
23 | end
24 | stream.puts
25 | end
26 | private :dump_schemas
27 |
28 | # Generates code to create schema.
29 | def dump_schema(schema_name, stream)
30 | stream.puts %( create_schema_if_not_exists "#{schema_name}")
31 | end
32 | private :dump_schema
33 | end
34 |
--------------------------------------------------------------------------------
/lib/pg_saurus/migration/command_recorder/schema_methods.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend ActiveRecord::Migration::CommandRecorder to
2 | # support multi schemas feature.
3 | module PgSaurus::Migration::CommandRecorder::SchemaMethods
4 |
5 | [
6 | :create_schema,
7 | :drop_schema,
8 | :move_table_to_schema,
9 | :create_schema_if_not_exists,
10 | :drop_schema_if_exists
11 | ].each do |method_name|
12 | define_method(method_name) do |*args|
13 | record method_name, args
14 | end
15 | end
16 |
17 | # :nodoc:
18 | def invert_create_schema(args)
19 | [:drop_schema, [args.first]]
20 | end
21 |
22 | # :nodoc:
23 | def invert_drop_schema(args)
24 | [:create_schema, [args.first]]
25 | end
26 |
27 | # :nodoc:
28 | def invert_move_table_to_schema(args)
29 | table_name = args.first
30 | current_schema = args.second
31 |
32 | new_schema, table = ::PgSaurus::Tools.to_schema_and_table(table_name)
33 |
34 | invert_args = ["#{current_schema}.#{table}", new_schema]
35 | [:move_table_to_schema, invert_args]
36 | end
37 |
38 | # :nodoc:
39 | def invert_create_schema_if_not_exists(*args)
40 | [:drop_schema_if_exists, args]
41 | end
42 |
43 | # :nodoc:
44 | def invert_drop_schema_if_exists(*args)
45 | [:create_schema_if_not_exists, args]
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 HornsAndHooves
2 | Initial foreign key code taken from foreigner, Copyright (c) 2009 Matthew Higgins
3 | pg_comment Copyright (c) 2011 Arthur Shagall
4 | Partial index Copyright (c) 2012 Marcelo Silveira
5 | PgPower Copyright (c) 2012 TMX Credit
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining
8 | a copy of this software and associated documentation files (the
9 | "Software"), to deal in the Software without restriction, including
10 | without limitation the rights to use, copy, modify, merge, publish,
11 | distribute, sublicense, and/or sell copies of the Software, and to
12 | permit persons to whom the Software is furnished to do so, subject to
13 | the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be
16 | included in all copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 |
--------------------------------------------------------------------------------
/.simplecov:
--------------------------------------------------------------------------------
1 | require "simplecov-rcov-text"
2 | require "colorized_text"
3 | include ColorizedText
4 |
5 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
6 | SimpleCov::Formatter::RcovTextFormatter,
7 | SimpleCov::Formatter::HTMLFormatter
8 | ])
9 | SimpleCov.start do
10 | add_filter "/spec/"
11 |
12 | # Fail the build when coverage is weak:
13 | at_exit do
14 | SimpleCov.result.format!
15 | threshold, actual = 98.481, SimpleCov.result.covered_percent
16 | if actual < threshold
17 | msg = "\nLow coverage: "
18 | msg << red("#{actual}%")
19 | msg << " is #{red 'under'} the threshold: "
20 | msg << green("#{threshold}%.")
21 | msg << "\n"
22 | $stderr.puts msg
23 | exit 1
24 | else
25 | # Precision: three decimal places:
26 | actual_trunc = (actual * 1000).floor / 1000.0
27 | msg = "\nCoverage: "
28 | msg << green("#{actual}%")
29 | msg << " is #{green 'over'} the threshold: "
30 | if actual_trunc > threshold
31 | msg << bold(yellow("#{threshold}%. "))
32 | msg << "Please update the threshold to: "
33 | msg << bold(green("#{actual_trunc}% "))
34 | msg << "in ./.simplecov."
35 | else
36 | msg << green("#{threshold}%.")
37 | end
38 | msg << "\n"
39 | $stdout.puts msg
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/core_ext/active_record/schema_dumper.rb:
--------------------------------------------------------------------------------
1 | module ActiveRecord #:nodoc:
2 | # Patched version: 3.1.3
3 | # Patched methods::
4 | # * indexes
5 | class SchemaDumper #:nodoc:
6 | # Writes out index-related details to the schema stream
7 | #
8 | # == Patch:
9 | # Add support of skip_column_quoting option for json indexes.
10 | #
11 | def index_parts(index)
12 | is_json_index = index.columns.is_a?(String) && index.columns =~ /^(.+->.+)$/
13 |
14 | index_parts = [
15 | index.columns.inspect,
16 | "name: #{index.name.inspect}",
17 | ]
18 | index_parts << "unique: true" if index.unique
19 | index_parts << "length: #{format_index_parts(index.lengths)}" if index.lengths.present?
20 | index_parts << "order: #{format_index_parts(index.orders)}" if index.orders.present?
21 | index_parts << "opclass: #{format_index_parts(index.opclasses)}" if index.opclasses.present?
22 | index_parts << "where: #{index.where.inspect}" if index.where
23 | index_parts << "using: #{index.using.inspect}" if !@connection.default_index_type?(index)
24 | index_parts << "skip_column_quoting: true" if is_json_index
25 | index_parts << "type: #{index.type.inspect}" if index.type
26 | index_parts << "comment: #{index.comment.inspect}" if index.comment
27 | index_parts
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # To test against different rails versions with TravisCI
4 | rails_version = ENV["RAILS_VERSION"] || "< 8"
5 |
6 | # NOTE: This is a Gemfile for a gem.
7 | # Using "platforms" is contraindicated because they won't make it into
8 | # the gemspec correctly.
9 | version2x = (RUBY_VERSION =~ /^2\.\d/)
10 |
11 | # 2017-01-12: Note: The GitHub pg mirror lacks the recent tags appearing in the Bitbucket Hg repo:
12 | # https://github.com/ged/ruby-pg/blob/master/History.rdoc
13 | # https://bitbucket.org/ged/ruby-pg/wiki/Home
14 |
15 | gem "pg"
16 | gem "psych"
17 |
18 | gem "railties", rails_version
19 | gem "activemodel", rails_version
20 | gem "activerecord", rails_version
21 | gem "activesupport", rails_version
22 |
23 | group :development do
24 | gem "rspec-rails"
25 |
26 | # code metrics:
27 | gem "yard"
28 | gem "metric_fu", require: false
29 | gem "jeweler" , require: false
30 |
31 |
32 | unless ENV["RM_INFO"]
33 | # debugger does not support Ruby 2.x:
34 | # ref: https://github.com/cldwalker/debugger/issues/125#issuecomment-43353446
35 | gem "byebug" if version2x
36 | end
37 | end
38 |
39 | group :development, :test do
40 | gem "pry"
41 | gem "pry-byebug"
42 | gem "rubocop"
43 | end
44 |
45 | group :test do
46 | gem "simplecov" , require: false
47 | gem "simplecov-rcov-text", require: false
48 | end
49 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/postgresql_adapter/extension_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PgSaurus::ConnectionAdapters::PostgreSQLAdapter::ExtensionMethods do
4 | class FakePostgreSQLAdapter
5 | include ::PgSaurus::ConnectionAdapters::PostgreSQLAdapter::ExtensionMethods
6 | end
7 |
8 | let(:adapter_stub) { FakePostgreSQLAdapter.new }
9 |
10 | it ".supports_extensions?" do
11 | expect(adapter_stub.supports_extensions?).to be true
12 | end
13 |
14 | it ".create_extension" do
15 | expect(adapter_stub).to receive(:execute).with(/CREATE EXTENSION(.+)\"someextension\"(.?)/)
16 |
17 | adapter_stub.create_extension("someextension", {})
18 | end
19 |
20 | it ".enable_extension" do
21 | expect(adapter_stub).to receive(:execute).with(/CREATE EXTENSION(.+)\"someextension\"(.?)/)
22 | allow_any_instance_of(FakePostgreSQLAdapter).to receive(:reload_type_map)
23 |
24 | adapter_stub.enable_extension("someextension", {})
25 | end
26 |
27 | describe ".drop_extension" do
28 | it "raises ArgumentError on invalid mode" do
29 | expect(adapter_stub).to receive(:execute).with(/DROP EXTENSION(.+)\"someextension\"(.?)/)
30 |
31 | adapter_stub.drop_extension("someextension", {})
32 | end
33 |
34 | it ".drop_extension" do
35 | expect {
36 | adapter_stub.drop_extension("someextension", {mode: :invalidmode})
37 | }.to raise_error(ArgumentError, /Expected one of/)
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/abstract_adapter/schema_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe PgSaurus::ConnectionAdapters::AbstractAdapter::SchemaMethods do
4 | let(:connection) { ActiveRecord::Base.connection }
5 |
6 | describe "#create_table_with_schema_option" do
7 | it "creates table with schema option" do
8 | connection.create_table("something", schema: "demography")
9 |
10 | expect(connection.table_exists?("demography.something")).to be true
11 |
12 | connection.drop_table("something", schema: "demography")
13 | end
14 |
15 | it "allows options to be a frozen Hash" do
16 | options = { schema: "demography" }.freeze
17 | expect { connection.create_table("something", options) }.not_to raise_error
18 | end
19 | end
20 |
21 | describe "#drop_table_with_schema_option" do
22 | it "drops table with schema option" do
23 | connection.create_table("something", schema: "demography")
24 | expect(connection.table_exists?("demography.something")).to be true
25 |
26 | connection.drop_table("something", schema: "demography")
27 |
28 | expect(connection.table_exists?("demography.something")).to be false
29 | end
30 |
31 | it "allows options to be a frozen Hash" do
32 | options = { schema: "demography" }.freeze
33 | connection.create_table("something", options)
34 |
35 | expect { connection.drop_table("something", options) }.not_to raise_error
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/abstract_adapter/trigger_methods.rb:
--------------------------------------------------------------------------------
1 | # Adapter definitions for db functions
2 | module PgSaurus::ConnectionAdapters::AbstractAdapter::TriggerMethods
3 |
4 | # :nodoc
5 | def supports_triggers?
6 | false
7 | end
8 |
9 | # Returns the listing of currently defined db triggers
10 | def triggers
11 |
12 | end
13 |
14 | # Creates a trigger.
15 | #
16 | # Example:
17 | #
18 | # create_trigger :pets, # Table or view name
19 | # :pets_not_empty_trigger_proc, # Procedure name. Parentheses are optional if you have no arguments.
20 | # 'AFTER INSERT', # Trigger event
21 | # for_each: 'ROW', # Can be row or statement. Default is row.
22 | # schema: 'public', # Optional schema name
23 | # constraint: true, # Sets if the trigger is a constraint. Default is false.
24 | # deferrable: true, # Sets if the trigger is immediate or deferrable. Default is immediate.
25 | # initially_deferred: true, # Sets if the trigger is initially deferred. Default is immediate. Only relevant if the trigger is deferrable.
26 | # condition: "new.name = 'fluffy'" # Optional when condition. Default is none.
27 | #
28 | def create_trigger(table_name, proc_name, event, options = {})
29 |
30 | end
31 |
32 | # Removes a trigger.
33 | #
34 | # Example:
35 | #
36 | # remove_trigger :pets, :pets_not_empty_trigger_proc
37 | #
38 | def remove_trigger(table_name, proc_name, options = {})
39 |
40 | end
41 |
42 | end
43 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/table/comment_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PgSaurus::ConnectionAdapters::Table::CommentMethods do
4 | class AbstractTable
5 | include ::PgSaurus::ConnectionAdapters::Table::CommentMethods
6 |
7 | def initialize
8 | @base = Object.new
9 | @name = "sometable"
10 | end
11 |
12 | end
13 |
14 | let(:table_stub) { AbstractTable.new }
15 | let(:base) { table_stub.instance_variable_get(:@base) }
16 |
17 | it ".set_table_comment" do
18 | expect(base).to receive(:set_table_comment).with("sometable", "somecomment")
19 | table_stub.set_table_comment("somecomment")
20 | end
21 |
22 | it ".remove_table_comment" do
23 | expect(base).to receive(:remove_table_comment).with("sometable")
24 | table_stub.remove_table_comment
25 | end
26 |
27 | it ".set_column_comment" do
28 | expect(base).to receive(:set_column_comment).with("sometable", "somecolumn", "somecomment")
29 | table_stub.set_column_comment("somecolumn", "somecomment")
30 | end
31 |
32 | it ".set_column_comments" do
33 | expect(base).
34 | to receive(:set_column_comments).
35 | with("sometable", {"column1" => "comment1", "column2" => "comment2"})
36 | table_stub.set_column_comments("column1" => "comment1", "column2" => "comment2")
37 | end
38 |
39 | it ".remove_column_comment" do
40 | expect(base).to receive(:remove_column_comment).with("sometable", "somecolumn")
41 | table_stub.remove_column_comment("somecolumn")
42 | end
43 |
44 | it ".remove_column_comments" do
45 | expect(base).to receive(:remove_column_comments).with("sometable", "column1", "column2")
46 | table_stub.remove_column_comments("column1", "column2")
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/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 | # Configure static asset server for tests with Cache-Control for performance
11 | config.serve_static_files = true
12 | config.static_cache_control = "public, max-age=3600"
13 |
14 | # Show full error reports and disable caching
15 | config.consider_all_requests_local = true
16 | #config.action_controller.perform_caching = false
17 |
18 | # Raise exceptions instead of rendering exception templates
19 | #config.action_dispatch.show_exceptions = false
20 |
21 | # Disable request forgery protection in test environment
22 | #config.action_controller.allow_forgery_protection = false
23 |
24 | # Tell Action Mailer not to deliver emails to the real world.
25 | # The :test delivery method accumulates sent emails in the
26 | # ActionMailer::Base.deliveries array.
27 | #config.action_mailer.delivery_method = :test
28 |
29 | # Use SQL instead of Active Record's schema dumper when creating the test database.
30 | # This is necessary if your schema can't be completely dumped by the schema dumper,
31 | # like if you have constraints or database-specific column types
32 | # config.active_record.schema_format = :sql
33 |
34 | # Print deprecation notices to the stderr
35 | #config.active_support.deprecation = :stderr
36 |
37 | config.eager_load = true
38 | end
39 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/postgresql_adapter.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend {ActiveRecord::ConnectionAdapters::PostgreSQLAdapter}
2 | # to support pg_saurus features.
3 | module PgSaurus::ConnectionAdapters::PostgreSQLAdapter
4 | extend ActiveSupport::Autoload
5 | extend ActiveSupport::Concern
6 |
7 | # TODO: Looks like explicit path specification can be omitted -- aignatyev 20120904
8 | autoload :ExtensionMethods, 'pg_saurus/connection_adapters/postgresql_adapter/extension_methods'
9 | autoload :SchemaMethods, 'pg_saurus/connection_adapters/postgresql_adapter/schema_methods'
10 | autoload :CommentMethods, 'pg_saurus/connection_adapters/postgresql_adapter/comment_methods'
11 | autoload :ForeignKeyMethods, 'pg_saurus/connection_adapters/postgresql_adapter/foreign_key_methods'
12 | autoload :IndexMethods, 'pg_saurus/connection_adapters/postgresql_adapter/index_methods'
13 | autoload :TranslateException, 'pg_saurus/connection_adapters/postgresql_adapter/translate_exception'
14 | autoload :ViewMethods, 'pg_saurus/connection_adapters/postgresql_adapter/view_methods'
15 | autoload :FunctionMethods, 'pg_saurus/connection_adapters/postgresql_adapter/function_methods'
16 | autoload :TriggerMethods, 'pg_saurus/connection_adapters/postgresql_adapter/trigger_methods'
17 |
18 | include ExtensionMethods
19 | include SchemaMethods
20 | include CommentMethods
21 | include ForeignKeyMethods
22 | include IndexMethods
23 | include TranslateException
24 | include ViewMethods
25 | include FunctionMethods
26 | include TriggerMethods
27 |
28 | included do
29 | ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.module_eval do
30 | def from_schema
31 | options[:from_schema] || 'public'
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/pg_saurus/migration/command_recorder/comment_methods.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend ActiveRecord::Migration::CommandRecorder to
2 | # support comments feature.
3 | module PgSaurus::Migration::CommandRecorder::CommentMethods
4 | # :nodoc:
5 | def set_table_comment(*args)
6 | record(:set_table_comment, args)
7 | end
8 |
9 | # :nodoc:
10 | def remove_table_comment(*args)
11 | record(:remove_table_comment, args)
12 | end
13 |
14 | # :nodoc:
15 | def set_column_comment(*args)
16 | record(:set_column_comment, args)
17 | end
18 |
19 | # :nodoc:
20 | def set_column_comments(*args)
21 | record(:set_column_comments, args)
22 | end
23 |
24 | # :nodoc:
25 | def remove_column_comment(*args)
26 | record(:remove_column_comment, args)
27 | end
28 |
29 | # :nodoc:
30 | def remove_column_comments(*args)
31 | record(:remove_column_comments, args)
32 | end
33 |
34 | # :nodoc:
35 | def set_index_comment(*args)
36 | record(:set_index_comment, args)
37 | end
38 |
39 | # :nodoc:
40 | def remove_index_comment(*args)
41 | record(:remove_index_comment, args)
42 | end
43 |
44 | # :nodoc:
45 | def invert_set_table_comment(args)
46 | table_name = args.first
47 | [:remove_table_comment, [table_name]]
48 | end
49 |
50 | # :nodoc:
51 | def invert_set_column_comment(args)
52 | table_name = args[0]
53 | column_name = args[1]
54 | [:remove_column_comment, [table_name, column_name]]
55 | end
56 |
57 | # :nodoc:
58 | def invert_set_column_comments(args)
59 | i_args = [args[0]] + args[1].collect{|name, value| name }
60 | [:remove_column_comments, i_args]
61 | end
62 |
63 | # :nodoc:
64 | def invert_set_index_comment(args)
65 | index_name = args.first
66 | [:remove_index_comment, [index_name]]
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/pg_saurus/schema_dumper/comment_methods.rb:
--------------------------------------------------------------------------------
1 | # Extends ActiveRecord::SchemaDumper class to dump comments on tables and columns.
2 | module PgSaurus::SchemaDumper::CommentMethods
3 | # Hook ActiveRecord::SchemaDumper#table method to dump comments on
4 | # table and columns.
5 | def tables(stream)
6 | super(stream)
7 |
8 | # Dump table and column comments
9 | @connection.tables.sort.each do |table_name|
10 | dump_comments(table_name, stream)
11 | end
12 |
13 | # Now dump index comments
14 | unless (index_comments = @connection.index_comments).empty?
15 | index_comments.each do |schema_name, table_name, raw_comment|
16 | index_name = schema_name == 'public' ? "'#{table_name}'" : "'#{schema_name}.#{table_name}'"
17 | comment = format_comment(raw_comment)
18 | stream.puts " set_index_comment #{index_name}, '#{comment}'"
19 | end
20 | stream.puts
21 | end
22 | end
23 |
24 | # Find all comments related to passed table and write appropriate
25 | # statements to stream.
26 | def dump_comments(table_name, stream)
27 | unless (comments = @connection.comments(table_name)).empty?
28 | comment_statements = comments.map do |row|
29 | column_name = row[0]
30 | comment = format_comment(row[1])
31 |
32 | if column_name
33 | " set_column_comment '#{table_name}', '#{column_name}', '#{comment}'"
34 | else
35 | " set_table_comment '#{table_name}', '#{comment}'"
36 | end
37 |
38 | end
39 |
40 | stream.puts comment_statements.join("\n")
41 | stream.puts
42 | end
43 | end
44 | private :dump_comments
45 |
46 | # Escape single quotes from comments.
47 | def format_comment(comment)
48 | comment.gsub(/'/, "\\\\'")
49 | end
50 | private :format_comment
51 | end
52 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/postgresql_adapter/trigger_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe PgSaurus::ConnectionAdapters::PostgreSQLAdapter::TriggerMethods do
4 |
5 | let(:connection) { ActiveRecord::Base.connection }
6 |
7 | it ".supports_triggers?" do
8 | expect(connection.supports_triggers?).to be true
9 | end
10 |
11 | context ".create_trigger" do
12 |
13 | it "executes a query to create a trigger" do
14 | sql = <<-SQL.gsub(/^[ ]{8}/, "")
15 | CREATE CONSTRAINT TRIGGER trigger_pets_not_empty_trigger_proc
16 | AFTER INSERT
17 | ON "public"."pets"
18 | DEFERRABLE INITIALLY DEFERRED
19 | FOR EACH ROW
20 | WHEN (name = 'Fluffy')
21 | EXECUTE PROCEDURE pets_not_empty_trigger_proc()
22 | SQL
23 |
24 | expect(connection).to receive(:execute).with(sql.strip)
25 |
26 | connection.create_trigger :pets,
27 | :pets_not_empty_trigger_proc,
28 | "AFTER INSERT",
29 | for_each: "ROW",
30 | schema: "public",
31 | constraint: true,
32 | deferrable: true,
33 | initially_deferred: true,
34 | condition: "name = 'Fluffy'"
35 | end
36 |
37 | end
38 |
39 | context ".remove_trigger" do
40 |
41 | it "derives the trigger name" do
42 | expect(connection).to receive(:execute).with('DROP TRIGGER trigger_foo_bar ON "pets"')
43 |
44 | connection.remove_trigger :pets, "foo_bar()"
45 | end
46 |
47 | it "accepts an explicitly named trigger" do
48 | expect(connection).to receive(:execute).with('DROP TRIGGER trigger_foo_bar ON "pets"')
49 |
50 | connection.remove_trigger :pets, "foo_bar_baz", name: "trigger_foo_bar"
51 | end
52 |
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/spec/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.routes.draw do
2 | # The priority is based upon order of creation:
3 | # first created -> highest priority.
4 |
5 | # Sample of regular route:
6 | # match 'products/:id' => 'catalog#view'
7 | # Keep in mind you can assign values other than :controller and :action
8 |
9 | # Sample of named route:
10 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase
11 | # This route can be invoked with purchase_url(:id => product.id)
12 |
13 | # Sample resource route (maps HTTP verbs to controller actions automatically):
14 | # resources :products
15 |
16 | # Sample resource route with options:
17 | # resources :products do
18 | # member do
19 | # get 'short'
20 | # post 'toggle'
21 | # end
22 | #
23 | # collection do
24 | # get 'sold'
25 | # end
26 | # end
27 |
28 | # Sample resource route with sub-resources:
29 | # resources :products do
30 | # resources :comments, :sales
31 | # resource :seller
32 | # end
33 |
34 | # Sample resource route with more complex sub-resources
35 | # resources :products do
36 | # resources :comments
37 | # resources :sales do
38 | # get 'recent', :on => :collection
39 | # end
40 | # end
41 |
42 | # Sample resource route within a namespace:
43 | # namespace :admin do
44 | # # Directs /admin/products/* to Admin::ProductsController
45 | # # (app/controllers/admin/products_controller.rb)
46 | # resources :products
47 | # end
48 |
49 | # You can have the root of your site routed with "root"
50 | # just remember to delete public/index.html.
51 | # root :to => 'welcome#index'
52 |
53 | # See how all your routes lay out with "rake routes"
54 |
55 | # This is a legacy wild controller route that's not recommended for RESTful applications.
56 | # Note: This route will make all actions in every controller accessible via GET requests.
57 | # match ':controller(/:action(/:id(.:format)))'
58 | end
59 |
--------------------------------------------------------------------------------
/lib/pg_saurus/schema_dumper/foreign_key_methods.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend ActiveRecord::SchemaDumper to dump
2 | # foreign keys.
3 | module PgSaurus::SchemaDumper::ForeignKeyMethods
4 |
5 | # See activerecord/lib/active_record/schema_dumper.rb
6 | def foreign_keys(table, stream)
7 | if (foreign_keys = @connection.foreign_keys(table)).any?
8 | add_foreign_key_statements = foreign_keys.map do |foreign_key|
9 |
10 | from_table = if foreign_key.from_schema && foreign_key.from_schema != 'public'
11 | "#{foreign_key.from_schema}.#{remove_prefix_and_suffix(foreign_key.from_table)}"
12 | else
13 | remove_prefix_and_suffix(foreign_key.from_table)
14 | end
15 |
16 | parts = [
17 | "add_foreign_key #{from_table.inspect}",
18 | remove_prefix_and_suffix(foreign_key.to_table).inspect,
19 | ]
20 |
21 | if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table)
22 | parts << "column: #{foreign_key.column.inspect}"
23 | end
24 |
25 | if foreign_key.custom_primary_key?
26 | parts << "primary_key: #{foreign_key.primary_key.inspect}"
27 | end
28 |
29 | if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/
30 | parts << "name: #{foreign_key.name.inspect}"
31 | end
32 |
33 | parts << "on_update: #{foreign_key.on_update.inspect}" if foreign_key.on_update
34 | parts << "on_delete: #{foreign_key.on_delete.inspect}" if foreign_key.on_delete
35 |
36 | # Always exclude the index
37 | # If an index was created in a migration, it will get dumped to the schema
38 | # separately from the foreign key. This will raise an exception if
39 | # add_foreign_key is run without :exclude_index => true.
40 | parts << "exclude_index: true"
41 |
42 | " #{parts.join(', ')}"
43 | end
44 |
45 | stream.puts add_foreign_key_statements.sort.join("\n")
46 | end
47 | end
48 |
49 | end
50 |
--------------------------------------------------------------------------------
/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 "sprockets/railtie"
8 | # require "rails/test_unit/railtie"
9 |
10 | Bundler.require
11 | require "pg_saurus"
12 |
13 | module Dummy
14 | class Application < Rails::Application
15 | # Settings in config/environments/* take precedence over those specified here.
16 | # Application configuration should go into files in config/initializers
17 | # -- all .rb files in that directory are automatically loaded.
18 |
19 | # Custom directories with classes and modules you want to be autoloadable.
20 | # config.autoload_paths += %W(#{config.root}/extras)
21 |
22 | # Only load the plugins named here, in the order given (default is alphabetical).
23 | # :all can be used as a placeholder for all plugins not explicitly named.
24 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
25 |
26 | # Activate observers that should always be running.
27 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
28 |
29 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
30 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
31 | # config.time_zone = 'Central Time (US & Canada)'
32 |
33 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
34 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
35 | # config.i18n.default_locale = :de
36 |
37 | # Configure the default encoding used in templates for Ruby 1.9.
38 | config.encoding = "utf-8"
39 |
40 | # Configure sensitive parameters which will be filtered from the log file.
41 | config.filter_parameters += [:password]
42 |
43 | # Enable the asset pipeline
44 | #config.assets.enabled = true
45 |
46 | # Version of your assets, change this if you want to expire all your assets
47 | #config.assets.version = '1.0'
48 | end
49 | end
50 |
51 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | begin
3 | require 'bundler/setup'
4 | rescue LoadError
5 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6 | end
7 | begin
8 | require 'rdoc/task'
9 | rescue LoadError
10 | require 'rdoc/rdoc'
11 | require 'rake/rdoctask'
12 | RDoc::Task = Rake::RDocTask
13 | end
14 |
15 | require './lib/pg_saurus/version'
16 |
17 | begin
18 | require "jeweler"
19 |
20 | Jeweler::Tasks.new do |gem|
21 | gem.name = "pg_saurus"
22 | gem.summary = "ActiveRecord extensions for PostgreSQL."
23 | gem.description =
24 | "ActiveRecord extensions for PostgreSQL. Provides useful tools for schema, foreign_key, " \
25 | "index, function, trigger, comment and extension manipulations in migrations."
26 | gem.email = ["blake131313@gmail.com", "arthur.shagall@gmail.com", "cryo28@gmail.com",
27 | "matt.dressel@gmail.com", "rubygems.org@bruceburdick.com"]
28 | gem.authors = ["Potapov Sergey", "Arthur Shagall", "Artem Ignatyev",
29 | "Matt Dressel", "Bruce Burdick", "HornsAndHooves"]
30 | gem.files = Dir["{app,config,db,lib}/**/*"] + Dir['Rakefie', 'README.markdown']
31 | gem.executables = []
32 | # Need to explicitly specify version here so gemspec:validate task doesn't whine.
33 | gem.version = PgSaurus::VERSION
34 | gem.homepage = "https://github.com/HornsAndHooves/pg_saurus"
35 | gem.license = 'MIT'
36 | end
37 | rescue
38 | puts "Jeweler or one of its dependencies is not installed."
39 | end
40 |
41 | RDoc::Task.new(:rdoc) do |rdoc|
42 | rdoc.rdoc_dir = 'rdoc'
43 | rdoc.title = 'PgSaurus'
44 | rdoc.options << '--line-numbers'
45 | rdoc.rdoc_files.include('README.rdoc')
46 | rdoc.rdoc_files.include('lib/**/*.rb')
47 | end
48 |
49 | APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
50 | load 'rails/tasks/engine.rake'
51 |
52 | require 'rspec/core'
53 | require 'rspec/core/rake_task'
54 | RSpec::Core::RakeTask.new(:spec) do |spec|
55 | spec.pattern = 'spec/**/*_spec.rb'
56 | end
57 |
58 | task default: :spec
59 |
60 | task 'spec' => ['db:drop', 'db:create', 'db:migrate', 'app:db:test:load_schema']
61 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/table/comment_methods.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend ActiveRecord::ConnectionAdapters::Table
2 | # to support comments feature.
3 | module PgSaurus::ConnectionAdapters::Table::CommentMethods
4 | # Set the comment on the table.
5 | #
6 | # ===== Example
7 | # ====== Set comment on table
8 | # t.set_table_comment 'This table stores phone numbers that conform to the North American Numbering Plan.'
9 | def set_table_comment(comment)
10 | @base.set_table_comment(@name, comment)
11 | end
12 |
13 | # Remove any comment from the table.
14 | #
15 | # ===== Example
16 | # ====== Remove table comment
17 | # t.remove_table_comment
18 | def remove_table_comment
19 | @base.remove_table_comment(@name)
20 | end
21 |
22 | # Set the comment for a given column.
23 | #
24 | # ===== Example
25 | # ====== Set comment on the npa column
26 | # t.set_column_comment :npa, 'Numbering Plan Area Code - Allowed ranges: [2-9] for first digit, [0-9] for second and third digit.'
27 | def set_column_comment(column_name, comment)
28 | @base.set_column_comment(@name, column_name, comment)
29 | end
30 |
31 | # Set comments on multiple columns. 'comments' is a hash of column_name => comment pairs.
32 | #
33 | # ===== Example
34 | # ====== Setting comments on the columns of the phone_numbers table
35 | # t.set_column_comments :npa => 'Numbering Plan Area Code - Allowed ranges: [2-9] for first digit, [0-9] for second and third digit.',
36 | # :nxx => 'Central Office Number'
37 | def set_column_comments(comments)
38 | @base.set_column_comments(@name, comments)
39 | end
40 |
41 | # Remove any comment for a given column.
42 | #
43 | # ===== Example
44 | # ====== Remove comment from the npa column
45 | # t.remove_column_comment :npa
46 | def remove_column_comment(column_name)
47 | @base.remove_column_comment(@name, column_name)
48 | end
49 |
50 | # Remove any comments from the given columns.
51 | #
52 | # ===== Example
53 | # ====== Remove comment from the npa and nxx columns
54 | # t.remove_column_comment :npa, :nxx
55 | def remove_column_comments(*column_names)
56 | @base.remove_column_comments(@name, *column_names)
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/tools_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PgSaurus::Tools do
4 | describe '#move_table_to_schema' do
5 | it 'moves table to another schema' do
6 | Pet.create!(name: "Flaaffy", color: "#FFAABB")
7 | PgSaurus::Explorer.table_exists?('public.pets').should == true
8 |
9 | # Move table
10 | PgSaurus::Tools.move_table_to_schema :pets, :demography
11 | PgSaurus::Explorer.table_exists?('public.pets').should == false
12 | PgSaurus::Explorer.table_exists?('demography.pets').should == true
13 |
14 | # Move table back
15 | PgSaurus::Tools.move_table_to_schema 'demography.pets', :public
16 | PgSaurus::Explorer.table_exists?('public.pets').should == true
17 | PgSaurus::Explorer.table_exists?('demography.pets').should == false
18 |
19 | # Make sure data is not lost
20 | Pet.where(name: "Flaaffy", color: "#FFAABB").size.should == 1
21 | end
22 | end
23 |
24 | let(:connection) { PgSaurus::Tools.send(:connection) }
25 |
26 | it ".create_schema_if_not_exists" do
27 | expect(connection).to receive(:execute).with('CREATE SCHEMA "someschema"')
28 | PgSaurus::Tools.create_schema_if_not_exists("someschema")
29 | end
30 |
31 | it ".drop_schema_if_exists" do
32 | expect(connection).to receive(:drop_schema).with("someschema", if_exists: true)
33 | PgSaurus::Tools.drop_schema_if_exists("someschema")
34 | end
35 |
36 | it ".create_view" do
37 | expect(connection).to receive(:execute).with("CREATE VIEW someview AS SELECT 1")
38 | PgSaurus::Tools.create_view("someview", "SELECT 1")
39 | end
40 |
41 | it ".drop_view" do
42 | expect(connection).to receive(:execute).with("DROP VIEW someview")
43 | PgSaurus::Tools.drop_view("someview")
44 | end
45 |
46 | it ".schemas" do
47 | expect(PgSaurus::Tools.schemas).to include("demography")
48 | end
49 |
50 | it ".views" do
51 | PgSaurus::Tools.create_view("someview", "SELECT 1")
52 |
53 | result = PgSaurus::Tools.views.to_a.find do |view|
54 | view['table_schema'] == "public" && view['table_name'] == "someview"
55 | end
56 |
57 | expect(result).not_to be_nil
58 | expect(result['view_definition']).to include(%(SELECT 1 AS "?column?";))
59 |
60 | PgSaurus::Tools.drop_view("someview")
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/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 | # Full error reports are disabled and caching is turned on
8 | config.consider_all_requests_local = false
9 | #config.action_controller.perform_caching = true
10 |
11 | # Disable Rails's static asset server (Apache or nginx will already do this)
12 | config.serve_static_files = false
13 |
14 | # Compress JavaScripts and CSS
15 | #config.assets.compress = true
16 |
17 | # Don't fallback to assets pipeline if a precompiled asset is missed
18 | #config.assets.compile = false
19 |
20 | # Generate digests for assets URLs
21 | #config.assets.digest = true
22 |
23 | # Defaults to Rails.root.join("public/assets")
24 | # config.assets.manifest = YOUR_PATH
25 |
26 | # Specifies the header that your server uses for sending files
27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
29 |
30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
31 | # config.force_ssl = true
32 |
33 | # See everything in the log (default is :info)
34 | # config.log_level = :debug
35 |
36 | # Use a different logger for distributed setups
37 | # config.logger = SyslogLogger.new
38 |
39 | # Use a different cache store in production
40 | # config.cache_store = :mem_cache_store
41 |
42 | # Enable serving of images, stylesheets, and JavaScripts from an asset server
43 | # config.action_controller.asset_host = "http://assets.example.com"
44 |
45 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
46 | # config.assets.precompile += %w( search.js )
47 |
48 | # Disable delivery errors, bad email addresses will be ignored
49 | # config.action_mailer.raise_delivery_errors = false
50 |
51 | # Enable threaded mode
52 | # config.threadsafe!
53 |
54 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
55 | # the I18n.default_locale when a translation can not be found)
56 | config.i18n.fallbacks = true
57 |
58 | # Send deprecation notices to registered listeners
59 | config.active_support.deprecation = :notify
60 |
61 | config.eager_load = true
62 | end
63 |
--------------------------------------------------------------------------------
/spec/foreign_keys_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Foreign keys' do
4 | describe '#add_foreign_key' do
5 | # AddForeignKeys migration
6 | # add_foreign_key 'pets', 'users'
7 | it 'adds foreign key' do
8 | PgSaurus::Explorer.has_foreign_key?('pets', :user_id).should == true
9 | end
10 |
11 | # AddForeignKeys migration
12 | # add_foreign_key 'demography.citizens', 'users', :exclude_index => true
13 | it 'should not add an index on the foreign key when :exclude_index is true' do
14 | PgSaurus::Explorer.index_exists?('demography.citizens', :user_id).should == false
15 | end
16 |
17 | it 'should raise a PgSaurus::IndexExistsError when the index already exists' do
18 | expect {
19 | connection = ActiveRecord::Base::connection
20 | connection.add_index 'demography.citizens', :user_id
21 | connection.add_foreign_key 'demography.citizens', 'users'
22 | }.to raise_exception(PgSaurus::IndexExistsError)
23 | end
24 | end
25 |
26 | describe '#remove_foreign_key' do
27 | # RemoveForeignKeys migration
28 | # remove_foreign_key 'demography.citizens', 'demography.countries'
29 | # remove_foreign_key 'pets', :name => "pets_owner_id_fk"
30 | it 'removes foreign key' do
31 | PgSaurus::Explorer.has_foreign_key?('demography.citizens', :country_id).should == false
32 | PgSaurus::Explorer.has_foreign_key?('pets', :owner_id).should == false
33 | end
34 |
35 | # RemoveForeignKeys migration
36 | # remove_foreign_key 'demography.citizens', 'demography.countries'
37 | # remove_foreign_key 'pets', :name => "pets_owner_id_fk"
38 | it 'removes the index on the foreign key' do
39 | PgSaurus::Explorer.index_exists?('demography.citizens', :country_id).should == false
40 | PgSaurus::Explorer.index_exists?('pets', :owner_id).should == false
41 | end
42 |
43 | # RemoveForeignKeys migration
44 | # remove_foreign_key 'pets', 'demography.countries', :exclude_index => true
45 | # remove_foreign_key 'pets', :name => "pets_breed_id_fk", :exclude_index => true
46 | it 'should remove foreign key but not remove the index when :exclude_index is true' do
47 | PgSaurus::Explorer.has_foreign_key?('pets', :country_id).should == false
48 | PgSaurus::Explorer.has_foreign_key?('pets', :breed_id).should == false
49 | PgSaurus::Explorer.index_exists?('pets', :country_id).should == true
50 | PgSaurus::Explorer.index_exists?('pets', :breed_id).should == true
51 | end
52 |
53 | it 'should not raise an exception if the index does not exist' do
54 | expect {
55 | connection = ActiveRecord::Base::connection
56 | connection.add_foreign_key 'pets', 'demography.citizens', exclude_index: true
57 | connection.remove_foreign_key 'pets', 'demography.citizens'
58 | }.not_to raise_error
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/migration/command_recorder_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PgSaurus::Migration::CommandRecorder do
4 | class CommandRecorderStub
5 | include ::PgSaurus::Migration::CommandRecorder
6 | end
7 |
8 | let(:command_recorder_stub) { CommandRecorderStub.new }
9 |
10 | describe 'Triggers' do
11 |
12 | [ :create_trigger, :remove_trigger ].each do |method_name|
13 | it ".#{method_name}" do
14 | expect(command_recorder_stub).to receive(:record).with(method_name, [])
15 | command_recorder_stub.send(method_name)
16 | end
17 | end
18 |
19 | it '.invert_create_trigger' do
20 | expect(
21 | command_recorder_stub.invert_create_trigger(
22 | ['pets', 'pets_not_empty', 'AFTER CREATE', {}]
23 | )
24 | ).to eq([:remove_trigger, ['pets', 'pets_not_empty', {}]])
25 | end
26 |
27 | end
28 |
29 | describe 'Functions' do
30 |
31 | [ :create_function, :drop_function ].each do |method_name|
32 | it ".#{method_name}" do
33 | expect(command_recorder_stub).to receive(:record).with(method_name, [])
34 | command_recorder_stub.send(method_name)
35 | end
36 | end
37 |
38 | it '.invert_create_functions' do
39 | expect(
40 | command_recorder_stub.invert_create_function(
41 | [ 'pets_not_empty()', :boolean, 'FU', { schema: 'public' } ]
42 | )
43 | ).to eq([ :drop_function, [ "pets_not_empty()", { schema: "public" } ] ])
44 | end
45 |
46 | end
47 |
48 | describe 'Comments' do
49 | [ :set_table_comment,
50 | :remove_table_comment,
51 | :set_column_comment,
52 | :set_column_comments,
53 | :remove_column_comment,
54 | :remove_column_comments,
55 | :set_index_comment,
56 | :remove_index_comment
57 | ].each{ |method_name|
58 |
59 | it ".#{method_name}" do
60 | expect(command_recorder_stub).to receive(:record).with(method_name, [])
61 | command_recorder_stub.send(method_name)
62 | end
63 | }
64 |
65 | it '.invert_set_table_comment' do
66 | command_recorder_stub.invert_set_table_comment([:foo, :bar]).
67 | should == [:remove_table_comment, [:foo]]
68 | end
69 |
70 | it '.invert_set_column_comment' do
71 | command_recorder_stub.invert_set_column_comment([:foo, :bar, :baz]).
72 | should == [:remove_column_comment, [:foo, :bar]]
73 | end
74 |
75 | it '.invert_set_column_comments' do
76 | command_recorder_stub.invert_set_column_comments([:foo, { bar: :baz }]).
77 | should == [:remove_column_comments, [:foo, :bar]]
78 | end
79 |
80 | it '.invert_set_index_comment' do
81 | command_recorder_stub.invert_set_index_comment([:foo, :bar]).
82 | should == [:remove_index_comment, [:foo]]
83 | end
84 | end
85 |
86 | end
87 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/abstract_adapter/comment_methods.rb:
--------------------------------------------------------------------------------
1 | # Extends ActiveRecord::ConnectionAdapters::AbstractAdapter with
2 | # empty methods for comments feature.
3 | module PgSaurus::ConnectionAdapters::AbstractAdapter::CommentMethods
4 | def supports_comments?
5 | false
6 | end
7 |
8 | # Sets a comment on the given table.
9 | #
10 | # ===== Example
11 | # ====== Creating a comment on phone_numbers table
12 | # set_table_comment :phone_numbers, 'This table stores phone numbers that conform to the North American Numbering Plan.'
13 | def set_table_comment(table_name, comment)
14 | # Does nothing
15 | end
16 |
17 | # Sets a comment on a given column of a given table.
18 | #
19 | # ===== Example
20 | # ====== Creating a comment on npa column of table phone_numbers
21 | # set_column_comment :phone_numbers, :npa, 'Numbering Plan Area Code - Allowed ranges: [2-9] for first digit, [0-9] for second and third digit.'
22 | def set_column_comment(table_name, column_name, comment)
23 | # Does nothing
24 | end
25 |
26 | # Sets comments on multiple columns. 'comments' is a hash of column_name => comment pairs.
27 | #
28 | # ===== Example
29 | # ====== Setting comments on the columns of the phone_numbers table
30 | # set_column_comments :phone_numbers, :npa => 'Numbering Plan Area Code - Allowed ranges: [2-9] for first digit, [0-9] for second and third digit.',
31 | # :nxx => 'Central Office Number'
32 | def set_column_comments(table_name, comments)
33 |
34 | end
35 |
36 | # Sets the comment on the given index
37 | #
38 | # ===== Example
39 | # ====== Setting comment on the index_pets_on_breed_id index
40 | # set_index_comment 'index_pets_on_breed_id', 'Index on breed_id'
41 | def set_index_comment(index_name, comment)
42 |
43 | end
44 |
45 | # Removes any comment from the given table.
46 | #
47 | # ===== Example
48 | # ====== Removing comment from phone numbers table
49 | # remove_table_comment :phone_numbers
50 | def remove_table_comment(table_name)
51 |
52 | end
53 |
54 | # Removes any comment from the given column of a given table.
55 | #
56 | # ===== Example
57 | # ====== Removing comment from the npa column of table phone_numbers
58 | # remove_column_comment :phone_numbers, :npa
59 | def remove_column_comment(table_name, column_name)
60 |
61 | end
62 |
63 | # Removes any comment from the given columns of a given table.
64 | #
65 | # ===== Example
66 | # ====== Removing comment from the npa and nxx columns of table phone_numbers
67 | # remove_column_comments :phone_numbers, :npa, :nxx
68 | def remove_column_comments(table_name, *column_names)
69 |
70 | end
71 |
72 | # Removes the comment from the given index
73 | #
74 | # ===== Example
75 | # ====== Removing comment from the index_pets_on_breed_id index
76 | # remove_index_comment :index_pets_on_breed_id
77 | def remove_index_comment(index_name)
78 |
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/postgresql_adapter/schema_methods.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend {ActiveRecord::ConnectionAdapters::PostgreSQLAdapter}
2 | # to support schemas feature.
3 | module PgSaurus::ConnectionAdapters::PostgreSQLAdapter::SchemaMethods
4 | # Move table to another schema
5 | # @param [String] table table name. Can be with schema prefix e.g. "demography.people"
6 | # @param [String] schema schema where table should be moved to.
7 | def move_table_to_schema(table, schema)
8 | ::PgSaurus::Tools.move_table_to_schema(table, schema)
9 | end
10 |
11 | # Create schema if it does not exist yet.
12 | #
13 | # @param schema_name [String]
14 | def create_schema_if_not_exists(schema_name)
15 | ::PgSaurus::Tools.create_schema_if_not_exists(schema_name)
16 | end
17 |
18 | # Drop schema if it exists.
19 | #
20 | # @param schema_name [String]
21 | def drop_schema_if_exists(schema_name)
22 | ::PgSaurus::Tools.drop_schema_if_exists(schema_name)
23 | end
24 |
25 | # Provide :schema option to +drop_table+ method.
26 | def drop_table(table_name, options = {})
27 | options = options.dup
28 | schema_name = options.delete(:schema)
29 | table_name = "#{schema_name}.#{table_name}" if schema_name
30 | super(table_name, **options)
31 | end
32 |
33 | # Make method +tables+ return tables not only from public schema.
34 | #
35 | # @note
36 | # Tables from public schema have no "public." prefix. It's done for
37 | # compatibility with other libraries that relies on a table name.
38 | # Tables from other schemas has appropriate prefix with schema name.
39 | # See: https://github.com/TMXCredit/pg_power/pull/42
40 | #
41 | # @return [Array] table names
42 | def tables(*args)
43 | public_tables = super(*args)
44 |
45 | non_public_tables =
46 | query(<<-SQL, 'SCHEMA').map { |row| row[0] }
47 | SELECT schemaname || '.' || tablename AS table
48 | FROM pg_tables
49 | WHERE schemaname NOT IN ('pg_catalog', 'information_schema', 'public')
50 | SQL
51 |
52 | public_tables + non_public_tables
53 | end
54 |
55 | # Provide :schema option to +rename_table+ method.
56 | def rename_table(table_name, new_name, options = {})
57 | schema_name = options[:schema]
58 | if schema_name
59 | in_schema schema_name do
60 | super(table_name, new_name)
61 | end
62 | else
63 | super(table_name, new_name)
64 | end
65 | end
66 |
67 | # Execute operations in the context of the schema
68 | def in_schema(schema_name)
69 | search_path = current_schema_search_path
70 | begin
71 | execute("SET search_path TO '%s'" % schema_name)
72 | yield
73 | ensure
74 | execute("SET search_path TO #{search_path};")
75 | end
76 | end
77 |
78 | # Reads the current schema search path (it may have been altered
79 | # from the initial value used when creating the connection)
80 | def current_schema_search_path
81 | select_value("SHOW search_path;")
82 | end
83 |
84 | end
85 |
--------------------------------------------------------------------------------
/lib/pg_saurus/engine.rb:
--------------------------------------------------------------------------------
1 | module PgSaurus
2 | # :nodoc:
3 | class Engine < Rails::Engine
4 |
5 | # Postgres server version.
6 | #
7 | # @return [Array]
8 | def self.pg_server_version
9 | @pg_server_version ||=
10 | ::ActiveRecord::Base.connection.
11 | select_value('SHOW SERVER_VERSION').
12 | split('.')[0..1].map(&:to_i)
13 | end
14 |
15 | initializer "pg_saurus" do
16 | ActiveSupport.on_load(:active_record) do
17 | # load monkey patches
18 | %w[
19 | schema_dumper
20 | errors
21 | connection_adapters/postgresql/schema_statements
22 | migration/compatibility
23 | ].each do |path|
24 | require ::PgSaurus::Engine.root + "lib/core_ext/active_record/" + path
25 | end
26 |
27 | ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaDumper.class_eval do
28 | prepend ::PgSaurus::SchemaDumper::SchemaMethods
29 | prepend ::PgSaurus::SchemaDumper::ExtensionMethods
30 | prepend ::PgSaurus::SchemaDumper::ViewMethods
31 | prepend ::PgSaurus::SchemaDumper::FunctionMethods
32 | prepend ::PgSaurus::SchemaDumper::CommentMethods
33 | prepend ::PgSaurus::SchemaDumper::TriggerMethods
34 | prepend ::PgSaurus::SchemaDumper::ForeignKeyMethods
35 |
36 | include ::PgSaurus::SchemaDumper
37 | end
38 |
39 | ActiveRecord::Migration.class_eval do
40 | prepend ::PgSaurus::Migration::SetRoleMethod::Extension
41 | include ::PgSaurus::Migration::SetRoleMethod
42 | end
43 |
44 | if defined?(ActiveRecord::Migration::CommandRecorder)
45 | ActiveRecord::Migration::CommandRecorder.class_eval do
46 | include ::PgSaurus::Migration::CommandRecorder
47 | end
48 | end
49 |
50 | # The following three include statements add support for concurrently
51 | # creating indexes in migrations.
52 | ActiveRecord::Migration.class_eval do
53 | include ::PgSaurus::CreateIndexConcurrently::Migration
54 | end
55 |
56 | ActiveRecord::Migrator.class_eval do
57 | prepend PgSaurus::CreateIndexConcurrently::Migrator
58 | end
59 |
60 | ActiveRecord::MigrationProxy.class_eval do
61 | include ::PgSaurus::CreateIndexConcurrently::MigrationProxy
62 | end
63 |
64 | ActiveRecord::ConnectionAdapters::Table.module_eval do
65 | include ::PgSaurus::ConnectionAdapters::Table
66 | end
67 |
68 | ActiveRecord::ConnectionAdapters::AbstractAdapter.module_eval do
69 | prepend ::PgSaurus::ConnectionAdapters::AbstractAdapter::SchemaMethods
70 | include ::PgSaurus::ConnectionAdapters::AbstractAdapter
71 | end
72 |
73 | ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
74 | prepend ::PgSaurus::ConnectionAdapters::PostgreSQLAdapter::SchemaMethods
75 | prepend ::PgSaurus::ConnectionAdapters::PostgreSQLAdapter::ForeignKeyMethods
76 |
77 | include ::PgSaurus::ConnectionAdapters::PostgreSQLAdapter
78 | end
79 |
80 | end
81 | end
82 |
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/spec/lib/core_ext/connection_adapters/abstract/schema_statements_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveRecord::ConnectionAdapters::SchemaStatements do
4 | describe '#add_index' do
5 | context "concurrently creates index" do
6 | let(:expected_query) do
7 | ["CREATE", "INDEX", "CONCURRENTLY", %("index_users_on_phone_number"), "ON", %("users"), %{("phone_number")}]
8 | end
9 |
10 | it 'concurrently creates index' do
11 | ActiveRecord::Migration.clear_queue
12 |
13 | expect(ActiveRecord::Base.connection).to receive(:execute) do |query|
14 | query.split(" ").should == expected_query
15 | end
16 |
17 | ActiveRecord::Migration.add_index :users, :phone_number, concurrently: true
18 | ActiveRecord::Migration.process_postponed_queries
19 | end
20 | end
21 |
22 | context "creates index for column with operator" do
23 | let(:expected_query) do
24 | ["CREATE", "INDEX", %("index_users_on_phone_number_varchar_pattern_ops"), "ON", %("users"), %{(phone_number}, %{varchar_pattern_ops)}]
25 | end
26 |
27 | it 'creates index for column with operator' do
28 | ActiveRecord::Migration.clear_queue
29 |
30 | expect(ActiveRecord::Base.connection).to receive(:execute) do |query|
31 | query.split(" ").should == expected_query
32 | end
33 |
34 | ActiveRecord::Migration.add_index :users, "phone_number varchar_pattern_ops"
35 | ActiveRecord::Migration.process_postponed_queries
36 | end
37 | end
38 |
39 | context "for functional index with longer operator string" do
40 | let(:expected_query) do
41 | ["CREATE", "INDEX", %("index_users_on_lower_first_name_desc_nulls_last"), "ON", %("users"),
42 | %{(trim(lower(first_name))}, "DESC", "NULLS", "LAST)"]
43 | end
44 |
45 | it 'creates functional index for column with longer operator string' do
46 | ActiveRecord::Migration.clear_queue
47 |
48 | expect(ActiveRecord::Base.connection).to receive(:execute) do |query|
49 | query.split(" ").should == expected_query
50 | end
51 |
52 | ActiveRecord::Migration.add_index :users, "trim(lower(first_name)) DESC NULLS LAST"
53 | ActiveRecord::Migration.process_postponed_queries
54 | end
55 | end
56 |
57 | it 'raises index exists error' do
58 | expect(ActiveRecord::Base.connection).
59 | to receive(:index_exists?).once.and_return(true)
60 |
61 | ActiveRecord::Migration.add_index :users, :phone_number, concurrently: true
62 |
63 | expect {
64 | ActiveRecord::Migration.process_postponed_queries
65 | }.to raise_exception(::PgSaurus::IndexExistsError)
66 | end
67 | end
68 |
69 | describe '#index_name' do
70 | let(:connection) { ActiveRecord::Base.connection }
71 |
72 | it "returns options[:name] if it's present" do
73 | expect(connection.index_name("sometable", name: "somename")).to eq "somename"
74 | end
75 |
76 | it "raises ArgumentError if there is no :column or :name in options" do
77 | expect {
78 | connection.index_name("sometable", {})
79 | }.to raise_error(ArgumentError, "You must specify the index name")
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/migration_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveRecord::Migration[5.2] do
4 | let(:conn) { double(:connection) }
5 |
6 | let(:ensure_role_set) { false }
7 |
8 | before do
9 | allow(PgSaurus.config).
10 | to receive(:ensure_role_set).and_return(ensure_role_set)
11 | end
12 |
13 | after { Object.send(:remove_const, :TestMigration) }
14 |
15 |
16 | describe "#exec_migration" do
17 | context "role is set" do
18 | before do
19 | class TestMigration < described_class
20 | set_role "mikki"
21 | end
22 | end
23 |
24 | it "executes migration as role" do
25 | migration = TestMigration.new
26 |
27 | expect(conn).to receive(:execute).with("SET ROLE mikki")
28 | expect(migration).to receive(:up)
29 | expect(conn).to receive(:execute).with("RESET ROLE")
30 |
31 | migration.exec_migration(conn, :up)
32 | end
33 |
34 | context "for data change migration" do
35 | it "raises an error" do
36 | expect {
37 | module SeedMigrator; end
38 |
39 | class TestMigration < described_class
40 | include SeedMigrator
41 |
42 | set_role "mikki"
43 | end
44 | }.to raise_error(PgSaurus::UseKeepDefaultRoleError)
45 | end
46 | end
47 | end
48 |
49 | context "role is not set" do
50 | before do
51 | class TestMigration < described_class; end
52 | end
53 |
54 | context "config.ensure_role_set=true" do
55 | let(:ensure_role_set) { true }
56 |
57 | it "raises error" do
58 | migration = TestMigration.new
59 |
60 | expect { migration.exec_migration(conn, :up) }.
61 | to raise_error(PgSaurus::RoleNotSetError, /TestMigration/)
62 | end
63 |
64 | context "keep_default_role was called" do
65 | before do
66 | module SeedMigrator; end
67 |
68 | class TestMigration < described_class
69 | include SeedMigrator
70 |
71 | keep_default_role
72 | end
73 | end
74 |
75 | it "executes migrations" do
76 | migration = TestMigration.new
77 |
78 | expect(migration).to receive(:up)
79 | migration.exec_migration(conn, :up)
80 | end
81 |
82 | context "for structure change migration" do
83 | it "raises an error" do
84 | expect {
85 | module SeedMigrator; end
86 |
87 | class TestMigrations < described_class
88 | keep_default_role
89 | end
90 | }.to raise_error(PgSaurus::UseSetRoleError)
91 | end
92 | end
93 | end
94 | end
95 |
96 | context "config.ensure_role_set=false" do
97 | let(:ensure_role_set) { false }
98 |
99 | it "executes migrations" do
100 | migration = TestMigration.new
101 |
102 | expect(migration).to receive(:up)
103 | migration.exec_migration(conn, :up)
104 | end
105 |
106 | end
107 | end
108 |
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/lib/pg_saurus/tools.rb:
--------------------------------------------------------------------------------
1 | module PgSaurus
2 | # Provides utility methods to work with PostgreSQL databases.
3 | # Usage:
4 | # PgSaurus::Tools.create_schema "services" # => create new PG schema "services"
5 | # PgSaurus::Tools.create_schema "nets"
6 | # PgSaurus::Tools.drop_schema "services" # => remove the schema
7 | # PgSaurus::Tools.schemas # => ["public", "information_schema", "nets"]
8 | # PgSaurus::Tools.move_table_to_schema :computers, :nets
9 | # PgSaurus::Tools.create_view view_name, view_definition # => creates new DB view
10 | # PgSaurus::Tools.drop_view view_name # => removes the view
11 | # PgSaurus::Tools.views # => ["x_view", "y_view", "z_view"]
12 | module Tools
13 | extend self
14 |
15 | # Create a schema if it does not exist yet.
16 | #
17 | # @note
18 | # Supports PostgreSQL 9.3+
19 | #
20 | # @return [void]
21 | def create_schema_if_not_exists(schema_name)
22 | unless schemas.include?(schema_name.to_s)
23 | sql = %{CREATE SCHEMA "#{schema_name}"}
24 | connection.execute sql
25 | end
26 | end
27 |
28 | # Ensure schema does not exists.
29 | #
30 | # @return [void]
31 | def drop_schema_if_exists(schema_name)
32 | connection.drop_schema(schema_name, if_exists: true)
33 | end
34 |
35 | # Returns an array of existing schemas.
36 | def schemas
37 | sql = "SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' order by nspname"
38 | connection.query(sql).flatten
39 | end
40 |
41 | # Move table to another schema without loosing data, indexes or constraints.
42 | # @param [String] table table name (schema prefix is allowed)
43 | # @param [String] new_schema schema where table should be moved to
44 | def move_table_to_schema(table, new_schema)
45 | schema, table = to_schema_and_table(table)
46 | sql = %{ALTER TABLE "#{schema}"."#{table}" SET SCHEMA "#{new_schema}"}
47 | connection.execute sql
48 | end
49 |
50 | # Creates PostgreSQL view
51 | # @param [String, Symbol] view_name
52 | # @param [String] view_definition
53 | def create_view(view_name, view_definition)
54 | sql = "CREATE VIEW #{view_name} AS #{view_definition}"
55 | connection.execute sql
56 | end
57 |
58 | # Drops PostgreSQL view
59 | # @param [String, Symbol] view_name
60 | def drop_view(view_name)
61 | sql = "DROP VIEW #{view_name}"
62 | connection.execute sql
63 | end
64 |
65 | # Returns an array of existing, non system views.
66 | def views
67 | sql = <<-SQL
68 | SELECT table_schema, table_name, view_definition
69 | FROM INFORMATION_SCHEMA.views
70 | WHERE table_schema NOT IN ('pg_catalog','information_schema')
71 | SQL
72 | connection.execute sql
73 | end
74 |
75 | # Return database connections
76 | def connection
77 | ActiveRecord::Base.connection
78 | end
79 | private :connection
80 |
81 | # Extract schema name and table name from qualified table name
82 | # @param [String, Symbol] table_name table name
83 | # @return [Array[String, String]] schema and table
84 | def to_schema_and_table(table_name)
85 | table, schema = table_name.to_s.split(".", 2).reverse
86 | schema ||= "public"
87 | [schema, table]
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/postgresql_adapter/schema_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe PgSaurus::ConnectionAdapters::PostgreSQLAdapter::SchemaMethods do
4 | class PostgreSQLAdapter
5 | include ::PgSaurus::ConnectionAdapters::PostgreSQLAdapter::SchemaMethods
6 | end
7 |
8 | let(:adapter_stub) { PostgreSQLAdapter.new }
9 |
10 | describe ".create_schema_if_not_exists" do
11 | it "refers to tools create_schema" do
12 | expect(::PgSaurus::Tools).to receive(:create_schema_if_not_exists).with("someschema")
13 | adapter_stub.create_schema_if_not_exists("someschema")
14 | end
15 |
16 | it "doesn't create the schema if it already exists" do
17 | ActiveRecord::Base.connection.create_schema "aschema"
18 | expect(::PgSaurus::Tools).to receive(:create_schema_if_not_exists).with("aschema")
19 | expect { adapter_stub.create_schema_if_not_exists("aschema") }.not_to raise_error
20 | end
21 | end
22 |
23 | describe ".drop_schema_if_exists" do
24 | it "refers to tools drop_schema" do
25 | ActiveRecord::Base.connection.create_schema "someschema"
26 | expect(::PgSaurus::Tools).to receive(:drop_schema_if_exists).with("someschema")
27 | adapter_stub.drop_schema_if_exists("someschema")
28 | end
29 |
30 | it "doesn't try to drop a non-existent schema" do
31 | expect(::PgSaurus::Tools).not_to receive(:drop_schema).with("someotherschema")
32 | adapter_stub.drop_schema_if_exists("someotherschema")
33 | end
34 | end
35 |
36 | describe ".move_table_to_schema" do
37 | it "refers to tools move_table_to_schema" do
38 | expect(::PgSaurus::Tools).to receive(:move_table_to_schema).with("sometable", "someschema")
39 | adapter_stub.move_table_to_schema("sometable", "someschema")
40 | end
41 | end
42 |
43 | describe "#rename_table_with_schema_option" do
44 | let(:connection) { ActiveRecord::Base.connection }
45 |
46 | it "renames table with schema option" do
47 | connection.create_table("something", schema: "demography") do |t|
48 | t.integer :foo
49 | end
50 | connection.add_index 'demography.something', 'foo'
51 | expect(connection.table_exists?("demography.something")).to be true
52 |
53 | connection.rename_table("something", "something_else", schema: "demography")
54 |
55 | expect(connection.table_exists?("demography.something") ).to be false
56 | expect(connection.table_exists?("demography.something_else")).to be true
57 |
58 | connection.drop_table("something_else", schema: "demography")
59 | end
60 |
61 | it "allows options to be a frozen Hash" do
62 | options = { schema: "demography" }.freeze
63 | connection.create_table("something", options)
64 | expect { connection.rename_table("something", "something_else", options) }.not_to raise_error
65 | end
66 |
67 | it 'renames the table created in the default schema' do
68 | connection.create_table("something") do |t|
69 | t.integer :foo
70 | end
71 | connection.add_index 'something', 'foo'
72 |
73 | connection.rename_table("something", "something_else")
74 |
75 | expect(connection.table_exists?("public.something") ).to be false
76 | expect(connection.table_exists?("public.something_else")).to be true
77 |
78 | connection.drop_table("something_else")
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/spec/support/explorer.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to fetch meta information about DB like comments and
2 | # foreign keys. It's used in test purpose.
3 | module PgSaurus::Explorer
4 | extend self
5 |
6 | def get_table_comment(table_name)
7 | schema, table = to_schema_and_table(table_name)
8 |
9 | connection.query(<<-SQL).flatten.first
10 | SELECT pg_desc.description
11 | FROM pg_catalog.pg_description pg_desc
12 | INNER JOIN pg_catalog.pg_class pg_class ON pg_class.oid = pg_desc.objoid
13 | INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid
14 | WHERE pg_class.relname = '#{table}'
15 | AND pg_namespace.nspname = '#{schema}'
16 | AND pg_desc.objsubid = 0 -- means table
17 | SQL
18 | end
19 |
20 | def get_column_comment(table_name, column)
21 | schema, table = to_schema_and_table(table_name)
22 |
23 | connection.query(<<-SQL).flatten.first
24 | SELECT d.description
25 | FROM pg_description d
26 | JOIN pg_class c on c.oid = d.objoid
27 | JOIN pg_attribute a ON c.oid = a.attrelid AND a.attnum = d.objsubid
28 | JOIN pg_namespace ON c.relnamespace = pg_namespace.oid
29 | WHERE c.relkind = 'r'
30 | AND c.relname = '#{table}'
31 | AND pg_namespace.nspname = '#{schema}'
32 | AND a.attname = '#{column}'
33 | SQL
34 | end
35 |
36 | def get_index_comment(index_name)
37 | schema, index = to_schema_and_table(index_name)
38 | connection.query(<<-SQL).flatten.first
39 | SELECT d.description AS comment
40 | FROM pg_description d
41 | JOIN pg_class c ON c.oid = d.objoid
42 | JOIN pg_namespace ON c.relnamespace = pg_namespace.oid
43 | WHERE c.relkind = 'i'
44 | AND c.relname = '#{index}'
45 | AND pg_namespace.nspname = '#{schema}'
46 | SQL
47 | end
48 |
49 | def has_foreign_key?(table_name, column)
50 | schema, table = to_schema_and_table(table_name)
51 |
52 | !!connection.query(<<-SQL).flatten.first
53 | SELECT tc.constraint_name
54 | FROM information_schema.table_constraints AS tc
55 | JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
56 | JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
57 | WHERE constraint_type = 'FOREIGN KEY'
58 | AND tc.table_name='#{table}'
59 | AND tc.table_schema = '#{schema}'
60 | AND kcu.column_name = '#{column}'
61 | SQL
62 | end
63 |
64 | def index_exists?(table_name, column_name, options = {})
65 | connection.index_exists?(table_name.to_s, column_name, options)
66 | end
67 |
68 | def table_exists?(table_name)
69 | schema, table = to_schema_and_table(table_name)
70 | !!connection.query(<<-SQL).flatten.first
71 | SELECT *
72 | FROM pg_class
73 | INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid
74 | WHERE pg_class.relname = '#{table}'
75 | AND pg_namespace.nspname = '#{schema}'
76 | SQL
77 | end
78 |
79 |
80 | # private
81 |
82 | def to_schema_and_table(table_name)
83 | table, schema = table_name.to_s.split(".", 2).reverse
84 | schema ||= "public"
85 | [schema, table]
86 | end
87 | private :to_schema_and_table
88 |
89 | def connection
90 | @connection || ActiveRecord::Base.connection
91 | end
92 | private :connection
93 | end
94 |
--------------------------------------------------------------------------------
/spec/comment_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Comment methods' do
4 |
5 | describe '#set_table_comment' do
6 | it "sets comment on table" do
7 | comment = PgSaurus::Explorer.get_table_comment "users"
8 | comment.should == "Information about users"
9 | end
10 |
11 | it "sets comment on table of non-public schema" do
12 | comment = PgSaurus::Explorer.get_table_comment "demography.citizens"
13 | comment.should == "Citizens Info"
14 | end
15 | end
16 |
17 | describe '#set_column_comment' do
18 | it "sets comment on column" do
19 | comment = PgSaurus::Explorer.get_column_comment "users", "name"
20 | comment.should == "User name"
21 | end
22 |
23 | it "sets comment on column of non-public schema" do
24 | comment = PgSaurus::Explorer.get_column_comment "demography.citizens", "country_id"
25 | comment.should == "Country key"
26 | end
27 | end
28 |
29 | describe '#set_column_comments' do
30 | it 'sets comments on columns' do
31 | PgSaurus::Explorer.get_column_comment("users", "email").should == "Email address"
32 | PgSaurus::Explorer.get_column_comment("users", "phone_number").should == "Phone number"
33 | end
34 |
35 | it "sets comments on columns of non-public schemas" do
36 | PgSaurus::Explorer.get_column_comment("demography.citizens", "first_name").
37 | should == "First name"
38 | PgSaurus::Explorer.get_column_comment("demography.citizens", "last_name").
39 | should == "Last name"
40 | end
41 | end
42 |
43 | describe '#set_index_comment' do
44 | it 'sets a comment on an index' do
45 | PgSaurus::Explorer.get_index_comment('index_pets_on_to_tsvector_name_gist').
46 | should == 'Functional index on name'
47 | end
48 |
49 | it 'sets a comment on an index in a non-public schema' do
50 | PgSaurus::Explorer.get_index_comment('demography.index_demography_citizens_on_country_id_and_user_id').
51 | should == 'Unique index on active citizens'
52 |
53 | end
54 | end
55 |
56 | # In migrations comments were set and then removed.
57 | # These tests suppose that #set_table_comment works as expected.
58 | describe '#remove_table_comment' do
59 | it 'removes comment on table' do
60 | PgSaurus::Explorer.get_table_comment("pets").should be_nil
61 | end
62 |
63 | it 'removes comment on table of non-public schema' do
64 | PgSaurus::Explorer.get_table_comment("demography.countries").should be_nil
65 | end
66 | end
67 |
68 | describe '#remove_column_comment' do
69 | it 'removes comment on column' do
70 | PgSaurus::Explorer.get_column_comment("demography.countries", "name").should == "Country name"
71 | PgSaurus::Explorer.get_column_comment("demography.countries", "continent").should be_nil
72 | end
73 | end
74 |
75 | describe '#remove_column_comments' do
76 | it 'removes comment on columns' do
77 | PgSaurus::Explorer.get_column_comment("demography.citizens", "bio").should be_nil
78 | PgSaurus::Explorer.get_column_comment("demography.citizens", "birthday").should be_nil
79 | end
80 | end
81 |
82 | describe '#remove_index_comment' do
83 | it 'removes comment on index' do
84 | PgSaurus::Explorer.get_index_comment('demography.index_demography_cities_on_country_id').
85 | should be_nil
86 | PgSaurus::Explorer.get_index_comment('index_pets_on_breed_id').
87 | should be_nil
88 | end
89 | end
90 |
91 | end
92 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/postgresql_adapter/comment_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PgSaurus::ConnectionAdapters::PostgreSQLAdapter::CommentMethods do
4 | class PostgreSQLAdapter
5 | include ::PgSaurus::ConnectionAdapters::PostgreSQLAdapter::CommentMethods
6 | end
7 |
8 | context "stubbed object" do
9 | let(:adapter_stub) { PostgreSQLAdapter.new }
10 |
11 | it ".supports_comments?" do
12 | expect(adapter_stub.supports_comments?).to be true
13 | end
14 | end
15 |
16 | context "connection object" do
17 | let(:connection) { ActiveRecord::Base.connection }
18 |
19 | it "#set_table_comment" do
20 | expect(connection).to receive(:execute).
21 | with("COMMENT ON TABLE \"users\" IS $$Users list$$;")
22 |
23 | connection.set_table_comment("users", "Users list")
24 | end
25 |
26 | it "#set_column_comment" do
27 | expect(connection).to receive(:execute).
28 | with("COMMENT ON COLUMN \"users\".\"name\" IS $$User name$$;")
29 | connection.set_column_comment("users", "name", "User name")
30 | end
31 |
32 | it "#set_column_comments" do
33 | expect(connection).to receive(:set_column_comment).
34 | with("users", "name", "User name")
35 | expect(connection).to receive(:set_column_comment).
36 | with("users", "email", "User email")
37 |
38 | connection.set_column_comments("users", {'name' => "User name", 'email' => "User email"})
39 | end
40 |
41 | it "#set_index_comment" do
42 | expect(connection).to receive(:execute).
43 | with("COMMENT ON INDEX index_users_on_email IS $$Index on user email$$;")
44 |
45 | connection.set_index_comment("index_users_on_email", "Index on user email")
46 | end
47 |
48 | it "#remove_table_comment" do
49 | expect(connection).to receive(:execute).
50 | with("COMMENT ON TABLE \"users\" IS NULL;")
51 |
52 | connection.remove_table_comment("users")
53 | end
54 |
55 | it "#remove_column_comment" do
56 | expect(connection).to receive(:execute).
57 | with("COMMENT ON COLUMN \"users\".\"name\" IS NULL;")
58 |
59 | connection.remove_column_comment("users", "name")
60 | end
61 |
62 | it "#remove_column_comments" do
63 | expect(connection).to receive(:remove_column_comment).with("users", "name")
64 | expect(connection).to receive(:remove_column_comment).with("users", "email")
65 |
66 | connection.remove_column_comments("users", "name", "email")
67 | end
68 |
69 | it "#remove_index_comment" do
70 | expect(connection).to receive(:execute).
71 | with("COMMENT ON INDEX index_users_on_email IS NULL;")
72 | connection.remove_index_comment("index_users_on_email")
73 | end
74 |
75 | it "#comments" do
76 | connection.set_table_comment("users", "Users list")
77 | connection.set_column_comment("users", "email", "User email")
78 |
79 | result = connection.comments("users")
80 |
81 | expect(result).to include([nil, 'Users list'])
82 | expect(result).to include(['email', 'User email'])
83 | end
84 |
85 | it "#index_comments" do
86 | connection.set_index_comment("index_users_on_email", "Index on user email")
87 | connection.set_index_comment("index_users_on_name", "Index on user name")
88 |
89 | result = connection.index_comments
90 |
91 | expect(result).to include(['public', 'index_users_on_email', 'Index on user email'])
92 | expect(result).to include(['public', 'index_users_on_name', 'Index on user name'])
93 | end
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/lib/pg_saurus/migration/set_role_method.rb:
--------------------------------------------------------------------------------
1 | module PgSaurus
2 | # Wrap original `exec_migration` to run migration with set postgresql role.
3 | # If config.ensure_role_set=true but no role is set for the migration, then an
4 | # exception is raised.
5 | module Migration::SetRoleMethod
6 | extend ActiveSupport::Concern
7 |
8 | included do
9 | class << self
10 | attr_reader :role
11 |
12 | # Set role
13 | #
14 | # @param role [String]
15 | def set_role(role)
16 | if const_defined?("SeedMigrator") && self.ancestors.include?(SeedMigrator)
17 | msg = <<~MSG
18 | Use keep_default_role instead of set_role for data change migration #{self}
19 |
20 | Example:
21 |
22 | class PopulateExample < ActiveRecord::Migration
23 | include #{self.ancestors[1]}
24 |
25 | keep_default_role
26 |
27 | def up
28 | apply_update "populate_example_data_update"
29 | end
30 |
31 | def down
32 | revert_update "populate_example_data_update"
33 | end
34 | end
35 | MSG
36 |
37 | raise PgSaurus::UseKeepDefaultRoleError, msg
38 | end
39 |
40 | @role = role
41 | end
42 |
43 | # Prevents raising exception when ensure_role_set=true and no role is set.
44 | def keep_default_role
45 | if const_defined?("SeedMigrator") && !self.ancestors.include?(SeedMigrator)
46 | msg = <<~MSG
47 | Use set_role instead of keep_default_role for structure migration #{self}
48 |
49 | Example:
50 |
51 | class CreateExamples < ActiveRecord::Migration
52 | set_role "superhero"
53 |
54 | def up
55 | ...
56 | end
57 |
58 | def down
59 | ...
60 | end
61 | end
62 | MSG
63 |
64 | raise PgSaurus::UseSetRoleError, msg
65 | end
66 |
67 | @keep_default_role = true
68 | end
69 |
70 | # Was +keep_default_role+ called for the migration?
71 | #
72 | # @return [Boolean]
73 | def keep_default_role?
74 | @keep_default_role
75 | end
76 | end
77 | end
78 |
79 | # Get role
80 | def role
81 | self.class.role
82 | end
83 |
84 | # :nodoc:
85 | def keep_default_role?
86 | self.class.keep_default_role?
87 | end
88 |
89 | # Module to be prepended into ActiveRecord::Migration which allows
90 | # enhancing the exec_migration method.
91 | module Extension
92 | # Wrap original `exec_migration` to run migration with set role.
93 | #
94 | # @param conn [ActiveRecord::ConnectionAdapters::PostgreSQLAdapter]
95 | # @param direction [Symbol] :up or :down
96 | #
97 | # @return [void]
98 | def exec_migration(conn, direction)
99 | if role
100 | begin
101 | conn.execute "SET ROLE #{role}"
102 | super(conn, direction)
103 | ensure
104 | conn.execute "RESET ROLE"
105 | end
106 | elsif PgSaurus.config.ensure_role_set && !keep_default_role?
107 | msg =
108 | "Role for migration #{self.class} is not set\n\n" \
109 | "You've configured PgSaurus with ensure_role_set=true. \n" \
110 | "That means that every migration must explicitly set role with set_role method.\n\n" \
111 | "Example:\n" \
112 | " class CreateNewTable < ActiveRecord::Migration\n" \
113 | " set_role \"superhero\"\n" \
114 | " end\n\n" \
115 | "If you want to set ensure_role_set=false, take a look at config/initializers/pg_saurus.rb\n\n"
116 | raise PgSaurus::RoleNotSetError, msg
117 | else
118 | super(conn, direction)
119 | end
120 | end
121 | end
122 |
123 | end
124 | end
125 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/postgresql_adapter/comment_methods.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend {ActiveRecord::ConnectionAdapters::PostgreSQLAdapter}
2 | # to support comments feature.
3 | module PgSaurus::ConnectionAdapters::PostgreSQLAdapter::CommentMethods
4 | def supports_comments?
5 | true
6 | end
7 |
8 | # Executes SQL to set comment on table
9 | # @param [String, Symbol] table_name name of table to set a comment on
10 | # @param [String] comment
11 | def set_table_comment(table_name, comment)
12 | sql = "COMMENT ON TABLE #{quote_table_name(table_name)} IS $$#{comment}$$;"
13 | execute sql
14 | end
15 |
16 | # Executes SQL to set comment on column.
17 | # @param [String, Symbol] table_name
18 | # @param [String, Symbol] column_name
19 | # @param [String] comment
20 | def set_column_comment(table_name, column_name, comment)
21 | sql = "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS $$#{comment}$$;"
22 | execute sql
23 | end
24 |
25 | # Sets comments on columns of passed table.
26 | # @param [String, Symbol] table_name
27 | # @param [Hash] comments every key is a column name and value is a comment.
28 | def set_column_comments(table_name, comments)
29 | comments.each_pair do |column_name, comment|
30 | set_column_comment table_name, column_name, comment
31 | end
32 | end
33 |
34 | # Sets the given comment on the given index
35 | # @param [String, Symbol] index_name The name of the index
36 | # @param [String, Symbol] comment The comment to set on the index
37 | def set_index_comment(index_name, comment)
38 | sql = "COMMENT ON INDEX #{quote_string(index_name)} IS $$#{comment}$$;"
39 | execute sql
40 | end
41 |
42 | # Executes SQL to remove comment on passed table.
43 | # @param [String, Symbol] table_name
44 | def remove_table_comment(table_name)
45 | sql = "COMMENT ON TABLE #{quote_table_name(table_name)} IS NULL;"
46 | execute sql
47 | end
48 |
49 | # Executes SQL to remove comment on column.
50 | # @param [String, Symbol] table_name
51 | # @param [String, Symbol] column_name
52 | def remove_column_comment(table_name, column_name)
53 | sql = "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS NULL;"
54 | execute sql
55 | end
56 |
57 | # Remove comments on passed table columns.
58 | def remove_column_comments(table_name, *column_names)
59 | column_names.each do |column_name|
60 | remove_column_comment table_name, column_name
61 | end
62 | end
63 |
64 | # Removes any comment from the given index
65 | # @param [String, Symbol] index_name The name of the index
66 | def remove_index_comment(index_name)
67 | sql = "COMMENT ON INDEX #{quote_string(index_name)} IS NULL;"
68 | execute sql
69 | end
70 |
71 | # Fetches all comments related to passed table.
72 | # I returns table comment and column comments as well.
73 | # ===Example
74 | # comments("users") # => [[ "" , "Comment on table" ],
75 | # ["id" , "Comment on id column" ],
76 | # ["email", "Comment on email column"]]
77 | def comments(table_name)
78 | relation_name, schema_name = table_name.split(".", 2).reverse
79 | schema_name ||= :public
80 |
81 | com = select_all <<-SQL
82 | SELECT a.attname AS column_name, d.description AS comment
83 | FROM pg_description d
84 | JOIN pg_class c on c.oid = d.objoid
85 | LEFT OUTER JOIN pg_attribute a ON c.oid = a.attrelid AND a.attnum = d.objsubid
86 | JOIN pg_namespace ON c.relnamespace = pg_namespace.oid
87 | WHERE c.relkind = 'r' AND c.relname = '#{relation_name}' AND
88 | pg_namespace.nspname = '#{schema_name}'
89 | SQL
90 | com.map do |row|
91 | [ row['column_name'], row['comment'] ]
92 | end
93 | end
94 |
95 | # Fetches index comments
96 | # returns an Array of Arrays, each element representing a single index with comment as
97 | # [ 'schema_name', 'index_name', 'comment' ]
98 | def index_comments
99 | query = <<-SQL
100 | SELECT c.relname AS index_name, d.description AS comment, pg_namespace.nspname AS schema_name
101 | FROM pg_description d
102 | JOIN pg_class c ON c.oid = d.objoid
103 | JOIN pg_namespace ON c.relnamespace = pg_namespace.oid
104 | WHERE c.relkind = 'i'
105 | ORDER BY schema_name, index_name
106 | SQL
107 |
108 | com = select_all(query)
109 |
110 | com.map do |row|
111 | [ row['schema_name'], row['index_name'], row['comment'] ]
112 | end
113 | end
114 | end
115 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/postgresql_adapter/foreign_key_methods.rb:
--------------------------------------------------------------------------------
1 | module PgSaurus # :nodoc:
2 | # Provides methods to extend {ActiveRecord::ConnectionAdapters::PostgreSQLAdapter}
3 | # to support foreign keys feature.
4 | module ConnectionAdapters::PostgreSQLAdapter::ForeignKeyMethods
5 | # Drop table and optionally disable triggers.
6 | # Changes adapted from https://github.com/matthuhiggins/foreigner/blob/e72ab9c454c156056d3f037d55e3359cd972af32/lib/foreigner/connection_adapters/sql2003.rb
7 | # NOTE: Disabling referential integrity requires superuser access in postgres.
8 | # Default AR behavior is just to drop_table.
9 | #
10 | # == Options:
11 | # * :force - force disabling of referential integrity
12 | #
13 | # Note: I don't know a good way to test this -mike 20120420
14 | def drop_table(*args)
15 | options = args.clone.extract_options!
16 | if options[:force]
17 | disable_referential_integrity { super }
18 | else
19 | super
20 | end
21 | end
22 |
23 | # See activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
24 | #
25 | # Creates index on the FK column by default. Pass in the option exclude_index: true
26 | # to disable this.
27 | def add_foreign_key(from_table, to_table, **options)
28 | exclude_index = (options.has_key?(:exclude_index) ? options.delete(:exclude_index) : false)
29 | column = options[:column] || foreign_key_column_for(to_table)
30 |
31 | if index_exists?(from_table, column) && !exclude_index
32 | raise PgSaurus::IndexExistsError,
33 | "The index, #{index_name(from_table, column)}, already exists." \
34 | " Use :exclude_index => true when adding the foreign key."
35 | end
36 |
37 | super from_table, to_table, **options
38 |
39 | unless exclude_index
40 | add_index from_table, column
41 | end
42 | end
43 |
44 | # See activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
45 | #
46 | # Pass in the option remove_index: true to remove index as well.
47 | def remove_foreign_key(from_table, to_table = nil, **options)
48 | if options[:remove_index]
49 | column = options[:column]
50 | remove_index from_table, column
51 | options.delete(:remove_index)
52 | end
53 |
54 | super(from_table, to_table, **options)
55 | end
56 |
57 | # See: activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
58 | #
59 | # Removes schema name from table name.
60 | def foreign_key_column_for(table_name, column_name = "id")
61 | table = table_name.to_s.split('.').last
62 |
63 | if Rails.gem_version >= "7.2"
64 | super table, column_name
65 | else
66 | super table
67 | end
68 | end
69 |
70 | # See activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
71 | #
72 | # Add from_schema option to foreign key definition options.
73 | def foreign_keys(table_name)
74 | namespace = table_name.to_s.split('.').first
75 | table_name = table_name.to_s.split('.').last
76 |
77 | namespace = if namespace == table_name
78 | "ANY (current_schemas(false))"
79 | else
80 | quote(namespace)
81 | end
82 |
83 | sql = <<-SQL.strip_heredoc
84 | SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, t3.nspname AS from_schema
85 | FROM pg_constraint c
86 | JOIN pg_class t1 ON c.conrelid = t1.oid
87 | JOIN pg_class t2 ON c.confrelid = t2.oid
88 | JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
89 | JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
90 | JOIN pg_namespace t3 ON c.connamespace = t3.oid
91 | WHERE c.contype = 'f'
92 | AND t1.relname = #{quote(table_name)}
93 | AND t3.nspname = #{namespace}
94 | ORDER BY c.conname
95 | SQL
96 |
97 | fk_info = select_all(sql)
98 |
99 | fk_info.map do |row|
100 | options = {
101 | column: row['column'],
102 | name: row['name'],
103 | primary_key: row['primary_key'],
104 | from_schema: row['from_schema']
105 | }
106 |
107 | options[:on_delete] = extract_foreign_key_action(row['on_delete'])
108 | options[:on_update] = extract_foreign_key_action(row['on_update'])
109 |
110 | ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name, row['to_table'], options)
111 | end
112 | end
113 | end
114 | end
115 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/postgresql_adapter/trigger_methods.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend {ActiveRecord::ConnectionAdapters::PostgreSQLAdapter}
2 | # to support db triggers.
3 | module PgSaurus::ConnectionAdapters::PostgreSQLAdapter::TriggerMethods
4 |
5 | # :nodoc
6 | def supports_triggers?
7 | true
8 | end
9 |
10 | # See lib/pg_saurus/connection_adapters/trigger_methods.rb
11 | def create_trigger(table_name, proc_name, event, options = {})
12 | proc_name = "#{proc_name}"
13 | proc_name = "#{proc_name}()" unless proc_name.end_with?(')')
14 |
15 | for_each = options[:for_each] || 'ROW'
16 | constraint = options[:constraint]
17 |
18 | sql = "CREATE #{!!constraint ? "CONSTRAINT " : ""}TRIGGER #{trigger_name(proc_name, options)}\n #{event}\n"
19 |
20 | sql << " ON #{quote_table_or_view(table_name, options)}\n"
21 | if constraint
22 | sql << if options[:deferrable]
23 | " DEFERRABLE INITIALLY #{!!options[:initially_deferred] ? 'DEFERRED' : 'IMMEDIATE'}\n"
24 | else
25 | " NOT DEFERRABLE\n"
26 | end
27 | end
28 | sql << " FOR EACH #{for_each}\n"
29 | if condition = options[:condition]
30 | sql << " WHEN (#{condition})\n"
31 | end
32 | sql << " EXECUTE PROCEDURE #{proc_name}"
33 |
34 | execute sql
35 | end
36 |
37 | # See lib/pg_saurus/connection_adapters/trigger_methods.rb
38 | def remove_trigger(table_name, proc_name, options = {})
39 | execute "DROP TRIGGER #{trigger_name(proc_name, options)} ON #{quote_table_or_view(table_name, options)}"
40 | end
41 |
42 | # Returns the listing of currently defined db triggers
43 | #
44 | # @return [Array<::PgSaurus::ConnectionAdapters::TriggerDefinition>]
45 | def triggers
46 | res = select_all <<-SQL
47 | SELECT n.nspname as schema,
48 | c.relname as table,
49 | t.tgname as trigger_name,
50 | t.tgenabled as enable_mode,
51 | t.tgdeferrable as is_deferrable,
52 | t.tginitdeferred as is_initially_deferrable,
53 | pg_catalog.pg_get_triggerdef(t.oid, true) as trigger_definition
54 | FROM pg_catalog.pg_trigger t
55 | INNER JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid
56 | INNER JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
57 | WHERE c.relkind IN ('r', 'v')
58 | AND NOT t.tgisinternal
59 | ORDER BY 1, 2, 3;
60 | SQL
61 |
62 | res.inject([]) do |buffer, row|
63 | schema = row['schema']
64 | table = row['table']
65 | trigger_name = row['trigger_name']
66 | is_deferrable = row['is_deferrable']
67 | is_initially_deferred = row['is_initially_deferred']
68 |
69 | trigger_definition = row['trigger_definition']
70 |
71 | is_constraint = is_constraint?(trigger_definition)
72 | proc_name = parse_proc_name(trigger_definition)
73 | event = parse_event(trigger_definition, trigger_name)
74 | condition = parse_condition(trigger_definition)
75 |
76 | for_every = !!(trigger_definition =~ /FOR[\s]EACH[\s]ROW/) ? :row : :statement
77 |
78 | if proc_name && event
79 | buffer << ::PgSaurus::ConnectionAdapters::TriggerDefinition.new(
80 | trigger_name,
81 | proc_name,
82 | is_constraint,
83 | event,
84 | for_every,
85 | is_deferrable,
86 | is_initially_deferred,
87 | condition,
88 | table,
89 | schema
90 | )
91 | end
92 | buffer
93 | end
94 | end
95 |
96 | # Parse the condition from the trigger definition.
97 | def parse_condition(trigger_definition)
98 | trigger_definition[/WHEN[\s](.*?)[\s]EXECUTE[\s](FUNCTION|PROCEDURE)/m, 1]
99 | end
100 | private :parse_condition
101 |
102 | # Parse the event from the trigger definition.
103 | def parse_event(trigger_definition, trigger_name)
104 | trigger_definition[/^CREATE[\sA-Z]+TRIGGER[\s]#{Regexp.escape(trigger_name)}[\s](.*?)[\s]ON[\s]/m, 1]
105 | end
106 | private :parse_event
107 |
108 | # Parse the procedure name from the trigger definition.
109 | def parse_proc_name(trigger_definition)
110 | trigger_definition[/EXECUTE[\s](FUNCTION|PROCEDURE)[\s](.*?)$/m, 2]
111 | end
112 | private :parse_proc_name
113 |
114 | # Whether the trigger is a constraint.
115 | def is_constraint?(trigger_definition)
116 | !!(trigger_definition =~ /^CREATE CONSTRAINT TRIGGER/)
117 | end
118 | private :is_constraint?
119 |
120 | # Properly quote the table name or view name.
121 | def quote_table_or_view(name, options)
122 | schema = options[:schema]
123 | if schema
124 | "\"#{schema}\".\"#{name}\""
125 | else
126 | "\"#{name}\""
127 | end
128 | end
129 | private :quote_table_or_view
130 |
131 | # The name provided in the options, or constructed from the procedure name.
132 | def trigger_name(proc_name, options)
133 | if name = options[:name]
134 | name
135 | else
136 | "trigger_#{proc_name.gsub('(', '').gsub(')', '')}"
137 | end
138 | end
139 | private :trigger_name
140 |
141 | end
142 |
--------------------------------------------------------------------------------
/spec/lib/pg_saurus/connection_adapters/postgresql_adapter/function_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe PgSaurus::ConnectionAdapters::PostgreSQLAdapter::FunctionMethods do
4 |
5 | let(:connection) { ActiveRecord::Base.connection }
6 |
7 | it ".supports_functions?" do
8 | expect(connection.supports_functions?).to be true
9 | end
10 |
11 | context ".create_function" do
12 |
13 | specify "default behavior" do
14 | sql = <<-SQL.gsub(/^[ ]{8}/, "")
15 | CREATE OR REPLACE FUNCTION "public".pets_not_empty()
16 | RETURNS boolean
17 | LANGUAGE plpgsql
18 | AS $function$
19 | BEGIN
20 | IF (SELECT COUNT(*) FROM pets) > 0
21 | THEN
22 | RETURN true;
23 | ELSE
24 | RETURN false;
25 | END IF;
26 | END;
27 | $function$
28 | SQL
29 |
30 | expect(connection).to receive(:execute).with(sql)
31 |
32 | connection.create_function "pets_not_empty()",
33 | :boolean,
34 | <<-FUNCTION.gsub(/^[ ]{8}/, ""), schema: "public"
35 | BEGIN
36 | IF (SELECT COUNT(*) FROM pets) > 0
37 | THEN
38 | RETURN true;
39 | ELSE
40 | RETURN false;
41 | END IF;
42 | END;
43 | FUNCTION
44 | end
45 |
46 | specify "function with arguments" do
47 | sql = <<-SQL.gsub(/^[ ]{8}/, "")
48 | CREATE OR REPLACE FUNCTION "public".pet_exists(type text, size numeric)
49 | RETURNS boolean
50 | LANGUAGE plpgsql
51 | AS $function$
52 | BEGIN
53 | IF (SELECT 1 FROM pets p WHERE p.type = type AND p.size = size) = 1
54 | THEN
55 | RETURN true;
56 | ELSE
57 | RETURN false;
58 | END IF;
59 | END;
60 | $function$
61 | SQL
62 |
63 | expect(connection).to receive(:execute).with(sql)
64 |
65 | connection.create_function "pet_exists(type text, size numeric)",
66 | :boolean,
67 | <<-FUNCTION.gsub(/^[ ]{8}/, ""), schema: "public"
68 | BEGIN
69 | IF (SELECT 1 FROM pets p WHERE p.type = type AND p.size = size) = 1
70 | THEN
71 | RETURN true;
72 | ELSE
73 | RETURN false;
74 | END IF;
75 | END;
76 | FUNCTION
77 | end
78 |
79 | specify "no schema and replace set to false" do
80 | sql = <<-SQL.gsub(/^[ ]{8}/, "")
81 | CREATE FUNCTION pets_not_empty()
82 | RETURNS boolean
83 | LANGUAGE plpgsql
84 | AS $function$
85 | BEGIN
86 | IF (SELECT COUNT(*) FROM pets) > 0
87 | THEN
88 | RETURN true;
89 | ELSE
90 | RETURN false;
91 | END IF;
92 | END;
93 | $function$
94 | SQL
95 |
96 | expect(connection).to receive(:execute).with(sql)
97 |
98 | connection.create_function "pets_not_empty()",
99 | :boolean,
100 | <<-FUNCTION.gsub(/^[ ]{8}/, ""), replace: false
101 | BEGIN
102 | IF (SELECT COUNT(*) FROM pets) > 0
103 | THEN
104 | RETURN true;
105 | ELSE
106 | RETURN false;
107 | END IF;
108 | END;
109 | FUNCTION
110 | end
111 |
112 | specify "set volatility" do
113 | sql = <<-SQL.gsub(/^[ ]{8}/, "")
114 | CREATE OR REPLACE FUNCTION "public".pets_not_empty()
115 | RETURNS boolean
116 | LANGUAGE plpgsql
117 | STABLE
118 | AS $function$
119 | BEGIN
120 | IF (SELECT COUNT(*) FROM pets) > 0
121 | THEN
122 | RETURN true;
123 | ELSE
124 | RETURN false;
125 | END IF;
126 | END;
127 | $function$
128 | SQL
129 |
130 | expect(connection).to receive(:execute).with(sql)
131 |
132 | connection.create_function "pets_not_empty()",
133 | :boolean,
134 | <<-FUNCTION.gsub(/^[ ]{8}/, ""), schema: "public", volatility: :stable
135 | BEGIN
136 | IF (SELECT COUNT(*) FROM pets) > 0
137 | THEN
138 | RETURN true;
139 | ELSE
140 | RETURN false;
141 | END IF;
142 | END;
143 | FUNCTION
144 | end
145 | end
146 |
147 | it ".drop_function" do
148 | expect(connection).to receive(:execute).with("DROP FUNCTION foo_bar()")
149 |
150 | connection.drop_function "foo_bar()"
151 | end
152 |
153 | context "#functions" do
154 | it "volatile function" do
155 | function = connection.functions.find { |f| f.name == "public.pets_not_empty()" }
156 |
157 | expect(function).not_to be_nil
158 | expect(function.returning).to eq("boolean")
159 | expect(function.volatility).to eq(:volatile)
160 | end
161 |
162 | it "immutable function" do
163 | function = connection.functions.find { |f| f.name == "public.pets_not_empty_trigger_proc()" }
164 |
165 | expect(function).not_to be_nil
166 | expect(function.returning).to eq("trigger")
167 | expect(function.volatility).to eq(:immutable)
168 | end
169 | end
170 |
171 | end
172 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/postgresql_adapter/function_methods.rb:
--------------------------------------------------------------------------------
1 | # Methods to extend {ActiveRecord::ConnectionAdapters::PostgreSQLAdapter}
2 | # to support database functions.
3 | module PgSaurus::ConnectionAdapters::PostgreSQLAdapter::FunctionMethods
4 | # Regular expression used in function signature parsing:
5 | FUNCTION_PARSE_REGEXP = /^CREATE[\s\S]+FUNCTION /
6 |
7 | # Return +true+.
8 | def supports_functions?
9 | true
10 | end
11 |
12 | # Return a list of defined DB functions. Ignore function definitions that can't be parsed.
13 | def functions
14 | pg_major = ::PgSaurus::Engine.pg_server_version[0]
15 | res = select_all <<-SQL
16 | SELECT n.nspname AS "Schema",
17 | p.proname AS "Name",
18 | pg_catalog.pg_get_function_result(p.oid) AS "Returning",
19 | CASE
20 | WHEN #{pg_major >= 11 ? "p.prokind = 'w'" : "p.proiswindow"} THEN 'window'
21 | WHEN p.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN 'trigger'
22 | ELSE 'normal'
23 | END AS "Type",
24 | p.oid AS "Oid"
25 | FROM pg_catalog.pg_proc p
26 | LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
27 | WHERE pg_catalog.pg_function_is_visible(p.oid)
28 | AND n.nspname <> 'pg_catalog'
29 | AND n.nspname <> 'information_schema'
30 | AND #{pg_major >= 11 ? "p.prokind <> 'a'" : "p.proisagg <> TRUE"}
31 | ORDER BY 1, 2, 3, 4;
32 | SQL
33 | res.inject([]) do |buffer, row|
34 | returning = row['Returning']
35 | function_type = row['Type']
36 | oid = row['Oid']
37 |
38 | function_str = select_value("SELECT pg_get_functiondef(#{oid});")
39 |
40 | name = parse_function_name(function_str)
41 | language = parse_function_language(function_str)
42 | definition = parse_function_definition(function_str)
43 | volatility = parse_function_volatility(function_str)
44 |
45 | if definition
46 | buffer << ::PgSaurus::ConnectionAdapters::FunctionDefinition.new(name,
47 | returning,
48 | definition.strip,
49 | function_type,
50 | language,
51 | oid,
52 | volatility)
53 | end
54 | buffer
55 | end
56 | end
57 |
58 | # Create a new database function.
59 | def create_function(function_name, returning, definition, options = {})
60 |
61 | function_name = full_function_name(function_name, options)
62 | language = options[:language] || 'plpgsql'
63 | replace = if options[:replace] == false
64 | ''
65 | else
66 | 'OR REPLACE '
67 | end
68 | volatility = case options[:volatility]
69 | when :volatile, :stable, :immutable
70 | "\n #{options[:volatility].to_s.upcase}"
71 | else
72 | ""
73 | end
74 |
75 | sql = <<-SQL.gsub(/^[ ]{6}/, "")
76 | CREATE #{replace}FUNCTION #{function_name}
77 | RETURNS #{returning}
78 | LANGUAGE #{language}#{volatility}
79 | AS $function$
80 | #{definition.strip}
81 | $function$
82 | SQL
83 |
84 | execute(sql)
85 | end
86 |
87 | # Drop the given database function.
88 | def drop_function(function_name, options = {})
89 | function_name = full_function_name(function_name, options)
90 |
91 | execute "DROP FUNCTION #{function_name}"
92 | end
93 |
94 | # Retrieve the function name from the function SQL.
95 | def parse_function_name(function_str)
96 | function_str.
97 | split("\n").
98 | find { |line| line =~ FUNCTION_PARSE_REGEXP }.
99 | sub(FUNCTION_PARSE_REGEXP, '')
100 | end
101 | private :parse_function_name
102 |
103 | # Retrieve the function language from the function SQL.
104 | def parse_function_language(function_str)
105 | function_str.split("\n").find { |line| line =~ /LANGUAGE/ }.split(' ').last
106 | end
107 | private :parse_function_language
108 |
109 | # Retrieve the volatility of the function: volatile, stable, or immutable.
110 | # @return [Symbol]
111 | def parse_function_volatility(function_str)
112 | rows = function_str.split("\n")
113 | lang_index = rows.index { |line| line =~ /LANGUAGE/ }
114 | def_index = rows.index { |line| line =~ /AS \$function\$/ }
115 |
116 | if lang_index && def_index && def_index - lang_index == 2
117 | tokens = rows[def_index - 1].strip.downcase.split(" ")
118 | token = tokens.find { |t| %w(volatile stable immutable).include? t }
119 |
120 | token.nil? ? :volatile : token.to_sym
121 | else
122 | :volatile
123 | end
124 | end
125 | private :parse_function_volatility
126 |
127 | # Retrieve the function definition from the function SQL.
128 | def parse_function_definition(function_str)
129 | function_str[/#{Regexp.escape("AS $function$\n")}(.*?)#{Regexp.escape("$function$")}/m, 1]
130 | end
131 | private :parse_function_definition
132 |
133 | # Write out the fully qualified function name if the :schema option is passed.
134 | def full_function_name(function_name, options)
135 | schema = options[:schema]
136 | function_name = %Q{"#{schema}".#{function_name}} if schema
137 | function_name
138 | end
139 | private :full_function_name
140 | end
141 |
--------------------------------------------------------------------------------
/lib/pg_saurus/connection_adapters/postgresql_adapter/extension_methods.rb:
--------------------------------------------------------------------------------
1 | # Provides methods to extend {ActiveRecord::ConnectionAdapters::PostgreSQLAdapter}
2 | # to support extensions feature.
3 | module PgSaurus::ConnectionAdapters::PostgreSQLAdapter::ExtensionMethods
4 | # Default options for {#create_extension} method.
5 | CREATE_EXTENSION_DEFAULTS = {
6 | if_not_exists: true,
7 | schema_name: nil,
8 | version: nil,
9 | old_version: nil
10 | }
11 |
12 | # Default options for {#drop_extension} method.
13 | DROP_EXTENSION_DEFAULTS = {
14 | if_exists: true,
15 | mode: :restrict
16 | }
17 |
18 | # The modes which determine postgresql behavior on DROP EXTENSION operation.
19 | #
20 | # *:restrict* refuse to drop the extension if any objects depend on it
21 | # *:cascade* automatically drop objects that depend on the extension
22 | AVAILABLE_DROP_MODES = {
23 | restrict: 'RESTRICT',
24 | cascade: 'CASCADE'
25 | }
26 |
27 | # @return [Boolean] if adapter supports postgresql extension manipulation
28 | def supports_extensions?
29 | true
30 | end
31 |
32 | # Execute SQL to load a postgresql extension module into the current database.
33 | #
34 | # @param [#to_s] extension_name Name of the extension module to load
35 | # @param [Hash] options
36 | # @option options [Boolean] :if_not_exists should the 'IF NOT EXISTS' clause be added
37 | # @option options [#to_s,nil] :schema_name The name of the schema in which to install the extension's objects
38 | # @option options [#to_s,nil] :version The version of the extension to install
39 | # @option options [#to_s,nil] :old_version Alternative installation script name
40 | # that absorbs the existing objects into the extension, instead of creating new objects
41 | def create_extension(extension_name, options = {})
42 | options = CREATE_EXTENSION_DEFAULTS.merge(options.symbolize_keys)
43 |
44 | sql = ['CREATE EXTENSION']
45 | sql << 'IF NOT EXISTS' if options[:if_not_exists]
46 | sql << %Q{"#{extension_name.to_s}"}
47 | sql << "SCHEMA #{options[:schema_name]}" if options[:schema_name].present?
48 | sql << "VERSION '#{options[:version]}'" if options[:version].present?
49 | sql << "FROM #{options[:old_version]}" if options[:old_version].present?
50 |
51 | sql = sql.join(' ')
52 | execute(sql)
53 | end
54 |
55 | # Execute SQL to load a postgresql extension module into the current database
56 | # if it does not already exist. Then reload the type map.
57 | #
58 | # @param [#to_s] extension_name Name of the extension module to load
59 | # @param [Hash] options
60 | # @option options [#to_s,nil] :schema_name The name of the schema in which to install the extension's objects
61 | # @option options [#to_s,nil] :version The version of the extension to install
62 | # @option options [#to_s,nil] :old_version Alternative installation script name
63 | # that absorbs the existing objects into the extension, instead of creating new objects
64 | def enable_extension(extension_name, options = {})
65 | options[:if_not_exists] = true
66 | create_extension(extension_name, options).tap { reload_type_map }
67 | end
68 |
69 | # Execute SQL to remove a postgresql extension module from the current database.
70 | #
71 | # @param [#to_s] extension_name Name of the extension module to unload
72 | # @param [Hash] options
73 | # @option options [Boolean] :if_exists should the 'IF EXISTS' clause be added
74 | # @option options [Symbol] :mode Operation mode. See {AVAILABLE_DROP_MODES} for details
75 | def drop_extension(extension_name, options = {})
76 | options = DROP_EXTENSION_DEFAULTS.merge(options.symbolize_keys)
77 |
78 | sql = ['DROP EXTENSION']
79 | sql << 'IF EXISTS' if options[:if_exists]
80 | sql << %Q{"#{extension_name.to_s}"}
81 |
82 | mode = options[:mode]
83 | if mode.present?
84 | mode = mode.to_sym
85 |
86 | unless AVAILABLE_DROP_MODES.include?(mode)
87 | raise ArgumentError,
88 | "Expected one of #{AVAILABLE_DROP_MODES.keys.inspect} drop modes, " \
89 | "but #{mode} received"
90 | end
91 |
92 | sql << AVAILABLE_DROP_MODES[mode]
93 | end
94 |
95 | sql = sql.join(' ')
96 | execute(sql)
97 | end
98 |
99 | # Query the pg_catalog for all extension modules loaded to the current database.
100 | #
101 | # @note
102 | # Since Rails 4 connection has method +extensions+ that returns array of extensions.
103 | # This method is slightly different, since it returns also additional options.
104 | #
105 | # Please note all extensions which belong to pg_catalog schema are omitted
106 | # ===Example
107 | #
108 | # extension # => {
109 | # "fuzzystrmatch" => {:schema_name => "public", :version => "1.0" }
110 | # }
111 | #
112 | # @return [Hash{String => Hash{Symbol => String}}] A list of loaded extensions with their options
113 | def pg_extensions
114 | # Check postgresql version to not break on Postgresql < 9.1 during schema dump
115 | return {} if (::PgSaurus::Engine.pg_server_version <=> [9, 1]) < 0
116 |
117 | sql = <<-SQL
118 | SELECT pge.extname AS ext_name, pgn.nspname AS schema_name, pge.extversion AS ext_version
119 | FROM pg_extension pge
120 | INNER JOIN pg_namespace pgn on pge.extnamespace = pgn.oid
121 | WHERE pgn.nspname <> 'pg_catalog'
122 | SQL
123 |
124 | result = select_all(sql)
125 | formatted_result = result.map do |row|
126 | [
127 | row['ext_name'],
128 | {
129 | schema_name: row['schema_name'],
130 | version: row['ext_version']
131 | }
132 | ]
133 | end
134 |
135 | Hash[formatted_result]
136 | end
137 | end
138 |
--------------------------------------------------------------------------------
/pg_saurus.gemspec:
--------------------------------------------------------------------------------
1 | # Generated by jeweler
2 | # DO NOT EDIT THIS FILE DIRECTLY
3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4 | # -*- encoding: utf-8 -*-
5 | # stub: pg_saurus 6.0.0 ruby lib
6 |
7 | Gem::Specification.new do |s|
8 | s.name = "pg_saurus".freeze
9 | s.version = "6.0.0".freeze
10 |
11 | s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12 | s.require_paths = ["lib".freeze]
13 | s.authors = ["Potapov Sergey".freeze, "Arthur Shagall".freeze, "Artem Ignatyev".freeze, "Matt Dressel".freeze, "Bruce Burdick".freeze, "HornsAndHooves".freeze]
14 | s.date = "2025-10-29"
15 | s.description = "ActiveRecord extensions for PostgreSQL. Provides useful tools for schema, foreign_key, index, function, trigger, comment and extension manipulations in migrations.".freeze
16 | s.email = ["blake131313@gmail.com".freeze, "arthur.shagall@gmail.com".freeze, "cryo28@gmail.com".freeze, "matt.dressel@gmail.com".freeze, "rubygems.org@bruceburdick.com".freeze]
17 | s.extra_rdoc_files = [
18 | "README.markdown"
19 | ]
20 | s.files = [
21 | "README.markdown",
22 | "lib/colorized_text.rb",
23 | "lib/core_ext/active_record/connection_adapters/postgresql/schema_statements.rb",
24 | "lib/core_ext/active_record/errors.rb",
25 | "lib/core_ext/active_record/migration/compatibility.rb",
26 | "lib/core_ext/active_record/schema_dumper.rb",
27 | "lib/generators/pg_saurus/install/install_generator.rb",
28 | "lib/generators/pg_saurus/install/templates/config/initializers/pg_saurus.rb",
29 | "lib/pg_saurus.rb",
30 | "lib/pg_saurus/config.rb",
31 | "lib/pg_saurus/connection_adapters.rb",
32 | "lib/pg_saurus/connection_adapters/abstract_adapter.rb",
33 | "lib/pg_saurus/connection_adapters/abstract_adapter/comment_methods.rb",
34 | "lib/pg_saurus/connection_adapters/abstract_adapter/function_methods.rb",
35 | "lib/pg_saurus/connection_adapters/abstract_adapter/index_methods.rb",
36 | "lib/pg_saurus/connection_adapters/abstract_adapter/schema_methods.rb",
37 | "lib/pg_saurus/connection_adapters/abstract_adapter/trigger_methods.rb",
38 | "lib/pg_saurus/connection_adapters/foreign_key_definition.rb",
39 | "lib/pg_saurus/connection_adapters/function_definition.rb",
40 | "lib/pg_saurus/connection_adapters/postgresql_adapter.rb",
41 | "lib/pg_saurus/connection_adapters/postgresql_adapter/comment_methods.rb",
42 | "lib/pg_saurus/connection_adapters/postgresql_adapter/extension_methods.rb",
43 | "lib/pg_saurus/connection_adapters/postgresql_adapter/foreign_key_methods.rb",
44 | "lib/pg_saurus/connection_adapters/postgresql_adapter/function_methods.rb",
45 | "lib/pg_saurus/connection_adapters/postgresql_adapter/index_methods.rb",
46 | "lib/pg_saurus/connection_adapters/postgresql_adapter/schema_methods.rb",
47 | "lib/pg_saurus/connection_adapters/postgresql_adapter/translate_exception.rb",
48 | "lib/pg_saurus/connection_adapters/postgresql_adapter/trigger_methods.rb",
49 | "lib/pg_saurus/connection_adapters/postgresql_adapter/view_methods.rb",
50 | "lib/pg_saurus/connection_adapters/table.rb",
51 | "lib/pg_saurus/connection_adapters/table/comment_methods.rb",
52 | "lib/pg_saurus/connection_adapters/table/trigger_methods.rb",
53 | "lib/pg_saurus/connection_adapters/trigger_definition.rb",
54 | "lib/pg_saurus/create_index_concurrently.rb",
55 | "lib/pg_saurus/engine.rb",
56 | "lib/pg_saurus/errors.rb",
57 | "lib/pg_saurus/migration.rb",
58 | "lib/pg_saurus/migration/command_recorder.rb",
59 | "lib/pg_saurus/migration/command_recorder/comment_methods.rb",
60 | "lib/pg_saurus/migration/command_recorder/extension_methods.rb",
61 | "lib/pg_saurus/migration/command_recorder/function_methods.rb",
62 | "lib/pg_saurus/migration/command_recorder/schema_methods.rb",
63 | "lib/pg_saurus/migration/command_recorder/trigger_methods.rb",
64 | "lib/pg_saurus/migration/command_recorder/view_methods.rb",
65 | "lib/pg_saurus/migration/set_role_method.rb",
66 | "lib/pg_saurus/schema_dumper.rb",
67 | "lib/pg_saurus/schema_dumper/comment_methods.rb",
68 | "lib/pg_saurus/schema_dumper/extension_methods.rb",
69 | "lib/pg_saurus/schema_dumper/foreign_key_methods.rb",
70 | "lib/pg_saurus/schema_dumper/function_methods.rb",
71 | "lib/pg_saurus/schema_dumper/schema_methods.rb",
72 | "lib/pg_saurus/schema_dumper/trigger_methods.rb",
73 | "lib/pg_saurus/schema_dumper/view_methods.rb",
74 | "lib/pg_saurus/tools.rb",
75 | "lib/pg_saurus/version.rb",
76 | "lib/tasks/pg_saurus_tasks.rake"
77 | ]
78 | s.homepage = "https://github.com/HornsAndHooves/pg_saurus".freeze
79 | s.licenses = ["MIT".freeze]
80 | s.rubygems_version = "3.5.22".freeze
81 | s.summary = "ActiveRecord extensions for PostgreSQL.".freeze
82 |
83 | s.specification_version = 4
84 |
85 | s.add_runtime_dependency(%q.freeze, [">= 0".freeze])
86 | s.add_runtime_dependency(%q.freeze, [">= 0".freeze])
87 | s.add_runtime_dependency(%q.freeze, ["< 8".freeze])
88 | s.add_runtime_dependency(%q.freeze, ["< 8".freeze])
89 | s.add_runtime_dependency(%q.freeze, ["< 8".freeze])
90 | s.add_runtime_dependency(%q.freeze, ["< 8".freeze])
91 | s.add_development_dependency(%q.freeze, [">= 0".freeze])
92 | s.add_development_dependency(%q.freeze, [">= 0".freeze])
93 | s.add_development_dependency(%q.freeze, [">= 0".freeze])
94 | s.add_development_dependency(%q.freeze, [">= 0".freeze])
95 | s.add_development_dependency(%q.freeze, [">= 0".freeze])
96 | s.add_development_dependency(%q.freeze, [">= 0".freeze])
97 | s.add_development_dependency(%q.freeze, [">= 0".freeze])
98 | end
99 |
100 |
--------------------------------------------------------------------------------
/spec/active_record/pg_saurus_notes.txt:
--------------------------------------------------------------------------------
1 | https://github.com/rails/rails/pull/4956
2 | Per Bruce: Just disable the functionality for Rails 4+
3 |
4 |
5 | schema_dumper_spec
6 |
7 | # verify that the dump includes standard add_index options
8 | @dump.should =~ /add_index "demography\.citizens", \["country_id", "user_id"\].*:unique => true/
9 | # verify that the dump includes pg_saurus add_index options
10 | @dump.should =~ /add_index "demography\.citizens", \["country_id", "user_id"\].*:where => "active"/
11 |
12 |
13 | class AddDemographyCitizensActiveColumn < ActiveRecord::Migration
14 | def change
15 | add_column 'demography.citizens', :active, :boolean, :null => false, :default => false
16 | end
17 | end
18 |
19 |
20 | class AddFunctionalIndexToDemographyCitizens < ActiveRecord::Migration
21 | def change
22 | add_index 'demography.citizens', [:country_id, :user_id], :name => 'index_demography.citizens_on_country_id_and_user_id_and_active', :unique => true, :where => 'active'
23 | end
24 | end
25 |
26 |
27 | select
28 | pgi.schemaname as schema_name,
29 | t.relname as table_name,
30 | i.relname as index_name,
31 | array_to_string(array_agg(a.attname), ', ') as column_names
32 | from
33 | pg_class t,
34 | pg_class i,
35 | pg_index ix,
36 | pg_attribute a,
37 | pg_indexes pgi
38 | where
39 | t.oid = ix.indrelid
40 | and i.oid = ix.indexrelid
41 | and a.attrelid = t.oid
42 | and a.attnum = ANY(ix.indkey)
43 | and t.relkind = 'r'
44 | and pgi.indexname = i.relname
45 | group by
46 | pgi.schemaname,
47 | t.relname,
48 | i.relname
49 | order by
50 | pgi.schemaname,
51 | t.relname,
52 | i.relname;
53 |
54 |
55 |
56 | schema_name | table_name | index_name | column_names
57 | ------------+------------+------------+--------------
58 | demography | test | pk_test | a, b
59 | test2 | test2 | uk_test2 | b, c
60 | test3 | test3 | uk_test3ab | a, b
61 | test3 | test3 | uk_test3b | b
62 | test3 | test3 | uk_test3c | c
63 |
64 |
65 |
66 | Functional awesomeness:
67 |
68 | pg_saurus_dummy_development=# explain select * from demography.citizens where country_id = 1 and user_id = 2 and active = true;
69 | QUERY PLAN
70 | ----------------------------------------------------------------------------------------------------------------------------------
71 | Index Scan using index_demography_citizens_on_country_id_and_user_id_and_active on citizens (cost=0.00..8.27 rows=1 width=1097)
72 | Index Cond: ((country_id = 1) AND (user_id = 2))
73 | (2 rows)
74 |
75 |
76 | pg_saurus_dummy_development=# explain select * from demography.citizens where country_id = 1 and user_id = 2 and active = false;
77 | QUERY PLAN
78 | ----------------------------------------------------------------------------------------------------------
79 | Index Scan using "index_demography.citizens_on_user_id" on citizens (cost=0.00..8.27 rows=1 width=1097)
80 | Index Cond: (user_id = 2)
81 | Filter: ((NOT active) AND (country_id = 1))
82 | (3 rows)
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | select * from information_schema.table_constraints limit 50;
95 | select * from information_schema.key_column_usage limit 50;
96 | select * from information_schema.constraint_column_usage limit 50;
97 |
98 | select * from pg_catalog.pg_indexes where tablename = 'rule_sets' AND indexdef LIKE '%UNIQUE%' limit 50;
99 | # WHERE i.schemaname = c.table_schema AND i.tablename = c.table_name AND indexdef LIKE '%UNIQUE%'
100 |
101 | SELECT c.table_schema, c.table_name, c.table_type
102 | FROM information_schema.tables c
103 | WHERE c.table_schema NOT IN('information_schema', 'pg_catalog') AND c.table_type = 'BASE TABLE'
104 | AND NOT EXISTS(SELECT i.tablename
105 | FROM pg_catalog.pg_indexes i
106 | WHERE i.schemaname = c.table_schema
107 | AND i.tablename = c.table_name AND indexdef LIKE '%UNIQUE%')
108 | AND
109 | NOT EXISTS (SELECT cu.table_name
110 | FROM information_schema.key_column_usage cu
111 | WHERE cu.table_schema = c.table_schema AND
112 | cu.table_name = c.table_name)
113 | ORDER BY c.table_schema, c.table_name;
114 |
115 |
116 |
117 | SELECT relname
118 | FROM pg_class
119 | WHERE oid IN (
120 | SELECT indexrelid
121 | FROM pg_index, pg_class
122 | WHERE pg_class.relname='test2'
123 | AND pg_class.oid=pg_index.indrelid
124 | AND indisunique != 't'
125 | AND indisprimary != 't'
126 | );
127 |
128 |
129 | select
130 | t.relname as table_name,
131 | i.relname as index_name,
132 | array_to_string(array_agg(a.attname), ', ') as column_names
133 | from
134 | pg_class t,
135 | pg_class i,
136 | pg_index ix,
137 | pg_attribute a
138 | where
139 | t.oid = ix.indrelid
140 | and i.oid = ix.indexrelid
141 | and a.attrelid = t.oid
142 | and a.attnum = ANY(ix.indkey)
143 | and t.relkind = 'r'
144 | and t.relname like 'test%'
145 | group by
146 | t.relname,
147 | i.relname
148 | order by
149 | t.relname,
150 | i.relname;
151 |
152 |
153 |
154 |
155 |
156 | Indexes:
157 | "customers_pkey" PRIMARY KEY, btree (id)
158 | "index_customers_on_customer_status_id" btree (customer_status_id)
159 | Foreign-key constraints:
160 | "customers_customer_status_id_fk" FOREIGN KEY (customer_status_id) REFERENCES customer_statuses(id)
161 | "customers_person_name_id_fk" FOREIGN KEY (person_name_id) REFERENCES person_names(id)
162 |
163 |
164 |
165 |
166 |
167 | select
168 | pgi.schemaname as schema_name,
169 | t.relname as table_name,
170 | i.relname as index_name,
171 | array_to_string(array_agg(a.attname), ', ') as column_names
172 | from
173 | pg_class t,
174 | pg_class i,
175 | pg_index ix,
176 | pg_attribute a,
177 | pg_indexes pgi
178 | where
179 | t.oid = ix.indrelid
180 | and i.oid = ix.indexrelid
181 | and a.attrelid = t.oid
182 | and a.attnum = ANY(ix.indkey)
183 | and t.relkind = 'r'
184 | and pgi.indexname = i.relname
185 | group by
186 | pgi.schemaname,
187 | t.relname,
188 | i.relname
189 | order by
190 | pgi.schemaname,
191 | t.relname,
192 | i.relname;
193 |
--------------------------------------------------------------------------------
/spec/active_record/schema_dumper_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveRecord::SchemaDumper do
4 |
5 | describe '.dump' do
6 | before(:all) do
7 | stream = StringIO.new
8 | ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection_pool, stream)
9 | @dump = stream.string
10 | end
11 |
12 | context 'Schemas' do
13 | it 'dumps schemas' do
14 | @dump.should =~ /create_schema_if_not_exists "demography"/
15 | @dump.should =~ /create_schema_if_not_exists "later"/
16 | @dump.should =~ /create_schema_if_not_exists "latest"/
17 | end
18 | it 'dumps schemas in alphabetical order' do
19 | @dump.should =~ /create_schema_if_not_exists "demography".*create_schema_if_not_exists "later".*create_schema_if_not_exists "latest"/m
20 | end
21 | end
22 |
23 | context 'Views' do
24 | it 'dumps views' do
25 | # Space or new line. PostgreSQL 9.3 formats output in a different way
26 | # than the previous versions, so we want to ignore difference between
27 | # spaces and new lines.
28 | s = /(\s|\n)+/
29 |
30 | @dump.should =~
31 | /create_view#{s}"demography.citizens_view",#{s}<<-SQL#{s}
32 | \s?SELECT#{s}id,#{s}\
33 | country_id,#{s}
34 | user_id,#{s}
35 | first_name,#{s}
36 | last_name,#{s}
37 | birthday,#{s}
38 | bio,#{s}
39 | created_at,#{s}
40 | updated_at,#{s}
41 | active#{s}
42 | FROM#{s}demography.citizens;
43 | #{s}SQL/x
44 |
45 |
46 | end
47 | end
48 |
49 | context "Extensions" do
50 | it 'dumps loaded extension modules' do
51 | @dump.should =~ /create_extension "fuzzystrmatch", version: "\d+\.\d+"/
52 | @dump.should =~ /create_extension "btree_gist", schema_name: "demography", version: "\d+\.\d+"/
53 | end
54 | end
55 |
56 | context 'Tables' do
57 | it 'dumps tables' do
58 | @dump.should =~ /create_table "users"/
59 | end
60 |
61 | it 'dumps tables from non-public schemas' do
62 | @dump.should =~ /create_table "demography.citizens"/
63 | end
64 | end
65 |
66 | context 'Indexes' do
67 | it 'dumps indexes' do
68 | # added via standard add_index
69 | @dump.should =~ /t.index \["name"\], name: "index_users_on_name"/
70 | # added via foreign key
71 | @dump.should =~ /t.index \["user_id"\], name: "index_pets_on_user_id"/
72 | # foreign key :exclude_index
73 | @dump.should_not =~ /t.index \["user_id"\], name: "index_demography_citizens_on_user_id"/
74 | # partial index
75 | @dump.should =~ /t.index \["country_id", "user_id"\], name: "index_demography_citizens_on_country_id_and_user_id", unique: true, where: "active", comment: "Unique index on active citizens"/
76 | end
77 |
78 | # This index is added via add_foreign_key
79 | it 'dumps indexes from non-public schemas' do
80 | @dump.should =~ /t.index \["country_id"\], name: "index_demography_cities_on_country_id"/
81 | end
82 |
83 | it 'dumps functional indexes' do
84 | @dump.should =~ /t.index "lower\(name\)", name: "index_pets_on_lower_name"/
85 | end
86 |
87 | it 'dumps partial functional indexes' do
88 | @dump.should =~ /t.index "upper\(color\)", name: "index_pets_on_upper_color", where: "\(name IS NULL\)"/
89 | end
90 |
91 | it 'dumps indexes with non-default access method' do
92 | @dump.should =~ /t.index \["user_id"\], name: "index_pets_on_user_id_gist", using: :gist/
93 | end
94 |
95 | it 'dumps indexes with non-default access method and multiple args' do
96 | @dump.should =~ /t.index "to_tsvector\(\'english\'::regconfig, name\)", name: "index_pets_on_to_tsvector_name_gist", using: :gist, comment: "Functional index on name"/
97 | end
98 |
99 | it "dumps indexes with operator name" do
100 | @dump.should =~ /t.index \["title"\], name: "index_books_on_title_varchar_pattern_ops", opclass: :varchar_pattern_ops/
101 | end
102 |
103 | it "dumps indexes with simple columns with an alternate order" do
104 | @dump.should =~ /t.index \["author_id", "publisher_id"\], name: "books_author_id_and_publisher_id", order: { author_id: :desc, publisher_id: "DESC NULLS LAST" }/
105 | end
106 |
107 | it "dumps functional indexes with longer operator strings" do
108 | @dump.should =~ /t.index "TRIM\(BOTH FROM lower\(name\)\) DESC NULLS LAST", name: "index_pets_on_lower_name_desc_nulls_last"/
109 | end
110 | end
111 |
112 | context 'Functions' do
113 | it 'dumps function definitions' do
114 | @dump.should =~ /create_function 'public.pets_not_empty\(\)'/
115 | end
116 |
117 | it "dumps function definitions returning result sets" do
118 | @dump.should =~ / create_function 'public.select_authors\(\)', 'TABLE\(author_id integer\)'/
119 | end
120 | end
121 |
122 | context 'Triggers' do
123 | it 'dumps trigger definitions' do
124 | @dump.should =~ /create_trigger 'pets', 'pets_not_empty_trigger_proc\(\)', 'AFTER INSERT'/
125 | end
126 | end
127 |
128 | context 'Comments' do
129 | it 'dumps table comments' do
130 | @dump.should =~ /set_table_comment 'users', 'Information about users'/
131 | end
132 |
133 | it 'dumps table comments from non-public schemas' do
134 | @dump.should =~ /set_table_comment 'demography.citizens', 'Citizens Info'/
135 | end
136 |
137 | it 'dumps column comments' do
138 | @dump.should =~ /set_column_comment 'users', 'name', 'User name'/
139 | end
140 |
141 | it 'dumps column comments from non-public schemas' do
142 | @dump.should =~ /set_column_comment 'demography.citizens', 'first_name', 'First name'/
143 | end
144 |
145 | it 'dumps index comments' do
146 | @dump.should =~ /set_index_comment 'index_pets_on_to_tsvector_name_gist', 'Functional index on name'/
147 | end
148 |
149 | it 'dumps index comments from non-public schemas' do
150 | @dump.should =~/set_index_comment 'demography.index_demography_citizens_on_country_id_and_user_id', 'Unique index on active citizens'/
151 | end
152 | end
153 |
154 | end
155 | end
156 |
--------------------------------------------------------------------------------
/spec/indexes_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Indexes' do
4 | describe '#add_index' do
5 | it 'is built with the :where option' do
6 | index_options = { where: "active" }
7 |
8 | ActiveRecord::Migration.add_index(:pets, :name, index_options)
9 |
10 | PgSaurus::Explorer.index_exists?(:pets, :name, index_options).should be true
11 | end
12 |
13 | it 'allows indexes with expressions using functions' do
14 | ActiveRecord::Migration.add_index(:pets, ["lower(name)", "lower(color)"])
15 |
16 | PgSaurus::Explorer.index_exists?(:pets, ["lower(name)", "lower(color)"] ).should be true
17 | end
18 |
19 | it 'allows indexes with expressions using functions with multiple arguments' do
20 | ActiveRecord::Migration.add_index(:pets, "to_tsvector('english', name)", using: 'gin')
21 |
22 | PgSaurus::Explorer.index_exists?(:pets, "gin(to_tsvector('english', name))" ).should be true
23 | end
24 |
25 | it 'allows indexes with expressions using functions with multiple arguments as dumped' do
26 | ActiveRecord::Migration.add_index(:pets,
27 | "to_tsvector('english'::regconfig, name)",
28 | using: 'gin')
29 |
30 | PgSaurus::Explorer.index_exists?(:pets, "gin(to_tsvector('english', name))" ).should be true
31 | end
32 |
33 | # TODO support this canonical example
34 | it 'allows indexes with advanced expressions' do
35 | pending "Not sophisticated enough for this yet"
36 | ActiveRecord::Migration.add_index(:pets, ["(color || ' ' || name)"])
37 |
38 | PgSaurus::Explorer.index_exists?(:pets, ["(color || ' ' || name)"] ).should be true
39 | end
40 |
41 | it "allows partial indexes with expressions" do
42 | opts = { where: 'color IS NULL' }
43 |
44 | ActiveRecord::Migration.add_index(:pets, ['upper(name)', 'lower(color)'], opts)
45 | PgSaurus::Explorer.index_exists?(:pets, ['upper(name)', 'lower(color)'], opts).should be true
46 | end
47 |
48 | it "allows compound functional indexes for schema-qualified table names" do
49 | opts = { name: 'idx_demography_citizens_on_lower_last_name_lower_first_name' }
50 | args = [ "demography.citizens", ["lower(last_name)", "lower(first_name)"], opts ]
51 |
52 | ActiveRecord::Migration.add_index(*args)
53 | expect(PgSaurus::Explorer.index_exists?(*args)).to be_truthy
54 | end
55 | end
56 |
57 | describe '#remove_index' do
58 | it 'removes indexes with expressions using functions' do
59 | ActiveRecord::Migration.add_index(:pets, ["lower(name)", "lower(color)"])
60 | ActiveRecord::Migration.remove_index(:pets, ["lower(name)", "lower(color)"])
61 |
62 | PgSaurus::Explorer.index_exists?(:pets, ["lower(name)", "lower(color)"] ).should be false
63 | end
64 |
65 | it 'removes indexes built with the :where option' do
66 |
67 | index_options = { where: "active" }
68 |
69 | ActiveRecord::Migration.add_index(:pets, :name, index_options)
70 | ActiveRecord::Migration.remove_index(:pets, :name)
71 |
72 | PgSaurus::Explorer.index_exists?(:pets, :name, index_options).should be false
73 | end
74 | end
75 |
76 | describe '#index_exists' do
77 | it 'is true for simple options' do
78 | PgSaurus::Explorer.index_exists?('pets', :color).should be true
79 | end
80 |
81 | it 'supports table name as a symbol' do
82 | PgSaurus::Explorer.index_exists?(:pets, :color).should be true
83 | end
84 |
85 | it 'is true for simple options on a schema table' do
86 | PgSaurus::Explorer.index_exists?('demography.cities', :country_id).should be true
87 | end
88 |
89 | it 'is true for a valid set of options' do
90 | index_options = { unique: true, where: 'active'}
91 | PgSaurus::Explorer.index_exists?('demography.citizens',
92 | [:country_id, :user_id],
93 | index_options
94 | ).should be true
95 | end
96 |
97 | it 'is true for a valid set of options including name' do
98 | index_options = { unique: true,
99 | where: 'active',
100 | name: 'index_demography_citizens_on_country_id_and_user_id' }
101 | PgSaurus::Explorer.index_exists?('demography.citizens',
102 | [:country_id, :user_id],
103 | index_options
104 | ).should be true
105 | end
106 |
107 | it 'is false for a subset of valid options' do
108 | index_options = { where: 'active' }
109 | PgSaurus::Explorer.index_exists?('demography.citizens',
110 | [:country_id, :user_id],
111 | index_options
112 | ).should be false
113 | end
114 |
115 | it 'is false for invalid options' do
116 | index_options = { where: 'active' }
117 | PgSaurus::Explorer.index_exists?('demography.citizens',
118 | [:country_id],
119 | index_options
120 | ).should be false
121 | end
122 |
123 | it 'is true for a :where clause that includes boolean comparison' do
124 | index_options = { where: 'active' }
125 | ActiveRecord::Migration.add_index(:pets, :name, index_options)
126 | PgSaurus::Explorer.index_exists?(:pets, :name, index_options).should be true
127 | end
128 |
129 | it 'is true for a :where clause that includes text comparison' do
130 | index_options = { where: "color = 'black'" }
131 | ActiveRecord::Migration.add_index(:pets, :name, index_options)
132 | PgSaurus::Explorer.index_exists?(:pets, :name, index_options).should be true
133 | end
134 |
135 | it 'is true for a :where clause that includes NULL comparison' do
136 | index_options = { where: 'color IS NULL' }
137 | ActiveRecord::Migration.add_index(:pets, :name, index_options)
138 | PgSaurus::Explorer.index_exists?(:pets, :name, index_options).should be true
139 | end
140 |
141 | it 'is true for a :where clause that includes integer comparison' do
142 | index_options = { where: 'id = 4' }
143 | ActiveRecord::Migration.add_index(:pets, :name, index_options)
144 | PgSaurus::Explorer.index_exists?(:pets, :name, index_options).should be true
145 | end
146 |
147 | it 'is true for a compound :where clause' do
148 | index_options = { where: "id = 4 and color = 'black' and active" }
149 | ActiveRecord::Migration.add_index(:pets, :name, index_options)
150 | PgSaurus::Explorer.index_exists?(:pets, :name, index_options).should be true
151 | end
152 |
153 | it 'is true for concurrently created index' do
154 | index_options = { concurrently: true }
155 | PgSaurus::Explorer.index_exists?(:users, :email, index_options).should be true
156 | end
157 |
158 | end
159 | end
160 |
--------------------------------------------------------------------------------
/lib/pg_saurus/create_index_concurrently.rb:
--------------------------------------------------------------------------------
1 | # Adds ability to configure in migration how index will be created.
2 | # See more details how to create index concurrently in PostgreSQL at
3 | # (see http://www.postgresql.org/docs/9.2/static/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY).
4 | #
5 | # There are several things you should be aware when use option to create index
6 | # concurrently.
7 | # Index can not be created concurrently inside transaction and such indexes
8 | # creation will be postponed till migration transaction will be closed.
9 | # In case of migration failure and transaction was rolled back indexes will not
10 | # be created concurrently. But if indexes which should be created concurrently
11 | # run with errors migration's transaction won't be rolled back. Error in that
12 | # case will be raised and migration process will be stoped.
13 | #
14 | # Migrations can not ensure that all indexes that tend to be created
15 | # concurrently were created even if the query for such index creation run
16 | # without errors. Such indexes creation are deferred because of its nature.
17 | # So, it's up to you to ensure that indexes was really created or remove
18 | # partially created invalid indexes.
19 | #
20 | # :concurrent_index option conflicts with :exclude_index option in method
21 | # `add_foreign_key`. So, if you put them together exception will be raised.
22 | #
23 | # @example
24 | #
25 | # class AddIndexToNameForUsers < ActiveRecord::Migration
26 | # def change
27 | # add_index :users, :name, :concurrently => true
28 | # end
29 | # end
30 | #
31 | # # or with foreign key
32 | #
33 | # class AddForeignKeyToRoleIdForUsers < ActiveRecord::Migration
34 | # def change
35 | # add_foreign_key :users, :roles, :concurrent_index => true
36 | # end
37 | # end
38 | #
39 | module PgSaurus::CreateIndexConcurrently
40 | # Provides ability to postpone index creation queries in migrations.
41 | #
42 | # Overrides `add_index` and `add_foreign_key` methods for migration to be
43 | # able to prevent indexes creation inside scope of transaction if they have to
44 | # be created concurrently.
45 | # Allows to run creation of postponed indexes.
46 | #
47 | # This module included into ActiveRecord::Migration class to extend it with
48 | # new features.
49 | #
50 | # All postponed index creation queries are stored inside migration instance.
51 | module Migration
52 | # @attribute postponed_queries
53 | # @return [Array] list of arguments to call `add_index` method.
54 | # @private
55 | attr_accessor :postponed_queries
56 | private :postponed_queries, :postponed_queries=
57 |
58 |
59 | # Add a new index to the table. +column_name+ can be a single Symbol, or
60 | # an Array of Symbols.
61 | #
62 | # @param [Symbol, String] table_name
63 | # @param [Symbol, String, Array] column_name
64 | # @param [optional, Hash] options
65 | # @option options [Boolean] :unique
66 | # @option options [Boolean] :concurrently
67 | # @option options [String] :where
68 | #
69 | # @return [nil]
70 | #
71 | # @see ActiveRecord::ConnectionAdapters::SchemaStatements.add_index in pg_saurus gem
72 | def add_index(table_name, column_name, options = {}, &block)
73 | table_name = proper_table_name(table_name)
74 | # GOTCHA:
75 | # checks if index should be created concurrently then put it into
76 | # the queue to wait till queue processing will be called (should be
77 | # happended after closing transaction).
78 | # Otherwise just delegate call to PgSaurus's `add_index`.
79 | # Block is given for future compatibility.
80 | # -- zekefast 2012-09-12
81 | unless options[:concurrently]
82 | return connection.add_index(table_name, column_name, options, &block)
83 | end
84 |
85 | enque(table_name, column_name, options, &block)
86 | nil
87 | end
88 |
89 | # Execute all postponed index creation.
90 | #
91 | # @return [::PgSaurus::CreateIndexConcurrently::Migration]
92 | def process_postponed_queries
93 | Array(@postponed_queries).each do |arguments, block|
94 | connection.add_index(*arguments, &block)
95 | end
96 |
97 | clear_queue
98 |
99 | self
100 | end
101 |
102 | # Clean postponed queries queue.
103 | #
104 | # @return [::PgSaurus::CreateIndexConcurrently::Migration] migration
105 | def clear_queue
106 | @postponed_queries = []
107 |
108 | self
109 | end
110 | private :clear_queue
111 |
112 | # Add to the queue add_index call parameters to be able execute call later.
113 | #
114 | # @param [Array] arguments
115 | # @param [Proc] block
116 | #
117 | # @return [::PgSaurus::CreateIndexConcurrently::Migration]
118 | def enque(*arguments, &block)
119 | @postponed_queries ||= []
120 | @postponed_queries << [arguments, block]
121 |
122 | self
123 | end
124 | private :enque
125 | end
126 |
127 | # Allows `process_postponed_queries` to be called on MigrationProxy instances.
128 | # So, (see ::PgSaurus::CreateIndexConcurrently::Migrator) could run index
129 | # creation concurrently.
130 | #
131 | # Default delegation in (see ActiveRecord::MigrationProxy) allows to call
132 | # only several methods.
133 | module MigrationProxy
134 | # :nodoc:
135 | def self.included(klass)
136 | klass.delegate :process_postponed_queries, to: :migration
137 | end
138 | end
139 |
140 | # Run postponed index creation for each migration.
141 | #
142 | # This module included into (see ::ActiveRecord::Migrator) class to make possible
143 | # to execute queries for postponed index creation after closing migration's
144 | # transaction.
145 | #
146 | # @see ::ActiveRecord::Migrator.migrate
147 | # @see ::ActiveRecord::Migrator.ddl_transaction
148 | module Migrator
149 | # Override (see ::ActiveRecord::Migrator.ddl_transaction) to call
150 | # (see ::PgSaurus::CreateIndexConcurrently::Migration.process_postponed_queries)
151 | # immediately after transaction.
152 | #
153 | # @see ::ActiveRecord::Migrator.ddl_transaction
154 | def ddl_transaction(*args, &block)
155 | super(*args, &block)
156 |
157 | # GOTCHA:
158 | # This might be a bit tricky, but I've decided that this is the best
159 | # way to retrieve migration instance after closing transaction.
160 | # The problem that (see ::ActiveRecord::Migrator) doesn't provide any
161 | # access to recently launched migration. All logic to iterate through
162 | # set of migrations incapsulated in (see ::ActiveRecord::Migrator.migrate)
163 | # method.
164 | # So, to get access to migration you need to override `migrate` method
165 | # and duplicated all logic inside it, plus add call to
166 | # `process_postponed_queries`.
167 | # I've decided this is less forward compatible then retrieving
168 | # value of `migration` variable in context where block
169 | # given to `ddl_transaction` method was created.
170 | # -- zekefast 2012-09-12
171 | migration = block.binding.eval('migration')
172 | migration.process_postponed_queries
173 | end
174 | end
175 | end
176 |
--------------------------------------------------------------------------------