activerecord-exclusive-arc 0.2.3 → 0.3.1
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/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
|