Skip to content

Commit

Permalink
Dump types to generate a correct schema.rb
Browse files Browse the repository at this point in the history
  • Loading branch information
Carlos Silva committed Dec 8, 2016
1 parent 75cfb15 commit 1543821
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 69 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
.bundle
.config
.yardoc
.byebug_history
Gemfile.lock
coverage
doc/
Expand Down
3 changes: 1 addition & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ Following the PostgreSQL features list on [this page](https://www.postgresql.org

- [ ] Arel windows functions [DOCS](https://www.postgresql.org/docs/9.3/static/functions-window.html)
- [ ] Replace the *postgres_ext* gem
- [ ] Sequence control on migartion
- [ ] Domain manager [DOCS](https://www.postgresql.org/docs/9.2/static/extend-type-system.html#AEN27940)
- [ ] 'Compound' type manager [DOCS](https://www.postgresql.org/docs/9.2/static/sql-createtype.html)
- [ ] 'Enum' type manager [DOCS](https://www.postgresql.org/docs/9.2/static/sql-createtype.html)
- [x] 'Enum' type manager [DOCS](https://www.postgresql.org/docs/9.2/static/sql-createtype.html)
1 change: 1 addition & 0 deletions lib/torque/postgresql/migration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ def quote_type_name(name)
end
end

require 'torque/postgresql/migration/types'
require 'torque/postgresql/migration/enum'
18 changes: 0 additions & 18 deletions lib/torque/postgresql/migration/enum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,29 +47,11 @@ def invert_create_enum(args, &block)
[:drop_type, args, block]
end

# Drops a type.
def drop_type(name)
execute "DROP TYPE IF EXISTS #{quote_type_name(name)}"
end

# Returns all values that an enum type can have.
def enum_values(name)
select_values("SELECT unnest(enum_range(NULL::#{name}))")
end

# Returns true if type exists.
def type_exists?(name)
select_value("SELECT 1 FROM pg_type WHERE typname = '#{name}'").to_i > 0
end

# Renames a type.
def rename_type(type_name, new_name)
execute <<-SQL
ALTER TYPE #{quote_type_name(type_name)}
RENAME TO #{quote_type_name(new_name)}
SQL
end

private

def quote_enum_values(name, values, options)
Expand Down
95 changes: 95 additions & 0 deletions lib/torque/postgresql/migration/types.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
module Torque
module Postgresql
module Migration

module TypesStatements
# Gets a list of user defined types.
# You can even choose the +typcategory+ filter
def user_defined_types(category = nil)
category_condition = "AND typcategory = '#{category}'" unless category.nil?
select_all(<<-SQL).rows.to_h
SELECT t.typname AS name,
CASE t.typcategory
WHEN 'E' THEN 'enum'
END AS type
FROM pg_type t
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
#{category_condition}
AND NOT EXISTS(
SELECT 1 FROM pg_catalog.pg_type el
WHERE el.oid = t.typelem AND el.typarray = t.oid
)
AND (t.typrelid = 0 OR (
SELECT c.relkind = 'c' FROM pg_catalog.pg_class c
WHERE c.oid = t.typrelid
))
SQL
end

# Check if a given type is valid.
def valid_type?(type)
super || user_defined_types.key?(type.to_s)
end

# Returns true if type exists.
def type_exists?(name)
select_value("SELECT 1 FROM pg_type WHERE typname = '#{name}'").to_i > 0
end

# Drops a type.
def drop_type(name)
execute "DROP TYPE IF EXISTS #{quote_type_name(name)}"
end

# Renames a type.
def rename_type(type_name, new_name)
execute <<-SQL
ALTER TYPE #{quote_type_name(type_name)}
RENAME TO #{quote_type_name(new_name)}
SQL
end
end

module TypesDumper

def self.included(base)
base.class_eval do
def dump(stream)
header(stream)
extensions(stream)
user_defined_types(stream)
tables(stream)
trailer(stream)
stream
end
end
end

private

def user_defined_types(stream)
types = @connection.user_defined_types
return unless types.any?

stream.puts " # These are user defined custom column types used on this database"
@connection.user_defined_types.each do |name, type|
send(type.to_sym, name, stream)
end
stream.puts
end

def enum(name, stream)
values = @connection.enum_values(name).map { |v| "\"#{v}\"" }
stream.puts " create_enum :#{name}, [#{values.join(', ')}]"
end

end

Adapter.send :include, TypesStatements

ActiveRecord::SchemaDumper.send :include, TypesDumper

end
end
end
2 changes: 1 addition & 1 deletion lib/torque/postgresql/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Torque
module Postgresql
VERSION = '0.0.1'
VERSION = '0.0.2'
end
end
115 changes: 67 additions & 48 deletions spec/lib/migration/enum_spec.rb
Original file line number Diff line number Diff line change
@@ -1,67 +1,86 @@
require 'spec_helper'

RSpec.describe 'Enum', 'on migration' do
RSpec.describe 'Enum' do
before(:each) { ActiveRecord::Base.connection.drop_type(:status) }
after(:all) { ActiveRecord::Base.connection.drop_type(:status) }

let(:migration) { ActiveRecord::Base.connection }
let(:connection) { ActiveRecord::Base.connection }

it 'can be created' do
migration.create_enum(:status, %i(foo bar))
expect(migration.type_exists?(:status)).to be_truthy
expect(migration.enum_values(:status)).to eql(['foo', 'bar'])
end
context 'on migration' do
it 'can be created' do
connection.create_enum(:status, %i(foo bar))
expect(connection.type_exists?(:status)).to be_truthy
expect(connection.enum_values(:status)).to eql(['foo', 'bar'])
end

it 'can be deleted' do
migration.create_enum(:status, %i(foo bar))
expect(migration.type_exists?(:status)).to be_truthy
it 'can be deleted' do
connection.create_enum(:status, %i(foo bar))
expect(connection.type_exists?(:status)).to be_truthy

migration.drop_type(:status)
expect(migration.type_exists?(:status)).to be_falsey
end
connection.drop_type(:status)
expect(connection.type_exists?(:status)).to be_falsey
end

it 'can be renamed' do
migration.create_enum(:status, %i(foo bar))
migration.rename_type(:status, :new_status)
expect(migration.type_exists?(:new_status)).to be_truthy
expect(migration.type_exists?(:status)).to be_falsey
end
it 'can be renamed' do
connection.create_enum(:status, %i(foo bar))
connection.rename_type(:status, :new_status)
expect(connection.type_exists?(:new_status)).to be_truthy
expect(connection.type_exists?(:status)).to be_falsey
end

it 'can have prefix' do
migration.create_enum(:status, %i(foo bar), prefix: true)
expect(migration.enum_values(:status)).to eql(['status_foo', 'status_bar'])
end
it 'can have prefix' do
connection.create_enum(:status, %i(foo bar), prefix: true)
expect(connection.enum_values(:status)).to eql(['status_foo', 'status_bar'])
end

it 'can have suffix' do
migration.create_enum(:status, %i(foo bar), suffix: 'tst')
expect(migration.enum_values(:status)).to eql(['foo_tst', 'bar_tst'])
end
it 'can have suffix' do
connection.create_enum(:status, %i(foo bar), suffix: 'tst')
expect(connection.enum_values(:status)).to eql(['foo_tst', 'bar_tst'])
end

it 'inserts values at the end' do
migration.create_enum(:status, %i(foo bar))
migration.add_enum_values(:status, %i(baz qux))
expect(migration.enum_values(:status)).to eql(['foo', 'bar', 'baz', 'qux'])
end
it 'inserts values at the end' do
connection.create_enum(:status, %i(foo bar))
connection.add_enum_values(:status, %i(baz qux))
expect(connection.enum_values(:status)).to eql(['foo', 'bar', 'baz', 'qux'])
end

it 'inserts values in the beginning' do
migration.create_enum(:status, %i(foo bar))
migration.add_enum_values(:status, %i(baz qux), prepend: true)
expect(migration.enum_values(:status)).to eql(['baz', 'qux', 'foo', 'bar'])
end
it 'inserts values in the beginning' do
connection.create_enum(:status, %i(foo bar))
connection.add_enum_values(:status, %i(baz qux), prepend: true)
expect(connection.enum_values(:status)).to eql(['baz', 'qux', 'foo', 'bar'])
end

it 'inserts values in the middle' do
migration.create_enum(:status, %i(foo bar))
migration.add_enum_values(:status, %i(baz), after: 'foo')
expect(migration.enum_values(:status)).to eql(['foo', 'baz', 'bar'])
it 'inserts values in the middle' do
connection.create_enum(:status, %i(foo bar))
connection.add_enum_values(:status, %i(baz), after: 'foo')
expect(connection.enum_values(:status)).to eql(['foo', 'baz', 'bar'])

migration.add_enum_values(:status, %i(qux), before: 'bar')
expect(migration.enum_values(:status)).to eql(['foo', 'baz', 'qux', 'bar'])
connection.add_enum_values(:status, %i(qux), before: 'bar')
expect(connection.enum_values(:status)).to eql(['foo', 'baz', 'qux', 'bar'])
end

it 'inserts values with prefix or suffix' do
connection.create_enum(:status, %i(foo bar))
connection.add_enum_values(:status, %i(baz), prefix: true)
connection.add_enum_values(:status, %i(qux), suffix: 'tst')
expect(connection.enum_values(:status)).to eql(['foo', 'bar', 'status_baz', 'qux_tst'])
end
end

it 'inserts values with prefix or suffix' do
migration.create_enum(:status, %i(foo bar))
migration.add_enum_values(:status, %i(baz), prefix: true)
migration.add_enum_values(:status, %i(qux), suffix: 'tst')
expect(migration.enum_values(:status)).to eql(['foo', 'bar', 'status_baz', 'qux_tst'])
context 'on schema' do
it 'dumps when has it' do
connection.create_enum(:status, %i(foo bar))

dumo_io = StringIO.new
ActiveRecord::SchemaDumper.dump(connection, dumo_io)
expect(dumo_io.string).to match /create_enum :status, \["foo", "bar"\]/
end
it 'doesn\'t dump when has none' do
connection.user_defined_types.map(&connection.method(:drop_type))

dumo_io = StringIO.new
ActiveRecord::SchemaDumper.dump(connection, dumo_io)
expect(dumo_io.string).not_to match /create_enum :\w+, \[[^\]]+\]/
end
end
end

0 comments on commit 1543821

Please sign in to comment.