updateable_views_inheritance 1.4.1 → 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 +5 -5
- data/.gitignore +2 -1
- data/CHANGELOG.md +24 -9
- data/README.md +163 -0
- data/lib/updateable_views_inheritance/active_record.rb +3 -1
- data/lib/updateable_views_inheritance/postgresql_adapter.rb +241 -227
- data/lib/updateable_views_inheritance/version.rb +1 -1
- data/test/content_test.rb +0 -3
- data/test/deep_hierarchy_test.rb +1 -1
- data/test/dummy/app/assets/config/manifest.js +0 -0
- data/test/dummy/config/environments/test.rb +2 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +1 -1
- data/test/instantiation_test.rb +40 -0
- data/test/schema_test.rb +19 -1
- data/test/test_helper.rb +0 -4
- data/updateable_views_inheritance.gemspec +6 -5
- metadata +45 -34
- data/README.rdoc +0 -121
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d1310b2e6e48b891171acd2c8eaebb3f194ee6cd9fac97ebbaa5a6eca5cc7ceb
|
4
|
+
data.tar.gz: 0d240ed0c411840f800e0e01a52e05933c5e64903d4ec648a39279fb358c8c53
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fdddba487660479c4ed5530764be9bee03aa13bea8ff658319d48938c34e8a276c9e5f7ba6d9769111a4c2e93e1d0b35632ccd111811402eb9188fce12e20d56
|
7
|
+
data.tar.gz: 4a50e703717eb50314beed8563134dc20cdd430533aead0398c42c7fd2082b63b00375dc0db39b012688d0d6ec6ad3a1b49c7a5b9e2873a90b556bdcada6515c
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,19 @@
|
|
1
|
-
## 1.4.
|
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
|
+
|
11
|
+
## 1.4.2 (28 March 2017)
|
12
|
+
|
13
|
+
Upgrade to Rails 4.2
|
14
|
+
|
15
|
+
|
16
|
+
## 1.4.1 (20 March 2017)
|
2
17
|
|
3
18
|
Upgrade to Rails 4.1
|
4
19
|
|
@@ -10,31 +25,31 @@ Upgrade to Rails 4
|
|
10
25
|
|
11
26
|
Features:
|
12
27
|
|
13
|
-
-
|
28
|
+
- Rebuild views in all inheritance chains (must be run when upgrading from <= 1.2.1)
|
14
29
|
|
15
30
|
## 1.2.2 (18 August 2015)
|
16
31
|
|
17
32
|
Bugfixes:
|
18
33
|
|
19
|
-
-
|
34
|
+
- Fixed compatibility with Rails 3.2.19+ and ActiveRecord's prepared statements
|
20
35
|
|
21
36
|
## 1.2.1 (27 August 2014)
|
22
37
|
|
23
38
|
Bugfixes:
|
24
39
|
|
25
|
-
-
|
40
|
+
- Parent relations can be in a schema
|
26
41
|
|
27
42
|
## 1.2.0 (27 August 2014)
|
28
43
|
|
29
44
|
Features:
|
30
45
|
|
31
|
-
-
|
46
|
+
- Support for PostgreSQL schemas
|
32
47
|
|
33
48
|
## 1.1.2 (14 June 2013)
|
34
49
|
|
35
50
|
Bugfixes:
|
36
51
|
|
37
|
-
-
|
52
|
+
- Fixed generating migration on installation
|
38
53
|
|
39
54
|
Documentation:
|
40
55
|
|
@@ -44,17 +59,17 @@ Documentation:
|
|
44
59
|
|
45
60
|
Features:
|
46
61
|
|
47
|
-
-
|
62
|
+
- Gemified and released on rubygems.org
|
48
63
|
|
49
64
|
## 1.1.0 (13 June 2013)
|
50
65
|
|
51
66
|
Features:
|
52
67
|
|
53
|
-
-
|
68
|
+
- Updated for Rails 3.2.x
|
54
69
|
|
55
70
|
## 1.0.0 (14 September 2009)
|
56
71
|
|
57
72
|
Features:
|
58
73
|
|
59
74
|
- class_table_inheritance plugin has behaved stably in production for a year
|
60
|
-
-
|
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,251 +1,263 @@
|
|
1
|
+
require 'active_record/connection_adapters/postgresql/utils'
|
2
|
+
|
1
3
|
module ActiveRecord #:nodoc:
|
2
4
|
module ConnectionAdapters #:nodoc:
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
parent_table =
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
5
|
+
module PostgreSQL
|
6
|
+
module SchemaStatements
|
7
|
+
# Use this in migration to create child table and view.
|
8
|
+
# Options:
|
9
|
+
# [:parent]
|
10
|
+
# parent relation
|
11
|
+
# [:table]
|
12
|
+
# default is <tt>"#{child_view}_data"</tt>
|
13
|
+
# [:skip_creating_child_table]
|
14
|
+
# use together with :table option
|
15
|
+
def create_child(child_view, options)
|
16
|
+
raise 'Please call me with a parent, for example: create_child(:steam_locomotives, :parent => :locomotives)' unless options[:parent]
|
17
|
+
|
18
|
+
parent_relation = options[:parent].to_s
|
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
|
28
|
+
|
29
|
+
child_table = options[:table] || quote_table_name("#{child_view}_data")
|
30
|
+
|
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"
|
42
|
+
end
|
24
43
|
|
25
|
-
|
26
|
-
t.integer child_table_pk, :null => false
|
27
|
-
yield t
|
44
|
+
create_child_view(parent_relation, child_view, child_table)
|
28
45
|
end
|
29
|
-
execute "ALTER TABLE #{child_table} ADD PRIMARY KEY (#{child_table_pk})"
|
30
|
-
execute "ALTER TABLE #{child_table} ADD FOREIGN KEY (#{child_table_pk})
|
31
|
-
REFERENCES #{parent_table} ON DELETE CASCADE ON UPDATE CASCADE"
|
32
46
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
drop_table(child_table)
|
41
|
-
execute "DELETE FROM updateable_views_inheritance WHERE child_aggregate_view = #{quote(child_view)}"
|
42
|
-
end
|
47
|
+
# Drop child view and table
|
48
|
+
def drop_child(child_view)
|
49
|
+
drop_view(child_view)
|
50
|
+
child_table = query("SELECT child_relation FROM updateable_views_inheritance WHERE child_aggregate_view = #{quote(child_view)}")[0][0]
|
51
|
+
drop_table(child_table)
|
52
|
+
execute "DELETE FROM updateable_views_inheritance WHERE child_aggregate_view = #{quote(child_view)}"
|
53
|
+
end
|
43
54
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
55
|
+
# Creates aggregate updateable view of parent and child relations. The convention for naming child tables is
|
56
|
+
# <tt>"#{child_view}_data"</tt>. If you don't follow it, supply +child_table_name+ as third argument.
|
57
|
+
def create_child_view(parent_table, child_view, child_table=nil)
|
58
|
+
child_table ||= child_view.to_s + "_data"
|
48
59
|
|
49
|
-
|
50
|
-
|
60
|
+
parent_columns = columns(parent_table)
|
61
|
+
child_columns = columns(child_table)
|
51
62
|
|
52
|
-
|
53
|
-
|
63
|
+
child_column_names = child_columns.collect{|c| c.name}
|
64
|
+
parent_column_names = parent_columns.collect{|c| c.name}
|
54
65
|
|
55
|
-
|
56
|
-
|
66
|
+
child_pk = pk_and_sequence_for(child_table)[0]
|
67
|
+
child_column_names.delete(child_pk)
|
57
68
|
|
58
|
-
|
59
|
-
|
69
|
+
parent_pk, parent_pk_seq = pk_and_sequence_for(parent_table)
|
70
|
+
parent_column_names.delete(parent_pk)
|
60
71
|
|
61
|
-
|
62
|
-
|
72
|
+
do_create_child_view(parent_table, parent_column_names, parent_pk, child_view, child_column_names, child_pk, child_table)
|
73
|
+
make_child_view_updateable(parent_table, parent_column_names, parent_pk, parent_pk_seq, child_view, child_column_names, child_pk, child_table)
|
63
74
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
75
|
+
# assign default values for table columns on the view - it is not automatic in Postgresql 8.1
|
76
|
+
set_defaults(child_view, parent_table)
|
77
|
+
set_defaults(child_view, child_table)
|
78
|
+
create_system_table_records(parent_table, child_view, child_table)
|
79
|
+
end
|
69
80
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
81
|
+
# 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).
|
82
|
+
def reset_pk_sequence!(table, pk = nil, sequence = nil)
|
83
|
+
parent = parent_table(table)
|
84
|
+
if parent
|
85
|
+
reset_pk_sequence!(parent, pk, sequence)
|
86
|
+
else
|
87
|
+
unless pk and sequence
|
88
|
+
default_pk, default_sequence = pk_and_sequence_for(table)
|
89
|
+
pk ||= default_pk
|
90
|
+
sequence ||= default_sequence
|
91
|
+
end
|
92
|
+
if pk
|
93
|
+
if sequence
|
94
|
+
select_value <<-end_sql, 'Reset sequence'
|
95
|
+
SELECT setval('#{sequence}', (SELECT COALESCE(MAX(#{pk})+(SELECT increment_by FROM #{sequence}), (SELECT min_value FROM #{sequence})) FROM #{table}), false)
|
96
|
+
end_sql
|
97
|
+
else
|
98
|
+
@logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
|
99
|
+
end
|
88
100
|
end
|
89
101
|
end
|
90
102
|
end
|
91
|
-
end
|
92
103
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
104
|
+
def primary_key(relation)
|
105
|
+
res = pk_and_sequence_for(relation)
|
106
|
+
res && res.first
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns a relation's primary key and belonging sequence. If +relation+ is a table the result is its PK and sequence.
|
110
|
+
# When it is a view, PK and sequence of the table at the root of the inheritance chain are returned.
|
111
|
+
def pk_and_sequence_for(relation)
|
112
|
+
result = query(<<-end_sql, 'PK')[0]
|
113
|
+
SELECT attr.attname
|
114
|
+
FROM pg_attribute attr,
|
115
|
+
pg_constraint cons
|
116
|
+
WHERE cons.conrelid = attr.attrelid
|
117
|
+
AND cons.conrelid = '#{relation}'::regclass
|
118
|
+
AND cons.contype = 'p'
|
119
|
+
AND attr.attnum = ANY(cons.conkey)
|
120
|
+
end_sql
|
121
|
+
if result.nil? or result.empty?
|
122
|
+
parent = parent_table(relation)
|
123
|
+
pk_and_sequence_for(parent) if parent
|
124
|
+
else
|
125
|
+
# log(result[0], "PK for #{relation}") {}
|
126
|
+
[result[0], query("SELECT pg_get_serial_sequence('#{relation}', '#{result[0]}') ")[0][0]]
|
127
|
+
end
|
128
|
+
rescue
|
129
|
+
nil
|
130
|
+
end
|
97
131
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
result = query(<<-end_sql, 'PK')[0]
|
102
|
-
SELECT attr.attname
|
103
|
-
FROM pg_attribute attr,
|
104
|
-
pg_constraint cons
|
105
|
-
WHERE cons.conrelid = attr.attrelid
|
106
|
-
AND cons.conrelid = '#{relation}'::regclass
|
107
|
-
AND cons.contype = 'p'
|
108
|
-
AND attr.attnum = ANY(cons.conkey)
|
109
|
-
end_sql
|
110
|
-
if result.nil? or result.empty?
|
111
|
-
parent = parent_table(relation)
|
112
|
-
pk_and_sequence_for(parent) if parent
|
113
|
-
else
|
114
|
-
# log(result[0], "PK for #{relation}") {}
|
115
|
-
[result[0], query("SELECT pg_get_serial_sequence('#{relation}', '#{result[0]}') ")[0][0]]
|
132
|
+
# Drops a view from the database.
|
133
|
+
def drop_view(name)
|
134
|
+
execute "DROP VIEW #{name}"
|
116
135
|
end
|
117
|
-
rescue
|
118
|
-
nil
|
119
|
-
end
|
120
136
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
137
|
+
# Return the list of all views in the schema search path.
|
138
|
+
def views(name=nil)
|
139
|
+
schemas = schema_search_path.split(/,\s*/).map { |p| quote(p) }.join(',')
|
140
|
+
query(<<~SQL, name).map { |row| row[0] }
|
141
|
+
SELECT viewname
|
142
|
+
FROM pg_views
|
143
|
+
WHERE schemaname IN (#{schemas})
|
144
|
+
SQL
|
145
|
+
end
|
125
146
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
147
|
+
# Checks whether relation +name+ is a view.
|
148
|
+
def is_view?(name)
|
149
|
+
result = query(<<~SQL, name).map { |row| row[0] }
|
150
|
+
SELECT viewname
|
151
|
+
FROM pg_views
|
152
|
+
WHERE viewname = '#{name}'
|
153
|
+
SQL
|
154
|
+
!result.empty?
|
155
|
+
end
|
135
156
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
157
|
+
# Recursively delete +parent_relation+ (if it is a view) and the children views the depend on it.
|
158
|
+
def remove_parent_and_children_views(parent_relation)
|
159
|
+
children_views = query(<<-end_sql).map{|row| row[0]}
|
160
|
+
SELECT child_aggregate_view
|
161
|
+
FROM updateable_views_inheritance
|
162
|
+
WHERE parent_relation = '#{parent_relation}'
|
163
|
+
end_sql
|
164
|
+
children_views.each do |cv|
|
165
|
+
remove_parent_and_children_views(cv)
|
166
|
+
# drop the view only if it wasn't dropped beforehand in recursive call from other method.
|
167
|
+
drop_view(cv) if is_view?(cv)
|
168
|
+
end
|
169
|
+
drop_view(parent_relation) if is_view?(parent_relation)
|
170
|
+
end
|
145
171
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
FROM updateable_views_inheritance
|
151
|
-
WHERE parent_relation = '#{parent_relation}'
|
152
|
-
end_sql
|
153
|
-
children_views.each do |cv|
|
154
|
-
remove_parent_and_children_views(cv)
|
155
|
-
# drop the view only if it wasn't dropped beforehand in recursive call from other method.
|
156
|
-
drop_view(cv) if is_view?(cv)
|
172
|
+
# Recreates all views in all hierarchy chains
|
173
|
+
def rebuild_all_parent_and_children_views
|
174
|
+
parent_relations = select_values('SELECT DISTINCT parent_relation FROM updateable_views_inheritance')
|
175
|
+
parent_relations.each { |parent_relation| rebuild_parent_and_children_views(parent_relation) }
|
157
176
|
end
|
158
|
-
drop_view(parent_relation) if is_view?(parent_relation)
|
159
|
-
end
|
160
177
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
178
|
+
# Recreates views in the part of the hierarchy chain starting from the +parent_relation+.
|
179
|
+
def rebuild_parent_and_children_views(parent_relation)
|
180
|
+
# Current implementation is not very efficient - it can drop and recreate one and the same view in the bottom of the hierarchy many times.
|
181
|
+
remove_parent_and_children_views(parent_relation)
|
182
|
+
children = query(<<-end_sql)
|
183
|
+
SELECT parent_relation, child_aggregate_view, child_relation
|
184
|
+
FROM updateable_views_inheritance
|
185
|
+
WHERE parent_relation = '#{parent_relation}'
|
186
|
+
end_sql
|
187
|
+
|
188
|
+
#if the parent is in the middle of the inheritance chain, it's a view that should be rebuilt as well
|
189
|
+
parent = query(<<-end_sql)[0]
|
190
|
+
SELECT parent_relation, child_aggregate_view, child_relation
|
191
|
+
FROM updateable_views_inheritance
|
192
|
+
WHERE child_aggregate_view = '#{parent_relation}'
|
193
|
+
end_sql
|
194
|
+
create_child_view(parent[0], parent[1], parent[2]) if (parent && !parent.empty?)
|
166
195
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
children = query(<<-end_sql)
|
172
|
-
SELECT parent_relation, child_aggregate_view, child_relation
|
173
|
-
FROM updateable_views_inheritance
|
174
|
-
WHERE parent_relation = '#{parent_relation}'
|
175
|
-
end_sql
|
176
|
-
|
177
|
-
#if the parent is in the middle of the inheritance chain, it's a view that should be rebuilt as well
|
178
|
-
parent = query(<<-end_sql)[0]
|
179
|
-
SELECT parent_relation, child_aggregate_view, child_relation
|
180
|
-
FROM updateable_views_inheritance
|
181
|
-
WHERE child_aggregate_view = '#{parent_relation}'
|
182
|
-
end_sql
|
183
|
-
create_child_view(parent[0], parent[1], parent[2]) if (parent && !parent.empty?)
|
184
|
-
|
185
|
-
children.each do |child|
|
186
|
-
create_child_view(child[0], child[1], child[2])
|
187
|
-
rebuild_parent_and_children_views(child[1])
|
196
|
+
children.each do |child|
|
197
|
+
create_child_view(child[0], child[1], child[2])
|
198
|
+
rebuild_parent_and_children_views(child[1])
|
199
|
+
end
|
188
200
|
end
|
189
|
-
end
|
190
201
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
202
|
+
# Creates Single Table Inheritanche-like aggregate view called +sti_aggregate_view+
|
203
|
+
# for +parent_relation+ and all its descendants. <i>The view isn't updateable.</i>
|
204
|
+
# The order of all or just the first few columns in the aggregate view can be explicitly set
|
205
|
+
# by passing array of column names as third argument.
|
206
|
+
# If there are columns with the same name but different types in two or more relations
|
207
|
+
# they will appear as a single column of type +text+ in the view.
|
208
|
+
def create_single_table_inheritance_view(sti_aggregate_view, parent_relation, columns_for_view = nil)
|
209
|
+
columns_for_view ||= []
|
210
|
+
relations_heirarchy = get_view_hierarchy_for(parent_relation)
|
211
|
+
relations = relations_heirarchy.flatten
|
212
|
+
leaves_relations = get_leaves_relations(relations_heirarchy)
|
213
|
+
all_columns = leaves_relations.map{|rel| columns(rel)}.flatten
|
214
|
+
columns_hash = {}
|
215
|
+
conflict_column_names = []
|
216
|
+
all_columns.each do |col|
|
217
|
+
c = columns_hash[col.name]
|
218
|
+
if(c && col.sql_type != c.sql_type)
|
219
|
+
conflict_column_names << col.name
|
220
|
+
else
|
221
|
+
columns_hash[col.name] = col
|
222
|
+
end
|
211
223
|
end
|
224
|
+
conflict_column_names = conflict_column_names.uniq.sort if !conflict_column_names.empty?
|
225
|
+
sorted_column_names = (columns_for_view + columns_hash.keys.sort).uniq
|
226
|
+
parent_klass_name = Tutuf::ClassTableReflection.get_klass_for_table(parent_relation)
|
227
|
+
quoted_inheritance_column = quote_column_name(parent_klass_name.inheritance_column)
|
228
|
+
queries = relations.map{|rel| generate_single_table_inheritanche_union_clause(rel, sorted_column_names, conflict_column_names, columns_hash, quoted_inheritance_column)}
|
229
|
+
unioin_clauses = queries.join("\n UNION ")
|
230
|
+
execute <<-end_sql
|
231
|
+
CREATE VIEW #{sti_aggregate_view} AS (
|
232
|
+
#{unioin_clauses}
|
233
|
+
)
|
234
|
+
end_sql
|
212
235
|
end
|
213
|
-
conflict_column_names = conflict_column_names.uniq.sort if !conflict_column_names.empty?
|
214
|
-
sorted_column_names = (columns_for_view + columns_hash.keys.sort).uniq
|
215
|
-
parent_klass_name = Tutuf::ClassTableReflection.get_klass_for_table(parent_relation)
|
216
|
-
quoted_inheritance_column = quote_column_name(parent_klass_name.inheritance_column)
|
217
|
-
queries = relations.map{|rel| generate_single_table_inheritanche_union_clause(rel, sorted_column_names, conflict_column_names, columns_hash, quoted_inheritance_column)}
|
218
|
-
unioin_clauses = queries.join("\n UNION ")
|
219
|
-
execute <<-end_sql
|
220
|
-
CREATE VIEW #{sti_aggregate_view} AS (
|
221
|
-
#{unioin_clauses}
|
222
|
-
)
|
223
|
-
end_sql
|
224
|
-
end
|
225
236
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
237
|
+
# Recreates the Single_Table_Inheritanche-like aggregate view +sti_aggregate_view+
|
238
|
+
# for +parent_relation+ and all its descendants.
|
239
|
+
def rebuild_single_table_inheritance_view(sti_aggregate_view, parent_relation, columns_for_view = nil)
|
240
|
+
drop_view(sti_aggregate_view)
|
241
|
+
create_single_table_inheritance_view(sti_aggregate_view, parent_relation, columns_for_view)
|
242
|
+
end
|
232
243
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
244
|
+
# Overriden - it solargraph-must return false, otherwise deleting fixtures won't work
|
245
|
+
def supports_disable_referential_integrity?
|
246
|
+
false
|
247
|
+
end
|
237
248
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
249
|
+
def table_exists_with_updateable_views_inheritance_support?(name)
|
250
|
+
is_view?(name) ? true : table_exists_without_updateable_views_inheritance_support?(name)
|
251
|
+
end
|
252
|
+
alias_method_chain :table_exists?, :updateable_views_inheritance_support
|
242
253
|
|
243
|
-
|
244
|
-
|
245
|
-
|
254
|
+
module Tutuf #:nodoc:
|
255
|
+
class ClassTableReflection
|
256
|
+
class << self
|
246
257
|
# Returns all models' class objects that are ActiveRecord::Base descendants
|
247
258
|
def all_db_klasses
|
248
259
|
return @@klasses if defined?(@@klasses)
|
260
|
+
|
249
261
|
@@klasses = []
|
250
262
|
# load model classes so that inheritance_column is set correctly where defined
|
251
263
|
model_filenames.collect{|m| load "#{Rails.root}/app/models/#{m}";m.match(%r{([^/]+?)\.rb$})[1].camelize.constantize }.each do |klass|
|
@@ -263,6 +275,7 @@ module ActiveRecord #:nodoc:
|
|
263
275
|
# {table_name1 => ClassName1, ...}
|
264
276
|
def klass_for_tables
|
265
277
|
return @@tables_klasses if defined?(@@tables_klasses)
|
278
|
+
|
266
279
|
@@tables_klasses = {}
|
267
280
|
all_db_klasses.each do |klass|
|
268
281
|
@@tables_klasses[klass.table_name] = klass if klass.respond_to?(:table_name)
|
@@ -274,11 +287,20 @@ module ActiveRecord #:nodoc:
|
|
274
287
|
def model_filenames
|
275
288
|
Dir.chdir("#{Rails.root}/app/models"){ Dir["**/*.rb"] }
|
276
289
|
end
|
290
|
+
end
|
277
291
|
end
|
278
292
|
end
|
279
|
-
end
|
280
293
|
|
281
|
-
|
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}")
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
private
|
282
304
|
|
283
305
|
def do_create_child_view(parent_table, parent_columns, parent_pk, child_view, child_columns, child_pk, child_table)
|
284
306
|
view_columns = parent_columns + child_columns
|
@@ -307,7 +329,7 @@ module ActiveRecord #:nodoc:
|
|
307
329
|
INSERT INTO #{child_table}
|
308
330
|
( #{ [child_pk, child_columns].flatten.join(",")} )
|
309
331
|
VALUES( currval('#{parent_pk_seq}') #{ child_columns.empty? ? '' : ' ,' + child_columns.collect{ |col| "NEW." + col}.join(", ") } )
|
310
|
-
#{insert_returning_clause(parent_pk, child_pk, child_view)
|
332
|
+
#{insert_returning_clause(parent_pk, child_pk, child_view)}
|
311
333
|
)
|
312
334
|
end_sql
|
313
335
|
|
@@ -324,11 +346,11 @@ module ActiveRecord #:nodoc:
|
|
324
346
|
ON UPDATE TO #{child_view} DO INSTEAD (
|
325
347
|
#{ parent_columns.empty? ? '':
|
326
348
|
"UPDATE #{parent_table}
|
327
|
-
SET #{ parent_columns.collect{ |col| col + "= NEW." +col }.join(", ") }
|
349
|
+
SET #{ parent_columns.collect{ |col| col + "= NEW." + col }.join(", ") }
|
328
350
|
WHERE #{parent_pk} = OLD.#{parent_pk};"}
|
329
351
|
#{ child_columns.empty? ? '':
|
330
352
|
"UPDATE #{child_table}
|
331
|
-
SET #{ child_columns.collect{ |col| col + " = NEW." +col }.join(", ") }
|
353
|
+
SET #{ child_columns.collect{ |col| col + " = NEW." + col }.join(", ") }
|
332
354
|
WHERE #{child_pk} = OLD.#{parent_pk}"
|
333
355
|
}
|
334
356
|
)
|
@@ -343,24 +365,15 @@ module ActiveRecord #:nodoc:
|
|
343
365
|
"RETURNING #{child_pk}, #{columns_cast_to_null}"
|
344
366
|
end
|
345
367
|
|
346
|
-
# Set default values from the table columns for a view
|
347
|
-
def set_defaults(view_name, table_name)
|
348
|
-
column_definitions(table_name).each do |column_name, type, default, notnull|
|
349
|
-
if !default.nil?
|
350
|
-
execute("ALTER TABLE #{quote_table_name(view_name)} ALTER #{quote_column_name(column_name)} SET DEFAULT #{default}")
|
351
|
-
end
|
352
|
-
end
|
353
|
-
end
|
354
|
-
|
355
368
|
def create_system_table_records(parent_relation, child_aggregate_view, child_relation)
|
356
369
|
parent_relation, child_aggregate_view, child_relation = [parent_relation, child_aggregate_view, child_relation].collect{|rel| quote(rel.to_s)}
|
357
|
-
exists = query
|
370
|
+
exists = query <<~SQL
|
358
371
|
SELECT parent_relation, child_aggregate_view, child_relation
|
359
372
|
FROM updateable_views_inheritance
|
360
373
|
WHERE parent_relation = #{parent_relation}
|
361
374
|
AND child_aggregate_view = #{child_aggregate_view}
|
362
375
|
AND child_relation = #{child_relation}
|
363
|
-
|
376
|
+
SQL
|
364
377
|
# log "res: #{exists}"
|
365
378
|
if exists.nil? or exists.empty?
|
366
379
|
execute "INSERT INTO updateable_views_inheritance (parent_relation, child_aggregate_view, child_relation)" +
|
@@ -431,6 +444,7 @@ module ActiveRecord #:nodoc:
|
|
431
444
|
where_clause = " WHERE #{quoted_inheritance_column} = '#{rel_klass_name}'"
|
432
445
|
["SELECT", columns_select, "FROM #{rel} #{where_clause}"].join(" ")
|
433
446
|
end
|
447
|
+
end
|
434
448
|
end
|
435
449
|
end
|
436
450
|
end
|
data/test/content_test.rb
CHANGED
@@ -3,9 +3,6 @@ require File.join(File.dirname(__FILE__), 'test_helper')
|
|
3
3
|
class UpdateableViewsInheritanceContentTest < ActiveSupport::TestCase
|
4
4
|
def setup
|
5
5
|
ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 5)
|
6
|
-
end
|
7
|
-
|
8
|
-
def teardown
|
9
6
|
ActiveRecord::FixtureSet.reset_cache
|
10
7
|
end
|
11
8
|
|
data/test/deep_hierarchy_test.rb
CHANGED
@@ -15,7 +15,7 @@ class DeepHierarchyTest < ActiveSupport::TestCase
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def test_deeper_hierarchy
|
18
|
-
assert_equal [["boats"], ["railed_vehicles", ["trains", ["
|
18
|
+
assert_equal [["boats"], ["railed_vehicles", ["trains", ["steam_trains"], ["rack_trains"], ["electric_trains", ["maglev_trains"]]]], ["wheeled_vehicles", ["bicycles"], ["cars"]]].sort,
|
19
19
|
@connection.send(:get_view_hierarchy_for, :vehicles).sort
|
20
20
|
end
|
21
21
|
|
File without changes
|
@@ -4,4 +4,4 @@
|
|
4
4
|
# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
|
5
5
|
|
6
6
|
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
|
7
|
-
|
7
|
+
Rails.backtrace_cleaner.remove_silencers!
|
@@ -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
@@ -16,7 +16,7 @@ class UpdateableViewsInheritanceSchemaTest < ActiveSupport::TestCase
|
|
16
16
|
|
17
17
|
|
18
18
|
def test_content_columns
|
19
|
-
assert !SteamLocomotive.content_columns.include?("id")
|
19
|
+
assert !SteamLocomotive.content_columns.map(&:name).include?("id")
|
20
20
|
end
|
21
21
|
|
22
22
|
def test_views
|
@@ -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
|
data/test/test_helper.rb
CHANGED
@@ -5,10 +5,6 @@ require File.expand_path("../dummy/config/environment.rb", __FILE__)
|
|
5
5
|
require 'rails/test_help'
|
6
6
|
require 'updateable_views_inheritance'
|
7
7
|
|
8
|
-
# get full stack trace on errors
|
9
|
-
require "minitest/reporters"
|
10
|
-
Minitest::Reporters.use!
|
11
|
-
|
12
8
|
begin
|
13
9
|
if RUBY_VERSION > "2"
|
14
10
|
require 'byebug'
|
@@ -18,12 +18,13 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
19
19
|
s.require_paths = ["lib"]
|
20
20
|
|
21
|
-
s.add_dependency "activerecord", "
|
22
|
-
s.add_dependency "pg"
|
21
|
+
s.add_dependency "activerecord", "~> 4.2.8"
|
22
|
+
s.add_dependency "pg", "~> 0.21"
|
23
23
|
|
24
24
|
s.add_development_dependency 'minitest'
|
25
|
-
s.add_development_dependency '
|
26
|
-
s.add_development_dependency "
|
27
|
-
s.add_development_dependency "bundler", "~> 1.3"
|
25
|
+
s.add_development_dependency "rails", '= 4.2.11.1'
|
26
|
+
s.add_development_dependency "bundler"
|
28
27
|
s.add_development_dependency "rake"
|
28
|
+
s.add_development_dependency 'bigdecimal', '1.3.5'
|
29
|
+
s.add_development_dependency "solargraph"
|
29
30
|
end
|
metadata
CHANGED
@@ -1,50 +1,44 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: updateable_views_inheritance
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.4.
|
4
|
+
version: 1.4.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:
|
12
|
+
date: 2024-10-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
|
-
- - "
|
19
|
-
- !ruby/object:Gem::Version
|
20
|
-
version: '4.0'
|
21
|
-
- - "<"
|
18
|
+
- - "~>"
|
22
19
|
- !ruby/object:Gem::Version
|
23
|
-
version:
|
20
|
+
version: 4.2.8
|
24
21
|
type: :runtime
|
25
22
|
prerelease: false
|
26
23
|
version_requirements: !ruby/object:Gem::Requirement
|
27
24
|
requirements:
|
28
|
-
- - "
|
29
|
-
- !ruby/object:Gem::Version
|
30
|
-
version: '4.0'
|
31
|
-
- - "<"
|
25
|
+
- - "~>"
|
32
26
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
27
|
+
version: 4.2.8
|
34
28
|
- !ruby/object:Gem::Dependency
|
35
29
|
name: pg
|
36
30
|
requirement: !ruby/object:Gem::Requirement
|
37
31
|
requirements:
|
38
|
-
- - "
|
32
|
+
- - "~>"
|
39
33
|
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
34
|
+
version: '0.21'
|
41
35
|
type: :runtime
|
42
36
|
prerelease: false
|
43
37
|
version_requirements: !ruby/object:Gem::Requirement
|
44
38
|
requirements:
|
45
|
-
- - "
|
39
|
+
- - "~>"
|
46
40
|
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
41
|
+
version: '0.21'
|
48
42
|
- !ruby/object:Gem::Dependency
|
49
43
|
name: minitest
|
50
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -60,7 +54,21 @@ dependencies:
|
|
60
54
|
- !ruby/object:Gem::Version
|
61
55
|
version: '0'
|
62
56
|
- !ruby/object:Gem::Dependency
|
63
|
-
name:
|
57
|
+
name: rails
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - '='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 4.2.11.1
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - '='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 4.2.11.1
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: bundler
|
64
72
|
requirement: !ruby/object:Gem::Requirement
|
65
73
|
requirements:
|
66
74
|
- - ">="
|
@@ -74,35 +82,35 @@ dependencies:
|
|
74
82
|
- !ruby/object:Gem::Version
|
75
83
|
version: '0'
|
76
84
|
- !ruby/object:Gem::Dependency
|
77
|
-
name:
|
85
|
+
name: rake
|
78
86
|
requirement: !ruby/object:Gem::Requirement
|
79
87
|
requirements:
|
80
|
-
- - "
|
88
|
+
- - ">="
|
81
89
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
90
|
+
version: '0'
|
83
91
|
type: :development
|
84
92
|
prerelease: false
|
85
93
|
version_requirements: !ruby/object:Gem::Requirement
|
86
94
|
requirements:
|
87
|
-
- - "
|
95
|
+
- - ">="
|
88
96
|
- !ruby/object:Gem::Version
|
89
|
-
version:
|
97
|
+
version: '0'
|
90
98
|
- !ruby/object:Gem::Dependency
|
91
|
-
name:
|
99
|
+
name: bigdecimal
|
92
100
|
requirement: !ruby/object:Gem::Requirement
|
93
101
|
requirements:
|
94
|
-
- -
|
102
|
+
- - '='
|
95
103
|
- !ruby/object:Gem::Version
|
96
|
-
version:
|
104
|
+
version: 1.3.5
|
97
105
|
type: :development
|
98
106
|
prerelease: false
|
99
107
|
version_requirements: !ruby/object:Gem::Requirement
|
100
108
|
requirements:
|
101
|
-
- -
|
109
|
+
- - '='
|
102
110
|
- !ruby/object:Gem::Version
|
103
|
-
version:
|
111
|
+
version: 1.3.5
|
104
112
|
- !ruby/object:Gem::Dependency
|
105
|
-
name:
|
113
|
+
name: solargraph
|
106
114
|
requirement: !ruby/object:Gem::Requirement
|
107
115
|
requirements:
|
108
116
|
- - ">="
|
@@ -128,7 +136,7 @@ files:
|
|
128
136
|
- CHANGELOG.md
|
129
137
|
- Gemfile
|
130
138
|
- MIT-LICENSE
|
131
|
-
- README.
|
139
|
+
- README.md
|
132
140
|
- Rakefile
|
133
141
|
- doc/template/horo.rb
|
134
142
|
- lib/generators/updateable_views_inheritance/install_generator.rb
|
@@ -141,6 +149,7 @@ files:
|
|
141
149
|
- test/content_test.rb
|
142
150
|
- test/deep_hierarchy_test.rb
|
143
151
|
- test/dummy/Rakefile
|
152
|
+
- test/dummy/app/assets/config/manifest.js
|
144
153
|
- test/dummy/app/assets/javascripts/application.js
|
145
154
|
- test/dummy/app/assets/stylesheets/application.css
|
146
155
|
- test/dummy/app/controllers/application_controller.rb
|
@@ -205,6 +214,7 @@ files:
|
|
205
214
|
- test/fixtures/steam_locomotives.yml
|
206
215
|
- test/fixtures/steam_trains.yml
|
207
216
|
- test/install_generator_test.rb
|
217
|
+
- test/instantiation_test.rb
|
208
218
|
- test/migration_test.rb
|
209
219
|
- test/pg_insert_returning_with_rules_spec.rb
|
210
220
|
- test/schema_test.rb
|
@@ -215,7 +225,7 @@ homepage: http://github.com/tutuf/updateable_views_inheritance
|
|
215
225
|
licenses:
|
216
226
|
- MIT
|
217
227
|
metadata: {}
|
218
|
-
post_install_message:
|
228
|
+
post_install_message:
|
219
229
|
rdoc_options: []
|
220
230
|
require_paths:
|
221
231
|
- lib
|
@@ -230,15 +240,15 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
230
240
|
- !ruby/object:Gem::Version
|
231
241
|
version: '0'
|
232
242
|
requirements: []
|
233
|
-
|
234
|
-
|
235
|
-
signing_key:
|
243
|
+
rubygems_version: 3.4.12
|
244
|
+
signing_key:
|
236
245
|
specification_version: 4
|
237
246
|
summary: Class table inheritance for ActiveRecord
|
238
247
|
test_files:
|
239
248
|
- test/content_test.rb
|
240
249
|
- test/deep_hierarchy_test.rb
|
241
250
|
- test/dummy/Rakefile
|
251
|
+
- test/dummy/app/assets/config/manifest.js
|
242
252
|
- test/dummy/app/assets/javascripts/application.js
|
243
253
|
- test/dummy/app/assets/stylesheets/application.css
|
244
254
|
- test/dummy/app/controllers/application_controller.rb
|
@@ -303,6 +313,7 @@ test_files:
|
|
303
313
|
- test/fixtures/steam_locomotives.yml
|
304
314
|
- test/fixtures/steam_trains.yml
|
305
315
|
- test/install_generator_test.rb
|
316
|
+
- test/instantiation_test.rb
|
306
317
|
- test/migration_test.rb
|
307
318
|
- test/pg_insert_returning_with_rules_spec.rb
|
308
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>).
|