activerecord_views 0.0.3 → 0.0.4

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 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