schema_plus_views 0.3.1 → 0.4.0

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.
@@ -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}]"