updateable_views_inheritance 1.4.4 → 1.4.6
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 +4 -4
- data/CHANGELOG.md +7 -1
- data/README.md +27 -1
- data/lib/updateable_views_inheritance/postgresql_adapter.rb +27 -20
- data/lib/updateable_views_inheritance/version.rb +1 -1
- data/test/fixtures/migrations/2_create_with_default_table.rb +6 -6
- data/test/schema_test.rb +37 -6
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1c36199deb17d6a32a617a2193c52ef0cb5e6b955464fb31b8509eb4aef0212d
|
4
|
+
data.tar.gz: 62286805f012a6db4bbc44de4a3ac9848f735d7d0e90d674f7605cbd48c590da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a533be494b122a7b0e9bf2fcebd0083b314892cfe6da865faed721bd7cd5859820bc1eb969f098cfbb43ef4ec6d762521dbd9b911e57444fd286bdfa85540911
|
7
|
+
data.tar.gz: 45cd523f6e1430c3638ce7df73aabc87e09246a51d6b22591eea762c4d191fc7114c3f9a3f7d358dadf808c09844b8d5fa6a5f7480b45e61158f91796d1b8bf0
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# Class Table Inheritance
|
2
2
|
|
3
3
|
[](https://github.com/tutuf/updateable_views_inheritance/actions?query=workflow:build)
|
4
|
-
[](https://codecov.io/gh/tutuf/updateable_views_inheritance)
|
5
|
+
[](https://app.deepsource.com/gh/tutuf/updateable_views_inheritance/)
|
5
6
|
|
6
7
|
Class Table Inheritance for ActiveRecord using updateable views
|
7
8
|
|
@@ -138,6 +139,31 @@ end
|
|
138
139
|
```
|
139
140
|
Useful when converting legacy DB schema to use inheritance.
|
140
141
|
|
142
|
+
### Using view as a child table
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
execute <<-SQL.squish
|
146
|
+
CREATE VIEW punk_locomotives_data AS (
|
147
|
+
SELECT steam_locomotives.id,
|
148
|
+
steam_locomotives.coal_consumption AS coal,
|
149
|
+
NULL AS electro
|
150
|
+
FROM steam_locomotives
|
151
|
+
UNION ALL
|
152
|
+
SELECT electric_locomotives.id,
|
153
|
+
NULL AS coal,
|
154
|
+
electric_locomotives.electricity_consumption AS electro
|
155
|
+
FROM electric_locomotives)
|
156
|
+
SQL
|
157
|
+
create_child(:punk_locomotives,
|
158
|
+
{ parent: :locomotives,
|
159
|
+
child_table: :punk_locomotives_data,
|
160
|
+
child_table_pk: :id,
|
161
|
+
skip_creating_child_table: true })
|
162
|
+
```
|
163
|
+
Views in PostgreSQL cannot have primary keys, so you have to manually specify it
|
164
|
+
when you use. Note that views also cannot have `NOT NULL` constraints, although
|
165
|
+
the `NOT NULL` constraint of the underlying table will still be enforced.
|
166
|
+
|
141
167
|
## Compatibility with Single Table Inheritance
|
142
168
|
|
143
169
|
The approach of this gem is completely independent from Rails built-in Single
|
@@ -7,11 +7,16 @@ module ActiveRecord #:nodoc:
|
|
7
7
|
# Use this in migration to create child table and view.
|
8
8
|
# Options:
|
9
9
|
# [:parent]
|
10
|
-
#
|
10
|
+
# Parent relation
|
11
11
|
# [:table]
|
12
|
-
#
|
12
|
+
# Deprecated. Use :child_table instead
|
13
|
+
# [:child_table]
|
14
|
+
# Default is <tt>"#{child_view}_data"</tt>
|
15
|
+
# [:child_table_pk]
|
16
|
+
# Handy when :child_table is a view and PK cannot be inferred
|
17
|
+
# from the database.
|
13
18
|
# [:skip_creating_child_table]
|
14
|
-
#
|
19
|
+
# When given, :child_table option also must be specified
|
15
20
|
def create_child(child_view, options)
|
16
21
|
raise 'Please call me with a parent, for example: create_child(:steam_locomotives, :parent => :locomotives)' unless options[:parent]
|
17
22
|
|
@@ -26,7 +31,7 @@ module ActiveRecord #:nodoc:
|
|
26
31
|
parent_relation
|
27
32
|
end
|
28
33
|
|
29
|
-
child_table = options[:table] || quote_table_name("#{child_view}_data")
|
34
|
+
child_table = options[:child_table] || options[:table] || quote_table_name("#{child_view}_data")
|
30
35
|
|
31
36
|
unless options.key?(:skip_creating_child_table)
|
32
37
|
unqualified_child_view_name = Utils.extract_schema_qualified_name(child_view).identifier
|
@@ -41,7 +46,9 @@ module ActiveRecord #:nodoc:
|
|
41
46
|
REFERENCES #{parent_table} ON DELETE CASCADE ON UPDATE CASCADE"
|
42
47
|
end
|
43
48
|
|
44
|
-
|
49
|
+
child_table_pk ||= options[:child_table_pk].to_s if options[:child_table_pk]
|
50
|
+
|
51
|
+
create_child_view(parent_relation, child_view, child_table, child_table_pk)
|
45
52
|
end
|
46
53
|
|
47
54
|
# Drop child view and table
|
@@ -54,7 +61,7 @@ module ActiveRecord #:nodoc:
|
|
54
61
|
|
55
62
|
# Creates aggregate updateable view of parent and child relations. The convention for naming child tables is
|
56
63
|
# <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)
|
64
|
+
def create_child_view(parent_table, child_view, child_table=nil, child_table_pk=nil)
|
58
65
|
child_table ||= child_view.to_s + "_data"
|
59
66
|
|
60
67
|
parent_columns = columns(parent_table)
|
@@ -63,7 +70,7 @@ module ActiveRecord #:nodoc:
|
|
63
70
|
child_column_names = child_columns.collect{|c| c.name}
|
64
71
|
parent_column_names = parent_columns.collect{|c| c.name}
|
65
72
|
|
66
|
-
child_pk = pk_and_sequence_for(child_table)[0]
|
73
|
+
child_pk = child_table_pk || pk_and_sequence_for(child_table)[0]
|
67
74
|
child_column_names.delete(child_pk)
|
68
75
|
|
69
76
|
parent_pk, parent_pk_seq = pk_and_sequence_for(parent_table)
|
@@ -106,10 +113,12 @@ module ActiveRecord #:nodoc:
|
|
106
113
|
res && res.first
|
107
114
|
end
|
108
115
|
|
109
|
-
# Returns a relation's primary key and belonging sequence.
|
110
|
-
#
|
116
|
+
# Returns a relation's primary key and belonging sequence.
|
117
|
+
# If +relation+ is a table the result is its PK and sequence.
|
118
|
+
# When it is a view, PK and sequence of the table at the root
|
119
|
+
# of the inheritance chain are returned.
|
111
120
|
def pk_and_sequence_for(relation)
|
112
|
-
result = query(<<-SQL, 'PK')[0]
|
121
|
+
result = query(<<-SQL.squish, 'PK')[0]
|
113
122
|
SELECT attr.attname
|
114
123
|
FROM pg_attribute attr,
|
115
124
|
pg_constraint cons
|
@@ -118,15 +127,13 @@ module ActiveRecord #:nodoc:
|
|
118
127
|
AND cons.contype = 'p'
|
119
128
|
AND attr.attnum = ANY(cons.conkey)
|
120
129
|
SQL
|
121
|
-
if result.
|
130
|
+
if result.blank? #result.empty?
|
122
131
|
parent = parent_table(relation)
|
123
132
|
pk_and_sequence_for(parent) if parent
|
124
133
|
else
|
125
134
|
# log(result[0], "PK for #{relation}") {}
|
126
135
|
[result[0], query("SELECT pg_get_serial_sequence('#{relation}', '#{result[0]}') ")[0][0]]
|
127
136
|
end
|
128
|
-
rescue
|
129
|
-
nil
|
130
137
|
end
|
131
138
|
|
132
139
|
# Drops a view from the database.
|
@@ -305,7 +312,7 @@ module ActiveRecord #:nodoc:
|
|
305
312
|
def do_create_child_view(parent_table, parent_columns, parent_pk, child_view, child_columns, child_pk, child_table)
|
306
313
|
view_columns = parent_columns + child_columns
|
307
314
|
execute(<<~SQL)
|
308
|
-
CREATE OR REPLACE VIEW #{
|
315
|
+
CREATE OR REPLACE VIEW #{quote_table_name(child_view)} AS (
|
309
316
|
SELECT parent.#{parent_pk},
|
310
317
|
#{ view_columns.map { |col| quote_column_name(col) }.join(",") }
|
311
318
|
FROM #{parent_table} parent
|
@@ -322,7 +329,7 @@ module ActiveRecord #:nodoc:
|
|
322
329
|
# Setting the sequence to its value (explicitly supplied or the default) covers both cases.
|
323
330
|
execute(<<~SQL)
|
324
331
|
CREATE OR REPLACE RULE #{quote_column_name("#{child_view}_insert")} AS
|
325
|
-
ON INSERT TO #{
|
332
|
+
ON INSERT TO #{quote_table_name(child_view)} DO INSTEAD (
|
326
333
|
INSERT INTO #{parent_table}
|
327
334
|
( #{ [parent_pk, parent_columns].flatten.map { |col| quote_column_name(col) }.join(", ") } )
|
328
335
|
VALUES( DEFAULT #{ parent_columns.empty? ? '' : ' ,' + parent_columns.collect{ |col| "NEW.#{quote_column_name(col)}" }.join(", ") } ) ;
|
@@ -336,14 +343,14 @@ module ActiveRecord #:nodoc:
|
|
336
343
|
# delete
|
337
344
|
execute(<<~SQL)
|
338
345
|
CREATE OR REPLACE RULE #{quote_column_name("#{child_view}_delete")} AS
|
339
|
-
ON DELETE TO #{
|
346
|
+
ON DELETE TO #{quote_table_name(child_view)} DO INSTEAD
|
340
347
|
DELETE FROM #{parent_table} WHERE #{parent_pk} = OLD.#{parent_pk}
|
341
348
|
SQL
|
342
349
|
|
343
350
|
# update
|
344
351
|
execute(<<~SQL)
|
345
352
|
CREATE OR REPLACE RULE #{quote_column_name("#{child_view}_update")} AS
|
346
|
-
ON UPDATE TO #{
|
353
|
+
ON UPDATE TO #{quote_table_name(child_view)} DO INSTEAD (
|
347
354
|
#{ if parent_columns.empty?
|
348
355
|
''
|
349
356
|
else
|
@@ -364,9 +371,9 @@ module ActiveRecord #:nodoc:
|
|
364
371
|
|
365
372
|
def insert_returning_clause(parent_pk, child_pk, child_view)
|
366
373
|
columns_cast_to_null = columns(child_view)
|
367
|
-
|
368
|
-
|
369
|
-
|
374
|
+
.reject { |c| c.name == parent_pk }
|
375
|
+
.map { |c| "CAST (NULL AS #{c.sql_type})" }
|
376
|
+
.join(", ")
|
370
377
|
"RETURNING #{child_pk}, #{columns_cast_to_null}"
|
371
378
|
end
|
372
379
|
|
@@ -5,15 +5,15 @@ class CreateWithDefaultTable < ActiveRecord::Migration
|
|
5
5
|
t.column :max_speed, :integer
|
6
6
|
t.column :type, :string
|
7
7
|
end
|
8
|
-
|
9
|
-
create_child(:steam_locomotives, :
|
10
|
-
t.decimal :water_consumption, :
|
11
|
-
t.decimal :coal_consumption, :
|
8
|
+
|
9
|
+
create_child(:steam_locomotives, parent: :locomotives) do |t|
|
10
|
+
t.decimal :water_consumption, precision: 6, scale: 2, null: false
|
11
|
+
t.decimal :coal_consumption, precision: 6, scale: 2, null: false
|
12
12
|
end
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
def self.down
|
16
16
|
drop_child :steam_locomotives
|
17
17
|
drop_table :locomotives
|
18
18
|
end
|
19
|
-
end
|
19
|
+
end
|
data/test/schema_test.rb
CHANGED
@@ -80,6 +80,10 @@ class UpdateableViewsInheritanceSchemaTest < ActiveSupport::TestCase
|
|
80
80
|
assert RackLocomotive.new.narrow_gauge
|
81
81
|
end
|
82
82
|
|
83
|
+
def test_does_not_preserve_not_null_on_views
|
84
|
+
assert SteamLocomotive.columns.find { |c| c.name == 'water_consumption' }.null
|
85
|
+
end
|
86
|
+
|
83
87
|
class ChangeDefaultValueOfColumn < ActiveRecord::Migration
|
84
88
|
def self.up
|
85
89
|
remove_parent_and_children_views(:rack_locomotives)
|
@@ -184,22 +188,22 @@ class UpdateableViewsInheritanceSchemaTest < ActiveSupport::TestCase
|
|
184
188
|
t.column :interrail_max_speed, :integer
|
185
189
|
t.column :type, :string
|
186
190
|
end
|
187
|
-
create_child('interrail.steam_locomotives', :
|
188
|
-
t.decimal :interrail_water_consumption, :
|
189
|
-
t.decimal :interrail_coal_consumption, :
|
191
|
+
create_child('interrail.steam_locomotives', parent: 'interrail.locomotives') do |t|
|
192
|
+
t.decimal :interrail_water_consumption, precision: 6, scale: 2
|
193
|
+
t.decimal :interrail_coal_consumption, precision: 6, scale: 2
|
190
194
|
end
|
191
195
|
end
|
192
196
|
end
|
193
197
|
|
194
198
|
def test_create_child_in_schema
|
195
199
|
CreateChildInSchema.up
|
196
|
-
assert_equal %w
|
200
|
+
assert_equal %w[id
|
197
201
|
interrail_coal_consumption
|
198
202
|
interrail_max_speed
|
199
203
|
interrail_name
|
200
204
|
interrail_water_consumption
|
201
|
-
type
|
202
|
-
@connection.columns('interrail.steam_locomotives').map
|
205
|
+
type],
|
206
|
+
@connection.columns('interrail.steam_locomotives').map(&:name).sort
|
203
207
|
end
|
204
208
|
|
205
209
|
class ChangeTablesInTwoInheritanceChains < ActiveRecord::Migration
|
@@ -251,4 +255,31 @@ class UpdateableViewsInheritanceSchemaTest < ActiveSupport::TestCase
|
|
251
255
|
ReservedSQLWords.up
|
252
256
|
assert @connection.columns(:table).map(&:name).include?("column")
|
253
257
|
end
|
258
|
+
|
259
|
+
class ChildTableIsActuallyView < ActiveRecord::Migration
|
260
|
+
def self.up
|
261
|
+
execute <<-SQL.squish
|
262
|
+
CREATE VIEW punk_locomotives_data AS (
|
263
|
+
SELECT steam_locomotives.id,
|
264
|
+
steam_locomotives.coal_consumption AS coal,
|
265
|
+
NULL AS electro
|
266
|
+
FROM steam_locomotives
|
267
|
+
UNION ALL
|
268
|
+
SELECT electric_locomotives.id,
|
269
|
+
NULL AS coal,
|
270
|
+
electric_locomotives.electricity_consumption AS electro
|
271
|
+
FROM electric_locomotives)
|
272
|
+
SQL
|
273
|
+
create_child(:punk_locomotives,
|
274
|
+
{ parent: :locomotives,
|
275
|
+
child_table: :punk_locomotives_data,
|
276
|
+
child_table_pk: :id,
|
277
|
+
skip_creating_child_table: true })
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def test_child_table_is_view
|
282
|
+
ChildTableIsActuallyView.up
|
283
|
+
assert @connection.columns(:punk_locomotives).map(&:name).sort == %w(coal electro id max_speed name type)
|
284
|
+
end
|
254
285
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
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.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sava Chankov
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2024-10-
|
12
|
+
date: 2024-10-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -271,7 +271,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
271
271
|
- !ruby/object:Gem::Version
|
272
272
|
version: '0'
|
273
273
|
requirements: []
|
274
|
-
rubygems_version: 3.
|
274
|
+
rubygems_version: 3.1.6
|
275
275
|
signing_key:
|
276
276
|
specification_version: 4
|
277
277
|
summary: Class table inheritance for ActiveRecord
|