schema_plus_views 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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", "~> 1.0"
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.6"
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 }
@@ -39,27 +37,77 @@ describe "Dumper" do
39
37
  # when in the (say) development database, but then uses it to
40
38
  # initialize the test database when testing. this meant that the
41
39
  # test database had views into the development database.
42
- 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]
43
41
  expect(dump).not_to match(%r{#{connection.quote_table_name(db)}[.]})
44
42
  end
45
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
+
46
90
  protected
47
91
 
48
- def view_re(name, re)
92
+ def view_re(name, re, extra_config = '')
49
93
  heredelim = "END_VIEW_#{name.upcase}"
50
- %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
51
95
  end
52
96
 
53
97
  def define_schema_and_data
54
- connection.views.each do |view| connection.drop_view view end
55
- 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
56
104
 
57
- schema.define do
105
+ apply_migration do
58
106
 
59
107
  create_table :items, :force => true do |t|
60
108
  t.integer :a
61
109
  t.integer :b
62
- t.string :s
110
+ t.string :s
63
111
  end
64
112
 
65
113
  create_view :a_ones, Item.select('b, s').where(:a => 1)
@@ -71,7 +119,7 @@ describe "Dumper" do
71
119
  connection.execute "insert into items (a, b, s) values (2, 2, 'two_two')"
72
120
  end
73
121
 
74
- def dump(opts={})
122
+ def dump(opts = {})
75
123
  StringIO.open { |stream|
76
124
  ActiveRecord::SchemaDumper.ignore_tables = Array.wrap(opts[:ignore_tables])
77
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,6 +25,29 @@ 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')
@@ -40,6 +58,11 @@ describe "Introspection" do
40
58
  expect(connection.view_definition('ab_ones')).to match(%r{^SELECT .*s.* FROM .*a_ones.* WHERE .*b.* = 1}mi)
41
59
  end
42
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)
64
+ end
65
+
43
66
  context "in mysql", :mysql => :only do
44
67
 
45
68
  around(:each) do |example|
@@ -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
@@ -4,12 +4,6 @@ module TestMiddleware
4
4
  module Middleware
5
5
 
6
6
  module Schema
7
- module Views
8
- SPY = []
9
- def after(env)
10
- SPY << env.to_hash.except(:connection)
11
- end
12
- end
13
7
  module ViewDefinition
14
8
  SPY = []
15
9
  def after(env)
@@ -40,12 +34,11 @@ SchemaMonkey.register TestMiddleware
40
34
 
41
35
  context SchemaPlus::Views::Middleware do
42
36
 
43
- let(:schema) { ActiveRecord::Schema }
44
37
  let(:migration) { ActiveRecord::Migration }
45
38
  let(:connection) { ActiveRecord::Base.connection }
46
39
 
47
40
  before(:each) do
48
- schema.define do
41
+ apply_migration do
49
42
  create_table :items, force: true do |t|
50
43
  t.integer :a
51
44
  end
@@ -53,16 +46,6 @@ context SchemaPlus::Views::Middleware do
53
46
  end
54
47
  end
55
48
 
56
- context TestMiddleware::Middleware::Schema::Views do
57
- it "calls middleware" do
58
- expect(spy_on {connection.views 'qn'}).to eq({
59
- #connection: connection,
60
- views: ['a_view'],
61
- query_name: 'qn'
62
- })
63
- end
64
- end
65
-
66
49
  context TestMiddleware::Middleware::Schema::ViewDefinition do
67
50
  it "calls middleware" do
68
51
  spied = spy_on {connection.view_definition('a_view', 'qn')}
@@ -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')"
@@ -6,10 +6,10 @@ describe "with multiple schemas" do
6
6
  end
7
7
 
8
8
  before(:each) do
9
- newdb = case connection.adapter_name
10
- when /^mysql/i then "CREATE SCHEMA IF NOT EXISTS schema_plus_views_test2"
11
- when /^postgresql/i then "CREATE SCHEMA schema_plus_views_test2"
12
- when /^sqlite/i then "ATTACH ':memory:' AS schema_plus_views_test2"
9
+ newdb = case
10
+ when SchemaDev::Rspec::Helpers.mysql? then "CREATE SCHEMA IF NOT EXISTS schema_plus_views_test2"
11
+ when SchemaDev::Rspec::Helpers.postgresql? then "CREATE SCHEMA schema_plus_views_test2"
12
+ when SchemaDev::Rspec::Helpers.sqlite3? then "ATTACH ':memory:' AS schema_plus_views_test2"
13
13
  end
14
14
  begin
15
15
  ActiveRecord::Base.connection.execute newdb
@@ -28,10 +28,10 @@ describe "with multiple schemas" do
28
28
  end
29
29
 
30
30
  connection.execute 'DROP TABLE IF EXISTS schema_plus_views_test2.users'
31
- connection.execute 'CREATE TABLE schema_plus_views_test2.users (id ' + case connection.adapter_name
32
- when /^mysql/i then "integer primary key auto_increment"
33
- when /^postgresql/i then "serial primary key"
34
- when /^sqlite/i then "integer primary key autoincrement"
31
+ connection.execute 'CREATE TABLE schema_plus_views_test2.users (id ' + case
32
+ when SchemaDev::Rspec::Helpers.mysql? then "integer primary key auto_increment"
33
+ when SchemaDev::Rspec::Helpers.postgresql? then "serial primary key"
34
+ when SchemaDev::Rspec::Helpers.sqlite3? then "integer primary key autoincrement"
35
35
  end + ", login varchar(255))"
36
36
  end
37
37
 
@@ -60,38 +60,4 @@ describe "with multiple schemas" do
60
60
  end
61
61
  end
62
62
 
63
- context "when using PostGIS", :postgresql => :only do
64
- before(:all) do
65
- begin
66
- connection.execute "CREATE SCHEMA postgis"
67
- rescue ActiveRecord::StatementInvalid => e
68
- raise unless e.message =~ /already exists/
69
- end
70
- end
71
-
72
- around(:each) do |example|
73
- begin
74
- connection.execute "SET search_path to '$user','public','postgis'"
75
- example.run
76
- ensure
77
- connection.execute "SET search_path to '$user','public'"
78
- end
79
- end
80
-
81
- before(:each) do
82
- allow(connection).to receive(:adapter_name).and_return('PostGIS')
83
- end
84
-
85
- it "should hide views in postgis schema" do
86
- begin
87
- connection.create_view "postgis.hidden", "select 1", :force => true
88
- connection.create_view :myview, "select 2", :force => true
89
- expect(connection.views).to eq(["myview"])
90
- ensure
91
- connection.execute 'DROP VIEW postgis.hidden' rescue nil
92
- connection.execute 'DROP VIEW myview' rescue nil
93
- end
94
- end
95
- end
96
-
97
63
  end
data/spec/spec_helper.rb CHANGED
@@ -12,7 +12,7 @@ require 'schema_dev/rspec'
12
12
 
13
13
  SchemaDev::Rspec.setup
14
14
 
15
- Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}
15
+ Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each { |f| require f }
16
16
 
17
17
  RSpec.configure do |config|
18
18
  config.warnings = true
@@ -29,4 +29,16 @@ RSpec.configure do |config|
29
29
  end
30
30
  end
31
31
 
32
+ def apply_migration(config = {}, &block)
33
+ ActiveRecord::Schema.define do
34
+ instance_eval &block
35
+ end
36
+ end
37
+
38
+ def build_migration(version: 5.0, &block)
39
+ Class.new(::ActiveRecord::Migration[version]) do
40
+ instance_eval &block
41
+ end
42
+ end
43
+
32
44
  SimpleCov.command_name "[ruby #{RUBY_VERSION} - ActiveRecord #{::ActiveRecord::VERSION::STRING} - #{ActiveRecord::Base.connection.adapter_name}]"