diff --git a/README.md b/README.md index 5a04bfa..0ee0530 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ [![Gem Version](https://badge.fury.io/rb/schema_plus_multischema.svg)](http://badge.fury.io/rb/schema_plus_multischema) [![Build Status](https://secure.travis-ci.org/SchemaPlus/schema_plus_multischema.svg)](http://travis-ci.org/SchemaPlus/schema_plus_multischema) [![Coverage Status](https://img.shields.io/coveralls/SchemaPlus/schema_plus_multischema.svg)](https://coveralls.io/r/SchemaPlus/schema_plus_multischema) -[![Dependency Status](https://gemnasium.com/lomba/schema_plus_multischema.svg)](https://gemnasium.com/SchemaPlus/schema_plus_multischema) + # SchemaPlusMultischema -Schema plus multischema is a schema plus extension for postgresql that adds support for multiple schemas for schema dumps. +SchemaPlusMultischema adds support for using multiple schemas with ActiveRecord. The new features are currently: + +* `ActiveRecord::Connection#tables` returns each table name in the form `"schema.table"` if there if the current search path isn't the DBMS default. + +* Schema dump includes the schema definitions and search path and creates each table in its appropriate schema, if the current search path isn't the DBMS default. + +Support is currently limited to Postgresql. See details below. SchemaPlusMultischema is part of the [SchemaPlus](https://github.com/SchemaPlus/) family of Ruby on Rails ActiveRecord extension gems. @@ -32,43 +38,50 @@ SchemaPlusMultischema is tested on: -## Usage +SchemaPlusMultischema should be a no-op if used with sqlite3 or mysql. -Using schema plus multiple schemas is easy - simply require it before you do a schema dump. If everything works, then the schema dump should include schema names before table names +## Background -For example, lets say we have a table ```wallets``` in schema ```private``` and table ```users``` in schema ```public```. Each user has 1 wallet. Without this gem, the schemadump would look like this: +Your PostgreSQL database might have multiple schemas, that provide namespaces for tables. For example, you could define: -``` -create_table 'wallets' do -end + connection.execute "CREATE SCHEMA first" + connection.execute "CREATE SCHEMA second" -create_table 'users' do |t| - t.integer :wallet_id -end -``` +ActiveRecord's PostgreSQL connection adapter lets you set PostgreSQL's search_path to choose which schemas to look at: -With this gem, the schemadump will look like this: + connection.schema_search_path = "first,second" + +And ActiveRecord let you use schema names in migrations, such as -``` -create_table 'private.wallets' do -end + create_table 'first.my_table' do ... + create_table 'second.my_table' do ... + +But without SchemaPlusMultishema, ActiveRecord's introspection doesn't handle them properly. It *does* find all tables that are in the current search path, but it *doesn't* prefix them with their schema names. And the schema dump `schema/dump.rb` doesn't take into account the schemas at all, so it's invalid. -create_table 'public.users' do |t| - t.integer :wallet_id, null: false -end +## Features -``` +With SchemaPlusMultischema installed, the following features are automatically in place. -The schema plus multischema also works with schema plus foreign keys. If schema plus foreign keys is enabled, the output will look like this: +### `connection.tables` -``` -create_table 'private.wallets' do -end +SchemaPlusMultischema modifies the output of ActiveRecord's `connection.tables` method. If the current search path is different from PostgreSQL's default (`"$user",public`), then each table name is prefixed with its schema. E.g. -create_table 'public.users' do |t| - t.integer :wallet_id, null: false, foreign_key: {references: "private.wallets", name: "fk_public_users_wallet_id", on_update: :no_action, on_delete: :cascade} -end -``` + connection.tables # => ["first.my_table", "second.my_table"] + +If the current search path is the same as the default, the table names are not prefixed, i.e. the behavior is the same as pure ActiveRecord. + +### Schema dump + +If the current search path isn't the default, the schema dump (`db/schema.rb`) will include the schema setup, and the table definitions will be prefixed with their schemas. E.g. + + connection.execute "CREATE SCHEMA IF NOT EXISTS first" + connection.execute "CREATE SCHEMA IF NOT EXISTS second" + connection.schema_search_path = "first,second" + + create_table "first.my_table" do ... + create_table "second.my_table" do ... + +If the current search path is the same as the default, no schema_setup is included and the table names are not prefixed, i.e. the behavior is the same as pure ActiveRecord. ## History diff --git a/lib/schema_plus_multischema.rb b/lib/schema_plus_multischema.rb index 3aa9b04..0d7faed 100644 --- a/lib/schema_plus_multischema.rb +++ b/lib/schema_plus_multischema.rb @@ -1,11 +1,6 @@ require 'schema_plus/core' require_relative 'schema_plus_multischema/version' -require_relative 'schema_plus_multischema/active_record/connection_adapters/postgresql_adapter' - -module SchemaPlusMultischema - module ActiveRecord - end -end +require_relative 'schema_plus_multischema/middleware' SchemaMonkey.register SchemaPlusMultischema diff --git a/lib/schema_plus_multischema/active_record/connection_adapters/postgresql_adapter.rb b/lib/schema_plus_multischema/active_record/connection_adapters/postgresql_adapter.rb deleted file mode 100644 index 9cc06a6..0000000 --- a/lib/schema_plus_multischema/active_record/connection_adapters/postgresql_adapter.rb +++ /dev/null @@ -1,30 +0,0 @@ -module SchemaPlusMultischema - module ActiveRecord - module ConnectionAdapters - module PostgresqlAdapter - - # Returns the list of all tables in the schema search path or a specified schema. - def tables(name = nil) - select_tablenames_with_schemas <<-SQL - SELECT schemaname, tablename - FROM pg_tables - WHERE schemaname = ANY(current_schemas(false)) - SQL - end - - def select_tablenames_with_schemas(arel) - arel, binds = binds_from_relation arel, [] - sql = to_sql(arel, binds) - execute_and_clear(sql, 'SCHEMA', binds) do |result| - if result.nfields > 0 - rows = result.column_values(0).count - (0..(rows - 1)).map{ |i| "#{result.column_values(0)[i]}.#{result.column_values(1)[i]}" } - else - [] - end - end - end - end - end - end -end diff --git a/lib/schema_plus_multischema/middleware.rb b/lib/schema_plus_multischema/middleware.rb new file mode 100644 index 0000000..253ba82 --- /dev/null +++ b/lib/schema_plus_multischema/middleware.rb @@ -0,0 +1,45 @@ +module SchemaPlusMultischema + module Middleware + + module PostgreSQL + DEFAULT_SCHEMA_SEARCH_PATH = %q{"$user",public} + + module Schema + module Tables + + def implement(env) + use_prefix = (env.connection.schema_search_path != DEFAULT_SCHEMA_SEARCH_PATH) + query = <<-SQL + SELECT schemaname, tablename + FROM pg_tables + WHERE schemaname = ANY(current_schemas(false)) + SQL + env.tables += env.connection.exec_query(query, 'SCHEMA').map { |row| + if use_prefix + "#{row['schemaname']}.#{row['tablename']}" + else + row['tablename'] + end + } + end + + end + end + + module Dumper + module Initial + + def after(env) + if (path = env.connection.schema_search_path) != DEFAULT_SCHEMA_SEARCH_PATH + path.split(',').each do |name| + env.initial << %Q{ connection.execute "CREATE SCHEMA IF NOT EXISTS #{name}"} + end + env.initial << %Q{ connection.schema_search_path = #{path.inspect}} + end + end + + end + end + end + end +end diff --git a/schema_multiple_schemas.gemspec b/schema_multiple_schemas.gemspec index 857639a..00c0f1d 100644 --- a/schema_multiple_schemas.gemspec +++ b/schema_multiple_schemas.gemspec @@ -26,4 +26,5 @@ Gem::Specification.new do |gem| gem.add_development_dependency "schema_dev", "~> 3.5", ">= 3.5.1" gem.add_development_dependency "simplecov" gem.add_development_dependency "simplecov-gem-profile" + gem.add_development_dependency "schema_plus_foreign_keys" end diff --git a/spec/schema_dumper_spec.rb b/spec/schema_dumper_spec.rb index 021f361..4463535 100644 --- a/spec/schema_dumper_spec.rb +++ b/spec/schema_dumper_spec.rb @@ -1,72 +1,75 @@ require 'spec_helper' describe 'Schema dump' do - before(:each) do - ActiveRecord::Migration.suppress_messages do + let(:connection) { ActiveRecord::Base.connection } - ActiveRecord::Schema.define do - connection.schema_search_path='first,second' - connection.tables.each do |table| drop_table table, force: :cascade end + context "with multiple schemas" do - execute <<-SQL - CREATE SCHEMA IF NOT EXISTS first; - CREATE TABLE first.dogs - ( - id INTEGER PRIMARY KEY - ); - SQL + around(:each) do |example| + with_schemas %w[first second] do + example.run + end + end + + it "includes the schema definitions and path in the dump" do + expect(dump_schema).to include('CREATE SCHEMA IF NOT EXISTS first') + expect(dump_schema).to include('CREATE SCHEMA IF NOT EXISTS second') + expect(dump_schema).to include('schema_search_path = "first,second"') + end + + context 'with a table that is created without a schema prefix' do + before(:each) do + schema_definitions do + create_table 'no_schema_prefix' + end + end + it 'includes schema prefix in dump' do + expect(dump_schema).to include('create_table "first.no_schema_prefix"') + end + end - execute <<-SQL - CREATE SCHEMA IF NOT EXISTS second; - CREATE TABLE second.dogs - ( - id INTEGER PRIMARY KEY - ); - SQL + context 'tables with same name in different schemas' do + before(:each) do + schema_definitions do + create_table 'first.dogs' + create_table 'second.dogs' + end + end - execute <<-SQL - CREATE SCHEMA IF NOT EXISTS second; - CREATE TABLE first.owners - ( - id INTEGER PRIMARY KEY, - dog_id INTEGER NOT NULL - ); - CREATE INDEX fk__first_owners_second_dogs ON first.owners USING btree (dog_id); + it 'includes both tables with schema prefixes' do + expect(dump_schema).to include('create_table "first.dogs"') + expect(dump_schema).to include('create_table "second.dogs"') + end - ALTER TABLE ONLY first.owners - ADD CONSTRAINT fk_first_owners_dog_id FOREIGN KEY (dog_id) REFERENCES second.dogs(id) ON DELETE CASCADE; - SQL + context 'with schema_plus_foreign_keys support' do + before(:each) do + schema_definitions do + create_table 'first.owners' do |t| + t.integer :dog_id, null: false, references: 'second.dogs' + end + end + end - execute <<-SQL - CREATE SCHEMA IF NOT EXISTS second; - CREATE TABLE no_schema_prefix - ( - id INTEGER PRIMARY KEY - ); - SQL + it 'includes foreign key references with schema prefixes' do + expect(dump_schema).to include('foreign_key: {references: "second.dogs", name: "fk_first_owners_dog_id"') + end end end end - def dump_schema(opts={}) - stream = StringIO.new - ActiveRecord::SchemaDumper.ignore_tables = Array.wrap(opts[:ignore]) || [] - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) - stream.string + context "without multiple schemas" do + it "does not include schema setup" do + expect(dump_schema).not_to include('CREATE SCHEMA') + expect(dump_schema).not_to include('schema_search_path') + end end - it 'should dump tables which are created without schema prefix' do - expect(dump_schema).to include('create_table "first.no_schema_prefix"') - end + private - it 'should dump tables with same names from different schemas' do - expect(dump_schema).to include('create_table "first.dogs"') - expect(dump_schema).to include('create_table "second.dogs"') + def dump_schema + stream = StringIO.new + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + stream.string end - context 'when foreign key schema plus gem required' do - it 'should dump foreign key references with schema names' do - expect(dump_schema).to include('foreign_key: {references: "second.dogs", name: "fk_first_owners_dog_id"') - end - end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ed212ad..8b5964a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,5 @@ require 'simplecov' require 'simplecov-gem-profile' -require 'pry' SimpleCov.start "gem" $LOAD_PATH.unshift(File.dirname(__FILE__)) @@ -18,6 +17,12 @@ RSpec.configure do |config| config.warnings = true + config.around(:each) do |example| + ActiveRecord::Migration.suppress_messages do + example.run + drop_all_tables + end + end end diff --git a/spec/support/schema.rb b/spec/support/schema.rb new file mode 100644 index 0000000..99abd66 --- /dev/null +++ b/spec/support/schema.rb @@ -0,0 +1,29 @@ +def with_schemas(names) + connection = ActiveRecord::Base.connection + begin + previous_schemas = connection.schema_search_path + names.each do |name| + connection.execute "CREATE SCHEMA IF NOT EXISTS #{name}" + end + connection.schema_search_path = names.join(',') + yield + ensure + drop_all_tables + names.each do |name| + connection.execute "DROP SCHEMA IF EXISTS #{name}" + end + connection.schema_search_path = previous_schemas + end +end + +def schema_definitions(&block) + ActiveRecord::Schema.define &block +end + +def drop_all_tables + ActiveRecord::Base.connection.tables.each do |table| + ActiveRecord::Base.connection.drop_table table, force: :cascade + end +end + + diff --git a/spec/tables_spec.rb b/spec/tables_spec.rb new file mode 100644 index 0000000..5f1f975 --- /dev/null +++ b/spec/tables_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe 'tables' do + + let(:connection) { ActiveRecord::Base.connection } + + context "with multiple schemas" do + + around(:each) do |example| + with_schemas %w[first second] do + example.run + end + end + + context 'with a table that is created without a schema prefix' do + before(:each) do + schema_definitions do + create_table 'no_schema_prefix' + end + end + it 'includes schema prefix in table name' do + expect(connection.tables).to eq %w[first.no_schema_prefix] + end + end + + context 'with tables with same name in different schemas' do + before(:each) do + schema_definitions do + create_table 'first.dogs' + create_table 'second.dogs' + end + end + + it 'includes schema prefix in table names' do + expect(connection.tables.sort).to eq %w[first.dogs second.dogs] + end + end + + end + + context "without multiple schemas" do + before(:each) do + schema_definitions do + create_table 'dogs' + end + end + + it 'does not include schema_prefix in table names' do + expect(connection.tables).to eq %w[dogs] + end + end + +end