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.
- checksums.yaml +5 -5
- data/.travis.yml +79 -8
- data/README.md +91 -5
- data/gemfiles/Gemfile.base +1 -1
- data/gemfiles/activerecord-5.2/Gemfile.base +4 -0
- data/gemfiles/activerecord-5.2/Gemfile.mysql2 +10 -0
- data/gemfiles/activerecord-5.2/Gemfile.postgresql +10 -0
- data/gemfiles/{activerecord-4.2 → activerecord-5.2}/Gemfile.sqlite3 +3 -3
- data/lib/schema_plus/views/active_record/connection_adapters/abstract_adapter.rb +42 -19
- data/lib/schema_plus/views/active_record/connection_adapters/mysql2_adapter.rb +15 -18
- data/lib/schema_plus/views/active_record/connection_adapters/postgresql_adapter.rb +79 -18
- data/lib/schema_plus/views/active_record/connection_adapters/sqlite3_adapter.rb +7 -6
- data/lib/schema_plus/views/active_record/migration/command_recorder.rb +3 -1
- data/lib/schema_plus/views/middleware.rb +65 -24
- data/lib/schema_plus/views/version.rb +1 -1
- data/schema_dev.yml +5 -2
- data/schema_plus_views.gemspec +4 -4
- data/spec/dumper_spec.rb +58 -11
- data/spec/introspection_spec.rb +54 -8
- data/spec/middleware_spec.rb +89 -0
- data/spec/migration_spec.rb +106 -58
- data/spec/named_schemas_spec.rb +8 -42
- data/spec/spec_helper.rb +16 -1
- metadata +25 -24
- data/gemfiles/activerecord-4.2/Gemfile.base +0 -3
- data/gemfiles/activerecord-4.2/Gemfile.mysql2 +0 -10
- data/gemfiles/activerecord-4.2/Gemfile.postgresql +0 -10
@@ -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
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
#{
|
25
|
-
|
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
|
-
|
34
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
77
|
+
module Schema
|
78
|
+
module ViewDefinition
|
79
|
+
ENV = [:connection, :view_name, :query_name, :definition, :view_type]
|
80
|
+
end
|
81
|
+
end
|
47
82
|
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
data/schema_dev.yml
CHANGED
data/schema_plus_views.gemspec
CHANGED
@@ -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", "~>
|
21
|
-
gem.add_dependency "schema_plus_core"
|
20
|
+
gem.add_dependency "activerecord", "~> 5.2"
|
21
|
+
gem.add_dependency "schema_plus_core"
|
22
22
|
|
23
|
-
gem.add_development_dependency "bundler"
|
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.
|
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|
|
56
|
-
|
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
|
-
|
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
|
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)
|
data/spec/introspection_spec.rb
CHANGED
@@ -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{^
|
40
|
-
expect(connection.view_definition('ab_ones')).to match(%r{^
|
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
|
-
|
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
|
data/spec/migration_spec.rb
CHANGED
@@ -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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
39
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
57
|
-
|
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
|
-
|
61
|
-
|
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 "
|
64
|
-
|
65
|
-
|
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
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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|
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
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')"
|