activerecord-exclusive-arc 0.2.3 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +6 -3
- data/README.md +43 -15
- data/lib/exclusive_arc/model.rb +11 -8
- data/lib/exclusive_arc/version.rb +1 -1
- data/lib/generators/exclusive_arc_generator.rb +48 -10
- data/lib/generators/templates/migration.rb.erb +1 -0
- 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: 2fa6d4449542465ef3cc8420e9428a6faee1b4a91c547f5df660aeb615c15638
|
4
|
+
data.tar.gz: f945d9a76c78658e9c3a7a357f9c46202377ccd11488ec1cd286dfb467dd8eb2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c829330cc1049ffddf223122cab10681e454f13184ea0ae7218bb55ed8f3b90e3ede9e140907917b2402f7a5740b0857ffe5c71dd0ed798a9a82cc364c30438
|
7
|
+
data.tar.gz: 140aea768b0e93d5e7cf22b7c0c6517a7ce19ecc64e48bce405501ed06adcc47072a8f789bccabb20fb9b0fb366b2928d5b14573bff9bed91245a4262fb360af
|
data/Gemfile
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
source "https://rubygems.org"
|
2
2
|
|
3
3
|
gem "debug"
|
4
|
-
|
5
|
-
gem "rails", "~> #{
|
6
|
-
|
4
|
+
if (rails_version = ENV["RAILS_VERSION"])
|
5
|
+
gem "rails", "~> #{rails_version}.0"
|
6
|
+
else
|
7
|
+
gem "rails"
|
8
|
+
end
|
7
9
|
gem "pg"
|
8
10
|
gem "sqlite3"
|
9
11
|
gem "standard", "~> 1.26"
|
12
|
+
gem "mysql2"
|
10
13
|
|
11
14
|
gemspec
|
data/README.md
CHANGED
@@ -5,16 +5,7 @@ types of ActiveRecord models.
|
|
5
5
|
|
6
6
|
### Doesn’t Rails already provide a way to do this?
|
7
7
|
|
8
|
-
|
9
|
-
Consider the fact that the Ruby class name is stored in the database as a string. If you want to
|
10
|
-
change the name of the Ruby class used for such reasons, you must also update the database strings
|
11
|
-
that represent it. The seeping of application-layer definitions into the database may become a
|
12
|
-
liability.
|
13
|
-
|
14
|
-
Another common argument concerns referential integrity. _Foreign Key Constraints_ are a common
|
15
|
-
mechanism to ensure primary keys of database tables can be reliably used as foreign keys on others.
|
16
|
-
This becomes harder to enforce in the databse when a string column that represents a Ruby class is
|
17
|
-
one of the components required for unique identification.
|
8
|
+
Yes but [here’s a post about why this exists](https://waymondo.com/posts/are-exclusive-arcs-evil/).
|
18
9
|
|
19
10
|
### So how does this work?
|
20
11
|
|
@@ -88,14 +79,14 @@ Continuing with our example, the generator command would also produce a migratio
|
|
88
79
|
this:
|
89
80
|
|
90
81
|
```ruby
|
91
|
-
class
|
82
|
+
class CommentCommentableExclusiveArcPostComment < ActiveRecord::Migration[7.0]
|
92
83
|
def change
|
93
84
|
add_reference :comments, :post, foreign_key: true, index: {where: "post_id IS NOT NULL"}
|
94
85
|
add_reference :comments, :comment, foreign_key: true, index: {where: "comment_id IS NOT NULL"}
|
95
86
|
add_check_constraint(
|
96
87
|
:comments,
|
97
88
|
"(CASE WHEN post_id IS NULL THEN 0 ELSE 1 END + CASE WHEN comment_id IS NULL THEN 0 ELSE 1 END) = 1",
|
98
|
-
name:
|
89
|
+
name: "commentable"
|
99
90
|
)
|
100
91
|
end
|
101
92
|
end
|
@@ -127,12 +118,49 @@ Notably, if you want to make an Exclusive Arc optional, you can use the `--optio
|
|
127
118
|
adjust the definition in your `ActiveRecord` model and loosen both the validation and database check
|
128
119
|
constraint so that there can be 0 or 1 foreign keys set for the polymorphic reference.
|
129
120
|
|
121
|
+
### Updating an existing exclusive arc
|
122
|
+
|
123
|
+
If you need to add an additional polymorphic option to an existing exclusive arc, you can simply run
|
124
|
+
the generator command again with the additional target. Existing references will be skipped and the
|
125
|
+
check constraint will be removed and re-added in a reversible manner.
|
126
|
+
|
127
|
+
```
|
128
|
+
bin/rails g exclusive_arc Comment commentable post comment page
|
129
|
+
```
|
130
|
+
|
131
|
+
``` ruby
|
132
|
+
class CommentCommentableExclusiveArcPostCommentPage < ActiveRecord::Migration[7.0]
|
133
|
+
def change
|
134
|
+
add_reference :comments, :page, foreign_key: true, index: {where: "page_id IS NOT NULL"}
|
135
|
+
remove_check_constraint(
|
136
|
+
:comments,
|
137
|
+
"(CASE WHEN post_id IS NULL THEN 0 ELSE 1 END + CASE WHEN comment_id IS NULL THEN 0 ELSE 1 END) = 1",
|
138
|
+
name: "commentable"
|
139
|
+
)
|
140
|
+
add_check_constraint(
|
141
|
+
:comments,
|
142
|
+
"(CASE WHEN post_id IS NULL THEN 0 ELSE 1 END + CASE WHEN comment_id IS NULL THEN 0 ELSE 1 END + CASE WHEN page_id IS NULL THEN 0 ELSE 1 END) = 1",
|
143
|
+
name: "commentable"
|
144
|
+
)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
The registration in the model will be updated as well.
|
150
|
+
|
151
|
+
``` ruby
|
152
|
+
class Comment < ApplicationRecord
|
153
|
+
include ExclusiveArc::Model
|
154
|
+
has_exclusive_arc :commentable, [:post, :comment, :page]
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
130
158
|
### Compatibility
|
131
159
|
|
132
160
|
Currently `activerecord-exclusive-arc` is tested against a matrix of:
|
133
|
-
* Ruby 2.7 and 3.
|
134
|
-
* Rails 6.1
|
135
|
-
* `postgresql` and `
|
161
|
+
* Ruby 2.7 and 3.3
|
162
|
+
* Rails 6.1, 7.0, 7.1
|
163
|
+
* `postgresql`, `sqlite3`, and `mysql2` database adapters
|
136
164
|
|
137
165
|
### Contributing
|
138
166
|
|
data/lib/exclusive_arc/model.rb
CHANGED
@@ -15,7 +15,9 @@ module ExclusiveArc
|
|
15
15
|
next if reflections[option.to_s]
|
16
16
|
|
17
17
|
belongs_to(option, optional: true)
|
18
|
+
validate :"validate_#{arc}"
|
18
19
|
end
|
20
|
+
|
19
21
|
exclusive_arcs[arc] = Definition.new(
|
20
22
|
reflections: reflections.slice(*belong_tos.map(&:to_s)),
|
21
23
|
options: args[2] || {}
|
@@ -39,9 +41,11 @@ module ExclusiveArc
|
|
39
41
|
assign_exclusive_arc(:#{arc}, polymorphic)
|
40
42
|
@#{arc} = polymorphic
|
41
43
|
end
|
42
|
-
RUBY
|
43
44
|
|
44
|
-
|
45
|
+
def validate_#{arc}
|
46
|
+
validate_exclusive_arc(:#{arc})
|
47
|
+
end
|
48
|
+
RUBY
|
45
49
|
end
|
46
50
|
end
|
47
51
|
|
@@ -54,12 +58,11 @@ module ExclusiveArc
|
|
54
58
|
assign_attributes attributes
|
55
59
|
end
|
56
60
|
|
57
|
-
def
|
58
|
-
exclusive_arcs.
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
end
|
61
|
+
def validate_exclusive_arc(arc)
|
62
|
+
definition = exclusive_arcs.fetch(arc)
|
63
|
+
foreign_key_count = definition.reflections.keys.count { |name| !!public_send(name) }
|
64
|
+
valid = definition.options[:optional] ? foreign_key_count.in?([0, 1]) : foreign_key_count == 1
|
65
|
+
errors.add(arc, :arc_not_exclusive) unless valid
|
63
66
|
end
|
64
67
|
end
|
65
68
|
end
|
@@ -31,15 +31,20 @@ class ExclusiveArcGenerator < ActiveRecord::Generators::Base
|
|
31
31
|
model_file_path,
|
32
32
|
class_name.demodulize,
|
33
33
|
<<~RB
|
34
|
-
#{indents}
|
34
|
+
#{indents}include ExclusiveArc::Model
|
35
35
|
RB
|
36
36
|
)
|
37
|
-
|
37
|
+
gsub_file(
|
38
38
|
model_file_path,
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
/has_exclusive_arc :#{arc}(.*)$/,
|
40
|
+
""
|
41
|
+
)
|
42
|
+
inject_into_file(
|
43
|
+
model_file_path,
|
44
|
+
<<~RB,
|
45
|
+
#{indents}has_exclusive_arc #{model_exclusive_arcs}
|
42
46
|
RB
|
47
|
+
after: "include ExclusiveArc::Model\n"
|
43
48
|
)
|
44
49
|
end
|
45
50
|
|
@@ -52,8 +57,8 @@ class ExclusiveArcGenerator < ActiveRecord::Generators::Base
|
|
52
57
|
|
53
58
|
def add_references
|
54
59
|
belong_tos.map do |reference|
|
55
|
-
add_reference(reference)
|
56
|
-
end.join("\n")
|
60
|
+
add_reference(reference) unless column_exists?(reference)
|
61
|
+
end.compact.join("\n")
|
57
62
|
end
|
58
63
|
|
59
64
|
def add_reference(reference)
|
@@ -78,11 +83,22 @@ class ExclusiveArcGenerator < ActiveRecord::Generators::Base
|
|
78
83
|
end
|
79
84
|
|
80
85
|
def migration_file_name
|
81
|
-
"#{class_name.delete(":").underscore}_#{arc}
|
86
|
+
"#{class_name.delete(":").underscore}_#{arc}_exclusive_arc_#{belong_tos.map(&:underscore).join("_")}.rb"
|
82
87
|
end
|
83
88
|
|
84
89
|
def migration_class_name
|
85
|
-
|
90
|
+
(
|
91
|
+
[class_name.delete(":").singularize, arc.classify, "ExclusiveArc"] |
|
92
|
+
belong_tos.map(&:classify)
|
93
|
+
).join
|
94
|
+
end
|
95
|
+
|
96
|
+
def existing_check_constraint
|
97
|
+
@existing_check_constraint ||=
|
98
|
+
class_name.constantize.connection.check_constraints(class_name.constantize.table_name)
|
99
|
+
.find { |constraint| constraint.name == arc }
|
100
|
+
rescue
|
101
|
+
nil
|
86
102
|
end
|
87
103
|
|
88
104
|
def reference_type(reference)
|
@@ -98,15 +114,37 @@ class ExclusiveArcGenerator < ActiveRecord::Generators::Base
|
|
98
114
|
reference.tableize
|
99
115
|
end
|
100
116
|
|
117
|
+
def column_exists?(reference)
|
118
|
+
foreign_key = foreign_key_name(reference)
|
119
|
+
class_name.constantize.column_names.include?(foreign_key)
|
120
|
+
rescue
|
121
|
+
false
|
122
|
+
end
|
123
|
+
|
101
124
|
def foreign_key_name(reference)
|
102
125
|
class_name.constantize.reflections[reference].foreign_key
|
103
126
|
rescue
|
104
127
|
"#{reference}_id"
|
105
128
|
end
|
106
129
|
|
130
|
+
def remove_check_constraint
|
131
|
+
return unless class_name.constantize.connection.supports_check_constraints?
|
132
|
+
return if options[:skip_check_constraint]
|
133
|
+
return unless existing_check_constraint&.expression
|
134
|
+
|
135
|
+
<<-RUBY.chomp
|
136
|
+
remove_check_constraint(
|
137
|
+
:#{table_name},
|
138
|
+
"#{existing_check_constraint.expression.squish}",
|
139
|
+
name: "#{arc}"
|
140
|
+
)
|
141
|
+
RUBY
|
142
|
+
end
|
143
|
+
|
107
144
|
def add_check_constraint
|
145
|
+
return unless class_name.constantize.connection.supports_check_constraints?
|
108
146
|
return if options[:skip_check_constraint]
|
109
|
-
<<-RUBY
|
147
|
+
<<-RUBY.chomp
|
110
148
|
add_check_constraint(
|
111
149
|
:#{table_name},
|
112
150
|
"#{check_constraint}",
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-exclusive-arc
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- justin talbott
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -89,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
89
|
- !ruby/object:Gem::Version
|
90
90
|
version: '0'
|
91
91
|
requirements: []
|
92
|
-
rubygems_version: 3.4
|
92
|
+
rubygems_version: 3.5.4
|
93
93
|
signing_key:
|
94
94
|
specification_version: 4
|
95
95
|
summary: An ActiveRecord extension for polymorphic exclusive arc relationships
|