updateable_views_inheritance 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/.gitignore +3 -0
  2. data/Gemfile +4 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +110 -0
  5. data/Rakefile +35 -0
  6. data/doc/template/horo.rb +613 -0
  7. data/lib/generators/updateable_views_inheritance_migration/USAGE +8 -0
  8. data/lib/generators/updateable_views_inheritance_migration/templates/migration.rb +17 -0
  9. data/lib/generators/updateable_views_inheritance_migration/uvi_migration_generator.rb +12 -0
  10. data/lib/updateable_views_inheritance.rb +5 -0
  11. data/lib/updateable_views_inheritance/active_record.rb +16 -0
  12. data/lib/updateable_views_inheritance/postgresql_adapter.rb +450 -0
  13. data/lib/updateable_views_inheritance/version.rb +3 -0
  14. data/tasks/updateable_views_inheritance_tasks.rake +19 -0
  15. data/test/app/models/bicycle.rb +3 -0
  16. data/test/app/models/boat.rb +3 -0
  17. data/test/app/models/car.rb +3 -0
  18. data/test/app/models/electric_locomotive.rb +3 -0
  19. data/test/app/models/electric_train.rb +3 -0
  20. data/test/app/models/locomotive.rb +3 -0
  21. data/test/app/models/maglev_locomotive.rb +3 -0
  22. data/test/app/models/maglev_train.rb +3 -0
  23. data/test/app/models/rack_locomotive.rb +3 -0
  24. data/test/app/models/rack_train.rb +3 -0
  25. data/test/app/models/railed_vehicle.rb +3 -0
  26. data/test/app/models/steam_locomotive.rb +3 -0
  27. data/test/app/models/steam_train.rb +3 -0
  28. data/test/app/models/train.rb +3 -0
  29. data/test/app/models/vehicle.rb +3 -0
  30. data/test/app/models/wheeled_vehicle.rb +3 -0
  31. data/test/config/database.yml +19 -0
  32. data/test/config/environment.rb +14 -0
  33. data/test/config/routes.rb +3 -0
  34. data/test/config/schema.rb +3 -0
  35. data/test/content_test.rb +66 -0
  36. data/test/database.yml +7 -0
  37. data/test/deep_hierarchy_test.rb +64 -0
  38. data/test/fixtures/bicycles.yml +6 -0
  39. data/test/fixtures/boats.yml +6 -0
  40. data/test/fixtures/cars.yml +12 -0
  41. data/test/fixtures/electric_locomotives.yml +6 -0
  42. data/test/fixtures/electric_trains.yml +7 -0
  43. data/test/fixtures/maglev_locomotives.yml +7 -0
  44. data/test/fixtures/maglev_trains.yml +8 -0
  45. data/test/fixtures/migrations/1_add_class_table_inheritance.rb +15 -0
  46. data/test/fixtures/migrations/2_create_with_default_table.rb +19 -0
  47. data/test/fixtures/migrations/3_create_with_explicit_table.rb +11 -0
  48. data/test/fixtures/migrations/4_create_deeper_hierarchy.rb +11 -0
  49. data/test/fixtures/migrations/5_default_column_values.rb +13 -0
  50. data/test/fixtures/migrations/6_single_table_inheritance_view.rb +10 -0
  51. data/test/fixtures/migrations/7_second_deep_hierarchy.rb +70 -0
  52. data/test/fixtures/migrations/8_second_single_table_inheritance_view.rb +10 -0
  53. data/test/fixtures/rack_trains.yml +7 -0
  54. data/test/fixtures/steam_locomotives.yml +7 -0
  55. data/test/fixtures/steam_trains.yml +8 -0
  56. data/test/migration_test.rb +30 -0
  57. data/test/schema_test.rb +179 -0
  58. data/test/single_table_inheritance.rb +141 -0
  59. data/test/test_helper.rb +80 -0
  60. data/updateable_views_inheritance.gemspec +26 -0
  61. metadata +219 -0
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Creates a migration for adding a table used by the updateable_views_inheritance gem.
3
+
4
+ The generator takes a migration name as its argument. The migration name may be
5
+ given in CamelCase or under_score.
6
+
7
+ Example:
8
+ ./script/generate updateable_views_inheritance_migration AddUvi
@@ -0,0 +1,17 @@
1
+ class <%= class_name %> < ActiveRecord::Migration
2
+ def self.up
3
+ create_table(:updateable_views_inheritance, :id => false) do |t|
4
+ t.column :parent_relation, :string, :null => false
5
+ t.column :child_aggregate_view, :string, :null => false
6
+ t.column :child_relation, :string, :null => false
7
+ end
8
+
9
+ execute "ALTER TABLE updateable_views_inheritance ADD PRIMARY KEY (parent_relation, child_aggregate_view, child_relation)"
10
+ execute "ALTER TABLE updateable_views_inheritance ADD UNIQUE (child_aggregate_view)"
11
+ execute "ALTER TABLE updateable_views_inheritance ADD UNIQUE (parent_relation, child_aggregate_view)"
12
+ end
13
+
14
+ def self.down
15
+ drop_table :updateable_views_inheritance
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ class ClassTableInheritanceMigrationGenerator < Rails::Generator::NamedBase #:nodoc:
2
+ def initialize(runtime_args, runtime_options = {})
3
+ runtime_args << 'add_updateable_views_inheritance_migration' if runtime_args.empty?
4
+ super
5
+ end
6
+
7
+ def manifest
8
+ record do |m|
9
+ m.migration_template 'migration.rb', 'db/migrate'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ require 'active_record'
2
+ require 'active_record/connection_adapters/postgresql_adapter'
3
+ require "updateable_views_inheritance/version"
4
+ require "updateable_views_inheritance/active_record"
5
+ require 'updateable_views_inheritance/postgresql_adapter'
@@ -0,0 +1,16 @@
1
+ module ActiveRecord #:nodoc:
2
+ class Base #:nodoc:
3
+ class << self
4
+ private
5
+ def instantiate_with_updateable_views_inheritance_support( record )
6
+ object = instantiate_without_updateable_views_inheritance_support( record )
7
+ if object.class.name == self.name
8
+ object
9
+ else
10
+ object.class.find( object.id )
11
+ end
12
+ end
13
+ alias_method_chain :instantiate, :updateable_views_inheritance_support
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,450 @@
1
+ module ActiveRecord #:nodoc:
2
+ module ConnectionAdapters #:nodoc:
3
+ class PostgreSQLAdapter
4
+ # Use this in migration to create child table and view.
5
+ # Options:
6
+ # [:parent]
7
+ # parent relation
8
+ # [:child_table_name]
9
+ # default is <tt>"#{child_view}_data"</tt>
10
+ def create_child(child_view, options)
11
+ raise 'Please call me with a parent, for example: create_child(:steam_locomotives, :parent => :locomotives)' unless options[:parent]
12
+ parent_relation = options[:parent].to_s
13
+ if tables.include?(parent_relation)
14
+ parent_table = parent_relation
15
+ else # view, interpreted as inheritance chain deeper than two levels
16
+ parent_table = query("SELECT child_relation FROM updateable_views_inheritance WHERE child_aggregate_view = #{quote(parent_relation)}")[0][0]
17
+ end
18
+ child_table = options[:table] || "#{child_view}_data"
19
+ child_table_pk = "#{child_view.singularize}_id"
20
+
21
+ create_table(child_table, :id => false) do |t|
22
+ t.integer child_table_pk, :null => false
23
+ yield t
24
+ end
25
+ execute "ALTER TABLE #{child_table} ADD PRIMARY KEY (#{child_table_pk})"
26
+ execute "ALTER TABLE #{child_table} ADD FOREIGN KEY (#{child_table_pk})
27
+ REFERENCES #{parent_table} ON DELETE CASCADE ON UPDATE CASCADE"
28
+
29
+ create_child_view(parent_relation, child_view, child_table)
30
+ end
31
+
32
+ # Drop child view and table
33
+ def drop_child(child_view)
34
+ drop_view(child_view)
35
+ child_table = query("SELECT child_relation FROM updateable_views_inheritance WHERE child_aggregate_view = #{quote(child_view)}")[0][0]
36
+ drop_table(child_table)
37
+ execute "DELETE FROM updateable_views_inheritance WHERE child_aggregate_view = #{quote(child_view)}"
38
+ end
39
+
40
+ # Creates aggregate updateable view of parent and child relations. The convention for naming child tables is
41
+ # <tt>"#{child_view}_data"</tt>. If you don't follow it, supply +child_table_name+ as third argument.
42
+ def create_child_view(parent_table, child_view, child_table=nil)
43
+ child_table ||= child_view.to_s + "_data"
44
+
45
+ parent_columns = columns(parent_table)
46
+ child_columns = columns(child_table)
47
+
48
+ child_column_names = child_columns.collect{|c| c.name}
49
+ parent_column_names = parent_columns.collect{|c| c.name}
50
+
51
+ child_pk = pk_and_sequence_for(child_table)[0]
52
+ child_column_names.delete(child_pk)
53
+
54
+ parent_pk, parent_pk_seq = pk_and_sequence_for(parent_table)
55
+ parent_column_names.delete(parent_pk)
56
+
57
+ do_create_child_view(parent_table, parent_column_names, parent_pk, child_view, child_column_names, child_pk, child_table)
58
+ make_child_view_updateable(parent_table, parent_column_names, parent_pk, parent_pk_seq, child_view, child_column_names, child_pk, child_table)
59
+
60
+ # assign default values for table columns on the view - it is not automatic in Postgresql 8.1
61
+ set_defaults(child_view, parent_table, parent_columns)
62
+ set_defaults(child_view, child_table, child_columns)
63
+ create_system_table_records(parent_table, child_view, child_table)
64
+ end
65
+
66
+ # 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).
67
+ def reset_pk_sequence!(table, pk = nil, sequence = nil)
68
+ parent = parent_table(table)
69
+ if parent
70
+ reset_pk_sequence!(parent, pk, sequence)
71
+ else
72
+ unless pk and sequence
73
+ default_pk, default_sequence = pk_and_sequence_for(table)
74
+ pk ||= default_pk
75
+ sequence ||= default_sequence
76
+ end
77
+ if pk
78
+ if sequence
79
+ select_value <<-end_sql, 'Reset sequence'
80
+ SELECT setval('#{sequence}', (SELECT COALESCE(MAX(#{pk})+(SELECT increment_by FROM #{sequence}), (SELECT min_value FROM #{sequence})) FROM #{table}), false)
81
+ end_sql
82
+ else
83
+ @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ def primary_key(relation)
90
+ res = pk_and_sequence_for(relation)
91
+ res && res.first
92
+ end
93
+
94
+ # Returns a relation's primary key and belonging sequence. If +relation+ is a table the result is its PK and sequence.
95
+ # When it is a view, PK and sequence of the table at the root of the inheritance chain are returned.
96
+ def pk_and_sequence_for(relation)
97
+ result = query(<<-end_sql, 'PK')[0]
98
+ SELECT attr.attname
99
+ FROM pg_attribute attr,
100
+ pg_constraint cons
101
+ WHERE cons.conrelid = attr.attrelid
102
+ AND cons.conrelid = '#{relation}'::regclass
103
+ AND cons.contype = 'p'
104
+ AND attr.attnum = ANY(cons.conkey)
105
+ end_sql
106
+ if result.nil? or result.empty?
107
+ parent = parent_table(relation)
108
+ pk_and_sequence_for(parent) if parent
109
+ else
110
+ # log(result[0], "PK for #{relation}")
111
+ [result[0], query("SELECT pg_get_serial_sequence('#{relation}', '#{result[0]}') ")[0][0]]
112
+ end
113
+ rescue
114
+ nil
115
+ end
116
+
117
+ # Drops a view from the database.
118
+ def drop_view(name)
119
+ execute "DROP VIEW #{name}"
120
+ end
121
+
122
+ # Return the list of all views in the schema search path.
123
+ def views(name=nil)
124
+ schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
125
+ query(<<-SQL, name).map { |row| row[0] }
126
+ SELECT viewname
127
+ FROM pg_views
128
+ WHERE schemaname IN (#{schemas})
129
+ SQL
130
+ end
131
+
132
+ # Checks whether relation +name+ is a view.
133
+ def is_view?(name)
134
+ result = query(<<-SQL, name).map { |row| row[0] }
135
+ SELECT viewname
136
+ FROM pg_views
137
+ WHERE viewname = '#{name}'
138
+ SQL
139
+ !result.empty?
140
+ end
141
+
142
+ # Recursively delete +parent_relation+ (if it is a view) and the children views the depend on it.
143
+ def remove_parent_and_children_views(parent_relation)
144
+ children_views = query(<<-end_sql).map{|row| row[0]}
145
+ SELECT child_aggregate_view
146
+ FROM updateable_views_inheritance
147
+ WHERE parent_relation = '#{parent_relation}'
148
+ end_sql
149
+ children_views.each do |cv|
150
+ remove_parent_and_children_views(cv)
151
+ # drop the view only if it wasn't dropped beforehand in recursive call from other method.
152
+ drop_view(cv) if is_view?(cv)
153
+ end
154
+ drop_view(parent_relation) if is_view?(parent_relation)
155
+ end
156
+
157
+ # Recreates views in the part of the hierarchy chain starting from the +parent_relation+.
158
+ def rebuild_parent_and_children_views(parent_relation)
159
+ # Current implementation is not very efficient - it can drop and recreate one and the same view in the bottom of the hierarchy many times.
160
+ remove_parent_and_children_views(parent_relation)
161
+ children = query(<<-end_sql)
162
+ SELECT parent_relation, child_aggregate_view, child_relation
163
+ FROM updateable_views_inheritance
164
+ WHERE parent_relation = '#{parent_relation}'
165
+ end_sql
166
+
167
+ #if the parent is in the middle of the inheritance chain, it's a view that should be rebuilt as well
168
+ parent = query(<<-end_sql)[0]
169
+ SELECT parent_relation, child_aggregate_view, child_relation
170
+ FROM updateable_views_inheritance
171
+ WHERE child_aggregate_view = '#{parent_relation}'
172
+ end_sql
173
+ create_child_view(parent[0], parent[1], parent[2]) if (parent && !parent.empty?)
174
+
175
+ children.each do |child|
176
+ create_child_view(child[0], child[1], child[2])
177
+ rebuild_parent_and_children_views(child[1])
178
+ end
179
+ end
180
+
181
+ # Creates Single Table Inheritanche-like aggregate view called +sti_aggregate_view+
182
+ # for +parent_relation+ and all its descendants. <i>The view isn't updateable.</i>
183
+ # The order of all or just the first few columns in the aggregate view can be explicitly set
184
+ # by passing array of column names as third argument.
185
+ # If there are columns with the same name but different types in two or more relations
186
+ # they will appear as a single column of type +text+ in the view.
187
+ def create_single_table_inheritance_view(sti_aggregate_view, parent_relation, columns_for_view = nil)
188
+ columns_for_view ||= []
189
+ relations_heirarchy = get_view_hierarchy_for(parent_relation)
190
+ relations = relations_heirarchy.flatten
191
+ leaves_relations = get_leaves_relations(relations_heirarchy)
192
+ all_columns = leaves_relations.map{|rel| columns(rel)}.flatten
193
+ columns_hash = {}
194
+ conflict_column_names = []
195
+ all_columns.each do |col|
196
+ c = columns_hash[col.name]
197
+ if(c && col.sql_type != c.sql_type)
198
+ conflict_column_names << col.name
199
+ else
200
+ columns_hash[col.name] = col
201
+ end
202
+ end
203
+ conflict_column_names = conflict_column_names.uniq.sort if !conflict_column_names.empty?
204
+ sorted_column_names = (columns_for_view + columns_hash.keys.sort).uniq
205
+ parent_klass_name = Tutuf::ClassTableReflection.get_klass_for_table(parent_relation)
206
+ quoted_inheritance_column = quote_column_name(parent_klass_name.inheritance_column)
207
+ queries = relations.map{|rel| generate_single_table_inheritanche_union_clause(rel, sorted_column_names, conflict_column_names, columns_hash, quoted_inheritance_column)}
208
+ unioin_clauses = queries.join("\n UNION ")
209
+ execute <<-end_sql
210
+ CREATE VIEW #{sti_aggregate_view} AS (
211
+ #{unioin_clauses}
212
+ )
213
+ end_sql
214
+ end
215
+
216
+ # Recreates the Single_Table_Inheritanche-like aggregate view +sti_aggregate_view+
217
+ # for +parent_relation+ and all its descendants.
218
+ def rebuild_single_table_inheritance_view(sti_aggregate_view, parent_relation, columns_for_view = nil)
219
+ drop_view(sti_aggregate_view)
220
+ create_single_table_inheritance_view(sti_aggregate_view, parent_relation, columns_for_view)
221
+ end
222
+
223
+ # Overriden - it must return false, otherwise deleting fixtures won't work
224
+ def supports_disable_referential_integrity?
225
+ false
226
+ end
227
+
228
+ def table_exists_with_updateable_views_inheritance_support?(name)
229
+ is_view?(name) ? true : table_exists_without_updateable_views_inheritance_support?(name)
230
+ end
231
+ alias_method_chain :table_exists?, :updateable_views_inheritance_support
232
+
233
+ module Tutuf #:nodoc:
234
+ class ClassTableReflection
235
+ class << self
236
+ # Returns all models' class objects that are ActiveRecord::Base descendants
237
+ def all_db_klasses
238
+ return @@klasses if defined?(@@klasses)
239
+ @@klasses = []
240
+ # load model classes so that inheritance_column is set correctly where defined
241
+ model_filenames.collect{|m| load "#{Rails.root}/app/models/#{m}";m.match(%r{([^/]+?)\.rb$})[1].camelize.constantize }.each do |klass|
242
+ @@klasses << klass if klass < ActiveRecord::Base
243
+ end
244
+ @@klasses.uniq
245
+ end
246
+
247
+ # Returns the class object for +table_name+
248
+ def get_klass_for_table(table_name)
249
+ klass_for_tables()[table_name.to_s]
250
+ end
251
+
252
+ # Returns hash with tables and thier corresponding class.
253
+ # {table_name1 => ClassName1, ...}
254
+ def klass_for_tables
255
+ return @@tables_klasses if defined?(@@tables_klasses)
256
+ @@tables_klasses = {}
257
+ all_db_klasses.each do |klass|
258
+ @@tables_klasses[klass.table_name] = klass if klass.respond_to?(:table_name)
259
+ end
260
+ @@tables_klasses
261
+ end
262
+
263
+ # Returns filenames for models in the current Rails application
264
+ def model_filenames
265
+ Dir.chdir("#{Rails.root}/app/models"){ Dir["**/*.rb"] }
266
+ end
267
+ end
268
+ end
269
+ end
270
+
271
+ private
272
+
273
+ def do_create_child_view(parent_table, parent_columns, parent_pk, child_view, child_columns, child_pk, child_table)
274
+ view_columns = parent_columns + child_columns
275
+ execute <<-end_sql
276
+ CREATE OR REPLACE VIEW #{child_view} AS (
277
+ SELECT parent.#{parent_pk},
278
+ #{ view_columns.join(",") }
279
+ FROM #{parent_table} parent
280
+ INNER JOIN #{child_table} child
281
+ ON ( parent.#{parent_pk}=child.#{child_pk} )
282
+ )
283
+ end_sql
284
+ end
285
+
286
+ # Creates rules for +INSERT+, +UPDATE+ and +DELETE+ on the view
287
+ def make_child_view_updateable(parent_table, parent_columns, parent_pk, parent_pk_seq, child_view, child_columns, child_pk, child_table)
288
+ # insert
289
+ # NEW.#{parent_pk} can be explicitly specified and when it is null every call to it increments the sequence.
290
+ # Setting the sequence to its value (explicitly supplied or the default) covers both cases.
291
+ execute <<-end_sql
292
+ CREATE OR REPLACE RULE #{child_view}_insert AS
293
+ ON INSERT TO #{child_view} DO INSTEAD (
294
+ SELECT setval('#{parent_pk_seq}', NEW.#{parent_pk});
295
+ INSERT INTO #{parent_table}
296
+ ( #{ [parent_pk, parent_columns].flatten.join(", ") } )
297
+ VALUES( currval('#{parent_pk_seq}') #{ parent_columns.empty? ? '' : ',' + parent_columns.collect{ |col| "NEW." + col}.join(",") } )
298
+ #{insert_returning_clause(child_view, parent_columns.unshift(parent_pk)) if supports_insert_with_returning?};
299
+ INSERT INTO #{child_table}
300
+ ( #{ [child_pk, child_columns].flatten.join(",")} )
301
+ VALUES( currval('#{parent_pk_seq}') #{ child_columns.empty? ? '' : ',' + child_columns.collect{ |col| "NEW." + col}.join(",") } )
302
+ )
303
+ end_sql
304
+
305
+ # delete
306
+ execute <<-end_sql
307
+ CREATE OR REPLACE RULE #{child_view}_delete AS
308
+ ON DELETE TO #{child_view} DO INSTEAD
309
+ DELETE FROM #{parent_table} WHERE #{parent_pk} = OLD.#{parent_pk}
310
+ end_sql
311
+
312
+ # update
313
+ execute <<-end_sql
314
+ CREATE OR REPLACE RULE #{child_view}_update AS
315
+ ON UPDATE TO #{child_view} DO INSTEAD (
316
+ #{ parent_columns.empty? ? '':
317
+ "UPDATE #{parent_table}
318
+ SET #{ parent_columns.collect{ |col| col + "= NEW." +col }.join(", ") }
319
+ WHERE #{parent_pk} = OLD.#{parent_pk};"}
320
+ #{ child_columns.empty? ? '':
321
+ "UPDATE #{child_table}
322
+ SET #{ child_columns.collect{ |col| col + " = NEW." +col }.join(", ") }
323
+ WHERE #{child_pk} = OLD.#{parent_pk}"
324
+ }
325
+ )
326
+ end_sql
327
+ end
328
+
329
+ def insert_returning_clause(child_view,parent_columns)
330
+ "RETURNING "+
331
+ columns(child_view).map do |c|
332
+ if parent_columns.include?(c.name)
333
+ c.name
334
+ else
335
+ "CAST (NULL AS #{c.sql_type})"
336
+ end
337
+ end.join(",")
338
+ end
339
+
340
+ # Set default values from the table columns for a view
341
+ def set_defaults(view_name, table_name, columns)
342
+ columns.each do |column|
343
+ if !(default_value = get_default_value(table_name, column.name)).nil?
344
+ execute("ALTER TABLE #{view_name} ALTER #{column.name} SET DEFAULT #{default_value}")
345
+ end
346
+ end
347
+ end
348
+
349
+ # ActiveRecord::ConnectionAdapters::Column objects have nil default value for serial primary key
350
+ def get_default_value(table_name, column_name)
351
+ result = query(<<-end_sql, 'Column default value')[0]
352
+ SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid) as constraint
353
+ FROM pg_catalog.pg_attrdef d, pg_catalog.pg_attribute a, pg_catalog.pg_class c
354
+ WHERE d.adrelid = a.attrelid
355
+ AND d.adnum = a.attnum
356
+ AND a.atthasdef
357
+ AND c.relname = '#{table_name}'
358
+ AND a.attrelid = c.oid
359
+ AND a.attname = '#{column_name}'
360
+ AND a.attnum > 0 AND NOT a.attisdropped
361
+ end_sql
362
+ if !result.nil? && !result.empty?
363
+ result[0]
364
+ else
365
+ nil
366
+ end
367
+ end
368
+
369
+ def create_system_table_records(parent_relation, child_aggregate_view, child_relation)
370
+ parent_relation, child_aggregate_view, child_relation = [parent_relation, child_aggregate_view, child_relation].collect{|rel| quote(rel.to_s)}
371
+ exists = query <<-end_sql
372
+ SELECT parent_relation, child_aggregate_view, child_relation
373
+ FROM updateable_views_inheritance
374
+ WHERE parent_relation = #{parent_relation}
375
+ AND child_aggregate_view = #{child_aggregate_view}
376
+ AND child_relation = #{child_relation}
377
+ end_sql
378
+ # log "res: #{exists}"
379
+ if exists.nil? or exists.empty?
380
+ execute "INSERT INTO updateable_views_inheritance (parent_relation, child_aggregate_view, child_relation)" +
381
+ "VALUES( #{parent_relation}, #{child_aggregate_view}, #{child_relation} )"
382
+ end
383
+ end
384
+
385
+ def parent_table(relation)
386
+ if table_exists?('updateable_views_inheritance')
387
+ res = query(<<-end_sql, 'Parent relation')[0]
388
+ SELECT parent_relation
389
+ FROM updateable_views_inheritance
390
+ WHERE child_aggregate_view = '#{relation}'
391
+ end_sql
392
+ res[0] if res
393
+ end
394
+ end
395
+
396
+ # Single Table Inheritance Aggregate View
397
+
398
+ # Nested list for the +parent_relation+ inheritance hierarchy
399
+ # Every descendant relation is presented as an array with relation's name as first element
400
+ # and the other elements are the relation's children presented in the same way as lists.
401
+ # For example:
402
+ # [[child_view1, [grandchild11,[...]], [grandchild12]],
403
+ # [child_view2, [...]
404
+ # ]
405
+ def get_view_hierarchy_for(parent_relation)
406
+ hierarchy = []
407
+ children = query(<<-end_sql)
408
+ SELECT parent_relation, child_aggregate_view, child_relation
409
+ FROM updateable_views_inheritance
410
+ WHERE parent_relation = '#{parent_relation}'
411
+ end_sql
412
+ children.each do |child|
413
+ hierarchy << [child[1], *get_view_hierarchy_for(child[1])]
414
+ end
415
+ hierarchy
416
+ end
417
+
418
+ def get_leaves_relations(hierarchy)
419
+ return [] if hierarchy.nil? || hierarchy.empty?
420
+ head, hierarchy = hierarchy.first, hierarchy[1..(hierarchy.size)]
421
+ if(head.is_a? Array)
422
+ return (get_leaves_relations(head) + get_leaves_relations(hierarchy)).compact
423
+ elsif(hierarchy.nil? || hierarchy.empty?)
424
+ return [head]
425
+ else
426
+ return get_leaves_relations(hierarchy).compact
427
+ end
428
+ end
429
+
430
+ def generate_single_table_inheritanche_union_clause(rel, column_names, conflict_column_names, columns_hash, quoted_inheritance_column)
431
+ relation_columns = columns(rel).collect{|c| c.name}
432
+ columns_select = column_names.inject([]) do |arr, col_name|
433
+ sql_type = conflict_column_names.include?(col_name) ? 'text' : columns_hash[col_name].sql_type
434
+ value = "NULL::#{sql_type}"
435
+ if(relation_columns.include?(col_name))
436
+ value = col_name
437
+ value = "#{value}::text" if conflict_column_names.include?(col_name)
438
+ end
439
+ statement = " AS #{col_name}"
440
+ statement = "#{value} #{statement}"
441
+ arr << " #{statement}"
442
+ end
443
+ columns_select = columns_select.join(", ")
444
+ rel_klass_name = Tutuf::ClassTableReflection.get_klass_for_table(rel)
445
+ where_clause = " WHERE #{quoted_inheritance_column} = '#{rel_klass_name}'"
446
+ ["SELECT", columns_select, "FROM #{rel} #{where_clause}"].join(" ")
447
+ end
448
+ end
449
+ end
450
+ end