require 'spec_helper' describe ActiveRecordViews do describe '.create_view' do let(:connection) { ActiveRecord::Base.connection } def create_test_view(sql, options = {}) ActiveRecordViews.create_view connection, 'test', 'Test', sql, options end def drop_test_view ActiveRecordViews.drop_view connection, 'test' end def test_view_sql connection.select_value(<<-SQL.squish).try(&:squish) SELECT view_definition FROM information_schema.views WHERE table_schema = 'public' AND table_name = 'test' SQL end def test_materialized_view_sql connection.select_value(<<-SQL.squish).try(&:squish) SELECT definition FROM pg_matviews WHERE schemaname = 'public' AND matviewname = 'test' SQL end it 'creates database view' do expect(test_view_sql).to be_nil create_test_view 'select 1 as id' expect(test_view_sql).to eq 'SELECT 1 AS id;' end it 'records checksum, class name, and options' do create_test_view 'select 1 as id', materialized: true expect(connection.select_all('select * from active_record_views').to_a).to eq [ { 'name' => 'test', 'class_name' => 'Test', 'checksum' => Digest::SHA1.hexdigest('select 1 as id'), 'options' => '{"materialized":true,"dependencies":[]}', 'refreshed_at' => nil, } ] end it 'persists views if transaction rolls back' do expect(test_view_sql).to be_nil connection.transaction :requires_new => true do create_test_view 'select 1 as id' raise ActiveRecord::Rollback end expect(test_view_sql).to eq 'SELECT 1 AS id;' end it 'raises descriptive error if view SQL is invalid' do expect { create_test_view 'select blah' }.to raise_error ActiveRecord::StatementInvalid, /column "blah" does not exist/ end context 'with existing view' do before do create_test_view 'select 1 as id' expect(test_view_sql).to eq 'SELECT 1 AS id;' end it 'updates view with compatible change' do create_test_view 'select 2 as id' expect(test_view_sql).to eq 'SELECT 2 AS id;' end it 'recreates view with incompatible change' do create_test_view "select 'foo'::text as name" expect(test_view_sql).to eq "SELECT 'foo'::text AS name;" end context 'having dependant views' do before do without_dependency_checks do ActiveRecordViews.create_view connection, 'dependant1', 'Dependant1', 'SELECT id FROM test;' ActiveRecordViews.create_view connection, 'dependant2a', 'Dependant2a', 'SELECT id, id * 2 AS id2 FROM dependant1;' ActiveRecordViews.create_view connection, 'dependant2b', 'Dependant2b', 'SELECT id, id * 4 AS id4 FROM dependant1;' ActiveRecordViews.create_view connection, 'dependant3', 'Dependant3', 'SELECT * FROM dependant2b;' ActiveRecordViews.create_view connection, 'dependant4', 'Dependant4', 'SELECT id FROM dependant1 UNION ALL SELECT id FROM dependant3;' end end it 'updates view with compatible change' do create_test_view 'select 2 as id' expect(test_view_sql).to eq 'SELECT 2 AS id;' expect(Integer(connection.select_value('SELECT id2 FROM dependant2a'))).to eq 4 end describe 'changes incompatible with CREATE OR REPLACE' do it 'updates view with new column added before existing' do create_test_view "select 'foo'::text as name, 3 as id" expect(test_view_sql).to eq "SELECT 'foo'::text AS name, 3 AS id;" expect(Integer(connection.select_value('SELECT id2 FROM dependant2a'))).to eq 6 end it 'fails to update view if column used by dependant view is removed' do expect { create_test_view "select 'foo'::text as name" }.to raise_error ActiveRecord::StatementInvalid, /column test.id does not exist/ expect(test_view_sql).to eq 'SELECT 1 AS id;' expect(Integer(connection.select_value('SELECT id2 FROM dependant2a'))).to eq 2 end end describe '.drop_all_views' do it 'can drop all managed views' do connection.execute 'CREATE VIEW unmanaged AS SELECT 2 AS id;' expect(view_names).to match_array %w[test dependant1 dependant2a dependant2b dependant3 dependant4 unmanaged] ActiveRecordViews.drop_all_views connection expect(view_names).to match_array %w[unmanaged] end it 'errors if an unmanaged view depends on a managed view' do connection.execute 'CREATE VIEW unmanaged AS SELECT * from dependant2a' expect { ActiveRecordViews.drop_all_views connection }.to raise_error ActiveRecord::StatementInvalid, /view unmanaged depends on view dependant2a/ end it 'can drop materialized views' do without_dependency_checks do ActiveRecordViews.create_view connection, 'materialized', 'Materialized', 'SELECT id FROM test;', materialized: true end ActiveRecordViews.drop_all_views connection expect(view_names).to match_array %w[] end end end describe 'with unmanaged dependant view' do before do connection.execute 'CREATE VIEW dependant AS SELECT id FROM test' end after do connection.execute 'DROP VIEW dependant;' end it 'updates view with compatible change' do create_test_view 'select 2 as id' expect(test_view_sql).to eq 'SELECT 2 AS id;' end it 'fails to update view with incompatible change' do expect { create_test_view "SELECT 'foo'::text as name, 4 as id" }.to raise_error ActiveRecord::StatementInvalid, /view dependant depends on view test/ expect(test_view_sql).to eq 'SELECT 1 AS id;' end end end it 'creates and drops materialized views' do create_test_view 'select 123 as id', materialized: true expect(test_view_sql).to eq nil expect(test_materialized_view_sql).to eq 'SELECT 123 AS id;' drop_test_view expect(test_view_sql).to eq nil expect(test_materialized_view_sql).to eq nil end it 'replaces a normal view with a materialized view' do create_test_view 'select 11 as id' create_test_view 'select 22 as id', materialized: true expect(test_view_sql).to eq nil expect(test_materialized_view_sql).to eq 'SELECT 22 AS id;' end it 'replaces a materialized view with a normal view' do create_test_view 'select 22 as id', materialized: true create_test_view 'select 11 as id' expect(test_view_sql).to eq 'SELECT 11 AS id;' expect(test_materialized_view_sql).to eq nil end it 'can test if materialized views can be refreshed concurrently' do expect(ActiveRecordViews.supports_concurrent_refresh?(connection)).to be true end it 'preserves materialized view if dropping/recreating' do without_dependency_checks do ActiveRecordViews.create_view connection, 'test1', 'Test1', 'SELECT 1 AS foo' ActiveRecordViews.create_view connection, 'test2', 'Test2', 'SELECT * FROM test1', materialized: true ActiveRecordViews.create_view connection, 'test1', 'Test1', 'SELECT 2 AS bar, 1 AS foo' end expect(materialized_view_names).to eq %w[test2] expect(view_names).to eq %w[test1] end it 'supports creating unique indexes on materialized views' do create_test_view 'select 1 as foo, 2 as bar, 3 as baz', materialized: true, unique_columns: [:foo, 'bar'] index_sql = connection.select_value("SELECT indexdef FROM pg_indexes WHERE schemaname = 'public' AND indexname = 'test_pkey';") expect(index_sql).to eq 'CREATE UNIQUE INDEX test_pkey ON public.test USING btree (foo, bar)' end it 'errors if trying to create unique index on non-materialized view' do expect { create_test_view 'select 1 as foo, 2 as bar, 3 as baz', materialized: false, unique_columns: [:foo, 'bar'] }.to raise_error ArgumentError, 'unique_columns option requires view to be materialized' end end describe '.without_transaction' do let(:original_connection) { ActiveRecord::Base.connection } it 'yields original connection if no active transaction' do ActiveRecordViews.without_transaction original_connection do |new_connection| expect(new_connection).to eq original_connection end end it 'yields a new connection if inside a transaction' do original_connection.transaction do ActiveRecordViews.without_transaction original_connection do |new_connection| expect(new_connection).to_not eq original_connection end end end it 'yields original connection if called recursively' do ActiveRecordViews.without_transaction original_connection do |new_connection_1| expect(new_connection_1).to eq original_connection new_connection_1.transaction do ActiveRecordViews.without_transaction new_connection_1 do |new_connection_2| expect(new_connection_2).to eq new_connection_1 end end end end it 'yields same isolated connection if called recursively on original connection inside transaction' do original_connection.transaction do ActiveRecordViews.without_transaction original_connection do |new_connection_1| expect(new_connection_1).to_not eq original_connection ActiveRecordViews.without_transaction original_connection do |new_connection_2| expect(new_connection_2).to eq new_connection_1 end end end end it 'yields different isolated connection if called recursively on different connections inside transcation' do begin original_connection_2 = original_connection.pool.checkout original_connection.transaction do ActiveRecordViews.without_transaction original_connection do |new_connection_1| expect(new_connection_1).to_not eq original_connection original_connection_2.transaction do ActiveRecordViews.without_transaction original_connection_2 do |new_connection_2| expect(new_connection_2).to_not eq original_connection expect(new_connection_2).to_not eq original_connection_2 expect(new_connection_2).to_not eq new_connection_1 end end end end ensure original_connection.pool.checkin original_connection_2 end end end end