updateable_views_inheritance 1.4.1 → 1.4.2
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 +4 -4
- data/CHANGELOG.md +6 -1
- data/lib/updateable_views_inheritance/postgresql_adapter.rb +377 -372
- data/lib/updateable_views_inheritance/version.rb +1 -1
- data/test/content_test.rb +0 -3
- data/test/deep_hierarchy_test.rb +1 -1
- data/test/dummy/config/environments/test.rb +2 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +1 -1
- data/test/schema_test.rb +1 -1
- data/test/test_helper.rb +0 -4
- data/updateable_views_inheritance.gemspec +2 -3
- metadata +8 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7a73bd114b93585afae503c742efea0bbb5fa03a
|
4
|
+
data.tar.gz: 50871f961687a777113315c999b6f578add39566
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 710e9a867878a14618a1ec28881a9dc48ca530a82df33690d55a69e996b3720fb7c0fe1a14b03e76b67528a73fac8d23d76c35d13578dd47648cf9ba6694ccc7
|
7
|
+
data.tar.gz: 0d7b56539848266e86167887dc686995f5f2691ded47e29b4ebf2ed66fa236b2c7e2c8ecd3843ad9c8f6f5d0eafdbf0463c67331866e56ce14aa5fce11ca9f97
|
data/CHANGELOG.md
CHANGED
@@ -1,436 +1,441 @@
|
|
1
|
+
require 'active_record/connection_adapters/postgresql/utils'
|
2
|
+
|
3
|
+
|
1
4
|
module ActiveRecord #:nodoc:
|
2
5
|
module ConnectionAdapters #:nodoc:
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
6
|
+
module PostgreSQL
|
7
|
+
module SchemaStatements
|
8
|
+
# Use this in migration to create child table and view.
|
9
|
+
# Options:
|
10
|
+
# [:parent]
|
11
|
+
# parent relation
|
12
|
+
# [:child_table_name]
|
13
|
+
# default is <tt>"#{child_view}_data"</tt>
|
14
|
+
def create_child(child_view, options)
|
15
|
+
raise 'Please call me with a parent, for example: create_child(:steam_locomotives, :parent => :locomotives)' unless options[:parent]
|
16
|
+
|
17
|
+
unqualified_child_view_name = Utils.extract_schema_qualified_name(child_view).identifier
|
18
|
+
|
19
|
+
parent_relation = options[:parent].to_s
|
20
|
+
if is_view?(parent_relation) # interpreted as inheritance chain deeper than two levels
|
21
|
+
parent_table = query("SELECT child_relation FROM updateable_views_inheritance WHERE child_aggregate_view = #{quote(parent_relation)}")[0][0]
|
22
|
+
else
|
23
|
+
parent_table = parent_relation
|
24
|
+
end
|
21
25
|
|
22
|
-
|
23
|
-
|
26
|
+
child_table = options[:table] || quote_table_name("#{child_view}_data")
|
27
|
+
child_table_pk = "#{unqualified_child_view_name.singularize}_id"
|
24
28
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
create_table(child_table, :id => false) do |t|
|
30
|
+
t.integer child_table_pk, :null => false
|
31
|
+
yield t
|
32
|
+
end
|
33
|
+
execute "ALTER TABLE #{child_table} ADD PRIMARY KEY (#{child_table_pk})"
|
34
|
+
execute "ALTER TABLE #{child_table} ADD FOREIGN KEY (#{child_table_pk})
|
35
|
+
REFERENCES #{parent_table} ON DELETE CASCADE ON UPDATE CASCADE"
|
32
36
|
|
33
|
-
|
34
|
-
|
37
|
+
create_child_view(parent_relation, child_view, child_table)
|
38
|
+
end
|
35
39
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
40
|
+
# Drop child view and table
|
41
|
+
def drop_child(child_view)
|
42
|
+
drop_view(child_view)
|
43
|
+
child_table = query("SELECT child_relation FROM updateable_views_inheritance WHERE child_aggregate_view = #{quote(child_view)}")[0][0]
|
44
|
+
drop_table(child_table)
|
45
|
+
execute "DELETE FROM updateable_views_inheritance WHERE child_aggregate_view = #{quote(child_view)}"
|
46
|
+
end
|
43
47
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
+
# Creates aggregate updateable view of parent and child relations. The convention for naming child tables is
|
49
|
+
# <tt>"#{child_view}_data"</tt>. If you don't follow it, supply +child_table_name+ as third argument.
|
50
|
+
def create_child_view(parent_table, child_view, child_table=nil)
|
51
|
+
child_table ||= child_view.to_s + "_data"
|
48
52
|
|
49
|
-
|
50
|
-
|
53
|
+
parent_columns = columns(parent_table)
|
54
|
+
child_columns = columns(child_table)
|
51
55
|
|
52
|
-
|
53
|
-
|
56
|
+
child_column_names = child_columns.collect{|c| c.name}
|
57
|
+
parent_column_names = parent_columns.collect{|c| c.name}
|
54
58
|
|
55
|
-
|
56
|
-
|
59
|
+
child_pk = pk_and_sequence_for(child_table)[0]
|
60
|
+
child_column_names.delete(child_pk)
|
57
61
|
|
58
|
-
|
59
|
-
|
62
|
+
parent_pk, parent_pk_seq = pk_and_sequence_for(parent_table)
|
63
|
+
parent_column_names.delete(parent_pk)
|
60
64
|
|
61
|
-
|
62
|
-
|
65
|
+
do_create_child_view(parent_table, parent_column_names, parent_pk, child_view, child_column_names, child_pk, child_table)
|
66
|
+
make_child_view_updateable(parent_table, parent_column_names, parent_pk, parent_pk_seq, child_view, child_column_names, child_pk, child_table)
|
63
67
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
68
|
+
# assign default values for table columns on the view - it is not automatic in Postgresql 8.1
|
69
|
+
set_defaults(child_view, parent_table)
|
70
|
+
set_defaults(child_view, child_table)
|
71
|
+
create_system_table_records(parent_table, child_view, child_table)
|
72
|
+
end
|
69
73
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
74
|
+
# Resets sequence to the max value of the table's pk if present respecting inheritance (i.e. one sequence can be shared by many tables).
|
75
|
+
def reset_pk_sequence!(table, pk = nil, sequence = nil)
|
76
|
+
parent = parent_table(table)
|
77
|
+
if parent
|
78
|
+
reset_pk_sequence!(parent, pk, sequence)
|
79
|
+
else
|
80
|
+
unless pk and sequence
|
81
|
+
default_pk, default_sequence = pk_and_sequence_for(table)
|
82
|
+
pk ||= default_pk
|
83
|
+
sequence ||= default_sequence
|
84
|
+
end
|
85
|
+
if pk
|
86
|
+
if sequence
|
87
|
+
select_value <<-end_sql, 'Reset sequence'
|
88
|
+
SELECT setval('#{sequence}', (SELECT COALESCE(MAX(#{pk})+(SELECT increment_by FROM #{sequence}), (SELECT min_value FROM #{sequence})) FROM #{table}), false)
|
89
|
+
end_sql
|
90
|
+
else
|
91
|
+
@logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
|
92
|
+
end
|
88
93
|
end
|
89
94
|
end
|
90
95
|
end
|
91
|
-
end
|
92
96
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
end
|
97
|
-
|
98
|
-
# Returns a relation's primary key and belonging sequence. If +relation+ is a table the result is its PK and sequence.
|
99
|
-
# When it is a view, PK and sequence of the table at the root of the inheritance chain are returned.
|
100
|
-
def pk_and_sequence_for(relation)
|
101
|
-
result = query(<<-end_sql, 'PK')[0]
|
102
|
-
SELECT attr.attname
|
103
|
-
FROM pg_attribute attr,
|
104
|
-
pg_constraint cons
|
105
|
-
WHERE cons.conrelid = attr.attrelid
|
106
|
-
AND cons.conrelid = '#{relation}'::regclass
|
107
|
-
AND cons.contype = 'p'
|
108
|
-
AND attr.attnum = ANY(cons.conkey)
|
109
|
-
end_sql
|
110
|
-
if result.nil? or result.empty?
|
111
|
-
parent = parent_table(relation)
|
112
|
-
pk_and_sequence_for(parent) if parent
|
113
|
-
else
|
114
|
-
# log(result[0], "PK for #{relation}") {}
|
115
|
-
[result[0], query("SELECT pg_get_serial_sequence('#{relation}', '#{result[0]}') ")[0][0]]
|
97
|
+
def primary_key(relation)
|
98
|
+
res = pk_and_sequence_for(relation)
|
99
|
+
res && res.first
|
116
100
|
end
|
117
|
-
rescue
|
118
|
-
nil
|
119
|
-
end
|
120
101
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
!result.empty?
|
144
|
-
end
|
102
|
+
# Returns a relation's primary key and belonging sequence. If +relation+ is a table the result is its PK and sequence.
|
103
|
+
# When it is a view, PK and sequence of the table at the root of the inheritance chain are returned.
|
104
|
+
def pk_and_sequence_for(relation)
|
105
|
+
result = query(<<-end_sql, 'PK')[0]
|
106
|
+
SELECT attr.attname
|
107
|
+
FROM pg_attribute attr,
|
108
|
+
pg_constraint cons
|
109
|
+
WHERE cons.conrelid = attr.attrelid
|
110
|
+
AND cons.conrelid = '#{relation}'::regclass
|
111
|
+
AND cons.contype = 'p'
|
112
|
+
AND attr.attnum = ANY(cons.conkey)
|
113
|
+
end_sql
|
114
|
+
if result.nil? or result.empty?
|
115
|
+
parent = parent_table(relation)
|
116
|
+
pk_and_sequence_for(parent) if parent
|
117
|
+
else
|
118
|
+
# log(result[0], "PK for #{relation}") {}
|
119
|
+
[result[0], query("SELECT pg_get_serial_sequence('#{relation}', '#{result[0]}') ")[0][0]]
|
120
|
+
end
|
121
|
+
rescue
|
122
|
+
nil
|
123
|
+
end
|
145
124
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
SELECT child_aggregate_view
|
150
|
-
FROM updateable_views_inheritance
|
151
|
-
WHERE parent_relation = '#{parent_relation}'
|
152
|
-
end_sql
|
153
|
-
children_views.each do |cv|
|
154
|
-
remove_parent_and_children_views(cv)
|
155
|
-
# drop the view only if it wasn't dropped beforehand in recursive call from other method.
|
156
|
-
drop_view(cv) if is_view?(cv)
|
125
|
+
# Drops a view from the database.
|
126
|
+
def drop_view(name)
|
127
|
+
execute "DROP VIEW #{name}"
|
157
128
|
end
|
158
|
-
drop_view(parent_relation) if is_view?(parent_relation)
|
159
|
-
end
|
160
129
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
130
|
+
# Return the list of all views in the schema search path.
|
131
|
+
def views(name=nil)
|
132
|
+
schemas = schema_search_path.split(/,\s*/).map { |p| quote(p) }.join(',')
|
133
|
+
query(<<-SQL, name).map { |row| row[0] }
|
134
|
+
SELECT viewname
|
135
|
+
FROM pg_views
|
136
|
+
WHERE schemaname IN (#{schemas})
|
137
|
+
SQL
|
138
|
+
end
|
166
139
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
end_sql
|
176
|
-
|
177
|
-
#if the parent is in the middle of the inheritance chain, it's a view that should be rebuilt as well
|
178
|
-
parent = query(<<-end_sql)[0]
|
179
|
-
SELECT parent_relation, child_aggregate_view, child_relation
|
180
|
-
FROM updateable_views_inheritance
|
181
|
-
WHERE child_aggregate_view = '#{parent_relation}'
|
182
|
-
end_sql
|
183
|
-
create_child_view(parent[0], parent[1], parent[2]) if (parent && !parent.empty?)
|
184
|
-
|
185
|
-
children.each do |child|
|
186
|
-
create_child_view(child[0], child[1], child[2])
|
187
|
-
rebuild_parent_and_children_views(child[1])
|
140
|
+
# Checks whether relation +name+ is a view.
|
141
|
+
def is_view?(name)
|
142
|
+
result = query(<<-SQL, name).map { |row| row[0] }
|
143
|
+
SELECT viewname
|
144
|
+
FROM pg_views
|
145
|
+
WHERE viewname = '#{name}'
|
146
|
+
SQL
|
147
|
+
!result.empty?
|
188
148
|
end
|
189
|
-
end
|
190
149
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
all_columns = leaves_relations.map{|rel| columns(rel)}.flatten
|
203
|
-
columns_hash = {}
|
204
|
-
conflict_column_names = []
|
205
|
-
all_columns.each do |col|
|
206
|
-
c = columns_hash[col.name]
|
207
|
-
if(c && col.sql_type != c.sql_type)
|
208
|
-
conflict_column_names << col.name
|
209
|
-
else
|
210
|
-
columns_hash[col.name] = col
|
150
|
+
# Recursively delete +parent_relation+ (if it is a view) and the children views the depend on it.
|
151
|
+
def remove_parent_and_children_views(parent_relation)
|
152
|
+
children_views = query(<<-end_sql).map{|row| row[0]}
|
153
|
+
SELECT child_aggregate_view
|
154
|
+
FROM updateable_views_inheritance
|
155
|
+
WHERE parent_relation = '#{parent_relation}'
|
156
|
+
end_sql
|
157
|
+
children_views.each do |cv|
|
158
|
+
remove_parent_and_children_views(cv)
|
159
|
+
# drop the view only if it wasn't dropped beforehand in recursive call from other method.
|
160
|
+
drop_view(cv) if is_view?(cv)
|
211
161
|
end
|
162
|
+
drop_view(parent_relation) if is_view?(parent_relation)
|
212
163
|
end
|
213
|
-
conflict_column_names = conflict_column_names.uniq.sort if !conflict_column_names.empty?
|
214
|
-
sorted_column_names = (columns_for_view + columns_hash.keys.sort).uniq
|
215
|
-
parent_klass_name = Tutuf::ClassTableReflection.get_klass_for_table(parent_relation)
|
216
|
-
quoted_inheritance_column = quote_column_name(parent_klass_name.inheritance_column)
|
217
|
-
queries = relations.map{|rel| generate_single_table_inheritanche_union_clause(rel, sorted_column_names, conflict_column_names, columns_hash, quoted_inheritance_column)}
|
218
|
-
unioin_clauses = queries.join("\n UNION ")
|
219
|
-
execute <<-end_sql
|
220
|
-
CREATE VIEW #{sti_aggregate_view} AS (
|
221
|
-
#{unioin_clauses}
|
222
|
-
)
|
223
|
-
end_sql
|
224
|
-
end
|
225
|
-
|
226
|
-
# Recreates the Single_Table_Inheritanche-like aggregate view +sti_aggregate_view+
|
227
|
-
# for +parent_relation+ and all its descendants.
|
228
|
-
def rebuild_single_table_inheritance_view(sti_aggregate_view, parent_relation, columns_for_view = nil)
|
229
|
-
drop_view(sti_aggregate_view)
|
230
|
-
create_single_table_inheritance_view(sti_aggregate_view, parent_relation, columns_for_view)
|
231
|
-
end
|
232
164
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
def table_exists_with_updateable_views_inheritance_support?(name)
|
239
|
-
is_view?(name) ? true : table_exists_without_updateable_views_inheritance_support?(name)
|
240
|
-
end
|
241
|
-
alias_method_chain :table_exists?, :updateable_views_inheritance_support
|
242
|
-
|
243
|
-
module Tutuf #:nodoc:
|
244
|
-
class ClassTableReflection
|
245
|
-
class << self
|
246
|
-
# Returns all models' class objects that are ActiveRecord::Base descendants
|
247
|
-
def all_db_klasses
|
248
|
-
return @@klasses if defined?(@@klasses)
|
249
|
-
@@klasses = []
|
250
|
-
# load model classes so that inheritance_column is set correctly where defined
|
251
|
-
model_filenames.collect{|m| load "#{Rails.root}/app/models/#{m}";m.match(%r{([^/]+?)\.rb$})[1].camelize.constantize }.each do |klass|
|
252
|
-
@@klasses << klass if klass < ActiveRecord::Base
|
253
|
-
end
|
254
|
-
@@klasses.uniq
|
255
|
-
end
|
165
|
+
# Recreates all views in all hierarchy chains
|
166
|
+
def rebuild_all_parent_and_children_views
|
167
|
+
parent_relations = select_values('SELECT DISTINCT parent_relation FROM updateable_views_inheritance')
|
168
|
+
parent_relations.each { |parent_relation| rebuild_parent_and_children_views(parent_relation) }
|
169
|
+
end
|
256
170
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
171
|
+
# Recreates views in the part of the hierarchy chain starting from the +parent_relation+.
|
172
|
+
def rebuild_parent_and_children_views(parent_relation)
|
173
|
+
# Current implementation is not very efficient - it can drop and recreate one and the same view in the bottom of the hierarchy many times.
|
174
|
+
remove_parent_and_children_views(parent_relation)
|
175
|
+
children = query(<<-end_sql)
|
176
|
+
SELECT parent_relation, child_aggregate_view, child_relation
|
177
|
+
FROM updateable_views_inheritance
|
178
|
+
WHERE parent_relation = '#{parent_relation}'
|
179
|
+
end_sql
|
261
180
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
end
|
270
|
-
@@tables_klasses
|
271
|
-
end
|
181
|
+
#if the parent is in the middle of the inheritance chain, it's a view that should be rebuilt as well
|
182
|
+
parent = query(<<-end_sql)[0]
|
183
|
+
SELECT parent_relation, child_aggregate_view, child_relation
|
184
|
+
FROM updateable_views_inheritance
|
185
|
+
WHERE child_aggregate_view = '#{parent_relation}'
|
186
|
+
end_sql
|
187
|
+
create_child_view(parent[0], parent[1], parent[2]) if (parent && !parent.empty?)
|
272
188
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
end
|
189
|
+
children.each do |child|
|
190
|
+
create_child_view(child[0], child[1], child[2])
|
191
|
+
rebuild_parent_and_children_views(child[1])
|
277
192
|
end
|
278
193
|
end
|
279
|
-
end
|
280
194
|
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
195
|
+
# Creates Single Table Inheritanche-like aggregate view called +sti_aggregate_view+
|
196
|
+
# for +parent_relation+ and all its descendants. <i>The view isn't updateable.</i>
|
197
|
+
# The order of all or just the first few columns in the aggregate view can be explicitly set
|
198
|
+
# by passing array of column names as third argument.
|
199
|
+
# If there are columns with the same name but different types in two or more relations
|
200
|
+
# they will appear as a single column of type +text+ in the view.
|
201
|
+
def create_single_table_inheritance_view(sti_aggregate_view, parent_relation, columns_for_view = nil)
|
202
|
+
columns_for_view ||= []
|
203
|
+
relations_heirarchy = get_view_hierarchy_for(parent_relation)
|
204
|
+
relations = relations_heirarchy.flatten
|
205
|
+
leaves_relations = get_leaves_relations(relations_heirarchy)
|
206
|
+
all_columns = leaves_relations.map{|rel| columns(rel)}.flatten
|
207
|
+
columns_hash = {}
|
208
|
+
conflict_column_names = []
|
209
|
+
all_columns.each do |col|
|
210
|
+
c = columns_hash[col.name]
|
211
|
+
if(c && col.sql_type != c.sql_type)
|
212
|
+
conflict_column_names << col.name
|
213
|
+
else
|
214
|
+
columns_hash[col.name] = col
|
215
|
+
end
|
216
|
+
end
|
217
|
+
conflict_column_names = conflict_column_names.uniq.sort if !conflict_column_names.empty?
|
218
|
+
sorted_column_names = (columns_for_view + columns_hash.keys.sort).uniq
|
219
|
+
parent_klass_name = Tutuf::ClassTableReflection.get_klass_for_table(parent_relation)
|
220
|
+
quoted_inheritance_column = quote_column_name(parent_klass_name.inheritance_column)
|
221
|
+
queries = relations.map{|rel| generate_single_table_inheritanche_union_clause(rel, sorted_column_names, conflict_column_names, columns_hash, quoted_inheritance_column)}
|
222
|
+
unioin_clauses = queries.join("\n UNION ")
|
285
223
|
execute <<-end_sql
|
286
|
-
CREATE
|
287
|
-
|
288
|
-
#{ view_columns.join(",") }
|
289
|
-
FROM #{parent_table} parent
|
290
|
-
INNER JOIN #{child_table} child
|
291
|
-
ON ( parent.#{parent_pk}=child.#{child_pk} )
|
224
|
+
CREATE VIEW #{sti_aggregate_view} AS (
|
225
|
+
#{unioin_clauses}
|
292
226
|
)
|
293
227
|
end_sql
|
294
228
|
end
|
295
229
|
|
296
|
-
#
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
CREATE OR REPLACE RULE #{quote_column_name("#{child_view}_insert")} AS
|
303
|
-
ON INSERT TO #{child_view} DO INSTEAD (
|
304
|
-
INSERT INTO #{parent_table}
|
305
|
-
( #{ [parent_pk, parent_columns].flatten.join(", ") } )
|
306
|
-
VALUES( DEFAULT #{ parent_columns.empty? ? '' : ' ,' + parent_columns.collect{ |col| "NEW." + col}.join(", ") } ) ;
|
307
|
-
INSERT INTO #{child_table}
|
308
|
-
( #{ [child_pk, child_columns].flatten.join(",")} )
|
309
|
-
VALUES( currval('#{parent_pk_seq}') #{ child_columns.empty? ? '' : ' ,' + child_columns.collect{ |col| "NEW." + col}.join(", ") } )
|
310
|
-
#{insert_returning_clause(parent_pk, child_pk, child_view) if supports_insert_with_returning?}
|
311
|
-
)
|
312
|
-
end_sql
|
313
|
-
|
314
|
-
# delete
|
315
|
-
execute <<-end_sql
|
316
|
-
CREATE OR REPLACE RULE #{quote_column_name("#{child_view}_delete")} AS
|
317
|
-
ON DELETE TO #{child_view} DO INSTEAD
|
318
|
-
DELETE FROM #{parent_table} WHERE #{parent_pk} = OLD.#{parent_pk}
|
319
|
-
end_sql
|
230
|
+
# Recreates the Single_Table_Inheritanche-like aggregate view +sti_aggregate_view+
|
231
|
+
# for +parent_relation+ and all its descendants.
|
232
|
+
def rebuild_single_table_inheritance_view(sti_aggregate_view, parent_relation, columns_for_view = nil)
|
233
|
+
drop_view(sti_aggregate_view)
|
234
|
+
create_single_table_inheritance_view(sti_aggregate_view, parent_relation, columns_for_view)
|
235
|
+
end
|
320
236
|
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
ON UPDATE TO #{child_view} DO INSTEAD (
|
325
|
-
#{ parent_columns.empty? ? '':
|
326
|
-
"UPDATE #{parent_table}
|
327
|
-
SET #{ parent_columns.collect{ |col| col + "= NEW." +col }.join(", ") }
|
328
|
-
WHERE #{parent_pk} = OLD.#{parent_pk};"}
|
329
|
-
#{ child_columns.empty? ? '':
|
330
|
-
"UPDATE #{child_table}
|
331
|
-
SET #{ child_columns.collect{ |col| col + " = NEW." +col }.join(", ") }
|
332
|
-
WHERE #{child_pk} = OLD.#{parent_pk}"
|
333
|
-
}
|
334
|
-
)
|
335
|
-
end_sql
|
237
|
+
# Overriden - it must return false, otherwise deleting fixtures won't work
|
238
|
+
def supports_disable_referential_integrity?
|
239
|
+
false
|
336
240
|
end
|
337
241
|
|
338
|
-
def
|
339
|
-
|
340
|
-
.reject { |c| c.name == parent_pk}
|
341
|
-
.map { |c| "CAST (NULL AS #{c.sql_type})" }
|
342
|
-
.join(", ")
|
343
|
-
"RETURNING #{child_pk}, #{columns_cast_to_null}"
|
242
|
+
def table_exists_with_updateable_views_inheritance_support?(name)
|
243
|
+
is_view?(name) ? true : table_exists_without_updateable_views_inheritance_support?(name)
|
344
244
|
end
|
245
|
+
alias_method_chain :table_exists?, :updateable_views_inheritance_support
|
246
|
+
|
247
|
+
module Tutuf #:nodoc:
|
248
|
+
class ClassTableReflection
|
249
|
+
class << self
|
250
|
+
# Returns all models' class objects that are ActiveRecord::Base descendants
|
251
|
+
def all_db_klasses
|
252
|
+
return @@klasses if defined?(@@klasses)
|
253
|
+
@@klasses = []
|
254
|
+
# load model classes so that inheritance_column is set correctly where defined
|
255
|
+
model_filenames.collect{|m| load "#{Rails.root}/app/models/#{m}";m.match(%r{([^/]+?)\.rb$})[1].camelize.constantize }.each do |klass|
|
256
|
+
@@klasses << klass if klass < ActiveRecord::Base
|
257
|
+
end
|
258
|
+
@@klasses.uniq
|
259
|
+
end
|
345
260
|
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
261
|
+
# Returns the class object for +table_name+
|
262
|
+
def get_klass_for_table(table_name)
|
263
|
+
klass_for_tables()[table_name.to_s]
|
264
|
+
end
|
265
|
+
|
266
|
+
# Returns hash with tables and thier corresponding class.
|
267
|
+
# {table_name1 => ClassName1, ...}
|
268
|
+
def klass_for_tables
|
269
|
+
return @@tables_klasses if defined?(@@tables_klasses)
|
270
|
+
@@tables_klasses = {}
|
271
|
+
all_db_klasses.each do |klass|
|
272
|
+
@@tables_klasses[klass.table_name] = klass if klass.respond_to?(:table_name)
|
273
|
+
end
|
274
|
+
@@tables_klasses
|
275
|
+
end
|
276
|
+
|
277
|
+
# Returns filenames for models in the current Rails application
|
278
|
+
def model_filenames
|
279
|
+
Dir.chdir("#{Rails.root}/app/models"){ Dir["**/*.rb"] }
|
280
|
+
end
|
351
281
|
end
|
352
282
|
end
|
353
283
|
end
|
354
284
|
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
285
|
+
private
|
286
|
+
|
287
|
+
def do_create_child_view(parent_table, parent_columns, parent_pk, child_view, child_columns, child_pk, child_table)
|
288
|
+
view_columns = parent_columns + child_columns
|
289
|
+
execute <<-end_sql
|
290
|
+
CREATE OR REPLACE VIEW #{child_view} AS (
|
291
|
+
SELECT parent.#{parent_pk},
|
292
|
+
#{ view_columns.join(",") }
|
293
|
+
FROM #{parent_table} parent
|
294
|
+
INNER JOIN #{child_table} child
|
295
|
+
ON ( parent.#{parent_pk}=child.#{child_pk} )
|
296
|
+
)
|
297
|
+
end_sql
|
298
|
+
end
|
299
|
+
|
300
|
+
# Creates rules for +INSERT+, +UPDATE+ and +DELETE+ on the view
|
301
|
+
def make_child_view_updateable(parent_table, parent_columns, parent_pk, parent_pk_seq, child_view, child_columns, child_pk, child_table)
|
302
|
+
# insert
|
303
|
+
# NEW.#{parent_pk} can be explicitly specified and when it is null every call to it increments the sequence.
|
304
|
+
# Setting the sequence to its value (explicitly supplied or the default) covers both cases.
|
305
|
+
execute <<-end_sql
|
306
|
+
CREATE OR REPLACE RULE #{quote_column_name("#{child_view}_insert")} AS
|
307
|
+
ON INSERT TO #{child_view} DO INSTEAD (
|
308
|
+
INSERT INTO #{parent_table}
|
309
|
+
( #{ [parent_pk, parent_columns].flatten.join(", ") } )
|
310
|
+
VALUES( DEFAULT #{ parent_columns.empty? ? '' : ' ,' + parent_columns.collect{ |col| "NEW." + col}.join(", ") } ) ;
|
311
|
+
INSERT INTO #{child_table}
|
312
|
+
( #{ [child_pk, child_columns].flatten.join(",")} )
|
313
|
+
VALUES( currval('#{parent_pk_seq}') #{ child_columns.empty? ? '' : ' ,' + child_columns.collect{ |col| "NEW." + col}.join(", ") } )
|
314
|
+
#{insert_returning_clause(parent_pk, child_pk, child_view)}
|
315
|
+
)
|
316
|
+
end_sql
|
317
|
+
|
318
|
+
# delete
|
319
|
+
execute <<-end_sql
|
320
|
+
CREATE OR REPLACE RULE #{quote_column_name("#{child_view}_delete")} AS
|
321
|
+
ON DELETE TO #{child_view} DO INSTEAD
|
322
|
+
DELETE FROM #{parent_table} WHERE #{parent_pk} = OLD.#{parent_pk}
|
323
|
+
end_sql
|
324
|
+
|
325
|
+
# update
|
326
|
+
execute <<-end_sql
|
327
|
+
CREATE OR REPLACE RULE #{quote_column_name("#{child_view}_update")} AS
|
328
|
+
ON UPDATE TO #{child_view} DO INSTEAD (
|
329
|
+
#{ parent_columns.empty? ? '':
|
330
|
+
"UPDATE #{parent_table}
|
331
|
+
SET #{ parent_columns.collect{ |col| col + "= NEW." + col }.join(", ") }
|
332
|
+
WHERE #{parent_pk} = OLD.#{parent_pk};"}
|
333
|
+
#{ child_columns.empty? ? '':
|
334
|
+
"UPDATE #{child_table}
|
335
|
+
SET #{ child_columns.collect{ |col| col + " = NEW." + col }.join(", ") }
|
336
|
+
WHERE #{child_pk} = OLD.#{parent_pk}"
|
337
|
+
}
|
338
|
+
)
|
339
|
+
end_sql
|
340
|
+
end
|
341
|
+
|
342
|
+
def insert_returning_clause(parent_pk, child_pk, child_view)
|
343
|
+
columns_cast_to_null = columns(child_view)
|
344
|
+
.reject { |c| c.name == parent_pk}
|
345
|
+
.map { |c| "CAST (NULL AS #{c.sql_type})" }
|
346
|
+
.join(", ")
|
347
|
+
"RETURNING #{child_pk}, #{columns_cast_to_null}"
|
348
|
+
end
|
349
|
+
|
350
|
+
# Set default values from the table columns for a view
|
351
|
+
def set_defaults(view_name, table_name)
|
352
|
+
column_definitions(table_name).each do |column_name, type, default, notnull|
|
353
|
+
if !default.nil?
|
354
|
+
execute("ALTER TABLE #{quote_table_name(view_name)} ALTER #{quote_column_name(column_name)} SET DEFAULT #{default}")
|
355
|
+
end
|
356
|
+
end
|
368
357
|
end
|
369
|
-
end
|
370
358
|
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
SELECT parent_relation
|
359
|
+
def create_system_table_records(parent_relation, child_aggregate_view, child_relation)
|
360
|
+
parent_relation, child_aggregate_view, child_relation = [parent_relation, child_aggregate_view, child_relation].collect{|rel| quote(rel.to_s)}
|
361
|
+
exists = query <<-end_sql
|
362
|
+
SELECT parent_relation, child_aggregate_view, child_relation
|
375
363
|
FROM updateable_views_inheritance
|
376
|
-
WHERE
|
364
|
+
WHERE parent_relation = #{parent_relation}
|
365
|
+
AND child_aggregate_view = #{child_aggregate_view}
|
366
|
+
AND child_relation = #{child_relation}
|
377
367
|
end_sql
|
378
|
-
|
368
|
+
# log "res: #{exists}"
|
369
|
+
if exists.nil? or exists.empty?
|
370
|
+
execute "INSERT INTO updateable_views_inheritance (parent_relation, child_aggregate_view, child_relation)" +
|
371
|
+
"VALUES( #{parent_relation}, #{child_aggregate_view}, #{child_relation} )"
|
372
|
+
end
|
379
373
|
end
|
380
|
-
end
|
381
374
|
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
def get_view_hierarchy_for(parent_relation)
|
392
|
-
hierarchy = []
|
393
|
-
children = query(<<-end_sql)
|
394
|
-
SELECT parent_relation, child_aggregate_view, child_relation
|
395
|
-
FROM updateable_views_inheritance
|
396
|
-
WHERE parent_relation = '#{parent_relation}'
|
397
|
-
end_sql
|
398
|
-
children.each do |child|
|
399
|
-
hierarchy << [child[1], *get_view_hierarchy_for(child[1])]
|
375
|
+
def parent_table(relation)
|
376
|
+
if table_exists?('updateable_views_inheritance')
|
377
|
+
res = query(<<-end_sql, 'Parent relation')[0]
|
378
|
+
SELECT parent_relation
|
379
|
+
FROM updateable_views_inheritance
|
380
|
+
WHERE child_aggregate_view = '#{relation}'
|
381
|
+
end_sql
|
382
|
+
res[0] if res
|
383
|
+
end
|
400
384
|
end
|
401
|
-
hierarchy
|
402
|
-
end
|
403
385
|
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
386
|
+
# Single Table Inheritance Aggregate View
|
387
|
+
|
388
|
+
# Nested list for the +parent_relation+ inheritance hierarchy
|
389
|
+
# Every descendant relation is presented as an array with relation's name as first element
|
390
|
+
# and the other elements are the relation's children presented in the same way as lists.
|
391
|
+
# For example:
|
392
|
+
# [[child_view1, [grandchild11,[...]], [grandchild12]],
|
393
|
+
# [child_view2, [...]
|
394
|
+
# ]
|
395
|
+
def get_view_hierarchy_for(parent_relation)
|
396
|
+
hierarchy = []
|
397
|
+
children = query(<<-end_sql)
|
398
|
+
SELECT parent_relation, child_aggregate_view, child_relation
|
399
|
+
FROM updateable_views_inheritance
|
400
|
+
WHERE parent_relation = '#{parent_relation}'
|
401
|
+
end_sql
|
402
|
+
children.each do |child|
|
403
|
+
hierarchy << [child[1], *get_view_hierarchy_for(child[1])]
|
404
|
+
end
|
405
|
+
hierarchy
|
413
406
|
end
|
414
|
-
end
|
415
407
|
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
408
|
+
def get_leaves_relations(hierarchy)
|
409
|
+
return [] if hierarchy.nil? || hierarchy.empty?
|
410
|
+
head, hierarchy = hierarchy.first, hierarchy[1..(hierarchy.size)]
|
411
|
+
if(head.is_a? Array)
|
412
|
+
return (get_leaves_relations(head) + get_leaves_relations(hierarchy)).compact
|
413
|
+
elsif(hierarchy.nil? || hierarchy.empty?)
|
414
|
+
return [head]
|
415
|
+
else
|
416
|
+
return get_leaves_relations(hierarchy).compact
|
424
417
|
end
|
425
|
-
statement = " AS #{col_name}"
|
426
|
-
statement = "#{value} #{statement}"
|
427
|
-
arr << " #{statement}"
|
428
418
|
end
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
419
|
+
|
420
|
+
def generate_single_table_inheritanche_union_clause(rel, column_names, conflict_column_names, columns_hash, quoted_inheritance_column)
|
421
|
+
relation_columns = columns(rel).collect{|c| c.name}
|
422
|
+
columns_select = column_names.inject([]) do |arr, col_name|
|
423
|
+
sql_type = conflict_column_names.include?(col_name) ? 'text' : columns_hash[col_name].sql_type
|
424
|
+
value = "NULL::#{sql_type}"
|
425
|
+
if(relation_columns.include?(col_name))
|
426
|
+
value = col_name
|
427
|
+
value = "#{value}::text" if conflict_column_names.include?(col_name)
|
428
|
+
end
|
429
|
+
statement = " AS #{col_name}"
|
430
|
+
statement = "#{value} #{statement}"
|
431
|
+
arr << " #{statement}"
|
432
|
+
end
|
433
|
+
columns_select = columns_select.join(", ")
|
434
|
+
rel_klass_name = Tutuf::ClassTableReflection.get_klass_for_table(rel)
|
435
|
+
where_clause = " WHERE #{quoted_inheritance_column} = '#{rel_klass_name}'"
|
436
|
+
["SELECT", columns_select, "FROM #{rel} #{where_clause}"].join(" ")
|
437
|
+
end
|
438
|
+
end
|
434
439
|
end
|
435
440
|
end
|
436
441
|
end
|
data/test/content_test.rb
CHANGED
@@ -3,9 +3,6 @@ require File.join(File.dirname(__FILE__), 'test_helper')
|
|
3
3
|
class UpdateableViewsInheritanceContentTest < ActiveSupport::TestCase
|
4
4
|
def setup
|
5
5
|
ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 5)
|
6
|
-
end
|
7
|
-
|
8
|
-
def teardown
|
9
6
|
ActiveRecord::FixtureSet.reset_cache
|
10
7
|
end
|
11
8
|
|
data/test/deep_hierarchy_test.rb
CHANGED
@@ -15,7 +15,7 @@ class DeepHierarchyTest < ActiveSupport::TestCase
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def test_deeper_hierarchy
|
18
|
-
assert_equal [["boats"], ["railed_vehicles", ["trains", ["
|
18
|
+
assert_equal [["boats"], ["railed_vehicles", ["trains", ["steam_trains"], ["rack_trains"], ["electric_trains", ["maglev_trains"]]]], ["wheeled_vehicles", ["bicycles"], ["cars"]]].sort,
|
19
19
|
@connection.send(:get_view_hierarchy_for, :vehicles).sort
|
20
20
|
end
|
21
21
|
|
@@ -4,4 +4,4 @@
|
|
4
4
|
# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
|
5
5
|
|
6
6
|
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
|
7
|
-
|
7
|
+
Rails.backtrace_cleaner.remove_silencers!
|
data/test/schema_test.rb
CHANGED
data/test/test_helper.rb
CHANGED
@@ -5,10 +5,6 @@ require File.expand_path("../dummy/config/environment.rb", __FILE__)
|
|
5
5
|
require 'rails/test_help'
|
6
6
|
require 'updateable_views_inheritance'
|
7
7
|
|
8
|
-
# get full stack trace on errors
|
9
|
-
require "minitest/reporters"
|
10
|
-
Minitest::Reporters.use!
|
11
|
-
|
12
8
|
begin
|
13
9
|
if RUBY_VERSION > "2"
|
14
10
|
require 'byebug'
|
@@ -18,12 +18,11 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
19
19
|
s.require_paths = ["lib"]
|
20
20
|
|
21
|
-
s.add_dependency "activerecord", "
|
21
|
+
s.add_dependency "activerecord", "~> 4.2.8"
|
22
22
|
s.add_dependency "pg"
|
23
23
|
|
24
24
|
s.add_development_dependency 'minitest'
|
25
|
-
s.add_development_dependency '
|
26
|
-
s.add_development_dependency "rails", ' ~> 4.1.16' # ">= 4.0", "< 5"
|
25
|
+
s.add_development_dependency "rails", ' ~> 4.2.8'
|
27
26
|
s.add_development_dependency "bundler", "~> 1.3"
|
28
27
|
s.add_development_dependency "rake"
|
29
28
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: updateable_views_inheritance
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.4.
|
4
|
+
version: 1.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sava Chankov
|
@@ -9,28 +9,22 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2017-03-
|
12
|
+
date: 2017-03-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
|
-
- - "
|
19
|
-
- !ruby/object:Gem::Version
|
20
|
-
version: '4.0'
|
21
|
-
- - "<"
|
18
|
+
- - "~>"
|
22
19
|
- !ruby/object:Gem::Version
|
23
|
-
version:
|
20
|
+
version: 4.2.8
|
24
21
|
type: :runtime
|
25
22
|
prerelease: false
|
26
23
|
version_requirements: !ruby/object:Gem::Requirement
|
27
24
|
requirements:
|
28
|
-
- - "
|
29
|
-
- !ruby/object:Gem::Version
|
30
|
-
version: '4.0'
|
31
|
-
- - "<"
|
25
|
+
- - "~>"
|
32
26
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
27
|
+
version: 4.2.8
|
34
28
|
- !ruby/object:Gem::Dependency
|
35
29
|
name: pg
|
36
30
|
requirement: !ruby/object:Gem::Requirement
|
@@ -59,34 +53,20 @@ dependencies:
|
|
59
53
|
- - ">="
|
60
54
|
- !ruby/object:Gem::Version
|
61
55
|
version: '0'
|
62
|
-
- !ruby/object:Gem::Dependency
|
63
|
-
name: minitest-reporters
|
64
|
-
requirement: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
|
-
type: :development
|
70
|
-
prerelease: false
|
71
|
-
version_requirements: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
76
56
|
- !ruby/object:Gem::Dependency
|
77
57
|
name: rails
|
78
58
|
requirement: !ruby/object:Gem::Requirement
|
79
59
|
requirements:
|
80
60
|
- - "~>"
|
81
61
|
- !ruby/object:Gem::Version
|
82
|
-
version: 4.
|
62
|
+
version: 4.2.8
|
83
63
|
type: :development
|
84
64
|
prerelease: false
|
85
65
|
version_requirements: !ruby/object:Gem::Requirement
|
86
66
|
requirements:
|
87
67
|
- - "~>"
|
88
68
|
- !ruby/object:Gem::Version
|
89
|
-
version: 4.
|
69
|
+
version: 4.2.8
|
90
70
|
- !ruby/object:Gem::Dependency
|
91
71
|
name: bundler
|
92
72
|
requirement: !ruby/object:Gem::Requirement
|