activerecord_views 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9f28ee79b39ed0d431534523ef129ba9e63e8aad
4
- data.tar.gz: 39e683254984dff81f2fdf7e43a3b96588738612
3
+ metadata.gz: be3aa1741d6dced3c4afcf32a941d3821ec0eafc
4
+ data.tar.gz: f29929115236d12395bad521420b03a4f799f382
5
5
  SHA512:
6
- metadata.gz: 2654eb5b8107e4cac185668a89769569efae3a13d627029da037ad2a4b0faea6fbfeb9695f67453175300b7899de19aac21c911ea240f9be0c6dc72b54dcb3e9
7
- data.tar.gz: c5c7685c4598c46c43230958fcac5336b906a43b77e15d0c34a5ab73b373d02d51d263e1b8f11ebb8ae35780f26124cd3776fbd497d9f2f9a27b74485d262c92
6
+ metadata.gz: 6c623e3dec891c353d1bb3d2b4a6466a2ea4ea520d5c4082007a3303c358982b6e832d251e9127b37ce0ba1b366cb866547eaa0ae57173fb868d0c4683582e4f
7
+ data.tar.gz: 7cde80415bc90e545f7cdcc6f987525c7c53050e59a2e26b8a83838f40c0f9e9cbbeae29ad15568e4a4e886a028b3bfce359167453d56f7af0d80a89111e3d3d
@@ -6,19 +6,27 @@ module ActiveRecordViews
6
6
  end
7
7
 
8
8
  def init_state_table!
9
- unless @connection.table_exists?('active_record_views')
10
- @connection.execute 'CREATE TABLE active_record_views(name text PRIMARY KEY, checksum text NOT NULL);'
9
+ table_exists = @connection.table_exists?('active_record_views')
10
+
11
+ if table_exists && !@connection.column_exists?('active_record_views', 'class_name')
12
+ @connection.execute 'DROP TABLE active_record_views;'
13
+ table_exists = false
14
+ end
15
+
16
+ unless table_exists
17
+ @connection.execute 'CREATE TABLE active_record_views(name text PRIMARY KEY, class_name text NOT NULL UNIQUE, checksum text NOT NULL);'
11
18
  end
12
19
  end
13
20
 
14
21
  def get(name)
15
- @connection.select_value("SELECT checksum FROM active_record_views WHERE name = #{@connection.quote name}")
22
+ @connection.select_one("SELECT class_name, checksum FROM active_record_views WHERE name = #{@connection.quote name}").try(:symbolize_keys)
16
23
  end
17
24
 
18
- def set(name, checksum)
19
- if checksum
20
- if @connection.update("UPDATE active_record_views SET checksum = #{@connection.quote checksum} WHERE name = #{@connection.quote name}") == 0
21
- @connection.insert "INSERT INTO active_record_views (name, checksum) VALUES (#{@connection.quote name}, #{@connection.quote checksum})"
25
+ def set(name, data)
26
+ if data
27
+ data.assert_valid_keys :class_name, :checksum
28
+ if @connection.update("UPDATE active_record_views SET class_name = #{@connection.quote data[:class_name]}, checksum = #{@connection.quote data[:checksum]} WHERE name = #{@connection.quote name}") == 0
29
+ @connection.insert "INSERT INTO active_record_views (name, class_name, checksum) VALUES (#{@connection.quote name}, #{@connection.quote data[:class_name]}, #{@connection.quote data[:checksum]})"
22
30
  end
23
31
  else
24
32
  @connection.delete "DELETE FROM active_record_views WHERE name = #{@connection.quote name}"
@@ -1,3 +1,5 @@
1
+ require 'erb'
2
+
1
3
  module ActiveRecordViews
2
4
  module Extension
3
5
  extend ActiveSupport::Concern
@@ -13,10 +15,16 @@ module ActiveRecordViews
13
15
  sql ||= begin
14
16
  sql_path = ActiveRecordViews.find_sql_file(self.name.underscore)
15
17
  ActiveRecordViews.register_for_reload self, sql_path
16
- File.read sql_path
18
+
19
+ if sql_path.end_with?('.erb')
20
+ ERB.new(File.read(sql_path)).result
21
+ else
22
+ File.read(sql_path)
23
+ end
17
24
  end
25
+
18
26
  unless ActiveRecordViews::Extension.currently_migrating?
19
- ActiveRecordViews.create_view self.connection, self.table_name, sql
27
+ ActiveRecordViews.create_view self.connection, self.table_name, self.name, sql
20
28
  end
21
29
  end
22
30
  end
@@ -18,7 +18,7 @@ module ActiveRecordViews
18
18
 
19
19
  def reload!
20
20
  if File.exists? sql_path
21
- ActiveRecordViews.create_view model_class.connection, model_class.table_name, File.read(sql_path)
21
+ ActiveRecordViews.create_view model_class.connection, model_class.table_name, model_class.name, File.read(sql_path)
22
22
  else
23
23
  ActiveRecordViews.drop_view model_class.connection, model_class.table_name
24
24
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordViews
2
- VERSION = '0.0.3'
2
+ VERSION = '0.0.4'
3
3
  end
@@ -21,6 +21,8 @@ module ActiveRecordViews
21
21
  self.sql_load_path.each do |dir|
22
22
  path = "#{dir}/#{name}.sql"
23
23
  return path if File.exists?(path)
24
+ path = path + '.erb'
25
+ return path if File.exists?(path)
24
26
  end
25
27
  raise "could not find #{name}.sql"
26
28
  end
@@ -32,27 +34,35 @@ module ActiveRecordViews
32
34
  !connection.outside_transaction?
33
35
  end
34
36
 
35
- if in_transaction
36
- begin
37
- temp_connection = connection.pool.checkout
38
- yield temp_connection
39
- ensure
40
- connection.pool.checkin temp_connection
37
+ begin
38
+ recursing = Thread.current[:active_record_views_without_transaction]
39
+ Thread.current[:active_record_views_without_transaction] = true
40
+
41
+ if in_transaction && !recursing
42
+ begin
43
+ temp_connection = connection.pool.checkout
44
+ yield temp_connection
45
+ ensure
46
+ connection.pool.checkin temp_connection
47
+ end
48
+ else
49
+ yield connection
41
50
  end
42
- else
43
- yield connection
51
+ ensure
52
+ Thread.current[:active_record_views_without_transaction] = nil
44
53
  end
45
54
  end
46
55
 
47
- def self.create_view(connection, name, sql)
56
+ def self.create_view(connection, name, class_name, sql)
48
57
  without_transaction connection do |connection|
49
58
  cache = ActiveRecordViews::ChecksumCache.new(connection)
50
- checksum = Digest::SHA1.hexdigest(sql)
51
- return if cache.get(name) == checksum
59
+ data = {class_name: class_name, checksum: Digest::SHA1.hexdigest(sql)}
60
+ return if cache.get(name) == data
52
61
 
53
62
  begin
54
63
  connection.execute "CREATE OR REPLACE VIEW #{connection.quote_table_name name} AS #{sql}"
55
64
  rescue ActiveRecord::StatementInvalid => original_exception
65
+ raise unless view_exists?(connection, name)
56
66
  connection.transaction :requires_new => true do
57
67
  without_dependencies connection, name do
58
68
  connection.execute "DROP VIEW #{connection.quote_table_name name}"
@@ -61,7 +71,7 @@ module ActiveRecordViews
61
71
  end
62
72
  end
63
73
 
64
- cache.set name, checksum
74
+ cache.set name, data
65
75
  end
66
76
  end
67
77
 
@@ -73,6 +83,14 @@ module ActiveRecordViews
73
83
  end
74
84
  end
75
85
 
86
+ def self.view_exists?(connection, name)
87
+ connection.select_value(<<-SQL).present?
88
+ SELECT 1
89
+ FROM information_schema.views
90
+ WHERE table_schema = 'public' AND table_name = #{connection.quote name};
91
+ SQL
92
+ end
93
+
76
94
  def self.get_view_dependencies(connection, name)
77
95
  connection.select_rows <<-SQL
78
96
  WITH RECURSIVE dependants AS (
@@ -93,11 +111,12 @@ module ActiveRecordViews
93
111
 
94
112
  SELECT
95
113
  oid::regclass::text AS name,
114
+ class_name,
96
115
  pg_catalog.pg_get_viewdef(oid) AS definition
97
116
  FROM dependants
98
117
  INNER JOIN active_record_views ON active_record_views.name = oid::regclass::text
99
118
  WHERE level > 0
100
- GROUP BY oid
119
+ GROUP BY oid, class_name
101
120
  ORDER BY MAX(level)
102
121
  ;
103
122
  SQL
@@ -106,19 +125,24 @@ module ActiveRecordViews
106
125
  def self.without_dependencies(connection, name)
107
126
  dependencies = get_view_dependencies(connection, name)
108
127
 
109
- dependencies.reverse.each do |name, _|
128
+ dependencies.reverse.each do |name, _, _|
110
129
  connection.execute "DROP VIEW #{name};"
111
130
  end
112
131
 
113
132
  yield
114
133
 
115
- dependencies.each do |name, definition|
116
- connection.execute "CREATE VIEW #{name} AS #{definition};"
134
+ dependencies.each do |name, class_name, definition|
135
+ begin
136
+ class_name.constantize
137
+ rescue NameError => e
138
+ raise unless e.missing_name?(class_name)
139
+ connection.execute "CREATE VIEW #{name} AS #{definition};"
140
+ end
117
141
  end
118
142
  end
119
143
 
120
- def self.register_for_reload(sql_path, model_path)
121
- self.registered_views << RegisteredView.new(sql_path, model_path)
144
+ def self.register_for_reload(model_class, sql_path)
145
+ self.registered_views << RegisteredView.new(model_class, sql_path)
122
146
  end
123
147
 
124
148
  def self.reload_stale_views!
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveRecordViews::ChecksumCache do
4
+ let(:connection) { ActiveRecord::Base.connection }
5
+
6
+ describe 'initialisation' do
7
+ context 'with no existing table' do
8
+ it 'creates the table' do
9
+ expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ACREATE TABLE active_record_views/).once.and_call_original
10
+
11
+ expect(connection.table_exists?('active_record_views')).to eq false
12
+ ActiveRecordViews::ChecksumCache.new(connection)
13
+ expect(connection.table_exists?('active_record_views')).to eq true
14
+ end
15
+ end
16
+
17
+ context 'with existing table' do
18
+ before do
19
+ ActiveRecordViews::ChecksumCache.new(connection)
20
+ expect(connection.table_exists?('active_record_views')).to eq true
21
+ end
22
+
23
+ it 'does not recreate the table' do
24
+ expect(ActiveRecord::Base.connection).to receive(:execute).never
25
+
26
+ ActiveRecordViews::ChecksumCache.new(connection)
27
+ end
28
+ end
29
+
30
+ context 'with old table' do
31
+ before do
32
+ connection.execute 'CREATE TABLE active_record_views(name text PRIMARY KEY, checksum text NOT NULL);'
33
+ end
34
+
35
+ it 'recreates the table' do
36
+ expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ADROP TABLE active_record_views/).once.and_call_original
37
+ expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ACREATE TABLE active_record_views/).once.and_call_original
38
+
39
+ expect(connection.column_exists?('active_record_views', 'class_name')).to eq false
40
+ ActiveRecordViews::ChecksumCache.new(connection)
41
+ expect(connection.column_exists?('active_record_views', 'class_name')).to eq true
42
+ end
43
+ end
44
+ end
45
+ end
@@ -17,6 +17,11 @@ describe ActiveRecordViews::Extension do
17
17
  expect(Namespace::TestModel.first.name).to eq 'Namespaced SQL file'
18
18
  end
19
19
 
20
+ it 'creates database views from external ERB files' do
21
+ expect(ActiveRecordViews).to receive(:create_view).once.and_call_original
22
+ expect(ErbTestModel.first.name).to eq 'ERB file'
23
+ end
24
+
20
25
  it 'errors if external SQL file is missing' do
21
26
  expect {
22
27
  MissingFileTestModel
@@ -28,6 +33,7 @@ describe ActiveRecordViews::Extension do
28
33
  expect(ActiveRecordViews).to receive(:create_view).with(
29
34
  anything,
30
35
  'modified_file_test_models',
36
+ 'ModifiedFileTestModel',
31
37
  sql
32
38
  ).once.ordered
33
39
  end
@@ -76,11 +82,18 @@ describe ActiveRecordViews::Extension do
76
82
  end
77
83
 
78
84
  it 'does not create if database view is initially up to date' do
79
- ActiveRecordViews.create_view ActiveRecord::Base.connection, 'initial_create_test_models', 'SELECT 42 as id'
85
+ ActiveRecordViews.create_view ActiveRecord::Base.connection, 'initial_create_test_models', 'InitialCreateTestModel', 'SELECT 42 as id'
80
86
  expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ACREATE (?:OR REPLACE )?VIEW/).never
81
87
  class InitialCreateTestModel < ActiveRecord::Base
82
88
  is_view 'SELECT 42 as id'
83
89
  end
84
90
  end
91
+
92
+ it 'successfully recreates modified paired views with incompatible changes' do
93
+ ActiveRecordViews.create_view ActiveRecord::Base.connection, 'modified_as', 'ModifiedA', 'SELECT 11 AS old_name;'
94
+ ActiveRecordViews.create_view ActiveRecord::Base.connection, 'modified_bs', 'ModifiedB', 'SELECT old_name FROM modified_as;'
95
+
96
+ expect(ModifiedB.first.attributes.except(nil)).to eq('new_name' => 22)
97
+ end
85
98
  end
86
99
  end
@@ -5,7 +5,7 @@ describe ActiveRecordViews do
5
5
  let(:connection) { ActiveRecord::Base.connection }
6
6
 
7
7
  def create_test_view(sql)
8
- ActiveRecordViews.create_view connection, 'test', sql
8
+ ActiveRecordViews.create_view connection, 'test', 'Test', sql
9
9
  end
10
10
 
11
11
  def test_view_sql
@@ -30,6 +30,17 @@ describe ActiveRecordViews do
30
30
  expect(test_view_sql).to eq 'SELECT 1 AS id;'
31
31
  end
32
32
 
33
+ it 'records checksum and class name' do
34
+ create_test_view 'select 1 as id'
35
+ expect(connection.select_all('select * from active_record_views').to_a).to eq [
36
+ {
37
+ 'name' => 'test',
38
+ 'class_name' => 'Test',
39
+ 'checksum' => Digest::SHA1.hexdigest('select 1 as id')
40
+ }
41
+ ]
42
+ end
43
+
33
44
  it 'persists views if transaction rolls back' do
34
45
  expect(test_view_sql).to be_nil
35
46
  connection.transaction :requires_new => true do
@@ -39,6 +50,12 @@ describe ActiveRecordViews do
39
50
  expect(test_view_sql).to eq 'SELECT 1 AS id;'
40
51
  end
41
52
 
53
+ it 'raises descriptive error if view SQL is invalid' do
54
+ expect {
55
+ create_test_view 'select blah'
56
+ }.to raise_error ActiveRecord::StatementInvalid, /column "blah" does not exist/
57
+ end
58
+
42
59
  context 'with existing view' do
43
60
  before do
44
61
  create_test_view 'select 1 as id'
@@ -57,11 +74,11 @@ describe ActiveRecordViews do
57
74
 
58
75
  context 'having dependant views' do
59
76
  before do
60
- ActiveRecordViews.create_view connection, 'dependency1', 'SELECT id FROM test;'
61
- ActiveRecordViews.create_view connection, 'dependency2a', 'SELECT id, id * 2 AS id2 FROM dependency1;'
62
- ActiveRecordViews.create_view connection, 'dependency2b', 'SELECT id, id * 4 AS id4 FROM dependency1;'
63
- ActiveRecordViews.create_view connection, 'dependency3', 'SELECT * FROM dependency2b;'
64
- ActiveRecordViews.create_view connection, 'dependency4', 'SELECT id FROM dependency1 UNION ALL SELECT id FROM dependency3;'
77
+ ActiveRecordViews.create_view connection, 'dependency1', 'Dependency1', 'SELECT id FROM test;'
78
+ ActiveRecordViews.create_view connection, 'dependency2a', 'Dependency2a', 'SELECT id, id * 2 AS id2 FROM dependency1;'
79
+ ActiveRecordViews.create_view connection, 'dependency2b', 'Dependency2b', 'SELECT id, id * 4 AS id4 FROM dependency1;'
80
+ ActiveRecordViews.create_view connection, 'dependency3', 'Dependency3', 'SELECT * FROM dependency2b;'
81
+ ActiveRecordViews.create_view connection, 'dependency4', 'Dependency4', 'SELECT id FROM dependency1 UNION ALL SELECT id FROM dependency3;'
65
82
  end
66
83
 
67
84
  after do
@@ -118,4 +135,33 @@ describe ActiveRecordViews do
118
135
  end
119
136
  end
120
137
  end
138
+
139
+ describe '.without_transaction' do
140
+ let(:original_connection) { ActiveRecord::Base.connection }
141
+
142
+ it 'yields original connection if no active transaction' do
143
+ ActiveRecordViews.without_transaction original_connection do |new_connection|
144
+ expect(new_connection).to eq original_connection
145
+ end
146
+ end
147
+
148
+ it 'yields a new connection if inside a transaction' do
149
+ original_connection.transaction do
150
+ ActiveRecordViews.without_transaction original_connection do |new_connection|
151
+ expect(new_connection).to_not eq original_connection
152
+ end
153
+ end
154
+ end
155
+
156
+ it 'yields original connection if called recursively' do
157
+ ActiveRecordViews.without_transaction original_connection do |new_connection_1|
158
+ expect(new_connection_1).to eq original_connection
159
+ new_connection_1.transaction do
160
+ ActiveRecordViews.without_transaction new_connection_1 do |new_connection_2|
161
+ expect(new_connection_2).to eq new_connection_1
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
121
167
  end
@@ -0,0 +1,3 @@
1
+ class ErbTestModel < ActiveRecord::Base
2
+ is_view
3
+ end
@@ -0,0 +1 @@
1
+ SELECT 1 AS id, '<%= "ERB" %> file'::text AS name;
@@ -0,0 +1,3 @@
1
+ class ModifiedA < ActiveRecord::Base
2
+ is_view 'SELECT 22 AS new_name;'
3
+ end
@@ -0,0 +1,3 @@
1
+ class ModifiedB < ActiveRecord::Base
2
+ is_view "SELECT new_name FROM #{ModifiedA.table_name};"
3
+ end
data/spec/spec_helper.rb CHANGED
@@ -23,7 +23,7 @@ RSpec.configure do |config|
23
23
  WHERE table_schema = 'public';
24
24
  SQL
25
25
  view_names.each do |view_name|
26
- connection.execute "DROP VIEW IF EXISTS #{connection.quote_table_name view_name}"
26
+ connection.execute "DROP VIEW IF EXISTS #{connection.quote_table_name view_name} CASCADE"
27
27
  end
28
28
  end
29
29
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord_views
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Weathered
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-12-07 00:00:00.000000000 Z
11
+ date: 2014-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -111,12 +111,17 @@ files:
111
111
  - lib/active_record_views/registered_view.rb
112
112
  - lib/active_record_views/version.rb
113
113
  - lib/activerecord_views.rb
114
+ - spec/active_record_views_checksum_cache_spec.rb
114
115
  - spec/active_record_views_extension_spec.rb
115
116
  - spec/active_record_views_spec.rb
117
+ - spec/internal/app/models/erb_test_model.rb
118
+ - spec/internal/app/models/erb_test_model.sql.erb
116
119
  - spec/internal/app/models/external_file_test_model.rb
117
120
  - spec/internal/app/models/external_file_test_model.sql
118
121
  - spec/internal/app/models/heredoc_test_model.rb
119
122
  - spec/internal/app/models/missing_file_test_model.rb
123
+ - spec/internal/app/models/modified_a.rb
124
+ - spec/internal/app/models/modified_b.rb
120
125
  - spec/internal/app/models/namespace/test_model.rb
121
126
  - spec/internal/app/models/namespace/test_model.sql
122
127
  - spec/internal/config/database.yml
@@ -147,12 +152,17 @@ signing_key:
147
152
  specification_version: 4
148
153
  summary: Automatic database view creation for ActiveRecord
149
154
  test_files:
155
+ - spec/active_record_views_checksum_cache_spec.rb
150
156
  - spec/active_record_views_extension_spec.rb
151
157
  - spec/active_record_views_spec.rb
158
+ - spec/internal/app/models/erb_test_model.rb
159
+ - spec/internal/app/models/erb_test_model.sql.erb
152
160
  - spec/internal/app/models/external_file_test_model.rb
153
161
  - spec/internal/app/models/external_file_test_model.sql
154
162
  - spec/internal/app/models/heredoc_test_model.rb
155
163
  - spec/internal/app/models/missing_file_test_model.rb
164
+ - spec/internal/app/models/modified_a.rb
165
+ - spec/internal/app/models/modified_b.rb
156
166
  - spec/internal/app/models/namespace/test_model.rb
157
167
  - spec/internal/app/models/namespace/test_model.sql
158
168
  - spec/internal/config/database.yml