schema_plus_views 0.2.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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')"