real_data_tests 0.4.1 → 0.5.0
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 +14 -0
- data/Gemfile.lock +124 -97
- data/README.md +30 -0
- data/lib/real_data_tests/load_strategies/base.rb +25 -0
- data/lib/real_data_tests/load_strategies/native.rb +93 -0
- data/lib/real_data_tests/load_strategies/psql.rb +43 -0
- data/lib/real_data_tests/pg_dump_generator.rb +0 -16
- data/lib/real_data_tests/rspec_helper.rb +21 -367
- data/lib/real_data_tests/sql_dump_parser.rb +89 -0
- data/lib/real_data_tests/version.rb +1 -1
- data/lib/real_data_tests.rb +4 -0
- data/real_data_tests.gemspec +42 -0
- metadata +22 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 76313adb8becf99110aade7dae2741bb9fbb18426eab847ec46791dff93acec4
|
|
4
|
+
data.tar.gz: 2650573c1d3a7dc0d3e3866d0520c27696d4fe7d858b5d3b6380b3c88f640bd3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6ef108ecbc7c30db555e8fb8792a08fce6af245763f524f0d6c1d8fe1bb59cff69d63e902eb46a785f8122fab502abb0dfd0803747348183f031cacdd6080ad0
|
|
7
|
+
data.tar.gz: 900f6e4f32dfbabae2a5492d7cc78bb8490f33b5b7610c9acd1b735e61e2837101ca0b7b8b74cd849d1cbc857805556935b821a87c56e7efa3005962a3179550
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
### Added
|
|
3
|
+
- **Load strategies**: SQL dump loading is now pluggable via `RealDataTests::LoadStrategies`
|
|
4
|
+
- `LoadStrategies::Native` — loads on the ActiveRecord connection (default only via `load_real_test_data_native`)
|
|
5
|
+
- `LoadStrategies::Psql` — the previous `psql` shell-out behavior, for dumps that require psql itself (meta-commands like `\set`, or dumps too large to read into memory)
|
|
6
|
+
- Inject via `load_real_test_data("dump", strategy: RealDataTests::LoadStrategies::Psql)`
|
|
7
|
+
- Custom strategies: subclass `LoadStrategies::Base` and implement `#call(dump_path)`
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Reworked SQL dump loading around the new strategies
|
|
11
|
+
- `load_real_test_data` behavior is unchanged (psql shell-out, `LoadStrategies::Psql` default); pass `strategy: LoadStrategies::Native` to load transactionally on the ActiveRecord connection so data participates in the caller's transaction (e.g. DatabaseCleaner `:transaction` strategy) and rolls back with it
|
|
12
|
+
- Native loader: dumps without `COPY ... FROM stdin` blocks are executed as a single multi-statement `execute` (one server round-trip) instead of parsed and executed block-by-block; block parsing is kept only as the COPY fallback
|
|
13
|
+
- Native loader: `COPY ... FROM stdin` blocks are streamed through `raw_connection.copy_data` on the same libpq session, so COPY data is transactional too (previously broken — the whole COPY block went through `execute`)
|
|
14
|
+
|
|
1
15
|
## [0.4.1] - 2026-04-09
|
|
2
16
|
### Fixed
|
|
3
17
|
- Fixed `datetime` microsecond precision loss in `PgDumpGenerator`
|
data/Gemfile.lock
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
real_data_tests (0.
|
|
4
|
+
real_data_tests (0.5.0)
|
|
5
5
|
activerecord (>= 5.0)
|
|
6
|
+
csv
|
|
6
7
|
faker (~> 3.0)
|
|
7
8
|
pg (>= 1.1)
|
|
8
9
|
rails (>= 5.0)
|
|
@@ -11,66 +12,68 @@ PATH
|
|
|
11
12
|
GEM
|
|
12
13
|
remote: https://rubygems.org/
|
|
13
14
|
specs:
|
|
14
|
-
actioncable (7.2.
|
|
15
|
-
actionpack (= 7.2.
|
|
16
|
-
activesupport (= 7.2.
|
|
15
|
+
actioncable (7.2.3.1)
|
|
16
|
+
actionpack (= 7.2.3.1)
|
|
17
|
+
activesupport (= 7.2.3.1)
|
|
17
18
|
nio4r (~> 2.0)
|
|
18
19
|
websocket-driver (>= 0.6.1)
|
|
19
20
|
zeitwerk (~> 2.6)
|
|
20
|
-
actionmailbox (7.2.
|
|
21
|
-
actionpack (= 7.2.
|
|
22
|
-
activejob (= 7.2.
|
|
23
|
-
activerecord (= 7.2.
|
|
24
|
-
activestorage (= 7.2.
|
|
25
|
-
activesupport (= 7.2.
|
|
21
|
+
actionmailbox (7.2.3.1)
|
|
22
|
+
actionpack (= 7.2.3.1)
|
|
23
|
+
activejob (= 7.2.3.1)
|
|
24
|
+
activerecord (= 7.2.3.1)
|
|
25
|
+
activestorage (= 7.2.3.1)
|
|
26
|
+
activesupport (= 7.2.3.1)
|
|
26
27
|
mail (>= 2.8.0)
|
|
27
|
-
actionmailer (7.2.
|
|
28
|
-
actionpack (= 7.2.
|
|
29
|
-
actionview (= 7.2.
|
|
30
|
-
activejob (= 7.2.
|
|
31
|
-
activesupport (= 7.2.
|
|
28
|
+
actionmailer (7.2.3.1)
|
|
29
|
+
actionpack (= 7.2.3.1)
|
|
30
|
+
actionview (= 7.2.3.1)
|
|
31
|
+
activejob (= 7.2.3.1)
|
|
32
|
+
activesupport (= 7.2.3.1)
|
|
32
33
|
mail (>= 2.8.0)
|
|
33
34
|
rails-dom-testing (~> 2.2)
|
|
34
|
-
actionpack (7.2.
|
|
35
|
-
actionview (= 7.2.
|
|
36
|
-
activesupport (= 7.2.
|
|
35
|
+
actionpack (7.2.3.1)
|
|
36
|
+
actionview (= 7.2.3.1)
|
|
37
|
+
activesupport (= 7.2.3.1)
|
|
38
|
+
cgi
|
|
37
39
|
nokogiri (>= 1.8.5)
|
|
38
40
|
racc
|
|
39
|
-
rack (>= 2.2.4, < 3.
|
|
41
|
+
rack (>= 2.2.4, < 3.3)
|
|
40
42
|
rack-session (>= 1.0.1)
|
|
41
43
|
rack-test (>= 0.6.3)
|
|
42
44
|
rails-dom-testing (~> 2.2)
|
|
43
45
|
rails-html-sanitizer (~> 1.6)
|
|
44
46
|
useragent (~> 0.16)
|
|
45
|
-
actiontext (7.2.
|
|
46
|
-
actionpack (= 7.2.
|
|
47
|
-
activerecord (= 7.2.
|
|
48
|
-
activestorage (= 7.2.
|
|
49
|
-
activesupport (= 7.2.
|
|
47
|
+
actiontext (7.2.3.1)
|
|
48
|
+
actionpack (= 7.2.3.1)
|
|
49
|
+
activerecord (= 7.2.3.1)
|
|
50
|
+
activestorage (= 7.2.3.1)
|
|
51
|
+
activesupport (= 7.2.3.1)
|
|
50
52
|
globalid (>= 0.6.0)
|
|
51
53
|
nokogiri (>= 1.8.5)
|
|
52
|
-
actionview (7.2.
|
|
53
|
-
activesupport (= 7.2.
|
|
54
|
+
actionview (7.2.3.1)
|
|
55
|
+
activesupport (= 7.2.3.1)
|
|
54
56
|
builder (~> 3.1)
|
|
57
|
+
cgi
|
|
55
58
|
erubi (~> 1.11)
|
|
56
59
|
rails-dom-testing (~> 2.2)
|
|
57
60
|
rails-html-sanitizer (~> 1.6)
|
|
58
|
-
activejob (7.2.
|
|
59
|
-
activesupport (= 7.2.
|
|
61
|
+
activejob (7.2.3.1)
|
|
62
|
+
activesupport (= 7.2.3.1)
|
|
60
63
|
globalid (>= 0.3.6)
|
|
61
|
-
activemodel (7.2.
|
|
62
|
-
activesupport (= 7.2.
|
|
63
|
-
activerecord (7.2.
|
|
64
|
-
activemodel (= 7.2.
|
|
65
|
-
activesupport (= 7.2.
|
|
64
|
+
activemodel (7.2.3.1)
|
|
65
|
+
activesupport (= 7.2.3.1)
|
|
66
|
+
activerecord (7.2.3.1)
|
|
67
|
+
activemodel (= 7.2.3.1)
|
|
68
|
+
activesupport (= 7.2.3.1)
|
|
66
69
|
timeout (>= 0.4.0)
|
|
67
|
-
activestorage (7.2.
|
|
68
|
-
actionpack (= 7.2.
|
|
69
|
-
activejob (= 7.2.
|
|
70
|
-
activerecord (= 7.2.
|
|
71
|
-
activesupport (= 7.2.
|
|
70
|
+
activestorage (7.2.3.1)
|
|
71
|
+
actionpack (= 7.2.3.1)
|
|
72
|
+
activejob (= 7.2.3.1)
|
|
73
|
+
activerecord (= 7.2.3.1)
|
|
74
|
+
activesupport (= 7.2.3.1)
|
|
72
75
|
marcel (~> 1.0)
|
|
73
|
-
activesupport (7.2.
|
|
76
|
+
activesupport (7.2.3.1)
|
|
74
77
|
base64
|
|
75
78
|
benchmark (>= 0.3)
|
|
76
79
|
bigdecimal
|
|
@@ -79,135 +82,159 @@ GEM
|
|
|
79
82
|
drb
|
|
80
83
|
i18n (>= 1.6, < 2)
|
|
81
84
|
logger (>= 1.4.2)
|
|
82
|
-
minitest (>= 5.1)
|
|
85
|
+
minitest (>= 5.1, < 6)
|
|
83
86
|
securerandom (>= 0.3)
|
|
84
87
|
tzinfo (~> 2.0, >= 2.0.5)
|
|
85
|
-
base64 (0.
|
|
86
|
-
benchmark (0.
|
|
87
|
-
bigdecimal (
|
|
88
|
+
base64 (0.3.0)
|
|
89
|
+
benchmark (0.5.0)
|
|
90
|
+
bigdecimal (4.1.2)
|
|
88
91
|
builder (3.3.0)
|
|
92
|
+
cgi (0.5.2)
|
|
89
93
|
concurrent-ruby (1.3.4)
|
|
90
|
-
connection_pool (2.5.
|
|
91
|
-
crass (1.0.
|
|
94
|
+
connection_pool (2.5.5)
|
|
95
|
+
crass (1.0.7)
|
|
96
|
+
csv (3.3.6)
|
|
92
97
|
database_cleaner (2.1.0)
|
|
93
98
|
database_cleaner-active_record (>= 2, < 3)
|
|
94
|
-
database_cleaner-active_record (2.2.
|
|
99
|
+
database_cleaner-active_record (2.2.2)
|
|
95
100
|
activerecord (>= 5.a)
|
|
96
|
-
database_cleaner-core (~> 2.0
|
|
101
|
+
database_cleaner-core (~> 2.0)
|
|
97
102
|
database_cleaner-core (2.0.1)
|
|
98
|
-
date (3.
|
|
99
|
-
diff-lcs (1.
|
|
100
|
-
drb (2.2.
|
|
103
|
+
date (3.5.1)
|
|
104
|
+
diff-lcs (1.6.2)
|
|
105
|
+
drb (2.2.3)
|
|
106
|
+
erb (4.0.4.1)
|
|
107
|
+
cgi (>= 0.3.3)
|
|
101
108
|
erubi (1.13.1)
|
|
102
|
-
faker (3.
|
|
109
|
+
faker (3.6.1)
|
|
103
110
|
i18n (>= 1.8.11, < 2)
|
|
104
|
-
globalid (1.
|
|
111
|
+
globalid (1.4.0)
|
|
105
112
|
activesupport (>= 6.1)
|
|
106
113
|
i18n (1.14.6)
|
|
107
114
|
concurrent-ruby (~> 1.0)
|
|
108
|
-
io-console (0.8.
|
|
109
|
-
irb (1.
|
|
115
|
+
io-console (0.8.2)
|
|
116
|
+
irb (1.18.0)
|
|
117
|
+
pp (>= 0.6.0)
|
|
118
|
+
prism (>= 1.3.0)
|
|
110
119
|
rdoc (>= 4.0.0)
|
|
111
120
|
reline (>= 0.4.2)
|
|
112
121
|
logger (1.6.5)
|
|
113
|
-
loofah (2.
|
|
122
|
+
loofah (2.25.2)
|
|
114
123
|
crass (~> 1.0.2)
|
|
115
124
|
nokogiri (>= 1.12.0)
|
|
116
|
-
mail (2.
|
|
125
|
+
mail (2.9.1)
|
|
126
|
+
logger
|
|
117
127
|
mini_mime (>= 0.1.1)
|
|
118
128
|
net-imap
|
|
119
129
|
net-pop
|
|
120
130
|
net-smtp
|
|
121
|
-
marcel (1.
|
|
131
|
+
marcel (1.2.1)
|
|
122
132
|
mini_mime (1.1.5)
|
|
123
|
-
minitest (5.
|
|
124
|
-
net-imap (0.5.
|
|
133
|
+
minitest (5.27.0)
|
|
134
|
+
net-imap (0.5.15)
|
|
125
135
|
date
|
|
126
136
|
net-protocol
|
|
127
137
|
net-pop (0.1.2)
|
|
128
138
|
net-protocol
|
|
129
139
|
net-protocol (0.2.2)
|
|
130
140
|
timeout
|
|
131
|
-
net-smtp (0.5.
|
|
141
|
+
net-smtp (0.5.1)
|
|
132
142
|
net-protocol
|
|
133
|
-
nio4r (2.7.
|
|
134
|
-
nokogiri (1.18.
|
|
143
|
+
nio4r (2.7.5)
|
|
144
|
+
nokogiri (1.18.10-arm64-darwin)
|
|
135
145
|
racc (~> 1.4)
|
|
136
|
-
|
|
137
|
-
|
|
146
|
+
nokogiri (1.18.10-x86_64-darwin)
|
|
147
|
+
racc (~> 1.4)
|
|
148
|
+
nokogiri (1.18.10-x86_64-linux-gnu)
|
|
149
|
+
racc (~> 1.4)
|
|
150
|
+
pg (1.6.3-arm64-darwin)
|
|
151
|
+
pg (1.6.3-x86_64-darwin)
|
|
152
|
+
pg (1.6.3-x86_64-linux)
|
|
153
|
+
pp (0.6.4)
|
|
154
|
+
prettyprint
|
|
155
|
+
prettyprint (0.2.0)
|
|
156
|
+
prism (1.9.0)
|
|
157
|
+
psych (5.4.0)
|
|
138
158
|
date
|
|
139
159
|
stringio
|
|
140
160
|
racc (1.8.1)
|
|
141
|
-
rack (3.
|
|
142
|
-
rack-session (2.1.
|
|
161
|
+
rack (3.2.6)
|
|
162
|
+
rack-session (2.1.2)
|
|
143
163
|
base64 (>= 0.1.0)
|
|
144
164
|
rack (>= 3.0.0)
|
|
145
165
|
rack-test (2.2.0)
|
|
146
166
|
rack (>= 1.3)
|
|
147
|
-
rackup (2.
|
|
167
|
+
rackup (2.3.1)
|
|
148
168
|
rack (>= 3)
|
|
149
|
-
rails (7.2.
|
|
150
|
-
actioncable (= 7.2.
|
|
151
|
-
actionmailbox (= 7.2.
|
|
152
|
-
actionmailer (= 7.2.
|
|
153
|
-
actionpack (= 7.2.
|
|
154
|
-
actiontext (= 7.2.
|
|
155
|
-
actionview (= 7.2.
|
|
156
|
-
activejob (= 7.2.
|
|
157
|
-
activemodel (= 7.2.
|
|
158
|
-
activerecord (= 7.2.
|
|
159
|
-
activestorage (= 7.2.
|
|
160
|
-
activesupport (= 7.2.
|
|
169
|
+
rails (7.2.3.1)
|
|
170
|
+
actioncable (= 7.2.3.1)
|
|
171
|
+
actionmailbox (= 7.2.3.1)
|
|
172
|
+
actionmailer (= 7.2.3.1)
|
|
173
|
+
actionpack (= 7.2.3.1)
|
|
174
|
+
actiontext (= 7.2.3.1)
|
|
175
|
+
actionview (= 7.2.3.1)
|
|
176
|
+
activejob (= 7.2.3.1)
|
|
177
|
+
activemodel (= 7.2.3.1)
|
|
178
|
+
activerecord (= 7.2.3.1)
|
|
179
|
+
activestorage (= 7.2.3.1)
|
|
180
|
+
activesupport (= 7.2.3.1)
|
|
161
181
|
bundler (>= 1.15.0)
|
|
162
|
-
railties (= 7.2.
|
|
163
|
-
rails-dom-testing (2.
|
|
182
|
+
railties (= 7.2.3.1)
|
|
183
|
+
rails-dom-testing (2.3.0)
|
|
164
184
|
activesupport (>= 5.0.0)
|
|
165
185
|
minitest
|
|
166
186
|
nokogiri (>= 1.6)
|
|
167
|
-
rails-html-sanitizer (1.
|
|
168
|
-
loofah (~> 2.
|
|
187
|
+
rails-html-sanitizer (1.7.1)
|
|
188
|
+
loofah (~> 2.25, >= 2.25.2)
|
|
169
189
|
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
|
170
|
-
railties (7.2.
|
|
171
|
-
actionpack (= 7.2.
|
|
172
|
-
activesupport (= 7.2.
|
|
190
|
+
railties (7.2.3.1)
|
|
191
|
+
actionpack (= 7.2.3.1)
|
|
192
|
+
activesupport (= 7.2.3.1)
|
|
193
|
+
cgi
|
|
173
194
|
irb (~> 1.13)
|
|
174
195
|
rackup (>= 1.0.0)
|
|
175
196
|
rake (>= 12.2)
|
|
176
197
|
thor (~> 1.0, >= 1.2.2)
|
|
198
|
+
tsort (>= 0.2)
|
|
177
199
|
zeitwerk (~> 2.6)
|
|
178
|
-
rake (13.2
|
|
179
|
-
rdoc (
|
|
200
|
+
rake (13.4.2)
|
|
201
|
+
rdoc (7.2.0)
|
|
202
|
+
erb
|
|
180
203
|
psych (>= 4.0.0)
|
|
181
|
-
|
|
204
|
+
tsort
|
|
205
|
+
reline (0.6.3)
|
|
182
206
|
io-console (~> 0.5)
|
|
183
|
-
rspec (3.13.
|
|
207
|
+
rspec (3.13.2)
|
|
184
208
|
rspec-core (~> 3.13.0)
|
|
185
209
|
rspec-expectations (~> 3.13.0)
|
|
186
210
|
rspec-mocks (~> 3.13.0)
|
|
187
|
-
rspec-core (3.13.
|
|
211
|
+
rspec-core (3.13.6)
|
|
188
212
|
rspec-support (~> 3.13.0)
|
|
189
|
-
rspec-expectations (3.13.
|
|
213
|
+
rspec-expectations (3.13.5)
|
|
190
214
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
191
215
|
rspec-support (~> 3.13.0)
|
|
192
|
-
rspec-mocks (3.13.
|
|
216
|
+
rspec-mocks (3.13.8)
|
|
193
217
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
194
218
|
rspec-support (~> 3.13.0)
|
|
195
219
|
rspec-support (3.13.2)
|
|
196
220
|
securerandom (0.4.1)
|
|
197
|
-
stringio (3.
|
|
198
|
-
thor (1.
|
|
199
|
-
timeout (0.
|
|
221
|
+
stringio (3.2.0)
|
|
222
|
+
thor (1.5.0)
|
|
223
|
+
timeout (0.6.1)
|
|
224
|
+
tsort (0.2.0)
|
|
200
225
|
tzinfo (2.0.6)
|
|
201
226
|
concurrent-ruby (~> 1.0)
|
|
202
227
|
useragent (0.16.11)
|
|
203
|
-
websocket-driver (0.
|
|
228
|
+
websocket-driver (0.8.2)
|
|
204
229
|
base64
|
|
205
230
|
websocket-extensions (>= 0.1.0)
|
|
206
231
|
websocket-extensions (0.1.5)
|
|
207
232
|
zeitwerk (2.6.18)
|
|
208
233
|
|
|
209
234
|
PLATFORMS
|
|
235
|
+
arm64-darwin-24
|
|
210
236
|
x86_64-darwin-21
|
|
237
|
+
x86_64-linux
|
|
211
238
|
|
|
212
239
|
DEPENDENCIES
|
|
213
240
|
database_cleaner (~> 2.0)
|
data/README.md
CHANGED
|
@@ -245,6 +245,36 @@ RSpec.describe "Blog" do
|
|
|
245
245
|
end
|
|
246
246
|
```
|
|
247
247
|
|
|
248
|
+
### 3. Load Strategies
|
|
249
|
+
|
|
250
|
+
`load_real_test_data` accepts a strategy that controls how the dump is executed:
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
# Default: shells out to psql. Data commits on a separate connection,
|
|
254
|
+
# outside any test transaction. Supports psql meta-commands (\set, \connect).
|
|
255
|
+
load_real_test_data("active_user_with_posts")
|
|
256
|
+
|
|
257
|
+
# Native: executes on the ActiveRecord connection, so the data joins the
|
|
258
|
+
# caller's transaction (e.g. DatabaseCleaner's :transaction strategy) and
|
|
259
|
+
# rolls back with it.
|
|
260
|
+
load_real_test_data("active_user_with_posts", strategy: RealDataTests::LoadStrategies::Native)
|
|
261
|
+
|
|
262
|
+
# Shorthand for the Native strategy:
|
|
263
|
+
load_real_test_data_native("active_user_with_posts")
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Custom strategies subclass `RealDataTests::LoadStrategies::Base` and implement `#call(dump_path)`:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
class MyStrategy < RealDataTests::LoadStrategies::Base
|
|
270
|
+
def call(dump_path)
|
|
271
|
+
# execute the dump however you like
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
load_real_test_data("active_user_with_posts", strategy: MyStrategy)
|
|
276
|
+
```
|
|
277
|
+
|
|
248
278
|
## Association Control
|
|
249
279
|
|
|
250
280
|
Real Data Tests provides several ways to control how associations are collected and loaded.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RealDataTests
|
|
4
|
+
module LoadStrategies
|
|
5
|
+
# Base class for dump loading strategies. Subclasses implement #call,
|
|
6
|
+
# which receives the absolute path to the SQL dump file and executes it.
|
|
7
|
+
#
|
|
8
|
+
# class MyStrategy < RealDataTests::LoadStrategies::Base
|
|
9
|
+
# def call(dump_path)
|
|
10
|
+
# # load the dump
|
|
11
|
+
# end
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# load_real_test_data("dump", strategy: MyStrategy)
|
|
15
|
+
class Base
|
|
16
|
+
def self.call(dump_path)
|
|
17
|
+
new.call(dump_path)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(dump_path)
|
|
21
|
+
raise NotImplementedError, "#{self.class.name} must implement #call"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RealDataTests
|
|
4
|
+
module LoadStrategies
|
|
5
|
+
# Loads a SQL dump on the ActiveRecord connection, so the data
|
|
6
|
+
# participates in the caller's transaction (e.g. DatabaseCleaner's
|
|
7
|
+
# :transaction strategy) and rolls back with it.
|
|
8
|
+
#
|
|
9
|
+
# Dumps without COPY ... FROM stdin blocks are sent as a single
|
|
10
|
+
# multi-statement execute (one server round-trip). Dumps containing COPY
|
|
11
|
+
# blocks fall back to block-by-block execution, streaming COPY data
|
|
12
|
+
# through raw_connection.copy_data on the same libpq session.
|
|
13
|
+
class Native < Base
|
|
14
|
+
def call(dump_path)
|
|
15
|
+
sql_content = File.read(dump_path)
|
|
16
|
+
|
|
17
|
+
ActiveRecord::Base.transaction do
|
|
18
|
+
connection = ActiveRecord::Base.connection
|
|
19
|
+
|
|
20
|
+
# Disable foreign key checks
|
|
21
|
+
connection.execute('SET session_replication_role = replica;')
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
if sql_content.match?(/^COPY .* FROM stdin/i)
|
|
25
|
+
blocks = SqlDumpParser.parse(sql_content)
|
|
26
|
+
blocks.each_with_index do |block, index|
|
|
27
|
+
execute_block(block, index + 1, blocks.length)
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
# No COPY blocks: send the whole dump in one round-trip. The
|
|
31
|
+
# server parses the multi-statement string, so semicolons inside
|
|
32
|
+
# string literals are handled correctly without client-side splitting.
|
|
33
|
+
connection.execute(sql_content)
|
|
34
|
+
end
|
|
35
|
+
ensure
|
|
36
|
+
connection.execute('SET session_replication_role = DEFAULT;')
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def execute_block(block, index, total)
|
|
44
|
+
case block.type
|
|
45
|
+
when :insert
|
|
46
|
+
execute_insert_block(block, index, total)
|
|
47
|
+
when :copy
|
|
48
|
+
execute_copy_block(block, index, total)
|
|
49
|
+
else
|
|
50
|
+
execute_regular_block(block, index, total)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def execute_insert_block(block, index, total)
|
|
55
|
+
# Don't modify statements that already end with semicolon
|
|
56
|
+
statement = if block.content.strip.end_with?(';')
|
|
57
|
+
block.content
|
|
58
|
+
else
|
|
59
|
+
"#{block.content};"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
begin
|
|
63
|
+
ActiveRecord::Base.connection.execute(statement)
|
|
64
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
65
|
+
if e.message.include?('syntax error at or near "ON"')
|
|
66
|
+
# Try alternative formatting for ON CONFLICT
|
|
67
|
+
modified_statement = statement.gsub(/\)\s+ON\s+CONFLICT/, ') ON CONFLICT')
|
|
68
|
+
ActiveRecord::Base.connection.execute(modified_statement)
|
|
69
|
+
else
|
|
70
|
+
raise
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def execute_copy_block(block, index, total)
|
|
76
|
+
lines = block.content.lines.map(&:chomp)
|
|
77
|
+
copy_statement = lines.shift
|
|
78
|
+
data_lines = lines.take_while { |line| line != '\\.' }
|
|
79
|
+
|
|
80
|
+
# raw_connection is the same libpq session as the AR connection, so the
|
|
81
|
+
# COPY participates in the surrounding transaction.
|
|
82
|
+
raw = ActiveRecord::Base.connection.raw_connection
|
|
83
|
+
raw.copy_data(copy_statement) do
|
|
84
|
+
data_lines.each { |line| raw.put_copy_data("#{line}\n") }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def execute_regular_block(block, index, total)
|
|
89
|
+
ActiveRecord::Base.connection.execute(block.content)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RealDataTests
|
|
4
|
+
module LoadStrategies
|
|
5
|
+
# Loads a SQL dump by shelling out to psql. The psql subprocess uses its
|
|
6
|
+
# own Postgres connection, so the loaded data commits immediately and does
|
|
7
|
+
# NOT participate in the caller's transaction (a transaction is
|
|
8
|
+
# per-connection; a child process can never join it).
|
|
9
|
+
#
|
|
10
|
+
# This is the default strategy of load_real_test_data. Prefer the Native
|
|
11
|
+
# strategy (load_real_test_data_native) unless the dump requires psql
|
|
12
|
+
# itself — e.g. psql meta-commands (\set, \connect) or dumps too large to
|
|
13
|
+
# read into memory.
|
|
14
|
+
class Psql < Base
|
|
15
|
+
def call(dump_path)
|
|
16
|
+
# Load the SQL dump quietly. Invoked without a shell (argv array +
|
|
17
|
+
# :in redirect), so paths and connection values need no escaping.
|
|
18
|
+
# Note: no transaction or session_replication_role handling here —
|
|
19
|
+
# psql runs on its own Postgres session, so nothing set on the
|
|
20
|
+
# ActiveRecord connection can affect the load.
|
|
21
|
+
result = system('psql', *connection_args, in: dump_path)
|
|
22
|
+
raise Error, "Failed to load test data: #{dump_path}" unless result
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def connection_args
|
|
28
|
+
config = if ActiveRecord::Base.respond_to?(:connection_db_config)
|
|
29
|
+
ActiveRecord::Base.connection_db_config.configuration_hash
|
|
30
|
+
else
|
|
31
|
+
ActiveRecord::Base.connection_config
|
|
32
|
+
end
|
|
33
|
+
args = []
|
|
34
|
+
args += ['-h', config[:host].to_s] if config[:host]
|
|
35
|
+
args += ['-p', config[:port].to_s] if config[:port]
|
|
36
|
+
args += ['-U', config[:username].to_s] if config[:username]
|
|
37
|
+
args += ['-d', config[:database].to_s]
|
|
38
|
+
args << '-q'
|
|
39
|
+
args
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -297,21 +297,5 @@ module RealDataTests
|
|
|
297
297
|
def sanitize_string(str)
|
|
298
298
|
"'#{str.gsub("'", "''")}'"
|
|
299
299
|
end
|
|
300
|
-
|
|
301
|
-
def connection_options
|
|
302
|
-
config = if ActiveRecord::Base.respond_to?(:connection_db_config)
|
|
303
|
-
ActiveRecord::Base.connection_db_config.configuration_hash
|
|
304
|
-
else
|
|
305
|
-
ActiveRecord::Base.connection_config
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
options = []
|
|
309
|
-
options << "-h #{config[:host]}" if config[:host]
|
|
310
|
-
options << "-p #{config[:port]}" if config[:port]
|
|
311
|
-
options << "-U #{config[:username]}" if config[:username]
|
|
312
|
-
options << "-d #{config[:database]}"
|
|
313
|
-
options << "-q" # Run quietly
|
|
314
|
-
options.join(" ")
|
|
315
|
-
end
|
|
316
300
|
end
|
|
317
301
|
end
|
|
@@ -1,376 +1,30 @@
|
|
|
1
1
|
module RealDataTests
|
|
2
2
|
module RSpecHelper
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
private
|
|
13
|
-
|
|
14
|
-
def determine_block_type
|
|
15
|
-
case @content
|
|
16
|
-
when /\AINSERT INTO/i
|
|
17
|
-
:insert
|
|
18
|
-
when /\ACOPY.*FROM stdin/i
|
|
19
|
-
:copy
|
|
20
|
-
when /\AALTER TABLE/i
|
|
21
|
-
:alter
|
|
22
|
-
when /\ASET/i
|
|
23
|
-
:set
|
|
24
|
-
else
|
|
25
|
-
:other
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def extract_table_name
|
|
30
|
-
if @content =~ /INSERT INTO\s+"?([^\s"(]+)"?\s/i
|
|
31
|
-
$1
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def load_real_test_data(name)
|
|
3
|
+
# Loads a SQL dump using the given strategy (default: psql shell-out,
|
|
4
|
+
# matching pre-0.5 behavior — data commits outside the caller's
|
|
5
|
+
# transaction). Pass a different strategy to change how the dump is
|
|
6
|
+
# executed, e.g. transactional loading on the ActiveRecord connection:
|
|
7
|
+
#
|
|
8
|
+
# load_real_test_data("dump", strategy: RealDataTests::LoadStrategies::Native)
|
|
9
|
+
def load_real_test_data(name, strategy: LoadStrategies::Psql)
|
|
37
10
|
dump_path = File.join(RealDataTests.configuration.dump_path, "#{name}.sql")
|
|
38
11
|
raise Error, "Test data file not found: #{dump_path}" unless File.exist?(dump_path)
|
|
39
|
-
ActiveRecord::Base.transaction do
|
|
40
|
-
# Disable foreign key checks
|
|
41
|
-
ActiveRecord::Base.connection.execute('SET session_replication_role = replica;')
|
|
42
|
-
begin
|
|
43
|
-
# Load the SQL dump quietly
|
|
44
|
-
result = system("psql #{connection_options} -q < #{dump_path}")
|
|
45
|
-
raise Error, "Failed to load test data: #{dump_path}" unless result
|
|
46
|
-
ensure
|
|
47
|
-
# Re-enable foreign key checks
|
|
48
|
-
ActiveRecord::Base.connection.execute('SET session_replication_role = DEFAULT;')
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def load_real_test_data_native(name)
|
|
54
|
-
dump_path = File.join(RealDataTests.configuration.dump_path, "#{name}.sql")
|
|
55
|
-
raise Error, "Test data file not found: #{dump_path}" unless File.exist?(dump_path)
|
|
56
|
-
|
|
57
|
-
sql_content = File.read(dump_path)
|
|
58
|
-
blocks = parse_sql_blocks(sql_content)
|
|
59
|
-
|
|
60
|
-
ActiveRecord::Base.transaction do
|
|
61
|
-
connection = ActiveRecord::Base.connection
|
|
62
|
-
|
|
63
|
-
# Disable foreign key checks
|
|
64
|
-
connection.execute('SET session_replication_role = replica;')
|
|
65
|
-
|
|
66
|
-
begin
|
|
67
|
-
blocks.each_with_index do |block, index|
|
|
68
|
-
execute_block(block, index + 1, blocks.length)
|
|
69
|
-
end
|
|
70
|
-
ensure
|
|
71
|
-
connection.execute('SET session_replication_role = DEFAULT;')
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
private
|
|
77
|
-
|
|
78
|
-
def connection_options
|
|
79
|
-
config = if ActiveRecord::Base.respond_to?(:connection_db_config)
|
|
80
|
-
ActiveRecord::Base.connection_db_config.configuration_hash
|
|
81
|
-
else
|
|
82
|
-
ActiveRecord::Base.connection_config
|
|
83
|
-
end
|
|
84
|
-
options = []
|
|
85
|
-
options << "-h #{config[:host]}" if config[:host]
|
|
86
|
-
options << "-p #{config[:port]}" if config[:port]
|
|
87
|
-
options << "-U #{config[:username]}" if config[:username]
|
|
88
|
-
options << "-d #{config[:database]}"
|
|
89
|
-
options << "-q"
|
|
90
|
-
options.join(" ")
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
class SqlBlock
|
|
94
|
-
attr_reader :type, :content, :table_name
|
|
95
|
-
|
|
96
|
-
def initialize(content)
|
|
97
|
-
@content = content.strip
|
|
98
|
-
@type = determine_block_type
|
|
99
|
-
@table_name = extract_table_name if @type == :insert
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
private
|
|
103
|
-
|
|
104
|
-
def determine_block_type
|
|
105
|
-
if @content.match?(/\AINSERT INTO/i)
|
|
106
|
-
:insert
|
|
107
|
-
elsif @content.match?(/\ACOPY.*FROM stdin/i)
|
|
108
|
-
:copy
|
|
109
|
-
elsif @content.match?(/\AALTER TABLE/i)
|
|
110
|
-
:alter
|
|
111
|
-
elsif @content.match?(/\ASET/i)
|
|
112
|
-
:set
|
|
113
|
-
else
|
|
114
|
-
:other
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def extract_table_name
|
|
119
|
-
if @content =~ /INSERT INTO\s+"?([^\s"(]+)"?\s/i
|
|
120
|
-
$1
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def parse_sql_blocks(content)
|
|
126
|
-
blocks = []
|
|
127
|
-
current_block = []
|
|
128
|
-
in_copy_block = false
|
|
129
|
-
|
|
130
|
-
content.each_line do |line|
|
|
131
|
-
line = line.chomp
|
|
132
|
-
|
|
133
|
-
# Skip empty lines and comments unless in COPY block
|
|
134
|
-
next if !in_copy_block && (line.empty? || line.start_with?('--'))
|
|
135
|
-
|
|
136
|
-
# Handle start of COPY block
|
|
137
|
-
if !in_copy_block && line.upcase.match?(/\ACOPY.*FROM stdin/i)
|
|
138
|
-
current_block = [line]
|
|
139
|
-
in_copy_block = true
|
|
140
|
-
next
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Handle end of COPY block
|
|
144
|
-
if in_copy_block && line == '\\.'
|
|
145
|
-
current_block << line
|
|
146
|
-
blocks << SqlBlock.new(current_block.join("\n"))
|
|
147
|
-
current_block = []
|
|
148
|
-
in_copy_block = false
|
|
149
|
-
next
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# Accumulate lines in COPY block
|
|
153
|
-
if in_copy_block
|
|
154
|
-
current_block << line
|
|
155
|
-
next
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# Handle regular SQL statements
|
|
159
|
-
current_block << line
|
|
160
|
-
if line.end_with?(';')
|
|
161
|
-
blocks << SqlBlock.new(current_block.join("\n"))
|
|
162
|
-
current_block = []
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Handle any remaining block
|
|
167
|
-
blocks << SqlBlock.new(current_block.join("\n")) unless current_block.empty?
|
|
168
|
-
blocks
|
|
169
|
-
end
|
|
170
12
|
|
|
171
|
-
|
|
172
|
-
case block.type
|
|
173
|
-
when :insert
|
|
174
|
-
execute_insert_block(block, index, total)
|
|
175
|
-
when :copy
|
|
176
|
-
execute_copy_block(block, index, total)
|
|
177
|
-
else
|
|
178
|
-
execute_regular_block(block, index, total)
|
|
179
|
-
end
|
|
13
|
+
strategy.call(dump_path)
|
|
180
14
|
end
|
|
181
15
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if e.message.include?('syntax error at or near "ON"')
|
|
195
|
-
# Try alternative formatting for ON CONFLICT
|
|
196
|
-
modified_statement = statement.gsub(/\)\s+ON\s+CONFLICT/, ') ON CONFLICT')
|
|
197
|
-
ActiveRecord::Base.connection.execute(modified_statement)
|
|
198
|
-
else
|
|
199
|
-
raise
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
def execute_copy_block(block, index, total)
|
|
205
|
-
# puts "Executing COPY block #{index}/#{total}"
|
|
206
|
-
ActiveRecord::Base.connection.execute(block.content)
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def execute_regular_block(block, index, total)
|
|
210
|
-
# puts "Executing block #{index}/#{total} of type: #{block.type}"
|
|
211
|
-
ActiveRecord::Base.connection.execute(block.content)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def normalize_insert_statement(statement)
|
|
215
|
-
# First clean up any excess whitespace around parentheses
|
|
216
|
-
statement = statement.gsub(/\(\s+/, '(')
|
|
217
|
-
.gsub(/\s+\)/, ')')
|
|
218
|
-
.gsub(/\)\s+ON\s+CONFLICT/, ') ON CONFLICT')
|
|
219
|
-
|
|
220
|
-
# Ensure proper spacing around ON CONFLICT
|
|
221
|
-
if statement =~ /(.*?)\s*ON\s+CONFLICT\s+(.*?)\s*(?:DO\s+.*?)?\s*;\s*\z/i
|
|
222
|
-
base = $1.strip
|
|
223
|
-
conflict_part = $2.strip
|
|
224
|
-
action_part = $3&.strip || 'DO NOTHING'
|
|
225
|
-
|
|
226
|
-
# Rebuild the statement with consistent formatting
|
|
227
|
-
"#{base} ON CONFLICT #{conflict_part} #{action_part};"
|
|
228
|
-
else
|
|
229
|
-
# If no ON CONFLICT clause, just clean up the spacing
|
|
230
|
-
statement.strip.sub(/;?\s*$/, ';')
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
def split_sql_statements(sql)
|
|
235
|
-
statements = []
|
|
236
|
-
current_statement = ''
|
|
237
|
-
in_string = false
|
|
238
|
-
escaped = false
|
|
239
|
-
|
|
240
|
-
sql.each_char do |char|
|
|
241
|
-
if char == '\\'
|
|
242
|
-
escaped = !escaped
|
|
243
|
-
elsif char == "'" && !escaped
|
|
244
|
-
in_string = !in_string
|
|
245
|
-
elsif char == ';' && !in_string
|
|
246
|
-
# Add the completed statement
|
|
247
|
-
statements << current_statement.strip unless current_statement.strip.empty?
|
|
248
|
-
current_statement = ''
|
|
249
|
-
next
|
|
250
|
-
end
|
|
251
|
-
escaped = false
|
|
252
|
-
current_statement << char
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# Add the last statement if it exists
|
|
256
|
-
statements << current_statement.strip unless current_statement.strip.empty?
|
|
257
|
-
|
|
258
|
-
# Normalize `ON CONFLICT` clauses
|
|
259
|
-
statements = statements.each_with_object([]) do |stmt, result|
|
|
260
|
-
if stmt.strip.upcase.start_with?('ON CONFLICT')
|
|
261
|
-
result[-1] = "#{result.last.strip} #{stmt.strip}"
|
|
262
|
-
else
|
|
263
|
-
result << stmt.strip
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
# Ensure semicolons and spacing
|
|
268
|
-
statements.map! do |stmt|
|
|
269
|
-
stmt = stmt.gsub(/\)\s*ON CONFLICT/, ') ON CONFLICT') # Normalize spacing
|
|
270
|
-
stmt.strip.end_with?(';') ? stmt.strip : "#{stmt.strip};"
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
statements
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
def extract_conflict_clause(statement)
|
|
277
|
-
# Use a more precise regex that handles multiple closing parentheses
|
|
278
|
-
if statement =~ /(.+?\))\s*(ON\s+CONFLICT\s+.*?)(?:;?\s*$)/i
|
|
279
|
-
[$1, $2.strip]
|
|
280
|
-
else
|
|
281
|
-
[statement.sub(/;?\s*$/, ''), nil]
|
|
282
|
-
end
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def clean_sql_statement(statement)
|
|
286
|
-
# Match either INSERT INTO...VALUES or just VALUES
|
|
287
|
-
if statement =~ /(?:INSERT INTO.*)?VALUES\s*\(/i
|
|
288
|
-
# Split the statement into parts, being careful with the ending
|
|
289
|
-
if statement =~ /(.*?VALUES\s*\()(.*)(\)\s*(?:ON CONFLICT.*)?;?\s*$)/i
|
|
290
|
-
pre_values = $1
|
|
291
|
-
values_content = $2
|
|
292
|
-
post_values = $3
|
|
293
|
-
|
|
294
|
-
# Clean the values content while preserving complex JSON
|
|
295
|
-
cleaned_values = clean_complex_values(values_content)
|
|
296
|
-
|
|
297
|
-
# Reassemble the statement, ensuring exactly one semicolon at the end
|
|
298
|
-
statement = "#{pre_values}#{cleaned_values}#{post_values}"
|
|
299
|
-
statement = statement.gsub(/;*\s*$/, '') # Remove any trailing semicolons and whitespace
|
|
300
|
-
statement += ";"
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
statement
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
def clean_complex_values(values_str)
|
|
307
|
-
current_value = ''
|
|
308
|
-
values = []
|
|
309
|
-
in_quotes = false
|
|
310
|
-
in_json = false
|
|
311
|
-
json_brace_count = 0
|
|
312
|
-
escaped = false
|
|
313
|
-
|
|
314
|
-
chars = values_str.chars
|
|
315
|
-
i = 0
|
|
316
|
-
while i < chars.length
|
|
317
|
-
char = chars[i]
|
|
318
|
-
|
|
319
|
-
case char
|
|
320
|
-
when '\\'
|
|
321
|
-
current_value << char
|
|
322
|
-
escaped = !escaped
|
|
323
|
-
when "'"
|
|
324
|
-
if !escaped
|
|
325
|
-
in_quotes = !in_quotes
|
|
326
|
-
end
|
|
327
|
-
escaped = false
|
|
328
|
-
current_value << char
|
|
329
|
-
when '{'
|
|
330
|
-
if !in_quotes
|
|
331
|
-
in_json = true
|
|
332
|
-
json_brace_count += 1
|
|
333
|
-
end
|
|
334
|
-
current_value << char
|
|
335
|
-
when '}'
|
|
336
|
-
if !in_quotes
|
|
337
|
-
json_brace_count -= 1
|
|
338
|
-
in_json = json_brace_count > 0
|
|
339
|
-
end
|
|
340
|
-
current_value << char
|
|
341
|
-
when ','
|
|
342
|
-
if !in_quotes && !in_json
|
|
343
|
-
values << clean_value(current_value.strip)
|
|
344
|
-
current_value = ''
|
|
345
|
-
else
|
|
346
|
-
current_value << char
|
|
347
|
-
end
|
|
348
|
-
else
|
|
349
|
-
escaped = false
|
|
350
|
-
current_value << char
|
|
351
|
-
end
|
|
352
|
-
i += 1
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
# Add the last value
|
|
356
|
-
values << clean_value(current_value.strip) unless current_value.strip.empty?
|
|
357
|
-
|
|
358
|
-
values.join(', ')
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
def clean_value(value)
|
|
362
|
-
return value if value.start_with?("'") # Already quoted
|
|
363
|
-
return value if value.start_with?("'{") # JSON object
|
|
364
|
-
return 'NULL' if value.upcase == 'NULL'
|
|
365
|
-
return value.downcase if ['true', 'false'].include?(value.downcase)
|
|
366
|
-
return value if value.match?(/^\d+$/) # Numbers
|
|
367
|
-
|
|
368
|
-
if value.match?(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)
|
|
369
|
-
"'#{value}'" # UUID
|
|
370
|
-
else
|
|
371
|
-
# Handle any other string value, including those with commas
|
|
372
|
-
"'#{value}'" # Other strings
|
|
373
|
-
end
|
|
16
|
+
# Loads a SQL dump on the ActiveRecord connection
|
|
17
|
+
# (LoadStrategies::Native), so the data participates in the caller's
|
|
18
|
+
# transaction (e.g. DatabaseCleaner's :transaction strategy) and rolls
|
|
19
|
+
# back with it. Shorthand for:
|
|
20
|
+
#
|
|
21
|
+
# load_real_test_data(name, strategy: RealDataTests::LoadStrategies::Native)
|
|
22
|
+
#
|
|
23
|
+
# Unlike the default psql strategy, this never commits outside the test
|
|
24
|
+
# transaction, but it cannot process psql meta-commands (e.g. \set) and
|
|
25
|
+
# reads the whole dump into memory.
|
|
26
|
+
def load_real_test_data_native(name)
|
|
27
|
+
load_real_test_data(name, strategy: LoadStrategies::Native)
|
|
374
28
|
end
|
|
375
29
|
end
|
|
376
|
-
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RealDataTests
|
|
4
|
+
# Splits a SQL dump into executable blocks. Regular statements become one
|
|
5
|
+
# block each (terminated by ";"); COPY ... FROM stdin blocks span from the
|
|
6
|
+
# COPY line through the "\." terminator, preserving the data lines between.
|
|
7
|
+
class SqlDumpParser
|
|
8
|
+
# Returns an array of SqlBlock in dump order.
|
|
9
|
+
def self.parse(content)
|
|
10
|
+
blocks = []
|
|
11
|
+
current_block = []
|
|
12
|
+
in_copy_block = false
|
|
13
|
+
|
|
14
|
+
content.each_line do |line|
|
|
15
|
+
line = line.chomp
|
|
16
|
+
|
|
17
|
+
# Skip empty lines and comments unless in COPY block
|
|
18
|
+
next if !in_copy_block && (line.empty? || line.start_with?('--'))
|
|
19
|
+
|
|
20
|
+
# Handle start of COPY block
|
|
21
|
+
if !in_copy_block && line.upcase.match?(/\ACOPY.*FROM stdin/i)
|
|
22
|
+
current_block = [line]
|
|
23
|
+
in_copy_block = true
|
|
24
|
+
next
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Handle end of COPY block
|
|
28
|
+
if in_copy_block && line == '\\.'
|
|
29
|
+
current_block << line
|
|
30
|
+
blocks << SqlBlock.new(current_block.join("\n"))
|
|
31
|
+
current_block = []
|
|
32
|
+
in_copy_block = false
|
|
33
|
+
next
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Accumulate lines in COPY block
|
|
37
|
+
if in_copy_block
|
|
38
|
+
current_block << line
|
|
39
|
+
next
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Handle regular SQL statements
|
|
43
|
+
current_block << line
|
|
44
|
+
if line.end_with?(';')
|
|
45
|
+
blocks << SqlBlock.new(current_block.join("\n"))
|
|
46
|
+
current_block = []
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Handle any remaining block
|
|
51
|
+
blocks << SqlBlock.new(current_block.join("\n")) unless current_block.empty?
|
|
52
|
+
blocks
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# A single parsed block of the dump, tagged with its statement type.
|
|
56
|
+
class SqlBlock
|
|
57
|
+
attr_reader :type, :content, :table_name
|
|
58
|
+
|
|
59
|
+
def initialize(content)
|
|
60
|
+
@content = content.strip
|
|
61
|
+
@type = determine_block_type
|
|
62
|
+
@table_name = extract_table_name if @type == :insert
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def determine_block_type
|
|
68
|
+
case @content
|
|
69
|
+
when /\AINSERT INTO/i
|
|
70
|
+
:insert
|
|
71
|
+
when /\ACOPY.*FROM stdin/i
|
|
72
|
+
:copy
|
|
73
|
+
when /\AALTER TABLE/i
|
|
74
|
+
:alter
|
|
75
|
+
when /\ASET/i
|
|
76
|
+
:set
|
|
77
|
+
else
|
|
78
|
+
:other
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def extract_table_name
|
|
83
|
+
if @content =~ /INSERT INTO\s+"?([^\s"(]+)"?\s/i
|
|
84
|
+
$1
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/real_data_tests.rb
CHANGED
|
@@ -7,6 +7,10 @@ require_relative 'real_data_tests/data_anonymizer'
|
|
|
7
7
|
require_relative 'real_data_tests/engine' if defined?(Rails)
|
|
8
8
|
require_relative 'real_data_tests/pg_dump_generator'
|
|
9
9
|
require_relative 'real_data_tests/record_collector'
|
|
10
|
+
require_relative 'real_data_tests/sql_dump_parser'
|
|
11
|
+
require_relative 'real_data_tests/load_strategies/base'
|
|
12
|
+
require_relative 'real_data_tests/load_strategies/native'
|
|
13
|
+
require_relative 'real_data_tests/load_strategies/psql'
|
|
10
14
|
require_relative 'real_data_tests/rspec_helper'
|
|
11
15
|
require_relative 'real_data_tests/test_data_builder'
|
|
12
16
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/real_data_tests/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "real_data_tests"
|
|
7
|
+
spec.version = RealDataTests::VERSION
|
|
8
|
+
spec.authors = ["Kevin Dias"]
|
|
9
|
+
spec.email = ["diasks2@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Create realistic test data from local db records"
|
|
12
|
+
spec.description = "A Ruby gem that helps create test data by analyzing and extracting real records and their associations from your database."
|
|
13
|
+
spec.homepage = "https://github.com/diasks2/real_data_tests"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 2.6.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
19
|
+
spec.metadata["changelog_uri"] = spec.homepage
|
|
20
|
+
|
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
25
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
spec.bindir = "exe"
|
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
30
|
+
spec.require_paths = ["lib"]
|
|
31
|
+
|
|
32
|
+
spec.add_dependency "rails", ">= 5.0"
|
|
33
|
+
spec.add_dependency "activerecord", ">= 5.0"
|
|
34
|
+
spec.add_dependency "thor", "~> 1.0"
|
|
35
|
+
spec.add_dependency "pg", ">= 1.1"
|
|
36
|
+
spec.add_dependency "csv"
|
|
37
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
38
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
39
|
+
spec.add_development_dependency "database_cleaner", "~> 2.0"
|
|
40
|
+
spec.add_development_dependency "database_cleaner-active_record"
|
|
41
|
+
spec.add_dependency "faker", "~> 3.0"
|
|
42
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: real_data_tests
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kevin Dias
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rails
|
|
@@ -66,6 +65,20 @@ dependencies:
|
|
|
66
65
|
- - ">="
|
|
67
66
|
- !ruby/object:Gem::Version
|
|
68
67
|
version: '1.1'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: csv
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
69
82
|
- !ruby/object:Gem::Dependency
|
|
70
83
|
name: rake
|
|
71
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -156,11 +169,16 @@ files:
|
|
|
156
169
|
- lib/real_data_tests/configuration.rb
|
|
157
170
|
- lib/real_data_tests/data_anonymizer.rb
|
|
158
171
|
- lib/real_data_tests/engine.rb
|
|
172
|
+
- lib/real_data_tests/load_strategies/base.rb
|
|
173
|
+
- lib/real_data_tests/load_strategies/native.rb
|
|
174
|
+
- lib/real_data_tests/load_strategies/psql.rb
|
|
159
175
|
- lib/real_data_tests/pg_dump_generator.rb
|
|
160
176
|
- lib/real_data_tests/record_collector.rb
|
|
161
177
|
- lib/real_data_tests/rspec_helper.rb
|
|
178
|
+
- lib/real_data_tests/sql_dump_parser.rb
|
|
162
179
|
- lib/real_data_tests/test_data_builder.rb
|
|
163
180
|
- lib/real_data_tests/version.rb
|
|
181
|
+
- real_data_tests.gemspec
|
|
164
182
|
homepage: https://github.com/diasks2/real_data_tests
|
|
165
183
|
licenses:
|
|
166
184
|
- MIT
|
|
@@ -168,7 +186,6 @@ metadata:
|
|
|
168
186
|
homepage_uri: https://github.com/diasks2/real_data_tests
|
|
169
187
|
source_code_uri: https://github.com/diasks2/real_data_tests
|
|
170
188
|
changelog_uri: https://github.com/diasks2/real_data_tests
|
|
171
|
-
post_install_message:
|
|
172
189
|
rdoc_options: []
|
|
173
190
|
require_paths:
|
|
174
191
|
- lib
|
|
@@ -183,8 +200,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
183
200
|
- !ruby/object:Gem::Version
|
|
184
201
|
version: '0'
|
|
185
202
|
requirements: []
|
|
186
|
-
rubygems_version: 3.
|
|
187
|
-
signing_key:
|
|
203
|
+
rubygems_version: 3.7.2
|
|
188
204
|
specification_version: 4
|
|
189
205
|
summary: Create realistic test data from local db records
|
|
190
206
|
test_files: []
|