schema_plus_views 0.2.3 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,45 +9,86 @@ module SchemaPlus::Views
9
9
  re_view_referent = %r{(?:(?i)FROM|JOIN) \S*\b(\S+)\b}
10
10
  env.connection.views.each do |view_name|
11
11
  next if env.dumper.ignored?(view_name)
12
- view = View.new(name: view_name, definition: env.connection.view_definition(view_name))
12
+ definition, view_type = env.connection.view_full_definition(view_name)
13
+
14
+ indexes = []
15
+
16
+ if view_type == :materialized
17
+ env.connection.indexes(view_name).each do |index|
18
+ indexes << SchemaPlus::Core::SchemaDump::Table::Index.new(
19
+ name: index.name, columns: index.columns, options: view_index_options(index, env.connection)
20
+ )
21
+ end
22
+ end
23
+
24
+ view = View.new(
25
+ name: view_name,
26
+ definition: definition,
27
+ view_type: view_type,
28
+ indexes: indexes
29
+ )
30
+
13
31
  env.dump.tables[view.name] = view
14
32
  env.dump.depends(view.name, view.definition.scan(re_view_referent).flatten)
15
33
  end
16
34
  end
17
35
 
36
+ # Take from ActiveRecord::SchemaDumper#index_parts
37
+ def view_index_options(index, connection)
38
+ options = {}
39
+ options[:unique] = true if index.unique
40
+ options[:length] = index.lengths if index.lengths.present?
41
+ options[:order] = index.orders if index.orders.present?
42
+ options[:opclass] = index.opclasses if index.opclasses.present?
43
+ options[:where] = index.where if index.where
44
+ options[:using] = index.using if !connection.default_index_type?(index)
45
+ options[:type] = index.type if index.type
46
+ options[:comment] = index.comment if index.comment
47
+
48
+ options
49
+ end
50
+
18
51
  # quacks like a SchemaMonkey Dump::Table
19
- class View < KeyStruct[:name, :definition]
52
+ class View < KeyStruct[:name, :definition, :view_type, :indexes]
20
53
  def assemble(stream)
21
- heredelim = "END_VIEW_#{name.upcase}"
22
- stream.puts <<-ENDVIEW
23
- create_view "#{name}", <<-'#{heredelim}', :force => true
24
- #{definition}
25
- #{heredelim}
26
-
54
+ extra_options = ", materialized: true" if view_type == :materialized
55
+ heredelim = "END_VIEW_#{name.upcase}"
56
+ stream.puts <<~ENDVIEW
57
+ create_view "#{name}", <<-'#{heredelim}', :force => true#{extra_options}
58
+ #{definition}
59
+ #{heredelim}
27
60
  ENDVIEW
61
+
62
+ indexes.each do |index|
63
+ stream.write "add_index \"#{name}\", "
64
+ index.assemble(stream)
65
+ stream.puts ""
66
+ end
67
+ stream.puts ""
28
68
  end
29
69
  end
30
70
  end
31
71
  end
32
72
 
33
- module Schema
34
- module Tables
35
-
36
- module Mysql
37
- def after(env)
38
- Tables.filter_out_views(env)
39
- end
40
- end
73
+ #
74
+ # Define new middleware stacks patterned on SchemaPlus::Core's naming
75
+ # for tables
41
76
 
42
- module Sqlite3
43
- def after(env)
44
- Tables.filter_out_views(env)
45
- end
46
- end
77
+ module Schema
78
+ module ViewDefinition
79
+ ENV = [:connection, :view_name, :query_name, :definition, :view_type]
80
+ end
81
+ end
47
82
 
48
- def self.filter_out_views(env)
49
- env.tables -= env.connection.views(env.query_name)
50
- end
83
+ module Migration
84
+ module CreateView
85
+ ENV = [:connection, :view_name, :definition, :options]
86
+ end
87
+ module DropView
88
+ ENV = [:connection, :view_name, :options]
89
+ end
90
+ module RefreshView
91
+ ENV = [:connection, :view_name, :options]
51
92
  end
52
93
  end
53
94
  end
@@ -1,5 +1,5 @@
1
1
  module SchemaPlus
2
2
  module Views
3
- VERSION = "0.2.3"
3
+ VERSION = "0.4.1"
4
4
  end
5
5
  end
data/schema_dev.yml CHANGED
@@ -1,8 +1,11 @@
1
1
  ruby:
2
- - 2.1.5
2
+ - 2.5.9
3
+ - 2.7.3
3
4
  activerecord:
4
- - 4.2
5
+ - 5.2
5
6
  db:
6
7
  - mysql2
7
8
  - sqlite3
8
9
  - postgresql
10
+ dbversions:
11
+ postgresql: ['9.6', '10', '11', '12']
@@ -17,13 +17,13 @@ Gem::Specification.new do |gem|
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
19
 
20
- gem.add_dependency "activerecord", "~> 4.2"
21
- gem.add_dependency "schema_plus_core", "~> 0.1"
20
+ gem.add_dependency "activerecord", "~> 5.2"
21
+ gem.add_dependency "schema_plus_core"
22
22
 
23
- gem.add_development_dependency "bundler", "~> 1.7"
23
+ gem.add_development_dependency "bundler"
24
24
  gem.add_development_dependency "rake", "~> 10.0"
25
25
  gem.add_development_dependency "rspec", "~> 3.0"
26
- gem.add_development_dependency "schema_dev", "~> 3.3"
26
+ gem.add_development_dependency "schema_dev", "~> 3.13.1"
27
27
  gem.add_development_dependency "simplecov"
28
28
  gem.add_development_dependency "simplecov-gem-profile"
29
29
  end
data/spec/dumper_spec.rb CHANGED
@@ -5,8 +5,6 @@ end
5
5
 
6
6
  describe "Dumper" do
7
7
 
8
- let(:schema) { ActiveRecord::Schema }
9
-
10
8
  let(:migration) { ActiveRecord::Migration }
11
9
 
12
10
  let(:connection) { ActiveRecord::Base.connection }
@@ -16,7 +14,6 @@ describe "Dumper" do
16
14
  end
17
15
 
18
16
  it "should include view definitions" do
19
- puts dump
20
17
  expect(dump).to match(view_re("a_ones", /SELECT .*b.*,.*s.* FROM .*items.* WHERE \(?.*a.* = 1\)?/mi))
21
18
  expect(dump).to match(view_re("ab_ones", /SELECT .*s.* FROM .*a_ones.* WHERE \(?.*b.* = 1\)?/mi))
22
19
  end
@@ -40,27 +37,77 @@ describe "Dumper" do
40
37
  # when in the (say) development database, but then uses it to
41
38
  # initialize the test database when testing. this meant that the
42
39
  # test database had views into the development database.
43
- db = connection.respond_to?(:current_database)? connection.current_database : SchemaDev::Rspec.db_configuration[:database]
40
+ db = connection.respond_to?(:current_database) ? connection.current_database : SchemaDev::Rspec.db_configuration[:database]
44
41
  expect(dump).not_to match(%r{#{connection.quote_table_name(db)}[.]})
45
42
  end
46
43
 
44
+ context 'materialized views', postgresql: :only do
45
+ before do
46
+ apply_migration do
47
+ create_view :materialized, Item.select('b, s').where(:a => 1), materialized: true
48
+
49
+ add_index :items, :a, where: 'a = 1'
50
+
51
+ add_index :materialized, :s
52
+ add_index :materialized, :b, unique: true, name: 'index_materialized_unique'
53
+ end
54
+ end
55
+
56
+ it "include the view definitions" do
57
+ expect(dump).to match(view_re("materialized", /SELECT .*b.*,.*s.* FROM .*items.* WHERE \(?.*a.* = 1\)?/mi, ', materialized: true'))
58
+ end
59
+
60
+ it 'includes the index definitions' do
61
+ expect(dump).to match(/index_materialized_on_s/)
62
+ expect(dump).to match(/index_materialized_unique.+unique/)
63
+ end
64
+
65
+ context 'when indexes are function results', postgresql: :only do
66
+ before do
67
+ apply_migration do
68
+ add_index :materialized, 'length(s)', name: 'index_materialized_function'
69
+ end
70
+ end
71
+
72
+ it 'includes the index definition' do
73
+ expect(dump).to match(/length\(.+name.+index_materialized_function/)
74
+ end
75
+ end
76
+
77
+ context 'when index has a where clause', postgresql: :only do
78
+ before do
79
+ apply_migration do
80
+ add_index :materialized, :s, where: 'b = 1', name: 'index_materialized_conditional'
81
+ end
82
+ end
83
+
84
+ it 'includes the index definition' do
85
+ expect(dump).to match(/index_materialized_conditional.+where/)
86
+ end
87
+ end
88
+ end
89
+
47
90
  protected
48
91
 
49
- def view_re(name, re)
92
+ def view_re(name, re, extra_config = '')
50
93
  heredelim = "END_VIEW_#{name.upcase}"
51
- %r{create_view "#{name}", <<-'#{heredelim}', :force => true\n\s*#{re}\s*\n *#{heredelim}$}mi
94
+ %r{create_view "#{name}", <<-'#{heredelim}', :force => true#{extra_config}\n\s*#{re}\s*\n *#{heredelim}$}mi
52
95
  end
53
96
 
54
97
  def define_schema_and_data
55
- connection.views.each do |view| connection.drop_view view end
56
- connection.tables.each do |table| connection.drop_table table, cascade: true end
98
+ connection.views.each do |view|
99
+ connection.drop_view view
100
+ end
101
+ connection.tables.each do |table|
102
+ connection.drop_table table, cascade: true
103
+ end
57
104
 
58
- schema.define do
105
+ apply_migration do
59
106
 
60
107
  create_table :items, :force => true do |t|
61
108
  t.integer :a
62
109
  t.integer :b
63
- t.string :s
110
+ t.string :s
64
111
  end
65
112
 
66
113
  create_view :a_ones, Item.select('b, s').where(:a => 1)
@@ -72,7 +119,7 @@ describe "Dumper" do
72
119
  connection.execute "insert into items (a, b, s) values (2, 2, 'two_two')"
73
120
  end
74
121
 
75
- def dump(opts={})
122
+ def dump(opts = {})
76
123
  StringIO.open { |stream|
77
124
  ActiveRecord::SchemaDumper.ignore_tables = Array.wrap(opts[:ignore_tables])
78
125
  ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
@@ -4,9 +4,6 @@ class Item < ActiveRecord::Base
4
4
  end
5
5
 
6
6
  describe "Introspection" do
7
-
8
- let(:schema) { ActiveRecord::Schema }
9
-
10
7
  let(:migration) { ActiveRecord::Migration }
11
8
 
12
9
  let(:connection) { ActiveRecord::Base.connection }
@@ -17,8 +14,6 @@ describe "Introspection" do
17
14
 
18
15
  it "should list all views" do
19
16
  expect(connection.views.sort).to eq(%W[a_ones ab_ones])
20
- expect(connection.view_definition('a_ones')).to match(%r{^ ?SELECT .*b.*,.*s.* FROM .*items.* WHERE .*a.* = 1}mi)
21
- expect(connection.view_definition('ab_ones')).to match(%r{^ ?SELECT .*s.* FROM .*a_ones.* WHERE .*b.* = 1}mi)
22
17
  end
23
18
 
24
19
  it "should ignore views named pg_*", postgresql: :only do
@@ -30,14 +25,42 @@ describe "Introspection" do
30
25
  end
31
26
  end
32
27
 
28
+ context "when using PostGIS", postgresql: :only do
29
+ before do
30
+ apply_migration do
31
+ SchemaPlus::Views::ActiveRecord::ConnectionAdapters::PostgresqlAdapter::POSTGIS_VIEWS.each do |view|
32
+ create_view view, 'select 1'
33
+ end
34
+ end
35
+ end
36
+
37
+ after do
38
+ apply_migration do
39
+ SchemaPlus::Views::ActiveRecord::ConnectionAdapters::PostgresqlAdapter::POSTGIS_VIEWS.each do |view|
40
+ drop_view view
41
+ end
42
+ end
43
+ end
44
+
45
+ it "should hide views in postgis views" do
46
+ expect(connection.views.sort).to eq(%W[a_ones ab_ones])
47
+ end
48
+ end
49
+
50
+
33
51
  it "should not be listed as a table" do
34
52
  expect(connection.tables).not_to include('a_ones')
35
53
  expect(connection.tables).not_to include('ab_ones')
36
54
  end
37
55
 
38
56
  it "should introspect definition" do
39
- expect(connection.view_definition('a_ones')).to match(%r{^ ?SELECT .*b.*,.*s.* FROM .*items.* WHERE .*a.* = 1}mi)
40
- expect(connection.view_definition('ab_ones')).to match(%r{^ ?SELECT .*s.* FROM .*a_ones.* WHERE .*b.* = 1}mi)
57
+ expect(connection.view_definition('a_ones')).to match(%r{^SELECT .*b.*,.*s.* FROM .*items.* WHERE .*a.* = 1}mi)
58
+ expect(connection.view_definition('ab_ones')).to match(%r{^SELECT .*s.* FROM .*a_ones.* WHERE .*b.* = 1}mi)
59
+ end
60
+
61
+ it 'returns them as view types' do
62
+ expect(connection.view_type('a_ones')).to eq(:view)
63
+ expect(connection.view_type('ab_ones')).to eq(:view)
41
64
  end
42
65
 
43
66
  context "in mysql", :mysql => :only do
@@ -67,13 +90,36 @@ describe "Introspection" do
67
90
  end
68
91
  end
69
92
 
93
+ context 'in postgresql', postgresql: :only do
94
+ context 'for materialized views' do
95
+ around(:each) do |example|
96
+ begin
97
+ migration.drop_view :materialized, materialized: true, if_exists: true
98
+ example.run
99
+ ensure
100
+ migration.drop_view :materialized, materialized: true, if_exists: true
101
+ end
102
+ end
103
+
104
+ it 'returns the definition' do
105
+ migration.create_view :materialized, 'SELECT * FROM items WHERE (a=2)', materialized: true
106
+ expect(connection.view_definition('materialized')).to match(%r{FROM items})
107
+ end
108
+
109
+ it 'returns the type as materialized' do
110
+ migration.create_view :materialized, 'SELECT * FROM items WHERE (a=2)', materialized: true
111
+ expect(connection.view_type('materialized')).to eq(:materialized)
112
+ end
113
+ end
114
+ end
115
+
70
116
  protected
71
117
 
72
118
  def define_schema_and_data
73
119
  connection.views.each do |view| connection.drop_view view end
74
120
  connection.tables.each do |table| connection.drop_table table, cascade: true end
75
121
 
76
- schema.define do
122
+ apply_migration do
77
123
 
78
124
  create_table :items, :force => true do |t|
79
125
  t.integer :a
@@ -0,0 +1,89 @@
1
+ require 'spec_helper'
2
+
3
+ module TestMiddleware
4
+ module Middleware
5
+
6
+ module Schema
7
+ module ViewDefinition
8
+ SPY = []
9
+ def after(env)
10
+ SPY << env.to_hash.except(:connection)
11
+ end
12
+ end
13
+ end
14
+
15
+ module Migration
16
+ module CreateView
17
+ SPY = []
18
+ def after(env)
19
+ SPY << env.to_hash.except(:connection)
20
+ end
21
+ end
22
+ module DropView
23
+ SPY = []
24
+ def after(env)
25
+ SPY << env.to_hash.except(:connection)
26
+ end
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+
33
+ SchemaMonkey.register TestMiddleware
34
+
35
+ context SchemaPlus::Views::Middleware do
36
+
37
+ let(:migration) { ActiveRecord::Migration }
38
+ let(:connection) { ActiveRecord::Base.connection }
39
+
40
+ before(:each) do
41
+ apply_migration do
42
+ create_table :items, force: true do |t|
43
+ t.integer :a
44
+ end
45
+ create_view 'a_view', "select a from items"
46
+ end
47
+ end
48
+
49
+ context TestMiddleware::Middleware::Schema::ViewDefinition do
50
+ it "calls middleware" do
51
+ spied = spy_on {connection.view_definition('a_view', 'qn')}
52
+ expect(spied[:view_name]).to eq('a_view')
53
+ expect(spied[:definition]).to match(%r{SELECT .*a.* FROM .*items.*}mi)
54
+ expect(spied[:query_name]).to eq('qn')
55
+ end
56
+ end
57
+
58
+ context TestMiddleware::Middleware::Migration::CreateView do
59
+ it "calls middleware" do
60
+ expect(spy_on {migration.create_view('newview', 'select a from items', force: true)}).to eq({
61
+ #connection: connection,
62
+ view_name: 'newview',
63
+ definition: 'select a from items',
64
+ options: { force: true }
65
+ })
66
+ end
67
+ end
68
+
69
+ context TestMiddleware::Middleware::Migration::DropView do
70
+ it "calls middleware" do
71
+ expect(spy_on {migration.drop_view('a_items', if_exists: true)}).to eq({
72
+ #connection: connection,
73
+ view_name: 'a_items',
74
+ options: { if_exists: true }
75
+ })
76
+ end
77
+ end
78
+
79
+
80
+ private
81
+
82
+ def spy_on
83
+ spy = described_class.const_get :SPY
84
+ spy.clear
85
+ yield
86
+ spy.first
87
+ end
88
+
89
+ end
@@ -13,100 +13,148 @@ describe "Migration" do
13
13
 
14
14
  let(:migration) { ActiveRecord::Migration }
15
15
  let(:connection) { ActiveRecord::Base.connection }
16
- let(:schema) { ActiveRecord::Schema }
17
16
 
18
17
  before(:each) do
19
18
  define_schema_and_data
20
19
  end
21
20
 
22
- context "creation" do
23
- it "should create correct views" do
24
- expect(AOnes.all.collect(&:s)).to eq(%W[one_one one_two])
25
- expect(ABOnes.all.collect(&:s)).to eq(%W[one_one])
21
+ shared_examples 'view checks' do |options = {}|
22
+ context "creation" do
23
+ it "should create correct views" do
24
+ expect(AOnes.all.collect(&:s)).to eq(%W[one_one one_two])
25
+ expect(ABOnes.all.collect(&:s)).to eq(%W[one_one])
26
+ end
26
27
  end
27
- end
28
28
 
29
- context "duplicate creation" do
30
- before(:each) do
31
- migration.create_view('dupe_me', 'SELECT * FROM items WHERE (a=1)')
32
- end
29
+ context "duplicate creation" do
30
+ before(:each) do
31
+ migration.create_view('dupe_me', 'SELECT * FROM items WHERE (a=1)', options)
32
+ end
33
33
 
34
- it "should raise an error by default" do
35
- expect {migration.create_view('dupe_me', 'SELECT * FROM items WHERE (a=2)')}.to raise_error ActiveRecord::StatementInvalid
36
- end
34
+ it "should raise an error by default" do
35
+ expect { migration.create_view('dupe_me', 'SELECT * FROM items WHERE (a=2)', options) }.to raise_error ActiveRecord::StatementInvalid
36
+ end
37
37
 
38
- it "should override existing definition if :force true" do
39
- migration.create_view('dupe_me', 'SELECT * FROM items WHERE (a=2)', :force => true)
40
- expect(connection.view_definition('dupe_me')).to match(%r{WHERE .*a.*=.*2}i)
41
- end
42
-
43
- context "Postgres and MySQL only", :sqlite3 => :skip do
44
- it "should override existing definition if :allow_replace is true" do
45
- migration.create_view('dupe_me', 'SELECT * FROM items WHERE (a=2)', :allow_replace => true)
38
+ it "should override existing definition if :force true" do
39
+ migration.create_view('dupe_me', 'SELECT * FROM items WHERE (a=2)', options.merge(force: true))
46
40
  expect(connection.view_definition('dupe_me')).to match(%r{WHERE .*a.*=.*2}i)
47
41
  end
48
- end
49
- end
50
42
 
51
- context "dropping" do
52
- it "should raise an error if the view doesn't exist" do
53
- expect { migration.drop_view('doesnt_exist') }.to raise_error ActiveRecord::StatementInvalid
43
+ unless options[:materialized]
44
+ context "Postgres and MySQL only", :sqlite3 => :skip do
45
+ it "should override existing definition if :allow_replace is true" do
46
+ migration.create_view('dupe_me', 'SELECT * FROM items WHERE (a=2)', options.merge(allow_replace: true))
47
+ expect(connection.view_definition('dupe_me')).to match(%r{WHERE .*a.*=.*2}i)
48
+ end
49
+ end
50
+ end
54
51
  end
55
52
 
56
- it "should fail silently when using if_exists option" do
57
- expect { migration.drop_view('doesnt_exist', :if_exists => true) }.not_to raise_error
53
+ context "dropping" do
54
+ it "should raise an error if the view doesn't exist" do
55
+ expect { migration.drop_view('doesnt_exist', options) }.to raise_error ActiveRecord::StatementInvalid
56
+ end
57
+
58
+ it "should fail silently when using if_exists option" do
59
+ expect { migration.drop_view('doesnt_exist', options.merge(if_exists: true)) }.not_to raise_error
60
+ end
61
+
62
+ context "with a view that exists" do
63
+ before { migration.create_view('view_that_exists', 'SELECT * FROM items WHERE (a=1)', options) }
64
+
65
+ it "should succeed" do
66
+ migration.drop_view('view_that_exists', options)
67
+ expect(connection.views).not_to include('view_that_exists')
68
+ end
69
+ end
58
70
  end
59
71
 
60
- context "with a view that exists" do
61
- before { migration.create_view('view_that_exists', 'SELECT * FROM items WHERE (a=1)') }
72
+ describe "rollback" do
73
+ it "properly rolls back a create_view" do
74
+ m = build_migration do
75
+ define_method(:change) {
76
+ create_view :copy, "SELECT * FROM items", options
77
+ }
78
+ end
79
+ m.migrate(:up)
80
+ expect(connection.views).to include("copy")
81
+ m.migrate(:down)
82
+ expect(connection.views).not_to include("copy")
83
+ end
62
84
 
63
- it "should succeed" do
64
- migration.drop_view('view_that_exists')
65
- expect(connection.views).not_to include('view_that_exists')
85
+ it "raises error for drop_view" do
86
+ m = build_migration do
87
+ define_method(:change) {
88
+ drop_view :a_ones, options
89
+ }
90
+ end
91
+ expect { m.migrate(:down) }.to raise_error(::ActiveRecord::IrreversibleMigration)
66
92
  end
67
93
  end
68
94
  end
69
95
 
70
- describe "rollback" do
71
- it "properly rolls back a create_view" do
72
- m = Class.new ::ActiveRecord::Migration do
73
- define_method(:change) {
74
- create_view :copy, "SELECT * FROM items"
75
- }
96
+ describe 'regular views' do
97
+ before(:each) do
98
+ apply_migration do
99
+ create_view :a_ones, Item.select('b, s').where(:a => 1)
100
+ create_view :ab_ones, "select s from a_ones where b = 1"
76
101
  end
77
- m.migrate(:up)
78
- expect(connection.views).to include("copy")
79
- m.migrate(:down)
80
- expect(connection.views).not_to include("copy")
81
102
  end
82
103
 
83
- it "raises error for drop_view" do
84
- m = Class.new ::ActiveRecord::Migration do
85
- define_method(:change) {
86
- drop_view :a_ones
87
- }
104
+ include_examples 'view checks'
105
+ end
106
+
107
+ describe 'materialized views', postgresql: :only do
108
+ before(:each) do
109
+ apply_migration do
110
+ create_view :a_ones, Item.select('b, s').where(:a => 1), materialized: true
111
+ create_view :ab_ones, "select s from items where a = 1 AND b = 1", materialized: true
88
112
  end
89
- expect { m.migrate(:down) }.to raise_error(::ActiveRecord::IrreversibleMigration)
90
113
  end
91
- end
92
114
 
115
+ include_examples 'view checks', materialized: true
116
+
117
+ describe 'refreshing the view' do
118
+ it "refreshes the view" do
119
+ expect(AOnes.count).to eq(2)
120
+ Item.create!(a: 1, b: 3, s: 'one_three')
121
+ expect(AOnes.count).to eq(2)
122
+ connection.refresh_view('a_ones')
123
+ expect(AOnes.count).to eq(3)
124
+ end
125
+ end
126
+
127
+ context 'with indexes' do
128
+ it 'allows creating indexes on the materialized view' do
129
+ apply_migration do
130
+ add_index :a_ones, :s
131
+ add_index :a_ones, :b, unique: true
132
+ end
133
+
134
+ expect(connection.indexes(:a_ones)).to contain_exactly(
135
+ have_attributes(columns: ['s'], unique: false),
136
+ have_attributes(columns: ['b'], unique: true)
137
+ )
138
+ end
139
+ end
140
+ end
93
141
 
94
142
  protected
95
143
 
96
144
  def define_schema_and_data
97
- connection.views.each do |view| connection.drop_view view end
98
- connection.tables.each do |table| connection.drop_table table, cascade: true end
99
-
100
- schema.define do
145
+ connection.views.each do |view|
146
+ connection.drop_view view
147
+ end
148
+ connection.tables.each do |table|
149
+ connection.drop_table table, cascade: true
150
+ end
101
151
 
152
+ apply_migration do
102
153
  create_table :items, :force => true do |t|
103
154
  t.integer :a
104
155
  t.integer :b
105
- t.string :s
156
+ t.string :s
106
157
  end
107
-
108
- create_view :a_ones, Item.select('b, s').where(:a => 1)
109
- create_view :ab_ones, "select s from a_ones where b = 1"
110
158
  end
111
159
  connection.execute "insert into items (a, b, s) values (1, 1, 'one_one')"
112
160
  connection.execute "insert into items (a, b, s) values (1, 2, 'one_two')"