exwiw 0.9.8 → 0.9.10

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: 2aa2a3c673085993f1f62cd055370eef814301b9298bc486d7f49f71e4470ccc
4
- data.tar.gz: 04f5a73f8001d45e402a9d72839bcc824974e9aab4feff998cabc5b4311b762e
3
+ metadata.gz: b7ec13ebd9e8deb6ea3ea1f6d367f128b6bc2e78de3a8198a5d29701a8b4ac73
4
+ data.tar.gz: 243692b89d2972829ddaf9067a0bd3fe8372b693cbbcfcd09db12d17c363bac6
5
5
  SHA512:
6
- metadata.gz: c6a284caef2d3e5eb78d9252f36ef44a2f7e2d8531274ab13eedda9be0261cc9747319c211b556b17ebba1b471526df6bbea76670fed9005bca302f442582f26
7
- data.tar.gz: fd3ec7cdf18b6db28b194a4fe694f09deb79a39428dc280c27709725f0619b24d6f8d361b02192558f20e51ad63b8028f565a3b5e98b9ab8d5dfeb57c51b73ef
6
+ metadata.gz: 42423c759d8df30b1586e15f6545f23bce5f9660e2687bc86956ad9da4f826e656ba474ab82c0613cf64cc43f1ff601d8c4dbbaf99582ddd6dc6562fc6a6b316
7
+ data.tar.gz: 45e029e21bd8c86d55cc8ec7287b9d06405bc1d19d02db5a3255b4f0fe0af07a5f25ab164eebc31fbed5a60544ce2d17152e71eb61c4f93de281707f046b929a
data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.10] - 2026-07-09
6
+
7
+ ### Fixed
8
+
9
+ - **A whole-database schema restore no longer aborts when a preload-required extension (e.g. pglogical) is absent on the target.** `wrap_create_extension_in_do_block` already skipped a `CREATE EXTENSION` the target cannot provide for `feature_not_supported` (0A000) and `invalid_schema_name` (3F000), but an extension that must be loaded via `shared_preload_libraries` raises a bare `elog(ERROR, "<name> is not in shared_preload_libraries")`, which carries the default SQLSTATE `XX000` (`internal_error`) — not caught, so the whole `insert-000-schema.sql` restore aborted (observed restoring a pglogical-carrying production dump into a plain RDS mirage target). `internal_error` is now caught alongside the other two and re-raised as a WARNING, so the extension is skipped and the restore continues. `insufficient_privilege` (42501) is still deliberately NOT caught.
10
+
11
+ ## [0.9.9] - 2026-07-09
12
+
13
+ ### Changed
14
+
15
+ - **`replace_with_fake_data` name types now produce a coherent identity, and gained kana types.** The person-family types (`human_name`, `first_name`, `last_name`) previously drew from three independent faker pools, so a single seed's first name, last name, and full name were unrelated people. They now share one per-locale pool of whole people keyed by the seed, so for a given seed `human_name` always equals `last_name` + `first_name` (ordered per locale: `姓 名` for `ja`, `First Last` otherwise). Three kana types are added — `human_name_kana`, `first_name_kana`, `last_name_kana` — for the reading of the same person. Because faker's `ja` locale has kanji names with no readings, exwiw bundles its own paired (kanji, katakana) dataset (`Exwiw::JapaneseNames`) and builds the `ja` name pool from it, so kana matches the kanji and a `ja`-name-only config needs no faker at all. Kana types require `locale: ja` (they raise a clear error otherwise). The person pool is sized independently of the other types at 20,000 (`PERSON_POOL_SIZE`), and for `ja` is filled with 20,000 *distinct* people enumerated from a 142×142 name dataset (rather than sampled with replacement, which left ~37% of slots duplicated), so the name space is large and evenly used. Determinism for the `ja` name family is now pinned to exwiw's bundled dataset version rather than the faker version; other types and locales are unchanged.
16
+
5
17
  ## [0.9.8] - 2026-07-09
6
18
 
7
19
  ### Added
data/README.md CHANGED
@@ -850,34 +850,57 @@ fake value, across tables, runs, and adapters:
850
850
  because their text forms differ per adapter. A `NULL` seed value hashes `""`
851
851
  (still deterministic).
852
852
  - Like `replace_with`, it **preserves NULL** in the target column.
853
- - `locale` (optional) sets the faker locale used to generate the candidate
854
- values, e.g. `{ "seed": "id", "type": "human_name", "locale": "ja" }` produces
855
- Japanese names.
853
+ - `locale` (optional) sets the locale used to build the candidate values, e.g.
854
+ `{ "seed": "id", "type": "human_name", "locale": "ja" }` produces Japanese
855
+ names.
856
856
  - Supported `type`s:
857
857
 
858
- | type | example output |
859
- |------|----------------|
860
- | `human_name` | `Adrianna Kilback` |
861
- | `first_name` | `Adrianna` |
862
- | `last_name` | `Kilback` |
863
- | `phone_number` | `(555) 123-4567` |
864
- | `address` | `282 Kevin Brook, Imogeneborough, CA 58517` |
865
- | `company_name` | `Hirthe-Ritchie` |
866
- | `email` | `cliff.fay.9d6b804eff5a3f57@example.com` |
867
- | `username` | `cliff.fay_9d6b804eff5a3f57` |
868
-
869
- - Values are drawn from a pool of 10,000 pre-generated candidates per
870
- (type, locale), so distinct seeds can share a fake value. The
858
+ | type | example output (en) | example output (`locale: ja`) |
859
+ |------|----------------|----------------|
860
+ | `human_name` | `Adrianna Kilback` | `山田 太郎` |
861
+ | `first_name` | `Adrianna` | `太郎` |
862
+ | `last_name` | `Kilback` | `山田` |
863
+ | `human_name_kana` | (ja only) | `ヤマダ タロウ` |
864
+ | `first_name_kana` | (ja only) | `タロウ` |
865
+ | `last_name_kana` | (ja only) | `ヤマダ` |
866
+ | `phone_number` | `(555) 123-4567` | |
867
+ | `address` | `282 Kevin Brook, Imogeneborough, CA 58517` | |
868
+ | `company_name` | `Hirthe-Ritchie` | |
869
+ | `email` | `cliff.fay.9d6b804eff5a3f57@example.com` | |
870
+ | `username` | `cliff.fay_9d6b804eff5a3f57` | |
871
+
872
+ - **Coherent identity across the name family.** The person-family types
873
+ (`human_name`, `first_name`, `last_name` and their `*_kana` counterparts) all
874
+ draw from a single shared pool of people per locale, keyed by the same seed —
875
+ so for one seed value the last name, first name, full name, and every kana
876
+ reading belong to the **same person**: `human_name` always equals
877
+ `last_name` + `first_name`, and `human_name_kana` matches `human_name`. Full
878
+ names are ordered per locale (`姓 名` for `ja`, `First Last` otherwise).
879
+ - **Kana (`*_kana`) types require `locale: ja`.** faker's `ja` locale ships
880
+ kanji names with no reading, so exwiw bundles its own paired (kanji, katakana)
881
+ dataset for `ja`; this is what lets a fake person carry a kana reading that
882
+ actually matches its kanji. Requesting a `*_kana` type with any other locale
883
+ raises a clear error at build time.
884
+ - Values are drawn from a pre-generated pool per (type, locale), so distinct
885
+ seeds can share a fake value. The name family uses one shared **person** pool
886
+ of 20,000 identities — for `ja` these are 20,000 *distinct* people enumerated
887
+ from the bundled (kanji, kana) dataset (142 surnames × 142 given names); the
888
+ other types use an independent 10,000-candidate pool. The
871
889
  uniqueness-sensitive types (`email`, `username`) additionally embed a 64-bit
872
890
  hex token derived from the seed hash, so they stay collision-free under a
873
891
  unique index even at millions of rows (collision probability at 5M distinct
874
892
  seeds ≈ 7e-7) and always use the `example.com` domain.
875
- - **Determinism caveat**: values are stable for a given faker gem version +
876
- locale. Upgrading faker (or changing `locale`) regenerates the pool and maps
893
+ - **Determinism caveat**: values are stable for a given locale plus the version
894
+ of the value source — the faker gem for the non-`ja` name family and the
895
+ independent types, and exwiw's bundled dataset for the `ja` name family.
896
+ Upgrading that source (or changing `locale`) regenerates the pool and maps
877
897
  seeds to different values. The seed→value mapping itself never changes within
878
898
  one version.
879
899
  - The faker gem is **not** a runtime dependency of exwiw — add `gem "faker"` to
880
- your Gemfile to use this mode (exwiw raises a clear error otherwise).
900
+ your Gemfile to use this mode (exwiw raises a clear error otherwise). A config
901
+ that uses **only** `ja` person types needs no faker (that pool is built
902
+ entirely from the bundled dataset); faker is required for every other type
903
+ and locale.
881
904
  - Exclusive with the other masking keys on the same column. SQL adapters only
882
905
  (the MongoDB adapter rejects the key on load, see
883
906
  [Unknown keys are rejected](#unknown-keys-are-rejected)), and invisible to
@@ -86,12 +86,18 @@ module Exwiw
86
86
  # A bare `CREATE EXTENSION ...;` (as a full-database pg_dump emits, unlike a
87
87
  # `--table` dump, which omits extensions) has no graceful skip: a restore
88
88
  # target that cannot create the extension aborts the whole restore. Wrap
89
- # each in a DO block that catches only the two "cannot provide it here"
90
- # cases feature_not_supported (0A000, binaries absent) and
91
- # invalid_schema_name (3F000, required schema absent) — and re-raises them
92
- # as a WARNING so the skip surfaces in the restore logs. insufficient_
93
- # privilege (42501) is deliberately NOT caught: a restore role lacking
94
- # CREATE privilege is a misconfiguration to fix, not to skip silently.
89
+ # each in a DO block that catches only the "cannot provide it here" cases and
90
+ # re-raises them as a WARNING so the skip surfaces in the restore logs:
91
+ # - feature_not_supported (0A000, binaries absent)
92
+ # - invalid_schema_name (3F000, required schema absent)
93
+ # - internal_error (XX000): an extension that must be preloaded via
94
+ # shared_preload_libraries raises a bare `elog(ERROR, "<name> is not in
95
+ # shared_preload_libraries")` (default SQLSTATE XX000) when it is not
96
+ # preloaded on the target — e.g. pglogical restored into a plain RDS. Like
97
+ # the other two, this is the target being unable to provide the extension,
98
+ # not a broken dump, so the restore should skip it rather than abort.
99
+ # insufficient_privilege (42501) is deliberately NOT caught: a restore role
100
+ # lacking CREATE privilege is a misconfiguration to fix, not to skip silently.
95
101
  CREATE_EXTENSION_RE = /^[ \t]*CREATE\s+EXTENSION\b(?:\s+IF\s+NOT\s+EXISTS)?\s+(?<name>"[^"]+"|[^\s;]+)[^;]*;/i.freeze
96
102
 
97
103
  def wrap_create_extension_in_do_block(sql)
@@ -101,7 +107,7 @@ module Exwiw
101
107
  warning = "exwiw: skipped CREATE EXTENSION #{extname} (SQLSTATE %): %"
102
108
  warning_literal = "'#{warning.gsub("'", "''")}'"
103
109
  "DO $$ BEGIN #{stmt} " \
104
- "EXCEPTION WHEN feature_not_supported OR invalid_schema_name THEN " \
110
+ "EXCEPTION WHEN feature_not_supported OR invalid_schema_name OR internal_error THEN " \
105
111
  "RAISE WARNING #{warning_literal}, SQLSTATE, SQLERRM; END $$;"
106
112
  end
107
113
  end
@@ -0,0 +1,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exwiw
4
+ # Paired (kanji, katakana) Japanese name data for the `ja` locale of the
5
+ # person-family `replace_with_fake_data` types (see {RowTransformer}).
6
+ #
7
+ # faker's `ja` locale ships kanji names only, with no reading, so it cannot
8
+ # produce kana (フリガナ) at all, and its last_name / first_name pools are
9
+ # unrelated to each other. exwiw bundles this paired dataset instead so that:
10
+ #
11
+ # - a fake person carries a kana reading that actually matches its kanji, and
12
+ # - last_name / first_name / full name (and their kana) for a single seed all
13
+ # come from the same person (a coherent identity).
14
+ #
15
+ # The kanji sets extend faker's `ja` name list (its top 100 surnames / 100
16
+ # given names) with common additions; exwiw supplies the readings. 142 x 142
17
+ # = 20,164 combinations exceeds the person pool size
18
+ # (RowTransformer::PERSON_POOL_SIZE = 20,000), so the ja pool is filled with
19
+ # that many DISTINCT people rather than repeating a small subset.
20
+ #
21
+ # Readings are katakana (matching the katakana convention of typical `*_kana`
22
+ # columns). They are plausible common readings, not the only possible reading
23
+ # of each kanji — fake data only needs to look real, not be canonical.
24
+ module JapaneseNames
25
+ SURNAMES = [
26
+ %w[佐藤 サトウ],
27
+ %w[鈴木 スズキ],
28
+ %w[高橋 タカハシ],
29
+ %w[田中 タナカ],
30
+ %w[伊藤 イトウ],
31
+ %w[山本 ヤマモト],
32
+ %w[渡辺 ワタナベ],
33
+ %w[中村 ナカムラ],
34
+ %w[小林 コバヤシ],
35
+ %w[加藤 カトウ],
36
+ %w[吉田 ヨシダ],
37
+ %w[山田 ヤマダ],
38
+ %w[佐々木 ササキ],
39
+ %w[山口 ヤマグチ],
40
+ %w[松本 マツモト],
41
+ %w[井上 イノウエ],
42
+ %w[木村 キムラ],
43
+ %w[林 ハヤシ],
44
+ %w[斎藤 サイトウ],
45
+ %w[清水 シミズ],
46
+ %w[山崎 ヤマザキ],
47
+ %w[阿部 アベ],
48
+ %w[森 モリ],
49
+ %w[池田 イケダ],
50
+ %w[橋本 ハシモト],
51
+ %w[山下 ヤマシタ],
52
+ %w[石川 イシカワ],
53
+ %w[中島 ナカジマ],
54
+ %w[前田 マエダ],
55
+ %w[藤田 フジタ],
56
+ %w[後藤 ゴトウ],
57
+ %w[小川 オガワ],
58
+ %w[岡田 オカダ],
59
+ %w[村上 ムラカミ],
60
+ %w[長谷川 ハセガワ],
61
+ %w[近藤 コンドウ],
62
+ %w[石井 イシイ],
63
+ %w[斉藤 サイトウ],
64
+ %w[坂本 サカモト],
65
+ %w[遠藤 エンドウ],
66
+ %w[藤井 フジイ],
67
+ %w[青木 アオキ],
68
+ %w[福田 フクダ],
69
+ %w[三浦 ミウラ],
70
+ %w[西村 ニシムラ],
71
+ %w[藤原 フジワラ],
72
+ %w[太田 オオタ],
73
+ %w[松田 マツダ],
74
+ %w[原田 ハラダ],
75
+ %w[岡本 オカモト],
76
+ %w[中野 ナカノ],
77
+ %w[中川 ナカガワ],
78
+ %w[小野 オノ],
79
+ %w[田村 タムラ],
80
+ %w[竹内 タケウチ],
81
+ %w[金子 カネコ],
82
+ %w[中山 ナカヤマ],
83
+ %w[和田 ワダ],
84
+ %w[石田 イシダ],
85
+ %w[工藤 クドウ],
86
+ %w[上田 ウエダ],
87
+ %w[原 ハラ],
88
+ %w[森田 モリタ],
89
+ %w[酒井 サカイ],
90
+ %w[横山 ヨコヤマ],
91
+ %w[柴田 シバタ],
92
+ %w[宮崎 ミヤザキ],
93
+ %w[宮本 ミヤモト],
94
+ %w[内田 ウチダ],
95
+ %w[高木 タカギ],
96
+ %w[谷口 タニグチ],
97
+ %w[安藤 アンドウ],
98
+ %w[丸山 マルヤマ],
99
+ %w[今井 イマイ],
100
+ %w[大野 オオノ],
101
+ %w[高田 タカダ],
102
+ %w[菅原 スガワラ],
103
+ %w[河野 コウノ],
104
+ %w[武田 タケダ],
105
+ %w[藤本 フジモト],
106
+ %w[上野 ウエノ],
107
+ %w[杉山 スギヤマ],
108
+ %w[千葉 チバ],
109
+ %w[村田 ムラタ],
110
+ %w[増田 マスダ],
111
+ %w[小島 コジマ],
112
+ %w[小山 コヤマ],
113
+ %w[大塚 オオツカ],
114
+ %w[平野 ヒラノ],
115
+ %w[久保 クボ],
116
+ %w[渡部 ワタナベ],
117
+ %w[松井 マツイ],
118
+ %w[菊地 キクチ],
119
+ %w[岩崎 イワサキ],
120
+ %w[松尾 マツオ],
121
+ %w[佐野 サノ],
122
+ %w[木下 キノシタ],
123
+ %w[野口 ノグチ],
124
+ %w[野村 ノムラ],
125
+ %w[新井 アライ],
126
+ %w[北村 キタムラ],
127
+ %w[平田 ヒラタ],
128
+ %w[森本 モリモト],
129
+ %w[荒木 アラキ],
130
+ %w[大西 オオニシ],
131
+ %w[菅野 カンノ],
132
+ %w[小池 コイケ],
133
+ %w[桜井 サクライ],
134
+ %w[高野 タカノ],
135
+ %w[川口 カワグチ],
136
+ %w[大久保 オオクボ],
137
+ %w[中田 ナカタ],
138
+ %w[川崎 カワサキ],
139
+ %w[飯田 イイダ],
140
+ %w[星野 ホシノ],
141
+ %w[岩田 イワタ],
142
+ %w[大森 オオモリ],
143
+ %w[松岡 マツオカ],
144
+ %w[沢田 サワダ],
145
+ %w[内藤 ナイトウ],
146
+ %w[松浦 マツウラ],
147
+ %w[西田 ニシダ],
148
+ %w[白石 シライシ],
149
+ %w[浜田 ハマダ],
150
+ %w[早川 ハヤカワ],
151
+ %w[永井 ナガイ],
152
+ %w[松村 マツムラ],
153
+ %w[三宅 ミヤケ],
154
+ %w[福島 フクシマ],
155
+ %w[大橋 オオハシ],
156
+ %w[中西 ナカニシ],
157
+ %w[吉川 ヨシカワ],
158
+ %w[服部 ハットリ],
159
+ %w[尾崎 オザキ],
160
+ %w[水野 ミズノ],
161
+ %w[香川 カガワ],
162
+ %w[荒井 アライ],
163
+ %w[田口 タグチ],
164
+ %w[江口 エグチ],
165
+ %w[秋山 アキヤマ],
166
+ %w[黒田 クロダ],
167
+ %w[今村 イマムラ],
168
+ ].freeze
169
+
170
+ GIVEN_NAMES = [
171
+ %w[翔太 ショウタ],
172
+ %w[蓮 レン],
173
+ %w[翔 ショウ],
174
+ %w[陸 リク],
175
+ %w[颯太 ソウタ],
176
+ %w[悠斗 ユウト],
177
+ %w[大翔 ヒロト],
178
+ %w[翼 ツバサ],
179
+ %w[樹 イツキ],
180
+ %w[奏太 ソウタ],
181
+ %w[大和 ヤマト],
182
+ %w[大輝 ダイキ],
183
+ %w[悠 ユウ],
184
+ %w[隼人 ハヤト],
185
+ %w[健太 ケンタ],
186
+ %w[大輔 ダイスケ],
187
+ %w[駿 シュン],
188
+ %w[陽斗 ハルト],
189
+ %w[優 ユウ],
190
+ %w[陽 ヨウ],
191
+ %w[悠人 ユウト],
192
+ %w[誠 マコト],
193
+ %w[拓海 タクミ],
194
+ %w[仁 ジン],
195
+ %w[悠太 ユウタ],
196
+ %w[悠真 ユウマ],
197
+ %w[大地 ダイチ],
198
+ %w[健 ケン],
199
+ %w[遼 リョウ],
200
+ %w[大樹 ダイキ],
201
+ %w[諒 リョウ],
202
+ %w[響 ヒビキ],
203
+ %w[太一 タイチ],
204
+ %w[一郎 イチロウ],
205
+ %w[優斗 ユウト],
206
+ %w[亮 リョウ],
207
+ %w[海斗 カイト],
208
+ %w[颯 ソウ],
209
+ %w[亮太 リョウタ],
210
+ %w[匠 タクミ],
211
+ %w[陽太 ヨウタ],
212
+ %w[航 ワタル],
213
+ %w[瑛太 エイタ],
214
+ %w[直樹 ナオキ],
215
+ %w[空 ソラ],
216
+ %w[光 ヒカル],
217
+ %w[太郎 タロウ],
218
+ %w[輝 ヒカル],
219
+ %w[一輝 カズキ],
220
+ %w[蒼 アオイ],
221
+ %w[葵 アオイ],
222
+ %w[優那 ユウナ],
223
+ %w[優奈 ユウナ],
224
+ %w[凛 リン],
225
+ %w[陽菜 ヒナ],
226
+ %w[愛 アイ],
227
+ %w[結衣 ユイ],
228
+ %w[美咲 ミサキ],
229
+ %w[楓 カエデ],
230
+ %w[さくら サクラ],
231
+ %w[遥 ハルカ],
232
+ %w[美優 ミユ],
233
+ %w[莉子 リコ],
234
+ %w[七海 ナナミ],
235
+ %w[美月 ミヅキ],
236
+ %w[結菜 ユナ],
237
+ %w[真央 マオ],
238
+ %w[花音 カノン],
239
+ %w[陽子 ヨウコ],
240
+ %w[舞 マイ],
241
+ %w[美羽 ミウ],
242
+ %w[優衣 ユイ],
243
+ %w[未来 ミク],
244
+ %w[彩 アヤ],
245
+ %w[彩乃 アヤノ],
246
+ %w[彩花 アヤカ],
247
+ %w[智子 トモコ],
248
+ %w[奈々 ナナ],
249
+ %w[千尋 チヒロ],
250
+ %w[愛美 マナミ],
251
+ %w[優菜 ユウナ],
252
+ %w[杏 アン],
253
+ %w[裕子 ユウコ],
254
+ %w[芽衣 メイ],
255
+ %w[綾乃 アヤノ],
256
+ %w[琴音 コトネ],
257
+ %w[桜 サクラ],
258
+ %w[恵 メグミ],
259
+ %w[杏奈 アンナ],
260
+ %w[美桜 ミオ],
261
+ %w[優花 ユウカ],
262
+ %w[玲奈 レナ],
263
+ %w[結 ユイ],
264
+ %w[茜 アカネ],
265
+ %w[美穂 ミホ],
266
+ %w[明日香 アスカ],
267
+ %w[愛子 アイコ],
268
+ %w[美緒 ミオ],
269
+ %w[千夏 チナツ],
270
+ %w[真希 マキ],
271
+ %w[陽向 ヒナタ],
272
+ %w[大介 ダイスケ],
273
+ %w[涼介 リョウスケ],
274
+ %w[悠介 ユウスケ],
275
+ %w[拓也 タクヤ],
276
+ %w[和也 カズヤ],
277
+ %w[直人 ナオト],
278
+ %w[雄太 ユウタ],
279
+ %w[翔平 ショウヘイ],
280
+ %w[健太郎 ケンタロウ],
281
+ %w[慎太郎 シンタロウ],
282
+ %w[竜也 タツヤ],
283
+ %w[圭 ケイ],
284
+ %w[樹里 ジュリ],
285
+ %w[花子 ハナコ],
286
+ %w[由美 ユミ],
287
+ %w[由美子 ユミコ],
288
+ %w[美穂子 ミホコ],
289
+ %w[直子 ナオコ],
290
+ %w[京子 キョウコ],
291
+ %w[順子 ジュンコ],
292
+ %w[洋子 ヨウコ],
293
+ %w[幸子 サチコ],
294
+ %w[美和 ミワ],
295
+ %w[理沙 リサ],
296
+ %w[麻衣 マイ],
297
+ %w[早紀 サキ],
298
+ %w[春香 ハルカ],
299
+ %w[沙織 サオリ],
300
+ %w[香織 カオリ],
301
+ %w[友美 トモミ],
302
+ %w[恵子 ケイコ],
303
+ %w[真由美 マユミ],
304
+ %w[里奈 リナ],
305
+ %w[彩香 アヤカ],
306
+ %w[亜美 アミ],
307
+ %w[春菜 ハルナ],
308
+ %w[千秋 チアキ],
309
+ %w[桃子 モモコ],
310
+ %w[美里 ミサト],
311
+ %w[結子 ユウコ],
312
+ %w[陽介 ヨウスケ],
313
+ ].freeze
314
+ end
315
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "digest"
4
4
 
5
+ require_relative "japanese_names"
6
+
5
7
  module Exwiw
6
8
  # Applies the Ruby-process-side masking modes — `map` and
7
9
  # `replace_with_fake_data` — to the rows streamed out of a SQL adapter's
@@ -31,16 +33,46 @@ module Exwiw
31
33
  # given faker gem version + locale.
32
34
  POOL_RANDOM_SEED = 715_517
33
35
 
34
- # Supported `replace_with_fake_data` types. `pool` builds one candidate
35
- # value (called POOL_SIZE times under a seeded Faker random). Types with
36
- # `compose` are uniqueness-sensitive: the pooled base alone would collide
37
- # under a unique index at scale, so the final value composes the base with
38
- # a 64-bit hex token derived from the same seed digest (collision
39
- # probability at 5M distinct seeds ≈ 7e-7).
36
+ # The person pool (PERSON_TYPES) is sized independently of POOL_SIZE — at
37
+ # twice the size so the coherent-identity name space is larger without
38
+ # disturbing the independent types' seed->value mappings. The ja pool is
39
+ # filled with distinct people (see .build_japanese_person_pool), so
40
+ # JapaneseNames must supply at least this many (surname, given) combinations.
41
+ PERSON_POOL_SIZE = POOL_SIZE * 2
42
+
43
+ # A coherent fake identity. All person-family types (PERSON_TYPES) draw
44
+ # from one shared pool of these per locale, and a seed picks the same pool
45
+ # index for every such column — so last_name / first_name / full name (and
46
+ # their kana) for one seed all belong to the same person. kana fields are
47
+ # only populated for the `ja` locale (faker has no readings; exwiw bundles
48
+ # a paired dataset — see JapaneseNames); they are nil otherwise.
49
+ Person = Struct.new(:last_name, :first_name, :last_name_kana, :first_name_kana)
50
+
51
+ # Person-family types: they share the per-locale person pool (built by
52
+ # .person_pool), and each extractor derives its value from the picked
53
+ # Person. Full-name ordering is locale-aware ("姓 名" for ja, "First Last"
54
+ # elsewhere). This is what keeps a single seed's name columns consistent.
55
+ PERSON_TYPES = {
56
+ "human_name" => ->(p, locale) { join_full_name(p.last_name, p.first_name, locale) },
57
+ "last_name" => ->(p, _locale) { p.last_name },
58
+ "first_name" => ->(p, _locale) { p.first_name },
59
+ "human_name_kana" => ->(p, locale) { join_full_name(p.last_name_kana, p.first_name_kana, locale) },
60
+ "last_name_kana" => ->(p, _locale) { p.last_name_kana },
61
+ "first_name_kana" => ->(p, _locale) { p.first_name_kana },
62
+ }.freeze
63
+
64
+ # Person-family types whose value is a kana reading, only available for the
65
+ # `ja` locale (the person pool carries nil kana for other locales, so these
66
+ # are rejected at build time with a clear error).
67
+ KANA_TYPES = %w[human_name_kana last_name_kana first_name_kana].freeze
68
+
69
+ # Independent (non-person) types. `pool` builds one candidate value (called
70
+ # POOL_SIZE times under a seeded Faker random). Types with `compose` are
71
+ # uniqueness-sensitive: the pooled base alone would collide under a unique
72
+ # index at scale, so the final value composes the base with a 64-bit hex
73
+ # token derived from the same seed digest (collision probability at 5M
74
+ # distinct seeds ≈ 7e-7).
40
75
  FAKE_TYPES = {
41
- "human_name" => { pool: -> { Faker::Name.name } },
42
- "first_name" => { pool: -> { Faker::Name.first_name } },
43
- "last_name" => { pool: -> { Faker::Name.last_name } },
44
76
  "phone_number" => { pool: -> { Faker::PhoneNumber.phone_number } },
45
77
  "address" => { pool: -> { Faker::Address.full_address } },
46
78
  "company_name" => { pool: -> { Faker::Company.name } },
@@ -50,6 +82,13 @@ module Exwiw
50
82
  compose: ->(base, token) { "#{base}_#{token}" } },
51
83
  }.freeze
52
84
 
85
+ # "姓 名" for Japanese, "First Last" otherwise. A nil part (e.g. missing
86
+ # kana) collapses so the result never has a dangling separator.
87
+ def self.join_full_name(last, first, locale)
88
+ parts = locale.to_s == "ja" ? [last, first] : [first, last]
89
+ parts.compact.join(" ")
90
+ end
91
+
53
92
  # The `r` a map proc receives: read-only access to the current row's
54
93
  # values by column name (`r['id']`). One instance is reused across all
55
94
  # rows (zero per-row allocation) — do not retain it outside the proc.
@@ -118,6 +157,15 @@ module Exwiw
118
157
  "Add `gem \"faker\"` to your Gemfile (it is not a runtime dependency of exwiw)."
119
158
  end
120
159
 
160
+ # Whether a (type, locale) draws any value from faker. The `ja` person pool
161
+ # is built entirely from exwiw's bundled dataset, so a config that only
162
+ # uses `ja` person types needs no faker at all.
163
+ def self.type_needs_faker?(type, locale)
164
+ return true if FAKE_TYPES.key?(type)
165
+
166
+ PERSON_TYPES.key?(type) && locale.to_s != "ja"
167
+ end
168
+
121
169
  # Pools are memoized per (type, locale): every column sharing a type+locale
122
170
  # sees the same pool, which is what makes equal seed values map to equal
123
171
  # fake values across tables and runs.
@@ -138,6 +186,51 @@ module Exwiw
138
186
  Faker::Config.random = previous_random
139
187
  end
140
188
 
189
+ # One pool of coherent Person records per locale, shared by every
190
+ # person-family type. Memoized so equal seeds map to equal people across
191
+ # tables and runs. `ja` is built from exwiw's bundled (kanji, kana) dataset
192
+ # so kana matches kanji; other locales use faker (no kana).
193
+ def self.person_pool(locale)
194
+ @person_pools ||= {}
195
+ @person_pools[locale] ||= build_person_pool(locale)
196
+ end
197
+
198
+ def self.build_person_pool(locale)
199
+ random = Random.new(POOL_RANDOM_SEED)
200
+ return build_japanese_person_pool(random) if locale.to_s == "ja"
201
+
202
+ previous_locale = Faker::Config.locale
203
+ previous_random = Faker::Config.random
204
+ Faker::Config.locale = locale if locale
205
+ Faker::Config.random = random
206
+ Array.new(PERSON_POOL_SIZE) { Person.new(Faker::Name.last_name, Faker::Name.first_name, nil, nil).freeze }.freeze
207
+ ensure
208
+ unless locale.to_s == "ja"
209
+ Faker::Config.locale = previous_locale
210
+ Faker::Config.random = previous_random
211
+ end
212
+ end
213
+
214
+ # Fill the ja pool with DISTINCT people: enumerate every (surname, given)
215
+ # combination, shuffle deterministically, and take PERSON_POOL_SIZE. Unlike
216
+ # sampling with replacement (which wastes ~37% of slots to duplicates), this
217
+ # gives PERSON_POOL_SIZE distinct identities — so JapaneseNames must supply
218
+ # at least that many combinations.
219
+ def self.build_japanese_person_pool(random)
220
+ surnames = JapaneseNames::SURNAMES
221
+ given_names = JapaneseNames::GIVEN_NAMES
222
+ combinations = surnames.size * given_names.size
223
+ if combinations < PERSON_POOL_SIZE
224
+ raise "JapaneseNames has #{combinations} (surname, given) combinations, " \
225
+ "need at least PERSON_POOL_SIZE=#{PERSON_POOL_SIZE}"
226
+ end
227
+
228
+ people = surnames.flat_map do |last|
229
+ given_names.map { |first| Person.new(last[0], first[0], last[1], first[1]).freeze }
230
+ end
231
+ people.shuffle(random: random).first(PERSON_POOL_SIZE).freeze
232
+ end
233
+
141
234
  def initialize(table)
142
235
  @table_name = table.name
143
236
  @name_to_index = {}
@@ -145,7 +238,11 @@ module Exwiw
145
238
  @name_to_index.freeze
146
239
  @row_accessor = Row.new(@table_name, @name_to_index)
147
240
 
148
- self.class.require_faker! if table.columns.any?(&:replace_with_fake_data)
241
+ needs_faker = table.columns.any? do |column|
242
+ fd = column.replace_with_fake_data
243
+ fd && self.class.type_needs_faker?(fd.type, fd.locale)
244
+ end
245
+ self.class.require_faker! if needs_faker
149
246
 
150
247
  @transforms = table.columns.each_with_index.filter_map do |column, index|
151
248
  if column.map
@@ -210,11 +307,26 @@ module Exwiw
210
307
 
211
308
  private def compile_fake(column, column_index)
212
309
  fake_data = column.replace_with_fake_data
213
- spec = FAKE_TYPES.fetch(fake_data.type)
310
+ type = fake_data.type
311
+ unless PERSON_TYPES.key?(type) || FAKE_TYPES.key?(type)
312
+ raise ArgumentError,
313
+ "replace_with_fake_data for column '#{@table_name}.#{column.name}': " \
314
+ "unknown type '#{type}'. Supported: #{(PERSON_TYPES.keys + FAKE_TYPES.keys).join(', ')}"
315
+ end
214
316
 
215
- # Re-resolve the seed here, against the effective (post-ignore) columns:
216
- # load-time validation sees the full column list, so a seed column that
217
- # was ignore:true is only caught at dump time.
317
+ seed_index = resolve_seed_index(fake_data, column)
318
+
319
+ if PERSON_TYPES.key?(type)
320
+ compile_person_fake(fake_data, column, column_index, seed_index)
321
+ else
322
+ compile_independent_fake(fake_data, column_index, seed_index)
323
+ end
324
+ end
325
+
326
+ # Re-resolve the seed here, against the effective (post-ignore) columns:
327
+ # load-time validation sees the full column list, so a seed column that was
328
+ # ignore:true is only caught at dump time.
329
+ private def resolve_seed_index(fake_data, column)
218
330
  seed_column = fake_data.seed.delete_prefix("#{@table_name}.")
219
331
  seed_index = @name_to_index[seed_column]
220
332
  unless seed_index
@@ -223,14 +335,44 @@ module Exwiw
223
335
  "seed '#{fake_data.seed}' does not resolve to an extracted column " \
224
336
  "(is it ignore:true?)"
225
337
  end
338
+ seed_index
339
+ end
340
+
341
+ # Person-family type: pick a coherent Person from the per-locale pool. The
342
+ # digest[0,8] index is shared with every other person column, so one seed's
343
+ # name columns all belong to the same person.
344
+ private def compile_person_fake(fake_data, column, column_index, seed_index)
345
+ type = fake_data.type
346
+ locale = fake_data.locale
347
+ pool = self.class.person_pool(locale)
348
+ extractor = PERSON_TYPES.fetch(type)
349
+
350
+ if KANA_TYPES.include?(type) && pool.first.last_name_kana.nil?
351
+ raise ArgumentError,
352
+ "replace_with_fake_data for column '#{@table_name}.#{column.name}': " \
353
+ "type '#{type}' needs kana readings, which are only available with locale: ja " \
354
+ "(got locale: #{locale.inspect})"
355
+ end
356
+
357
+ # NULL-preserving like replace_with: a NULL target stays NULL.
358
+ lambda do |row|
359
+ next nil if row[column_index].nil?
360
+
361
+ digest = Digest::SHA256.digest(row[seed_index].to_s)
362
+ person = pool[digest[0, 8].unpack1("Q>") % PERSON_POOL_SIZE]
363
+ extractor.call(person, locale)
364
+ end
365
+ end
226
366
 
367
+ # Independent type: pick a value from its own (type, locale) pool.
368
+ private def compile_independent_fake(fake_data, column_index, seed_index)
227
369
  pool = self.class.fake_pool(fake_data.type, fake_data.locale)
228
- compose = spec[:compose]
370
+ compose = FAKE_TYPES.fetch(fake_data.type)[:compose]
229
371
 
230
- # NULL-preserving like replace_with: a NULL target stays NULL. A nil
231
- # seed value hashes "" (deterministic). Seed values are normalized with
232
- # to_s so sqlite's native Integer 123 and pg/mysql's string "123" pick
233
- # the same fake value.
372
+ # NULL-preserving like replace_with: a NULL target stays NULL. A nil seed
373
+ # value hashes "" (deterministic). Seed values are normalized with to_s so
374
+ # sqlite's native Integer 123 and pg/mysql's string "123" pick the same
375
+ # fake value.
234
376
  lambda do |row|
235
377
  next nil if row[column_index].nil?
236
378
 
@@ -253,10 +253,11 @@ module Exwiw
253
253
  fake_data = column.replace_with_fake_data
254
254
  return unless fake_data
255
255
 
256
- unless RowTransformer::FAKE_TYPES.key?(fake_data.type)
256
+ supported_types = RowTransformer::PERSON_TYPES.keys + RowTransformer::FAKE_TYPES.keys
257
+ unless supported_types.include?(fake_data.type)
257
258
  raise ArgumentError,
258
259
  "Table '#{name}' column '#{column.name}': unknown replace_with_fake_data type '#{fake_data.type}' " \
259
- "(supported: #{RowTransformer::FAKE_TYPES.keys.join(', ')})."
260
+ "(supported: #{supported_types.join(', ')})."
260
261
  end
261
262
 
262
263
  seed_column = fake_data.seed.delete_prefix("#{name}.")
data/lib/exwiw/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Exwiw
4
- VERSION = "0.9.8"
4
+ VERSION = "0.9.10"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exwiw
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.8
4
+ version: 0.9.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shia
@@ -71,6 +71,7 @@ files:
71
71
  - lib/exwiw/explain_runner.rb
72
72
  - lib/exwiw/ext_json.rb
73
73
  - lib/exwiw/fake_data.rb
74
+ - lib/exwiw/japanese_names.rb
74
75
  - lib/exwiw/mongo_query.rb
75
76
  - lib/exwiw/mongodb_collection_config.rb
76
77
  - lib/exwiw/mongodb_field.rb