Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use middleware; include schema definitions in dump; minor cleanups #2

Merged
merged 11 commits into from
Aug 29, 2015
69 changes: 41 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -32,43 +38,50 @@ SchemaPlusMultischema is tested on:

<!-- SCHEMA_DEV: MATRIX - end -->

## 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

Expand Down
7 changes: 1 addition & 6 deletions lib/schema_plus_multischema.rb
Original file line number Diff line number Diff line change
@@ -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

This file was deleted.

45 changes: 45 additions & 0 deletions lib/schema_plus_multischema/middleware.rb
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions schema_multiple_schemas.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
109 changes: 56 additions & 53 deletions spec/schema_dumper_spec.rb
Original file line number Diff line number Diff line change
@@ -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
7 changes: 6 additions & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
require 'simplecov'
require 'simplecov-gem-profile'
require 'pry'
SimpleCov.start "gem"

$LOAD_PATH.unshift(File.dirname(__FILE__))
Expand All @@ -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


Expand Down
29 changes: 29 additions & 0 deletions spec/support/schema.rb
Original file line number Diff line number Diff line change
@@ -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


Loading