Fix migration hygiene: UTF-8-safe comment strip + rerun-safe index 168
Two pre-existing issues surfaced by Sando's cargo_test gate (audit Run 21/22
artifacts never deployed since prod is at 0.10.6):
- tests/migration_hygiene.rs statements() mixed byte-index iteration with
string slicing (raw[i..i+2]) and pushed bytes as chars, panicking on the
first multibyte char (migration 168 contains '…'). Rewrite with str::find
offsets + push_str so it is UTF-8-safe.
- migration 168 CREATE INDEX lacked IF NOT EXISTS, violating the rerun-safety
convention the hygiene test enforces. Add IF NOT EXISTS. Safe: 168 postdates
0.10.6 and is unapplied on every pipeline tier.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 files changed,
+15 insertions,
-13 deletions
| 10 |
10 |
|
|
| 11 |
11 |
|
-- Partial index for the reaper's `status = 'processing' AND heartbeat_at < …`
|
| 12 |
12 |
|
-- sweep — only in-flight jobs are ever scanned.
|
| 13 |
|
- |
CREATE INDEX idx_import_jobs_processing_heartbeat
|
|
13 |
+ |
CREATE INDEX IF NOT EXISTS idx_import_jobs_processing_heartbeat
|
| 14 |
14 |
|
ON import_jobs (heartbeat_at)
|
| 15 |
15 |
|
WHERE status = 'processing';
|
| 147 |
147 |
|
/// Split into statements: strip `--` line comments and `/* */` block comments,
|
| 148 |
148 |
|
/// lowercase, collapse whitespace, split on `;`.
|
| 149 |
149 |
|
fn statements(raw: &str) -> Vec<String> {
|
| 150 |
|
- |
// Strip block comments.
|
|
150 |
+ |
// Strip block comments. Uses `str::find` (whose offsets are always char
|
|
151 |
+ |
// boundaries) and `push_str`, so it is UTF-8-safe — an earlier byte-index
|
|
152 |
+ |
// version panicked when a migration comment contained a multibyte char
|
|
153 |
+ |
// like '…'.
|
| 151 |
154 |
|
let mut no_block = String::with_capacity(raw.len());
|
| 152 |
|
- |
let bytes = raw.as_bytes();
|
| 153 |
|
- |
let mut i = 0;
|
| 154 |
|
- |
while i < bytes.len() {
|
| 155 |
|
- |
if i + 1 < bytes.len() && &raw[i..i + 2] == "/*" {
|
| 156 |
|
- |
if let Some(end) = raw[i..].find("*/") {
|
| 157 |
|
- |
i += end + 2;
|
| 158 |
|
- |
no_block.push(' ');
|
| 159 |
|
- |
continue;
|
|
155 |
+ |
let mut rest = raw;
|
|
156 |
+ |
while let Some(start) = rest.find("/*") {
|
|
157 |
+ |
no_block.push_str(&rest[..start]);
|
|
158 |
+ |
no_block.push(' ');
|
|
159 |
+ |
match rest[start + 2..].find("*/") {
|
|
160 |
+ |
Some(end) => rest = &rest[start + 2 + end + 2..],
|
|
161 |
+ |
None => {
|
|
162 |
+ |
rest = "";
|
|
163 |
+ |
break;
|
| 160 |
164 |
|
}
|
| 161 |
|
- |
break;
|
| 162 |
165 |
|
}
|
| 163 |
|
- |
no_block.push(bytes[i] as char);
|
| 164 |
|
- |
i += 1;
|
| 165 |
166 |
|
}
|
|
167 |
+ |
no_block.push_str(rest);
|
| 166 |
168 |
|
// Strip line comments and normalize.
|
| 167 |
169 |
|
let cleaned: String = no_block
|
| 168 |
170 |
|
.lines()
|