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 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