updateable_views_inheritance 1.1.1

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