exwiw 0.2.7 → 0.2.8
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 +6 -0
- data/README.md +41 -0
- data/lib/exwiw/adapter/mysql2_adapter.rb +19 -1
- data/lib/exwiw/adapter/postgresql_adapter.rb +19 -1
- data/lib/exwiw/adapter/sqlite3_adapter.rb +19 -1
- data/lib/exwiw/belongs_to.rb +13 -0
- data/lib/exwiw/query_ast.rb +11 -2
- data/lib/exwiw/query_ast_builder.rb +39 -1
- data/lib/exwiw/schema_generator.rb +38 -9
- data/lib/exwiw/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21a9432692220a23b0109497b0ffcb3f1c7f0fc360248c3590f730a4a98452ec
|
|
4
|
+
data.tar.gz: bf0847f90a24c8da3cf078f1c18e5d22f9827b705bc13945ddfc28f8e42460f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c59e201f1180e2fb6191506647b78a82c43d7d124117c0d87e2cdcbfd8fa68ff30653accc9a22d82434d7e680496eed41d91c7b150b8b884b3b545b344b91d72
|
|
7
|
+
data.tar.gz: 600e4a3b8d134d43182b81017b8b4ad9a352824a76a351300ca3fc22ee4942ceccf6f8404eb6e04e2a2320a79849ddc6b3cacf0c3510c7b8acca0c06820eaa22
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.2.8] - 2026-05-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `schema:generate` now supports polymorphic `belongs_to` associations. Each polymorphic relation (`belongs_to :reviewable, polymorphic: true`) is expanded into one `belongs_to` entry per concrete target table — discovered from the other models' `has_many` / `has_one ..., as:` declarations — carrying `foreign_type` (the type column, e.g. `reviewable_type`) and `type_value` (the stored type, e.g. `"Product"`). Targets are ordered by table name so the generated output is stable across Ruby versions. At dump time, a polymorphic `belongs_to` on the path to the dump target is constrained by both the foreign key and the type column — in both the `SELECT` and the `delete-*.sql` bulk-delete subquery — so only rows of the matching type are extracted. ([#43](https://github.com/heyinc/exwiw/pull/43))
|
|
10
|
+
|
|
5
11
|
## [0.2.7] - 2026-05-30
|
|
6
12
|
|
|
7
13
|
### Added
|
data/README.md
CHANGED
|
@@ -241,6 +241,47 @@ Constraints:
|
|
|
241
241
|
- Specifying a skipped table as `--target-table` raises `ArgumentError`.
|
|
242
242
|
- `skip: true` is preserved by `exwiw:schema:generate` regenerations (the receiver value wins over the auto-generated config).
|
|
243
243
|
|
|
244
|
+
### Polymorphic `belongs_to`
|
|
245
|
+
|
|
246
|
+
A Rails polymorphic association (`belongs_to :reviewable, polymorphic: true`) does not point at a single table — the target row is selected at runtime by a type column. exwiw models this as **one `belongs_to` entry per concrete target table**, each carrying two extra fields:
|
|
247
|
+
|
|
248
|
+
- `foreign_type` — the type column on *this* table (e.g. `reviewable_type`).
|
|
249
|
+
- `type_value` — the value stored in that column for this target (e.g. `"Product"`), i.e. the target model's `polymorphic_name`.
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"name": "reviews",
|
|
254
|
+
"primary_key": "id",
|
|
255
|
+
"belongs_tos": [
|
|
256
|
+
{
|
|
257
|
+
"table_name": "products",
|
|
258
|
+
"foreign_key": "reviewable_id",
|
|
259
|
+
"foreign_type": "reviewable_type",
|
|
260
|
+
"type_value": "Product"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"table_name": "shops",
|
|
264
|
+
"foreign_key": "reviewable_id",
|
|
265
|
+
"foreign_type": "reviewable_type",
|
|
266
|
+
"type_value": "Shop"
|
|
267
|
+
}
|
|
268
|
+
],
|
|
269
|
+
"columns": [{ "name": "id" }, { "name": "reviewable_type" }, { "name": "reviewable_id" }]
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`exwiw:schema:generate` expands a polymorphic `belongs_to` automatically: it finds every model that registers the association as a target via `has_many` / `has_one ..., as: :reviewable` and emits one entry per target table (ordered by table name so the output is stable across Ruby versions). A plain (non-polymorphic) `belongs_to` simply omits `foreign_type` / `type_value`.
|
|
274
|
+
|
|
275
|
+
At dump time, when a polymorphic `belongs_to` lies on the path to the dump target, exwiw constrains **both** the foreign key and the type column, so only rows of the matching type are extracted. For example, dumping `products` pulls only reviews whose `reviewable_type = 'Product'`:
|
|
276
|
+
|
|
277
|
+
```sql
|
|
278
|
+
SELECT reviews.* FROM reviews
|
|
279
|
+
WHERE reviews.reviewable_id IN (/* products subquery */)
|
|
280
|
+
AND reviews.reviewable_type = 'Product'
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The same type filter is applied on the join path — and in the matching `delete-*.sql` bulk-delete subquery — when the polymorphic table is an intermediate hop rather than the directly-dumped table.
|
|
284
|
+
|
|
244
285
|
### Rails-managed tables (special `type` values)
|
|
245
286
|
|
|
246
287
|
Some tables are owned by Rails itself rather than the application — they have no ActiveRecord model and Rails reserves the right to evolve their column shape between versions (e.g. `schema_migrations`, `ar_internal_metadata`). exwiw treats them as a distinct category via the `type` field on a table config:
|
|
@@ -136,7 +136,18 @@ module Exwiw
|
|
|
136
136
|
|
|
137
137
|
foreign_key = first_join.foreign_key
|
|
138
138
|
subquery_sql = compile_ast(subquery_ast)
|
|
139
|
-
sql += "\nWHERE #{select_query_ast.from_table_name}.#{foreign_key} IN (#{subquery_sql})
|
|
139
|
+
sql += "\nWHERE #{select_query_ast.from_table_name}.#{foreign_key} IN (#{subquery_sql})"
|
|
140
|
+
|
|
141
|
+
# first_join.base_where_clauses は外側の削除対象テーブル
|
|
142
|
+
# (from_table_name) 上の条件 (polymorphic 型カラム等)。subquery には
|
|
143
|
+
# 含まれないため、外側の WHERE に追加する。これにより、別の
|
|
144
|
+
# polymorphic 型に属する行まで削除してしまうのを防ぐ。
|
|
145
|
+
first_join.base_where_clauses.each do |where|
|
|
146
|
+
next unless where.is_a?(Exwiw::QueryAst::WhereClause)
|
|
147
|
+
|
|
148
|
+
sql += " AND #{compile_where_condition(where, select_query_ast.from_table_name)}"
|
|
149
|
+
end
|
|
150
|
+
sql += ";"
|
|
140
151
|
|
|
141
152
|
sql
|
|
142
153
|
end
|
|
@@ -159,6 +170,13 @@ module Exwiw
|
|
|
159
170
|
compiled_where_condition = compile_where_condition(where, join.join_table_name)
|
|
160
171
|
sql += " AND #{compiled_where_condition}"
|
|
161
172
|
end
|
|
173
|
+
|
|
174
|
+
# base_where_clauses は結合元テーブル (base_table_name) に対して
|
|
175
|
+
# コンパイルする。polymorphic な結合元テーブルの型カラム絞り込み等。
|
|
176
|
+
join.base_where_clauses.each do |where|
|
|
177
|
+
compiled_where_condition = compile_where_condition(where, join.base_table_name)
|
|
178
|
+
sql += " AND #{compiled_where_condition}"
|
|
179
|
+
end
|
|
162
180
|
end
|
|
163
181
|
|
|
164
182
|
if query_ast.where_clauses.any?
|
|
@@ -178,7 +178,18 @@ module Exwiw
|
|
|
178
178
|
|
|
179
179
|
foreign_key = first_join.foreign_key
|
|
180
180
|
subquery_sql = compile_ast(subquery_ast)
|
|
181
|
-
sql += "\nWHERE #{select_query_ast.from_table_name}.#{foreign_key} IN (#{subquery_sql})
|
|
181
|
+
sql += "\nWHERE #{select_query_ast.from_table_name}.#{foreign_key} IN (#{subquery_sql})"
|
|
182
|
+
|
|
183
|
+
# first_join.base_where_clauses は外側の削除対象テーブル
|
|
184
|
+
# (from_table_name) 上の条件 (polymorphic 型カラム等)。subquery には
|
|
185
|
+
# 含まれないため、外側の WHERE に追加する。これにより、別の
|
|
186
|
+
# polymorphic 型に属する行まで削除してしまうのを防ぐ。
|
|
187
|
+
first_join.base_where_clauses.each do |where|
|
|
188
|
+
next unless where.is_a?(Exwiw::QueryAst::WhereClause)
|
|
189
|
+
|
|
190
|
+
sql += " AND #{compile_where_condition(where, select_query_ast.from_table_name)}"
|
|
191
|
+
end
|
|
192
|
+
sql += ";"
|
|
182
193
|
|
|
183
194
|
sql
|
|
184
195
|
end
|
|
@@ -201,6 +212,13 @@ module Exwiw
|
|
|
201
212
|
compiled_where_condition = compile_where_condition(where, join.join_table_name)
|
|
202
213
|
sql += " AND #{compiled_where_condition}"
|
|
203
214
|
end
|
|
215
|
+
|
|
216
|
+
# base_where_clauses は結合元テーブル (base_table_name) に対して
|
|
217
|
+
# コンパイルする。polymorphic な結合元テーブルの型カラム絞り込み等。
|
|
218
|
+
join.base_where_clauses.each do |where|
|
|
219
|
+
compiled_where_condition = compile_where_condition(where, join.base_table_name)
|
|
220
|
+
sql += " AND #{compiled_where_condition}"
|
|
221
|
+
end
|
|
204
222
|
end
|
|
205
223
|
|
|
206
224
|
if query_ast.where_clauses.any?
|
|
@@ -123,7 +123,18 @@ module Exwiw
|
|
|
123
123
|
|
|
124
124
|
foreign_key = first_join.foreign_key
|
|
125
125
|
subquery_sql = compile_ast(subquery_ast)
|
|
126
|
-
sql += "\nWHERE #{select_query_ast.from_table_name}.#{foreign_key} IN (#{subquery_sql})
|
|
126
|
+
sql += "\nWHERE #{select_query_ast.from_table_name}.#{foreign_key} IN (#{subquery_sql})"
|
|
127
|
+
|
|
128
|
+
# first_join.base_where_clauses は外側の削除対象テーブル
|
|
129
|
+
# (from_table_name) 上の条件 (polymorphic 型カラム等)。subquery には
|
|
130
|
+
# 含まれないため、外側の WHERE に追加する。これにより、別の
|
|
131
|
+
# polymorphic 型に属する行まで削除してしまうのを防ぐ。
|
|
132
|
+
first_join.base_where_clauses.each do |where|
|
|
133
|
+
next unless where.is_a?(Exwiw::QueryAst::WhereClause)
|
|
134
|
+
|
|
135
|
+
sql += " AND #{compile_where_condition(where, select_query_ast.from_table_name)}"
|
|
136
|
+
end
|
|
137
|
+
sql += ";"
|
|
127
138
|
|
|
128
139
|
sql
|
|
129
140
|
end
|
|
@@ -146,6 +157,13 @@ module Exwiw
|
|
|
146
157
|
compiled_where_condition = compile_where_condition(where, join.join_table_name)
|
|
147
158
|
sql += " AND #{compiled_where_condition}"
|
|
148
159
|
end
|
|
160
|
+
|
|
161
|
+
# base_where_clauses は結合元テーブル (base_table_name) に対して
|
|
162
|
+
# コンパイルする。polymorphic な結合元テーブルの型カラム絞り込み等。
|
|
163
|
+
join.base_where_clauses.each do |where|
|
|
164
|
+
compiled_where_condition = compile_where_condition(where, join.base_table_name)
|
|
165
|
+
sql += " AND #{compiled_where_condition}"
|
|
166
|
+
end
|
|
149
167
|
end
|
|
150
168
|
|
|
151
169
|
if query_ast.where_clauses.any?
|
data/lib/exwiw/belongs_to.rb
CHANGED
|
@@ -6,9 +6,22 @@ module Exwiw
|
|
|
6
6
|
|
|
7
7
|
attribute :foreign_key, String
|
|
8
8
|
attribute :table_name, String
|
|
9
|
+
# polymorphic 関連の場合のみ設定される。`foreign_type` は型を格納するカラム名
|
|
10
|
+
# (例: `reviewable_type`)、`type_value` はそのカラムに入る値 (例: `"Product"`)。
|
|
11
|
+
# 非 polymorphic の belongs_to では両方とも nil。
|
|
12
|
+
attribute :foreign_type, optional(String), skip_serializing_if_nil: true
|
|
13
|
+
attribute :type_value, optional(String), skip_serializing_if_nil: true
|
|
9
14
|
|
|
10
15
|
def self.from_symbol_keys(hash)
|
|
11
16
|
from(hash.transform_keys(&:to_s))
|
|
12
17
|
end
|
|
18
|
+
|
|
19
|
+
def polymorphic?
|
|
20
|
+
!foreign_type.nil?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_hash
|
|
24
|
+
super.compact
|
|
25
|
+
end
|
|
13
26
|
end
|
|
14
27
|
end
|
data/lib/exwiw/query_ast.rb
CHANGED
|
@@ -3,14 +3,20 @@
|
|
|
3
3
|
module Exwiw
|
|
4
4
|
module QueryAst
|
|
5
5
|
class JoinClause
|
|
6
|
-
|
|
6
|
+
# `where_clauses` はこの join の join_table_name (= 結合先テーブル) に対して
|
|
7
|
+
# コンパイルされる。一方 `base_where_clauses` は base_table_name (= 結合元
|
|
8
|
+
# テーブル) に対してコンパイルされる。後者は、結合元テーブルが結合先へ
|
|
9
|
+
# polymorphic belongs_to していて型カラム (foreign_type) が結合元テーブル
|
|
10
|
+
# 側に存在するケースのために使う。
|
|
11
|
+
attr_reader :base_table_name, :foreign_key, :join_table_name, :primary_key, :where_clauses, :base_where_clauses
|
|
7
12
|
|
|
8
|
-
def initialize(base_table_name:, foreign_key:, join_table_name:, primary_key:, where_clauses: [])
|
|
13
|
+
def initialize(base_table_name:, foreign_key:, join_table_name:, primary_key:, where_clauses: [], base_where_clauses: [])
|
|
9
14
|
@base_table_name = base_table_name
|
|
10
15
|
@foreign_key = foreign_key
|
|
11
16
|
@join_table_name = join_table_name
|
|
12
17
|
@primary_key = primary_key
|
|
13
18
|
@where_clauses = where_clauses
|
|
19
|
+
@base_where_clauses = base_where_clauses
|
|
14
20
|
end
|
|
15
21
|
|
|
16
22
|
def to_h
|
|
@@ -23,6 +29,9 @@ module Exwiw
|
|
|
23
29
|
if where_clauses.size.positive?
|
|
24
30
|
hash[:where_clauses] = where_clauses.map { |wc| wc.is_a?(String) ? wc : wc.to_h }
|
|
25
31
|
end
|
|
32
|
+
if base_where_clauses.size.positive?
|
|
33
|
+
hash[:base_where_clauses] = base_where_clauses.map { |wc| wc.is_a?(String) ? wc : wc.to_h }
|
|
34
|
+
end
|
|
26
35
|
hash
|
|
27
36
|
end
|
|
28
37
|
end
|
|
@@ -54,8 +54,23 @@ module Exwiw
|
|
|
54
54
|
foreign_key: relation.foreign_key,
|
|
55
55
|
join_table_name: to_table.name,
|
|
56
56
|
primary_key: to_table.primary_key,
|
|
57
|
-
where_clauses: []
|
|
57
|
+
where_clauses: [],
|
|
58
|
+
base_where_clauses: []
|
|
58
59
|
)
|
|
60
|
+
|
|
61
|
+
# この hop 自体が polymorphic belongs_to の場合 (例: comments が
|
|
62
|
+
# commentable として posts へ polymorphic belongs_to)、型カラム
|
|
63
|
+
# (foreign_type) は結合元テーブル (from_table = base_table_name) 側に
|
|
64
|
+
# 存在する。外部キーだけでは reviewable_id=1 のような値が別モデルの
|
|
65
|
+
# 行と衝突しうるため、base_where_clauses に型条件を追加して結合元
|
|
66
|
+
# テーブルを絞り込む。
|
|
67
|
+
if relation.polymorphic?
|
|
68
|
+
join_clause.base_where_clauses.push QueryAst::WhereClause.new(
|
|
69
|
+
column_name: relation.foreign_type,
|
|
70
|
+
operator: :eq,
|
|
71
|
+
value: [relation.type_value]
|
|
72
|
+
)
|
|
73
|
+
end
|
|
59
74
|
relation_to_dump_target = to_table.belongs_to(dump_target.table_name)
|
|
60
75
|
if relation_to_dump_target
|
|
61
76
|
join_clause.where_clauses.push QueryAst::WhereClause.new(
|
|
@@ -63,6 +78,18 @@ module Exwiw
|
|
|
63
78
|
operator: :eq,
|
|
64
79
|
value: dump_target.ids
|
|
65
80
|
)
|
|
81
|
+
|
|
82
|
+
# 中間テーブルが dump target へ polymorphic belongs_to している場合は、
|
|
83
|
+
# 型カラム (foreign_type) も join 条件に追加する。型カラムは to_table
|
|
84
|
+
# (= join_table_name) 上に存在するため、JoinClause の where_clauses が
|
|
85
|
+
# join_table_name に対してコンパイルされる仕組みにそのまま乗せられる。
|
|
86
|
+
if relation_to_dump_target.polymorphic?
|
|
87
|
+
join_clause.where_clauses.push QueryAst::WhereClause.new(
|
|
88
|
+
column_name: relation_to_dump_target.foreign_type,
|
|
89
|
+
operator: :eq,
|
|
90
|
+
value: [relation_to_dump_target.type_value]
|
|
91
|
+
)
|
|
92
|
+
end
|
|
66
93
|
end
|
|
67
94
|
|
|
68
95
|
# Add filter from intermediate table to join clause
|
|
@@ -98,6 +125,17 @@ module Exwiw
|
|
|
98
125
|
value: dump_target.ids
|
|
99
126
|
)
|
|
100
127
|
|
|
128
|
+
# polymorphic belongs_to の場合は外部キーだけでは型を区別できないため
|
|
129
|
+
# (例: reviewable_id=1 が Product なのか別モデルなのか判別できない)、
|
|
130
|
+
# 型カラム (foreign_type) を type_value で絞り込む条件を追加する。
|
|
131
|
+
if belongs_to.polymorphic?
|
|
132
|
+
clauses.push Exwiw::QueryAst::WhereClause.new(
|
|
133
|
+
column_name: belongs_to.foreign_type,
|
|
134
|
+
operator: :eq,
|
|
135
|
+
value: [belongs_to.type_value]
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
101
139
|
if table.filter
|
|
102
140
|
clauses.push table.filter
|
|
103
141
|
end
|
|
@@ -145,15 +145,44 @@ module Exwiw
|
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
private def aggregate_belongs_tos(models)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
148
|
+
belongs_to_assocs = models.flat_map { |m| m.reflect_on_all_associations(:belongs_to) }
|
|
149
|
+
|
|
150
|
+
non_polymorphic = belongs_to_assocs
|
|
151
|
+
.reject(&:polymorphic?)
|
|
152
|
+
.map { |assoc| { table_name: assoc.table_name, foreign_key: assoc.foreign_key } }
|
|
153
|
+
|
|
154
|
+
# polymorphic な belongs_to (`belongs_to :reviewable, polymorphic: true`) は
|
|
155
|
+
# 単一の対象テーブルを持たない。対象になりうるテーブルは、他モデルで
|
|
156
|
+
# `has_many/has_one ..., as: <association_name>` と宣言されている側から逆引き
|
|
157
|
+
# する。各候補テーブルごとに、型カラム (`foreign_type`) と格納される型の値
|
|
158
|
+
# (`type_value`) を添えた belongs_to を 1 件ずつ展開する。
|
|
159
|
+
polymorphic = belongs_to_assocs
|
|
160
|
+
.select(&:polymorphic?)
|
|
161
|
+
.flat_map do |assoc|
|
|
162
|
+
polymorphic_target_models(assoc.name).map do |target_model|
|
|
163
|
+
{
|
|
164
|
+
table_name: target_model.table_name,
|
|
165
|
+
foreign_key: assoc.foreign_key,
|
|
166
|
+
foreign_type: assoc.foreign_type,
|
|
167
|
+
type_value: target_model.polymorphic_name,
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
(non_polymorphic + polymorphic).uniq
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# polymorphic 関連 `association_name` の対象となりうる具象モデルを、全モデルの
|
|
176
|
+
# `has_many` / `has_one` の `as:` オプションから逆引きして列挙する。
|
|
177
|
+
# `concrete_models` の並びは `ActiveRecord::Base.descendants` の順に依存し、
|
|
178
|
+
# Ruby バージョンによって変わりうるため、生成される belongs_to の並びが安定する
|
|
179
|
+
# よう `table_name` でソートして決定的に返す。
|
|
180
|
+
private def polymorphic_target_models(association_name)
|
|
181
|
+
concrete_models.select do |model|
|
|
182
|
+
(model.reflect_on_all_associations(:has_many) +
|
|
183
|
+
model.reflect_on_all_associations(:has_one))
|
|
184
|
+
.any? { |reflection| reflection.options[:as] == association_name }
|
|
185
|
+
end.sort_by(&:table_name)
|
|
157
186
|
end
|
|
158
187
|
|
|
159
188
|
# Identifies which database a model belongs to. With Rails multi-DB
|
data/lib/exwiw/version.rb
CHANGED