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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 315a60d914bac0fcce5129cddb165afa90604355e2c4bc9c46cad1b2d5aac79a
4
- data.tar.gz: 5b93a4be4ddba10b47eaaaddbd5ba7cb5df072b2462114e01945e2c2f9b051d6
3
+ metadata.gz: 2fa6d4449542465ef3cc8420e9428a6faee1b4a91c547f5df660aeb615c15638
4
+ data.tar.gz: f945d9a76c78658e9c3a7a357f9c46202377ccd11488ec1cd286dfb467dd8eb2
5
5
  SHA512:
6
- metadata.gz: 502088f01538ad5c31fbb4052f2a03c7f4dbe5723533d452b07abb331dae7d20c6dc4b87f3c5e019731c2b6cfce43834726f4cc89febf32e971593d2ecb39e44
7
- data.tar.gz: 74c62d1c094c033ff0224af98128fe0d65a114e1f36099845cf1d3312cbd39d34a73db71f65b275eda92746be51d063e62ca65feb31849578833951af8115976
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
- gem "minitest"
5
- gem "rails", "~> #{ENV.fetch("RAILS_VERSION", "7.0")}"
6
- gem "rake"
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
- It does, but there are decent arguments against the default Rails way of doing polymorphism.
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 CommentCommentableExclusiveArc < ActiveRecord::Migration[7.0]
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: :commentable
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.2
134
- * Rails 6.1 and 7.0
135
- * `postgresql` and `sqlite3` database adapters
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
 
@@ -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
- validate :validate_exclusive_arcs
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 validate_exclusive_arcs
58
- exclusive_arcs.each do |(arc, definition)|
59
- foreign_key_count = definition.reflections.keys.count { |name| !!public_send(name) }
60
- valid = definition.options[:optional] ? foreign_key_count.in?([0, 1]) : foreign_key_count == 1
61
- errors.add(arc, :arc_not_exclusive) unless valid
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
@@ -1,3 +1,3 @@
1
1
  module ExclusiveArc
2
- VERSION = "0.2.3"
2
+ VERSION = "0.3.1"
3
3
  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}has_exclusive_arc #{model_exclusive_arcs}
34
+ #{indents}include ExclusiveArc::Model
35
35
  RB
36
36
  )
37
- inject_into_class(
37
+ gsub_file(
38
38
  model_file_path,
39
- class_name.demodulize,
40
- <<~RB
41
- #{indents}include ExclusiveArc::Model
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}_exclusive_arc.rb"
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
- [class_name.delete(":").singularize, arc.classify, "ExclusiveArc"].join
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}",
@@ -1,6 +1,7 @@
1
1
  class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
3
  <%= add_references %>
4
+ <%= remove_check_constraint %>
4
5
  <%= add_check_constraint %>
5
6
  end
6
7
  end
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.2.3
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: 2023-05-19 00:00:00.000000000 Z
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.12
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