updateable_views_inheritance 1.4.2 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- 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>).