├── .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 | --------------------------------------------------------------------------------