updateable_views_inheritance 1.4.2 → 1.4.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 7a73bd114b93585afae503c742efea0bbb5fa03a
4
- data.tar.gz: 50871f961687a777113315c999b6f578add39566
2
+ SHA256:
3
+ metadata.gz: d1310b2e6e48b891171acd2c8eaebb3f194ee6cd9fac97ebbaa5a6eca5cc7ceb
4
+ data.tar.gz: 0d240ed0c411840f800e0e01a52e05933c5e64903d4ec648a39279fb358c8c53
5
5
  SHA512:
6
- metadata.gz: 710e9a867878a14618a1ec28881a9dc48ca530a82df33690d55a69e996b3720fb7c0fe1a14b03e76b67528a73fac8d23d76c35d13578dd47648cf9ba6694ccc7
7
- data.tar.gz: 0d7b56539848266e86167887dc686995f5f2691ded47e29b4ebf2ed66fa236b2c7e2c8ecd3843ad9c8f6f5d0eafdbf0463c67331866e56ce14aa5fce11ca9f97
6
+ metadata.gz: fdddba487660479c4ed5530764be9bee03aa13bea8ff658319d48938c34e8a276c9e5f7ba6d9769111a4c2e93e1d0b35632ccd111811402eb9188fce12e20d56
7
+ data.tar.gz: 4a50e703717eb50314beed8563134dc20cdd430533aead0398c42c7fd2082b63b00375dc0db39b012688d0d6ec6ad3a1b49c7a5b9e2873a90b556bdcada6515c
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
- Gemfile.lock
2
1
  .bundle/
2
+ .byebug_history
3
+ Gemfile.lock
3
4
  html/*
4
5
  pkg/*
5
6
  test/dummy/log/*.log
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 1.4.3 (01 October 2024)
2
+
3
+ Features:
4
+
5
+ - Add option to disable inheritance instantiation for less
6
+ database hits when loading large object collections from a
7
+ parent class.
8
+
9
+ - Add option to skip creating child table in migrations.
10
+
1
11
  ## 1.4.2 (28 March 2017)
2
12
 
3
13
  Upgrade to Rails 4.2
@@ -15,31 +25,31 @@ Upgrade to Rails 4
15
25
 
16
26
  Features:
17
27
 
18
- - rebuild views in all inheritance chains (must be run when upgrading from <= 1.2.1)
28
+ - Rebuild views in all inheritance chains (must be run when upgrading from <= 1.2.1)
19
29
 
20
30
  ## 1.2.2 (18 August 2015)
21
31
 
22
32
  Bugfixes:
23
33
 
24
- - fixed compatibility with Rails 3.2.19+ and ActiveRecord's prepared statements
34
+ - Fixed compatibility with Rails 3.2.19+ and ActiveRecord's prepared statements
25
35
 
26
36
  ## 1.2.1 (27 August 2014)
27
37
 
28
38
  Bugfixes:
29
39
 
30
- - parent relations can be in a schema
40
+ - Parent relations can be in a schema
31
41
 
32
42
  ## 1.2.0 (27 August 2014)
33
43
 
34
44
  Features:
35
45
 
36
- - support for PostgreSQL schemas
46
+ - Support for PostgreSQL schemas
37
47
 
38
48
  ## 1.1.2 (14 June 2013)
39
49
 
40
50
  Bugfixes:
41
51
 
42
- - fixed generating migration on installation
52
+ - Fixed generating migration on installation
43
53
 
44
54
  Documentation:
45
55
 
@@ -49,17 +59,17 @@ Documentation:
49
59
 
50
60
  Features:
51
61
 
52
- - gemified and released on rubygems.org
62
+ - Gemified and released on rubygems.org
53
63
 
54
64
  ## 1.1.0 (13 June 2013)
55
65
 
56
66
  Features:
57
67
 
58
- - updated for rails 3.2.x
68
+ - Updated for Rails 3.2.x
59
69
 
60
70
  ## 1.0.0 (14 September 2009)
61
71
 
62
72
  Features:
63
73
 
64
74
  - class_table_inheritance plugin has behaved stably in production for a year
65
- - supports rails 2.1, 2.2 and 2.3
75
+ - Supports Rails 2.1, 2.2 and 2.3
data/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # Class Table Inheritance
2
+
3
+ Class Table Inheritance for ActiveRecord using updateable views
4
+
5
+ More about the pattern on
6
+ http://www.martinfowler.com/eaaCatalog/classTableInheritance.html. This gem
7
+ messes very little with Rails inheritance mechanism. Instead it relies on
8
+ updatable views in the database to represent classes in the inheritance chain.
9
+ The approach was [first suggested by John
10
+ Wilger](http://web.archive.org/web/20060408145717/johnwilger.com/articles/2005/09/29/class-table-inheritance-in-rails-with-postgresql).
11
+
12
+
13
+ # Requirements
14
+
15
+ Rails: 4.x
16
+
17
+ Ruby: 1.9.3+
18
+
19
+ Database: PostgreSQL only. Patches for other DBMS are welcome. Note that you are
20
+ not required to use updateable views, children relations can be tables with
21
+ some triggers involved.
22
+
23
+ # Usage
24
+
25
+ ## Setup
26
+
27
+ * Add `gem 'updateable_views_inheritance'` to your `Gemfile`
28
+ * Run `rails generate updateable_views_inheritance:install && rake db:migrate`
29
+ * In `config/environment.rb` set `config.active_record.schema_format = :sql`
30
+
31
+ ## Example
32
+
33
+ The database migration:
34
+
35
+ ```ruby
36
+ class CtiExample < ActiveRecord::Migration
37
+ def self.up
38
+ create_table :locomotives do |t|
39
+ t.column :name, :string
40
+ t.column :max_speed, :integer
41
+ t.column :type, :string
42
+ end
43
+
44
+ create_child(:steam_locomotives, parent: :locomotives) do |t|
45
+ t.decimal :water_consumption, precision: 6, scale: 2
46
+ t.decimal :coal_consumption, precision: 6, scale: 2
47
+ end
48
+
49
+ create_child(:electric_locomotives,
50
+ table: :raw_electric_locomotives,
51
+ parent: :locomotives) do |t|
52
+ t.decimal :electricity_consumption, precision: 6, scale: 2
53
+ end
54
+ end
55
+
56
+ def self.down
57
+ drop_child :steam_locomotives
58
+ drop_child :electric_locomotives
59
+ drop_table :locomotives
60
+ end
61
+ end
62
+ ```
63
+
64
+ And the models:
65
+
66
+ ```ruby
67
+ class Locomotive
68
+ end
69
+
70
+ class SteamLocomotive < Locomotive
71
+ self.table_name = :steam_locomotives
72
+ end
73
+
74
+ class ElectricLocomotive < Locomotive
75
+ self.table_name = :electric_locomotives
76
+ end
77
+ ```
78
+
79
+ Note that models of children classes must specify table name explicitly.
80
+
81
+ ### Changing Columns in Underlying Tables
82
+
83
+ ```ruby
84
+ class RemoveColumnInParentTable < ActiveRecord::Migration
85
+ def self.up
86
+ remove_parent_and_children_views(:locomotives)
87
+ remove_column(:locomotives, :max_speed)
88
+ rename_column(:name, :title)
89
+ rebuild_parent_and_children_views(:locomotives)
90
+ end
91
+ end
92
+ ```
93
+
94
+ ### Renaming Underlying Tables
95
+
96
+ ```ruby
97
+ remove_parent_and_children_views(:old_name)
98
+ rename_table(:old_name,:new_name)
99
+ execute "UPDATE updateable_views_inheritance SET child_aggregate_view = 'new_name' WHERE child_aggregate_view = 'old_name'"
100
+ execute "UPDATE updateable_views_inheritance SET parent_relation = 'new_name' WHERE parent_relation = 'old_name'"
101
+ rebuild_parent_and_children_views(:new_name)
102
+ ```
103
+
104
+ ### Removing Classes
105
+
106
+ Note that you should remove only leaf classes (i.e. those that do not have
107
+ descendants). If you want to erase a whole chain or part of chain you have to
108
+ remove first the leaves and then their ancestors. Use `drop_child(child_view)`
109
+ in migrations.
110
+
111
+ ### Using parent class without instantiating subclass
112
+
113
+ If you don't want to make a second SQL query to the subclass table when you instantiate
114
+ parent class with `Locomotive.find(1)` use
115
+ ```ruby
116
+ class Locomotive
117
+ self.disable_inheritance_instantiation = true
118
+ end
119
+ ```
120
+ Quite handy for flat and wide class hierarchies (one parent class, many subclasses).
121
+
122
+ ### Using existing table for inherited class
123
+
124
+ ```ruby
125
+ class CreateIkarusBus < ActiveRecord::Migration
126
+ def self.up
127
+ # table `tbl_ikarus_buses` exists in the database
128
+ end
129
+ create_child(:ikarus_buses,
130
+ table: :tbl_ikarus_buses,
131
+ parent: :buses,
132
+ skip_creating_child_table: true)
133
+ end
134
+ end
135
+ ```
136
+ Useful when converting legacy DB schema to use inheritance.
137
+
138
+ ## Compatibility with Single Table Inheritance
139
+
140
+ The approach of this gem is completely independent from Rails built-in Single
141
+ Table Inheritance. STI and CLTI can safely be mixed in one inheritance chain.
142
+
143
+ ## Testing Your App
144
+
145
+ If you use fixtures, you must run `rake updateable_views_inheritance:fixture` to
146
+ generate fixture for the updateable_views_inheritance table after you
147
+ add/remove classes from the hierarchy or change underlying table or view names.
148
+ **Without it primary key sequence for inheritors' tables won't be bumped to the
149
+ max and it might not be possible to save objects!** If you don't use fixtures
150
+ for the classes in the hierarchy you don't need to do that.
151
+
152
+ This gem re-enables referential integrity on fixture loading. This means that
153
+ `fixtures :all` may fail when there are foreign key constraints on tables. To
154
+ fix this, explicitly declare fixture load order in `test_helper.rb`:
155
+
156
+ ```
157
+ fixtures :roots, :trunks, :leafs, ...
158
+ ```
159
+ for all fixtures you want to load.
160
+
161
+ ## Gem Development & Testing
162
+
163
+ In order to run gem tests, you have to be a superuser in PostgreSQL.
@@ -1,10 +1,12 @@
1
1
  module ActiveRecord #:nodoc:
2
2
  class Base #:nodoc:
3
3
  class << self
4
+ attr_accessor :disable_inheritance_instantiation
5
+
4
6
  private
5
7
  def instantiate_with_updateable_views_inheritance_support(attributes, column_types = {})
6
8
  object = instantiate_without_updateable_views_inheritance_support(attributes, column_types = {})
7
- if object.class.name == self.name
9
+ if object.class.name == self.name || self.disable_inheritance_instantiation
8
10
  object
9
11
  else
10
12
  object.class.find(attributes.with_indifferent_access[:id])
@@ -1,6 +1,5 @@
1
1
  require 'active_record/connection_adapters/postgresql/utils'
2
2
 
3
-
4
3
  module ActiveRecord #:nodoc:
5
4
  module ConnectionAdapters #:nodoc:
6
5
  module PostgreSQL
@@ -9,30 +8,38 @@ module ActiveRecord #:nodoc:
9
8
  # Options:
10
9
  # [:parent]
11
10
  # parent relation
12
- # [:child_table_name]
11
+ # [:table]
13
12
  # default is <tt>"#{child_view}_data"</tt>
13
+ # [:skip_creating_child_table]
14
+ # use together with :table option
14
15
  def create_child(child_view, options)
15
16
  raise 'Please call me with a parent, for example: create_child(:steam_locomotives, :parent => :locomotives)' unless options[:parent]
16
17
 
17
- unqualified_child_view_name = Utils.extract_schema_qualified_name(child_view).identifier
18
-
19
18
  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
19
+ parent_table = if is_view?(parent_relation) # interpreted as inheritance chain deeper than two levels
20
+ query(<<~SQL)[0][0]
21
+ SELECT child_relation
22
+ FROM updateable_views_inheritance
23
+ WHERE child_aggregate_view = #{quote(parent_relation)}
24
+ SQL
25
+ else
26
+ parent_relation
27
+ end
25
28
 
26
29
  child_table = options[:table] || quote_table_name("#{child_view}_data")
27
- child_table_pk = "#{unqualified_child_view_name.singularize}_id"
28
30
 
29
- create_table(child_table, :id => false) do |t|
30
- t.integer child_table_pk, :null => false
31
- yield t
31
+ unless options.key?(:skip_creating_child_table)
32
+ unqualified_child_view_name = Utils.extract_schema_qualified_name(child_view).identifier
33
+ child_table_pk = "#{unqualified_child_view_name.singularize}_id"
34
+
35
+ create_table(child_table, :id => false) do |t|
36
+ t.integer child_table_pk, :null => false
37
+ yield t
38
+ end
39
+ execute "ALTER TABLE #{child_table} ADD PRIMARY KEY (#{child_table_pk})"
40
+ execute "ALTER TABLE #{child_table} ADD FOREIGN KEY (#{child_table_pk})
41
+ REFERENCES #{parent_table} ON DELETE CASCADE ON UPDATE CASCADE"
32
42
  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"
36
43
 
37
44
  create_child_view(parent_relation, child_view, child_table)
38
45
  end
@@ -130,7 +137,7 @@ module ActiveRecord #:nodoc:
130
137
  # Return the list of all views in the schema search path.
131
138
  def views(name=nil)
132
139
  schemas = schema_search_path.split(/,\s*/).map { |p| quote(p) }.join(',')
133
- query(<<-SQL, name).map { |row| row[0] }
140
+ query(<<~SQL, name).map { |row| row[0] }
134
141
  SELECT viewname
135
142
  FROM pg_views
136
143
  WHERE schemaname IN (#{schemas})
@@ -139,7 +146,7 @@ module ActiveRecord #:nodoc:
139
146
 
140
147
  # Checks whether relation +name+ is a view.
141
148
  def is_view?(name)
142
- result = query(<<-SQL, name).map { |row| row[0] }
149
+ result = query(<<~SQL, name).map { |row| row[0] }
143
150
  SELECT viewname
144
151
  FROM pg_views
145
152
  WHERE viewname = '#{name}'
@@ -234,7 +241,7 @@ module ActiveRecord #:nodoc:
234
241
  create_single_table_inheritance_view(sti_aggregate_view, parent_relation, columns_for_view)
235
242
  end
236
243
 
237
- # Overriden - it must return false, otherwise deleting fixtures won't work
244
+ # Overriden - it solargraph-must return false, otherwise deleting fixtures won't work
238
245
  def supports_disable_referential_integrity?
239
246
  false
240
247
  end
@@ -247,194 +254,196 @@ module ActiveRecord #:nodoc:
247
254
  module Tutuf #:nodoc:
248
255
  class ClassTableReflection
249
256
  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
257
+ # Returns all models' class objects that are ActiveRecord::Base descendants
258
+ def all_db_klasses
259
+ return @@klasses if defined?(@@klasses)
260
+
261
+ @@klasses = []
262
+ # load model classes so that inheritance_column is set correctly where defined
263
+ model_filenames.collect{|m| load "#{Rails.root}/app/models/#{m}";m.match(%r{([^/]+?)\.rb$})[1].camelize.constantize }.each do |klass|
264
+ @@klasses << klass if klass < ActiveRecord::Base
259
265
  end
266
+ @@klasses.uniq
267
+ end
260
268
 
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
269
+ # Returns the class object for +table_name+
270
+ def get_klass_for_table(table_name)
271
+ klass_for_tables()[table_name.to_s]
272
+ end
265
273
 
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
274
+ # Returns hash with tables and thier corresponding class.
275
+ # {table_name1 => ClassName1, ...}
276
+ def klass_for_tables
277
+ return @@tables_klasses if defined?(@@tables_klasses)
276
278
 
277
- # Returns filenames for models in the current Rails application
278
- def model_filenames
279
- Dir.chdir("#{Rails.root}/app/models"){ Dir["**/*.rb"] }
279
+ @@tables_klasses = {}
280
+ all_db_klasses.each do |klass|
281
+ @@tables_klasses[klass.table_name] = klass if klass.respond_to?(:table_name)
280
282
  end
283
+ @@tables_klasses
284
+ end
285
+
286
+ # Returns filenames for models in the current Rails application
287
+ def model_filenames
288
+ Dir.chdir("#{Rails.root}/app/models"){ Dir["**/*.rb"] }
289
+ end
290
+ end
291
+ end
292
+ end
293
+
294
+ # Set default values from the table columns for a view
295
+ def set_defaults(view_name, table_name)
296
+ column_definitions(table_name).each do |column_name, type, default, notnull|
297
+ if !default.nil?
298
+ execute("ALTER TABLE #{quote_table_name(view_name)} ALTER #{quote_column_name(column_name)} SET DEFAULT #{default}")
281
299
  end
282
300
  end
283
301
  end
284
302
 
285
303
  private
286
304
 
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
305
+ def do_create_child_view(parent_table, parent_columns, parent_pk, child_view, child_columns, child_pk, child_table)
306
+ view_columns = parent_columns + child_columns
307
+ execute <<-end_sql
308
+ CREATE OR REPLACE VIEW #{child_view} AS (
309
+ SELECT parent.#{parent_pk},
310
+ #{ view_columns.join(",") }
311
+ FROM #{parent_table} parent
312
+ INNER JOIN #{child_table} child
313
+ ON ( parent.#{parent_pk}=child.#{child_pk} )
314
+ )
315
+ end_sql
316
+ end
299
317
 
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
318
+ # Creates rules for +INSERT+, +UPDATE+ and +DELETE+ on the view
319
+ def make_child_view_updateable(parent_table, parent_columns, parent_pk, parent_pk_seq, child_view, child_columns, child_pk, child_table)
320
+ # insert
321
+ # NEW.#{parent_pk} can be explicitly specified and when it is null every call to it increments the sequence.
322
+ # Setting the sequence to its value (explicitly supplied or the default) covers both cases.
323
+ execute <<-end_sql
324
+ CREATE OR REPLACE RULE #{quote_column_name("#{child_view}_insert")} AS
325
+ ON INSERT TO #{child_view} DO INSTEAD (
326
+ INSERT INTO #{parent_table}
327
+ ( #{ [parent_pk, parent_columns].flatten.join(", ") } )
328
+ VALUES( DEFAULT #{ parent_columns.empty? ? '' : ' ,' + parent_columns.collect{ |col| "NEW." + col}.join(", ") } ) ;
329
+ INSERT INTO #{child_table}
330
+ ( #{ [child_pk, child_columns].flatten.join(",")} )
331
+ VALUES( currval('#{parent_pk_seq}') #{ child_columns.empty? ? '' : ' ,' + child_columns.collect{ |col| "NEW." + col}.join(", ") } )
332
+ #{insert_returning_clause(parent_pk, child_pk, child_view)}
333
+ )
334
+ end_sql
317
335
 
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
336
+ # delete
337
+ execute <<-end_sql
338
+ CREATE OR REPLACE RULE #{quote_column_name("#{child_view}_delete")} AS
339
+ ON DELETE TO #{child_view} DO INSTEAD
340
+ DELETE FROM #{parent_table} WHERE #{parent_pk} = OLD.#{parent_pk}
341
+ end_sql
324
342
 
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
343
+ # update
344
+ execute <<-end_sql
345
+ CREATE OR REPLACE RULE #{quote_column_name("#{child_view}_update")} AS
346
+ ON UPDATE TO #{child_view} DO INSTEAD (
347
+ #{ parent_columns.empty? ? '':
348
+ "UPDATE #{parent_table}
349
+ SET #{ parent_columns.collect{ |col| col + "= NEW." + col }.join(", ") }
350
+ WHERE #{parent_pk} = OLD.#{parent_pk};"}
351
+ #{ child_columns.empty? ? '':
352
+ "UPDATE #{child_table}
353
+ SET #{ child_columns.collect{ |col| col + " = NEW." + col }.join(", ") }
354
+ WHERE #{child_pk} = OLD.#{parent_pk}"
355
+ }
356
+ )
357
+ end_sql
358
+ end
341
359
 
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
360
+ def insert_returning_clause(parent_pk, child_pk, child_view)
361
+ columns_cast_to_null = columns(child_view)
362
+ .reject { |c| c.name == parent_pk}
363
+ .map { |c| "CAST (NULL AS #{c.sql_type})" }
364
+ .join(", ")
365
+ "RETURNING #{child_pk}, #{columns_cast_to_null}"
366
+ end
349
367
 
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
+ def create_system_table_records(parent_relation, child_aggregate_view, child_relation)
369
+ parent_relation, child_aggregate_view, child_relation = [parent_relation, child_aggregate_view, child_relation].collect{|rel| quote(rel.to_s)}
370
+ exists = query <<~SQL
371
+ SELECT parent_relation, child_aggregate_view, child_relation
372
+ FROM updateable_views_inheritance
373
+ WHERE parent_relation = #{parent_relation}
374
+ AND child_aggregate_view = #{child_aggregate_view}
375
+ AND child_relation = #{child_relation}
376
+ SQL
377
+ # log "res: #{exists}"
378
+ if exists.nil? or exists.empty?
379
+ execute "INSERT INTO updateable_views_inheritance (parent_relation, child_aggregate_view, child_relation)" +
380
+ "VALUES( #{parent_relation}, #{child_aggregate_view}, #{child_relation} )"
357
381
  end
382
+ end
358
383
 
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
384
+ def parent_table(relation)
385
+ if table_exists?('updateable_views_inheritance')
386
+ res = query(<<-end_sql, 'Parent relation')[0]
387
+ SELECT parent_relation
363
388
  FROM updateable_views_inheritance
364
- WHERE parent_relation = #{parent_relation}
365
- AND child_aggregate_view = #{child_aggregate_view}
366
- AND child_relation = #{child_relation}
389
+ WHERE child_aggregate_view = '#{relation}'
367
390
  end_sql
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
373
- end
374
-
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
391
+ res[0] if res
384
392
  end
393
+ end
385
394
 
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
395
+ # Single Table Inheritance Aggregate View
396
+
397
+ # Nested list for the +parent_relation+ inheritance hierarchy
398
+ # Every descendant relation is presented as an array with relation's name as first element
399
+ # and the other elements are the relation's children presented in the same way as lists.
400
+ # For example:
401
+ # [[child_view1, [grandchild11,[...]], [grandchild12]],
402
+ # [child_view2, [...]
403
+ # ]
404
+ def get_view_hierarchy_for(parent_relation)
405
+ hierarchy = []
406
+ children = query(<<-end_sql)
407
+ SELECT parent_relation, child_aggregate_view, child_relation
408
+ FROM updateable_views_inheritance
409
+ WHERE parent_relation = '#{parent_relation}'
410
+ end_sql
411
+ children.each do |child|
412
+ hierarchy << [child[1], *get_view_hierarchy_for(child[1])]
406
413
  end
414
+ hierarchy
415
+ end
407
416
 
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
417
- end
417
+ def get_leaves_relations(hierarchy)
418
+ return [] if hierarchy.nil? || hierarchy.empty?
419
+ head, hierarchy = hierarchy.first, hierarchy[1..(hierarchy.size)]
420
+ if(head.is_a? Array)
421
+ return (get_leaves_relations(head) + get_leaves_relations(hierarchy)).compact
422
+ elsif(hierarchy.nil? || hierarchy.empty?)
423
+ return [head]
424
+ else
425
+ return get_leaves_relations(hierarchy).compact
418
426
  end
427
+ end
419
428
 
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}"
429
+ def generate_single_table_inheritanche_union_clause(rel, column_names, conflict_column_names, columns_hash, quoted_inheritance_column)
430
+ relation_columns = columns(rel).collect{|c| c.name}
431
+ columns_select = column_names.inject([]) do |arr, col_name|
432
+ sql_type = conflict_column_names.include?(col_name) ? 'text' : columns_hash[col_name].sql_type
433
+ value = "NULL::#{sql_type}"
434
+ if(relation_columns.include?(col_name))
435
+ value = col_name
436
+ value = "#{value}::text" if conflict_column_names.include?(col_name)
432
437
  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(" ")
438
+ statement = " AS #{col_name}"
439
+ statement = "#{value} #{statement}"
440
+ arr << " #{statement}"
437
441
  end
442
+ columns_select = columns_select.join(", ")
443
+ rel_klass_name = Tutuf::ClassTableReflection.get_klass_for_table(rel)
444
+ where_clause = " WHERE #{quoted_inheritance_column} = '#{rel_klass_name}'"
445
+ ["SELECT", columns_select, "FROM #{rel} #{where_clause}"].join(" ")
446
+ end
438
447
  end
439
448
  end
440
449
  end
@@ -1,3 +1,3 @@
1
1
  module UpdateableViewsInheritance
2
- VERSION = "1.4.2"
2
+ VERSION = "1.4.3"
3
3
  end
File without changes
@@ -0,0 +1,40 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class InstantiationTest < ActiveSupport::TestCase
4
+ def setup
5
+ ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 7)
6
+ # order of fixtures is important for the test - last loaded should not be with max(id)
7
+ %w[steam_locomotives electric_locomotives maglev_locomotives bicycles].each do |f|
8
+ ActiveRecord::FixtureSet.create_fixtures(File.dirname(__FILE__) + '/fixtures/', f)
9
+ end
10
+ @connection = ActiveRecord::Base.connection
11
+ end
12
+
13
+ def teardown
14
+ ActiveRecord::FixtureSet.reset_cache
15
+ end
16
+
17
+ class ::Locomotive < ActiveRecord::Base
18
+ self.disable_inheritance_instantiation = true
19
+ end
20
+
21
+ class ::ElectricLocomotive < Locomotive
22
+ self.disable_inheritance_instantiation = false
23
+ end
24
+
25
+ def test_setting_disable_inheritance_instantiation_does_not_load_child_columns
26
+ assert_equal %w[id max_speed name type],
27
+ Locomotive.first.attributes.keys.sort
28
+ end
29
+
30
+ def test_switching_off_disable_inheritance_instantiation_loads_child_columns
31
+ assert_equal %w[electricity_consumption id magnetic_field max_speed name type],
32
+ MaglevLocomotive.first.attributes.keys.sort
33
+ end
34
+
35
+ def test_disable_inheritance_instantiatioon_not_set_loads_child_attributes
36
+ assert_equal %w[id name number_of_gears number_of_wheels vehicle_type],
37
+ Bicycle.first.attributes.keys.sort
38
+
39
+ end
40
+ end
data/test/schema_test.rb CHANGED
@@ -221,4 +221,22 @@ class UpdateableViewsInheritanceSchemaTest < ActiveSupport::TestCase
221
221
  assert @connection.columns(:bicycles).map{ |c| c.name }.include?('wheel_size'),
222
222
  "Newly added column not present in view after rebuild for 2. hierarchy"
223
223
  end
224
+
225
+ class UseExistingTable < ActiveRecord::Migration
226
+ def self.up
227
+ create_table :tbl_diesel_locomotives do |t|
228
+ t.belongs_to :locomotives
229
+ t.integer :num_cylinders
230
+ end
231
+ create_child(:diesel_locomotives,
232
+ table: :tbl_diesel_locomotives,
233
+ parent: :locomotives,
234
+ skip_creating_child_table: true)
235
+ end
236
+ end
237
+
238
+ def test_skip_creating_child_table
239
+ UseExistingTable.up
240
+ assert @connection.columns(:diesel_locomotives).map(&:name).include?("num_cylinders")
241
+ end
224
242
  end
@@ -19,10 +19,12 @@ Gem::Specification.new do |s|
19
19
  s.require_paths = ["lib"]
20
20
 
21
21
  s.add_dependency "activerecord", "~> 4.2.8"
22
- s.add_dependency "pg"
22
+ s.add_dependency "pg", "~> 0.21"
23
23
 
24
24
  s.add_development_dependency 'minitest'
25
- s.add_development_dependency "rails", ' ~> 4.2.8'
26
- s.add_development_dependency "bundler", "~> 1.3"
25
+ s.add_development_dependency "rails", '= 4.2.11.1'
26
+ s.add_development_dependency "bundler"
27
27
  s.add_development_dependency "rake"
28
+ s.add_development_dependency 'bigdecimal', '1.3.5'
29
+ s.add_development_dependency "solargraph"
28
30
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: updateable_views_inheritance
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.2
4
+ version: 1.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sava Chankov
8
8
  - Denitsa Belogusheva
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-03-28 00:00:00.000000000 Z
12
+ date: 2024-10-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -29,16 +29,16 @@ dependencies:
29
29
  name: pg
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
- - - ">="
32
+ - - "~>"
33
33
  - !ruby/object:Gem::Version
34
- version: '0'
34
+ version: '0.21'
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
- - - ">="
39
+ - - "~>"
40
40
  - !ruby/object:Gem::Version
41
- version: '0'
41
+ version: '0.21'
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: minitest
44
44
  requirement: !ruby/object:Gem::Requirement
@@ -57,30 +57,30 @@ dependencies:
57
57
  name: rails
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
- - - "~>"
60
+ - - '='
61
61
  - !ruby/object:Gem::Version
62
- version: 4.2.8
62
+ version: 4.2.11.1
63
63
  type: :development
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
- - - "~>"
67
+ - - '='
68
68
  - !ruby/object:Gem::Version
69
- version: 4.2.8
69
+ version: 4.2.11.1
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: bundler
72
72
  requirement: !ruby/object:Gem::Requirement
73
73
  requirements:
74
- - - "~>"
74
+ - - ">="
75
75
  - !ruby/object:Gem::Version
76
- version: '1.3'
76
+ version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
79
  version_requirements: !ruby/object:Gem::Requirement
80
80
  requirements:
81
- - - "~>"
81
+ - - ">="
82
82
  - !ruby/object:Gem::Version
83
- version: '1.3'
83
+ version: '0'
84
84
  - !ruby/object:Gem::Dependency
85
85
  name: rake
86
86
  requirement: !ruby/object:Gem::Requirement
@@ -95,6 +95,34 @@ dependencies:
95
95
  - - ">="
96
96
  - !ruby/object:Gem::Version
97
97
  version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: bigdecimal
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - '='
103
+ - !ruby/object:Gem::Version
104
+ version: 1.3.5
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - '='
110
+ - !ruby/object:Gem::Version
111
+ version: 1.3.5
112
+ - !ruby/object:Gem::Dependency
113
+ name: solargraph
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
98
126
  description: Class table inheritance for ActiveRecord based on updatable views in
99
127
  the database that join parent and children tables
100
128
  email:
@@ -108,7 +136,7 @@ files:
108
136
  - CHANGELOG.md
109
137
  - Gemfile
110
138
  - MIT-LICENSE
111
- - README.rdoc
139
+ - README.md
112
140
  - Rakefile
113
141
  - doc/template/horo.rb
114
142
  - lib/generators/updateable_views_inheritance/install_generator.rb
@@ -121,6 +149,7 @@ files:
121
149
  - test/content_test.rb
122
150
  - test/deep_hierarchy_test.rb
123
151
  - test/dummy/Rakefile
152
+ - test/dummy/app/assets/config/manifest.js
124
153
  - test/dummy/app/assets/javascripts/application.js
125
154
  - test/dummy/app/assets/stylesheets/application.css
126
155
  - test/dummy/app/controllers/application_controller.rb
@@ -185,6 +214,7 @@ files:
185
214
  - test/fixtures/steam_locomotives.yml
186
215
  - test/fixtures/steam_trains.yml
187
216
  - test/install_generator_test.rb
217
+ - test/instantiation_test.rb
188
218
  - test/migration_test.rb
189
219
  - test/pg_insert_returning_with_rules_spec.rb
190
220
  - test/schema_test.rb
@@ -195,7 +225,7 @@ homepage: http://github.com/tutuf/updateable_views_inheritance
195
225
  licenses:
196
226
  - MIT
197
227
  metadata: {}
198
- post_install_message:
228
+ post_install_message:
199
229
  rdoc_options: []
200
230
  require_paths:
201
231
  - lib
@@ -210,15 +240,15 @@ required_rubygems_version: !ruby/object:Gem::Requirement
210
240
  - !ruby/object:Gem::Version
211
241
  version: '0'
212
242
  requirements: []
213
- rubyforge_project:
214
- rubygems_version: 2.6.10
215
- signing_key:
243
+ rubygems_version: 3.4.12
244
+ signing_key:
216
245
  specification_version: 4
217
246
  summary: Class table inheritance for ActiveRecord
218
247
  test_files:
219
248
  - test/content_test.rb
220
249
  - test/deep_hierarchy_test.rb
221
250
  - test/dummy/Rakefile
251
+ - test/dummy/app/assets/config/manifest.js
222
252
  - test/dummy/app/assets/javascripts/application.js
223
253
  - test/dummy/app/assets/stylesheets/application.css
224
254
  - test/dummy/app/controllers/application_controller.rb
@@ -283,6 +313,7 @@ test_files:
283
313
  - test/fixtures/steam_locomotives.yml
284
314
  - test/fixtures/steam_trains.yml
285
315
  - test/install_generator_test.rb
316
+ - test/instantiation_test.rb
286
317
  - test/migration_test.rb
287
318
  - test/pg_insert_returning_with_rules_spec.rb
288
319
  - test/schema_test.rb
data/README.rdoc DELETED
@@ -1,121 +0,0 @@
1
- ==Class Table Inheritance
2
-
3
- Class Table Inheritance for ActiveRecord using updateable views.
4
-
5
- More about the pattern on http://www.martinfowler.com/eaaCatalog/classTableInheritance.html. This gem messes very little with Rails inheritance mechanism.
6
- Instead it relies on updatable views in the database to represent classes in the inheritance chain. The approach was {first suggested by John
7
- Wilger}[http://web.archive.org/web/20060408145717/johnwilger.com/articles/2005/09/29/class-table-inheritance-in-rails-with-postgresql].
8
-
9
-
10
- ==Requirements
11
-
12
- Rails: 4.x
13
-
14
- Ruby: 1.9.3+
15
-
16
- Database: PostgreSQL 8.1+ only. Patches for other DBMS are welcome. Note that you are not required to use updateable views, children relations can be tables (with some triggers involved) or materialized views.
17
-
18
- ==Install
19
- Run
20
- gem install updateable_views_inheritance
21
-
22
- ==Usage
23
-
24
- ===Setup
25
-
26
- * In <tt>Gemfile</tt> add <tt>gem 'updateable_views_inheritance'</tt>
27
- * Run <tt>rails generate updateable_views_inheritance:install && rake db:migrate</tt>
28
- * In <tt>config/environment.rb</tt> set <tt>config.active_record.schema_format = :sql</tt>
29
- * In case you're using fixtures, don't forget to run
30
-
31
- rake updateable_views_inheritance:fixture
32
-
33
- after every change to the class hierarchy. Otherwise tests may fail.
34
-
35
- ===Example
36
-
37
- class CtiExample < ActiveRecord::Migration
38
- def self.up
39
- create_table :locomotives do |t|
40
- t.column :name, :string
41
- t.column :max_speed, :integer
42
- t.column :type, :string
43
- end
44
-
45
- create_child(:steam_locomotives, :parent => :locomotives) do |t|
46
- t.decimal :water_consumption, :precision => 6, :scale => 2
47
- t.decimal :coal_consumption, :precision => 6, :scale => 2
48
- end
49
-
50
- create_child(:electric_locomotives, :table => :raw_electric_locomotives, :parent => :locomotives) do |t|
51
- t.decimal :electricity_consumption, :precision => 6, :scale => 2
52
- end
53
- end
54
-
55
- def self.down
56
- drop_child :steam_locomotives
57
- drop_child :electric_locomotives
58
- drop_table :locomotives
59
- end
60
- end
61
-
62
- And the models:
63
- class Locomotive
64
- end
65
-
66
- class SteamLocomotive < Locomotive
67
- self.table_name = :steam_locomotives
68
- end
69
-
70
- class ElectricLocomotive < Locomotive
71
- self.table_name = :electric_locomotives
72
- end
73
-
74
- Note that models of children classes must specify table name explicitly.
75
-
76
- ===Changing Columns in Underlying Tables
77
-
78
- class RemoveColumnInParentTable < ActiveRecord::Migration
79
- def self.up
80
- remove_parent_and_children_views(:locomotives)
81
- remove_column(:locomotives, :max_speed)
82
- rename_column(:name, :title)
83
- rebuild_parent_and_children_views(:locomotives)
84
- end
85
- end
86
-
87
- ===Renaming Underlying Tables
88
-
89
- remove_parent_and_children_views(:old_name)
90
- rename_table(:old_name,:new_name)
91
- execute "UPDATE updateable_views_inheritance SET child_aggregate_view = 'new_name' WHERE child_aggregate_view = 'old_name'"
92
- execute "UPDATE updateable_views_inheritance SET parent_relation = 'new_name' WHERE parent_relation = 'old_name'"
93
- rebuild_parent_and_children_views(:new_name)
94
-
95
- ===Removing Classes
96
-
97
- Note that you should remove only leaf classes (i.e. those that do not have descendants). If you want to erase a whole chain or part of chain you have to remove first the leaves and then their ancestors. Use <tt>drop_child(child_view)</tt> in migrations.
98
-
99
- ==Compatibility with Single Table Inheritance
100
-
101
- The approach of this gem is completely independent from Rails built-in Single Table Inheritance. STI and CLTI can safely be mixed in one inheritance chain.
102
-
103
- ==Testing Your App
104
-
105
- If you use fixtures, you must run <tt>rake updateable_views_inheritance:fixture</tt> to generate fixture for the updateable_views_inheritance table after you add/remove
106
- classes from the hierarchy or change underlying table or view names. <b>Without it primary key sequence for inheritors' tables won't be bumped to the max and it might not be possible to save objects!</b> If you don't use fixtures for the classes in the hierarchy you don't need to do that.
107
-
108
- This gem re-enables referential integrity on fixture loading. This means that
109
-
110
- fixtures :all
111
-
112
- may fail when there are foreign key constraints on tables. To fix this, explicitly declare fixture load order in <tt>test_helper.rb</tt>:
113
-
114
- fixtures :roots, :trunks, :leafs, ...
115
-
116
- for all fixtures you want to load.
117
-
118
- ==Gem Development & Testing
119
-
120
- The gem has a comprehensive test suite. In order to run it, your user must be a superuser in PostgreSQL.
121
- If this is not the case, run <tt>createuser -s pesho</tt> (assuming your Unix account is <tt>pesho</tt>).