From a2423ad4ad2a6c1fd0b54b8a6eff5f23e30e4de2 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 23 Jun 2026 22:25:25 +0900 Subject: [PATCH 001/109] Improve performance --- Cargo.lock | 68 ++--------- crates/vespertide-lsp/Cargo.toml | 13 +- .../benches/workspace_fanout.rs | 112 ++++++++++++++++++ crates/vespertide-lsp/src/backend/mod.rs | 73 +++++++++++- .../src/backend/tests/lifecycle.rs | 46 +++++++ crates/vespertide-lsp/src/cache.rs | 12 +- .../src/diagnostics/validation/mod.rs | 1 + .../vespertide-lsp/src/references/search.rs | 43 ++++--- crates/vespertide-lsp/src/workspace_tables.rs | 19 +++ crates/vespertide-macro/Cargo.toml | 9 +- crates/vespertide-query/src/sql/helpers.rs | 15 ++- 11 files changed, 324 insertions(+), 87 deletions(-) create mode 100644 crates/vespertide-lsp/benches/workspace_fanout.rs diff --git a/Cargo.lock b/Cargo.lock index d102c03a..4e75c2f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1304,17 +1304,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "mio" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - [[package]] name = "multimap" version = "0.10.1" @@ -2262,16 +2251,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - [[package]] name = "similar" version = "2.7.0" @@ -2290,16 +2269,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "sqlite-wasm-rs" version = "0.5.5" @@ -2502,15 +2471,8 @@ version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", - "socket2", "tokio-macros", - "windows-sys 0.61.2", ] [[package]] @@ -2836,7 +2798,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vespertide" -version = "0.1.61" +version = "0.2.1" dependencies = [ "sea-orm", "tokio", @@ -2846,7 +2808,7 @@ dependencies = [ [[package]] name = "vespertide-cli" -version = "0.1.61" +version = "0.2.1" dependencies = [ "anyhow", "assert_cmd", @@ -2878,7 +2840,7 @@ dependencies = [ [[package]] name = "vespertide-config" -version = "0.1.61" +version = "0.2.1" dependencies = [ "clap", "schemars", @@ -2888,7 +2850,7 @@ dependencies = [ [[package]] name = "vespertide-core" -version = "0.1.61" +version = "0.2.1" dependencies = [ "criterion", "proptest", @@ -2903,7 +2865,7 @@ dependencies = [ [[package]] name = "vespertide-exporter" -version = "0.1.61" +version = "0.2.1" dependencies = [ "criterion", "insta", @@ -2932,7 +2894,7 @@ dependencies = [ [[package]] name = "vespertide-loader" -version = "0.1.61" +version = "0.2.1" dependencies = [ "anyhow", "rayon", @@ -2948,9 +2910,11 @@ dependencies = [ [[package]] name = "vespertide-lsp" -version = "0.1.61" +version = "0.2.1" dependencies = [ + "criterion", "dashmap", + "futures", "insta", "lsp-textdocument", "lsp-types", @@ -2977,7 +2941,7 @@ dependencies = [ [[package]] name = "vespertide-macro" -version = "0.1.61" +version = "0.2.1" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2996,7 +2960,7 @@ dependencies = [ [[package]] name = "vespertide-naming" -version = "0.1.61" +version = "0.2.1" dependencies = [ "criterion", "proptest", @@ -3004,7 +2968,7 @@ dependencies = [ [[package]] name = "vespertide-planner" -version = "0.1.61" +version = "0.2.1" dependencies = [ "criterion", "insta", @@ -3019,7 +2983,7 @@ dependencies = [ [[package]] name = "vespertide-query" -version = "0.1.61" +version = "0.2.1" dependencies = [ "criterion", "insta", @@ -3071,12 +3035,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - [[package]] name = "wasip2" version = "1.0.3+wasi-0.2.9" diff --git a/crates/vespertide-lsp/Cargo.toml b/crates/vespertide-lsp/Cargo.toml index f7b507c9..3723ecf9 100644 --- a/crates/vespertide-lsp/Cargo.toml +++ b/crates/vespertide-lsp/Cargo.toml @@ -22,8 +22,11 @@ path = "src/main.rs" # LSP framework - tower-lsp-server (community fork; tower-lsp 0.20 is abandoned) tower-lsp-server = "=0.23.0" -# Async runtime -tokio = { version = "1", features = ["full"] } +# Async runtime. Only the multi-thread runtime (#[tokio::main]), the proc-macro +# attrs, and stdin/stdout are used directly here; tower-lsp-server pulls in any +# further tokio features it needs transitively. Avoiding "full" trims the LSP +# binary's build (drops net/time/process/signal/fs/sync codegen we never touch). +tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std"] } # Tree-sitter parsers (used from Wave 2+) tree-sitter = "0.26" @@ -62,3 +65,9 @@ proptest = "1" rstest = "0.26" tempfile = "3" tower = { version = "0.5", features = ["util"] } +criterion = { version = "0.8", features = ["html_reports"] } +futures = "0.3" + +[[bench]] +name = "workspace_fanout" +harness = false diff --git a/crates/vespertide-lsp/benches/workspace_fanout.rs b/crates/vespertide-lsp/benches/workspace_fanout.rs new file mode 100644 index 00000000..4117c1c3 --- /dev/null +++ b/crates/vespertide-lsp/benches/workspace_fanout.rs @@ -0,0 +1,112 @@ +//! Benchmark the per-`did_change` diagnostics fan-out. +//! +//! A single `did_change` runs `publish` + `publish_related`, so a workspace +//! with `N` open documents triggers `N` calls to `Backend::collect_workspace_tables` +//! (one per published document). Before the workspace-table cache, each of +//! those calls re-deserialized + re-normalized + re-cloned every open model, +//! making one keystroke O(N²). The cache turns the fan-out into 1 build + +//! (N-1) hits — O(N). +//! +//! This bench drives the REAL `Backend` through the public `LanguageServer` +//! API and measures one `did_change` as a function of `N`, so the number it +//! reports is the actual user-visible per-keystroke latency. +//! +//! Run: cargo bench -p vespertide-lsp --bench workspace_fanout +//! Compare: `--save-baseline ` / `--baseline ` (criterion). +//! +//! To measure the pre-cache baseline, make `collect_workspace_tables` bypass +//! the cache (early `return Arc::new(self.build_workspace_tables());`) and +//! re-run with a second `--save-baseline`. + +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use futures::StreamExt; +use serde_json::json; +use tower_lsp_server::ls_types::{ + DidChangeTextDocumentParams, DidOpenTextDocumentParams, InitializeParams, +}; +use tower_lsp_server::{LanguageServer, LspService}; +use vespertide_lsp::Backend; + +/// A valid `TableDef` JSON for document `i`. `variant` toggles a trailing +/// column so the docstore fingerprint actually changes on each measured edit +/// (otherwise every `did_change` would be a pure cache hit and not represent a +/// real keystroke). +fn model_json(i: usize, variant: u8) -> String { + let note = if variant == 0 { + "" + } else { + r#",{"name":"note","type":"text","nullable":true}"# + }; + format!( + r#"{{"name":"t{i}","columns":[{{"name":"id","type":"integer","nullable":false,"primary_key":true}},{{"name":"label","type":"text","nullable":false}}{note}]}}"# + ) +} + +fn did_open(i: usize) -> DidOpenTextDocumentParams { + serde_json::from_value(json!({ + "textDocument": { + "uri": format!("file:///bench/t{i}.json"), + "languageId": "json", + "version": 1, + "text": model_json(i, 0), + } + })) + .expect("did_open params") +} + +fn did_change(version: i32, variant: u8) -> DidChangeTextDocumentParams { + serde_json::from_value(json!({ + "textDocument": { "uri": "file:///bench/t0.json", "version": version }, + "contentChanges": [{ "text": model_json(0, variant) }], + })) + .expect("did_change params") +} + +fn bench_did_change_fanout(c: &mut Criterion) { + // Current-thread runtime: the bounded client `channel(1)` drainer then runs + // cooperatively during each `block_on` await instead of on a separate + // worker thread, eliminating cross-thread wakeup latency (~ms on Windows + // when the worker parks) that otherwise contaminates the measurement. + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("tokio runtime"); + let mut group = c.benchmark_group("did_change_fanout"); + + for &n in &[5usize, 10, 25, 50, 100] { + let (service, socket) = LspService::new(Backend::new); + // Drain server→client messages: the client channel is bounded + // (mpsc::channel(1)), so without a consumer `publish_diagnostics` + // would block once full and the fan-out would deadlock. + rt.spawn(async move { + let mut socket = socket; + while socket.next().await.is_some() {} + }); + let backend = service.inner(); + + rt.block_on(async { + let init: InitializeParams = + serde_json::from_value(json!({ "capabilities": {} })).expect("init params"); + let _ = backend.initialize(init).await; + for i in 0..n { + backend.did_open(did_open(i)).await; + } + }); + + let mut version = 2i32; + let mut variant = 0u8; + group.bench_with_input(BenchmarkId::from_parameter(n), &n, |b, _| { + b.iter(|| { + variant ^= 1; + let params = did_change(version, variant); + version += 1; + rt.block_on(backend.did_change(params)); + }); + }); + } + + group.finish(); +} + +criterion_group!(benches, bench_did_change_fanout); +criterion_main!(benches); diff --git a/crates/vespertide-lsp/src/backend/mod.rs b/crates/vespertide-lsp/src/backend/mod.rs index be73afa8..06c0e2f2 100644 --- a/crates/vespertide-lsp/src/backend/mod.rs +++ b/crates/vespertide-lsp/src/backend/mod.rs @@ -14,7 +14,7 @@ use std::collections::BTreeSet; use std::path::{Path, PathBuf}; use std::str::FromStr; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use tower_lsp_server::jsonrpc::Result; use tower_lsp_server::ls_types::{ @@ -62,8 +62,20 @@ pub struct Backend { pub workspace_tables: Arc, /// Drift loader cache reused across did_change-triggered refreshes. pub drift_cache: Arc, + /// Memoized result of [`Self::collect_workspace_tables`], keyed on + /// `(docstore_fingerprint, workspace_tables_generation)`. A single + /// `did_change` fans out diagnostics to every open document + /// (`publish` + `publish_related`), so without this each keystroke would + /// re-deserialize + re-normalize + re-clone EVERY open model once per open + /// document — O(N²) per keystroke. The fan-out shares one docstore state, + /// so all but the first call hit this cache, collapsing it to O(N). + workspace_tables_cache: Mutex>, } +/// `(docstore_fingerprint, disk_generation, value)` — see +/// [`Backend::workspace_tables_cache`]. +type WorkspaceTablesCacheEntry = (u64, u64, Arc>); + impl Backend { /// Construct a new backend bound to the given LSP [`Client`]. /// @@ -77,6 +89,7 @@ impl Backend { index: Arc::new(WorkspaceIndex::new()), workspace_tables: Arc::new(WorkspaceTables::new()), drift_cache: Arc::new(DriftCache::new()), + workspace_tables_cache: Mutex::new(None), } } @@ -116,7 +129,63 @@ impl Backend { } } - fn collect_workspace_tables(&self) -> Vec { + fn collect_workspace_tables(&self) -> Arc> { + // Sample the cache key: open-document fingerprint + disk-table + // generation. `compute_lsp_diagnostics` later reads the current + // document text SEPARATELY, so to avoid serving a workspace snapshot + // that is inconsistent with that read under concurrent handlers, every + // cache decision below is gated on the state being UNCHANGED across the + // operation (`workspace_state_unchanged`). This keeps the cache's + // consistency window equal to the pre-cache two-read window while still + // collapsing the per-`did_change` `publish` + `publish_related` fan-out + // (which shares one docstore state) from O(N²) to O(N). + let disk_generation = self.workspace_tables.generation(); + let fingerprint = crate::cache::docstore_fingerprint(self.store.as_ref()); + + let cached = { + let cache = self.workspace_tables_cache.lock().expect( + "workspace_tables_cache lock poisoned — invariant: no panic while holding lock", + ); + match cache.as_ref() { + Some((fp, generation, value)) + if *fp == fingerprint && *generation == disk_generation => + { + Some(Arc::clone(value)) + } + _ => None, + } + }; + if let Some(value) = cached + && self.workspace_state_unchanged(fingerprint, disk_generation) + { + // Re-validated: no concurrent handler moved the workspace between + // sampling the key and now, so this snapshot is still current. + return value; + } + + let built = Arc::new(self.build_workspace_tables()); + // Only memoize when the sampled state is still current. If a concurrent + // mutation raced the build, caching it could later serve a stale + // snapshot, so skip the store and let the next call rebuild. + if self.workspace_state_unchanged(fingerprint, disk_generation) { + *self.workspace_tables_cache.lock().expect( + "workspace_tables_cache lock poisoned — invariant: no panic while holding lock", + ) = Some((fingerprint, disk_generation, Arc::clone(&built))); + } + built + } + + /// `true` when the open-document fingerprint and disk-table generation still + /// match the sampled `(fingerprint, disk_generation)` — i.e. no concurrent + /// handler changed the workspace since the caller sampled the cache key. + /// `generation` is read first (cheap atomic) so a disk refresh short-circuits + /// before recomputing the document fingerprint. + fn workspace_state_unchanged(&self, fingerprint: u64, disk_generation: u64) -> bool { + self.workspace_tables.generation() == disk_generation + && crate::cache::docstore_fingerprint(self.store.as_ref()) == fingerprint + } + + fn build_workspace_tables(&self) -> Vec { let mut workspace = Vec::new(); // Dedup by NORMALIZED FILESYSTEM PATH so a file that is both open // in the editor and present on disk is registered only once. diff --git a/crates/vespertide-lsp/src/backend/tests/lifecycle.rs b/crates/vespertide-lsp/src/backend/tests/lifecycle.rs index 1ec598d1..37da8bcd 100644 --- a/crates/vespertide-lsp/src/backend/tests/lifecycle.rs +++ b/crates/vespertide-lsp/src/backend/tests/lifecycle.rs @@ -180,6 +180,52 @@ fn collect_workspace_tables_prefers_open_document_over_disk_duplicate() { }); } +#[test] +fn collect_workspace_tables_caches_until_state_changes() { + block_on_with_trace(async { + let fixture = workspace_fixture(); + let (service, _socket) = make_service(); + let backend = service.inner(); + backend + .initialize(params::(json!({ + "capabilities": {}, + "workspaceFolders": [{ "uri": fixture.root_uri.as_str(), "name": "fixture" }], + }))) + .await + .unwrap(); + open_doc(backend, &fixture.user_uri, "json", USER_MODEL).await; + + // Identical docstore + disk generation: the per-keystroke fan-out + // (publish + publish_related call this once per open doc) must reuse + // one cached Arc instead of rebuilding every open model each time. + let first = backend.collect_workspace_tables(); + let second = backend.collect_workspace_tables(); + assert!( + std::sync::Arc::ptr_eq(&first, &second), + "unchanged workspace must return the cached Arc (no rebuild)" + ); + + // Editing the document changes the docstore fingerprint, so the cache + // must invalidate and the rebuilt tables must reflect the new content. + backend + .did_change(params(json!({ + "textDocument": { "uri": fixture.user_uri.as_str(), "version": 2 }, + "contentChanges": [{ "text": CHANGED_USER_MODEL }], + }))) + .await; + let third = backend.collect_workspace_tables(); + assert!( + !std::sync::Arc::ptr_eq(&first, &third), + "a document edit must invalidate the workspace-tables cache" + ); + assert!( + third.iter().any(|entry| entry.table.name == "user" + && entry.table.columns.iter().any(|col| col.name == "nickname")), + "post-change tables must reflect the edited document, not a stale cache" + ); + }); +} + #[test] fn workspace_table_helpers_cover_degenerate_open_and_disk_state() { let pool = crate::parser::ParserPool::new(); diff --git a/crates/vespertide-lsp/src/cache.rs b/crates/vespertide-lsp/src/cache.rs index c2c5ec85..957a5eb1 100644 --- a/crates/vespertide-lsp/src/cache.rs +++ b/crates/vespertide-lsp/src/cache.rs @@ -12,11 +12,11 @@ //! ``` //! where `SymbolKey: Hash + Eq + Copy`. -use std::collections::hash_map::DefaultHasher; use std::collections::{HashMap, VecDeque}; use std::hash::{Hash, Hasher}; use std::sync::{Arc, Mutex, OnceLock}; +use rustc_hash::FxHasher; use tower_lsp_server::ls_types::Uri; use crate::store::DocumentStore; @@ -110,7 +110,11 @@ where } pub(crate) fn hash_text(text: &str) -> u64 { - let mut h = DefaultHasher::new(); + // FxHasher (not SipHash): cache keys are in-memory only and tolerate the + // 64-bit-hash + length collision model by design (see module docs); FxHash + // is markedly faster at digesting whole-document byte streams on the + // per-keystroke cache path. + let mut h = FxHasher::default(); h.write(text.as_bytes()); h.finish() } @@ -188,7 +192,9 @@ fn cached_docstore_fingerprint(docs: &DocumentStore, docs_addr: usize) -> Option } fn compute_docstore_fingerprint(docs: &DocumentStore) -> (u64, Vec) { - let mut h = DefaultHasher::new(); + // FxHasher to match `hash_text`'s digest model and speed up the workspace + // fingerprint over every open document's text on the hot cache path. + let mut h = FxHasher::default(); let mut entries = Vec::with_capacity(docs.len()); docs.for_each(|uri, state| { let text = state.text(); diff --git a/crates/vespertide-lsp/src/diagnostics/validation/mod.rs b/crates/vespertide-lsp/src/diagnostics/validation/mod.rs index 5014d98c..7d4eb6f1 100644 --- a/crates/vespertide-lsp/src/diagnostics/validation/mod.rs +++ b/crates/vespertide-lsp/src/diagnostics/validation/mod.rs @@ -22,6 +22,7 @@ pub(super) use visitors::{ }; /// Parsed table plus source context for workspace-wide validation. +#[derive(Debug)] pub struct WorkspaceTable { /// URI that owns this table definition. pub uri: Uri, diff --git a/crates/vespertide-lsp/src/references/search.rs b/crates/vespertide-lsp/src/references/search.rs index de2efccd..7e5b36ab 100644 --- a/crates/vespertide-lsp/src/references/search.rs +++ b/crates/vespertide-lsp/src/references/search.rs @@ -39,32 +39,33 @@ pub(super) fn find_all( ); } + // Snapshot the open URIs once and reuse it for both the open-document + // scan and the disk-file dedup set below (was queried twice). + let open_uris = docs.open_uris(); + // Every OTHER open document. - let other_uris: Vec = docs - .open_uris() - .into_iter() - .filter(|uri| uri != current_uri) - .collect(); - for uri in other_uris { - docs.with_doc(&uri, |text, tree| { + for uri in open_uris.iter().filter(|uri| *uri != current_uri) { + docs.with_doc(uri, |text, tree| { if let Some(tree) = tree { - collect_in_document(symbol, &uri, text, tree, include_declaration, &mut out); + collect_in_document(symbol, uri, text, tree, include_declaration, &mut out); } }); } // Disk-only models that the editor has not opened. if let Some(disk) = disk_tables { - let open_paths: std::collections::BTreeSet = docs - .open_uris() - .into_iter() - .filter_map(|uri| crate::position::uri_to_path(&uri)) + let open_paths: std::collections::BTreeSet = open_uris + .iter() + .filter_map(crate::position::uri_to_path) .collect(); + // One parser pool for the whole disk sweep instead of constructing a + // fresh tree-sitter parser per file inside `scan_disk_file`. + let pool = crate::parser::ParserPool::new(); for name in disk.names() { if let Some(path) = disk.model_path(&name) { // Already scanned via the open document above. if !open_paths.contains(&path) { - scan_disk_file(symbol, &path, include_declaration, &mut out); + scan_disk_file(symbol, &path, &pool, include_declaration, &mut out); } } } @@ -89,6 +90,7 @@ pub(super) fn find_all( fn scan_disk_file( symbol: &ReferenceSymbol, path: &std::path::Path, + pool: &crate::parser::ParserPool, include_declaration: bool, out: &mut Vec, ) { @@ -98,12 +100,11 @@ fn scan_disk_file( Some("yaml" | "yml") => Some(crate::parser::DocumentFormat::Yaml), _ => None, }; - if let Some(format) = format { - let pool = crate::parser::ParserPool::new(); - if let Some(tree) = pool.parse(&text, format) { - let uri = path_to_uri(path); - collect_in_document(symbol, &uri, &text, &tree, include_declaration, out); - } + if let Some(format) = format + && let Some(tree) = pool.parse(&text, format) + { + let uri = path_to_uri(path); + collect_in_document(symbol, &uri, &text, &tree, include_declaration, out); } } } @@ -546,11 +547,13 @@ mod tests { std::fs::write(&txt, "name: user\ncolumns: []\n").unwrap(); let mut out = Vec::new(); + let pool = crate::parser::ParserPool::new(); scan_disk_file( &ReferenceSymbol::Table { name: "user".to_string(), }, &yaml, + &pool, true, &mut out, ); @@ -559,6 +562,7 @@ mod tests { name: "account".to_string(), }, &yml, + &pool, true, &mut out, ); @@ -568,6 +572,7 @@ mod tests { name: "user".to_string(), }, &txt, + &pool, true, &mut out, ); diff --git a/crates/vespertide-lsp/src/workspace_tables.rs b/crates/vespertide-lsp/src/workspace_tables.rs index 9acdc82d..2742d3c5 100644 --- a/crates/vespertide-lsp/src/workspace_tables.rs +++ b/crates/vespertide-lsp/src/workspace_tables.rs @@ -8,6 +8,7 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, RwLock}; use std::time::SystemTime; use tree_sitter::Tree; @@ -18,6 +19,11 @@ use vespertide_core::TableDef; #[derive(Debug, Default)] pub struct WorkspaceTables { inner: RwLock, + /// Monotonic counter bumped every time `inner`'s table set is replaced by + /// `refresh()`. Consumers (the diagnostics workspace-table cache) read this + /// as part of their cache key to detect disk-side changes in O(1) without + /// hashing the whole table set. See `generation()`. + generation: AtomicU64, } #[derive(Debug, Default)] @@ -75,6 +81,10 @@ impl WorkspaceTables { path_by_name, tree_cache: BTreeMap::new(), }; + // Bump AFTER the write completes so a concurrent reader that + // observes the new generation is guaranteed (via the lock) to + // see the new table set. + self.generation.fetch_add(1, Ordering::Relaxed); count > 0 } else { @@ -84,10 +94,19 @@ impl WorkspaceTables { *self.inner.write().expect( "workspace_tables lock poisoned — invariant: no panic while holding lock", ) = Inner::default(); + self.generation.fetch_add(1, Ordering::Relaxed); false } } + /// Monotonic generation counter, incremented whenever `refresh()` replaces + /// the in-memory table set. Cheap (`Relaxed` atomic load) cache-key input + /// for consumers that must invalidate when disk-discovered tables change. + #[must_use] + pub fn generation(&self) -> u64 { + self.generation.load(Ordering::Relaxed) + } + pub fn get(&self, name: &str) -> Option { self.inner .read() diff --git a/crates/vespertide-macro/Cargo.toml b/crates/vespertide-macro/Cargo.toml index eb6b201a..959f829b 100644 --- a/crates/vespertide-macro/Cargo.toml +++ b/crates/vespertide-macro/Cargo.toml @@ -20,13 +20,20 @@ vespertide-config = { workspace = true } vespertide-loader = { workspace = true } vespertide-query = { workspace = true } vespertide-planner = { workspace = true } -syn = { version = "2.0", features = ["parsing", "proc-macro", "full"] } +# Only LitStr/Ident/Token/parse are used (no Expr/Item/Stmt), so the heavy +# "full" syntax-tree feature is intentionally omitted to cut macro-crate build time. +syn = { version = "2.0", features = ["parsing", "proc-macro"] } proc-macro2 = "1.0" quote = "1" proc-macro-crate = "3.5.0" [dev-dependencies] tempfile = "3" +# `runtime-macros` (test-only coverage driver) under-declares its syn features +# and needs the full syntax tree + visitor. Supplying them here keeps the +# production `syn` dependency lean (parsing+proc-macro only) while satisfying +# the dev/test build via feature unification. +syn = { version = "2.0", features = ["full", "visit"] } runtime-macros = "1.1" trybuild = { version = "1.0.116", features = ["diff"] } # `serial_test::serial` for tests that mutate the process-global diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs index ae723901..58158d1c 100644 --- a/crates/vespertide-query/src/sql/helpers.rs +++ b/crates/vespertide-query/src/sql/helpers.rs @@ -696,11 +696,16 @@ pub fn quote_ident(name: &str, backend: DatabaseBackend) -> String { /// Quote a list of identifiers and join them with comma. #[must_use] pub fn quote_idents>(names: &[T], backend: DatabaseBackend) -> String { - names - .iter() - .map(|n| quote_ident(n.as_ref(), backend)) - .collect::>() - .join(", ") + // Write straight into one buffer instead of collecting an intermediate + // `Vec` and joining — same output, one fewer allocation per call. + let mut out = String::new(); + for (i, n) in names.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + out.push_str("e_ident(n.as_ref(), backend)); + } + out } #[cfg(test)] From 0fed25dca4fdcf3bdcec349b0225c92fc93efd20 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 25 Jun 2026 17:21:39 +0900 Subject: [PATCH 002/109] retry-now#0001: hoist &[ColumnName] join into vespertide-core helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `vespertide_core::schema::names::join_column_names(&[ColumnName], &str)` with a single-buffer push loop (no intermediate Vec), mirroring the recent `vespertide_query::sql::helpers::quote_idents` perf pattern. Replaces 4 open-coded `cols.iter().map(...).collect::>().join(sep)` sites in `vespertide-planner/src/validate/`: - `constraint_drops::constraint_label` (6 calls) — deletes the file-private `join_columns` helper and the now-redundant `join_columns_empty_returns_empty_string` rstest; the empty-slice contract migrates to a unit test on the shared helper. - `foreign_keys::build_suggested_index_name` (1 call, `_` separator). - `constraint_type_changes::find_primary_key_removals` (1 call) — also drops the per-element `ColumnName::to_string` allocation by switching to `as_str()` via the helper. - `constraint_type_changes::render_fk_hint` (1 call inside the unnamed-FK label branch). Output strings byte-identical (locked by existing `constraint_label_formats_integrity_constraints` rstest matrix and the `find_constraint_type_changes` / `find_primary_key_removals` case_01..case_12 tests). No struct / enum / serde-shape change, schema-gen drift remains 0, public API gains one new pub fn (additive). --- crates/vespertide-core/src/schema/names.rs | 62 +++++++++++++++++++ .../src/validate/constraint_drops.rs | 40 +++++------- .../src/validate/constraint_type_changes.rs | 25 +++----- .../src/validate/foreign_keys.rs | 8 +-- 4 files changed, 86 insertions(+), 49 deletions(-) diff --git a/crates/vespertide-core/src/schema/names.rs b/crates/vespertide-core/src/schema/names.rs index 28a3ce86..f7069074 100644 --- a/crates/vespertide-core/src/schema/names.rs +++ b/crates/vespertide-core/src/schema/names.rs @@ -182,6 +182,34 @@ impl_name_newtype!(TableName); impl_name_newtype!(ColumnName); impl_name_newtype!(IndexName); +/// Join a slice of [`ColumnName`]s with a separator using a single buffer +/// — no intermediate `Vec` allocation. +/// +/// Mirrors the buffer-push pattern used by +/// `vespertide_query::sql::helpers::quote_idents`. Folds four open-coded +/// `cols.iter().map(...).collect::>().join(sep)` callsites in +/// `vespertide-planner::validate::*` into a single shared helper. +/// +/// ```rust +/// use vespertide_core::schema::names::{ColumnName, join_column_names}; +/// +/// let cols: Vec = vec!["a".into(), "b".into(), "c".into()]; +/// assert_eq!(join_column_names(&cols, ", "), "a, b, c"); +/// assert_eq!(join_column_names(&cols, "_"), "a_b_c"); +/// assert_eq!(join_column_names(&[], ", "), ""); +/// ``` +#[must_use] +pub fn join_column_names(columns: &[ColumnName], separator: &str) -> String { + let mut out = String::new(); + for (i, c) in columns.iter().enumerate() { + if i > 0 { + out.push_str(separator); + } + out.push_str(c.as_str()); + } + out +} + #[cfg(test)] mod tests { //! Coverage-closure tests for the `impl_name_newtype!` expansions. @@ -235,4 +263,38 @@ mod tests { let s: String = String::from(name); assert_eq!(s, "ix_orders__id"); } + + #[test] + fn join_column_names_empty_returns_empty_string() { + // Empty-slice path: `for ... in []` never runs, returns the + // pristine `String::new()`. Locks the empty-input contract that + // the deleted `validate::constraint_drops::join_columns` helper + // used to assert directly. + let cols: Vec = vec![]; + assert_eq!(join_column_names(&cols, ", "), ""); + assert_eq!(join_column_names(&cols, "_"), ""); + } + + #[test] + fn join_column_names_comma_separator() { + // The validate-diagnostic site shape: `"a, b, c"`. Single-column + // case must skip the separator entirely (no leading `", "`). + let cols: Vec = vec!["a".into(), "b".into(), "c".into()]; + assert_eq!(join_column_names(&cols, ", "), "a, b, c"); + + let single: Vec = vec!["only".into()]; + assert_eq!(join_column_names(&single, ", "), "only"); + } + + #[test] + fn join_column_names_underscore_separator() { + // The `ix_{table}__{cols}` index-name builder shape: `"fk_id"`. + // Locks the separator variation against the validate + // `build_suggested_index_name` callsite. + let cols: Vec = vec!["fk".into(), "id".into()]; + assert_eq!(join_column_names(&cols, "_"), "fk_id"); + + let composite: Vec = vec!["tenant_id".into(), "user_id".into()]; + assert_eq!(join_column_names(&composite, "_"), "tenant_id_user_id"); + } } diff --git a/crates/vespertide-planner/src/validate/constraint_drops.rs b/crates/vespertide-planner/src/validate/constraint_drops.rs index 9ab7f5fb..e7eab2a2 100644 --- a/crates/vespertide-planner/src/validate/constraint_drops.rs +++ b/crates/vespertide-planner/src/validate/constraint_drops.rs @@ -16,7 +16,10 @@ //! `ReplaceConstraint` is the explicit "I am swapping this constraint for //! another" action and is therefore considered safe. -use vespertide_core::{ConstraintKind, MigrationAction, MigrationPlan, TableConstraint}; +use vespertide_core::{ + ConstraintKind, MigrationAction, MigrationPlan, TableConstraint, + schema::names::join_column_names, +}; /// One `RemoveConstraint` action that drops an integrity-preserving /// constraint with no `ReplaceConstraint` counterpart in the same plan. @@ -91,11 +94,11 @@ fn warning_for_action(idx: usize, action: &MigrationAction) -> Option String { match constraint { TableConstraint::PrimaryKey { columns, .. } => { - format!("PRIMARY KEY ({})", join_columns(columns)) + format!("PRIMARY KEY ({})", join_column_names(columns, ", ")) } TableConstraint::Unique { name, columns, .. } => match name { - Some(n) => format!("{n} UNIQUE ({})", join_columns(columns)), - None => format!("UNIQUE ({})", join_columns(columns)), + Some(n) => format!("{n} UNIQUE ({})", join_column_names(columns, ", ")), + None => format!("UNIQUE ({})", join_column_names(columns, ", ")), }, TableConstraint::ForeignKey { name, @@ -103,13 +106,16 @@ fn constraint_label(constraint: &TableConstraint) -> String { ref_table, .. } => match name { - Some(n) => format!("{n} FK ({}) -> {ref_table}", join_columns(columns)), - None => format!("FK ({}) -> {ref_table}", join_columns(columns)), + Some(n) => format!( + "{n} FK ({}) -> {ref_table}", + join_column_names(columns, ", ") + ), + None => format!("FK ({}) -> {ref_table}", join_column_names(columns, ", ")), }, TableConstraint::Check { name, expr, .. } => format!("{name} CHECK ({expr})"), TableConstraint::Index { name, columns } => match name { - Some(n) => format!("{n} INDEX ({})", join_columns(columns)), - None => format!("INDEX ({})", join_columns(columns)), + Some(n) => format!("{n} INDEX ({})", join_column_names(columns, ", ")), + None => format!("INDEX ({})", join_column_names(columns, ", ")), }, // reason: unreachable - exhaustive over current TableConstraint variants; fallback required only for #[non_exhaustive] future variants #[cfg(not(tarpaulin_include))] @@ -117,14 +123,6 @@ fn constraint_label(constraint: &TableConstraint) -> String { } } -fn join_columns(columns: &[vespertide_core::ColumnName]) -> String { - columns - .iter() - .map(vespertide_core::ColumnName::as_str) - .collect::>() - .join(", ") -} - #[cfg(test)] mod private_helpers { //! Inline tests for the private `constraint_label` helper. The public @@ -155,16 +153,6 @@ mod private_helpers { assert_eq!(constraint_label(&c), "INDEX (email, tenant_id)"); } - /// `join_columns` over an empty slice returns the empty string - /// (covers the `Vec<_>::join` empty-input arm via every label that - /// uses zero columns, but the helper has its own trivial contract - /// worth pinning). - #[rstest] - fn join_columns_empty_returns_empty_string() { - let cols: Vec = vec![]; - assert_eq!(join_columns(&cols), ""); - } - #[rstest] #[case::primary_key(TableConstraint::PrimaryKey { auto_increment: false, columns: vec!["id".into()], strategy: vespertide_core::PrimaryKeyAdditionStrategy::default() }, "PRIMARY KEY (id)")] #[case::unique_named(TableConstraint::Unique { name: Some("uq_users__email".into()), columns: vec!["email".into()], strategy: vespertide_core::UniqueConstraintStrategy::default() }, "uq_users__email UNIQUE (email)")] diff --git a/crates/vespertide-planner/src/validate/constraint_type_changes.rs b/crates/vespertide-planner/src/validate/constraint_type_changes.rs index 00eeed18..c0283bb7 100644 --- a/crates/vespertide-planner/src/validate/constraint_type_changes.rs +++ b/crates/vespertide-planner/src/validate/constraint_type_changes.rs @@ -29,7 +29,10 @@ use std::collections::{BTreeMap, BTreeSet}; -use vespertide_core::{ColumnName, MigrationAction, MigrationPlan, TableConstraint, TableDef}; +use vespertide_core::{ + ColumnName, MigrationAction, MigrationPlan, TableConstraint, TableDef, + schema::names::join_column_names, +}; use crate::error::PlannerError; @@ -146,14 +149,9 @@ pub fn find_primary_key_removals( if tables_dropped.contains(table_str) || tables_gaining_pk.contains(table_str) { continue; } - let cols = columns - .iter() - .map(ColumnName::to_string) - .collect::>() - .join(", "); out.push(PlannerError::PrimaryKeyRemovedWithoutReplacement { table: table_str.to_string(), - columns: cols, + columns: join_column_names(columns, ", "), }); } out @@ -221,16 +219,9 @@ fn render_fk_hint(baseline: &[TableDef], target_table: &str, target_columns: &[S if ref_set != target_set { continue; } - let label = name.clone().unwrap_or_else(|| { - format!( - "({})", - columns - .iter() - .map(ColumnName::as_str) - .collect::>() - .join(", ") - ) - }); + let label = name + .clone() + .unwrap_or_else(|| format!("({})", join_column_names(columns, ", "))); hits.push(format!("{}.{}", table.name.as_str(), label)); } } diff --git a/crates/vespertide-planner/src/validate/foreign_keys.rs b/crates/vespertide-planner/src/validate/foreign_keys.rs index 4b7e24bd..8f8d933c 100644 --- a/crates/vespertide-planner/src/validate/foreign_keys.rs +++ b/crates/vespertide-planner/src/validate/foreign_keys.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, HashSet}; -use vespertide_core::{ColumnName, TableConstraint, TableDef}; +use vespertide_core::{ColumnName, TableConstraint, TableDef, schema::names::join_column_names}; use crate::error::PlannerError; @@ -170,10 +170,6 @@ fn has_covering_index(covering_prefixes: &[&[ColumnName]], fk_columns: &[ColumnN } fn build_suggested_index_name(table: &str, columns: &[ColumnName]) -> String { - let cols_joined = columns - .iter() - .map(ColumnName::as_str) - .collect::>() - .join("_"); + let cols_joined = join_column_names(columns, "_"); format!("ix_{table}__{cols_joined}") } From d09e41c26bb3c34b74d86d19065d76d1e63e02c7 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 25 Jun 2026 19:15:53 +0900 Subject: [PATCH 003/109] retry-now#0001: hoist EnumValues::to_sql_values().join into buffer-push helper Add EnumValues::sql_values_joined(sep) that formats every variant for CREATE TYPE / CHECK IN (...) directly into one String buffer with no intermediate Vec allocation. Mirrors the existing single-buffer push patterns in quote_idents and join_column_names. Replace three production callsites that did to_sql_values().join(', '): - vespertide-query/sql/helpers.rs:build_create_enum_type_sql (PG CREATE TYPE) - vespertide-query/sql/helpers.rs:build_sqlite_enum_check_clause (SQLite CHECK) - vespertide-query/sql/modify_column_type/direct.rs (PG enum type change) The legacy EnumValues::to_sql_values() stays in place; three core unit tests still exercise it directly. Output is byte-identical: a new rstest case (sql_values_joined_matches_to_sql_values_join, 7 cases) explicitly asserts joined == to_sql_values().join(sep) across String/Integer empty, single, multi, single-quote-escape, and alt-separator inputs. The full exporter snapshot suite re-runs with zero .snap.new files. --- crates/vespertide-core/src/schema/column.rs | 58 +++++++++++++++++++ crates/vespertide-core/src/schema/mod.rs | 44 ++++++++++++++ crates/vespertide-query/src/sql/helpers.rs | 4 +- .../src/sql/modify_column_type/direct.rs | 2 +- 4 files changed, 105 insertions(+), 3 deletions(-) diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs index 2e891967..fe35b010 100644 --- a/crates/vespertide-core/src/schema/column.rs +++ b/crates/vespertide-core/src/schema/column.rs @@ -472,6 +472,64 @@ impl EnumValues { EnumValues::Integer(values) => values.iter().map(|v| v.value.to_string()).collect(), } } + + /// Format every variant for `CREATE TYPE … AS ENUM(...)` / + /// `CHECK (col IN (...))` and join with `separator`, writing into one + /// buffer — no intermediate `Vec` allocation. + /// + /// Mirrors `vespertide_query::sql::helpers::quote_idents` and + /// `vespertide_core::schema::names::join_column_names`. Output is + /// byte-identical to `self.to_sql_values().join(separator)`. + /// + /// ```rust + /// use vespertide_core::{EnumValues, NumValue}; + /// + /// let s = EnumValues::String(vec!["active".into(), "O'Brien".into()]); + /// assert_eq!(s.sql_values_joined(", "), "'active', 'O''Brien'"); + /// + /// let i = EnumValues::Integer(vec![ + /// NumValue { name: "low".into(), value: 0 }, + /// NumValue { name: "high".into(), value: 10 }, + /// ]); + /// assert_eq!(i.sql_values_joined(", "), "0, 10"); + /// + /// let empty = EnumValues::String(vec![]); + /// assert_eq!(empty.sql_values_joined(", "), ""); + /// ``` + #[must_use] + pub fn sql_values_joined(&self, separator: &str) -> String { + use std::fmt::Write as _; + let mut out = String::new(); + match self { + EnumValues::String(values) => { + for (i, s) in values.iter().enumerate() { + if i > 0 { + out.push_str(separator); + } + out.push('\''); + // Inline '' escape — matches the existing + // `format!("'{}'", s.replace('\'', "''"))` byte-for-byte. + for ch in s.chars() { + if ch == '\'' { + out.push('\''); + } + out.push(ch); + } + out.push('\''); + } + } + EnumValues::Integer(values) => { + for (i, v) in values.iter().enumerate() { + if i > 0 { + out.push_str(separator); + } + // i64 Display into String is infallible. + write!(out, "{}", v.value).expect("writing an i64 to a String never fails"); + } + } + } + out + } } impl From> for EnumValues { diff --git a/crates/vespertide-core/src/schema/mod.rs b/crates/vespertide-core/src/schema/mod.rs index f2db4668..2a3a946a 100644 --- a/crates/vespertide-core/src/schema/mod.rs +++ b/crates/vespertide-core/src/schema/mod.rs @@ -310,6 +310,50 @@ mod tests { assert_eq!(vals.to_sql_values(), vec!["0", "10"]); } + #[rstest] + // Matches `to_sql_values().join(sep)` byte-for-byte across every arm. + #[case::string_two_variants( + EnumValues::String(vec!["active".into(), "pending".into()]), + ", ", + "'active', 'pending'", + )] + #[case::string_single_quote_escape( + EnumValues::String(vec!["O'Brien".into(), "'leading".into()]), + ", ", + "'O''Brien', '''leading'", + )] + #[case::integer_two_variants( + EnumValues::Integer(vec![ + NumValue { name: "low".into(), value: 0 }, + NumValue { name: "high".into(), value: 10 }, + ]), + ", ", + "0, 10", + )] + #[case::string_empty(EnumValues::String(vec![]), ", ", "")] + #[case::integer_empty(EnumValues::Integer(vec![]), ", ", "")] + #[case::string_single_variant( + EnumValues::String(vec!["only".into()]), + ", ", + "'only'", + )] + #[case::alt_separator( + EnumValues::String(vec!["a".into(), "b".into(), "c".into()]), + "_", + "'a'_'b'_'c'", + )] + fn sql_values_joined_matches_to_sql_values_join( + #[case] vals: EnumValues, + #[case] separator: &str, + #[case] expected: &str, + ) { + let joined = vals.sql_values_joined(separator); + assert_eq!(joined, expected); + // Equivalence lock: the buffer-push helper must be byte-identical + // to the legacy `Vec` + `join` pipeline. + assert_eq!(joined, vals.to_sql_values().join(separator)); + } + #[test] fn test_enum_values_from_vec_string() { let vals: EnumValues = vec!["a".to_string(), "b".to_string()].into(); diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs index 58158d1c..79da55c7 100644 --- a/crates/vespertide-query/src/sql/helpers.rs +++ b/crates/vespertide-query/src/sql/helpers.rs @@ -450,7 +450,7 @@ pub fn build_create_enum_type_sql( return None; } - let values_sql = values.to_sql_values().join(", "); + let values_sql = values.sql_values_joined(", "); // Generate unique type name with table prefix let type_name = build_enum_type_name(table, name); @@ -513,7 +513,7 @@ pub fn build_sqlite_enum_check_clause( ) -> Option { if let ColumnType::Complex(ComplexColumnType::Enum { values, .. }) = column_type { let name = build_check_constraint_name(table, column); - let values_sql = values.to_sql_values().join(", "); + let values_sql = values.sql_values_joined(", "); let name = quote_ident(&name, DatabaseBackend::Sqlite); let column = quote_ident(column, DatabaseBackend::Sqlite); Some(format!( diff --git a/crates/vespertide-query/src/sql/modify_column_type/direct.rs b/crates/vespertide-query/src/sql/modify_column_type/direct.rs index 7d95df65..fcfcba28 100644 --- a/crates/vespertide-query/src/sql/modify_column_type/direct.rs +++ b/crates/vespertide-query/src/sql/modify_column_type/direct.rs @@ -107,7 +107,7 @@ fn build_postgres_enum_migration(queries: &mut Vec, context: &Direct // 1. CREATE TYPE target_type AS ENUM (new values). let column_default = column_default(context); - let create_values = new_values.to_sql_values().join(", "); + let create_values = new_values.sql_values_joined(", "); let quoted_target_type = quote_ident(&target_type_name, DatabaseBackend::Postgres); let quoted_table = quote_ident(context.table, DatabaseBackend::Postgres); let quoted_column = quote_ident(context.column, DatabaseBackend::Postgres); From 3a637873803e37f646d466a6bf800525284aff20 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 25 Jun 2026 20:13:28 +0900 Subject: [PATCH 004/109] retry-now#0002: extract escape_sql_string_literal helper to vespertide-core Adds a single Cow-based escape helper in vespertide-core::sql_escape (zero alloc when no single quote present) and routes 5 production callsites through it: - vespertide-core::schema::column::EnumValues::to_sql_values (CREATE TYPE ENUM string-value emission) - vespertide-core::schema::column::EnumValues::sql_values_joined (folds the inline `for ch in s.chars() { if ch == '\'' ... }` loop) - vespertide-cli::commands::revision::prompts::narrowing::quote_value_for_target (interactive set_to_value SQL literal quoting) - vespertide-query::sql::modify_column_type::build_pg_alter_with_timezone (PG ALTER COLUMN TYPE ... USING col AT TIME ZONE 'tz') - vespertide-query::sql::modify_column_comment (PG COMMENT ON COLUMN + MySQL MODIFY COLUMN COMMENT branches) Pure refactor. Helper output is byte-identical to the previous `s.replace('\'', "''")` -- locked by 11 inline unit tests in sql_escape.rs (empty/clean/leading/trailing/only/multiple/consecutive /unicode-borrow/unicode-own/naive-replace-oracle), the existing EnumValues::sql_values_joined_matches_to_sql_values_join equivalence case in vespertide-core, plus every cross-backend snapshot triple in vespertide-query and the 4-ORM matrix in vespertide-exporter. Single audit point for future SQL-literal hardening (NUL bytes, backslash for MySQL ANSI_QUOTES, etc.) instead of 5 re-applications. Removes per-call allocation when the input contains no single quote (the overwhelmingly common case for column comments, timezone literals, and CLI prompt inputs). --- .../commands/revision/prompts/narrowing.rs | 4 +- crates/vespertide-core/src/lib.rs | 2 + crates/vespertide-core/src/schema/column.rs | 14 +- crates/vespertide-core/src/sql_escape.rs | 130 ++++++++++++++++++ .../src/sql/modify_column_comment.rs | 4 +- .../src/sql/modify_column_type/mod.rs | 2 +- 6 files changed, 142 insertions(+), 14 deletions(-) create mode 100644 crates/vespertide-core/src/sql_escape.rs diff --git a/crates/vespertide-cli/src/commands/revision/prompts/narrowing.rs b/crates/vespertide-cli/src/commands/revision/prompts/narrowing.rs index 432c21d2..b661c8b7 100644 --- a/crates/vespertide-cli/src/commands/revision/prompts/narrowing.rs +++ b/crates/vespertide-cli/src/commands/revision/prompts/narrowing.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use colored::Colorize; use dialoguer::{Input, Select}; -use vespertide_core::{MigrationPlan, NarrowingStrategy}; +use vespertide_core::{MigrationPlan, NarrowingStrategy, escape_sql_string_literal}; use vespertide_planner::{NarrowingKind, TypeNarrowingWarning}; /// Strategies that can be safely emitted by the SQL generator for a given @@ -172,7 +172,7 @@ fn quote_value_for_target(raw: &str, kind: &NarrowingKind) -> String { if raw.starts_with('\'') && raw.ends_with('\'') { return raw.to_string(); } - format!("'{}'", raw.replace('\'', "''")) + format!("'{}'", escape_sql_string_literal(raw)) } /// Apply user-selected strategies onto the plan in place. Each warning's diff --git a/crates/vespertide-core/src/lib.rs b/crates/vespertide-core/src/lib.rs index d0818a42..7f967160 100644 --- a/crates/vespertide-core/src/lib.rs +++ b/crates/vespertide-core/src/lib.rs @@ -9,6 +9,7 @@ pub mod action; pub mod arbitrary; pub mod migration; pub mod schema; +pub mod sql_escape; pub use action::{MigrationAction, MigrationPlan, NarrowingStrategy}; pub use migration::{MigrationError, MigrationOptions}; @@ -18,3 +19,4 @@ pub use schema::{ PrimaryKeyAdditionStrategy, ReferenceAction, SimpleColumnType, StrOrBoolOrArray, StringOrBool, TableConstraint, TableDef, TableName, TableValidationError, UniqueConstraintStrategy, }; +pub use sql_escape::escape_sql_string_literal; diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs index fe35b010..516cdc03 100644 --- a/crates/vespertide-core/src/schema/column.rs +++ b/crates/vespertide-core/src/schema/column.rs @@ -467,7 +467,7 @@ impl EnumValues { match self { EnumValues::String(values) => values .iter() - .map(|s| format!("'{}'", s.replace('\'', "''"))) + .map(|s| format!("'{}'", crate::escape_sql_string_literal(s))) .collect(), EnumValues::Integer(values) => values.iter().map(|v| v.value.to_string()).collect(), } @@ -507,14 +507,10 @@ impl EnumValues { out.push_str(separator); } out.push('\''); - // Inline '' escape — matches the existing - // `format!("'{}'", s.replace('\'', "''"))` byte-for-byte. - for ch in s.chars() { - if ch == '\'' { - out.push('\''); - } - out.push(ch); - } + // Centralized '' escape — matches the existing + // `format!("'{}'", s.replace('\'', "''"))` byte-for-byte, + // and borrows when no quote is present (zero alloc). + out.push_str(&crate::escape_sql_string_literal(s)); out.push('\''); } } diff --git a/crates/vespertide-core/src/sql_escape.rs b/crates/vespertide-core/src/sql_escape.rs new file mode 100644 index 00000000..70b45516 --- /dev/null +++ b/crates/vespertide-core/src/sql_escape.rs @@ -0,0 +1,130 @@ +//! SQL string-literal escaping helpers, shared across crates. +//! +//! Mirrors how production SQL emitters handle single-quote escape in +//! string literals. Centralizing the helper means future hardening (NUL +//! byte filtering, backslash escapes for MySQL `ANSI_QUOTES`, …) lands in +//! one place instead of being re-applied across 5+ callsites in +//! `vespertide-cli`, `vespertide-core`, and `vespertide-query`. + +use std::borrow::Cow; + +/// Escape SQL single quotes by doubling them — the canonical, portable +/// way to embed a string inside a `'...'` literal across Postgres / +/// `MySQL` / `SQLite`. Returns a borrowed slice when no escape is required +/// (zero allocation), otherwise an owned `String`. +/// +/// ```rust +/// use std::borrow::Cow; +/// use vespertide_core::escape_sql_string_literal; +/// +/// assert!(matches!(escape_sql_string_literal("hello"), Cow::Borrowed(_))); +/// assert_eq!(escape_sql_string_literal("O'Brien"), "O''Brien"); +/// assert_eq!(escape_sql_string_literal("'leading"), "''leading"); +/// assert_eq!(escape_sql_string_literal(""), ""); +/// ``` +#[must_use] +pub fn escape_sql_string_literal(s: &str) -> Cow<'_, str> { + if s.contains('\'') { + Cow::Owned(s.replace('\'', "''")) + } else { + Cow::Borrowed(s) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_string_borrows() { + let out = escape_sql_string_literal(""); + assert!(matches!(out, Cow::Borrowed(_))); + assert_eq!(out, ""); + } + + #[test] + fn clean_string_borrows() { + let out = escape_sql_string_literal("hello world"); + assert!(matches!(out, Cow::Borrowed(_))); + assert_eq!(out, "hello world"); + } + + #[test] + fn single_quote_in_middle_escapes() { + let out = escape_sql_string_literal("O'Brien"); + assert!(matches!(out, Cow::Owned(_))); + assert_eq!(out, "O''Brien"); + } + + #[test] + fn leading_quote_escapes() { + let out = escape_sql_string_literal("'leading"); + assert!(matches!(out, Cow::Owned(_))); + assert_eq!(out, "''leading"); + } + + #[test] + fn trailing_quote_escapes() { + let out = escape_sql_string_literal("trailing'"); + assert!(matches!(out, Cow::Owned(_))); + assert_eq!(out, "trailing''"); + } + + #[test] + fn only_quote_escapes() { + let out = escape_sql_string_literal("'"); + assert!(matches!(out, Cow::Owned(_))); + assert_eq!(out, "''"); + } + + #[test] + fn multiple_quotes_escape_each() { + let out = escape_sql_string_literal("a'b'c'"); + assert!(matches!(out, Cow::Owned(_))); + assert_eq!(out, "a''b''c''"); + } + + #[test] + fn consecutive_quotes_escape_each() { + let out = escape_sql_string_literal("''"); + assert!(matches!(out, Cow::Owned(_))); + assert_eq!(out, "''''"); + } + + #[test] + fn unicode_preserved_when_borrowed() { + let out = escape_sql_string_literal("héllo 世界"); + assert!(matches!(out, Cow::Borrowed(_))); + assert_eq!(out, "héllo 世界"); + } + + #[test] + fn unicode_preserved_when_owned() { + let out = escape_sql_string_literal("héllo'世界"); + assert!(matches!(out, Cow::Owned(_))); + assert_eq!(out, "héllo''世界"); + } + + #[test] + fn matches_naive_replace() { + // Equivalence oracle: helper output must equal raw .replace() + for s in [ + "", + "x", + "'", + "''", + "a'", + "'a", + "a'b", + "O'Brien's", + "no quotes here", + "héllo 世界", + ] { + assert_eq!( + escape_sql_string_literal(s).as_ref(), + s.replace('\'', "''"), + "mismatch for input {s:?}" + ); + } + } +} diff --git a/crates/vespertide-query/src/sql/modify_column_comment.rs b/crates/vespertide-query/src/sql/modify_column_comment.rs index 4e71992b..f1a8ffe9 100644 --- a/crates/vespertide-query/src/sql/modify_column_comment.rs +++ b/crates/vespertide-query/src/sql/modify_column_comment.rs @@ -23,7 +23,7 @@ pub fn build_modify_column_comment( let quoted_column = quote_ident(column, backend); let comment_sql = if let Some(comment) = new_comment { // Escape single quotes in comment - let escaped = comment.replace('\'', "''"); + let escaped = vespertide_core::escape_sql_string_literal(comment); format!("COMMENT ON COLUMN {quoted_table}.{quoted_column} IS '{escaped}'") } else { format!("COMMENT ON COLUMN {quoted_table}.{quoted_column} IS NULL") @@ -67,7 +67,7 @@ pub fn build_modify_column_comment( // Add COMMENT clause if needed (sea-query doesn't support COMMENT) let final_sql = if let Some(comment) = modified_col_def.comment.as_deref() { - let escaped = comment.replace('\'', "''"); + let escaped = vespertide_core::escape_sql_string_literal(comment); format!("{base_sql} COMMENT '{escaped}'") } else { base_sql diff --git a/crates/vespertide-query/src/sql/modify_column_type/mod.rs b/crates/vespertide-query/src/sql/modify_column_type/mod.rs index 7911635a..d9173f37 100644 --- a/crates/vespertide-query/src/sql/modify_column_type/mod.rs +++ b/crates/vespertide-query/src/sql/modify_column_type/mod.rs @@ -115,7 +115,7 @@ fn build_pg_alter_with_timezone( let qc = super::helpers::quote_ident(column, DatabaseBackend::Postgres); // validate_timezone in the CLI already rejected anything with quotes, // but escape defensively to keep this layer safe in isolation. - let tz_lit = tz.replace('\'', "''"); + let tz_lit = vespertide_core::escape_sql_string_literal(tz); let (target_sql_type, using_expr) = match new_type { ColumnType::Simple(SimpleColumnType::Timestamptz) => ( From d932bff6066d88fd5b2a739fba4fa3840b8e3ab7 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 25 Jun 2026 21:57:37 +0900 Subject: [PATCH 005/109] Optimize --- .../src/sql/add_constraint/foreign_key.rs | 14 +++----------- .../src/sql/add_constraint/primary_key.rs | 6 +----- .../src/sql/add_constraint/unique.rs | 8 ++------ 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/crates/vespertide-query/src/sql/add_constraint/foreign_key.rs b/crates/vespertide-query/src/sql/add_constraint/foreign_key.rs index c74b0dbc..eb3b6a88 100644 --- a/crates/vespertide-query/src/sql/add_constraint/foreign_key.rs +++ b/crates/vespertide-query/src/sql/add_constraint/foreign_key.rs @@ -1,7 +1,7 @@ use sea_query::{Alias, ForeignKey}; use vespertide_core::{ForeignKeyOrphanStrategy, ReferenceAction, TableConstraint}; -use super::super::helpers::{quote_ident, to_sea_fk_action}; +use super::super::helpers::{quote_ident, quote_idents, to_sea_fk_action}; use super::super::types::{BuiltQuery, DatabaseBackend, RawSql}; use super::{QueryError, TableDef, rebuild_sqlite_table_with_added_constraint}; @@ -79,16 +79,8 @@ pub(super) fn build_foreign_key, U: AsRef>( let quoted_table = quote_ident(table, backend); let quoted_name = quote_ident(&fk_name, backend); let quoted_ref_table = quote_ident(ref_table, backend); - let cols = columns - .iter() - .map(|c| quote_ident(c.as_ref(), backend)) - .collect::>() - .join(", "); - let ref_cols = ref_columns - .iter() - .map(|c| quote_ident(c.as_ref(), backend)) - .collect::>() - .join(", "); + let cols = quote_idents(columns, backend); + let ref_cols = quote_idents(ref_columns, backend); let on_delete_clause = render_fk_action_clause("ON DELETE", on_delete); let on_update_clause = render_fk_action_clause("ON UPDATE", on_update); diff --git a/crates/vespertide-query/src/sql/add_constraint/primary_key.rs b/crates/vespertide-query/src/sql/add_constraint/primary_key.rs index c70a7dee..692ca3a7 100644 --- a/crates/vespertide-query/src/sql/add_constraint/primary_key.rs +++ b/crates/vespertide-query/src/sql/add_constraint/primary_key.rs @@ -78,11 +78,7 @@ fn build_pk_pre_cleanup>( }; let quoted_table = quote_ident(table, backend); let quoted_old_pk = quote_ident(&old_pk_column, backend); - let group_by = new_pk_columns - .iter() - .map(|c| quote_ident(c.as_ref(), backend)) - .collect::>() - .join(", "); + let group_by = quote_idents(new_pk_columns, backend); let sql = format!( "DELETE FROM {quoted_table} WHERE {quoted_old_pk} NOT IN (\ SELECT {agg}({quoted_old_pk}) FROM {quoted_table} GROUP BY {group_by})" diff --git a/crates/vespertide-query/src/sql/add_constraint/unique.rs b/crates/vespertide-query/src/sql/add_constraint/unique.rs index 51900646..7d6120eb 100644 --- a/crates/vespertide-query/src/sql/add_constraint/unique.rs +++ b/crates/vespertide-query/src/sql/add_constraint/unique.rs @@ -2,7 +2,7 @@ use sea_query::{Alias, Index}; use vespertide_core::{KeepPolicy, TableConstraint, TableDef, UniqueConstraintStrategy}; -use super::super::helpers::{build_unique_constraint_name, quote_ident}; +use super::super::helpers::{build_unique_constraint_name, quote_ident, quote_idents}; use super::super::types::{BuiltQuery, DatabaseBackend, RawSql}; use crate::error::QueryError; @@ -69,11 +69,7 @@ fn build_pre_cleanup_delete>( }; let quoted_table = quote_ident(table, backend); let quoted_pk = quote_ident(&pk_column, backend); - let quoted_unique_cols: Vec = columns - .iter() - .map(|c| quote_ident(c.as_ref(), backend)) - .collect(); - let group_by = quoted_unique_cols.join(", "); + let group_by = quote_idents(columns, backend); let sql = format!( "DELETE FROM {quoted_table} WHERE {quoted_pk} NOT IN (\ SELECT {agg}({quoted_pk}) FROM {quoted_table} GROUP BY {group_by})", From 973e4c31f634760e4f00e4ed99e029ec354943b4 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Jun 2026 06:41:36 +0900 Subject: [PATCH 006/109] retry-now#0003: unify path_to_uri helper into vespertide-lsp::position The path -> file:// URI lowering helper was duplicated FIVE times across vespertide-lsp (Backend::path_to_uri in backend/mod.rs, path_to_uri in drift/compute.rs + references/search.rs + symbols.rs, and the slightly divergent path_to_file_uri in definition/foreign_key.rs), with three copy-pasted unit tests guarding three of those copies. Hoist one canonical pub fn path_to_uri(&Path) -> Option into position.rs next to its inverse uri_to_path, re-export it from lib.rs, delete the five private copies and the three duplicate tests, and rewire every call site (4 production + 9 backend-test) through the canonical function. The references::search.rs fallback to Uri("file:///") is preserved inline at the single call site, so the fallback semantics are visible instead of buried inside a helper. Net: -30 production lines; one audit point for future URI hardening (percent-encoding %?#, smarter Windows UNC handling) instead of five. Verification: - cargo test --workspace --all-features --exclude vespertide-query: 0 failed - cargo test -p vespertide-lsp --all-features: 0 failed - cargo clippy --workspace --all-targets --all-features --exclude vespertide-query -- -D warnings: clean - cargo fmt --all --check: clean - scripts/check-line-budget.sh: clean - schema-gen drift: 0 - cargo bench --workspace --exclude vespertide-query (2 before + 2 after): median delta +2.16%, within the before-runs noise band (mean 8.8%, max 70.9%). Macro benches (did_change_fanout family, closest to the changed LSP code) improved -3.4% to -9.3%. Statistically neutral. - git grep -nE '^([a-z]*\s)*fn (path_to_uri|path_to_file_uri)\(' -- crates/vespertide-lsp/src/ -> exactly one production hit (position.rs). --- crates/vespertide-lsp/src/backend/mod.rs | 14 +---- .../src/backend/tests/harness.rs | 8 +-- .../src/backend/tests/helpers.rs | 2 +- .../src/backend/tests/lifecycle.rs | 17 +++--- .../src/definition/foreign_key.rs | 21 +------ crates/vespertide-lsp/src/drift/compute.rs | 32 +---------- crates/vespertide-lsp/src/lib.rs | 2 +- crates/vespertide-lsp/src/position.rs | 56 +++++++++++++++++++ .../vespertide-lsp/src/references/search.rs | 29 +++------- crates/vespertide-lsp/src/symbols.rs | 22 +------- 10 files changed, 86 insertions(+), 117 deletions(-) diff --git a/crates/vespertide-lsp/src/backend/mod.rs b/crates/vespertide-lsp/src/backend/mod.rs index 06c0e2f2..fffd96fb 100644 --- a/crates/vespertide-lsp/src/backend/mod.rs +++ b/crates/vespertide-lsp/src/backend/mod.rs @@ -12,7 +12,7 @@ //! would fail to resolve. use std::collections::BTreeSet; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, Mutex}; @@ -224,14 +224,6 @@ impl Backend { workspace } - fn path_to_uri(path: &Path) -> Option { - let mut path_text = path.to_string_lossy().replace('\\', "/"); - if !path_text.starts_with('/') { - path_text = format!("/{path_text}"); - } - Uri::from_str(&format!("file://{path_text}")).ok() - } - fn fallback_disk_uri(table_name: &str) -> Uri { Uri::from_str(&format!("file:///__disk__/{table_name}.json")) .expect("synthetic disk model URI should parse") @@ -377,8 +369,8 @@ fn disk_workspace_table( disk_path: Option, ) -> Option<(PathBuf, diagnostics::WorkspaceTable)> { let disk_path = disk_path?; - let disk_uri = - Backend::path_to_uri(&disk_path).unwrap_or_else(|| Backend::fallback_disk_uri(name)); + let disk_uri = crate::position::path_to_uri(&disk_path) + .unwrap_or_else(|| Backend::fallback_disk_uri(name)); let entry = diagnostics::WorkspaceTable { uri: disk_uri, table, diff --git a/crates/vespertide-lsp/src/backend/tests/harness.rs b/crates/vespertide-lsp/src/backend/tests/harness.rs index 6f9ea37b..cfbdfb64 100644 --- a/crates/vespertide-lsp/src/backend/tests/harness.rs +++ b/crates/vespertide-lsp/src/backend/tests/harness.rs @@ -165,10 +165,10 @@ pub(super) fn workspace_fixture() -> WorkspaceFixture { fs::write(&outside_path, "not a model").unwrap(); WorkspaceFixture { - root_uri: Backend::path_to_uri(root).unwrap(), - user_uri: Backend::path_to_uri(&user_path).unwrap(), - post_uri: Backend::path_to_uri(&post_path).unwrap(), - outside_uri: Backend::path_to_uri(&outside_path).unwrap(), + root_uri: crate::position::path_to_uri(root).unwrap(), + user_uri: crate::position::path_to_uri(&user_path).unwrap(), + post_uri: crate::position::path_to_uri(&post_path).unwrap(), + outside_uri: crate::position::path_to_uri(&outside_path).unwrap(), _tmp: tmp, } } diff --git a/crates/vespertide-lsp/src/backend/tests/helpers.rs b/crates/vespertide-lsp/src/backend/tests/helpers.rs index 3199dc22..251ccf8b 100644 --- a/crates/vespertide-lsp/src/backend/tests/helpers.rs +++ b/crates/vespertide-lsp/src/backend/tests/helpers.rs @@ -151,7 +151,7 @@ fn disk_symbol_reference_and_rename_helpers_lower_disk_paths( let tmp = tempdir().unwrap(); let path = tmp.path().join(file_name); std::fs::write(&path, source).unwrap(); - let target_uri = super::super::Backend::path_to_uri(&path).unwrap(); + let target_uri = crate::position::path_to_uri(&path).unwrap(); let (service, _socket) = make_service(); let backend = service.inner(); let email_start = source.find("email").unwrap(); diff --git a/crates/vespertide-lsp/src/backend/tests/lifecycle.rs b/crates/vespertide-lsp/src/backend/tests/lifecycle.rs index 37da8bcd..502ae38f 100644 --- a/crates/vespertide-lsp/src/backend/tests/lifecycle.rs +++ b/crates/vespertide-lsp/src/backend/tests/lifecycle.rs @@ -58,7 +58,7 @@ async fn direct_lifecycle_methods_return_capabilities_and_log_without_mocking_cl .await; backend.shutdown().await.unwrap(); - let relative = super::super::Backend::path_to_uri(Path::new("relative-model.json")) + let relative = crate::position::path_to_uri(Path::new("relative-model.json")) .expect("relative paths should still lower to synthetic file URIs"); assert!(relative.as_str().starts_with("file:///")); let fallback = super::super::Backend::fallback_disk_uri("ghost"); @@ -125,7 +125,7 @@ fn workspace_collection_skips_normalize_failures_and_keeps_disk_tables() { .unwrap(); let bad_uri = - super::super::Backend::path_to_uri(&fixture._tmp.path().join("models/bad_index.json")) + crate::position::path_to_uri(&fixture._tmp.path().join("models/bad_index.json")) .unwrap(); backend .did_open(did_open_params(&bad_uri, "json", 1, DUP_INDEX_MODEL)) @@ -278,15 +278,12 @@ fn workspace_table_helpers_cover_degenerate_open_and_disk_state() { async fn service_lifecycle_and_text_document_notifications_exercise_publish_paths() { let fixture = workspace_fixture(); let yaml_uri = - super::super::Backend::path_to_uri(&fixture._tmp.path().join("models/account.yaml")) - .unwrap(); + crate::position::path_to_uri(&fixture._tmp.path().join("models/account.yaml")).unwrap(); let bad_json_uri = - super::super::Backend::path_to_uri(&fixture._tmp.path().join("models/bad.json")).unwrap(); + crate::position::path_to_uri(&fixture._tmp.path().join("models/bad.json")).unwrap(); let bad_fk_uri = - super::super::Backend::path_to_uri(&fixture._tmp.path().join("models/bad_fk.json")) - .unwrap(); - let text_uri = - super::super::Backend::path_to_uri(&fixture._tmp.path().join("notes.txt")).unwrap(); + crate::position::path_to_uri(&fixture._tmp.path().join("models/bad_fk.json")).unwrap(); + let text_uri = crate::position::path_to_uri(&fixture._tmp.path().join("notes.txt")).unwrap(); let (mut service, socket) = make_service(); initialize_service( @@ -535,7 +532,7 @@ async fn workspace_refresh_from_document_uri_finds_config_above_models_dir() { fs::write(tmp.path().join("vespertide.json"), workspace_config()).unwrap(); let user_path = models.join("user.json"); fs::write(&user_path, USER_MODEL).unwrap(); - let user_uri = super::super::Backend::path_to_uri(&user_path).unwrap(); + let user_uri = crate::position::path_to_uri(&user_path).unwrap(); let (service, _socket) = make_service(); let backend = service.inner(); diff --git a/crates/vespertide-lsp/src/definition/foreign_key.rs b/crates/vespertide-lsp/src/definition/foreign_key.rs index 07a5770d..6f9eb41c 100644 --- a/crates/vespertide-lsp/src/definition/foreign_key.rs +++ b/crates/vespertide-lsp/src/definition/foreign_key.rs @@ -12,10 +12,6 @@ //! sees the document. use std::ops::Range; -use std::path::Path; -use std::str::FromStr; - -use tower_lsp_server::ls_types::Uri; use crate::store::DocumentStore; use crate::text_util::strip_quotes; @@ -129,7 +125,7 @@ fn resolve_target( // Fall back to the on-disk model so closed files still navigate. let path = disk_tables?.model_path(table_name)?; - let uri = path_to_file_uri(&path)?; + let uri = crate::position::path_to_uri(&path)?; Some(DomainLocation { uri, byte_range: 0..0, @@ -264,19 +260,6 @@ fn direct_named_child_pair<'tree>( .find(|&child| is_pair(child) && key_is(child, source, target_key)) } -fn path_to_file_uri(path: &Path) -> Option { - let path_str = path.to_str()?; - // Normalize to forward slashes for the URI; on Windows the drive letter - // gets a leading slash so `C:\a\b` becomes `file:///C:/a/b`. - let normalized = path_str.replace('\\', "/"); - let url = if normalized.starts_with('/') { - format!("file://{normalized}") - } else { - format!("file:///{normalized}") - }; - Uri::from_str(&url).ok() -} - fn find_top_level_name_range(tree: &tree_sitter::Tree, text: &str) -> Option> { let root = tree.root_node(); let mapping = first_mapping(root)?; @@ -345,6 +328,8 @@ mod tests { use super::*; use crate::parser::DocumentFormat; use crate::test_support::parse; + use std::str::FromStr; + use tower_lsp_server::ls_types::Uri; fn node_at<'tree>( tree: &'tree tree_sitter::Tree, diff --git a/crates/vespertide-lsp/src/drift/compute.rs b/crates/vespertide-lsp/src/drift/compute.rs index 6df74a38..f605ec89 100644 --- a/crates/vespertide-lsp/src/drift/compute.rs +++ b/crates/vespertide-lsp/src/drift/compute.rs @@ -6,7 +6,6 @@ //! that owns a process-static `DriftCache` for ad-hoc callers (CLI, tests). use std::path::{Path, PathBuf}; -use std::str::FromStr; use std::sync::{Arc, OnceLock}; use tower_lsp_server::ls_types::Uri; @@ -215,20 +214,12 @@ fn guess_uri(models_dir: &Path, table_name: &str) -> Option { for ext in ["json", "yaml", "yml"] { path.set_extension(ext); if path.exists() { - return path_to_uri(&path); + return crate::position::path_to_uri(&path); } } None } -fn path_to_uri(path: &Path) -> Option { - let mut path_text = path.to_string_lossy().replace('\\', "/"); - if !path_text.starts_with('/') { - path_text = format!("/{path_text}"); - } - Uri::from_str(&format!("file://{path_text}")).ok() -} - #[cfg(test)] mod tests { use super::*; @@ -326,7 +317,8 @@ mod tests { .parse(user_model(), crate::parser::DocumentFormat::Json) .unwrap(); let index = WorkspaceIndex::new(); - let missing_uri = path_to_uri(&tmp.path().join("models/missing.json")).unwrap(); + let missing_uri = + crate::position::path_to_uri(&tmp.path().join("models/missing.json")).unwrap(); index.upsert(&missing_uri, user_model(), &tree); let out = compute_with_cache( @@ -365,22 +357,4 @@ mod tests { assert!(guess_uri(tmp.path(), "user").is_some()); assert!(guess_uri(tmp.path(), "missing").is_none()); } - - #[test] - fn path_to_uri_prepends_leading_slash_for_relative_path() { - // A relative path never starts with `/` on any platform, so this - // exercises the leading-slash normalization branch deterministically - // (absolute POSIX paths on CI would otherwise skip it). - let uri = path_to_uri(std::path::Path::new("models/user.json")).expect("uri"); - assert!( - uri.as_str().starts_with("file:///"), - "got: {}", - uri.as_str() - ); - assert!( - uri.as_str().ends_with("models/user.json"), - "got: {}", - uri.as_str() - ); - } } diff --git a/crates/vespertide-lsp/src/lib.rs b/crates/vespertide-lsp/src/lib.rs index 16fb53d7..54ad87ca 100644 --- a/crates/vespertide-lsp/src/lib.rs +++ b/crates/vespertide-lsp/src/lib.rs @@ -71,7 +71,7 @@ pub use inlay_hints::{DomainInlayHint, compute as compute_inlay_hints}; pub use parser::{DocumentFormat, ParserPool}; pub use position::{ byte_to_lsp_position, ls_to_lsp_position, ls_to_lsp_range, lsp_position_to_byte, - lsp_to_ls_position, uri_to_path, + lsp_to_ls_position, path_to_uri, uri_to_path, }; pub use references::{ DomainReference, ReferenceSymbol, compute as compute_references, diff --git a/crates/vespertide-lsp/src/position.rs b/crates/vespertide-lsp/src/position.rs index 364ae127..a165d92f 100644 --- a/crates/vespertide-lsp/src/position.rs +++ b/crates/vespertide-lsp/src/position.rs @@ -56,6 +56,27 @@ pub fn ls_to_lsp_range(r: tower_lsp_server::ls_types::Range) -> lsp_types::Range } } +/// Build a `file://...` URI from a local filesystem path. +/// +/// Counterpart to [`uri_to_path`]: normalises Windows backslashes to +/// forward slashes and ensures the path component starts with `/` so +/// the resulting URI is well-formed (`file:///c:/...` on Windows, +/// `file:///abs/path` on POSIX). Returns `None` only when the path +/// is so degenerate that `Uri::from_str` rejects the result. +/// +/// This is the single canonical inverse of [`uri_to_path`]; the LSP +/// crate previously carried five near-identical copies (one per module +/// that needed to lower a discovered disk path back to an LSP URI). +#[must_use] +pub fn path_to_uri(path: &std::path::Path) -> Option { + use std::str::FromStr as _; + let mut path_text = path.to_string_lossy().replace('\\', "/"); + if !path_text.starts_with('/') { + path_text = format!("/{path_text}"); + } + Uri::from_str(&format!("file://{path_text}")).ok() +} + /// Convert a `file://` URI into a local filesystem path. /// /// Handles percent-encoding (VS Code sends `file:///c%3A/...`, Zed sends @@ -308,4 +329,39 @@ mod tests { assert!(!has_windows_drive_prefix("ab")); assert!(!has_windows_drive_prefix("")); } + + /// Single canonical test for `path_to_uri`. Previously this same + /// assertion was copy-pasted into three modules (`drift::compute`, + /// `references::search`, `symbols`) — each guarding its own private + /// copy of the helper. With the helper hoisted here, one test covers + /// every prior call site. + #[rstest] + #[case::relative("models/user.json", "models/user.json")] + #[case::posix_absolute_like("/abs/path/file.json", "/abs/path/file.json")] + #[case::windows_backslashes("C:\\Users\\x\\m.json", "C:/Users/x/m.json")] + fn path_to_uri_normalises_and_prepends_leading_slash( + #[case] input: &str, + #[case] expected_tail: &str, + ) { + let uri = path_to_uri(std::path::Path::new(input)).expect("uri"); + let text = uri.as_str(); + + assert!(text.starts_with("file:///"), "got: {text}"); + assert!(text.ends_with(expected_tail), "got: {text}"); + } + + /// Round-trip with [`uri_to_path`] on a POSIX-like path: lowering then + /// raising must round-trip when the input was already `file://`-shaped. + #[test] + fn path_to_uri_then_uri_to_path_round_trips_posix_like_path() { + let original = std::path::PathBuf::from("/tmp/round_trip.json"); + let uri = path_to_uri(&original).expect("uri"); + let path = uri_to_path(&uri).expect("path"); + + // On Windows the round-trip lossy-converts separators; assert by + // string-tail rather than exact equality so the test is platform- + // independent. + let text = path.to_string_lossy().replace('\\', "/"); + assert!(text.ends_with("/tmp/round_trip.json"), "got: {text}"); + } } diff --git a/crates/vespertide-lsp/src/references/search.rs b/crates/vespertide-lsp/src/references/search.rs index 7e5b36ab..f793afbb 100644 --- a/crates/vespertide-lsp/src/references/search.rs +++ b/crates/vespertide-lsp/src/references/search.rs @@ -103,21 +103,18 @@ fn scan_disk_file( if let Some(format) = format && let Some(tree) = pool.parse(&text, format) { - let uri = path_to_uri(path); + // Fallback to the synthetic empty `file:///` URI keeps + // `scan_disk_file` infallible even on the (practically + // unreachable) path that `path_to_uri` rejects — better to + // attribute references to an obviously-synthetic URI than to + // silently drop them. + let uri = crate::position::path_to_uri(path) + .unwrap_or_else(|| ::from_str("file:///").unwrap()); collect_in_document(symbol, &uri, &text, &tree, include_declaration, out); } } } -fn path_to_uri(path: &std::path::Path) -> Uri { - let mut text = path.to_string_lossy().replace('\\', "/"); - if !text.starts_with('/') { - text = format!("/{text}"); - } - std::str::FromStr::from_str(&format!("file://{text}")) - .unwrap_or_else(|_| std::str::FromStr::from_str("file:///").unwrap()) -} - fn collect_in_document( symbol: &ReferenceSymbol, uri: &Uri, @@ -740,16 +737,4 @@ mod tests { "declaration and CHECK expression should both be found: {out:?}" ); } - - #[test] - fn path_to_uri_prepends_leading_slash_for_relative_path() { - // A relative path never starts with `/` on any platform, so this - // exercises the leading-slash normalization branch deterministically. - let uri = path_to_uri(std::path::Path::new("models/user.json")); - assert!( - uri.as_str().starts_with("file:///"), - "got: {}", - uri.as_str() - ); - } } diff --git a/crates/vespertide-lsp/src/symbols.rs b/crates/vespertide-lsp/src/symbols.rs index 497c4bc7..7a3224f7 100644 --- a/crates/vespertide-lsp/src/symbols.rs +++ b/crates/vespertide-lsp/src/symbols.rs @@ -184,7 +184,7 @@ fn build_workspace_symbol_list( let pool = shared_parser_pool(); for name in disk.names() { if let Some(path) = disk.model_path(&name) - && let Some(uri) = path_to_uri(&path) + && let Some(uri) = crate::position::path_to_uri(&path) && !seen_uris.contains(&uri) && let Some((text_arc, tree_arc)) = disk.cached_parse(&path, pool) { @@ -358,14 +358,6 @@ fn trim_one_byte(range: &Range) -> Range { } } -fn path_to_uri(path: &std::path::Path) -> Option { - let mut text = path.to_string_lossy().replace('\\', "/"); - if !text.starts_with('/') { - text = format!("/{text}"); - } - std::str::FromStr::from_str(&format!("file://{text}")).ok() -} - /// ASCII case-insensitive substring search. `needle_lower` must already be /// lowercase (the public `compute()` entry-point lowercases the query once). /// Allocates zero — walks `haystack` byte-by-byte folding only ASCII case. @@ -1001,16 +993,4 @@ mod tests { assert!(compute("xyz_nothing_matches", &docs, None).is_empty()); } - - #[test] - fn path_to_uri_prepends_leading_slash_for_relative_path() { - // A relative path never starts with `/` on any platform, so this - // exercises the leading-slash normalization branch deterministically. - let uri = path_to_uri(std::path::Path::new("models/user.json")).expect("uri"); - assert!( - uri.as_str().starts_with("file:///"), - "got: {}", - uri.as_str() - ); - } } From 6de734bda6ebd0ed44b35970455465c39a4f2b72 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Jun 2026 07:20:30 +0900 Subject: [PATCH 007/109] retry-now#0004: drop intermediate Vec<&str> alloc in ColumnType::enum_variant_names --- crates/vespertide-core/src/schema/column.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs index 516cdc03..46565f9a 100644 --- a/crates/vespertide-core/src/schema/column.rs +++ b/crates/vespertide-core/src/schema/column.rs @@ -160,16 +160,13 @@ impl ColumnType { /// Get enum variant names if this is an enum type /// Returns None if not an enum, Some(names) otherwise pub fn enum_variant_names(&self) -> Option> { - match self { - ColumnType::Complex(ComplexColumnType::Enum { values, .. }) => Some( - values - .variant_names() - .into_iter() - .map(String::from) - .collect(), - ), - _ => None, - } + let ColumnType::Complex(ComplexColumnType::Enum { values, .. }) = self else { + return None; + }; + Some(match values { + EnumValues::String(v) => v.clone(), + EnumValues::Integer(v) => v.iter().map(|n| n.name.clone()).collect(), + }) } } From 72d9901b9b65d07a619d51e3c361fcc57b1fc2f6 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Jun 2026 13:25:00 +0900 Subject: [PATCH 008/109] retry-now#0005: remove dead empty inner loop in topological_sort_tables The outer Kahn's-algorithm loop in topological_sort_tables ran an empty `for _dep in deps {}` body - N iterations per outer pair with zero side effect - paired with two comments (`The table has dependencies, so those referenced tables must come first` / `We actually want the reverse: tables with dependencies have higher in-degree`) that directly contradicted the immediately following surviving line `*in_degree.entry(table_name).or_insert(0) += deps.len();` (which DOES give dependent tables a higher in-degree, exactly the convention the comment said was wrong). Classic mid-refactor leftover: someone collapsed a `for _dep in deps { *in_degree += 1; }` accumulator into a single `+= deps.len()` but never deleted the now-empty loop or its stale rationale. Algorithm is byte-identical post-change - the compiler already elided the empty loop - and the misleading comments no longer contradict the code that survives. --- crates/vespertide-planner/src/diff/ordering.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/crates/vespertide-planner/src/diff/ordering.rs b/crates/vespertide-planner/src/diff/ordering.rs index 10071603..ea74d5bf 100644 --- a/crates/vespertide-planner/src/diff/ordering.rs +++ b/crates/vespertide-planner/src/diff/ordering.rs @@ -45,14 +45,9 @@ pub(super) fn topological_sort_tables<'a>( in_degree.entry(table.name.as_str()).or_insert(0); } - // For each dependency, increment the in-degree of the dependent table + // For each dependency, increment the in-degree of the dependent table. + // (If A depends on B, A's in-degree rises; A is processed after B.) for (table_name, deps) in &dependencies { - for _dep in deps { - // The table has dependencies, so those referenced tables must come first - // We actually want the reverse: tables with dependencies have higher in-degree - } - // Actually, we need to track: if A depends on B, then A has in-degree from B - // So A cannot be processed until B is processed *in_degree.entry(table_name).or_insert(0) += deps.len(); } From 6ef965f073f15e66eff7702564d5d0ac7bfa5acb Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Jun 2026 15:29:00 +0900 Subject: [PATCH 009/109] retry-now#0006: replace 4 inline RawSql::per_backend(s.clone(), s.clone(), s) with RawSql::uniform(s) --- .../vespertide-query/src/sql/create_table.rs | 6 +----- crates/vespertide-query/src/sql/helpers.rs | 18 +++--------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/crates/vespertide-query/src/sql/create_table.rs b/crates/vespertide-query/src/sql/create_table.rs index 1c325a58..044b737e 100644 --- a/crates/vespertide-query/src/sql/create_table.rs +++ b/crates/vespertide-query/src/sql/create_table.rs @@ -260,11 +260,7 @@ pub fn build_create_table( let check_sql = enum_check_clauses.join(", "); modified_sql.insert_str(pos, &format!(", {check_sql}")); } - queries.push(BuiltQuery::Raw(RawSql::per_backend( - modified_sql.clone(), - modified_sql.clone(), - modified_sql, - ))); + queries.push(BuiltQuery::Raw(RawSql::uniform(modified_sql))); } } else { queries.push(BuiltQuery::CreateTable(Box::new(create_table_stmt))); diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs index 79da55c7..23965dd7 100644 --- a/crates/vespertide-query/src/sql/helpers.rs +++ b/crates/vespertide-query/src/sql/helpers.rs @@ -586,11 +586,7 @@ pub fn build_create_with_checks( let check_sql = check_clauses.join(", "); modified_sql.insert_str(pos, &format!(", {check_sql}")); } - BuiltQuery::Raw(RawSql::per_backend( - modified_sql.clone(), - modified_sql.clone(), - modified_sql, - )) + BuiltQuery::Raw(RawSql::uniform(modified_sql)) } } @@ -638,11 +634,7 @@ pub fn recreate_indexes_after_rebuild( let index_name = quote_ident(&index_name, DatabaseBackend::Sqlite); let table = quote_ident(table, DatabaseBackend::Sqlite); let sql = format!("CREATE INDEX {index_name} ON {table} ({cols_sql})"); - queries.push(BuiltQuery::Raw(RawSql::per_backend( - sql.clone(), - sql.clone(), - sql, - ))); + queries.push(BuiltQuery::Raw(RawSql::uniform(sql))); } TableConstraint::Unique { name, columns, .. } => { let index_name = build_unique_constraint_name(table, columns, name.as_deref()); @@ -650,11 +642,7 @@ pub fn recreate_indexes_after_rebuild( let index_name = quote_ident(&index_name, DatabaseBackend::Sqlite); let table = quote_ident(table, DatabaseBackend::Sqlite); let sql = format!("CREATE UNIQUE INDEX {index_name} ON {table} ({cols_sql})"); - queries.push(BuiltQuery::Raw(RawSql::per_backend( - sql.clone(), - sql.clone(), - sql, - ))); + queries.push(BuiltQuery::Raw(RawSql::uniform(sql))); } _ => {} } From 24d4a05473414352e0f5126b249116a906954f47 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Jun 2026 16:53:12 +0900 Subject: [PATCH 010/109] retry-now#0007: hoist invariant quote_ident out of recreate_indexes_after_rebuild loop and dedupe Index/Unique arms --- crates/vespertide-query/src/sql/helpers.rs | 37 +++++++++++++++------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs index 23965dd7..194218f3 100644 --- a/crates/vespertide-query/src/sql/helpers.rs +++ b/crates/vespertide-query/src/sql/helpers.rs @@ -622,6 +622,21 @@ pub fn recreate_indexes_after_rebuild( let mut queries = Vec::with_capacity(constraints.len()); // perf: BTreeSet membership avoids nested Vec::contains scans during SQLite rebuilds. let pending_constraints: std::collections::BTreeSet<_> = pending_constraints.iter().collect(); + // perf: `table` is loop-invariant — quote it once instead of per surviving constraint. + let quoted_table = quote_ident(table, DatabaseBackend::Sqlite); + // dedup: both Index and Unique arms differ only by name builder + `UNIQUE` keyword. + let mut push_index = + |index_name: String, columns: &[vespertide_core::ColumnName], unique: bool| { + let cols_sql = quote_idents(columns, DatabaseBackend::Sqlite); + let quoted_index = quote_ident(&index_name, DatabaseBackend::Sqlite); + let keyword = if unique { + "CREATE UNIQUE INDEX" + } else { + "CREATE INDEX" + }; + let sql = format!("{keyword} {quoted_index} ON {quoted_table} ({cols_sql})"); + queries.push(BuiltQuery::Raw(RawSql::uniform(sql))); + }; for constraint in constraints { // Skip constraints that will be created by future AddConstraint actions if pending_constraints.contains(constraint) { @@ -629,20 +644,18 @@ pub fn recreate_indexes_after_rebuild( } match constraint { TableConstraint::Index { name, columns } => { - let index_name = build_index_name(table, columns, name.as_deref()); - let cols_sql = quote_idents(columns, DatabaseBackend::Sqlite); - let index_name = quote_ident(&index_name, DatabaseBackend::Sqlite); - let table = quote_ident(table, DatabaseBackend::Sqlite); - let sql = format!("CREATE INDEX {index_name} ON {table} ({cols_sql})"); - queries.push(BuiltQuery::Raw(RawSql::uniform(sql))); + push_index( + build_index_name(table, columns, name.as_deref()), + columns, + false, + ); } TableConstraint::Unique { name, columns, .. } => { - let index_name = build_unique_constraint_name(table, columns, name.as_deref()); - let cols_sql = quote_idents(columns, DatabaseBackend::Sqlite); - let index_name = quote_ident(&index_name, DatabaseBackend::Sqlite); - let table = quote_ident(table, DatabaseBackend::Sqlite); - let sql = format!("CREATE UNIQUE INDEX {index_name} ON {table} ({cols_sql})"); - queries.push(BuiltQuery::Raw(RawSql::uniform(sql))); + push_index( + build_unique_constraint_name(table, columns, name.as_deref()), + columns, + true, + ); } _ => {} } From 37fcfe6171d24e650c3cbd34924f0af67609426b Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Jun 2026 18:53:49 +0900 Subject: [PATCH 011/109] retry-now#0008: drop dead RawSql backend fields in remove_constraint/{postgres,mysql}.rs - switch to RawSql::uniform --- .../src/sql/remove_constraint/mysql.rs | 43 ++++++------------- .../src/sql/remove_constraint/postgres.rs | 32 ++++---------- 2 files changed, 21 insertions(+), 54 deletions(-) diff --git a/crates/vespertide-query/src/sql/remove_constraint/mysql.rs b/crates/vespertide-query/src/sql/remove_constraint/mysql.rs index 63de68b8..ca4cf9c2 100644 --- a/crates/vespertide-query/src/sql/remove_constraint/mysql.rs +++ b/crates/vespertide-query/src/sql/remove_constraint/mysql.rs @@ -9,42 +9,28 @@ use crate::sql::{DatabaseBackend, RawSql}; pub fn build_remove_constraint(table: &str, constraint: &TableConstraint) -> Vec { match constraint { TableConstraint::PrimaryKey { .. } => { - let pg_table = quote_ident(table, DatabaseBackend::Postgres); - let pg_pkey = quote_ident(&format!("{table}_pkey"), DatabaseBackend::Postgres); let mysql_table = quote_ident(table, DatabaseBackend::MySql); - let pg_sql = format!("ALTER TABLE {pg_table} DROP CONSTRAINT {pg_pkey}"); - vec![BuiltQuery::Raw(RawSql::per_backend( - pg_sql.clone(), - format!("ALTER TABLE {mysql_table} DROP PRIMARY KEY"), - pg_sql, - ))] + vec![BuiltQuery::Raw(RawSql::uniform(format!( + "ALTER TABLE {mysql_table} DROP PRIMARY KEY" + )))] } TableConstraint::Unique { name, columns, .. } => { let constraint_name = vespertide_naming::build_unique_constraint_name(table, columns, name.as_deref()); - let pg_constraint = quote_ident(&constraint_name, DatabaseBackend::Postgres); let mysql_table = quote_ident(table, DatabaseBackend::MySql); let mysql_constraint = quote_ident(&constraint_name, DatabaseBackend::MySql); - let pg_sql = format!("DROP INDEX {pg_constraint}"); - vec![BuiltQuery::Raw(RawSql::per_backend( - pg_sql.clone(), - format!("ALTER TABLE {mysql_table} DROP INDEX {mysql_constraint}"), - pg_sql, - ))] + vec![BuiltQuery::Raw(RawSql::uniform(format!( + "ALTER TABLE {mysql_table} DROP INDEX {mysql_constraint}" + )))] } TableConstraint::ForeignKey { name, columns, .. } => { let constraint_name = vespertide_naming::build_foreign_key_name(table, columns, name.as_deref()); - let pg_table = quote_ident(table, DatabaseBackend::Postgres); - let pg_constraint = quote_ident(&constraint_name, DatabaseBackend::Postgres); let mysql_table = quote_ident(table, DatabaseBackend::MySql); let mysql_constraint = quote_ident(&constraint_name, DatabaseBackend::MySql); - let pg_sql = format!("ALTER TABLE {pg_table} DROP CONSTRAINT {pg_constraint}"); - vec![BuiltQuery::Raw(RawSql::per_backend( - pg_sql.clone(), - format!("ALTER TABLE {mysql_table} DROP FOREIGN KEY {mysql_constraint}"), - pg_sql, - ))] + vec![BuiltQuery::Raw(RawSql::uniform(format!( + "ALTER TABLE {mysql_table} DROP FOREIGN KEY {mysql_constraint}" + )))] } TableConstraint::Index { name, columns } => { let index_name = vespertide_naming::build_index_name(table, columns, name.as_deref()); @@ -55,16 +41,11 @@ pub fn build_remove_constraint(table: &str, constraint: &TableConstraint) -> Vec vec![BuiltQuery::DropIndex(Box::new(idx_drop))] } TableConstraint::Check { name, .. } => { - let pg_table = quote_ident(table, DatabaseBackend::Postgres); - let pg_name = quote_ident(name, DatabaseBackend::Postgres); let mysql_table = quote_ident(table, DatabaseBackend::MySql); let mysql_name = quote_ident(name, DatabaseBackend::MySql); - let pg_sql = format!("ALTER TABLE {pg_table} DROP CONSTRAINT {pg_name}"); - vec![BuiltQuery::Raw(RawSql::per_backend( - pg_sql.clone(), - format!("ALTER TABLE {mysql_table} DROP CHECK {mysql_name}"), - pg_sql, - ))] + vec![BuiltQuery::Raw(RawSql::uniform(format!( + "ALTER TABLE {mysql_table} DROP CHECK {mysql_name}" + )))] } _ => unreachable!("TableConstraint is #[non_exhaustive]; all variants are matched above"), } diff --git a/crates/vespertide-query/src/sql/remove_constraint/postgres.rs b/crates/vespertide-query/src/sql/remove_constraint/postgres.rs index 1ce48861..d43c840c 100644 --- a/crates/vespertide-query/src/sql/remove_constraint/postgres.rs +++ b/crates/vespertide-query/src/sql/remove_constraint/postgres.rs @@ -11,26 +11,17 @@ pub fn build_remove_constraint(table: &str, constraint: &TableConstraint) -> Vec TableConstraint::PrimaryKey { .. } => { let pg_table = quote_ident(table, DatabaseBackend::Postgres); let pg_pkey = quote_ident(&format!("{table}_pkey"), DatabaseBackend::Postgres); - let mysql_table = quote_ident(table, DatabaseBackend::MySql); - let pg_sql = format!("ALTER TABLE {pg_table} DROP CONSTRAINT {pg_pkey}"); - vec![BuiltQuery::Raw(RawSql::per_backend( - pg_sql.clone(), - format!("ALTER TABLE {mysql_table} DROP PRIMARY KEY"), - pg_sql, - ))] + vec![BuiltQuery::Raw(RawSql::uniform(format!( + "ALTER TABLE {pg_table} DROP CONSTRAINT {pg_pkey}" + )))] } TableConstraint::Unique { name, columns, .. } => { let constraint_name = vespertide_naming::build_unique_constraint_name(table, columns, name.as_deref()); let pg_constraint = quote_ident(&constraint_name, DatabaseBackend::Postgres); - let mysql_table = quote_ident(table, DatabaseBackend::MySql); - let mysql_constraint = quote_ident(&constraint_name, DatabaseBackend::MySql); - let pg_sql = format!("DROP INDEX {pg_constraint}"); - vec![BuiltQuery::Raw(RawSql::per_backend( - pg_sql.clone(), - format!("ALTER TABLE {mysql_table} DROP INDEX {mysql_constraint}"), - pg_sql, - ))] + vec![BuiltQuery::Raw(RawSql::uniform(format!( + "DROP INDEX {pg_constraint}" + )))] } TableConstraint::ForeignKey { name, columns, .. } => { let constraint_name = @@ -52,14 +43,9 @@ pub fn build_remove_constraint(table: &str, constraint: &TableConstraint) -> Vec TableConstraint::Check { name, .. } => { let pg_table = quote_ident(table, DatabaseBackend::Postgres); let pg_name = quote_ident(name, DatabaseBackend::Postgres); - let mysql_table = quote_ident(table, DatabaseBackend::MySql); - let mysql_name = quote_ident(name, DatabaseBackend::MySql); - let pg_sql = format!("ALTER TABLE {pg_table} DROP CONSTRAINT {pg_name}"); - vec![BuiltQuery::Raw(RawSql::per_backend( - pg_sql.clone(), - format!("ALTER TABLE {mysql_table} DROP CHECK {mysql_name}"), - pg_sql, - ))] + vec![BuiltQuery::Raw(RawSql::uniform(format!( + "ALTER TABLE {pg_table} DROP CONSTRAINT {pg_name}" + )))] } _ => unreachable!("TableConstraint is #[non_exhaustive]; all variants are matched above"), } From 5b11ea6be0dab104390f02276c94725547d8d2ae Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Jun 2026 20:21:51 +0900 Subject: [PATCH 012/109] retry-now#0009: drop to_lowercase() allocation in convert_default_for_backend Replace let lower = default.to_lowercase(); lower == \\` with direct default.eq_ignore_ascii_case(\\) comparisons in vespertide-query::sql::helpers::convert_default_for_backend. All 7 literals (gen_random_uuid(), uuid(), lower(hex(randomblob(16))), current_timestamp(), now(), current_timestamp, getdate()) are pure ASCII, so the case-insensitive comparison is observationally identical for every input. Removes one heap-allocated String per call on the SQL emission hot path used by CreateTable / AddColumn / ModifyColumnType / ModifyColumnNullable (6 production call sites). Mirrors the in-file convention already used by needs_quoting at the same module. --- crates/vespertide-query/src/sql/helpers.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs index 194218f3..0b9fbfd0 100644 --- a/crates/vespertide-query/src/sql/helpers.rs +++ b/crates/vespertide-query/src/sql/helpers.rs @@ -240,10 +240,13 @@ pub fn reference_action_sql(action: &ReferenceAction) -> &'static str { /// Convert a default value string to the appropriate backend-specific expression pub fn convert_default_for_backend(default: &str, backend: DatabaseBackend) -> String { - let lower = default.to_lowercase(); - - // UUID generation functions - if lower == "gen_random_uuid()" || lower == "uuid()" || lower == "lower(hex(randomblob(16)))" { + // UUID generation functions (case-insensitive match against ASCII literals + // — avoids the per-call `String` allocation that `to_lowercase()` would + // incur, mirroring the convention `needs_quoting` already uses below). + if default.eq_ignore_ascii_case("gen_random_uuid()") + || default.eq_ignore_ascii_case("uuid()") + || default.eq_ignore_ascii_case("lower(hex(randomblob(16)))") + { return match backend { DatabaseBackend::Postgres => "gen_random_uuid()".to_string(), DatabaseBackend::MySql => "(UUID())".to_string(), @@ -252,10 +255,10 @@ pub fn convert_default_for_backend(default: &str, backend: DatabaseBackend) -> S } // Timestamp functions (case-insensitive) - if lower == "current_timestamp()" - || lower == "now()" - || lower == "current_timestamp" - || lower == "getdate()" + if default.eq_ignore_ascii_case("current_timestamp()") + || default.eq_ignore_ascii_case("now()") + || default.eq_ignore_ascii_case("current_timestamp") + || default.eq_ignore_ascii_case("getdate()") { return "CURRENT_TIMESTAMP".to_string(); } From 1a67d242bda7f74203129ccad88379c7fcdf8277 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Jun 2026 22:06:47 +0900 Subject: [PATCH 013/109] retry-now#0010: quote_ident fast-path skips str::replace when identifier has no embedded delimiter --- crates/vespertide-query/src/sql/helpers.rs | 35 +++++++++++++++------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs index 0b9fbfd0..24c1a43a 100644 --- a/crates/vespertide-query/src/sql/helpers.rs +++ b/crates/vespertide-query/src/sql/helpers.rs @@ -685,16 +685,31 @@ pub fn get_enum_name(column_type: &ColumnType) -> Option<&str> { /// - `MySQL`: `` `identifier` `` (backticks; embedded `` ` `` escaped as ` `` `) #[must_use] pub fn quote_ident(name: &str, backend: DatabaseBackend) -> String { - match backend { - DatabaseBackend::Postgres | DatabaseBackend::Sqlite => { - let escaped = name.replace('"', "\"\""); - format!("\"{escaped}\"") - } - DatabaseBackend::MySql => { - let escaped = name.replace('`', "``"); - format!("`{escaped}`") - } - } + let delim = match backend { + DatabaseBackend::Postgres | DatabaseBackend::Sqlite => '"', + DatabaseBackend::MySql => '`', + }; + // Hot path: every valid identifier produced by the codebase carries no + // embedded quote char, so one exact-size allocation suffices and the + // `str::replace` + `format!` double-allocation is unnecessary. Mirrors + // the borrowed-fast-path / owned-slow-path pattern already used by + // `vespertide_core::sql_escape::escape_sql_string_literal`. + if !name.contains(delim) { + let mut out = String::with_capacity(name.len() + 2); + out.push(delim); + out.push_str(name); + out.push(delim); + return out; + } + // Slow path (defense-in-depth): identifier embeds the quote char and + // must be escaped by doubling it. Byte-identical output to the + // pre-fast-path implementation. + let escaped = name.replace(delim, &format!("{delim}{delim}")); + let mut out = String::with_capacity(escaped.len() + 2); + out.push(delim); + out.push_str(&escaped); + out.push(delim); + out } /// Quote a list of identifiers and join them with comma. From 8d310202f3d031b132b9dbdf1945335671dfefc5 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Jun 2026 23:42:46 +0900 Subject: [PATCH 014/109] retry-now#0011: remove dead public relation-naming API + tests from vespertide-naming Delete five pub fns that had zero production callers anywhere in the workspace (every ORM exporter ships its own private to_pascal_case / pluralize / relation-naming helpers): - extract_relation_prefix - build_reverse_relation_field_name - build_relation_enum_name - to_pascal_case - pluralize Plus their 13 unit tests + 1 proptest block + the now-unused 'use proptest::prelude::*;' import inside mod tests. Leaves the surviving Constraint Naming section (build_index_name, build_unique_constraint_name, build_foreign_key_name, build_check_constraint_name, build_enum_type_name) and its 18 unit tests entirely untouched. lib.rs: 638 -> 234 lines (-404). No wire-format / JSON schema impact (Rust-only helpers); schema-gen drift remains 0. --- crates/vespertide-naming/src/lib.rs | 404 ---------------------------- 1 file changed, 404 deletions(-) diff --git a/crates/vespertide-naming/src/lib.rs b/crates/vespertide-naming/src/lib.rs index ebc36e57..39597d3e 100644 --- a/crates/vespertide-naming/src/lib.rs +++ b/crates/vespertide-naming/src/lib.rs @@ -4,198 +4,6 @@ //! indexes, constraints, and foreign keys. It has no dependencies and can be //! used by any other vespertide crate. -// ============================================================================ -// Relation Naming (for ORM exporters) -// ============================================================================ - -/// Extract semantic prefix from FK column for reverse relation naming. -/// -/// Given an FK column name, the current (target) table name, and the referenced -/// column name (e.g., "id", "idx"), extracts the semantic role portion. -/// -/// # Arguments -/// * `fk_column` - The FK column name (e.g., "`user_id`", "`answered_by_user_id`", "`author_id`") -/// * `current_table` - The table being referenced (e.g., "user") -/// * `ref_column` - The referenced column name (e.g., "id", "idx", "pk") -/// -/// # Returns -/// The semantic prefix (empty string for default FK, or the role/prefix for others) -/// -/// # Examples -/// ``` -/// use vespertide_naming::extract_relation_prefix; -/// -/// // Default FK: column matches table name + ref_column suffix -/// assert_eq!(extract_relation_prefix("user_id", "user", "id"), ""); -/// assert_eq!(extract_relation_prefix("user_idx", "user", "idx"), ""); -/// -/// // Prefixed FK: has semantic prefix before table name -/// assert_eq!(extract_relation_prefix("answered_by_user_id", "user", "id"), "answered_by"); -/// assert_eq!(extract_relation_prefix("target_user_id", "user", "id"), "target"); -/// -/// // Role FK: column doesn't end with table name -/// assert_eq!(extract_relation_prefix("author_id", "user", "id"), "author"); -/// assert_eq!(extract_relation_prefix("owner_id", "user", "id"), "owner"); -/// ``` -pub fn extract_relation_prefix(fk_column: &str, current_table: &str, ref_column: &str) -> String { - // Build the suffix to strip: _{ref_column} (e.g., "_id", "_idx") - let ref_suffix = format!("_{ref_column}"); - - // Remove the ref_column suffix if present - let without_ref = if let Some(stripped) = fk_column.strip_suffix(&ref_suffix) { - stripped - } else { - fk_column - }; - - let current_lower = current_table.to_lowercase(); - let without_ref_lower = without_ref.to_lowercase(); - - // Case 1: FK column exactly matches current table (e.g., "user_id" for table "user") - // This is the "default" FK - return empty prefix - if without_ref_lower == current_lower { - return String::new(); - } - - // Case 2: FK column ends with _{current_table} (e.g., "answered_by_user_id" for table "user") - // Strip the _{table} suffix to get the semantic prefix - let table_suffix = format!("_{current_lower}"); - if without_ref_lower.ends_with(&table_suffix) { - let prefix_chars = without_ref_lower.chars().count() - table_suffix.chars().count(); - return without_ref.chars().take(prefix_chars).collect(); - } - - // Case 3: FK column is a different role (e.g., "author_id" for table "user") - // Use the column name as the prefix - without_ref.to_string() -} - -/// Generate reverse relation field name for `has_many/has_one` relations. -/// -/// # Arguments -/// * `fk_columns` - The FK column names -/// * `current_table` - The table being referenced (e.g., "user") -/// * `source_table` - The table that has the FK (e.g., "inquiry") -/// * `ref_column` - The referenced column name (e.g., "id") -/// * `has_multiple_fks` - Whether `source_table` has multiple FKs to `current_table` -/// * `is_one_to_one` - Whether this is a `has_one` relation -/// -/// # Returns -/// The field name (e.g., "inquiries", "`answered_by_inquiries`") -pub fn build_reverse_relation_field_name( - fk_columns: &[String], - current_table: &str, - source_table: &str, - ref_column: &str, - has_multiple_fks: bool, - is_one_to_one: bool, -) -> String { - let base_name = if is_one_to_one { - source_table.to_string() - } else { - pluralize(source_table) - }; - - if !has_multiple_fks || fk_columns.is_empty() { - return base_name; - } - - let prefix = extract_relation_prefix(&fk_columns[0], current_table, ref_column); - - if prefix.is_empty() { - base_name - } else { - format!("{prefix}_{base_name}") - } -} - -/// Generate relation enum name for FK relations. -/// -/// Uses the same logic as field naming but converts to `PascalCase`. -/// This ensures `relation_enum` aligns with field names for consistency. -/// -/// # Examples -/// ``` -/// use vespertide_naming::build_relation_enum_name; -/// -/// assert_eq!(build_relation_enum_name(&["user_id".into()], "user", "id"), ""); -/// assert_eq!(build_relation_enum_name(&["answered_by_user_id".into()], "user", "id"), "AnsweredBy"); -/// assert_eq!(build_relation_enum_name(&["author_id".into()], "user", "id"), "Author"); -/// ``` -pub fn build_relation_enum_name( - fk_columns: &[String], - current_table: &str, - ref_column: &str, -) -> String { - if fk_columns.is_empty() { - return String::new(); - } - - let prefix = extract_relation_prefix(&fk_columns[0], current_table, ref_column); - - if prefix.is_empty() { - String::new() - } else { - to_pascal_case(&prefix) - } -} - -/// Convert `snake_case` to `PascalCase`. -/// -/// # Examples -/// ``` -/// use vespertide_naming::to_pascal_case; -/// -/// assert_eq!(to_pascal_case("hello_world"), "HelloWorld"); -/// assert_eq!(to_pascal_case("answered_by"), "AnsweredBy"); -/// assert_eq!(to_pascal_case("user"), "User"); -/// ``` -pub fn to_pascal_case(s: &str) -> String { - let mut result = String::new(); - let mut capitalize = true; - for c in s.chars() { - let is_separator = c == '_' || c == '-'; - if is_separator { - capitalize = true; - continue; - } - let ch = if capitalize { - c.to_ascii_uppercase() - } else { - c - }; - capitalize = false; - result.push(ch); - } - result -} - -/// Simple pluralization for relation field names. -/// -/// # Examples -/// ``` -/// use vespertide_naming::pluralize; -/// -/// assert_eq!(pluralize("inquiry"), "inquiries"); -/// assert_eq!(pluralize("comment"), "comments"); -/// assert_eq!(pluralize("status"), "status"); -/// ``` -pub fn pluralize(name: &str) -> String { - if name.ends_with('s') || name.ends_with("es") { - name.to_string() - } else if name.ends_with('y') - && !name.ends_with("ay") - && !name.ends_with("ey") - && !name.ends_with("oy") - && !name.ends_with("uy") - { - // e.g., category -> categories, inquiry -> inquiries - format!("{}ies", name.strip_suffix('y').unwrap_or(name)) - } else { - format!("{name}s") - } -} - // ============================================================================ // Constraint Naming (for SQL generation) // ============================================================================ @@ -267,218 +75,6 @@ pub fn build_enum_type_name(table: &str, enum_name: &str) -> String { #[cfg(test)] mod tests { use super::*; - use proptest::prelude::*; - - // ======================================================================== - // Relation Naming Tests - // ======================================================================== - - #[test] - fn test_extract_relation_prefix_default_fk() { - // Default FK: column matches table name + ref_column suffix - assert_eq!(extract_relation_prefix("user_id", "user", "id"), ""); - assert_eq!(extract_relation_prefix("org_id", "org", "id"), ""); - assert_eq!(extract_relation_prefix("post_id", "post", "id"), ""); - } - - #[test] - fn test_extract_relation_prefix_different_ref_column() { - // Handle different ref_column suffixes (not just _id) - assert_eq!(extract_relation_prefix("user_idx", "user", "idx"), ""); - assert_eq!(extract_relation_prefix("user_pk", "user", "pk"), ""); - assert_eq!(extract_relation_prefix("user_key", "user", "key"), ""); - } - - #[test] - fn test_extract_relation_prefix_semantic_prefix() { - // Prefixed FK: has semantic prefix before table name - assert_eq!( - extract_relation_prefix("answered_by_user_id", "user", "id"), - "answered_by" - ); - assert_eq!( - extract_relation_prefix("created_by_user_id", "user", "id"), - "created_by" - ); - assert_eq!( - extract_relation_prefix("target_user_id", "user", "id"), - "target" - ); - assert_eq!( - extract_relation_prefix("parent_org_id", "org", "id"), - "parent" - ); - } - - #[test] - fn test_extract_relation_prefix_role_fk() { - // Role FK: column doesn't end with table name - assert_eq!(extract_relation_prefix("author_id", "user", "id"), "author"); - assert_eq!(extract_relation_prefix("owner_id", "user", "id"), "owner"); - assert_eq!( - extract_relation_prefix("creator_id", "user", "id"), - "creator" - ); - } - - #[test] - fn test_extract_relation_prefix_no_suffix() { - // Edge case: no ref_column suffix - assert_eq!(extract_relation_prefix("user", "user", "id"), ""); - assert_eq!(extract_relation_prefix("admin_user", "user", "id"), "admin"); - } - - #[test] - fn test_extract_relation_prefix_unicode_table_name() { - assert_eq!( - extract_relation_prefix("작성자_한국어테이블_id", "한국어테이블", "id"), - "작성자" - ); - assert_eq!(extract_relation_prefix("📊_stats_id", "📊_stats", "id"), ""); - } - - #[test] - fn test_pluralize_unicode_name_ending_with_ascii_y() { - assert_eq!(pluralize("café_category"), "café_categories"); - } - - proptest! { - #[test] - fn extract_relation_prefix_does_not_panic_on_unicode( - table in proptest::collection::vec(any::(), 0..30).prop_map(|v| v.into_iter().collect::()), - prefix in proptest::collection::vec(any::(), 0..30).prop_map(|v| v.into_iter().collect::()) - ) { - let fk_column = format!("{prefix}_{table}_id"); - let _ = extract_relation_prefix(&fk_column, &table, "id"); - } - } - - #[test] - fn test_build_reverse_relation_field_name_single_fk() { - // Single FK - just use source table name - assert_eq!( - build_reverse_relation_field_name( - &["user_id".into()], - "user", - "inquiry", - "id", - false, - false - ), - "inquiries" - ); - assert_eq!( - build_reverse_relation_field_name( - &["author_id".into()], - "user", - "comment", - "id", - false, - false - ), - "comments" - ); - } - - #[test] - fn test_build_reverse_relation_field_name_multiple_fks() { - // Multiple FKs - need disambiguation - assert_eq!( - build_reverse_relation_field_name( - &["user_id".into()], - "user", - "inquiry", - "id", - true, - false - ), - "inquiries" - ); - assert_eq!( - build_reverse_relation_field_name( - &["answered_by_user_id".into()], - "user", - "inquiry", - "id", - true, - false - ), - "answered_by_inquiries" - ); - } - - #[test] - fn test_build_reverse_relation_field_name_one_to_one() { - assert_eq!( - build_reverse_relation_field_name( - &["user_id".into()], - "user", - "profile", - "id", - false, - true - ), - "profile" - ); - assert_eq!( - build_reverse_relation_field_name( - &["backup_user_id".into()], - "user", - "settings", - "id", - true, - true - ), - "backup_settings" - ); - } - - #[test] - fn test_build_relation_enum_name() { - // Empty fk_columns - early return - assert_eq!(build_relation_enum_name(&[], "user", "id"), ""); - - // Default FK - empty enum name (not needed or use table name) - assert_eq!( - build_relation_enum_name(&["user_id".into()], "user", "id"), - "" - ); - - // Semantic prefix - PascalCase - assert_eq!( - build_relation_enum_name(&["answered_by_user_id".into()], "user", "id"), - "AnsweredBy" - ); - assert_eq!( - build_relation_enum_name(&["target_user_id".into()], "user", "id"), - "Target" - ); - - // Role FK - PascalCase of role - assert_eq!( - build_relation_enum_name(&["author_id".into()], "user", "id"), - "Author" - ); - } - - #[test] - fn test_to_pascal_case() { - assert_eq!(to_pascal_case("hello_world"), "HelloWorld"); - assert_eq!(to_pascal_case("answered_by"), "AnsweredBy"); - assert_eq!(to_pascal_case("user"), "User"); - assert_eq!(to_pascal_case("hello-world"), "HelloWorld"); - assert_eq!(to_pascal_case(""), ""); - } - - #[test] - fn test_pluralize() { - assert_eq!(pluralize("inquiry"), "inquiries"); - assert_eq!(pluralize("category"), "categories"); - assert_eq!(pluralize("comment"), "comments"); - assert_eq!(pluralize("user"), "users"); - assert_eq!(pluralize("status"), "status"); - assert_eq!(pluralize("address"), "address"); - } // ======================================================================== // Constraint Naming Tests From 325723a2278cd1b178f946c1ffedc077685019a3 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Jun 2026 02:39:33 +0900 Subject: [PATCH 015/109] retry-now#0013: consolidate vespertide-naming constraint-name builders and drop intermediate join allocation --- crates/vespertide-naming/src/lib.rs | 62 +++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/crates/vespertide-naming/src/lib.rs b/crates/vespertide-naming/src/lib.rs index 39597d3e..de7e4bc2 100644 --- a/crates/vespertide-naming/src/lib.rs +++ b/crates/vespertide-naming/src/lib.rs @@ -13,10 +13,7 @@ /// Uses double underscore to separate table name from the rest. /// Format: ix_{table}__{key} or ix_{table}__{col1}_{col2}... pub fn build_index_name>(table: &str, columns: &[T], key: Option<&str>) -> String { - match key { - Some(k) => format!("ix_{table}__{k}"), - None => format!("ix_{}__{}", table, sort_columns_for_name(columns).join("_")), - } + build_constraint_name("ix_", table, columns, key) } /// Generate unique constraint name from table name, columns, and optional user-provided key. @@ -28,10 +25,7 @@ pub fn build_unique_constraint_name>( columns: &[T], key: Option<&str>, ) -> String { - match key { - Some(k) => format!("uq_{table}__{k}"), - None => format!("uq_{}__{}", table, sort_columns_for_name(columns).join("_")), - } + build_constraint_name("uq_", table, columns, key) } /// Generate foreign key constraint name from table name, columns, and optional user-provided key. @@ -43,16 +37,58 @@ pub fn build_foreign_key_name>( columns: &[T], key: Option<&str>, ) -> String { - match key { - Some(k) => format!("fk_{table}__{k}"), - None => format!("fk_{}__{}", table, sort_columns_for_name(columns).join("_")), + build_constraint_name("fk_", table, columns, key) +} + +/// Shared body for the three constraint name builders above. +/// +/// Folds the `{prefix}{table}__{key|sorted-columns}` template into a single +/// pre-sized `String` so the auto-named branch ( `key.is_none()`) does only +/// two allocations: the column-sort scratchpad (`Vec<&str>`) and the final +/// `String`. The previous implementation went through `format!(... join("_"))` +/// which allocated an extra intermediate `String` for the joined columns +/// before formatting them into the final result. +fn build_constraint_name>( + prefix: &str, + table: &str, + columns: &[T], + key: Option<&str>, +) -> String { + if let Some(k) = key { + let mut out = String::with_capacity(prefix.len() + table.len() + 2 + k.len()); + out.push_str(prefix); + out.push_str(table); + out.push_str("__"); + out.push_str(k); + out + } else { + let cols_capacity: usize = columns + .iter() + .map(|c| c.as_ref().len() + 1) + .sum::() + .saturating_sub(1); + let mut out = String::with_capacity(prefix.len() + table.len() + 2 + cols_capacity); + out.push_str(prefix); + out.push_str(table); + out.push_str("__"); + write_sorted_columns(&mut out, columns); + out } } -fn sort_columns_for_name>(columns: &[T]) -> Vec<&str> { +/// Sort the column slice into a local scratchpad and write the columns into +/// `out` joined by `'_'`. Replaces the previous `sort_columns_for_name(...).join("_")` +/// pair which allocated a fresh `String` for the joined columns; here the +/// columns go directly into the caller-supplied buffer. +fn write_sorted_columns>(out: &mut String, columns: &[T]) { let mut sorted: Vec<&str> = columns.iter().map(AsRef::as_ref).collect(); sorted.sort_unstable(); - sorted + for (i, c) in sorted.iter().enumerate() { + if i > 0 { + out.push('_'); + } + out.push_str(c); + } } /// Generate CHECK constraint name for `SQLite` enum column. From 8b879035da73799a58ab9b3fdd3f8f7a9504a83b Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Jun 2026 21:14:08 +0900 Subject: [PATCH 016/109] retry-now#0015: batch -- postgres_only helper, drop dead to_sql_values, TableName::with_prefix (3 kept) --- crates/vespertide-core/src/action/prefix.rs | 35 +++++------- crates/vespertide-core/src/schema/column.rs | 15 +----- .../vespertide-core/src/schema/constraint.rs | 2 +- crates/vespertide-core/src/schema/mod.rs | 54 +++---------------- crates/vespertide-core/src/schema/names.rs | 33 ++++++++++++ crates/vespertide-query/src/sql/helpers.rs | 12 +---- .../src/sql/modify_column_type/direct.rs | 48 ++++++++--------- crates/vespertide-query/src/sql/types.rs | 16 ++++++ 8 files changed, 94 insertions(+), 121 deletions(-) diff --git a/crates/vespertide-core/src/action/prefix.rs b/crates/vespertide-core/src/action/prefix.rs index bd9da1f0..3dd46fe1 100644 --- a/crates/vespertide-core/src/action/prefix.rs +++ b/crates/vespertide-core/src/action/prefix.rs @@ -1,5 +1,4 @@ use super::{MigrationAction, MigrationPlan}; -use crate::schema::TableName; impl MigrationPlan { /// Apply a prefix to all table names in the migration plan. @@ -37,7 +36,7 @@ fn prefix_migration_action(action: MigrationAction, prefix: &str) -> MigrationAc columns, constraints, } => MigrationAction::CreateTable { - table: add_prefix(table, prefix), + table: table.with_prefix(prefix), columns, constraints: constraints .into_iter() @@ -45,11 +44,11 @@ fn prefix_migration_action(action: MigrationAction, prefix: &str) -> MigrationAc .collect(), }, MigrationAction::DeleteTable { table } => MigrationAction::DeleteTable { - table: add_prefix(table, prefix), + table: table.with_prefix(prefix), }, MigrationAction::RenameTable { from, to } => MigrationAction::RenameTable { - from: add_prefix(from, prefix), - to: add_prefix(to, prefix), + from: from.with_prefix(prefix), + to: to.with_prefix(prefix), }, MigrationAction::RawSql { sql } => MigrationAction::RawSql { sql }, action => prefix_column_or_constraint_action(action, prefix), @@ -63,17 +62,17 @@ fn prefix_column_or_constraint_action(action: MigrationAction, prefix: &str) -> column, fill_with, } => MigrationAction::AddColumn { - table: add_prefix(table, prefix), + table: table.with_prefix(prefix), column, fill_with, }, MigrationAction::RenameColumn { table, from, to } => MigrationAction::RenameColumn { - table: add_prefix(table, prefix), + table: table.with_prefix(prefix), from, to, }, MigrationAction::DeleteColumn { table, column } => MigrationAction::DeleteColumn { - table: add_prefix(table, prefix), + table: table.with_prefix(prefix), column, }, MigrationAction::ModifyColumnType { @@ -84,7 +83,7 @@ fn prefix_column_or_constraint_action(action: MigrationAction, prefix: &str) -> narrowing_strategy, timezone, } => MigrationAction::ModifyColumnType { - table: add_prefix(table, prefix), + table: table.with_prefix(prefix), column, new_type, fill_with, @@ -98,7 +97,7 @@ fn prefix_column_or_constraint_action(action: MigrationAction, prefix: &str) -> fill_with, delete_null_rows, } => MigrationAction::ModifyColumnNullable { - table: add_prefix(table, prefix), + table: table.with_prefix(prefix), column, nullable, fill_with, @@ -116,7 +115,7 @@ fn prefix_remaining_action(action: MigrationAction, prefix: &str) -> MigrationAc new_default, backfill, } => MigrationAction::ModifyColumnDefault { - table: add_prefix(table, prefix), + table: table.with_prefix(prefix), column, new_default, backfill, @@ -126,23 +125,23 @@ fn prefix_remaining_action(action: MigrationAction, prefix: &str) -> MigrationAc column, new_comment, } => MigrationAction::ModifyColumnComment { - table: add_prefix(table, prefix), + table: table.with_prefix(prefix), column, new_comment, }, MigrationAction::AddConstraint { table, constraint } => MigrationAction::AddConstraint { - table: format!("{prefix}{table}").into(), + table: table.with_prefix(prefix), constraint: constraint.with_prefix(prefix), }, MigrationAction::RemoveConstraint { table, constraint } => { MigrationAction::RemoveConstraint { - table: add_prefix(table, prefix), + table: table.with_prefix(prefix), constraint: constraint.with_prefix(prefix), } } MigrationAction::ReplaceConstraint { table, from, to } => { MigrationAction::ReplaceConstraint { - table: add_prefix(table, prefix), + table: table.with_prefix(prefix), from: from.with_prefix(prefix), to: to.with_prefix(prefix), } @@ -151,12 +150,6 @@ fn prefix_remaining_action(action: MigrationAction, prefix: &str) -> MigrationAc } } -fn add_prefix(table: TableName, prefix: &str) -> TableName { - let mut table = table.into_inner(); - table.insert_str(0, prefix); - table.into() -} - #[cfg(test)] mod tests { //! Coverage-closure tests for the `RawSql` arm of `prefix_migration_action`. diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs index 46565f9a..28cb545e 100644 --- a/crates/vespertide-core/src/schema/column.rs +++ b/crates/vespertide-core/src/schema/column.rs @@ -458,25 +458,12 @@ impl EnumValues { } } - /// Get SQL values for CREATE TYPE ENUM (only for string enums) - /// Returns quoted strings like 'value1', 'value2' - pub fn to_sql_values(&self) -> Vec { - match self { - EnumValues::String(values) => values - .iter() - .map(|s| format!("'{}'", crate::escape_sql_string_literal(s))) - .collect(), - EnumValues::Integer(values) => values.iter().map(|v| v.value.to_string()).collect(), - } - } - /// Format every variant for `CREATE TYPE … AS ENUM(...)` / /// `CHECK (col IN (...))` and join with `separator`, writing into one /// buffer — no intermediate `Vec` allocation. /// /// Mirrors `vespertide_query::sql::helpers::quote_idents` and - /// `vespertide_core::schema::names::join_column_names`. Output is - /// byte-identical to `self.to_sql_values().join(separator)`. + /// `vespertide_core::schema::names::join_column_names`. /// /// ```rust /// use vespertide_core::{EnumValues, NumValue}; diff --git a/crates/vespertide-core/src/schema/constraint.rs b/crates/vespertide-core/src/schema/constraint.rs index b7693ef5..25fb4957 100644 --- a/crates/vespertide-core/src/schema/constraint.rs +++ b/crates/vespertide-core/src/schema/constraint.rs @@ -227,7 +227,7 @@ impl TableConstraint { } => TableConstraint::ForeignKey { name, columns, - ref_table: format!("{prefix}{ref_table}").into(), + ref_table: ref_table.with_prefix(prefix), ref_columns, on_delete, on_update, diff --git a/crates/vespertide-core/src/schema/mod.rs b/crates/vespertide-core/src/schema/mod.rs index 2a3a946a..aff5693b 100644 --- a/crates/vespertide-core/src/schema/mod.rs +++ b/crates/vespertide-core/src/schema/mod.rs @@ -269,49 +269,11 @@ mod tests { assert_eq!(non_empty_int.len(), 2); } - #[test] - fn test_enum_values_to_sql_values_string() { - let vals = EnumValues::String(vec!["active".into(), "pending".into()]); - assert_eq!(vals.to_sql_values(), vec!["'active'", "'pending'"]); - } - - #[test] - fn to_sql_values_escapes_single_quotes() { - let vals = - EnumValues::String(vec!["O'Brien".into(), "Smith".into(), "'leading".into()]); - let sql = vals.to_sql_values(); - - assert!( - sql.iter().any(|s| s == "'O''Brien'"), - "single quote inside value must be doubled" - ); - assert!( - sql.iter().any(|s| s == "'''leading'"), - "leading single quote must be doubled" - ); - assert!( - sql.iter().any(|s| s == "'Smith'"), - "values without quotes are unchanged" - ); - } - - #[test] - fn test_enum_values_to_sql_values_integer() { - let vals = EnumValues::Integer(vec![ - NumValue { - name: "Low".into(), - value: 0, - }, - NumValue { - name: "High".into(), - value: 10, - }, - ]); - assert_eq!(vals.to_sql_values(), vec!["0", "10"]); - } - #[rstest] - // Matches `to_sql_values().join(sep)` byte-for-byte across every arm. + // Locks the buffer-push helper's expected output across every arm. + // Prior versions also cross-checked against a now-deleted + // `to_sql_values() -> Vec` pipeline; the case data below is + // the actual contract. #[case::string_two_variants( EnumValues::String(vec!["active".into(), "pending".into()]), ", ", @@ -342,16 +304,12 @@ mod tests { "_", "'a'_'b'_'c'", )] - fn sql_values_joined_matches_to_sql_values_join( + fn sql_values_joined_locks_expected_output( #[case] vals: EnumValues, #[case] separator: &str, #[case] expected: &str, ) { - let joined = vals.sql_values_joined(separator); - assert_eq!(joined, expected); - // Equivalence lock: the buffer-push helper must be byte-identical - // to the legacy `Vec` + `join` pipeline. - assert_eq!(joined, vals.to_sql_values().join(separator)); + assert_eq!(vals.sql_values_joined(separator), expected); } #[test] diff --git a/crates/vespertide-core/src/schema/names.rs b/crates/vespertide-core/src/schema/names.rs index f7069074..c5026de0 100644 --- a/crates/vespertide-core/src/schema/names.rs +++ b/crates/vespertide-core/src/schema/names.rs @@ -182,6 +182,39 @@ impl_name_newtype!(TableName); impl_name_newtype!(ColumnName); impl_name_newtype!(IndexName); +impl TableName { + /// Prepend `prefix` to this table name in place, reusing the existing + /// `String` allocation when capacity allows. No-op when + /// `prefix.is_empty()`. + /// + /// Unifies the two pre-existing prepend patterns in + /// `vespertide-core` (`action/prefix.rs`'s private `add_prefix` and + /// `schema/constraint.rs`'s open-coded + /// `format!("{prefix}{table}").into()`) — every "prefix a `TableName`" + /// site now reads the same way and avoids the fresh `format!` + /// allocation the latter shape always paid. + /// + /// ```rust + /// use vespertide_core::schema::names::TableName; + /// + /// let prefixed = TableName::new("user").with_prefix("tenant_"); + /// assert_eq!(prefixed.as_str(), "tenant_user"); + /// + /// // Empty prefix is a pure no-op. + /// let unchanged = TableName::new("user").with_prefix(""); + /// assert_eq!(unchanged.as_str(), "user"); + /// ``` + #[must_use] + pub fn with_prefix(self, prefix: &str) -> Self { + if prefix.is_empty() { + return self; + } + let mut s = self.0; + s.insert_str(0, prefix); + Self(s) + } +} + /// Join a slice of [`ColumnName`]s with a separator using a single buffer /// — no intermediate `Vec` allocation. /// diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs index 24c1a43a..ff9ddaa4 100644 --- a/crates/vespertide-query/src/sql/helpers.rs +++ b/crates/vespertide-query/src/sql/helpers.rs @@ -464,11 +464,7 @@ pub fn build_create_enum_type_sql( // MySQL: ENUMs are inline, no CREATE TYPE needed // SQLite: Uses TEXT, no CREATE TYPE needed - Some(super::types::RawSql::per_backend( - pg_sql, - String::new(), - String::new(), - )) + Some(super::types::RawSql::postgres_only(pg_sql)) } else { None } @@ -491,11 +487,7 @@ pub fn build_drop_enum_type_sql( let pg_sql = format!("DROP TYPE {type_name}"); // MySQL/SQLite: No action needed - Some(super::types::RawSql::per_backend( - pg_sql, - String::new(), - String::new(), - )) + Some(super::types::RawSql::postgres_only(pg_sql)) } else { None } diff --git a/crates/vespertide-query/src/sql/modify_column_type/direct.rs b/crates/vespertide-query/src/sql/modify_column_type/direct.rs index fcfcba28..91466543 100644 --- a/crates/vespertide-query/src/sql/modify_column_type/direct.rs +++ b/crates/vespertide-query/src/sql/modify_column_type/direct.rs @@ -112,45 +112,41 @@ fn build_postgres_enum_migration(queries: &mut Vec, context: &Direct let quoted_table = quote_ident(context.table, DatabaseBackend::Postgres); let quoted_column = quote_ident(context.column, DatabaseBackend::Postgres); let quoted_old_type = quote_ident(&old_type_name, DatabaseBackend::Postgres); - queries.push(BuiltQuery::Raw(RawSql::per_backend( - format!("CREATE TYPE {quoted_target_type} AS ENUM ({create_values})"), - String::new(), - String::new(), - ))); + queries.push(BuiltQuery::Raw(RawSql::postgres_only(format!( + "CREATE TYPE {quoted_target_type} AS ENUM ({create_values})" + )))); // 2. DROP DEFAULT if exists (must be done before type change). if column_default.is_some() { - queries.push(BuiltQuery::Raw(RawSql::per_backend( - format!("ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} DROP DEFAULT"), - String::new(), - String::new(), - ))); + queries.push(BuiltQuery::Raw(RawSql::postgres_only(format!( + "ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} DROP DEFAULT" + )))); } // 3. ALTER TABLE ... ALTER COLUMN ... TYPE target_type USING col::text::target_type. - queries.push(BuiltQuery::Raw(RawSql::per_backend(format!("ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} TYPE {quoted_target_type} USING {quoted_column}::text::{quoted_target_type}"), String::new(), String::new()))); + queries.push(BuiltQuery::Raw(RawSql::postgres_only(format!( + "ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} TYPE {quoted_target_type} USING {quoted_column}::text::{quoted_target_type}" + )))); // 4. DROP old enum type. - queries.push(BuiltQuery::Raw(RawSql::per_backend( - format!("DROP TYPE {quoted_old_type}"), - String::new(), - String::new(), - ))); + queries.push(BuiltQuery::Raw(RawSql::postgres_only(format!( + "DROP TYPE {quoted_old_type}" + )))); // 5. RENAME temp to final (only for same-name value changes). if needs_rename { - queries.push(BuiltQuery::Raw(RawSql::per_backend( - format!("ALTER TYPE {quoted_target_type} RENAME TO {quoted_old_type}"), - String::new(), - String::new(), - ))); + queries.push(BuiltQuery::Raw(RawSql::postgres_only(format!( + "ALTER TYPE {quoted_target_type} RENAME TO {quoted_old_type}" + )))); } // 6. Restore DEFAULT if it existed. if let Some(default_value) = column_default { let normalized_default = normalize_enum_default(context.new_type, &default_value.to_sql()); - queries.push(BuiltQuery::Raw(RawSql::per_backend(format!("ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} SET DEFAULT {normalized_default}"), String::new(), String::new()))); + queries.push(BuiltQuery::Raw(RawSql::postgres_only(format!( + "ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} SET DEFAULT {normalized_default}" + )))); } } } @@ -245,11 +241,9 @@ fn drop_old_enum_type_if_needed(queries: &mut Vec, context: &DirectB if should_drop { let old_type_name = crate::sql::helpers::build_enum_type_name(context.table, old_name); let old_type_name = quote_ident(&old_type_name, DatabaseBackend::Postgres); - queries.push(BuiltQuery::Raw(RawSql::per_backend( - format!("DROP TYPE {old_type_name}"), - String::new(), - String::new(), - ))); + queries.push(BuiltQuery::Raw(RawSql::postgres_only(format!( + "DROP TYPE {old_type_name}" + )))); } } } diff --git a/crates/vespertide-query/src/sql/types.rs b/crates/vespertide-query/src/sql/types.rs index 4f91c79b..b8f8646b 100644 --- a/crates/vespertide-query/src/sql/types.rs +++ b/crates/vespertide-query/src/sql/types.rs @@ -48,6 +48,22 @@ impl RawSql { sqlite, } } + + /// Create a `RawSql` carrying `PostgreSQL`-only SQL; `MySQL` and `SQLite` + /// get no output. + /// + /// Replaces the repeated 3-line construction + /// `RawSql::per_backend(pg, String::new(), String::new())` so the + /// PG-only intent is visible at the call site (and the two + /// zero-capacity `String::new()` allocations are written once, in + /// this constructor). + pub fn postgres_only(sql: String) -> Self { + Self { + postgres: sql, + mysql: String::new(), + sqlite: String::new(), + } + } } impl BuiltQuery { From 6dc66e1f06f93015920f4cae7f24544cd2b8ecb7 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Jun 2026 21:53:30 +0900 Subject: [PATCH 017/109] retry-now#0016: builder/foreign_key dedup -- hoist pending_constraints_for_action, extract builder test_support, drop nn_col shadow (3 kept) --- crates/vespertide-query/src/builder/mod.rs | 42 +++++++- .../vespertide-query/src/builder/parallel.rs | 99 +------------------ .../src/builder/sequential.rs | 98 +----------------- .../src/builder/test_support.rs | 74 ++++++++++++++ .../src/sql/add_constraint/foreign_key.rs | 12 ++- 5 files changed, 129 insertions(+), 196 deletions(-) create mode 100644 crates/vespertide-query/src/builder/test_support.rs diff --git a/crates/vespertide-query/src/builder/mod.rs b/crates/vespertide-query/src/builder/mod.rs index 263dbe11..15075959 100644 --- a/crates/vespertide-query/src/builder/mod.rs +++ b/crates/vespertide-query/src/builder/mod.rs @@ -1,4 +1,4 @@ -use vespertide_core::{MigrationAction, MigrationPlan, TableDef}; +use vespertide_core::{MigrationAction, MigrationPlan, TableConstraint, TableDef}; use crate::DatabaseBackend; use crate::error::QueryError; @@ -7,6 +7,8 @@ use crate::sql::BuiltQuery; mod parallel; mod sequential; +#[cfg(test)] +mod test_support; mod transaction; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] @@ -48,6 +50,44 @@ fn action_target_table(action: &MigrationAction) -> Option<&str> { } } +/// Collect Index/Unique constraints that LATER actions in the same plan will +/// add to the same table. The scan starts strictly AFTER `action_index`, so an +/// `AddConstraint(Index)` action never sees itself as pending. The result lets +/// the SQLite temp-table rebuild defer recreating an index a later +/// `AddConstraint` will create, avoiding "index already exists" errors. +/// +/// Returns an empty vec for actions without a target table (`RawSql`, +/// `RenameTable`) — there is no table-scope to compare against. +pub(super) fn pending_constraints_for_action( + plan: &MigrationPlan, + action_index: usize, + action: &MigrationAction, +) -> Vec { + let Some(table) = action_target_table(action) else { + return vec![]; + }; + + plan.actions[action_index + 1..] + .iter() + .filter_map(|a| { + if let MigrationAction::AddConstraint { + table: t, + constraint, + } = a + && t == table + && matches!( + constraint, + TableConstraint::Index { .. } | TableConstraint::Unique { .. } + ) + { + Some(constraint.clone()) + } else { + None + } + }) + .collect() +} + /// Build SQL queries for a full migration plan with sequential schema evolution. /// /// Each action is built against the schema state AFTER previous actions have been diff --git a/crates/vespertide-query/src/builder/parallel.rs b/crates/vespertide-query/src/builder/parallel.rs index 51770e9e..013eca84 100644 --- a/crates/vespertide-query/src/builder/parallel.rs +++ b/crates/vespertide-query/src/builder/parallel.rs @@ -2,7 +2,7 @@ use rayon::prelude::*; use vespertide_core::{MigrationAction, MigrationPlan, TableConstraint, TableDef}; use vespertide_planner::apply_action; -use super::{PlanQueries, action_target_table}; +use super::{PlanQueries, pending_constraints_for_action}; use crate::DatabaseBackend; use crate::error::QueryError; use crate::parallel_config::PLAN_QUERY_PAR_ACTION_MIN_LEN; @@ -44,36 +44,6 @@ fn prepare_actions(plan: &MigrationPlan, current_schema: &[TableDef]) -> Vec
 Vec {
-    let Some(table) = action_target_table(action) else {
-        return vec![];
-    };
-
-    plan.actions[action_index + 1..]
-        .iter()
-        .filter_map(|a| {
-            if let MigrationAction::AddConstraint {
-                table: t,
-                constraint,
-            } = a
-                && t == table
-                && matches!(
-                    constraint,
-                    TableConstraint::Index { .. } | TableConstraint::Unique { .. }
-                )
-            {
-                Some(constraint.clone())
-            } else {
-                None
-            }
-        })
-        .collect()
-}
-
 fn build_prepared_action_queries(prepared: &PreparedAction) -> Result {
     let postgres_queries = build_action_queries_with_pending(
         DatabaseBackend::Postgres,
@@ -108,71 +78,11 @@ fn build_prepared_action_queries(prepared: &PreparedAction) -> Result ColumnDef {
-        ColumnDef::new(name, ColumnType::Simple(ty), false)
-    }
-
-    fn index(name: Option<&str>, column: &str) -> TableConstraint {
-        TableConstraint::Index {
-            name: name.map(Into::into),
-            columns: vec![column.into()],
-        }
-    }
-
-    fn foreign_key() -> TableConstraint {
-        TableConstraint::ForeignKey {
-            name: Some("fk_u__pk".into()),
-            columns: vec!["pk".into()],
-            ref_table: "other".into(),
-            ref_columns: vec!["id".into()],
-            on_delete: Some(ReferenceAction::Cascade),
-            on_update: None,
-            orphan_strategy: ForeignKeyOrphanStrategy::default(),
-        }
-    }
-
-    fn table(name: &str, constraints: Vec) -> TableDef {
-        TableDef {
-            name: name.into(),
-            description: None,
-            columns: vec![nn_col("pk", SimpleColumnType::Integer)],
-            constraints,
-        }
-    }
-
-    fn schema_u_with_constraints(constraints: Vec) -> Vec {
-        vec![table("u", constraints)]
-    }
-
-    fn schema_u_and_v_with_u_constraints(constraints: Vec) -> Vec {
-        vec![table("u", constraints), table("v", vec![])]
-    }
-
-    fn add_required_column(table: &str, column: &str) -> MigrationAction {
-        MigrationAction::AddColumn {
-            table: table.into(),
-            column: Box::new(nn_col(column, SimpleColumnType::Integer)),
-            fill_with: None,
-        }
-    }
-
-    fn sqlite_sql(queries: &[BuiltQuery]) -> String {
-        queries
-            .iter()
-            .map(|q| q.build(_Backend::Sqlite))
-            .collect::>()
-            .join("\n")
-    }
+    use vespertide_core::SimpleColumnType;
 
     /// The parallel `build_plan_queries_in_parallel` path activates when the
     /// plan size meets `VESPERTIDE_PLAN_QUERY_PAR_THRESHOLD`. The env override is
@@ -375,8 +285,5 @@ mod tests {
         unsafe {
             std::env::remove_var("VESPERTIDE_PLAN_QUERY_PAR_THRESHOLD");
         }
-
-        // touch the parallel-imported alias to keep the `use` warning-free
-        let _ = _Backend::Postgres;
     }
 }
diff --git a/crates/vespertide-query/src/builder/sequential.rs b/crates/vespertide-query/src/builder/sequential.rs
index a7e7ddbf..28c53f00 100644
--- a/crates/vespertide-query/src/builder/sequential.rs
+++ b/crates/vespertide-query/src/builder/sequential.rs
@@ -1,7 +1,7 @@
-use vespertide_core::{MigrationAction, MigrationPlan, TableConstraint, TableDef};
+use vespertide_core::{MigrationPlan, TableDef};
 use vespertide_planner::apply_action;
 
-use super::{PlanQueries, action_target_table};
+use super::{PlanQueries, pending_constraints_for_action};
 use crate::DatabaseBackend;
 use crate::error::QueryError;
 use crate::sql::build_action_queries_with_pending;
@@ -51,101 +51,11 @@ pub(super) fn build_plan_queries_sequentially(
     Ok(queries)
 }
 
-fn pending_constraints_for_action(
-    plan: &MigrationPlan,
-    action_index: usize,
-    action: &MigrationAction,
-) -> Vec {
-    let Some(table) = action_target_table(action) else {
-        return vec![];
-    };
-
-    plan.actions[action_index + 1..]
-        .iter()
-        .filter_map(|a| {
-            if let MigrationAction::AddConstraint {
-                table: t,
-                constraint,
-            } = a
-                && t == table
-                && matches!(
-                    constraint,
-                    TableConstraint::Index { .. } | TableConstraint::Unique { .. }
-                )
-            {
-                Some(constraint.clone())
-            } else {
-                None
-            }
-        })
-        .collect()
-}
-
 #[cfg(test)]
 mod tests {
+    use super::super::test_support::*;
     use super::{build_plan_queries_sequentially, pending_constraints_for_action};
-    use crate::DatabaseBackend;
-    use crate::sql::BuiltQuery;
-    use vespertide_core::{
-        ColumnDef, ColumnType, ForeignKeyOrphanStrategy, MigrationAction, MigrationPlan,
-        ReferenceAction, SimpleColumnType, TableConstraint, TableDef,
-    };
-
-    fn nn_col(name: &str, ty: SimpleColumnType) -> ColumnDef {
-        ColumnDef::new(name, ColumnType::Simple(ty), false)
-    }
-
-    fn index(name: Option<&str>, column: &str) -> TableConstraint {
-        TableConstraint::Index {
-            name: name.map(Into::into),
-            columns: vec![column.into()],
-        }
-    }
-
-    fn foreign_key() -> TableConstraint {
-        TableConstraint::ForeignKey {
-            name: Some("fk_u__pk".into()),
-            columns: vec!["pk".into()],
-            ref_table: "other".into(),
-            ref_columns: vec!["id".into()],
-            on_delete: Some(ReferenceAction::Cascade),
-            on_update: None,
-            orphan_strategy: ForeignKeyOrphanStrategy::default(),
-        }
-    }
-
-    fn table(name: &str, constraints: Vec) -> TableDef {
-        TableDef {
-            name: name.into(),
-            description: None,
-            columns: vec![nn_col("pk", SimpleColumnType::Integer)],
-            constraints,
-        }
-    }
-
-    fn schema_u_with_constraints(constraints: Vec) -> Vec {
-        vec![table("u", constraints)]
-    }
-
-    fn schema_u_and_v_with_u_constraints(constraints: Vec) -> Vec {
-        vec![table("u", constraints), table("v", vec![])]
-    }
-
-    fn add_required_column(table: &str, column: &str) -> MigrationAction {
-        MigrationAction::AddColumn {
-            table: table.into(),
-            column: Box::new(nn_col(column, SimpleColumnType::Integer)),
-            fill_with: None,
-        }
-    }
-
-    fn sqlite_sql(queries: &[BuiltQuery]) -> String {
-        queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect::>()
-            .join("\n")
-    }
+    use vespertide_core::{MigrationAction, MigrationPlan};
 
     /// The pending scan must start after the current action. If it starts at
     /// `i`, an `AddConstraint(Index)` sees itself as pending.
diff --git a/crates/vespertide-query/src/builder/test_support.rs b/crates/vespertide-query/src/builder/test_support.rs
new file mode 100644
index 00000000..37907552
--- /dev/null
+++ b/crates/vespertide-query/src/builder/test_support.rs
@@ -0,0 +1,74 @@
+//! Shared test-only helpers for `builder::parallel` and `builder::sequential`.
+//!
+//! Both submodules previously carried byte-identical copies of these eight
+//! helpers in their inline `mod tests` blocks. They now live here as
+//! `pub(super)` items, reachable from both child modules' tests via
+//! `use super::test_support::*;`.
+
+#![cfg(test)]
+
+use vespertide_core::{
+    ColumnDef, ColumnType, ForeignKeyOrphanStrategy, MigrationAction, ReferenceAction,
+    SimpleColumnType, TableConstraint, TableDef,
+};
+
+use crate::DatabaseBackend;
+use crate::sql::BuiltQuery;
+
+pub(super) fn nn_col(name: &str, ty: SimpleColumnType) -> ColumnDef {
+    ColumnDef::new(name, ColumnType::Simple(ty), false)
+}
+
+pub(super) fn index(name: Option<&str>, column: &str) -> TableConstraint {
+    TableConstraint::Index {
+        name: name.map(Into::into),
+        columns: vec![column.into()],
+    }
+}
+
+pub(super) fn foreign_key() -> TableConstraint {
+    TableConstraint::ForeignKey {
+        name: Some("fk_u__pk".into()),
+        columns: vec!["pk".into()],
+        ref_table: "other".into(),
+        ref_columns: vec!["id".into()],
+        on_delete: Some(ReferenceAction::Cascade),
+        on_update: None,
+        orphan_strategy: ForeignKeyOrphanStrategy::default(),
+    }
+}
+
+pub(super) fn table(name: &str, constraints: Vec) -> TableDef {
+    TableDef {
+        name: name.into(),
+        description: None,
+        columns: vec![nn_col("pk", SimpleColumnType::Integer)],
+        constraints,
+    }
+}
+
+pub(super) fn schema_u_with_constraints(constraints: Vec) -> Vec {
+    vec![table("u", constraints)]
+}
+
+pub(super) fn schema_u_and_v_with_u_constraints(
+    constraints: Vec,
+) -> Vec {
+    vec![table("u", constraints), table("v", vec![])]
+}
+
+pub(super) fn add_required_column(table: &str, column: &str) -> MigrationAction {
+    MigrationAction::AddColumn {
+        table: table.into(),
+        column: Box::new(nn_col(column, SimpleColumnType::Integer)),
+        fill_with: None,
+    }
+}
+
+pub(super) fn sqlite_sql(queries: &[BuiltQuery]) -> String {
+    queries
+        .iter()
+        .map(|q| q.build(DatabaseBackend::Sqlite))
+        .collect::>()
+        .join("\n")
+}
diff --git a/crates/vespertide-query/src/sql/add_constraint/foreign_key.rs b/crates/vespertide-query/src/sql/add_constraint/foreign_key.rs
index eb3b6a88..29460b57 100644
--- a/crates/vespertide-query/src/sql/add_constraint/foreign_key.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/foreign_key.rs
@@ -189,9 +189,7 @@ mod tests {
     use rstest::rstest;
     use vespertide_core::{ColumnDef, ColumnType, MigrationAction, SimpleColumnType};
 
-    fn nn_col(name: &str, ty: SimpleColumnType) -> ColumnDef {
-        ColumnDef::new(name, ColumnType::Simple(ty), false)
-    }
+    use crate::test_support::col_n;
 
     fn fk_constraint(strategy: ForeignKeyOrphanStrategy) -> TableConstraint {
         TableConstraint::ForeignKey {
@@ -210,14 +208,18 @@ mod tests {
             TableDef {
                 name: "users".into(),
                 description: None,
-                columns: vec![nn_col("id", SimpleColumnType::Integer)],
+                columns: vec![col_n(
+                    "id",
+                    ColumnType::Simple(SimpleColumnType::Integer),
+                    false,
+                )],
                 constraints: vec![],
             },
             TableDef {
                 name: "posts".into(),
                 description: None,
                 columns: vec![
-                    nn_col("id", SimpleColumnType::Integer),
+                    col_n("id", ColumnType::Simple(SimpleColumnType::Integer), false),
                     ColumnDef::new(
                         "user_id",
                         ColumnType::Simple(SimpleColumnType::Integer),

From c3564212a2525ef44580c7297ef64c12f6ab2730 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sat, 27 Jun 2026 22:25:29 +0900
Subject: [PATCH 018/109] retry-now#0017: hoist duplicated validate/* test
 helpers into test_support (2 kept)

- plan(actions) -> MigrationPlan: removed 9 inline copies across cascade_reach,
  check_additions, check_strengthening, check_type_mismatch, constraint_type_changes,
  fk_addcolumn_nullable, fk_orphan_additions, pk_additions, unique_additions;
  added single canonical helper in test_support.rs.
- check(name, expr) -> TableConstraint + add_check(table, name, expr) -> MigrationAction:
  removed 3 inline check copies (check_self_contradiction, check_strengthening,
  check_type_mismatch), renamed 8 check_constraint(...) call sites in
  check_between_order to check(...), and removed 3 inline add_check copies
  (check_additions, check_strengthening, check_type_mismatch); added the two
  helpers to test_support.rs.

Test bodies and behavior preserved (no test asserted on plan id/version/comment/
created_at; the dummy values are inert). Cleaned up 6 newly-unused MigrationPlan/
MigrationAction imports that would have triggered -D warnings.

Item 3 of the analyze batch (BTreeSet hoist in diff_constraints) was reverted
in this iteration: the targeted bench (diff_constraint_replacement_100) showed
a clear -31.7% improvement across 3/3 AFTER runs, but diff_add_column/1000
showed a +128% median regression (2 of 3 AFTER runs at ~9ms vs BEFORE 3.85ms
median) that exceeds the observed BEFORE noise band; per IMPROVE prompt 4
the safer call is to revert and keep only the proven-safe dedup.

All gates green: cargo test/clippy --workspace --exclude vespertide-query
(pre-existing pg_query Windows MSVC blocker per iter 6/9/11/14), cargo fmt
--all --check, scripts/check-line-budget.sh, schema-gen drift = 0.
---
 crates/vespertide-planner/src/test_support.rs | 47 ++++++++++++++++++-
 .../src/validate/cascade_reach.rs             | 15 ++----
 .../src/validate/check_additions.rs           | 27 +----------
 .../src/validate/check_between_order.rs       | 29 ++++--------
 .../src/validate/check_self_contradiction.rs  | 13 +----
 .../src/validate/check_strengthening.rs       | 28 +----------
 .../src/validate/check_type_mismatch.rs       | 30 +-----------
 .../src/validate/constraint_type_changes.rs   | 12 +----
 .../src/validate/fk_addcolumn_nullable.rs     | 15 ++----
 .../src/validate/fk_orphan_additions.rs       | 15 ++----
 .../src/validate/pk_additions.rs              | 15 ++----
 .../src/validate/unique_additions.rs          | 12 +----
 12 files changed, 78 insertions(+), 180 deletions(-)

diff --git a/crates/vespertide-planner/src/test_support.rs b/crates/vespertide-planner/src/test_support.rs
index 89b52d2e..073e9d50 100644
--- a/crates/vespertide-planner/src/test_support.rs
+++ b/crates/vespertide-planner/src/test_support.rs
@@ -4,7 +4,10 @@
 //! in `lib.rs` and exposed `pub(crate)` so every inline `mod tests` and
 //! `tests/mod.rs` entry can reuse the same implementation.
 
-use vespertide_core::{ColumnDef, ColumnType, TableConstraint, TableDef};
+use vespertide_core::{
+    CheckViolationStrategy, ColumnDef, ColumnType, MigrationAction, MigrationPlan, TableConstraint,
+    TableDef,
+};
 
 /// Default test column (NOT NULL). Mirrors the production convention that
 /// every example model declares `nullable` explicitly and NOT NULL is the
@@ -48,3 +51,45 @@ pub(crate) fn idx(name: &str, columns: Vec<&str>) -> TableConstraint {
         columns: columns.into_iter().map(Into::into).collect(),
     }
 }
+
+/// Build a dummy [`MigrationPlan`] from a list of actions.
+///
+/// The `id` / `version` / `comment` / `created_at` fields are filled with
+/// inert default values because every validator test we share this helper
+/// with asserts only on the produced actions / errors and never on plan
+/// metadata. Keeping one canonical helper avoids re-pasting an 8-line
+/// constructor in every `validate/*.rs` test module.
+pub(crate) fn plan(actions: Vec) -> MigrationPlan {
+    MigrationPlan {
+        id: String::new(),
+        version: 0,
+        comment: None,
+        created_at: None,
+        actions,
+    }
+}
+
+/// CHECK [`TableConstraint`] with the default violation strategy.
+///
+/// Hoisted because every CHECK-related validator test reconstructed this
+/// 4-line constructor (`name.to_string()`, `expr.to_string()`,
+/// `strategy: CheckViolationStrategy::default()`) verbatim.
+pub(crate) fn check(name: &str, expr: &str) -> TableConstraint {
+    TableConstraint::Check {
+        name: name.to_string(),
+        expr: expr.to_string(),
+        strategy: CheckViolationStrategy::default(),
+    }
+}
+
+/// `AddConstraint` action wrapping a [`check`] for the given table.
+///
+/// Same rationale as [`check`]: collapse the duplicated `AddConstraint {
+/// table, constraint: check(...) }` boilerplate that lived in every
+/// `validate/check_*.rs` test module.
+pub(crate) fn add_check(table: &str, name: &str, expr: &str) -> MigrationAction {
+    MigrationAction::AddConstraint {
+        table: table.into(),
+        constraint: check(name, expr),
+    }
+}
diff --git a/crates/vespertide-planner/src/validate/cascade_reach.rs b/crates/vespertide-planner/src/validate/cascade_reach.rs
index 39cba94e..141d84ea 100644
--- a/crates/vespertide-planner/src/validate/cascade_reach.rs
+++ b/crates/vespertide-planner/src/validate/cascade_reach.rs
@@ -239,10 +239,11 @@ fn classify_risk(depth: usize, max_fanout: usize) -> Option {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::test_support::plan;
     use rstest::rstest;
     use vespertide_core::{
-        ColumnDef, ColumnType, ForeignKeyOrphanStrategy, MigrationAction, MigrationPlan,
-        ReferenceAction, SimpleColumnType, TableConstraint, TableDef, TableName,
+        ColumnDef, ColumnType, ForeignKeyOrphanStrategy, MigrationAction, ReferenceAction,
+        SimpleColumnType, TableConstraint, TableDef, TableName,
     };
 
     fn col(name: &str) -> ColumnDef {
@@ -316,16 +317,6 @@ mod tests {
         }
     }
 
-    fn plan(actions: Vec) -> MigrationPlan {
-        MigrationPlan {
-            id: "test".into(),
-            version: 1,
-            comment: None,
-            created_at: None,
-            actions,
-        }
-    }
-
     #[rstest]
     fn case_01_shallow_narrow_chain_no_warning() {
         // posts -> users only (depth 1) — normal pattern.
diff --git a/crates/vespertide-planner/src/validate/check_additions.rs b/crates/vespertide-planner/src/validate/check_additions.rs
index 71f1a194..859e94fb 100644
--- a/crates/vespertide-planner/src/validate/check_additions.rs
+++ b/crates/vespertide-planner/src/validate/check_additions.rs
@@ -125,11 +125,9 @@ fn find_check_target_column(expr: &str, table: &TableDef) -> Option<(String, boo
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::test_support::{add_check, plan};
     use rstest::rstest;
-    use vespertide_core::{
-        CheckViolationStrategy, ColumnDef, ColumnType, MigrationAction, MigrationPlan,
-        SimpleColumnType, TableConstraint, TableDef, TableName,
-    };
+    use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableDef};
 
     fn col(name: &str, nullable: bool) -> ColumnDef {
         ColumnDef {
@@ -154,27 +152,6 @@ mod tests {
         }
     }
 
-    fn add_check(table: &str, name: &str, expr: &str) -> MigrationAction {
-        MigrationAction::AddConstraint {
-            table: TableName::from(table),
-            constraint: TableConstraint::Check {
-                name: name.into(),
-                expr: expr.into(),
-                strategy: CheckViolationStrategy::default(),
-            },
-        }
-    }
-
-    fn plan(actions: Vec) -> MigrationPlan {
-        MigrationPlan {
-            id: "test".into(),
-            version: 1,
-            comment: None,
-            created_at: None,
-            actions,
-        }
-    }
-
     #[rstest]
     fn case_01_simple_comparison_nullable_column_flagged() {
         let baseline = vec![table(
diff --git a/crates/vespertide-planner/src/validate/check_between_order.rs b/crates/vespertide-planner/src/validate/check_between_order.rs
index 2a70a8d2..6241779d 100644
--- a/crates/vespertide-planner/src/validate/check_between_order.rs
+++ b/crates/vespertide-planner/src/validate/check_between_order.rs
@@ -138,17 +138,8 @@ fn format_literal(lit: &Literal) -> String {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use vespertide_core::{
-        CheckViolationStrategy, ColumnDef, ColumnType, SimpleColumnType, TableDef,
-    };
-
-    fn check_constraint(name: &str, expr: &str) -> TableConstraint {
-        TableConstraint::Check {
-            name: name.to_string(),
-            expr: expr.to_string(),
-            strategy: CheckViolationStrategy::default(),
-        }
-    }
+    use crate::test_support::check;
+    use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableDef};
 
     fn table_with_check(name: &str, check_expr: &str) -> TableDef {
         TableDef {
@@ -167,7 +158,7 @@ mod tests {
                 index: None,
                 foreign_key: None,
             }],
-            constraints: vec![check_constraint("chk_test", check_expr)],
+            constraints: vec![check("chk_test", check_expr)],
         }
     }
 
@@ -317,8 +308,8 @@ mod tests {
                 foreign_key: None,
             }],
             constraints: vec![
-                check_constraint("chk_first", "age BETWEEN 100 AND 0"),
-                check_constraint("chk_second", "score BETWEEN 50 AND 10"),
+                check("chk_first", "age BETWEEN 100 AND 0"),
+                check("chk_second", "score BETWEEN 50 AND 10"),
             ],
         };
         let err = validate_between_boundary_order(&table).unwrap_err();
@@ -347,8 +338,8 @@ mod tests {
                 foreign_key: None,
             }],
             constraints: vec![
-                check_constraint("chk_first", "age BETWEEN 100 AND 0"),
-                check_constraint("chk_second", "score BETWEEN 50 AND 10"),
+                check("chk_first", "age BETWEEN 100 AND 0"),
+                check("chk_second", "score BETWEEN 50 AND 10"),
             ],
         };
 
@@ -387,7 +378,7 @@ mod tests {
                 index: None,
                 foreign_key: None,
             }],
-            constraints: vec![check_constraint(
+            constraints: vec![check(
                 "chk_or_both",
                 "age BETWEEN 100 AND 0 OR score BETWEEN 50 AND 10",
             )],
@@ -432,8 +423,8 @@ mod tests {
                 foreign_key: None,
             }],
             constraints: vec![
-                check_constraint("chk_valid", "age BETWEEN 0 AND 100"),
-                check_constraint("chk_reversed", "score BETWEEN 50 AND 10"),
+                check("chk_valid", "age BETWEEN 0 AND 100"),
+                check("chk_reversed", "score BETWEEN 50 AND 10"),
             ],
         };
 
diff --git a/crates/vespertide-planner/src/validate/check_self_contradiction.rs b/crates/vespertide-planner/src/validate/check_self_contradiction.rs
index 483c46e4..5fec338a 100644
--- a/crates/vespertide-planner/src/validate/check_self_contradiction.rs
+++ b/crates/vespertide-planner/src/validate/check_self_contradiction.rs
@@ -355,17 +355,8 @@ fn format_is_null(column: &str, negated: bool) -> String {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use vespertide_core::{
-        CheckViolationStrategy, ColumnDef, ColumnType, SimpleColumnType, TableDef,
-    };
-
-    fn check(name: &str, expr: &str) -> TableConstraint {
-        TableConstraint::Check {
-            name: name.to_string(),
-            expr: expr.to_string(),
-            strategy: CheckViolationStrategy::default(),
-        }
-    }
+    use crate::test_support::check;
+    use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableDef};
 
     fn table(checks: Vec) -> TableDef {
         TableDef {
diff --git a/crates/vespertide-planner/src/validate/check_strengthening.rs b/crates/vespertide-planner/src/validate/check_strengthening.rs
index 31825fe3..7fd2821c 100644
--- a/crates/vespertide-planner/src/validate/check_strengthening.rs
+++ b/crates/vespertide-planner/src/validate/check_strengthening.rs
@@ -429,15 +429,8 @@ fn i64_to_f64(v: i64) -> f64 {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use vespertide_core::{CheckViolationStrategy, MigrationAction, MigrationPlan, TableDef};
-
-    fn check(name: &str, expr: &str) -> TableConstraint {
-        TableConstraint::Check {
-            name: name.to_string(),
-            expr: expr.to_string(),
-            strategy: CheckViolationStrategy::default(),
-        }
-    }
+    use crate::test_support::{add_check, check, plan};
+    use vespertide_core::{MigrationAction, TableDef};
 
     fn baseline_with_check(table: &str, name: &str, expr: &str) -> Vec {
         vec![TableDef {
@@ -448,23 +441,6 @@ mod tests {
         }]
     }
 
-    fn plan(actions: Vec) -> MigrationPlan {
-        MigrationPlan {
-            id: String::new(),
-            comment: None,
-            created_at: None,
-            version: 0,
-            actions,
-        }
-    }
-
-    fn add_check(table: &str, name: &str, expr: &str) -> MigrationAction {
-        MigrationAction::AddConstraint {
-            table: table.into(),
-            constraint: check(name, expr),
-        }
-    }
-
     fn remove_check(table: &str, name: &str, expr: &str) -> MigrationAction {
         MigrationAction::RemoveConstraint {
             table: table.into(),
diff --git a/crates/vespertide-planner/src/validate/check_type_mismatch.rs b/crates/vespertide-planner/src/validate/check_type_mismatch.rs
index 6e7b0d98..78411dc1 100644
--- a/crates/vespertide-planner/src/validate/check_type_mismatch.rs
+++ b/crates/vespertide-planner/src/validate/check_type_mismatch.rs
@@ -441,9 +441,8 @@ fn format_literal(lit: &Literal) -> String {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use vespertide_core::{
-        CheckViolationStrategy, ColumnDef, ColumnType, EnumValues, SimpleColumnType, TableDef,
-    };
+    use crate::test_support::{add_check, check, plan};
+    use vespertide_core::{ColumnDef, ColumnType, EnumValues, SimpleColumnType, TableDef};
 
     fn col(name: &str, ty: ColumnType) -> ColumnDef {
         ColumnDef {
@@ -468,31 +467,6 @@ mod tests {
         }
     }
 
-    fn plan(actions: Vec) -> MigrationPlan {
-        MigrationPlan {
-            id: String::new(),
-            comment: None,
-            created_at: None,
-            version: 0,
-            actions,
-        }
-    }
-
-    fn check(name: &str, expr: &str) -> TableConstraint {
-        TableConstraint::Check {
-            name: name.to_string(),
-            expr: expr.to_string(),
-            strategy: CheckViolationStrategy::default(),
-        }
-    }
-
-    fn add_check(table: &str, name: &str, expr: &str) -> MigrationAction {
-        MigrationAction::AddConstraint {
-            table: table.into(),
-            constraint: check(name, expr),
-        }
-    }
-
     // A numeric column compared to a string literal is a mismatch; the
     // warning's type label must render as `numeric(P, S)`. Pins the
     // `complex_type_label` Numeric arm (deleting it falls through to the
diff --git a/crates/vespertide-planner/src/validate/constraint_type_changes.rs b/crates/vespertide-planner/src/validate/constraint_type_changes.rs
index c0283bb7..f421fceb 100644
--- a/crates/vespertide-planner/src/validate/constraint_type_changes.rs
+++ b/crates/vespertide-planner/src/validate/constraint_type_changes.rs
@@ -239,7 +239,7 @@ fn render_fk_hint(baseline: &[TableDef], target_table: &str, target_columns: &[S
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::{pk, table};
+    use crate::test_support::{pk, plan, table};
     use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableName};
 
     fn col(name: &str) -> ColumnDef {
@@ -273,16 +273,6 @@ mod tests {
         }
     }
 
-    fn plan(actions: Vec) -> MigrationPlan {
-        MigrationPlan {
-            id: String::new(),
-            comment: None,
-            created_at: None,
-            version: 1,
-            actions,
-        }
-    }
-
     fn remove(table: &str, c: TableConstraint) -> MigrationAction {
         MigrationAction::RemoveConstraint {
             table: TableName::from(table),
diff --git a/crates/vespertide-planner/src/validate/fk_addcolumn_nullable.rs b/crates/vespertide-planner/src/validate/fk_addcolumn_nullable.rs
index 28d27b3b..62919820 100644
--- a/crates/vespertide-planner/src/validate/fk_addcolumn_nullable.rs
+++ b/crates/vespertide-planner/src/validate/fk_addcolumn_nullable.rs
@@ -97,11 +97,12 @@ pub fn find_addcolumn_fk_nullable_violations(plan: &MigrationPlan) -> Vec) -> MigrationPlan {
-        MigrationPlan {
-            id: "test".into(),
-            version: 1,
-            comment: None,
-            created_at: None,
-            actions,
-        }
-    }
-
     #[rstest]
     fn case_01_nullable_true_fill_inline_fk_ok() {
         // nullable: true + fill_with + inline FK -> no violation
diff --git a/crates/vespertide-planner/src/validate/fk_orphan_additions.rs b/crates/vespertide-planner/src/validate/fk_orphan_additions.rs
index 8e42754f..5fc52182 100644
--- a/crates/vespertide-planner/src/validate/fk_orphan_additions.rs
+++ b/crates/vespertide-planner/src/validate/fk_orphan_additions.rs
@@ -133,10 +133,11 @@ fn columns_to_strings(cols: &[ColumnName]) -> Vec {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::test_support::plan;
     use rstest::rstest;
     use vespertide_core::{
-        ColumnDef, ColumnType, MigrationAction, MigrationPlan, SimpleColumnType, TableConstraint,
-        TableDef, TableName,
+        ColumnDef, ColumnType, MigrationAction, SimpleColumnType, TableConstraint, TableDef,
+        TableName,
     };
 
     fn col(name: &str, nullable: bool) -> ColumnDef {
@@ -183,16 +184,6 @@ mod tests {
         }
     }
 
-    fn plan(actions: Vec) -> MigrationPlan {
-        MigrationPlan {
-            id: "test".into(),
-            version: 1,
-            comment: None,
-            created_at: None,
-            actions,
-        }
-    }
-
     #[rstest]
     fn case_01_existing_nullable_column_flagged_with_nullify_available() {
         // Child column exists in baseline and is nullable -> warning + NullifyOrphans valid.
diff --git a/crates/vespertide-planner/src/validate/pk_additions.rs b/crates/vespertide-planner/src/validate/pk_additions.rs
index e5ea2878..3e7a4f3c 100644
--- a/crates/vespertide-planner/src/validate/pk_additions.rs
+++ b/crates/vespertide-planner/src/validate/pk_additions.rs
@@ -170,10 +170,11 @@ pub fn find_primary_key_additions(
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::test_support::plan;
     use rstest::rstest;
     use vespertide_core::{
-        ColumnDef, ColumnType, MigrationAction, MigrationPlan, PrimaryKeyAdditionStrategy,
-        SimpleColumnType, TableConstraint, TableDef, TableName,
+        ColumnDef, ColumnType, MigrationAction, PrimaryKeyAdditionStrategy, SimpleColumnType,
+        TableConstraint, TableDef, TableName,
     };
 
     fn col(name: &str, nullable: bool) -> ColumnDef {
@@ -210,16 +211,6 @@ mod tests {
         }
     }
 
-    fn plan(actions: Vec) -> MigrationPlan {
-        MigrationPlan {
-            id: "test".into(),
-            version: 1,
-            comment: None,
-            created_at: None,
-            actions,
-        }
-    }
-
     #[rstest]
     fn case_01_single_existing_not_null_no_unique_flagged() {
         let baseline = vec![table(
diff --git a/crates/vespertide-planner/src/validate/unique_additions.rs b/crates/vespertide-planner/src/validate/unique_additions.rs
index 0b52e220..214a588d 100644
--- a/crates/vespertide-planner/src/validate/unique_additions.rs
+++ b/crates/vespertide-planner/src/validate/unique_additions.rs
@@ -215,7 +215,7 @@ mod tests {
         ColumnDef::new(name, ColumnType::Simple(SimpleColumnType::Text), false)
     }
 
-    use crate::test_support::{pk, table};
+    use crate::test_support::{pk, plan, table};
 
     fn unique(name: Option<&str>, columns: Vec<&str>) -> TableConstraint {
         TableConstraint::Unique {
@@ -232,16 +232,6 @@ mod tests {
         }
     }
 
-    fn plan(actions: Vec) -> MigrationPlan {
-        MigrationPlan {
-            id: String::new(),
-            comment: None,
-            created_at: None,
-            version: 1,
-            actions,
-        }
-    }
-
     /// Case 1: AddConstraint(Unique) on an existing column with a single-
     /// column PK that's not in the unique set → flagged + auto-cleanup capable.
     #[test]

From feabbea9df15ce8ec3a8e2719fd57ce2c7821cf0 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sat, 27 Jun 2026 22:53:12 +0900
Subject: [PATCH 019/109] retry-now#0018: batch -- drop Cow ceremony in
 normalize_fill_with, dedup planner+exporter proptest strategies via
 core::arbitrary (3 kept)

---
 crates/vespertide-exporter/Cargo.toml         |  1 +
 .../tests/codegen_determinism.rs              | 68 +++++--------------
 crates/vespertide-planner/Cargo.toml          |  2 +-
 .../vespertide-planner/tests/diff_proptest.rs | 50 ++++----------
 crates/vespertide-query/src/sql/add_column.rs |  5 +-
 crates/vespertide-query/src/sql/helpers.rs    | 18 ++---
 .../src/sql/modify_column_nullable.rs         |  2 +-
 7 files changed, 44 insertions(+), 102 deletions(-)

diff --git a/crates/vespertide-exporter/Cargo.toml b/crates/vespertide-exporter/Cargo.toml
index 35f13948..36c2e0c0 100644
--- a/crates/vespertide-exporter/Cargo.toml
+++ b/crates/vespertide-exporter/Cargo.toml
@@ -23,6 +23,7 @@ criterion = { version = "0.8", features = ["html_reports"] }
 rstest = "0.26"
 insta = { version = "1.47", features = ["yaml"] }
 proptest = "1"
+vespertide-core = { workspace = true, features = ["arbitrary"] }
 
 [[bench]]
 name = "codegen_benchmarks"
diff --git a/crates/vespertide-exporter/tests/codegen_determinism.rs b/crates/vespertide-exporter/tests/codegen_determinism.rs
index d6899606..85b4b169 100644
--- a/crates/vespertide-exporter/tests/codegen_determinism.rs
+++ b/crates/vespertide-exporter/tests/codegen_determinism.rs
@@ -1,5 +1,6 @@
 use proptest::prelude::*;
 use rayon::ThreadPoolBuilder;
+use vespertide_core::arbitrary::{arb_safe_ident, arb_simple_column_type};
 use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableDef};
 
 proptest! {
@@ -106,14 +107,26 @@ fn wide_schema(table_count: usize) -> Vec {
         .collect()
 }
 
+/// Locally-shaped table generator: FK-free, constraint-free schemas with 0..=8
+/// columns. Core's `arb_table_def` produces a heavier schema (constraints, FKs,
+/// 1..=8 columns) that does not match this determinism test's exact shape —
+/// the codegen-determinism assertion targets the simple-column variant of
+/// `ColumnType` only, bridged here via `arb_simple_column_type().prop_map(ColumnType::Simple)`.
 fn arb_table_def() -> impl Strategy {
     (
         arb_safe_ident(),
-        prop::collection::vec((arb_safe_ident(), arb_column_type(), any::()), 0..=8)
-            .prop_filter("unique column names", |columns| {
-                let mut names = std::collections::BTreeSet::new();
-                columns.iter().all(|(name, _, _)| names.insert(name))
-            }),
+        prop::collection::vec(
+            (
+                arb_safe_ident(),
+                arb_simple_column_type().prop_map(ColumnType::Simple),
+                any::(),
+            ),
+            0..=8,
+        )
+        .prop_filter("unique column names", |columns| {
+            let mut names = std::collections::BTreeSet::new();
+            columns.iter().all(|(name, _, _)| names.insert(name))
+        }),
     )
         .prop_map(|(name, columns)| TableDef {
             name: name.into(),
@@ -135,48 +148,3 @@ fn arb_table_def() -> impl Strategy {
             constraints: Vec::new(),
         })
 }
-
-fn arb_column_type() -> impl Strategy {
-    prop_oneof![
-        Just(SimpleColumnType::SmallInt),
-        Just(SimpleColumnType::Integer),
-        Just(SimpleColumnType::BigInt),
-        Just(SimpleColumnType::Real),
-        Just(SimpleColumnType::DoublePrecision),
-        Just(SimpleColumnType::Text),
-        Just(SimpleColumnType::Boolean),
-        Just(SimpleColumnType::Date),
-        Just(SimpleColumnType::Time),
-        Just(SimpleColumnType::Timestamp),
-        Just(SimpleColumnType::Timestamptz),
-        Just(SimpleColumnType::Interval),
-        Just(SimpleColumnType::Bytea),
-        Just(SimpleColumnType::Uuid),
-        Just(SimpleColumnType::Json),
-        Just(SimpleColumnType::Inet),
-        Just(SimpleColumnType::Cidr),
-        Just(SimpleColumnType::Macaddr),
-        Just(SimpleColumnType::Xml),
-    ]
-    .prop_map(ColumnType::Simple)
-}
-
-fn arb_safe_ident() -> impl Strategy {
-    (
-        prop::char::range('a', 'z'),
-        prop::collection::vec(
-            prop_oneof![
-                prop::char::range('a', 'z'),
-                prop::char::range('0', '9'),
-                Just('_'),
-            ],
-            0..=20,
-        ),
-    )
-        .prop_map(|(first, rest)| {
-            let mut ident = String::with_capacity(rest.len() + 1);
-            ident.push(first);
-            ident.extend(rest);
-            ident
-        })
-}
diff --git a/crates/vespertide-planner/Cargo.toml b/crates/vespertide-planner/Cargo.toml
index 8c635aab..3f6ea481 100644
--- a/crates/vespertide-planner/Cargo.toml
+++ b/crates/vespertide-planner/Cargo.toml
@@ -23,7 +23,7 @@ rstest = "0.26"
 insta = "1.47"
 proptest = "1"
 serial_test = "3.5"
-vespertide-core = { workspace = true }
+vespertide-core = { workspace = true, features = ["arbitrary"] }
 
 [[bench]]
 name = "diff_benchmarks"
diff --git a/crates/vespertide-planner/tests/diff_proptest.rs b/crates/vespertide-planner/tests/diff_proptest.rs
index d9945280..355cdfed 100644
--- a/crates/vespertide-planner/tests/diff_proptest.rs
+++ b/crates/vespertide-planner/tests/diff_proptest.rs
@@ -1,4 +1,5 @@
 use proptest::prelude::*;
+use vespertide_core::arbitrary::{arb_safe_ident, arb_simple_column_type};
 use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableConstraint, TableDef};
 use vespertide_planner::{apply_action, diff_schemas};
 
@@ -122,7 +123,9 @@ fn mutate_schema(from: &[TableDef]) -> impl Strategy> + us
     ]
 }
 
-// TODO: deduplicate with vespertide-core::arbitrary once available.
+/// Locally-shaped table generator: FK-free, constraint-free schemas matched to
+/// `mutate_schema`'s mutation domain. Core's `arb_table_def` produces a heavier
+/// schema (constraints, FKs, options) that does not fit this proptest's invariants.
 fn arb_table_def() -> impl Strategy {
     (arb_safe_ident(), arb_unique_columns()).prop_map(|(name, columns)| TableDef {
         name: name.into(),
@@ -132,7 +135,9 @@ fn arb_table_def() -> impl Strategy {
     })
 }
 
-// TODO: deduplicate with vespertide-core::arbitrary once available.
+/// Locally-shaped column-set generator: 1..6 columns with unique names, no
+/// inline constraints. Mirrors `mutate_schema`'s assumption that columns can
+/// be appended freely.
 fn arb_unique_columns() -> impl Strategy> {
     prop::collection::vec(arb_column_def(), 1..6).prop_filter("unique column names", |columns| {
         let mut seen = std::collections::BTreeSet::new();
@@ -142,39 +147,14 @@ fn arb_unique_columns() -> impl Strategy> {
     })
 }
 
-// TODO: deduplicate with vespertide-core::arbitrary once available.
+/// Locally-shaped column generator: no defaults, no comments, no inline
+/// constraints, simple types only. Bridges `arb_simple_column_type` (which
+/// yields `SimpleColumnType`) to `ColumnType::Simple` for `ColumnDef::new`.
 fn arb_column_def() -> impl Strategy {
-    (arb_safe_ident(), arb_simple_column_type(), any::())
+    (
+        arb_safe_ident(),
+        arb_simple_column_type().prop_map(ColumnType::Simple),
+        any::(),
+    )
         .prop_map(|(name, column_type, nullable)| ColumnDef::new(name, column_type, nullable))
 }
-
-// TODO: deduplicate with vespertide-core::arbitrary once available.
-fn arb_simple_column_type() -> impl Strategy {
-    prop_oneof![
-        Just(SimpleColumnType::SmallInt),
-        Just(SimpleColumnType::Integer),
-        Just(SimpleColumnType::BigInt),
-        Just(SimpleColumnType::Real),
-        Just(SimpleColumnType::DoublePrecision),
-        Just(SimpleColumnType::Text),
-        Just(SimpleColumnType::Boolean),
-        Just(SimpleColumnType::Date),
-        Just(SimpleColumnType::Time),
-        Just(SimpleColumnType::Timestamp),
-        Just(SimpleColumnType::Timestamptz),
-        Just(SimpleColumnType::Interval),
-        Just(SimpleColumnType::Bytea),
-        Just(SimpleColumnType::Uuid),
-        Just(SimpleColumnType::Json),
-        Just(SimpleColumnType::Inet),
-        Just(SimpleColumnType::Cidr),
-        Just(SimpleColumnType::Macaddr),
-        Just(SimpleColumnType::Xml),
-    ]
-    .prop_map(ColumnType::Simple)
-}
-
-// TODO: deduplicate with vespertide-core::arbitrary once available.
-fn arb_safe_ident() -> impl Strategy {
-    "[a-z][a-z0-9_]{0,10}"
-}
diff --git a/crates/vespertide-query/src/sql/add_column.rs b/crates/vespertide-query/src/sql/add_column.rs
index 3fbda8e2..a5246aac 100644
--- a/crates/vespertide-query/src/sql/add_column.rs
+++ b/crates/vespertide-query/src/sql/add_column.rs
@@ -66,8 +66,7 @@ pub fn build_add_column(
         for col in &table_def.columns {
             select_query.column(Alias::new(&col.name));
         }
-        let normalized_fill = normalize_fill_with(fill_with);
-        let fill_expr = if let Some(fill) = normalized_fill.as_deref() {
+        let fill_expr = if let Some(fill) = normalize_fill_with(fill_with) {
             let converted = convert_default_for_backend(fill, backend);
             Expr::cust(normalize_enum_default(&column.r#type, &converted))
         } else if let Some(def) = &column.default {
@@ -129,7 +128,7 @@ pub fn build_add_column(
 
         // Backfill with provided value
         if let Some(fill) = normalize_fill_with(fill_with) {
-            let fill = convert_default_for_backend(&fill, backend);
+            let fill = convert_default_for_backend(fill, backend);
             let update_stmt = Query::update()
                 .table(Alias::new(table))
                 .value(Alias::new(&column.name), Expr::cust(fill))
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index ff9ddaa4..12fc2ea2 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -1,5 +1,3 @@
-use std::borrow::Cow;
-
 use sea_query::{
     Alias, ColumnDef as SeaColumnDef, ForeignKeyAction, MysqlQueryBuilder, PostgresQueryBuilder,
     QueryStatementWriter, SchemaStatementBuilder, SimpleExpr, SqliteQueryBuilder,
@@ -12,17 +10,13 @@ use vespertide_core::{
 use super::create_table::build_create_table_for_backend;
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
 
-/// Normalize `fill_with` value - empty string becomes '' (SQL empty string literal)
-/// Returns a Cow to avoid allocations when possible.
+/// Normalize `fill_with` value - empty string becomes `''` (SQL empty-string
+/// literal). Returns a borrowed `&str` because both arms are static or borrowed
+/// from the caller — no allocation ever happens, so the `Cow` wrapper was
+/// purely ceremonial.
 #[must_use]
-pub fn normalize_fill_with(fill_with: Option<&str>) -> Option> {
-    fill_with.map(|s| {
-        if s.is_empty() {
-            Cow::Borrowed("''")
-        } else {
-            Cow::Borrowed(s)
-        }
-    })
+pub fn normalize_fill_with(fill_with: Option<&str>) -> Option<&str> {
+    fill_with.map(|s| if s.is_empty() { "''" } else { s })
 }
 
 /// Helper function to convert a schema statement to SQL for a specific backend
diff --git a/crates/vespertide-query/src/sql/modify_column_nullable.rs b/crates/vespertide-query/src/sql/modify_column_nullable.rs
index 554e07ce..373dd2b1 100644
--- a/crates/vespertide-query/src/sql/modify_column_nullable.rs
+++ b/crates/vespertide-query/src/sql/modify_column_nullable.rs
@@ -37,7 +37,7 @@ pub fn build_modify_column_nullable(
     }
     // If changing to NOT NULL, first update existing NULL values if fill_with is provided
     else if !nullable && let Some(fill_value) = normalize_fill_with(fill_with) {
-        let fill_value = convert_default_for_backend(&fill_value, backend);
+        let fill_value = convert_default_for_backend(fill_value, backend);
         let quoted_table = quote_ident(table, backend);
         let quoted_column = quote_ident(column, backend);
         let update_sql = format!(

From d4647422374822edcb6b4a7ef05430d5ebed35e5 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sat, 27 Jun 2026 23:20:54 +0900
Subject: [PATCH 020/109] retry-now#0019: batch -- extract print_section_rule
 helper, add EnumValues::contains_value, dedupe PK/FK clear arms (3 kept)

1. Centralise the 34 repeated CLI banner-rule lines under a single
   print_section_rule() helper in commands::revision::prompts (visible only
   inside the revision module). Replaces every
   `println!("{}", "\u{2500}".repeat(60).bright_black());` (and the two raw
   em-dash variants in fill_with.rs) with `super::print_section_rule();`, so
   the separator's width/glyph/colour live in one place.

2. Move the EnumValues value-match logic out of the planner validator and
   onto EnumValues itself as contains_value(). Mirrors the existing
   variant_names / len / is_string / is_integer peers in vespertide-core.
   The planner validator collapses to a single call, and a parameterised
   rstest covers all five branches (string match/miss, integer numeric
   match/miss, integer name fallback).

3. Dedupe the byte-identical PrimaryKey and ForeignKey arms of
   clear_inline_constraint_fields() in vespertide-planner::apply::constraint_ops
   via a closure-taking helper clear_columns_field(). Prevents future drift
   between the two paths if the column lookup ever needs case-folding or
   aliasing.
---
 .../revision/prompts/choices_and_apply/mod.rs | 36 ++++++-------
 .../prompts/drop_recreate_fk_policy.rs        | 12 ++---
 .../commands/revision/prompts/fill_with.rs    |  8 +--
 .../src/commands/revision/prompts/mod.rs      | 10 ++++
 .../commands/revision/prompts/narrowing.rs    |  4 +-
 .../src/commands/revision/prompts/timezone.rs |  8 +--
 crates/vespertide-core/src/schema/column.rs   | 21 ++++++++
 crates/vespertide-core/src/schema/mod.rs      | 54 +++++++++++++++++++
 .../src/apply/constraint_ops.rs               | 32 +++++++----
 .../vespertide-planner/src/validate/enums.rs  |  8 +--
 10 files changed, 141 insertions(+), 52 deletions(-)

diff --git a/crates/vespertide-cli/src/commands/revision/prompts/choices_and_apply/mod.rs b/crates/vespertide-cli/src/commands/revision/prompts/choices_and_apply/mod.rs
index 481a6c71..ec0790c4 100644
--- a/crates/vespertide-cli/src/commands/revision/prompts/choices_and_apply/mod.rs
+++ b/crates/vespertide-cli/src/commands/revision/prompts/choices_and_apply/mod.rs
@@ -41,9 +41,9 @@ pub(in crate::commands::revision) fn prompt_default_change_resolution(
     warning: &DefaultChangeWarning,
 ) -> Result> {
     println!();
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     println!("{}", format_default_change_header(warning));
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
 
     let backfill_available = warning.new_default.is_some();
 
@@ -126,9 +126,9 @@ pub(in crate::commands::revision) fn prompt_unique_additions(
     warning: &UniqueAdditionWarning,
 ) -> Result> {
     println!();
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     println!("{}", format_unique_addition_header(warning));
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
 
     let auto_cleanup_available = matches!(warning.pk_kind, PkKind::SingleAutoCleanupCapable { .. });
 
@@ -264,9 +264,9 @@ pub(in crate::commands::revision) fn prompt_fk_orphan_additions(
     warning: &FkOrphanAdditionWarning,
 ) -> Result> {
     println!();
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     println!("{}", format_fk_orphan_addition_header(warning));
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
 
     let mut labels: Vec = Vec::new();
     let mut outcomes: Vec> = Vec::new();
@@ -379,9 +379,9 @@ pub(in crate::commands::revision) fn prompt_check_additions(
     warning: &CheckAdditionWarning,
 ) -> Result> {
     println!();
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     println!("{}", format_check_addition_header(warning));
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
 
     let mut labels: Vec = Vec::new();
     let mut outcomes: Vec> = Vec::new();
@@ -489,9 +489,9 @@ pub(in crate::commands::revision) fn prompt_pk_additions(
     warning: &PrimaryKeyAdditionWarning,
 ) -> Result> {
     println!();
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     println!("{}", format_pk_addition_header(warning));
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
 
     let mut labels: Vec = Vec::new();
     let mut outcomes: Vec> = Vec::new();
@@ -601,9 +601,9 @@ pub(in crate::commands::revision) fn prompt_cascade_reach(
     warning: &CascadeReachWarning,
 ) -> Result> {
     println!();
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     println!("{}", format_cascade_reach_header(warning));
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
 
     let labels = [
         "Proceed (cascade chain is intentional)".to_string(),
@@ -648,9 +648,9 @@ pub(in crate::commands::revision) fn prompt_sequence_exhaustion(
     warning: &SequenceExhaustionWarning,
 ) -> Result> {
     println!();
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     println!("{}", format_sequence_exhaustion_header(warning));
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
 
     let mutable = warning_is_mutable(warning);
     let mut labels: Vec = Vec::new();
@@ -831,9 +831,9 @@ pub(in crate::commands::revision) fn prompt_check_strengthening(
     warning: &CheckStrengtheningWarning,
 ) -> Result> {
     println!();
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     println!("{}", format_check_strengthening_header(warning));
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
 
     let labels = [
         "Proceed (existing rows already satisfy the stricter predicate)".to_string(),
@@ -900,9 +900,9 @@ pub(in crate::commands::revision) fn prompt_check_type_mismatch(
     warning: &CheckTypeMismatchWarning,
 ) -> Result> {
     println!();
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     println!("{}", format_check_type_mismatch_header(warning));
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
 
     let labels = [
         "Proceed (literal type is intentional / backend coerces it)".to_string(),
diff --git a/crates/vespertide-cli/src/commands/revision/prompts/drop_recreate_fk_policy.rs b/crates/vespertide-cli/src/commands/revision/prompts/drop_recreate_fk_policy.rs
index 6e223123..b18be19f 100644
--- a/crates/vespertide-cli/src/commands/revision/prompts/drop_recreate_fk_policy.rs
+++ b/crates/vespertide-cli/src/commands/revision/prompts/drop_recreate_fk_policy.rs
@@ -52,7 +52,7 @@ pub(in crate::commands::revision) fn prompt_fk_policy_changes(
          — backend behavior will SILENTLY differ:"
             .bright_yellow()
     );
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     for w in warnings {
         println!(
             "  {} {}",
@@ -60,7 +60,7 @@ pub(in crate::commands::revision) fn prompt_fk_policy_changes(
             format_fk_policy_change_line(w).bright_white()
         );
     }
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     println!(
         "  {} {}",
         "\u{26a0}".bright_red(),
@@ -86,7 +86,7 @@ pub(in crate::commands::revision) fn prompt_recreate_tables(
         "\u{26a0}".bright_yellow(),
         "The following tables need to be RECREATED:".bright_yellow()
     );
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
 
     for item in tables {
         let reason_msg = match item.reason {
@@ -102,7 +102,7 @@ pub(in crate::commands::revision) fn prompt_recreate_tables(
         );
     }
 
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     println!(
         "  {} {}",
         "\u{26a0}".bright_red(),
@@ -135,7 +135,7 @@ pub(in crate::commands::revision) fn prompt_drop_resolution(
 ) -> Result> {
     let header = format_drop_header(&resolution.target);
     println!();
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     println!("{header}");
     if !resolution.candidates.is_empty() {
         println!(
@@ -143,7 +143,7 @@ pub(in crate::commands::revision) fn prompt_drop_resolution(
             "Same-plan add actions detected as possible rename targets.".bright_white()
         );
     }
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
 
     let mut labels: Vec = Vec::new();
     for c in &resolution.candidates {
diff --git a/crates/vespertide-cli/src/commands/revision/prompts/fill_with.rs b/crates/vespertide-cli/src/commands/revision/prompts/fill_with.rs
index e51bc9aa..c1bbc4c0 100644
--- a/crates/vespertide-cli/src/commands/revision/prompts/fill_with.rs
+++ b/crates/vespertide-cli/src/commands/revision/prompts/fill_with.rs
@@ -53,12 +53,12 @@ pub(in crate::commands::revision) fn print_fill_with_header() {
         "⚠".bright_yellow(),
         "The following columns require fill_with values:".bright_yellow()
     );
-    println!("{}", "─".repeat(60).bright_black());
+    super::print_section_rule();
 }
 
 /// Print the footer for `fill_with` prompts.
 pub(in crate::commands::revision) fn print_fill_with_footer() {
-    println!("{}", "─".repeat(60).bright_black());
+    super::print_section_rule();
 }
 
 /// Print a `fill_with` item and return the formatted prompt.
@@ -303,7 +303,7 @@ where
         "\u{26a0}".bright_yellow(),
         "The following enum value removals require replacement mappings:".bright_yellow()
     );
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
 
     for item in missing {
         println!(
@@ -337,7 +337,7 @@ where
         results.push((item.action_index, mappings));
     }
 
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     Ok(results)
 }
 
diff --git a/crates/vespertide-cli/src/commands/revision/prompts/mod.rs b/crates/vespertide-cli/src/commands/revision/prompts/mod.rs
index d6079586..673f9461 100644
--- a/crates/vespertide-cli/src/commands/revision/prompts/mod.rs
+++ b/crates/vespertide-cli/src/commands/revision/prompts/mod.rs
@@ -9,3 +9,13 @@ pub(in crate::commands::revision) use drop_recreate_fk_policy::*;
 pub(in crate::commands::revision) use fill_with::*;
 pub(in crate::commands::revision) use narrowing::*;
 pub(in crate::commands::revision) use timezone::*;
+
+use colored::Colorize;
+
+/// Print the standard 60-glyph horizontal separator used by every
+/// revision-prompt banner. Centralises the width/glyph/colour so the
+/// look-and-feel cannot drift across files.
+#[cfg(not(tarpaulin_include))] // reason: trivial CLI display helper, exercised only by humans
+pub(in crate::commands::revision) fn print_section_rule() {
+    println!("{}", "\u{2500}".repeat(60).bright_black());
+}
diff --git a/crates/vespertide-cli/src/commands/revision/prompts/narrowing.rs b/crates/vespertide-cli/src/commands/revision/prompts/narrowing.rs
index b661c8b7..3d9dcee2 100644
--- a/crates/vespertide-cli/src/commands/revision/prompts/narrowing.rs
+++ b/crates/vespertide-cli/src/commands/revision/prompts/narrowing.rs
@@ -98,7 +98,7 @@ pub(in crate::commands::revision) fn prompt_type_narrowings(
 
     let mut strategies = Vec::with_capacity(warnings.len());
     for (idx, w) in warnings.iter().enumerate() {
-        println!("{}", "\u{2500}".repeat(60).bright_black());
+        super::print_section_rule();
         println!(
             "  {} {}/{}: {} ({} {} {})",
             "\u{25b6}".bright_cyan(),
@@ -157,7 +157,7 @@ pub(in crate::commands::revision) fn prompt_type_narrowings(
         };
         strategies.push(strategy);
     }
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     Ok(Some(strategies))
 }
 
diff --git a/crates/vespertide-cli/src/commands/revision/prompts/timezone.rs b/crates/vespertide-cli/src/commands/revision/prompts/timezone.rs
index 96da77cf..0336bc4d 100644
--- a/crates/vespertide-cli/src/commands/revision/prompts/timezone.rs
+++ b/crates/vespertide-cli/src/commands/revision/prompts/timezone.rs
@@ -39,7 +39,7 @@ pub(in crate::commands::revision) fn prompt_timezone_conversions(
 
     let mut choices = Vec::with_capacity(warnings.len());
     for (idx, w) in warnings.iter().enumerate() {
-        println!("{}", "\u{2500}".repeat(60).bright_black());
+        super::print_section_rule();
         println!(
             "  {} {}/{}: {} ({})",
             "\u{25b6}".bright_cyan(),
@@ -98,7 +98,7 @@ pub(in crate::commands::revision) fn prompt_timezone_conversions(
         );
         choices.push(tz);
     }
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     Ok(Some(choices))
 }
 
@@ -155,7 +155,7 @@ pub(in crate::commands::revision) fn prompt_remap_enum_values(
         )
         .bright_yellow()
     );
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     for action in &remaps {
         if let MigrationAction::RemapEnumValues {
             table,
@@ -177,7 +177,7 @@ pub(in crate::commands::revision) fn prompt_remap_enum_values(
             );
         }
     }
-    println!("{}", "\u{2500}".repeat(60).bright_black());
+    super::print_section_rule();
     println!(
         "  {} {}",
         "\u{26a0}".bright_red(),
diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs
index 28cb545e..3190fa2b 100644
--- a/crates/vespertide-core/src/schema/column.rs
+++ b/crates/vespertide-core/src/schema/column.rs
@@ -458,6 +458,27 @@ impl EnumValues {
         }
     }
 
+    /// Returns `true` when `value` matches any variant of this enum.
+    ///
+    /// For string enums the comparison is exact-string against each variant.
+    /// For integer enums the value is first parsed as `i32` (matching the
+    /// underlying DB representation): successful parses match against
+    /// `NumValue::value`, failed parses fall back to matching against
+    /// `NumValue::name`. Mirrors the loose JSON-default behaviour expected by
+    /// the planner validator so a model author can write either the numeric
+    /// literal (`5`) or the variant name (`"Active"`) for an integer enum
+    /// default.
+    #[must_use]
+    pub fn contains_value(&self, value: &str) -> bool {
+        match self {
+            EnumValues::String(variants) => variants.iter().any(|v| v == value),
+            EnumValues::Integer(variants) => value.parse::().map_or_else(
+                |_| variants.iter().any(|v| v.name == value),
+                |n| variants.iter().any(|v| v.value == i64::from(n)),
+            ),
+        }
+    }
+
     /// Format every variant for `CREATE TYPE … AS ENUM(...)` /
     /// `CHECK (col IN (...))` and join with `separator`, writing into one
     /// buffer — no intermediate `Vec` allocation.
diff --git a/crates/vespertide-core/src/schema/mod.rs b/crates/vespertide-core/src/schema/mod.rs
index aff5693b..ebff4269 100644
--- a/crates/vespertide-core/src/schema/mod.rs
+++ b/crates/vespertide-core/src/schema/mod.rs
@@ -241,6 +241,60 @@ mod tests {
             assert_eq!(vals.variant_names(), vec!["Low", "High"]);
         }
 
+        #[rstest]
+        // String match: exact-string check against each variant.
+        #[case::string_match(
+            EnumValues::String(vec!["active".into(), "pending".into()]),
+            "active",
+            true,
+        )]
+        #[case::string_miss(
+            EnumValues::String(vec!["active".into(), "pending".into()]),
+            "banned",
+            false,
+        )]
+        // Integer enum, numeric input: parses as i32 then matches `NumValue::value`.
+        #[case::integer_numeric_match(
+            EnumValues::Integer(vec![
+                NumValue { name: "Low".into(),  value: 0  },
+                NumValue { name: "High".into(), value: 10 },
+            ]),
+            "10",
+            true,
+        )]
+        #[case::integer_numeric_miss(
+            EnumValues::Integer(vec![
+                NumValue { name: "Low".into(),  value: 0  },
+                NumValue { name: "High".into(), value: 10 },
+            ]),
+            "99",
+            false,
+        )]
+        // Integer enum, non-numeric input: falls back to matching `NumValue::name`.
+        #[case::integer_name_match(
+            EnumValues::Integer(vec![
+                NumValue { name: "Low".into(),  value: 0  },
+                NumValue { name: "High".into(), value: 10 },
+            ]),
+            "High",
+            true,
+        )]
+        #[case::integer_name_miss(
+            EnumValues::Integer(vec![
+                NumValue { name: "Low".into(),  value: 0  },
+                NumValue { name: "High".into(), value: 10 },
+            ]),
+            "Unknown",
+            false,
+        )]
+        fn contains_value_matches_variants_per_enum_kind(
+            #[case] values: EnumValues,
+            #[case] candidate: &str,
+            #[case] expected: bool,
+        ) {
+            assert_eq!(values.contains_value(candidate), expected);
+        }
+
         #[test]
         fn test_enum_values_len_and_is_empty() {
             // String variant
diff --git a/crates/vespertide-planner/src/apply/constraint_ops.rs b/crates/vespertide-planner/src/apply/constraint_ops.rs
index bc1f03a2..fbcc0a9a 100644
--- a/crates/vespertide-planner/src/apply/constraint_ops.rs
+++ b/crates/vespertide-planner/src/apply/constraint_ops.rs
@@ -1,4 +1,4 @@
-use vespertide_core::{ColumnName, StrOrBoolOrArray, TableConstraint, TableDef};
+use vespertide_core::{ColumnDef, ColumnName, StrOrBoolOrArray, TableConstraint, TableDef};
 
 use crate::error::PlannerError;
 
@@ -73,18 +73,10 @@ pub(super) fn clear_inline_constraint_fields(
             clear_unique_fields(tbl, name.as_deref(), columns);
         }
         TableConstraint::PrimaryKey { columns, .. } => {
-            for col_name in columns {
-                if let Some(col) = tbl.columns.iter_mut().find(|c| &c.name == col_name) {
-                    col.primary_key = None;
-                }
-            }
+            clear_columns_field(tbl, columns, |c| c.primary_key = None);
         }
         TableConstraint::ForeignKey { columns, .. } => {
-            for col_name in columns {
-                if let Some(col) = tbl.columns.iter_mut().find(|c| &c.name == col_name) {
-                    col.foreign_key = None;
-                }
-            }
+            clear_columns_field(tbl, columns, |c| c.foreign_key = None);
         }
         TableConstraint::Check { .. } => {}
         TableConstraint::Index { name, columns } => {
@@ -94,6 +86,24 @@ pub(super) fn clear_inline_constraint_fields(
     }
 }
 
+/// Run `clear` on every column in `tbl` whose name appears in `columns`.
+/// Used by the PK/FK arms of [`clear_inline_constraint_fields`] to drop the
+/// single inline field that mirrors the table-level constraint being
+/// removed. Centralises the "find each column and clear one field" pattern
+/// so the two arms cannot drift (e.g. if the lookup later needs case-folding
+/// or aliasing).
+fn clear_columns_field(
+    tbl: &mut TableDef,
+    columns: &[ColumnName],
+    mut clear: impl FnMut(&mut ColumnDef),
+) {
+    for col_name in columns {
+        if let Some(col) = tbl.columns.iter_mut().find(|c| &c.name == col_name) {
+            clear(col);
+        }
+    }
+}
+
 fn clear_unique_fields(tbl: &mut TableDef, name: Option<&str>, columns: &[ColumnName]) {
     if name.is_none()
         && columns.len() == 1
diff --git a/crates/vespertide-planner/src/validate/enums.rs b/crates/vespertide-planner/src/validate/enums.rs
index bba4ddd4..c45427ee 100644
--- a/crates/vespertide-planner/src/validate/enums.rs
+++ b/crates/vespertide-planner/src/validate/enums.rs
@@ -46,13 +46,7 @@ pub(super) fn validate_enum_value(
         return Ok(());
     };
 
-    let is_valid = match enum_values {
-        EnumValues::String(variants) => variants.iter().any(|v| v == extracted),
-        EnumValues::Integer(variants) => extracted.parse::().map_or_else(
-            |_| variants.iter().any(|v| v.name == extracted),
-            |n| variants.iter().any(|v| v.value == i64::from(n)),
-        ),
-    };
+    let is_valid = enum_values.contains_value(extracted);
 
     if !is_valid {
         let allowed = enum_values.variant_names().join(", ");

From 67cf752f6a39c75f25ffed0d4bb278989f86502c Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sat, 27 Jun 2026 23:47:11 +0900
Subject: [PATCH 021/109] retry-now#0020: batch -- hoist loader test_support,
 drop dead NameCase predicates, dedupe collect paths (3 kept)

- Item 1: Extract CwdGuard + write_default_config into vespertide-loader/src/test_support.rs.
  Previously the same RAII guard and write-config helper were triplicated across
  config.rs, migrations.rs, and models.rs #[cfg(test)] mod tests blocks; now they
  live in one shared #[cfg(test)] module behind impl AsRef.
- Item 2: Remove never-shipped NameCase::is_snake/is_camel/is_pascal predicate
  methods. Only callers were 4 in-crate test assertions; rewrite to direct
  equality (cfg.table_case() == NameCase::Snake). serde_rename_all kept (used by
  vespertide-exporter).
- Item 3: Reduce collect_model_paths_internal and collect_migration_paths_internal
  to 1-line wrappers over their anyhow siblings (.map_err(|e| e.to_string())).
  Same byte-for-byte traversal logic - duplication was purely error-type alias.
---
 crates/vespertide-config/src/lib.rs          |  8 +--
 crates/vespertide-config/src/name_case.rs    | 15 -----
 crates/vespertide-loader/src/config.rs       | 42 +++----------
 crates/vespertide-loader/src/lib.rs          |  2 +
 crates/vespertide-loader/src/migrations.rs   | 55 ++++-------------
 crates/vespertide-loader/src/models.rs       | 62 ++++----------------
 crates/vespertide-loader/src/test_support.rs | 39 ++++++++++++
 7 files changed, 76 insertions(+), 147 deletions(-)
 create mode 100644 crates/vespertide-loader/src/test_support.rs

diff --git a/crates/vespertide-config/src/lib.rs b/crates/vespertide-config/src/lib.rs
index e8f6640d..57ebace6 100644
--- a/crates/vespertide-config/src/lib.rs
+++ b/crates/vespertide-config/src/lib.rs
@@ -22,8 +22,8 @@ mod tests {
         let cfg = VespertideConfig::default();
         assert_eq!(cfg.models_dir, PathBuf::from("models"));
         assert_eq!(cfg.migrations_dir, PathBuf::from("migrations"));
-        assert!(cfg.table_case().is_snake());
-        assert!(cfg.column_case().is_snake());
+        assert_eq!(cfg.table_case(), NameCase::Snake);
+        assert_eq!(cfg.column_case(), NameCase::Snake);
     }
 
     #[test]
@@ -38,8 +38,8 @@ mod tests {
 
         assert_eq!(cfg.models_dir(), Path::new("custom_models"));
         assert_eq!(cfg.migrations_dir(), Path::new("custom_migrations"));
-        assert!(cfg.table_case().is_camel());
-        assert!(cfg.column_case().is_pascal());
+        assert_eq!(cfg.table_case(), NameCase::Camel);
+        assert_eq!(cfg.column_case(), NameCase::Pascal);
     }
 
     #[test]
diff --git a/crates/vespertide-config/src/name_case.rs b/crates/vespertide-config/src/name_case.rs
index d5d9c702..cc77236d 100644
--- a/crates/vespertide-config/src/name_case.rs
+++ b/crates/vespertide-config/src/name_case.rs
@@ -11,21 +11,6 @@ pub enum NameCase {
 }
 
 impl NameCase {
-    /// Returns true when snake case.
-    pub fn is_snake(self) -> bool {
-        matches!(self, NameCase::Snake)
-    }
-
-    /// Returns true when camel case.
-    pub fn is_camel(self) -> bool {
-        matches!(self, NameCase::Camel)
-    }
-
-    /// Returns true when pascal case.
-    pub fn is_pascal(self) -> bool {
-        matches!(self, NameCase::Pascal)
-    }
-
     /// Returns the serde `rename_all` attribute value for this case.
     pub fn serde_rename_all(self) -> &'static str {
         match self {
diff --git a/crates/vespertide-loader/src/config.rs b/crates/vespertide-loader/src/config.rs
index f3e27789..b5e57273 100644
--- a/crates/vespertide-loader/src/config.rs
+++ b/crates/vespertide-loader/src/config.rs
@@ -48,40 +48,16 @@ pub fn load_config_or_default(project_root: Option) -> Result Self {
-            let original = std::env::current_dir().unwrap();
-            std::env::set_current_dir(dir).unwrap();
-            Self { original }
-        }
-    }
-
-    impl Drop for CwdGuard {
-        fn drop(&mut self) {
-            let _ = std::env::set_current_dir(&self.original);
-        }
-    }
-
-    fn write_config(path: &PathBuf) {
-        let cfg = VespertideConfig::default();
-        let text = serde_json::to_string_pretty(&cfg).unwrap();
-        fs::write(path, text).unwrap();
-    }
-
     #[test]
     #[serial]
     fn test_load_config_from_path_success() {
         let tmp = tempdir().unwrap();
         let config_path = tmp.path().join("vespertide.json");
-        write_config(&config_path);
+        write_default_config(&config_path);
 
         let result = load_config_from_path(config_path);
         assert!(result.is_ok());
@@ -107,7 +83,7 @@ mod tests {
     fn test_load_config_or_default_with_root() {
         let tmp = tempdir().unwrap();
         let config_path = tmp.path().join("vespertide.json");
-        write_config(&config_path);
+        write_default_config(&config_path);
 
         let result = load_config_or_default(Some(tmp.path().to_path_buf()));
         assert!(result.is_ok());
@@ -119,9 +95,9 @@ mod tests {
     #[serial]
     fn test_load_config_or_default_without_root() {
         let tmp = tempdir().unwrap();
-        let _guard = CwdGuard::new(&tmp.path().to_path_buf());
+        let _guard = CwdGuard::new(tmp.path());
         let config_path = PathBuf::from("vespertide.json");
-        write_config(&config_path);
+        write_default_config(&config_path);
 
         let result = load_config_or_default(None);
         assert!(result.is_ok());
@@ -133,7 +109,7 @@ mod tests {
     #[serial]
     fn test_load_config_or_default_fallback_to_default() {
         let tmp = tempdir().unwrap();
-        let _guard = CwdGuard::new(&tmp.path().to_path_buf());
+        let _guard = CwdGuard::new(tmp.path());
 
         let result = load_config_or_default(None);
         assert!(result.is_ok());
@@ -145,9 +121,9 @@ mod tests {
     #[serial]
     fn test_load_config_success() {
         let tmp = tempdir().unwrap();
-        let _guard = CwdGuard::new(&tmp.path().to_path_buf());
+        let _guard = CwdGuard::new(tmp.path());
         let config_path = PathBuf::from("vespertide.json");
-        write_config(&config_path);
+        write_default_config(&config_path);
 
         let result = load_config();
         assert!(result.is_ok());
@@ -159,7 +135,7 @@ mod tests {
     #[serial]
     fn test_load_config_not_found() {
         let tmp = tempdir().unwrap();
-        let _guard = CwdGuard::new(&tmp.path().to_path_buf());
+        let _guard = CwdGuard::new(tmp.path());
 
         let result = load_config();
         assert!(result.is_err());
diff --git a/crates/vespertide-loader/src/lib.rs b/crates/vespertide-loader/src/lib.rs
index 5595763b..0ea8b768 100644
--- a/crates/vespertide-loader/src/lib.rs
+++ b/crates/vespertide-loader/src/lib.rs
@@ -18,6 +18,8 @@ pub mod config;
 pub mod migrations;
 pub mod models;
 mod parallel_config;
+#[cfg(test)]
+mod test_support;
 
 pub use config::{load_config, load_config_from_path, load_config_or_default};
 pub use migrations::{load_migrations, load_migrations_at_compile_time, load_migrations_from_dir};
diff --git a/crates/vespertide-loader/src/migrations.rs b/crates/vespertide-loader/src/migrations.rs
index ca2b15a8..a9b2a538 100644
--- a/crates/vespertide-loader/src/migrations.rs
+++ b/crates/vespertide-loader/src/migrations.rs
@@ -101,19 +101,7 @@ fn collect_migration_paths(dir: &Path) -> Result> {
 }
 
 fn collect_migration_paths_internal(dir: &Path) -> Result, String> {
-    let entries =
-        fs::read_dir(dir).map_err(|e| format!("Failed to read migrations directory: {e}"))?;
-    let mut paths = Vec::new();
-
-    for entry in entries {
-        let entry = entry.map_err(|e| format!("Failed to read directory entry: {e}"))?;
-        let path = entry.path();
-        if path.is_file() && has_migration_extension(&path) {
-            paths.push(path);
-        }
-    }
-
-    Ok(paths)
+    collect_migration_paths(dir).map_err(|e| e.to_string())
 }
 
 fn has_migration_extension(path: &Path) -> bool {
@@ -169,41 +157,18 @@ pub fn load_migrations_at_compile_time() -> Result, Box Self {
-            let original = env::current_dir().unwrap();
-            env::set_current_dir(dir).unwrap();
-            Self { original }
-        }
-    }
-
-    impl Drop for CwdGuard {
-        fn drop(&mut self) {
-            let _ = env::set_current_dir(&self.original);
-        }
-    }
-
-    fn write_config(dir: &std::path::Path) {
-        let cfg = VespertideConfig::default();
-        let text = serde_json::to_string_pretty(&cfg).unwrap();
-        fs::write(dir.join("vespertide.json"), text).unwrap();
-    }
-
     #[test]
     #[serial]
     fn test_load_migrations_returns_empty_when_no_migrations_dir() {
         let temp_dir = TempDir::new().unwrap();
-        let _guard = CwdGuard::new(&temp_dir.path().to_path_buf());
-        write_config(temp_dir.path());
+        let _guard = CwdGuard::new(temp_dir.path());
+        write_default_config(temp_dir.path().join("vespertide.json"));
 
         let result = load_migrations(&VespertideConfig::default()).unwrap();
         assert!(result.is_empty());
@@ -213,8 +178,8 @@ mod tests {
     #[serial]
     fn test_load_migrations_reads_json_and_sorts_versions() {
         let temp_dir = TempDir::new().unwrap();
-        let _guard = CwdGuard::new(&temp_dir.path().to_path_buf());
-        write_config(temp_dir.path());
+        let _guard = CwdGuard::new(temp_dir.path());
+        write_default_config(temp_dir.path().join("vespertide.json"));
         fs::create_dir_all("migrations").unwrap();
         fs::write(
             "migrations/0002_second.json",
@@ -242,8 +207,8 @@ mod tests {
     #[serial]
     fn load_migrations_ignores_non_migration_extension_files() {
         let temp_dir = TempDir::new().unwrap();
-        let _guard = CwdGuard::new(&temp_dir.path().to_path_buf());
-        write_config(temp_dir.path());
+        let _guard = CwdGuard::new(temp_dir.path());
+        write_default_config(temp_dir.path().join("vespertide.json"));
         fs::create_dir_all("migrations").unwrap();
         fs::write("migrations/notes.txt", "not a migration: {{{ invalid").unwrap();
 
@@ -403,8 +368,8 @@ actions:
     #[serial]
     fn test_load_migrations_reads_yaml_for_runtime_loader() {
         let temp_dir = TempDir::new().unwrap();
-        let _guard = CwdGuard::new(&temp_dir.path().to_path_buf());
-        write_config(temp_dir.path());
+        let _guard = CwdGuard::new(temp_dir.path());
+        write_default_config(temp_dir.path().join("vespertide.json"));
         fs::create_dir_all("migrations").unwrap();
 
         let migration_content = r"---
diff --git a/crates/vespertide-loader/src/models.rs b/crates/vespertide-loader/src/models.rs
index 8eff470d..c8a27e73 100644
--- a/crates/vespertide-loader/src/models.rs
+++ b/crates/vespertide-loader/src/models.rs
@@ -163,22 +163,7 @@ fn load_models_recursive_internal(
 }
 
 fn collect_model_paths_internal(dir: &Path) -> Result, String> {
-    let entries = fs::read_dir(dir)
-        .map_err(|e| format!("Failed to read models directory {}: {}", dir.display(), e))?;
-    let mut paths = Vec::new();
-
-    for entry in entries {
-        let entry = entry.map_err(|e| format!("Failed to read directory entry: {e}"))?;
-        let path = entry.path();
-
-        if path.is_dir() {
-            paths.extend(collect_model_paths_internal(&path)?);
-        } else if path.is_file() && has_model_extension(&path) {
-            paths.push(path);
-        }
-    }
-
-    Ok(paths)
+    collect_model_paths(dir).map_err(|e| e.to_string())
 }
 
 fn load_normalized_model_file_internal(path: &Path) -> Result {
@@ -211,6 +196,7 @@ pub fn load_models_at_compile_time() -> Result, Box Self {
-            let original = std::env::current_dir().unwrap();
-            std::env::set_current_dir(dir).unwrap();
-            Self { original }
-        }
-    }
-
-    impl Drop for CwdGuard {
-        fn drop(&mut self) {
-            let _ = std::env::set_current_dir(&self.original);
-        }
-    }
-
-    fn write_config() {
-        let cfg = VespertideConfig::default();
-        let text = serde_json::to_string_pretty(&cfg).unwrap();
-        fs::write("vespertide.json", text).unwrap();
-    }
-
     #[test]
     #[serial]
     fn load_models_returns_empty_when_no_models_dir() {
         let tmp = tempdir().unwrap();
-        let _guard = CwdGuard::new(&tmp.path().to_path_buf());
-        write_config();
+        let _guard = CwdGuard::new(tmp.path());
+        write_default_config("vespertide.json");
 
         // Don't create models directory
         let models = load_models(&VespertideConfig::default()).unwrap();
@@ -259,8 +221,8 @@ mod tests {
     #[serial]
     fn load_models_reads_yaml_and_validates() {
         let tmp = tempdir().unwrap();
-        let _guard = CwdGuard::new(&tmp.path().to_path_buf());
-        write_config();
+        let _guard = CwdGuard::new(tmp.path());
+        write_default_config("vespertide.json");
 
         fs::create_dir_all("models").unwrap();
         let table = TableDef {
@@ -294,8 +256,8 @@ mod tests {
     #[serial]
     fn load_models_recursive_processes_subdirectories() {
         let tmp = tempdir().unwrap();
-        let _guard = CwdGuard::new(&tmp.path().to_path_buf());
-        write_config();
+        let _guard = CwdGuard::new(tmp.path());
+        write_default_config("vespertide.json");
 
         fs::create_dir_all("models/subdir").unwrap();
 
@@ -332,8 +294,8 @@ mod tests {
     #[serial]
     fn load_models_fails_on_invalid_fk_format() {
         let tmp = tempdir().unwrap();
-        let _guard = CwdGuard::new(&tmp.path().to_path_buf());
-        write_config();
+        let _guard = CwdGuard::new(tmp.path());
+        write_default_config("vespertide.json");
 
         fs::create_dir_all("models").unwrap();
 
@@ -375,8 +337,8 @@ mod tests {
     #[serial]
     fn load_models_ignores_non_model_extension_files() {
         let tmp = tempdir().unwrap();
-        let _guard = CwdGuard::new(&tmp.path().to_path_buf());
-        write_config();
+        let _guard = CwdGuard::new(tmp.path());
+        write_default_config("vespertide.json");
         fs::create_dir_all("models").unwrap();
         fs::write("models/README.txt", "not a model: {{{ invalid").unwrap();
 
diff --git a/crates/vespertide-loader/src/test_support.rs b/crates/vespertide-loader/src/test_support.rs
new file mode 100644
index 00000000..71d9d0cf
--- /dev/null
+++ b/crates/vespertide-loader/src/test_support.rs
@@ -0,0 +1,39 @@
+//! Shared `#[cfg(test)]` helpers for the `vespertide-loader` crate.
+//!
+//! Hosts the RAII `CwdGuard` (used to enter a `tempfile::TempDir` for the
+//! duration of a `#[serial]` test that exercises code reading
+//! `vespertide.json` from the current directory) and a `write_default_config`
+//! helper that serialises `VespertideConfig::default()` to a target path.
+
+use std::fs;
+use std::path::{Path, PathBuf};
+
+use vespertide_config::VespertideConfig;
+
+/// RAII guard that switches `std::env::current_dir()` into `dir` on
+/// construction and restores the original directory on drop. Use with
+/// `#[serial]` since `current_dir` is process-global state.
+pub(crate) struct CwdGuard {
+    original: PathBuf,
+}
+
+impl CwdGuard {
+    pub(crate) fn new(dir: impl AsRef) -> Self {
+        let original = std::env::current_dir().unwrap();
+        std::env::set_current_dir(dir.as_ref()).unwrap();
+        Self { original }
+    }
+}
+
+impl Drop for CwdGuard {
+    fn drop(&mut self) {
+        let _ = std::env::set_current_dir(&self.original);
+    }
+}
+
+/// Write a pretty-printed `VespertideConfig::default()` JSON to `at`.
+pub(crate) fn write_default_config(at: impl AsRef) {
+    let cfg = VespertideConfig::default();
+    let text = serde_json::to_string_pretty(&cfg).unwrap();
+    fs::write(at.as_ref(), text).unwrap();
+}

From 947cdbb3688131bd2ed89234ebb17b60aa4d9d44 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 00:21:51 +0900
Subject: [PATCH 022/109] retry-now#0021: batch -- hoist 3 vespertide-query
 test helpers (3 kept)

Dedupe boilerplate across vespertide-query test modules by hoisting
three pub(crate) helpers into crates/vespertide-query/src/test_support.rs:

1. table_def(name, columns, constraints) -- byte-identical local helper
   was repeated in three sql/modify_column_* test modules (comment, default,
   nullable); also drop the duplicate local fn col in modify_column_default.rs
   already covered by existing test_support::col_n.

2. joined_sql(backend, &queries) -- replaces 10 occurrences of the chained
   queries.iter().map(|q| q.build(backend)).collect::>().join("\n")
   pattern in sql/add_constraint/mod.rs.

3. backend_tag(backend) -> &'static str -- replaces 4 multi-line
   match backend { Postgres => "postgres", ... } snapshot-suffix tags in
   sql/tests/naming.rs.

Net: 6 files, +67 / -115 lines. No production code, no public API, no SQL
output, and no .snap files change -- every kept change is a pure test-code
dedup that shrinks the drift surface when a new TableDef field, new
BuiltQuery variant, or new DatabaseBackend variant is added.
---
 .../src/sql/add_constraint/mod.rs             | 57 ++++---------------
 .../src/sql/modify_column_comment.rs          | 17 +-----
 .../src/sql/modify_column_default.rs          | 20 +------
 .../src/sql/modify_column_nullable.rs         | 17 +-----
 .../vespertide-query/src/sql/tests/naming.rs  | 25 ++------
 crates/vespertide-query/src/test_support.rs   | 46 ++++++++++++++-
 6 files changed, 67 insertions(+), 115 deletions(-)

diff --git a/crates/vespertide-query/src/sql/add_constraint/mod.rs b/crates/vespertide-query/src/sql/add_constraint/mod.rs
index 69b535a2..794d3916 100644
--- a/crates/vespertide-query/src/sql/add_constraint/mod.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/mod.rs
@@ -6,6 +6,7 @@ mod primary_key;
 mod tests {
     use super::*;
     use crate::sql::types::DatabaseBackend;
+    use crate::test_support::joined_sql;
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{
@@ -173,11 +174,7 @@ mod tests {
         // F3 introduced multi-statement output for FK additions (pre-cleanup +
         // ADD CONSTRAINT). Search across every emitted statement so the
         // assertion still locates the constraint SQL regardless of position.
-        let sql = result
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &result);
         for exp in expected {
             assert!(
                 sql.contains(exp),
@@ -185,7 +182,7 @@ mod tests {
             );
         }
         with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("add_constraint_{}", title) }, {
-            assert_snapshot!(result.iter().map(|q| q.build(backend)).collect::>().join("\n"));
+            assert_snapshot!(joined_sql(backend, &result));
         });
     }
     #[test]
@@ -244,11 +241,7 @@ mod tests {
         // into NOT VALID + VALIDATE. Validate identifier escaping appears in
         // both statements by joining the full result and asserting on every
         // distinct emitted form.
-        let pg_sql = pg_results
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Postgres))
-            .collect::>()
-            .join("\n");
+        let pg_sql = joined_sql(DatabaseBackend::Postgres, &pg_results);
         assert!(pg_sql.contains("ALTER TABLE \"users\"\"archive\" ADD CONSTRAINT \"chk_age\"\"quote\" CHECK (age > 0) NOT VALID"), "PG NOT VALID statement missing or mis-escaped, got: {pg_sql}");
         assert!(
             pg_sql.contains(
@@ -316,11 +309,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(DatabaseBackend::Sqlite, &queries);
         assert!(sql.contains("CONSTRAINT \"chk_id\" CHECK"));
     }
     #[test]
@@ -358,11 +347,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(DatabaseBackend::Sqlite, &queries);
         assert!(sql.contains("CREATE INDEX"));
         assert!(sql.contains("idx_id"));
     }
@@ -404,11 +389,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(DatabaseBackend::Sqlite, &queries);
         assert!(sql.contains("CREATE TABLE"));
     }
     #[test]
@@ -462,11 +443,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(DatabaseBackend::Sqlite, &queries);
         assert!(sql.contains("CREATE TABLE"));
         assert!(sql.contains("CONSTRAINT \"chk_age\" CHECK"));
     }
@@ -502,11 +479,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(DatabaseBackend::Sqlite, &queries);
         assert!(sql.contains("CREATE TABLE"));
         assert!(sql.contains("PRIMARY KEY"));
     }
@@ -546,11 +519,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(DatabaseBackend::Sqlite, &queries);
         assert!(sql.contains("CREATE INDEX"));
         assert!(sql.contains("idx_age"));
     }
@@ -592,11 +561,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(DatabaseBackend::Sqlite, &queries);
         assert!(sql.contains("CREATE TABLE"));
     }
     #[test]
diff --git a/crates/vespertide-query/src/sql/modify_column_comment.rs b/crates/vespertide-query/src/sql/modify_column_comment.rs
index f1a8ffe9..1d251301 100644
--- a/crates/vespertide-query/src/sql/modify_column_comment.rs
+++ b/crates/vespertide-query/src/sql/modify_column_comment.rs
@@ -89,23 +89,10 @@ pub fn build_modify_column_comment(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::col_n as col;
+    use crate::test_support::{col_n as col, table_def};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
-    use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableConstraint};
-
-    fn table_def(
-        name: &str,
-        columns: Vec,
-        constraints: Vec,
-    ) -> TableDef {
-        TableDef {
-            name: name.into(),
-            description: None,
-            columns,
-            constraints,
-        }
-    }
+    use vespertide_core::{ColumnType, SimpleColumnType};
 
     #[rstest]
     #[case::postgres_set_comment(DatabaseBackend::Postgres, Some("User email address"))]
diff --git a/crates/vespertide-query/src/sql/modify_column_default.rs b/crates/vespertide-query/src/sql/modify_column_default.rs
index 34d8a4fb..526b6745 100644
--- a/crates/vespertide-query/src/sql/modify_column_default.rs
+++ b/crates/vespertide-query/src/sql/modify_column_default.rs
@@ -176,26 +176,10 @@ pub fn build_modify_column_default(
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::test_support::{col_n as col, table_def};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
-    use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableConstraint};
-
-    fn col(name: &str, ty: ColumnType, nullable: bool) -> ColumnDef {
-        ColumnDef::new(name, ty, nullable)
-    }
-
-    fn table_def(
-        name: &str,
-        columns: Vec,
-        constraints: Vec,
-    ) -> TableDef {
-        TableDef {
-            name: name.into(),
-            description: None,
-            columns,
-            constraints,
-        }
-    }
+    use vespertide_core::{ColumnType, SimpleColumnType, TableConstraint};
 
     #[rstest]
     #[case::postgres_set_default(DatabaseBackend::Postgres, Some("'unknown'"))]
diff --git a/crates/vespertide-query/src/sql/modify_column_nullable.rs b/crates/vespertide-query/src/sql/modify_column_nullable.rs
index 373dd2b1..be2af2af 100644
--- a/crates/vespertide-query/src/sql/modify_column_nullable.rs
+++ b/crates/vespertide-query/src/sql/modify_column_nullable.rs
@@ -146,23 +146,10 @@ pub fn build_modify_column_nullable(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::col_n as col;
+    use crate::test_support::{col_n as col, table_def};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
-    use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableConstraint};
-
-    fn table_def(
-        name: &str,
-        columns: Vec,
-        constraints: Vec,
-    ) -> TableDef {
-        TableDef {
-            name: name.into(),
-            description: None,
-            columns,
-            constraints,
-        }
-    }
+    use vespertide_core::{ColumnType, SimpleColumnType, TableConstraint};
 
     #[rstest]
     #[case::postgres_set_not_null(DatabaseBackend::Postgres, false, None)]
diff --git a/crates/vespertide-query/src/sql/tests/naming.rs b/crates/vespertide-query/src/sql/tests/naming.rs
index f47df2ef..ffef0d88 100644
--- a/crates/vespertide-query/src/sql/tests/naming.rs
+++ b/crates/vespertide-query/src/sql/tests/naming.rs
@@ -1,4 +1,5 @@
 use super::*;
+use crate::test_support::backend_tag;
 use vespertide_core::TableDef;
 
 // Comprehensive unique constraint naming tests
@@ -329,11 +330,7 @@ fn test_build_action_queries_modify_column_nullable(#[case] backend: DatabaseBac
 
     let suffix = format!(
         "{}_modify_nullable",
-        match backend {
-            DatabaseBackend::Postgres => "postgres",
-            DatabaseBackend::MySql => "mysql",
-            DatabaseBackend::Sqlite => "sqlite",
-        }
+        backend_tag(backend)
     );
 
     with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => suffix }, {
@@ -382,11 +379,7 @@ fn test_build_action_queries_modify_column_default(#[case] backend: DatabaseBack
 
     let suffix = format!(
         "{}_modify_default",
-        match backend {
-            DatabaseBackend::Postgres => "postgres",
-            DatabaseBackend::MySql => "mysql",
-            DatabaseBackend::Sqlite => "sqlite",
-        }
+        backend_tag(backend)
     );
 
     with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => suffix }, {
@@ -435,11 +428,7 @@ fn test_build_action_queries_modify_column_comment(#[case] backend: DatabaseBack
 
     let suffix = format!(
         "{}_modify_comment",
-        match backend {
-            DatabaseBackend::Postgres => "postgres",
-            DatabaseBackend::MySql => "mysql",
-            DatabaseBackend::Sqlite => "sqlite",
-        }
+        backend_tag(backend)
     );
 
     with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => suffix }, {
@@ -601,11 +590,7 @@ fn test_replace_fk_constraint(#[case] backend: DatabaseBackend) {
 
     let suffix = format!(
         "replace_fk_constraint_{}",
-        match backend {
-            DatabaseBackend::Postgres => "postgres",
-            DatabaseBackend::MySql => "mysql",
-            DatabaseBackend::Sqlite => "sqlite",
-        }
+        backend_tag(backend)
     );
 
     with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => suffix }, {
diff --git a/crates/vespertide-query/src/test_support.rs b/crates/vespertide-query/src/test_support.rs
index 4cab23c2..8ca95856 100644
--- a/crates/vespertide-query/src/test_support.rs
+++ b/crates/vespertide-query/src/test_support.rs
@@ -4,7 +4,7 @@
 //! in `lib.rs` and exposed `pub(crate)` so every inline `mod tests` and
 //! `sql/tests/mod.rs` entry can reuse the same implementation.
 
-use vespertide_core::{ColumnDef, ColumnType};
+use vespertide_core::{ColumnDef, ColumnType, TableConstraint, TableDef};
 
 /// Test column helper defaulting to `nullable: true`, matching the existing
 /// convention in `crates/vespertide-query/src/sql/tests/mod.rs`.
@@ -16,3 +16,47 @@ pub(crate) fn col(name: &str, ty: ColumnType) -> ColumnDef {
 pub(crate) fn col_n(name: &str, ty: ColumnType, nullable: bool) -> ColumnDef {
     ColumnDef::new(name, ty, nullable)
 }
+
+/// Build a `TableDef` from name + columns + constraints. Hoisted from a
+/// byte-identical local helper that lived in three `sql/modify_column_*` test
+/// modules (`comment`, `default`, `nullable`).
+pub(crate) fn table_def(
+    name: &str,
+    columns: Vec,
+    constraints: Vec,
+) -> TableDef {
+    TableDef {
+        name: name.into(),
+        description: None,
+        columns,
+        constraints,
+    }
+}
+
+/// Render every `BuiltQuery` for one backend and join with newlines. Mirrors
+/// the chained `.iter().map(...).collect::>().join("\n")` pattern
+/// repeated across `sql/**/tests` modules. The fully-qualified types keep
+/// this helper independent of any consumer's `use` order.
+pub(crate) fn joined_sql(
+    backend: crate::sql::DatabaseBackend,
+    queries: &[crate::sql::BuiltQuery],
+) -> String {
+    queries
+        .iter()
+        .map(|q| q.build(backend))
+        .collect::>()
+        .join("\n")
+}
+
+/// Snapshot-suffix tag for a backend. The string is part of the snapshot
+/// file name, so changing it would invalidate every `assert_snapshot!` that
+/// uses it — keep it byte-stable. Hoisted from a `match backend { ... }`
+/// block repeated across `sql/tests/naming.rs` (and several
+/// `sql/modify_column_*` tests, left for a follow-up iteration).
+pub(crate) fn backend_tag(backend: crate::sql::DatabaseBackend) -> &'static str {
+    match backend {
+        crate::sql::DatabaseBackend::Postgres => "postgres",
+        crate::sql::DatabaseBackend::MySql => "mysql",
+        crate::sql::DatabaseBackend::Sqlite => "sqlite",
+    }
+}

From cad969c446b574190ff00f6619f552527121eb98 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 02:01:42 +0900
Subject: [PATCH 023/109] retry-now#0022: batch -- hoist col_int helper, drop
 dead col helper, cache ERD edge_geometry (3 kept)

1. Hoist col_int(name, nullable) to vespertide-planner/src/test_support.rs
   and delete the four byte-identical local n col(name, nullable) helpers
   in validate/{check_additions, fk_addcolumn_nullable, fk_orphan_additions,
   pk_additions}.rs. Net -43 LOC across the four modules, +13 LOC in
   test_support, 58 test-call sites renamed col -> col_int. Continues the
   iter-17/iter-20/iter-21 trajectory of consolidating per-validator
   ColumnDef builders into a single shared helper.

2. Remove redundant local n col(name, ty) in
   validate/check_type_mismatch.rs (byte-equivalent to the existing
   test_support::col). Adds col to the test_support import line and
   deletes the 13-line dead helper. Call sites stay identical.

3. Precompute edge_geometry once per edge in commands/erd/svg/render.rs
   so the two render passes (path / label) reuse a cached EdgeGeometry
   struct instead of recomputing pick_anchors + parallel_curvature_offset
   for every edge twice. edge_geometry now returns a struct (was a 7-tuple)
   and render_edge_path / render_edge_label take geom: EdgeGeometry as a
   trailing parameter; render_edge_label no longer takes child/parent
   since its only use of them was the inlined edge_geometry call.
   Verified byte-identical SVG output via 0 .snap.new across the entire
   erd/svg insta snapshot suite.
---
 .../src/commands/erd/svg/edges.rs             | 43 ++++++++++++++-----
 .../src/commands/erd/svg/render.rs            | 24 +++++++----
 crates/vespertide-planner/src/test_support.rs | 19 +++++++-
 .../src/validate/check_additions.rs           | 32 ++++----------
 .../src/validate/check_type_mismatch.rs       | 16 +------
 .../src/validate/fk_addcolumn_nullable.rs     | 41 ++++++------------
 .../src/validate/fk_orphan_additions.rs       | 43 ++++++-------------
 .../src/validate/pk_additions.rs              | 42 ++++++------------
 8 files changed, 115 insertions(+), 145 deletions(-)

diff --git a/crates/vespertide-cli/src/commands/erd/svg/edges.rs b/crates/vespertide-cli/src/commands/erd/svg/edges.rs
index b2dd7295..79f0da72 100644
--- a/crates/vespertide-cli/src/commands/erd/svg/edges.rs
+++ b/crates/vespertide-cli/src/commands/erd/svg/edges.rs
@@ -38,16 +38,40 @@ pub(super) enum Side {
     Bottom,
 }
 
-fn edge_geometry(
+/// Pre-computed routing data for a single edge.
+///
+/// Computed once per edge in [`super::render::render_doc`] and reused by both
+/// [`render_edge_path`] and [`render_edge_label`] so the two render passes do
+/// not recompute the anchors / curvature for every edge twice.
+#[derive(Copy, Clone, Debug)]
+pub(super) struct EdgeGeometry {
+    pub sx: f64,
+    pub sy: f64,
+    pub ex: f64,
+    pub ey: f64,
+    pub sdir: Side,
+    pub edir: Side,
+    pub curvature: f64,
+}
+
+pub(super) fn edge_geometry(
     child: &TableBox,
     parent: &TableBox,
     edge: &EdgeSpec,
-) -> (f64, f64, f64, f64, Side, Side, f64) {
+) -> EdgeGeometry {
     let child_y = child.y + HEADER_H + edge.child_row as f64 * ROW_H + ROW_H / 2.0;
     let parent_y = parent.y + HEADER_H + edge.parent_row as f64 * ROW_H + ROW_H / 2.0;
     let (sx, sy, ex, ey, sdir, edir) = pick_anchors(child, parent, child_y, parent_y);
     let curvature = parallel_curvature_offset(edge.parallel_index, edge.parallel_count);
-    (sx, sy, ex, ey, sdir, edir, curvature)
+    EdgeGeometry {
+        sx,
+        sy,
+        ex,
+        ey,
+        sdir,
+        edir,
+        curvature,
+    }
 }
 
 pub(super) fn render_edge_path(
@@ -55,9 +79,9 @@ pub(super) fn render_edge_path(
     child: &TableBox,
     parent: &TableBox,
     edge: &EdgeSpec,
+    geom: EdgeGeometry,
 ) {
-    let (sx, sy, ex, ey, sdir, edir, curvature) = edge_geometry(child, parent, edge);
-    let path = bezier_path(sx, sy, ex, ey, sdir, edir, curvature);
+    let path = bezier_path(geom.sx, geom.sy, geom.ex, geom.ey, geom.sdir, geom.edir, geom.curvature);
 
     // Two-layer stroke: subtle wide halo + crisp narrow stroke for a soft look.
     let _ = writeln!(
@@ -76,16 +100,15 @@ pub(super) fn render_edge_path(
 
 pub(super) fn render_edge_label(
     out: &mut String,
-    child: &TableBox,
-    parent: &TableBox,
     edge: &EdgeSpec,
+    geom: EdgeGeometry,
 ) {
-    let (sx, sy, ex, ey, sdir, edir, curvature) = edge_geometry(child, parent, edge);
-
     // Label position is spread along the curve for parallel edges so the
     // cardinality badges no longer stack on top of one another.
     let label_t = label_t_for_parallel(edge.parallel_index, edge.parallel_count);
-    let (label_x, label_y) = bezier_at(sx, sy, ex, ey, sdir, edir, curvature, label_t);
+    let (label_x, label_y) = bezier_at(
+        geom.sx, geom.sy, geom.ex, geom.ey, geom.sdir, geom.edir, geom.curvature, label_t,
+    );
 
     // Pill-shaped white background guarantees the label stays readable when
     // curves or other labels cross it.
diff --git a/crates/vespertide-cli/src/commands/erd/svg/render.rs b/crates/vespertide-cli/src/commands/erd/svg/render.rs
index 73a6285b..bd3446e3 100644
--- a/crates/vespertide-cli/src/commands/erd/svg/render.rs
+++ b/crates/vespertide-cli/src/commands/erd/svg/render.rs
@@ -17,7 +17,7 @@
 
 use std::fmt::Write as _;
 
-use super::edges::{render_edge_label, render_edge_path};
+use super::edges::{EdgeGeometry, edge_geometry, render_edge_label, render_edge_path};
 use super::model::{EdgeSpec, RowSpec, TableBox};
 use super::style::{
     BADGE_FS, BADGE_GAP, BADGE_H, BADGE_W, BG, CARD_BG, CARD_BORDER, FK_BG, FK_FG, FONT_FAMILY,
@@ -49,16 +49,27 @@ pub(super) fn render_doc(boxes: &[TableBox], edges: &[EdgeSpec], vw: f64, vh: f6
         bg = BG
     );
 
+    // Pre-compute every edge's routing geometry once. Previously each edge
+    // ran `edge_geometry` twice — once during the path pass and again during
+    // the label pass — so for an ERD with N edges we were doing 2N anchor +
+    // curvature computations. Caching the per-edge result before the two
+    // passes halves that work without changing a single pixel of the output.
+    let geometries: Vec = edges
+        .iter()
+        .map(|e| edge_geometry(&boxes[e.child_idx], &boxes[e.parent_idx], e))
+        .collect();
+
     // Pass 1: draw every edge path. Doing all paths before any labels
     // guarantees label pills are never overdrawn by another edge in a
     // dense bundle (junction tables, self-references, etc.).
     out.push_str("  \n");
-    for edge in edges {
+    for (edge, geom) in edges.iter().zip(&geometries) {
         render_edge_path(
             &mut out,
             &boxes[edge.child_idx],
             &boxes[edge.parent_idx],
             edge,
+            *geom,
         );
     }
     out.push_str("  \n");
@@ -74,13 +85,8 @@ pub(super) fn render_doc(boxes: &[TableBox], edges: &[EdgeSpec], vw: f64, vh: f6
     // Pass 2: cardinality labels (pill + text). Always on top so they stay
     // readable regardless of how many curves cross their location.
     out.push_str("  \n");
-    for edge in edges {
-        render_edge_label(
-            &mut out,
-            &boxes[edge.child_idx],
-            &boxes[edge.parent_idx],
-            edge,
-        );
+    for (edge, geom) in edges.iter().zip(&geometries) {
+        render_edge_label(&mut out, edge, *geom);
     }
     out.push_str("  \n");
 
diff --git a/crates/vespertide-planner/src/test_support.rs b/crates/vespertide-planner/src/test_support.rs
index 073e9d50..6166a1b2 100644
--- a/crates/vespertide-planner/src/test_support.rs
+++ b/crates/vespertide-planner/src/test_support.rs
@@ -5,8 +5,8 @@
 //! `tests/mod.rs` entry can reuse the same implementation.
 
 use vespertide_core::{
-    CheckViolationStrategy, ColumnDef, ColumnType, MigrationAction, MigrationPlan, TableConstraint,
-    TableDef,
+    CheckViolationStrategy, ColumnDef, ColumnType, MigrationAction, MigrationPlan, SimpleColumnType,
+    TableConstraint, TableDef,
 };
 
 /// Default test column (NOT NULL). Mirrors the production convention that
@@ -21,6 +21,21 @@ pub(crate) fn col_nullable(name: &str, ty: ColumnType) -> ColumnDef {
     ColumnDef::new(name, ty, true)
 }
 
+/// Integer-typed test column with explicit nullability.
+///
+/// Hoisted from four byte-identical `fn col(name, nullable) -> ColumnDef`
+/// helpers inside `validate/*` test modules. Every caller hard-coded the
+/// column type to `Integer` and set every inline-constraint field to `None`,
+/// which is precisely what `ColumnDef::new(name, Integer, nullable)`
+/// produces.
+pub(crate) fn col_int(name: &str, nullable: bool) -> ColumnDef {
+    ColumnDef::new(
+        name,
+        ColumnType::Simple(SimpleColumnType::Integer),
+        nullable,
+    )
+}
+
 /// Build a table from name + columns + constraints.
 pub(crate) fn table(
     name: &str,
diff --git a/crates/vespertide-planner/src/validate/check_additions.rs b/crates/vespertide-planner/src/validate/check_additions.rs
index 859e94fb..3d33f04e 100644
--- a/crates/vespertide-planner/src/validate/check_additions.rs
+++ b/crates/vespertide-planner/src/validate/check_additions.rs
@@ -125,23 +125,9 @@ fn find_check_target_column(expr: &str, table: &TableDef) -> Option<(String, boo
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::{add_check, plan};
+    use crate::test_support::{add_check, col_int, plan};
     use rstest::rstest;
-    use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableDef};
-
-    fn col(name: &str, nullable: bool) -> ColumnDef {
-        ColumnDef {
-            name: name.into(),
-            r#type: ColumnType::Simple(SimpleColumnType::Integer),
-            nullable,
-            default: None,
-            comment: None,
-            primary_key: None,
-            unique: None,
-            index: None,
-            foreign_key: None,
-        }
-    }
+    use vespertide_core::{ColumnDef, TableDef};
 
     fn table(name: &str, cols: Vec) -> TableDef {
         TableDef {
@@ -156,7 +142,7 @@ mod tests {
     fn case_01_simple_comparison_nullable_column_flagged() {
         let baseline = vec![table(
             "products",
-            vec![col("id", false), col("price", true)],
+            vec![col_int("id", false), col_int("price", true)],
         )];
         let p = plan(vec![add_check("products", "chk_positive", "price > 0")]);
         let ws = find_check_additions(&p, &baseline);
@@ -171,7 +157,7 @@ mod tests {
     fn case_02_simple_comparison_not_null_column_flagged_without_nullify() {
         let baseline = vec![table(
             "products",
-            vec![col("id", false), col("price", false)],
+            vec![col_int("id", false), col_int("price", false)],
         )];
         let p = plan(vec![add_check("products", "chk_positive", "price > 0")]);
         let ws = find_check_additions(&p, &baseline);
@@ -181,7 +167,7 @@ mod tests {
 
     #[rstest]
     fn case_03_in_clause_flagged() {
-        let baseline = vec![table("orders", vec![col("id", false), col("status", true)])];
+        let baseline = vec![table("orders", vec![col_int("id", false), col_int("status", true)])];
         let p = plan(vec![add_check(
             "orders",
             "chk_status",
@@ -199,7 +185,7 @@ mod tests {
         // positives.
         let baseline = vec![table(
             "audit",
-            vec![col("id", false), col("a", true), col("b", true)],
+            vec![col_int("id", false), col_int("a", true), col_int("b", true)],
         )];
         let p = plan(vec![add_check("audit", "chk_complex", "a > b")]);
         let ws = find_check_additions(&p, &baseline);
@@ -208,7 +194,7 @@ mod tests {
 
     #[rstest]
     fn case_05_function_call_skipped() {
-        let baseline = vec![table("users", vec![col("id", false), col("name", true)])];
+        let baseline = vec![table("users", vec![col_int("id", false), col_int("name", true)])];
         let p = plan(vec![add_check("users", "chk_name", "length(name) > 0")]);
         let ws = find_check_additions(&p, &baseline);
         assert!(ws.is_empty());
@@ -229,7 +215,7 @@ mod tests {
         // returns None for every existing column, so warning is
         // suppressed. (Either F12 will catch the unknown column or the
         // DB will reject the constraint at apply time.)
-        let baseline = vec![table("products", vec![col("id", false)])];
+        let baseline = vec![table("products", vec![col_int("id", false)])];
         let p = plan(vec![add_check("products", "chk_missing", "price > 0")]);
         let ws = find_check_additions(&p, &baseline);
         assert!(ws.is_empty());
@@ -239,7 +225,7 @@ mod tests {
     fn case_08_multiple_checks_each_flagged_separately() {
         let baseline = vec![table(
             "products",
-            vec![col("id", false), col("price", true), col("stock", true)],
+            vec![col_int("id", false), col_int("price", true), col_int("stock", true)],
         )];
         let p = plan(vec![
             add_check("products", "chk_price", "price > 0"),
diff --git a/crates/vespertide-planner/src/validate/check_type_mismatch.rs b/crates/vespertide-planner/src/validate/check_type_mismatch.rs
index 78411dc1..aa64af24 100644
--- a/crates/vespertide-planner/src/validate/check_type_mismatch.rs
+++ b/crates/vespertide-planner/src/validate/check_type_mismatch.rs
@@ -441,23 +441,9 @@ fn format_literal(lit: &Literal) -> String {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::{add_check, check, plan};
+    use crate::test_support::{add_check, check, col, plan};
     use vespertide_core::{ColumnDef, ColumnType, EnumValues, SimpleColumnType, TableDef};
 
-    fn col(name: &str, ty: ColumnType) -> ColumnDef {
-        ColumnDef {
-            name: name.into(),
-            r#type: ty,
-            nullable: false,
-            default: None,
-            comment: None,
-            primary_key: None,
-            unique: None,
-            index: None,
-            foreign_key: None,
-        }
-    }
-
     fn baseline_table(table: &str, cols: Vec) -> TableDef {
         TableDef {
             name: table.into(),
diff --git a/crates/vespertide-planner/src/validate/fk_addcolumn_nullable.rs b/crates/vespertide-planner/src/validate/fk_addcolumn_nullable.rs
index 62919820..149a5b33 100644
--- a/crates/vespertide-planner/src/validate/fk_addcolumn_nullable.rs
+++ b/crates/vespertide-planner/src/validate/fk_addcolumn_nullable.rs
@@ -97,30 +97,13 @@ pub fn find_addcolumn_fk_nullable_violations(plan: &MigrationPlan) -> Vec ColumnDef {
-        ColumnDef {
-            name: name.into(),
-            r#type: ColumnType::Simple(SimpleColumnType::Integer),
-            nullable,
-            default: None,
-            comment: None,
-            primary_key: None,
-            unique: None,
-            index: None,
-            foreign_key: None,
-        }
-    }
-
     fn add_column(table: &str, column: ColumnDef, fill: Option<&str>) -> MigrationAction {
         MigrationAction::AddColumn {
             table: TableName::from(table),
@@ -162,7 +145,7 @@ mod tests {
     #[rstest]
     fn case_01_nullable_true_fill_inline_fk_ok() {
         // nullable: true + fill_with + inline FK -> no violation
-        let mut c = col("user_id", true);
+        let mut c = col_int("user_id", true);
         c.foreign_key = Some(fk_def("users", &["id"]));
         let p = plan(vec![add_column("posts", c, Some("1"))]);
         assert!(find_addcolumn_fk_nullable_violations(&p).is_empty());
@@ -171,7 +154,7 @@ mod tests {
     #[rstest]
     fn case_02_nullable_false_fill_inline_fk_violation() {
         // nullable: false + fill_with + inline FK -> violation
-        let mut c = col("user_id", false);
+        let mut c = col_int("user_id", false);
         c.foreign_key = Some(fk_def("users", &["id"]));
         let p = plan(vec![add_column("posts", c, Some("1"))]);
         let errs = find_addcolumn_fk_nullable_violations(&p);
@@ -191,7 +174,7 @@ mod tests {
     fn paired_fk_on_other_column_does_not_flag_unrelated_addcolumn() {
         // AddColumn `a` (non-nullable, has fill, NO inline FK) on `posts`,
         // plus an out-of-line FK that covers a DIFFERENT column `b`.
-        let c = col("a", false);
+        let c = col_int("a", false);
         let p = plan(vec![
             add_column("posts", c, Some("1")),
             add_constraint_fk("posts", &["b"], "users", &["id"]),
@@ -205,7 +188,7 @@ mod tests {
     #[rstest]
     fn case_03_nullable_false_default_inline_fk_violation() {
         // nullable: false + default + inline FK -> violation
-        let mut c = col("user_id", false);
+        let mut c = col_int("user_id", false);
         c.default = Some(StringOrBool::String("1".into()));
         c.foreign_key = Some(fk_def("users", &["id"]));
         let p = plan(vec![add_column("posts", c, None)]);
@@ -216,7 +199,7 @@ mod tests {
     #[rstest]
     fn case_04_nullable_false_fill_no_fk_ok() {
         // nullable: false + fill_with + no FK -> not a F3 case, no violation
-        let c = col("name", false);
+        let c = col_int("name", false);
         let p = plan(vec![add_column("posts", c, Some("'unknown'"))]);
         assert!(find_addcolumn_fk_nullable_violations(&p).is_empty());
     }
@@ -224,7 +207,7 @@ mod tests {
     #[rstest]
     fn case_05_paired_addconstraint_fk_violation() {
         // nullable: false + fill_with + separate AddConstraint(FK) -> violation
-        let c = col("user_id", false);
+        let c = col_int("user_id", false);
         let p = plan(vec![
             add_column("posts", c, Some("1")),
             add_constraint_fk("posts", &["user_id"], "users", &["id"]),
@@ -236,7 +219,7 @@ mod tests {
     #[rstest]
     fn case_06_paired_addconstraint_fk_nullable_ok() {
         // nullable: true + paired FK -> ok
-        let c = col("user_id", true);
+        let c = col_int("user_id", true);
         let p = plan(vec![
             add_column("posts", c, Some("1")),
             add_constraint_fk("posts", &["user_id"], "users", &["id"]),
@@ -248,7 +231,7 @@ mod tests {
     fn case_07_nullable_false_no_fill_no_default_ok() {
         // nullable: false but no fill_with/default -> separate fault (F1
         // via find_missing_fill_with). Not F3 Edge #1.
-        let mut c = col("user_id", false);
+        let mut c = col_int("user_id", false);
         c.foreign_key = Some(fk_def("users", &["id"]));
         let p = plan(vec![add_column("posts", c, None)]);
         assert!(find_addcolumn_fk_nullable_violations(&p).is_empty());
@@ -258,11 +241,11 @@ mod tests {
     fn case_08_composite_fk_one_violating() {
         // composite FK over two new columns, one violating nullable invariant
         let bad = {
-            let mut c = col("col_a", false);
+            let mut c = col_int("col_a", false);
             c.default = Some(StringOrBool::String("1".into()));
             c
         };
-        let good = col("col_b", true);
+        let good = col_int("col_b", true);
         let p = plan(vec![
             add_column("posts", bad, None),
             add_column("posts", good, None),
diff --git a/crates/vespertide-planner/src/validate/fk_orphan_additions.rs b/crates/vespertide-planner/src/validate/fk_orphan_additions.rs
index 5fc52182..d9d4c573 100644
--- a/crates/vespertide-planner/src/validate/fk_orphan_additions.rs
+++ b/crates/vespertide-planner/src/validate/fk_orphan_additions.rs
@@ -133,27 +133,12 @@ fn columns_to_strings(cols: &[ColumnName]) -> Vec {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::plan;
+    use crate::test_support::{col_int, plan};
     use rstest::rstest;
     use vespertide_core::{
-        ColumnDef, ColumnType, MigrationAction, SimpleColumnType, TableConstraint, TableDef,
-        TableName,
+        ColumnDef, MigrationAction, TableConstraint, TableDef, TableName,
     };
 
-    fn col(name: &str, nullable: bool) -> ColumnDef {
-        ColumnDef {
-            name: name.into(),
-            r#type: ColumnType::Simple(SimpleColumnType::Integer),
-            nullable,
-            default: None,
-            comment: None,
-            primary_key: None,
-            unique: None,
-            index: None,
-            foreign_key: None,
-        }
-    }
-
     fn table(name: &str, cols: Vec) -> TableDef {
         TableDef {
             name: name.into(),
@@ -187,7 +172,7 @@ mod tests {
     #[rstest]
     fn case_01_existing_nullable_column_flagged_with_nullify_available() {
         // Child column exists in baseline and is nullable -> warning + NullifyOrphans valid.
-        let baseline = vec![table("posts", vec![col("id", false), col("user_id", true)])];
+        let baseline = vec![table("posts", vec![col_int("id", false), col_int("user_id", true)])];
         let p = plan(vec![add_fk(
             "posts",
             Some("fk_user"),
@@ -208,7 +193,7 @@ mod tests {
         // Child column exists in baseline but is NOT NULL -> warning + only DeleteOrphans valid.
         let baseline = vec![table(
             "posts",
-            vec![col("id", false), col("user_id", false)],
+            vec![col_int("id", false), col_int("user_id", false)],
         )];
         let p = plan(vec![add_fk("posts", None, &["user_id"], "users", &["id"])]);
         let ws = find_fk_orphan_additions(&p, &baseline);
@@ -219,7 +204,7 @@ mod tests {
     #[rstest]
     fn case_03_new_column_skipped() {
         // FK references a column that doesn't yet exist in baseline -> no warning.
-        let baseline = vec![table("posts", vec![col("id", false)])];
+        let baseline = vec![table("posts", vec![col_int("id", false)])];
         let p = plan(vec![add_fk("posts", None, &["user_id"], "users", &["id"])]);
         let ws = find_fk_orphan_additions(&p, &baseline);
         assert!(ws.is_empty());
@@ -231,9 +216,9 @@ mod tests {
         let baseline = vec![table(
             "audit",
             vec![
-                col("id", false),
-                col("team_id", true),
-                col("member_id", true),
+                col_int("id", false),
+                col_int("team_id", true),
+                col_int("member_id", true),
             ],
         )];
         let p = plan(vec![add_fk(
@@ -255,7 +240,7 @@ mod tests {
     #[rstest]
     fn case_05_composite_fk_mixed_existing_and_new_skipped() {
         // Composite FK with one new column -> Edge #1's responsibility, skipped here.
-        let baseline = vec![table("audit", vec![col("id", false), col("team_id", true)])];
+        let baseline = vec![table("audit", vec![col_int("id", false), col_int("team_id", true)])];
         let p = plan(vec![add_fk(
             "audit",
             None,
@@ -273,9 +258,9 @@ mod tests {
         let baseline = vec![table(
             "audit",
             vec![
-                col("id", false),
-                col("team_id", true),
-                col("member_id", false),
+                col_int("id", false),
+                col_int("team_id", true),
+                col_int("member_id", false),
             ],
         )];
         let p = plan(vec![add_fk(
@@ -295,7 +280,7 @@ mod tests {
         // FK referencing the same table - still a warning (parent_id may dangle).
         let baseline = vec![table(
             "category",
-            vec![col("id", false), col("parent_id", true)],
+            vec![col_int("id", false), col_int("parent_id", true)],
         )];
         let p = plan(vec![add_fk(
             "category",
@@ -324,7 +309,7 @@ mod tests {
     /// action variant in addition to the warned one.
     #[rstest]
     fn case_09_mixed_plan_only_emits_fk_warning_and_skips_other_actions() {
-        let baseline = vec![table("posts", vec![col("id", false), col("user_id", true)])];
+        let baseline = vec![table("posts", vec![col_int("id", false), col_int("user_id", true)])];
         let p = plan(vec![
             // 0: AddConstraint Unique - not a FK, hits let-else continue
             MigrationAction::AddConstraint {
diff --git a/crates/vespertide-planner/src/validate/pk_additions.rs b/crates/vespertide-planner/src/validate/pk_additions.rs
index 3e7a4f3c..fbd7da5c 100644
--- a/crates/vespertide-planner/src/validate/pk_additions.rs
+++ b/crates/vespertide-planner/src/validate/pk_additions.rs
@@ -170,27 +170,13 @@ pub fn find_primary_key_additions(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::plan;
+    use crate::test_support::{col_int, plan};
     use rstest::rstest;
     use vespertide_core::{
-        ColumnDef, ColumnType, MigrationAction, PrimaryKeyAdditionStrategy, SimpleColumnType,
-        TableConstraint, TableDef, TableName,
+        ColumnDef, MigrationAction, PrimaryKeyAdditionStrategy, TableConstraint, TableDef,
+        TableName,
     };
 
-    fn col(name: &str, nullable: bool) -> ColumnDef {
-        ColumnDef {
-            name: name.into(),
-            r#type: ColumnType::Simple(SimpleColumnType::Integer),
-            nullable,
-            default: None,
-            comment: None,
-            primary_key: None,
-            unique: None,
-            index: None,
-            foreign_key: None,
-        }
-    }
-
     fn table(name: &str, cols: Vec, constraints: Vec) -> TableDef {
         TableDef {
             name: name.into(),
@@ -215,7 +201,7 @@ mod tests {
     fn case_01_single_existing_not_null_no_unique_flagged() {
         let baseline = vec![table(
             "users",
-            vec![col("id", false), col("email", false)],
+            vec![col_int("id", false), col_int("email", false)],
             vec![],
         )];
         let p = plan(vec![add_pk("users", &["email"])]);
@@ -232,7 +218,7 @@ mod tests {
     fn case_02_single_existing_nullable_flagged_with_nullable_list() {
         let baseline = vec![table(
             "users",
-            vec![col("id", false), col("email", true)],
+            vec![col_int("id", false), col_int("email", true)],
             vec![],
         )];
         let p = plan(vec![add_pk("users", &["email"])]);
@@ -244,7 +230,7 @@ mod tests {
 
     #[rstest]
     fn case_03_new_column_skipped() {
-        let baseline = vec![table("users", vec![col("id", false)], vec![])];
+        let baseline = vec![table("users", vec![col_int("id", false)], vec![])];
         let p = plan(vec![add_pk("users", &["new_col"])]);
         let ws = find_primary_key_additions(&p, &baseline);
         assert!(ws.is_empty());
@@ -252,7 +238,7 @@ mod tests {
 
     #[rstest]
     fn case_04_mixed_new_existing_skipped() {
-        let baseline = vec![table("users", vec![col("id", false)], vec![])];
+        let baseline = vec![table("users", vec![col_int("id", false)], vec![])];
         let p = plan(vec![add_pk("users", &["id", "new_col"])]);
         let ws = find_primary_key_additions(&p, &baseline);
         assert!(ws.is_empty());
@@ -270,7 +256,7 @@ mod tests {
     fn case_06_composite_pk_existing_columns_flagged() {
         let baseline = vec![table(
             "audit",
-            vec![col("team_id", true), col("member_id", false)],
+            vec![col_int("team_id", true), col_int("member_id", false)],
             vec![],
         )];
         let p = plan(vec![add_pk("audit", &["team_id", "member_id"])]);
@@ -289,7 +275,7 @@ mod tests {
     fn case_07_existing_unique_covers_pk_skipped() {
         let baseline = vec![table(
             "users",
-            vec![col("id", false), col("email", false)],
+            vec![col_int("id", false), col_int("email", false)],
             vec![TableConstraint::Unique {
                 name: Some("uq_email".into()),
                 columns: vec!["email".into()],
@@ -309,7 +295,7 @@ mod tests {
         // F5 should still flag for NULL handling.
         let baseline = vec![table(
             "users",
-            vec![col("id", false), col("email", true)],
+            vec![col_int("id", false), col_int("email", true)],
             vec![TableConstraint::Unique {
                 name: Some("uq_email".into()),
                 columns: vec!["email".into()],
@@ -328,9 +314,9 @@ mod tests {
 
     #[rstest]
     fn case_09_inline_pk_baseline_is_detected_as_single_pk() {
-        let mut id = col("id", false);
+        let mut id = col_int("id", false);
         id.primary_key = Some(vespertide_core::schema::primary_key::PrimaryKeySyntax::Bool(true));
-        let baseline = vec![table("users", vec![id, col("email", false)], vec![])];
+        let baseline = vec![table("users", vec![id, col_int("email", false)], vec![])];
         let p = plan(vec![add_pk("users", &["email"])]);
 
         let ws = find_primary_key_additions(&p, &baseline);
@@ -348,7 +334,7 @@ mod tests {
     fn case_10_non_unique_constraints_skipped_in_any_closure() {
         let baseline = vec![table(
             "users",
-            vec![col("id", false), col("email", false)],
+            vec![col_int("id", false), col_int("email", false)],
             vec![
                 // L125 wildcard arm: Check returns `false` from the
                 // closure so `iter().any(...)` keeps scanning.
@@ -390,7 +376,7 @@ mod tests {
     fn case_11_only_non_unique_constraints_still_warns_via_wildcard() {
         let baseline = vec![table(
             "users",
-            vec![col("id", false), col("email", false)],
+            vec![col_int("id", false), col_int("email", false)],
             vec![
                 // Only non-Unique constraint — every iter.any() step
                 // takes L125's `_ => false` branch.

From 4c47babbbd8ccaca1b4029cffea83cca933435ad Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 02:41:05 +0900
Subject: [PATCH 024/109] retry-now#0023: batch -- hoist node_at_byte to
 tree_util, inline i64_to_f64 (2 kept)

Item 1 (kept): consolidate 6 byte-identical BFS copies of node_at_byte across vespertide-lsp into pub(crate) tree_util::node_at_byte. completion/context.rs keeps its own descendant_for_byte_range variant (different algorithm for end-of-token cursor semantics).

Item 3 (kept): inline 4-line i64_to_f64 helper at its 6 call sites in check_self_contradiction.rs (literal_compare) and check_strengthening.rs (literal_equals + literal_compare); hoist #[expect(clippy::cast_precision_loss, ...)] to each enclosing function.

Item 2 reverted: replacing private strip_quotes with text_util::strip_quotes broke code_actions::extract_default_to_enum_offered_when_default_is_sql_literal because text_util also strips single quotes, which enum_extraction relies on to detect SQL string literals via strip_prefix('\\'').
---
 .../vespertide-lsp/src/check_expr_locate.rs   | 18 +--------
 crates/vespertide-lsp/src/definition/mod.rs   | 16 +-------
 crates/vespertide-lsp/src/file_features.rs    | 16 +-------
 crates/vespertide-lsp/src/hover/mod.rs        | 16 +-------
 crates/vespertide-lsp/src/lib.rs              |  1 +
 .../vespertide-lsp/src/references/resolver.rs | 16 +-------
 crates/vespertide-lsp/src/rename.rs           | 16 +-------
 crates/vespertide-lsp/src/tree_util.rs        | 37 +++++++++++++++++++
 .../src/validate/check_self_contradiction.rs  | 16 +++-----
 .../src/validate/check_strengthening.rs       | 24 ++++++------
 10 files changed, 62 insertions(+), 114 deletions(-)
 create mode 100644 crates/vespertide-lsp/src/tree_util.rs

diff --git a/crates/vespertide-lsp/src/check_expr_locate.rs b/crates/vespertide-lsp/src/check_expr_locate.rs
index 29bf384c..f6e57a55 100644
--- a/crates/vespertide-lsp/src/check_expr_locate.rs
+++ b/crates/vespertide-lsp/src/check_expr_locate.rs
@@ -15,6 +15,7 @@ use std::ops::Range;
 use tree_sitter::{Node, Tree};
 
 use crate::text_util::strip_quotes;
+use crate::tree_util::node_at_byte;
 
 /// Cursor-based result of [`find_check_expr_at`]: the CHECK `expr` string
 /// the cursor sits in, the inner byte range of its predicate text, and the
@@ -183,23 +184,6 @@ fn enclosing_string(node: Node<'_>) -> Option> {
     None
 }
 
-/// Descend from the root to the deepest node whose byte range contains
-/// `byte_offset`.
-fn node_at_byte(tree: &Tree, byte_offset: usize) -> Option> {
-    let root = tree.root_node();
-    let mut current = root;
-    'outer: loop {
-        let mut cursor = current.walk();
-        for child in current.children(&mut cursor) {
-            if child.byte_range().contains(&byte_offset) {
-                current = child;
-                continue 'outer;
-            }
-        }
-        return Some(current);
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/vespertide-lsp/src/definition/mod.rs b/crates/vespertide-lsp/src/definition/mod.rs
index 56001cc3..a967e17f 100644
--- a/crates/vespertide-lsp/src/definition/mod.rs
+++ b/crates/vespertide-lsp/src/definition/mod.rs
@@ -8,6 +8,7 @@ use tower_lsp_server::ls_types::Uri;
 
 use crate::parser::DocumentFormat;
 use crate::store::DocumentStore;
+use crate::tree_util::node_at_byte;
 use crate::workspace_index::WorkspaceIndex;
 use crate::workspace_tables::WorkspaceTables;
 
@@ -49,21 +50,6 @@ pub fn compute_with_workspace_tables(
     foreign_key::try_definition(node, source, index, docs, disk_tables)
 }
 
-fn node_at_byte(tree: &tree_sitter::Tree, byte_offset: usize) -> Option> {
-    let root = tree.root_node();
-    let mut current = root;
-    'outer: loop {
-        let mut cursor = current.walk();
-        for child in current.children(&mut cursor) {
-            if child.byte_range().contains(&byte_offset) {
-                current = child;
-                continue 'outer;
-            }
-        }
-        return Some(current);
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/vespertide-lsp/src/file_features.rs b/crates/vespertide-lsp/src/file_features.rs
index 211101cd..d4d4d3ef 100644
--- a/crates/vespertide-lsp/src/file_features.rs
+++ b/crates/vespertide-lsp/src/file_features.rs
@@ -5,6 +5,7 @@
 //! none has enough surface area to deserve its own directory.
 
 use crate::text_util::strip_quotes;
+use crate::tree_util::node_at_byte;
 use std::ops::Range;
 
 // =====================================================================
@@ -367,21 +368,6 @@ fn unwrap_yaml(node: tree_sitter::Node<'_>) -> tree_sitter::Node<'_> {
     current
 }
 
-fn node_at_byte(tree: &tree_sitter::Tree, byte_offset: usize) -> Option> {
-    let root = tree.root_node();
-    let mut current = root;
-    'outer: loop {
-        let mut cursor = current.walk();
-        for child in current.children(&mut cursor) {
-            if child.byte_range().contains(&byte_offset) {
-                current = child;
-                continue 'outer;
-            }
-        }
-        return Some(current);
-    }
-}
-
 fn trim_one_byte(range: &Range) -> Range {
     if range.end.saturating_sub(range.start) >= 2 {
         (range.start + 1)..(range.end - 1)
diff --git a/crates/vespertide-lsp/src/hover/mod.rs b/crates/vespertide-lsp/src/hover/mod.rs
index ba3ad465..cf4caecc 100644
--- a/crates/vespertide-lsp/src/hover/mod.rs
+++ b/crates/vespertide-lsp/src/hover/mod.rs
@@ -8,6 +8,7 @@ use std::ops::Range;
 
 use crate::parser::DocumentFormat;
 use crate::store::DocumentStore;
+use crate::tree_util::node_at_byte;
 use crate::workspace_index::WorkspaceIndex;
 use crate::workspace_tables::WorkspaceTables;
 
@@ -65,21 +66,6 @@ pub fn compute_with_workspace_tables(
     column::try_hover(node, text)
 }
 
-fn node_at_byte(tree: &tree_sitter::Tree, byte_offset: usize) -> Option> {
-    let root = tree.root_node();
-    let mut current = root;
-    'outer: loop {
-        let mut cursor = current.walk();
-        for child in current.children(&mut cursor) {
-            if child.byte_range().contains(&byte_offset) {
-                current = child;
-                continue 'outer;
-            }
-        }
-        return Some(current);
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/vespertide-lsp/src/lib.rs b/crates/vespertide-lsp/src/lib.rs
index 54ad87ca..9545ca90 100644
--- a/crates/vespertide-lsp/src/lib.rs
+++ b/crates/vespertide-lsp/src/lib.rs
@@ -37,6 +37,7 @@ mod symbols;
 #[cfg(test)]
 pub(crate) mod test_support;
 mod text_util;
+mod tree_util;
 pub mod watched_files;
 mod workspace_index;
 pub mod workspace_tables;
diff --git a/crates/vespertide-lsp/src/references/resolver.rs b/crates/vespertide-lsp/src/references/resolver.rs
index 9134a684..61a35ff0 100644
--- a/crates/vespertide-lsp/src/references/resolver.rs
+++ b/crates/vespertide-lsp/src/references/resolver.rs
@@ -1,6 +1,7 @@
 //! Resolve "what symbol is the cursor on?" for the references provider.
 
 use crate::text_util::strip_quotes;
+use crate::tree_util::node_at_byte;
 use tower_lsp_server::ls_types::Uri;
 
 use super::ReferenceSymbol;
@@ -226,21 +227,6 @@ fn skip_yaml_wrappers(node: tree_sitter::Node<'_>) -> Option Option> {
-    let root = tree.root_node();
-    let mut current = root;
-    'outer: loop {
-        let mut cursor = current.walk();
-        for child in current.children(&mut cursor) {
-            if child.byte_range().contains(&byte_offset) {
-                current = child;
-                continue 'outer;
-            }
-        }
-        return Some(current);
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/vespertide-lsp/src/rename.rs b/crates/vespertide-lsp/src/rename.rs
index 2375d6fd..24a50514 100644
--- a/crates/vespertide-lsp/src/rename.rs
+++ b/crates/vespertide-lsp/src/rename.rs
@@ -16,6 +16,7 @@ use tower_lsp_server::ls_types::Uri;
 use crate::parser::DocumentFormat;
 use crate::references::{self, ReferenceSymbol};
 use crate::store::DocumentStore;
+use crate::tree_util::node_at_byte;
 use crate::workspace_index::WorkspaceIndex;
 use crate::workspace_tables::WorkspaceTables;
 
@@ -138,21 +139,6 @@ fn trim_one_byte(range: &Range) -> Range {
     }
 }
 
-fn node_at_byte(tree: &tree_sitter::Tree, byte_offset: usize) -> Option> {
-    let root = tree.root_node();
-    let mut current = root;
-    'outer: loop {
-        let mut cursor = current.walk();
-        for child in current.children(&mut cursor) {
-            if child.byte_range().contains(&byte_offset) {
-                current = child;
-                continue 'outer;
-            }
-        }
-        return Some(current);
-    }
-}
-
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct DomainTextEdit {
     pub byte_range: Range,
diff --git a/crates/vespertide-lsp/src/tree_util.rs b/crates/vespertide-lsp/src/tree_util.rs
new file mode 100644
index 00000000..2e868a7c
--- /dev/null
+++ b/crates/vespertide-lsp/src/tree_util.rs
@@ -0,0 +1,37 @@
+//! Shared tree-sitter traversal helpers used across every LSP feature.
+//!
+//! Previously every feature module (hover, definition, references, rename,
+//! file_features, check_expr_locate, …) carried its own byte-identical
+//! private copy of [`node_at_byte`]. Hoisting the BFS descent into one
+//! `pub(crate)` helper kills ~80 lines of pure duplication and prevents
+//! the next LSP feature from copy-pasting an eighth variant.
+//!
+//! NOTE: `completion::context` keeps its own `node_at_byte` because that
+//! call site uses [`tree_sitter::Node::descendant_for_byte_range`] for
+//! end-of-token cursor semantics, which differs from this BFS descent at
+//! byte boundaries. Do not migrate it here.
+
+/// Descend from the root to the deepest node whose byte range contains
+/// `byte_offset`.
+///
+/// Returns `Some(_)` for every well-formed tree — the loop always
+/// terminates at a leaf when no child contains the offset — so the
+/// `Option` wrapper is preserved purely so existing call sites keep
+/// their `?` short-circuits unchanged.
+pub(crate) fn node_at_byte(
+    tree: &tree_sitter::Tree,
+    byte_offset: usize,
+) -> Option> {
+    let root = tree.root_node();
+    let mut current = root;
+    'outer: loop {
+        let mut cursor = current.walk();
+        for child in current.children(&mut cursor) {
+            if child.byte_range().contains(&byte_offset) {
+                current = child;
+                continue 'outer;
+            }
+        }
+        return Some(current);
+    }
+}
diff --git a/crates/vespertide-planner/src/validate/check_self_contradiction.rs b/crates/vespertide-planner/src/validate/check_self_contradiction.rs
index 5fec338a..fb815c01 100644
--- a/crates/vespertide-planner/src/validate/check_self_contradiction.rs
+++ b/crates/vespertide-planner/src/validate/check_self_contradiction.rs
@@ -301,12 +301,16 @@ fn is_null_vs_other(column: &str, a: &CheckExpr, b: &CheckExpr) -> Option<(bool,
     ))
 }
 
+#[expect(
+    clippy::cast_precision_loss,
+    reason = "F-novel-1 self-contradiction comparison: rounding integers beyond 2^53 acceptable; conservative comparator silently skips ambiguous cases"
+)]
 fn literal_compare(a: &Literal, b: &Literal) -> Option {
     match (a, b) {
         (Literal::Integer(x), Literal::Integer(y)) => Some(x.cmp(y)),
         (Literal::Float(x), Literal::Float(y)) => x.partial_cmp(y),
-        (Literal::Integer(x), Literal::Float(y)) => i64_to_f64(*x).partial_cmp(y),
-        (Literal::Float(x), Literal::Integer(y)) => x.partial_cmp(&i64_to_f64(*y)),
+        (Literal::Integer(x), Literal::Float(y)) => (*x as f64).partial_cmp(y),
+        (Literal::Float(x), Literal::Integer(y)) => x.partial_cmp(&(*y as f64)),
         (Literal::String(x), Literal::String(y)) => Some(x.cmp(y)),
         (Literal::Bool(x), Literal::Bool(y)) => Some(x.cmp(y)),
         // Mixed / Null: can't conclude.
@@ -314,14 +318,6 @@ fn literal_compare(a: &Literal, b: &Literal) -> Option {
     }
 }
 
-#[expect(
-    clippy::cast_precision_loss,
-    reason = "F-novel-1 self-contradiction comparison: rounding integers beyond 2^53 acceptable; conservative comparator silently skips ambiguous cases"
-)]
-fn i64_to_f64(v: i64) -> f64 {
-    v as f64
-}
-
 fn format_literal(lit: &Literal) -> String {
     match lit {
         Literal::Integer(i) => i.to_string(),
diff --git a/crates/vespertide-planner/src/validate/check_strengthening.rs b/crates/vespertide-planner/src/validate/check_strengthening.rs
index 7fd2821c..310cfb53 100644
--- a/crates/vespertide-planner/src/validate/check_strengthening.rs
+++ b/crates/vespertide-planner/src/validate/check_strengthening.rs
@@ -394,12 +394,16 @@ fn between_is_narrower(
     lo_ok && hi_ok && any_strict
 }
 
+#[expect(
+    clippy::cast_precision_loss,
+    reason = "F29 strictness comparison: rounding integers beyond 2^53 acceptable; F29 silent-passes on ambiguity"
+)]
 fn literal_equals(a: &Literal, b: &Literal) -> bool {
     match (a, b) {
         (Literal::Integer(x), Literal::Integer(y)) => x == y,
         (Literal::Float(x), Literal::Float(y)) => (x - y).abs() < f64::EPSILON,
-        (Literal::Integer(x), Literal::Float(y)) => (i64_to_f64(*x) - y).abs() < f64::EPSILON,
-        (Literal::Float(x), Literal::Integer(y)) => (x - i64_to_f64(*y)).abs() < f64::EPSILON,
+        (Literal::Integer(x), Literal::Float(y)) => (*x as f64 - y).abs() < f64::EPSILON,
+        (Literal::Float(x), Literal::Integer(y)) => (x - *y as f64).abs() < f64::EPSILON,
         (Literal::String(x), Literal::String(y)) => x == y,
         (Literal::Bool(x), Literal::Bool(y)) => x == y,
         (Literal::Null, Literal::Null) => true,
@@ -407,25 +411,21 @@ fn literal_equals(a: &Literal, b: &Literal) -> bool {
     }
 }
 
+#[expect(
+    clippy::cast_precision_loss,
+    reason = "F29 strictness comparison: rounding integers beyond 2^53 acceptable; F29 silent-passes on ambiguity"
+)]
 fn literal_compare(a: &Literal, b: &Literal) -> Option {
     match (a, b) {
         (Literal::Integer(x), Literal::Integer(y)) => Some(x.cmp(y)),
         (Literal::Float(x), Literal::Float(y)) => x.partial_cmp(y),
-        (Literal::Integer(x), Literal::Float(y)) => i64_to_f64(*x).partial_cmp(y),
-        (Literal::Float(x), Literal::Integer(y)) => x.partial_cmp(&i64_to_f64(*y)),
+        (Literal::Integer(x), Literal::Float(y)) => (*x as f64).partial_cmp(y),
+        (Literal::Float(x), Literal::Integer(y)) => x.partial_cmp(&(*y as f64)),
         (Literal::String(x), Literal::String(y)) => Some(x.cmp(y)),
         _ => None,
     }
 }
 
-#[expect(
-    clippy::cast_precision_loss,
-    reason = "F29 strictness comparison: rounding integers beyond 2^53 acceptable; F29 silent-passes on ambiguity"
-)]
-fn i64_to_f64(v: i64) -> f64 {
-    v as f64
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;

From 4f596fdd611337f252628379e0848afe99ea2a6b Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 04:29:18 +0900
Subject: [PATCH 025/109] retry-now#0024: batch -- hoist SQLite
 copy-into-temp-table helper, share Python naming helpers, lift CHECK
 quoted-col format! (3 kept)

Item 1: hoist build_copy_into_temp_table into sql/helpers.rs and dedup 6
open-coded SQLite INSERT...SELECT temp-table copy sites (delete_column,
modify_column_type, modify_column_default, modify_column_nullable,
add_constraint, replace_constraint) plus collapse the private wrapper
in remove_constraint/sqlite.rs. The new helper takes `&[ColumnDef]` so
delete_column can pass its filtered new_columns slice. SQL output is
byte-identical (every SQL snapshot unchanged). Also drops the now-
unfulfilled `#[expect(clippy::too_many_lines)]` on
build_modify_column_default since the dedup shrunk it below threshold.

Item 2: extract identical to_pascal_case and to_screaming_snake_case
from sqlalchemy/render.rs and sqlmodel/enums.rs into a shared
crate::python_naming module; both sites now `pub(super) use
crate::python_naming::{...}` so every existing super::render::* and
super::enums::* call site keeps working. SeaORM's separate
to_pascal_case (different keyword-guard semantics) is intentionally
untouched. All 232 cross-ORM exporter snapshots unchanged.

Item 3 (perf): hoist format!(""{column}"") out of the CHECK
constraint filter closure in delete_column/sqlite_rebuild.rs so the
per-constraint heap allocation of the quoted column literal collapses
to a single allocation per DELETE COLUMN action. Bench (--workspace
--exclude vespertide-query, 2 runs each before/after, median):
statistically neutral within observed run-to-run noise band (mean
BEFORE spread 9.2 percent, AFTER spread 15.4 percent, mean delta
+5.1 percent; item lives in excluded crate so cannot move these numbers
by construction).
---
 crates/vespertide-exporter/src/lib.rs         |  1 +
 .../vespertide-exporter/src/python_naming.rs  | 48 +++++++++++++++++++
 .../src/sqlalchemy/render.rs                  | 38 +++------------
 .../vespertide-exporter/src/sqlmodel/enums.rs | 40 +++-------------
 .../src/sql/add_constraint/mod.rs             | 24 ++--------
 .../src/sql/delete_column/sqlite_rebuild.rs   | 25 ++++------
 crates/vespertide-query/src/sql/helpers.rs    | 39 ++++++++++++++-
 .../src/sql/modify_column_default.rs          | 33 ++++---------
 .../src/sql/modify_column_nullable.rs         | 29 ++++-------
 .../sql/modify_column_type/sqlite_rebuild.rs  | 25 ++--------
 .../src/sql/remove_constraint/sqlite.rs       | 31 ++----------
 .../src/sql/replace_constraint.rs             | 24 ++--------
 12 files changed, 143 insertions(+), 214 deletions(-)
 create mode 100644 crates/vespertide-exporter/src/python_naming.rs

diff --git a/crates/vespertide-exporter/src/lib.rs b/crates/vespertide-exporter/src/lib.rs
index 0d4732c5..e09a7f72 100644
--- a/crates/vespertide-exporter/src/lib.rs
+++ b/crates/vespertide-exporter/src/lib.rs
@@ -4,6 +4,7 @@
 pub mod jpa;
 pub mod orm;
 mod parallel_config;
+mod python_naming;
 pub mod seaorm;
 pub mod sqlalchemy;
 pub mod sqlmodel;
diff --git a/crates/vespertide-exporter/src/python_naming.rs b/crates/vespertide-exporter/src/python_naming.rs
new file mode 100644
index 00000000..1ae00e89
--- /dev/null
+++ b/crates/vespertide-exporter/src/python_naming.rs
@@ -0,0 +1,48 @@
+//! Shared naming helpers for the Python-targeted ORM exporters (SQLAlchemy,
+//! SQLModel). Both backends share an identical, snake-case-aware
+//! `to_pascal_case` and an identical `to_screaming_snake_case` that doubles
+//! as a Python `enum.Enum` member-name sanitiser.
+//!
+//! `seaorm` deliberately keeps its own `to_pascal_case` in
+//! `seaorm/imports.rs` — that variant carries reserved-keyword guards and a
+//! different allocation pattern and is NOT in scope for this consolidation.
+
+/// Convert snake_case (or single-word) input to PascalCase. Splits on
+/// underscores, upper-cases the first character of each segment, and
+/// preserves the remainder verbatim.
+pub(crate) fn to_pascal_case(s: &str) -> String {
+    s.split('_')
+        .map(|word| {
+            let mut chars = word.chars();
+            match chars.next() {
+                None => String::new(),
+                Some(first) => first.to_uppercase().chain(chars).collect(),
+            }
+        })
+        .collect()
+}
+
+/// Convert any input to SCREAMING_SNAKE_CASE: inserts `_` before interior
+/// uppercase characters, upper-cases everything, then replaces any
+/// non-alphanumeric character with `_` so the result is safe as a Python
+/// `enum.Enum` member name.
+pub(crate) fn to_screaming_snake_case(s: &str) -> String {
+    let mut result = String::new();
+    for (i, ch) in s.chars().enumerate() {
+        if ch.is_uppercase() && i > 0 {
+            result.push('_');
+        }
+        result.push(ch.to_ascii_uppercase());
+    }
+    // Replace any non-alphanumeric with underscore
+    result
+        .chars()
+        .map(|c| {
+            if c.is_alphanumeric() || c == '_' {
+                c
+            } else {
+                '_'
+            }
+        })
+        .collect()
+}
diff --git a/crates/vespertide-exporter/src/sqlalchemy/render.rs b/crates/vespertide-exporter/src/sqlalchemy/render.rs
index 5f28b435..b8deb0ff 100644
--- a/crates/vespertide-exporter/src/sqlalchemy/render.rs
+++ b/crates/vespertide-exporter/src/sqlalchemy/render.rs
@@ -397,35 +397,9 @@ fn render_column(
     ));
 }
 
-pub(super) fn to_pascal_case(s: &str) -> String {
-    s.split('_')
-        .map(|word| {
-            let mut chars = word.chars();
-            match chars.next() {
-                None => String::new(),
-                Some(first) => first.to_uppercase().chain(chars).collect(),
-            }
-        })
-        .collect()
-}
-
-pub(super) fn to_screaming_snake_case(s: &str) -> String {
-    let mut result = String::new();
-    for (i, ch) in s.chars().enumerate() {
-        if ch.is_uppercase() && i > 0 {
-            result.push('_');
-        }
-        result.push(ch.to_ascii_uppercase());
-    }
-    // Replace any non-alphanumeric with underscore
-    result
-        .chars()
-        .map(|c| {
-            if c.is_alphanumeric() || c == '_' {
-                c
-            } else {
-                '_'
-            }
-        })
-        .collect()
-}
+// Naming helpers shared with the `SQLModel` exporter — both Python ORMs
+// produce identical PascalCase class names and identical
+// SCREAMING_SNAKE_CASE enum member names, so the implementation lives in
+// `crate::python_naming` and we re-export it here to keep every existing
+// `super::render::to_*` path working without churn.
+pub(super) use crate::python_naming::{to_pascal_case, to_screaming_snake_case};
diff --git a/crates/vespertide-exporter/src/sqlmodel/enums.rs b/crates/vespertide-exporter/src/sqlmodel/enums.rs
index 4d95a925..8c032ba6 100644
--- a/crates/vespertide-exporter/src/sqlmodel/enums.rs
+++ b/crates/vespertide-exporter/src/sqlmodel/enums.rs
@@ -1,5 +1,12 @@
 use vespertide_core::schema::column::EnumValues;
 
+// Naming helpers shared with the `SQLAlchemy` exporter — both Python ORMs
+// produce identical PascalCase class names and identical
+// SCREAMING_SNAKE_CASE enum member names, so the implementation lives in
+// `crate::python_naming` and we re-export it here to keep every existing
+// `super::enums::to_*` path working without churn.
+pub(super) use crate::python_naming::{to_pascal_case, to_screaming_snake_case};
+
 pub(super) fn render_enum(lines: &mut Vec, name: &str, values: &EnumValues) {
     let class_name = to_pascal_case(name);
 
@@ -19,36 +26,3 @@ pub(super) fn render_enum(lines: &mut Vec, name: &str, values: &EnumValu
         }
     }
 }
-
-pub(super) fn to_pascal_case(s: &str) -> String {
-    s.split('_')
-        .map(|word| {
-            let mut chars = word.chars();
-            match chars.next() {
-                None => String::new(),
-                Some(first) => first.to_uppercase().chain(chars).collect(),
-            }
-        })
-        .collect()
-}
-
-pub(super) fn to_screaming_snake_case(s: &str) -> String {
-    let mut result = String::new();
-    for (i, ch) in s.chars().enumerate() {
-        if ch.is_uppercase() && i > 0 {
-            result.push('_');
-        }
-        result.push(ch.to_ascii_uppercase());
-    }
-    // Replace any non-alphanumeric with underscore
-    result
-        .chars()
-        .map(|c| {
-            if c.is_alphanumeric() || c == '_' {
-                c
-            } else {
-                '_'
-            }
-        })
-        .collect()
-}
diff --git a/crates/vespertide-query/src/sql/add_constraint/mod.rs b/crates/vespertide-query/src/sql/add_constraint/mod.rs
index 794d3916..c5b13766 100644
--- a/crates/vespertide-query/src/sql/add_constraint/mod.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/mod.rs
@@ -928,10 +928,12 @@ mod tests {
 
 mod unique;
 
-use sea_query::{Alias, Query, Table};
+use sea_query::{Alias, Table};
 use vespertide_core::{TableConstraint, TableDef};
 
-use super::helpers::{build_sqlite_temp_table_create, recreate_indexes_after_rebuild};
+use super::helpers::{
+    build_copy_into_temp_table, build_sqlite_temp_table_create, recreate_indexes_after_rebuild,
+};
 use super::rename_table::build_rename_table;
 use super::types::{BuiltQuery, DatabaseBackend};
 use crate::error::QueryError;
@@ -1087,23 +1089,7 @@ pub(super) fn rebuild_sqlite_table_with_added_constraint(
         &table_def.columns,
         &new_constraints,
     );
-    let column_aliases: Vec = table_def
-        .columns
-        .iter()
-        .map(|c| Alias::new(&c.name))
-        .collect();
-    let mut select_query = Query::select();
-    for col_alias in &column_aliases {
-        select_query.column(col_alias.clone());
-    }
-    select_query.from(Alias::new(table));
-    let insert_stmt = Query::insert()
-        .into_table(Alias::new(&temp_table))
-        .columns(column_aliases)
-        .select_from(select_query)
-        .unwrap()
-        .to_owned();
-    let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
+    let insert_query = build_copy_into_temp_table(table, &temp_table, &table_def.columns);
     let drop_table = Table::drop().table(Alias::new(table)).to_owned();
     let drop_query = BuiltQuery::DropTable(Box::new(drop_table));
     let rename_query = build_rename_table(&temp_table, table);
diff --git a/crates/vespertide-query/src/sql/delete_column/sqlite_rebuild.rs b/crates/vespertide-query/src/sql/delete_column/sqlite_rebuild.rs
index 7aa5f16e..94d4f477 100644
--- a/crates/vespertide-query/src/sql/delete_column/sqlite_rebuild.rs
+++ b/crates/vespertide-query/src/sql/delete_column/sqlite_rebuild.rs
@@ -1,9 +1,10 @@
-use sea_query::{Alias, Query, Table};
+use sea_query::{Alias, Table};
 
 use vespertide_core::{ColumnType, TableConstraint, TableDef};
 
 use crate::sql::helpers::{
-    build_drop_enum_type_sql, build_sqlite_temp_table_create, recreate_indexes_after_rebuild,
+    build_copy_into_temp_table, build_drop_enum_type_sql, build_sqlite_temp_table_create,
+    recreate_indexes_after_rebuild,
 };
 use crate::sql::rename_table::build_rename_table;
 use crate::sql::types::{BuiltQuery, DatabaseBackend};
@@ -37,13 +38,16 @@ pub(super) fn build_delete_column_sqlite_temp_table(
         .collect();
 
     // Build new constraints list without constraints referencing the deleted column.
+    // perf: pre-quote the column identifier once instead of re-allocating the
+    // same `"col"` literal inside the closure on every CHECK constraint visited.
+    let quoted_col = format!("\"{column}\"");
     let new_constraints: Vec<_> = table_def
         .constraints
         .iter()
         .filter(|c| {
             // For CHECK constraints, check if expression references the column.
             if let TableConstraint::Check { expr, .. } = c {
-                return !expr.contains(&format!("\"{column}\"")) && !expr.contains(column);
+                return !expr.contains("ed_col) && !expr.contains(column);
             }
             !c.columns().iter().any(|col| col == column)
         })
@@ -61,20 +65,7 @@ pub(super) fn build_delete_column_sqlite_temp_table(
     stmts.push(create_query);
 
     // 2. Copy data (excluding the deleted column).
-    let column_aliases: Vec = new_columns.iter().map(|c| Alias::new(&c.name)).collect();
-    let mut select_query = Query::select();
-    for col_alias in &column_aliases {
-        select_query.column(col_alias.clone());
-    }
-    select_query.from(Alias::new(table));
-
-    let insert_stmt = Query::insert()
-        .into_table(Alias::new(&temp_table))
-        .columns(column_aliases.clone())
-        .select_from(select_query)
-        .unwrap()
-        .to_owned();
-    stmts.push(BuiltQuery::Insert(Box::new(insert_stmt)));
+    stmts.push(build_copy_into_temp_table(table, &temp_table, &new_columns));
 
     // 3. Drop original table.
     let drop_table = Table::drop().table(Alias::new(table)).to_owned();
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index 12fc2ea2..337a6bce 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -1,6 +1,6 @@
 use sea_query::{
     Alias, ColumnDef as SeaColumnDef, ForeignKeyAction, MysqlQueryBuilder, PostgresQueryBuilder,
-    QueryStatementWriter, SchemaStatementBuilder, SimpleExpr, SqliteQueryBuilder,
+    Query, QueryStatementWriter, SchemaStatementBuilder, SimpleExpr, SqliteQueryBuilder,
 };
 
 use vespertide_core::{
@@ -596,6 +596,43 @@ pub fn build_sqlite_temp_table_create(
     build_create_with_checks(backend, &create_stmt, &check_clauses)
 }
 
+/// Canonical `INSERT INTO temp_table (cols) SELECT cols FROM table` builder used
+/// by every `SQLite` temp-table rebuild path.
+///
+/// Six call sites previously open-coded this identical sequence (delete column,
+/// modify column type, modify column default, modify column nullable, add
+/// constraint, replace constraint, remove constraint). Centralising it here
+/// removes the drift risk that comes with keeping six copies in lock-step.
+///
+/// `columns` is the column slice to copy — pass `&table_def.columns` for the
+/// "copy everything" rebuilds and a filtered slice (without the dropped column)
+/// for the `DELETE COLUMN` path.
+pub(super) fn build_copy_into_temp_table(
+    table: &str,
+    temp_table: &str,
+    columns: &[ColumnDef],
+) -> BuiltQuery {
+    let column_aliases: Vec = columns
+        .iter()
+        .map(|column| Alias::new(&column.name))
+        .collect();
+
+    let mut select_query = Query::select();
+    for column_alias in &column_aliases {
+        select_query.column(column_alias.clone());
+    }
+    select_query.from(Alias::new(table));
+
+    let insert_stmt = Query::insert()
+        .into_table(Alias::new(temp_table))
+        .columns(column_aliases)
+        .select_from(select_query)
+        .expect("SQLite temp table copy SELECT should be valid")
+        .to_owned();
+
+    BuiltQuery::Insert(Box::new(insert_stmt))
+}
+
 /// Recreate all indexes (both regular and UNIQUE) after a `SQLite` temp table rebuild.
 /// After DROP TABLE + RENAME, all original indexes are gone, so plain CREATE INDEX is correct.
 ///
diff --git a/crates/vespertide-query/src/sql/modify_column_default.rs b/crates/vespertide-query/src/sql/modify_column_default.rs
index 526b6745..0ebcc8f6 100644
--- a/crates/vespertide-query/src/sql/modify_column_default.rs
+++ b/crates/vespertide-query/src/sql/modify_column_default.rs
@@ -1,10 +1,10 @@
-use sea_query::{Alias, Query, Table};
+use sea_query::{Alias, Table};
 
 use vespertide_core::{ColumnDef, TableDef};
 
 use super::helpers::{
-    build_sea_column_def_with_table, build_sqlite_temp_table_create, normalize_enum_default,
-    quote_ident, recreate_indexes_after_rebuild,
+    build_copy_into_temp_table, build_sea_column_def_with_table, build_sqlite_temp_table_create,
+    normalize_enum_default, quote_ident, recreate_indexes_after_rebuild,
 };
 use super::rename_table::build_rename_table;
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
@@ -19,10 +19,6 @@ use crate::error::QueryError;
 /// (already-quoted literals for strings, bare expressions like `NOW()` for
 /// functions). When `backfill` is `None` the action behaves exactly as in
 /// v0.2.0 — only the schema is touched, existing rows keep their values.
-#[expect(
-    clippy::too_many_lines,
-    reason = "three-backend dispatch (PG / MySQL / SQLite) plus optional backfill UPDATE; splitting per-backend helpers scatters the read flow"
-)]
 pub fn build_modify_column_default(
     backend: DatabaseBackend,
     table: &str,
@@ -123,24 +119,11 @@ pub fn build_modify_column_default(
             queries.push(create_query);
 
             // 2. Copy data (all columns)
-            let column_aliases: Vec = table_def
-                .columns
-                .iter()
-                .map(|c| Alias::new(&c.name))
-                .collect();
-            let mut select_query = Query::select();
-            for col_alias in &column_aliases {
-                select_query.column(col_alias.clone());
-            }
-            select_query.from(Alias::new(table));
-
-            let insert_stmt = Query::insert()
-                .into_table(Alias::new(&temp_table))
-                .columns(column_aliases.clone())
-                .select_from(select_query)
-                .unwrap()
-                .to_owned();
-            queries.push(BuiltQuery::Insert(Box::new(insert_stmt)));
+            queries.push(build_copy_into_temp_table(
+                table,
+                &temp_table,
+                &table_def.columns,
+            ));
 
             // 3. Drop original table
             let drop_table = Table::drop().table(Alias::new(table)).to_owned();
diff --git a/crates/vespertide-query/src/sql/modify_column_nullable.rs b/crates/vespertide-query/src/sql/modify_column_nullable.rs
index be2af2af..c8598f66 100644
--- a/crates/vespertide-query/src/sql/modify_column_nullable.rs
+++ b/crates/vespertide-query/src/sql/modify_column_nullable.rs
@@ -1,10 +1,10 @@
-use sea_query::{Alias, Query, Table};
+use sea_query::{Alias, Table};
 
 use vespertide_core::{ColumnDef, TableDef};
 
 use super::helpers::{
-    build_sea_column_def_with_table, build_sqlite_temp_table_create, convert_default_for_backend,
-    normalize_fill_with, quote_ident, recreate_indexes_after_rebuild,
+    build_copy_into_temp_table, build_sea_column_def_with_table, build_sqlite_temp_table_create,
+    convert_default_for_backend, normalize_fill_with, quote_ident, recreate_indexes_after_rebuild,
 };
 use super::rename_table::build_rename_table;
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
@@ -105,24 +105,11 @@ pub fn build_modify_column_nullable(
             queries.push(create_query);
 
             // 2. Copy data (all columns)
-            let column_aliases: Vec = table_def
-                .columns
-                .iter()
-                .map(|c| Alias::new(&c.name))
-                .collect();
-            let mut select_query = Query::select();
-            for col_alias in &column_aliases {
-                select_query.column(col_alias.clone());
-            }
-            select_query.from(Alias::new(table));
-
-            let insert_stmt = Query::insert()
-                .into_table(Alias::new(&temp_table))
-                .columns(column_aliases.clone())
-                .select_from(select_query)
-                .unwrap()
-                .to_owned();
-            queries.push(BuiltQuery::Insert(Box::new(insert_stmt)));
+            queries.push(build_copy_into_temp_table(
+                table,
+                &temp_table,
+                &table_def.columns,
+            ));
 
             // 3. Drop original table
             let drop_table = Table::drop().table(Alias::new(table)).to_owned();
diff --git a/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs b/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs
index dfa928bd..0d7709c9 100644
--- a/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs
+++ b/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs
@@ -1,11 +1,13 @@
 use std::collections::BTreeMap;
 
-use sea_query::{Alias, Query, Table};
+use sea_query::{Alias, Table};
 
 use vespertide_core::{ColumnType, TableConstraint, TableDef};
 
 use crate::error::QueryError;
-use crate::sql::helpers::{build_sqlite_temp_table_create, recreate_indexes_after_rebuild};
+use crate::sql::helpers::{
+    build_copy_into_temp_table, build_sqlite_temp_table_create, recreate_indexes_after_rebuild,
+};
 use crate::sql::rename_table::build_rename_table;
 use crate::sql::types::{BuiltQuery, DatabaseBackend};
 
@@ -46,24 +48,7 @@ pub(super) fn build_modify_column_type_sqlite_temp_table(
     );
 
     // 2. Copy data (all columns) - Use INSERT INTO ... SELECT
-    let column_aliases: Vec = new_columns.iter().map(|c| Alias::new(&c.name)).collect();
-
-    // Build SELECT query
-    let mut select_query = Query::select();
-    for col_alias in &column_aliases {
-        select_query.column(col_alias.clone());
-    }
-    select_query.from(Alias::new(table));
-
-    // Build INSERT query
-    let insert_stmt = Query::insert()
-        .into_table(Alias::new(&temp_table))
-        .columns(column_aliases.clone())
-        .select_from(select_query)
-        .unwrap()
-        .to_owned();
-
-    let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
+    let insert_query = build_copy_into_temp_table(table, &temp_table, &new_columns);
 
     // 3. Drop original table
     let drop_table = Table::drop().table(Alias::new(table)).to_owned();
diff --git a/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs b/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs
index c45e59d7..d6e41080 100644
--- a/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs
+++ b/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs
@@ -1,9 +1,11 @@
-use sea_query::{Alias, Query, Table};
+use sea_query::{Alias, Table};
 
 use vespertide_core::{TableConstraint, TableDef};
 
 use crate::error::QueryError;
-use crate::sql::helpers::{build_sqlite_temp_table_create, recreate_indexes_after_rebuild};
+use crate::sql::helpers::{
+    build_copy_into_temp_table, build_sqlite_temp_table_create, recreate_indexes_after_rebuild,
+};
 use crate::sql::rename_table::build_rename_table;
 use crate::sql::types::{BuiltQuery, DatabaseBackend};
 
@@ -130,7 +132,7 @@ fn rebuild_table_without_constraint(
         &table_def.columns,
         new_constraints,
     );
-    let insert_query = build_copy_into_temp_table(table, &temp_table, table_def);
+    let insert_query = build_copy_into_temp_table(table, &temp_table, &table_def.columns);
     let drop_query =
         BuiltQuery::DropTable(Box::new(Table::drop().table(Alias::new(table)).to_owned()));
     let rename_query = build_rename_table(&temp_table, table);
@@ -141,26 +143,3 @@ fn rebuild_table_without_constraint(
     queries.extend(index_queries);
     queries
 }
-
-fn build_copy_into_temp_table(table: &str, temp_table: &str, table_def: &TableDef) -> BuiltQuery {
-    let column_aliases: Vec = table_def
-        .columns
-        .iter()
-        .map(|column| Alias::new(&column.name))
-        .collect();
-
-    let mut select_query = Query::select();
-    for column_alias in &column_aliases {
-        select_query.column(column_alias.clone());
-    }
-    select_query.from(Alias::new(table));
-
-    let insert_stmt = Query::insert()
-        .into_table(Alias::new(temp_table))
-        .columns(column_aliases)
-        .select_from(select_query)
-        .expect("SQLite temp table copy SELECT should be valid")
-        .to_owned();
-
-    BuiltQuery::Insert(Box::new(insert_stmt))
-}
diff --git a/crates/vespertide-query/src/sql/replace_constraint.rs b/crates/vespertide-query/src/sql/replace_constraint.rs
index b3f8dd6d..121572e2 100644
--- a/crates/vespertide-query/src/sql/replace_constraint.rs
+++ b/crates/vespertide-query/src/sql/replace_constraint.rs
@@ -1,9 +1,10 @@
-use sea_query::{Alias, ForeignKey, Query, Table};
+use sea_query::{Alias, ForeignKey, Table};
 
 use vespertide_core::{TableConstraint, TableDef};
 
 use super::helpers::{
-    build_sqlite_temp_table_create, recreate_indexes_after_rebuild, to_sea_fk_action,
+    build_copy_into_temp_table, build_sqlite_temp_table_create, recreate_indexes_after_rebuild,
+    to_sea_fk_action,
 };
 use super::rename_table::build_rename_table;
 use super::types::{BuiltQuery, DatabaseBackend};
@@ -216,24 +217,7 @@ fn build_sqlite_constraint_replace(
     );
 
     // 2. Copy data (all columns)
-    let column_aliases: Vec = table_def
-        .columns
-        .iter()
-        .map(|c| Alias::new(&c.name))
-        .collect();
-    let mut select_query = Query::select();
-    for col_alias in &column_aliases {
-        select_query.column(col_alias.clone());
-    }
-    select_query.from(Alias::new(table));
-
-    let insert_stmt = Query::insert()
-        .into_table(Alias::new(&temp_table))
-        .columns(column_aliases.clone())
-        .select_from(select_query)
-        .unwrap()
-        .to_owned();
-    let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
+    let insert_query = build_copy_into_temp_table(table, &temp_table, &table_def.columns);
 
     // 3. Drop original table
     let drop_table = Table::drop().table(Alias::new(table)).to_owned();

From 0a127b5545c5b460d5cf4a6de3ea972beab70746 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 04:58:20 +0900
Subject: [PATCH 026/109] retry-now#0025: batch -- hoist build_delete_table for
 8 DROP TABLE sites, extract build_drop_index_query helper (2 kept)

---
 crates/vespertide-query/src/sql/add_column.rs |  3 +--
 .../src/sql/add_constraint/mod.rs             |  4 +---
 .../src/sql/delete_column/sqlite_rebuild.rs   |  5 +----
 .../src/sql/modify_column_default.rs          |  3 +--
 .../src/sql/modify_column_nullable.rs         |  3 +--
 .../sql/modify_column_type/sqlite_rebuild.rs  |  5 +----
 .../src/sql/remove_constraint/mod.rs          | 21 ++++++++++++++-----
 .../src/sql/remove_constraint/mysql.rs        |  8 +------
 .../src/sql/remove_constraint/postgres.rs     |  6 +-----
 .../src/sql/remove_constraint/sqlite.rs       |  5 +----
 .../src/sql/replace_constraint.rs             |  5 ++---
 11 files changed, 27 insertions(+), 41 deletions(-)

diff --git a/crates/vespertide-query/src/sql/add_column.rs b/crates/vespertide-query/src/sql/add_column.rs
index a5246aac..069a7d93 100644
--- a/crates/vespertide-query/src/sql/add_column.rs
+++ b/crates/vespertide-query/src/sql/add_column.rs
@@ -93,8 +93,7 @@ pub fn build_add_column(
             .to_owned();
         let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
 
-        let drop_query =
-            BuiltQuery::DropTable(Box::new(Table::drop().table(Alias::new(table)).to_owned()));
+        let drop_query = super::delete_table::build_delete_table(table);
         let rename_query = build_rename_table(&temp_table, table);
 
         // Recreate indexes (both regular and UNIQUE)
diff --git a/crates/vespertide-query/src/sql/add_constraint/mod.rs b/crates/vespertide-query/src/sql/add_constraint/mod.rs
index c5b13766..08d50e57 100644
--- a/crates/vespertide-query/src/sql/add_constraint/mod.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/mod.rs
@@ -928,7 +928,6 @@ mod tests {
 
 mod unique;
 
-use sea_query::{Alias, Table};
 use vespertide_core::{TableConstraint, TableDef};
 
 use super::helpers::{
@@ -1090,8 +1089,7 @@ pub(super) fn rebuild_sqlite_table_with_added_constraint(
         &new_constraints,
     );
     let insert_query = build_copy_into_temp_table(table, &temp_table, &table_def.columns);
-    let drop_table = Table::drop().table(Alias::new(table)).to_owned();
-    let drop_query = BuiltQuery::DropTable(Box::new(drop_table));
+    let drop_query = super::delete_table::build_delete_table(table);
     let rename_query = build_rename_table(&temp_table, table);
     let index_queries =
         recreate_indexes_after_rebuild(table, &table_def.constraints, pending_constraints);
diff --git a/crates/vespertide-query/src/sql/delete_column/sqlite_rebuild.rs b/crates/vespertide-query/src/sql/delete_column/sqlite_rebuild.rs
index 94d4f477..7d086ccf 100644
--- a/crates/vespertide-query/src/sql/delete_column/sqlite_rebuild.rs
+++ b/crates/vespertide-query/src/sql/delete_column/sqlite_rebuild.rs
@@ -1,5 +1,3 @@
-use sea_query::{Alias, Table};
-
 use vespertide_core::{ColumnType, TableConstraint, TableDef};
 
 use crate::sql::helpers::{
@@ -68,8 +66,7 @@ pub(super) fn build_delete_column_sqlite_temp_table(
     stmts.push(build_copy_into_temp_table(table, &temp_table, &new_columns));
 
     // 3. Drop original table.
-    let drop_table = Table::drop().table(Alias::new(table)).to_owned();
-    stmts.push(BuiltQuery::DropTable(Box::new(drop_table)));
+    stmts.push(crate::sql::delete_table::build_delete_table(table));
 
     // 4. Rename temp table to original name.
     stmts.push(build_rename_table(&temp_table, table));
diff --git a/crates/vespertide-query/src/sql/modify_column_default.rs b/crates/vespertide-query/src/sql/modify_column_default.rs
index 0ebcc8f6..a8f83829 100644
--- a/crates/vespertide-query/src/sql/modify_column_default.rs
+++ b/crates/vespertide-query/src/sql/modify_column_default.rs
@@ -126,8 +126,7 @@ pub fn build_modify_column_default(
             ));
 
             // 3. Drop original table
-            let drop_table = Table::drop().table(Alias::new(table)).to_owned();
-            queries.push(BuiltQuery::DropTable(Box::new(drop_table)));
+            queries.push(super::delete_table::build_delete_table(table));
 
             // 4. Rename temporary table to original name
             queries.push(build_rename_table(&temp_table, table));
diff --git a/crates/vespertide-query/src/sql/modify_column_nullable.rs b/crates/vespertide-query/src/sql/modify_column_nullable.rs
index c8598f66..b16a4d4e 100644
--- a/crates/vespertide-query/src/sql/modify_column_nullable.rs
+++ b/crates/vespertide-query/src/sql/modify_column_nullable.rs
@@ -112,8 +112,7 @@ pub fn build_modify_column_nullable(
             ));
 
             // 3. Drop original table
-            let drop_table = Table::drop().table(Alias::new(table)).to_owned();
-            queries.push(BuiltQuery::DropTable(Box::new(drop_table)));
+            queries.push(super::delete_table::build_delete_table(table));
 
             // 4. Rename temporary table to original name
             queries.push(build_rename_table(&temp_table, table));
diff --git a/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs b/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs
index 0d7709c9..86c59fab 100644
--- a/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs
+++ b/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs
@@ -1,7 +1,5 @@
 use std::collections::BTreeMap;
 
-use sea_query::{Alias, Table};
-
 use vespertide_core::{ColumnType, TableConstraint, TableDef};
 
 use crate::error::QueryError;
@@ -51,8 +49,7 @@ pub(super) fn build_modify_column_type_sqlite_temp_table(
     let insert_query = build_copy_into_temp_table(table, &temp_table, &new_columns);
 
     // 3. Drop original table
-    let drop_table = Table::drop().table(Alias::new(table)).to_owned();
-    let drop_query = BuiltQuery::DropTable(Box::new(drop_table));
+    let drop_query = crate::sql::delete_table::build_delete_table(table);
 
     // 4. Rename temporary table to original name
     let rename_query = build_rename_table(&temp_table, table);
diff --git a/crates/vespertide-query/src/sql/remove_constraint/mod.rs b/crates/vespertide-query/src/sql/remove_constraint/mod.rs
index da50e400..6b4dfdaf 100644
--- a/crates/vespertide-query/src/sql/remove_constraint/mod.rs
+++ b/crates/vespertide-query/src/sql/remove_constraint/mod.rs
@@ -9,6 +9,21 @@ use vespertide_core::{TableConstraint, TableDef};
 use super::types::{BuiltQuery, DatabaseBackend};
 use crate::error::QueryError;
 
+/// Build a `DROP INDEX` query for the given table-qualified index name.
+///
+/// Shared by the SQLite-rebuild fallback in this module and the
+/// `TableConstraint::Index` arms of [`mysql::build_remove_constraint`]
+/// and [`postgres::build_remove_constraint`] — they all want the same
+/// `sea_query::Index::drop().table(...).name(...)` shape.
+#[must_use]
+pub(super) fn build_drop_index_query(table: &str, index_name: &str) -> BuiltQuery {
+    let idx_drop = sea_query::Index::drop()
+        .table(Alias::new(table))
+        .name(index_name)
+        .to_owned();
+    BuiltQuery::DropIndex(Box::new(idx_drop))
+}
+
 pub fn build_remove_constraint(
     backend: DatabaseBackend,
     table: &str,
@@ -49,11 +64,7 @@ fn build_drop_index(
     };
 
     let index_name = vespertide_naming::build_index_name(table, columns, name.as_deref());
-    let idx_drop = sea_query::Index::drop()
-        .table(Alias::new(table))
-        .name(&index_name)
-        .to_owned();
-    Ok(vec![BuiltQuery::DropIndex(Box::new(idx_drop))])
+    Ok(vec![build_drop_index_query(table, &index_name)])
 }
 
 fn constraint_kind(constraint: &TableConstraint) -> &'static str {
diff --git a/crates/vespertide-query/src/sql/remove_constraint/mysql.rs b/crates/vespertide-query/src/sql/remove_constraint/mysql.rs
index ca4cf9c2..453cfaa1 100644
--- a/crates/vespertide-query/src/sql/remove_constraint/mysql.rs
+++ b/crates/vespertide-query/src/sql/remove_constraint/mysql.rs
@@ -1,5 +1,3 @@
-use sea_query::Alias;
-
 use vespertide_core::TableConstraint;
 
 use crate::sql::helpers::quote_ident;
@@ -34,11 +32,7 @@ pub fn build_remove_constraint(table: &str, constraint: &TableConstraint) -> Vec
         }
         TableConstraint::Index { name, columns } => {
             let index_name = vespertide_naming::build_index_name(table, columns, name.as_deref());
-            let idx_drop = sea_query::Index::drop()
-                .table(Alias::new(table))
-                .name(&index_name)
-                .to_owned();
-            vec![BuiltQuery::DropIndex(Box::new(idx_drop))]
+            vec![super::build_drop_index_query(table, &index_name)]
         }
         TableConstraint::Check { name, .. } => {
             let mysql_table = quote_ident(table, DatabaseBackend::MySql);
diff --git a/crates/vespertide-query/src/sql/remove_constraint/postgres.rs b/crates/vespertide-query/src/sql/remove_constraint/postgres.rs
index d43c840c..3e51a16c 100644
--- a/crates/vespertide-query/src/sql/remove_constraint/postgres.rs
+++ b/crates/vespertide-query/src/sql/remove_constraint/postgres.rs
@@ -34,11 +34,7 @@ pub fn build_remove_constraint(table: &str, constraint: &TableConstraint) -> Vec
         }
         TableConstraint::Index { name, columns } => {
             let index_name = vespertide_naming::build_index_name(table, columns, name.as_deref());
-            let idx_drop = sea_query::Index::drop()
-                .table(Alias::new(table))
-                .name(&index_name)
-                .to_owned();
-            vec![BuiltQuery::DropIndex(Box::new(idx_drop))]
+            vec![super::build_drop_index_query(table, &index_name)]
         }
         TableConstraint::Check { name, .. } => {
             let pg_table = quote_ident(table, DatabaseBackend::Postgres);
diff --git a/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs b/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs
index d6e41080..c64bab6f 100644
--- a/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs
+++ b/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs
@@ -1,5 +1,3 @@
-use sea_query::{Alias, Table};
-
 use vespertide_core::{TableConstraint, TableDef};
 
 use crate::error::QueryError;
@@ -133,8 +131,7 @@ fn rebuild_table_without_constraint(
         new_constraints,
     );
     let insert_query = build_copy_into_temp_table(table, &temp_table, &table_def.columns);
-    let drop_query =
-        BuiltQuery::DropTable(Box::new(Table::drop().table(Alias::new(table)).to_owned()));
+    let drop_query = crate::sql::delete_table::build_delete_table(table);
     let rename_query = build_rename_table(&temp_table, table);
     let index_queries =
         recreate_indexes_after_rebuild(table, constraints_to_recreate, pending_constraints);
diff --git a/crates/vespertide-query/src/sql/replace_constraint.rs b/crates/vespertide-query/src/sql/replace_constraint.rs
index 121572e2..1d22dc17 100644
--- a/crates/vespertide-query/src/sql/replace_constraint.rs
+++ b/crates/vespertide-query/src/sql/replace_constraint.rs
@@ -1,4 +1,4 @@
-use sea_query::{Alias, ForeignKey, Table};
+use sea_query::{Alias, ForeignKey};
 
 use vespertide_core::{TableConstraint, TableDef};
 
@@ -220,8 +220,7 @@ fn build_sqlite_constraint_replace(
     let insert_query = build_copy_into_temp_table(table, &temp_table, &table_def.columns);
 
     // 3. Drop original table
-    let drop_table = Table::drop().table(Alias::new(table)).to_owned();
-    let drop_query = BuiltQuery::DropTable(Box::new(drop_table));
+    let drop_query = super::delete_table::build_delete_table(table);
 
     // 4. Rename temporary table to original name
     let rename_query = build_rename_table(&temp_table, table);

From faf52dac98dce623d24006ed2e0c0dcab4b33bf0 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 05:39:17 +0900
Subject: [PATCH 027/109] retry-now#0026: batch -- hoist
 require_table_in_schema helper, hoist names_to_strings helper, refresh
 AGENTS.md docs (3 kept)

---
 crates/vespertide-core/AGENTS.md              | 15 ++++--
 crates/vespertide-core/src/schema/names.rs    | 50 +++++++++++++++++
 crates/vespertide-planner/AGENTS.md           | 27 +++++++---
 .../vespertide-planner/src/drop_resolution.rs |  5 +-
 .../src/validate/cascade_reach.rs             |  7 ++-
 .../src/validate/dangling_fk_drops.rs         | 11 ++--
 .../src/validate/fk_orphan_additions.rs       | 26 ++-------
 .../src/validate/fk_policy_changes.rs         |  9 ++--
 .../src/validate/foreign_keys.rs              |  9 ++--
 .../src/validate/unique_additions.rs          | 11 ++--
 crates/vespertide-query/AGENTS.md             | 27 ++++++----
 crates/vespertide-query/src/sql/add_column.rs |  8 ++-
 .../src/sql/add_constraint/mod.rs             |  7 ++-
 .../src/sql/add_constraint/primary_key.rs     |  6 ++-
 .../src/sql/add_constraint/unique.rs          |  7 ++-
 crates/vespertide-query/src/sql/helpers.rs    | 53 +++++++++++++++++++
 crates/vespertide-query/src/sql/mod.rs        |  2 +-
 .../src/sql/modify_column_nullable.rs         | 13 ++++-
 .../sql/modify_column_type/sqlite_rebuild.rs  |  7 ++-
 .../src/sql/remove_constraint/sqlite.rs       |  7 ++-
 20 files changed, 235 insertions(+), 72 deletions(-)

diff --git a/crates/vespertide-core/AGENTS.md b/crates/vespertide-core/AGENTS.md
index f5650fa4..17ffa808 100644
--- a/crates/vespertide-core/AGENTS.md
+++ b/crates/vespertide-core/AGENTS.md
@@ -7,11 +7,20 @@ Core data structures for schema definition and migration planning.
 ```
 src/
 ├── lib.rs              # Re-exports all public types
-├── action.rs           # MigrationAction (14 variants), MigrationPlan (1236 lines; scheduled split)
+├── action/             # MigrationAction (15 variants), MigrationPlan
+│   ├── mod.rs          #   Variant definitions + plan struct (937 lines)
+│   ├── display.rs      #   Human-readable rendering of actions
+│   ├── prefix.rs       #   Table-name prefixing (uses TableName::with_prefix)
+│   ├── narrowing_strategy.rs # Type-narrowing strategy enum + helpers
+│   └── remap_mapping_serde.rs # RemapEnumValues mapping (de)serialization
 ├── migration.rs        # MigrationError, MigrationOptions
 └── schema/
     ├── column.rs       # ColumnDef, ColumnType, SimpleColumnType, ComplexColumnType
-    ├── table.rs        # TableDef, normalize() method
+    ├── table/          # TableDef + normalize()
+    │   ├── mod.rs      #   TableDef definition + re-exports (49 lines)
+    │   ├── normalize.rs#   Inline→table-level constraint conversion
+    │   ├── normalize_proptest.rs # Property-based tests for normalize()
+    │   └── validation.rs # Structural validation (duplicate columns, etc.)
     ├── constraint.rs   # TableConstraint (5 variants)
     ├── foreign_key.rs  # ForeignKeySyntax for inline FK definition
     ├── primary_key.rs  # PrimaryKeySyntax, PrimaryKeyDef
@@ -72,5 +81,5 @@ MigrationAction::AddColumn { table, column, fill_with }
 - Model and migration serialization supports both JSON and YAML.
 - Prefer typed `MigrationAction` enums; `MigrationAction::RawSql` exists as a documented emergency escape hatch, but is not recommended for normal use.
 - Shared proptest strategies live behind the `arbitrary` feature. Run property tests with `cargo test -p vespertide-core --features arbitrary`.
-- Every `.rs` file must stay ≤ 1000 lines (CI enforced); current hotspots include `action.rs` (1236 lines) and `schema/table.rs` (1526 lines).
+- Every production-only `.rs` file must stay ≤ 1000 lines, files carrying inline `#[cfg(test)] mod tests` ≤ 1200 lines (CI enforced via `scripts/check-line-budget.sh`); the current `action/mod.rs` (937 lines) is the near-ceiling file in this crate. `action.rs` and `schema/table.rs` were already split into the directories shown above.
 - Workspace lints warn on unsafe code and Clippy all: `unsafe_code = "warn"`, `clippy::all = { level = "warn", priority = -1 }`.
diff --git a/crates/vespertide-core/src/schema/names.rs b/crates/vespertide-core/src/schema/names.rs
index c5026de0..2d5bfda3 100644
--- a/crates/vespertide-core/src/schema/names.rs
+++ b/crates/vespertide-core/src/schema/names.rs
@@ -215,6 +215,36 @@ impl TableName {
     }
 }
 
+/// Convert any slice of stringy items (typically a `Vec`) into
+/// a `Vec` — hoisted out of 14+ verbatim
+/// `xxx.iter().map(ToString::to_string).collect()` chains across the planner
+/// and query crates.
+///
+/// The generic bound `T: ToString` keeps the helper usable with both the
+/// 0.2.0 [`ColumnName`] newtype (where `Display` produces the bare
+/// identifier) and the older `&str` / `String` aliases that still sit
+/// behind a few `Vec<&&str>` collect call sites.
+///
+/// ```rust
+/// use vespertide_core::schema::names::{ColumnName, names_to_strings};
+///
+/// let cols: Vec = vec!["a".into(), "b".into()];
+/// assert_eq!(names_to_strings(&cols), vec!["a".to_string(), "b".to_string()]);
+///
+/// // Works with `&str` slices too — same `ToString` bound, no allocation
+/// // change versus the inline pattern.
+/// let strs = ["x", "y", "z"];
+/// assert_eq!(names_to_strings(&strs), vec!["x".to_string(), "y".to_string(), "z".to_string()]);
+///
+/// // Empty slice is the canonical empty `Vec`.
+/// let empty: Vec = vec![];
+/// assert_eq!(names_to_strings(&empty), Vec::::new());
+/// ```
+#[must_use]
+pub fn names_to_strings(names: &[T]) -> Vec {
+    names.iter().map(ToString::to_string).collect()
+}
+
 /// Join a slice of [`ColumnName`]s with a separator using a single buffer
 /// — no intermediate `Vec` allocation.
 ///
@@ -330,4 +360,24 @@ mod tests {
         let composite: Vec = vec!["tenant_id".into(), "user_id".into()];
         assert_eq!(join_column_names(&composite, "_"), "tenant_id_user_id");
     }
+
+    /// `names_to_strings` empty-slice produces an empty `Vec`. Locks
+    /// the same empty-input contract previously baked into the inline
+    /// `xxx.iter().map(ToString::to_string).collect()` form at every replaced
+    /// callsite.
+    #[test]
+    fn names_to_strings_empty_returns_empty_vec() {
+        let cols: Vec = vec![];
+        assert_eq!(names_to_strings(&cols), Vec::::new());
+    }
+
+    /// `names_to_strings` on a populated slice produces the canonical
+    /// per-element `to_string()` output in source order — the contract every
+    /// planner / query crate call site silently depended on.
+    #[test]
+    fn names_to_strings_two_element_case() {
+        let cols: Vec = vec!["a".into(), "b".into()];
+        let out = names_to_strings(&cols);
+        assert_eq!(out, vec!["a".to_string(), "b".to_string()]);
+    }
 }
diff --git a/crates/vespertide-planner/AGENTS.md b/crates/vespertide-planner/AGENTS.md
index 0c903a48..a7ae045a 100644
--- a/crates/vespertide-planner/AGENTS.md
+++ b/crates/vespertide-planner/AGENTS.md
@@ -6,9 +6,20 @@ Schema diffing engine - compares baseline vs target schema to emit typed migrati
 
 ```
 src/
-├── diff.rs      # 4739 lines, scheduled for split - Schema comparison, topological sort
-├── validate.rs  # 2299 lines, scheduled for split - Schema/plan validation
-├── apply.rs     # 1617 lines, scheduled for split - Apply actions to in-memory schema
+├── diff/        # Schema comparison, topological sort
+│   ├── mod.rs   #   Public entry diff_schemas() (91 lines)
+│   ├── tables.rs#   CreateTable / DeleteTable detection
+│   ├── columns.rs#  Column-level add/modify/delete diffing
+│   ├── constraints.rs # PK / Unique / FK / Check / Index diffing
+│   └── ordering.rs # Topological sort by FK dependencies (Kahn's algorithm)
+├── validate/    # Schema/plan validation (mod.rs 67 lines + 24 per-fault validator modules)
+├── apply/       # Apply actions to in-memory schema
+│   ├── mod.rs   #   Public entry apply_action() (76 lines)
+│   ├── column_ops.rs   # AddColumn / RenameColumn / DeleteColumn / ModifyColumn*
+│   ├── constraint_ops.rs # AddConstraint / RemoveConstraint / ReplaceConstraint
+│   ├── table_ops.rs    # CreateTable / DeleteTable / RenameTable
+│   └── raw_sql.rs      # RawSql no-op (opaque to baseline replay)
+├── drop_resolution.rs  # Interactive rename-vs-drop resolution for DeleteColumn/Table
 ├── schema.rs    # Replay migrations → baseline schema
 ├── plan.rs      # High-level planning API
 └── error.rs     # PlannerError enum
@@ -18,12 +29,12 @@ src/
 
 | Task | File | Key Functions |
 |------|------|---------------|
-| Compare schemas | `diff.rs` | `diff_schemas()` |
+| Compare schemas | `diff/mod.rs` | `diff_schemas()` |
 | Replay migrations | `schema.rs` | `schema_from_plans()` |
 | One-shot planning | `plan.rs` | `plan_next_migration()` |
-| Apply single action | `apply.rs` | `apply_action()` |
-| Validate schema | `validate.rs` | `validate_schema()`, `validate_migration_plan()` |
-| FK dependency sort | `diff.rs` | `topological_sort_tables()`, `sort_delete_tables()` |
+| Apply single action | `apply/mod.rs` | `apply_action()` |
+| Validate schema | `validate/mod.rs` | `validate_schema()`, `validate_migration_plan()` |
+| FK dependency sort | `diff/ordering.rs` | `topological_sort_tables()`, `sort_delete_tables()` |
 
 ## ALGORITHM NOTES
 
@@ -87,5 +98,5 @@ Statically analysable faults that **are** detected here:
 
 - YAML and JSON are both fully supported for models and migrations.
 - Prefer typed `MigrationAction` enums; `RawSql` exists as a documented emergency escape hatch, but is opaque to baseline replay and not recommended for normal use.
-- Every `.rs` file must stay ≤ 1000 lines (CI enforced); current planner hotspots are `diff.rs` (4739), `validate.rs` (2299), and `apply.rs` (1617).
+- Two-tier line policy (CI-enforced via `scripts/check-line-budget.sh`): production-only `.rs` ≤ 1000 lines, files carrying inline `#[cfg(test)] mod tests` ≤ 1200 lines. The previously oversized `diff.rs`, `validate.rs`, and `apply.rs` have all been split into the directories shown above; the near-ceiling files now are `validate/check_expr_parser.rs` (~1199 lines, inline tests) and `drop_resolution.rs` (~1186 lines, inline tests).
 - Workspace lints warn on unsafe code and Clippy all: `unsafe_code = "warn"`, `clippy::all = { level = "warn", priority = -1 }`.
diff --git a/crates/vespertide-planner/src/drop_resolution.rs b/crates/vespertide-planner/src/drop_resolution.rs
index 87ac81b8..7f92e8c2 100644
--- a/crates/vespertide-planner/src/drop_resolution.rs
+++ b/crates/vespertide-planner/src/drop_resolution.rs
@@ -302,12 +302,13 @@ fn table_candidate(
 
     let mut differences = Vec::new();
     if !only_in_baseline.is_empty() {
-        let mut names: Vec = only_in_baseline.iter().map(ToString::to_string).collect();
+        let mut names: Vec =
+            vespertide_core::schema::names::names_to_strings(&only_in_baseline);
         names.sort();
         differences.push(format!("removed columns: {}", names.join(", ")));
     }
     if !only_in_new.is_empty() {
-        let mut names: Vec = only_in_new.iter().map(ToString::to_string).collect();
+        let mut names: Vec = vespertide_core::schema::names::names_to_strings(&only_in_new);
         names.sort();
         differences.push(format!("added columns: {}", names.join(", ")));
     }
diff --git a/crates/vespertide-planner/src/validate/cascade_reach.rs b/crates/vespertide-planner/src/validate/cascade_reach.rs
index 141d84ea..117ef9da 100644
--- a/crates/vespertide-planner/src/validate/cascade_reach.rs
+++ b/crates/vespertide-planner/src/validate/cascade_reach.rs
@@ -41,7 +41,10 @@
 
 use std::collections::{BTreeMap, HashSet};
 
-use vespertide_core::{MigrationAction, MigrationPlan, ReferenceAction, TableConstraint, TableDef};
+use vespertide_core::{
+    MigrationAction, MigrationPlan, ReferenceAction, TableConstraint, TableDef,
+    schema::names::names_to_strings,
+};
 
 /// Depth at or above which a cascade chain is flagged as "Deep".
 const DEEP_THRESHOLD: usize = 3;
@@ -127,7 +130,7 @@ pub fn find_cascade_reach_violations(
         warnings.push(CascadeReachWarning {
             action_index: idx,
             origin_child_table: table.to_string(),
-            origin_columns: columns.iter().map(ToString::to_string).collect(),
+            origin_columns: names_to_strings(columns),
             parent_table: ref_table.to_string(),
             depth,
             reached_tables: reached,
diff --git a/crates/vespertide-planner/src/validate/dangling_fk_drops.rs b/crates/vespertide-planner/src/validate/dangling_fk_drops.rs
index 04c2eaa5..3e546506 100644
--- a/crates/vespertide-planner/src/validate/dangling_fk_drops.rs
+++ b/crates/vespertide-planner/src/validate/dangling_fk_drops.rs
@@ -48,7 +48,10 @@
 
 use std::collections::{BTreeSet, HashSet};
 
-use vespertide_core::{ColumnName, MigrationAction, MigrationPlan, TableConstraint, TableDef};
+use vespertide_core::{
+    ColumnName, MigrationAction, MigrationPlan, TableConstraint, TableDef,
+    schema::names::names_to_strings,
+};
 
 /// A single dangling FK reference: dropping `(dropped_table, dropped_column)`
 /// would leave `referencing_table` with a foreign key that points at nothing.
@@ -297,7 +300,7 @@ fn collect_explicitly_removed_fks(plan: &MigrationPlan) -> HashSet
             set.insert((
                 table.to_string(),
                 name.clone(),
-                columns.iter().map(ToString::to_string).collect(),
+                names_to_strings(columns),
                 ref_table.to_string(),
             ));
         }
@@ -318,7 +321,7 @@ fn collect_explicitly_removed_fks(plan: &MigrationPlan) -> HashSet
             set.insert((
                 table.to_string(),
                 name.clone(),
-                columns.iter().map(ToString::to_string).collect(),
+                names_to_strings(columns),
                 ref_table.to_string(),
             ));
         }
@@ -336,7 +339,7 @@ fn fk_in_removed_set(
     let key: RemovedFkKey = (
         owner.to_string(),
         name.cloned(),
-        columns.iter().map(ToString::to_string).collect(),
+        names_to_strings(columns),
         ref_table.to_string(),
     );
     removed.contains(&key)
diff --git a/crates/vespertide-planner/src/validate/fk_orphan_additions.rs b/crates/vespertide-planner/src/validate/fk_orphan_additions.rs
index d9d4c573..2f8b305f 100644
--- a/crates/vespertide-planner/src/validate/fk_orphan_additions.rs
+++ b/crates/vespertide-planner/src/validate/fk_orphan_additions.rs
@@ -28,7 +28,9 @@
 
 use std::collections::HashSet;
 
-use vespertide_core::{ColumnName, MigrationAction, MigrationPlan, TableConstraint, TableDef};
+use vespertide_core::{
+    MigrationAction, MigrationPlan, TableConstraint, TableDef, schema::names::names_to_strings,
+};
 
 /// One risky FK addition needing user resolution.
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -116,9 +118,9 @@ pub fn find_fk_orphan_additions(
             action_index: idx,
             table: table.to_string(),
             constraint_name: name.clone(),
-            columns: columns_to_strings(columns),
+            columns: names_to_strings(columns),
             ref_table: ref_table.to_string(),
-            ref_columns: columns_to_strings(ref_columns),
+            ref_columns: names_to_strings(ref_columns),
             all_columns_nullable,
         });
     }
@@ -126,10 +128,6 @@ pub fn find_fk_orphan_additions(
     out
 }
 
-fn columns_to_strings(cols: &[ColumnName]) -> Vec {
-    cols.iter().map(ToString::to_string).collect()
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -331,18 +329,4 @@ mod tests {
         assert_eq!(ws.len(), 1);
         assert_eq!(ws[0].action_index, 2);
     }
-
-    /// Coverage-closure: ensure `columns_to_strings` produces the expected
-    /// owned String list, including the empty-input edge case (defensive
-    /// helper contract).
-    #[rstest]
-    fn case_10_columns_to_strings_helper_contract() {
-        let empty: Vec = vec![];
-        assert!(columns_to_strings(&empty).is_empty());
-        let some: Vec = vec!["a".into(), "b".into()];
-        assert_eq!(
-            columns_to_strings(&some),
-            vec!["a".to_string(), "b".to_string()]
-        );
-    }
 }
diff --git a/crates/vespertide-planner/src/validate/fk_policy_changes.rs b/crates/vespertide-planner/src/validate/fk_policy_changes.rs
index 4c0fecd5..ca9d601b 100644
--- a/crates/vespertide-planner/src/validate/fk_policy_changes.rs
+++ b/crates/vespertide-planner/src/validate/fk_policy_changes.rs
@@ -14,7 +14,10 @@
 //! between `None` and `Some(NoAction)` is therefore not flagged, but any
 //! transition that changes observable behaviour is.
 
-use vespertide_core::{MigrationAction, MigrationPlan, ReferenceAction, TableConstraint};
+use vespertide_core::{
+    MigrationAction, MigrationPlan, ReferenceAction, TableConstraint,
+    schema::names::names_to_strings,
+};
 
 /// A single FK constraint whose `on_delete` or `on_update` policy is being
 /// changed by a `ReplaceConstraint` action in the migration plan.
@@ -133,9 +136,9 @@ fn warning_for_action(idx: usize, action: &MigrationAction) -> Option Vec PkKin
         .iter()
         .find_map(|c| {
             if let TableConstraint::PrimaryKey { columns, .. } = c {
-                Some(columns.iter().map(ToString::to_string).collect())
+                Some(names_to_strings(columns))
             } else {
                 None
             }
@@ -198,7 +201,7 @@ fn collect_fk_references(
                 out.push(FkReference {
                     child_table: tbl.name.to_string(),
                     constraint_name: name.clone(),
-                    child_columns: columns.iter().map(ToString::to_string).collect(),
+                    child_columns: names_to_strings(columns),
                 });
             }
         }
diff --git a/crates/vespertide-query/AGENTS.md b/crates/vespertide-query/AGENTS.md
index 9c026ed7..7353be3a 100644
--- a/crates/vespertide-query/AGENTS.md
+++ b/crates/vespertide-query/AGENTS.md
@@ -7,20 +7,26 @@ Converts `MigrationAction` enums to SQL via sea-query intermediate representatio
 ```
 src/
 ├── lib.rs              # Re-exports: build_action_queries, BuiltQuery, DatabaseBackend
-├── builder.rs          # build_plan_queries() - orchestrates full plan with schema evolution
+├── builder/            # build_plan_queries() - orchestrates full plan with schema evolution
+│   ├── mod.rs          #   Re-export entry + shared helpers (pending_constraints_for_action)
+│   ├── sequential.rs   #   Single-threaded dispatch path
+│   ├── parallel.rs     #   Rayon-driven parallel path
+│   ├── transaction.rs  #   BEGIN/COMMIT wrapping
+│   └── test_support.rs #   Shared #[cfg(test)] fixtures for sequential/parallel tests
 ├── error.rs            # QueryError enum
 └── sql/
-    ├── mod.rs          # build_action_queries() dispatch - matches all 14 MigrationAction variants (1507 lines)
+    ├── mod.rs          # build_action_queries() dispatch - matches all 15 MigrationAction variants (218 lines)
     ├── types.rs        # BuiltQuery (11 variants), DatabaseBackend, RawSql
-    ├── helpers.rs      # Column type mapping, FK actions, enum handling, naming
+    ├── helpers.rs      # Column type mapping, FK actions, enum handling, naming, require_table_in_schema
     ├── create_table.rs # build_create_table(), build_create_table_for_backend()
     ├── add_column.rs   # Temp table for SQLite non-nullable/enum columns
-    ├── add_constraint.rs     # Constraint SQL generation (1356 lines)
-    ├── remove_constraint.rs  # Complex SQLite temp table workarounds (1465 lines)
-    ├── modify_column_*.rs    # type, nullable, default, comment handlers
+    ├── add_constraint/ # Constraint SQL generation (mod.rs 1086 lines + check / foreign_key / index / primary_key / unique)
+    ├── remove_constraint/ # SQLite temp-table workarounds (mod.rs 644 lines + per-backend submodules)
+    ├── modify_column_*.rs # type, nullable, default, comment handlers (modify_column_type/ is itself a directory: direct / sqlite_rebuild / fill_with)
     ├── rename_*.rs     # Simple ALTER statements
-    ├── delete_column.rs # DROP COLUMN with SQLite rebuild (1084 lines)
+    ├── delete_column/  # DROP COLUMN with SQLite rebuild (mod.rs 1068 lines + tests)
     ├── delete_*.rs     # Other DROP statements
+    ├── remap_enum_values.rs # RemapEnumValues handler (string- and integer-enum value remapping)
     └── raw_sql.rs      # RawSql emergency escape hatch
 ```
 
@@ -30,10 +36,11 @@ src/
 |------|------|--------------|
 | Add new action handler | `sql/mod.rs` | Add to `build_action_queries()` match |
 | Column type mapping | `sql/helpers.rs` | `apply_column_type_with_table()` |
-| SQLite workarounds | `sql/remove_constraint.rs` | `{table}_temp` pattern |
+| SQLite workarounds | `sql/remove_constraint/sqlite.rs` | `{table}_temp` pattern |
 | Backend-specific emergency SQL | `sql/types.rs` | `RawSql::per_backend()` |
 | Default value conversion | `sql/helpers.rs` | `convert_default_for_backend()` |
 | Enum type handling | `sql/helpers.rs` | `build_create_enum_type_sql()` |
+| Required table lookup (uniform error) | `sql/helpers.rs` | `require_table_in_schema()` |
 
 ## CONVENTIONS
 
@@ -164,9 +171,9 @@ extends. The change must include:
 
 ## NOTES
 
-- `build_action_queries()` is exhaustive for all 14 `MigrationAction` variants: CreateTable, DeleteTable, AddColumn, RenameColumn, DeleteColumn, ModifyColumnType, ModifyColumnNullable, ModifyColumnDefault, ModifyColumnComment, AddConstraint, RemoveConstraint, ReplaceConstraint, RenameTable, RawSql.
+- `build_action_queries()` is exhaustive for all 15 `MigrationAction` variants: CreateTable, DeleteTable, AddColumn, RenameColumn, DeleteColumn, ModifyColumnType, ModifyColumnNullable, ModifyColumnDefault, ModifyColumnComment, AddConstraint, RemoveConstraint, ReplaceConstraint, RemapEnumValues, RenameTable, RawSql.
 - Prefer typed `MigrationAction` enums; `RawSql` exists as a documented emergency escape hatch, but is not recommended for normal use.
 - SQLite has full feature support via temp-table-rebuild workarounds for ALTER limitations.
 - YAML and JSON are both fully supported upstream input formats.
-- Every `.rs` file must stay ≤ 1000 lines (CI enforced); current hotspots are `sql/mod.rs` (1507), `remove_constraint.rs` (1465), `add_constraint.rs` (1356), and `delete_column.rs` (1084).
+- Two-tier line policy (CI-enforced via `scripts/check-line-budget.sh`): production-only `.rs` ≤ 1000 lines, files carrying inline `#[cfg(test)] mod tests` ≤ 1200 lines. Current near-ceiling files in this crate: `add_constraint/mod.rs` (1086, inline tests), `delete_column/mod.rs` (1068, inline tests); the previously oversized `sql/mod.rs`, `remove_constraint.rs`, and `delete_column.rs` have all been split into the directories shown above.
 - Workspace lints warn on unsafe code and Clippy all: `unsafe_code = "warn"`, `clippy::all = { level = "warn", priority = -1 }`.
diff --git a/crates/vespertide-query/src/sql/add_column.rs b/crates/vespertide-query/src/sql/add_column.rs
index 069a7d93..f6ba5419 100644
--- a/crates/vespertide-query/src/sql/add_column.rs
+++ b/crates/vespertide-query/src/sql/add_column.rs
@@ -5,7 +5,7 @@ use vespertide_core::{ColumnDef, TableDef};
 use super::helpers::{
     build_create_enum_type_sql, build_sea_column_def_with_table, build_sqlite_temp_table_create,
     convert_default_for_backend, normalize_enum_default, normalize_fill_with,
-    recreate_indexes_after_rebuild,
+    recreate_indexes_after_rebuild, require_table_in_schema,
 };
 use super::rename_table::build_rename_table;
 use super::types::{BuiltQuery, DatabaseBackend};
@@ -45,7 +45,11 @@ pub fn build_add_column(
         backend == DatabaseBackend::Sqlite && (!column.nullable || is_enum_column(column));
 
     if sqlite_needs_recreation {
-        let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::SchemaError(format!("Table '{table}' not found in current schema. SQLite requires current schema information to add columns.")))?;
+        let table_def = require_table_in_schema(
+            current_schema,
+            table,
+            "SQLite requires current schema information to add columns",
+        )?;
 
         let mut new_columns = table_def.columns.clone();
         new_columns.push(column.clone());
diff --git a/crates/vespertide-query/src/sql/add_constraint/mod.rs b/crates/vespertide-query/src/sql/add_constraint/mod.rs
index 08d50e57..acb51332 100644
--- a/crates/vespertide-query/src/sql/add_constraint/mod.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/mod.rs
@@ -932,6 +932,7 @@ use vespertide_core::{TableConstraint, TableDef};
 
 use super::helpers::{
     build_copy_into_temp_table, build_sqlite_temp_table_create, recreate_indexes_after_rebuild,
+    require_table_in_schema,
 };
 use super::rename_table::build_rename_table;
 use super::types::{BuiltQuery, DatabaseBackend};
@@ -1078,7 +1079,11 @@ pub(super) fn rebuild_sqlite_table_with_added_constraint(
     current_schema: &[TableDef],
     pending_constraints: &[TableConstraint],
 ) -> Result, QueryError> {
-    let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::SchemaError(format!("Table '{table}' not found in current schema. SQLite requires current schema information to add constraints.")))?;
+    let table_def = require_table_in_schema(
+        current_schema,
+        table,
+        "SQLite requires current schema information to add constraints",
+    )?;
     let new_constraints = merge_constraint(&table_def.constraints, constraint);
     let temp_table = format!("{table}_temp");
     let create_query = build_sqlite_temp_table_create(
diff --git a/crates/vespertide-query/src/sql/add_constraint/primary_key.rs b/crates/vespertide-query/src/sql/add_constraint/primary_key.rs
index 692ca3a7..11a6968f 100644
--- a/crates/vespertide-query/src/sql/add_constraint/primary_key.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/primary_key.rs
@@ -1,4 +1,6 @@
-use vespertide_core::{KeepPolicy, PrimaryKeyAdditionStrategy, TableConstraint};
+use vespertide_core::{
+    KeepPolicy, PrimaryKeyAdditionStrategy, TableConstraint, schema::names::names_to_strings,
+};
 
 use super::super::helpers::{quote_ident, quote_idents};
 use super::super::types::{BuiltQuery, DatabaseBackend, RawSql};
@@ -98,7 +100,7 @@ fn try_resolve_single_pk_column>(
         .iter()
         .find_map(|c| {
             if let TableConstraint::PrimaryKey { columns, .. } = c {
-                Some(columns.iter().map(ToString::to_string).collect())
+                Some(names_to_strings(columns))
             } else {
                 None
             }
diff --git a/crates/vespertide-query/src/sql/add_constraint/unique.rs b/crates/vespertide-query/src/sql/add_constraint/unique.rs
index 7d6120eb..788eb09e 100644
--- a/crates/vespertide-query/src/sql/add_constraint/unique.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/unique.rs
@@ -1,6 +1,9 @@
 use sea_query::{Alias, Index};
 
-use vespertide_core::{KeepPolicy, TableConstraint, TableDef, UniqueConstraintStrategy};
+use vespertide_core::{
+    KeepPolicy, TableConstraint, TableDef, UniqueConstraintStrategy,
+    schema::names::names_to_strings,
+};
 
 use super::super::helpers::{build_unique_constraint_name, quote_ident, quote_idents};
 use super::super::types::{BuiltQuery, DatabaseBackend, RawSql};
@@ -98,7 +101,7 @@ fn try_resolve_single_pk_column>(
         .iter()
         .find_map(|c| {
             if let TableConstraint::PrimaryKey { columns, .. } = c {
-                Some(columns.iter().map(ToString::to_string).collect())
+                Some(names_to_strings(columns))
             } else {
                 None
             }
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index 337a6bce..8b5a8617 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -5,6 +5,7 @@ use sea_query::{
 
 use vespertide_core::{
     ColumnDef, ColumnType, ComplexColumnType, ReferenceAction, SimpleColumnType, TableConstraint,
+    TableDef,
 };
 
 use super::create_table::build_create_table_for_backend;
@@ -750,6 +751,27 @@ pub fn quote_idents>(names: &[T], backend: DatabaseBackend) -> Str
     out
 }
 
+/// Look up `table` in `current_schema` and return a reference to its
+/// [`TableDef`], or a uniform `QueryError::SchemaError` describing why the
+/// lookup is mandatory for the calling backend.
+///
+/// Centralises the six SQL-builder call sites that previously open-coded
+/// the same `current_schema.iter().find(...).ok_or_else(...)` chain. The
+/// emitted message is `"Table '{table}' not found in current schema.
+/// {context}."` — pass `context` WITHOUT a trailing period so the helper
+/// is the single source of truth for the sentence terminator.
+pub(crate) fn require_table_in_schema<'a>(
+    schema: &'a [TableDef],
+    table: &str,
+    context: &str,
+) -> Result<&'a TableDef, crate::QueryError> {
+    schema.iter().find(|t| t.name == table).ok_or_else(|| {
+        crate::QueryError::SchemaError(format!(
+            "Table '{table}' not found in current schema. {context}."
+        ))
+    })
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -780,4 +802,35 @@ mod tests {
             "empty-checks branch must return BuiltQuery::CreateTable"
         );
     }
+
+    /// `require_table_in_schema` returns the matching table when present.
+    #[test]
+    fn require_table_in_schema_hit_returns_table_ref() {
+        let schema = vec![TableDef {
+            name: "users".into(),
+            description: None,
+            columns: vec![],
+            constraints: vec![],
+        }];
+        let table_def = require_table_in_schema(&schema, "users", "test context")
+            .expect("table 'users' should be found in the schema");
+        assert_eq!(table_def.name.as_str(), "users");
+    }
+
+    /// `require_table_in_schema` emits the canonical `SchemaError`
+    /// message — same template as every replaced call site, so wire-error
+    /// output stays byte-identical for callers and downstream tests.
+    #[test]
+    fn require_table_in_schema_miss_emits_canonical_message() {
+        let schema: Vec = vec![];
+        let err = require_table_in_schema(&schema, "missing", "SQLite requires test context")
+            .expect_err("missing table must error");
+        let msg = err.to_string();
+        assert!(
+            msg.contains(
+                "Table 'missing' not found in current schema. SQLite requires test context."
+            ),
+            "expected canonical message with trailing period, got: {msg}"
+        );
+    }
 }
diff --git a/crates/vespertide-query/src/sql/mod.rs b/crates/vespertide-query/src/sql/mod.rs
index 0582ba56..511afba2 100644
--- a/crates/vespertide-query/src/sql/mod.rs
+++ b/crates/vespertide-query/src/sql/mod.rs
@@ -52,7 +52,7 @@ pub fn build_action_queries(
 /// to avoid recreating indexes that will be created by future `AddConstraint` actions.
 #[expect(
     clippy::too_many_lines,
-    reason = "flat 14-variant MigrationAction dispatcher kept inline so the variant→builder mapping stays auditable; extracting individual arms scatters the routing logic"
+    reason = "flat 15-variant MigrationAction dispatcher kept inline so the variant→builder mapping stays auditable; extracting individual arms scatters the routing logic"
 )]
 pub fn build_action_queries_with_pending(
     backend: DatabaseBackend,
diff --git a/crates/vespertide-query/src/sql/modify_column_nullable.rs b/crates/vespertide-query/src/sql/modify_column_nullable.rs
index b16a4d4e..3d88f4f3 100644
--- a/crates/vespertide-query/src/sql/modify_column_nullable.rs
+++ b/crates/vespertide-query/src/sql/modify_column_nullable.rs
@@ -5,6 +5,7 @@ use vespertide_core::{ColumnDef, TableDef};
 use super::helpers::{
     build_copy_into_temp_table, build_sea_column_def_with_table, build_sqlite_temp_table_create,
     convert_default_for_backend, normalize_fill_with, quote_ident, recreate_indexes_after_rebuild,
+    require_table_in_schema,
 };
 use super::rename_table::build_rename_table;
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
@@ -61,7 +62,11 @@ pub fn build_modify_column_nullable(
         DatabaseBackend::MySql => {
             // MySQL requires the full column definition in MODIFY COLUMN
             // We need to get the column type from current schema
-            let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::SchemaError(format!("Table '{table}' not found in current schema. MySQL requires current schema information to modify column nullability.")))?;
+            let table_def = require_table_in_schema(
+                current_schema,
+                table,
+                "MySQL requires current schema information to modify column nullability",
+            )?;
 
             let column_def = table_def.columns.iter().find(|c| c.name == column).ok_or_else(|| QueryError::SchemaError(format!("Column '{column}' not found in table '{table}'. MySQL requires column information to modify nullability.")))?;
 
@@ -83,7 +88,11 @@ pub fn build_modify_column_nullable(
         DatabaseBackend::Sqlite => {
             // SQLite doesn't support ALTER COLUMN for nullability changes
             // Use temporary table approach
-            let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::SchemaError(format!("Table '{table}' not found in current schema. SQLite requires current schema information to modify column nullability.")))?;
+            let table_def = require_table_in_schema(
+                current_schema,
+                table,
+                "SQLite requires current schema information to modify column nullability",
+            )?;
 
             // Create modified columns with the new nullability
             let mut new_columns = table_def.columns.clone();
diff --git a/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs b/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs
index 86c59fab..8f555f66 100644
--- a/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs
+++ b/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs
@@ -5,6 +5,7 @@ use vespertide_core::{ColumnType, TableConstraint, TableDef};
 use crate::error::QueryError;
 use crate::sql::helpers::{
     build_copy_into_temp_table, build_sqlite_temp_table_create, recreate_indexes_after_rebuild,
+    require_table_in_schema,
 };
 use crate::sql::rename_table::build_rename_table;
 use crate::sql::types::{BuiltQuery, DatabaseBackend};
@@ -20,7 +21,11 @@ pub(super) fn build_modify_column_type_sqlite_temp_table(
     pending_constraints: &[TableConstraint],
 ) -> Result, QueryError> {
     // Current schema information is required
-    let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::SchemaError(format!("Table '{table}' not found in current schema. SQLite requires current schema information to modify column types.")))?;
+    let table_def = require_table_in_schema(
+        current_schema,
+        table,
+        "SQLite requires current schema information to modify column types",
+    )?;
 
     // Create new column definitions with the modified column
     let mut new_columns = table_def.columns.clone();
diff --git a/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs b/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs
index c64bab6f..82f73ef4 100644
--- a/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs
+++ b/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs
@@ -3,6 +3,7 @@ use vespertide_core::{TableConstraint, TableDef};
 use crate::error::QueryError;
 use crate::sql::helpers::{
     build_copy_into_temp_table, build_sqlite_temp_table_create, recreate_indexes_after_rebuild,
+    require_table_in_schema,
 };
 use crate::sql::rename_table::build_rename_table;
 use crate::sql::types::{BuiltQuery, DatabaseBackend};
@@ -23,7 +24,11 @@ pub fn build_remove_constraint(
     current_schema: &[TableDef],
     pending_constraints: &[TableConstraint],
 ) -> Result, QueryError> {
-    let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::SchemaError(format!("Table '{table}' not found in current schema. SQLite requires current schema information to remove constraints.")))?;
+    let table_def = require_table_in_schema(
+        current_schema,
+        table,
+        "SQLite requires current schema information to remove constraints",
+    )?;
 
     let new_constraints = constraints_without(table_def, constraint);
     let constraints_to_recreate = if matches!(constraint, TableConstraint::Unique { .. }) {

From 27b28b098122fec5b057e8617019120606f9f714 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 06:01:24 +0900
Subject: [PATCH 028/109] retry-now#0027: batch -- hoist is_pair,
 find_outer_mapping, unwrap_yaml_node to tree_util (3 kept)

---
 .../vespertide-lsp/src/completion/context.rs  | 51 ++++++-------------
 .../src/definition/foreign_key.rs             |  5 +-
 .../vespertide-lsp/src/diagnostics/locator.rs | 19 ++-----
 .../src/diagnostics/validation/types.rs       | 17 +------
 crates/vespertide-lsp/src/file_features.rs    | 13 +----
 crates/vespertide-lsp/src/hover/check_expr.rs |  5 +-
 crates/vespertide-lsp/src/hover/column.rs     |  5 +-
 .../vespertide-lsp/src/hover/foreign_key.rs   |  5 +-
 crates/vespertide-lsp/src/inlay_hints.rs      | 16 +-----
 crates/vespertide-lsp/src/symbols.rs          | 28 +---------
 crates/vespertide-lsp/src/tree_util.rs        | 46 +++++++++++++++++
 crates/vespertide-lsp/src/workspace_index.rs  | 15 +-----
 12 files changed, 74 insertions(+), 151 deletions(-)

diff --git a/crates/vespertide-lsp/src/completion/context.rs b/crates/vespertide-lsp/src/completion/context.rs
index 4cfa9d72..64542a01 100644
--- a/crates/vespertide-lsp/src/completion/context.rs
+++ b/crates/vespertide-lsp/src/completion/context.rs
@@ -6,6 +6,7 @@ use vespertide_planner::{CheckToken, CheckTokenKind, lex_check_expr};
 
 use crate::check_expr_range::expr_inner_range;
 use crate::text_util::strip_quotes;
+use crate::tree_util::is_pair;
 
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub(super) enum Context {
@@ -151,7 +152,7 @@ fn check_expr_context(
         return None;
     }
 
-    let expr_value = expr_pair.named_child(1).map(unwrap_flow_node)?;
+    let expr_value = expr_pair.named_child(1).map(unwrap_yaml_node)?;
     let inner = expr_inner_range(expr_value)?;
     if byte_offset < inner.start || byte_offset > inner.end {
         return None;
@@ -259,7 +260,7 @@ fn current_table_columns(node: tree_sitter::Node<'_>, source: &str) -> Vec) -> tree_sitter::Node<'_> {
         node = parent;
     }
 
-    node.named_child(0).map_or(node, unwrap_flow_node)
+    node.named_child(0).map_or(node, unwrap_yaml_node)
 }
 
 fn collect_column_names(columns_value: tree_sitter::Node<'_>, source: &str) -> Vec {
@@ -281,7 +282,7 @@ fn collect_column_names(columns_value: tree_sitter::Node<'_>, source: &str) -> V
     ) {
         let mut cursor = columns_value.walk();
         for raw_child in columns_value.children(&mut cursor) {
-            let child = unwrap_flow_node(raw_child);
+            let child = unwrap_yaml_node(raw_child);
             if let Some(column_object) = column_object_from_sequence_child(child)
                 && let Some(name) = string_value_for_key(column_object, source, "name")
                 && !name.is_empty()
@@ -301,7 +302,7 @@ fn column_object_from_sequence_child(
         "block_sequence_item" => {
             let mut cursor = child.walk();
             child.children(&mut cursor).find_map(|raw_inner| {
-                let inner = unwrap_flow_node(raw_inner);
+                let inner = unwrap_yaml_node(raw_inner);
                 matches!(inner.kind(), "object" | "block_mapping" | "flow_mapping").then_some(inner)
             })
         }
@@ -315,7 +316,7 @@ fn string_value_for_key<'source>(
     key: &str,
 ) -> Option<&'source str> {
     let pair = find_pair_with_key(object, source, key)?;
-    let value = pair.named_child(1).map(unwrap_flow_node)?;
+    let value = pair.named_child(1).map(unwrap_yaml_node)?;
     source.get(value.byte_range()).map(strip_quotes)
 }
 
@@ -338,7 +339,7 @@ fn analyze_sibling_type(
         .and_then(|column_object| find_pair_with_key(column_object, source, "type"))
         .and_then(|type_pair| type_pair.named_child(1))
     {
-        let effective = unwrap_flow_node(type_value);
+        let effective = unwrap_yaml_node(type_value);
         match effective.kind() {
             "string"
             | "double_quote_scalar"
@@ -351,7 +352,7 @@ fn analyze_sibling_type(
             "object" | "block_mapping" | "flow_mapping" => {
                 let kind = find_pair_with_key(effective, source, "kind")
                     .and_then(|pair| pair.named_child(1))
-                    .map(unwrap_flow_node)
+                    .map(unwrap_yaml_node)
                     .and_then(|node| source.get(node.byte_range()))
                     .map(|raw| strip_quotes(raw).to_string());
 
@@ -369,25 +370,7 @@ fn analyze_sibling_type(
     }
 }
 
-/// tree-sitter-yaml wraps scalars in `flow_node` (inline values) and
-/// multi-line mappings/sequences in `block_node`. Both are pure wrappers
-/// over their first named child — peel them so downstream `match`es see
-/// the real kind. We loop to handle the (rare) double-wrapping case.
-/// JSON's grammar has no such wrapper, so this is a no-op there.
-fn unwrap_flow_node(node: tree_sitter::Node<'_>) -> tree_sitter::Node<'_> {
-    // Fused while-let so the empty-wrapper case shares the same exit as the
-    // kind-mismatch case — no defensive `return current` branch dependent on
-    // tree-sitter-yaml producing empty wrappers.
-    let mut current = node;
-    while matches!(current.kind(), "flow_node" | "block_node")
-        && let Some(inner) = current
-            .named_child(0)
-            .filter(|inner| inner.id() != current.id())
-    {
-        current = inner;
-    }
-    current
-}
+use crate::tree_util::unwrap_yaml_node;
 
 /// Walk up to the smallest enclosing object that lives inside a `columns`
 /// array — i.e. the column object the cursor belongs to.
@@ -421,14 +404,14 @@ fn collect_enum_values(type_object: tree_sitter::Node<'_>, source: &str) -> Vec<
     if let Some(values_array_raw) = find_pair_with_key(type_object, source, "values")
         .and_then(|values_pair| values_pair.named_child(1))
     {
-        let values_array = unwrap_flow_node(values_array_raw);
+        let values_array = unwrap_yaml_node(values_array_raw);
         if matches!(
             values_array.kind(),
             "array" | "block_sequence" | "flow_sequence"
         ) {
             let mut cursor = values_array.walk();
             for raw_child in values_array.children(&mut cursor) {
-                let child = unwrap_flow_node(raw_child);
+                let child = unwrap_yaml_node(raw_child);
                 match child.kind() {
                     "string"
                     | "double_quote_scalar"
@@ -444,7 +427,7 @@ fn collect_enum_values(type_object: tree_sitter::Node<'_>, source: &str) -> Vec<
                         if let Some(name_pair) = find_pair_with_key(child, source, "name")
                             && let Some(name_value_raw) = name_pair.named_child(1)
                         {
-                            let name_value = unwrap_flow_node(name_value_raw);
+                            let name_value = unwrap_yaml_node(name_value_raw);
                             if let Some(raw) = source.get(name_value.byte_range()) {
                                 out.push(strip_quotes(raw).to_string());
                             }
@@ -456,7 +439,7 @@ fn collect_enum_values(type_object: tree_sitter::Node<'_>, source: &str) -> Vec<
                     "block_sequence_item" => {
                         let mut inner_cursor = child.walk();
                         for inner in child.children(&mut inner_cursor) {
-                            let inner = unwrap_flow_node(inner);
+                            let inner = unwrap_yaml_node(inner);
                             if let Some(raw) = source.get(inner.byte_range())
                                 && matches!(
                                     inner.kind(),
@@ -684,10 +667,6 @@ fn node_at_byte(tree: &tree_sitter::Tree, byte_offset: usize) -> tree_sitter::No
     root.descendant_for_byte_range(start, end).unwrap_or(root)
 }
 
-fn is_pair(node: tree_sitter::Node<'_>) -> bool {
-    matches!(node.kind(), "pair" | "block_mapping_pair")
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -994,7 +973,7 @@ mod tests {
         let src = "name: u\ncolumns:\n  - name: amount\n    type: integer\nconstraints:\n  - type: check\n    expr: amount > 0\n";
         let tree = parse(src, DocumentFormat::Yaml);
         let columns_pair = find_pair_recursive(tree.root_node(), src, "columns").unwrap();
-        let columns_value = unwrap_flow_node(columns_pair.named_child(1).unwrap());
+        let columns_value = unwrap_yaml_node(columns_pair.named_child(1).unwrap());
         let names = collect_column_names(columns_value, src);
 
         assert_eq!(names, vec!["amount".to_string()]);
diff --git a/crates/vespertide-lsp/src/definition/foreign_key.rs b/crates/vespertide-lsp/src/definition/foreign_key.rs
index 6f9eb41c..bc7c434c 100644
--- a/crates/vespertide-lsp/src/definition/foreign_key.rs
+++ b/crates/vespertide-lsp/src/definition/foreign_key.rs
@@ -15,6 +15,7 @@ use std::ops::Range;
 
 use crate::store::DocumentStore;
 use crate::text_util::strip_quotes;
+use crate::tree_util::is_pair;
 use crate::workspace_index::WorkspaceIndex;
 use crate::workspace_tables::WorkspaceTables;
 
@@ -319,10 +320,6 @@ fn is_mapping(node: tree_sitter::Node<'_>) -> bool {
     matches!(node.kind(), "object" | "block_mapping")
 }
 
-fn is_pair(node: tree_sitter::Node<'_>) -> bool {
-    matches!(node.kind(), "pair" | "block_mapping_pair")
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/vespertide-lsp/src/diagnostics/locator.rs b/crates/vespertide-lsp/src/diagnostics/locator.rs
index 0ec398c7..d41c8e40 100644
--- a/crates/vespertide-lsp/src/diagnostics/locator.rs
+++ b/crates/vespertide-lsp/src/diagnostics/locator.rs
@@ -4,6 +4,8 @@ use std::ops::Range;
 
 use vespertide_planner::PlannerError;
 
+use crate::tree_util::is_pair;
+
 /// Specific column field a diagnostic should attach to. The locator narrows
 /// the highlighted range from the whole column object down to this child
 /// pair so the squiggle lands on the *responsible* line.
@@ -320,18 +322,7 @@ pub(crate) fn locate_top_name(
     direct_name_value_range(mapping, source_bytes)
 }
 
-fn find_outer_mapping(node: tree_sitter::Node<'_>) -> Option> {
-    if matches!(node.kind(), "object" | "block_mapping" | "flow_mapping") {
-        return Some(node);
-    }
-    let mut cursor = node.walk();
-    for child in node.children(&mut cursor) {
-        if let Some(found) = find_outer_mapping(child) {
-            return Some(found);
-        }
-    }
-    None
-}
+use crate::tree_util::find_outer_mapping;
 
 fn direct_name_value_range(mapping: tree_sitter::Node<'_>, source: &[u8]) -> Option> {
     let mut cursor = mapping.walk();
@@ -411,10 +402,6 @@ fn is_mapping(node: tree_sitter::Node<'_>) -> bool {
     matches!(node.kind(), "object" | "block_mapping")
 }
 
-fn is_pair(node: tree_sitter::Node<'_>) -> bool {
-    matches!(node.kind(), "pair" | "block_mapping_pair")
-}
-
 fn pair_key_matches(node: tree_sitter::Node<'_>, source: &[u8], expected: &str) -> bool {
     // Fused chain so a key-less pair (no `named_child(0)`) folds into the
     // same `false` result without a separate defensive `return` line.
diff --git a/crates/vespertide-lsp/src/diagnostics/validation/types.rs b/crates/vespertide-lsp/src/diagnostics/validation/types.rs
index 25b23f47..0020f933 100644
--- a/crates/vespertide-lsp/src/diagnostics/validation/types.rs
+++ b/crates/vespertide-lsp/src/diagnostics/validation/types.rs
@@ -34,22 +34,7 @@ pub(super) struct EnumValueDescriptor {
     pub(super) integer_value_range: std::ops::Range,
 }
 
-/// Peel YAML's `flow_node` / `block_node` wrappers (no-op on JSON).
-pub(super) fn unwrap_yaml_node(node: tree_sitter::Node<'_>) -> tree_sitter::Node<'_> {
-    // Fused while-let so the empty-wrapper case (no usable `named_child(0)`)
-    // and the kind-mismatch case share the same loop exit — no defensive
-    // `return` line that only an (unobservable) empty tree-sitter wrapper
-    // could reach.
-    let mut current = node;
-    while matches!(current.kind(), "flow_node" | "block_node")
-        && let Some(inner) = current
-            .named_child(0)
-            .filter(|inner| inner.id() != current.id())
-    {
-        current = inner;
-    }
-    current
-}
+pub(super) use crate::tree_util::unwrap_yaml_node;
 
 pub(super) fn collect_enum_value_descriptors(
     array: tree_sitter::Node<'_>,
diff --git a/crates/vespertide-lsp/src/file_features.rs b/crates/vespertide-lsp/src/file_features.rs
index d4d4d3ef..8d09c985 100644
--- a/crates/vespertide-lsp/src/file_features.rs
+++ b/crates/vespertide-lsp/src/file_features.rs
@@ -250,18 +250,7 @@ pub fn compute_selection_ranges(
 // Shared helpers
 // =====================================================================
 
-fn find_outer_mapping(node: tree_sitter::Node<'_>) -> Option> {
-    if matches!(node.kind(), "object" | "block_mapping" | "flow_mapping") {
-        return Some(node);
-    }
-    let mut cursor = node.walk();
-    for child in node.children(&mut cursor) {
-        if let Some(found) = find_outer_mapping(child) {
-            return Some(found);
-        }
-    }
-    None
-}
+use crate::tree_util::find_outer_mapping;
 
 fn find_pair_with_key<'tree>(
     mapping: tree_sitter::Node<'tree>,
diff --git a/crates/vespertide-lsp/src/hover/check_expr.rs b/crates/vespertide-lsp/src/hover/check_expr.rs
index 78343ff4..1ccf2f6f 100644
--- a/crates/vespertide-lsp/src/hover/check_expr.rs
+++ b/crates/vespertide-lsp/src/hover/check_expr.rs
@@ -11,6 +11,7 @@ use vespertide_planner::{CheckExprAst, CheckExprLiteral, CheckExprOp, parse_chec
 
 use super::DomainHover;
 use crate::check_expr_range::expr_inner_range;
+use crate::tree_util::is_pair;
 
 pub(super) fn try_hover(
     node: tree_sitter::Node<'_>,
@@ -249,10 +250,6 @@ fn is_inside_constraints(node: tree_sitter::Node<'_>, source: &str) -> bool {
     false
 }
 
-fn is_pair(node: tree_sitter::Node<'_>) -> bool {
-    matches!(node.kind(), "pair" | "block_mapping_pair")
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/vespertide-lsp/src/hover/column.rs b/crates/vespertide-lsp/src/hover/column.rs
index 9fabfea6..f32d6715 100644
--- a/crates/vespertide-lsp/src/hover/column.rs
+++ b/crates/vespertide-lsp/src/hover/column.rs
@@ -1,6 +1,7 @@
 //! Column hover: markdown showing name, type, nullable, default, constraints.
 
 use crate::text_util::strip_quotes;
+use crate::tree_util::is_pair;
 use std::fmt::Write as _;
 use std::ops::Range;
 
@@ -94,10 +95,6 @@ fn is_mapping(node: tree_sitter::Node<'_>) -> bool {
     matches!(node.kind(), "object" | "block_mapping")
 }
 
-fn is_pair(node: tree_sitter::Node<'_>) -> bool {
-    matches!(node.kind(), "pair" | "block_mapping_pair")
-}
-
 fn constraint_is_enabled(value: &str) -> bool {
     !matches!(value.trim(), "false" | "null" | "[]" | "{}")
 }
diff --git a/crates/vespertide-lsp/src/hover/foreign_key.rs b/crates/vespertide-lsp/src/hover/foreign_key.rs
index a0f45194..9c6e670c 100644
--- a/crates/vespertide-lsp/src/hover/foreign_key.rs
+++ b/crates/vespertide-lsp/src/hover/foreign_key.rs
@@ -2,6 +2,7 @@
 
 use crate::store::DocumentStore;
 use crate::text_util::strip_quotes;
+use crate::tree_util::is_pair;
 use crate::workspace_index::WorkspaceIndex;
 use crate::workspace_tables::WorkspaceTables;
 
@@ -88,10 +89,6 @@ fn column_summary(table: &vespertide_core::TableDef) -> String {
     format!("columns: {columns}")
 }
 
-fn is_pair(node: tree_sitter::Node<'_>) -> bool {
-    matches!(node.kind(), "pair" | "block_mapping_pair")
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/vespertide-lsp/src/inlay_hints.rs b/crates/vespertide-lsp/src/inlay_hints.rs
index 30f88520..c557e0ef 100644
--- a/crates/vespertide-lsp/src/inlay_hints.rs
+++ b/crates/vespertide-lsp/src/inlay_hints.rs
@@ -371,21 +371,7 @@ fn find_pair_with_key<'tree>(
     })
 }
 
-fn unwrap_yaml_node(node: tree_sitter::Node<'_>) -> tree_sitter::Node<'_> {
-    // Fused while-let so the empty-wrapper case (no usable `named_child(0)`)
-    // and the kind-mismatch case share the same loop exit — no defensive
-    // `return` line that only an (unobservable) empty tree-sitter wrapper
-    // could reach.
-    let mut current = node;
-    while matches!(current.kind(), "flow_node" | "block_node")
-        && let Some(inner) = current
-            .named_child(0)
-            .filter(|inner| inner.id() != current.id())
-    {
-        current = inner;
-    }
-    current
-}
+use crate::tree_util::unwrap_yaml_node;
 
 fn ranges_overlap(a: &Range, b: &Range) -> bool {
     a.start < b.end && b.start < a.end
diff --git a/crates/vespertide-lsp/src/symbols.rs b/crates/vespertide-lsp/src/symbols.rs
index 7a3224f7..521687ae 100644
--- a/crates/vespertide-lsp/src/symbols.rs
+++ b/crates/vespertide-lsp/src/symbols.rs
@@ -322,33 +322,9 @@ fn find_pair_with_key<'tree>(
     })
 }
 
-fn find_outer_mapping(node: tree_sitter::Node<'_>) -> Option> {
-    if matches!(node.kind(), "object" | "block_mapping" | "flow_mapping") {
-        return Some(node);
-    }
-    let mut cursor = node.walk();
-    for child in node.children(&mut cursor) {
-        if let Some(found) = find_outer_mapping(child) {
-            return Some(found);
-        }
-    }
-    None
-}
+use crate::tree_util::find_outer_mapping;
 
-fn unwrap_yaml_node(node: tree_sitter::Node<'_>) -> tree_sitter::Node<'_> {
-    // Fused while-let so the empty-wrapper case (no usable `named_child(0)`)
-    // and the kind-mismatch case share the same exit — no defensive `return`
-    // line that depends on a tree-sitter-yaml release producing empty wrappers.
-    let mut current = node;
-    while matches!(current.kind(), "flow_node" | "block_node")
-        && let Some(inner) = current
-            .named_child(0)
-            .filter(|inner| inner.id() != current.id())
-    {
-        current = inner;
-    }
-    current
-}
+use crate::tree_util::unwrap_yaml_node;
 
 fn trim_one_byte(range: &Range) -> Range {
     if range.end.saturating_sub(range.start) >= 2 {
diff --git a/crates/vespertide-lsp/src/tree_util.rs b/crates/vespertide-lsp/src/tree_util.rs
index 2e868a7c..479dd665 100644
--- a/crates/vespertide-lsp/src/tree_util.rs
+++ b/crates/vespertide-lsp/src/tree_util.rs
@@ -35,3 +35,49 @@ pub(crate) fn node_at_byte(
         return Some(current);
     }
 }
+
+/// Is `node` a YAML/JSON key-value pair? `pair` is the JSON grammar's name,
+/// `block_mapping_pair` is the YAML grammar's. Hoisted here so every LSP
+/// feature module can call one canonical helper instead of carrying its own
+/// byte-identical copy (was duplicated in 6 sibling modules pre-0.2.0).
+pub(crate) fn is_pair(node: tree_sitter::Node<'_>) -> bool {
+    matches!(node.kind(), "pair" | "block_mapping_pair")
+}
+
+/// Walk the tree depth-first and return the first node whose kind is a
+/// top-level mapping (`object`/`block_mapping`/`flow_mapping`). Every LSP
+/// feature that wants the document's outermost table object needs this
+/// primitive — previously duplicated in 4 sibling modules pre-0.2.0.
+pub(crate) fn find_outer_mapping(node: tree_sitter::Node<'_>) -> Option> {
+    if matches!(node.kind(), "object" | "block_mapping" | "flow_mapping") {
+        return Some(node);
+    }
+    let mut cursor = node.walk();
+    for child in node.children(&mut cursor) {
+        if let Some(found) = find_outer_mapping(child) {
+            return Some(found);
+        }
+    }
+    None
+}
+
+/// Peel tree-sitter-yaml's `flow_node` / `block_node` wrappers so downstream
+/// `match`es see the real kind (no-op on JSON, whose grammar has no such
+/// wrappers). Fused while-let so the empty-wrapper case (no usable
+/// `named_child(0)`) and the kind-mismatch case share the same loop exit —
+/// no defensive `return` line that depends on a tree-sitter-yaml release
+/// producing empty wrappers. Hoisted here so every LSP feature module can
+/// call one canonical helper instead of carrying its own byte-identical
+/// copy (was duplicated in 4 sibling modules pre-0.2.0, including one
+/// `unwrap_flow_node` rename in `completion::context`).
+pub(crate) fn unwrap_yaml_node(node: tree_sitter::Node<'_>) -> tree_sitter::Node<'_> {
+    let mut current = node;
+    while matches!(current.kind(), "flow_node" | "block_node")
+        && let Some(inner) = current
+            .named_child(0)
+            .filter(|inner| inner.id() != current.id())
+    {
+        current = inner;
+    }
+    current
+}
diff --git a/crates/vespertide-lsp/src/workspace_index.rs b/crates/vespertide-lsp/src/workspace_index.rs
index 57002a92..037a7733 100644
--- a/crates/vespertide-lsp/src/workspace_index.rs
+++ b/crates/vespertide-lsp/src/workspace_index.rs
@@ -144,23 +144,10 @@ impl WorkspaceIndex {
 /// `columns` before `name` — that polluted the workspace index with column
 /// names like `id` and `media_id` as if they were tables.
 fn extract_top_level_name(source: &str, tree: &Tree) -> Option {
-    let mapping = find_outer_mapping(tree.root_node())?;
+    let mapping = crate::tree_util::find_outer_mapping(tree.root_node())?;
     find_direct_name_value(mapping, source.as_bytes())
 }
 
-fn find_outer_mapping(node: Node<'_>) -> Option> {
-    if matches!(node.kind(), "object" | "block_mapping" | "flow_mapping") {
-        return Some(node);
-    }
-    let mut cursor = node.walk();
-    for child in node.children(&mut cursor) {
-        if let Some(found) = find_outer_mapping(child) {
-            return Some(found);
-        }
-    }
-    None
-}
-
 fn find_direct_name_value(mapping: Node<'_>, source: &[u8]) -> Option {
     let mut cursor = mapping.walk();
     for child in mapping.children(&mut cursor) {

From 610f206af443c0af19e14df0bd19574a4aa539e5 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 06:31:17 +0900
Subject: [PATCH 029/109] retry-now#0028: batch ? backend_tag (20 sites),
 test_support::uri (9 sites), joined_sql (11 sites) (3 kept)

---
 crates/vespertide-lsp/src/definition/mod.rs   | 10 +--
 crates/vespertide-lsp/src/drift/cache.rs      | 11 ++-
 crates/vespertide-lsp/src/drift/mod.rs        |  6 +-
 crates/vespertide-lsp/src/hover/mod.rs        | 11 ++-
 crates/vespertide-query/src/sql/add_column.rs | 27 ++-----
 .../src/sql/modify_column_comment.rs          | 72 +++----------------
 .../src/sql/modify_column_default.rs          | 62 +++-------------
 .../src/sql/modify_column_nullable.rs         | 44 ++----------
 .../src/sql/tests/dispatch.rs                 | 33 ++-------
 9 files changed, 53 insertions(+), 223 deletions(-)

diff --git a/crates/vespertide-lsp/src/definition/mod.rs b/crates/vespertide-lsp/src/definition/mod.rs
index a967e17f..2d24d057 100644
--- a/crates/vespertide-lsp/src/definition/mod.rs
+++ b/crates/vespertide-lsp/src/definition/mod.rs
@@ -55,8 +55,8 @@ mod tests {
     use super::*;
     use crate::parser::{DocumentFormat, ParserPool};
     use crate::store::DocumentStore;
+    use crate::test_support::uri;
     use crate::workspace_index::WorkspaceIndex;
-    use std::str::FromStr;
 
     #[test]
     fn cross_file_go_to_def() {
@@ -64,7 +64,7 @@ mod tests {
         let idx = WorkspaceIndex::new();
         let docs = DocumentStore::new();
 
-        let user_uri = Uri::from_str("file:///user.json").unwrap();
+        let user_uri = uri("user.json");
         let user_src = r#"{"name":"user","columns":[{"name":"id","type":"integer","nullable":false,"primary_key":true}]}"#;
         let user_tree = pool.parse(user_src, DocumentFormat::Json).unwrap();
         idx.upsert(&user_uri, user_src, &user_tree);
@@ -109,7 +109,7 @@ mod tests {
         let idx = WorkspaceIndex::new();
         let docs = DocumentStore::new();
 
-        let user_uri = Uri::from_str("file:///user.json").unwrap();
+        let user_uri = uri("user.json");
         let user_src = r#"{"name":"user","columns":[{"name":"id","type":"integer","nullable":false,"primary_key":true},{"name":"email","type":"text","nullable":false}]}"#;
         let user_tree = pool.parse(user_src, DocumentFormat::Json).unwrap();
         idx.upsert(&user_uri, user_src, &user_tree);
@@ -149,7 +149,7 @@ mod tests {
         let idx = WorkspaceIndex::new();
         let docs = DocumentStore::new();
 
-        let user_uri = Uri::from_str("file:///user.yaml").unwrap();
+        let user_uri = uri("user.yaml");
         let user_src =
             "name: user\ncolumns:\n  - name: id\n    type: integer\n    primary_key: true\n";
         let user_tree = pool.parse(user_src, DocumentFormat::Yaml).unwrap();
@@ -188,7 +188,7 @@ mod tests {
         let idx = WorkspaceIndex::new();
         let docs = DocumentStore::new();
 
-        let user_uri = Uri::from_str("file:///user.yaml").unwrap();
+        let user_uri = uri("user.yaml");
         let user_src = "name: user\ncolumns:\n  - name: id\n    type: integer\n    primary_key: true\n  - name: email\n    type: text\n";
         let user_tree = pool.parse(user_src, DocumentFormat::Yaml).unwrap();
         idx.upsert(&user_uri, user_src, &user_tree);
diff --git a/crates/vespertide-lsp/src/drift/cache.rs b/crates/vespertide-lsp/src/drift/cache.rs
index 5cb5d56d..04a96415 100644
--- a/crates/vespertide-lsp/src/drift/cache.rs
+++ b/crates/vespertide-lsp/src/drift/cache.rs
@@ -210,14 +210,13 @@ pub(super) fn max_mtime_in_dir(dir: &Path) -> SystemTime {
 mod tests {
     use super::super::DriftKind;
     use super::*;
+    use crate::test_support::uri;
     use std::fs;
-    use std::str::FromStr;
     use tempfile::tempdir;
-    use tower_lsp_server::ls_types::Uri;
 
     fn dummy_drift() -> DomainDrift {
         DomainDrift {
-            uri: Uri::from_str("file:///p.json").unwrap(),
+            uri: uri("p.json"),
             kind: DriftKind::RawSql,
             byte_range: None,
             message: "dummy".to_string(),
@@ -377,11 +376,11 @@ mod tests {
     #[test]
     fn docstore_fingerprint_changes_on_text_change() {
         let docs = DocumentStore::new();
-        let uri = Uri::from_str("file:///a.json").unwrap();
-        docs.open(uri.clone(), "json".to_string(), 1, "{}".to_string());
+        let doc_uri = uri("a.json");
+        docs.open(doc_uri.clone(), "json".to_string(), 1, "{}".to_string());
         let fp1 = docstore_fingerprint(&docs);
 
-        docs.update_full(&uri, r#"{"name":"x"}"#.to_string(), 2);
+        docs.update_full(&doc_uri, r#"{"name":"x"}"#.to_string(), 2);
         let fp2 = docstore_fingerprint(&docs);
 
         assert_ne!(fp1, fp2);
diff --git a/crates/vespertide-lsp/src/drift/mod.rs b/crates/vespertide-lsp/src/drift/mod.rs
index 8469881a..c70f89cc 100644
--- a/crates/vespertide-lsp/src/drift/mod.rs
+++ b/crates/vespertide-lsp/src/drift/mod.rs
@@ -22,8 +22,6 @@ const _: fn(&vespertide_planner::PlannerError) -> Option ColumnDef {
@@ -78,7 +76,7 @@ mod tests {
     }
 
     fn uri() -> Uri {
-        Uri::from_str("file:///user.json").unwrap()
+        ts_uri("user.json")
     }
 
     #[test]
diff --git a/crates/vespertide-lsp/src/hover/mod.rs b/crates/vespertide-lsp/src/hover/mod.rs
index cf4caecc..0fc0ad7a 100644
--- a/crates/vespertide-lsp/src/hover/mod.rs
+++ b/crates/vespertide-lsp/src/hover/mod.rs
@@ -89,9 +89,9 @@ mod tests {
     #[test]
     fn hover_on_ref_table_falls_back_to_disk_when_target_is_closed() {
         use std::fs;
-        use std::str::FromStr;
         use tempfile::tempdir;
-        use tower_lsp_server::ls_types::Uri;
+
+        use crate::test_support::uri;
 
         let tmp = tempdir().unwrap();
         let models_dir = tmp.path().join("models");
@@ -107,7 +107,7 @@ mod tests {
         let docs = DocumentStore::new();
         // article.vespertide.json references media via FK. Media is on disk, NOT open.
         let article_src = r#"{"name":"article","columns":[{"name":"media_id","type":"uuid","foreign_key":{"ref_table":"media","ref_columns":["id"]}}]}"#;
-        let article_uri = Uri::from_str("file:///article.vespertide.json").unwrap();
+        let article_uri = uri("article.vespertide.json");
         let article_tree = pool.parse(article_src, DocumentFormat::Json).unwrap();
         idx.upsert(&article_uri, article_src, &article_tree);
 
@@ -142,14 +142,13 @@ mod tests {
 
     #[test]
     fn yaml_hover_on_ref_table_previews_target_columns() {
-        use std::str::FromStr;
-        use tower_lsp_server::ls_types::Uri;
+        use crate::test_support::uri;
 
         let pool = ParserPool::new();
         let idx = WorkspaceIndex::new();
         let docs = DocumentStore::new();
 
-        let user_uri = Uri::from_str("file:///user.yaml").unwrap();
+        let user_uri = uri("user.yaml");
         let user_src = "name: user\ncolumns:\n  - name: id\n    type: integer\n    nullable: false\n    primary_key: true\n  - name: email\n    type: text\n    nullable: false\n";
         let user_tree = pool.parse(user_src, DocumentFormat::Yaml).unwrap();
         idx.upsert(&user_uri, user_src, &user_tree);
diff --git a/crates/vespertide-query/src/sql/add_column.rs b/crates/vespertide-query/src/sql/add_column.rs
index f6ba5419..96086961 100644
--- a/crates/vespertide-query/src/sql/add_column.rs
+++ b/crates/vespertide-query/src/sql/add_column.rs
@@ -158,6 +158,7 @@ pub fn build_add_column(
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::test_support::joined_sql;
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{ColumnType, SimpleColumnType, TableDef};
@@ -267,7 +268,7 @@ mod tests {
         }
 
         with_settings!({ snapshot_suffix => format!("add_column_{}", title) }, {
-            assert_snapshot!(result.iter().map(|q| q.build(backend)).collect::>().join("\n"));
+            assert_snapshot!(joined_sql(backend, &result));
         });
     }
 
@@ -337,11 +338,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(DatabaseBackend::Sqlite, &queries);
         // Should use default value (18) for fill
         assert!(sql.contains("18"));
     }
@@ -385,11 +382,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(DatabaseBackend::Sqlite, &queries);
         // Should use NULL for fill
         assert!(sql.contains("NULL"));
     }
@@ -438,11 +431,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(DatabaseBackend::Sqlite, &queries);
         // Should recreate index
         assert!(sql.contains("CREATE INDEX"));
         assert!(sql.contains("idx_id"));
@@ -649,11 +638,7 @@ mod tests {
         }];
         let result =
             build_add_column(backend, "project", &column, None, ¤t_schema, &[]).unwrap();
-        let sql = result
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &result);
 
         // SQLite must NOT contain ::json syntax
         if backend == DatabaseBackend::Sqlite {
diff --git a/crates/vespertide-query/src/sql/modify_column_comment.rs b/crates/vespertide-query/src/sql/modify_column_comment.rs
index 1d251301..ccd4c910 100644
--- a/crates/vespertide-query/src/sql/modify_column_comment.rs
+++ b/crates/vespertide-query/src/sql/modify_column_comment.rs
@@ -89,7 +89,7 @@ pub fn build_modify_column_comment(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::{col_n as col, table_def};
+    use crate::test_support::{backend_tag, col_n as col, table_def};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{ColumnType, SimpleColumnType};
@@ -125,11 +125,7 @@ mod tests {
 
         let suffix = format!(
             "{}_{}_users",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            },
+            backend_tag(backend),
             if new_comment.is_some() {
                 "set_comment"
             } else {
@@ -181,14 +177,7 @@ mod tests {
             );
         }
 
-        let suffix = format!(
-            "{}_comment_with_quotes",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_comment_with_quotes", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
@@ -264,14 +253,7 @@ mod tests {
             .collect::>()
             .join("\n");
 
-        let suffix = format!(
-            "{}_long_comment",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_long_comment", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
@@ -316,14 +298,7 @@ mod tests {
             assert!(sql.contains("DEFAULT"), "Should preserve DEFAULT clause");
         }
 
-        let suffix = format!(
-            "{}_preserves_properties",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_preserves_properties", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
@@ -358,14 +333,7 @@ mod tests {
             .collect::>()
             .join("\n");
 
-        let suffix = format!(
-            "{}_change_comment",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_change_comment", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
@@ -402,14 +370,7 @@ mod tests {
             .collect::>()
             .join("\n");
 
-        let suffix = format!(
-            "{}_drop_existing_comment",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_drop_existing_comment", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
@@ -484,15 +445,7 @@ mod tests {
             .join("\n");
 
         let type_name = format!("{column_type:?}").to_lowercase();
-        let suffix = format!(
-            "{}_{}_comment",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            },
-            type_name
-        );
+        let suffix = format!("{}_{}_comment", backend_tag(backend), type_name);
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
@@ -623,14 +576,7 @@ mod tests {
             .collect::>()
             .join("\n");
 
-        let suffix = format!(
-            "{}_not_null_column",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_not_null_column", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
diff --git a/crates/vespertide-query/src/sql/modify_column_default.rs b/crates/vespertide-query/src/sql/modify_column_default.rs
index a8f83829..5953c567 100644
--- a/crates/vespertide-query/src/sql/modify_column_default.rs
+++ b/crates/vespertide-query/src/sql/modify_column_default.rs
@@ -158,7 +158,7 @@ pub fn build_modify_column_default(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::{col_n as col, table_def};
+    use crate::test_support::{backend_tag, col_n as col, table_def};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{ColumnType, SimpleColumnType, TableConstraint};
@@ -195,11 +195,7 @@ mod tests {
 
         let suffix = format!(
             "{}_{}_users",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            },
+            backend_tag(backend),
             if new_default.is_some() {
                 "set_default"
             } else {
@@ -351,14 +347,7 @@ mod tests {
             assert!(sql.contains("idx_users_email"));
         }
 
-        let suffix = format!(
-            "{}_with_index",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_with_index", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
@@ -400,14 +389,7 @@ mod tests {
             .collect::>()
             .join("\n");
 
-        let suffix = format!(
-            "{}_change_default",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_change_default", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
@@ -450,14 +432,7 @@ mod tests {
             .collect::>()
             .join("\n");
 
-        let suffix = format!(
-            "{}_integer_default",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_integer_default", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
@@ -500,14 +475,7 @@ mod tests {
             .collect::>()
             .join("\n");
 
-        let suffix = format!(
-            "{}_boolean_default",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_boolean_default", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
@@ -555,14 +523,7 @@ mod tests {
             .collect::>()
             .join("\n");
 
-        let suffix = format!(
-            "{}_function_default",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_function_default", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
@@ -604,14 +565,7 @@ mod tests {
             .collect::>()
             .join("\n");
 
-        let suffix = format!(
-            "{}_drop_existing_default",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_drop_existing_default", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
diff --git a/crates/vespertide-query/src/sql/modify_column_nullable.rs b/crates/vespertide-query/src/sql/modify_column_nullable.rs
index 3d88f4f3..076e9da4 100644
--- a/crates/vespertide-query/src/sql/modify_column_nullable.rs
+++ b/crates/vespertide-query/src/sql/modify_column_nullable.rs
@@ -141,7 +141,7 @@ pub fn build_modify_column_nullable(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::{col_n as col, table_def};
+    use crate::test_support::{backend_tag, col_n as col, table_def};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{ColumnType, SimpleColumnType, TableConstraint};
@@ -194,11 +194,7 @@ mod tests {
 
         let suffix = format!(
             "{}_{}_users{}",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            },
+            backend_tag(backend),
             if nullable { "nullable" } else { "not_null" },
             if fill_with.is_some() {
                 "_with_fill"
@@ -309,14 +305,7 @@ mod tests {
             assert!(sql.contains("idx_email"));
         }
 
-        let suffix = format!(
-            "{}_with_index",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_with_index", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
@@ -370,14 +359,7 @@ mod tests {
             "SQL should contain CURRENT_TIMESTAMP, got: {sql}"
         );
 
-        let suffix = format!(
-            "{}_fill_now",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_fill_now", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
@@ -425,14 +407,7 @@ mod tests {
             assert!(sql.contains("DEFAULT"));
         }
 
-        let suffix = format!(
-            "{}_with_default",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_with_default", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
@@ -489,14 +464,7 @@ mod tests {
             "Should NOT contain UPDATE, got: {sql}"
         );
 
-        let suffix = format!(
-            "{}_delete_null_rows",
-            match backend {
-                DatabaseBackend::Postgres => "postgres",
-                DatabaseBackend::MySql => "mysql",
-                DatabaseBackend::Sqlite => "sqlite",
-            }
-        );
+        let suffix = format!("{}_delete_null_rows", backend_tag(backend));
 
         with_settings!({ snapshot_suffix => suffix }, {
             assert_snapshot!(sql);
diff --git a/crates/vespertide-query/src/sql/tests/dispatch.rs b/crates/vespertide-query/src/sql/tests/dispatch.rs
index b4b388de..8efaf509 100644
--- a/crates/vespertide-query/src/sql/tests/dispatch.rs
+++ b/crates/vespertide-query/src/sql/tests/dispatch.rs
@@ -1,4 +1,5 @@
 use super::*;
+use crate::test_support::joined_sql;
 use vespertide_core::TableDef;
 
 #[test]
@@ -223,7 +224,7 @@ fn test_build_migration_action(
     }
 
     with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("build_migration_action_{}", title) }, {
-        assert_snapshot!(result.iter().map(|q| q.build(backend)).collect::>().join("\n"));
+        assert_snapshot!(joined_sql(backend, &result));
     });
 }
 
@@ -303,11 +304,7 @@ fn test_build_action_queries_modify_column_type(#[case] backend: DatabaseBackend
     }];
     let result = build_action_queries(backend, &action, ¤t_schema).unwrap();
     assert!(!result.is_empty());
-    let sql = result
-        .iter()
-        .map(|q| q.build(backend))
-        .collect::>()
-        .join("\n");
+    let sql = joined_sql(backend, &result);
     assert!(sql.contains("ALTER TABLE"));
 
     with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("modify_column_type_{:?}", backend) }, {
@@ -408,11 +405,7 @@ fn test_build_action_queries_add_constraint(#[case] backend: DatabaseBackend) {
     }];
     let result = build_action_queries(backend, &action, ¤t_schema).unwrap();
     assert!(!result.is_empty());
-    let sql = result
-        .iter()
-        .map(|q| q.build(backend))
-        .collect::>()
-        .join("\n");
+    let sql = joined_sql(backend, &result);
     assert!(sql.contains("UNIQUE") || sql.contains("uq_email"));
 
     with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("add_constraint_{:?}", backend) }, {
@@ -473,11 +466,7 @@ fn test_build_action_queries_remove_constraint(#[case] backend: DatabaseBackend)
     }];
     let result = build_action_queries(backend, &action, ¤t_schema).unwrap();
     assert!(!result.is_empty());
-    let sql = result
-        .iter()
-        .map(|q| q.build(backend))
-        .collect::>()
-        .join("\n");
+    let sql = joined_sql(backend, &result);
     assert!(sql.contains("DROP") || sql.contains("CONSTRAINT"));
 
     with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("remove_constraint_{:?}", backend) }, {
@@ -524,11 +513,7 @@ fn test_build_action_queries_add_column(#[case] backend: DatabaseBackend) {
     }];
     let result = build_action_queries(backend, &action, ¤t_schema).unwrap();
     assert!(!result.is_empty());
-    let sql = result
-        .iter()
-        .map(|q| q.build(backend))
-        .collect::>()
-        .join("\n");
+    let sql = joined_sql(backend, &result);
     assert!(sql.contains("ALTER TABLE"));
     assert!(sql.contains("ADD COLUMN") || sql.contains("ADD"));
 
@@ -829,11 +814,7 @@ fn build_action_queries_modify_column_default_dispatch(#[case] backend: Database
         backfill: None,
     };
     let queries = build_action_queries(backend, &action, &schema).unwrap();
-    let sql = queries
-        .iter()
-        .map(|q| q.build(backend))
-        .collect::>()
-        .join("\n");
+    let sql = joined_sql(backend, &queries);
     assert!(sql.contains("'pending'") || sql.contains("pending"));
 }
 

From 05819c3a87cf4cdf7fa63bb05de7326bb1e81c08 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 06:51:27 +0900
Subject: [PATCH 030/109] retry-now#0029: batch - hoist 3
 require_table_in_schema lookups in vespertide-query SQL builders (3 kept)

Migrates the remaining open-coded current_schema.iter().find().ok_or_else() chains in:

- modify_column_default.rs MySQL+SQLite branches

- modify_column_comment.rs MySQL branch

- replace_constraint.rs SQLite branch

Each site now routes through the canonical pub(crate) helper require_table_in_schema(schema, table, context) in sql/helpers.rs. Wire-error format unchanged for replace_constraint (byte-identical) and strictly improved for modify_column_{default,comment} (adds trailing 'MySQL/SQLite requires current schema information to ...' context sentence already used by every sibling builder). Existing err_msg.contains('Table ... not found') asserts still pass.
---
 .../src/sql/modify_column_comment.rs          | 13 +++++-----
 .../src/sql/modify_column_default.rs          | 24 +++++++++----------
 .../src/sql/replace_constraint.rs             | 16 +++++--------
 3 files changed, 23 insertions(+), 30 deletions(-)

diff --git a/crates/vespertide-query/src/sql/modify_column_comment.rs b/crates/vespertide-query/src/sql/modify_column_comment.rs
index ccd4c910..36fb9ffd 100644
--- a/crates/vespertide-query/src/sql/modify_column_comment.rs
+++ b/crates/vespertide-query/src/sql/modify_column_comment.rs
@@ -2,7 +2,7 @@ use sea_query::Alias;
 
 use vespertide_core::TableDef;
 
-use super::helpers::{build_sea_column_def_with_table, quote_ident};
+use super::helpers::{build_sea_column_def_with_table, quote_ident, require_table_in_schema};
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
 use crate::error::QueryError;
 
@@ -32,12 +32,11 @@ pub fn build_modify_column_comment(
         }
         DatabaseBackend::MySql => {
             // MySQL requires the full column definition in MODIFY COLUMN to change comment
-            let table_def = current_schema
-                .iter()
-                .find(|t| t.name == table)
-                .ok_or_else(|| {
-                    QueryError::SchemaError(format!("Table '{table}' not found in current schema."))
-                })?;
+            let table_def = require_table_in_schema(
+                current_schema,
+                table,
+                "MySQL requires current schema information to modify column comments",
+            )?;
 
             let column_def = table_def
                 .columns
diff --git a/crates/vespertide-query/src/sql/modify_column_default.rs b/crates/vespertide-query/src/sql/modify_column_default.rs
index 5953c567..7383c102 100644
--- a/crates/vespertide-query/src/sql/modify_column_default.rs
+++ b/crates/vespertide-query/src/sql/modify_column_default.rs
@@ -4,7 +4,7 @@ use vespertide_core::{ColumnDef, TableDef};
 
 use super::helpers::{
     build_copy_into_temp_table, build_sea_column_def_with_table, build_sqlite_temp_table_create,
-    normalize_enum_default, quote_ident, recreate_indexes_after_rebuild,
+    normalize_enum_default, quote_ident, recreate_indexes_after_rebuild, require_table_in_schema,
 };
 use super::rename_table::build_rename_table;
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
@@ -58,12 +58,11 @@ pub fn build_modify_column_default(
         }
         DatabaseBackend::MySql => {
             // MySQL requires the full column definition in ALTER COLUMN
-            let table_def = current_schema
-                .iter()
-                .find(|t| t.name == table)
-                .ok_or_else(|| {
-                    QueryError::SchemaError(format!("Table '{table}' not found in current schema."))
-                })?;
+            let table_def = require_table_in_schema(
+                current_schema,
+                table,
+                "MySQL requires current schema information to modify column defaults",
+            )?;
 
             let column_def = table_def
                 .columns
@@ -92,12 +91,11 @@ pub fn build_modify_column_default(
         DatabaseBackend::Sqlite => {
             // SQLite doesn't support ALTER COLUMN for default changes
             // Use temporary table approach
-            let table_def = current_schema
-                .iter()
-                .find(|t| t.name == table)
-                .ok_or_else(|| {
-                    QueryError::SchemaError(format!("Table '{table}' not found in current schema."))
-                })?;
+            let table_def = require_table_in_schema(
+                current_schema,
+                table,
+                "SQLite requires current schema information to modify column defaults",
+            )?;
 
             // Create modified columns with the new default
             let mut new_columns = table_def.columns.clone();
diff --git a/crates/vespertide-query/src/sql/replace_constraint.rs b/crates/vespertide-query/src/sql/replace_constraint.rs
index 1d22dc17..c6d84fec 100644
--- a/crates/vespertide-query/src/sql/replace_constraint.rs
+++ b/crates/vespertide-query/src/sql/replace_constraint.rs
@@ -4,7 +4,7 @@ use vespertide_core::{TableConstraint, TableDef};
 
 use super::helpers::{
     build_copy_into_temp_table, build_sqlite_temp_table_create, recreate_indexes_after_rebuild,
-    to_sea_fk_action,
+    require_table_in_schema, to_sea_fk_action,
 };
 use super::rename_table::build_rename_table;
 use super::types::{BuiltQuery, DatabaseBackend};
@@ -188,15 +188,11 @@ fn build_sqlite_constraint_replace(
     current_schema: &[TableDef],
     pending_constraints: &[TableConstraint],
 ) -> Result, QueryError> {
-    let table_def = current_schema
-        .iter()
-        .find(|t| t.name == table)
-        .ok_or_else(|| {
-            QueryError::SchemaError(format!(
-                "Table '{table}' not found in current schema. SQLite requires current schema \
-                 information to replace constraints."
-            ))
-        })?;
+    let table_def = require_table_in_schema(
+        current_schema,
+        table,
+        "SQLite requires current schema information to replace constraints",
+    )?;
 
     // Build new constraints: replace old constraint with new one
     let new_constraints: Vec = table_def

From cb52dc4d8268b5aced739896a198461cc2d50e0f Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 07:10:48 +0900
Subject: [PATCH 031/109] retry-now#0030: batch - hoist build_drop_index_query,
 add require_column_in_table, add find_column_in_schema (3 kept)

---
 .../src/sql/delete_column/mod.rs              | 15 +----
 crates/vespertide-query/src/sql/helpers.rs    | 62 +++++++++++++++++++
 crates/vespertide-query/src/sql/mod.rs        |  5 +-
 .../src/sql/modify_column_comment.rs          | 14 ++---
 .../src/sql/modify_column_default.rs          | 20 ++----
 .../src/sql/modify_column_nullable.rs         |  4 +-
 .../src/sql/modify_column_type/direct.rs      | 21 ++-----
 .../src/sql/remove_constraint/mod.rs          | 21 ++-----
 8 files changed, 87 insertions(+), 75 deletions(-)

diff --git a/crates/vespertide-query/src/sql/delete_column/mod.rs b/crates/vespertide-query/src/sql/delete_column/mod.rs
index 6f0b5a74..b85dd51b 100644
--- a/crates/vespertide-query/src/sql/delete_column/mod.rs
+++ b/crates/vespertide-query/src/sql/delete_column/mod.rs
@@ -1,9 +1,8 @@
-use sea_query::{Alias, Index};
-
 use vespertide_core::{ColumnType, ComplexColumnType, TableConstraint, TableDef};
 
 use self::direct::build_direct_delete_column;
 use self::sqlite_rebuild::build_delete_column_sqlite_temp_table;
+use super::helpers::build_drop_index_query;
 use super::types::{BuiltQuery, DatabaseBackend};
 
 mod direct;
@@ -104,20 +103,12 @@ fn sqlite_constraint_handling(
                     columns,
                     name.as_deref(),
                 );
-                let drop_idx = Index::drop()
-                    .name(&index_name)
-                    .table(Alias::new(table))
-                    .to_owned();
-                stmts.push(BuiltQuery::DropIndex(Box::new(drop_idx)));
+                stmts.push(build_drop_index_query(table, &index_name));
             }
             TableConstraint::Index { name, columns } => {
                 let index_name =
                     vespertide_naming::build_index_name(table, columns, name.as_deref());
-                let drop_idx = Index::drop()
-                    .name(&index_name)
-                    .table(Alias::new(table))
-                    .to_owned();
-                stmts.push(BuiltQuery::DropIndex(Box::new(drop_idx)));
+                stmts.push(build_drop_index_query(table, &index_name));
             }
             _ => {
                 unreachable!("TableConstraint is #[non_exhaustive]; all variants are matched above")
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index 8b5a8617..797e63d6 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -772,6 +772,68 @@ pub(crate) fn require_table_in_schema<'a>(
     })
 }
 
+/// Build a `DROP INDEX` query for the given table-qualified `index_name`.
+///
+/// Single source of truth for the
+/// `sea_query::Index::drop().table(...).name(...)` shape. Centralises the
+/// four call sites that previously open-coded it (the SQLite-rebuild
+/// fallback in [`crate::sql::remove_constraint`], the Postgres / MySQL
+/// `Index` arms there, and the two inline constraint-drop arms in
+/// [`crate::sql::delete_column`]).
+#[must_use]
+pub(crate) fn build_drop_index_query(table: &str, index_name: &str) -> BuiltQuery {
+    let idx_drop = sea_query::Index::drop()
+        .table(Alias::new(table))
+        .name(index_name)
+        .to_owned();
+    BuiltQuery::DropIndex(Box::new(idx_drop))
+}
+
+/// Look up `column` in `table_def.columns` and return a reference to its
+/// [`ColumnDef`], or a uniform `QueryError::SchemaError` describing the
+/// miss. Mirrors [`require_table_in_schema`] one level down: centralises
+/// the call sites that previously open-coded
+/// `table_def.columns.iter().find(|c| c.name == column).ok_or_else(...)`
+/// with the canonical `"Column '{column}' not found in table '{table}'."`
+/// message (trailing period included so callers stay byte-identical to
+/// existing string-match assertions).
+pub(crate) fn require_column_in_table<'a>(
+    table_def: &'a TableDef,
+    column: &str,
+) -> Result<&'a ColumnDef, crate::QueryError> {
+    table_def
+        .columns
+        .iter()
+        .find(|c| c.name == column)
+        .ok_or_else(|| {
+            crate::QueryError::SchemaError(format!(
+                "Column '{column}' not found in table '{table}'.",
+                table = table_def.name,
+            ))
+        })
+}
+
+/// Look up `column` in the table named `table` inside `schema`, returning
+/// `None` when either the table or the column is absent.
+///
+/// Centralises the `schema.iter().find(|t| t.name == table).and_then(|t|
+/// t.columns.iter().find(|c| c.name == column))` chain used by the five
+/// non-error-returning column lookups across the SQL builders (the
+/// `DeleteColumn` dispatcher, the Postgres `ModifyColumnDefault` path,
+/// and the three direct `ModifyColumnType` helpers). The error-returning
+/// twin lives one helper up as [`require_column_in_table`].
+#[must_use]
+pub(crate) fn find_column_in_schema<'a>(
+    schema: &'a [TableDef],
+    table: &str,
+    column: &str,
+) -> Option<&'a ColumnDef> {
+    schema
+        .iter()
+        .find(|t| t.name == table)
+        .and_then(|t| t.columns.iter().find(|c| c.name == column))
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/vespertide-query/src/sql/mod.rs b/crates/vespertide-query/src/sql/mod.rs
index 511afba2..ca7e3e09 100644
--- a/crates/vespertide-query/src/sql/mod.rs
+++ b/crates/vespertide-query/src/sql/mod.rs
@@ -88,10 +88,7 @@ pub fn build_action_queries_with_pending(
 
         MigrationAction::DeleteColumn { table, column } => {
             // Find the column type from current schema for enum DROP TYPE support
-            let column_type = current_schema
-                .iter()
-                .find(|t| t.name == *table)
-                .and_then(|t| t.columns.iter().find(|c| c.name == *column))
+            let column_type = helpers::find_column_in_schema(current_schema, table, column)
                 .map(|c| &c.r#type);
             Ok(build_delete_column(
                 backend,
diff --git a/crates/vespertide-query/src/sql/modify_column_comment.rs b/crates/vespertide-query/src/sql/modify_column_comment.rs
index 36fb9ffd..c3405815 100644
--- a/crates/vespertide-query/src/sql/modify_column_comment.rs
+++ b/crates/vespertide-query/src/sql/modify_column_comment.rs
@@ -2,7 +2,9 @@ use sea_query::Alias;
 
 use vespertide_core::TableDef;
 
-use super::helpers::{build_sea_column_def_with_table, quote_ident, require_table_in_schema};
+use super::helpers::{
+    build_sea_column_def_with_table, quote_ident, require_column_in_table, require_table_in_schema,
+};
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
 use crate::error::QueryError;
 
@@ -38,15 +40,7 @@ pub fn build_modify_column_comment(
                 "MySQL requires current schema information to modify column comments",
             )?;
 
-            let column_def = table_def
-                .columns
-                .iter()
-                .find(|c| c.name == column)
-                .ok_or_else(|| {
-                    QueryError::SchemaError(format!(
-                        "Column '{column}' not found in table '{table}'."
-                    ))
-                })?;
+            let column_def = require_column_in_table(table_def, column)?;
 
             // Build the full column definition with updated comment
             let mut modified_col_def = column_def.clone();
diff --git a/crates/vespertide-query/src/sql/modify_column_default.rs b/crates/vespertide-query/src/sql/modify_column_default.rs
index 7383c102..428cfd5d 100644
--- a/crates/vespertide-query/src/sql/modify_column_default.rs
+++ b/crates/vespertide-query/src/sql/modify_column_default.rs
@@ -4,7 +4,8 @@ use vespertide_core::{ColumnDef, TableDef};
 
 use super::helpers::{
     build_copy_into_temp_table, build_sea_column_def_with_table, build_sqlite_temp_table_create,
-    normalize_enum_default, quote_ident, recreate_indexes_after_rebuild, require_table_in_schema,
+    find_column_in_schema, normalize_enum_default, quote_ident, recreate_indexes_after_rebuild,
+    require_column_in_table, require_table_in_schema,
 };
 use super::rename_table::build_rename_table;
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
@@ -36,11 +37,8 @@ pub fn build_modify_column_default(
             let quoted_column = quote_ident(column, backend);
             let alter_sql = if let Some(default_value) = new_default {
                 // Look up column type to properly quote enum defaults
-                let column_type = current_schema
-                    .iter()
-                    .find(|t| t.name == table)
-                    .and_then(|t| t.columns.iter().find(|c| c.name == column))
-                    .map(|c| &c.r#type);
+                let column_type =
+                    find_column_in_schema(current_schema, table, column).map(|c| &c.r#type);
 
                 let normalized_default = if let Some(col_type) = column_type {
                     normalize_enum_default(col_type, default_value)
@@ -64,15 +62,7 @@ pub fn build_modify_column_default(
                 "MySQL requires current schema information to modify column defaults",
             )?;
 
-            let column_def = table_def
-                .columns
-                .iter()
-                .find(|c| c.name == column)
-                .ok_or_else(|| {
-                    QueryError::SchemaError(format!(
-                        "Column '{column}' not found in table '{table}'."
-                    ))
-                })?;
+            let column_def = require_column_in_table(table_def, column)?;
 
             // Create a modified column def with the new default
             let modified_col_def = ColumnDef {
diff --git a/crates/vespertide-query/src/sql/modify_column_nullable.rs b/crates/vespertide-query/src/sql/modify_column_nullable.rs
index 076e9da4..288d142e 100644
--- a/crates/vespertide-query/src/sql/modify_column_nullable.rs
+++ b/crates/vespertide-query/src/sql/modify_column_nullable.rs
@@ -5,7 +5,7 @@ use vespertide_core::{ColumnDef, TableDef};
 use super::helpers::{
     build_copy_into_temp_table, build_sea_column_def_with_table, build_sqlite_temp_table_create,
     convert_default_for_backend, normalize_fill_with, quote_ident, recreate_indexes_after_rebuild,
-    require_table_in_schema,
+    require_column_in_table, require_table_in_schema,
 };
 use super::rename_table::build_rename_table;
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
@@ -68,7 +68,7 @@ pub fn build_modify_column_nullable(
                 "MySQL requires current schema information to modify column nullability",
             )?;
 
-            let column_def = table_def.columns.iter().find(|c| c.name == column).ok_or_else(|| QueryError::SchemaError(format!("Column '{column}' not found in table '{table}'. MySQL requires column information to modify nullability.")))?;
+            let column_def = require_column_in_table(table_def, column)?;
 
             // Create a modified column def with the new nullability
             let modified_col_def = ColumnDef {
diff --git a/crates/vespertide-query/src/sql/modify_column_type/direct.rs b/crates/vespertide-query/src/sql/modify_column_type/direct.rs
index 91466543..f81e4f52 100644
--- a/crates/vespertide-query/src/sql/modify_column_type/direct.rs
+++ b/crates/vespertide-query/src/sql/modify_column_type/direct.rs
@@ -6,7 +6,7 @@ use vespertide_core::{ColumnType, ComplexColumnType, TableDef};
 
 use crate::sql::helpers::{
     apply_column_type_with_table, build_create_enum_type_sql, convert_default_for_backend,
-    normalize_enum_default, quote_ident,
+    find_column_in_schema, normalize_enum_default, quote_ident,
 };
 use crate::sql::types::{BuiltQuery, DatabaseBackend, RawSql};
 
@@ -53,11 +53,7 @@ fn old_column_type<'a>(
     table: &str,
     column: &str,
 ) -> Option<&'a ColumnType> {
-    current_schema
-        .iter()
-        .find(|t| t.name == table)
-        .and_then(|t| t.columns.iter().find(|c| c.name == column))
-        .map(|c| &c.r#type)
+    find_column_in_schema(current_schema, table, column).map(|c| &c.r#type)
 }
 
 fn needs_postgres_enum_migration(context: &DirectBuildContext<'_>) -> bool {
@@ -152,11 +148,7 @@ fn build_postgres_enum_migration(queries: &mut Vec, context: &Direct
 }
 
 fn column_default(context: &DirectBuildContext<'_>) -> Option {
-    context
-        .current_schema
-        .iter()
-        .find(|t| t.name == context.table)
-        .and_then(|t| t.columns.iter().find(|c| c.name == context.column))
+    find_column_in_schema(context.current_schema, context.table, context.column)
         .and_then(|c| c.default.clone())
 }
 
@@ -209,11 +201,8 @@ fn create_new_enum_type_if_needed(queries: &mut Vec, context: &Direc
 
 fn preserve_mysql_column_attributes(col: &mut SeaColumnDef, context: &DirectBuildContext<'_>) {
     if context.backend == DatabaseBackend::MySql
-        && let Some(column_def) = context
-            .current_schema
-            .iter()
-            .find(|t| t.name == context.table)
-            .and_then(|t| t.columns.iter().find(|c| c.name == context.column))
+        && let Some(column_def) =
+            find_column_in_schema(context.current_schema, context.table, context.column)
     {
         if !column_def.nullable {
             col.not_null();
diff --git a/crates/vespertide-query/src/sql/remove_constraint/mod.rs b/crates/vespertide-query/src/sql/remove_constraint/mod.rs
index 6b4dfdaf..a1d3a395 100644
--- a/crates/vespertide-query/src/sql/remove_constraint/mod.rs
+++ b/crates/vespertide-query/src/sql/remove_constraint/mod.rs
@@ -2,27 +2,16 @@ mod mysql;
 mod postgres;
 mod sqlite;
 
-use sea_query::Alias;
-
 use vespertide_core::{TableConstraint, TableDef};
 
 use super::types::{BuiltQuery, DatabaseBackend};
 use crate::error::QueryError;
 
-/// Build a `DROP INDEX` query for the given table-qualified index name.
-///
-/// Shared by the SQLite-rebuild fallback in this module and the
-/// `TableConstraint::Index` arms of [`mysql::build_remove_constraint`]
-/// and [`postgres::build_remove_constraint`] — they all want the same
-/// `sea_query::Index::drop().table(...).name(...)` shape.
-#[must_use]
-pub(super) fn build_drop_index_query(table: &str, index_name: &str) -> BuiltQuery {
-    let idx_drop = sea_query::Index::drop()
-        .table(Alias::new(table))
-        .name(index_name)
-        .to_owned();
-    BuiltQuery::DropIndex(Box::new(idx_drop))
-}
+// Re-export the canonical `DROP INDEX` builder from `sql::helpers` so
+// the SQLite fallback below and the `TableConstraint::Index` arms in
+// `mysql.rs` / `postgres.rs` keep resolving `super::build_drop_index_query`
+// unchanged. The implementation now lives in one place.
+pub(super) use super::helpers::build_drop_index_query;
 
 pub fn build_remove_constraint(
     backend: DatabaseBackend,

From 6f9c0750ebed31759d0f75a28e760daed8317dd6 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 07:50:12 +0900
Subject: [PATCH 032/109] retry-now#0031: batch -- hoist SQLite rebuild + unify
 Literal helpers + hoist LSP pair helpers (3 kept)

- vespertide-query: hoist the canonical 5-step SQLite temp-table rebuild
  sequence into `build_sqlite_table_rebuild` in `sql/helpers.rs`; rewire 7
  open-coded call sites (add_constraint, remove_constraint/sqlite,
  modify_column_default SQLite arm, modify_column_nullable SQLite arm,
  modify_column_type/sqlite_rebuild, replace_constraint, delete_column/
  sqlite_rebuild). Emitted SQL stays byte-identical (all 357 snapshots
  unchanged); orchestration contract now lives in one place.
- vespertide-planner: unify duplicated `literal_compare` (2 copies, one with
  Bool arm) and `format_literal` (2 byte-identical copies) into
  `Literal::cmp_value` / `Literal::display_value` methods on
  `check_expr_parser::Literal`; rewire 4 call sites across
  check_self_contradiction.rs, check_strengthening.rs, check_type_mismatch.rs.
  Bool arm is kept (strict superset of strengthening's prior behavior).
- vespertide-lsp: hoist `enclosing_pair_with_key` (2 copies) and
  `direct_child_value` (3 copies) into `tree_util.rs`; rewire 3 LSP feature
  modules (definition/foreign_key, completion/context, references/resolver).
  All copies use safe `source.get(..)` form to avoid panics on stale byte
  ranges mid-edit. Orphan private shims (`pair_key_is`, `value_text`) dropped.

Net 16 files changed, +268 insertions, -383 deletions (115-line drop).
Test/clippy GREEN per the established pg_query MSVC workaround precedent
(workspace --exclude vespertide-query + vespertide-query lib separately;
732+931 tests pass; clippy --workspace --all-targets --all-features clean
with CFLAGS=-Wno-incompatible-pointer-types).
---
 .../vespertide-lsp/src/completion/context.rs  | 49 ++---------
 .../src/definition/foreign_key.rs             | 37 +-------
 .../vespertide-lsp/src/references/resolver.rs | 22 +----
 crates/vespertide-lsp/src/tree_util.rs        | 59 +++++++++++++
 .../src/validate/check_expr_parser.rs         | 45 ++++++++++
 .../src/validate/check_self_contradiction.rs  | 88 +++++++------------
 .../src/validate/check_strengthening.rs       | 21 +----
 .../src/validate/check_type_mismatch.rs       | 26 ++----
 .../src/sql/add_constraint/mod.rs             | 23 ++---
 .../src/sql/delete_column/sqlite_rebuild.rs   | 37 ++------
 crates/vespertide-query/src/sql/helpers.rs    | 58 ++++++++++++
 .../src/sql/modify_column_default.rs          | 36 ++------
 .../src/sql/modify_column_nullable.rs         | 36 ++------
 .../sql/modify_column_type/sqlite_rebuild.rs  | 48 +++-------
 .../src/sql/remove_constraint/sqlite.rs       | 30 ++-----
 .../src/sql/replace_constraint.rs             | 36 ++------
 16 files changed, 268 insertions(+), 383 deletions(-)

diff --git a/crates/vespertide-lsp/src/completion/context.rs b/crates/vespertide-lsp/src/completion/context.rs
index 64542a01..560d47ed 100644
--- a/crates/vespertide-lsp/src/completion/context.rs
+++ b/crates/vespertide-lsp/src/completion/context.rs
@@ -6,7 +6,7 @@ use vespertide_planner::{CheckToken, CheckTokenKind, lex_check_expr};
 
 use crate::check_expr_range::expr_inner_range;
 use crate::text_util::strip_quotes;
-use crate::tree_util::is_pair;
+use crate::tree_util::{direct_child_value, enclosing_pair_with_key, is_pair};
 
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub(super) enum Context {
@@ -603,39 +603,12 @@ fn collect_key_path(node: tree_sitter::Node<'_>, source: &str) -> Vec {
 fn sibling_ref_table(node: tree_sitter::Node<'_>, source: &str) -> Option {
     let ref_columns_pair = enclosing_pair_with_key(node, source, "ref_columns")?;
     let parent = ref_columns_pair.parent()?;
-    direct_child_value(parent, source, "ref_table").map(ToString::to_string)
-}
-
-fn enclosing_pair_with_key<'tree>(
-    node: tree_sitter::Node<'tree>,
-    source: &str,
-    expected: &str,
-) -> Option> {
-    let mut current = Some(node);
-
-    while let Some(candidate) = current {
-        if is_pair(candidate) && key_text(candidate, source) == Some(expected) {
-            return Some(candidate);
-        }
-        current = candidate.parent();
-    }
-
-    None
-}
-
-fn direct_child_value<'source>(
-    node: tree_sitter::Node<'_>,
-    source: &'source str,
-    expected_key: &str,
-) -> Option<&'source str> {
-    let mut cursor = node.walk();
-    for child in node.children(&mut cursor) {
-        if is_pair(child) && key_text(child, source) == Some(expected_key) {
-            return value_text(child, source);
-        }
-    }
-
-    None
+    // Hoisted `direct_child_value` returns the verbatim value-side slice
+    // (quotes preserved), so strip them here to match the previous
+    // private-helper contract that already stripped via `value_text`.
+    direct_child_value(parent, source, "ref_table")
+        .map(strip_quotes)
+        .map(ToString::to_string)
 }
 
 fn key_text<'source>(
@@ -646,14 +619,6 @@ fn key_text<'source>(
     source.get(key.byte_range()).map(strip_quotes)
 }
 
-fn value_text<'source>(
-    pair_node: tree_sitter::Node<'_>,
-    source: &'source str,
-) -> Option<&'source str> {
-    let value = pair_node.named_child(1)?;
-    source.get(value.byte_range()).map(strip_quotes)
-}
-
 fn node_at_byte(tree: &tree_sitter::Tree, byte_offset: usize) -> tree_sitter::Node<'_> {
     let root = tree.root_node();
     if root.end_byte() == 0 {
diff --git a/crates/vespertide-lsp/src/definition/foreign_key.rs b/crates/vespertide-lsp/src/definition/foreign_key.rs
index bc7c434c..5572f5d8 100644
--- a/crates/vespertide-lsp/src/definition/foreign_key.rs
+++ b/crates/vespertide-lsp/src/definition/foreign_key.rs
@@ -15,7 +15,7 @@ use std::ops::Range;
 
 use crate::store::DocumentStore;
 use crate::text_util::strip_quotes;
-use crate::tree_util::is_pair;
+use crate::tree_util::{direct_child_value, enclosing_pair_with_key, is_pair};
 use crate::workspace_index::WorkspaceIndex;
 use crate::workspace_tables::WorkspaceTables;
 
@@ -164,41 +164,6 @@ fn enclosing_string(node: tree_sitter::Node<'_>) -> Option
     None
 }
 
-fn enclosing_pair_with_key<'tree>(
-    node: tree_sitter::Node<'tree>,
-    source: &str,
-    expected_key: &str,
-) -> Option> {
-    let mut current = Some(node);
-    while let Some(candidate) = current {
-        if is_pair(candidate) && pair_key_is(candidate, source, expected_key) {
-            return Some(candidate);
-        }
-        current = candidate.parent();
-    }
-    None
-}
-
-fn pair_key_is(pair: tree_sitter::Node<'_>, source: &str, expected: &str) -> bool {
-    pair.named_child(0)
-        .is_some_and(|key| strip_quotes(&source[key.byte_range()]) == expected)
-}
-
-fn direct_child_value<'a>(
-    object: tree_sitter::Node<'_>,
-    source: &'a str,
-    target_key: &str,
-) -> Option<&'a str> {
-    let mut cursor = object.walk();
-    for child in object.children(&mut cursor) {
-        if is_pair(child) && pair_key_is(child, source, target_key) {
-            let value = child.named_child(1)?;
-            return Some(&source[value.byte_range()]);
-        }
-    }
-    None
-}
-
 fn find_column_name_range(
     tree: &tree_sitter::Tree,
     text: &str,
diff --git a/crates/vespertide-lsp/src/references/resolver.rs b/crates/vespertide-lsp/src/references/resolver.rs
index 61a35ff0..d4b6ce13 100644
--- a/crates/vespertide-lsp/src/references/resolver.rs
+++ b/crates/vespertide-lsp/src/references/resolver.rs
@@ -1,7 +1,7 @@
 //! Resolve "what symbol is the cursor on?" for the references provider.
 
 use crate::text_util::strip_quotes;
-use crate::tree_util::node_at_byte;
+use crate::tree_util::{direct_child_value, node_at_byte};
 use tower_lsp_server::ls_types::Uri;
 
 use super::ReferenceSymbol;
@@ -197,26 +197,6 @@ fn enclosing_table_name(name_pair: tree_sitter::Node<'_>, source: &str) -> Optio
     None
 }
 
-fn direct_child_value<'a>(
-    object: tree_sitter::Node<'_>,
-    source: &'a str,
-    target_key: &str,
-) -> Option<&'a str> {
-    let mut cursor = object.walk();
-    for child in object.children(&mut cursor) {
-        if matches!(child.kind(), "pair" | "block_mapping_pair")
-            && let Some(key) = child.named_child(0)
-            && let Some(key_text) = source.get(key.byte_range())
-            && strip_quotes(key_text) == target_key
-            && let Some(value) = child.named_child(1)
-            && let Some(text) = source.get(value.byte_range())
-        {
-            return Some(text);
-        }
-    }
-    None
-}
-
 fn skip_yaml_wrappers(node: tree_sitter::Node<'_>) -> Option> {
     let mut current = node;
     while matches!(current.kind(), "flow_node" | "block_node") {
diff --git a/crates/vespertide-lsp/src/tree_util.rs b/crates/vespertide-lsp/src/tree_util.rs
index 479dd665..048ba537 100644
--- a/crates/vespertide-lsp/src/tree_util.rs
+++ b/crates/vespertide-lsp/src/tree_util.rs
@@ -81,3 +81,62 @@ pub(crate) fn unwrap_yaml_node(node: tree_sitter::Node<'_>) -> tree_sitter::Node
     }
     current
 }
+
+/// Walk the parent chain looking for the nearest [`is_pair`] ancestor whose
+/// key (after [`crate::text_util::strip_quotes`]) equals `expected_key`.
+///
+/// Centralises the two byte-identical private copies that lived in
+/// `definition::foreign_key` and `completion::context`. Uses the safe
+/// `source.get(..)` form so mid-edit byte ranges that fall outside `source`
+/// return `None` instead of panicking — `tree_sitter` ranges and the
+/// document text can momentarily disagree while the user types.
+pub(crate) fn enclosing_pair_with_key<'tree>(
+    node: tree_sitter::Node<'tree>,
+    source: &str,
+    expected_key: &str,
+) -> Option> {
+    let mut current = Some(node);
+    while let Some(candidate) = current {
+        if is_pair(candidate)
+            && let Some(key) = candidate.named_child(0)
+            && let Some(key_text) = source.get(key.byte_range())
+            && crate::text_util::strip_quotes(key_text) == expected_key
+        {
+            return Some(candidate);
+        }
+        current = candidate.parent();
+    }
+    None
+}
+
+/// Find a direct child pair of `object` whose key (after
+/// [`crate::text_util::strip_quotes`]) equals `target_key` and return the
+/// value-side byte slice **verbatim** (quotes preserved for quoted
+/// scalars).
+///
+/// Centralises the three byte-identical private copies that lived in
+/// `definition::foreign_key`, `completion::context`, and
+/// `references::resolver`. Each pre-existing caller already stripped
+/// quotes off the returned slice, so this helper deliberately keeps the
+/// raw form to preserve the existing call-site contract. Uses the safe
+/// `source.get(..)` form on both the key and value spans so a stale
+/// byte range mid-edit returns `None` instead of panicking.
+pub(crate) fn direct_child_value<'a>(
+    object: tree_sitter::Node<'_>,
+    source: &'a str,
+    target_key: &str,
+) -> Option<&'a str> {
+    let mut cursor = object.walk();
+    for child in object.children(&mut cursor) {
+        if is_pair(child)
+            && let Some(key) = child.named_child(0)
+            && let Some(key_text) = source.get(key.byte_range())
+            && crate::text_util::strip_quotes(key_text) == target_key
+            && let Some(value) = child.named_child(1)
+            && let Some(text) = source.get(value.byte_range())
+        {
+            return Some(text);
+        }
+    }
+    None
+}
diff --git a/crates/vespertide-planner/src/validate/check_expr_parser.rs b/crates/vespertide-planner/src/validate/check_expr_parser.rs
index a4df0665..a03b5bd9 100644
--- a/crates/vespertide-planner/src/validate/check_expr_parser.rs
+++ b/crates/vespertide-planner/src/validate/check_expr_parser.rs
@@ -124,6 +124,51 @@ pub enum Literal {
     Null,
 }
 
+impl Literal {
+    /// Total/partial order over comparable literal kinds.
+    ///
+    /// Returns `None` for mixed-kind pairs or anything involving `Null`
+    /// where no answer is sound. The conservative behaviour (silent-pass
+    /// on ambiguity) is shared by every CHECK validator that needs to
+    /// reason about ordering — F-novel-1 (self-contradiction), F29
+    /// (strengthening), and any future caller. Single source of truth for
+    /// `(Integer, Integer)` / `(Float, Float)` / mixed numeric /
+    /// `(String, String)` / `(Bool, Bool)` orderings.
+    #[expect(
+        clippy::cast_precision_loss,
+        reason = "shared CHECK comparator: rounding integers beyond 2^53 acceptable; conservative comparator silently skips ambiguous cases"
+    )]
+    #[must_use]
+    pub(super) fn cmp_value(&self, other: &Self) -> Option {
+        match (self, other) {
+            (Literal::Integer(a), Literal::Integer(b)) => Some(a.cmp(b)),
+            (Literal::Float(a), Literal::Float(b)) => a.partial_cmp(b),
+            (Literal::Integer(a), Literal::Float(b)) => (*a as f64).partial_cmp(b),
+            (Literal::Float(a), Literal::Integer(b)) => a.partial_cmp(&(*b as f64)),
+            (Literal::String(a), Literal::String(b)) => Some(a.cmp(b)),
+            (Literal::Bool(a), Literal::Bool(b)) => Some(a.cmp(b)),
+            // Mixed kinds / Null: cannot conclude.
+            _ => None,
+        }
+    }
+
+    /// Render the literal as the canonical surface form used in CHECK
+    /// fault messages. `Null` renders as the SQL keyword `NULL`; every
+    /// other variant renders via its natural `Display` form. Single
+    /// source of truth for fault-message rendering across the
+    /// self-contradiction and type-mismatch validators.
+    #[must_use]
+    pub(super) fn display_value(&self) -> String {
+        match self {
+            Literal::Integer(i) => i.to_string(),
+            Literal::Float(f) => f.to_string(),
+            Literal::String(s) => s.clone(),
+            Literal::Bool(b) => b.to_string(),
+            Literal::Null => "NULL".to_string(),
+        }
+    }
+}
+
 // Bound recursive grouping so hostile CHECK strings cannot overflow
 // planner/LSP stacks; unsupported shapes conservatively become Unparseable.
 const MAX_CHECK_EXPR_DEPTH: usize = 64;
diff --git a/crates/vespertide-planner/src/validate/check_self_contradiction.rs b/crates/vespertide-planner/src/validate/check_self_contradiction.rs
index fb815c01..87e7e2d8 100644
--- a/crates/vespertide-planner/src/validate/check_self_contradiction.rs
+++ b/crates/vespertide-planner/src/validate/check_self_contradiction.rs
@@ -250,17 +250,17 @@ fn compare_pair_contradicts(
     op_b: Op,
     vb: &Literal,
 ) -> Option<(String, String)> {
-    let cmp = literal_compare(va, vb)?; // Need ordered literals.
+    let cmp = va.cmp_value(vb)?; // Need ordered literals.
 
     // Equality conflict: col = X AND col = Y where X != Y.
     if op_a == Op::Eq && op_b == Op::Eq && cmp != Ordering::Equal {
-        return Some((format_literal(va), format_literal(vb)));
+        return Some((va.display_value(), vb.display_value()));
     }
     // Equality vs negation: col = X AND col != X.
     if (op_a == Op::Eq && op_b == Op::Ne || op_a == Op::Ne && op_b == Op::Eq)
         && cmp == Ordering::Equal
     {
-        return Some((format_literal(va), format_literal(vb)));
+        return Some((va.display_value(), vb.display_value()));
     }
 
     // Range impossibility: at most one direction each.
@@ -271,12 +271,12 @@ fn compare_pair_contradicts(
         (Op::Lt | Op::Le, Op::Gt | Op::Ge) => (op_b, vb, op_a, va),
         _ => return None,
     };
-    let lower_vs_upper = literal_compare(lower_val, upper_val)?;
+    let lower_vs_upper = lower_val.cmp_value(upper_val)?;
     let strict_boundary = lower_op == Op::Gt || upper_op == Op::Lt;
     let unsatisfiable = matches!(lower_vs_upper, Ordering::Greater)
         || strict_boundary && lower_vs_upper == Ordering::Equal;
     if unsatisfiable {
-        Some((format_literal(va), format_literal(vb)))
+        Some((va.display_value(), vb.display_value()))
     } else {
         None
     }
@@ -297,37 +297,10 @@ fn is_null_vs_other(column: &str, a: &CheckExpr, b: &CheckExpr) -> Option<(bool,
     Some((
         negated,
         format_is_null(column, negated),
-        format_compare(column, op, &format_literal(value)),
+        format_compare(column, op, &value.display_value()),
     ))
 }
 
-#[expect(
-    clippy::cast_precision_loss,
-    reason = "F-novel-1 self-contradiction comparison: rounding integers beyond 2^53 acceptable; conservative comparator silently skips ambiguous cases"
-)]
-fn literal_compare(a: &Literal, b: &Literal) -> Option {
-    match (a, b) {
-        (Literal::Integer(x), Literal::Integer(y)) => Some(x.cmp(y)),
-        (Literal::Float(x), Literal::Float(y)) => x.partial_cmp(y),
-        (Literal::Integer(x), Literal::Float(y)) => (*x as f64).partial_cmp(y),
-        (Literal::Float(x), Literal::Integer(y)) => x.partial_cmp(&(*y as f64)),
-        (Literal::String(x), Literal::String(y)) => Some(x.cmp(y)),
-        (Literal::Bool(x), Literal::Bool(y)) => Some(x.cmp(y)),
-        // Mixed / Null: can't conclude.
-        _ => None,
-    }
-}
-
-fn format_literal(lit: &Literal) -> String {
-    match lit {
-        Literal::Integer(i) => i.to_string(),
-        Literal::Float(f) => f.to_string(),
-        Literal::String(s) => s.clone(),
-        Literal::Bool(b) => b.to_string(),
-        Literal::Null => "NULL".to_string(),
-    }
-}
-
 fn format_compare(column: &str, op: Op, value_text: &str) -> String {
     let op_str = match op {
         Op::Eq => "=",
@@ -794,51 +767,58 @@ mod tests {
         assert!(validate_self_contradiction(&t).is_err());
     }
 
-    /// L276-277, L283, L285, L286: direct unit tests for private
-    /// helpers (`i64_to_f64` reached via 266/267 above; `format_literal`
-    /// Float / Bool / Null arms via direct call).
+    /// Pin every reachable arm of `Literal::display_value` (the unified
+    /// fault-message formatter on `check_expr_parser::Literal`). Float /
+    /// Bool / Null arms aren't reached by the public flow because the
+    /// production path filters them upstream, so this is the canonical
+    /// regression test for those arms.
     #[test]
-    fn format_literal_covers_all_arms() {
-        assert_eq!(format_literal(&Literal::Integer(7)), "7");
-        assert_eq!(format_literal(&Literal::Float(1.5)), "1.5");
-        assert_eq!(format_literal(&Literal::String("'x'".into())), "'x'");
-        assert_eq!(format_literal(&Literal::Bool(true)), "true");
-        assert_eq!(format_literal(&Literal::Null), "NULL");
+    fn display_value_covers_all_arms() {
+        assert_eq!(Literal::Integer(7).display_value(), "7");
+        assert_eq!(Literal::Float(1.5).display_value(), "1.5");
+        assert_eq!(Literal::String("'x'".into()).display_value(), "'x'");
+        assert_eq!(Literal::Bool(true).display_value(), "true");
+        assert_eq!(Literal::Null.display_value(), "NULL");
     }
 
-    /// Direct unit test for literal_compare's reachable arms — lock
-    /// the Float/Float, Int/Float, Float/Int, Bool/Bool, String/String
-    /// behaviour and the mixed-type None fallthrough.
+    /// Lock every reachable arm of `Literal::cmp_value` (the unified
+    /// CHECK comparator) — `Integer/Integer`, `Float/Float`,
+    /// `Integer/Float`, `Float/Integer`, `String/String`, `Bool/Bool`,
+    /// and the mixed-type / Null silent-`None` fallthrough.
     #[test]
-    fn literal_compare_covers_all_reachable_arms() {
+    fn cmp_value_covers_all_reachable_arms() {
         use std::cmp::Ordering;
         assert_eq!(
-            literal_compare(&Literal::Integer(1), &Literal::Integer(2)),
+            Literal::Integer(1).cmp_value(&Literal::Integer(2)),
             Some(Ordering::Less)
         );
         assert_eq!(
-            literal_compare(&Literal::Float(1.0), &Literal::Float(2.0)),
+            Literal::Float(1.0).cmp_value(&Literal::Float(2.0)),
             Some(Ordering::Less)
         );
         assert_eq!(
-            literal_compare(&Literal::Integer(1), &Literal::Float(2.0)),
+            Literal::Integer(1).cmp_value(&Literal::Float(2.0)),
             Some(Ordering::Less)
         );
         assert_eq!(
-            literal_compare(&Literal::Float(2.0), &Literal::Integer(1)),
+            Literal::Float(2.0).cmp_value(&Literal::Integer(1)),
             Some(Ordering::Greater)
         );
         assert_eq!(
-            literal_compare(&Literal::String("a".into()), &Literal::String("b".into())),
+            Literal::String("a".into()).cmp_value(&Literal::String("b".into())),
             Some(Ordering::Less)
         );
         assert_eq!(
-            literal_compare(&Literal::Bool(false), &Literal::Bool(true)),
+            Literal::Bool(false).cmp_value(&Literal::Bool(true)),
             Some(Ordering::Less)
         );
         // Mixed-type returns None.
-        assert!(literal_compare(&Literal::Integer(1), &Literal::String("a".into())).is_none());
-        assert!(literal_compare(&Literal::Null, &Literal::Integer(1)).is_none());
+        assert!(
+            Literal::Integer(1)
+                .cmp_value(&Literal::String("a".into()))
+                .is_none()
+        );
+        assert!(Literal::Null.cmp_value(&Literal::Integer(1)).is_none());
     }
 
     /// L238: `_ => false,` inside the (lower_op, upper_op) match.
diff --git a/crates/vespertide-planner/src/validate/check_strengthening.rs b/crates/vespertide-planner/src/validate/check_strengthening.rs
index 310cfb53..948186b1 100644
--- a/crates/vespertide-planner/src/validate/check_strengthening.rs
+++ b/crates/vespertide-planner/src/validate/check_strengthening.rs
@@ -331,7 +331,7 @@ fn compare_strictness(
     }
     // Same operator family, tighter literal:
     if op1 == op2 {
-        let cmp = literal_compare(v1, v2)?;
+        let cmp = v1.cmp_value(v2)?;
         let tighter = match op1 {
             Op::Gt | Op::Ge => cmp == Ordering::Less, // newer literal is larger
             Op::Lt | Op::Le => cmp == Ordering::Greater, // newer literal is smaller
@@ -380,10 +380,10 @@ fn between_is_narrower(
     new_low: &Literal,
     new_high: &Literal,
 ) -> bool {
-    let Some(lo_cmp) = literal_compare(old_low, new_low) else {
+    let Some(lo_cmp) = old_low.cmp_value(new_low) else {
         return false;
     };
-    let Some(hi_cmp) = literal_compare(old_high, new_high) else {
+    let Some(hi_cmp) = old_high.cmp_value(new_high) else {
         return false;
     };
     // old_low <= new_low (new low is tighter or equal)
@@ -411,21 +411,6 @@ fn literal_equals(a: &Literal, b: &Literal) -> bool {
     }
 }
 
-#[expect(
-    clippy::cast_precision_loss,
-    reason = "F29 strictness comparison: rounding integers beyond 2^53 acceptable; F29 silent-passes on ambiguity"
-)]
-fn literal_compare(a: &Literal, b: &Literal) -> Option {
-    match (a, b) {
-        (Literal::Integer(x), Literal::Integer(y)) => Some(x.cmp(y)),
-        (Literal::Float(x), Literal::Float(y)) => x.partial_cmp(y),
-        (Literal::Integer(x), Literal::Float(y)) => (*x as f64).partial_cmp(y),
-        (Literal::Float(x), Literal::Integer(y)) => x.partial_cmp(&(*y as f64)),
-        (Literal::String(x), Literal::String(y)) => Some(x.cmp(y)),
-        _ => None,
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/vespertide-planner/src/validate/check_type_mismatch.rs b/crates/vespertide-planner/src/validate/check_type_mismatch.rs
index aa64af24..1441130b 100644
--- a/crates/vespertide-planner/src/validate/check_type_mismatch.rs
+++ b/crates/vespertide-planner/src/validate/check_type_mismatch.rs
@@ -253,7 +253,7 @@ fn check_one(
         constraint_name: check_name.to_string(),
         column: column.to_string(),
         column_type_label: column_type_label(col_type),
-        literal_text: format_literal(literal),
+        literal_text: literal.display_value(),
         literal_kind: literal_kind_name(literal).to_string(),
         expr: expr_text.to_string(),
     });
@@ -428,16 +428,6 @@ fn literal_kind_name(lit: &Literal) -> &'static str {
     }
 }
 
-fn format_literal(lit: &Literal) -> String {
-    match lit {
-        Literal::Integer(i) => i.to_string(),
-        Literal::Float(f) => f.to_string(),
-        Literal::String(s) => s.clone(),
-        Literal::Bool(b) => b.to_string(),
-        Literal::Null => "NULL".to_string(),
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -957,22 +947,22 @@ mod tests {
     }
 
     /// Direct unit test for `literal_kind_name` Null arm (line 425) and
-    /// `format_literal` Null arm (line 435): these arms are unreachable
+    /// the unified `Literal::display_value` Null arm: both are unreachable
     /// from the public flow because `is_definitely_mismatch` returns
     /// false for `Null` literals before the formatters run.
     #[rstest]
-    fn literal_kind_name_and_format_literal_cover_null_and_float_arms() {
+    fn literal_kind_name_and_display_value_cover_null_and_float_arms() {
         assert_eq!(literal_kind_name(&Literal::Integer(1)), "Integer");
         assert_eq!(literal_kind_name(&Literal::Float(1.0)), "Float");
         assert_eq!(literal_kind_name(&Literal::String("x".into())), "String");
         assert_eq!(literal_kind_name(&Literal::Bool(true)), "Bool");
         assert_eq!(literal_kind_name(&Literal::Null), "Null");
 
-        assert_eq!(format_literal(&Literal::Integer(7)), "7");
-        assert_eq!(format_literal(&Literal::Float(1.5)), "1.5");
-        assert_eq!(format_literal(&Literal::String("alice".into())), "alice");
-        assert_eq!(format_literal(&Literal::Bool(false)), "false");
-        assert_eq!(format_literal(&Literal::Null), "NULL");
+        assert_eq!(Literal::Integer(7).display_value(), "7");
+        assert_eq!(Literal::Float(1.5).display_value(), "1.5");
+        assert_eq!(Literal::String("alice".into()).display_value(), "alice");
+        assert_eq!(Literal::Bool(false).display_value(), "false");
+        assert_eq!(Literal::Null.display_value(), "NULL");
     }
 
     #[rstest]
diff --git a/crates/vespertide-query/src/sql/add_constraint/mod.rs b/crates/vespertide-query/src/sql/add_constraint/mod.rs
index acb51332..7fcccc53 100644
--- a/crates/vespertide-query/src/sql/add_constraint/mod.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/mod.rs
@@ -930,11 +930,7 @@ mod unique;
 
 use vespertide_core::{TableConstraint, TableDef};
 
-use super::helpers::{
-    build_copy_into_temp_table, build_sqlite_temp_table_create, recreate_indexes_after_rebuild,
-    require_table_in_schema,
-};
-use super::rename_table::build_rename_table;
+use super::helpers::{build_sqlite_table_rebuild, require_table_in_schema};
 use super::types::{BuiltQuery, DatabaseBackend};
 use crate::error::QueryError;
 
@@ -1085,20 +1081,13 @@ pub(super) fn rebuild_sqlite_table_with_added_constraint(
         "SQLite requires current schema information to add constraints",
     )?;
     let new_constraints = merge_constraint(&table_def.constraints, constraint);
-    let temp_table = format!("{table}_temp");
-    let create_query = build_sqlite_temp_table_create(
+    Ok(build_sqlite_table_rebuild(
         backend,
-        &temp_table,
         table,
         &table_def.columns,
         &new_constraints,
-    );
-    let insert_query = build_copy_into_temp_table(table, &temp_table, &table_def.columns);
-    let drop_query = super::delete_table::build_delete_table(table);
-    let rename_query = build_rename_table(&temp_table, table);
-    let index_queries =
-        recreate_indexes_after_rebuild(table, &table_def.constraints, pending_constraints);
-    let mut queries = vec![create_query, insert_query, drop_query, rename_query];
-    queries.extend(index_queries);
-    Ok(queries)
+        &table_def.columns,
+        &table_def.constraints,
+        pending_constraints,
+    ))
 }
diff --git a/crates/vespertide-query/src/sql/delete_column/sqlite_rebuild.rs b/crates/vespertide-query/src/sql/delete_column/sqlite_rebuild.rs
index 7d086ccf..0921023b 100644
--- a/crates/vespertide-query/src/sql/delete_column/sqlite_rebuild.rs
+++ b/crates/vespertide-query/src/sql/delete_column/sqlite_rebuild.rs
@@ -1,10 +1,6 @@
 use vespertide_core::{ColumnType, TableConstraint, TableDef};
 
-use crate::sql::helpers::{
-    build_copy_into_temp_table, build_drop_enum_type_sql, build_sqlite_temp_table_create,
-    recreate_indexes_after_rebuild,
-};
-use crate::sql::rename_table::build_rename_table;
+use crate::sql::helpers::{build_drop_enum_type_sql, build_sqlite_table_rebuild};
 use crate::sql::types::{BuiltQuery, DatabaseBackend};
 
 /// `SQLite` temp table approach for deleting a column that has FK, PK, CHECK, or
@@ -16,6 +12,8 @@ use crate::sql::types::{BuiltQuery, DatabaseBackend};
 /// 3. Drop original table
 /// 4. Rename temp table to original name
 /// 5. Recreate indexes that don't reference the deleted column
+/// 6. If the column type was an enum, drop the enum type (PostgreSQL only;
+///    included for completeness)
 pub(super) fn build_delete_column_sqlite_temp_table(
     table: &str,
     column: &str,
@@ -23,10 +21,6 @@ pub(super) fn build_delete_column_sqlite_temp_table(
     column_type: Option<&ColumnType>,
     pending_constraints: &[TableConstraint],
 ) -> Vec {
-    // perf: SQLite rebuild emits four fixed statements plus recreated indexes.
-    let mut stmts = Vec::with_capacity(4 + table_def.constraints.len());
-    let temp_table = format!("{table}_temp");
-
     // Build new columns list without the deleted column.
     let new_columns: Vec<_> = table_def
         .columns
@@ -52,33 +46,18 @@ pub(super) fn build_delete_column_sqlite_temp_table(
         .cloned()
         .collect();
 
-    // 1. Create temp table without the column + CHECK constraints.
-    let create_query = build_sqlite_temp_table_create(
+    let mut stmts = build_sqlite_table_rebuild(
         DatabaseBackend::Sqlite,
-        &temp_table,
         table,
         &new_columns,
         &new_constraints,
-    );
-    stmts.push(create_query);
-
-    // 2. Copy data (excluding the deleted column).
-    stmts.push(build_copy_into_temp_table(table, &temp_table, &new_columns));
-
-    // 3. Drop original table.
-    stmts.push(crate::sql::delete_table::build_delete_table(table));
-
-    // 4. Rename temp table to original name.
-    stmts.push(build_rename_table(&temp_table, table));
-
-    // 5. Recreate indexes (both regular and UNIQUE) that don't reference the deleted column.
-    stmts.extend(recreate_indexes_after_rebuild(
-        table,
+        &new_columns,
         &new_constraints,
         pending_constraints,
-    ));
+    );
 
-    // If column type is an enum, drop the type after (PostgreSQL only, but include for completeness).
+    // If column type is an enum, drop the type after (PostgreSQL only,
+    // but include for completeness).
     if let Some(col_type) = column_type
         && let Some(drop_type_sql) = build_drop_enum_type_sql(table, col_type)
     {
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index 797e63d6..300ca3e5 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -690,6 +690,64 @@ pub fn recreate_indexes_after_rebuild(
     queries
 }
 
+/// Build the canonical 5-step `SQLite` temp-table rebuild sequence:
+///
+/// 1. `CREATE TABLE {table}_temp(...)` from `create_columns` + `create_constraints`
+/// 2. `INSERT INTO {table}_temp ... SELECT FROM {table}` over `copy_columns`
+/// 3. `DROP TABLE {table}`
+/// 4. `ALTER TABLE {table}_temp RENAME TO {table}`
+/// 5. Recreate indexes / UNIQUE indexes from `recreate_constraints`
+///    minus anything already in `pending_constraints`
+///
+/// Centralises the seven open-coded call sites
+/// (`add_constraint::rebuild_sqlite_table_with_added_constraint`,
+/// `remove_constraint::sqlite::rebuild_table_without_constraint`,
+/// the `SQLite` arms of `modify_column_default` / `modify_column_nullable`,
+/// `modify_column_type::sqlite_rebuild::build_modify_column_type_sqlite_temp_table`,
+/// `replace_constraint::build_sqlite_constraint_replace`, and
+/// `delete_column::sqlite_rebuild::build_delete_column_sqlite_temp_table`)
+/// that previously re-emitted the same fixed four-statement vec + index
+/// extension by hand. Each call site keeps its surrounding context
+/// (`fill_with` UPDATEs, enum DROP TYPE, ...) OUTSIDE this helper — the
+/// helper covers only the invariant rebuild contract, so emitted SQL
+/// stays byte-identical to the previous open-coded sequences (every
+/// existing snapshot must continue to match without regeneration).
+///
+/// The seven slices ARE the rebuild contract; bundling them into a
+/// struct hides which slice plays which role at each call site, so
+/// the parameters stay flat (and sit exactly at clippy's default
+/// 7-arg threshold for `too_many_arguments`).
+pub(super) fn build_sqlite_table_rebuild(
+    backend: DatabaseBackend,
+    table: &str,
+    create_columns: &[ColumnDef],
+    create_constraints: &[TableConstraint],
+    copy_columns: &[ColumnDef],
+    recreate_constraints: &[TableConstraint],
+    pending_constraints: &[TableConstraint],
+) -> Vec {
+    let temp_table = format!("{table}_temp");
+    let create_query = build_sqlite_temp_table_create(
+        backend,
+        &temp_table,
+        table,
+        create_columns,
+        create_constraints,
+    );
+    let insert_query = build_copy_into_temp_table(table, &temp_table, copy_columns);
+    let drop_query = super::delete_table::build_delete_table(table);
+    let rename_query = super::rename_table::build_rename_table(&temp_table, table);
+    let index_queries =
+        recreate_indexes_after_rebuild(table, recreate_constraints, pending_constraints);
+    let mut queries = Vec::with_capacity(4 + index_queries.len());
+    queries.push(create_query);
+    queries.push(insert_query);
+    queries.push(drop_query);
+    queries.push(rename_query);
+    queries.extend(index_queries);
+    queries
+}
+
 /// Extract enum name from column type if it's an enum
 pub fn get_enum_name(column_type: &ColumnType) -> Option<&str> {
     if let ColumnType::Complex(ComplexColumnType::Enum { name, .. }) = column_type {
diff --git a/crates/vespertide-query/src/sql/modify_column_default.rs b/crates/vespertide-query/src/sql/modify_column_default.rs
index 428cfd5d..3a825b72 100644
--- a/crates/vespertide-query/src/sql/modify_column_default.rs
+++ b/crates/vespertide-query/src/sql/modify_column_default.rs
@@ -3,11 +3,9 @@ use sea_query::{Alias, Table};
 use vespertide_core::{ColumnDef, TableDef};
 
 use super::helpers::{
-    build_copy_into_temp_table, build_sea_column_def_with_table, build_sqlite_temp_table_create,
-    find_column_in_schema, normalize_enum_default, quote_ident, recreate_indexes_after_rebuild,
-    require_column_in_table, require_table_in_schema,
+    build_sea_column_def_with_table, build_sqlite_table_rebuild, find_column_in_schema,
+    normalize_enum_default, quote_ident, require_column_in_table, require_table_in_schema,
 };
-use super::rename_table::build_rename_table;
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
 use crate::error::QueryError;
 
@@ -79,49 +77,25 @@ pub fn build_modify_column_default(
             queries.push(BuiltQuery::AlterTable(Box::new(stmt)));
         }
         DatabaseBackend::Sqlite => {
-            // SQLite doesn't support ALTER COLUMN for default changes
-            // Use temporary table approach
+            // SQLite doesn't support ALTER COLUMN for default changes;
+            // use the canonical temp-table rebuild with the modified column.
             let table_def = require_table_in_schema(
                 current_schema,
                 table,
                 "SQLite requires current schema information to modify column defaults",
             )?;
 
-            // Create modified columns with the new default
             let mut new_columns = table_def.columns.clone();
             if let Some(col) = new_columns.iter_mut().find(|c| c.name == column) {
                 col.default = new_default.map(std::convert::Into::into);
             }
 
-            // Generate temporary table name
-            let temp_table = format!("{table}_temp");
-
-            // 1. Create temporary table with modified column + CHECK constraints
-            let create_query = build_sqlite_temp_table_create(
+            queries.extend(build_sqlite_table_rebuild(
                 backend,
-                &temp_table,
                 table,
                 &new_columns,
                 &table_def.constraints,
-            );
-            queries.push(create_query);
-
-            // 2. Copy data (all columns)
-            queries.push(build_copy_into_temp_table(
-                table,
-                &temp_table,
                 &table_def.columns,
-            ));
-
-            // 3. Drop original table
-            queries.push(super::delete_table::build_delete_table(table));
-
-            // 4. Rename temporary table to original name
-            queries.push(build_rename_table(&temp_table, table));
-
-            // 5. Recreate indexes (both regular and UNIQUE)
-            queries.extend(recreate_indexes_after_rebuild(
-                table,
                 &table_def.constraints,
                 pending_constraints,
             ));
diff --git a/crates/vespertide-query/src/sql/modify_column_nullable.rs b/crates/vespertide-query/src/sql/modify_column_nullable.rs
index 288d142e..318f3602 100644
--- a/crates/vespertide-query/src/sql/modify_column_nullable.rs
+++ b/crates/vespertide-query/src/sql/modify_column_nullable.rs
@@ -3,11 +3,9 @@ use sea_query::{Alias, Table};
 use vespertide_core::{ColumnDef, TableDef};
 
 use super::helpers::{
-    build_copy_into_temp_table, build_sea_column_def_with_table, build_sqlite_temp_table_create,
-    convert_default_for_backend, normalize_fill_with, quote_ident, recreate_indexes_after_rebuild,
-    require_column_in_table, require_table_in_schema,
+    build_sea_column_def_with_table, build_sqlite_table_rebuild, convert_default_for_backend,
+    normalize_fill_with, quote_ident, require_column_in_table, require_table_in_schema,
 };
-use super::rename_table::build_rename_table;
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
 use crate::error::QueryError;
 
@@ -86,49 +84,25 @@ pub fn build_modify_column_nullable(
             queries.push(BuiltQuery::AlterTable(Box::new(stmt)));
         }
         DatabaseBackend::Sqlite => {
-            // SQLite doesn't support ALTER COLUMN for nullability changes
-            // Use temporary table approach
+            // SQLite doesn't support ALTER COLUMN for nullability changes;
+            // use the canonical temp-table rebuild with the modified column.
             let table_def = require_table_in_schema(
                 current_schema,
                 table,
                 "SQLite requires current schema information to modify column nullability",
             )?;
 
-            // Create modified columns with the new nullability
             let mut new_columns = table_def.columns.clone();
             if let Some(col) = new_columns.iter_mut().find(|c| c.name == column) {
                 col.nullable = nullable;
             }
 
-            // Generate temporary table name
-            let temp_table = format!("{table}_temp");
-
-            // 1. Create temporary table with modified column + CHECK constraints
-            let create_query = build_sqlite_temp_table_create(
+            queries.extend(build_sqlite_table_rebuild(
                 backend,
-                &temp_table,
                 table,
                 &new_columns,
                 &table_def.constraints,
-            );
-            queries.push(create_query);
-
-            // 2. Copy data (all columns)
-            queries.push(build_copy_into_temp_table(
-                table,
-                &temp_table,
                 &table_def.columns,
-            ));
-
-            // 3. Drop original table
-            queries.push(super::delete_table::build_delete_table(table));
-
-            // 4. Rename temporary table to original name
-            queries.push(build_rename_table(&temp_table, table));
-
-            // 5. Recreate indexes (both regular and UNIQUE)
-            queries.extend(recreate_indexes_after_rebuild(
-                table,
                 &table_def.constraints,
                 pending_constraints,
             ));
diff --git a/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs b/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs
index 8f555f66..3d6d0a9b 100644
--- a/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs
+++ b/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs
@@ -3,11 +3,7 @@ use std::collections::BTreeMap;
 use vespertide_core::{ColumnType, TableConstraint, TableDef};
 
 use crate::error::QueryError;
-use crate::sql::helpers::{
-    build_copy_into_temp_table, build_sqlite_temp_table_create, recreate_indexes_after_rebuild,
-    require_table_in_schema,
-};
-use crate::sql::rename_table::build_rename_table;
+use crate::sql::helpers::{build_sqlite_table_rebuild, require_table_in_schema};
 use crate::sql::types::{BuiltQuery, DatabaseBackend};
 
 /// Build the canonical `SQLite` temp-table rebuild sequence for column type changes.
@@ -20,14 +16,12 @@ pub(super) fn build_modify_column_type_sqlite_temp_table(
     current_schema: &[TableDef],
     pending_constraints: &[TableConstraint],
 ) -> Result, QueryError> {
-    // Current schema information is required
     let table_def = require_table_in_schema(
         current_schema,
         table,
         "SQLite requires current schema information to modify column types",
     )?;
 
-    // Create new column definitions with the modified column
     let mut new_columns = table_def.columns.clone();
     let col_index = new_columns
         .iter()
@@ -35,43 +29,25 @@ pub(super) fn build_modify_column_type_sqlite_temp_table(
         .ok_or_else(|| {
             QueryError::SchemaError(format!("Column '{column}' not found in table '{table}'"))
         })?;
-
     new_columns[col_index].r#type = new_type.clone();
 
-    // Generate temporary table name
-    let temp_table = format!("{table}_temp");
-
-    // 1. Create temporary table with new column types + CHECK constraints
-    let create_query = build_sqlite_temp_table_create(
-        backend,
-        &temp_table,
-        table,
-        &new_columns,
-        &table_def.constraints,
-    );
-
-    // 2. Copy data (all columns) - Use INSERT INTO ... SELECT
-    let insert_query = build_copy_into_temp_table(table, &temp_table, &new_columns);
-
-    // 3. Drop original table
-    let drop_query = crate::sql::delete_table::build_delete_table(table);
-
-    // 4. Rename temporary table to original name
-    let rename_query = build_rename_table(&temp_table, table);
-
-    // 5. Recreate indexes (both regular and UNIQUE)
-    let index_queries =
-        recreate_indexes_after_rebuild(table, &table_def.constraints, pending_constraints);
-
     let mut queries = Vec::new();
 
-    // Insert fill_with UPDATE statements before table recreation
+    // Fill-with UPDATE statements run BEFORE the rebuild so the rows
+    // copied into the temp table already carry the remapped values.
     if let Some(fw) = fill_with {
         queries.extend(super::build_fill_with_updates(table, column, fw));
     }
 
-    queries.extend([create_query, insert_query, drop_query, rename_query]);
-    queries.extend(index_queries);
+    queries.extend(build_sqlite_table_rebuild(
+        backend,
+        table,
+        &new_columns,
+        &table_def.constraints,
+        &new_columns,
+        &table_def.constraints,
+        pending_constraints,
+    ));
 
     Ok(queries)
 }
diff --git a/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs b/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs
index 82f73ef4..a3dd93b1 100644
--- a/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs
+++ b/crates/vespertide-query/src/sql/remove_constraint/sqlite.rs
@@ -1,11 +1,7 @@
 use vespertide_core::{TableConstraint, TableDef};
 
 use crate::error::QueryError;
-use crate::sql::helpers::{
-    build_copy_into_temp_table, build_sqlite_temp_table_create, recreate_indexes_after_rebuild,
-    require_table_in_schema,
-};
-use crate::sql::rename_table::build_rename_table;
+use crate::sql::helpers::{build_sqlite_table_rebuild, require_table_in_schema};
 use crate::sql::types::{BuiltQuery, DatabaseBackend};
 
 pub fn requires_rebuild(constraint: &TableConstraint) -> bool {
@@ -124,24 +120,16 @@ fn rebuild_table_without_constraint(
     constraints_to_recreate: &[TableConstraint],
     pending_constraints: &[TableConstraint],
 ) -> Vec {
-    let temp_table = format!("{table}_temp");
-
-    // SQLite has no native ALTER TABLE DROP CONSTRAINT. Keep the canonical rebuild sequence:
-    // create temp table, copy rows, drop original, rename temp, then recreate indexes.
-    let create_query = build_sqlite_temp_table_create(
+    // SQLite has no native ALTER TABLE DROP CONSTRAINT. Use the canonical
+    // rebuild sequence: create temp table without the removed constraint,
+    // copy rows, drop original, rename temp, then recreate indexes.
+    build_sqlite_table_rebuild(
         DatabaseBackend::Sqlite,
-        &temp_table,
         table,
         &table_def.columns,
         new_constraints,
-    );
-    let insert_query = build_copy_into_temp_table(table, &temp_table, &table_def.columns);
-    let drop_query = crate::sql::delete_table::build_delete_table(table);
-    let rename_query = build_rename_table(&temp_table, table);
-    let index_queries =
-        recreate_indexes_after_rebuild(table, constraints_to_recreate, pending_constraints);
-
-    let mut queries = vec![create_query, insert_query, drop_query, rename_query];
-    queries.extend(index_queries);
-    queries
+        &table_def.columns,
+        constraints_to_recreate,
+        pending_constraints,
+    )
 }
diff --git a/crates/vespertide-query/src/sql/replace_constraint.rs b/crates/vespertide-query/src/sql/replace_constraint.rs
index c6d84fec..dab7d794 100644
--- a/crates/vespertide-query/src/sql/replace_constraint.rs
+++ b/crates/vespertide-query/src/sql/replace_constraint.rs
@@ -2,11 +2,7 @@ use sea_query::{Alias, ForeignKey};
 
 use vespertide_core::{TableConstraint, TableDef};
 
-use super::helpers::{
-    build_copy_into_temp_table, build_sqlite_temp_table_create, recreate_indexes_after_rebuild,
-    require_table_in_schema, to_sea_fk_action,
-};
-use super::rename_table::build_rename_table;
+use super::helpers::{build_sqlite_table_rebuild, require_table_in_schema, to_sea_fk_action};
 use super::types::{BuiltQuery, DatabaseBackend};
 use crate::error::QueryError;
 
@@ -194,40 +190,22 @@ fn build_sqlite_constraint_replace(
         "SQLite requires current schema information to replace constraints",
     )?;
 
-    // Build new constraints: replace old constraint with new one
+    // Build new constraints: replace old constraint with new one.
     let new_constraints: Vec = table_def
         .constraints
         .iter()
         .map(|c| if c == from { to.clone() } else { c.clone() })
         .collect();
 
-    let temp_table = format!("{table}_temp");
-
-    // 1. Create temporary table with replaced constraint
-    let create_query = build_sqlite_temp_table_create(
+    Ok(build_sqlite_table_rebuild(
         backend,
-        &temp_table,
         table,
         &table_def.columns,
         &new_constraints,
-    );
-
-    // 2. Copy data (all columns)
-    let insert_query = build_copy_into_temp_table(table, &temp_table, &table_def.columns);
-
-    // 3. Drop original table
-    let drop_query = super::delete_table::build_delete_table(table);
-
-    // 4. Rename temporary table to original name
-    let rename_query = build_rename_table(&temp_table, table);
-
-    // 5. Recreate indexes (both regular and UNIQUE)
-    let index_queries =
-        recreate_indexes_after_rebuild(table, &table_def.constraints, pending_constraints);
-
-    let mut queries = vec![create_query, insert_query, drop_query, rename_query];
-    queries.extend(index_queries);
-    Ok(queries)
+        &table_def.columns,
+        &table_def.constraints,
+        pending_constraints,
+    ))
 }
 
 #[cfg(test)]

From 9cd2a99fd3d441b01fee0e4ac4878b0169909249 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 08:19:05 +0900
Subject: [PATCH 033/109] retry-now#0032: batch -- swap 3 .unwrap()s for
 invariant-stating .expect()s / let-else (3 kept)

---
 .../src/diagnostics/validation/visitors.rs           |  4 +++-
 crates/vespertide-lsp/src/parser.rs                  | 12 ++++++++++--
 crates/vespertide-query/src/sql/add_column.rs        |  2 +-
 3 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
index 43256f2f..85a46857 100644
--- a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
+++ b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
@@ -439,7 +439,9 @@ fn check_enum_shape(
         return;
     }
 
-    let values_pair = values_pair.unwrap();
+    let Some(values_pair) = values_pair else {
+        return;
+    };
     if let Some(values_value_raw) = values_pair.named_child(1) {
         let values_value = unwrap_yaml_node(values_value_raw);
         if !matches!(
diff --git a/crates/vespertide-lsp/src/parser.rs b/crates/vespertide-lsp/src/parser.rs
index 4f3de901..82979652 100644
--- a/crates/vespertide-lsp/src/parser.rs
+++ b/crates/vespertide-lsp/src/parser.rs
@@ -70,8 +70,16 @@ impl ParserPool {
     #[must_use]
     pub fn parse(&self, text: &str, format: DocumentFormat) -> Option {
         match format {
-            DocumentFormat::Json => self.json.lock().unwrap().parse(text, None),
-            DocumentFormat::Yaml => self.yaml.lock().unwrap().parse(text, None),
+            DocumentFormat::Json => self
+                .json
+                .lock()
+                .expect("ParserPool lock poisoned — invariant: tree_sitter::Parser::parse must not panic")
+                .parse(text, None),
+            DocumentFormat::Yaml => self
+                .yaml
+                .lock()
+                .expect("ParserPool lock poisoned — invariant: tree_sitter::Parser::parse must not panic")
+                .parse(text, None),
         }
     }
 }
diff --git a/crates/vespertide-query/src/sql/add_column.rs b/crates/vespertide-query/src/sql/add_column.rs
index 96086961..4a6811a9 100644
--- a/crates/vespertide-query/src/sql/add_column.rs
+++ b/crates/vespertide-query/src/sql/add_column.rs
@@ -93,7 +93,7 @@ pub fn build_add_column(
             .into_table(Alias::new(&temp_table))
             .columns(columns_alias)
             .select_from(select_query)
-            .unwrap()
+            .expect("SQLite temp table copy SELECT should be valid")
             .to_owned();
         let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
 

From a8bced95377e21cbbe96abd654987d8c6cddb77e Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 08:45:02 +0900
Subject: [PATCH 034/109] retry-now#0033: batch -- restore cargo fmt, dedupe
 column_names_to_strings, hoist find_baseline_column (3 kept)

---
 crates/vespertide-cli/src/commands/erd/mod.rs | 18 ++++-------
 .../src/commands/erd/svg/edges.rs             | 31 ++++++++++++-------
 .../vespertide-planner/src/drop_resolution.rs | 28 +++++++++++------
 crates/vespertide-planner/src/test_support.rs |  4 +--
 .../src/validate/check_additions.rs           | 16 ++++++++--
 .../src/validate/fk_orphan_additions.rs       | 19 ++++++++----
 crates/vespertide-query/src/sql/mod.rs        |  4 +--
 .../vespertide-query/src/sql/tests/naming.rs  | 20 +++---------
 8 files changed, 78 insertions(+), 62 deletions(-)

diff --git a/crates/vespertide-cli/src/commands/erd/mod.rs b/crates/vespertide-cli/src/commands/erd/mod.rs
index a4600c88..189763ed 100644
--- a/crates/vespertide-cli/src/commands/erd/mod.rs
+++ b/crates/vespertide-cli/src/commands/erd/mod.rs
@@ -8,6 +8,7 @@ use std::path::PathBuf;
 use anyhow::{Context, Result};
 use clap::ValueEnum;
 use vespertide_core::schema::foreign_key::ForeignKeySyntax;
+use vespertide_core::schema::names::names_to_strings;
 use vespertide_core::schema::primary_key::PrimaryKeySyntax;
 use vespertide_core::{ColumnDef, ReferenceAction, StrOrBoolOrArray, TableConstraint, TableDef};
 
@@ -195,9 +196,9 @@ pub(super) fn collect_foreign_key_relations(tables: &[TableDef]) -> BTreeSet (
             definition.ref_table.to_string(),
-            column_names_to_strings(&definition.ref_columns),
+            names_to_strings(&definition.ref_columns),
             definition.on_delete.clone(),
             definition.on_update.clone(),
         ),
@@ -429,7 +430,7 @@ fn primary_key_columns(table: &TableDef) -> Vec {
             None
         }
     }) {
-        return column_names_to_strings(&columns);
+        return names_to_strings(&columns);
     }
 
     table
@@ -446,7 +447,7 @@ fn foreign_key_column_groups(table: &TableDef) -> Vec> {
         if let TableConstraint::ForeignKey { columns, .. } = constraint
             && !groups.iter().any(|group| same_column_set(group, columns))
         {
-            groups.push(column_names_to_strings(columns));
+            groups.push(names_to_strings(columns));
         }
     }
 
@@ -520,13 +521,6 @@ fn same_column_set, U: AsRef>(left: &[T], right: &[U]) -> boo
     left == right
 }
 
-fn column_names_to_strings>(columns: &[T]) -> Vec {
-    columns
-        .iter()
-        .map(|column| column.as_ref().to_string())
-        .collect()
-}
-
 fn normalized_filter_names(names: &[String]) -> Vec {
     names
         .iter()
diff --git a/crates/vespertide-cli/src/commands/erd/svg/edges.rs b/crates/vespertide-cli/src/commands/erd/svg/edges.rs
index 79f0da72..6868ad69 100644
--- a/crates/vespertide-cli/src/commands/erd/svg/edges.rs
+++ b/crates/vespertide-cli/src/commands/erd/svg/edges.rs
@@ -54,11 +54,7 @@ pub(super) struct EdgeGeometry {
     pub curvature: f64,
 }
 
-pub(super) fn edge_geometry(
-    child: &TableBox,
-    parent: &TableBox,
-    edge: &EdgeSpec,
-) -> EdgeGeometry {
+pub(super) fn edge_geometry(child: &TableBox, parent: &TableBox, edge: &EdgeSpec) -> EdgeGeometry {
     let child_y = child.y + HEADER_H + edge.child_row as f64 * ROW_H + ROW_H / 2.0;
     let parent_y = parent.y + HEADER_H + edge.parent_row as f64 * ROW_H + ROW_H / 2.0;
     let (sx, sy, ex, ey, sdir, edir) = pick_anchors(child, parent, child_y, parent_y);
@@ -81,7 +77,15 @@ pub(super) fn render_edge_path(
     edge: &EdgeSpec,
     geom: EdgeGeometry,
 ) {
-    let path = bezier_path(geom.sx, geom.sy, geom.ex, geom.ey, geom.sdir, geom.edir, geom.curvature);
+    let path = bezier_path(
+        geom.sx,
+        geom.sy,
+        geom.ex,
+        geom.ey,
+        geom.sdir,
+        geom.edir,
+        geom.curvature,
+    );
 
     // Two-layer stroke: subtle wide halo + crisp narrow stroke for a soft look.
     let _ = writeln!(
@@ -98,16 +102,19 @@ pub(super) fn render_edge_path(
     );
 }
 
-pub(super) fn render_edge_label(
-    out: &mut String,
-    edge: &EdgeSpec,
-    geom: EdgeGeometry,
-) {
+pub(super) fn render_edge_label(out: &mut String, edge: &EdgeSpec, geom: EdgeGeometry) {
     // Label position is spread along the curve for parallel edges so the
     // cardinality badges no longer stack on top of one another.
     let label_t = label_t_for_parallel(edge.parallel_index, edge.parallel_count);
     let (label_x, label_y) = bezier_at(
-        geom.sx, geom.sy, geom.ex, geom.ey, geom.sdir, geom.edir, geom.curvature, label_t,
+        geom.sx,
+        geom.sy,
+        geom.ex,
+        geom.ey,
+        geom.sdir,
+        geom.edir,
+        geom.curvature,
+        label_t,
     );
 
     // Pill-shaped white background guarantees the label stays readable when
diff --git a/crates/vespertide-planner/src/drop_resolution.rs b/crates/vespertide-planner/src/drop_resolution.rs
index 7f92e8c2..6f10814f 100644
--- a/crates/vespertide-planner/src/drop_resolution.rs
+++ b/crates/vespertide-planner/src/drop_resolution.rs
@@ -151,6 +151,23 @@ pub fn find_drop_resolutions(plan: &MigrationPlan, baseline: &[TableDef]) -> Vec
     out
 }
 
+/// Locate a column in the baseline schema by table and column name.
+///
+/// Folds the two open-coded `baseline.iter().find(...).and_then(...)` chains
+/// in this module (`resolve_column_drop`, `apply_column_rename`) into a single
+/// named helper. Returns a borrowed `&ColumnDef`; callers `.cloned()` when
+/// they need ownership.
+fn find_baseline_column<'a>(
+    baseline: &'a [TableDef],
+    table: &str,
+    column: &str,
+) -> Option<&'a ColumnDef> {
+    baseline
+        .iter()
+        .find(|t| t.name == table)
+        .and_then(|t| t.columns.iter().find(|c| c.name == column))
+}
+
 fn resolve_column_drop(
     action_index: usize,
     table: &str,
@@ -162,10 +179,7 @@ fn resolve_column_drop(
     // against the candidates. If baseline lookup fails we still emit a
     // resolution — the user will see a Drop / Cancel prompt with no
     // candidates rather than crash.
-    let dropped = baseline
-        .iter()
-        .find(|t| t.name == table)
-        .and_then(|t| t.columns.iter().find(|c| c.name == column));
+    let dropped = find_baseline_column(baseline, table, column);
 
     let column_type = dropped.map_or_else(|| "(unknown)".to_string(), render_column_type);
 
@@ -452,11 +466,7 @@ fn apply_column_rename(
     };
 
     // Look up the old column's properties in the baseline.
-    let baseline_col = baseline
-        .iter()
-        .find(|t| t.name == table)
-        .and_then(|t| t.columns.iter().find(|c| c.name == old_column))
-        .cloned();
+    let baseline_col = find_baseline_column(baseline, table, old_column).cloned();
 
     // Compute follow-up actions BEFORE mutating the plan so we can append
     // them in a deterministic order right after the RenameColumn.
diff --git a/crates/vespertide-planner/src/test_support.rs b/crates/vespertide-planner/src/test_support.rs
index 6166a1b2..3bfd2529 100644
--- a/crates/vespertide-planner/src/test_support.rs
+++ b/crates/vespertide-planner/src/test_support.rs
@@ -5,8 +5,8 @@
 //! `tests/mod.rs` entry can reuse the same implementation.
 
 use vespertide_core::{
-    CheckViolationStrategy, ColumnDef, ColumnType, MigrationAction, MigrationPlan, SimpleColumnType,
-    TableConstraint, TableDef,
+    CheckViolationStrategy, ColumnDef, ColumnType, MigrationAction, MigrationPlan,
+    SimpleColumnType, TableConstraint, TableDef,
 };
 
 /// Default test column (NOT NULL). Mirrors the production convention that
diff --git a/crates/vespertide-planner/src/validate/check_additions.rs b/crates/vespertide-planner/src/validate/check_additions.rs
index 3d33f04e..c1478444 100644
--- a/crates/vespertide-planner/src/validate/check_additions.rs
+++ b/crates/vespertide-planner/src/validate/check_additions.rs
@@ -167,7 +167,10 @@ mod tests {
 
     #[rstest]
     fn case_03_in_clause_flagged() {
-        let baseline = vec![table("orders", vec![col_int("id", false), col_int("status", true)])];
+        let baseline = vec![table(
+            "orders",
+            vec![col_int("id", false), col_int("status", true)],
+        )];
         let p = plan(vec![add_check(
             "orders",
             "chk_status",
@@ -194,7 +197,10 @@ mod tests {
 
     #[rstest]
     fn case_05_function_call_skipped() {
-        let baseline = vec![table("users", vec![col_int("id", false), col_int("name", true)])];
+        let baseline = vec![table(
+            "users",
+            vec![col_int("id", false), col_int("name", true)],
+        )];
         let p = plan(vec![add_check("users", "chk_name", "length(name) > 0")]);
         let ws = find_check_additions(&p, &baseline);
         assert!(ws.is_empty());
@@ -225,7 +231,11 @@ mod tests {
     fn case_08_multiple_checks_each_flagged_separately() {
         let baseline = vec![table(
             "products",
-            vec![col_int("id", false), col_int("price", true), col_int("stock", true)],
+            vec![
+                col_int("id", false),
+                col_int("price", true),
+                col_int("stock", true),
+            ],
         )];
         let p = plan(vec![
             add_check("products", "chk_price", "price > 0"),
diff --git a/crates/vespertide-planner/src/validate/fk_orphan_additions.rs b/crates/vespertide-planner/src/validate/fk_orphan_additions.rs
index 2f8b305f..36e3e786 100644
--- a/crates/vespertide-planner/src/validate/fk_orphan_additions.rs
+++ b/crates/vespertide-planner/src/validate/fk_orphan_additions.rs
@@ -133,9 +133,7 @@ mod tests {
     use super::*;
     use crate::test_support::{col_int, plan};
     use rstest::rstest;
-    use vespertide_core::{
-        ColumnDef, MigrationAction, TableConstraint, TableDef, TableName,
-    };
+    use vespertide_core::{ColumnDef, MigrationAction, TableConstraint, TableDef, TableName};
 
     fn table(name: &str, cols: Vec) -> TableDef {
         TableDef {
@@ -170,7 +168,10 @@ mod tests {
     #[rstest]
     fn case_01_existing_nullable_column_flagged_with_nullify_available() {
         // Child column exists in baseline and is nullable -> warning + NullifyOrphans valid.
-        let baseline = vec![table("posts", vec![col_int("id", false), col_int("user_id", true)])];
+        let baseline = vec![table(
+            "posts",
+            vec![col_int("id", false), col_int("user_id", true)],
+        )];
         let p = plan(vec![add_fk(
             "posts",
             Some("fk_user"),
@@ -238,7 +239,10 @@ mod tests {
     #[rstest]
     fn case_05_composite_fk_mixed_existing_and_new_skipped() {
         // Composite FK with one new column -> Edge #1's responsibility, skipped here.
-        let baseline = vec![table("audit", vec![col_int("id", false), col_int("team_id", true)])];
+        let baseline = vec![table(
+            "audit",
+            vec![col_int("id", false), col_int("team_id", true)],
+        )];
         let p = plan(vec![add_fk(
             "audit",
             None,
@@ -307,7 +311,10 @@ mod tests {
     /// action variant in addition to the warned one.
     #[rstest]
     fn case_09_mixed_plan_only_emits_fk_warning_and_skips_other_actions() {
-        let baseline = vec![table("posts", vec![col_int("id", false), col_int("user_id", true)])];
+        let baseline = vec![table(
+            "posts",
+            vec![col_int("id", false), col_int("user_id", true)],
+        )];
         let p = plan(vec![
             // 0: AddConstraint Unique - not a FK, hits let-else continue
             MigrationAction::AddConstraint {
diff --git a/crates/vespertide-query/src/sql/mod.rs b/crates/vespertide-query/src/sql/mod.rs
index ca7e3e09..5ede24de 100644
--- a/crates/vespertide-query/src/sql/mod.rs
+++ b/crates/vespertide-query/src/sql/mod.rs
@@ -88,8 +88,8 @@ pub fn build_action_queries_with_pending(
 
         MigrationAction::DeleteColumn { table, column } => {
             // Find the column type from current schema for enum DROP TYPE support
-            let column_type = helpers::find_column_in_schema(current_schema, table, column)
-                .map(|c| &c.r#type);
+            let column_type =
+                helpers::find_column_in_schema(current_schema, table, column).map(|c| &c.r#type);
             Ok(build_delete_column(
                 backend,
                 table,
diff --git a/crates/vespertide-query/src/sql/tests/naming.rs b/crates/vespertide-query/src/sql/tests/naming.rs
index ffef0d88..3ba06843 100644
--- a/crates/vespertide-query/src/sql/tests/naming.rs
+++ b/crates/vespertide-query/src/sql/tests/naming.rs
@@ -328,10 +328,7 @@ fn test_build_action_queries_modify_column_nullable(#[case] backend: DatabaseBac
     assert!(sql.contains("UPDATE"));
     assert!(sql.contains("unknown"));
 
-    let suffix = format!(
-        "{}_modify_nullable",
-        backend_tag(backend)
-    );
+    let suffix = format!("{}_modify_nullable", backend_tag(backend));
 
     with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => suffix }, {
         assert_snapshot!(sql);
@@ -377,10 +374,7 @@ fn test_build_action_queries_modify_column_default(#[case] backend: DatabaseBack
     // Should contain DEFAULT and 'active'
     assert!(sql.contains("DEFAULT") || sql.contains("active"));
 
-    let suffix = format!(
-        "{}_modify_default",
-        backend_tag(backend)
-    );
+    let suffix = format!("{}_modify_default", backend_tag(backend));
 
     with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => suffix }, {
         assert_snapshot!(sql);
@@ -426,10 +420,7 @@ fn test_build_action_queries_modify_column_comment(#[case] backend: DatabaseBack
         assert!(sql.contains("COMMENT") || sql.contains("User email address"));
     }
 
-    let suffix = format!(
-        "{}_modify_comment",
-        backend_tag(backend)
-    );
+    let suffix = format!("{}_modify_comment", backend_tag(backend));
 
     with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => suffix }, {
         assert_snapshot!(sql);
@@ -588,10 +579,7 @@ fn test_replace_fk_constraint(#[case] backend: DatabaseBackend) {
         .collect::>()
         .join(";\n");
 
-    let suffix = format!(
-        "replace_fk_constraint_{}",
-        backend_tag(backend)
-    );
+    let suffix = format!("replace_fk_constraint_{}", backend_tag(backend));
 
     with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => suffix }, {
         assert_snapshot!(sql);

From 8e8cc73fbb50ae86fa66fac666f98749a9f050ae Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 09:13:00 +0900
Subject: [PATCH 035/109] retry-now#0034: batch -- hoist MySQL+SQLite
 modify-column dispatch, push_postgres_raw, extend_fill_with_updates (3 kept)

---
 crates/vespertide-query/src/sql/helpers.rs    | 81 ++++++++++++++++++-
 .../src/sql/modify_column_default.rs          | 58 ++++---------
 .../src/sql/modify_column_nullable.rs         | 60 ++++----------
 .../src/sql/modify_column_type/direct.rs      | 74 +++++++++--------
 .../src/sql/modify_column_type/mod.rs         | 21 +++++
 .../sql/modify_column_type/sqlite_rebuild.rs  |  4 +-
 6 files changed, 171 insertions(+), 127 deletions(-)

diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index 300ca3e5..c94f21c1 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -1,6 +1,6 @@
 use sea_query::{
     Alias, ColumnDef as SeaColumnDef, ForeignKeyAction, MysqlQueryBuilder, PostgresQueryBuilder,
-    Query, QueryStatementWriter, SchemaStatementBuilder, SimpleExpr, SqliteQueryBuilder,
+    Query, QueryStatementWriter, SchemaStatementBuilder, SimpleExpr, SqliteQueryBuilder, Table,
 };
 
 use vespertide_core::{
@@ -871,6 +871,85 @@ pub(crate) fn require_column_in_table<'a>(
         })
 }
 
+/// Build the `MySQL ALTER TABLE ... MODIFY COLUMN ...` query produced by
+/// every "modify one ColumnDef field" builder (nullability, default, …).
+///
+/// Folds the byte-identical six-line MySQL dispatch — look up the table,
+/// look up the column, clone & mutate one field, hand the result to
+/// `build_sea_column_def_with_table`, wrap as an `AlterTable` — into a
+/// single helper. `context` is the descriptor appended to the canonical
+/// `require_table_in_schema` error message ("MySQL requires …"); pass it
+/// WITHOUT a trailing period (the helper adds it).
+///
+/// `mutator` rewrites whichever single field the caller cares about
+/// (`|c| c.nullable = …`, `|c| c.default = …`, …) on a fresh clone, so
+/// the original `current_schema` is never mutated.
+///
+/// `modify_column_comment` deliberately does NOT use this helper: its
+/// MySQL emit hand-appends a `COMMENT '…'` suffix outside sea-query and
+/// produces `BuiltQuery::Raw` rather than `BuiltQuery::AlterTable`.
+pub(crate) fn build_mysql_modify_column_with(
+    table: &str,
+    column: &str,
+    current_schema: &[TableDef],
+    context: &str,
+    mutator: F,
+) -> Result
+where
+    F: FnOnce(&mut ColumnDef),
+{
+    let table_def = require_table_in_schema(current_schema, table, context)?;
+    let column_def = require_column_in_table(table_def, column)?;
+    let mut modified_col_def = column_def.clone();
+    mutator(&mut modified_col_def);
+    let sea_col = build_sea_column_def_with_table(DatabaseBackend::MySql, table, &modified_col_def);
+    let stmt = Table::alter()
+        .table(Alias::new(table))
+        .modify_column(sea_col)
+        .to_owned();
+    Ok(BuiltQuery::AlterTable(Box::new(stmt)))
+}
+
+/// Build the canonical `SQLite` temp-table rebuild for every "modify one
+/// ColumnDef field" builder (nullability, default, …). Every other column
+/// stays untouched; `mutator` rewrites only the single column the action
+/// targets — matching the existing
+/// `new_columns.iter_mut().find(|c| c.name == column)` shape, including
+/// the silent no-op when the column is absent (which is the documented
+/// behaviour: see the `column_not_found` test in `modify_column_nullable`).
+///
+/// Folds the byte-identical nine-line SQLite dispatch — look up the
+/// table, clone the column list, rewrite one column, forward the rebuild
+/// contract to `build_sqlite_table_rebuild` — into a single helper.
+/// `context` is the descriptor appended to the canonical
+/// `require_table_in_schema` error message.
+pub(crate) fn build_sqlite_modify_column_with(
+    table: &str,
+    column: &str,
+    current_schema: &[TableDef],
+    pending_constraints: &[TableConstraint],
+    context: &str,
+    mutator: F,
+) -> Result, crate::QueryError>
+where
+    F: FnOnce(&mut ColumnDef),
+{
+    let table_def = require_table_in_schema(current_schema, table, context)?;
+    let mut new_columns = table_def.columns.clone();
+    if let Some(col) = new_columns.iter_mut().find(|c| c.name == column) {
+        mutator(col);
+    }
+    Ok(build_sqlite_table_rebuild(
+        DatabaseBackend::Sqlite,
+        table,
+        &new_columns,
+        &table_def.constraints,
+        &table_def.columns,
+        &table_def.constraints,
+        pending_constraints,
+    ))
+}
+
 /// Look up `column` in the table named `table` inside `schema`, returning
 /// `None` when either the table or the column is absent.
 ///
diff --git a/crates/vespertide-query/src/sql/modify_column_default.rs b/crates/vespertide-query/src/sql/modify_column_default.rs
index 3a825b72..90607259 100644
--- a/crates/vespertide-query/src/sql/modify_column_default.rs
+++ b/crates/vespertide-query/src/sql/modify_column_default.rs
@@ -1,10 +1,8 @@
-use sea_query::{Alias, Table};
-
-use vespertide_core::{ColumnDef, TableDef};
+use vespertide_core::TableDef;
 
 use super::helpers::{
-    build_sea_column_def_with_table, build_sqlite_table_rebuild, find_column_in_schema,
-    normalize_enum_default, quote_ident, require_column_in_table, require_table_in_schema,
+    build_mysql_modify_column_with, build_sqlite_modify_column_with, find_column_in_schema,
+    normalize_enum_default, quote_ident,
 };
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
 use crate::error::QueryError;
@@ -53,52 +51,26 @@ pub fn build_modify_column_default(
             queries.push(BuiltQuery::Raw(RawSql::uniform(alter_sql)));
         }
         DatabaseBackend::MySql => {
-            // MySQL requires the full column definition in ALTER COLUMN
-            let table_def = require_table_in_schema(
-                current_schema,
+            // MySQL requires the full column definition in ALTER COLUMN.
+            queries.push(build_mysql_modify_column_with(
                 table,
+                column,
+                current_schema,
                 "MySQL requires current schema information to modify column defaults",
-            )?;
-
-            let column_def = require_column_in_table(table_def, column)?;
-
-            // Create a modified column def with the new default
-            let modified_col_def = ColumnDef {
-                default: new_default.map(std::convert::Into::into),
-                ..column_def.clone()
-            };
-
-            let sea_col = build_sea_column_def_with_table(backend, table, &modified_col_def);
-
-            let stmt = Table::alter()
-                .table(Alias::new(table))
-                .modify_column(sea_col)
-                .to_owned();
-            queries.push(BuiltQuery::AlterTable(Box::new(stmt)));
+                |c| c.default = new_default.map(std::convert::Into::into),
+            )?);
         }
         DatabaseBackend::Sqlite => {
             // SQLite doesn't support ALTER COLUMN for default changes;
             // use the canonical temp-table rebuild with the modified column.
-            let table_def = require_table_in_schema(
-                current_schema,
-                table,
-                "SQLite requires current schema information to modify column defaults",
-            )?;
-
-            let mut new_columns = table_def.columns.clone();
-            if let Some(col) = new_columns.iter_mut().find(|c| c.name == column) {
-                col.default = new_default.map(std::convert::Into::into);
-            }
-
-            queries.extend(build_sqlite_table_rebuild(
-                backend,
+            queries.extend(build_sqlite_modify_column_with(
                 table,
-                &new_columns,
-                &table_def.constraints,
-                &table_def.columns,
-                &table_def.constraints,
+                column,
+                current_schema,
                 pending_constraints,
-            ));
+                "SQLite requires current schema information to modify column defaults",
+                |c| c.default = new_default.map(std::convert::Into::into),
+            )?);
         }
     }
 
diff --git a/crates/vespertide-query/src/sql/modify_column_nullable.rs b/crates/vespertide-query/src/sql/modify_column_nullable.rs
index 318f3602..165552c9 100644
--- a/crates/vespertide-query/src/sql/modify_column_nullable.rs
+++ b/crates/vespertide-query/src/sql/modify_column_nullable.rs
@@ -1,10 +1,8 @@
-use sea_query::{Alias, Table};
-
-use vespertide_core::{ColumnDef, TableDef};
+use vespertide_core::TableDef;
 
 use super::helpers::{
-    build_sea_column_def_with_table, build_sqlite_table_rebuild, convert_default_for_backend,
-    normalize_fill_with, quote_ident, require_column_in_table, require_table_in_schema,
+    build_mysql_modify_column_with, build_sqlite_modify_column_with, convert_default_for_backend,
+    normalize_fill_with, quote_ident,
 };
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
 use crate::error::QueryError;
@@ -58,54 +56,26 @@ pub fn build_modify_column_nullable(
             queries.push(BuiltQuery::Raw(RawSql::uniform(alter_sql)));
         }
         DatabaseBackend::MySql => {
-            // MySQL requires the full column definition in MODIFY COLUMN
-            // We need to get the column type from current schema
-            let table_def = require_table_in_schema(
-                current_schema,
+            // MySQL requires the full column definition in MODIFY COLUMN.
+            queries.push(build_mysql_modify_column_with(
                 table,
+                column,
+                current_schema,
                 "MySQL requires current schema information to modify column nullability",
-            )?;
-
-            let column_def = require_column_in_table(table_def, column)?;
-
-            // Create a modified column def with the new nullability
-            let modified_col_def = ColumnDef {
-                nullable,
-                ..column_def.clone()
-            };
-
-            // Build sea-query ColumnDef with all properties (type, nullable, default)
-            let sea_col = build_sea_column_def_with_table(backend, table, &modified_col_def);
-
-            let stmt = Table::alter()
-                .table(Alias::new(table))
-                .modify_column(sea_col)
-                .to_owned();
-            queries.push(BuiltQuery::AlterTable(Box::new(stmt)));
+                |c| c.nullable = nullable,
+            )?);
         }
         DatabaseBackend::Sqlite => {
             // SQLite doesn't support ALTER COLUMN for nullability changes;
             // use the canonical temp-table rebuild with the modified column.
-            let table_def = require_table_in_schema(
-                current_schema,
-                table,
-                "SQLite requires current schema information to modify column nullability",
-            )?;
-
-            let mut new_columns = table_def.columns.clone();
-            if let Some(col) = new_columns.iter_mut().find(|c| c.name == column) {
-                col.nullable = nullable;
-            }
-
-            queries.extend(build_sqlite_table_rebuild(
-                backend,
+            queries.extend(build_sqlite_modify_column_with(
                 table,
-                &new_columns,
-                &table_def.constraints,
-                &table_def.columns,
-                &table_def.constraints,
+                column,
+                current_schema,
                 pending_constraints,
-            ));
+                "SQLite requires current schema information to modify column nullability",
+                |c| c.nullable = nullable,
+            )?);
         }
     }
 
diff --git a/crates/vespertide-query/src/sql/modify_column_type/direct.rs b/crates/vespertide-query/src/sql/modify_column_type/direct.rs
index f81e4f52..d54ad4cc 100644
--- a/crates/vespertide-query/src/sql/modify_column_type/direct.rs
+++ b/crates/vespertide-query/src/sql/modify_column_type/direct.rs
@@ -10,6 +10,17 @@ use crate::sql::helpers::{
 };
 use crate::sql::types::{BuiltQuery, DatabaseBackend, RawSql};
 
+/// Push a PostgreSQL-only raw SQL statement into the per-action query list.
+///
+/// Centralises the `queries.push(BuiltQuery::Raw(RawSql::postgres_only(...)))`
+/// four-layer constructor that the enum-migration / DROP-old-type pipelines
+/// emit repeatedly. The body stays a single function call so each callsite
+/// reads as "emit one PG-only statement" without burying the SQL string
+/// under three wrapper types.
+fn push_postgres_raw(queries: &mut Vec, sql: String) {
+    queries.push(BuiltQuery::Raw(RawSql::postgres_only(sql)));
+}
+
 struct DirectBuildContext<'a> {
     backend: DatabaseBackend,
     table: &'a str,
@@ -93,13 +104,7 @@ fn build_postgres_enum_migration(queries: &mut Vec, context: &Direct
         };
 
         // 0. INSERT fill_with UPDATEs before any type changes (rows still have old enum type).
-        if let Some(fw) = context.fill_with {
-            queries.extend(super::build_fill_with_updates(
-                context.table,
-                context.column,
-                fw,
-            ));
-        }
+        super::extend_fill_with_updates(queries, context.table, context.column, context.fill_with);
 
         // 1. CREATE TYPE target_type AS ENUM (new values).
         let column_default = column_default(context);
@@ -108,41 +113,48 @@ fn build_postgres_enum_migration(queries: &mut Vec, context: &Direct
         let quoted_table = quote_ident(context.table, DatabaseBackend::Postgres);
         let quoted_column = quote_ident(context.column, DatabaseBackend::Postgres);
         let quoted_old_type = quote_ident(&old_type_name, DatabaseBackend::Postgres);
-        queries.push(BuiltQuery::Raw(RawSql::postgres_only(format!(
-            "CREATE TYPE {quoted_target_type} AS ENUM ({create_values})"
-        ))));
+        push_postgres_raw(
+            queries,
+            format!("CREATE TYPE {quoted_target_type} AS ENUM ({create_values})"),
+        );
 
         // 2. DROP DEFAULT if exists (must be done before type change).
         if column_default.is_some() {
-            queries.push(BuiltQuery::Raw(RawSql::postgres_only(format!(
-                "ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} DROP DEFAULT"
-            ))));
+            push_postgres_raw(
+                queries,
+                format!("ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} DROP DEFAULT"),
+            );
         }
 
         // 3. ALTER TABLE ... ALTER COLUMN ... TYPE target_type USING col::text::target_type.
-        queries.push(BuiltQuery::Raw(RawSql::postgres_only(format!(
-            "ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} TYPE {quoted_target_type} USING {quoted_column}::text::{quoted_target_type}"
-        ))));
+        push_postgres_raw(
+            queries,
+            format!(
+                "ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} TYPE {quoted_target_type} USING {quoted_column}::text::{quoted_target_type}"
+            ),
+        );
 
         // 4. DROP old enum type.
-        queries.push(BuiltQuery::Raw(RawSql::postgres_only(format!(
-            "DROP TYPE {quoted_old_type}"
-        ))));
+        push_postgres_raw(queries, format!("DROP TYPE {quoted_old_type}"));
 
         // 5. RENAME temp to final (only for same-name value changes).
         if needs_rename {
-            queries.push(BuiltQuery::Raw(RawSql::postgres_only(format!(
-                "ALTER TYPE {quoted_target_type} RENAME TO {quoted_old_type}"
-            ))));
+            push_postgres_raw(
+                queries,
+                format!("ALTER TYPE {quoted_target_type} RENAME TO {quoted_old_type}"),
+            );
         }
 
         // 6. Restore DEFAULT if it existed.
         if let Some(default_value) = column_default {
             let normalized_default =
                 normalize_enum_default(context.new_type, &default_value.to_sql());
-            queries.push(BuiltQuery::Raw(RawSql::postgres_only(format!(
-                "ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} SET DEFAULT {normalized_default}"
-            ))));
+            push_postgres_raw(
+                queries,
+                format!(
+                    "ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} SET DEFAULT {normalized_default}"
+                ),
+            );
         }
     }
 }
@@ -156,13 +168,7 @@ fn build_standard_type_modification(
     queries: &mut Vec,
     context: &DirectBuildContext<'_>,
 ) {
-    if let Some(fw) = context.fill_with {
-        queries.extend(super::build_fill_with_updates(
-            context.table,
-            context.column,
-            fw,
-        ));
-    }
+    super::extend_fill_with_updates(queries, context.table, context.column, context.fill_with);
 
     create_new_enum_type_if_needed(queries, context);
 
@@ -230,9 +236,7 @@ fn drop_old_enum_type_if_needed(queries: &mut Vec, context: &DirectB
         if should_drop {
             let old_type_name = crate::sql::helpers::build_enum_type_name(context.table, old_name);
             let old_type_name = quote_ident(&old_type_name, DatabaseBackend::Postgres);
-            queries.push(BuiltQuery::Raw(RawSql::postgres_only(format!(
-                "DROP TYPE {old_type_name}"
-            ))));
+            push_postgres_raw(queries, format!("DROP TYPE {old_type_name}"));
         }
     }
 }
diff --git a/crates/vespertide-query/src/sql/modify_column_type/mod.rs b/crates/vespertide-query/src/sql/modify_column_type/mod.rs
index d9173f37..aa4f67dc 100644
--- a/crates/vespertide-query/src/sql/modify_column_type/mod.rs
+++ b/crates/vespertide-query/src/sql/modify_column_type/mod.rs
@@ -167,6 +167,27 @@ fn build_fill_with_updates(
         .collect()
 }
 
+/// Conditionally prepend `fill_with` UPDATEs to `queries`.
+///
+/// Centralises the byte-identical
+/// `if let Some(fw) = fill_with { queries.extend(build_fill_with_updates(...)); }`
+/// dance that the three `modify_column_type` paths each previously
+/// open-coded (`direct::build_postgres_enum_migration`,
+/// `direct::build_standard_type_modification`, and
+/// `sqlite_rebuild::build_modify_column_type_sqlite_temp_table`). Each
+/// callsite now collapses to a single line whose name reads
+/// "if a fill_with map exists, prepend its UPDATEs".
+pub(super) fn extend_fill_with_updates(
+    queries: &mut Vec,
+    table: &str,
+    column: &str,
+    fill_with: Option<&BTreeMap>,
+) {
+    if let Some(fw) = fill_with {
+        queries.extend(build_fill_with_updates(table, column, fw));
+    }
+}
+
 pub fn build_modify_column_type(
     backend: DatabaseBackend,
     table: &str,
diff --git a/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs b/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs
index 3d6d0a9b..993ea17d 100644
--- a/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs
+++ b/crates/vespertide-query/src/sql/modify_column_type/sqlite_rebuild.rs
@@ -35,9 +35,7 @@ pub(super) fn build_modify_column_type_sqlite_temp_table(
 
     // Fill-with UPDATE statements run BEFORE the rebuild so the rows
     // copied into the temp table already carry the remapped values.
-    if let Some(fw) = fill_with {
-        queries.extend(super::build_fill_with_updates(table, column, fw));
-    }
+    super::extend_fill_with_updates(&mut queries, table, column, fill_with);
 
     queries.extend(build_sqlite_table_rebuild(
         backend,

From 0c6a4111255307f2f76c1fc0757700ed2729a54a Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 09:37:35 +0900
Subject: [PATCH 036/109] retry-now#0035: batch - dedup CHECK helpers vs SoT,
 JPA pascal-case re-export, hoist LSP node_text (3 kept)

---
 crates/vespertide-exporter/src/jpa/render.rs  | 20 +++-----
 crates/vespertide-lsp/src/code_actions.rs     | 13 ++---
 .../vespertide-lsp/src/diagnostics/locator.rs |  5 +-
 crates/vespertide-lsp/src/text_util.rs        | 13 +++++
 .../src/validate/check_between_order.rs       | 49 +++++++------------
 5 files changed, 48 insertions(+), 52 deletions(-)

diff --git a/crates/vespertide-exporter/src/jpa/render.rs b/crates/vespertide-exporter/src/jpa/render.rs
index b6a49737..5d2b393f 100644
--- a/crates/vespertide-exporter/src/jpa/render.rs
+++ b/crates/vespertide-exporter/src/jpa/render.rs
@@ -484,18 +484,14 @@ fn build_default_initializer(col: &ColumnDef) -> Option {
 // ---------------------------------------------------------------------------
 // Naming utilities
 // ---------------------------------------------------------------------------
-
-pub(super) fn to_pascal_case(s: &str) -> String {
-    s.split('_')
-        .map(|word| {
-            let mut chars = word.chars();
-            match chars.next() {
-                None => String::new(),
-                Some(first) => first.to_uppercase().chain(chars).collect(),
-            }
-        })
-        .collect()
-}
+//
+// `to_pascal_case` is shared with the SQLAlchemy and SQLModel backends via
+// `python_naming::to_pascal_case` — JPA's class-name convention happens to
+// match the snake-case-aware PascalCase the Python backends already needed.
+// `seaorm` keeps its own variant (reserved-keyword guards, different
+// allocation pattern), so it is intentionally not in scope here.
+
+pub(super) use crate::python_naming::to_pascal_case;
 
 pub(super) fn to_camel_case(s: &str) -> String {
     let pascal = to_pascal_case(s);
diff --git a/crates/vespertide-lsp/src/code_actions.rs b/crates/vespertide-lsp/src/code_actions.rs
index 295228b2..5647bbad 100644
--- a/crates/vespertide-lsp/src/code_actions.rs
+++ b/crates/vespertide-lsp/src/code_actions.rs
@@ -17,6 +17,7 @@ use std::ops::Range;
 
 use crate::parser::DocumentFormat;
 use crate::rename::DomainTextEdit;
+use crate::text_util::node_text;
 
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct DomainCodeAction {
@@ -349,7 +350,7 @@ fn toggle_bool_flag(
 ) -> Option {
     if let Some(pair) = find_pair(column, source, flag) {
         let value = pair.named_child(1)?;
-        let value_text = std::str::from_utf8(&source[value.byte_range()]).ok()?;
+        let value_text = node_text(value, source)?;
         if value_text.trim() == "true" {
             return Some(DomainCodeAction {
                 title: title_when_removing.to_string(),
@@ -380,7 +381,7 @@ fn toggle_nullable(column: tree_sitter::Node<'_>, source: &[u8]) -> Option ("false", "Make column NOT NULL"),
             "false" => ("true", "Allow NULL"),
@@ -439,7 +440,7 @@ fn is_inside_columns_array(node: tree_sitter::Node<'_>, source: &[u8]) -> bool {
     while let Some(candidate) = current {
         if candidate.kind() == "pair"
             && let Some(key) = candidate.named_child(0)
-            && let Ok(text) = std::str::from_utf8(&source[key.byte_range()])
+            && let Some(text) = node_text(key, source)
             && strip_quotes(text) == "columns"
         {
             return true;
@@ -459,7 +460,7 @@ fn find_pair<'tree>(
         child.kind() == "pair"
             && child
                 .named_child(0)
-                .and_then(|key| std::str::from_utf8(&source[key.byte_range()]).ok())
+                .and_then(|key| node_text(key, source))
                 .map(strip_quotes)
                 == Some(target_key)
     })
@@ -471,7 +472,7 @@ fn insert_pair_edit(
     new_key: &str,
     new_value: &str,
 ) -> Option {
-    let object_text = std::str::from_utf8(&source[column.byte_range()]).ok()?;
+    let object_text = node_text(column, source)?;
     let close_idx = object_text.rfind('}')?;
     let absolute_close = column.start_byte() + close_idx;
 
@@ -498,7 +499,7 @@ fn remove_pair_edit(
 ) -> Option {
     // Decide which neighbouring comma to consume so the resulting JSON has
     // no `,,` or trailing `,}`.
-    let object_text = std::str::from_utf8(&source[column.byte_range()]).ok()?;
+    let object_text = node_text(column, source)?;
     let object_start = column.start_byte();
     let pair_start = pair.start_byte() - object_start;
     let pair_end = pair.end_byte() - object_start;
diff --git a/crates/vespertide-lsp/src/diagnostics/locator.rs b/crates/vespertide-lsp/src/diagnostics/locator.rs
index d41c8e40..14000273 100644
--- a/crates/vespertide-lsp/src/diagnostics/locator.rs
+++ b/crates/vespertide-lsp/src/diagnostics/locator.rs
@@ -4,6 +4,7 @@ use std::ops::Range;
 
 use vespertide_planner::PlannerError;
 
+use crate::text_util::node_text;
 use crate::tree_util::is_pair;
 
 /// Specific column field a diagnostic should attach to. The locator narrows
@@ -410,10 +411,6 @@ fn pair_key_matches(node: tree_sitter::Node<'_>, source: &[u8], expected: &str)
         .is_some_and(|text| strip_quotes(text) == expected)
 }
 
-fn node_text<'a>(node: tree_sitter::Node<'_>, source: &'a [u8]) -> Option<&'a str> {
-    std::str::from_utf8(&source[node.byte_range()]).ok()
-}
-
 fn strip_quotes(s: &str) -> &str {
     let trimmed = s.trim();
     trimmed
diff --git a/crates/vespertide-lsp/src/text_util.rs b/crates/vespertide-lsp/src/text_util.rs
index b0aaac8f..4482160c 100644
--- a/crates/vespertide-lsp/src/text_util.rs
+++ b/crates/vespertide-lsp/src/text_util.rs
@@ -13,3 +13,16 @@ pub(crate) fn strip_quotes(s: &str) -> &str {
         .trim_start_matches('\'')
         .trim_end_matches('\'')
 }
+
+/// UTF-8 slice of a tree-sitter node's byte range. Returns `None` when the
+/// slice is not valid UTF-8 — defensive only; the LSP parsers produce valid
+/// UTF-8 spans on every source we feed them. Single source of truth for the
+/// `std::str::from_utf8(&source[node.byte_range()]).ok()` chain that used to
+/// be open-coded across `diagnostics/locator`, `code_actions`, and friends.
+#[must_use]
+pub(crate) fn node_text<'a>(
+    node: tree_sitter::Node<'_>,
+    source: &'a [u8],
+) -> Option<&'a str> {
+    std::str::from_utf8(&source[node.byte_range()]).ok()
+}
diff --git a/crates/vespertide-planner/src/validate/check_between_order.rs b/crates/vespertide-planner/src/validate/check_between_order.rs
index 6241779d..293713fe 100644
--- a/crates/vespertide-planner/src/validate/check_between_order.rs
+++ b/crates/vespertide-planner/src/validate/check_between_order.rs
@@ -76,8 +76,8 @@ pub fn find_between_boundary_reversals(table: &TableDef) -> Vec {
                 table: table.name.to_string(),
                 column,
                 check_name: name.clone(),
-                low: format_literal(&low),
-                high: format_literal(&high),
+                low: low.display_value(),
+                high: high.display_value(),
             });
         }
     }
@@ -105,34 +105,18 @@ fn collect_reversed_between(expr: &CheckExpr, out: &mut Vec<(String, Literal, Li
     }
 }
 
+/// Thin BETWEEN-specific delegate over the shared [`Literal::cmp_value`] SoT.
+///
+/// F-novel-15 intentionally treats `BETWEEN TRUE AND FALSE` as silently
+/// passing (a Bool BETWEEN has no actionable ordering for a CHECK boundary
+/// reversal). The shared SoT does return `Some(Ordering)` for `(Bool, Bool)`
+/// because the strengthening / self-contradiction validators need it, so we
+/// guard that one pair here and forward everything else.
 fn literal_compare(a: &Literal, b: &Literal) -> Option {
-    match (a, b) {
-        (Literal::Integer(x), Literal::Integer(y)) => Some(x.cmp(y)),
-        (Literal::Float(x), Literal::Float(y)) => x.partial_cmp(y),
-        (Literal::Integer(x), Literal::Float(y)) => i64_to_f64(*x).partial_cmp(y),
-        (Literal::Float(x), Literal::Integer(y)) => x.partial_cmp(&i64_to_f64(*y)),
-        (Literal::String(x), Literal::String(y)) => Some(x.cmp(y)),
-        // Mixed / Bool / Null: cannot order without ambiguity.
-        _ => None,
-    }
-}
-
-#[expect(
-    clippy::cast_precision_loss,
-    reason = "F-novel-15 BETWEEN boundary comparison: rounding integers beyond 2^53 acceptable; conservative comparator silently skips ambiguous cases anyway"
-)]
-fn i64_to_f64(v: i64) -> f64 {
-    v as f64
-}
-
-fn format_literal(lit: &Literal) -> String {
-    match lit {
-        Literal::Integer(i) => i.to_string(),
-        Literal::Float(f) => f.to_string(),
-        Literal::String(s) => s.clone(),
-        Literal::Bool(b) => b.to_string(),
-        Literal::Null => "NULL".to_string(),
+    if matches!((a, b), (Literal::Bool(_), Literal::Bool(_))) {
+        return None;
     }
+    a.cmp_value(b)
 }
 
 #[cfg(test)]
@@ -436,10 +420,15 @@ mod tests {
         ));
     }
 
+    /// F-novel-15 renders the reversed boundaries via the shared
+    /// [`Literal::display_value`] SoT (the same renderer the self-contradiction
+    /// and strengthening validators use). Pin the labels for the two variants
+    /// the parser never produces directly for a numeric BETWEEN so any future
+    /// drift in the shared renderer surfaces here too.
     #[test]
     fn literal_formatting_covers_bool_and_null_labels() {
-        assert_eq!(format_literal(&Literal::Bool(true)), "true");
-        assert_eq!(format_literal(&Literal::Null), "NULL");
+        assert_eq!(Literal::Bool(true).display_value(), "true");
+        assert_eq!(Literal::Null.display_value(), "NULL");
     }
 
     /// L99: `literal_compare(Float, Integer)` arm. Existing tests

From 57c828e75516d40a614038298754c8de7457ca6f Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 10:02:07 +0900
Subject: [PATCH 037/109] =?UTF-8?q?retry-now#0036:=20batch=20=E2=80=94=20r?=
 =?UTF-8?q?estore=20cargo=20fmt=20green,=20hoist=20tree=5Futil::ancestor?=
 =?UTF-8?q?=5Fpair=5Fwith=5Fkey,=20hoist=20text=5Futil::strip=5Fjson=5Fquo?=
 =?UTF-8?q?tes=20(3=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Item 1 (kept): collapse multi-line node_text signature in text_util.rs so cargo fmt --all -- --check exits 0.

Item 2 (kept): add pub(crate) tree_util::ancestor_pair_with_key (parent-only sibling of enclosing_pair_with_key) and replace 3 byte-equivalent is_inside_* helpers in completion::context::is_inside_constraints, hover::check_expr::is_inside_constraints, and hover::column::is_inside_columns with one-line delegations.

Item 3 (kept): add pub(crate) text_util::strip_json_quotes (greedy double-quote-only peeler that preserves inner SQL single quotes) and replace 3 local strip_quotes copies + 9 call sites in code_actions, workspace_index, and diagnostics::locator with the canonical helper.
---
 crates/vespertide-lsp/src/code_actions.rs     | 16 ++++------
 .../vespertide-lsp/src/completion/context.rs  | 13 +++-----
 .../vespertide-lsp/src/diagnostics/locator.rs | 19 ++----------
 crates/vespertide-lsp/src/hover/check_expr.rs | 16 ++--------
 crates/vespertide-lsp/src/hover/column.rs     | 14 ++-------
 crates/vespertide-lsp/src/text_util.rs        | 23 +++++++++++---
 crates/vespertide-lsp/src/tree_util.rs        | 30 +++++++++++++++++++
 crates/vespertide-lsp/src/workspace_index.rs  | 11 +++----
 8 files changed, 70 insertions(+), 72 deletions(-)

diff --git a/crates/vespertide-lsp/src/code_actions.rs b/crates/vespertide-lsp/src/code_actions.rs
index 5647bbad..bb4f2b4d 100644
--- a/crates/vespertide-lsp/src/code_actions.rs
+++ b/crates/vespertide-lsp/src/code_actions.rs
@@ -17,7 +17,7 @@ use std::ops::Range;
 
 use crate::parser::DocumentFormat;
 use crate::rename::DomainTextEdit;
-use crate::text_util::node_text;
+use crate::text_util::{node_text, strip_json_quotes};
 
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct DomainCodeAction {
@@ -229,7 +229,7 @@ fn type_conversions(column: tree_sitter::Node<'_>, source: &[u8]) -> Vec {
                 out.push(replace_type_action(
@@ -281,7 +281,7 @@ fn enum_extraction(column: tree_sitter::Node<'_>, source: &[u8]) -> Vec, source: &[u8]) -> Vec, source: &[u8]) -> bool {
         if candidate.kind() == "pair"
             && let Some(key) = candidate.named_child(0)
             && let Some(text) = node_text(key, source)
-            && strip_quotes(text) == "columns"
+            && strip_json_quotes(text) == "columns"
         {
             return true;
         }
@@ -461,7 +461,7 @@ fn find_pair<'tree>(
             && child
                 .named_child(0)
                 .and_then(|key| node_text(key, source))
-                .map(strip_quotes)
+                .map(strip_json_quotes)
                 == Some(target_key)
     })
 }
@@ -534,10 +534,6 @@ fn remove_pair_edit(
     })
 }
 
-fn strip_quotes(text: &str) -> &str {
-    text.trim().trim_start_matches('"').trim_end_matches('"')
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/vespertide-lsp/src/completion/context.rs b/crates/vespertide-lsp/src/completion/context.rs
index 560d47ed..5c46e8ec 100644
--- a/crates/vespertide-lsp/src/completion/context.rs
+++ b/crates/vespertide-lsp/src/completion/context.rs
@@ -6,7 +6,9 @@ use vespertide_planner::{CheckToken, CheckTokenKind, lex_check_expr};
 
 use crate::check_expr_range::expr_inner_range;
 use crate::text_util::strip_quotes;
-use crate::tree_util::{direct_child_value, enclosing_pair_with_key, is_pair};
+use crate::tree_util::{
+    ancestor_pair_with_key, direct_child_value, enclosing_pair_with_key, is_pair,
+};
 
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub(super) enum Context {
@@ -245,14 +247,7 @@ fn keyword_expects_operand(keyword: &str) -> bool {
 }
 
 fn is_inside_constraints(node: tree_sitter::Node<'_>, source: &str) -> bool {
-    let mut current = node.parent();
-    while let Some(candidate) = current {
-        if is_pair(candidate) && key_text(candidate, source) == Some("constraints") {
-            return true;
-        }
-        current = candidate.parent();
-    }
-    false
+    ancestor_pair_with_key(node, source, "constraints").is_some()
 }
 
 fn current_table_columns(node: tree_sitter::Node<'_>, source: &str) -> Vec {
diff --git a/crates/vespertide-lsp/src/diagnostics/locator.rs b/crates/vespertide-lsp/src/diagnostics/locator.rs
index 14000273..f7e45248 100644
--- a/crates/vespertide-lsp/src/diagnostics/locator.rs
+++ b/crates/vespertide-lsp/src/diagnostics/locator.rs
@@ -4,7 +4,7 @@ use std::ops::Range;
 
 use vespertide_planner::PlannerError;
 
-use crate::text_util::node_text;
+use crate::text_util::{node_text, strip_json_quotes};
 use crate::tree_util::is_pair;
 
 /// Specific column field a diagnostic should attach to. The locator narrows
@@ -391,7 +391,7 @@ fn mapping_has_name(node: tree_sitter::Node<'_>, source: &[u8], target_name: &st
         if is_pair(child)
             && pair_key_matches(child, source, "name")
             && let Some(value) = child.named_child(1)
-            && node_text(value, source).is_some_and(|text| strip_quotes(text) == target_name)
+            && node_text(value, source).is_some_and(|text| strip_json_quotes(text) == target_name)
         {
             return true;
         }
@@ -408,20 +408,7 @@ fn pair_key_matches(node: tree_sitter::Node<'_>, source: &[u8], expected: &str)
     // same `false` result without a separate defensive `return` line.
     node.named_child(0)
         .and_then(|key| node_text(key, source))
-        .is_some_and(|text| strip_quotes(text) == expected)
-}
-
-fn strip_quotes(s: &str) -> &str {
-    let trimmed = s.trim();
-    trimmed
-        .strip_prefix('"')
-        .and_then(|without_prefix| without_prefix.strip_suffix('"'))
-        .or_else(|| {
-            trimmed
-                .strip_prefix('\'')
-                .and_then(|without_prefix| without_prefix.strip_suffix('\''))
-        })
-        .unwrap_or(trimmed)
+        .is_some_and(|text| strip_json_quotes(text) == expected)
 }
 
 #[cfg(test)]
diff --git a/crates/vespertide-lsp/src/hover/check_expr.rs b/crates/vespertide-lsp/src/hover/check_expr.rs
index 1ccf2f6f..e8e6ca24 100644
--- a/crates/vespertide-lsp/src/hover/check_expr.rs
+++ b/crates/vespertide-lsp/src/hover/check_expr.rs
@@ -11,7 +11,7 @@ use vespertide_planner::{CheckExprAst, CheckExprLiteral, CheckExprOp, parse_chec
 
 use super::DomainHover;
 use crate::check_expr_range::expr_inner_range;
-use crate::tree_util::is_pair;
+use crate::tree_util::{ancestor_pair_with_key, is_pair};
 
 pub(super) fn try_hover(
     node: tree_sitter::Node<'_>,
@@ -235,19 +235,7 @@ fn expr_pair_ancestor<'tree>(
 /// True when any ancestor pair has key `"constraints"`. Mirrors
 /// `column::is_inside_columns` from the column-hover handler.
 fn is_inside_constraints(node: tree_sitter::Node<'_>, source: &str) -> bool {
-    let mut cur = node.parent();
-    while let Some(candidate) = cur {
-        if is_pair(candidate)
-            && let Some(key) = candidate.named_child(0)
-            && source
-                .get(key.byte_range())
-                .is_some_and(|text| strip_quotes(text) == "constraints")
-        {
-            return true;
-        }
-        cur = candidate.parent();
-    }
-    false
+    ancestor_pair_with_key(node, source, "constraints").is_some()
 }
 
 #[cfg(test)]
diff --git a/crates/vespertide-lsp/src/hover/column.rs b/crates/vespertide-lsp/src/hover/column.rs
index f32d6715..a51e4f4f 100644
--- a/crates/vespertide-lsp/src/hover/column.rs
+++ b/crates/vespertide-lsp/src/hover/column.rs
@@ -1,7 +1,7 @@
 //! Column hover: markdown showing name, type, nullable, default, constraints.
 
 use crate::text_util::strip_quotes;
-use crate::tree_util::is_pair;
+use crate::tree_util::{ancestor_pair_with_key, is_pair};
 use std::fmt::Write as _;
 use std::ops::Range;
 
@@ -69,17 +69,7 @@ fn column_object_markdown(obj: tree_sitter::Node<'_>, source: &str) -> Option, source: &str) -> bool {
-    let mut cur = node.parent();
-    while let Some(candidate) = cur {
-        if is_pair(candidate)
-            && let Some(key) = candidate.named_child(0)
-            && strip_quotes(&source[key.byte_range()]) == "columns"
-        {
-            return true;
-        }
-        cur = candidate.parent();
-    }
-    false
+    ancestor_pair_with_key(node, source, "columns").is_some()
 }
 
 fn highlight_range(node: tree_sitter::Node<'_>, fallback: tree_sitter::Node<'_>) -> Range {
diff --git a/crates/vespertide-lsp/src/text_util.rs b/crates/vespertide-lsp/src/text_util.rs
index 4482160c..e33a6a8a 100644
--- a/crates/vespertide-lsp/src/text_util.rs
+++ b/crates/vespertide-lsp/src/text_util.rs
@@ -14,15 +14,30 @@ pub(crate) fn strip_quotes(s: &str) -> &str {
         .trim_end_matches('\'')
 }
 
+/// Peel ONLY the outer JSON `"..."` wrapper from a scalar's raw text after
+/// trimming whitespace. Leaves any inner SQL `'...'` literal untouched —
+/// required by the code-action `default → enum` path which peels the JSON
+/// quote first, then runs an explicit `.strip_prefix('\'').strip_suffix('\'')`
+/// pair to recognise the SQL literal inside.
+///
+/// Canonical replacement for the three byte-equivalent local `strip_quotes`
+/// helpers previously open-coded in `code_actions`, `workspace_index`, and
+/// `diagnostics::locator` (the locator version was a matched-pair `"..."` OR
+/// `'...'` peeler, but every actual JSON key/value the locator consumes is
+/// well-formed JSON so the greedy `"`-only form is observationally
+/// identical there). Use [`strip_quotes`] when you want both flavours
+/// stripped (the JSON/YAML scalar reading path).
+#[must_use]
+pub(crate) fn strip_json_quotes(s: &str) -> &str {
+    s.trim().trim_start_matches('"').trim_end_matches('"')
+}
+
 /// UTF-8 slice of a tree-sitter node's byte range. Returns `None` when the
 /// slice is not valid UTF-8 — defensive only; the LSP parsers produce valid
 /// UTF-8 spans on every source we feed them. Single source of truth for the
 /// `std::str::from_utf8(&source[node.byte_range()]).ok()` chain that used to
 /// be open-coded across `diagnostics/locator`, `code_actions`, and friends.
 #[must_use]
-pub(crate) fn node_text<'a>(
-    node: tree_sitter::Node<'_>,
-    source: &'a [u8],
-) -> Option<&'a str> {
+pub(crate) fn node_text<'a>(node: tree_sitter::Node<'_>, source: &'a [u8]) -> Option<&'a str> {
     std::str::from_utf8(&source[node.byte_range()]).ok()
 }
diff --git a/crates/vespertide-lsp/src/tree_util.rs b/crates/vespertide-lsp/src/tree_util.rs
index 048ba537..a4c32fbe 100644
--- a/crates/vespertide-lsp/src/tree_util.rs
+++ b/crates/vespertide-lsp/src/tree_util.rs
@@ -109,6 +109,36 @@ pub(crate) fn enclosing_pair_with_key<'tree>(
     None
 }
 
+/// Parent-only variant of [`enclosing_pair_with_key`]: walk strictly the
+/// PARENT chain (skip `node` itself) looking for the nearest [`is_pair`]
+/// ancestor whose key (after [`crate::text_util::strip_quotes`]) equals
+/// `expected_key`.
+///
+/// Centralises the three byte-equivalent `is_inside_*` ancestor walks
+/// previously open-coded in `completion::context::is_inside_constraints`,
+/// `hover::check_expr::is_inside_constraints`, and
+/// `hover::column::is_inside_columns`. Use this when "am I inside a pair
+/// whose key is ``?" is the question; use [`enclosing_pair_with_key`]
+/// when the cursor's own node may itself be the pair.
+pub(crate) fn ancestor_pair_with_key<'tree>(
+    node: tree_sitter::Node<'tree>,
+    source: &str,
+    expected_key: &str,
+) -> Option> {
+    let mut current = node.parent();
+    while let Some(candidate) = current {
+        if is_pair(candidate)
+            && let Some(key) = candidate.named_child(0)
+            && let Some(key_text) = source.get(key.byte_range())
+            && crate::text_util::strip_quotes(key_text) == expected_key
+        {
+            return Some(candidate);
+        }
+        current = candidate.parent();
+    }
+    None
+}
+
 /// Find a direct child pair of `object` whose key (after
 /// [`crate::text_util::strip_quotes`]) equals `target_key` and return the
 /// value-side byte slice **verbatim** (quotes preserved for quoted
diff --git a/crates/vespertide-lsp/src/workspace_index.rs b/crates/vespertide-lsp/src/workspace_index.rs
index 037a7733..996ec3e7 100644
--- a/crates/vespertide-lsp/src/workspace_index.rs
+++ b/crates/vespertide-lsp/src/workspace_index.rs
@@ -14,6 +14,8 @@ use std::sync::RwLock;
 use tower_lsp_server::ls_types::Uri;
 use tree_sitter::{Node, Tree};
 
+use crate::text_util::strip_json_quotes;
+
 /// Snapshot returned by [`WorkspaceIndex::lookup`]; not held across mutations.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct TableLocation {
@@ -157,7 +159,7 @@ fn find_direct_name_value(mapping: Node<'_>, source: &[u8]) -> Option {
             return source
                 .get(value.byte_range())
                 .and_then(|text| std::str::from_utf8(text).ok())
-                .map(|text| strip_quotes(text).to_string());
+                .map(|text| strip_json_quotes(text).to_string());
         }
     }
     None
@@ -170,7 +172,7 @@ fn is_name_key_node(node: Node<'_>, source: &[u8]) -> bool {
             .named_child(0)
             .and_then(|key| source.get(key.byte_range()))
             .and_then(|text| std::str::from_utf8(text).ok())
-            .is_some_and(|key_str| strip_quotes(key_str.trim()) == "name")
+            .is_some_and(|key_str| strip_json_quotes(key_str.trim()) == "name")
 }
 
 fn find_value_sibling(pair_node: Node<'_>) -> Option> {
@@ -179,11 +181,6 @@ fn find_value_sibling(pair_node: Node<'_>) -> Option> {
     pair_node.named_child(1)
 }
 
-fn strip_quotes(s: &str) -> &str {
-    let s = s.trim();
-    s.trim_start_matches('"').trim_end_matches('"')
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;

From ec644d2bc5112236b64354a85b638dfab5863392 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 10:27:11 +0900
Subject: [PATCH 038/109] retry-now#0037: batch -- hoist
 build_pg_alter_column_sql + dedup skip_yaml_wrappers + dedup enclosing_string
 (3 kept)

---
 .../src/definition/foreign_key.rs             | 35 ++----------
 .../vespertide-lsp/src/references/resolver.rs | 32 +----------
 crates/vespertide-lsp/src/tree_util.rs        | 53 +++++++++++++++++++
 crates/vespertide-query/src/sql/helpers.rs    | 18 +++++++
 .../src/sql/modify_column_default.rs          | 14 ++---
 .../src/sql/modify_column_nullable.rs         | 10 ++--
 .../src/sql/modify_column_type/direct.rs      | 21 +++++---
 7 files changed, 99 insertions(+), 84 deletions(-)

diff --git a/crates/vespertide-lsp/src/definition/foreign_key.rs b/crates/vespertide-lsp/src/definition/foreign_key.rs
index 5572f5d8..36caea4c 100644
--- a/crates/vespertide-lsp/src/definition/foreign_key.rs
+++ b/crates/vespertide-lsp/src/definition/foreign_key.rs
@@ -15,7 +15,9 @@ use std::ops::Range;
 
 use crate::store::DocumentStore;
 use crate::text_util::strip_quotes;
-use crate::tree_util::{direct_child_value, enclosing_pair_with_key, is_pair};
+use crate::tree_util::{
+    direct_child_value, enclosing_pair_with_key, enclosing_string, is_pair, skip_yaml_wrappers,
+};
 use crate::workspace_index::WorkspaceIndex;
 use crate::workspace_tables::WorkspaceTables;
 
@@ -133,37 +135,6 @@ fn resolve_target(
     })
 }
 
-/// Skip past tree-sitter-yaml's pure wrapper nodes (`flow_node`,
-/// `block_node`). Returns the first ancestor that has a meaningful kind.
-fn skip_yaml_wrappers(node: tree_sitter::Node<'_>) -> Option> {
-    let mut current = node;
-    while matches!(current.kind(), "flow_node" | "block_node") {
-        current = current.parent()?;
-    }
-    Some(current)
-}
-
-fn enclosing_string(node: tree_sitter::Node<'_>) -> Option> {
-    let mut current = Some(node);
-    while let Some(candidate) = current {
-        match candidate.kind() {
-            "string"
-            | "double_quote_scalar"
-            | "single_quote_scalar"
-            | "string_scalar"
-            | "plain_scalar" => return Some(candidate),
-            // String_content is the inner span without quotes; climb to the
-            // surrounding `string` node so the parent is the JSON array.
-            "string_content" => return candidate.parent(),
-            "array" | "object" | "pair" | "block_mapping_pair" | "block_mapping"
-            | "block_sequence" | "flow_mapping" | "flow_sequence" => return None,
-            _ => {}
-        }
-        current = candidate.parent();
-    }
-    None
-}
-
 fn find_column_name_range(
     tree: &tree_sitter::Tree,
     text: &str,
diff --git a/crates/vespertide-lsp/src/references/resolver.rs b/crates/vespertide-lsp/src/references/resolver.rs
index d4b6ce13..c93eab39 100644
--- a/crates/vespertide-lsp/src/references/resolver.rs
+++ b/crates/vespertide-lsp/src/references/resolver.rs
@@ -1,7 +1,7 @@
 //! Resolve "what symbol is the cursor on?" for the references provider.
 
 use crate::text_util::strip_quotes;
-use crate::tree_util::{direct_child_value, node_at_byte};
+use crate::tree_util::{direct_child_value, enclosing_string, node_at_byte, skip_yaml_wrappers};
 use tower_lsp_server::ls_types::Uri;
 
 use super::ReferenceSymbol;
@@ -107,26 +107,6 @@ fn resolve_check_expr_column(
     })
 }
 
-fn enclosing_string(node: tree_sitter::Node<'_>) -> Option> {
-    let mut current = Some(node);
-    while let Some(candidate) = current {
-        match candidate.kind() {
-            "string"
-            | "double_quote_scalar"
-            | "single_quote_scalar"
-            | "string_scalar"
-            | "plain_scalar" => return Some(candidate),
-            "string_content" => return candidate.parent(),
-            // Stop at structural boundaries.
-            "array" | "object" | "pair" | "block_mapping_pair" | "block_mapping"
-            | "block_sequence" | "flow_mapping" | "flow_sequence" => return None,
-            _ => {}
-        }
-        current = candidate.parent();
-    }
-    None
-}
-
 fn enclosing_pair(node: tree_sitter::Node<'_>) -> Option> {
     let mut current = node.parent();
     while let Some(candidate) = current {
@@ -197,16 +177,6 @@ fn enclosing_table_name(name_pair: tree_sitter::Node<'_>, source: &str) -> Optio
     None
 }
 
-fn skip_yaml_wrappers(node: tree_sitter::Node<'_>) -> Option> {
-    let mut current = node;
-    while matches!(current.kind(), "flow_node" | "block_node") {
-        let parent = current.parent();
-        parent?;
-        current = parent.expect("YAML wrapper reached from a mapping pair has a parent");
-    }
-    Some(current)
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/vespertide-lsp/src/tree_util.rs b/crates/vespertide-lsp/src/tree_util.rs
index a4c32fbe..2002e4c0 100644
--- a/crates/vespertide-lsp/src/tree_util.rs
+++ b/crates/vespertide-lsp/src/tree_util.rs
@@ -82,6 +82,59 @@ pub(crate) fn unwrap_yaml_node(node: tree_sitter::Node<'_>) -> tree_sitter::Node
     current
 }
 
+/// Walk the PARENT chain skipping tree-sitter-yaml's `flow_node` /
+/// `block_node` wrapper kinds. Counterpart to [`unwrap_yaml_node`], which
+/// descends INTO a wrapper; this helper escapes UP through them so the
+/// caller can reach the real ancestor (e.g. the surrounding mapping pair
+/// or constraint object). Returns `None` when the parent chain runs out
+/// before a non-wrapper node is reached.
+///
+/// Centralises the two byte-equivalent private copies that lived in
+/// `definition::foreign_key` and `references::resolver` pre-0.2.0.
+pub(crate) fn skip_yaml_wrappers(node: tree_sitter::Node<'_>) -> Option> {
+    let mut current = node;
+    while matches!(current.kind(), "flow_node" | "block_node") {
+        current = current.parent()?;
+    }
+    Some(current)
+}
+
+/// Walk from `node` up the parent chain returning the nearest tree-sitter
+/// node whose kind is a JSON / YAML string scalar (`string` /
+/// `double_quote_scalar` / `single_quote_scalar` / `string_scalar` /
+/// `plain_scalar`). When the cursor sits on `string_content` (the inner
+/// span without quotes), climb one level so the returned node covers the
+/// surrounding quotes. Stops at the first structural-container boundary
+/// (`array` / `object` / `pair` / `block_mapping_pair` / `block_mapping` /
+/// `block_sequence` / `flow_mapping` / `flow_sequence`) so a nested
+/// object value never counts as "in string".
+///
+/// Centralises the two byte-equivalent private copies that lived in
+/// `definition::foreign_key` and `references::resolver` pre-0.2.0.
+///
+/// NOTE: `completion::context::enclosing_string_range` deliberately stays
+/// SEPARATE — it returns a `Range` (not a `Node`) and uses an
+/// `unwrap_or(candidate)` fallback on the `string_content` arm, so its
+/// behaviour differs at end-of-token boundaries. Do not migrate it here.
+pub(crate) fn enclosing_string(node: tree_sitter::Node<'_>) -> Option> {
+    let mut current = Some(node);
+    while let Some(candidate) = current {
+        match candidate.kind() {
+            "string"
+            | "double_quote_scalar"
+            | "single_quote_scalar"
+            | "string_scalar"
+            | "plain_scalar" => return Some(candidate),
+            "string_content" => return candidate.parent(),
+            "array" | "object" | "pair" | "block_mapping_pair" | "block_mapping"
+            | "block_sequence" | "flow_mapping" | "flow_sequence" => return None,
+            _ => {}
+        }
+        current = candidate.parent();
+    }
+    None
+}
+
 /// Walk the parent chain looking for the nearest [`is_pair`] ancestor whose
 /// key (after [`crate::text_util::strip_quotes`]) equals `expected_key`.
 ///
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index c94f21c1..a73cf2b2 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -809,6 +809,24 @@ pub fn quote_idents>(names: &[T], backend: DatabaseBackend) -> Str
     out
 }
 
+/// Build a PostgreSQL `ALTER TABLE "t" ALTER COLUMN "c" ` statement.
+///
+/// Single source of truth for the seven open-coded PG-only emit sites
+/// across [`crate::sql::modify_column_default`],
+/// [`crate::sql::modify_column_nullable`], and
+/// [`crate::sql::modify_column_type::direct`]. Each call site used to
+/// repeat the same `quote_ident(table, …) + quote_ident(column, …) +
+/// format!("ALTER TABLE {qt} ALTER COLUMN {qc} ")` skeleton; this
+/// helper folds them into one call so the next PG ALTER variant only has
+/// to choose its suffix. SQL output is byte-identical (every existing
+/// snapshot must continue to match).
+#[must_use]
+pub(super) fn build_pg_alter_column_sql(table: &str, column: &str, suffix: &str) -> String {
+    let quoted_table = quote_ident(table, DatabaseBackend::Postgres);
+    let quoted_column = quote_ident(column, DatabaseBackend::Postgres);
+    format!("ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} {suffix}")
+}
+
 /// Look up `table` in `current_schema` and return a reference to its
 /// [`TableDef`], or a uniform `QueryError::SchemaError` describing why the
 /// lookup is mandatory for the calling backend.
diff --git a/crates/vespertide-query/src/sql/modify_column_default.rs b/crates/vespertide-query/src/sql/modify_column_default.rs
index 90607259..55ed33ba 100644
--- a/crates/vespertide-query/src/sql/modify_column_default.rs
+++ b/crates/vespertide-query/src/sql/modify_column_default.rs
@@ -1,8 +1,8 @@
 use vespertide_core::TableDef;
 
 use super::helpers::{
-    build_mysql_modify_column_with, build_sqlite_modify_column_with, find_column_in_schema,
-    normalize_enum_default, quote_ident,
+    build_mysql_modify_column_with, build_pg_alter_column_sql, build_sqlite_modify_column_with,
+    find_column_in_schema, normalize_enum_default, quote_ident,
 };
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
 use crate::error::QueryError;
@@ -29,8 +29,6 @@ pub fn build_modify_column_default(
 
     match backend {
         DatabaseBackend::Postgres => {
-            let quoted_table = quote_ident(table, backend);
-            let quoted_column = quote_ident(column, backend);
             let alter_sql = if let Some(default_value) = new_default {
                 // Look up column type to properly quote enum defaults
                 let column_type =
@@ -42,11 +40,13 @@ pub fn build_modify_column_default(
                     default_value.to_string()
                 };
 
-                format!(
-                    "ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} SET DEFAULT {normalized_default}"
+                build_pg_alter_column_sql(
+                    table,
+                    column,
+                    &format!("SET DEFAULT {normalized_default}"),
                 )
             } else {
-                format!("ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} DROP DEFAULT")
+                build_pg_alter_column_sql(table, column, "DROP DEFAULT")
             };
             queries.push(BuiltQuery::Raw(RawSql::uniform(alter_sql)));
         }
diff --git a/crates/vespertide-query/src/sql/modify_column_nullable.rs b/crates/vespertide-query/src/sql/modify_column_nullable.rs
index 165552c9..624f9e89 100644
--- a/crates/vespertide-query/src/sql/modify_column_nullable.rs
+++ b/crates/vespertide-query/src/sql/modify_column_nullable.rs
@@ -1,8 +1,8 @@
 use vespertide_core::TableDef;
 
 use super::helpers::{
-    build_mysql_modify_column_with, build_sqlite_modify_column_with, convert_default_for_backend,
-    normalize_fill_with, quote_ident,
+    build_mysql_modify_column_with, build_pg_alter_column_sql, build_sqlite_modify_column_with,
+    convert_default_for_backend, normalize_fill_with, quote_ident,
 };
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
 use crate::error::QueryError;
@@ -46,12 +46,10 @@ pub fn build_modify_column_nullable(
     // Generate ALTER TABLE statement based on backend
     match backend {
         DatabaseBackend::Postgres => {
-            let quoted_table = quote_ident(table, backend);
-            let quoted_column = quote_ident(column, backend);
             let alter_sql = if nullable {
-                format!("ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} DROP NOT NULL")
+                build_pg_alter_column_sql(table, column, "DROP NOT NULL")
             } else {
-                format!("ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} SET NOT NULL")
+                build_pg_alter_column_sql(table, column, "SET NOT NULL")
             };
             queries.push(BuiltQuery::Raw(RawSql::uniform(alter_sql)));
         }
diff --git a/crates/vespertide-query/src/sql/modify_column_type/direct.rs b/crates/vespertide-query/src/sql/modify_column_type/direct.rs
index d54ad4cc..8d19f0f2 100644
--- a/crates/vespertide-query/src/sql/modify_column_type/direct.rs
+++ b/crates/vespertide-query/src/sql/modify_column_type/direct.rs
@@ -5,8 +5,8 @@ use sea_query::{Alias, ColumnDef as SeaColumnDef, Table};
 use vespertide_core::{ColumnType, ComplexColumnType, TableDef};
 
 use crate::sql::helpers::{
-    apply_column_type_with_table, build_create_enum_type_sql, convert_default_for_backend,
-    find_column_in_schema, normalize_enum_default, quote_ident,
+    apply_column_type_with_table, build_create_enum_type_sql, build_pg_alter_column_sql,
+    convert_default_for_backend, find_column_in_schema, normalize_enum_default, quote_ident,
 };
 use crate::sql::types::{BuiltQuery, DatabaseBackend, RawSql};
 
@@ -110,7 +110,6 @@ fn build_postgres_enum_migration(queries: &mut Vec, context: &Direct
         let column_default = column_default(context);
         let create_values = new_values.sql_values_joined(", ");
         let quoted_target_type = quote_ident(&target_type_name, DatabaseBackend::Postgres);
-        let quoted_table = quote_ident(context.table, DatabaseBackend::Postgres);
         let quoted_column = quote_ident(context.column, DatabaseBackend::Postgres);
         let quoted_old_type = quote_ident(&old_type_name, DatabaseBackend::Postgres);
         push_postgres_raw(
@@ -122,15 +121,19 @@ fn build_postgres_enum_migration(queries: &mut Vec, context: &Direct
         if column_default.is_some() {
             push_postgres_raw(
                 queries,
-                format!("ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} DROP DEFAULT"),
+                build_pg_alter_column_sql(context.table, context.column, "DROP DEFAULT"),
             );
         }
 
         // 3. ALTER TABLE ... ALTER COLUMN ... TYPE target_type USING col::text::target_type.
         push_postgres_raw(
             queries,
-            format!(
-                "ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} TYPE {quoted_target_type} USING {quoted_column}::text::{quoted_target_type}"
+            build_pg_alter_column_sql(
+                context.table,
+                context.column,
+                &format!(
+                    "TYPE {quoted_target_type} USING {quoted_column}::text::{quoted_target_type}"
+                ),
             ),
         );
 
@@ -151,8 +154,10 @@ fn build_postgres_enum_migration(queries: &mut Vec, context: &Direct
                 normalize_enum_default(context.new_type, &default_value.to_sql());
             push_postgres_raw(
                 queries,
-                format!(
-                    "ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} SET DEFAULT {normalized_default}"
+                build_pg_alter_column_sql(
+                    context.table,
+                    context.column,
+                    &format!("SET DEFAULT {normalized_default}"),
                 ),
             );
         }

From 1b991209bb19e4cf678caffbe807fd570c3d5a2d Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 10:55:00 +0900
Subject: [PATCH 039/109] retry-now#0038: batch -- hoist find_pair_with_key,
 adopt joined_sql, hoist joined_sql_semicolon (3 kept)

---
 crates/vespertide-lsp/src/file_features.rs    | 19 +------
 crates/vespertide-lsp/src/inlay_hints.rs      | 18 +-----
 crates/vespertide-lsp/src/symbols.rs          | 20 +------
 crates/vespertide-lsp/src/tree_util.rs        | 30 ++++++++++
 crates/vespertide-query/src/builder/mod.rs    | 18 ++----
 .../src/sql/delete_column/mod.rs              | 20 ++-----
 .../src/sql/modify_column_comment.rs          | 56 ++++---------------
 .../src/sql/modify_column_default.rs          | 56 ++++---------------
 .../src/sql/modify_column_nullable.rs         | 38 +++----------
 crates/vespertide-query/src/test_support.rs   | 18 ++++++
 10 files changed, 89 insertions(+), 204 deletions(-)

diff --git a/crates/vespertide-lsp/src/file_features.rs b/crates/vespertide-lsp/src/file_features.rs
index 8d09c985..ea0b1771 100644
--- a/crates/vespertide-lsp/src/file_features.rs
+++ b/crates/vespertide-lsp/src/file_features.rs
@@ -4,7 +4,6 @@
 //! here because they share the same tree-sitter walk patterns and
 //! none has enough surface area to deserve its own directory.
 
-use crate::text_util::strip_quotes;
 use crate::tree_util::node_at_byte;
 use std::ops::Range;
 
@@ -250,23 +249,7 @@ pub fn compute_selection_ranges(
 // Shared helpers
 // =====================================================================
 
-use crate::tree_util::find_outer_mapping;
-
-fn find_pair_with_key<'tree>(
-    mapping: tree_sitter::Node<'tree>,
-    source: &[u8],
-    target_key: &str,
-) -> Option> {
-    let mut cursor = mapping.walk();
-    mapping.children(&mut cursor).find(|&child| {
-        matches!(child.kind(), "pair" | "block_mapping_pair")
-            && child
-                .named_child(0)
-                .and_then(|key| std::str::from_utf8(&source[key.byte_range()]).ok())
-                .map(strip_quotes)
-                == Some(target_key)
-    })
-}
+use crate::tree_util::{find_outer_mapping, find_pair_with_key};
 
 fn direct_string_value(
     mapping: tree_sitter::Node<'_>,
diff --git a/crates/vespertide-lsp/src/inlay_hints.rs b/crates/vespertide-lsp/src/inlay_hints.rs
index c557e0ef..0cee1493 100644
--- a/crates/vespertide-lsp/src/inlay_hints.rs
+++ b/crates/vespertide-lsp/src/inlay_hints.rs
@@ -355,23 +355,7 @@ fn find_value_for_key<'tree>(
     None
 }
 
-fn find_pair_with_key<'tree>(
-    object: tree_sitter::Node<'tree>,
-    source: &[u8],
-    target_key: &str,
-) -> Option> {
-    let mut cursor = object.walk();
-    object.children(&mut cursor).find(|&child| {
-        matches!(child.kind(), "pair" | "block_mapping_pair")
-            && child
-                .named_child(0)
-                .and_then(|key| std::str::from_utf8(&source[key.byte_range()]).ok())
-                .map(strip_quotes)
-                == Some(target_key)
-    })
-}
-
-use crate::tree_util::unwrap_yaml_node;
+use crate::tree_util::{find_pair_with_key, unwrap_yaml_node};
 
 fn ranges_overlap(a: &Range, b: &Range) -> bool {
     a.start < b.end && b.start < a.end
diff --git a/crates/vespertide-lsp/src/symbols.rs b/crates/vespertide-lsp/src/symbols.rs
index 521687ae..3c022f2a 100644
--- a/crates/vespertide-lsp/src/symbols.rs
+++ b/crates/vespertide-lsp/src/symbols.rs
@@ -306,25 +306,7 @@ fn direct_pair_node<'tree>(
     find_pair_with_key(mapping, source, target_key)?.named_child(1)
 }
 
-fn find_pair_with_key<'tree>(
-    mapping: tree_sitter::Node<'tree>,
-    source: &[u8],
-    target_key: &str,
-) -> Option> {
-    let mut cursor = mapping.walk();
-    mapping.children(&mut cursor).find(|&child| {
-        matches!(child.kind(), "pair" | "block_mapping_pair")
-            && child
-                .named_child(0)
-                .and_then(|key| std::str::from_utf8(&source[key.byte_range()]).ok())
-                .map(strip_quotes)
-                == Some(target_key)
-    })
-}
-
-use crate::tree_util::find_outer_mapping;
-
-use crate::tree_util::unwrap_yaml_node;
+use crate::tree_util::{find_outer_mapping, find_pair_with_key, unwrap_yaml_node};
 
 fn trim_one_byte(range: &Range) -> Range {
     if range.end.saturating_sub(range.start) >= 2 {
diff --git a/crates/vespertide-lsp/src/tree_util.rs b/crates/vespertide-lsp/src/tree_util.rs
index 2002e4c0..7d4c0b18 100644
--- a/crates/vespertide-lsp/src/tree_util.rs
+++ b/crates/vespertide-lsp/src/tree_util.rs
@@ -223,3 +223,33 @@ pub(crate) fn direct_child_value<'a>(
     }
     None
 }
+
+/// Direct-child variant of [`enclosing_pair_with_key`] for the
+/// **byte-slice** call sites: walk only the immediate children of
+/// `mapping` (no parent traversal, no recursion) and return the first
+/// [`is_pair`] child whose key — UTF-8 decoded then run through
+/// [`crate::text_util::strip_quotes`] — equals `target_key`.
+///
+/// Centralises the three byte-identical private copies that lived in
+/// `file_features`, `inlay_hints`, and `symbols` pre-0.2.0. The
+/// `references::search` variant stays put because it does a RECURSIVE
+/// walk, and the `completion::context` / `diagnostics::validation::types`
+/// variants stay put because they operate on `&str` source. Use
+/// [`direct_child_value`] when you only need the value-side text;
+/// reach for this helper when you need the pair `Node` itself (e.g. to
+/// continue navigating into its named children).
+pub(crate) fn find_pair_with_key<'tree>(
+    mapping: tree_sitter::Node<'tree>,
+    source: &[u8],
+    target_key: &str,
+) -> Option> {
+    let mut cursor = mapping.walk();
+    mapping.children(&mut cursor).find(|&child| {
+        is_pair(child)
+            && child
+                .named_child(0)
+                .and_then(|key| std::str::from_utf8(&source[key.byte_range()]).ok())
+                .map(crate::text_util::strip_quotes)
+                == Some(target_key)
+    })
+}
diff --git a/crates/vespertide-query/src/builder/mod.rs b/crates/vespertide-query/src/builder/mod.rs
index 15075959..c4455746 100644
--- a/crates/vespertide-query/src/builder/mod.rs
+++ b/crates/vespertide-query/src/builder/mod.rs
@@ -126,8 +126,8 @@ pub fn build_plan_queries_with_options(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::sql::{BuiltQuery, DatabaseBackend};
-    use crate::test_support::col;
+    use crate::sql::DatabaseBackend;
+    use crate::test_support::{col, joined_sql_semicolon};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{
@@ -135,14 +135,6 @@ mod tests {
         TableConstraint, TableDef,
     };
 
-    fn build_sql_snapshot(result: &[BuiltQuery], backend: DatabaseBackend) -> String {
-        result
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join(";\n")
-    }
-
     #[rstest]
     #[case::empty(
         MigrationPlan {
@@ -238,7 +230,7 @@ mod tests {
             DatabaseBackend::MySql => &result[1].mysql,
             DatabaseBackend::Sqlite => &result[1].sqlite,
         };
-        let sql = build_sql_snapshot(queries, backend);
+        let sql = joined_sql_semicolon(backend, queries);
 
         with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("inline_unique_{}", title) }, {
             assert_snapshot!(sql);
@@ -284,7 +276,7 @@ mod tests {
             DatabaseBackend::MySql => &result[1].mysql,
             DatabaseBackend::Sqlite => &result[1].sqlite,
         };
-        let sql = build_sql_snapshot(queries, backend);
+        let sql = joined_sql_semicolon(backend, queries);
 
         with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("inline_index_{}", title) }, {
             assert_snapshot!(sql);
@@ -489,7 +481,7 @@ mod tests {
                     DatabaseBackend::MySql => &pq.mysql,
                     DatabaseBackend::Sqlite => &pq.sqlite,
                 };
-                let sql = build_sql_snapshot(queries, backend);
+                let sql = joined_sql_semicolon(backend, queries);
                 format!("-- Action {}: {:?}\n{}", i, pq.action, sql)
             })
             .collect::>()
diff --git a/crates/vespertide-query/src/sql/delete_column/mod.rs b/crates/vespertide-query/src/sql/delete_column/mod.rs
index b85dd51b..aceaf2bd 100644
--- a/crates/vespertide-query/src/sql/delete_column/mod.rs
+++ b/crates/vespertide-query/src/sql/delete_column/mod.rs
@@ -123,7 +123,7 @@ fn sqlite_constraint_handling(
 mod tests {
     use super::*;
     use crate::sql::types::DatabaseBackend;
-    use crate::test_support::col;
+    use crate::test_support::{col, joined_sql_semicolon};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{ComplexColumnType, SimpleColumnType};
@@ -746,14 +746,6 @@ mod tests {
 
     // ==================== Snapshot Tests ====================
 
-    fn build_sql_snapshot(result: &[BuiltQuery], backend: DatabaseBackend) -> String {
-        result
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join(";\n")
-    }
-
     #[rstest]
     #[case::postgres("postgres", DatabaseBackend::Postgres)]
     #[case::mysql("mysql", DatabaseBackend::MySql)]
@@ -780,7 +772,7 @@ mod tests {
         }];
 
         let result = build_delete_column(backend, "users", "email", None, &schema, &[]);
-        let sql = build_sql_snapshot(&result, backend);
+        let sql = joined_sql_semicolon(backend, &result);
 
         with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("delete_column_with_unique_{}", title) }, {
             assert_snapshot!(sql);
@@ -813,7 +805,7 @@ mod tests {
         }];
 
         let result = build_delete_column(backend, "posts", "created_at", None, &schema, &[]);
-        let sql = build_sql_snapshot(&result, backend);
+        let sql = joined_sql_semicolon(backend, &result);
 
         with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("delete_column_with_index_{}", title) }, {
             assert_snapshot!(sql);
@@ -848,7 +840,7 @@ mod tests {
         }];
 
         let result = build_delete_column(backend, "orders", "user_id", None, &schema, &[]);
-        let sql = build_sql_snapshot(&result, backend);
+        let sql = joined_sql_semicolon(backend, &result);
 
         with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("delete_column_with_fk_{}", title) }, {
             assert_snapshot!(sql);
@@ -879,7 +871,7 @@ mod tests {
         }];
 
         let result = build_delete_column(backend, "order_items", "product_id", None, &schema, &[]);
-        let sql = build_sql_snapshot(&result, backend);
+        let sql = joined_sql_semicolon(backend, &result);
 
         with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("delete_column_with_pk_{}", title) }, {
             assert_snapshot!(sql);
@@ -925,7 +917,7 @@ mod tests {
         }];
 
         let result = build_delete_column(backend, "orders", "user_id", None, &schema, &[]);
-        let sql = build_sql_snapshot(&result, backend);
+        let sql = joined_sql_semicolon(backend, &result);
 
         with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("delete_column_with_fk_and_index_{}", title) }, {
             assert_snapshot!(sql);
diff --git a/crates/vespertide-query/src/sql/modify_column_comment.rs b/crates/vespertide-query/src/sql/modify_column_comment.rs
index c3405815..5c850e29 100644
--- a/crates/vespertide-query/src/sql/modify_column_comment.rs
+++ b/crates/vespertide-query/src/sql/modify_column_comment.rs
@@ -82,7 +82,7 @@ pub fn build_modify_column_comment(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::{backend_tag, col_n as col, table_def};
+    use crate::test_support::{backend_tag, col_n as col, joined_sql, table_def};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{ColumnType, SimpleColumnType};
@@ -110,11 +110,7 @@ mod tests {
         let result = build_modify_column_comment(backend, "users", "email", new_comment, &schema);
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         let suffix = format!(
             "{}_{}_users",
@@ -156,11 +152,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         // Postgres and MySQL should escape quotes, SQLite returns empty
         if backend != DatabaseBackend::Sqlite {
@@ -240,11 +232,7 @@ mod tests {
             build_modify_column_comment(backend, "users", "bio", Some(long_comment), &schema);
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         let suffix = format!("{}_long_comment", backend_tag(backend));
 
@@ -280,11 +268,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         // MySQL should preserve the default value in the MODIFY COLUMN statement
         if backend == DatabaseBackend::MySql {
@@ -320,11 +304,7 @@ mod tests {
             build_modify_column_comment(backend, "users", "email", Some("New comment"), &schema);
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         let suffix = format!("{}_change_comment", backend_tag(backend));
 
@@ -357,11 +337,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         let suffix = format!("{}_drop_existing_comment", backend_tag(backend));
 
@@ -431,11 +407,7 @@ mod tests {
         let result = build_modify_column_comment(backend, "data", "field", Some(comment), &schema);
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         let type_name = format!("{column_type:?}").to_lowercase();
         let suffix = format!("{}_{}_comment", backend_tag(backend), type_name);
@@ -472,11 +444,7 @@ mod tests {
 
         let queries = build_modify_column_comment(backend, "users", "email", new_comment, &schema)
             .expect("build_modify_column_comment should succeed");
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         match (backend, new_comment) {
             (DatabaseBackend::Sqlite, _) => {
@@ -563,11 +531,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         let suffix = format!("{}_not_null_column", backend_tag(backend));
 
diff --git a/crates/vespertide-query/src/sql/modify_column_default.rs b/crates/vespertide-query/src/sql/modify_column_default.rs
index 55ed33ba..e7326858 100644
--- a/crates/vespertide-query/src/sql/modify_column_default.rs
+++ b/crates/vespertide-query/src/sql/modify_column_default.rs
@@ -92,7 +92,7 @@ pub fn build_modify_column_default(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::{backend_tag, col_n as col, table_def};
+    use crate::test_support::{backend_tag, col_n as col, joined_sql, table_def};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{ColumnType, SimpleColumnType, TableConstraint};
@@ -121,11 +121,7 @@ mod tests {
             build_modify_column_default(backend, "users", "email", new_default, None, &schema, &[]);
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         let suffix = format!(
             "{}_{}_users",
@@ -230,11 +226,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Postgres))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(DatabaseBackend::Postgres, &queries);
 
         // Should still generate valid SQL, using the default value as-is
         assert!(sql.contains("ALTER TABLE \"users\" ALTER COLUMN \"status\" SET DEFAULT 'active'"));
@@ -269,11 +261,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         // SQLite should recreate the index after table rebuild
         if backend == DatabaseBackend::Sqlite {
@@ -317,11 +305,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         let suffix = format!("{}_change_default", backend_tag(backend));
 
@@ -360,11 +344,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         let suffix = format!("{}_integer_default", backend_tag(backend));
 
@@ -403,11 +383,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         let suffix = format!("{}_boolean_default", backend_tag(backend));
 
@@ -451,11 +427,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         let suffix = format!("{}_function_default", backend_tag(backend));
 
@@ -493,11 +465,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         let suffix = format!("{}_drop_existing_default", backend_tag(backend));
 
@@ -535,11 +503,7 @@ mod tests {
             &[],
         )
         .expect("backfill path should succeed");
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         // The trailing UPDATE was emitted exactly once.
         let update_count = sql.matches("UPDATE").count();
diff --git a/crates/vespertide-query/src/sql/modify_column_nullable.rs b/crates/vespertide-query/src/sql/modify_column_nullable.rs
index 624f9e89..fc1d7300 100644
--- a/crates/vespertide-query/src/sql/modify_column_nullable.rs
+++ b/crates/vespertide-query/src/sql/modify_column_nullable.rs
@@ -83,7 +83,7 @@ pub fn build_modify_column_nullable(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::{backend_tag, col_n as col, table_def};
+    use crate::test_support::{backend_tag, col_n as col, joined_sql, table_def};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{ColumnType, SimpleColumnType, TableConstraint};
@@ -128,11 +128,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         let suffix = format!(
             "{}_{}_users{}",
@@ -235,11 +231,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         // SQLite should recreate the index after table rebuild
         if backend == DatabaseBackend::Sqlite {
@@ -285,11 +277,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         // NOW() should be converted to CURRENT_TIMESTAMP for all backends
         assert!(
@@ -338,11 +326,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         // MySQL and SQLite should include DEFAULT clause
         if backend == DatabaseBackend::MySql || backend == DatabaseBackend::Sqlite {
@@ -387,11 +371,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         assert!(
             sql.contains("DELETE FROM"),
@@ -442,11 +422,7 @@ mod tests {
         );
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         assert!(
             !sql.contains("DELETE FROM"),
diff --git a/crates/vespertide-query/src/test_support.rs b/crates/vespertide-query/src/test_support.rs
index 8ca95856..ff9afc54 100644
--- a/crates/vespertide-query/src/test_support.rs
+++ b/crates/vespertide-query/src/test_support.rs
@@ -48,6 +48,24 @@ pub(crate) fn joined_sql(
         .join("\n")
 }
 
+/// Same shape as [`joined_sql`] but uses `";\n"` as the separator — matches
+/// the canonical multi-statement SQL formatting that `builder::mod` and
+/// `sql::delete_column::mod` snapshot. Hoisted from two byte-identical
+/// `build_sql_snapshot` helpers that lived in those modules pre-0.2.0; the
+/// broader inline `.join(";\n")` adoption pass across the rest of the
+/// crate is deliberately deferred to a follow-up iteration to keep each
+/// dedupe item independently revertible.
+pub(crate) fn joined_sql_semicolon(
+    backend: crate::sql::DatabaseBackend,
+    queries: &[crate::sql::BuiltQuery],
+) -> String {
+    queries
+        .iter()
+        .map(|q| q.build(backend))
+        .collect::>()
+        .join(";\n")
+}
+
 /// Snapshot-suffix tag for a backend. The string is part of the snapshot
 /// file name, so changing it would invalidate every `assert_snapshot!` that
 /// uses it — keep it byte-stable. Hoisted from a `match backend { ... }`

From 02360af8d850fe14fc748d1d58c1ce911067e486 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 11:18:27 +0900
Subject: [PATCH 040/109] =?UTF-8?q?retry-now#0039:=20batch=20=E2=80=94=20h?=
 =?UTF-8?q?oist=20try=5Fresolve=5Fsingle=5Fpk=5Fcolumn=20shared=20helper,?=
 =?UTF-8?q?=20collapse=20dead=20seaorm=20naming=20else-if,=20dedupe=20to?=
 =?UTF-8?q?=5Fpascal=5Fcase=20across=20cli+exporter=20(3=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 crates/vespertide-cli/src/commands/export.rs  | 26 ++------
 crates/vespertide-exporter/src/lib.rs         |  2 +-
 .../vespertide-exporter/src/python_naming.rs  |  6 +-
 .../src/seaorm/relations/naming.rs            | 10 +--
 .../src/sql/add_constraint/mod.rs             | 52 +++++++++++++++-
 .../src/sql/add_constraint/primary_key.rs     | 50 ++-------------
 .../src/sql/add_constraint/unique.rs          | 62 ++-----------------
 7 files changed, 73 insertions(+), 135 deletions(-)

diff --git a/crates/vespertide-cli/src/commands/export.rs b/crates/vespertide-cli/src/commands/export.rs
index 7eec2f53..86b57e69 100644
--- a/crates/vespertide-cli/src/commands/export.rs
+++ b/crates/vespertide-cli/src/commands/export.rs
@@ -8,7 +8,10 @@ use rayon::prelude::*;
 use tokio::fs;
 use vespertide_config::VespertideConfig;
 use vespertide_core::TableDef;
-use vespertide_exporter::{Orm, render_entity_with_schema, seaorm::SeaOrmExporterWithConfig};
+use vespertide_exporter::{
+    Orm, python_naming::to_pascal_case, render_entity_with_schema,
+    seaorm::SeaOrmExporterWithConfig,
+};
 
 use crate::parallel_config::{EXPORT_RENDER_PAR_MIN_LEN, EXPORT_RENDER_PAR_THRESHOLD};
 use crate::utils::load_config;
@@ -343,18 +346,6 @@ fn build_output_path(root: &Path, rel_path: &Path, orm: Orm) -> PathBuf {
     out
 }
 
-fn to_pascal_case(s: &str) -> String {
-    s.split('_')
-        .map(|word| {
-            let mut chars = word.chars();
-            match chars.next() {
-                None => String::new(),
-                Some(first) => first.to_uppercase().chain(chars).collect(),
-            }
-        })
-        .collect()
-}
-
 fn sanitize_filename(name: &str) -> String {
     name.chars()
         .map(|ch| {
@@ -963,13 +954,4 @@ mod tests {
         assert_eq!(out, Path::new("src/models/User.java"));
     }
 
-    #[rstest]
-    #[case("order_item", "OrderItem")]
-    #[case("users", "Users")]
-    #[case("a", "A")]
-    #[case("user_profile_image", "UserProfileImage")]
-    #[case("a__b", "AB")]
-    fn test_to_pascal_case(#[case] input: &str, #[case] expected: &str) {
-        assert_eq!(to_pascal_case(input), expected);
-    }
 }
diff --git a/crates/vespertide-exporter/src/lib.rs b/crates/vespertide-exporter/src/lib.rs
index e09a7f72..d59c382e 100644
--- a/crates/vespertide-exporter/src/lib.rs
+++ b/crates/vespertide-exporter/src/lib.rs
@@ -4,7 +4,7 @@
 pub mod jpa;
 pub mod orm;
 mod parallel_config;
-mod python_naming;
+pub mod python_naming;
 pub mod seaorm;
 pub mod sqlalchemy;
 pub mod sqlmodel;
diff --git a/crates/vespertide-exporter/src/python_naming.rs b/crates/vespertide-exporter/src/python_naming.rs
index 1ae00e89..0f77980b 100644
--- a/crates/vespertide-exporter/src/python_naming.rs
+++ b/crates/vespertide-exporter/src/python_naming.rs
@@ -10,7 +10,11 @@
 /// Convert snake_case (or single-word) input to PascalCase. Splits on
 /// underscores, upper-cases the first character of each segment, and
 /// preserves the remainder verbatim.
-pub(crate) fn to_pascal_case(s: &str) -> String {
+///
+/// Public so the `vespertide-cli` `export` command can reuse the exact same
+/// PascalCase semantics for JPA filename derivation without keeping a
+/// duplicate private implementation.
+pub fn to_pascal_case(s: &str) -> String {
     s.split('_')
         .map(|word| {
             let mut chars = word.chars();
diff --git a/crates/vespertide-exporter/src/seaorm/relations/naming.rs b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
index 343cdc60..34f90327 100644
--- a/crates/vespertide-exporter/src/seaorm/relations/naming.rs
+++ b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
@@ -91,16 +91,12 @@ pub(in crate::seaorm) fn infer_field_name_from_fk_column(
     }
 
     // If the sanitized name is exactly the table name (e.g., "user_id" -> "user" for table "user"),
-    // we need to fall back to the table name for proper disambiguation
+    // we need to fall back to the table name for proper disambiguation.
+    // Otherwise, use the inferred sanitized name from the column — this naturally
+    // covers compound forms like "creator_user" for table "user".
     if sanitized_lower == table_lower {
         sanitize_field_name(table_name)
-    }
-    // If the sanitized name ends with (but is not equal to) the table name, use it as-is
-    // This handles cases like "creator_user" for table "user"
-    else if sanitized_lower.ends_with(&table_lower) {
-        sanitized
     } else {
-        // Otherwise, use the inferred name from the column
         sanitized
     }
 }
diff --git a/crates/vespertide-query/src/sql/add_constraint/mod.rs b/crates/vespertide-query/src/sql/add_constraint/mod.rs
index 7fcccc53..e78f93a9 100644
--- a/crates/vespertide-query/src/sql/add_constraint/mod.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/mod.rs
@@ -928,12 +928,62 @@ mod tests {
 
 mod unique;
 
-use vespertide_core::{TableConstraint, TableDef};
+use vespertide_core::{TableConstraint, TableDef, schema::names::names_to_strings};
 
 use super::helpers::{build_sqlite_table_rebuild, require_table_in_schema};
 use super::types::{BuiltQuery, DatabaseBackend};
 use crate::error::QueryError;
 
+/// Locate the single-column PRIMARY KEY for `table` in `current_schema`,
+/// returning `None` whenever the table has no usable PK (missing, composite,
+/// or the PK column appears inside `cols` — the last case would turn a
+/// `NOT IN (SELECT MIN(pk) ... GROUP BY pk)` cleanup into a tautology).
+///
+/// Shared by [`primary_key::build_primary_key`]'s pre-cleanup and
+/// [`unique::build_unique`]'s pre-cleanup; co-locating the resolver here
+/// eliminates the drift risk of two byte-identical private copies.
+pub(super) fn try_resolve_single_pk_column>(
+    table: &str,
+    current_schema: &[TableDef],
+    cols: &[T],
+) -> Option {
+    let table_def = current_schema.iter().find(|t| t.name.as_str() == table)?;
+
+    let pk_columns: Vec = table_def
+        .constraints
+        .iter()
+        .find_map(|c| {
+            if let TableConstraint::PrimaryKey { columns, .. } = c {
+                Some(names_to_strings(columns))
+            } else {
+                None
+            }
+        })
+        .or_else(|| {
+            let inline: Vec = table_def
+                .columns
+                .iter()
+                .filter(|col| col.primary_key.is_some())
+                .map(|col| col.name.to_string())
+                .collect();
+            if inline.is_empty() {
+                None
+            } else {
+                Some(inline)
+            }
+        })?;
+
+    if pk_columns.len() != 1 {
+        return None;
+    }
+    let pk_column = pk_columns.into_iter().next().expect("len == 1");
+    let cols_ref: Vec<&str> = cols.iter().map(AsRef::as_ref).collect();
+    if cols_ref.iter().any(|c| *c == pk_column) {
+        return None;
+    }
+    Some(pk_column)
+}
+
 pub fn build_add_constraint(
     backend: DatabaseBackend,
     table: &str,
diff --git a/crates/vespertide-query/src/sql/add_constraint/primary_key.rs b/crates/vespertide-query/src/sql/add_constraint/primary_key.rs
index 11a6968f..0f75922a 100644
--- a/crates/vespertide-query/src/sql/add_constraint/primary_key.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/primary_key.rs
@@ -1,6 +1,4 @@
-use vespertide_core::{
-    KeepPolicy, PrimaryKeyAdditionStrategy, TableConstraint, schema::names::names_to_strings,
-};
+use vespertide_core::{KeepPolicy, PrimaryKeyAdditionStrategy, TableConstraint};
 
 use super::super::helpers::{quote_ident, quote_idents};
 use super::super::types::{BuiltQuery, DatabaseBackend, RawSql};
@@ -70,7 +68,8 @@ fn build_pk_pre_cleanup>(
         #[cfg(not(tarpaulin_include))]
         _ => return vec![], // `#[non_exhaustive]` future-variant guard; unreachable today.
     };
-    let Some(old_pk_column) = try_resolve_single_pk_column(table, current_schema, new_pk_columns)
+    let Some(old_pk_column) =
+        super::try_resolve_single_pk_column(table, current_schema, new_pk_columns)
     else {
         return vec![];
     };
@@ -88,51 +87,10 @@ fn build_pk_pre_cleanup>(
     vec![BuiltQuery::Raw(RawSql::uniform(sql))]
 }
 
-fn try_resolve_single_pk_column>(
-    table: &str,
-    current_schema: &[TableDef],
-    new_pk_columns: &[T],
-) -> Option {
-    let table_def = current_schema.iter().find(|t| t.name.as_str() == table)?;
-
-    let pk_columns: Vec = table_def
-        .constraints
-        .iter()
-        .find_map(|c| {
-            if let TableConstraint::PrimaryKey { columns, .. } = c {
-                Some(names_to_strings(columns))
-            } else {
-                None
-            }
-        })
-        .or_else(|| {
-            let inline: Vec = table_def
-                .columns
-                .iter()
-                .filter(|col| col.primary_key.is_some())
-                .map(|col| col.name.to_string())
-                .collect();
-            if inline.is_empty() {
-                None
-            } else {
-                Some(inline)
-            }
-        })?;
-
-    if pk_columns.len() != 1 {
-        return None;
-    }
-    let pk_column = pk_columns.into_iter().next().expect("len == 1");
-    let new_set: Vec<&str> = new_pk_columns.iter().map(AsRef::as_ref).collect();
-    if new_set.iter().any(|c| *c == pk_column) {
-        return None;
-    }
-    Some(pk_column)
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
+    use super::super::try_resolve_single_pk_column;
     use rstest::rstest;
     use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType};
 
diff --git a/crates/vespertide-query/src/sql/add_constraint/unique.rs b/crates/vespertide-query/src/sql/add_constraint/unique.rs
index 788eb09e..f3199d92 100644
--- a/crates/vespertide-query/src/sql/add_constraint/unique.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/unique.rs
@@ -1,9 +1,6 @@
 use sea_query::{Alias, Index};
 
-use vespertide_core::{
-    KeepPolicy, TableConstraint, TableDef, UniqueConstraintStrategy,
-    schema::names::names_to_strings,
-};
+use vespertide_core::{KeepPolicy, TableDef, UniqueConstraintStrategy};
 
 use super::super::helpers::{build_unique_constraint_name, quote_ident, quote_idents};
 use super::super::types::{BuiltQuery, DatabaseBackend, RawSql};
@@ -63,7 +60,8 @@ fn build_pre_cleanup_delete>(
     // pre-cleanup `DELETE` and let the database decide. Production schemas
     // are guaranteed to have a single-column PK by F12 Scenario E, so this
     // path is reached only by test fixtures or hand-edited migrations.
-    let Some(pk_column) = try_resolve_single_pk_column(table, current_schema, columns) else {
+    let Some(pk_column) = super::try_resolve_single_pk_column(table, current_schema, columns)
+    else {
         return Ok(vec![]);
     };
     let agg = match keep {
@@ -80,57 +78,6 @@ fn build_pre_cleanup_delete>(
     Ok(vec![BuiltQuery::Raw(RawSql::uniform(sql))])
 }
 
-/// Locate the single-column PRIMARY KEY for `table` in `current_schema`,
-/// returning `None` for tables without a usable PK (missing, composite,
-/// or PK column inside the unique set — the last would make
-/// `NOT IN (SELECT MIN(pk) ... GROUP BY pk)` a tautology).
-///
-/// The caller skips pre-cleanup when this returns `None`. Production
-/// schemas reach the cleanup path only when F12 Scenario E has confirmed
-/// a single-column PK; non-production fixtures degrade to v0.1.x
-/// behaviour (no DELETE, DB-side rejection).
-fn try_resolve_single_pk_column>(
-    table: &str,
-    current_schema: &[TableDef],
-    unique_columns: &[T],
-) -> Option {
-    let table_def = current_schema.iter().find(|t| t.name.as_str() == table)?;
-
-    let pk_columns: Vec = table_def
-        .constraints
-        .iter()
-        .find_map(|c| {
-            if let TableConstraint::PrimaryKey { columns, .. } = c {
-                Some(names_to_strings(columns))
-            } else {
-                None
-            }
-        })
-        .or_else(|| {
-            let inline: Vec = table_def
-                .columns
-                .iter()
-                .filter(|col| col.primary_key.is_some())
-                .map(|col| col.name.to_string())
-                .collect();
-            if inline.is_empty() {
-                None
-            } else {
-                Some(inline)
-            }
-        })?;
-
-    if pk_columns.len() != 1 {
-        return None;
-    }
-    let pk_column = pk_columns.into_iter().next().expect("len == 1");
-    let unique_set: Vec<&str> = unique_columns.iter().map(AsRef::as_ref).collect();
-    if unique_set.iter().any(|c| *c == pk_column) {
-        return None;
-    }
-    Some(pk_column)
-}
-
 fn build_unique_index>(table: &str, name: Option<&str>, columns: &[T]) -> BuiltQuery {
     let index_name = build_unique_constraint_name(table, columns, name);
     let mut idx = Index::create()
@@ -147,8 +94,9 @@ fn build_unique_index>(table: &str, name: Option<&str>, columns: &
 #[cfg(test)]
 mod tests {
     use super::*;
+    use super::super::try_resolve_single_pk_column;
     use rstest::rstest;
-    use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType};
+    use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableConstraint};
 
     fn schema_with_single_pk() -> Vec {
         let mut id_col = ColumnDef::new("id", ColumnType::Simple(SimpleColumnType::Integer), false);

From 4472e8b6dee49d4fd535df6914db4bfc8607147b Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 11:39:23 +0900
Subject: [PATCH 041/109] =?UTF-8?q?retry-now#0040:=20batch=20=E2=80=94=20d?=
 =?UTF-8?q?edupe=20LSP=20lock-poisoned=20expects=20and=20PG=20ALTER=20COLU?=
 =?UTF-8?q?MN=20helper=20(3=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Item 1 — Hoist WorkspaceTables::{read_inner, write_inner} lock helpers

  9 sites collapsed to one read_inner/write_inner pair + LOCK_POISONED_MSG const.

Item 2 — Hoist WorkspaceIndex::{read_inner, write_inner} lock helpers

  5 sites collapsed to one read_inner/write_inner pair + LOCK_POISONED_MSG const.

Item 3 — Hoist push_pg_alter_column composition helper in modify_column_type/direct.rs

  3 push_postgres_raw(queries, build_pg_alter_column_sql(...)) nested calls collapsed.
---
 crates/vespertide-lsp/src/workspace_index.rs  | 37 +++++------
 crates/vespertide-lsp/src/workspace_tables.rs | 61 ++++++-------------
 .../src/sql/modify_column_type/direct.rs      | 37 +++++------
 3 files changed, 55 insertions(+), 80 deletions(-)

diff --git a/crates/vespertide-lsp/src/workspace_index.rs b/crates/vespertide-lsp/src/workspace_index.rs
index 996ec3e7..4030476c 100644
--- a/crates/vespertide-lsp/src/workspace_index.rs
+++ b/crates/vespertide-lsp/src/workspace_index.rs
@@ -36,12 +36,23 @@ struct WorkspaceIndexInner {
     by_uri: BTreeMap,
 }
 
+const LOCK_POISONED_MSG: &str =
+    "workspace_index lock poisoned — invariant: no panic while holding lock";
+
 impl WorkspaceIndex {
     #[must_use]
     pub fn new() -> Self {
         Self::default()
     }
 
+    fn read_inner(&self) -> std::sync::RwLockReadGuard<'_, WorkspaceIndexInner> {
+        self.inner.read().expect(LOCK_POISONED_MSG)
+    }
+
+    fn write_inner(&self) -> std::sync::RwLockWriteGuard<'_, WorkspaceIndexInner> {
+        self.inner.write().expect(LOCK_POISONED_MSG)
+    }
+
     /// Upsert: parse the document's top-level `name` field and update both
     /// maps. Returns the previous name (if any) for diagnostic display.
     ///
@@ -49,10 +60,7 @@ impl WorkspaceIndex {
     /// Panics if the internal lock is poisoned (process-wide invariant).
     pub fn upsert(&self, uri: &Uri, source: &str, tree: &Tree) -> Option {
         let new_name = extract_top_level_name(source, tree);
-        let mut inner = self
-            .inner
-            .write()
-            .expect("workspace_index lock poisoned — invariant: no panic while holding lock");
+        let mut inner = self.write_inner();
         let old = inner.by_uri.get(uri).cloned();
         if let Some(old_name) = &old {
             // Only remove the by_name entry if it still points to this URI
@@ -78,10 +86,7 @@ impl WorkspaceIndex {
     /// # Panics
     /// Panics if the internal lock is poisoned.
     pub fn remove(&self, uri: &Uri) {
-        let mut inner = self
-            .inner
-            .write()
-            .expect("workspace_index lock poisoned — invariant: no panic while holding lock");
+        let mut inner = self.write_inner();
         if let Some(name) = inner.by_uri.remove(uri)
             && inner.by_name.get(&name) == Some(uri)
         {
@@ -95,10 +100,7 @@ impl WorkspaceIndex {
     /// Panics if the internal lock is poisoned.
     #[must_use]
     pub fn lookup(&self, table_name: &str) -> Option {
-        let inner = self
-            .inner
-            .read()
-            .expect("workspace_index lock poisoned — invariant: no panic while holding lock");
+        let inner = self.read_inner();
         inner
             .by_name
             .get(table_name)
@@ -111,10 +113,7 @@ impl WorkspaceIndex {
     /// Panics if the internal lock is poisoned.
     #[must_use]
     pub fn tables(&self) -> Vec {
-        let inner = self
-            .inner
-            .read()
-            .expect("workspace_index lock poisoned — invariant: no panic while holding lock");
+        let inner = self.read_inner();
         inner.by_name.keys().cloned().collect()
     }
 
@@ -124,11 +123,7 @@ impl WorkspaceIndex {
     /// Panics if the internal lock is poisoned.
     #[must_use]
     pub fn len(&self) -> usize {
-        self.inner
-            .read()
-            .expect("workspace_index lock poisoned — invariant: no panic while holding lock")
-            .by_name
-            .len()
+        self.read_inner().by_name.len()
     }
 
     /// `true` if no tables are indexed.
diff --git a/crates/vespertide-lsp/src/workspace_tables.rs b/crates/vespertide-lsp/src/workspace_tables.rs
index 2742d3c5..72058672 100644
--- a/crates/vespertide-lsp/src/workspace_tables.rs
+++ b/crates/vespertide-lsp/src/workspace_tables.rs
@@ -50,11 +50,22 @@ struct CachedTree {
     format: DocumentFormat,
 }
 
+const LOCK_POISONED_MSG: &str =
+    "workspace_tables lock poisoned — invariant: no panic while holding lock";
+
 impl WorkspaceTables {
     pub fn new() -> Self {
         Self::default()
     }
 
+    fn read_inner(&self) -> std::sync::RwLockReadGuard<'_, Inner> {
+        self.inner.read().expect(LOCK_POISONED_MSG)
+    }
+
+    fn write_inner(&self) -> std::sync::RwLockWriteGuard<'_, Inner> {
+        self.inner.write().expect(LOCK_POISONED_MSG)
+    }
+
     /// Walk up from `start` looking for `vespertide.json`, then load all models.
     ///
     /// Returns `true` only when a config was found and at least one table loaded.
@@ -73,9 +84,7 @@ impl WorkspaceTables {
                 collect_models(&models_dir, &mut by_name, &mut path_by_name);
                 let count = by_name.len();
 
-                *self.inner.write().expect(
-                    "workspace_tables lock poisoned — invariant: no panic while holding lock",
-                ) = Inner {
+                *self.write_inner() = Inner {
                     root: Some(root),
                     by_name,
                     path_by_name,
@@ -91,9 +100,7 @@ impl WorkspaceTables {
                 false
             }
         } else {
-            *self.inner.write().expect(
-                "workspace_tables lock poisoned — invariant: no panic while holding lock",
-            ) = Inner::default();
+            *self.write_inner() = Inner::default();
             self.generation.fetch_add(1, Ordering::Relaxed);
             false
         }
@@ -108,28 +115,15 @@ impl WorkspaceTables {
     }
 
     pub fn get(&self, name: &str) -> Option {
-        self.inner
-            .read()
-            .expect("workspace_tables lock poisoned — invariant: no panic while holding lock")
-            .by_name
-            .get(name)
-            .cloned()
+        self.read_inner().by_name.get(name).cloned()
     }
 
     pub fn names(&self) -> Vec {
-        self.inner
-            .read()
-            .expect("workspace_tables lock poisoned — invariant: no panic while holding lock")
-            .by_name
-            .keys()
-            .cloned()
-            .collect()
+        self.read_inner().by_name.keys().cloned().collect()
     }
 
     pub fn all(&self) -> Vec<(String, TableDef)> {
-        self.inner
-            .read()
-            .expect("workspace_tables lock poisoned — invariant: no panic while holding lock")
+        self.read_inner()
             .by_name
             .iter()
             .map(|(name, table)| (name.clone(), table.clone()))
@@ -137,11 +131,7 @@ impl WorkspaceTables {
     }
 
     pub fn root(&self) -> Option {
-        self.inner
-            .read()
-            .expect("workspace_tables lock poisoned — invariant: no panic while holding lock")
-            .root
-            .clone()
+        self.read_inner().root.clone()
     }
 
     /// Look up the on-disk file path that declared `table_name`. Cached at
@@ -149,12 +139,7 @@ impl WorkspaceTables {
     /// `media.json`, `media.vespertide.json`, or `models/whatever.json`
     /// regardless of the filename convention.
     pub fn model_path(&self, table_name: &str) -> Option {
-        self.inner
-            .read()
-            .expect("workspace_tables lock poisoned — invariant: no panic while holding lock")
-            .path_by_name
-            .get(table_name)
-            .cloned()
+        self.read_inner().path_by_name.get(table_name).cloned()
     }
 
     /// Lookup or parse on-demand.
@@ -174,10 +159,7 @@ impl WorkspaceTables {
         let format = format_for_path(path);
 
         {
-            let inner = self
-                .inner
-                .read()
-                .expect("workspace_tables lock poisoned — invariant: no panic while holding lock");
+            let inner = self.read_inner();
             if let Some(entry) = inner.tree_cache.get(path)
                 && entry.mtime == mtime
                 && entry.format == format
@@ -193,10 +175,7 @@ impl WorkspaceTables {
         let tree_arc = Arc::new(tree);
 
         {
-            let mut inner = self
-                .inner
-                .write()
-                .expect("workspace_tables lock poisoned — invariant: no panic while holding lock");
+            let mut inner = self.write_inner();
             inner.tree_cache.insert(
                 path.to_path_buf(),
                 CachedTree {
diff --git a/crates/vespertide-query/src/sql/modify_column_type/direct.rs b/crates/vespertide-query/src/sql/modify_column_type/direct.rs
index 8d19f0f2..8e7cbce5 100644
--- a/crates/vespertide-query/src/sql/modify_column_type/direct.rs
+++ b/crates/vespertide-query/src/sql/modify_column_type/direct.rs
@@ -21,6 +21,16 @@ fn push_postgres_raw(queries: &mut Vec, sql: String) {
     queries.push(BuiltQuery::Raw(RawSql::postgres_only(sql)));
 }
 
+/// Emit one PostgreSQL-only `ALTER TABLE ... ALTER COLUMN ...` statement.
+///
+/// Centralises the `push_postgres_raw(queries, build_pg_alter_column_sql(...))`
+/// two-call composition used three times by `build_postgres_enum_migration`
+/// (DROP DEFAULT / TYPE … USING …::text::… / SET DEFAULT …) so each call site
+/// reads as "alter this column with this suffix" instead of plumbing.
+fn push_pg_alter_column(queries: &mut Vec, table: &str, column: &str, suffix: &str) {
+    push_postgres_raw(queries, build_pg_alter_column_sql(table, column, suffix));
+}
+
 struct DirectBuildContext<'a> {
     backend: DatabaseBackend,
     table: &'a str,
@@ -119,22 +129,15 @@ fn build_postgres_enum_migration(queries: &mut Vec, context: &Direct
 
         // 2. DROP DEFAULT if exists (must be done before type change).
         if column_default.is_some() {
-            push_postgres_raw(
-                queries,
-                build_pg_alter_column_sql(context.table, context.column, "DROP DEFAULT"),
-            );
+            push_pg_alter_column(queries, context.table, context.column, "DROP DEFAULT");
         }
 
         // 3. ALTER TABLE ... ALTER COLUMN ... TYPE target_type USING col::text::target_type.
-        push_postgres_raw(
+        push_pg_alter_column(
             queries,
-            build_pg_alter_column_sql(
-                context.table,
-                context.column,
-                &format!(
-                    "TYPE {quoted_target_type} USING {quoted_column}::text::{quoted_target_type}"
-                ),
-            ),
+            context.table,
+            context.column,
+            &format!("TYPE {quoted_target_type} USING {quoted_column}::text::{quoted_target_type}"),
         );
 
         // 4. DROP old enum type.
@@ -152,13 +155,11 @@ fn build_postgres_enum_migration(queries: &mut Vec, context: &Direct
         if let Some(default_value) = column_default {
             let normalized_default =
                 normalize_enum_default(context.new_type, &default_value.to_sql());
-            push_postgres_raw(
+            push_pg_alter_column(
                 queries,
-                build_pg_alter_column_sql(
-                    context.table,
-                    context.column,
-                    &format!("SET DEFAULT {normalized_default}"),
-                ),
+                context.table,
+                context.column,
+                &format!("SET DEFAULT {normalized_default}"),
             );
         }
     }

From 5c9fd3abc191b6404168f36c3ee1f369ff0f2fbf Mon Sep 17 00:00:00 2001
From: retry-now 
Date: Sun, 28 Jun 2026 12:11:05 +0900
Subject: [PATCH 042/109] retry-now#0041: batch - hoist find_baseline_column,
 drop dead CheckExprAt.table, adopt tree_util::is_pair (3 kept)

Iteration 41 IMPROVE - 3/3 batch items kept (analyze report .retry-now/reports/0041-analyze.md):

1. crates/vespertide-planner: widen drop_resolution::find_baseline_column to pub(crate) and replace two open-coded inline lookups in validate::type_narrowing / validate::timezone_conversion (6-line iter chains -> 3-line shared call); SoT for baseline column lookup now lives in one place.

2. crates/vespertide-lsp/src/check_expr_locate.rs: delete dead CheckExprAt.table field (was #[expect(dead_code, reason = ...)]) and the only-used-to-populate-it owning_table_name helper (-61 lines); production callers (code_actions, rename) only read ctx.inner.

3. crates/vespertide-lsp/src/diagnostics/validation: delete duplicate is_pair_node and route the two types.rs call sites + one visitors.rs call site through the canonical tree_util::is_pair (which already consolidated 6 pre-0.2.0 copies).

Verified GREEN under documented Windows MSVC pg_query workaround (CFLAGS=-Wno-error=incompatible-pointer-types): clippy --workspace --all-targets --all-features -- -D warnings clean; cargo test --workspace --all-features --exclude vespertide-query 46/0; cargo test -p vespertide-query --lib 732/0; rustfmt --check on touched files exit 0.
---
 .../vespertide-lsp/src/check_expr_locate.rs   | 61 +------------------
 .../src/diagnostics/validation/types.rs       | 10 ++-
 .../src/diagnostics/validation/visitors.rs    |  5 +-
 .../vespertide-planner/src/drop_resolution.rs |  2 +-
 .../src/validate/timezone_conversion.rs       |  9 +--
 .../src/validate/type_narrowing.rs            |  9 +--
 6 files changed, 17 insertions(+), 79 deletions(-)

diff --git a/crates/vespertide-lsp/src/check_expr_locate.rs b/crates/vespertide-lsp/src/check_expr_locate.rs
index f6e57a55..9cf86786 100644
--- a/crates/vespertide-lsp/src/check_expr_locate.rs
+++ b/crates/vespertide-lsp/src/check_expr_locate.rs
@@ -18,16 +18,12 @@ use crate::text_util::strip_quotes;
 use crate::tree_util::node_at_byte;
 
 /// Cursor-based result of [`find_check_expr_at`]: the CHECK `expr` string
-/// the cursor sits in, the inner byte range of its predicate text, and the
-/// name of the owning table.
+/// the cursor sits in and the inner byte range of its predicate text.
 pub(crate) struct CheckExprAt {
     /// Byte range of the CHECK predicate text inside the document,
     /// excluding surrounding quotes / YAML block-scalar indicators.
     /// Same range [`crate::check_expr_range::expr_inner_range`] returns.
     pub inner: Range,
-    /// Owning table (the outermost mapping's `name` value).
-    #[expect(dead_code, reason = "field reserved for future use")]
-    pub table: String,
 }
 
 /// If `byte_offset` lands inside a table-level CHECK `expr` string,
@@ -58,9 +54,8 @@ pub(crate) fn find_check_expr_at(
     }
 
     let inner = crate::check_expr_range::expr_inner_range(string_node)?;
-    let table = owning_table_name(source, pair)?;
 
-    Some(CheckExprAt { inner, table })
+    Some(CheckExprAt { inner })
 }
 
 /// True when `expr_pair` (a pair whose key is `expr`) belongs to a CHECK
@@ -73,42 +68,6 @@ pub(crate) fn is_check_constraint_pair(source: &[u8], expr_pair: Node<'_>) -> bo
     sibling_value(source, expr_pair, "type").is_some_and(|v| v == "check")
 }
 
-/// The owning table name — the document's outermost mapping's `name`
-/// value. Walks up from `node` to the outermost `object`/`block_mapping`/
-/// `flow_mapping` and returns its direct `name` child's stripped scalar.
-pub(crate) fn owning_table_name(source: &[u8], node: Node<'_>) -> Option {
-    let mut current = node.parent();
-    let mut outer = None;
-    while let Some(candidate) = current {
-        if matches!(
-            candidate.kind(),
-            "object" | "block_mapping" | "flow_mapping"
-        ) {
-            outer = Some(candidate);
-        }
-        current = candidate.parent();
-    }
-    let outer = outer?;
-    let mut cursor = outer.walk();
-    for child in outer.children(&mut cursor) {
-        if matches!(child.kind(), "pair" | "block_mapping_pair")
-            && let Some(key) = child.named_child(0)
-            && let Some(key_text) = source
-                .get(key.byte_range())
-                .and_then(|bytes| std::str::from_utf8(bytes).ok())
-            && strip_quotes(key_text) == "name"
-        {
-            let value = child.named_child(1)?;
-            let actual = peel_wrapper(value);
-            let text = source
-                .get(actual.byte_range())
-                .and_then(|bytes| std::str::from_utf8(bytes).ok())?;
-            return Some(strip_quotes(text).to_string());
-        }
-    }
-    None
-}
-
 // ---------------------------------------------------------------------------
 // Private helpers
 // ---------------------------------------------------------------------------
@@ -223,24 +182,10 @@ mod tests {
     }
 
     #[test]
-    fn owning_table_name_skips_non_name_keys_and_returns_none_without_name() {
-        let src = r#"{"comment":"x","constraints":[{"type":"check","expr":"age > 0"}]}"#;
-        let tree = parse(src);
-        let expr_pair = find_pair(tree.root_node(), src.as_bytes(), "expr").unwrap();
-
-        assert!(owning_table_name(src.as_bytes(), expr_pair).is_none());
-    }
-
-    #[test]
-    fn owning_table_name_and_sibling_value_skip_invalid_utf8_keys() {
+    fn sibling_value_skips_invalid_utf8_type_key() {
         let src = r#"{"name":"u","constraints":[{"type":"check","expr":"age > 0"}]}"#;
         let tree = parse(src);
         let expr_pair = find_pair(tree.root_node(), src.as_bytes(), "expr").unwrap();
-        let mut bad = src.as_bytes().to_vec();
-        let name_key = src.find("name").unwrap();
-        bad[name_key] = 0xff;
-
-        assert!(owning_table_name(&bad, expr_pair).is_none());
 
         let type_key = src.find("type").unwrap();
         let mut bad_type = src.as_bytes().to_vec();
diff --git a/crates/vespertide-lsp/src/diagnostics/validation/types.rs b/crates/vespertide-lsp/src/diagnostics/validation/types.rs
index 0020f933..14dc71c8 100644
--- a/crates/vespertide-lsp/src/diagnostics/validation/types.rs
+++ b/crates/vespertide-lsp/src/diagnostics/validation/types.rs
@@ -36,6 +36,8 @@ pub(super) struct EnumValueDescriptor {
 
 pub(super) use crate::tree_util::unwrap_yaml_node;
 
+use crate::tree_util::is_pair;
+
 pub(super) fn collect_enum_value_descriptors(
     array: tree_sitter::Node<'_>,
     source: &[u8],
@@ -133,7 +135,7 @@ pub(super) fn find_value_for_key<'tree>(
 ) -> Option> {
     let mut cursor = node.walk();
     for child in node.children(&mut cursor) {
-        if is_pair_node(child)
+        if is_pair(child)
             && pair_key_text(child, source).is_some_and(|k| k == target_key)
             && let Some(value) = child.named_child(1)
         {
@@ -153,14 +155,10 @@ pub(super) fn find_pair_with_key<'tree>(
 ) -> Option> {
     let mut cursor = object.walk();
     object.children(&mut cursor).find(|&child| {
-        is_pair_node(child) && pair_key_text(child, source).is_some_and(|k| k == target_key)
+        is_pair(child) && pair_key_text(child, source).is_some_and(|k| k == target_key)
     })
 }
 
-pub(super) fn is_pair_node(node: tree_sitter::Node<'_>) -> bool {
-    matches!(node.kind(), "pair" | "block_mapping_pair")
-}
-
 pub(super) fn pair_key_text<'a>(pair: tree_sitter::Node<'_>, source: &'a [u8]) -> Option<&'a str> {
     let key = pair.named_child(0)?;
     let text = source
diff --git a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
index 85a46857..f67107ec 100644
--- a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
+++ b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
@@ -4,8 +4,9 @@ use crate::diagnostics::{DomainDiagnostic, Severity};
 use super::types::find_value_for_key;
 use super::types::{
     EnumValueDescriptor, KNOWN_SIMPLE_TYPES, collect_enum_value_descriptors, find_pair_with_key,
-    is_pair_node, pair_key_text, scalar_text, strip_quotes_str, unwrap_yaml_node,
+    pair_key_text, scalar_text, strip_quotes_str, unwrap_yaml_node,
 };
+use crate::tree_util::is_pair;
 
 pub(in crate::diagnostics) fn collect_all(
     tree: &tree_sitter::Tree,
@@ -92,7 +93,7 @@ impl<'source, 'tree> FusedCollector<'source, 'tree> {
         node: tree_sitter::Node<'tree>,
     ) -> Option> {
         if self.columns.is_some()
-            || !is_pair_node(node)
+            || !is_pair(node)
             || pair_key_text(node, self.source).is_none_or(|key| key != "columns")
         {
             return None;
diff --git a/crates/vespertide-planner/src/drop_resolution.rs b/crates/vespertide-planner/src/drop_resolution.rs
index 6f10814f..326f27ea 100644
--- a/crates/vespertide-planner/src/drop_resolution.rs
+++ b/crates/vespertide-planner/src/drop_resolution.rs
@@ -157,7 +157,7 @@ pub fn find_drop_resolutions(plan: &MigrationPlan, baseline: &[TableDef]) -> Vec
 /// in this module (`resolve_column_drop`, `apply_column_rename`) into a single
 /// named helper. Returns a borrowed `&ColumnDef`; callers `.cloned()` when
 /// they need ownership.
-fn find_baseline_column<'a>(
+pub(crate) fn find_baseline_column<'a>(
     baseline: &'a [TableDef],
     table: &str,
     column: &str,
diff --git a/crates/vespertide-planner/src/validate/timezone_conversion.rs b/crates/vespertide-planner/src/validate/timezone_conversion.rs
index 19817d1e..43cd6053 100644
--- a/crates/vespertide-planner/src/validate/timezone_conversion.rs
+++ b/crates/vespertide-planner/src/validate/timezone_conversion.rs
@@ -24,6 +24,8 @@
 
 use vespertide_core::{ColumnType, MigrationAction, MigrationPlan, SimpleColumnType, TableDef};
 
+use crate::drop_resolution::find_baseline_column;
+
 /// Direction of a `timestamp` ⇄ `timestamptz` conversion.
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum TimezoneConversionDirection {
@@ -104,12 +106,7 @@ fn warning_for_action(
     else {
         return None;
     };
-    let old_type = baseline
-        .iter()
-        .find(|t| t.name == *table)?
-        .columns
-        .iter()
-        .find(|c| c.name == *column)?
+    let old_type = find_baseline_column(baseline, table.as_str(), column.as_str())?
         .r#type
         .clone();
     let direction = classify_direction(&old_type, new_type)?;
diff --git a/crates/vespertide-planner/src/validate/type_narrowing.rs b/crates/vespertide-planner/src/validate/type_narrowing.rs
index 42171c30..76e518ce 100644
--- a/crates/vespertide-planner/src/validate/type_narrowing.rs
+++ b/crates/vespertide-planner/src/validate/type_narrowing.rs
@@ -24,6 +24,8 @@ use vespertide_core::{
     ColumnType, ComplexColumnType, MigrationAction, MigrationPlan, SimpleColumnType, TableDef,
 };
 
+use crate::drop_resolution::find_baseline_column;
+
 /// A single column whose `ModifyColumnType` action narrows the storable
 /// value range. Returned by [`find_type_narrowings`].
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -192,12 +194,7 @@ fn warning_for_action(
     else {
         return None;
     };
-    let old_type = baseline
-        .iter()
-        .find(|t| t.name == *table)?
-        .columns
-        .iter()
-        .find(|c| c.name == *column)?
+    let old_type = find_baseline_column(baseline, table.as_str(), column.as_str())?
         .r#type
         .clone();
     let kind = is_narrowing(&old_type, new_type)?;

From d53d124d198af17a574186351f7fec03e6ba61e0 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 12:40:26 +0900
Subject: [PATCH 043/109] retry-now#0042: batch - hoist apply::find_table_mut,
 drop dead JPA RenderedEntities accumulator, drop dead _baseline param (3
 kept)

---
 .../src/commands/revision/mod.rs              |  2 +-
 crates/vespertide-exporter/src/jpa/mod.rs     | 22 ++----------
 crates/vespertide-exporter/src/jpa/types.rs   |  6 ----
 .../src/apply/column_ops.rs                   | 11 +-----
 .../src/apply/constraint_ops.rs               | 11 +-----
 crates/vespertide-planner/src/apply/mod.rs    | 14 ++++++++
 .../vespertide-planner/src/apply/table_ops.rs |  6 ++--
 .../src/validate/constraint_type_changes.rs   | 36 +++++++------------
 8 files changed, 34 insertions(+), 74 deletions(-)

diff --git a/crates/vespertide-cli/src/commands/revision/mod.rs b/crates/vespertide-cli/src/commands/revision/mod.rs
index c10f8a2c..9d213912 100644
--- a/crates/vespertide-cli/src/commands/revision/mod.rs
+++ b/crates/vespertide-cli/src/commands/revision/mod.rs
@@ -88,7 +88,7 @@ fn ensure_no_dangling_fk_drops(plan: &MigrationPlan, baseline_schema: &[TableDef
 fn ensure_no_f12_errors(plan: &MigrationPlan, baseline_schema: &[TableDef]) -> Result<()> {
     let mut f12_errors: Vec = Vec::new();
     f12_errors.extend(find_constraint_type_changes(plan, baseline_schema));
-    f12_errors.extend(find_primary_key_removals(plan, baseline_schema));
+    f12_errors.extend(find_primary_key_removals(plan));
     if !f12_errors.is_empty() {
         let err = single_or_multiple_error(f12_errors);
         anyhow::bail!("{err}");
diff --git a/crates/vespertide-exporter/src/jpa/mod.rs b/crates/vespertide-exporter/src/jpa/mod.rs
index 8cc9927d..c441fceb 100644
--- a/crates/vespertide-exporter/src/jpa/mod.rs
+++ b/crates/vespertide-exporter/src/jpa/mod.rs
@@ -46,27 +46,11 @@ pub fn render_entities(schema: &[TableDef]) -> Result, String> {
             .collect::>()
     };
 
-    Ok(merge_rendered_entities(rendered).entities)
+    Ok(merge_rendered_entities(rendered))
 }
 
-struct RenderedEntities {
-    entities: Vec,
-    _imports: UsedImports,
-}
-
-fn merge_rendered_entities(rendered: Vec<(String, UsedImports)>) -> RenderedEntities {
-    let mut entities = Vec::with_capacity(rendered.len());
-    let mut imports = UsedImports::default();
-
-    for (entity, local_imports) in rendered {
-        entities.push(entity);
-        imports.merge(local_imports);
-    }
-
-    RenderedEntities {
-        entities,
-        _imports: imports,
-    }
+fn merge_rendered_entities(rendered: Vec<(String, UsedImports)>) -> Vec {
+    rendered.into_iter().map(|(entity, _)| entity).collect()
 }
 
 #[cfg(test)]
diff --git a/crates/vespertide-exporter/src/jpa/types.rs b/crates/vespertide-exporter/src/jpa/types.rs
index 70a4c130..7978a099 100644
--- a/crates/vespertide-exporter/src/jpa/types.rs
+++ b/crates/vespertide-exporter/src/jpa/types.rs
@@ -16,12 +16,6 @@ pub(super) struct UsedImports {
 }
 
 impl UsedImports {
-    pub(super) fn merge(&mut self, other: Self) {
-        self.java_time_types.extend(other.java_time_types);
-        self.needs_uuid |= other.needs_uuid;
-        self.needs_big_decimal |= other.needs_big_decimal;
-    }
-
     pub(super) fn add_column_type(&mut self, col_type: &ColumnType) {
         match col_type {
             ColumnType::Simple(ty) => match ty {
diff --git a/crates/vespertide-planner/src/apply/column_ops.rs b/crates/vespertide-planner/src/apply/column_ops.rs
index 392056f7..bc5a54d3 100644
--- a/crates/vespertide-planner/src/apply/column_ops.rs
+++ b/crates/vespertide-planner/src/apply/column_ops.rs
@@ -4,6 +4,7 @@ use vespertide_core::{
     ColumnDef, ColumnName, ColumnType, ComplexColumnType, EnumValues, TableConstraint, TableDef,
 };
 
+use super::find_table_mut;
 use crate::error::PlannerError;
 
 pub(super) fn add_column(
@@ -145,16 +146,6 @@ pub(super) fn modify_column_comment(
     Ok(())
 }
 
-fn find_table_mut<'a>(
-    schema: &'a mut [TableDef],
-    table: &str,
-) -> Result<&'a mut TableDef, PlannerError> {
-    schema
-        .iter_mut()
-        .find(|t| t.name == table)
-        .ok_or_else(|| PlannerError::TableNotFound(table.to_string()))
-}
-
 fn find_column_mut<'a>(
     schema: &'a mut [TableDef],
     table: &str,
diff --git a/crates/vespertide-planner/src/apply/constraint_ops.rs b/crates/vespertide-planner/src/apply/constraint_ops.rs
index fbcc0a9a..251f8ee7 100644
--- a/crates/vespertide-planner/src/apply/constraint_ops.rs
+++ b/crates/vespertide-planner/src/apply/constraint_ops.rs
@@ -1,5 +1,6 @@
 use vespertide_core::{ColumnDef, ColumnName, StrOrBoolOrArray, TableConstraint, TableDef};
 
+use super::find_table_mut;
 use crate::error::PlannerError;
 
 pub(super) fn add_constraint(
@@ -51,16 +52,6 @@ pub(super) fn replace_constraint(
     Ok(())
 }
 
-fn find_table_mut<'a>(
-    schema: &'a mut [TableDef],
-    table: &str,
-) -> Result<&'a mut TableDef, PlannerError> {
-    schema
-        .iter_mut()
-        .find(|t| t.name == table)
-        .ok_or_else(|| PlannerError::TableNotFound(table.to_string()))
-}
-
 /// Clear inline column fields that correspond to a constraint.
 /// This ensures `normalize()` won't re-add the constraint from stale inline fields.
 pub(super) fn clear_inline_constraint_fields(
diff --git a/crates/vespertide-planner/src/apply/mod.rs b/crates/vespertide-planner/src/apply/mod.rs
index 6c678c5b..de75d6a3 100644
--- a/crates/vespertide-planner/src/apply/mod.rs
+++ b/crates/vespertide-planner/src/apply/mod.rs
@@ -10,6 +10,20 @@ use vespertide_core::{MigrationAction, TableDef};
 
 use crate::error::PlannerError;
 
+/// Locate a table in `schema` by name and return a mutable reference, or
+/// [`PlannerError::TableNotFound`] when no match exists. Shared by the
+/// `column_ops`, `constraint_ops`, and `table_ops` submodules so the
+/// `TableNotFound` error semantics live in exactly one place.
+pub(super) fn find_table_mut<'a>(
+    schema: &'a mut [TableDef],
+    table: &str,
+) -> Result<&'a mut TableDef, PlannerError> {
+    schema
+        .iter_mut()
+        .find(|t| t.name == table)
+        .ok_or_else(|| PlannerError::TableNotFound(table.to_string()))
+}
+
 /// Apply a single migration action to an in-memory schema snapshot.
 pub fn apply_action(
     schema: &mut Vec,
diff --git a/crates/vespertide-planner/src/apply/table_ops.rs b/crates/vespertide-planner/src/apply/table_ops.rs
index 2d1303fb..28ff9618 100644
--- a/crates/vespertide-planner/src/apply/table_ops.rs
+++ b/crates/vespertide-planner/src/apply/table_ops.rs
@@ -1,5 +1,6 @@
 use vespertide_core::{ColumnDef, TableConstraint, TableDef};
 
+use super::find_table_mut;
 use crate::error::PlannerError;
 
 pub(super) fn create_table(
@@ -47,10 +48,7 @@ pub(super) fn rename_table(
         Err(PlannerError::TableExists(to.to_string()))
     } else {
         {
-            let tbl = schema
-                .iter_mut()
-                .find(|t| t.name == from)
-                .ok_or_else(|| PlannerError::TableNotFound(from.to_string()))?;
+            let tbl = find_table_mut(schema, from)?;
             tbl.name = to.into();
         }
         for tbl in schema {
diff --git a/crates/vespertide-planner/src/validate/constraint_type_changes.rs b/crates/vespertide-planner/src/validate/constraint_type_changes.rs
index f421fceb..eef56c0f 100644
--- a/crates/vespertide-planner/src/validate/constraint_type_changes.rs
+++ b/crates/vespertide-planner/src/validate/constraint_type_changes.rs
@@ -99,11 +99,12 @@ pub fn find_constraint_type_changes(
 /// Scan the plan for any PRIMARY KEY removal that is not paired with an
 /// add (or a table drop). Each unpaired drop yields one
 /// [`PlannerError::PrimaryKeyRemovedWithoutReplacement`].
+///
+/// Pure plan-only analysis: the baseline schema is never consulted because
+/// every signal — PK adds, PK drops, and table drops — is already encoded
+/// in `plan.actions`.
 #[must_use]
-pub fn find_primary_key_removals(
-    plan: &MigrationPlan,
-    _baseline: &[TableDef],
-) -> Vec {
+pub fn find_primary_key_removals(plan: &MigrationPlan) -> Vec {
     // Collect the tables that gain a PRIMARY KEY in this plan (either via
     // AddConstraint or as part of a CreateTable). These can absorb PK
     // removals on the same table.
@@ -422,10 +423,9 @@ mod tests {
     /// Case 7: Bare RemoveConstraint(PK) without any AddConstraint(PK).
     #[test]
     fn case_07_pk_removal_no_replacement() {
-        let baseline = vec![table("user", vec![col("id")], vec![pk(vec!["id"])])];
         let p = plan(vec![remove("user", pk(vec!["id"]))]);
 
-        let errs = find_primary_key_removals(&p, &baseline);
+        let errs = find_primary_key_removals(&p);
         assert_eq!(errs.len(), 1);
         assert!(matches!(
             &errs[0],
@@ -439,7 +439,6 @@ mod tests {
     /// the CreateTable arm (a `-> true` mutant would absorb the removal).
     #[test]
     fn create_table_without_pk_does_not_absorb_pk_removal() {
-        let baseline = vec![table("user", vec![col("id")], vec![pk(vec!["id"])])];
         let p = plan(vec![
             MigrationAction::CreateTable {
                 table: "user".into(),
@@ -449,7 +448,7 @@ mod tests {
             remove("user", pk(vec!["id"])),
         ]);
 
-        let errs = find_primary_key_removals(&p, &baseline);
+        let errs = find_primary_key_removals(&p);
         assert_eq!(
             errs.len(),
             1,
@@ -461,17 +460,12 @@ mod tests {
     /// (legitimate PK replacement).
     #[test]
     fn case_08_pk_removal_with_replacement() {
-        let baseline = vec![table(
-            "user",
-            vec![col("id"), col("uuid")],
-            vec![pk(vec!["id"])],
-        )];
         let p = plan(vec![
             remove("user", pk(vec!["id"])),
             add("user", pk(vec!["uuid"])),
         ]);
 
-        let errs = find_primary_key_removals(&p, &baseline);
+        let errs = find_primary_key_removals(&p);
         assert!(errs.is_empty());
     }
 
@@ -479,7 +473,6 @@ mod tests {
     /// going away).
     #[test]
     fn case_09_pk_removal_with_table_drop() {
-        let baseline = vec![table("user", vec![col("id")], vec![pk(vec!["id"])])];
         let p = plan(vec![
             remove("user", pk(vec!["id"])),
             MigrationAction::DeleteTable {
@@ -487,23 +480,19 @@ mod tests {
             },
         ]);
 
-        let errs = find_primary_key_removals(&p, &baseline);
+        let errs = find_primary_key_removals(&p);
         assert!(errs.is_empty());
     }
 
     /// Case 10: multiple unpaired PK removals → multiple errors.
     #[test]
     fn case_10_multiple_pk_removals_no_replacement() {
-        let baseline = vec![
-            table("a", vec![col("id")], vec![pk(vec!["id"])]),
-            table("b", vec![col("id")], vec![pk(vec!["id"])]),
-        ];
         let p = plan(vec![
             remove("a", pk(vec!["id"])),
             remove("b", pk(vec!["id"])),
         ]);
 
-        let errs = find_primary_key_removals(&p, &baseline);
+        let errs = find_primary_key_removals(&p);
         assert_eq!(errs.len(), 2);
     }
 
@@ -523,7 +512,7 @@ mod tests {
         ]);
 
         let type_change_errs = find_constraint_type_changes(&p, &baseline);
-        let pk_removal_errs = find_primary_key_removals(&p, &baseline);
+        let pk_removal_errs = find_primary_key_removals(&p);
 
         assert_eq!(type_change_errs.len(), 1, "A/B detector must fire");
         assert_eq!(
@@ -537,7 +526,6 @@ mod tests {
     /// (e.g. table recreation flow). No error.
     #[test]
     fn case_12_pk_removal_absorbed_by_create_table() {
-        let baseline = vec![table("user", vec![col("id")], vec![pk(vec!["id"])])];
         let p = plan(vec![
             remove("user", pk(vec!["id"])),
             MigrationAction::CreateTable {
@@ -547,7 +535,7 @@ mod tests {
             },
         ]);
 
-        let errs = find_primary_key_removals(&p, &baseline);
+        let errs = find_primary_key_removals(&p);
         assert!(errs.is_empty(), "expected absorbed PK, got: {errs:?}");
     }
 

From c44780b1816e69521330f14083e98ac5722847cf Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 13:09:03 +0900
Subject: [PATCH 044/109] =?UTF-8?q?retry-now#0043:=20batch=20=E2=80=94=20h?=
 =?UTF-8?q?oist=20LSP=20is=5Finside=5Fconstraints,=20dedupe=20CLI=20test?=
 =?UTF-8?q?=20bootstrap,=20hoist=20completion=5Flabels=20(3=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/commands/diff/tests/mod.rs            | 41 +++---------
 .../src/commands/revision/tests/mod.rs        | 29 +-------
 .../src/commands/revision/tests/recreate.rs   |  4 +-
 crates/vespertide-cli/src/commands/sql.rs     | 66 +++++--------------
 crates/vespertide-cli/src/commands/status.rs  | 58 ++++------------
 crates/vespertide-cli/src/test_support.rs     | 44 +++++++++++++
 .../vespertide-lsp/src/completion/context.rs  | 10 +--
 crates/vespertide-lsp/src/completion/mod.rs   |  9 +++
 .../vespertide-lsp/src/completion/values.rs   |  5 +-
 crates/vespertide-lsp/src/hover/check_expr.rs |  8 +--
 crates/vespertide-lsp/src/tree_util.rs        |  8 +++
 11 files changed, 103 insertions(+), 179 deletions(-)

diff --git a/crates/vespertide-cli/src/commands/diff/tests/mod.rs b/crates/vespertide-cli/src/commands/diff/tests/mod.rs
index 4ffc4974..4c0c17db 100644
--- a/crates/vespertide-cli/src/commands/diff/tests/mod.rs
+++ b/crates/vespertide-cli/src/commands/diff/tests/mod.rs
@@ -1,12 +1,10 @@
 use super::*;
-use crate::test_support::CwdGuard;
+use crate::test_support::{CwdGuard, write_default_config, write_simple_id_model};
 use colored::Colorize;
 use rstest::rstest;
 use serial_test::serial;
 use std::fs;
-use std::path::PathBuf;
 use tempfile::tempdir;
-use vespertide_config::VespertideConfig;
 use vespertide_core::{
     ColumnDef, ColumnType, MigrationPlan, ReferenceAction, SimpleColumnType, TableConstraint,
     TableDef,
@@ -15,29 +13,6 @@ use vespertide_planner::{
     NarrowingKind, PolicyDelta, TimezoneConversionDirection, TimezoneConversionWarning,
 };
 
-fn write_config() {
-    let cfg = VespertideConfig::default();
-    let text = serde_json::to_string_pretty(&cfg).unwrap();
-    fs::write("vespertide.json", text).unwrap();
-}
-
-fn write_model(name: &str) {
-    let models_dir = PathBuf::from("models");
-    fs::create_dir_all(&models_dir).unwrap();
-    let table = TableDef {
-        name: name.into(),
-        description: None,
-        columns: vec![ColumnDef::new(
-            "id",
-            ColumnType::Simple(SimpleColumnType::Integer),
-            false,
-        )],
-        constraints: vec![pk_id()],
-    };
-    let path = models_dir.join(format!("{name}.json"));
-    fs::write(path, serde_json::to_string_pretty(&table).unwrap()).unwrap();
-}
-
 fn idx(name: Option<&str>, cols: &[&str]) -> TableConstraint {
     TableConstraint::Index {
         name: name.map(Into::into),
@@ -210,8 +185,8 @@ async fn cmd_diff_with_model_and_no_migrations() {
     let tmp = tempdir().unwrap();
     let _guard = CwdGuard::new(&tmp.path().to_path_buf());
 
-    write_config();
-    write_model("users");
+    write_default_config();
+    write_simple_id_model("users");
     fs::create_dir_all("migrations").unwrap();
 
     let result = cmd_diff().await;
@@ -225,7 +200,7 @@ async fn cmd_diff_when_no_changes() {
     let tmp = tempdir().unwrap();
     let _guard = CwdGuard::new(&tmp.path().to_path_buf());
 
-    write_config();
+    write_default_config();
     // No models, no migrations -> planner should report no actions.
     fs::create_dir_all("models").unwrap();
     fs::create_dir_all("migrations").unwrap();
@@ -651,8 +626,8 @@ async fn cmd_diff_with_actual_change_runs_format_action_loop() {
     let tmp = tempdir().unwrap();
     let _guard = CwdGuard::new(&tmp.path().to_path_buf());
 
-    write_config();
-    write_model("books");
+    write_default_config();
+    write_simple_id_model("books");
     fs::create_dir_all("migrations").unwrap();
 
     cmd_diff().await.unwrap();
@@ -668,9 +643,9 @@ async fn cmd_diff_emits_fk_supporting_index_warning() {
     let tmp = tempdir().unwrap();
     let _guard = CwdGuard::new(&tmp.path().to_path_buf());
 
-    write_config();
+    write_default_config();
     fs::create_dir_all("migrations").unwrap();
-    let models_dir = PathBuf::from("models");
+    let models_dir = std::path::PathBuf::from("models");
     fs::create_dir_all(&models_dir).unwrap();
 
     // users.json: simple PK (table name matches fk_user's ref_table "users")
diff --git a/crates/vespertide-cli/src/commands/revision/tests/mod.rs b/crates/vespertide-cli/src/commands/revision/tests/mod.rs
index 803c63b8..83f7f65f 100644
--- a/crates/vespertide-cli/src/commands/revision/tests/mod.rs
+++ b/crates/vespertide-cli/src/commands/revision/tests/mod.rs
@@ -1,5 +1,5 @@
 use super::*;
-pub(super) use crate::test_support::CwdGuard;
+pub(super) use crate::test_support::{CwdGuard, write_simple_id_model};
 pub(super) use anyhow::Result;
 pub(super) use std::{
     collections::{BTreeMap, HashMap, HashSet},
@@ -27,33 +27,6 @@ fn write_config_with_format(fmt: Option) -> VespertideConfig {
     cfg
 }
 
-fn write_model(name: &str) {
-    let models_dir = PathBuf::from("models");
-    std_fs::create_dir_all(&models_dir).unwrap();
-    let table = TableDef {
-        name: name.into(),
-        description: None,
-        columns: vec![ColumnDef {
-            name: "id".into(),
-            r#type: ColumnType::Simple(SimpleColumnType::Integer),
-            nullable: false,
-            default: None,
-            comment: None,
-            primary_key: None,
-            unique: None,
-            index: None,
-            foreign_key: None,
-        }],
-        constraints: vec![TableConstraint::PrimaryKey {
-            auto_increment: false,
-            columns: vec!["id".into()],
-            strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
-        }],
-    };
-    let path = models_dir.join(format!("{name}.json"));
-    std_fs::write(path, serde_json::to_string_pretty(&table).unwrap()).unwrap();
-}
-
 mod branches;
 mod branches_more;
 mod choices_apply;
diff --git a/crates/vespertide-cli/src/commands/revision/tests/recreate.rs b/crates/vespertide-cli/src/commands/revision/tests/recreate.rs
index 2f69c5c8..321c9570 100644
--- a/crates/vespertide-cli/src/commands/revision/tests/recreate.rs
+++ b/crates/vespertide-cli/src/commands/revision/tests/recreate.rs
@@ -7,7 +7,7 @@ async fn cmd_revision_writes_migration() {
     let _guard = CwdGuard::new(&tmp.path().to_path_buf());
 
     let cfg = write_config();
-    write_model("users");
+    write_simple_id_model("users");
     std_fs::create_dir_all(cfg.migrations_dir()).unwrap();
 
     cmd_revision("init".into(), vec![], vec![]).await.unwrap();
@@ -36,7 +36,7 @@ async fn cmd_revision_writes_yaml_when_configured() {
     let _guard = CwdGuard::new(&tmp.path().to_path_buf());
 
     let cfg = write_config_with_format(Some(FileFormat::Yaml));
-    write_model("users");
+    write_simple_id_model("users");
     // ensure migrations dir absent to exercise create_dir_all branch
     if cfg.migrations_dir().exists() {
         std_fs::remove_dir_all(cfg.migrations_dir()).unwrap();
diff --git a/crates/vespertide-cli/src/commands/sql.rs b/crates/vespertide-cli/src/commands/sql.rs
index 45ef2c47..51aead12 100644
--- a/crates/vespertide-cli/src/commands/sql.rs
+++ b/crates/vespertide-cli/src/commands/sql.rs
@@ -138,59 +138,23 @@ fn emit_sql(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::CwdGuard;
+    use crate::test_support::{CwdGuard, write_default_config, write_simple_id_model};
     use serial_test::serial;
     use std::fs;
-    use std::path::PathBuf;
     use tempfile::tempdir;
-    use vespertide_config::VespertideConfig;
     use vespertide_core::{
         ColumnDef, ColumnType, MigrationAction, MigrationPlan, SimpleColumnType, TableConstraint,
         TableDef,
     };
 
-    fn write_config() -> VespertideConfig {
-        let cfg = VespertideConfig::default();
-        let text = serde_json::to_string_pretty(&cfg).unwrap();
-        fs::write("vespertide.json", text).unwrap();
-        cfg
-    }
-
-    fn write_model(name: &str) {
-        let models_dir = PathBuf::from("models");
-        fs::create_dir_all(&models_dir).unwrap();
-        let table = TableDef {
-            name: name.into(),
-            description: None,
-            columns: vec![ColumnDef {
-                name: "id".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }],
-            constraints: vec![TableConstraint::PrimaryKey {
-                auto_increment: false,
-                columns: vec!["id".into()],
-                strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
-            }],
-        };
-        let path = models_dir.join(format!("{name}.json"));
-        fs::write(path, serde_json::to_string_pretty(&table).unwrap()).unwrap();
-    }
-
     #[tokio::test]
     #[serial]
     async fn cmd_sql_emits_queries_postgres() {
         let tmp = tempdir().unwrap();
         let _guard = CwdGuard::new(&tmp.path().to_path_buf());
 
-        let _cfg = write_config();
-        write_model("users");
+        let _cfg = write_default_config();
+        write_simple_id_model("users");
 
         let result = cmd_sql(DatabaseBackend::Postgres, false).await;
         assert!(result.is_ok());
@@ -202,8 +166,8 @@ mod tests {
         let tmp = tempdir().unwrap();
         let _guard = CwdGuard::new(&tmp.path().to_path_buf());
 
-        let _cfg = write_config();
-        write_model("users");
+        let _cfg = write_default_config();
+        write_simple_id_model("users");
 
         let result = cmd_sql(DatabaseBackend::MySql, false).await;
         assert!(result.is_ok());
@@ -215,8 +179,8 @@ mod tests {
         let tmp = tempdir().unwrap();
         let _guard = CwdGuard::new(&tmp.path().to_path_buf());
 
-        let _cfg = write_config();
-        write_model("users");
+        let _cfg = write_default_config();
+        write_simple_id_model("users");
 
         let result = cmd_sql(DatabaseBackend::Sqlite, false).await;
         assert!(result.is_ok());
@@ -228,8 +192,8 @@ mod tests {
         let tmp = tempdir().unwrap();
         let _guard = CwdGuard::new(&tmp.path().to_path_buf());
 
-        let cfg = write_config();
-        write_model("users");
+        let cfg = write_default_config();
+        write_simple_id_model("users");
 
         let plan = MigrationPlan {
             id: String::new(),
@@ -270,8 +234,8 @@ mod tests {
         let tmp = tempdir().unwrap();
         let _guard = CwdGuard::new(&tmp.path().to_path_buf());
 
-        let cfg = write_config();
-        write_model("users");
+        let cfg = write_default_config();
+        write_simple_id_model("users");
 
         let plan = MigrationPlan {
             id: String::new(),
@@ -312,8 +276,8 @@ mod tests {
         let tmp = tempdir().unwrap();
         let _guard = CwdGuard::new(&tmp.path().to_path_buf());
 
-        let cfg = write_config();
-        write_model("users");
+        let cfg = write_default_config();
+        write_simple_id_model("users");
 
         let plan = MigrationPlan {
             id: String::new(),
@@ -539,8 +503,8 @@ mod tests {
         // This should trigger the queries.len() > 1 branch (line 89)
         let tmp = tempdir().unwrap();
         let _guard = CwdGuard::new(&tmp.path().to_path_buf());
-        let _cfg = write_config();
-        write_model("users");
+        let _cfg = write_default_config();
+        write_simple_id_model("users");
 
         // Create a migration that adds a NOT NULL column in SQLite, which generates multiple queries
         let plan = MigrationPlan {
diff --git a/crates/vespertide-cli/src/commands/status.rs b/crates/vespertide-cli/src/commands/status.rs
index 95083c66..82b695e5 100644
--- a/crates/vespertide-cli/src/commands/status.rs
+++ b/crates/vespertide-cli/src/commands/status.rs
@@ -161,9 +161,9 @@ pub async fn cmd_status() -> Result<()> {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::CwdGuard;
+    use crate::test_support::{CwdGuard, write_default_config, write_simple_id_model};
     use serial_test::serial;
-    use std::{fs, path::PathBuf};
+    use std::fs;
     use tempfile::tempdir;
     use vespertide_config::VespertideConfig;
     use vespertide_core::{
@@ -171,40 +171,6 @@ mod tests {
         TableDef,
     };
 
-    fn write_config() -> VespertideConfig {
-        let cfg = VespertideConfig::default();
-        let text = serde_json::to_string_pretty(&cfg).unwrap();
-        fs::write("vespertide.json", text).unwrap();
-        cfg
-    }
-
-    fn write_model(name: &str) {
-        let models_dir = PathBuf::from("models");
-        fs::create_dir_all(&models_dir).unwrap();
-        let table = TableDef {
-            name: name.into(),
-            description: None,
-            columns: vec![ColumnDef {
-                name: "id".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }],
-            constraints: vec![TableConstraint::PrimaryKey {
-                auto_increment: false,
-                columns: vec!["id".into()],
-                strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
-            }],
-        };
-        let path = models_dir.join(format!("{name}.json"));
-        fs::write(path, serde_json::to_string_pretty(&table).unwrap()).unwrap();
-    }
-
     fn write_migration(cfg: &VespertideConfig) {
         fs::create_dir_all(cfg.migrations_dir()).unwrap();
         let plan = MigrationPlan {
@@ -238,8 +204,8 @@ mod tests {
         let tmp = tempdir().unwrap();
         let _guard = CwdGuard::new(&tmp.path().to_path_buf());
 
-        let cfg = write_config();
-        write_model("users");
+        let cfg = write_default_config();
+        write_simple_id_model("users");
         write_migration(&cfg);
 
         cmd_status().await.unwrap();
@@ -250,7 +216,7 @@ mod tests {
     async fn cmd_status_no_models_no_migrations_prints_message() {
         let tmp = tempdir().unwrap();
         let _guard = CwdGuard::new(&tmp.path().to_path_buf());
-        let cfg = write_config();
+        let cfg = write_default_config();
         fs::create_dir_all(cfg.models_dir()).unwrap(); // empty models dir
         fs::create_dir_all(cfg.migrations_dir()).unwrap(); // empty migrations dir
 
@@ -262,7 +228,7 @@ mod tests {
     async fn cmd_status_empty_migration_list_returns_ok() {
         let tmp = tempdir().unwrap();
         let _guard = CwdGuard::new(&tmp.path().to_path_buf());
-        let cfg = write_config();
+        let cfg = write_default_config();
         fs::create_dir_all(cfg.models_dir()).unwrap();
         fs::create_dir_all(cfg.migrations_dir()).unwrap();
 
@@ -282,8 +248,8 @@ mod tests {
     async fn cmd_status_models_no_migrations_prints_hint() {
         let tmp = tempdir().unwrap();
         let _guard = CwdGuard::new(&tmp.path().to_path_buf());
-        let cfg = write_config();
-        write_model("users");
+        let cfg = write_default_config();
+        write_simple_id_model("users");
         fs::create_dir_all(cfg.migrations_dir()).unwrap();
 
         cmd_status().await.unwrap();
@@ -295,10 +261,10 @@ mod tests {
         let tmp = tempdir().unwrap();
         let _guard = CwdGuard::new(&tmp.path().to_path_buf());
 
-        let cfg = write_config();
-        write_model("users");
+        let cfg = write_default_config();
+        write_simple_id_model("users");
         // add another model to differ from baseline
-        write_model("posts");
+        write_simple_id_model("posts");
         write_migration(&cfg); // baseline only has users
 
         cmd_status().await.unwrap();
@@ -310,7 +276,7 @@ mod tests {
         let tmp = tempdir().unwrap();
         let _guard = CwdGuard::new(&tmp.path().to_path_buf());
 
-        let cfg = write_config();
+        let cfg = write_default_config();
         fs::create_dir_all(cfg.models_dir()).unwrap();
         fs::create_dir_all(cfg.migrations_dir()).unwrap();
 
diff --git a/crates/vespertide-cli/src/test_support.rs b/crates/vespertide-cli/src/test_support.rs
index 85e0ddda..0c8e22b5 100644
--- a/crates/vespertide-cli/src/test_support.rs
+++ b/crates/vespertide-cli/src/test_support.rs
@@ -4,8 +4,12 @@
 //! in `main.rs` and exposed `pub(crate)` so every inline `mod tests` and
 //! `commands//tests/mod.rs` entry can reuse the same implementation.
 
+use std::fs;
 use std::path::{Path, PathBuf};
 
+use vespertide_config::VespertideConfig;
+use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableConstraint, TableDef};
+
 /// RAII guard that swaps the process current directory for the duration of a
 /// test and restores it on drop. Used in combination with `serial_test::serial`
 /// for filesystem-isolated CLI integration tests.
@@ -26,3 +30,43 @@ impl Drop for CwdGuard {
         let _ = std::env::set_current_dir(&self.original);
     }
 }
+
+/// Writes a default `vespertide.json` to the current directory and returns
+/// the matching `VespertideConfig`. Centralises the byte-identical bootstrap
+/// previously open-coded in every CLI command's test module.
+pub(crate) fn write_default_config() -> VespertideConfig {
+    let cfg = VespertideConfig::default();
+    let text = serde_json::to_string_pretty(&cfg).unwrap();
+    fs::write("vespertide.json", text).unwrap();
+    cfg
+}
+
+/// Writes a single-column `{name}.json` model with an `id INTEGER NOT NULL`
+/// PK to `models/{name}.json`. Centralises the byte-identical bootstrap
+/// previously open-coded in every CLI command's test module.
+pub(crate) fn write_simple_id_model(name: &str) {
+    let models_dir = PathBuf::from("models");
+    fs::create_dir_all(&models_dir).unwrap();
+    let table = TableDef {
+        name: name.into(),
+        description: None,
+        columns: vec![ColumnDef {
+            name: "id".into(),
+            r#type: ColumnType::Simple(SimpleColumnType::Integer),
+            nullable: false,
+            default: None,
+            comment: None,
+            primary_key: None,
+            unique: None,
+            index: None,
+            foreign_key: None,
+        }],
+        constraints: vec![TableConstraint::PrimaryKey {
+            auto_increment: false,
+            columns: vec!["id".into()],
+            strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
+        }],
+    };
+    let path = models_dir.join(format!("{name}.json"));
+    fs::write(path, serde_json::to_string_pretty(&table).unwrap()).unwrap();
+}
diff --git a/crates/vespertide-lsp/src/completion/context.rs b/crates/vespertide-lsp/src/completion/context.rs
index 5c46e8ec..83a9bcbb 100644
--- a/crates/vespertide-lsp/src/completion/context.rs
+++ b/crates/vespertide-lsp/src/completion/context.rs
@@ -7,7 +7,7 @@ use vespertide_planner::{CheckToken, CheckTokenKind, lex_check_expr};
 use crate::check_expr_range::expr_inner_range;
 use crate::text_util::strip_quotes;
 use crate::tree_util::{
-    ancestor_pair_with_key, direct_child_value, enclosing_pair_with_key, is_pair,
+    direct_child_value, enclosing_pair_with_key, is_inside_constraints, is_pair,
 };
 
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -246,10 +246,6 @@ fn keyword_expects_operand(keyword: &str) -> bool {
         .any(|expected| keyword.eq_ignore_ascii_case(expected))
 }
 
-fn is_inside_constraints(node: tree_sitter::Node<'_>, source: &str) -> bool {
-    ancestor_pair_with_key(node, source, "constraints").is_some()
-}
-
 fn current_table_columns(node: tree_sitter::Node<'_>, source: &str) -> Vec {
     let root = document_value(node);
     if let Some(columns_value_raw) = find_pair_with_key(root, source, "columns")
@@ -653,9 +649,7 @@ mod tests {
         super::super::compute(src, format, Some(&tree), &idx, &docs, pos)
     }
 
-    fn completion_labels(items: &[super::super::DomainCompletion]) -> Vec<&str> {
-        items.iter().map(|item| item.label.as_str()).collect()
-    }
+    use super::super::completion_labels;
 
     fn assert_label_expectations(
         labels: &[&str],
diff --git a/crates/vespertide-lsp/src/completion/mod.rs b/crates/vespertide-lsp/src/completion/mod.rs
index 30f9ba65..ef54e02c 100644
--- a/crates/vespertide-lsp/src/completion/mod.rs
+++ b/crates/vespertide-lsp/src/completion/mod.rs
@@ -137,6 +137,15 @@ fn compute_inner(
     }
 }
 
+/// Test-only helper shared by `completion::context::tests` and
+/// `completion::values::tests`: map a slice of `DomainCompletion` items to a
+/// `Vec<&str>` of their labels. Centralises the byte-identical helper that
+/// previously lived in both sibling test modules.
+#[cfg(test)]
+pub(super) fn completion_labels(items: &[DomainCompletion]) -> Vec<&str> {
+    items.iter().map(|item| item.label.as_str()).collect()
+}
+
 #[cfg(test)]
 mod tests {
     use std::fs;
diff --git a/crates/vespertide-lsp/src/completion/values.rs b/crates/vespertide-lsp/src/completion/values.rs
index 9b184528..787157d0 100644
--- a/crates/vespertide-lsp/src/completion/values.rs
+++ b/crates/vespertide-lsp/src/completion/values.rs
@@ -634,13 +634,10 @@ fn parse_table(text: &str) -> Option {
 
 #[cfg(test)]
 mod tests {
+    use super::super::completion_labels;
     use super::*;
     use rstest::rstest;
 
-    fn completion_labels(items: &[DomainCompletion]) -> Vec<&str> {
-        items.iter().map(|item| item.label.as_str()).collect()
-    }
-
     #[rstest]
     #[case::complex_enum(Some("enum"), &["alpha", "beta"], &["'alpha'", "'beta'"], &["now()"])]
     #[case::unknown_complex_kind(Some("custom"), &[], &["null"], &[])]
diff --git a/crates/vespertide-lsp/src/hover/check_expr.rs b/crates/vespertide-lsp/src/hover/check_expr.rs
index e8e6ca24..6f400735 100644
--- a/crates/vespertide-lsp/src/hover/check_expr.rs
+++ b/crates/vespertide-lsp/src/hover/check_expr.rs
@@ -11,7 +11,7 @@ use vespertide_planner::{CheckExprAst, CheckExprLiteral, CheckExprOp, parse_chec
 
 use super::DomainHover;
 use crate::check_expr_range::expr_inner_range;
-use crate::tree_util::{ancestor_pair_with_key, is_pair};
+use crate::tree_util::{is_inside_constraints, is_pair};
 
 pub(super) fn try_hover(
     node: tree_sitter::Node<'_>,
@@ -232,12 +232,6 @@ fn expr_pair_ancestor<'tree>(
     None
 }
 
-/// True when any ancestor pair has key `"constraints"`. Mirrors
-/// `column::is_inside_columns` from the column-hover handler.
-fn is_inside_constraints(node: tree_sitter::Node<'_>, source: &str) -> bool {
-    ancestor_pair_with_key(node, source, "constraints").is_some()
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/vespertide-lsp/src/tree_util.rs b/crates/vespertide-lsp/src/tree_util.rs
index 7d4c0b18..0008b057 100644
--- a/crates/vespertide-lsp/src/tree_util.rs
+++ b/crates/vespertide-lsp/src/tree_util.rs
@@ -253,3 +253,11 @@ pub(crate) fn find_pair_with_key<'tree>(
                 == Some(target_key)
     })
 }
+
+/// True when any ancestor pair of `node` has key `"constraints"`. Shared LSP
+/// predicate for "cursor is inside a `constraints` array" — used by both the
+/// CHECK-expression hover (`hover::check_expr`) and the CHECK-expression
+/// completion context (`completion::context`).
+pub(crate) fn is_inside_constraints(node: tree_sitter::Node<'_>, source: &str) -> bool {
+    ancestor_pair_with_key(node, source, "constraints").is_some()
+}

From 5ee8dc237f6fd1248b4dbb4ab257c2cf33309941 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 13:33:09 +0900
Subject: [PATCH 045/109] retry-now#0044: batch - hoist LSP trim_one_byte,
 adopt unwrap_yaml_node, hoist ancestor_pair (3 kept)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Three independent byte-equivalent LSP helper dedupes consolidated into tree_util:

1. trim_one_byte (3 dupes) -> tree_util::trim_one_byte_each_side

   file_features/rename/symbols now import the canonical helper aliased back

   to 'trim_one_byte' so existing call sites stay verbatim. Three identical

   private function bodies removed; existing in-module unit tests exercise

   the same body via the re-exported alias.

2. unwrap_yaml in semantic_tokens/classify_yaml.rs (13 sites) -> adopt

   tree_util::unwrap_yaml_node. Removes the last stale local copy of this

   YAML wrapper peeler; 9 sibling LSP modules already routed through the

   canonical helper. All 11 production + 2 test call sites renamed; local

   fn deleted; import added.

3. enclosing_pair (2 dupes) -> tree_util::ancestor_pair

   check_expr_locate.rs and references/resolver.rs both carried byte-identical

   'walk strict parent chain to nearest pair' helpers; hoisted as the

   unfiltered-key counterpart to existing ancestor_pair_with_key. Two local

   fns deleted; 4 call sites (2 production + 2 unit-test) renamed; imports

   extended.

Net diff: +61/-83 = -22 LOC across 7 files (all in vespertide-lsp/src).

Verification (under documented CFLAGS=-Wno-error=incompatible-pointer-types

Windows MSVC pg_query workaround used by iters 0029-0043):

  Group 1 (items 1+2): cargo test --workspace --all-features --exclude

    vespertide-query GREEN; vespertide-query --lib 732/0; clippy clean.

  Group 2 (item 3): same three commands GREEN.

  cargo fmt --all --check: only 4 pre-existing drifts (export.rs:9, :953;

    add_constraint/{primary_key,unique}.rs) — explicitly documented in

    iter 0043 report as out-of-scope, NOT mine.

  schema-gen drift gate: zero diff (no public surface change).

  bash scripts/check-line-budget.sh: all files within budget.
---
 .../vespertide-lsp/src/check_expr_locate.rs   | 18 ++-------
 crates/vespertide-lsp/src/file_features.rs    | 10 +----
 .../vespertide-lsp/src/references/resolver.rs | 19 +++------
 crates/vespertide-lsp/src/rename.rs           | 10 +----
 .../src/semantic_tokens/classify_yaml.rs      | 39 ++++++-------------
 crates/vespertide-lsp/src/symbols.rs          | 13 ++-----
 crates/vespertide-lsp/src/tree_util.rs        | 35 +++++++++++++++++
 7 files changed, 61 insertions(+), 83 deletions(-)

diff --git a/crates/vespertide-lsp/src/check_expr_locate.rs b/crates/vespertide-lsp/src/check_expr_locate.rs
index 9cf86786..42e711eb 100644
--- a/crates/vespertide-lsp/src/check_expr_locate.rs
+++ b/crates/vespertide-lsp/src/check_expr_locate.rs
@@ -15,7 +15,7 @@ use std::ops::Range;
 use tree_sitter::{Node, Tree};
 
 use crate::text_util::strip_quotes;
-use crate::tree_util::node_at_byte;
+use crate::tree_util::{ancestor_pair, node_at_byte};
 
 /// Cursor-based result of [`find_check_expr_at`]: the CHECK `expr` string
 /// the cursor sits in and the inner byte range of its predicate text.
@@ -38,7 +38,7 @@ pub(crate) fn find_check_expr_at(
     let node = node_at_byte(tree, byte_offset)?;
     let string_node = enclosing_string(node)?;
 
-    let pair = enclosing_pair(string_node)?;
+    let pair = ancestor_pair(string_node)?;
     let key = pair.named_child(0)?;
     let key_text = std::str::from_utf8(source.get(key.byte_range())?).ok()?;
     if strip_quotes(key_text) != "expr" {
@@ -107,18 +107,6 @@ fn peel_wrapper(node: Node<'_>) -> Node<'_> {
     }
 }
 
-/// Closest ancestor `pair` / `block_mapping_pair`.
-fn enclosing_pair(node: Node<'_>) -> Option> {
-    let mut current = node.parent();
-    while let Some(candidate) = current {
-        if matches!(candidate.kind(), "pair" | "block_mapping_pair") {
-            return Some(candidate);
-        }
-        current = candidate.parent();
-    }
-    None
-}
-
 /// Closest ancestor that is a JSON / YAML string scalar. Stops at
 /// structural boundaries (arrays, objects, mappings, pairs) so a cursor
 /// that lives between strings does not accidentally bind to a string
@@ -207,7 +195,7 @@ mod tests {
         let tree = parse(r#"{"name":"u"}"#);
         let root = tree.root_node();
 
-        assert!(enclosing_pair(root).is_none());
+        assert!(ancestor_pair(root).is_none());
         assert!(enclosing_string(root).is_none());
     }
 
diff --git a/crates/vespertide-lsp/src/file_features.rs b/crates/vespertide-lsp/src/file_features.rs
index ea0b1771..d8e6d65f 100644
--- a/crates/vespertide-lsp/src/file_features.rs
+++ b/crates/vespertide-lsp/src/file_features.rs
@@ -4,7 +4,7 @@
 //! here because they share the same tree-sitter walk patterns and
 //! none has enough surface area to deserve its own directory.
 
-use crate::tree_util::node_at_byte;
+use crate::tree_util::{node_at_byte, trim_one_byte_each_side as trim_one_byte};
 use std::ops::Range;
 
 // =====================================================================
@@ -340,14 +340,6 @@ fn unwrap_yaml(node: tree_sitter::Node<'_>) -> tree_sitter::Node<'_> {
     current
 }
 
-fn trim_one_byte(range: &Range) -> Range {
-    if range.end.saturating_sub(range.start) >= 2 {
-        (range.start + 1)..(range.end - 1)
-    } else {
-        range.clone()
-    }
-}
-
 // =====================================================================
 // Tests
 // =====================================================================
diff --git a/crates/vespertide-lsp/src/references/resolver.rs b/crates/vespertide-lsp/src/references/resolver.rs
index c93eab39..48d0a36c 100644
--- a/crates/vespertide-lsp/src/references/resolver.rs
+++ b/crates/vespertide-lsp/src/references/resolver.rs
@@ -1,7 +1,9 @@
 //! Resolve "what symbol is the cursor on?" for the references provider.
 
 use crate::text_util::strip_quotes;
-use crate::tree_util::{direct_child_value, enclosing_string, node_at_byte, skip_yaml_wrappers};
+use crate::tree_util::{
+    ancestor_pair, direct_child_value, enclosing_string, node_at_byte, skip_yaml_wrappers,
+};
 use tower_lsp_server::ls_types::Uri;
 
 use super::ReferenceSymbol;
@@ -25,7 +27,7 @@ pub(super) fn resolve(
     }
 
     // What pair owns the string?
-    let pair = enclosing_pair(string_node)?;
+    let pair = ancestor_pair(string_node)?;
     let key = pair.named_child(0)?;
     let key_text = strip_quotes(source.get(key.byte_range())?);
 
@@ -107,17 +109,6 @@ fn resolve_check_expr_column(
     })
 }
 
-fn enclosing_pair(node: tree_sitter::Node<'_>) -> Option> {
-    let mut current = node.parent();
-    while let Some(candidate) = current {
-        if matches!(candidate.kind(), "pair" | "block_mapping_pair") {
-            return Some(candidate);
-        }
-        current = candidate.parent();
-    }
-    None
-}
-
 /// A pair is "top level" when its direct ancestor mapping is itself the
 /// outermost mapping of the document. This is how we distinguish the
 /// table's own `name: ...` from a column's `name: ...`.
@@ -266,7 +257,7 @@ mod tests {
 
         assert!(!is_check_constraint_pair(root, r#"{"name":"u"}"#));
         assert!(enclosing_string(root).is_none());
-        assert!(enclosing_pair(root).is_none());
+        assert!(ancestor_pair(root).is_none());
         assert!(!is_top_level_pair(root));
     }
 
diff --git a/crates/vespertide-lsp/src/rename.rs b/crates/vespertide-lsp/src/rename.rs
index 24a50514..27077ea5 100644
--- a/crates/vespertide-lsp/src/rename.rs
+++ b/crates/vespertide-lsp/src/rename.rs
@@ -16,7 +16,7 @@ use tower_lsp_server::ls_types::Uri;
 use crate::parser::DocumentFormat;
 use crate::references::{self, ReferenceSymbol};
 use crate::store::DocumentStore;
-use crate::tree_util::node_at_byte;
+use crate::tree_util::{node_at_byte, trim_one_byte_each_side as trim_one_byte};
 use crate::workspace_index::WorkspaceIndex;
 use crate::workspace_tables::WorkspaceTables;
 
@@ -131,14 +131,6 @@ fn inner_content_range(node: tree_sitter::Node<'_>, source: &str) -> Range) -> Range {
-    if range.end.saturating_sub(range.start) >= 2 {
-        (range.start + 1)..(range.end - 1)
-    } else {
-        range.clone()
-    }
-}
-
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct DomainTextEdit {
     pub byte_range: Range,
diff --git a/crates/vespertide-lsp/src/semantic_tokens/classify_yaml.rs b/crates/vespertide-lsp/src/semantic_tokens/classify_yaml.rs
index ec55ca37..e6299750 100644
--- a/crates/vespertide-lsp/src/semantic_tokens/classify_yaml.rs
+++ b/crates/vespertide-lsp/src/semantic_tokens/classify_yaml.rs
@@ -23,6 +23,7 @@
 )]
 
 use super::{RawToken, check_expr_tokens, legend::ModIdx, legend::TokenIdx};
+use crate::tree_util::unwrap_yaml_node;
 
 #[must_use]
 pub fn classify(source: &str, tree: &tree_sitter::Tree) -> Vec {
@@ -76,8 +77,8 @@ fn classify_pair(pair: tree_sitter::Node<'_>, source: &[u8], ctx: Ctx, out: &mut
     if let Some(key_node) = pair.named_child(0)
         && let Some(value_node) = pair.named_child(1)
     {
-        let key_node = unwrap_yaml(key_node);
-        let value_node = unwrap_yaml(value_node);
+        let key_node = unwrap_yaml_node(key_node);
+        let value_node = unwrap_yaml_node(value_node);
         if let Some(key_text) = scalar_text(key_node, source) {
             match key_text {
                 "name" if ctx.mapping_depth == 1 && !ctx.inside_columns => {
@@ -163,12 +164,12 @@ fn classify_pair(pair: tree_sitter::Node<'_>, source: &[u8], ctx: Ctx, out: &mut
 }
 
 fn recurse_value(value: tree_sitter::Node<'_>, source: &[u8], ctx: Ctx, out: &mut Vec) {
-    let v = unwrap_yaml(value);
+    let v = unwrap_yaml_node(value);
 
     if ctx.inside_ref_columns && matches!(v.kind(), "block_sequence" | "flow_sequence") {
         let mut cursor = v.walk();
         for child in v.children(&mut cursor) {
-            let element = unwrap_yaml(child);
+            let element = unwrap_yaml_node(child);
             if is_scalar(element.kind()) {
                 push_scalar(element, TokenIdx::Property, ModIdx::Definition as u32, out);
             } else {
@@ -181,7 +182,7 @@ fn recurse_value(value: tree_sitter::Node<'_>, source: &[u8], ctx: Ctx, out: &mu
     if ctx.inside_enum_values_array && matches!(v.kind(), "block_sequence" | "flow_sequence") {
         let mut cursor = v.walk();
         for child in v.children(&mut cursor) {
-            let element = unwrap_yaml(child);
+            let element = unwrap_yaml_node(child);
             if is_scalar(element.kind()) {
                 push_scalar(element, TokenIdx::EnumMember, 0, out);
             } else {
@@ -197,7 +198,7 @@ fn recurse_value(value: tree_sitter::Node<'_>, source: &[u8], ctx: Ctx, out: &mu
     if ctx.inside_columns && matches!(v.kind(), "block_sequence" | "flow_sequence") {
         let mut cursor = v.walk();
         for child in v.children(&mut cursor) {
-            let element = unwrap_yaml(child);
+            let element = unwrap_yaml_node(child);
             let item_ctx = if matches!(
                 element.kind(),
                 "block_mapping" | "flow_mapping" | "block_sequence_item"
@@ -223,7 +224,7 @@ fn classify_type_value(
     ctx: Ctx,
     out: &mut Vec,
 ) {
-    let v = unwrap_yaml(value);
+    let v = unwrap_yaml_node(value);
     match v.kind() {
         k if is_scalar(k) => {
             push_scalar(v, TokenIdx::Type, 0, out);
@@ -240,7 +241,7 @@ fn classify_type_value(
 }
 
 fn classify_default(value: tree_sitter::Node<'_>, out: &mut Vec) {
-    let v = unwrap_yaml(value);
+    let v = unwrap_yaml_node(value);
     if is_scalar(v.kind()) {
         // YAML doesn't distinguish bool/null/string/number scalars by
         // tree-sitter node kind alone — treat every scalar default as a
@@ -255,7 +256,7 @@ fn emit_yaml_check_expr_tokens(
     source: &[u8],
     out: &mut Vec,
 ) {
-    let scalar = unwrap_yaml(value);
+    let scalar = unwrap_yaml_node(value);
     if let Some(range) = check_expr_scalar_range(scalar)
         && range.end > range.start
         && let Some(expr_text) = source
@@ -297,22 +298,6 @@ fn inner_scalar_range(node: tree_sitter::Node<'_>) -> std::ops::Range {
     }
 }
 
-fn unwrap_yaml(node: tree_sitter::Node<'_>) -> tree_sitter::Node<'_> {
-    // Fused while-let so the empty-wrapper case (no usable `named_child(0)`)
-    // and the kind-mismatch case share the same loop exit — no defensive
-    // `return` line that only an (unobservable) empty tree-sitter wrapper
-    // could reach.
-    let mut current = node;
-    while matches!(current.kind(), "flow_node" | "block_node")
-        && let Some(inner) = current
-            .named_child(0)
-            .filter(|inner| inner.id() != current.id())
-    {
-        current = inner;
-    }
-    current
-}
-
 fn is_scalar(kind: &str) -> bool {
     matches!(
         kind,
@@ -719,7 +704,7 @@ mod tests {
         let pool = ParserPool::new();
         let src = "name: \"\"\n";
         let tree = pool.parse(src, DocumentFormat::Yaml).unwrap();
-        let value = unwrap_yaml(first_pair(&tree, src).named_child(1).unwrap());
+        let value = unwrap_yaml_node(first_pair(&tree, src).named_child(1).unwrap());
         let mut out = Vec::new();
         push_scalar(value, TokenIdx::Class, 0, &mut out);
         assert!(out.is_empty());
@@ -727,7 +712,7 @@ mod tests {
         let empty = "name:\n";
         let tree = pool.parse(empty, DocumentFormat::Yaml).unwrap();
         if let Some(wrapper) = first_pair(&tree, empty).named_child(1) {
-            let _ = unwrap_yaml(wrapper);
+            let _ = unwrap_yaml_node(wrapper);
         }
     }
 }
diff --git a/crates/vespertide-lsp/src/symbols.rs b/crates/vespertide-lsp/src/symbols.rs
index 3c022f2a..80522e75 100644
--- a/crates/vespertide-lsp/src/symbols.rs
+++ b/crates/vespertide-lsp/src/symbols.rs
@@ -306,15 +306,10 @@ fn direct_pair_node<'tree>(
     find_pair_with_key(mapping, source, target_key)?.named_child(1)
 }
 
-use crate::tree_util::{find_outer_mapping, find_pair_with_key, unwrap_yaml_node};
-
-fn trim_one_byte(range: &Range) -> Range {
-    if range.end.saturating_sub(range.start) >= 2 {
-        (range.start + 1)..(range.end - 1)
-    } else {
-        range.clone()
-    }
-}
+use crate::tree_util::{
+    find_outer_mapping, find_pair_with_key, trim_one_byte_each_side as trim_one_byte,
+    unwrap_yaml_node,
+};
 
 /// ASCII case-insensitive substring search. `needle_lower` must already be
 /// lowercase (the public `compute()` entry-point lowercases the query once).
diff --git a/crates/vespertide-lsp/src/tree_util.rs b/crates/vespertide-lsp/src/tree_util.rs
index 0008b057..141801e5 100644
--- a/crates/vespertide-lsp/src/tree_util.rs
+++ b/crates/vespertide-lsp/src/tree_util.rs
@@ -82,6 +82,23 @@ pub(crate) fn unwrap_yaml_node(node: tree_sitter::Node<'_>) -> tree_sitter::Node
     current
 }
 
+/// Strip one byte from each side of `range` if it spans at least 2 bytes.
+/// Used to peel quote delimiters off `double_quote_scalar` /
+/// `single_quote_scalar` byte ranges (and JSON `string` ranges when the
+/// `string_content` named child is absent — the empty-string case).
+///
+/// Centralises the three byte-identical private copies that lived in
+/// `file_features`, `rename`, and `symbols` pre-this-hoist. The
+/// `references::search::trim_one_byte_each_side` variant takes `Range`
+/// by value and stays put; its signature change is a separate decision.
+pub(crate) fn trim_one_byte_each_side(range: &std::ops::Range) -> std::ops::Range {
+    if range.end.saturating_sub(range.start) >= 2 {
+        (range.start + 1)..(range.end - 1)
+    } else {
+        range.clone()
+    }
+}
+
 /// Walk the PARENT chain skipping tree-sitter-yaml's `flow_node` /
 /// `block_node` wrapper kinds. Counterpart to [`unwrap_yaml_node`], which
 /// descends INTO a wrapper; this helper escapes UP through them so the
@@ -192,6 +209,24 @@ pub(crate) fn ancestor_pair_with_key<'tree>(
     None
 }
 
+/// Walk strictly the PARENT chain (skip `node` itself) and return the
+/// nearest [`is_pair`] ancestor — no key filter. This is the
+/// unfiltered-key counterpart to [`ancestor_pair_with_key`]; use this
+/// when "is there *any* pair ancestor?" is the question.
+///
+/// Centralises the two byte-equivalent private copies that lived in
+/// `check_expr_locate` and `references::resolver` pre-this-hoist.
+pub(crate) fn ancestor_pair(node: tree_sitter::Node<'_>) -> Option> {
+    let mut current = node.parent();
+    while let Some(candidate) = current {
+        if is_pair(candidate) {
+            return Some(candidate);
+        }
+        current = candidate.parent();
+    }
+    None
+}
+
 /// Find a direct child pair of `object` whose key (after
 /// [`crate::text_util::strip_quotes`]) equals `target_key` and return the
 /// value-side byte slice **verbatim** (quotes preserved for quoted

From 4f62c7846acb8c2309cfbbd77fb6174c81ad0de8 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Sun, 28 Jun 2026 14:00:16 +0900
Subject: [PATCH 046/109] retry-now#0045: batch - drop 2 Vec allocs in
 try_resolve_single_pk_column, hoist multi-edit + single-edit appliers into
 LSP test_support (3 kept)

---
 crates/vespertide-lsp/src/code_actions.rs     | 23 ++------
 crates/vespertide-lsp/src/rename.rs           | 11 +---
 crates/vespertide-lsp/src/test_support.rs     | 36 ++++++++++++
 .../src/sql/add_constraint/mod.rs             | 55 +++++++++----------
 4 files changed, 69 insertions(+), 56 deletions(-)

diff --git a/crates/vespertide-lsp/src/code_actions.rs b/crates/vespertide-lsp/src/code_actions.rs
index bb4f2b4d..d4318e62 100644
--- a/crates/vespertide-lsp/src/code_actions.rs
+++ b/crates/vespertide-lsp/src/code_actions.rs
@@ -569,9 +569,7 @@ mod tests {
             .expect("primary_key remove action missing");
         let edit = &action.edits[0];
         // Confirm the edit produces a valid object when applied.
-        let mut after = String::from(&src[..edit.byte_range.start]);
-        after.push_str(&edit.new_text);
-        after.push_str(&src[edit.byte_range.end..]);
+        let after = crate::test_support::apply_text_edit(src, edit);
         assert!(serde_json::from_str::(&after).is_ok());
         assert!(!after.contains("primary_key"));
     }
@@ -617,9 +615,7 @@ mod tests {
             .find(|a| a.title == "Convert to varchar(255)")
             .expect("text → varchar action");
         let edit = &convert.edits[0];
-        let mut after = String::from(&src[..edit.byte_range.start]);
-        after.push_str(&edit.new_text);
-        after.push_str(&src[edit.byte_range.end..]);
+        let after = crate::test_support::apply_text_edit(src, edit);
         assert!(after.contains(r#""kind":"varchar""#));
         serde_json::from_str::(&after).expect("valid JSON");
     }
@@ -687,9 +683,7 @@ mod tests {
             .find(|a| a.title == "Add foreign_key skeleton")
             .expect("foreign_key skeleton action");
         let edit = &fk.edits[0];
-        let mut after = String::from(&src[..edit.byte_range.start]);
-        after.push_str(&edit.new_text);
-        after.push_str(&src[edit.byte_range.end..]);
+        let after = crate::test_support::apply_text_edit(src, edit);
         assert!(after.contains(r#""foreign_key""#));
         serde_json::from_str::(&after).expect("valid JSON");
     }
@@ -726,15 +720,10 @@ mod tests {
     // hard-error diagnostic.
 
     /// Apply `action`'s edits to `src` (front-to-back safe) and return the
-    /// resulting document.
+    /// resulting document. Thin wrapper around the shared
+    /// [`crate::test_support::apply_text_edits`] helper.
     fn apply(src: &str, action: &DomainCodeAction) -> String {
-        let mut edits = action.edits.clone();
-        edits.sort_by_key(|e| std::cmp::Reverse(e.byte_range.start));
-        let mut out = src.to_string();
-        for e in &edits {
-            out.replace_range(e.byte_range.clone(), &e.new_text);
-        }
-        out
+        crate::test_support::apply_text_edits(src, &action.edits)
     }
 
     #[test]
diff --git a/crates/vespertide-lsp/src/rename.rs b/crates/vespertide-lsp/src/rename.rs
index 27077ea5..3e40ed7d 100644
--- a/crates/vespertide-lsp/src/rename.rs
+++ b/crates/vespertide-lsp/src/rename.rs
@@ -364,9 +364,7 @@ mod tests {
         );
 
         // Apply the edit and confirm the result is still valid JSON.
-        let mut after = String::from(&src[..edit.byte_range.start]);
-        after.push_str(&edit.new_text);
-        after.push_str(&src[edit.byte_range.end..]);
+        let after = crate::test_support::apply_text_edit(src, edit);
         assert!(
             after.contains(r#""a""#),
             "result must keep the quotes: {after}"
@@ -538,12 +536,7 @@ mod tests {
 
         // Apply all edits front-to-back and confirm the CHECK now reads
         // `years > 0` and the document still parses as JSON.
-        let mut sorted = file_edits.clone();
-        sorted.sort_by_key(|e| std::cmp::Reverse(e.byte_range.start));
-        let mut after = src.to_string();
-        for edit in &sorted {
-            after.replace_range(edit.byte_range.clone(), &edit.new_text);
-        }
+        let after = crate::test_support::apply_text_edits(src, file_edits);
         assert!(
             after.contains(r#""expr":"years > 0""#),
             "CHECK expr must be rewritten to `years > 0`, got: {after}"
diff --git a/crates/vespertide-lsp/src/test_support.rs b/crates/vespertide-lsp/src/test_support.rs
index 4d130ce2..7c1bdb13 100644
--- a/crates/vespertide-lsp/src/test_support.rs
+++ b/crates/vespertide-lsp/src/test_support.rs
@@ -47,3 +47,39 @@ pub(crate) fn parse_json(src: &str) -> tree_sitter::Tree {
 pub(crate) fn parse_yaml(src: &str) -> tree_sitter::Tree {
     ParserPool::new().parse(src, DocumentFormat::Yaml).unwrap()
 }
+
+/// Apply a slice of [`crate::rename::DomainTextEdit`]s to `src`, walking
+/// back-to-front so earlier-edit byte ranges stay valid after later edits
+/// land. Consolidates two byte-identical inline copies that lived in
+/// `code_actions::tests::apply` and in the rename test
+/// `rn_s1_rename_column_rewrites_check_expr_occurrence`. Iterates by
+/// reference to avoid an extra `Vec` clone — the original
+/// `code_actions::apply` called `action.edits.clone()` to sort the owned
+/// vector; here only the `&DomainTextEdit` references are sorted.
+pub(crate) fn apply_text_edits(src: &str, edits: &[crate::rename::DomainTextEdit]) -> String {
+    let mut sorted: Vec<&crate::rename::DomainTextEdit> = edits.iter().collect();
+    sorted.sort_by_key(|e| std::cmp::Reverse(e.byte_range.start));
+    let mut out = src.to_string();
+    for e in &sorted {
+        out.replace_range(e.byte_range.clone(), &e.new_text);
+    }
+    out
+}
+
+/// Apply a single [`crate::rename::DomainTextEdit`] to `src` and return
+/// the result. Consolidates four byte-identical inline copies that lived
+/// in three `code_actions` tests (`remove_primary_key_when_present`,
+/// `text_column_offers_varchar_conversion`,
+/// `add_foreign_key_skeleton_offered_when_absent`) and the rename test
+/// `rename_quoted_column_name_inside_columns_array`. Preserves the
+/// allocate-prefix → push-new-text → push-suffix pattern of the original
+/// inline copies (no sort needed — only one edit).
+pub(crate) fn apply_text_edit(src: &str, edit: &crate::rename::DomainTextEdit) -> String {
+    let mut out = String::with_capacity(
+        src.len() - (edit.byte_range.end - edit.byte_range.start) + edit.new_text.len(),
+    );
+    out.push_str(&src[..edit.byte_range.start]);
+    out.push_str(&edit.new_text);
+    out.push_str(&src[edit.byte_range.end..]);
+    out
+}
diff --git a/crates/vespertide-query/src/sql/add_constraint/mod.rs b/crates/vespertide-query/src/sql/add_constraint/mod.rs
index e78f93a9..a3068740 100644
--- a/crates/vespertide-query/src/sql/add_constraint/mod.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/mod.rs
@@ -928,7 +928,7 @@ mod tests {
 
 mod unique;
 
-use vespertide_core::{TableConstraint, TableDef, schema::names::names_to_strings};
+use vespertide_core::{TableConstraint, TableDef};
 
 use super::helpers::{build_sqlite_table_rebuild, require_table_in_schema};
 use super::types::{BuiltQuery, DatabaseBackend};
@@ -949,36 +949,31 @@ pub(super) fn try_resolve_single_pk_column>(
 ) -> Option {
     let table_def = current_schema.iter().find(|t| t.name.as_str() == table)?;
 
-    let pk_columns: Vec = table_def
-        .constraints
-        .iter()
-        .find_map(|c| {
-            if let TableConstraint::PrimaryKey { columns, .. } = c {
-                Some(names_to_strings(columns))
-            } else {
-                None
-            }
-        })
-        .or_else(|| {
-            let inline: Vec = table_def
-                .columns
-                .iter()
-                .filter(|col| col.primary_key.is_some())
-                .map(|col| col.name.to_string())
-                .collect();
-            if inline.is_empty() {
-                None
-            } else {
-                Some(inline)
-            }
-        })?;
+    // Prefer the first table-level PRIMARY KEY constraint; fall back to
+    // inline `primary_key: true` columns only when no table-level PK exists.
+    // Either way the resolver fires only when exactly one column is the PK
+    // (composite PK → `None`).
+    let pk_column = if let Some(columns) = table_def.constraints.iter().find_map(|c| match c {
+        TableConstraint::PrimaryKey { columns, .. } => Some(columns),
+        _ => None,
+    }) {
+        if columns.len() != 1 {
+            return None;
+        }
+        columns[0].to_string()
+    } else {
+        let mut inline = table_def
+            .columns
+            .iter()
+            .filter(|col| col.primary_key.is_some());
+        let first = inline.next()?;
+        if inline.next().is_some() {
+            return None;
+        }
+        first.name.to_string()
+    };
 
-    if pk_columns.len() != 1 {
-        return None;
-    }
-    let pk_column = pk_columns.into_iter().next().expect("len == 1");
-    let cols_ref: Vec<&str> = cols.iter().map(AsRef::as_ref).collect();
-    if cols_ref.iter().any(|c| *c == pk_column) {
+    if cols.iter().any(|c| c.as_ref() == pk_column) {
         return None;
     }
     Some(pk_column)

From b6f49a3ae61a89376d1c5b2e28df798b019e46bf Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 2 Jul 2026 10:55:08 +0900
Subject: [PATCH 047/109] retry-now#0046: batch - hoist loader rayon dispatch
 helper, unify CLI $schema writers, add FileFormat::extension, fix stale doc
 pointer, single-pass migration-name rendering (5 kept)

---
 Cargo.lock                                    |   1 +
 crates/vespertide-cli/Cargo.toml              |   1 +
 crates/vespertide-cli/src/commands/new.rs     |  44 +----
 .../src/commands/revision/write.rs            |  43 +---
 crates/vespertide-cli/src/parallel_config.rs  |   3 +-
 crates/vespertide-cli/src/utils.rs            | 184 ++++++++++++------
 crates/vespertide-config/src/file_format.rs   |  19 ++
 .../src/parallel_config.rs                    |   3 +-
 crates/vespertide-loader/src/migrations.rs    |  26 +--
 crates/vespertide-loader/src/models.rs        |  26 +--
 .../vespertide-loader/src/parallel_config.rs  |  29 ++-
 .../vespertide-planner/src/parallel_config.rs |   3 +-
 .../vespertide-query/src/parallel_config.rs   |   3 +-
 13 files changed, 202 insertions(+), 183 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 4e75c2f9..50fd8f76 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2823,6 +2823,7 @@ dependencies = [
  "predicates",
  "rayon",
  "rstest",
+ "serde",
  "serde_json",
  "serde_yaml",
  "serial_test",
diff --git a/crates/vespertide-cli/Cargo.toml b/crates/vespertide-cli/Cargo.toml
index 6d79792e..3c41dd5d 100644
--- a/crates/vespertide-cli/Cargo.toml
+++ b/crates/vespertide-cli/Cargo.toml
@@ -21,6 +21,7 @@ colored = "3"
 strsim = "0.11"
 dialoguer = "0.12"
 uuid = { version = "1", features = ["v4"] }
+serde = "1"
 serde_json = "1"
 serde_yaml = "0.9"
 tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs"] }
diff --git a/crates/vespertide-cli/src/commands/new.rs b/crates/vespertide-cli/src/commands/new.rs
index 4ec38d12..0696fad4 100644
--- a/crates/vespertide-cli/src/commands/new.rs
+++ b/crates/vespertide-cli/src/commands/new.rs
@@ -1,12 +1,9 @@
-use std::path::Path;
-
 use anyhow::{Context, Result, bail};
 use colored::Colorize;
-use serde_json::Value;
 use tokio::fs;
 use vespertide_core::TableDef;
 
-use crate::utils::{load_config, schema_url};
+use crate::utils::{load_config, schema_url, write_json_with_schema, write_yaml_with_schema};
 use vespertide_config::FileFormat;
 
 pub async fn cmd_new(name: String, format: Option) -> Result<()> {
@@ -19,14 +16,8 @@ pub async fn cmd_new(name: String, format: Option) -> Result<()> {
             .context("create models directory")?;
     }
 
-    let ext = match format {
-        FileFormat::Json => "json",
-        FileFormat::Yaml => "yaml",
-        FileFormat::Yml => "yml",
-    };
-
     let schema_url = schema_url("model.schema.json");
-    let path = dir.join(format!("{name}.vespertide.{ext}"));
+    let path = dir.join(format!("{name}.vespertide.{ext}", ext = format.extension()));
     if path.exists() {
         bail!("model file already exists: {}", path.display());
     }
@@ -40,7 +31,9 @@ pub async fn cmd_new(name: String, format: Option) -> Result<()> {
 
     match format {
         FileFormat::Json => write_json_with_schema(&path, &table, &schema_url).await?,
-        FileFormat::Yaml | FileFormat::Yml => write_yaml(&path, &table, &schema_url).await?,
+        FileFormat::Yaml | FileFormat::Yml => {
+            write_yaml_with_schema(&path, &table, &schema_url).await?;
+        }
     }
 
     println!(
@@ -51,33 +44,6 @@ pub async fn cmd_new(name: String, format: Option) -> Result<()> {
     Ok(())
 }
 
-async fn write_json_with_schema(path: &Path, table: &TableDef, schema_url: &str) -> Result<()> {
-    let mut value = serde_json::to_value(table).context("serialize table to json")?;
-    if let Value::Object(ref mut map) = value {
-        map.insert("$schema".to_string(), Value::String(schema_url.to_string()));
-    }
-    let text = serde_json::to_string_pretty(&value).context("stringify json with schema")?;
-    fs::write(path, text)
-        .await
-        .with_context(|| format!("write file: {}", path.display()))?;
-    Ok(())
-}
-
-async fn write_yaml(path: &Path, table: &TableDef, schema_url: &str) -> Result<()> {
-    let mut value = serde_yaml::to_value(table).context("serialize table to yaml value")?;
-    if let serde_yaml::Value::Mapping(ref mut map) = value {
-        map.insert(
-            serde_yaml::Value::String("$schema".to_string()),
-            serde_yaml::Value::String(schema_url.to_string()),
-        );
-    }
-    let text = serde_yaml::to_string(&value).context("serialize yaml with schema")?;
-    fs::write(path, text)
-        .await
-        .with_context(|| format!("write file: {}", path.display()))?;
-    Ok(())
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/vespertide-cli/src/commands/revision/write.rs b/crates/vespertide-cli/src/commands/revision/write.rs
index dfb12fda..c8db4d8e 100644
--- a/crates/vespertide-cli/src/commands/revision/write.rs
+++ b/crates/vespertide-cli/src/commands/revision/write.rs
@@ -1,12 +1,14 @@
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
 
 use anyhow::{Context, Result};
-use serde_json::Value;
 use tokio::fs;
 use vespertide_config::{FileFormat, VespertideConfig};
 use vespertide_core::MigrationPlan;
 
-use crate::utils::{migration_filename_with_format_and_pattern, schema_url};
+use crate::utils::{
+    migration_filename_with_format_and_pattern, schema_url, write_json_with_schema,
+    write_yaml_with_schema,
+};
 
 pub(super) async fn write_migration_file(
     config: &VespertideConfig,
@@ -31,39 +33,10 @@ pub(super) async fn write_migration_file(
     let schema_url = schema_url("migration.schema.json");
     match format {
         FileFormat::Json => write_json_with_schema(&path, plan, &schema_url).await?,
-        FileFormat::Yaml | FileFormat::Yml => write_yaml(&path, plan, &schema_url).await?,
+        FileFormat::Yaml | FileFormat::Yml => {
+            write_yaml_with_schema(&path, plan, &schema_url).await?;
+        }
     }
 
     Ok(path)
 }
-
-pub(super) async fn write_json_with_schema(
-    path: &Path,
-    plan: &MigrationPlan,
-    schema_url: &str,
-) -> Result<()> {
-    let mut value = serde_json::to_value(plan).context("serialize migration plan to json")?;
-    if let Value::Object(ref mut map) = value {
-        map.insert("$schema".to_string(), Value::String(schema_url.to_string()));
-    }
-    let text = serde_json::to_string_pretty(&value).context("stringify json with schema")?;
-    fs::write(path, text)
-        .await
-        .with_context(|| format!("write file: {}", path.display()))?;
-    Ok(())
-}
-
-pub(super) async fn write_yaml(path: &Path, plan: &MigrationPlan, schema_url: &str) -> Result<()> {
-    let mut value = serde_yaml::to_value(plan).context("serialize migration plan to yaml value")?;
-    if let serde_yaml::Value::Mapping(ref mut map) = value {
-        map.insert(
-            serde_yaml::Value::String("$schema".to_string()),
-            serde_yaml::Value::String(schema_url.to_string()),
-        );
-    }
-    let text = serde_yaml::to_string(&value).context("serialize yaml with schema")?;
-    fs::write(path, text)
-        .await
-        .with_context(|| format!("write file: {}", path.display()))?;
-    Ok(())
-}
diff --git a/crates/vespertide-cli/src/parallel_config.rs b/crates/vespertide-cli/src/parallel_config.rs
index 932f139e..dc3516d6 100644
--- a/crates/vespertide-cli/src/parallel_config.rs
+++ b/crates/vespertide-cli/src/parallel_config.rs
@@ -1,6 +1,7 @@
 //! Empirically tuned Rayon thresholds.
 //!
-//! See `docs/PARALLELIZATION.md` for the Wave 6 measurement notes.
+//! Measured during the Wave 6 parallelization pass via the criterion
+//! benchmarks in `crates/*/benches/`.
 //!
 //! CLI export iterates at most four ORM variants, but per-table render work is
 //! CPU-bound. Wave 6 kept the Wave 1 threshold unchanged.
diff --git a/crates/vespertide-cli/src/utils.rs b/crates/vespertide-cli/src/utils.rs
index 940d8ad5..61710cb8 100644
--- a/crates/vespertide-cli/src/utils.rs
+++ b/crates/vespertide-cli/src/utils.rs
@@ -1,9 +1,56 @@
 use std::fmt::Write as _;
+use std::path::Path;
+
+use anyhow::{Context, Result};
+use serde::Serialize;
+use tokio::fs;
 use vespertide_config::FileFormat;
 
 // Re-export loader functions for convenience
 pub use vespertide_loader::{load_config, load_migrations, load_models};
 
+/// Serialize `value` as pretty JSON, inject a top-level `$schema` key, and
+/// write the result to `path`.
+pub(crate) async fn write_json_with_schema(
+    path: &Path,
+    value: &T,
+    schema_url: &str,
+) -> Result<()> {
+    let mut value = serde_json::to_value(value).context("serialize value to json")?;
+    if let serde_json::Value::Object(ref mut map) = value {
+        map.insert(
+            "$schema".to_string(),
+            serde_json::Value::String(schema_url.to_string()),
+        );
+    }
+    let text = serde_json::to_string_pretty(&value).context("stringify json with schema")?;
+    fs::write(path, text)
+        .await
+        .with_context(|| format!("write file: {}", path.display()))?;
+    Ok(())
+}
+
+/// Serialize `value` as YAML, inject a top-level `$schema` key, and write the
+/// result to `path`.
+pub(crate) async fn write_yaml_with_schema(
+    path: &Path,
+    value: &T,
+    schema_url: &str,
+) -> Result<()> {
+    let mut value = serde_yaml::to_value(value).context("serialize value to yaml value")?;
+    if let serde_yaml::Value::Mapping(ref mut map) = value {
+        map.insert(
+            serde_yaml::Value::String("$schema".to_string()),
+            serde_yaml::Value::String(schema_url.to_string()),
+        );
+    }
+    let text = serde_yaml::to_string(&value).context("serialize yaml with schema")?;
+    fs::write(path, text)
+        .await
+        .with_context(|| format!("write file: {}", path.display()))?;
+    Ok(())
+}
+
 pub(crate) fn schema_url(schema_filename: &str) -> String {
     // If not set, default to public raw GitHub schema location.
     // Users can override via VESP_SCHEMA_BASE_URL.
@@ -25,77 +72,91 @@ pub fn migration_filename_with_format_and_pattern(
     let sanitized = sanitize_comment(comment);
     let name = render_migration_name(pattern, version, &sanitized);
 
-    let ext = match format {
-        FileFormat::Json => "json",
-        FileFormat::Yaml => "yaml",
-        FileFormat::Yml => "yml",
-    };
-
-    format!("{name}.vespertide.{ext}")
+    format!("{name}.vespertide.{ext}", ext = format.extension())
 }
 
+/// Lowercase `comment`, replace non-alphanumeric characters with `_`, and
+/// collapse whitespace runs to a single `_` — in one pass, no intermediate
+/// allocations.
 fn sanitize_comment(comment: Option<&str>) -> String {
-    comment
-        .map(|c| {
-            c.to_lowercase()
-                .chars()
-                .map(|ch| {
-                    if ch.is_alphanumeric() || ch == ' ' {
-                        ch
-                    } else {
-                        '_'
-                    }
-                })
-                .collect::()
-                .split_whitespace()
-                .collect::>()
-                .join("_")
-        })
-        .unwrap_or_default()
+    let Some(comment) = comment else {
+        return String::new();
+    };
+
+    let mut out = String::with_capacity(comment.len());
+    let mut pending_separator = false;
+    for ch in comment.chars() {
+        if ch.is_whitespace() {
+            // Leading whitespace is dropped; inner runs flush as one `_`.
+            pending_separator = !out.is_empty();
+            continue;
+        }
+        if pending_separator {
+            out.push('_');
+            pending_separator = false;
+        }
+        if ch.is_alphanumeric() {
+            out.extend(ch.to_lowercase());
+        } else {
+            out.push('_');
+        }
+    }
+    out
 }
 
 fn render_migration_name(pattern: &str, version: u32, sanitized_comment: &str) -> String {
     let default_version = format!("{version:04}");
-    let chars: Vec = pattern.chars().collect();
+    // Byte-wise scan: every placeholder byte (`%`, `v`, `m`, digits) is ASCII,
+    // and UTF-8 continuation bytes can never equal an ASCII byte, so scanning
+    // bytes is correct for multi-byte patterns. Non-placeholder spans are
+    // copied verbatim without the per-char `Vec` staging buffer.
+    let bytes = pattern.as_bytes();
+    let mut out = String::with_capacity(pattern.len() + sanitized_comment.len());
+    let mut verbatim_start = 0;
     let mut i = 0;
-    let mut out = String::new();
-
-    while i < chars.len() {
-        if chars[i] == '%' {
-            // Handle %v, %m, and %0Nv (width-padded).
-            if i + 1 < chars.len() {
-                let next = chars[i + 1];
-                if next == 'v' {
-                    out.push_str(&default_version);
-                    i += 2;
-                    continue;
-                } else if next == 'm' {
-                    out.push_str(sanitized_comment);
-                    i += 2;
-                    continue;
-                } else if next == '0' {
-                    let mut j = i + 2;
-                    let mut width = String::new();
-                    while j < chars.len() && chars[j].is_ascii_digit() {
-                        width.push(chars[j]);
-                        j += 1;
-                    }
-                    if j < chars.len() && chars[j] == 'v' {
-                        let w: usize = width.parse().unwrap_or(0);
-                        if w == 0 {
-                            out.push_str(&default_version);
-                        } else {
-                            let _ = write!(out, "{version:0w$}");
-                        }
-                        i = j + 1;
-                        continue;
+
+    while i < bytes.len() {
+        if bytes[i] != b'%' || i + 1 >= bytes.len() {
+            i += 1;
+            continue;
+        }
+        // Handle %v, %m, and %0Nv (width-padded).
+        match bytes[i + 1] {
+            b'v' => {
+                out.push_str(&pattern[verbatim_start..i]);
+                out.push_str(&default_version);
+                i += 2;
+                verbatim_start = i;
+            }
+            b'm' => {
+                out.push_str(&pattern[verbatim_start..i]);
+                out.push_str(sanitized_comment);
+                i += 2;
+                verbatim_start = i;
+            }
+            b'0' => {
+                let mut j = i + 2;
+                while j < bytes.len() && bytes[j].is_ascii_digit() {
+                    j += 1;
+                }
+                if j < bytes.len() && bytes[j] == b'v' {
+                    out.push_str(&pattern[verbatim_start..i]);
+                    let width: usize = pattern[i + 2..j].parse().unwrap_or(0);
+                    if width == 0 {
+                        out.push_str(&default_version);
+                    } else {
+                        let _ = write!(out, "{version:0width$}");
                     }
+                    i = j + 1;
+                    verbatim_start = i;
+                } else {
+                    i += 1;
                 }
             }
+            _ => i += 1,
         }
-        out.push(chars[i]);
-        i += 1;
     }
+    out.push_str(&pattern[verbatim_start..]);
 
     let mut name = out;
 
@@ -263,6 +324,13 @@ mod tests {
     #[case(3, None, FileFormat::Json, "%0v__", "0003.vespertide.json")] // width 0 falls back to default version and trailing separators are trimmed
     #[case(12, None, FileFormat::Json, "%v", "0012.vespertide.json")]
     #[case(7, None, FileFormat::Json, "%m", "0007.vespertide.json")] // uses default when comment only and empty
+    #[case(
+        4,
+        Some("Añadir  Tabla"),
+        FileFormat::Yaml,
+        "한_%v_%m",
+        "한_0004_añadir_tabla.vespertide.yaml"
+    )] // UTF-8 pattern + comment lock the byte-scanner and one-pass sanitize
     fn migration_filename_with_format_and_pattern_tests(
         #[case] version: u32,
         #[case] comment: Option<&str>,
diff --git a/crates/vespertide-config/src/file_format.rs b/crates/vespertide-config/src/file_format.rs
index 76ba3317..1d29f293 100644
--- a/crates/vespertide-config/src/file_format.rs
+++ b/crates/vespertide-config/src/file_format.rs
@@ -13,6 +13,18 @@ pub enum FileFormat {
     Yml,
 }
 
+impl FileFormat {
+    /// File extension (without the leading dot) for artifacts in this format.
+    #[must_use]
+    pub const fn extension(self) -> &'static str {
+        match self {
+            Self::Json => "json",
+            Self::Yaml => "yaml",
+            Self::Yml => "yml",
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::FileFormat;
@@ -21,4 +33,11 @@ mod tests {
     fn default_is_json() {
         assert_eq!(FileFormat::default(), FileFormat::Json);
     }
+
+    #[test]
+    fn extension_matches_serde_wire_name() {
+        assert_eq!(FileFormat::Json.extension(), "json");
+        assert_eq!(FileFormat::Yaml.extension(), "yaml");
+        assert_eq!(FileFormat::Yml.extension(), "yml");
+    }
 }
diff --git a/crates/vespertide-exporter/src/parallel_config.rs b/crates/vespertide-exporter/src/parallel_config.rs
index 39b93998..08c3fe7e 100644
--- a/crates/vespertide-exporter/src/parallel_config.rs
+++ b/crates/vespertide-exporter/src/parallel_config.rs
@@ -1,6 +1,7 @@
 //! Empirically tuned Rayon thresholds.
 //!
-//! See `docs/PARALLELIZATION.md` for the Wave 6 measurement notes.
+//! Measured during the Wave 6 parallelization pass via the criterion
+//! benchmarks in `crates/*/benches/`.
 
 /// Schema-level `SeaORM` export already wins at 50 tables.
 pub(crate) const SEAORM_EXPORT_PAR_TABLE_THRESHOLD: usize = 50;
diff --git a/crates/vespertide-loader/src/migrations.rs b/crates/vespertide-loader/src/migrations.rs
index a9b2a538..712c498a 100644
--- a/crates/vespertide-loader/src/migrations.rs
+++ b/crates/vespertide-loader/src/migrations.rs
@@ -3,12 +3,11 @@ use std::fs;
 use std::path::{Path, PathBuf};
 
 use anyhow::{Context, Result};
-use rayon::prelude::*;
 use vespertide_config::VespertideConfig;
 use vespertide_core::MigrationPlan;
 use vespertide_planner::validate_migration_plan;
 
-use crate::parallel_config::{LOAD_FILES_PAR_MIN_LEN, LOAD_FILES_PAR_THRESHOLD};
+use crate::parallel_config::map_paths_with_threshold;
 
 /// Load all migration plans from the migrations directory, sorted by version.
 pub fn load_migrations(config: &VespertideConfig) -> Result> {
@@ -18,15 +17,7 @@ pub fn load_migrations(config: &VespertideConfig) -> Result>
     }
 
     let paths = collect_migration_paths(migrations_dir)?;
-    let results: Vec> = if paths.len() < LOAD_FILES_PAR_THRESHOLD {
-        paths.iter().map(|path| load_migration_file(path)).collect()
-    } else {
-        paths
-            .par_iter()
-            .with_min_len(LOAD_FILES_PAR_MIN_LEN)
-            .map(|path| load_migration_file(path))
-            .collect()
-    };
+    let results = map_paths_with_threshold(&paths, load_migration_file);
 
     let mut plans = Vec::with_capacity(results.len());
     for result in results {
@@ -62,18 +53,7 @@ pub fn load_migrations_from_dir(
     }
 
     let paths = collect_migration_paths_internal(&migrations_dir)?;
-    let results: Vec> = if paths.len() < LOAD_FILES_PAR_THRESHOLD {
-        paths
-            .iter()
-            .map(|path| load_migration_file_internal(path))
-            .collect()
-    } else {
-        paths
-            .par_iter()
-            .with_min_len(LOAD_FILES_PAR_MIN_LEN)
-            .map(|path| load_migration_file_internal(path))
-            .collect()
-    };
+    let results = map_paths_with_threshold(&paths, load_migration_file_internal);
 
     let mut plans = Vec::with_capacity(results.len());
     for result in results {
diff --git a/crates/vespertide-loader/src/models.rs b/crates/vespertide-loader/src/models.rs
index c8a27e73..66b5aeb2 100644
--- a/crates/vespertide-loader/src/models.rs
+++ b/crates/vespertide-loader/src/models.rs
@@ -2,12 +2,11 @@ use std::fs;
 use std::path::{Path, PathBuf};
 
 use anyhow::{Context, Result};
-use rayon::prelude::*;
 use vespertide_config::VespertideConfig;
 use vespertide_core::TableDef;
 use vespertide_planner::validate_schema;
 
-use crate::parallel_config::{LOAD_FILES_PAR_MIN_LEN, LOAD_FILES_PAR_THRESHOLD};
+use crate::parallel_config::map_paths_with_threshold;
 
 /// Load all model definitions from the models directory (recursively).
 pub fn load_models(config: &VespertideConfig) -> Result> {
@@ -40,15 +39,7 @@ pub fn load_models(config: &VespertideConfig) -> Result> {
 /// Recursively walk directory and load model files.
 fn load_models_recursive(dir: &Path, tables: &mut Vec) -> Result<()> {
     let paths = collect_model_paths(dir)?;
-    let results: Vec> = if paths.len() < LOAD_FILES_PAR_THRESHOLD {
-        paths.iter().map(|path| load_model_file(path)).collect()
-    } else {
-        paths
-            .par_iter()
-            .with_min_len(LOAD_FILES_PAR_MIN_LEN)
-            .map(|path| load_model_file(path))
-            .collect()
-    };
+    let results = map_paths_with_threshold(&paths, load_model_file);
 
     for result in results {
         tables.push(result?);
@@ -142,18 +133,7 @@ fn load_models_recursive_internal(
     tables: &mut Vec,
 ) -> Result<(), Box> {
     let paths = collect_model_paths_internal(dir)?;
-    let results: Vec> = if paths.len() < LOAD_FILES_PAR_THRESHOLD {
-        paths
-            .iter()
-            .map(|path| load_normalized_model_file_internal(path))
-            .collect()
-    } else {
-        paths
-            .par_iter()
-            .with_min_len(LOAD_FILES_PAR_MIN_LEN)
-            .map(|path| load_normalized_model_file_internal(path))
-            .collect()
-    };
+    let results = map_paths_with_threshold(&paths, load_normalized_model_file_internal);
 
     for result in results {
         tables.push(result.map_err(|e| -> Box { e.into() })?);
diff --git a/crates/vespertide-loader/src/parallel_config.rs b/crates/vespertide-loader/src/parallel_config.rs
index 9509a0cb..ed596919 100644
--- a/crates/vespertide-loader/src/parallel_config.rs
+++ b/crates/vespertide-loader/src/parallel_config.rs
@@ -1,8 +1,35 @@
 //! Empirically tuned Rayon thresholds.
 //!
-//! See `docs/PARALLELIZATION.md` for the Wave 6 measurement notes.
+//! Measured during the Wave 6 parallelization pass via the criterion
+//! benchmarks in `crates/*/benches/`.
 //!
 //! Loader work is IO-bound; Wave 6 kept the Wave 1 threshold unchanged.
 
+use std::path::{Path, PathBuf};
+
+use rayon::prelude::*;
+
 pub(crate) const LOAD_FILES_PAR_THRESHOLD: usize = 20;
 pub(crate) const LOAD_FILES_PAR_MIN_LEN: usize = 4;
+
+/// Map `f` over `paths`, staying sequential below the tuned threshold and
+/// dispatching to Rayon (with the tuned chunk floor) above it.
+///
+/// Single home for the sequential/parallel dispatch shape shared by every
+/// file loader in this crate, so threshold tuning happens in one place.
+pub(crate) fn map_paths_with_threshold(paths: &[PathBuf], f: F) -> Vec>
+where
+    F: Fn(&Path) -> Result + Sync,
+    T: Send,
+    E: Send,
+{
+    if paths.len() < LOAD_FILES_PAR_THRESHOLD {
+        paths.iter().map(|path| f(path)).collect()
+    } else {
+        paths
+            .par_iter()
+            .with_min_len(LOAD_FILES_PAR_MIN_LEN)
+            .map(|path| f(path))
+            .collect()
+    }
+}
diff --git a/crates/vespertide-planner/src/parallel_config.rs b/crates/vespertide-planner/src/parallel_config.rs
index 5f21d76b..61215eb8 100644
--- a/crates/vespertide-planner/src/parallel_config.rs
+++ b/crates/vespertide-planner/src/parallel_config.rs
@@ -1,6 +1,7 @@
 //! Empirically tuned Rayon thresholds.
 //!
-//! See `docs/PARALLELIZATION.md` for the Wave 6 measurement notes.
+//! Measured during the Wave 6 parallelization pass via the criterion
+//! benchmarks in `crates/*/benches/`.
 
 /// `diff_schemas` per-table work breaks even just above 5,000 tables.
 /// Use the >5,000 rule's safety threshold so small/medium diffs stay sequential.
diff --git a/crates/vespertide-query/src/parallel_config.rs b/crates/vespertide-query/src/parallel_config.rs
index 405b3aba..7e99d9e1 100644
--- a/crates/vespertide-query/src/parallel_config.rs
+++ b/crates/vespertide-query/src/parallel_config.rs
@@ -1,6 +1,7 @@
 //! Empirically tuned Rayon thresholds.
 //!
-//! See `docs/PARALLELIZATION.md` for the Wave 6 measurement notes.
+//! Measured during the Wave 6 parallelization pass via the criterion
+//! benchmarks in `crates/*/benches/`.
 
 /// `build_plan_queries` pays an up-front schema-preparation cost before the
 /// per-action Rayon phase. It did not win through 1,000 actions, so ordinary

From 4e3457b49f8ed6964aad99fbb85d75ed382ed064 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 2 Jul 2026 13:12:57 +0900
Subject: [PATCH 048/109] retry-now#0047: batch - fix RingCache double-insert
 race, layered ERD BFS frontier, single-lookup normalize grouping,
 quote_ident_into buffer writes, dedupe comment truncation (5 kept)

---
 .../vespertide-cli/src/commands/diff/mod.rs   | 12 ++----
 crates/vespertide-cli/src/commands/erd/mod.rs | 21 ++++++----
 .../src/commands/erd/tests/mod.rs             | 15 +++++++
 crates/vespertide-core/src/action/display.rs  |  9 +++-
 crates/vespertide-core/src/action/mod.rs      |  1 +
 .../src/schema/table/normalize.rs             | 11 ++---
 crates/vespertide-lsp/src/cache.rs            | 42 +++++++++++++++++++
 crates/vespertide-query/src/sql/helpers.rs    | 23 +++++++---
 8 files changed, 107 insertions(+), 27 deletions(-)

diff --git a/crates/vespertide-cli/src/commands/diff/mod.rs b/crates/vespertide-cli/src/commands/diff/mod.rs
index 033029db..f93c4c8d 100644
--- a/crates/vespertide-cli/src/commands/diff/mod.rs
+++ b/crates/vespertide-cli/src/commands/diff/mod.rs
@@ -10,6 +10,7 @@ use vespertide_planner::{
 };
 
 use crate::utils::{load_config, load_migrations, load_models};
+use vespertide_core::action::truncate_comment;
 use vespertide_core::{MigrationAction, MigrationPlan, TableDef};
 
 pub async fn cmd_diff() -> Result<()> {
@@ -485,14 +486,9 @@ fn format_action(action: &MigrationAction) -> String {
             ..
         } => {
             let comment_display = new_comment.as_deref().unwrap_or("(none)");
-            let truncated = if comment_display.chars().count() > 30 {
-                format!(
-                    "{}...",
-                    comment_display.chars().take(27).collect::()
-                )
-            } else {
-                comment_display.to_string()
-            };
+            // Shared 30-char display budget — single source of truth in
+            // `vespertide_core::action::truncate_comment`.
+            let truncated = truncate_comment(comment_display);
             format!(
                 "{} {}.{} {} '{}'",
                 "Modify column comment:".bright_yellow(),
diff --git a/crates/vespertide-cli/src/commands/erd/mod.rs b/crates/vespertide-cli/src/commands/erd/mod.rs
index 189763ed..bcf6eec9 100644
--- a/crates/vespertide-cli/src/commands/erd/mod.rs
+++ b/crates/vespertide-cli/src/commands/erd/mod.rs
@@ -107,18 +107,25 @@ pub(super) fn filter_tables_with_warnings(
     };
 
     let adjacency = build_fk_adjacency(&tables);
+    // Layered BFS: each round expands only the names discovered in the
+    // previous round instead of re-scanning (and re-cloning) all of `kept`,
+    // so every node's adjacency is walked at most once regardless of depth.
+    let mut frontier: Vec = kept.iter().cloned().collect();
     for _ in 0..depth {
-        let frontier: Vec = kept.iter().cloned().collect();
+        if frontier.is_empty() {
+            break;
+        }
+        let mut next = Vec::new();
         for name in frontier {
             if let Some(neighbors) = adjacency.get(&name) {
-                kept.extend(
-                    neighbors
-                        .iter()
-                        .filter(|neighbor| all_names.contains(*neighbor))
-                        .cloned(),
-                );
+                for neighbor in neighbors {
+                    if all_names.contains(neighbor) && kept.insert(neighbor.clone()) {
+                        next.push(neighbor.clone());
+                    }
+                }
             }
         }
+        frontier = next;
     }
 
     for name in exclude {
diff --git a/crates/vespertide-cli/src/commands/erd/tests/mod.rs b/crates/vespertide-cli/src/commands/erd/tests/mod.rs
index 2d39a862..362aa398 100644
--- a/crates/vespertide-cli/src/commands/erd/tests/mod.rs
+++ b/crates/vespertide-cli/src/commands/erd/tests/mod.rs
@@ -300,6 +300,21 @@ fn test_filter_include_with_depth_2() {
     );
 }
 
+#[test]
+fn test_filter_include_with_depth_beyond_saturation() {
+    // Depth far beyond the FK graph's diameter: the BFS frontier empties
+    // after every reachable table is found and the expansion stops early,
+    // yielding the same result as the exact-diameter depth.
+    let (filtered, warnings) =
+        filter_tables_with_warnings(filter_schema(), &only_include(&["user"]), &[], 10);
+
+    assert!(warnings.is_empty());
+    assert_eq!(
+        table_names(&filtered),
+        vec!["user", "media", "article", "article_user", "comment"]
+    );
+}
+
 #[test]
 fn test_filter_exclude() {
     let (filtered, warnings) =
diff --git a/crates/vespertide-core/src/action/display.rs b/crates/vespertide-core/src/action/display.rs
index 43cf1b01..358b171c 100644
--- a/crates/vespertide-core/src/action/display.rs
+++ b/crates/vespertide-core/src/action/display.rs
@@ -104,7 +104,14 @@ fn write_comment_action(
     }
 }
 
-fn truncate_comment(comment: &str) -> String {
+/// Truncate a column comment for display: comments longer than 30 characters
+/// are cut to their first 27 characters followed by `...`.
+///
+/// Single source of truth for the `ModifyColumnComment` display budget —
+/// shared by [`MigrationAction`]'s `Display` impl and the CLI diff formatter
+/// so the two renderings can never drift apart.
+#[must_use]
+pub fn truncate_comment(comment: &str) -> String {
     if comment.chars().count() > 30 {
         format!("{}...", truncate_chars(comment, 27))
     } else {
diff --git a/crates/vespertide-core/src/action/mod.rs b/crates/vespertide-core/src/action/mod.rs
index dcd53ff4..59459c23 100644
--- a/crates/vespertide-core/src/action/mod.rs
+++ b/crates/vespertide-core/src/action/mod.rs
@@ -4,6 +4,7 @@ mod prefix;
 mod remap_mapping_serde;
 
 use crate::schema::{ColumnDef, ColumnName, ColumnType, TableConstraint, TableName};
+pub use display::truncate_comment;
 pub use narrowing_strategy::NarrowingStrategy;
 use serde::{Deserialize, Serialize};
 use std::collections::BTreeMap;
diff --git a/crates/vespertide-core/src/schema/table/normalize.rs b/crates/vespertide-core/src/schema/table/normalize.rs
index 877d1543..24c39e56 100644
--- a/crates/vespertide-core/src/schema/table/normalize.rs
+++ b/crates/vespertide-core/src/schema/table/normalize.rs
@@ -143,13 +143,14 @@ fn push_grouped_column(
     group_name: &str,
     column_name: &str,
 ) {
-    if !groups.contains_key(group_name) {
+    // Single map lookup: groups never store empty vectors (a group exists
+    // only after a push below), so a Vec just created by `or_default()` is
+    // exactly the "first sighting" that must register in `order`.
+    let cols = groups.entry(group_name.to_string()).or_default();
+    if cols.is_empty() {
         order.push(group_name.to_string());
     }
-    groups
-        .entry(group_name.to_string())
-        .or_default()
-        .push(column_name.into());
+    cols.push(column_name.into());
 }
 
 fn generated_name_to_constraint_name(name: &str) -> Option {
diff --git a/crates/vespertide-lsp/src/cache.rs b/crates/vespertide-lsp/src/cache.rs
index 957a5eb1..17846242 100644
--- a/crates/vespertide-lsp/src/cache.rs
+++ b/crates/vespertide-lsp/src/cache.rs
@@ -87,6 +87,14 @@ where
             .inner
             .lock()
             .expect("RingCache lock poisoned — invariant: compute_fn must not panic");
+        // Re-check after the unlocked compute: another thread may have missed
+        // on the same key concurrently and already inserted. Returning the
+        // canonical `Arc` (and dropping our duplicate computation) keeps
+        // `insertion_order` free of duplicate keys — a duplicate would shrink
+        // effective capacity and evict a recently re-inserted hot entry early.
+        if let Some(existing) = inner.entries.get(&key) {
+            return Arc::clone(existing);
+        }
         if inner.entries.len() >= N
             && let Some(oldest) = inner.insertion_order.pop_front()
         {
@@ -274,6 +282,40 @@ mod tests {
         assert_eq!(counter.load(Ordering::SeqCst), 4);
     }
 
+    #[test]
+    fn ring_cache_concurrent_miss_returns_canonical_arc() {
+        // Deterministic double-compute: the barrier inside `compute_fn` can
+        // only release once BOTH threads have missed and entered compute
+        // (a thread that hit the cache would never reach the barrier, and
+        // the winner cannot insert while blocked on it). The loser must then
+        // discard its value and return the winner's canonical `Arc`, leaving
+        // `insertion_order` without a duplicate key.
+        let cache: Arc, 4>> = Arc::new(RingCache::new());
+        let barrier = Arc::new(std::sync::Barrier::new(2));
+        let compute_calls = Arc::new(AtomicUsize::new(0));
+        let handles: Vec<_> = (0..2)
+            .map(|i| {
+                let cache = Arc::clone(&cache);
+                let barrier = Arc::clone(&barrier);
+                let compute_calls = Arc::clone(&compute_calls);
+                std::thread::spawn(move || {
+                    cache.get_or_compute(1, || {
+                        compute_calls.fetch_add(1, Ordering::SeqCst);
+                        barrier.wait();
+                        vec![i]
+                    })
+                })
+            })
+            .collect();
+        let results: Vec>> = handles.into_iter().map(|h| h.join().unwrap()).collect();
+        // Both threads genuinely computed (the race happened) …
+        assert_eq!(compute_calls.load(Ordering::SeqCst), 2);
+        // … yet both callers share the single canonical cached value.
+        assert!(Arc::ptr_eq(&results[0], &results[1]));
+        let again = cache.get_or_compute(1, || unreachable!("must be cached"));
+        assert!(Arc::ptr_eq(&results[0], &again));
+    }
+
     #[test]
     fn ring_cache_threadsafe() {
         let cache: Arc, 8>> = Arc::new(RingCache::new());
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index a73cf2b2..98721e1d 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -767,31 +767,42 @@ pub fn get_enum_name(column_type: &ColumnType) -> Option<&str> {
 /// - `MySQL`: `` `identifier` `` (backticks; embedded `` ` `` escaped as ` `` `)
 #[must_use]
 pub fn quote_ident(name: &str, backend: DatabaseBackend) -> String {
+    let mut out = String::with_capacity(name.len() + 2);
+    quote_ident_into(&mut out, name, backend);
+    out
+}
+
+/// Append the quoted form of `name` for `backend` directly to `out`.
+///
+/// Core of [`quote_ident`], exposed as an append-style helper so
+/// multi-identifier emitters ([`quote_idents`]) can write every identifier
+/// straight into their single output buffer without a per-identifier
+/// `String` round-trip (one allocation + one memcpy saved per element).
+fn quote_ident_into(out: &mut String, name: &str, backend: DatabaseBackend) {
     let delim = match backend {
         DatabaseBackend::Postgres | DatabaseBackend::Sqlite => '"',
         DatabaseBackend::MySql => '`',
     };
     // Hot path: every valid identifier produced by the codebase carries no
-    // embedded quote char, so one exact-size allocation suffices and the
+    // embedded quote char, so one exact-size reservation suffices and the
     // `str::replace` + `format!` double-allocation is unnecessary. Mirrors
     // the borrowed-fast-path / owned-slow-path pattern already used by
     // `vespertide_core::sql_escape::escape_sql_string_literal`.
     if !name.contains(delim) {
-        let mut out = String::with_capacity(name.len() + 2);
+        out.reserve(name.len() + 2);
         out.push(delim);
         out.push_str(name);
         out.push(delim);
-        return out;
+        return;
     }
     // Slow path (defense-in-depth): identifier embeds the quote char and
     // must be escaped by doubling it. Byte-identical output to the
     // pre-fast-path implementation.
     let escaped = name.replace(delim, &format!("{delim}{delim}"));
-    let mut out = String::with_capacity(escaped.len() + 2);
+    out.reserve(escaped.len() + 2);
     out.push(delim);
     out.push_str(&escaped);
     out.push(delim);
-    out
 }
 
 /// Quote a list of identifiers and join them with comma.
@@ -804,7 +815,7 @@ pub fn quote_idents>(names: &[T], backend: DatabaseBackend) -> Str
         if i > 0 {
             out.push_str(", ");
         }
-        out.push_str("e_ident(n.as_ref(), backend));
+        quote_ident_into(&mut out, n.as_ref(), backend);
     }
     out
 }

From e35a60e641c3cacb9237779003630129f8459b4d Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 2 Jul 2026 15:18:31 +0900
Subject: [PATCH 049/109] retry-now#0048: batch - drop phantom jsonb simple
 type from README + LSP known-types, wire-format labels via
 SimpleColumnType::model_name, direct enum-removal membership test in diff
 ordering, dead pluralize branch; hotfix keyword-safe arb default idents (5
 kept + 1 hotfix)

---
 README.md                                     | 15 ++++----
 crates/vespertide-core/src/arbitrary/mod.rs   |  7 +++-
 crates/vespertide-core/src/schema/column.rs   | 29 +++++++++++++++
 crates/vespertide-core/src/schema/mod.rs      | 32 +++++++++++++++++
 .../src/seaorm/relations/naming.rs            |  2 +-
 crates/vespertide-lsp/src/diagnostics/mod.rs  | 35 +++++++++++++++++++
 .../src/diagnostics/validation/types.rs       |  1 -
 .../vespertide-planner/src/diff/ordering.rs   | 32 +++++------------
 .../src/validate/check_type_mismatch.rs       | 17 ++++++++-
 .../tests/sql_pg_query.proptest-regressions   |  1 +
 10 files changed, 138 insertions(+), 33 deletions(-)

diff --git a/README.md b/README.md
index 079b810f..7bc3578e 100644
--- a/README.md
+++ b/README.md
@@ -123,12 +123,15 @@ Models are JSON files in the `models/` directory. Always include `$schema` for I
 | `"big_int"` | BIGINT | `"boolean"` | BOOLEAN |
 | `"small_int"` | SMALLINT | `"uuid"` | UUID |
 | `"real"` | REAL | `"json"` | JSON |
-| `"double_precision"` | DOUBLE PRECISION | `"jsonb"` | JSONB |
-| `"date"` | DATE | `"bytea"` | BYTEA |
-| `"time"` | TIME | `"inet"` | INET |
-| `"timestamp"` | TIMESTAMP | `"cidr"` | CIDR |
-| `"timestamptz"` | TIMESTAMPTZ | `"macaddr"` | MACADDR |
-| `"interval"` | INTERVAL | `"xml"` | XML |
+| `"double_precision"` | DOUBLE PRECISION | `"bytea"` | BYTEA |
+| `"date"` | DATE | `"inet"` | INET |
+| `"time"` | TIME | `"cidr"` | CIDR |
+| `"timestamp"` | TIMESTAMP | `"macaddr"` | MACADDR |
+| `"timestamptz"` | TIMESTAMPTZ | `"xml"` | XML |
+| `"interval"` | INTERVAL | | |
+
+> Need `JSONB`? There is no `"jsonb"` simple type — use the custom complex type instead:
+> `{ "kind": "custom", "custom_type": "JSONB" }`.
 
 **Complex Types:**
 
diff --git a/crates/vespertide-core/src/arbitrary/mod.rs b/crates/vespertide-core/src/arbitrary/mod.rs
index 23fc6d44..3b177c97 100644
--- a/crates/vespertide-core/src/arbitrary/mod.rs
+++ b/crates/vespertide-core/src/arbitrary/mod.rs
@@ -466,7 +466,12 @@ fn names_are_unique<'a>(names: impl Iterator) -> bool {
 
 fn arb_default_string() -> impl Strategy {
     prop_oneof![
-        arb_safe_ident(),
+        // Bare identifier used as a raw SQL default expression. The `_d`
+        // suffix guarantees the ident never collides with a fully-reserved
+        // SQL keyword (`do`, `as`, `end`, ...) that PostgreSQL's parser
+        // rejects as an expression head — no reserved keyword in PG, MySQL,
+        // or SQLite ends in `_d`.
+        arb_safe_ident().prop_map(|ident| format!("{ident}_d")),
         arb_safe_ident().prop_map(|ident| format!("'{ident}'")),
         Just("NOW()".to_string()),
         Just("CURRENT_TIMESTAMP".to_string()),
diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs
index 3190fa2b..2de66a0e 100644
--- a/crates/vespertide-core/src/schema/column.rs
+++ b/crates/vespertide-core/src/schema/column.rs
@@ -333,6 +333,35 @@ impl SimpleColumnType {
         }
     }
 
+    /// Returns the snake_case model-file spelling of this type — the exact
+    /// string users write in JSON/YAML models (the serde wire name), e.g.
+    /// `SmallInt` → `"small_int"`. Use this for user-facing messages that
+    /// reference the model syntax; use [`Self::sql_type`] for SQL rendering.
+    #[must_use]
+    pub fn model_name(&self) -> &'static str {
+        match self {
+            SimpleColumnType::SmallInt => "small_int",
+            SimpleColumnType::Integer => "integer",
+            SimpleColumnType::BigInt => "big_int",
+            SimpleColumnType::Real => "real",
+            SimpleColumnType::DoublePrecision => "double_precision",
+            SimpleColumnType::Text => "text",
+            SimpleColumnType::Boolean => "boolean",
+            SimpleColumnType::Date => "date",
+            SimpleColumnType::Time => "time",
+            SimpleColumnType::Timestamp => "timestamp",
+            SimpleColumnType::Timestamptz => "timestamptz",
+            SimpleColumnType::Interval => "interval",
+            SimpleColumnType::Bytea => "bytea",
+            SimpleColumnType::Uuid => "uuid",
+            SimpleColumnType::Json => "json",
+            SimpleColumnType::Inet => "inet",
+            SimpleColumnType::Cidr => "cidr",
+            SimpleColumnType::Macaddr => "macaddr",
+            SimpleColumnType::Xml => "xml",
+        }
+    }
+
     /// Returns true if this type supports `auto_increment` (integer types only)
     pub fn supports_auto_increment(&self) -> bool {
         matches!(
diff --git a/crates/vespertide-core/src/schema/mod.rs b/crates/vespertide-core/src/schema/mod.rs
index ebff4269..c9e8bc15 100644
--- a/crates/vespertide-core/src/schema/mod.rs
+++ b/crates/vespertide-core/src/schema/mod.rs
@@ -590,6 +590,38 @@ mod tests {
             assert_eq!(column_type.sql_type(), expected);
         }
 
+        #[rstest]
+        #[case(SimpleColumnType::SmallInt, "small_int")]
+        #[case(SimpleColumnType::Integer, "integer")]
+        #[case(SimpleColumnType::BigInt, "big_int")]
+        #[case(SimpleColumnType::Real, "real")]
+        #[case(SimpleColumnType::DoublePrecision, "double_precision")]
+        #[case(SimpleColumnType::Text, "text")]
+        #[case(SimpleColumnType::Boolean, "boolean")]
+        #[case(SimpleColumnType::Date, "date")]
+        #[case(SimpleColumnType::Time, "time")]
+        #[case(SimpleColumnType::Timestamp, "timestamp")]
+        #[case(SimpleColumnType::Timestamptz, "timestamptz")]
+        #[case(SimpleColumnType::Interval, "interval")]
+        #[case(SimpleColumnType::Bytea, "bytea")]
+        #[case(SimpleColumnType::Uuid, "uuid")]
+        #[case(SimpleColumnType::Json, "json")]
+        #[case(SimpleColumnType::Inet, "inet")]
+        #[case(SimpleColumnType::Cidr, "cidr")]
+        #[case(SimpleColumnType::Macaddr, "macaddr")]
+        #[case(SimpleColumnType::Xml, "xml")]
+        fn test_simple_column_type_model_name_matches_serde_wire_format(
+            #[case] column_type: SimpleColumnType,
+            #[case] expected: &str,
+        ) {
+            assert_eq!(column_type.model_name(), expected);
+            // model_name() must agree with the serde wire format byte-for-byte.
+            assert_eq!(
+                serde_json::to_value(column_type).unwrap(),
+                serde_json::Value::String(expected.to_string())
+            );
+        }
+
         #[rstest]
         #[case(ComplexColumnType::Varchar { length: 255 }, "VARCHAR")]
         #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 }, "NUMERIC")]
diff --git a/crates/vespertide-exporter/src/seaorm/relations/naming.rs b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
index 34f90327..4dc40b16 100644
--- a/crates/vespertide-exporter/src/seaorm/relations/naming.rs
+++ b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
@@ -102,7 +102,7 @@ pub(in crate::seaorm) fn infer_field_name_from_fk_column(
 }
 
 pub(in crate::seaorm) fn pluralize(name: &str) -> String {
-    if name.ends_with('s') || name.ends_with("es") {
+    if name.ends_with('s') {
         name.to_string()
     } else if name.ends_with('y')
         && !name.ends_with("ay")
diff --git a/crates/vespertide-lsp/src/diagnostics/mod.rs b/crates/vespertide-lsp/src/diagnostics/mod.rs
index 46e036b3..ce696dcc 100644
--- a/crates/vespertide-lsp/src/diagnostics/mod.rs
+++ b/crates/vespertide-lsp/src/diagnostics/mod.rs
@@ -363,6 +363,41 @@ mod tests {
         assert!(diags.iter().all(|d| d.code != "unknown-type"));
     }
 
+    #[rstest]
+    #[case::json(
+        r#"{"name":"u","columns":[{"name":"payload","type":"jsonb","nullable":false}]}"#,
+        DocumentFormat::Json
+    )]
+    #[case::yaml(
+        "name: u\ncolumns:\n  - name: payload\n    type: jsonb\n    nullable: false\n",
+        DocumentFormat::Yaml
+    )]
+    fn jsonb_is_not_a_simple_type_and_gets_flagged(
+        #[case] src: &str,
+        #[case] format: DocumentFormat,
+    ) {
+        // `SimpleColumnType` has no `Jsonb` variant: the loader rejects
+        // `"type": "jsonb"`, so the editor must flag it too.
+        let pool = ParserPool::new();
+        let idx = WorkspaceIndex::new();
+        let tree = pool.parse(src, format);
+        let diags = compute(src, format, tree.as_ref(), &idx);
+
+        let err = diags
+            .iter()
+            .find(|d| d.code == "unknown-type")
+            .expect("`jsonb` must produce an unknown-type diagnostic");
+        let suggested = err
+            .message
+            .split_once("Expected one of:")
+            .map(|(_, tail)| tail)
+            .expect("diagnostic should list the expected types");
+        assert!(
+            !suggested.contains("jsonb"),
+            "the suggested type list must not advertise `jsonb`, got: {suggested}"
+        );
+    }
+
     #[test]
     fn yaml_unknown_column_type_highlights_type_pair() {
         let pool = ParserPool::new();
diff --git a/crates/vespertide-lsp/src/diagnostics/validation/types.rs b/crates/vespertide-lsp/src/diagnostics/validation/types.rs
index 14dc71c8..bf2a25be 100644
--- a/crates/vespertide-lsp/src/diagnostics/validation/types.rs
+++ b/crates/vespertide-lsp/src/diagnostics/validation/types.rs
@@ -19,7 +19,6 @@ pub(super) const KNOWN_SIMPLE_TYPES: &[&str] = &[
     "bytea",
     "uuid",
     "json",
-    "jsonb",
     "inet",
     "cidr",
     "macaddr",
diff --git a/crates/vespertide-planner/src/diff/ordering.rs b/crates/vespertide-planner/src/diff/ordering.rs
index ea74d5bf..d3886966 100644
--- a/crates/vespertide-planner/src/diff/ordering.rs
+++ b/crates/vespertide-planner/src/diff/ordering.rs
@@ -304,12 +304,10 @@ pub(super) fn sort_create_before_add_constraint(actions: &mut [MigrationAction])
     actions.sort_by(|a, b| compare_actions_for_create_order(a, b, &created_tables));
 }
 
-/// Get the set of string enum values that were removed (present in `from` but not in `to`).
-/// Returns None if either type is not a string enum.
-fn get_removed_string_enum_values(
-    from_type: &ColumnType,
-    to_type: &ColumnType,
-) -> Option> {
+/// Returns true when both types are string enums and `needle` is a value the
+/// change removes (present in `from`, absent in `to`). Direct membership test:
+/// `needle ∈ (from \ to)` ⇔ `needle ∈ from ∧ needle ∉ to` — no set or clones.
+fn string_enum_value_removed(from_type: &ColumnType, to_type: &ColumnType, needle: &str) -> bool {
     match (from_type, to_type) {
         (
             ColumnType::Complex(ComplexColumnType::Enum {
@@ -320,20 +318,8 @@ fn get_removed_string_enum_values(
                 values: EnumValues::String(to_values),
                 ..
             }),
-        ) => {
-            let to_set: HashSet<&str> = to_values.iter().map(std::string::String::as_str).collect();
-            let removed: Vec = from_values
-                .iter()
-                .filter(|v| !to_set.contains(v.as_str()))
-                .cloned()
-                .collect();
-            if removed.is_empty() {
-                None
-            } else {
-                Some(removed)
-            }
-        }
-        _ => None,
+        ) => from_values.iter().any(|v| v == needle) && !to_values.iter().any(|v| v == needle),
+        _ => false,
     }
 }
 
@@ -393,8 +379,6 @@ pub(super) fn sort_enum_default_dependencies(
         if let Some(&default_idx) = default_changes.get(&(*table, *column))
             && let Some(from_table) = from_map.get(table)
             && let Some(from_column) = from_table.columns.iter().find(|c| c.name == *column)
-            && let Some(removed_values) =
-                get_removed_string_enum_values(&from_column.r#type, new_type)
             && let Some(ref old_default) = from_column.default
         {
             // Both ModifyColumnType and ModifyColumnDefault exist for same column
@@ -402,7 +386,9 @@ pub(super) fn sort_enum_default_dependencies(
             let old_default_sql = old_default.to_sql();
             let old_default_unquoted = extract_unquoted_default(&old_default_sql);
 
-            if removed_values.iter().any(|v| v == old_default_unquoted) && *type_idx < default_idx {
+            if string_enum_value_removed(&from_column.r#type, new_type, old_default_unquoted)
+                && *type_idx < default_idx
+            {
                 // Old default is being removed - must change default BEFORE type
                 swaps.push((*type_idx, default_idx));
             }
diff --git a/crates/vespertide-planner/src/validate/check_type_mismatch.rs b/crates/vespertide-planner/src/validate/check_type_mismatch.rs
index 1441130b..fada55e4 100644
--- a/crates/vespertide-planner/src/validate/check_type_mismatch.rs
+++ b/crates/vespertide-planner/src/validate/check_type_mismatch.rs
@@ -400,7 +400,9 @@ fn is_definitely_mismatch(col_type: &ColumnType, lit: &Literal) -> bool {
 
 fn column_type_label(col_type: &ColumnType) -> String {
     match col_type {
-        ColumnType::Simple(simple) => format!("{simple:?}").to_lowercase(),
+        // Wire-format spelling (`small_int`, not `smallint`) so the warning
+        // echoes exactly what the user wrote in the model file.
+        ColumnType::Simple(simple) => simple.model_name().to_string(),
         ColumnType::Complex(complex) => complex_type_label(complex),
     }
 }
@@ -984,4 +986,17 @@ mod tests {
         }));
         assert_eq!(label, "custom(TSVECTOR)");
     }
+
+    #[rstest]
+    #[case::small_int(SimpleColumnType::SmallInt, "small_int")]
+    #[case::big_int(SimpleColumnType::BigInt, "big_int")]
+    #[case::double_precision(SimpleColumnType::DoublePrecision, "double_precision")]
+    fn column_type_label_uses_wire_format_for_multi_word_simple_types(
+        #[case] simple: SimpleColumnType,
+        #[case] expected: &str,
+    ) {
+        // A user who wrote `"small_int"` must be warned about `small_int`,
+        // not the Debug-derived `smallint`.
+        assert_eq!(column_type_label(&ColumnType::Simple(simple)), expected);
+    }
 }
diff --git a/crates/vespertide-query/tests/sql_pg_query.proptest-regressions b/crates/vespertide-query/tests/sql_pg_query.proptest-regressions
index 9634085e..5e7c806b 100644
--- a/crates/vespertide-query/tests/sql_pg_query.proptest-regressions
+++ b/crates/vespertide-query/tests/sql_pg_query.proptest-regressions
@@ -5,3 +5,4 @@
 # It is recommended to check this file in to source control so that
 # everyone who runs the test benefits from these saved cases.
 cc 0ec68d5c1070055735e7241790245a65e294dc41483dd006ba9aa52b08e1ae78 # shrinks to table = TableDef { name: "a", description: None, columns: [], constraints: [] }
+cc 2a6614799d2d7415c9c2e03117c0a1383eaef4a4f2e082ebb4f46f3b1cf46115 # shrinks to table = TableDef { name: "a", description: None, columns: [ColumnDef { name: "a", type: Simple(SmallInt), nullable: false, default: Some(String("do")), comment: None, primary_key: None, unique: None, index: None, foreign_key: None }], constraints: [] }

From 8f1158c970b6711f8929f28c25b0b57b4b82275c Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 2 Jul 2026 20:00:22 +0900
Subject: [PATCH 050/109] retry-now#0049: batch - borrow-keyed FK/added sets in
 emit, hoist delete_set key, shared exporter constraint_scan, drop dead LSP
 compute params, planner sort_unstable (5 kept)

---
 .../src/commands/revision/emit.rs             |  13 +-
 .../commands/revision/prompts/fill_with.rs    |  11 +-
 .../src/constraint_scan.rs                    |  40 +++++
 crates/vespertide-exporter/src/jpa/render.rs  |  32 +---
 crates/vespertide-exporter/src/lib.rs         |   1 +
 .../src/seaorm/relations/reverse.rs           |   5 +-
 .../vespertide-exporter/src/seaorm/render.rs  |  15 +-
 .../src/sqlalchemy/render.rs                  |  32 +---
 .../src/sqlmodel/render.rs                    |  30 +---
 .../src/backend/handler_file_features.rs      |  10 +-
 .../src/backend/handler_navigation.rs         |  16 +-
 .../src/backend/handler_rename.rs             |  12 +-
 crates/vespertide-lsp/src/definition/mod.rs   |  51 +-----
 crates/vespertide-lsp/src/diagnostics/mod.rs  | 142 ++++++----------
 crates/vespertide-lsp/src/file_features.rs    |  18 +-
 crates/vespertide-lsp/src/hover/check_expr.rs |  19 +--
 crates/vespertide-lsp/src/hover/mod.rs        |  22 +--
 crates/vespertide-lsp/src/references/mod.rs   |  15 +-
 .../vespertide-lsp/src/references/resolver.rs |  14 +-
 .../vespertide-lsp/src/references/search.rs   |  11 --
 crates/vespertide-lsp/src/rename.rs           | 156 +++---------------
 crates/vespertide-lsp/tests/check_lsp_e2e.rs  |   8 +-
 crates/vespertide-lsp/tests/diagnostics.rs    |  11 +-
 .../vespertide-lsp/tests/hover_definition.rs  |  19 +--
 crates/vespertide-lsp/tests/references.rs     |   8 -
 .../vespertide-planner/src/drop_resolution.rs |   4 +-
 .../src/validate/constraint_type_changes.rs   |   2 +-
 fuzz/fuzz_targets/fuzz_lsp_request.rs         |  50 +-----
 tools/lsp-profile/src/workload.rs             |  14 +-
 29 files changed, 208 insertions(+), 573 deletions(-)
 create mode 100644 crates/vespertide-exporter/src/constraint_scan.rs

diff --git a/crates/vespertide-cli/src/commands/revision/emit.rs b/crates/vespertide-cli/src/commands/revision/emit.rs
index 5ba5c18b..4f8c287d 100644
--- a/crates/vespertide-cli/src/commands/revision/emit.rs
+++ b/crates/vespertide-cli/src/commands/revision/emit.rs
@@ -111,7 +111,8 @@ pub(super) fn find_non_nullable_fk_add_columns(
     current_models: &[TableDef],
 ) -> Vec {
     // Collect FK columns from AddConstraint actions; lookup-only, ordering unused.
-    let mut fk_columns: HashSet<(String, String)> = HashSet::new();
+    // Keys borrow from `plan` so construction and probes stay allocation-free.
+    let mut fk_columns: HashSet<(&str, &str)> = HashSet::new();
     for action in &plan.actions {
         if let MigrationAction::AddConstraint {
             table,
@@ -119,16 +120,16 @@ pub(super) fn find_non_nullable_fk_add_columns(
         } = action
         {
             for col in columns {
-                fk_columns.insert((table.to_string(), col.to_string()));
+                fk_columns.insert((table.as_str(), col.as_str()));
             }
         }
     }
 
     // Collect columns being added in this migration (to distinguish new vs existing); lookup-only, ordering unused.
-    let mut added_columns: HashSet<(String, String)> = HashSet::new();
+    let mut added_columns: HashSet<(&str, &str)> = HashSet::new();
     for action in &plan.actions {
         if let MigrationAction::AddColumn { table, column, .. } = action {
-            added_columns.insert((table.to_string(), column.name.to_string()));
+            added_columns.insert((table.as_str(), column.name.as_str()));
         }
     }
 
@@ -138,7 +139,7 @@ pub(super) fn find_non_nullable_fk_add_columns(
     for action in &plan.actions {
         if let MigrationAction::AddColumn { table, column, .. } = action {
             let has_fk = column.foreign_key.is_some()
-                || fk_columns.contains(&(table.to_string(), column.name.to_string()));
+                || fk_columns.contains(&(table.as_str(), column.name.as_str()));
             if has_fk && !column.nullable && column.default.is_none() {
                 result.push(RecreateTableRequired {
                     table: table.to_string(),
@@ -158,7 +159,7 @@ pub(super) fn find_non_nullable_fk_add_columns(
         {
             for col_name in columns {
                 // Skip if this column is being added in this migration (handled by Case 1)
-                if added_columns.contains(&(table.to_string(), col_name.to_string())) {
+                if added_columns.contains(&(table.as_str(), col_name.as_str())) {
                     continue;
                 }
                 // Look up column in current models to check nullability
diff --git a/crates/vespertide-cli/src/commands/revision/prompts/fill_with.rs b/crates/vespertide-cli/src/commands/revision/prompts/fill_with.rs
index c1bbc4c0..c3bb907d 100644
--- a/crates/vespertide-cli/src/commands/revision/prompts/fill_with.rs
+++ b/crates/vespertide-cli/src/commands/revision/prompts/fill_with.rs
@@ -233,8 +233,9 @@ where
     let mut remaining = Vec::new();
 
     for item in missing.drain(..) {
-        if item.has_foreign_key && !delete_set.contains(&(item.table.clone(), item.column.clone()))
-        {
+        let key = (item.table.clone(), item.column.clone());
+        let in_delete_set = delete_set.contains(&key);
+        if item.has_foreign_key && !in_delete_set {
             // FK column without CLI arg — prompt user
             println!(
                 "  {} {}.{} has a foreign key constraint — fill_with may not work.",
@@ -243,12 +244,12 @@ where
                 item.column.bright_green()
             );
             if prompt_fn(&item.table, &item.column)? {
-                to_delete.push((item.table.clone(), item.column.clone()));
+                to_delete.push(key);
             } else {
                 remaining.push(item);
             }
-        } else if delete_set.contains(&(item.table.clone(), item.column.clone())) {
-            to_delete.push((item.table.clone(), item.column.clone()));
+        } else if in_delete_set {
+            to_delete.push(key);
         } else {
             remaining.push(item);
         }
diff --git a/crates/vespertide-exporter/src/constraint_scan.rs b/crates/vespertide-exporter/src/constraint_scan.rs
new file mode 100644
index 00000000..412b8176
--- /dev/null
+++ b/crates/vespertide-exporter/src/constraint_scan.rs
@@ -0,0 +1,40 @@
+//! Shared constraint-scan helpers used by the ORM renderers.
+//!
+//! Every backend needs the same two lookup sets when rendering a table:
+//! the columns covered by table-level primary keys and the columns that
+//! carry a single-column unique constraint. Centralising the scans keeps
+//! the four renderers from drifting apart.
+
+use std::collections::HashSet;
+
+use vespertide_core::TableConstraint;
+
+/// Collect the column names covered by table-level `PrimaryKey` constraints.
+///
+/// Lookup-only, ordering unused.
+pub(crate) fn primary_key_columns(constraints: &[TableConstraint]) -> HashSet {
+    let mut keys = HashSet::new();
+    for constraint in constraints {
+        if let TableConstraint::PrimaryKey { columns, .. } = constraint {
+            for col in columns {
+                keys.insert(col.to_string());
+            }
+        }
+    }
+    keys
+}
+
+/// Collect the column names that carry a single-column `Unique` constraint.
+///
+/// Lookup-only, ordering unused.
+pub(crate) fn single_column_uniques(constraints: &[TableConstraint]) -> HashSet {
+    let mut unique_cols = HashSet::new();
+    for constraint in constraints {
+        if let TableConstraint::Unique { columns, .. } = constraint
+            && columns.len() == 1
+        {
+            unique_cols.insert(columns[0].to_string());
+        }
+    }
+    unique_cols
+}
diff --git a/crates/vespertide-exporter/src/jpa/render.rs b/crates/vespertide-exporter/src/jpa/render.rs
index 5d2b393f..5f1b433b 100644
--- a/crates/vespertide-exporter/src/jpa/render.rs
+++ b/crates/vespertide-exporter/src/jpa/render.rs
@@ -1,4 +1,4 @@
-use std::collections::{HashMap, HashSet};
+use std::collections::HashMap;
 use std::fmt::Write as _;
 
 use vespertide_core::schema::column::{
@@ -79,19 +79,7 @@ pub(super) fn render_entity_with_imports(table: &TableDef) -> (String, UsedImpor
     lines.push(String::new());
 
     // Collect primary key columns
-    let pk_columns: HashSet = table
-        .constraints
-        .iter()
-        .filter_map(|c| {
-            if let TableConstraint::PrimaryKey { columns, .. } = c {
-                Some(columns.clone())
-            } else {
-                None
-            }
-        })
-        .flatten()
-        .map(|col| col.to_string())
-        .collect();
+    let pk_columns = crate::constraint_scan::primary_key_columns(&table.constraints);
 
     let auto_increment = table.constraints.iter().any(|c| {
         matches!(
@@ -104,21 +92,7 @@ pub(super) fn render_entity_with_imports(table: &TableDef) -> (String, UsedImpor
     });
 
     // Collect single-column unique constraints
-    let unique_columns: HashSet = table
-        .constraints
-        .iter()
-        .filter_map(|c| {
-            if let TableConstraint::Unique { columns, .. } = c {
-                if columns.len() == 1 {
-                    Some(columns[0].to_string())
-                } else {
-                    None
-                }
-            } else {
-                None
-            }
-        })
-        .collect();
+    let unique_columns = crate::constraint_scan::single_column_uniques(&table.constraints);
 
     // --- Render fields ---
     for col in &table.columns {
diff --git a/crates/vespertide-exporter/src/lib.rs b/crates/vespertide-exporter/src/lib.rs
index d59c382e..d4bc38df 100644
--- a/crates/vespertide-exporter/src/lib.rs
+++ b/crates/vespertide-exporter/src/lib.rs
@@ -1,6 +1,7 @@
 //! Helpers to convert `TableDef` models into ORM-specific representations
 //! such as `SeaORM`, `SQLAlchemy`, `SQLModel`, and JPA.
 
+mod constraint_scan;
 pub mod jpa;
 pub mod orm;
 mod parallel_config;
diff --git a/crates/vespertide-exporter/src/seaorm/relations/reverse.rs b/crates/vespertide-exporter/src/seaorm/relations/reverse.rs
index 931fd230..7548a46f 100644
--- a/crates/vespertide-exporter/src/seaorm/relations/reverse.rs
+++ b/crates/vespertide-exporter/src/seaorm/relations/reverse.rs
@@ -12,8 +12,9 @@ use super::super::imports::{
     resolve_relation_entity_module_path, sanitize_field_name, to_pascal_case, to_snake_case,
     unique_name,
 };
-use super::super::render::{primary_key_columns, single_column_unique_set};
+use super::super::render::primary_key_columns;
 use super::naming::{generate_relation_enum_name, pluralize, unique_relation_enum_name};
+use crate::constraint_scan::single_column_uniques;
 
 /// Information about a reverse relation to be generated.
 struct ReverseRelation {
@@ -203,7 +204,7 @@ fn reverse_relation_field_defs_inner(ctx: ReverseRelationFieldCtx<'_>) -> Vec HashSet {
-    let mut unique_cols = HashSet::new();
-    for constraint in constraints {
-        if let TableConstraint::Unique { columns, .. } = constraint
-            && columns.len() == 1
-        {
-            unique_cols.insert(columns[0].to_string());
-        }
-    }
-    unique_cols
-}
-
 /// Build a set of column names that have single-column indexes from constraints.
 pub(super) fn single_column_index_set(constraints: &[TableConstraint]) -> HashSet {
     let mut indexed_cols = HashSet::new();
diff --git a/crates/vespertide-exporter/src/sqlalchemy/render.rs b/crates/vespertide-exporter/src/sqlalchemy/render.rs
index b8deb0ff..1df9593d 100644
--- a/crates/vespertide-exporter/src/sqlalchemy/render.rs
+++ b/crates/vespertide-exporter/src/sqlalchemy/render.rs
@@ -1,4 +1,4 @@
-use std::collections::{HashMap, HashSet};
+use std::collections::HashMap;
 
 use super::enums::render_enum;
 use super::types::{UsedTypes, column_type_to_python, column_type_to_sqlalchemy};
@@ -132,36 +132,10 @@ fn render_entity_part(table: &TableDef, used_types: &mut UsedTypes<'static>) ->
     lines.push(String::new());
 
     // Collect primary key columns; lookup-only, ordering unused.
-    let pk_columns: HashSet = table
-        .constraints
-        .iter()
-        .filter_map(|c| {
-            if let TableConstraint::PrimaryKey { columns, .. } = c {
-                Some(columns.clone())
-            } else {
-                None
-            }
-        })
-        .flatten()
-        .map(|col| col.to_string())
-        .collect();
+    let pk_columns = crate::constraint_scan::primary_key_columns(&table.constraints);
 
     // Collect unique columns (single-column unique constraints); lookup-only, ordering unused.
-    let unique_columns: HashSet = table
-        .constraints
-        .iter()
-        .filter_map(|c| {
-            if let TableConstraint::Unique { columns, .. } = c {
-                if columns.len() == 1 {
-                    Some(columns[0].to_string())
-                } else {
-                    None
-                }
-            } else {
-                None
-            }
-        })
-        .collect();
+    let unique_columns = crate::constraint_scan::single_column_uniques(&table.constraints);
 
     // Collect foreign key info; lookup-only, ordering unused.
     let fk_info: HashMap = table
diff --git a/crates/vespertide-exporter/src/sqlmodel/render.rs b/crates/vespertide-exporter/src/sqlmodel/render.rs
index de486f9b..06d2fb94 100644
--- a/crates/vespertide-exporter/src/sqlmodel/render.rs
+++ b/crates/vespertide-exporter/src/sqlmodel/render.rs
@@ -244,36 +244,10 @@ fn render_entity_body(table: &TableDef, composite_fks: &[CompositeFk<'_>]) -> Ve
     lines.push(String::new());
 
     // Collect primary key columns; lookup-only, ordering unused.
-    let pk_columns: std::collections::HashSet = table
-        .constraints
-        .iter()
-        .filter_map(|c| {
-            if let TableConstraint::PrimaryKey { columns, .. } = c {
-                Some(columns.clone())
-            } else {
-                None
-            }
-        })
-        .flatten()
-        .map(|col| col.to_string())
-        .collect();
+    let pk_columns = crate::constraint_scan::primary_key_columns(&table.constraints);
 
     // Collect unique columns (single-column unique constraints); lookup-only, ordering unused.
-    let unique_columns: std::collections::HashSet = table
-        .constraints
-        .iter()
-        .filter_map(|c| {
-            if let TableConstraint::Unique { columns, .. } = c {
-                if columns.len() == 1 {
-                    Some(columns[0].to_string())
-                } else {
-                    None
-                }
-            } else {
-                None
-            }
-        })
-        .collect();
+    let unique_columns = crate::constraint_scan::single_column_uniques(&table.constraints);
 
     // Collect indexed columns (single-column indexes); lookup-only, ordering unused.
     let indexed_columns: std::collections::HashSet = table
diff --git a/crates/vespertide-lsp/src/backend/handler_file_features.rs b/crates/vespertide-lsp/src/backend/handler_file_features.rs
index c1b25704..28508aa6 100644
--- a/crates/vespertide-lsp/src/backend/handler_file_features.rs
+++ b/crates/vespertide-lsp/src/backend/handler_file_features.rs
@@ -44,8 +44,7 @@ pub(super) async fn folding_range_impl(
 ) -> Result>> {
     let uri = params.text_document.uri;
     let folds = backend.store.docs_iter_for_uri(&uri, |state| {
-        let text = state.text();
-        let domain = crate::file_features::compute_folding_ranges(text, state.tree.as_ref());
+        let domain = crate::file_features::compute_folding_ranges(state.tree.as_ref());
         domain
             .into_iter()
             .filter_map(|f| {
@@ -121,11 +120,8 @@ pub(super) async fn selection_range_impl(
             .map(|pos_ls| {
                 let pos_lsp = crate::position::ls_to_lsp_position(pos_ls);
                 let byte = crate::position::lsp_position_to_byte(&state.doc, pos_lsp);
-                let chain = crate::file_features::compute_selection_ranges(
-                    state.text(),
-                    state.tree.as_ref(),
-                    byte,
-                );
+                let chain =
+                    crate::file_features::compute_selection_ranges(state.tree.as_ref(), byte);
                 let mut acc: Option> = None;
                 for entry in chain.into_iter().rev() {
                     let lsp_range = Range {
diff --git a/crates/vespertide-lsp/src/backend/handler_navigation.rs b/crates/vespertide-lsp/src/backend/handler_navigation.rs
index e07d3e05..46d2d028 100644
--- a/crates/vespertide-lsp/src/backend/handler_navigation.rs
+++ b/crates/vespertide-lsp/src/backend/handler_navigation.rs
@@ -162,16 +162,15 @@ pub(super) async fn hover_impl(backend: &Backend, params: HoverParams) -> Result
     let uri = ¶ms.text_document_position_params.text_document.uri;
     let pos_ls = params.text_document_position_params.position;
     let pos_lsp = crate::position::ls_to_lsp_position(pos_ls);
-    let Some(format) = DocumentFormat::from_uri(uri) else {
+    if DocumentFormat::from_uri(uri).is_none() {
         return Ok(None);
-    };
+    }
 
     let result = backend.store.docs_iter_for_uri(uri, |state| {
         let text = state.text();
         let byte = crate::position::lsp_position_to_byte(&state.doc, pos_lsp);
         let domain = crate::hover::compute_with_workspace_tables(
             text,
-            format,
             state.tree.as_ref(),
             backend.index.as_ref(),
             backend.store.as_ref(),
@@ -207,9 +206,9 @@ pub(super) async fn goto_definition_impl(
     let uri = params.text_document_position_params.text_document.uri;
     let pos_ls = params.text_document_position_params.position;
     let pos_lsp = crate::position::ls_to_lsp_position(pos_ls);
-    let Some(format) = DocumentFormat::from_uri(&uri) else {
+    if DocumentFormat::from_uri(&uri).is_none() {
         return Ok(None);
-    };
+    }
 
     let domain = backend
         .store
@@ -218,7 +217,6 @@ pub(super) async fn goto_definition_impl(
             let byte = crate::position::lsp_position_to_byte(&state.doc, pos_lsp);
             crate::definition::compute_with_workspace_tables(
                 text,
-                format,
                 state.tree.as_ref(),
                 backend.index.as_ref(),
                 backend.store.as_ref(),
@@ -275,19 +273,17 @@ pub(super) async fn references_impl(
     let pos_ls = params.text_document_position.position;
     let pos_lsp = crate::position::ls_to_lsp_position(pos_ls);
     let include_declaration = params.context.include_declaration;
-    let Some(format) = DocumentFormat::from_uri(&uri) else {
+    if DocumentFormat::from_uri(&uri).is_none() {
         return Ok(None);
-    };
+    }
 
     let domain_refs = backend.store.docs_iter_for_uri(&uri, |state| {
         let text = state.text();
         let byte = crate::position::lsp_position_to_byte(&state.doc, pos_lsp);
         crate::references::compute(
             text,
-            format,
             state.tree.as_ref(),
             &uri,
-            backend.index.as_ref(),
             backend.store.as_ref(),
             Some(backend.workspace_tables.as_ref()),
             byte,
diff --git a/crates/vespertide-lsp/src/backend/handler_rename.rs b/crates/vespertide-lsp/src/backend/handler_rename.rs
index 29d091ac..e466919e 100644
--- a/crates/vespertide-lsp/src/backend/handler_rename.rs
+++ b/crates/vespertide-lsp/src/backend/handler_rename.rs
@@ -63,14 +63,14 @@ pub(super) async fn prepare_rename_impl(
     let uri = params.text_document.uri;
     let pos_ls = params.position;
     let pos_lsp = crate::position::ls_to_lsp_position(pos_ls);
-    let Some(format) = DocumentFormat::from_uri(&uri) else {
+    if DocumentFormat::from_uri(&uri).is_none() {
         return Ok(None);
-    };
+    }
 
     let domain = backend.store.docs_iter_for_uri(&uri, |state| {
         let text = state.text();
         let byte = crate::position::lsp_position_to_byte(&state.doc, pos_lsp);
-        crate::rename::prepare(text, format, state.tree.as_ref(), &uri, byte)
+        crate::rename::prepare(text, state.tree.as_ref(), byte)
     });
     let Some(Some(domain)) = domain else {
         log_prepare_rename_not_renameable(&uri);
@@ -110,19 +110,17 @@ pub(super) async fn rename_impl(
     let pos_ls = params.text_document_position.position;
     let pos_lsp = crate::position::ls_to_lsp_position(pos_ls);
     let new_name = params.new_name;
-    let Some(format) = DocumentFormat::from_uri(&uri) else {
+    if DocumentFormat::from_uri(&uri).is_none() {
         return Ok(None);
-    };
+    }
 
     let domain = backend.store.docs_iter_for_uri(&uri, |state| {
         let text = state.text();
         let byte = crate::position::lsp_position_to_byte(&state.doc, pos_lsp);
         crate::rename::compute(
             text,
-            format,
             state.tree.as_ref(),
             &uri,
-            backend.index.as_ref(),
             backend.store.as_ref(),
             Some(backend.workspace_tables.as_ref()),
             byte,
diff --git a/crates/vespertide-lsp/src/definition/mod.rs b/crates/vespertide-lsp/src/definition/mod.rs
index 2d24d057..4d50bbc6 100644
--- a/crates/vespertide-lsp/src/definition/mod.rs
+++ b/crates/vespertide-lsp/src/definition/mod.rs
@@ -6,7 +6,6 @@ use std::ops::Range;
 
 use tower_lsp_server::ls_types::Uri;
 
-use crate::parser::DocumentFormat;
 use crate::store::DocumentStore;
 use crate::tree_util::node_at_byte;
 use crate::workspace_index::WorkspaceIndex;
@@ -23,13 +22,12 @@ pub struct DomainLocation {
 #[must_use]
 pub fn compute(
     source: &str,
-    format: DocumentFormat,
     tree: Option<&tree_sitter::Tree>,
     index: &WorkspaceIndex,
     docs: &DocumentStore,
     byte_offset: usize,
 ) -> Option {
-    compute_with_workspace_tables(source, format, tree, index, docs, None, byte_offset)
+    compute_with_workspace_tables(source, tree, index, docs, None, byte_offset)
 }
 
 /// Compute definition target including disk-discovered tables. Falls back to
@@ -37,14 +35,12 @@ pub fn compute(
 #[must_use]
 pub fn compute_with_workspace_tables(
     source: &str,
-    format: DocumentFormat,
     tree: Option<&tree_sitter::Tree>,
     index: &WorkspaceIndex,
     docs: &DocumentStore,
     disk_tables: Option<&WorkspaceTables>,
     byte_offset: usize,
 ) -> Option {
-    let _ = format;
     let tree = tree?;
     let node = node_at_byte(tree, byte_offset)?;
     foreign_key::try_definition(node, source, index, docs, disk_tables)
@@ -79,14 +75,7 @@ mod tests {
         let post_tree = pool.parse(post_src, DocumentFormat::Json);
 
         let pos = post_src.find(r#""ref_table":"user""#).unwrap() + 14;
-        let location = compute(
-            post_src,
-            DocumentFormat::Json,
-            post_tree.as_ref(),
-            &idx,
-            &docs,
-            pos,
-        );
+        let location = compute(post_src, post_tree.as_ref(), &idx, &docs, pos);
         assert!(location.is_some(), "definition should resolve to user.json");
         assert_eq!(location.unwrap().uri, user_uri);
     }
@@ -99,7 +88,7 @@ mod tests {
         let src = r#"{"name":"x","columns":[{"name":"a","type":"integer","nullable":false,"foreign_key":{"ref_table":"nonexistent","ref_columns":["id"]}}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
         let pos = src.find("nonexistent").unwrap() + 2;
-        let location = compute(src, DocumentFormat::Json, tree.as_ref(), &idx, &docs, pos);
+        let location = compute(src, tree.as_ref(), &idx, &docs, pos);
         assert!(location.is_none());
     }
 
@@ -125,15 +114,8 @@ mod tests {
         // Cursor INSIDE the `"email"` element of ref_columns.
         let pos = post_src.find(r#""email""#).unwrap() + 3;
 
-        let location = compute(
-            post_src,
-            DocumentFormat::Json,
-            post_tree.as_ref(),
-            &idx,
-            &docs,
-            pos,
-        )
-        .expect("ref_columns entry should resolve to its target column");
+        let location = compute(post_src, post_tree.as_ref(), &idx, &docs, pos)
+            .expect("ref_columns entry should resolve to its target column");
         assert_eq!(location.uri, user_uri);
         // Range should pinpoint the user.email column's `name` value, not 0..0.
         let snippet = &user_src[location.byte_range.clone()];
@@ -165,15 +147,8 @@ mod tests {
         let post_tree = pool.parse(post_src, DocumentFormat::Yaml);
         let pos = post_src.find("ref_table: user").unwrap() + 12;
 
-        let location = compute(
-            post_src,
-            DocumentFormat::Yaml,
-            post_tree.as_ref(),
-            &idx,
-            &docs,
-            pos,
-        )
-        .expect("YAML ref_table should resolve to user.yaml");
+        let location = compute(post_src, post_tree.as_ref(), &idx, &docs, pos)
+            .expect("YAML ref_table should resolve to user.yaml");
         assert_eq!(location.uri, user_uri);
         let snippet = &user_src[location.byte_range.clone()];
         assert!(
@@ -203,15 +178,8 @@ mod tests {
         let post_tree = pool.parse(post_src, DocumentFormat::Yaml);
         let pos = post_src.find("[email]").unwrap() + 2;
 
-        let location = compute(
-            post_src,
-            DocumentFormat::Yaml,
-            post_tree.as_ref(),
-            &idx,
-            &docs,
-            pos,
-        )
-        .expect("YAML ref_columns entry should resolve to user.email");
+        let location = compute(post_src, post_tree.as_ref(), &idx, &docs, pos)
+            .expect("YAML ref_columns entry should resolve to user.email");
         assert_eq!(location.uri, user_uri);
         let snippet = &user_src[location.byte_range.clone()];
         assert!(
@@ -246,7 +214,6 @@ mod tests {
 
         let location = super::compute_with_workspace_tables(
             post_src,
-            DocumentFormat::Json,
             post_tree.as_ref(),
             &idx,
             &docs,
diff --git a/crates/vespertide-lsp/src/diagnostics/mod.rs b/crates/vespertide-lsp/src/diagnostics/mod.rs
index ce696dcc..272bd080 100644
--- a/crates/vespertide-lsp/src/diagnostics/mod.rs
+++ b/crates/vespertide-lsp/src/diagnostics/mod.rs
@@ -16,7 +16,6 @@ use vespertide_core::TableDef;
 
 use crate::cache::{RingCache, hash_text};
 use crate::parser::DocumentFormat;
-use crate::workspace_index::WorkspaceIndex;
 
 #[expect(
     clippy::match_same_arms,
@@ -79,9 +78,7 @@ pub fn compute_shared(
     text: &str,
     format: DocumentFormat,
     tree: Option<&tree_sitter::Tree>,
-    index: &WorkspaceIndex,
 ) -> Arc> {
-    let _ = index;
     diagnostics_cache().get_or_compute((hash_text(text), text.len(), format), || {
         compute_uncached(text, format, tree)
     })
@@ -93,9 +90,8 @@ pub fn compute(
     text: &str,
     format: DocumentFormat,
     tree: Option<&tree_sitter::Tree>,
-    index: &WorkspaceIndex,
 ) -> Vec {
-    (*compute_shared(text, format, tree, index)).clone()
+    (*compute_shared(text, format, tree)).clone()
 }
 
 /// Uncached implementation used by the cache on miss.
@@ -204,7 +200,6 @@ mod tests {
     #[test]
     fn valid_table_no_diagnostics() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{
             "name": "user",
             "columns": [
@@ -212,7 +207,7 @@ mod tests {
             ]
         }"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
         assert!(diags.is_empty(), "expected zero diagnostics, got {diags:?}");
     }
 
@@ -221,11 +216,10 @@ mod tests {
         let pool = ParserPool::new();
         let source = r#"{"name":"user","columns":[{"name":"id","type":"integer","nullable":false,"primary_key":true}]}"#;
         let tree = pool.parse(source, DocumentFormat::Json).unwrap();
-        let index = WorkspaceIndex::new();
 
-        let first = compute(source, DocumentFormat::Json, Some(&tree), &index);
-        let second = compute(source, DocumentFormat::Json, Some(&tree), &index);
-        let third = compute(source, DocumentFormat::Json, Some(&tree), &index);
+        let first = compute(source, DocumentFormat::Json, Some(&tree));
+        let second = compute(source, DocumentFormat::Json, Some(&tree));
+        let third = compute(source, DocumentFormat::Json, Some(&tree));
 
         // Cache must not change observable behaviour.
         assert_eq!(first, second);
@@ -238,11 +232,10 @@ mod tests {
         let pool = ParserPool::new();
         let source = r#"{"name":"u","columns":[{"name":"id","type":"integer"}]}"#;
         let tree = pool.parse(source, DocumentFormat::Json).unwrap();
-        let idx = WorkspaceIndex::new();
 
-        let first = compute(source, DocumentFormat::Json, Some(&tree), &idx);
-        let second = compute(source, DocumentFormat::Json, Some(&tree), &idx);
-        let third = compute(source, DocumentFormat::Json, Some(&tree), &idx);
+        let first = compute(source, DocumentFormat::Json, Some(&tree));
+        let second = compute(source, DocumentFormat::Json, Some(&tree));
+        let third = compute(source, DocumentFormat::Json, Some(&tree));
 
         assert_eq!(first, second);
         assert_eq!(second, third);
@@ -255,17 +248,16 @@ mod tests {
         let source = "name: u\ncolumns: []\n";
         let json_tree = pool.parse(source, DocumentFormat::Json).unwrap();
         let yaml_tree = pool.parse(source, DocumentFormat::Yaml).unwrap();
-        let idx = WorkspaceIndex::new();
 
-        let json_diags = compute(source, DocumentFormat::Json, Some(&json_tree), &idx);
-        let yaml_diags = compute(source, DocumentFormat::Yaml, Some(&yaml_tree), &idx);
+        let json_diags = compute(source, DocumentFormat::Json, Some(&json_tree));
+        let yaml_diags = compute(source, DocumentFormat::Yaml, Some(&yaml_tree));
 
         assert_ne!(
             json_diags, yaml_diags,
             "JSON syntax diagnostics must not pollute YAML results"
         );
-        let json_diags_2 = compute(source, DocumentFormat::Json, Some(&json_tree), &idx);
-        let yaml_diags_2 = compute(source, DocumentFormat::Yaml, Some(&yaml_tree), &idx);
+        let json_diags_2 = compute(source, DocumentFormat::Json, Some(&json_tree));
+        let yaml_diags_2 = compute(source, DocumentFormat::Yaml, Some(&yaml_tree));
         assert_eq!(json_diags, json_diags_2);
         assert_eq!(yaml_diags, yaml_diags_2);
     }
@@ -274,19 +266,18 @@ mod tests {
     fn diagnostics_cache_does_not_pollute_across_distinct_texts() {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let valid = r#"{"name":"a","columns":[{"name":"id","type":"integer"}]}"#;
         let invalid = r#"{"name":"b","columns":[{"name":"id","type":"wrong"}]}"#;
         let valid_tree = pool.parse(valid, DocumentFormat::Json).unwrap();
         let invalid_tree = pool.parse(invalid, DocumentFormat::Json).unwrap();
 
-        let valid_diags = compute(valid, DocumentFormat::Json, Some(&valid_tree), &idx);
-        let invalid_diags = compute(invalid, DocumentFormat::Json, Some(&invalid_tree), &idx);
+        let valid_diags = compute(valid, DocumentFormat::Json, Some(&valid_tree));
+        let invalid_diags = compute(invalid, DocumentFormat::Json, Some(&invalid_tree));
 
         assert!(valid_diags.iter().all(|diag| diag.code != "unknown-type"));
         assert!(invalid_diags.iter().any(|diag| diag.code == "unknown-type"));
-        let valid_diags_2 = compute(valid, DocumentFormat::Json, Some(&valid_tree), &idx);
-        let invalid_diags_2 = compute(invalid, DocumentFormat::Json, Some(&invalid_tree), &idx);
+        let valid_diags_2 = compute(valid, DocumentFormat::Json, Some(&valid_tree));
+        let invalid_diags_2 = compute(invalid, DocumentFormat::Json, Some(&invalid_tree));
         assert_eq!(valid_diags, valid_diags_2);
         assert_eq!(invalid_diags, invalid_diags_2);
     }
@@ -297,9 +288,8 @@ mod tests {
         let pool = ParserPool::new();
         let source = r#"{"name":"u","columns":[{"name":"id","type":"unknown_type"}]}"#;
         let tree = pool.parse(source, DocumentFormat::Json).unwrap();
-        let idx = WorkspaceIndex::new();
 
-        let cached = compute(source, DocumentFormat::Json, Some(&tree), &idx);
+        let cached = compute(source, DocumentFormat::Json, Some(&tree));
         let uncached = compute_uncached(source, DocumentFormat::Json, Some(&tree));
 
         assert_eq!(cached, uncached);
@@ -308,10 +298,9 @@ mod tests {
     #[test]
     fn truncated_json_produces_syntax_error() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name": "user","#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
         assert!(!diags.is_empty());
         assert!(
             diags
@@ -323,10 +312,9 @@ mod tests {
     #[test]
     fn unknown_column_type_highlights_type_pair_not_braces() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"u","columns":[{"name":"id","type":"wrong","nullable":false,"primary_key":true}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let err = diags
             .iter()
@@ -356,10 +344,9 @@ mod tests {
     #[test]
     fn known_simple_types_produce_no_unknown_type_diagnostic() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"u","columns":[{"name":"id","type":"integer","nullable":false,"primary_key":true}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
         assert!(diags.iter().all(|d| d.code != "unknown-type"));
     }
 
@@ -379,9 +366,8 @@ mod tests {
         // `SimpleColumnType` has no `Jsonb` variant: the loader rejects
         // `"type": "jsonb"`, so the editor must flag it too.
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let tree = pool.parse(src, format);
-        let diags = compute(src, format, tree.as_ref(), &idx);
+        let diags = compute(src, format, tree.as_ref());
 
         let err = diags
             .iter()
@@ -401,10 +387,9 @@ mod tests {
     #[test]
     fn yaml_unknown_column_type_highlights_type_pair() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = "name: u\ncolumns:\n  - name: id\n    type: wrong\n    nullable: false\n    primary_key: true\n";
         let tree = pool.parse(src, DocumentFormat::Yaml);
-        let diags = compute(src, DocumentFormat::Yaml, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Yaml, tree.as_ref());
 
         let err = diags
             .iter()
@@ -424,10 +409,9 @@ mod tests {
     #[test]
     fn yaml_valid_simple_type_produces_no_unknown_type_diagnostic() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = "name: u\ncolumns:\n  - name: id\n    type: uuid\n    nullable: false\n    primary_key: true\n";
         let tree = pool.parse(src, DocumentFormat::Yaml);
-        let diags = compute(src, DocumentFormat::Yaml, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Yaml, tree.as_ref());
         assert!(diags.iter().all(|d| d.code != "unknown-type"));
     }
 
@@ -435,10 +419,9 @@ mod tests {
     fn yaml_complex_type_object_skips_unknown_type_check() {
         // varchar lives in an object, not a string. The pre-pass must skip it.
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = "name: u\ncolumns:\n  - name: title\n    type: {kind: varchar, length: 200}\n    nullable: false\n";
         let tree = pool.parse(src, DocumentFormat::Yaml);
-        let diags = compute(src, DocumentFormat::Yaml, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Yaml, tree.as_ref());
         assert!(
             diags.iter().all(|d| d.code != "unknown-type"),
             "object type values must not trigger unknown-type, got: {diags:?}"
@@ -448,10 +431,9 @@ mod tests {
     #[test]
     fn enum_without_values_field_emits_complex_type_error() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"u","columns":[{"name":"status","type":{"kind":"enum","name":"s"},"nullable":false}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let err = diags
             .iter()
@@ -469,10 +451,9 @@ mod tests {
     #[test]
     fn enum_with_empty_values_array_is_flagged() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"u","columns":[{"name":"s","type":{"kind":"enum","name":"st","values":[]}}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let err = diags
             .iter()
@@ -484,10 +465,9 @@ mod tests {
     #[test]
     fn enum_string_duplicate_value_is_flagged_on_the_offending_element() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"u","columns":[{"name":"s","type":{"kind":"enum","name":"st","values":["active","banned","active"]}}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let err = diags
             .iter()
@@ -507,10 +487,9 @@ mod tests {
     #[test]
     fn varchar_without_length_field_is_flagged() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"u","columns":[{"name":"title","type":{"kind":"varchar"}}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let err = diags
             .iter()
@@ -522,10 +501,9 @@ mod tests {
     #[test]
     fn numeric_missing_precision_and_scale_is_flagged() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"u","columns":[{"name":"amount","type":{"kind":"numeric"}}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let err = diags
             .iter()
@@ -538,10 +516,9 @@ mod tests {
     #[test]
     fn unknown_complex_kind_is_flagged_on_kind_pair() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"u","columns":[{"name":"x","type":{"kind":"nope"}}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let err = diags
             .iter()
@@ -557,10 +534,9 @@ mod tests {
     #[test]
     fn integer_enum_duplicate_numeric_value_is_flagged() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"u","columns":[{"name":"p","type":{"kind":"enum","name":"pl","values":[{"name":"low","value":0},{"name":"high","value":0}]}}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let err = diags
             .iter()
@@ -580,7 +556,6 @@ mod tests {
     #[test]
     fn enum_integer_member_name_is_not_treated_as_column() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         // `priority.value` enum has a member literally called `id`, the
         // same as the first column's name. This MUST NOT trigger a
         // duplicate-column diagnostic.
@@ -603,7 +578,7 @@ mod tests {
             ]
         }"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
         assert!(
             diags.iter().all(|d| d.code != "duplicate-column"),
             "enum member name `id` must not collide with column name `id`, got: {diags:#?}"
@@ -613,10 +588,9 @@ mod tests {
     #[test]
     fn duplicate_column_name_pinpoints_the_second_occurrence() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"u","columns":[{"name":"id","type":"integer","nullable":false,"primary_key":true},{"name":"id","type":"text","nullable":true}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let dup = diags
             .iter()
@@ -693,11 +667,10 @@ mod tests {
     #[test]
     fn missing_columns_field_produces_validation_error() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         // Valid JSON syntax but missing required `columns`.
         let src = r#"{"name": "user"}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
         // Either serde rejects (parse-error) or validate_schema rejects. Both
         // are acceptable as long as we emit something.
         assert!(!diags.is_empty());
@@ -711,7 +684,6 @@ mod tests {
     fn fk_without_supporting_index_emits_warning() {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         // orders.user_id has an inline FK but no index → F51 warning.
         let src = r#"{
             "name": "orders_f51_uncovered",
@@ -722,7 +694,7 @@ mod tests {
             ]
         }"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let fk_diag = diags
             .iter()
@@ -757,7 +729,6 @@ mod tests {
     fn fk_with_inline_index_emits_no_warning() {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         // Same orders table, this time WITH `"index": true` on user_id.
         let src = r#"{
             "name": "orders_f51_covered",
@@ -769,7 +740,7 @@ mod tests {
             ]
         }"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         assert!(
             diags.iter().all(|d| d.code != "fk-supporting-index"),
@@ -781,7 +752,6 @@ mod tests {
     fn self_referential_fk_without_index_emits_warning() {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         // Tree structure — parent_id → self.id with no index.
         let src = r#"{
             "name": "categories_f51_selfref",
@@ -792,7 +762,7 @@ mod tests {
             ]
         }"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let fk_diag = diags
             .iter()
@@ -814,7 +784,6 @@ mod tests {
     fn integer_auto_increment_pk_emits_sequence_exhaustion_warning() {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{
             "name": "events_f76_int",
             "columns": [
@@ -824,7 +793,7 @@ mod tests {
             ]
         }"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let warn = diags
             .iter()
@@ -857,7 +826,6 @@ mod tests {
     fn small_int_auto_increment_pk_emits_high_severity_message() {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{
             "name": "tiny_seq_f76",
             "columns": [
@@ -866,7 +834,7 @@ mod tests {
             ]
         }"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let warn = diags
             .iter()
@@ -888,7 +856,6 @@ mod tests {
     fn big_int_auto_increment_pk_emits_no_warning() {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{
             "name": "safe_seq_f76",
             "columns": [
@@ -897,7 +864,7 @@ mod tests {
             ]
         }"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         assert!(
             diags.iter().all(|d| d.code != "sequence-exhaustion"),
@@ -909,10 +876,9 @@ mod tests {
     fn check_valid_expression_emits_no_check_diagnostics() {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"users","columns":[{"name":"id","type":"integer","nullable":false,"primary_key":true},{"name":"age","type":"integer","nullable":false}],"constraints":[{"type":"check","name":"check_age_reasonable","expr":"age > 0 AND age < 150"}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let check_diag_count = diags
             .iter()
@@ -931,10 +897,9 @@ mod tests {
     fn check_between_reversed_emits_error() {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"users","columns":[{"name":"id","type":"integer","nullable":false,"primary_key":true},{"name":"age","type":"integer","nullable":false}],"constraints":[{"type":"check","name":"check_age_between","expr":"age BETWEEN 100 AND 0"}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let check_diags = diags
             .iter()
@@ -964,10 +929,9 @@ mod tests {
     fn check_self_contradiction_emits_error() {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"users","columns":[{"name":"id","type":"integer","nullable":false,"primary_key":true},{"name":"age","type":"integer","nullable":false}],"constraints":[{"type":"check","name":"check_age_impossible","expr":"age > 100 AND age < 0"}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let check_diags = diags
             .iter()
@@ -994,10 +958,9 @@ mod tests {
     fn check_type_mismatch_emits_warning() {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"users","columns":[{"name":"id","type":"integer","nullable":false,"primary_key":true},{"name":"age","type":"integer","nullable":false}],"constraints":[{"type":"check","name":"check_age_type","expr":"age = 'abc'"}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let mismatch_diags = diags
             .iter()
@@ -1015,10 +978,9 @@ mod tests {
     fn f86_default_violates_check_still_works() {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"users","columns":[{"name":"id","type":"integer","nullable":false,"primary_key":true},{"name":"age","type":"integer","nullable":false,"default":5}],"constraints":[{"type":"check","name":"check_age_min","expr":"age > 10"}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         let default_diags = diags
             .iter()
@@ -1042,10 +1004,9 @@ mod tests {
     fn yaml_check_type_mismatch_emits_warning() {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = "name: users\ncolumns:\n  - name: id\n    type: integer\n    nullable: false\n    primary_key: true\n  - name: age\n    type: integer\n    nullable: false\nconstraints:\n  - type: check\n    name: check_age_type\n    expr: age = 'abc'\n";
         let tree = pool.parse(src, DocumentFormat::Yaml);
-        let diags = compute(src, DocumentFormat::Yaml, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Yaml, tree.as_ref());
 
         let mismatch_diags = diags
             .iter()
@@ -1063,12 +1024,11 @@ mod tests {
     fn diagnostics_shared_returns_arc_consistently() {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"u","columns":[{"name":"id","type":"integer","nullable":false,"primary_key":true}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json).unwrap();
 
-        let first = compute_shared(src, DocumentFormat::Json, Some(&tree), &idx);
-        let second = compute_shared(src, DocumentFormat::Json, Some(&tree), &idx);
+        let first = compute_shared(src, DocumentFormat::Json, Some(&tree));
+        let second = compute_shared(src, DocumentFormat::Json, Some(&tree));
 
         assert_eq!(&*first, &*second);
     }
@@ -1080,10 +1040,9 @@ mod tests {
     fn diagnostics_invalid_schema_cases_emit_diagnostics(#[case] src: &str) {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let tree = pool.parse(src, DocumentFormat::Json);
 
-        let diags = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let diags = compute(src, DocumentFormat::Json, tree.as_ref());
 
         assert!(!diags.is_empty(), "invalid schema should emit diagnostics");
     }
@@ -1092,10 +1051,9 @@ mod tests {
     fn missing_primary_key_warning_path_does_not_panic() {
         diagnostics_cache().clear();
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let src = r#"{"name":"u","columns":[{"name":"data","type":"text","nullable":false}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
 
-        let _ = compute(src, DocumentFormat::Json, tree.as_ref(), &idx);
+        let _ = compute(src, DocumentFormat::Json, tree.as_ref());
     }
 }
diff --git a/crates/vespertide-lsp/src/file_features.rs b/crates/vespertide-lsp/src/file_features.rs
index d8e6d65f..d300d878 100644
--- a/crates/vespertide-lsp/src/file_features.rs
+++ b/crates/vespertide-lsp/src/file_features.rs
@@ -118,11 +118,7 @@ pub fn compute_document_symbols(
 // =====================================================================
 
 #[must_use]
-pub fn compute_folding_ranges(
-    source: &str,
-    tree: Option<&tree_sitter::Tree>,
-) -> Vec {
-    let _ = source;
+pub fn compute_folding_ranges(tree: Option<&tree_sitter::Tree>) -> Vec {
     let mut out = Vec::new();
     if let Some(tree) = tree {
         collect_foldable(tree.root_node(), &mut out);
@@ -220,11 +216,9 @@ fn collect_matching_strings(
 /// collapsed so the LSP client doesn't show the same selection twice.
 #[must_use]
 pub fn compute_selection_ranges(
-    source: &str,
     tree: Option<&tree_sitter::Tree>,
     cursor_byte: usize,
 ) -> Vec {
-    let _ = source;
     let mut chain = Vec::new();
     if let Some(tree) = tree
         && let Some(start) = node_at_byte(tree, cursor_byte)
@@ -388,7 +382,7 @@ mod tests {
             ]
         }"#;
         let tree = parse_json(src);
-        let ranges = compute_folding_ranges(src, Some(&tree));
+        let ranges = compute_folding_ranges(Some(&tree));
         // At minimum: top-level object + columns array + each column.
         assert!(ranges.len() >= 4, "got: {ranges:?}");
     }
@@ -416,7 +410,7 @@ mod tests {
         let src = r#"{"name":"u","columns":[{"name":"id","type":"integer"}]}"#;
         let tree = parse_json(src);
         let cursor = src.find(r#""id""#).unwrap() + 1;
-        let chain = compute_selection_ranges(src, Some(&tree), cursor);
+        let chain = compute_selection_ranges(Some(&tree), cursor);
         assert!(
             chain.len() >= 3,
             "expected token → pair → object → ..., got: {chain:?}"
@@ -516,9 +510,9 @@ mod tests {
     #[test]
     fn none_tree_returns_empty_for_all_file_features() {
         assert!(compute_document_symbols("x", None).is_empty());
-        assert!(compute_folding_ranges("x", None).is_empty());
+        assert!(compute_folding_ranges(None).is_empty());
         assert!(compute_document_highlight("x", None, 0).is_empty());
-        assert!(compute_selection_ranges("x", None, 0).is_empty());
+        assert!(compute_selection_ranges(None, 0).is_empty());
     }
 
     #[rstest]
@@ -545,6 +539,6 @@ mod tests {
         let tree = parse_json(src);
         let cursor = src.find(r#""id""#).unwrap() + 1;
 
-        assert!(compute_selection_ranges(src, Some(&tree), cursor).len() >= 2);
+        assert!(compute_selection_ranges(Some(&tree), cursor).len() >= 2);
     }
 }
diff --git a/crates/vespertide-lsp/src/hover/check_expr.rs b/crates/vespertide-lsp/src/hover/check_expr.rs
index 6f400735..b8812150 100644
--- a/crates/vespertide-lsp/src/hover/check_expr.rs
+++ b/crates/vespertide-lsp/src/hover/check_expr.rs
@@ -578,9 +578,8 @@ constraints:
         let needle = "age > 0 AND";
         let pos = src.find(needle).expect("needle present") + needle.len() - 2;
 
-        let hover =
-            super::super::compute(src, DocumentFormat::Json, tree.as_ref(), &idx, &docs, pos)
-                .expect("hover inside parseable CHECK expression");
+        let hover = super::super::compute(src, tree.as_ref(), &idx, &docs, pos)
+            .expect("hover inside parseable CHECK expression");
 
         assert!(
             hover.markdown.contains("AND"),
@@ -623,8 +622,7 @@ constraints:
         let tree = pool.parse(src, DocumentFormat::Json);
         let pos = src.find("LOWER(").expect("needle present") + 2;
 
-        let hover =
-            super::super::compute(src, DocumentFormat::Json, tree.as_ref(), &idx, &docs, pos);
+        let hover = super::super::compute(src, tree.as_ref(), &idx, &docs, pos);
 
         if let Some(h) = hover {
             let lower = h.markdown.to_lowercase();
@@ -650,15 +648,8 @@ constraints:
         let post_tree = pool.parse(post_src, DocumentFormat::Json);
         let pos = post_src.find(r#""ref_table":"user""#).unwrap() + 14;
 
-        let hover = super::super::compute(
-            post_src,
-            DocumentFormat::Json,
-            post_tree.as_ref(),
-            &idx,
-            &docs,
-            pos,
-        )
-        .expect("hover on ref_table must still resolve");
+        let hover = super::super::compute(post_src, post_tree.as_ref(), &idx, &docs, pos)
+            .expect("hover on ref_table must still resolve");
 
         assert!(
             hover.markdown.contains("Target table"),
diff --git a/crates/vespertide-lsp/src/hover/mod.rs b/crates/vespertide-lsp/src/hover/mod.rs
index 0fc0ad7a..fd4994e8 100644
--- a/crates/vespertide-lsp/src/hover/mod.rs
+++ b/crates/vespertide-lsp/src/hover/mod.rs
@@ -6,7 +6,6 @@ mod foreign_key;
 
 use std::ops::Range;
 
-use crate::parser::DocumentFormat;
 use crate::store::DocumentStore;
 use crate::tree_util::node_at_byte;
 use crate::workspace_index::WorkspaceIndex;
@@ -25,13 +24,12 @@ pub struct DomainHover {
 #[must_use]
 pub fn compute(
     text: &str,
-    format: DocumentFormat,
     tree: Option<&tree_sitter::Tree>,
     index: &WorkspaceIndex,
     docs: &DocumentStore,
     byte_offset: usize,
 ) -> Option {
-    compute_with_workspace_tables(text, format, tree, index, docs, None, byte_offset)
+    compute_with_workspace_tables(text, tree, index, docs, None, byte_offset)
 }
 
 /// Compute hover with optional disk-discovered tables. When provided, FK
@@ -39,14 +37,12 @@ pub fn compute(
 #[must_use]
 pub fn compute_with_workspace_tables(
     text: &str,
-    format: DocumentFormat,
     tree: Option<&tree_sitter::Tree>,
     index: &WorkspaceIndex,
     docs: &DocumentStore,
     disk_tables: Option<&WorkspaceTables>,
     byte_offset: usize,
 ) -> Option {
-    let _ = format;
     let tree = tree?;
     let node = node_at_byte(tree, byte_offset)?;
 
@@ -80,7 +76,7 @@ mod tests {
         let docs = DocumentStore::new();
         let src = r#"{"name": "user"}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
-        let hover = compute(src, DocumentFormat::Json, tree.as_ref(), &idx, &docs, 0);
+        let hover = compute(src, tree.as_ref(), &idx, &docs, 0);
         // First char is `{` — no hover content. Some impls may return generic
         // hover; OK either way as long as there is no panic.
         let _ = hover;
@@ -114,7 +110,6 @@ mod tests {
         let pos = article_src.find(r#""ref_table":"media""#).unwrap() + 14;
         let hover = super::compute_with_workspace_tables(
             article_src,
-            DocumentFormat::Json,
             Some(&article_tree),
             &idx,
             &docs,
@@ -162,15 +157,8 @@ mod tests {
         let post_src = "name: post\ncolumns:\n  - name: author_id\n    type: integer\n    foreign_key:\n      ref_table: user\n      ref_columns: [id]\n";
         let post_tree = pool.parse(post_src, DocumentFormat::Yaml);
         let pos = post_src.find("ref_table: user").unwrap() + 12;
-        let hover = compute(
-            post_src,
-            DocumentFormat::Yaml,
-            post_tree.as_ref(),
-            &idx,
-            &docs,
-            pos,
-        )
-        .expect("YAML hover should resolve ref_table");
+        let hover = compute(post_src, post_tree.as_ref(), &idx, &docs, pos)
+            .expect("YAML hover should resolve ref_table");
 
         assert!(
             hover.markdown.contains("user"),
@@ -192,7 +180,7 @@ mod tests {
         let src = r#"{"name":"user","columns":[{"name":"id","type":"integer","nullable":false,"primary_key":true}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
         let pos = src.find(r#""name":"id""#).unwrap() + 5;
-        let hover = compute(src, DocumentFormat::Json, tree.as_ref(), &idx, &docs, pos);
+        let hover = compute(src, tree.as_ref(), &idx, &docs, pos);
         assert!(hover.is_some(), "hover on column should return Some");
     }
 }
diff --git a/crates/vespertide-lsp/src/references/mod.rs b/crates/vespertide-lsp/src/references/mod.rs
index 53eb900d..cc98a475 100644
--- a/crates/vespertide-lsp/src/references/mod.rs
+++ b/crates/vespertide-lsp/src/references/mod.rs
@@ -20,9 +20,7 @@ use std::ops::Range;
 
 use tower_lsp_server::ls_types::Uri;
 
-use crate::parser::DocumentFormat;
 use crate::store::DocumentStore;
-use crate::workspace_index::WorkspaceIndex;
 use crate::workspace_tables::WorkspaceTables;
 
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -43,23 +41,16 @@ pub enum ReferenceSymbol {
 
 /// Compute references for the symbol at `byte_offset`.
 #[must_use]
-#[expect(
-    clippy::too_many_arguments,
-    reason = "references compute threads document parse state, workspace stores, cursor, and declaration policy; ReferenceContext is a deferred refactor"
-)]
 pub fn compute(
     source: &str,
-    format: DocumentFormat,
     tree: Option<&tree_sitter::Tree>,
     current_uri: &Uri,
-    index: &WorkspaceIndex,
     docs: &DocumentStore,
     disk_tables: Option<&WorkspaceTables>,
     byte_offset: usize,
     include_declaration: bool,
 ) -> Vec {
-    let _ = format;
-    let Some(symbol) = resolve_symbol(source, tree, current_uri, byte_offset) else {
+    let Some(symbol) = resolve_symbol(source, tree, byte_offset) else {
         return Vec::new();
     };
 
@@ -68,7 +59,6 @@ pub fn compute(
         current_uri,
         source,
         tree,
-        index,
         docs,
         disk_tables,
         include_declaration,
@@ -80,8 +70,7 @@ pub fn compute(
 pub fn resolve_symbol(
     source: &str,
     tree: Option<&tree_sitter::Tree>,
-    current_uri: &Uri,
     byte_offset: usize,
 ) -> Option {
-    resolver::resolve(source, tree, current_uri, byte_offset)
+    resolver::resolve(source, tree, byte_offset)
 }
diff --git a/crates/vespertide-lsp/src/references/resolver.rs b/crates/vespertide-lsp/src/references/resolver.rs
index 48d0a36c..67da5693 100644
--- a/crates/vespertide-lsp/src/references/resolver.rs
+++ b/crates/vespertide-lsp/src/references/resolver.rs
@@ -4,7 +4,6 @@ use crate::text_util::strip_quotes;
 use crate::tree_util::{
     ancestor_pair, direct_child_value, enclosing_string, node_at_byte, skip_yaml_wrappers,
 };
-use tower_lsp_server::ls_types::Uri;
 
 use super::ReferenceSymbol;
 
@@ -13,10 +12,8 @@ use super::ReferenceSymbol;
 pub(super) fn resolve(
     source: &str,
     tree: Option<&tree_sitter::Tree>,
-    current_uri: &Uri,
     byte_offset: usize,
 ) -> Option {
-    let _ = current_uri;
     let tree = tree?;
     let node = node_at_byte(tree, byte_offset)?;
     let string_node = enclosing_string(node)?;
@@ -178,7 +175,7 @@ mod tests {
     fn resolve_returns_none_when_tree_is_none() {
         let src = r#"{"name":"u"}"#;
 
-        assert!(resolve(src, None, &uri("u.json"), 0).is_none());
+        assert!(resolve(src, None, 0).is_none());
     }
 
     #[rstest]
@@ -200,7 +197,7 @@ mod tests {
         let tree = parse_json(src);
         let pos = src.find(needle).unwrap() + cursor_delta;
 
-        assert_eq!(resolve(src, Some(&tree), &uri("u.json"), pos), expected);
+        assert_eq!(resolve(src, Some(&tree), pos), expected);
     }
 
     #[rstest]
@@ -215,10 +212,7 @@ mod tests {
         let tree = parse_yaml(src);
         let pos = src.find(needle).unwrap() + cursor_delta;
 
-        assert_eq!(
-            resolve(src, Some(&tree), &uri("u.yaml"), pos),
-            Some(expected)
-        );
+        assert_eq!(resolve(src, Some(&tree), pos), Some(expected));
     }
 
     fn first_node<'tree>(
@@ -247,7 +241,7 @@ mod tests {
         let tree = parse_json(src);
         let pos = src.find("hello").unwrap();
 
-        assert!(resolve(src, Some(&tree), &uri("u.json"), pos).is_none());
+        assert!(resolve(src, Some(&tree), pos).is_none());
     }
 
     #[test]
diff --git a/crates/vespertide-lsp/src/references/search.rs b/crates/vespertide-lsp/src/references/search.rs
index f793afbb..3efa944f 100644
--- a/crates/vespertide-lsp/src/references/search.rs
+++ b/crates/vespertide-lsp/src/references/search.rs
@@ -6,21 +6,15 @@ use std::path::PathBuf;
 use tower_lsp_server::ls_types::Uri;
 
 use crate::store::DocumentStore;
-use crate::workspace_index::WorkspaceIndex;
 use crate::workspace_tables::WorkspaceTables;
 
 use super::{DomainReference, ReferenceSymbol};
 
-#[expect(
-    clippy::too_many_arguments,
-    reason = "reference search needs target symbol, current document, open/disk workspace stores, and declaration policy; ReferenceSearchContext is deferred"
-)]
 pub(super) fn find_all(
     symbol: &ReferenceSymbol,
     current_uri: &Uri,
     current_source: &str,
     current_tree: Option<&tree_sitter::Tree>,
-    index: &WorkspaceIndex,
     docs: &DocumentStore,
     disk_tables: Option<&WorkspaceTables>,
     include_declaration: bool,
@@ -80,10 +74,6 @@ pub(super) fn find_all(
     });
     out.dedup();
 
-    // Resolved declarations are valuable to surface even without
-    // include_declaration via cross-file lookups (some clients ignore the
-    // flag and rely on us to be authoritative). Keep callsite explicit.
-    let _ = index;
     out
 }
 
@@ -598,7 +588,6 @@ mod tests {
             ¤t_uri,
             src,
             Some(¤t_tree),
-            &WorkspaceIndex::new(),
             &docs,
             None,
             false,
diff --git a/crates/vespertide-lsp/src/rename.rs b/crates/vespertide-lsp/src/rename.rs
index 3e40ed7d..43fed02d 100644
--- a/crates/vespertide-lsp/src/rename.rs
+++ b/crates/vespertide-lsp/src/rename.rs
@@ -13,11 +13,9 @@ use std::ops::Range;
 
 use tower_lsp_server::ls_types::Uri;
 
-use crate::parser::DocumentFormat;
 use crate::references::{self, ReferenceSymbol};
 use crate::store::DocumentStore;
 use crate::tree_util::{node_at_byte, trim_one_byte_each_side as trim_one_byte};
-use crate::workspace_index::WorkspaceIndex;
 use crate::workspace_tables::WorkspaceTables;
 
 /// Result of `textDocument/prepareRename`. When the cursor is on a
@@ -38,12 +36,10 @@ pub struct DomainPrepareRename {
 #[must_use]
 pub fn prepare(
     source: &str,
-    _format: DocumentFormat,
     tree: Option<&tree_sitter::Tree>,
-    current_uri: &Uri,
     byte_offset: usize,
 ) -> Option {
-    let symbol = references::resolve_symbol(source, tree, current_uri, byte_offset)?;
+    let symbol = references::resolve_symbol(source, tree, byte_offset)?;
     let placeholder = match &symbol {
         ReferenceSymbol::Table { name } => name.clone(),
         ReferenceSymbol::Column { column, .. } => column.clone(),
@@ -152,16 +148,10 @@ pub struct DomainRename {
 ///   * `new_name` is empty, identical to the old name, or contains
 ///     invalid characters (whitespace, quotes, control chars).
 #[must_use]
-#[expect(
-    clippy::too_many_arguments,
-    reason = "rename needs source document, cursor, open/disk workspace stores, and new identifier; RenameContext is a deferred 0.3.x refactor"
-)]
 pub fn compute(
     source: &str,
-    format: DocumentFormat,
     tree: Option<&tree_sitter::Tree>,
     current_uri: &Uri,
-    index: &WorkspaceIndex,
     docs: &DocumentStore,
     disk_tables: Option<&WorkspaceTables>,
     byte_offset: usize,
@@ -170,7 +160,7 @@ pub fn compute(
     if !is_valid_identifier(new_name) {
         return None;
     }
-    let symbol = references::resolve_symbol(source, tree, current_uri, byte_offset)?;
+    let symbol = references::resolve_symbol(source, tree, byte_offset)?;
     let old_name = match &symbol {
         ReferenceSymbol::Table { name } => name.clone(),
         ReferenceSymbol::Column { column, .. } => column.clone(),
@@ -181,10 +171,8 @@ pub fn compute(
 
     let refs = references::compute(
         source,
-        format,
         tree,
         current_uri,
-        index,
         docs,
         disk_tables,
         byte_offset,
@@ -235,13 +223,13 @@ fn is_valid_identifier(name: &str) -> bool {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::parser::ParserPool;
+    use crate::parser::{DocumentFormat, ParserPool};
     use crate::test_support::uri;
+    use crate::workspace_index::WorkspaceIndex;
 
     #[test]
     fn rejects_empty_or_whitespace_or_same_name() {
         let pool = ParserPool::new();
-        let idx = WorkspaceIndex::new();
         let docs = DocumentStore::new();
         let src = r#"{"name":"user","columns":[{"name":"id","type":"integer"}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json);
@@ -249,18 +237,7 @@ mod tests {
 
         for bad in ["", " ", "\"", "user", "a b"] {
             assert!(
-                compute(
-                    src,
-                    DocumentFormat::Json,
-                    tree.as_ref(),
-                    &uri("user.json"),
-                    &idx,
-                    &docs,
-                    None,
-                    pos,
-                    bad,
-                )
-                .is_none(),
+                compute(src, tree.as_ref(), &uri("user.json"), &docs, None, pos, bad,).is_none(),
                 "rename to `{bad}` should be rejected"
             );
         }
@@ -298,10 +275,8 @@ mod tests {
         let pos = user_src.find(r#""name":"user""#).unwrap() + 9;
         let plan = compute(
             user_src,
-            DocumentFormat::Json,
             Some(&user_tree),
             &user_uri,
-            &idx,
             &docs,
             None,
             pos,
@@ -341,18 +316,8 @@ mod tests {
         docs.open(user_uri.clone(), "json".to_string(), 1, src.to_string());
 
         let pos = src.find(r#""name":"id""#).unwrap() + 9;
-        let plan = compute(
-            src,
-            DocumentFormat::Json,
-            Some(&tree),
-            &user_uri,
-            &idx,
-            &docs,
-            None,
-            pos,
-            "a",
-        )
-        .expect("rename should succeed");
+        let plan = compute(src, Some(&tree), &user_uri, &docs, None, pos, "a")
+            .expect("rename should succeed");
 
         let file_edits = plan.edits.get(&user_uri).expect("edits for user.json");
         assert_eq!(file_edits.len(), 1);
@@ -380,14 +345,7 @@ mod tests {
         let tree = pool.parse(src, DocumentFormat::Json).unwrap();
         // Cursor inside `"user"` value.
         let pos = src.find(r#""name":"user""#).unwrap() + 9;
-        let result = prepare(
-            src,
-            DocumentFormat::Json,
-            Some(&tree),
-            &uri("user.json"),
-            pos,
-        )
-        .expect("table name should be renameable");
+        let result = prepare(src, Some(&tree), pos).expect("table name should be renameable");
         assert_eq!(result.placeholder, "user");
         assert_eq!(
             &src[result.byte_range.clone()],
@@ -402,8 +360,7 @@ mod tests {
         let src = r#"{"name":"u","columns":[{"name":"email","type":"text"}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json).unwrap();
         let pos = src.find(r#""name":"email""#).unwrap() + 10;
-        let result = prepare(src, DocumentFormat::Json, Some(&tree), &uri("u.json"), pos)
-            .expect("column name should be renameable");
+        let result = prepare(src, Some(&tree), pos).expect("column name should be renameable");
         assert_eq!(result.placeholder, "email");
         assert_eq!(&src[result.byte_range.clone()], "email");
     }
@@ -414,7 +371,7 @@ mod tests {
         let src = r#"{"name":"u","columns":[{"name":"id","type":"integer"}]}"#;
         let tree = pool.parse(src, DocumentFormat::Json).unwrap();
         // Cursor on the opening brace — not a symbol.
-        let result = prepare(src, DocumentFormat::Json, Some(&tree), &uri("u.json"), 0);
+        let result = prepare(src, Some(&tree), 0);
         assert!(
             result.is_none(),
             "non-symbol positions must not be renameable"
@@ -465,10 +422,8 @@ mod tests {
         let pos = user_src.find(r#""name":"email""#).unwrap() + 10;
         let plan = compute(
             user_src,
-            DocumentFormat::Json,
             Some(&user_tree),
             &user_uri,
-            &idx,
             &docs,
             None,
             pos,
@@ -506,18 +461,8 @@ mod tests {
 
         // Rename from the column declaration.
         let pos = src.find(r#""name":"age""#).unwrap() + 8;
-        let plan = compute(
-            src,
-            DocumentFormat::Json,
-            Some(&tree),
-            &user_uri,
-            &idx,
-            &docs,
-            None,
-            pos,
-            "years",
-        )
-        .expect("rename should succeed");
+        let plan = compute(src, Some(&tree), &user_uri, &docs, None, pos, "years")
+            .expect("rename should succeed");
 
         let file_edits = plan.edits.get(&user_uri).expect("edits for user.json");
         // Declaration edit + the `age` inside the CHECK expr.
@@ -559,18 +504,8 @@ mod tests {
 
         // Cursor placed ON the first `age` inside the CHECK expr.
         let pos = src.find(r#""expr":"age"#).unwrap() + 8;
-        let plan = compute(
-            src,
-            DocumentFormat::Json,
-            Some(&tree),
-            &user_uri,
-            &idx,
-            &docs,
-            None,
-            pos,
-            "years",
-        )
-        .expect("rename from inside CHECK should succeed");
+        let plan = compute(src, Some(&tree), &user_uri, &docs, None, pos, "years")
+            .expect("rename from inside CHECK should succeed");
 
         let file_edits = plan.edits.get(&user_uri).expect("edits for user.json");
         // Declaration + two CHECK occurrences = 3 edits, all replacing `age`.
@@ -629,10 +564,8 @@ mod tests {
         let pos = user_src.find(r#""name":"age""#).unwrap() + 8;
         let plan = compute(
             user_src,
-            DocumentFormat::Json,
             Some(&user_tree),
             &user_uri,
-            &idx,
             &docs,
             None,
             pos,
@@ -658,14 +591,8 @@ mod tests {
         let tree = pool.parse(src, DocumentFormat::Json).unwrap();
         // Cursor on the SECOND `age` inside the CHECK expr.
         let second_age = src.find("AND age").unwrap() + 4;
-        let result = prepare(
-            src,
-            DocumentFormat::Json,
-            Some(&tree),
-            &uri("user.json"),
-            second_age,
-        )
-        .expect("CHECK-expr column should be renameable");
+        let result =
+            prepare(src, Some(&tree), second_age).expect("CHECK-expr column should be renameable");
         assert_eq!(result.placeholder, "age");
         assert_eq!(
             &src[result.byte_range.clone()],
@@ -689,27 +616,14 @@ mod tests {
         let quoted = "name: \"user\"\ncolumns: []\n";
         let quoted_tree = pool.parse(quoted, DocumentFormat::Yaml).unwrap();
         let quoted_pos = quoted.find("user").unwrap();
-        let quoted_result = prepare(
-            quoted,
-            DocumentFormat::Yaml,
-            Some("ed_tree),
-            &uri("user.yaml"),
-            quoted_pos,
-        )
-        .expect("quoted YAML name");
+        let quoted_result =
+            prepare(quoted, Some("ed_tree), quoted_pos).expect("quoted YAML name");
         assert_eq!("ed[quoted_result.byte_range.clone()], "user");
 
         let plain = "name: user\ncolumns: []\n";
         let plain_tree = pool.parse(plain, DocumentFormat::Yaml).unwrap();
         let plain_pos = plain.find("user").unwrap();
-        let plain_result = prepare(
-            plain,
-            DocumentFormat::Yaml,
-            Some(&plain_tree),
-            &uri("user.yaml"),
-            plain_pos,
-        )
-        .expect("plain YAML name");
+        let plain_result = prepare(plain, Some(&plain_tree), plain_pos).expect("plain YAML name");
         assert_eq!(&plain[plain_result.byte_range.clone()], "user");
     }
 
@@ -731,20 +645,7 @@ mod tests {
         docs.open(u.clone(), "json".to_string(), 1, src.to_string());
         let pos = src.find(r#""name":"user""#).unwrap() + 9;
 
-        assert!(
-            compute(
-                src,
-                DocumentFormat::Json,
-                Some(&tree),
-                &u,
-                &idx,
-                &docs,
-                None,
-                pos,
-                "user"
-            )
-            .is_none()
-        );
+        assert!(compute(src, Some(&tree), &u, &docs, None, pos, "user").is_none());
     }
 
     #[test]
@@ -758,24 +659,11 @@ mod tests {
         idx.upsert(&u, src, &tree);
         docs.open(u.clone(), "json".to_string(), 1, src.to_string());
 
-        assert!(
-            compute(
-                src,
-                DocumentFormat::Json,
-                Some(&tree),
-                &u,
-                &idx,
-                &docs,
-                None,
-                0,
-                "new"
-            )
-            .is_none()
-        );
+        assert!(compute(src, Some(&tree), &u, &docs, None, 0, "new").is_none());
     }
 
     #[test]
     fn prepare_returns_none_when_tree_missing() {
-        assert!(prepare("x", DocumentFormat::Json, None, &uri("x.json"), 0).is_none());
+        assert!(prepare("x", None, 0).is_none());
     }
 }
diff --git a/crates/vespertide-lsp/tests/check_lsp_e2e.rs b/crates/vespertide-lsp/tests/check_lsp_e2e.rs
index cfb0ab51..e50c033f 100644
--- a/crates/vespertide-lsp/tests/check_lsp_e2e.rs
+++ b/crates/vespertide-lsp/tests/check_lsp_e2e.rs
@@ -47,12 +47,11 @@ constraints:
 #[test]
 fn json_check_diagnostics_and_semantic_tokens_render_user_visible_output() {
     let pool = ParserPool::new();
-    let index = WorkspaceIndex::new();
     let tree = pool
         .parse(JSON_MODEL, DocumentFormat::Json)
         .expect("JSON model parses");
 
-    let diagnostics = compute_diagnostics(JSON_MODEL, DocumentFormat::Json, Some(&tree), &index);
+    let diagnostics = compute_diagnostics(JSON_MODEL, DocumentFormat::Json, Some(&tree));
     let tokens = classify(JSON_MODEL, DocumentFormat::Json, Some(&tree));
     let valid_expr_range = source_range(JSON_MODEL, "age > 0 AND age < 150");
     let valid_tokens = tokens_in_range(&tokens, valid_expr_range.clone());
@@ -132,12 +131,11 @@ fn json_check_diagnostics_and_semantic_tokens_render_user_visible_output() {
 #[test]
 fn yaml_check_type_mismatch_and_tokens_render_user_visible_output() {
     let pool = ParserPool::new();
-    let index = WorkspaceIndex::new();
     let tree = pool
         .parse(YAML_MODEL, DocumentFormat::Yaml)
         .expect("YAML model parses");
 
-    let diagnostics = compute_diagnostics(YAML_MODEL, DocumentFormat::Yaml, Some(&tree), &index);
+    let diagnostics = compute_diagnostics(YAML_MODEL, DocumentFormat::Yaml, Some(&tree));
     let tokens = classify(YAML_MODEL, DocumentFormat::Yaml, Some(&tree));
     let expr_range = source_range(YAML_MODEL, "age = 'abc'");
     let expr_tokens = tokens_in_range(&tokens, expr_range);
@@ -391,7 +389,7 @@ fn h_s1_hover_on_check_expr_real_surface() {
         "cursor byte must be on `>` operator"
     );
 
-    let hover = compute_hover(src, DocumentFormat::Json, Some(&tree), &idx, &docs, cursor)
+    let hover = compute_hover(src, Some(&tree), &idx, &docs, cursor)
         .expect("hover should resolve inside CHECK expr");
 
     println!("=== HOVER (h_s1) ===");
diff --git a/crates/vespertide-lsp/tests/diagnostics.rs b/crates/vespertide-lsp/tests/diagnostics.rs
index 61494ae9..64904b5d 100644
--- a/crates/vespertide-lsp/tests/diagnostics.rs
+++ b/crates/vespertide-lsp/tests/diagnostics.rs
@@ -2,7 +2,7 @@
 
 use std::path::PathBuf;
 
-use vespertide_lsp::{DocumentFormat, ParserPool, WorkspaceIndex, compute_diagnostics};
+use vespertide_lsp::{DocumentFormat, ParserPool, compute_diagnostics};
 
 fn fixture_path(name: &str) -> PathBuf {
     let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@@ -21,10 +21,9 @@ fn read_fixture(name: &str) -> String {
 #[test]
 fn valid_user_fixture_zero_diagnostics() {
     let pool = ParserPool::new();
-    let idx = WorkspaceIndex::new();
     let text = read_fixture("valid_user.json");
     let tree = pool.parse(&text, DocumentFormat::Json);
-    let diags = compute_diagnostics(&text, DocumentFormat::Json, tree.as_ref(), &idx);
+    let diags = compute_diagnostics(&text, DocumentFormat::Json, tree.as_ref());
 
     assert!(diags.is_empty(), "expected zero, got {diags:?}");
 }
@@ -32,10 +31,9 @@ fn valid_user_fixture_zero_diagnostics() {
 #[test]
 fn truncated_json_emits_diagnostic() {
     let pool = ParserPool::new();
-    let idx = WorkspaceIndex::new();
     let text = r#"{"name": "x","#;
     let tree = pool.parse(text, DocumentFormat::Json);
-    let diags = compute_diagnostics(text, DocumentFormat::Json, tree.as_ref(), &idx);
+    let diags = compute_diagnostics(text, DocumentFormat::Json, tree.as_ref());
 
     assert!(!diags.is_empty());
 }
@@ -43,9 +41,8 @@ fn truncated_json_emits_diagnostic() {
 #[test]
 fn cjk_comment_fixture_compiles() {
     let pool = ParserPool::new();
-    let idx = WorkspaceIndex::new();
     let text = read_fixture("cjk_comment.json");
     let tree = pool.parse(&text, DocumentFormat::Json);
-    let _diags = compute_diagnostics(&text, DocumentFormat::Json, tree.as_ref(), &idx);
+    let _diags = compute_diagnostics(&text, DocumentFormat::Json, tree.as_ref());
     // No assertion on diagnostic count — just verifying no panic with CJK.
 }
diff --git a/crates/vespertide-lsp/tests/hover_definition.rs b/crates/vespertide-lsp/tests/hover_definition.rs
index 7030e63f..b8463282 100644
--- a/crates/vespertide-lsp/tests/hover_definition.rs
+++ b/crates/vespertide-lsp/tests/hover_definition.rs
@@ -19,15 +19,7 @@ fn hover_on_fk_ref_table_previews_target_columns() {
     let post_src = r#"{"name":"post","columns":[{"name":"author_id","type":"integer","nullable":false,"foreign_key":{"ref_table":"user","ref_columns":["id"]}}]}"#;
     let post_tree = pool.parse(post_src, DocumentFormat::Json);
     let pos = post_src.find(r#""ref_table":"user""#).unwrap() + 14;
-    let hover = compute_hover(
-        post_src,
-        DocumentFormat::Json,
-        post_tree.as_ref(),
-        &idx,
-        &docs,
-        pos,
-    )
-    .unwrap();
+    let hover = compute_hover(post_src, post_tree.as_ref(), &idx, &docs, pos).unwrap();
 
     assert!(hover.markdown.contains("Target table"));
     assert!(hover.markdown.contains("columns: id"));
@@ -52,14 +44,7 @@ fn cross_file_definition_resolves() {
     let post_src = r#"{"name":"post","columns":[{"name":"author_id","type":"integer","nullable":false,"foreign_key":{"ref_table":"user","ref_columns":["id"]}}]}"#;
     let post_tree = pool.parse(post_src, DocumentFormat::Json);
     let pos = post_src.find(r#""ref_table":"user""#).unwrap() + 14;
-    let location = compute_definition(
-        post_src,
-        DocumentFormat::Json,
-        post_tree.as_ref(),
-        &idx,
-        &docs,
-        pos,
-    );
+    let location = compute_definition(post_src, post_tree.as_ref(), &idx, &docs, pos);
     assert!(location.is_some());
     assert_eq!(location.unwrap().uri, user_uri);
 }
diff --git a/crates/vespertide-lsp/tests/references.rs b/crates/vespertide-lsp/tests/references.rs
index 8744efc4..10cfbcb3 100644
--- a/crates/vespertide-lsp/tests/references.rs
+++ b/crates/vespertide-lsp/tests/references.rs
@@ -56,10 +56,8 @@ fn references_for_table_find_cross_file_ref_table_usages() {
     let pos = user_src.find(r#""name":"user""#).unwrap() + 9;
     let refs = compute_references(
         user_src,
-        DocumentFormat::Json,
         Some(&user_tree),
         &user_uri,
-        &idx,
         &docs,
         None,
         pos,
@@ -127,10 +125,8 @@ fn references_for_column_find_cross_file_ref_columns_only_for_matching_table() {
     let pos = user_src.find(r#""name":"email""#).unwrap() + 10;
     let refs = compute_references(
         user_src,
-        DocumentFormat::Json,
         Some(&user_tree),
         &user_uri,
-        &idx,
         &docs,
         None,
         pos,
@@ -188,10 +184,8 @@ fn references_include_disk_only_target_files() {
     let pos = user_src.find(r#""name":"user""#).unwrap() + 9;
     let refs = compute_references(
         user_src,
-        DocumentFormat::Json,
         Some(&user_tree),
         &user_uri,
-        &idx,
         &docs,
         Some(&disk),
         pos,
@@ -235,10 +229,8 @@ fn references_yaml_cross_file_table() {
     let pos = user_src.find("name: user").unwrap() + 6;
     let refs = compute_references(
         user_src,
-        DocumentFormat::Yaml,
         Some(&user_tree),
         &user_uri,
-        &idx,
         &docs,
         None,
         pos,
diff --git a/crates/vespertide-planner/src/drop_resolution.rs b/crates/vespertide-planner/src/drop_resolution.rs
index 326f27ea..2cf7a658 100644
--- a/crates/vespertide-planner/src/drop_resolution.rs
+++ b/crates/vespertide-planner/src/drop_resolution.rs
@@ -318,12 +318,12 @@ fn table_candidate(
     if !only_in_baseline.is_empty() {
         let mut names: Vec =
             vespertide_core::schema::names::names_to_strings(&only_in_baseline);
-        names.sort();
+        names.sort_unstable();
         differences.push(format!("removed columns: {}", names.join(", ")));
     }
     if !only_in_new.is_empty() {
         let mut names: Vec = vespertide_core::schema::names::names_to_strings(&only_in_new);
-        names.sort();
+        names.sort_unstable();
         differences.push(format!("added columns: {}", names.join(", ")));
     }
 
diff --git a/crates/vespertide-planner/src/validate/constraint_type_changes.rs b/crates/vespertide-planner/src/validate/constraint_type_changes.rs
index eef56c0f..0880aa21 100644
--- a/crates/vespertide-planner/src/validate/constraint_type_changes.rs
+++ b/crates/vespertide-planner/src/validate/constraint_type_changes.rs
@@ -175,7 +175,7 @@ struct ConstraintKey {
 
 impl ConstraintKey {
     fn new(table: &str, mut columns: Vec) -> Self {
-        columns.sort();
+        columns.sort_unstable();
         Self {
             table: table.to_string(),
             columns,
diff --git a/fuzz/fuzz_targets/fuzz_lsp_request.rs b/fuzz/fuzz_targets/fuzz_lsp_request.rs
index 1d6104c8..17f24537 100644
--- a/fuzz/fuzz_targets/fuzz_lsp_request.rs
+++ b/fuzz/fuzz_targets/fuzz_lsp_request.rs
@@ -107,10 +107,10 @@ fuzz_target!(|data: &[u8]| {
         // -------------------------------------------------------------
         // Cursor-free capabilities
         // -------------------------------------------------------------
-        let _ = compute_diagnostics(&text, format, tree.as_ref(), &index);
+        let _ = compute_diagnostics(&text, format, tree.as_ref());
         let _ = format_text(&text, format);
         let _ = compute_document_symbols(&text, tree.as_ref());
-        let _ = compute_folding_ranges(&text, tree.as_ref());
+        let _ = compute_folding_ranges(tree.as_ref());
 
         // workspace_symbols: empty query + a query derived from input.
         let _ = compute_workspace_symbols("", &docs, None);
@@ -126,67 +126,35 @@ fuzz_target!(|data: &[u8]| {
         // -------------------------------------------------------------
         let offsets = cursor_offsets(data, text_len);
         for &byte_offset in &offsets {
-            let _ = compute_hover(&text, format, tree.as_ref(), &index, &docs, byte_offset);
-            let _ = compute_definition(&text, format, tree.as_ref(), &index, &docs, byte_offset);
+            let _ = compute_hover(&text, tree.as_ref(), &index, &docs, byte_offset);
+            let _ = compute_definition(&text, tree.as_ref(), &index, &docs, byte_offset);
             let _ = compute_completion(&text, format, tree.as_ref(), &index, &docs, byte_offset);
             let _ = compute_document_highlight(&text, tree.as_ref(), byte_offset);
-            let _ = compute_selection_ranges(&text, tree.as_ref(), byte_offset);
+            let _ = compute_selection_ranges(tree.as_ref(), byte_offset);
 
             // references — both include and exclude declaration variants.
-            let _ = compute_references(
-                &text,
-                format,
-                tree.as_ref(),
-                &uri,
-                &index,
-                &docs,
-                None,
-                byte_offset,
-                true,
-            );
-            let _ = compute_references(
-                &text,
-                format,
-                tree.as_ref(),
-                &uri,
-                &index,
-                &docs,
-                None,
-                byte_offset,
-                false,
-            );
+            let _ = compute_references(&text, tree.as_ref(), &uri, &docs, None, byte_offset, true);
+            let _ = compute_references(&text, tree.as_ref(), &uri, &docs, None, byte_offset, false);
 
             // prepare_rename + rename. Use a deterministic-but-derived
             // new name so we exercise the identifier validator with
             // varying lengths instead of a constant.
-            let _ = prepare_rename(&text, format, tree.as_ref(), &uri, byte_offset);
+            let _ = prepare_rename(&text, tree.as_ref(), byte_offset);
             let new_name_seed = data
                 .iter()
                 .fold(0u32, |acc, b| acc.wrapping_add(u32::from(*b)));
             let new_name = format!("renamed_{new_name_seed:x}");
             let _ = compute_rename(
                 &text,
-                format,
                 tree.as_ref(),
                 &uri,
-                &index,
                 &docs,
                 None,
                 byte_offset,
                 &new_name,
             );
             // Empty rename — must safely return `None`, not panic.
-            let _ = compute_rename(
-                &text,
-                format,
-                tree.as_ref(),
-                &uri,
-                &index,
-                &docs,
-                None,
-                byte_offset,
-                "",
-            );
+            let _ = compute_rename(&text, tree.as_ref(), &uri, &docs, None, byte_offset, "");
         }
 
         // completion_with_workspace_tables exercises the same code path
diff --git a/tools/lsp-profile/src/workload.rs b/tools/lsp-profile/src/workload.rs
index 667c99e1..0ea8f6bb 100644
--- a/tools/lsp-profile/src/workload.rs
+++ b/tools/lsp-profile/src/workload.rs
@@ -153,7 +153,7 @@ pub fn run(scenario: &Scenario) -> Result> {
 
     let opened_uris = docs.open_uris();
     let timings = vec![
-        run_diagnostics_phase(&docs, &index, &opened_uris),
+        run_diagnostics_phase(&docs, &opened_uris),
         run_completion_phase(&docs, &index, &opened_uris),
         run_semantic_tokens_phase(&docs, &opened_uris),
         run_workspace_symbols_phase(&docs, &disk_tables),
@@ -260,7 +260,6 @@ pub fn run_realistic(scenario: &Scenario) -> Result> {
 
             record_realistic_diagnostics(
                 &docs,
-                &index,
                 &opened_uris,
                 &mut metrics,
                 did_change_started,
@@ -283,7 +282,6 @@ pub fn run_realistic(scenario: &Scenario) -> Result> {
 
 fn record_realistic_diagnostics(
     docs: &DocumentStore,
-    index: &WorkspaceIndex,
     opened_uris: &[Uri],
     metrics: &mut RealisticMetrics,
     did_change_started: Instant,
@@ -296,7 +294,7 @@ fn record_realistic_diagnostics(
         let call_started = Instant::now();
         let items = docs
             .docs_iter_for_uri(uri, |state| {
-                compute_diagnostics_shared(state.text(), state.format, state.tree.as_ref(), index)
+                compute_diagnostics_shared(state.text(), state.format, state.tree.as_ref())
                     .len()
             })
             .unwrap_or_default();
@@ -453,11 +451,7 @@ fn update_doc_and_index(
     Ok(())
 }
 
-fn run_diagnostics_phase(
-    docs: &DocumentStore,
-    index: &WorkspaceIndex,
-    opened_uris: &[Uri],
-) -> PhaseTiming {
+fn run_diagnostics_phase(docs: &DocumentStore, opened_uris: &[Uri]) -> PhaseTiming {
     let started = Instant::now();
     let mut diagnostic_count = 0;
 
@@ -465,7 +459,7 @@ fn run_diagnostics_phase(
         for uri in opened_uris {
             diagnostic_count += docs
                 .docs_iter_for_uri(uri, |state| {
-                    compute_diagnostics_shared(state.text(), state.format, state.tree.as_ref(), index)
+                    compute_diagnostics_shared(state.text(), state.format, state.tree.as_ref())
                         .len()
                 })
                 .unwrap_or_default();

From f2075e69f452d0d1c9dde6cd3bb8306bfb6ed518 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 2 Jul 2026 20:41:41 +0900
Subject: [PATCH 051/109] retry-now#0050: batch - borrowed self-contradiction
 grouping, dedupe PK/UQ column checks, order-aware F29 pairing,
 Literal::approx_eq SoT, wire-format ColumnType::display_label (5 kept)

---
 crates/vespertide-core/src/schema/column.rs   |  33 +++++
 crates/vespertide-lsp/src/drift/actions.rs    |  10 +-
 crates/vespertide-lsp/src/drift/mod.rs        |   6 +-
 .../src/validate/check_expr_parser.rs         |  25 ++++
 .../src/validate/check_self_contradiction.rs  |  20 +--
 .../src/validate/check_strengthening.rs       | 114 ++++++++----------
 .../src/validate/check_type_mismatch.rs       |  59 +++------
 .../vespertide-planner/src/validate/schema.rs |  68 ++++++-----
 8 files changed, 180 insertions(+), 155 deletions(-)

diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs
index 2de66a0e..bbcdd2c7 100644
--- a/crates/vespertide-core/src/schema/column.rs
+++ b/crates/vespertide-core/src/schema/column.rs
@@ -148,6 +148,22 @@ impl ColumnType {
         }
     }
 
+    /// Render the type in the **model-file (wire-format) spelling** — the
+    /// exact syntax users write in JSON/YAML models: `small_int`,
+    /// `varchar(32)`, `numeric(10, 2)`, `enum(status)`, `custom(TSVECTOR)`.
+    /// Use this for user-facing diagnostics that echo model syntax.
+    /// Distinct from [`Self::to_display_string`], which renders
+    /// SQL-flavoured names for CLI prompts (`smallint`, `double precision`,
+    /// `enum`), and from `Debug`, which leaks Rust internals
+    /// (`Simple(Integer)`, `Varchar { length: 32 }`).
+    #[must_use]
+    pub fn display_label(&self) -> String {
+        match self {
+            ColumnType::Simple(simple) => simple.model_name().to_string(),
+            ColumnType::Complex(complex) => complex.display_label(),
+        }
+    }
+
     /// Get the default fill value for this column type (for CLI prompts)
     /// Returns None if no sensible default exists for the type
     pub fn default_fill_value(&self) -> &'static str {
@@ -639,6 +655,23 @@ impl ComplexColumnType {
         }
     }
 
+    /// Wire-format spelling of the complex type for user-facing
+    /// diagnostics: `varchar(32)`, `char(2)`, `numeric(10, 2)`,
+    /// `custom(TSVECTOR)`, `enum(status)`. See
+    /// [`ColumnType::display_label`].
+    #[must_use]
+    pub fn display_label(&self) -> String {
+        match self {
+            ComplexColumnType::Varchar { length } => format!("varchar({length})"),
+            ComplexColumnType::Char { length } => format!("char({length})"),
+            ComplexColumnType::Numeric { precision, scale } => {
+                format!("numeric({precision}, {scale})")
+            }
+            ComplexColumnType::Custom { custom_type } => format!("custom({custom_type})"),
+            ComplexColumnType::Enum { name, .. } => format!("enum({name})"),
+        }
+    }
+
     /// Get the default fill value for this type.
     pub fn default_fill_value(&self) -> &'static str {
         match self {
diff --git a/crates/vespertide-lsp/src/drift/actions.rs b/crates/vespertide-lsp/src/drift/actions.rs
index 3a895f41..3c1d33fc 100644
--- a/crates/vespertide-lsp/src/drift/actions.rs
+++ b/crates/vespertide-lsp/src/drift/actions.rs
@@ -139,12 +139,10 @@ pub(super) fn lookup_baseline_column<'a>(
         .and_then(|table| table.columns.iter().find(|c| c.name == column_name))
 }
 
-/// Render a column type as a human-readable string.
+/// Render a column type in the model-file (wire-format) spelling,
+/// e.g. `integer`, `varchar(32)` — not the Rust `Debug` form.
 pub(super) fn render_column_type(t: &ColumnType) -> String {
-    match t {
-        ColumnType::Simple(st) => format!("{st:?}"),
-        ColumnType::Complex(ct) => format!("{ct:?}"),
-    }
+    t.display_label()
 }
 
 /// Render a default value as a human-readable string.
@@ -379,7 +377,7 @@ mod tests {
             length: 32,
         }));
 
-        assert!(rendered.contains("Varchar"));
+        assert_eq!(rendered, "varchar(32)");
     }
 
     #[test]
diff --git a/crates/vespertide-lsp/src/drift/mod.rs b/crates/vespertide-lsp/src/drift/mod.rs
index c70f89cc..ef4837a3 100644
--- a/crates/vespertide-lsp/src/drift/mod.rs
+++ b/crates/vespertide-lsp/src/drift/mod.rs
@@ -177,7 +177,7 @@ mod tests {
     #[test]
     fn render_helpers_format_correctly() {
         let int_type = ColumnType::Simple(SimpleColumnType::Integer);
-        assert!(render_column_type(&int_type).contains("Integer"));
+        assert_eq!(render_column_type(&int_type), "integer");
         assert_eq!(render_default(None), "");
         assert_eq!(render_default(Some("0")), "\"0\"");
         assert_eq!(render_nullable(true), "nullable");
@@ -245,8 +245,8 @@ mod tests {
 
         let (_, _, message) = action_to_drift(&action, &baseline, source, Some(&tree)).unwrap();
 
-        assert!(message.contains("Integer"));
-        assert!(message.contains("BigInt"));
+        assert!(message.contains("integer"));
+        assert!(message.contains("big_int"));
     }
 
     #[test]
diff --git a/crates/vespertide-planner/src/validate/check_expr_parser.rs b/crates/vespertide-planner/src/validate/check_expr_parser.rs
index a03b5bd9..7bc10012 100644
--- a/crates/vespertide-planner/src/validate/check_expr_parser.rs
+++ b/crates/vespertide-planner/src/validate/check_expr_parser.rs
@@ -152,6 +152,31 @@ impl Literal {
         }
     }
 
+    /// Approximate literal equality: numeric literals compare across the
+    /// Integer/Float divide (`1` equals `1.0`) with epsilon tolerance for
+    /// floats. Deliberately distinct from `cmp_value(..) == Some(Equal)`,
+    /// which is exact — two floats less than `f64::EPSILON` apart are
+    /// `approx_eq` but not `cmp_value`-equal. Single source of truth for
+    /// `Literal`-vs-`Literal` equality across CHECK validators (F29
+    /// strengthening today; any future caller).
+    #[expect(
+        clippy::cast_precision_loss,
+        reason = "shared CHECK literal equality: rounding integers beyond 2^53 acceptable; conservative validators silently skip ambiguous cases"
+    )]
+    #[must_use]
+    pub(super) fn approx_eq(&self, other: &Self) -> bool {
+        match (self, other) {
+            (Literal::Integer(x), Literal::Integer(y)) => x == y,
+            (Literal::Float(x), Literal::Float(y)) => (x - y).abs() < f64::EPSILON,
+            (Literal::Integer(x), Literal::Float(y)) => (*x as f64 - y).abs() < f64::EPSILON,
+            (Literal::Float(x), Literal::Integer(y)) => (x - *y as f64).abs() < f64::EPSILON,
+            (Literal::String(x), Literal::String(y)) => x == y,
+            (Literal::Bool(x), Literal::Bool(y)) => x == y,
+            (Literal::Null, Literal::Null) => true,
+            _ => false,
+        }
+    }
+
     /// Render the literal as the canonical surface form used in CHECK
     /// fault messages. `Null` renders as the SQL keyword `NULL`; every
     /// other variant renders via its natural `Display` form. Single
diff --git a/crates/vespertide-planner/src/validate/check_self_contradiction.rs b/crates/vespertide-planner/src/validate/check_self_contradiction.rs
index 87e7e2d8..0c245256 100644
--- a/crates/vespertide-planner/src/validate/check_self_contradiction.rs
+++ b/crates/vespertide-planner/src/validate/check_self_contradiction.rs
@@ -118,7 +118,7 @@ fn find_contradiction(expr: &CheckExpr) -> Option {
             // Pairwise contradiction check within the same column.
             for i in 0..preds.len() {
                 for j in (i + 1)..preds.len() {
-                    if let Some(c) = check_pair(&column, preds[i], preds[j]) {
+                    if let Some(c) = check_pair(column, preds[i], preds[j]) {
                         return Some(c);
                     }
                 }
@@ -165,13 +165,13 @@ fn flatten_and(parts: &[CheckExpr]) -> Vec<&CheckExpr> {
 /// Bucket `Compare` / `In` / `Between` / `IsNull` predicates by the
 /// column they reference. Predicates that don't directly reference a
 /// single column (And/Or/Not/Unparseable) are skipped.
-fn group_predicates_by_column<'a>(flat: &[&'a CheckExpr]) -> Vec<(String, Vec<&'a CheckExpr>)> {
-    let mut groups: Vec<(String, Vec<&'a CheckExpr>)> = Vec::new();
+fn group_predicates_by_column<'a>(flat: &[&'a CheckExpr]) -> Vec<(&'a str, Vec<&'a CheckExpr>)> {
+    let mut groups: Vec<(&'a str, Vec<&'a CheckExpr>)> = Vec::new();
     for pred in flat {
         // `if let` (not `let … else { continue; }`) so the skip path folds
         // into the loop tail — LLVM coverage mis-attributes a bare `continue`.
         if let Some(col) = predicate_column(pred) {
-            if let Some((_, existing)) = groups.iter_mut().find(|(c, _)| c == &col) {
+            if let Some((_, existing)) = groups.iter_mut().find(|(c, _)| *c == col) {
                 existing.push(pred);
             } else {
                 groups.push((col, vec![pred]));
@@ -181,12 +181,12 @@ fn group_predicates_by_column<'a>(flat: &[&'a CheckExpr]) -> Vec<(String, Vec<&'
     groups
 }
 
-fn predicate_column(expr: &CheckExpr) -> Option {
+fn predicate_column(expr: &CheckExpr) -> Option<&str> {
     match expr {
         CheckExpr::Compare { column, .. }
         | CheckExpr::In { column, .. }
         | CheckExpr::Between { column, .. }
-        | CheckExpr::IsNull { column, .. } => Some(column.clone()),
+        | CheckExpr::IsNull { column, .. } => Some(column),
         _ => None,
     }
 }
@@ -937,10 +937,10 @@ mod tests {
             column: "d".into(),
             negated: false,
         };
-        assert_eq!(predicate_column(&cmp).as_deref(), Some("a"));
-        assert_eq!(predicate_column(&in_e).as_deref(), Some("b"));
-        assert_eq!(predicate_column(&bw).as_deref(), Some("c"));
-        assert_eq!(predicate_column(&isn).as_deref(), Some("d"));
+        assert_eq!(predicate_column(&cmp), Some("a"));
+        assert_eq!(predicate_column(&in_e), Some("b"));
+        assert_eq!(predicate_column(&bw), Some("c"));
+        assert_eq!(predicate_column(&isn), Some("d"));
         // `_ => None`: And node.
         let and = CheckExpr::And(vec![cmp.clone()]);
         assert!(predicate_column(&and).is_none());
diff --git a/crates/vespertide-planner/src/validate/check_strengthening.rs b/crates/vespertide-planner/src/validate/check_strengthening.rs
index 948186b1..74f37db9 100644
--- a/crates/vespertide-planner/src/validate/check_strengthening.rs
+++ b/crates/vespertide-planner/src/validate/check_strengthening.rs
@@ -123,11 +123,21 @@ pub fn find_check_strengthenings(
     baseline: &[TableDef],
 ) -> Vec {
     let baseline_checks = build_baseline_check_map(baseline);
-    let removed_in_plan = build_removed_in_plan_map(plan);
+    // Running `(table, constraint_name) -> expr` map of Checks removed by an
+    // *earlier* action in the plan (module doc: an AddConstraint only pairs
+    // with a preceding RemoveConstraint, so an Add-before-Remove plan whose
+    // final state is "constraint removed" never warns).
+    let mut removed_in_plan: HashMap<(&str, &str), &str> = HashMap::new();
 
     let mut out = Vec::new();
     for (idx, action) in plan.actions.iter().enumerate() {
         match action {
+            MigrationAction::RemoveConstraint {
+                table,
+                constraint: TableConstraint::Check { name, expr, .. },
+            } => {
+                removed_in_plan.insert((table.as_str(), name.as_str()), expr.as_str());
+            }
             MigrationAction::ReplaceConstraint {
                 table,
                 from:
@@ -164,23 +174,23 @@ pub fn find_check_strengthenings(
                         ..
                     },
             } => {
-                let key = (table.to_string(), name.clone());
+                let key = (table.as_str(), name.as_str());
                 // Source 2: same-plan RemoveConstraint wins over baseline
                 // because it represents the user's *explicit* intent in
                 // this plan, while baseline match is inferred.
                 let old_expr_opt = removed_in_plan
                     .get(&key)
-                    .cloned()
-                    .or_else(|| baseline_checks.get(&key).cloned());
+                    .or_else(|| baseline_checks.get(&key))
+                    .copied();
                 let Some(old_expr) = old_expr_opt else {
                     continue;
                 };
-                if let Some(kind) = classify_strengthening(&old_expr, new_expr) {
+                if let Some(kind) = classify_strengthening(old_expr, new_expr) {
                     out.push(CheckStrengtheningWarning {
                         action_index: idx,
                         table: table.to_string(),
                         constraint_name: name.clone(),
-                        old_expr,
+                        old_expr: old_expr.to_string(),
                         new_expr: new_expr.clone(),
                         kind,
                     });
@@ -192,32 +202,21 @@ pub fn find_check_strengthenings(
     out
 }
 
-fn build_baseline_check_map(baseline: &[TableDef]) -> HashMap<(String, String), String> {
+/// Borrow-keyed `(table, constraint_name) -> expr` map over the baseline.
+/// Keys and values borrow the input; owned strings are produced only when
+/// a warning actually fires.
+fn build_baseline_check_map(baseline: &[TableDef]) -> HashMap<(&str, &str), &str> {
     let mut out = HashMap::new();
     for table in baseline {
         for constraint in &table.constraints {
             if let TableConstraint::Check { name, expr, .. } = constraint {
-                out.insert((table.name.to_string(), name.clone()), expr.clone());
+                out.insert((table.name.as_str(), name.as_str()), expr.as_str());
             }
         }
     }
     out
 }
 
-fn build_removed_in_plan_map(plan: &MigrationPlan) -> HashMap<(String, String), String> {
-    let mut out = HashMap::new();
-    for action in &plan.actions {
-        if let MigrationAction::RemoveConstraint {
-            table,
-            constraint: TableConstraint::Check { name, expr, .. },
-        } = action
-        {
-            out.insert((table.to_string(), name.clone()), expr.clone());
-        }
-    }
-    out
-}
-
 /// Classify whether `new_expr_str` is *demonstrably* stricter than
 /// `old_expr_str`. Returns `None` for ambiguous, equal, or
 /// non-stricter pairs.
@@ -326,7 +325,7 @@ fn compare_strictness(
     op2: Op,
     v2: &Literal,
 ) -> Option {
-    if op1 == op2 && literal_equals(v1, v2) {
+    if op1 == op2 && v1.approx_eq(v2) {
         return None; // identical
     }
     // Same operator family, tighter literal:
@@ -345,7 +344,7 @@ fn compare_strictness(
         return None;
     }
     // Boundary operator tightening with same literal:
-    if literal_equals(v1, v2) {
+    if v1.approx_eq(v2) {
         match (op1, op2) {
             (Op::Ge, Op::Gt) | (Op::Le, Op::Lt) => {
                 return Some(CheckStrengtheningKind::OperatorTightened);
@@ -368,7 +367,7 @@ fn in_is_strict_subset(subset: &[Literal], superset: &[Literal]) -> bool {
     }
     subset
         .iter()
-        .all(|s| superset.iter().any(|o| literal_equals(s, o)))
+        .all(|s| superset.iter().any(|o| s.approx_eq(o)))
 }
 
 /// True when the new BETWEEN range is strictly inside the old range:
@@ -394,23 +393,6 @@ fn between_is_narrower(
     lo_ok && hi_ok && any_strict
 }
 
-#[expect(
-    clippy::cast_precision_loss,
-    reason = "F29 strictness comparison: rounding integers beyond 2^53 acceptable; F29 silent-passes on ambiguity"
-)]
-fn literal_equals(a: &Literal, b: &Literal) -> bool {
-    match (a, b) {
-        (Literal::Integer(x), Literal::Integer(y)) => x == y,
-        (Literal::Float(x), Literal::Float(y)) => (x - y).abs() < f64::EPSILON,
-        (Literal::Integer(x), Literal::Float(y)) => (*x as f64 - y).abs() < f64::EPSILON,
-        (Literal::Float(x), Literal::Integer(y)) => (x - *y as f64).abs() < f64::EPSILON,
-        (Literal::String(x), Literal::String(y)) => x == y,
-        (Literal::Bool(x), Literal::Bool(y)) => x == y,
-        (Literal::Null, Literal::Null) => true,
-        _ => false,
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -784,6 +766,19 @@ mod tests {
         assert_eq!(warnings[0].kind, CheckStrengtheningKind::BoundaryTightened);
     }
 
+    /// Action order matters: an `AddConstraint` only pairs with an *earlier*
+    /// `RemoveConstraint` (module doc, source 2). Add-before-Remove means the
+    /// plan's final state is "constraint removed" — no strengthening warning.
+    #[test]
+    fn add_then_remove_same_name_does_not_warn() {
+        let p = plan(vec![
+            add_check("t", "chk_age", "age > 18"),
+            remove_check("t", "chk_age", "age > 0"),
+        ]);
+        let warnings = find_check_strengthenings(&p, &[]);
+        assert!(warnings.is_empty());
+    }
+
     // -- Conservative behaviour ------------------------------------------
 
     #[test]
@@ -1051,29 +1046,22 @@ mod tests {
         assert_eq!(warnings[0].kind, CheckStrengtheningKind::BoundaryTightened);
     }
 
-    /// L276-280: literal_equals — direct unit test pinning every
-    /// reachable arm including Bool/Bool (L279) and Null/Null (L280).
-    /// L297-298 `i64_to_f64` is exercised via L276-277 (Int/Float
-    /// cross-arms).
+    /// `Literal::approx_eq` (the shared epsilon literal-equality SoT in
+    /// `check_expr_parser`) — direct unit test pinning every reachable
+    /// arm including Bool/Bool, Null/Null, and the Int/Float cross-arms.
     #[test]
-    fn literal_equals_covers_all_reachable_arms() {
-        assert!(literal_equals(&Literal::Integer(5), &Literal::Integer(5)));
-        assert!(literal_equals(&Literal::Float(1.5), &Literal::Float(1.5)));
-        // Cross-numeric arms (L276-277) exercise i64_to_f64 (L297-298).
-        assert!(literal_equals(&Literal::Integer(5), &Literal::Float(5.0)));
-        assert!(literal_equals(&Literal::Float(5.0), &Literal::Integer(5)));
-        assert!(literal_equals(
-            &Literal::String("a".into()),
-            &Literal::String("a".into())
-        ));
-        assert!(literal_equals(&Literal::Bool(true), &Literal::Bool(true)));
-        assert!(literal_equals(&Literal::Null, &Literal::Null));
+    fn literal_approx_eq_covers_all_reachable_arms() {
+        assert!(Literal::Integer(5).approx_eq(&Literal::Integer(5)));
+        assert!(Literal::Float(1.5).approx_eq(&Literal::Float(1.5)));
+        // Cross-numeric arms exercise the i64 → f64 epsilon comparison.
+        assert!(Literal::Integer(5).approx_eq(&Literal::Float(5.0)));
+        assert!(Literal::Float(5.0).approx_eq(&Literal::Integer(5)));
+        assert!(Literal::String("a".into()).approx_eq(&Literal::String("a".into())));
+        assert!(Literal::Bool(true).approx_eq(&Literal::Bool(true)));
+        assert!(Literal::Null.approx_eq(&Literal::Null));
         // Mixed-type fallthrough returns false.
-        assert!(!literal_equals(
-            &Literal::Integer(1),
-            &Literal::String("1".into())
-        ));
-        assert!(!literal_equals(&Literal::Bool(true), &Literal::Null));
+        assert!(!Literal::Integer(1).approx_eq(&Literal::String("1".into())));
+        assert!(!Literal::Bool(true).approx_eq(&Literal::Null));
     }
 
     /// L249: `if subset.is_empty() { return false; }` is a defensive
diff --git a/crates/vespertide-planner/src/validate/check_type_mismatch.rs b/crates/vespertide-planner/src/validate/check_type_mismatch.rs
index fada55e4..363d43bc 100644
--- a/crates/vespertide-planner/src/validate/check_type_mismatch.rs
+++ b/crates/vespertide-planner/src/validate/check_type_mismatch.rs
@@ -132,7 +132,7 @@ fn scan_check(
     table: &str,
     check_name: &str,
     expr: &str,
-    type_map: &HashMap<(String, String), ColumnType>,
+    type_map: &HashMap<(&str, &str), &ColumnType>,
     out: &mut Vec,
 ) {
     let parsed = parse(expr);
@@ -153,7 +153,7 @@ fn walk_check_expr(
     table: &str,
     check_name: &str,
     expr_text: &str,
-    type_map: &HashMap<(String, String), ColumnType>,
+    type_map: &HashMap<(&str, &str), &ColumnType>,
     out: &mut Vec,
 ) {
     match expr_node {
@@ -238,10 +238,10 @@ fn check_one(
     column: &str,
     literal: &Literal,
     expr_text: &str,
-    type_map: &HashMap<(String, String), ColumnType>,
+    type_map: &HashMap<(&str, &str), &ColumnType>,
     out: &mut Vec,
 ) {
-    let Some(col_type) = type_map.get(&(table.to_string(), column.to_string())) else {
+    let Some(&col_type) = type_map.get(&(table, column)) else {
         return; // unknown column - other validators handle it
     };
     if !is_definitely_mismatch(col_type, literal) {
@@ -259,17 +259,17 @@ fn check_one(
     });
 }
 
-fn build_column_type_map(
-    plan: &MigrationPlan,
-    baseline: &[TableDef],
-) -> HashMap<(String, String), ColumnType> {
+/// Borrow-keyed lookup map: `(table, column) -> column type`, borrowing
+/// from `plan` + `baseline` so per-literal lookups in [`check_one`] are
+/// allocation-free. Owned strings are produced only when a warning fires.
+fn build_column_type_map<'a>(
+    plan: &'a MigrationPlan,
+    baseline: &'a [TableDef],
+) -> HashMap<(&'a str, &'a str), &'a ColumnType> {
     let mut map = HashMap::new();
     for table in baseline {
         for col in &table.columns {
-            map.insert(
-                (table.name.to_string(), col.name.to_string()),
-                col.r#type.clone(),
-            );
+            map.insert((table.name.as_str(), col.name.as_str()), &col.r#type);
         }
     }
     // Plan-added tables / columns supersede baseline so a CreateTable
@@ -278,17 +278,11 @@ fn build_column_type_map(
         match action {
             MigrationAction::CreateTable { table, columns, .. } => {
                 for col in columns {
-                    map.insert(
-                        (table.to_string(), col.name.to_string()),
-                        col.r#type.clone(),
-                    );
+                    map.insert((table.as_str(), col.name.as_str()), &col.r#type);
                 }
             }
             MigrationAction::AddColumn { table, column, .. } => {
-                map.insert(
-                    (table.to_string(), column.name.to_string()),
-                    column.r#type.clone(),
-                );
+                map.insert((table.as_str(), column.name.as_str()), &column.r#type);
             }
             MigrationAction::ModifyColumnType {
                 table,
@@ -296,7 +290,7 @@ fn build_column_type_map(
                 new_type,
                 ..
             } => {
-                map.insert((table.to_string(), column.to_string()), new_type.clone());
+                map.insert((table.as_str(), column.as_str()), new_type);
             }
             _ => {}
         }
@@ -399,25 +393,10 @@ fn is_definitely_mismatch(col_type: &ColumnType, lit: &Literal) -> bool {
 }
 
 fn column_type_label(col_type: &ColumnType) -> String {
-    match col_type {
-        // Wire-format spelling (`small_int`, not `smallint`) so the warning
-        // echoes exactly what the user wrote in the model file.
-        ColumnType::Simple(simple) => simple.model_name().to_string(),
-        ColumnType::Complex(complex) => complex_type_label(complex),
-    }
-}
-
-fn complex_type_label(complex: &ComplexColumnType) -> String {
-    match complex {
-        ComplexColumnType::Varchar { length } => format!("varchar({length})"),
-        ComplexColumnType::Char { length } => format!("char({length})"),
-        ComplexColumnType::Numeric { precision, scale } => format!("numeric({precision}, {scale})"),
-        ComplexColumnType::Custom { custom_type } => format!("custom({custom_type})"),
-        ComplexColumnType::Enum { name, .. } => format!("enum({name})"),
-        // reason: unreachable - exhaustive over current ComplexColumnType variants; fallback required only for #[non_exhaustive] future variants
-        #[cfg(not(tarpaulin_include))]
-        _ => "complex".to_string(),
-    }
+    // Wire-format spelling (`small_int`, not `smallint`) so the warning
+    // echoes exactly what the user wrote in the model file. Delegates to
+    // the shared renderer beside `SimpleColumnType::model_name` in core.
+    col_type.display_label()
 }
 
 fn literal_kind_name(lit: &Literal) -> &'static str {
diff --git a/crates/vespertide-planner/src/validate/schema.rs b/crates/vespertide-planner/src/validate/schema.rs
index 3906de36..9f0a72c4 100644
--- a/crates/vespertide-planner/src/validate/schema.rs
+++ b/crates/vespertide-planner/src/validate/schema.rs
@@ -1,7 +1,9 @@
 use std::collections::{BTreeMap, HashSet};
 
 use rayon::prelude::*;
-use vespertide_core::{TableConstraint, TableDef, schema::primary_key::PrimaryKeySyntax};
+use vespertide_core::{
+    TableConstraint, TableDef, schema::names::ColumnName, schema::primary_key::PrimaryKeySyntax,
+};
 
 use super::enums::validate_column;
 use super::foreign_keys::validate_foreign_key_constraint;
@@ -179,7 +181,7 @@ pub(super) fn validate_table(
                     return Err(PlannerError::InvalidAutoIncrement(
                         table.name.to_string(),
                         col_name.to_string(),
-                        format!("{:?}", column.r#type),
+                        column.r#type.display_label(),
                     ));
                 }
             }
@@ -197,7 +199,7 @@ pub(super) fn validate_table(
                 return Err(PlannerError::InvalidAutoIncrement(
                     table.name.to_string(),
                     column.name.to_string(),
-                    format!("{:?}", column.r#type),
+                    column.r#type.display_label(),
                 ));
             }
         }
@@ -224,38 +226,10 @@ fn validate_constraint(
 ) -> Result<(), PlannerError> {
     match constraint {
         TableConstraint::PrimaryKey { columns, .. } => {
-            if columns.is_empty() {
-                return Err(PlannerError::EmptyConstraintColumns(
-                    table_name.to_string(),
-                    "PrimaryKey".to_string(),
-                ));
-            }
-            for col in columns {
-                if !table_columns.contains(col.as_str()) {
-                    return Err(PlannerError::ConstraintColumnNotFound(
-                        table_name.to_string(),
-                        "PrimaryKey".to_string(),
-                        col.to_string(),
-                    ));
-                }
-            }
+            validate_named_constraint_columns(table_name, "PrimaryKey", columns, table_columns)?;
         }
         TableConstraint::Unique { columns, .. } => {
-            if columns.is_empty() {
-                return Err(PlannerError::EmptyConstraintColumns(
-                    table_name.to_string(),
-                    "Unique".to_string(),
-                ));
-            }
-            for col in columns {
-                if !table_columns.contains(col.as_str()) {
-                    return Err(PlannerError::ConstraintColumnNotFound(
-                        table_name.to_string(),
-                        "Unique".to_string(),
-                        col.to_string(),
-                    ));
-                }
-            }
+            validate_named_constraint_columns(table_name, "Unique", columns, table_columns)?;
         }
         TableConstraint::ForeignKey {
             columns,
@@ -298,3 +272,31 @@ fn validate_constraint(
 
     Ok(())
 }
+
+/// Shared column-membership validation for named table constraints
+/// (`PrimaryKey` / `Unique`): the column list must be non-empty and every
+/// column must exist on the table. `label` is echoed verbatim into the
+/// error variants (`"PrimaryKey"` / `"Unique"`).
+fn validate_named_constraint_columns(
+    table_name: &str,
+    label: &str,
+    columns: &[ColumnName],
+    table_columns: &HashSet<&str>,
+) -> Result<(), PlannerError> {
+    if columns.is_empty() {
+        return Err(PlannerError::EmptyConstraintColumns(
+            table_name.to_string(),
+            label.to_string(),
+        ));
+    }
+    for col in columns {
+        if !table_columns.contains(col.as_str()) {
+            return Err(PlannerError::ConstraintColumnNotFound(
+                table_name.to_string(),
+                label.to_string(),
+                col.to_string(),
+            ));
+        }
+    }
+    Ok(())
+}

From ea856b439cdee00fabe86ae5254bc4e12343e282 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 2 Jul 2026 22:25:20 +0900
Subject: [PATCH 052/109] retry-now#0051: batch - consolidate join_quoted
 across 4 ORM exporters, drop dead JPA UsedImports return, direct
 &str->newtype .into(), single-pass to_screaming_snake_case, single-buffer
 RemapEnumValues SQL (5 kept)

---
 .../src/commands/revision/emit.rs             |  2 +-
 crates/vespertide-exporter/src/jpa/mod.rs     | 12 ++----
 crates/vespertide-exporter/src/jpa/render.rs  | 12 +-----
 .../vespertide-exporter/src/python_naming.rs  | 21 +++++-----
 .../vespertide-exporter/src/seaorm/render.rs  |  6 +--
 .../src/sqlalchemy/render.rs                  | 20 ++--------
 .../src/sqlmodel/render.rs                    | 20 ++--------
 .../vespertide-exporter/src/utils/common.rs   | 40 +++++++++++++++++++
 crates/vespertide-exporter/src/utils/mod.rs   |  5 +--
 .../src/apply/column_ops.rs                   |  2 +-
 .../vespertide-planner/src/apply/table_ops.rs |  2 +-
 crates/vespertide-planner/src/diff/columns.rs | 24 +++++------
 .../src/diff/constraints.rs                   |  6 +--
 crates/vespertide-planner/src/diff/tables.rs  |  2 +-
 .../src/sql/remap_enum_values.rs              | 26 +++++++-----
 15 files changed, 100 insertions(+), 100 deletions(-)
 create mode 100644 crates/vespertide-exporter/src/utils/common.rs

diff --git a/crates/vespertide-cli/src/commands/revision/emit.rs b/crates/vespertide-cli/src/commands/revision/emit.rs
index 4f8c287d..8a705af0 100644
--- a/crates/vespertide-cli/src/commands/revision/emit.rs
+++ b/crates/vespertide-cli/src/commands/revision/emit.rs
@@ -222,7 +222,7 @@ pub(super) fn rewrite_plan_for_recreation(
             .find(|m| m.name.as_str() == *table_name)
         {
             plan.actions.push(MigrationAction::DeleteTable {
-                table: table_name.to_string().into(),
+                table: (*table_name).into(),
             });
             plan.actions.push(MigrationAction::CreateTable {
                 table: model.name.clone(),
diff --git a/crates/vespertide-exporter/src/jpa/mod.rs b/crates/vespertide-exporter/src/jpa/mod.rs
index c441fceb..5f992e1b 100644
--- a/crates/vespertide-exporter/src/jpa/mod.rs
+++ b/crates/vespertide-exporter/src/jpa/mod.rs
@@ -6,8 +6,6 @@ use crate::parallel_config::{JPA_EXPORT_PAR_TABLE_MIN_LEN, JPA_EXPORT_PAR_TABLE_
 use rayon::prelude::*;
 use vespertide_core::TableDef;
 
-use self::types::UsedImports;
-
 pub struct JpaExporter;
 
 impl OrmExporter for JpaExporter {
@@ -36,21 +34,17 @@ pub fn render_entities(schema: &[TableDef]) -> Result, String> {
     let rendered = if schema.len() < JPA_EXPORT_PAR_TABLE_THRESHOLD {
         schema
             .iter()
-            .map(render::render_entity_with_imports)
+            .map(render::render_entity_inner)
             .collect::>()
     } else {
         schema
             .par_iter()
             .with_min_len(JPA_EXPORT_PAR_TABLE_MIN_LEN)
-            .map(render::render_entity_with_imports)
+            .map(render::render_entity_inner)
             .collect::>()
     };
 
-    Ok(merge_rendered_entities(rendered))
-}
-
-fn merge_rendered_entities(rendered: Vec<(String, UsedImports)>) -> Vec {
-    rendered.into_iter().map(|(entity, _)| entity).collect()
+    Ok(rendered)
 }
 
 #[cfg(test)]
diff --git a/crates/vespertide-exporter/src/jpa/render.rs b/crates/vespertide-exporter/src/jpa/render.rs
index 5f1b433b..9435d464 100644
--- a/crates/vespertide-exporter/src/jpa/render.rs
+++ b/crates/vespertide-exporter/src/jpa/render.rs
@@ -10,10 +10,6 @@ use vespertide_core::{ColumnDef, TableDef};
 use crate::jpa::types::{UsedImports, java_type_for_column};
 
 pub(super) fn render_entity_inner(table: &TableDef) -> String {
-    render_entity_with_imports(table).0
-}
-
-pub(super) fn render_entity_with_imports(table: &TableDef) -> (String, UsedImports) {
     let mut lines: Vec = Vec::new();
 
     // Collect enums for this table
@@ -114,7 +110,7 @@ pub(super) fn render_entity_with_imports(table: &TableDef) -> (String, UsedImpor
     lines.push("}".into());
     lines.push(String::new());
 
-    (lines.join("\n"), used_imports)
+    lines.join("\n")
 }
 
 // ---------------------------------------------------------------------------
@@ -215,11 +211,7 @@ fn render_table_annotation(
     if !unique_constraints.is_empty() {
         annotation.push_str(", uniqueConstraints = {\n");
         for (i, (name, columns)) in unique_constraints.iter().enumerate() {
-            let cols = columns
-                .iter()
-                .map(|c| format!("\"{c}\""))
-                .collect::>()
-                .join(", ");
+            let cols = crate::utils::common::join_quoted(columns);
             let comma = if i < unique_constraints.len() - 1 {
                 ","
             } else {
diff --git a/crates/vespertide-exporter/src/python_naming.rs b/crates/vespertide-exporter/src/python_naming.rs
index 0f77980b..9bb33e74 100644
--- a/crates/vespertide-exporter/src/python_naming.rs
+++ b/crates/vespertide-exporter/src/python_naming.rs
@@ -31,22 +31,19 @@ pub fn to_pascal_case(s: &str) -> String {
 /// non-alphanumeric character with `_` so the result is safe as a Python
 /// `enum.Enum` member name.
 pub(crate) fn to_screaming_snake_case(s: &str) -> String {
-    let mut result = String::new();
+    let mut result = String::with_capacity(s.len() + 4);
     for (i, ch) in s.chars().enumerate() {
         if ch.is_uppercase() && i > 0 {
             result.push('_');
         }
-        result.push(ch.to_ascii_uppercase());
+        let upper = ch.to_ascii_uppercase();
+        // Sanitise in the same pass: any non-alphanumeric becomes `_` so the
+        // result is safe as a Python `enum.Enum` member name.
+        if upper.is_alphanumeric() || upper == '_' {
+            result.push(upper);
+        } else {
+            result.push('_');
+        }
     }
-    // Replace any non-alphanumeric with underscore
     result
-        .chars()
-        .map(|c| {
-            if c.is_alphanumeric() || c == '_' {
-                c
-            } else {
-                '_'
-            }
-        })
-        .collect()
 }
diff --git a/crates/vespertide-exporter/src/seaorm/render.rs b/crates/vespertide-exporter/src/seaorm/render.rs
index 2b6fff46..23071bc5 100644
--- a/crates/vespertide-exporter/src/seaorm/render.rs
+++ b/crates/vespertide-exporter/src/seaorm/render.rs
@@ -305,11 +305,7 @@ pub(super) fn render_indexes_and_uniques(lines: &mut Vec, constraints: &
         );
         lines.push("pub const COMPOSITE_UNIQUES: &[&[&str]] = &[".into());
         for (name, columns) in composite_uniques {
-            let cols_str = columns
-                .iter()
-                .map(|c| format!("\"{c}\""))
-                .collect::>()
-                .join(", ");
+            let cols_str = crate::utils::common::join_quoted(columns);
             let comment = name
                 .as_deref()
                 .map(|n| format!(" // {n}"))
diff --git a/crates/vespertide-exporter/src/sqlalchemy/render.rs b/crates/vespertide-exporter/src/sqlalchemy/render.rs
index 1df9593d..13e4105b 100644
--- a/crates/vespertide-exporter/src/sqlalchemy/render.rs
+++ b/crates/vespertide-exporter/src/sqlalchemy/render.rs
@@ -5,6 +5,7 @@ use super::types::{UsedTypes, column_type_to_python, column_type_to_sqlalchemy};
 use crate::parallel_config::{
     PYTHON_EXPORT_PAR_TABLE_MIN_LEN, SQLALCHEMY_EXPORT_PAR_TABLE_THRESHOLD,
 };
+use crate::utils::common::join_quoted;
 use crate::utils::python::collect_composite_fks;
 use rayon::prelude::*;
 use vespertide_core::schema::column::{ColumnType, ComplexColumnType, EnumValues};
@@ -209,11 +210,7 @@ fn render_entity_part(table: &TableDef, used_types: &mut UsedTypes<'static>) ->
         lines.push("    __table_args__ = (".into());
 
         for (name, columns) in &indexes {
-            let cols_str = columns
-                .iter()
-                .map(|c| format!("\"{c}\""))
-                .collect::>()
-                .join(", ");
+            let cols_str = join_quoted(columns);
             if let Some(idx_name) = name {
                 lines.push(format!("        Index(\"{idx_name}\", {cols_str}),"));
             } else {
@@ -222,11 +219,7 @@ fn render_entity_part(table: &TableDef, used_types: &mut UsedTypes<'static>) ->
         }
 
         for (name, columns) in &composite_uniques {
-            let cols_str = columns
-                .iter()
-                .map(|c| format!("\"{c}\""))
-                .collect::>()
-                .join(", ");
+            let cols_str = join_quoted(columns);
             if let Some(uq_name) = name {
                 lines.push(format!(
                     "        UniqueConstraint({cols_str}, name=\"{uq_name}\"),"
@@ -237,12 +230,7 @@ fn render_entity_part(table: &TableDef, used_types: &mut UsedTypes<'static>) ->
         }
 
         for fk in &composite_fks {
-            let local_cols = fk
-                .local_cols
-                .iter()
-                .map(|col| format!("\"{col}\""))
-                .collect::>()
-                .join(", ");
+            let local_cols = join_quoted(&fk.local_cols);
             let ref_cols = fk
                 .ref_cols
                 .iter()
diff --git a/crates/vespertide-exporter/src/sqlmodel/render.rs b/crates/vespertide-exporter/src/sqlmodel/render.rs
index 06d2fb94..7f1a84e9 100644
--- a/crates/vespertide-exporter/src/sqlmodel/render.rs
+++ b/crates/vespertide-exporter/src/sqlmodel/render.rs
@@ -3,6 +3,7 @@ use rayon::prelude::*;
 use crate::parallel_config::{
     PYTHON_EXPORT_PAR_TABLE_MIN_LEN, SQLMODEL_EXPORT_PAR_TABLE_THRESHOLD,
 };
+use crate::utils::common::join_quoted;
 use crate::utils::python::{CompositeFk, collect_composite_fks};
 use vespertide_core::schema::column::{ColumnType, ComplexColumnType, EnumValues};
 use vespertide_core::schema::constraint::TableConstraint;
@@ -342,11 +343,7 @@ fn render_entity_body(table: &TableDef, composite_fks: &[CompositeFk<'_>]) -> Ve
         lines.push("    __table_args__ = (".into());
 
         for (name, columns) in &composite_indexes {
-            let cols_str = columns
-                .iter()
-                .map(|c| format!("\"{c}\""))
-                .collect::>()
-                .join(", ");
+            let cols_str = join_quoted(columns);
             if let Some(idx_name) = name {
                 lines.push(format!("        Index(\"{idx_name}\", {cols_str}),"));
             } else {
@@ -355,11 +352,7 @@ fn render_entity_body(table: &TableDef, composite_fks: &[CompositeFk<'_>]) -> Ve
         }
 
         for (name, columns) in &composite_uniques {
-            let cols_str = columns
-                .iter()
-                .map(|c| format!("\"{c}\""))
-                .collect::>()
-                .join(", ");
+            let cols_str = join_quoted(columns);
             if let Some(uq_name) = name {
                 lines.push(format!(
                     "        UniqueConstraint({cols_str}, name=\"{uq_name}\"),"
@@ -370,12 +363,7 @@ fn render_entity_body(table: &TableDef, composite_fks: &[CompositeFk<'_>]) -> Ve
         }
 
         for fk in composite_fks {
-            let local_cols = fk
-                .local_cols
-                .iter()
-                .map(|col| format!("\"{col}\""))
-                .collect::>()
-                .join(", ");
+            let local_cols = join_quoted(&fk.local_cols);
             let ref_cols = fk
                 .ref_cols
                 .iter()
diff --git a/crates/vespertide-exporter/src/utils/common.rs b/crates/vespertide-exporter/src/utils/common.rs
new file mode 100644
index 00000000..2b17540c
--- /dev/null
+++ b/crates/vespertide-exporter/src/utils/common.rs
@@ -0,0 +1,40 @@
+//! Cross-language helpers shared by every ORM exporter backend.
+
+/// Join items as a double-quoted, comma-separated list: `"a", "b", "c"`.
+///
+/// Consolidates the quoted-comma-join pattern previously copy-pasted across
+/// the JPA, `SeaORM`, `SQLAlchemy` and `SQLModel` renderers, and builds the
+/// result in a single buffer instead of collecting an intermediate
+/// `Vec` per call site.
+pub(crate) fn join_quoted>(items: &[T]) -> String {
+    let mut out = String::new();
+    for item in items {
+        if !out.is_empty() {
+            out.push_str(", ");
+        }
+        out.push('"');
+        out.push_str(item.as_ref());
+        out.push('"');
+    }
+    out
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn empty_slice_yields_empty_string() {
+        assert_eq!(join_quoted::<&str>(&[]), "");
+    }
+
+    #[test]
+    fn single_item_is_quoted_without_separator() {
+        assert_eq!(join_quoted(&["id"]), "\"id\"");
+    }
+
+    #[test]
+    fn multiple_items_are_comma_separated() {
+        assert_eq!(join_quoted(&["a", "b", "c"]), "\"a\", \"b\", \"c\"");
+    }
+}
diff --git a/crates/vespertide-exporter/src/utils/mod.rs b/crates/vespertide-exporter/src/utils/mod.rs
index 58d01185..a5d73293 100644
--- a/crates/vespertide-exporter/src/utils/mod.rs
+++ b/crates/vespertide-exporter/src/utils/mod.rs
@@ -6,12 +6,11 @@
 //! without polluting the crate root with a flat list of `*_common.rs` files.
 //!
 //! Cross-language helpers (case conversion, naming sanitization that is
-//! language-agnostic) belong in `crate::utils::common` once they exist;
-//! for now keep them in their language submodule until a second user appears.
+//! language-agnostic) belong in `crate::utils::common`.
 
+pub(crate) mod common;
 pub(crate) mod python;
 // Add future language helpers as siblings, e.g.
 // pub(crate) mod rust;
 // pub(crate) mod typescript;
 // pub(crate) mod java;
-// pub(crate) mod common;
diff --git a/crates/vespertide-planner/src/apply/column_ops.rs b/crates/vespertide-planner/src/apply/column_ops.rs
index bc5a54d3..9ef50393 100644
--- a/crates/vespertide-planner/src/apply/column_ops.rs
+++ b/crates/vespertide-planner/src/apply/column_ops.rs
@@ -26,7 +26,7 @@ pub(super) fn add_column(
         let table_to_normalize = std::mem::replace(
             tbl,
             TableDef {
-                name: table.to_string().into(),
+                name: table.into(),
                 description: None,
                 columns: Vec::new(),
                 constraints: Vec::new(),
diff --git a/crates/vespertide-planner/src/apply/table_ops.rs b/crates/vespertide-planner/src/apply/table_ops.rs
index 28ff9618..74892092 100644
--- a/crates/vespertide-planner/src/apply/table_ops.rs
+++ b/crates/vespertide-planner/src/apply/table_ops.rs
@@ -14,7 +14,7 @@ pub(super) fn create_table(
     }
 
     let table_def = TableDef {
-        name: table.to_string().into(),
+        name: table.into(),
         description: None,
         columns: columns.to_vec(),
         constraints: constraints.to_vec(),
diff --git a/crates/vespertide-planner/src/diff/columns.rs b/crates/vespertide-planner/src/diff/columns.rs
index 990bf037..0357ac79 100644
--- a/crates/vespertide-planner/src/diff/columns.rs
+++ b/crates/vespertide-planner/src/diff/columns.rs
@@ -58,8 +58,8 @@ fn diff_integer_enum_remappings(
             continue;
         }
         actions.push(MigrationAction::RemapEnumValues {
-            table: table_name.to_string().into(),
-            column: (*col).to_string().into(),
+            table: table_name.into(),
+            column: (*col).into(),
             mapping,
         });
     }
@@ -116,7 +116,7 @@ fn diff_deleted_columns(
 
     for col in &deleted_columns {
         actions.push(MigrationAction::DeleteColumn {
-            table: table_name.to_string().into(),
+            table: table_name.into(),
             column: col.clone().into(),
         });
     }
@@ -152,8 +152,8 @@ fn diff_column_types(
 
             if needs_type_migration || needs_enum_rename {
                 actions.push(MigrationAction::ModifyColumnType {
-                    table: table_name.to_string().into(),
-                    column: (*col).to_string().into(),
+                    table: table_name.into(),
+                    column: (*col).into(),
                     new_type: to_def.r#type.clone(),
                     fill_with: None,
                     // Set by `cmd_revision` after the user picks a strategy
@@ -179,8 +179,8 @@ fn diff_column_nullability(
             && from_def.nullable != to_def.nullable
         {
             actions.push(MigrationAction::ModifyColumnNullable {
-                table: table_name.to_string().into(),
-                column: (*col).to_string().into(),
+                table: table_name.into(),
+                column: (*col).into(),
                 nullable: to_def.nullable,
                 fill_with: None,
                 delete_null_rows: None,
@@ -207,8 +207,8 @@ fn diff_column_defaults(
                 .map(vespertide_core::DefaultValue::to_sql);
             if from_default != to_default {
                 actions.push(MigrationAction::ModifyColumnDefault {
-                    table: table_name.to_string().into(),
-                    column: (*col).to_string().into(),
+                    table: table_name.into(),
+                    column: (*col).into(),
                     new_default: to_default,
                     backfill: None,
                 });
@@ -228,8 +228,8 @@ fn diff_column_comments(
             && from_def.comment != to_def.comment
         {
             actions.push(MigrationAction::ModifyColumnComment {
-                table: table_name.to_string().into(),
-                column: (*col).to_string().into(),
+                table: table_name.into(),
+                column: (*col).into(),
                 new_comment: to_def.comment.clone(),
             });
         }
@@ -250,7 +250,7 @@ fn diff_added_columns(
             col_def.index = None;
             col_def.foreign_key = None;
             actions.push(MigrationAction::AddColumn {
-                table: table_name.to_string().into(),
+                table: table_name.into(),
                 column: Box::new(col_def),
                 fill_with: None,
             });
diff --git a/crates/vespertide-planner/src/diff/constraints.rs b/crates/vespertide-planner/src/diff/constraints.rs
index 42b33a54..d7733f1e 100644
--- a/crates/vespertide-planner/src/diff/constraints.rs
+++ b/crates/vespertide-planner/src/diff/constraints.rs
@@ -116,7 +116,7 @@ fn diff_replaced_constraints(
             replaced_from.push(fi);
             replaced_to.push(ti);
             actions.push(MigrationAction::ReplaceConstraint {
-                table: table_name.to_string().into(),
+                table: table_name.into(),
                 from: from_constraint.clone(),
                 to: to_tbl.constraints[ti].clone(),
             });
@@ -144,7 +144,7 @@ fn diff_removed_constraints(
 
         if !all_columns_deleted {
             actions.push(MigrationAction::RemoveConstraint {
-                table: table_name.to_string().into(),
+                table: table_name.into(),
                 constraint: from_constraint.clone(),
             });
         }
@@ -163,7 +163,7 @@ fn diff_added_constraints(
             continue;
         }
         actions.push(MigrationAction::AddConstraint {
-            table: table_name.to_string().into(),
+            table: table_name.into(),
             constraint: to_constraint.clone(),
         });
     }
diff --git a/crates/vespertide-planner/src/diff/tables.rs b/crates/vespertide-planner/src/diff/tables.rs
index 7292c39c..ca3572ad 100644
--- a/crates/vespertide-planner/src/diff/tables.rs
+++ b/crates/vespertide-planner/src/diff/tables.rs
@@ -28,7 +28,7 @@ pub(super) fn diff_deleted_tables(
     for name in from_map.keys() {
         if !to_map.contains_key(name) {
             actions.push(MigrationAction::DeleteTable {
-                table: (*name).to_string().into(),
+                table: (*name).into(),
             });
         }
     }
diff --git a/crates/vespertide-query/src/sql/remap_enum_values.rs b/crates/vespertide-query/src/sql/remap_enum_values.rs
index 7c90b1f7..4441763e 100644
--- a/crates/vespertide-query/src/sql/remap_enum_values.rs
+++ b/crates/vespertide-query/src/sql/remap_enum_values.rs
@@ -12,6 +12,7 @@
 //! `CASE WHEN` and `IN (...)` are universal.
 
 use std::collections::BTreeMap;
+use std::fmt::Write as _;
 
 use super::helpers::quote_ident;
 use super::types::{BuiltQuery, DatabaseBackend, RawSql};
@@ -32,16 +33,21 @@ pub fn build_remap_enum_values(
     }
     let qt = quote_ident(table, backend);
     let qc = quote_ident(column, backend);
-    let case_arms: Vec = mapping
-        .iter()
-        .map(|(old, new)| format!("WHEN {qc} = {old} THEN {new}"))
-        .collect();
-    let in_list: Vec = mapping.keys().map(i64::to_string).collect();
-    let sql = format!(
-        "UPDATE {qt} SET {qc} = CASE {arms} END WHERE {qc} IN ({in_list})",
-        arms = case_arms.join(" "),
-        in_list = in_list.join(", "),
-    );
+    // Write the CASE arms and the IN list into two reusable buffers in a
+    // single pass over `mapping` — no intermediate `Vec` per pair.
+    let mut arms = String::new();
+    let mut in_list = String::new();
+    for (old, new) in mapping {
+        if !arms.is_empty() {
+            arms.push(' ');
+        }
+        let _ = write!(arms, "WHEN {qc} = {old} THEN {new}");
+        if !in_list.is_empty() {
+            in_list.push_str(", ");
+        }
+        let _ = write!(in_list, "{old}");
+    }
+    let sql = format!("UPDATE {qt} SET {qc} = CASE {arms} END WHERE {qc} IN ({in_list})");
     Ok(vec![BuiltQuery::Raw(RawSql::uniform(sql))])
 }
 

From 4aa564ebadd848b32eeb7956942a7488c79c2bac Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 2 Jul 2026 22:52:52 +0900
Subject: [PATCH 053/109] retry-now#0052: batch - dedupe loader ext filter,
 shared Kahn core, fold parser lock arms, fix fingerprint expect msgs, drop
 dead cli models (5 kept)

---
 crates/vespertide-loader/src/lib.rs           |  10 ++
 crates/vespertide-loader/src/migrations.rs    |  15 +-
 crates/vespertide-loader/src/models.rs        |  15 +-
 crates/vespertide-lsp/src/cache.rs            |   8 +-
 crates/vespertide-lsp/src/parser.rs           |  22 ++-
 .../vespertide-planner/src/diff/ordering.rs   | 153 +++++++-----------
 6 files changed, 90 insertions(+), 133 deletions(-)

diff --git a/crates/vespertide-loader/src/lib.rs b/crates/vespertide-loader/src/lib.rs
index 0ea8b768..1b58f21f 100644
--- a/crates/vespertide-loader/src/lib.rs
+++ b/crates/vespertide-loader/src/lib.rs
@@ -22,5 +22,15 @@ mod parallel_config;
 mod test_support;
 
 pub use config::{load_config, load_config_from_path, load_config_or_default};
+
+/// True when the path has a supported model/migration file extension
+/// (`.json`, `.yaml`, `.yml`). Single source of truth for the loader's
+/// supported-extension set.
+pub(crate) fn has_supported_extension(path: &std::path::Path) -> bool {
+    matches!(
+        path.extension().and_then(|s| s.to_str()),
+        Some("json" | "yaml" | "yml")
+    )
+}
 pub use migrations::{load_migrations, load_migrations_at_compile_time, load_migrations_from_dir};
 pub use models::{load_models, load_models_at_compile_time, load_models_from_dir};
diff --git a/crates/vespertide-loader/src/migrations.rs b/crates/vespertide-loader/src/migrations.rs
index 712c498a..115ee1c3 100644
--- a/crates/vespertide-loader/src/migrations.rs
+++ b/crates/vespertide-loader/src/migrations.rs
@@ -52,7 +52,7 @@ pub fn load_migrations_from_dir(
         return Ok(Vec::new());
     }
 
-    let paths = collect_migration_paths_internal(&migrations_dir)?;
+    let paths = collect_migration_paths(&migrations_dir).map_err(|e| e.to_string())?;
     let results = map_paths_with_threshold(&paths, load_migration_file_internal);
 
     let mut plans = Vec::with_capacity(results.len());
@@ -72,7 +72,7 @@ fn collect_migration_paths(dir: &Path) -> Result> {
     for entry in entries {
         let entry = entry.context("read directory entry")?;
         let path = entry.path();
-        if path.is_file() && has_migration_extension(&path) {
+        if path.is_file() && crate::has_supported_extension(&path) {
             paths.push(path);
         }
     }
@@ -80,17 +80,6 @@ fn collect_migration_paths(dir: &Path) -> Result> {
     Ok(paths)
 }
 
-fn collect_migration_paths_internal(dir: &Path) -> Result, String> {
-    collect_migration_paths(dir).map_err(|e| e.to_string())
-}
-
-fn has_migration_extension(path: &Path) -> bool {
-    matches!(
-        path.extension().and_then(|s| s.to_str()),
-        Some("json" | "yaml" | "yml")
-    )
-}
-
 fn load_migration_file(path: &Path) -> Result {
     let ext = path.extension().and_then(|s| s.to_str());
     let content = fs::read_to_string(path)
diff --git a/crates/vespertide-loader/src/models.rs b/crates/vespertide-loader/src/models.rs
index 66b5aeb2..32c65ca9 100644
--- a/crates/vespertide-loader/src/models.rs
+++ b/crates/vespertide-loader/src/models.rs
@@ -59,7 +59,7 @@ fn collect_model_paths(dir: &Path) -> Result> {
 
         if path.is_dir() {
             paths.extend(collect_model_paths(&path)?);
-        } else if path.is_file() && has_model_extension(&path) {
+        } else if path.is_file() && crate::has_supported_extension(&path) {
             paths.push(path);
         }
     }
@@ -67,13 +67,6 @@ fn collect_model_paths(dir: &Path) -> Result> {
     Ok(paths)
 }
 
-fn has_model_extension(path: &Path) -> bool {
-    matches!(
-        path.extension().and_then(|s| s.to_str()),
-        Some("json" | "yaml" | "yml")
-    )
-}
-
 fn load_model_file(path: &Path) -> Result {
     let ext = path.extension().and_then(|s| s.to_str());
     let content =
@@ -132,7 +125,7 @@ fn load_models_recursive_internal(
     dir: &Path,
     tables: &mut Vec,
 ) -> Result<(), Box> {
-    let paths = collect_model_paths_internal(dir)?;
+    let paths = collect_model_paths(dir).map_err(|e| e.to_string())?;
     let results = map_paths_with_threshold(&paths, load_normalized_model_file_internal);
 
     for result in results {
@@ -142,10 +135,6 @@ fn load_models_recursive_internal(
     Ok(())
 }
 
-fn collect_model_paths_internal(dir: &Path) -> Result, String> {
-    collect_model_paths(dir).map_err(|e| e.to_string())
-}
-
 fn load_normalized_model_file_internal(path: &Path) -> Result {
     let ext = path.extension().and_then(|s| s.to_str());
     let content = fs::read_to_string(path)
diff --git a/crates/vespertide-lsp/src/cache.rs b/crates/vespertide-lsp/src/cache.rs
index 17846242..6b5e38cb 100644
--- a/crates/vespertide-lsp/src/cache.rs
+++ b/crates/vespertide-lsp/src/cache.rs
@@ -168,7 +168,9 @@ pub(crate) fn docstore_fingerprint(docs: &DocumentStore) -> u64 {
     let (fingerprint, entries) = compute_docstore_fingerprint(docs);
     *docstore_fingerprint_cache()
         .lock()
-        .expect("RingCache lock poisoned — invariant: compute_fn must not panic") =
+        .expect(
+            "docstore fingerprint cache lock poisoned — invariant: fingerprint computation must not panic",
+        ) =
         Some(CachedDocstoreFingerprint {
             docs_addr,
             len: entries.len(),
@@ -181,7 +183,9 @@ pub(crate) fn docstore_fingerprint(docs: &DocumentStore) -> u64 {
 fn cached_docstore_fingerprint(docs: &DocumentStore, docs_addr: usize) -> Option {
     let cache = docstore_fingerprint_cache()
         .lock()
-        .expect("RingCache lock poisoned — invariant: compute_fn must not panic");
+        .expect(
+            "docstore fingerprint cache lock poisoned — invariant: fingerprint computation must not panic",
+        );
     let cached = cache.as_ref()?;
     if cached.docs_addr != docs_addr || cached.len != docs.len() {
         return None;
diff --git a/crates/vespertide-lsp/src/parser.rs b/crates/vespertide-lsp/src/parser.rs
index 82979652..3427ee48 100644
--- a/crates/vespertide-lsp/src/parser.rs
+++ b/crates/vespertide-lsp/src/parser.rs
@@ -69,18 +69,16 @@ impl ParserPool {
     /// V2 may use `Tree::edit` + incremental reparse if profiling demands.
     #[must_use]
     pub fn parse(&self, text: &str, format: DocumentFormat) -> Option {
-        match format {
-            DocumentFormat::Json => self
-                .json
-                .lock()
-                .expect("ParserPool lock poisoned — invariant: tree_sitter::Parser::parse must not panic")
-                .parse(text, None),
-            DocumentFormat::Yaml => self
-                .yaml
-                .lock()
-                .expect("ParserPool lock poisoned — invariant: tree_sitter::Parser::parse must not panic")
-                .parse(text, None),
-        }
+        let parser = match format {
+            DocumentFormat::Json => &self.json,
+            DocumentFormat::Yaml => &self.yaml,
+        };
+        parser
+            .lock()
+            .expect(
+                "ParserPool lock poisoned — invariant: tree_sitter::Parser::parse must not panic",
+            )
+            .parse(text, None)
     }
 }
 
diff --git a/crates/vespertide-planner/src/diff/ordering.rs b/crates/vespertide-planner/src/diff/ordering.rs
index d3886966..a85874cd 100644
--- a/crates/vespertide-planner/src/diff/ordering.rs
+++ b/crates/vespertide-planner/src/diff/ordering.rs
@@ -6,13 +6,58 @@ use vespertide_core::{
 
 use crate::error::PlannerError;
 
+/// Kahn's-algorithm core shared by creation and deletion ordering.
+///
+/// `dependencies` maps each name to the set of names it depends on and must
+/// contain an entry for EVERY name to order. Returns names in ready order —
+/// deterministic because the zero-degree seed follows `BTreeMap` key order and
+/// each step collects newly-ready names into a `BTreeSet`. On a cycle the
+/// result is partial (cyclic names are absent); callers decide how to react.
+fn kahn_ready_order<'a>(dependencies: &BTreeMap<&'a str, BTreeSet<&'a str>>) -> Vec<&'a str> {
+    // SEQUENTIAL BY NATURE: Kahn's algorithm requires in-degree state evolution.
+    let mut in_degree: BTreeMap<&'a str, usize> = dependencies
+        .iter()
+        .map(|(name, deps)| (*name, deps.len()))
+        .collect();
+
+    // Start with names that have no dependencies.
+    // BTreeMap iteration is already sorted by key.
+    let mut queue: VecDeque<&'a str> = in_degree
+        .iter()
+        .filter(|(_, deg)| **deg == 0)
+        .map(|(name, _)| *name)
+        .collect();
+
+    let mut order: Vec<&'a str> = Vec::with_capacity(dependencies.len());
+    while let Some(name) = queue.pop_front() {
+        order.push(name);
+
+        // Collect names that become ready (in-degree becomes 0).
+        // Use BTreeSet for consistent ordering.
+        let mut ready: BTreeSet<&'a str> = BTreeSet::new();
+        for (dependent, deps) in dependencies {
+            if deps.contains(&name)
+                && let Some(degree) = in_degree.get_mut(dependent)
+            {
+                *degree -= 1;
+                if *degree == 0 {
+                    ready.insert(dependent);
+                }
+            }
+        }
+        for t in ready {
+            queue.push_back(t);
+        }
+    }
+    order
+}
+
 /// Topologically sort tables based on foreign key dependencies.
 /// Returns tables in order where tables with no FK dependencies come first,
 /// and tables that reference other tables come after their referenced tables.
 pub(super) fn topological_sort_tables<'a>(
     tables: &[&'a TableDef],
 ) -> Result, PlannerError> {
-    // SEQUENTIAL BY NATURE: Kahn's algorithm requires in-degree state evolution.
     if tables.is_empty() {
         return Ok(vec![]);
     }
@@ -37,54 +82,14 @@ pub(super) fn topological_sort_tables<'a>(
         dependencies.insert(table.name.as_str(), deps_set);
     }
 
-    // Kahn's algorithm for topological sort
-    // Calculate in-degrees (number of tables that depend on each table)
-    // Use BTreeMap for consistent ordering
-    let mut in_degree: BTreeMap<&str, usize> = BTreeMap::new();
-    for table in tables {
-        in_degree.entry(table.name.as_str()).or_insert(0);
-    }
-
-    // For each dependency, increment the in-degree of the dependent table.
-    // (If A depends on B, A's in-degree rises; A is processed after B.)
-    for (table_name, deps) in &dependencies {
-        *in_degree.entry(table_name).or_insert(0) += deps.len();
-    }
-
-    // Start with tables that have no dependencies
-    // BTreeMap iteration is already sorted by key
-    let mut queue: VecDeque<&str> = in_degree
-        .iter()
-        .filter(|(_, deg)| **deg == 0)
-        .map(|(name, _)| *name)
-        .collect();
-
-    let mut result: Vec<&TableDef> = Vec::with_capacity(tables.len());
+    // Kahn's algorithm for topological sort (shared core), then map the
+    // ready-ordered names back to their table definitions.
     let table_map: BTreeMap<&str, &TableDef> =
         tables.iter().map(|t| (t.name.as_str(), *t)).collect();
-
-    while let Some(table_name) = queue.pop_front() {
-        if let Some(&table) = table_map.get(table_name) {
-            result.push(table);
-        }
-
-        // Collect tables that become ready (in-degree becomes 0)
-        // Use BTreeSet for consistent ordering
-        let mut ready_tables: BTreeSet<&str> = BTreeSet::new();
-        for (dependent, deps) in &dependencies {
-            if deps.contains(&table_name)
-                && let Some(degree) = in_degree.get_mut(dependent)
-            {
-                *degree -= 1;
-                if *degree == 0 {
-                    ready_tables.insert(dependent);
-                }
-            }
-        }
-        for t in ready_tables {
-            queue.push_back(t);
-        }
-    }
+    let result: Vec<&TableDef> = kahn_ready_order(&dependencies)
+        .into_iter()
+        .filter_map(|name| table_map.get(name).copied())
+        .collect();
 
     // Check for cycles
     if result.len() != tables.len() {
@@ -116,7 +121,6 @@ pub(super) fn sort_delete_tables(
     actions: &mut [MigrationAction],
     all_tables: &BTreeMap<&str, &TableDef>,
 ) {
-    // SEQUENTIAL BY NATURE: Kahn's algorithm requires in-degree state evolution.
     // Collect DeleteTable actions and their indices
     let delete_indices: Vec = actions
         .iter()
@@ -161,48 +165,9 @@ pub(super) fn sort_delete_tables(
         dependencies.insert(table_name, deps_set);
     }
 
-    // Use Kahn's algorithm for topological sort
-    // in_degree[A] = number of tables A depends on
-    // Use BTreeMap for consistent ordering
-    let mut in_degree: BTreeMap<&str, usize> = BTreeMap::new();
-    for &table_name in &delete_table_names {
-        in_degree.insert(
-            table_name,
-            dependencies
-                .get(table_name)
-                .map_or(0, std::collections::BTreeSet::len),
-        );
-    }
-
-    // Start with tables that have no dependencies (can be deleted last in creation order)
-    // BTreeMap iteration is already sorted
-    let mut queue: VecDeque<&str> = in_degree
-        .iter()
-        .filter(|(_, deg)| **deg == 0)
-        .map(|(name, _)| *name)
-        .collect();
-
-    let mut sorted_tables: Vec<&str> = Vec::with_capacity(delete_table_names.len());
-    while let Some(table_name) = queue.pop_front() {
-        sorted_tables.push(table_name);
-
-        // For each table that has this one as a dependency, decrement its in-degree
-        // Use BTreeSet for consistent ordering of newly ready tables
-        let mut ready_tables: BTreeSet<&str> = BTreeSet::new();
-        for (&dependent, deps) in &dependencies {
-            if deps.contains(&table_name)
-                && let Some(degree) = in_degree.get_mut(dependent)
-            {
-                *degree -= 1;
-                if *degree == 0 {
-                    ready_tables.insert(dependent);
-                }
-            }
-        }
-        for t in ready_tables {
-            queue.push_back(t);
-        }
-    }
+    // Kahn's algorithm for topological sort (shared core): creation order
+    // first, then reversed below into deletion order.
+    let mut sorted_tables = kahn_ready_order(&dependencies);
 
     // Reverse to get deletion order (tables with dependencies should be deleted first)
     sorted_tables.reverse();
@@ -226,9 +191,11 @@ pub(super) fn sort_delete_tables(
         a_pos.cmp(&b_pos)
     });
 
-    // Put them back
-    for (i, idx) in delete_indices.iter().enumerate() {
-        actions[*idx] = delete_actions[i].clone();
+    // Put them back (move-back instead of clone-back — `delete_actions` is owned
+    // and unused afterwards, so each `MigrationAction` is moved into place
+    // without an extra deep clone of its enum payload).
+    for (idx, action) in delete_indices.iter().zip(delete_actions) {
+        actions[*idx] = action;
     }
 }
 

From 94f80c1bcd0edd00486015f1b48035d599ac1727 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 00:18:48 +0900
Subject: [PATCH 054/109] retry-now#0053: batch - LSP disk_document +
 byte_range_to_ls + non_empty helpers, drop dead exporter path resolver, CLI
 alloc cleanup (5 kept)

---
 crates/vespertide-cli/src/commands/init.rs    |  2 +-
 crates/vespertide-cli/src/commands/new.rs     |  4 +-
 .../src/commands/revision/mod.rs              |  2 +-
 crates/vespertide-cli/src/commands/status.rs  |  4 +-
 .../vespertide-exporter/src/seaorm/imports.rs | 29 ------
 .../src/seaorm/tests/module_path.rs           | 21 ++--
 .../src/backend/handler_file_features.rs      | 45 ++-------
 .../src/backend/handler_navigation.rs         | 26 +----
 .../src/backend/handler_rename.rs             | 13 +--
 crates/vespertide-lsp/src/backend/helpers.rs  | 96 ++++++++++---------
 10 files changed, 83 insertions(+), 159 deletions(-)

diff --git a/crates/vespertide-cli/src/commands/init.rs b/crates/vespertide-cli/src/commands/init.rs
index 340409e2..5f42ad05 100644
--- a/crates/vespertide-cli/src/commands/init.rs
+++ b/crates/vespertide-cli/src/commands/init.rs
@@ -19,7 +19,7 @@ pub async fn cmd_init() -> Result<()> {
     println!(
         "{} {}",
         "created".bright_green().bold(),
-        format!("{}", path.display()).bright_white()
+        path.display().to_string().bright_white()
     );
     Ok(())
 }
diff --git a/crates/vespertide-cli/src/commands/new.rs b/crates/vespertide-cli/src/commands/new.rs
index 0696fad4..66bec51f 100644
--- a/crates/vespertide-cli/src/commands/new.rs
+++ b/crates/vespertide-cli/src/commands/new.rs
@@ -23,7 +23,7 @@ pub async fn cmd_new(name: String, format: Option) -> Result<()> {
     }
 
     let table = TableDef {
-        name: name.clone().into(),
+        name: name.into(),
         description: None,
         columns: Vec::new(),
         constraints: Vec::new(),
@@ -39,7 +39,7 @@ pub async fn cmd_new(name: String, format: Option) -> Result<()> {
     println!(
         "{} {}",
         "Created model template:".bright_green().bold(),
-        format!("{}", path.display()).bright_white()
+        path.display().to_string().bright_white()
     );
     Ok(())
 }
diff --git a/crates/vespertide-cli/src/commands/revision/mod.rs b/crates/vespertide-cli/src/commands/revision/mod.rs
index 9d213912..4135eb8f 100644
--- a/crates/vespertide-cli/src/commands/revision/mod.rs
+++ b/crates/vespertide-cli/src/commands/revision/mod.rs
@@ -601,7 +601,7 @@ where
     println!(
         "{} {}",
         "Created migration:".bright_green().bold(),
-        format!("{}", path.display()).bright_white()
+        path.display().to_string().bright_white()
     );
     println!(
         "  {} {}",
diff --git a/crates/vespertide-cli/src/commands/status.rs b/crates/vespertide-cli/src/commands/status.rs
index 82b695e5..975748bb 100644
--- a/crates/vespertide-cli/src/commands/status.rs
+++ b/crates/vespertide-cli/src/commands/status.rs
@@ -14,12 +14,12 @@ pub async fn cmd_status() -> Result<()> {
     println!(
         "  {} {}",
         "Models directory:".cyan(),
-        format!("{}", config.models_dir().display()).bright_white()
+        config.models_dir().display().to_string().bright_white()
     );
     println!(
         "  {} {}",
         "Migrations directory:".cyan(),
-        format!("{}", config.migrations_dir().display()).bright_white()
+        config.migrations_dir().display().to_string().bright_white()
     );
     println!(
         "  {} {:?}",
diff --git a/crates/vespertide-exporter/src/seaorm/imports.rs b/crates/vespertide-exporter/src/seaorm/imports.rs
index d2e6c77e..21baa9f7 100644
--- a/crates/vespertide-exporter/src/seaorm/imports.rs
+++ b/crates/vespertide-exporter/src/seaorm/imports.rs
@@ -15,35 +15,6 @@ pub(super) fn absolute_module_path(crate_prefix: &str, to_module: &[String]) ->
     path
 }
 
-/// Look up the module path for a table name from the `module_paths` map.
-/// Uses `super::` for sibling modules in the same folder, `crate::` absolute paths for
-/// cross-directory relations when mappings are available, and falls back to `super::{table_name}`.
-#[cfg(test)]
-pub(super) fn resolve_entity_module_path(
-    current_table: &str,
-    target_table: &str,
-    module_paths: &HashMap>,
-    crate_prefix: &str,
-) -> String {
-    if let (Some(current), Some(target)) = (
-        module_paths.get(current_table),
-        module_paths.get(target_table),
-    ) {
-        let current_parent = current.split_last().map_or(&[][..], |(_, parent)| parent);
-        let target_parent = target.split_last().map_or(&[][..], |(_, parent)| parent);
-
-        if current_parent == target_parent {
-            return format!("super::{target_table}");
-        }
-
-        if !crate_prefix.is_empty() {
-            return absolute_module_path(crate_prefix, target);
-        }
-    }
-
-    format!("super::{target_table}")
-}
-
 /// Resolve relation field entity paths for `SeaORM` model macros.
 ///
 /// Rule:
diff --git a/crates/vespertide-exporter/src/seaorm/tests/module_path.rs b/crates/vespertide-exporter/src/seaorm/tests/module_path.rs
index c9e61a15..7c07f417 100644
--- a/crates/vespertide-exporter/src/seaorm/tests/module_path.rs
+++ b/crates/vespertide-exporter/src/seaorm/tests/module_path.rs
@@ -37,19 +37,20 @@ fn absolute_module_path_deep_nesting() {
 }
 
 #[test]
-fn resolve_entity_module_path_with_crate_prefix() {
+fn resolve_relation_entity_module_path_with_crate_prefix() {
     let mut module_paths = HashMap::new();
     module_paths.insert(
         "estimate".into(),
         vec!["estimate".into(), "estimate".into()],
     );
     module_paths.insert("admin".into(), vec!["admin".into(), "admin".into()]);
-    let result = resolve_entity_module_path("estimate", "admin", &module_paths, "crate::models");
+    let result =
+        resolve_relation_entity_module_path("estimate", "admin", &module_paths, "crate::models");
     assert_eq!(result, "crate::models::admin::admin");
 }
 
 #[test]
-fn resolve_entity_module_path_prefers_super_for_siblings() {
+fn resolve_relation_entity_module_path_prefers_super_for_siblings() {
     let mut module_paths = HashMap::new();
     module_paths.insert("admin".into(), vec!["admin".into(), "admin".into()]);
     module_paths.insert(
@@ -57,22 +58,16 @@ fn resolve_entity_module_path_prefers_super_for_siblings() {
         vec!["admin".into(), "admin_stamp".into()],
     );
 
-    let result = resolve_entity_module_path("admin_stamp", "admin", &module_paths, "crate::models");
+    let result =
+        resolve_relation_entity_module_path("admin_stamp", "admin", &module_paths, "crate::models");
     assert_eq!(result, "super::admin");
 }
 
 #[test]
-fn resolve_entity_module_path_fallback_when_no_mapping() {
-    let module_paths = HashMap::new();
-    let result = resolve_entity_module_path("post", "user", &module_paths, "crate::models");
-    assert_eq!(result, "super::user");
-}
-
-#[test]
-fn resolve_entity_module_path_fallback_when_empty_prefix() {
+fn resolve_relation_entity_module_path_fallback_when_empty_prefix() {
     let mut module_paths = HashMap::new();
     module_paths.insert("admin".into(), vec!["admin".into(), "admin".into()]);
-    let result = resolve_entity_module_path("user", "admin", &module_paths, "");
+    let result = resolve_relation_entity_module_path("user", "admin", &module_paths, "");
     assert_eq!(result, "super::admin");
 }
 
diff --git a/crates/vespertide-lsp/src/backend/handler_file_features.rs b/crates/vespertide-lsp/src/backend/handler_file_features.rs
index 28508aa6..10d723ba 100644
--- a/crates/vespertide-lsp/src/backend/handler_file_features.rs
+++ b/crates/vespertide-lsp/src/backend/handler_file_features.rs
@@ -15,12 +15,12 @@
 use tower_lsp_server::jsonrpc::Result;
 use tower_lsp_server::ls_types::{
     DocumentHighlight, DocumentHighlightKind, DocumentHighlightParams, DocumentSymbol,
-    DocumentSymbolParams, DocumentSymbolResponse, FoldingRange, FoldingRangeParams, Range,
-    SelectionRange, SelectionRangeParams, SymbolKind as LspSymbolKind,
+    DocumentSymbolParams, DocumentSymbolResponse, FoldingRange, FoldingRangeParams, SelectionRange,
+    SelectionRangeParams, SymbolKind as LspSymbolKind,
 };
 
 use super::Backend;
-use super::helpers::byte_to_ls_position;
+use super::helpers::{byte_range_to_ls, byte_to_ls_position, non_empty};
 
 pub(super) async fn document_symbol_impl(
     backend: &Backend,
@@ -64,12 +64,7 @@ pub(super) async fn folding_range_impl(
             })
             .collect::>()
     });
-    let folds = folds.unwrap_or_default();
-    if folds.is_empty() {
-        Ok(None)
-    } else {
-        Ok(Some(folds))
-    }
+    Ok(non_empty(folds.unwrap_or_default()))
 }
 
 pub(super) async fn document_highlight_impl(
@@ -85,10 +80,7 @@ pub(super) async fn document_highlight_impl(
         crate::file_features::compute_document_highlight(text, state.tree.as_ref(), byte)
             .into_iter()
             .map(|h| DocumentHighlight {
-                range: Range {
-                    start: byte_to_ls_position(&state.doc, h.byte_range.start),
-                    end: byte_to_ls_position(&state.doc, h.byte_range.end),
-                },
+                range: byte_range_to_ls(&state.doc, &h.byte_range),
                 kind: Some(match h.kind {
                     crate::file_features::DomainDocumentHighlightKind::Read => {
                         DocumentHighlightKind::READ
@@ -100,12 +92,7 @@ pub(super) async fn document_highlight_impl(
             })
             .collect::>()
     });
-    let hits = hits.unwrap_or_default();
-    if hits.is_empty() {
-        Ok(None)
-    } else {
-        Ok(Some(hits))
-    }
+    Ok(non_empty(hits.unwrap_or_default()))
 }
 
 pub(super) async fn selection_range_impl(
@@ -124,10 +111,7 @@ pub(super) async fn selection_range_impl(
                     crate::file_features::compute_selection_ranges(state.tree.as_ref(), byte);
                 let mut acc: Option> = None;
                 for entry in chain.into_iter().rev() {
-                    let lsp_range = Range {
-                        start: byte_to_ls_position(&state.doc, entry.byte_range.start),
-                        end: byte_to_ls_position(&state.doc, entry.byte_range.end),
-                    };
+                    let lsp_range = byte_range_to_ls(&state.doc, &entry.byte_range);
                     acc = Some(Box::new(SelectionRange {
                         range: lsp_range,
                         parent: acc,
@@ -136,10 +120,7 @@ pub(super) async fn selection_range_impl(
                 match acc {
                     Some(boxed) => *boxed,
                     None => SelectionRange {
-                        range: Range {
-                            start: byte_to_ls_position(&state.doc, byte),
-                            end: byte_to_ls_position(&state.doc, byte),
-                        },
+                        range: byte_range_to_ls(&state.doc, &(byte..byte)),
                         parent: None,
                     },
                 }
@@ -153,14 +134,8 @@ fn domain_doc_sym_to_lsp(
     sym: crate::file_features::DomainDocumentSymbol,
     doc: &lsp_textdocument::FullTextDocument,
 ) -> DocumentSymbol {
-    let range = Range {
-        start: byte_to_ls_position(doc, sym.byte_range.start),
-        end: byte_to_ls_position(doc, sym.byte_range.end),
-    };
-    let selection_range = Range {
-        start: byte_to_ls_position(doc, sym.selection_byte_range.start),
-        end: byte_to_ls_position(doc, sym.selection_byte_range.end),
-    };
+    let range = byte_range_to_ls(doc, &sym.byte_range);
+    let selection_range = byte_range_to_ls(doc, &sym.selection_byte_range);
     let kind = match sym.kind {
         crate::file_features::DomainDocumentSymbolKind::Table => LspSymbolKind::CLASS,
         crate::file_features::DomainDocumentSymbolKind::Column => LspSymbolKind::FIELD,
diff --git a/crates/vespertide-lsp/src/backend/handler_navigation.rs b/crates/vespertide-lsp/src/backend/handler_navigation.rs
index 46d2d028..be152969 100644
--- a/crates/vespertide-lsp/src/backend/handler_navigation.rs
+++ b/crates/vespertide-lsp/src/backend/handler_navigation.rs
@@ -32,7 +32,7 @@ use tower_lsp_server::ls_types::{
 use super::Backend;
 use super::helpers::{
     byte_to_ls_position, domain_edits_to_lsp, domain_reference_to_location, domain_to_lsp,
-    symbol_to_lsp,
+    non_empty, symbol_to_lsp,
 };
 use crate::parser::DocumentFormat;
 
@@ -307,11 +307,7 @@ pub(super) async fn references_impl(
         .filter_map(|reference| domain_reference_to_location(&reference, backend))
         .collect::>();
 
-    if locations.is_empty() {
-        Ok(None)
-    } else {
-        Ok(Some(locations))
-    }
+    Ok(non_empty(locations))
 }
 
 pub(super) async fn code_action_impl(
@@ -355,11 +351,7 @@ pub(super) async fn code_action_impl(
         })
         .collect();
 
-    if actions.is_empty() {
-        Ok(None)
-    } else {
-        Ok(Some(actions))
-    }
+    Ok(non_empty(actions))
 }
 
 pub(super) async fn inlay_hint_impl(
@@ -399,11 +391,7 @@ pub(super) async fn inlay_hint_impl(
 
     log_inlay_hint(&uri, hints.len());
 
-    if hints.is_empty() {
-        Ok(None)
-    } else {
-        Ok(Some(hints))
-    }
+    Ok(non_empty(hints))
 }
 
 pub(super) async fn symbol_impl(
@@ -423,9 +411,5 @@ pub(super) async fn symbol_impl(
         .iter()
         .filter_map(|sym| symbol_to_lsp(sym, backend))
         .collect();
-    if lsp_symbols.is_empty() {
-        Ok(None)
-    } else {
-        Ok(Some(WorkspaceSymbolResponse::Flat(lsp_symbols)))
-    }
+    Ok(non_empty(lsp_symbols).map(WorkspaceSymbolResponse::Flat))
 }
diff --git a/crates/vespertide-lsp/src/backend/handler_rename.rs b/crates/vespertide-lsp/src/backend/handler_rename.rs
index e466919e..d3a1990a 100644
--- a/crates/vespertide-lsp/src/backend/handler_rename.rs
+++ b/crates/vespertide-lsp/src/backend/handler_rename.rs
@@ -22,7 +22,7 @@ use tower_lsp_server::ls_types::{
 };
 
 use super::Backend;
-use super::helpers::{byte_to_ls_position, domain_edits_to_lsp};
+use super::helpers::{byte_range_to_ls, domain_edits_to_lsp};
 use crate::parser::DocumentFormat;
 
 #[cfg(not(tarpaulin_include))]
@@ -79,9 +79,8 @@ pub(super) async fn prepare_rename_impl(
 
     let range = backend
         .store
-        .docs_iter_for_uri(&uri, |state| Range {
-            start: byte_to_ls_position(&state.doc, domain.byte_range.start),
-            end: byte_to_ls_position(&state.doc, domain.byte_range.end),
+        .docs_iter_for_uri(&uri, |state| {
+            byte_range_to_ls(&state.doc, &domain.byte_range)
         })
         .unwrap_or(Range {
             start: Position {
@@ -158,9 +157,5 @@ pub(super) fn lowered_rename_changes(
         changes.insert(target_uri, text_edits);
     }
 
-    if changes.is_empty() {
-        None
-    } else {
-        Some(changes)
-    }
+    (!changes.is_empty()).then_some(changes)
 }
diff --git a/crates/vespertide-lsp/src/backend/helpers.rs b/crates/vespertide-lsp/src/backend/helpers.rs
index 93713137..f23862bc 100644
--- a/crates/vespertide-lsp/src/backend/helpers.rs
+++ b/crates/vespertide-lsp/src/backend/helpers.rs
@@ -47,14 +47,12 @@ pub(super) fn domain_to_lsp(
     });
 
     let text_edit = item.replace_range_bytes.as_ref().map(|range| {
-        let start = byte_to_ls_position(doc, range.start);
-        let end = byte_to_ls_position(doc, range.end);
         let new_text = item
             .insert_text
             .clone()
             .unwrap_or_else(|| item.label.clone());
         CompletionTextEdit::Edit(TextEdit {
-            range: Range { start, end },
+            range: byte_range_to_ls(doc, range),
             new_text,
         })
     });
@@ -88,6 +86,26 @@ pub(super) fn byte_to_ls_position(
     crate::position::lsp_to_ls_position(crate::position::byte_to_lsp_position(doc, byte_offset))
 }
 
+/// Lower a byte-offset range to an LSP [`Range`] via UTF-16-aware position
+/// conversion. This is the single place the start/end pair lowering lives —
+/// every handler that turns a domain `byte_range` into a wire `Range` goes
+/// through here.
+pub(super) fn byte_range_to_ls(
+    doc: &lsp_textdocument::FullTextDocument,
+    range: &std::ops::Range,
+) -> Range {
+    Range {
+        start: byte_to_ls_position(doc, range.start),
+        end: byte_to_ls_position(doc, range.end),
+    }
+}
+
+/// `None` when the collected result set is empty, so handlers can answer
+/// `Ok(None)` instead of shipping an empty payload over the wire.
+pub(super) fn non_empty(v: Vec) -> Option> {
+    if v.is_empty() { None } else { Some(v) }
+}
+
 /// Best-effort filesystem path normalization for workspace dedup.
 ///
 /// 1. `std::fs::canonicalize` when the file exists — that is the most
@@ -106,6 +124,24 @@ pub(super) fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
     }
 }
 
+/// Read a disk-only file into a transient UTF-16-aware document, guessing
+/// the language id from the file extension. Shared fallback for symbol,
+/// reference, and rename lowering when the target URI is not an open
+/// document. Returns `None` when the URI has no path or the read fails.
+fn disk_document(uri: &Uri) -> Option {
+    let path = crate::position::uri_to_path(uri)?;
+    let text = std::fs::read_to_string(&path).ok()?;
+    let language_id = match path.extension().and_then(|e| e.to_str()) {
+        Some("yaml" | "yml") => "yaml",
+        _ => "json",
+    };
+    Some(lsp_textdocument::FullTextDocument::new(
+        language_id.to_string(),
+        1,
+        text,
+    ))
+}
+
 /// Lower a domain symbol into LSP `SymbolInformation`, resolving the byte
 /// range via either the open document or the on-disk file.
 #[expect(
@@ -116,25 +152,15 @@ pub(super) fn symbol_to_lsp(
     symbol: &crate::symbols::DomainSymbol,
     backend: &Backend,
 ) -> Option {
-    let range = backend.store.docs_iter_for_uri(&symbol.uri, |state| Range {
-        start: byte_to_ls_position(&state.doc, symbol.byte_range.start),
-        end: byte_to_ls_position(&state.doc, symbol.byte_range.end),
+    let range = backend.store.docs_iter_for_uri(&symbol.uri, |state| {
+        byte_range_to_ls(&state.doc, &symbol.byte_range)
     });
     let range = if let Some(r) = range {
         r
     } else {
         // Disk-only file — read it once to get a UTF-16-aware doc.
-        let path = crate::position::uri_to_path(&symbol.uri)?;
-        let text = std::fs::read_to_string(&path).ok()?;
-        let language_id = match path.extension().and_then(|e| e.to_str()) {
-            Some("yaml" | "yml") => "yaml",
-            _ => "json",
-        };
-        let doc = lsp_textdocument::FullTextDocument::new(language_id.to_string(), 1, text);
-        Range {
-            start: byte_to_ls_position(&doc, symbol.byte_range.start),
-            end: byte_to_ls_position(&doc, symbol.byte_range.end),
-        }
+        let doc = disk_document(&symbol.uri)?;
+        byte_range_to_ls(&doc, &symbol.byte_range)
     };
 
     Some(SymbolInformation {
@@ -164,10 +190,7 @@ pub(super) fn domain_edits_to_lsp(
         domain_edits
             .iter()
             .map(|edit| TextEdit {
-                range: Range {
-                    start: byte_to_ls_position(doc, edit.byte_range.start),
-                    end: byte_to_ls_position(doc, edit.byte_range.end),
-                },
+                range: byte_range_to_ls(doc, &edit.byte_range),
                 new_text: edit.new_text.clone(),
             })
             .collect()
@@ -180,13 +203,7 @@ pub(super) fn domain_edits_to_lsp(
         return Some(edits);
     }
 
-    let path = crate::position::uri_to_path(target_uri)?;
-    let text = std::fs::read_to_string(&path).ok()?;
-    let language_id = match path.extension().and_then(|e| e.to_str()) {
-        Some("yaml" | "yml") => "yaml",
-        _ => "json",
-    };
-    let doc = lsp_textdocument::FullTextDocument::new(language_id.to_string(), 1, text);
+    let doc = disk_document(target_uri)?;
     Some(to_lsp(&doc))
 }
 
@@ -200,13 +217,9 @@ pub(super) fn domain_reference_to_location(
     reference: &crate::references::DomainReference,
     backend: &Backend,
 ) -> Option {
-    if let Some(range) = backend
-        .store
-        .docs_iter_for_uri(&reference.uri, |state| Range {
-            start: byte_to_ls_position(&state.doc, reference.byte_range.start),
-            end: byte_to_ls_position(&state.doc, reference.byte_range.end),
-        })
-    {
+    if let Some(range) = backend.store.docs_iter_for_uri(&reference.uri, |state| {
+        byte_range_to_ls(&state.doc, &reference.byte_range)
+    }) {
         return Some(Location {
             uri: reference.uri.clone(),
             range,
@@ -214,18 +227,9 @@ pub(super) fn domain_reference_to_location(
     }
 
     // Disk-only file — read source on demand.
-    let path = crate::position::uri_to_path(&reference.uri)?;
-    let text = std::fs::read_to_string(&path).ok()?;
-    let language_id = match path.extension().and_then(|e| e.to_str()) {
-        Some("yaml" | "yml") => "yaml",
-        _ => "json",
-    };
-    let doc = lsp_textdocument::FullTextDocument::new(language_id.to_string(), 1, text);
+    let doc = disk_document(&reference.uri)?;
     Some(Location {
         uri: reference.uri.clone(),
-        range: Range {
-            start: byte_to_ls_position(&doc, reference.byte_range.start),
-            end: byte_to_ls_position(&doc, reference.byte_range.end),
-        },
+        range: byte_range_to_ls(&doc, &reference.byte_range),
     })
 }

From 26539ed6d4a66514b4d9544c20dfb1409b00b29e Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 00:51:20 +0900
Subject: [PATCH 055/109] retry-now#0054: batch - document erd/JPA in README,
 adopt query test helpers, refresh stale AGENTS.md, extract
 orm_file_extension, compact ColumnDef test literals (5 kept)

---
 AGENTS.md                                     |   2 +-
 README.md                                     |   4 +-
 crates/vespertide-cli/AGENTS.md               |  24 +-
 crates/vespertide-cli/src/commands/export.rs  |  27 +-
 crates/vespertide-exporter/AGENTS.md          |  27 +-
 .../src/validate/tests/plan_validation.rs     | 344 ++++++------------
 crates/vespertide-query/src/builder/mod.rs    |  66 ++--
 crates/vespertide-query/src/sql/add_column.rs |  26 +-
 .../src/sql/add_constraint/foreign_key.rs     |  57 +--
 .../src/sql/add_constraint/primary_key.rs     |   2 +-
 .../src/sql/add_constraint/unique.rs          |   2 +-
 .../vespertide-query/src/sql/create_table.rs  |   8 +-
 .../src/sql/delete_column/mod.rs              |  38 +-
 .../src/sql/modify_column_type/mod.rs         |  65 +---
 .../narrowing_preprocess.rs                   |  16 +-
 .../src/sql/remap_enum_values.rs              |  10 +-
 .../src/sql/remove_constraint/mod.rs          |  11 +-
 .../vespertide-query/src/sql/tests/naming.rs  |  62 +---
 crates/vespertide-query/src/test_support.rs   |  11 +-
 19 files changed, 259 insertions(+), 543 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 8eddf472..9a78ad49 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -42,7 +42,7 @@ vespertide/
 | Core types (TableDef, ColumnDef) | `vespertide-core/src/schema/` | Start with `table.rs`, `column.rs` |
 | **Newtype name wrappers** | `vespertide-core/src/schema/names.rs` | `TableName` / `ColumnName` / `IndexName` with `#[serde(transparent)]` |
 | Column type system | `vespertide-core/src/schema/column.rs` | `ColumnType::Simple/Complex` variants |
-| Migration actions | `vespertide-core/src/action/` | **14 action variants** (incl. `RawSql` escape hatch), `MigrationPlan` struct |
+| Migration actions | `vespertide-core/src/action/` | **15 action variants** (incl. `RawSql` escape hatch), `MigrationPlan` struct |
 | **QueryError variants** | `vespertide-query/src/error.rs` | `InvalidColumnType` / `SchemaError` / `BackendError` / `UnsupportedAction`; `Other` is `#[deprecated]` |
 | Schema diffing | `vespertide-planner/src/diff/` | topological sort for FK deps |
 | SQL generation | `vespertide-query/src/sql/` | One file per action type |
diff --git a/README.md b/README.md
index 7bc3578e..c77f0993 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Declarative database schema management. Define your schemas in JSON, and Vespert
 - **Enum Types**: Native string enums and integer enums (no migration needed for new values)
 - **Zero-Runtime Migrations**: Compile-time macro generates database-specific SQL
 - **JSON Schema Validation**: Ships with JSON Schemas for IDE autocompletion and validation
-- **ORM Export**: Export schemas to SeaORM, SQLAlchemy, SQLModel
+- **ORM Export**: Export schemas to SeaORM, SQLAlchemy, SQLModel, JPA
 - **Language Server**: First-class editor support via the bundled `vespertide-lsp` — see [LSP Features](#lsp-features) below
 
 ## What's new in 0.2.0
@@ -89,6 +89,7 @@ vespertide revision -m "create user table"
 | `vespertide status` | Show configuration and sync status overview |
 | `vespertide log` | List applied migrations with generated SQL |
 | `vespertide export --orm seaorm` | Export models to ORM code |
+| `vespertide erd` | Export schema as an ERD diagram (`--format svg\|mermaid\|dot`, `--include`/`--exclude`/`--depth` filters) |
 
 ## Model Definition
 
@@ -235,6 +236,7 @@ The only exception is adding `fill_with` values when prompted (for NOT NULL colu
 vespertide export --orm seaorm      # Rust - SeaORM entities
 vespertide export --orm sqlalchemy  # Python - SQLAlchemy models
 vespertide export --orm sqlmodel    # Python - SQLModel (FastAPI)
+vespertide export --orm jpa         # Java - JPA/Hibernate entities
 ```
 
 ## Runtime Migrations (Macro)
diff --git a/crates/vespertide-cli/AGENTS.md b/crates/vespertide-cli/AGENTS.md
index 778a065b..3f4c5452 100644
--- a/crates/vespertide-cli/AGENTS.md
+++ b/crates/vespertide-cli/AGENTS.md
@@ -9,15 +9,19 @@ src/
 ├── main.rs           # Clap CLI definition, command dispatch
 ├── utils.rs          # Re-exports loader functions, migration filename generation
 └── commands/
-    ├── mod.rs        # Public exports: cmd_{init,new,diff,sql,revision,status,log,export}
+    ├── mod.rs        # Public exports: cmd_{init,new,diff,sql,revision,status,log,export} + cmd_erd_with_filters
     ├── init.rs       # Create vespertide.json
     ├── new.rs        # Create model template with $schema reference
-    ├── diff.rs       # Show pending changes (colored action formatting)
+    ├── diff/         # Show pending changes (colored action formatting) — mod.rs + tests/
     ├── sql.rs        # Print SQL for next migration
-    ├── revision.rs   # Persist migration (interactive fill-with prompts)
+    ├── revision/     # Persist migration — mod.rs, parse.rs, emit.rs, write.rs, timezones.rs,
+    │                 #   prompts/ (fill_with, narrowing, timezone, drop_recreate_fk_policy,
+    │                 #   choices_and_apply/), tests/
     ├── status.rs     # Show config and sync status
     ├── log.rs        # List applied migrations with SQL
-    └── export.rs     # Export to ORM code (SeaORM/SQLAlchemy/SQLModel)
+    ├── export.rs     # Export to ORM code (SeaORM/SQLAlchemy/SQLModel/JPA)
+    └── erd/          # ERD diagram export — mod.rs, mermaid.rs, dot.rs, svg/ (style, model,
+                      #   layout, edges, render, util), tests/
 ```
 
 ## COMMANDS
@@ -32,25 +36,27 @@ src/
 | `status` | `cmd_status()` | Display config paths and migration count |
 | `log` | `cmd_log(backend)` | Iterate applied migrations, print SQL |
 | `export --orm` | `cmd_export(orm, dir)` | `render_entity_with_schema()` + mod.rs wiring |
+| `erd -f svg\|mermaid\|dot` | `cmd_erd_with_filters(format, output, include, exclude, depth)` | FK-graph filtered ERD rendering |
 
 ## WHERE TO LOOK
 
 | Task | File | Key Functions |
 |------|------|---------------|
 | Add new CLI command | `main.rs` | Add to `Commands` enum, match in `main()` |
-| Modify action display | `diff.rs` | `format_action()`, `format_constraint_type()` |
-| Change fill-with flow | `revision.rs` | `handle_missing_fill_with()`, `collect_fill_with_values()` |
+| Modify action display | `diff/mod.rs` | `format_action()`, `format_constraint_type()` |
+| Change fill-with flow | `revision/prompts/fill_with.rs` | fill-with prompt + collection helpers |
 | Export logic | `export.rs` | `walk_models()`, `ensure_mod_chain()`, `build_output_path()` |
+| ERD rendering | `erd/` | `cmd_erd_with_filters()`, `svg/render.rs`, `mermaid.rs`, `dot.rs` |
 | Filename patterns | `utils.rs` | `migration_filename_with_format_and_pattern()` |
 
 ## NOTES
 
-- **revision.rs** (3064 lines, scheduled for split per 1000-line rule): Most complex - handles interactive `--fill-with` prompts for NOT NULL columns without defaults
-- **export.rs**: Generates `mod.rs` chain for SeaORM exports; Python ORMs skip this
+- **revision/**: Most complex command — handles interactive `--fill-with` prompts for NOT NULL columns without defaults; long ago split from a single 3064-line file into `revision/{mod,parse,emit,write,timezones}.rs` + `prompts/` + `tests/`
+- **export.rs**: Generates `mod.rs` chain for SeaORM exports; Python/Java ORMs skip this
 - All commands use `load_config()`, `load_models()`, `load_migrations()` from `vespertide_loader`
 - YAML and JSON are both fully supported for models and migrations; `new  -f yaml` creates YAML templates.
 - Prefer typed `MigrationAction` enums; `RawSql` exists as a documented emergency escape hatch, but is not recommended for normal use.
 - Tests use `serial_test::serial` with `CwdGuard` for directory isolation
 - Schema URLs default to GitHub raw; override via `VESP_SCHEMA_BASE_URL` env var
-- Every `.rs` file must stay ≤ 1000 lines (CI enforced).
+- Two-tier line policy (CI-enforced via `scripts/check-line-budget.sh`): production-only `.rs` ≤ 1000 lines; files carrying test code (`tests/` dir or inline `#[cfg(test)] mod tests`) ≤ 1200 lines.
 - Workspace lints warn on unsafe code and Clippy all: `unsafe_code = "warn"`, `clippy::all = { level = "warn", priority = -1 }`.
diff --git a/crates/vespertide-cli/src/commands/export.rs b/crates/vespertide-cli/src/commands/export.rs
index 86b57e69..3de79363 100644
--- a/crates/vespertide-cli/src/commands/export.rs
+++ b/crates/vespertide-cli/src/commands/export.rs
@@ -9,8 +9,7 @@ use tokio::fs;
 use vespertide_config::VespertideConfig;
 use vespertide_core::TableDef;
 use vespertide_exporter::{
-    Orm, python_naming::to_pascal_case, render_entity_with_schema,
-    seaorm::SeaOrmExporterWithConfig,
+    Orm, python_naming::to_pascal_case, render_entity_with_schema, seaorm::SeaOrmExporterWithConfig,
 };
 
 use crate::parallel_config::{EXPORT_RENDER_PAR_MIN_LEN, EXPORT_RENDER_PAR_THRESHOLD};
@@ -230,6 +229,15 @@ fn resolve_export_dir(export_dir: Option, config: &VespertideConfig) ->
     config.model_export_dir().to_path_buf()
 }
 
+/// File extension for the source files a given ORM generates.
+fn orm_file_extension(orm: Orm) -> &'static str {
+    match orm {
+        Orm::SeaOrm => "rs",
+        Orm::SqlAlchemy | Orm::SqlModel => "py",
+        Orm::Jpa => "java",
+    }
+}
+
 /// Clean the export directory by removing all generated files.
 /// This ensures no stale files remain from previous exports.
 async fn clean_export_dir(root: &Path, orm: Orm) -> Result<()> {
@@ -237,13 +245,7 @@ async fn clean_export_dir(root: &Path, orm: Orm) -> Result<()> {
         return Ok(());
     }
 
-    let ext = match orm {
-        Orm::SeaOrm => "rs",
-        Orm::SqlAlchemy | Orm::SqlModel => "py",
-        Orm::Jpa => "java",
-    };
-
-    clean_dir_recursive(root, ext).await?;
+    clean_dir_recursive(root, orm_file_extension(orm)).await?;
     Ok(())
 }
 
@@ -329,11 +331,7 @@ fn build_output_path(root: &Path, rel_path: &Path, orm: Orm) -> PathBuf {
         let stem = stem.strip_suffix(".vespertide").unwrap_or(stem);
 
         let sanitized = sanitize_filename(stem);
-        let ext = match orm {
-            Orm::SeaOrm => "rs",
-            Orm::SqlAlchemy | Orm::SqlModel => "py",
-            Orm::Jpa => "java",
-        };
+        let ext = orm_file_extension(orm);
         // Java requires filename to match PascalCase class name
         let file_stem = if matches!(orm, Orm::Jpa) {
             to_pascal_case(&sanitized)
@@ -953,5 +951,4 @@ mod tests {
         let out = build_output_path(root, rel_path, Orm::Jpa);
         assert_eq!(out, Path::new("src/models/User.java"));
     }
-
 }
diff --git a/crates/vespertide-exporter/AGENTS.md b/crates/vespertide-exporter/AGENTS.md
index 1a9e82ba..2bd1c79e 100644
--- a/crates/vespertide-exporter/AGENTS.md
+++ b/crates/vespertide-exporter/AGENTS.md
@@ -1,17 +1,23 @@
 # vespertide-exporter
 
-ORM code generation from `TableDef` schemas → SeaORM (Rust), SQLAlchemy (Python), SQLModel (Python).
+ORM code generation from `TableDef` schemas → SeaORM (Rust), SQLAlchemy (Python), SQLModel (Python), JPA (Java).
 
 ## STRUCTURE
 
 ```
 src/
 ├── lib.rs              # Re-exports all backends
-├── orm.rs              # OrmExporter trait, Orm enum, dispatch functions
-├── seaorm/mod.rs       # 4122 lines, scheduled for split - Entity/Model/Relation generation
-├── sqlalchemy/mod.rs   # 1383 lines, scheduled for split - declarative_base models
-└── sqlmodel/mod.rs     # 1274 lines, scheduled for split - SQLModel + Pydantic models
-snapshots/              # insta snapshot files for testing
+├── orm.rs              # OrmExporter trait, Orm enum (SeaOrm/SqlAlchemy/SqlModel/Jpa), dispatch
+├── constraint_scan.rs  # Shared constraint scanning helpers
+├── parallel_config.rs  # Rayon parallelism thresholds
+├── python_naming.rs    # Shared Python identifier naming
+├── seaorm/             # mod.rs, render.rs, types.rs, enums.rs, imports.rs,
+│                       #   relations/ (fk_resolve, naming, self_ref, reverse), tests/
+├── sqlalchemy/         # mod.rs, render.rs, types.rs, enums.rs — declarative_base models
+├── sqlmodel/           # mod.rs, render.rs, types.rs, enums.rs — SQLModel + Pydantic models
+├── jpa/                # mod.rs, render.rs, types.rs — JPA/Hibernate entities
+├── utils/              # common.rs, python.rs
+└── tests/              # Shared orm_cases! cross-ORM snapshot suite + fixtures/ + snapshots/
 ```
 
 ## WHERE TO LOOK
@@ -45,6 +51,11 @@ snapshots/              # insta snapshot files for testing
 - Lighter import tracking (no `sa_types` - uses native Python types)
 - `sa_column_kwargs` for SQLAlchemy-specific options
 
+### JPA (Java)
+- Jakarta Persistence (`jakarta.persistence.*`) entity classes with `@Entity`/`@Table`/`@Column`
+- Enum types render as Java `enum` + `@Enumerated`
+- FK columns render as `@ManyToOne`/`@JoinColumn` relations
+
 ## TESTING
 
 ```bash
@@ -58,11 +69,11 @@ cargo insta accept
 
 - Snapshot testing with `insta` crate (YAML format)
 - `rstest` for parameterized tests across all ORM backends
-- 66 snapshot files across exporter snapshot directories
+- 240 snapshot files across exporter snapshot directories; every export scenario goes through the shared `orm_cases!` macro in `src/tests/mod.rs`, producing one snapshot per ORM (all four) in `src/tests/snapshots/`
 
 ## NOTES
 
 - YAML and JSON are both fully supported input formats; exporter tests also use YAML-formatted insta snapshots.
 - Generated ORM files are outputs only; edit Vespertide models, then regenerate.
-- Every `.rs` file must stay ≤ 1000 lines (CI enforced); current hotspots include SeaORM, SQLAlchemy, SQLModel, and JPA backends.
+- Two-tier line policy (CI-enforced via `scripts/check-line-budget.sh`): production-only `.rs` ≤ 1000 lines; files carrying test code (`tests/` dir or inline `#[cfg(test)] mod tests`) ≤ 1200 lines.
 - Workspace lints warn on unsafe code and Clippy all: `unsafe_code = "warn"`, `clippy::all = { level = "warn", priority = -1 }`.
diff --git a/crates/vespertide-planner/src/validate/tests/plan_validation.rs b/crates/vespertide-planner/src/validate/tests/plan_validation.rs
index 78dc6fcc..947175ab 100644
--- a/crates/vespertide-planner/src/validate/tests/plan_validation.rs
+++ b/crates/vespertide-planner/src/validate/tests/plan_validation.rs
@@ -2,7 +2,7 @@ use super::*;
 
 #[test]
 fn validate_migration_plan_missing_fill_with() {
-    use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
+    use vespertide_core::{ColumnType, MigrationAction, MigrationPlan};
 
     let plan = MigrationPlan {
         id: String::new(),
@@ -11,17 +11,7 @@ fn validate_migration_plan_missing_fill_with() {
         version: 1,
         actions: vec![MigrationAction::AddColumn {
             table: "users".into(),
-            column: Box::new(ColumnDef {
-                name: "email".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Text),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }),
+            column: Box::new(col("email", ColumnType::Simple(SimpleColumnType::Text))),
             fill_with: None,
         }],
     };
@@ -39,21 +29,11 @@ fn validate_migration_plan_missing_fill_with() {
 
 #[test]
 fn validate_migration_plan_missing_fill_with_for_not_null_add_column() {
-    use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
+    use vespertide_core::{ColumnType, MigrationAction, MigrationPlan};
 
     let action = MigrationAction::AddColumn {
         table: "users".into(),
-        column: Box::new(ColumnDef {
-            name: "email".into(),
-            r#type: ColumnType::Simple(SimpleColumnType::Text),
-            nullable: false,
-            default: None,
-            comment: None,
-            primary_key: None,
-            unique: None,
-            index: None,
-            foreign_key: None,
-        }),
+        column: Box::new(col("email", ColumnType::Simple(SimpleColumnType::Text))),
         fill_with: None,
     };
     let plan = MigrationPlan {
@@ -74,7 +54,7 @@ fn validate_migration_plan_missing_fill_with_for_not_null_add_column() {
 
 #[test]
 fn validate_migration_plan_with_fill_with() {
-    use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
+    use vespertide_core::{ColumnType, MigrationAction, MigrationPlan};
 
     let plan = MigrationPlan {
         id: String::new(),
@@ -83,17 +63,7 @@ fn validate_migration_plan_with_fill_with() {
         version: 1,
         actions: vec![MigrationAction::AddColumn {
             table: "users".into(),
-            column: Box::new(ColumnDef {
-                name: "email".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Text),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }),
+            column: Box::new(col("email", ColumnType::Simple(SimpleColumnType::Text))),
             fill_with: Some("default@example.com".into()),
         }],
     };
@@ -104,7 +74,7 @@ fn validate_migration_plan_with_fill_with() {
 
 #[test]
 fn validate_migration_plan_nullable_column() {
-    use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
+    use vespertide_core::{ColumnType, MigrationAction, MigrationPlan};
 
     let plan = MigrationPlan {
         id: String::new(),
@@ -113,17 +83,10 @@ fn validate_migration_plan_nullable_column() {
         version: 1,
         actions: vec![MigrationAction::AddColumn {
             table: "users".into(),
-            column: Box::new(ColumnDef {
-                name: "email".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Text),
-                nullable: true,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }),
+            column: Box::new(col_nullable(
+                "email",
+                ColumnType::Simple(SimpleColumnType::Text),
+            )),
             fill_with: None,
         }],
     };
@@ -144,15 +107,8 @@ fn validate_migration_plan_with_default() {
         actions: vec![MigrationAction::AddColumn {
             table: "users".into(),
             column: Box::new(ColumnDef {
-                name: "email".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Text),
-                nullable: false,
                 default: Some("default@example.com".into()),
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
+                ..col("email", ColumnType::Simple(SimpleColumnType::Text))
             }),
             fill_with: None,
         }],
@@ -421,22 +377,18 @@ fn validate_enum_add_column_invalid_default() {
         actions: vec![MigrationAction::AddColumn {
             table: "users".into(),
             column: Box::new(ColumnDef {
-                name: "status".into(),
-                r#type: ColumnType::Complex(ComplexColumnType::Enum {
-                    name: "user_status".into(),
-                    values: EnumValues::String(vec![
-                        "active".into(),
-                        "inactive".into(),
-                        "pending".into(),
-                    ]),
-                }),
-                nullable: false,
                 default: Some("invalid_value".into()),
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
+                ..col(
+                    "status",
+                    ColumnType::Complex(ComplexColumnType::Enum {
+                        name: "user_status".into(),
+                        values: EnumValues::String(vec![
+                            "active".into(),
+                            "inactive".into(),
+                            "pending".into(),
+                        ]),
+                    }),
+                )
             }),
             fill_with: None,
         }],
@@ -465,9 +417,9 @@ fn validate_enum_add_column_invalid_fill_with() {
         version: 1,
         actions: vec![MigrationAction::AddColumn {
             table: "users".into(),
-            column: Box::new(ColumnDef {
-                name: "status".into(),
-                r#type: ColumnType::Complex(ComplexColumnType::Enum {
+            column: Box::new(col(
+                "status",
+                ColumnType::Complex(ComplexColumnType::Enum {
                     name: "user_status".into(),
                     values: EnumValues::String(vec![
                         "active".into(),
@@ -475,14 +427,7 @@ fn validate_enum_add_column_invalid_fill_with() {
                         "pending".into(),
                     ]),
                 }),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }),
+            )),
             fill_with: Some("unknown_status".into()),
         }],
     };
@@ -511,22 +456,18 @@ fn validate_enum_add_column_valid_default_quoted() {
         actions: vec![MigrationAction::AddColumn {
             table: "users".into(),
             column: Box::new(ColumnDef {
-                name: "status".into(),
-                r#type: ColumnType::Complex(ComplexColumnType::Enum {
-                    name: "user_status".into(),
-                    values: EnumValues::String(vec![
-                        "active".into(),
-                        "inactive".into(),
-                        "pending".into(),
-                    ]),
-                }),
-                nullable: false,
                 default: Some("'active'".into()),
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
+                ..col(
+                    "status",
+                    ColumnType::Complex(ComplexColumnType::Enum {
+                        name: "user_status".into(),
+                        values: EnumValues::String(vec![
+                            "active".into(),
+                            "inactive".into(),
+                            "pending".into(),
+                        ]),
+                    }),
+                )
             }),
             fill_with: None,
         }],
@@ -546,22 +487,18 @@ fn validate_enum_add_column_valid_default_unquoted() {
         actions: vec![MigrationAction::AddColumn {
             table: "users".into(),
             column: Box::new(ColumnDef {
-                name: "status".into(),
-                r#type: ColumnType::Complex(ComplexColumnType::Enum {
-                    name: "user_status".into(),
-                    values: EnumValues::String(vec![
-                        "active".into(),
-                        "inactive".into(),
-                        "pending".into(),
-                    ]),
-                }),
-                nullable: false,
                 default: Some("active".into()),
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
+                ..col(
+                    "status",
+                    ColumnType::Complex(ComplexColumnType::Enum {
+                        name: "user_status".into(),
+                        values: EnumValues::String(vec![
+                            "active".into(),
+                            "inactive".into(),
+                            "pending".into(),
+                        ]),
+                    }),
+                )
             }),
             fill_with: None,
         }],
@@ -580,9 +517,9 @@ fn validate_enum_add_column_valid_fill_with() {
         version: 1,
         actions: vec![MigrationAction::AddColumn {
             table: "users".into(),
-            column: Box::new(ColumnDef {
-                name: "status".into(),
-                r#type: ColumnType::Complex(ComplexColumnType::Enum {
+            column: Box::new(col(
+                "status",
+                ColumnType::Complex(ComplexColumnType::Enum {
                     name: "user_status".into(),
                     values: EnumValues::String(vec![
                         "active".into(),
@@ -590,14 +527,7 @@ fn validate_enum_add_column_valid_fill_with() {
                         "pending".into(),
                     ]),
                 }),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }),
+            )),
             fill_with: Some("'pending'".into()),
         }],
     };
@@ -670,9 +600,9 @@ fn validate_enum_integer_add_column_valid() {
         version: 1,
         actions: vec![MigrationAction::AddColumn {
             table: "tasks".into(),
-            column: Box::new(ColumnDef {
-                name: "priority".into(),
-                r#type: ColumnType::Complex(ComplexColumnType::Enum {
+            column: Box::new(col(
+                "priority",
+                ColumnType::Complex(ComplexColumnType::Enum {
                     name: "priority_level".into(),
                     values: EnumValues::Integer(vec![
                         NumValue {
@@ -689,14 +619,7 @@ fn validate_enum_integer_add_column_valid() {
                         },
                     ]),
                 }),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }),
+            )),
             fill_with: Some("Low".into()),
         }],
     };
@@ -714,9 +637,9 @@ fn validate_enum_integer_add_column_invalid() {
         version: 1,
         actions: vec![MigrationAction::AddColumn {
             table: "tasks".into(),
-            column: Box::new(ColumnDef {
-                name: "priority".into(),
-                r#type: ColumnType::Complex(ComplexColumnType::Enum {
+            column: Box::new(col(
+                "priority",
+                ColumnType::Complex(ComplexColumnType::Enum {
                     name: "priority_level".into(),
                     values: EnumValues::Integer(vec![
                         NumValue {
@@ -733,14 +656,7 @@ fn validate_enum_integer_add_column_invalid() {
                         },
                     ]),
                 }),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }),
+            )),
             fill_with: Some("Critical".into()), // Not a valid enum name
         }],
     };
@@ -761,31 +677,27 @@ fn validate_enum_integer_add_column_invalid() {
 
 fn integer_priority_column(default: Option) -> ColumnDef {
     ColumnDef {
-        name: "priority".into(),
-        r#type: ColumnType::Complex(ComplexColumnType::Enum {
-            name: "priority_level".into(),
-            values: EnumValues::Integer(vec![
-                NumValue {
-                    name: "low".into(),
-                    value: 0,
-                },
-                NumValue {
-                    name: "normal".into(),
-                    value: 10,
-                },
-                NumValue {
-                    name: "high".into(),
-                    value: 20,
-                },
-            ]),
-        }),
-        nullable: false,
         default,
-        comment: None,
-        primary_key: None,
-        unique: None,
-        index: None,
-        foreign_key: None,
+        ..col(
+            "priority",
+            ColumnType::Complex(ComplexColumnType::Enum {
+                name: "priority_level".into(),
+                values: EnumValues::Integer(vec![
+                    NumValue {
+                        name: "low".into(),
+                        value: 0,
+                    },
+                    NumValue {
+                        name: "normal".into(),
+                        value: 10,
+                    },
+                    NumValue {
+                        name: "high".into(),
+                        value: 20,
+                    },
+                ]),
+            }),
+        )
     }
 }
 
@@ -863,18 +775,14 @@ fn validate_enum_null_value_skipped() {
         actions: vec![MigrationAction::AddColumn {
             table: "users".into(),
             column: Box::new(ColumnDef {
-                name: "status".into(),
-                r#type: ColumnType::Complex(ComplexColumnType::Enum {
-                    name: "user_status".into(),
-                    values: EnumValues::String(vec!["active".into(), "inactive".into()]),
-                }),
-                nullable: true,
                 default: Some("NULL".into()),
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
+                ..col_nullable(
+                    "status",
+                    ColumnType::Complex(ComplexColumnType::Enum {
+                        name: "user_status".into(),
+                        values: EnumValues::String(vec!["active".into(), "inactive".into()]),
+                    }),
+                )
             }),
             fill_with: None,
         }],
@@ -894,20 +802,13 @@ fn validate_enum_sql_expression_skipped() {
         version: 1,
         actions: vec![MigrationAction::AddColumn {
             table: "users".into(),
-            column: Box::new(ColumnDef {
-                name: "status".into(),
-                r#type: ColumnType::Complex(ComplexColumnType::Enum {
+            column: Box::new(col_nullable(
+                "status",
+                ColumnType::Complex(ComplexColumnType::Enum {
                     name: "user_status".into(),
                     values: EnumValues::String(vec!["active".into(), "inactive".into()]),
                 }),
-                nullable: true,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }),
+            )),
             fill_with: Some("COALESCE(old_status, 'active')".into()),
         }],
     };
@@ -927,20 +828,13 @@ fn validate_enum_empty_string_fill_with_skipped() {
         version: 1,
         actions: vec![MigrationAction::AddColumn {
             table: "users".into(),
-            column: Box::new(ColumnDef {
-                name: "status".into(),
-                r#type: ColumnType::Complex(ComplexColumnType::Enum {
+            column: Box::new(col_nullable(
+                "status",
+                ColumnType::Complex(ComplexColumnType::Enum {
                     name: "user_status".into(),
                     values: EnumValues::String(vec!["active".into(), "inactive".into()]),
                 }),
-                nullable: true,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }),
+            )),
             // Empty string - extract_enum_value returns None for empty trimmed values
             fill_with: Some("   ".into()),
         }],
@@ -959,18 +853,14 @@ fn string_enum_default_plan(default: &str) -> MigrationPlan {
         actions: vec![MigrationAction::AddColumn {
             table: "users".into(),
             column: Box::new(ColumnDef {
-                name: "status".into(),
-                r#type: ColumnType::Complex(ComplexColumnType::Enum {
-                    name: "user_status".into(),
-                    values: EnumValues::String(vec!["active".into(), "inactive".into()]),
-                }),
-                nullable: true,
                 default: Some(default.into()),
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
+                ..col_nullable(
+                    "status",
+                    ColumnType::Complex(ComplexColumnType::Enum {
+                        name: "user_status".into(),
+                        values: EnumValues::String(vec!["active".into(), "inactive".into()]),
+                    }),
+                )
             }),
             fill_with: None,
         }],
@@ -1000,24 +890,14 @@ fn validate_enum_rejects_unbalanced_quoted_defaults(#[case] default: &str) {
 /// user to fix-and-rerun for each one.
 #[test]
 fn validate_migration_plan_batches_multiple_violations() {
-    use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
+    use vespertide_core::{ColumnType, MigrationAction, MigrationPlan};
 
     // Helper: build a NOT NULL AddColumn action without a default or
     // fill_with — guaranteed to trigger MissingFillWith.
     fn add_not_null(table: &str, column: &str) -> MigrationAction {
         MigrationAction::AddColumn {
             table: table.into(),
-            column: Box::new(ColumnDef {
-                name: column.into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Text),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }),
+            column: Box::new(col(column, ColumnType::Simple(SimpleColumnType::Text))),
             fill_with: None,
         }
     }
@@ -1073,7 +953,7 @@ fn validate_migration_plan_batches_multiple_violations() {
 /// after the batch change.
 #[test]
 fn validate_migration_plan_single_violation_returns_bare_variant() {
-    use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
+    use vespertide_core::{ColumnType, MigrationAction, MigrationPlan};
 
     let plan = MigrationPlan {
         id: String::new(),
@@ -1082,17 +962,7 @@ fn validate_migration_plan_single_violation_returns_bare_variant() {
         version: 1,
         actions: vec![MigrationAction::AddColumn {
             table: "users".into(),
-            column: Box::new(ColumnDef {
-                name: "email".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Text),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }),
+            column: Box::new(col("email", ColumnType::Simple(SimpleColumnType::Text))),
             fill_with: None,
         }],
     };
diff --git a/crates/vespertide-query/src/builder/mod.rs b/crates/vespertide-query/src/builder/mod.rs
index c4455746..bba8f7c0 100644
--- a/crates/vespertide-query/src/builder/mod.rs
+++ b/crates/vespertide-query/src/builder/mod.rs
@@ -127,7 +127,7 @@ pub fn build_plan_queries_with_options(
 mod tests {
     use super::*;
     use crate::sql::DatabaseBackend;
-    use crate::test_support::{col, joined_sql_semicolon};
+    use crate::test_support::{backend_tag, col, joined_sql_semicolon};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{
@@ -306,40 +306,20 @@ mod tests {
         assert_eq!(result.len(), 2);
 
         // Test PostgreSQL output
-        let sql1 = result[0]
-            .postgres
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Postgres))
-            .collect::>()
-            .join(";\n");
+        let sql1 = joined_sql_semicolon(DatabaseBackend::Postgres, &result[0].postgres);
         assert!(sql1.contains("CREATE TABLE"));
         assert!(sql1.contains("\"users\""));
         assert!(sql1.contains("\"id\""));
 
-        let sql2 = result[1]
-            .postgres
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Postgres))
-            .collect::>()
-            .join(";\n");
+        let sql2 = joined_sql_semicolon(DatabaseBackend::Postgres, &result[1].postgres);
         assert!(sql2.contains("DROP TABLE"));
         assert!(sql2.contains("\"posts\""));
 
         // Test MySQL output
-        let sql1_mysql = result[0]
-            .mysql
-            .iter()
-            .map(|q| q.build(DatabaseBackend::MySql))
-            .collect::>()
-            .join(";\n");
+        let sql1_mysql = joined_sql_semicolon(DatabaseBackend::MySql, &result[0].mysql);
         assert!(sql1_mysql.contains("`users`"));
 
-        let sql2_mysql = result[1]
-            .mysql
-            .iter()
-            .map(|q| q.build(DatabaseBackend::MySql))
-            .collect::>()
-            .join(";\n");
+        let sql2_mysql = joined_sql_semicolon(DatabaseBackend::MySql, &result[1].mysql);
         assert!(sql2_mysql.contains("`posts`"));
     }
 
@@ -601,11 +581,12 @@ mod tests {
         assert_no_orphan_duplicate_indexes(&result);
 
         // Snapshot per backend
-        for (backend, label) in [
-            (DatabaseBackend::Postgres, "postgres"),
-            (DatabaseBackend::MySql, "mysql"),
-            (DatabaseBackend::Sqlite, "sqlite"),
+        for backend in [
+            DatabaseBackend::Postgres,
+            DatabaseBackend::MySql,
+            DatabaseBackend::Sqlite,
         ] {
+            let label = backend_tag(backend);
             let sql = collect_all_sql(&result, backend);
             with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("add_col_{}_{}", title, label) }, {
                 assert_snapshot!(sql);
@@ -631,11 +612,12 @@ mod tests {
         let result = build_plan_queries(&plan, &schema).unwrap();
 
         // Snapshot per backend
-        for (backend, label) in [
-            (DatabaseBackend::Postgres, "postgres"),
-            (DatabaseBackend::MySql, "mysql"),
-            (DatabaseBackend::Sqlite, "sqlite"),
+        for backend in [
+            DatabaseBackend::Postgres,
+            DatabaseBackend::MySql,
+            DatabaseBackend::Sqlite,
         ] {
+            let label = backend_tag(backend);
             let sql = collect_all_sql(&result, backend);
             with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("rm_col_{}_{}", title, label) }, {
                 assert_snapshot!(sql);
@@ -659,11 +641,12 @@ mod tests {
         assert_no_duplicate_indexes_per_action(&result);
         assert_no_orphan_duplicate_indexes(&result);
 
-        for (backend, label) in [
-            (DatabaseBackend::Postgres, "postgres"),
-            (DatabaseBackend::MySql, "mysql"),
-            (DatabaseBackend::Sqlite, "sqlite"),
+        for backend in [
+            DatabaseBackend::Postgres,
+            DatabaseBackend::MySql,
+            DatabaseBackend::Sqlite,
         ] {
+            let label = backend_tag(backend);
             let sql = collect_all_sql(&result, backend);
             with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("add_col_pair_{}_{}", title, label) }, {
                 assert_snapshot!(sql);
@@ -687,11 +670,12 @@ mod tests {
         assert_no_duplicate_indexes_per_action(&result);
         assert_no_orphan_duplicate_indexes(&result);
 
-        for (backend, label) in [
-            (DatabaseBackend::Postgres, "postgres"),
-            (DatabaseBackend::MySql, "mysql"),
-            (DatabaseBackend::Sqlite, "sqlite"),
+        for backend in [
+            DatabaseBackend::Postgres,
+            DatabaseBackend::MySql,
+            DatabaseBackend::Sqlite,
         ] {
+            let label = backend_tag(backend);
             let sql = collect_all_sql(&result, backend);
             with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("add_col_pair_{}_{}", title, label) }, {
                 assert_snapshot!(sql);
diff --git a/crates/vespertide-query/src/sql/add_column.rs b/crates/vespertide-query/src/sql/add_column.rs
index 4a6811a9..be7bb0c9 100644
--- a/crates/vespertide-query/src/sql/add_column.rs
+++ b/crates/vespertide-query/src/sql/add_column.rs
@@ -158,7 +158,7 @@ pub fn build_add_column(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::joined_sql;
+    use crate::test_support::{joined_sql, joined_sql_semicolon};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{ColumnType, SimpleColumnType, TableDef};
@@ -479,11 +479,7 @@ mod tests {
         let result = build_add_column(backend, "users", &column, None, ¤t_schema, &[]);
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join(";\n");
+        let sql = joined_sql_semicolon(backend, &queries);
 
         with_settings!({ snapshot_suffix => format!("add_column_with_enum_type_{:?}", backend) }, {
             assert_snapshot!(sql);
@@ -536,11 +532,7 @@ mod tests {
         let result = build_add_column(backend, "users", &column, None, ¤t_schema, &[]);
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join(";\n");
+        let sql = joined_sql_semicolon(backend, &queries);
 
         with_settings!({ snapshot_suffix => format!("enum_non_nullable_with_default_{:?}", backend) }, {
             assert_snapshot!(sql);
@@ -585,11 +577,7 @@ mod tests {
         let result = build_add_column(backend, "users", &column, None, ¤t_schema, &[]);
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join(";\n");
+        let sql = joined_sql_semicolon(backend, &queries);
 
         // Verify empty string becomes ''
         assert!(
@@ -700,11 +688,7 @@ mod tests {
         let result = build_add_column(backend, "users", &column, Some(""), ¤t_schema, &[]);
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join(";\n");
+        let sql = joined_sql_semicolon(backend, &queries);
 
         // Verify empty string becomes ''
         assert!(
diff --git a/crates/vespertide-query/src/sql/add_constraint/foreign_key.rs b/crates/vespertide-query/src/sql/add_constraint/foreign_key.rs
index 29460b57..b7fd37e8 100644
--- a/crates/vespertide-query/src/sql/add_constraint/foreign_key.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/foreign_key.rs
@@ -189,7 +189,7 @@ mod tests {
     use rstest::rstest;
     use vespertide_core::{ColumnDef, ColumnType, MigrationAction, SimpleColumnType};
 
-    use crate::test_support::col_n;
+    use crate::test_support::{col_n, joined_sql};
 
     fn fk_constraint(strategy: ForeignKeyOrphanStrategy) -> TableConstraint {
         TableConstraint::ForeignKey {
@@ -292,11 +292,7 @@ mod tests {
             &[],
         )
         .unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(DatabaseBackend::Sqlite, &queries);
         assert!(sql.contains("CONSTRAINT \"chk_user_id\" CHECK"));
     }
 
@@ -322,11 +318,7 @@ mod tests {
             &[],
         )
         .unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(DatabaseBackend::Sqlite, &queries);
         assert!(sql.contains("CREATE INDEX"));
         assert!(sql.contains("idx_user_id"));
     }
@@ -354,14 +346,7 @@ mod tests {
             &[],
         )
         .unwrap();
-        assert!(
-            queries
-                .iter()
-                .map(|q| q.build(DatabaseBackend::Sqlite))
-                .collect::>()
-                .join("\n")
-                .contains("CREATE TABLE")
-        );
+        assert!(joined_sql(DatabaseBackend::Sqlite, &queries).contains("CREATE TABLE"));
     }
 
     #[test]
@@ -382,11 +367,7 @@ mod tests {
             &[],
         )
         .unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(DatabaseBackend::Sqlite, &queries);
         assert!(sql.contains("CREATE TABLE"));
         assert!(sql.contains("FOREIGN KEY"));
     }
@@ -433,11 +414,7 @@ mod tests {
             &[],
         )
         .unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Postgres))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(DatabaseBackend::Postgres, &queries);
         assert!(sql.contains("NOT VALID"));
         assert!(sql.contains("VALIDATE CONSTRAINT"));
         assert!(sql.contains("ON DELETE CASCADE") && sql.contains("ON UPDATE RESTRICT"));
@@ -551,11 +528,7 @@ mod tests {
             &[],
         )
         .unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
         assert!(sql.contains("UPDATE"));
         assert!(sql.contains("= NULL"));
         assert!(sql.contains("NOT EXISTS"));
@@ -587,12 +560,10 @@ mod tests {
             constraint,
         };
 
-        let sql = crate::sql::build_action_queries(backend, &action, &parent_child_schema())
-            .unwrap()
-            .iter()
-            .map(|query| query.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(
+            backend,
+            &crate::sql::build_action_queries(backend, &action, &parent_child_schema()).unwrap(),
+        );
 
         assert!(sql.contains("ON DELETE CASCADE"), "{sql}");
         assert!(sql.contains("ON UPDATE CASCADE"), "{sql}");
@@ -629,11 +600,7 @@ mod tests {
             &[],
         )
         .unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
         assert!(sql.contains("DELETE FROM"));
         assert!(sql.contains("NOT EXISTS"));
         assert!(
diff --git a/crates/vespertide-query/src/sql/add_constraint/primary_key.rs b/crates/vespertide-query/src/sql/add_constraint/primary_key.rs
index 0f75922a..a72f7eae 100644
--- a/crates/vespertide-query/src/sql/add_constraint/primary_key.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/primary_key.rs
@@ -89,8 +89,8 @@ fn build_pk_pre_cleanup>(
 
 #[cfg(test)]
 mod tests {
-    use super::*;
     use super::super::try_resolve_single_pk_column;
+    use super::*;
     use rstest::rstest;
     use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType};
 
diff --git a/crates/vespertide-query/src/sql/add_constraint/unique.rs b/crates/vespertide-query/src/sql/add_constraint/unique.rs
index f3199d92..079c028f 100644
--- a/crates/vespertide-query/src/sql/add_constraint/unique.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/unique.rs
@@ -93,8 +93,8 @@ fn build_unique_index>(table: &str, name: Option<&str>, columns: &
 
 #[cfg(test)]
 mod tests {
-    use super::*;
     use super::super::try_resolve_single_pk_column;
+    use super::*;
     use rstest::rstest;
     use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableConstraint};
 
diff --git a/crates/vespertide-query/src/sql/create_table.rs b/crates/vespertide-query/src/sql/create_table.rs
index 044b737e..3ebe1074 100644
--- a/crates/vespertide-query/src/sql/create_table.rs
+++ b/crates/vespertide-query/src/sql/create_table.rs
@@ -320,7 +320,7 @@ pub fn build_create_table(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_support::col;
+    use crate::test_support::{col, joined_sql};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{ColumnType, EnumValues, SimpleColumnType};
@@ -737,11 +737,7 @@ mod tests {
             },
         ];
         let queries = build_create_table(backend, "users", &columns, &constraints).unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
         // The CHECK arm in `add_create_table_constraints` is a documented
         // no-op (CHECK appears in a separate ALTER ADD CONSTRAINT statement,
         // not in CREATE TABLE itself). We assert the dispatch ran (PK lands)
diff --git a/crates/vespertide-query/src/sql/delete_column/mod.rs b/crates/vespertide-query/src/sql/delete_column/mod.rs
index aceaf2bd..f5aa7e39 100644
--- a/crates/vespertide-query/src/sql/delete_column/mod.rs
+++ b/crates/vespertide-query/src/sql/delete_column/mod.rs
@@ -123,7 +123,7 @@ fn sqlite_constraint_handling(
 mod tests {
     use super::*;
     use crate::sql::types::DatabaseBackend;
-    use crate::test_support::{col, joined_sql_semicolon};
+    use crate::test_support::{col, joined_sql, joined_sql_semicolon};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{ComplexColumnType, SimpleColumnType};
@@ -409,11 +409,7 @@ mod tests {
             result.len()
         );
 
-        let all_sql: Vec = result
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect();
-        let combined_sql = all_sql.join("\n");
+        let combined_sql = joined_sql(DatabaseBackend::Sqlite, &result);
 
         // Verify temp table creation
         assert!(
@@ -484,11 +480,7 @@ mod tests {
             &[],
         );
 
-        let all_sql: Vec = result
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect();
-        let combined_sql = all_sql.join("\n");
+        let combined_sql = joined_sql(DatabaseBackend::Sqlite, &result);
 
         // Should preserve other columns
         assert!(combined_sql.contains("\"id\""), "Should preserve id column");
@@ -591,11 +583,7 @@ mod tests {
             result.len()
         );
 
-        let all_sql: Vec = result
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect();
-        let combined_sql = all_sql.join("\n");
+        let combined_sql = joined_sql(DatabaseBackend::Sqlite, &result);
 
         assert!(
             combined_sql.contains("order_items_temp"),
@@ -711,11 +699,7 @@ mod tests {
             &[],
         );
 
-        let all_sql: Vec = result
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect();
-        let combined_sql = all_sql.join("\n");
+        let combined_sql = joined_sql(DatabaseBackend::Sqlite, &result);
 
         // Should use temp table approach (FK triggers it)
         assert!(
@@ -975,11 +959,7 @@ mod tests {
             result.len()
         );
 
-        let all_sql: Vec = result
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Sqlite))
-            .collect();
-        let combined_sql = all_sql.join("\n");
+        let combined_sql = joined_sql(DatabaseBackend::Sqlite, &result);
 
         // Verify temp table approach
         assert!(
@@ -1082,11 +1062,7 @@ mod tests {
         }];
 
         let queries = build_delete_column(backend, "people", "age", None, &schema, &[]);
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join("\n");
+        let sql = joined_sql(backend, &queries);
 
         if backend == DatabaseBackend::Sqlite {
             assert!(
diff --git a/crates/vespertide-query/src/sql/modify_column_type/mod.rs b/crates/vespertide-query/src/sql/modify_column_type/mod.rs
index aa4f67dc..193234c5 100644
--- a/crates/vespertide-query/src/sql/modify_column_type/mod.rs
+++ b/crates/vespertide-query/src/sql/modify_column_type/mod.rs
@@ -224,6 +224,7 @@ pub fn build_modify_column_type(
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::test_support::{backend_tag, joined_sql_semicolon};
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{
@@ -293,12 +294,7 @@ mod tests {
         );
 
         // SQLite may return multiple queries
-        let sql = result
-            .unwrap()
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join(";\n");
+        let sql = joined_sql_semicolon(backend, &result.unwrap());
 
         for exp in expected {
             assert!(
@@ -430,11 +426,7 @@ mod tests {
         )
         .unwrap();
 
-        let sql = result
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join(";\n");
+        let sql = joined_sql_semicolon(backend, &result);
 
         // For SQLite, should recreate index
         if matches!(backend, DatabaseBackend::Sqlite) {
@@ -514,11 +506,7 @@ mod tests {
         )
         .unwrap();
 
-        let sql = result
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join(";\n");
+        let sql = joined_sql_semicolon(backend, &result);
 
         // For SQLite, unique constraint should be in CREATE TABLE statement
         if matches!(backend, DatabaseBackend::Sqlite) {
@@ -727,11 +715,7 @@ mod tests {
         )
         .unwrap();
 
-        let sql = result
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join(";\n");
+        let sql = joined_sql_semicolon(backend, &result);
 
         with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("modify_enum_types_{}", title) }, {
             assert_snapshot!(sql);
@@ -794,11 +778,7 @@ mod tests {
         )
         .unwrap();
 
-        let sql = result
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join(";\n");
+        let sql = joined_sql_semicolon(backend, &result);
 
         // PostgreSQL-specific: verify DROP DEFAULT -> TYPE change -> SET DEFAULT order
         if matches!(backend, DatabaseBackend::Postgres) {
@@ -851,11 +831,7 @@ mod tests {
 
         assert!(result.is_ok());
         let queries = result.unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(DatabaseBackend::Postgres))
-            .collect::>()
-            .join(";\n");
+        let sql = joined_sql_semicolon(DatabaseBackend::Postgres, &queries);
 
         // Should create the enum type since old_type is None
         assert!(sql.contains("CREATE TYPE"));
@@ -904,11 +880,7 @@ mod tests {
             &baseline,
             &[],
         )?;
-        Ok(queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join(";\n"))
+        Ok(joined_sql_semicolon(backend, &queries))
     }
 
     fn snap_tz(name: &str, sql: &str) {
@@ -927,11 +899,12 @@ mod tests {
         ($name:ident, $old:expr, $new:expr, $tz:expr) => {
             #[test]
             fn $name() {
-                for (backend, tag) in [
-                    (DatabaseBackend::Postgres, "postgres"),
-                    (DatabaseBackend::MySql, "mysql"),
-                    (DatabaseBackend::Sqlite, "sqlite"),
+                for backend in [
+                    DatabaseBackend::Postgres,
+                    DatabaseBackend::MySql,
+                    DatabaseBackend::Sqlite,
                 ] {
+                    let tag = backend_tag(backend);
                     let sql = run_with_tz(backend, $old, &$new, $tz)
                         .expect("timezone conversion across backends");
                     snap_tz(&format!("{}_{}", stringify!($name), tag), &sql);
@@ -1070,11 +1043,7 @@ mod tests {
             &[],
         )
         .unwrap();
-        let sql = queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join(";\n");
+        let sql = joined_sql_semicolon(backend, &queries);
         // The pre-cleanup phase touches "name" before the type change.
         assert!(
             sql.contains("UPDATE") || sql.contains("name"),
@@ -1156,11 +1125,7 @@ mod tests {
         )
         .unwrap();
 
-        let sql = result
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join(";\n");
+        let sql = joined_sql_semicolon(backend, &result);
 
         // All backends should include the UPDATE statement for fill_with
         assert!(
diff --git a/crates/vespertide-query/src/sql/modify_column_type/narrowing_preprocess.rs b/crates/vespertide-query/src/sql/modify_column_type/narrowing_preprocess.rs
index cf1faa15..51821f70 100644
--- a/crates/vespertide-query/src/sql/modify_column_type/narrowing_preprocess.rs
+++ b/crates/vespertide-query/src/sql/modify_column_type/narrowing_preprocess.rs
@@ -303,6 +303,7 @@ mod integer_bounds_tests {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::test_support::{backend_tag, joined_sql_semicolon};
     use insta::{assert_snapshot, with_settings};
     use vespertide_core::{ColumnDef, ColumnType, ComplexColumnType, SimpleColumnType, TableDef};
 
@@ -353,11 +354,7 @@ mod tests {
     ) -> Result {
         let baseline = baseline_with(old);
         let queries = build_narrowing_preprocess(backend, "tbl", "col", new, strategy, &baseline)?;
-        Ok(queries
-            .iter()
-            .map(|q| q.build(backend))
-            .collect::>()
-            .join(";\n"))
+        Ok(joined_sql_semicolon(backend, &queries))
     }
 
     fn snap(name: &str, sql: &str) {
@@ -377,11 +374,12 @@ mod tests {
         ($name:ident, $old:expr, $new:expr, $strategy:expr) => {
             #[test]
             fn $name() {
-                for (backend, tag) in [
-                    (DatabaseBackend::Postgres, "postgres"),
-                    (DatabaseBackend::MySql, "mysql"),
-                    (DatabaseBackend::Sqlite, "sqlite"),
+                for backend in [
+                    DatabaseBackend::Postgres,
+                    DatabaseBackend::MySql,
+                    DatabaseBackend::Sqlite,
                 ] {
+                    let tag = backend_tag(backend);
                     let sql = run(backend, $old, &$new, &$strategy)
                         .expect("supported (kind, strategy) combo");
                     snap(&format!("preprocess_{}_{}", stringify!($name), tag), &sql);
diff --git a/crates/vespertide-query/src/sql/remap_enum_values.rs b/crates/vespertide-query/src/sql/remap_enum_values.rs
index 4441763e..45d71576 100644
--- a/crates/vespertide-query/src/sql/remap_enum_values.rs
+++ b/crates/vespertide-query/src/sql/remap_enum_values.rs
@@ -54,6 +54,7 @@ pub fn build_remap_enum_values(
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::test_support::backend_tag;
     use insta::{assert_snapshot, with_settings};
 
     fn run(backend: DatabaseBackend, mapping: &[(i64, i64)]) -> String {
@@ -83,11 +84,12 @@ mod tests {
         ($name:ident, $mapping:expr) => {
             #[test]
             fn $name() {
-                for (backend, tag) in [
-                    (DatabaseBackend::Postgres, "postgres"),
-                    (DatabaseBackend::MySql, "mysql"),
-                    (DatabaseBackend::Sqlite, "sqlite"),
+                for backend in [
+                    DatabaseBackend::Postgres,
+                    DatabaseBackend::MySql,
+                    DatabaseBackend::Sqlite,
                 ] {
+                    let tag = backend_tag(backend);
                     let sql = run(backend, $mapping);
                     snap(&format!("{}_{}", stringify!($name), tag), &sql);
                 }
diff --git a/crates/vespertide-query/src/sql/remove_constraint/mod.rs b/crates/vespertide-query/src/sql/remove_constraint/mod.rs
index a1d3a395..aab178a5 100644
--- a/crates/vespertide-query/src/sql/remove_constraint/mod.rs
+++ b/crates/vespertide-query/src/sql/remove_constraint/mod.rs
@@ -71,6 +71,7 @@ fn constraint_kind(constraint: &TableConstraint) -> &'static str {
 mod tests {
     use super::*;
     use crate::sql::types::DatabaseBackend;
+    use crate::test_support::joined_sql;
     use insta::{assert_snapshot, with_settings};
     use rstest::rstest;
     use vespertide_core::{
@@ -159,12 +160,10 @@ mod tests {
         constraint: &TableConstraint,
         schema: &[TableDef],
     ) -> String {
-        build_remove_constraint(backend, table_name, constraint, schema, &[])
-            .unwrap()
-            .iter()
-            .map(|query| query.build(backend))
-            .collect::>()
-            .join("\n")
+        joined_sql(
+            backend,
+            &build_remove_constraint(backend, table_name, constraint, schema, &[]).unwrap(),
+        )
     }
 
     fn assert_rendered(
diff --git a/crates/vespertide-query/src/sql/tests/naming.rs b/crates/vespertide-query/src/sql/tests/naming.rs
index 3ba06843..9b33a400 100644
--- a/crates/vespertide-query/src/sql/tests/naming.rs
+++ b/crates/vespertide-query/src/sql/tests/naming.rs
@@ -1,5 +1,5 @@
 use super::*;
-use crate::test_support::backend_tag;
+use crate::test_support::{backend_tag, joined_sql, joined_sql_semicolon};
 use vespertide_core::TableDef;
 
 // Comprehensive unique constraint naming tests
@@ -54,11 +54,7 @@ fn test_add_unique_with_custom_name(
     }];
 
     let result = build_action_queries(backend, &action, ¤t_schema).unwrap();
-    let sql = result
-        .iter()
-        .map(|q| q.build(backend))
-        .collect::>()
-        .join("\n");
+    let sql = joined_sql(backend, &result);
 
     // Should use uq_table__name pattern
     let expected_name = format!("uq_user__{constraint_name}");
@@ -123,11 +119,7 @@ fn test_add_unnamed_unique(#[case] backend: DatabaseBackend, #[case] columns: Ve
     }];
 
     let result = build_action_queries(backend, &action, ¤t_schema).unwrap();
-    let sql = result
-        .iter()
-        .map(|q| q.build(backend))
-        .collect::>()
-        .join("\n");
+    let sql = joined_sql(backend, &result);
 
     // Should use uq_table__col1_col2... pattern
     let expected_name = format!("uq_user__{}", columns.join("_"));
@@ -194,11 +186,7 @@ fn test_remove_unique_with_custom_name(
     };
 
     let result = build_action_queries(backend, &action, ¤t_schema).unwrap();
-    let sql = result
-        .iter()
-        .map(|q| q.build(backend))
-        .collect::>()
-        .join("\n");
+    let sql = joined_sql(backend, &result);
 
     // Should use uq_table__name pattern (for Postgres/MySQL, not SQLite which rebuilds table)
     if backend != DatabaseBackend::Sqlite {
@@ -267,11 +255,7 @@ fn test_remove_unnamed_unique(#[case] backend: DatabaseBackend, #[case] columns:
     };
 
     let result = build_action_queries(backend, &action, ¤t_schema).unwrap();
-    let sql = result
-        .iter()
-        .map(|q| q.build(backend))
-        .collect::>()
-        .join("\n");
+    let sql = joined_sql(backend, &result);
 
     // Should use uq_table__col1_col2... pattern (for Postgres/MySQL, not SQLite which rebuilds table)
     if backend != DatabaseBackend::Sqlite {
@@ -318,11 +302,7 @@ fn test_build_action_queries_modify_column_nullable(#[case] backend: DatabaseBac
     }];
     let result = build_action_queries(backend, &action, ¤t_schema).unwrap();
     assert!(!result.is_empty());
-    let sql = result
-        .iter()
-        .map(|q| q.build(backend))
-        .collect::>()
-        .join("\n");
+    let sql = joined_sql(backend, &result);
 
     // Should contain UPDATE for fill_with and ALTER for nullable change
     assert!(sql.contains("UPDATE"));
@@ -365,11 +345,7 @@ fn test_build_action_queries_modify_column_default(#[case] backend: DatabaseBack
     }];
     let result = build_action_queries(backend, &action, ¤t_schema).unwrap();
     assert!(!result.is_empty());
-    let sql = result
-        .iter()
-        .map(|q| q.build(backend))
-        .collect::>()
-        .join("\n");
+    let sql = joined_sql(backend, &result);
 
     // Should contain DEFAULT and 'active'
     assert!(sql.contains("DEFAULT") || sql.contains("active"));
@@ -409,11 +385,7 @@ fn test_build_action_queries_modify_column_comment(#[case] backend: DatabaseBack
         constraints: vec![],
     }];
     let result = build_action_queries(backend, &action, ¤t_schema).unwrap();
-    let sql = result
-        .iter()
-        .map(|q| q.build(backend))
-        .collect::>()
-        .join("\n");
+    let sql = joined_sql(backend, &result);
 
     // Postgres and MySQL should have comment, SQLite returns empty
     if backend != DatabaseBackend::Sqlite {
@@ -463,11 +435,7 @@ fn test_create_table_with_function_default(#[case] backend: DatabaseBackend) {
         constraints: vec![],
     };
     let result = build_action_queries(backend, &action, &[]).unwrap();
-    let sql = result
-        .iter()
-        .map(|q| q.build(backend))
-        .collect::>()
-        .join(";\n");
+    let sql = joined_sql_semicolon(backend, &result);
 
     with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("create_table_func_default_{:?}", backend) }, {
         assert_snapshot!(sql);
@@ -511,11 +479,7 @@ fn test_delete_column_with_enum_type(#[case] backend: DatabaseBackend) {
         constraints: vec![],
     }];
     let result = build_action_queries(backend, &action, &schema).unwrap();
-    let sql = result
-        .iter()
-        .map(|q| q.build(backend))
-        .collect::>()
-        .join(";\n");
+    let sql = joined_sql_semicolon(backend, &result);
 
     with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("delete_enum_column_{:?}", backend) }, {
         assert_snapshot!(sql);
@@ -573,11 +537,7 @@ fn test_replace_fk_constraint(#[case] backend: DatabaseBackend) {
         },
     };
     let result = build_action_queries(backend, &action, &schema).unwrap();
-    let sql = result
-        .iter()
-        .map(|q| q.build(backend))
-        .collect::>()
-        .join(";\n");
+    let sql = joined_sql_semicolon(backend, &result);
 
     let suffix = format!("replace_fk_constraint_{}", backend_tag(backend));
 
diff --git a/crates/vespertide-query/src/test_support.rs b/crates/vespertide-query/src/test_support.rs
index ff9afc54..38854a97 100644
--- a/crates/vespertide-query/src/test_support.rs
+++ b/crates/vespertide-query/src/test_support.rs
@@ -51,10 +51,8 @@ pub(crate) fn joined_sql(
 /// Same shape as [`joined_sql`] but uses `";\n"` as the separator — matches
 /// the canonical multi-statement SQL formatting that `builder::mod` and
 /// `sql::delete_column::mod` snapshot. Hoisted from two byte-identical
-/// `build_sql_snapshot` helpers that lived in those modules pre-0.2.0; the
-/// broader inline `.join(";\n")` adoption pass across the rest of the
-/// crate is deliberately deferred to a follow-up iteration to keep each
-/// dedupe item independently revertible.
+/// `build_sql_snapshot` helpers that lived in those modules pre-0.2.0;
+/// adopted crate-wide wherever a test joins built queries with `";\n"`.
 pub(crate) fn joined_sql_semicolon(
     backend: crate::sql::DatabaseBackend,
     queries: &[crate::sql::BuiltQuery],
@@ -69,8 +67,9 @@ pub(crate) fn joined_sql_semicolon(
 /// Snapshot-suffix tag for a backend. The string is part of the snapshot
 /// file name, so changing it would invalidate every `assert_snapshot!` that
 /// uses it — keep it byte-stable. Hoisted from a `match backend { ... }`
-/// block repeated across `sql/tests/naming.rs` (and several
-/// `sql/modify_column_*` tests, left for a follow-up iteration).
+/// block repeated across `sql/tests/naming.rs` and adopted by the
+/// backend fan-out loops in `builder`, `remap_enum_values`, and the
+/// `modify_column_type` test modules.
 pub(crate) fn backend_tag(backend: crate::sql::DatabaseBackend) -> &'static str {
     match backend {
         crate::sql::DatabaseBackend::Postgres => "postgres",

From bca0bf6d59041f157ddd2093da44b119b5bf9fef Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 02:40:57 +0900
Subject: [PATCH 056/109] retry-now#0055: batch - drop dead CHECK-scan
 disjunct, dedupe SQLite CHECK injection, rstest naming tests, curate query
 helper exports, unify loader pipeline (5 kept)

---
 Cargo.lock                                    |   1 +
 crates/vespertide-loader/src/models.rs        |  24 +--
 crates/vespertide-naming/Cargo.toml           |   1 +
 crates/vespertide-naming/src/lib.rs           | 167 ++++++------------
 .../vespertide-query/src/sql/create_table.rs  |  19 +-
 .../src/sql/delete_column/mod.rs              |   4 +-
 crates/vespertide-query/src/sql/helpers.rs    |  66 ++++---
 crates/vespertide-query/src/sql/mod.rs        |   2 +-
 .../vespertide-query/src/sql/tests/helpers.rs |   6 +-
 9 files changed, 124 insertions(+), 166 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 50fd8f76..1e530443 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2965,6 +2965,7 @@ version = "0.2.1"
 dependencies = [
  "criterion",
  "proptest",
+ "rstest",
 ]
 
 [[package]]
diff --git a/crates/vespertide-loader/src/models.rs b/crates/vespertide-loader/src/models.rs
index 32c65ca9..79689487 100644
--- a/crates/vespertide-loader/src/models.rs
+++ b/crates/vespertide-loader/src/models.rs
@@ -135,23 +135,15 @@ fn load_models_recursive_internal(
     Ok(())
 }
 
+/// Compile-time variant of [`load_model_file`]: same read → parse → validate
+/// pipeline (delegated so extension dispatch and validation live in exactly
+/// one place), followed by normalization — the macro path needs inline
+/// constraints resolved to table level.
 fn load_normalized_model_file_internal(path: &Path) -> Result {
-    let ext = path.extension().and_then(|s| s.to_str());
-    let content = fs::read_to_string(path)
-        .map_err(|e| format!("Failed to read model file {}: {}", path.display(), e))?;
-
-    let table: TableDef = if ext == Some("json") {
-        serde_json::from_str(&content)
-            .map_err(|e| format!("Failed to parse JSON model {}: {}", path.display(), e))?
-    } else {
-        serde_yaml::from_str(&content)
-            .map_err(|e| format!("Failed to parse YAML model {}: {}", path.display(), e))?
-    };
-
-    table
-        .validate_unique_column_names()
-        .map_err(|e| format!("Failed to validate model {}: {}", path.display(), e))?;
-
+    // anyhow's alternate format renders the context chain as
+    // "parse JSON model: : "; prefixing "Failed to " keeps the
+    // messages this pipeline has always produced.
+    let table = load_model_file(path).map_err(|e| format!("Failed to {e:#}"))?;
     table
         .normalize()
         .map_err(|e| format!("Failed to normalize table '{}': {}", table.name, e))
diff --git a/crates/vespertide-naming/Cargo.toml b/crates/vespertide-naming/Cargo.toml
index 0daeee8a..f8f31970 100644
--- a/crates/vespertide-naming/Cargo.toml
+++ b/crates/vespertide-naming/Cargo.toml
@@ -16,6 +16,7 @@ readme = "../../README.md"
 [dev-dependencies]
 criterion = { version = "0.8", features = ["html_reports"] }
 proptest = "1"
+rstest = "0.26"
 
 [[bench]]
 name = "naming_benchmarks"
diff --git a/crates/vespertide-naming/src/lib.rs b/crates/vespertide-naming/src/lib.rs
index de7e4bc2..fae44e7d 100644
--- a/crates/vespertide-naming/src/lib.rs
+++ b/crates/vespertide-naming/src/lib.rs
@@ -110,123 +110,68 @@ pub fn build_enum_type_name(table: &str, enum_name: &str) -> String {
 
 #[cfg(test)]
 mod tests {
+    use rstest::rstest;
+
     use super::*;
 
     // ========================================================================
     // Constraint Naming Tests
     // ========================================================================
 
-    #[test]
-    fn test_build_index_name_with_key() {
-        assert_eq!(
-            build_index_name("users", &["email"], Some("email_idx")),
-            "ix_users__email_idx"
-        );
-    }
-
-    #[test]
-    fn test_build_index_name_without_key() {
-        assert_eq!(
-            build_index_name("users", &["email"], None),
-            "ix_users__email"
-        );
-    }
-
-    #[test]
-    fn test_build_index_name_multiple_columns() {
-        assert_eq!(
-            build_index_name("users", &["first_name", "last_name"], None),
-            "ix_users__first_name_last_name"
-        );
-    }
-
-    #[test]
-    fn test_build_index_name_multiple_columns_is_deterministic() {
-        assert_eq!(
-            build_index_name("users", &["last_name", "first_name"], None),
-            build_index_name("users", &["first_name", "last_name"], None)
-        );
-    }
-
-    #[test]
-    fn test_build_index_name_sorts_columns_for_deterministic_name() {
-        let columns = vec!["last_name".to_string(), "first_name".to_string()];
-        let reversed = vec!["first_name".to_string(), "last_name".to_string()];
-
-        assert_eq!(
-            build_index_name("users", &columns, None),
-            build_index_name("users", &reversed, None)
-        );
-    }
-
-    #[test]
-    fn test_build_unique_constraint_name_with_key() {
-        assert_eq!(
-            build_unique_constraint_name("users", &["email"], Some("email_unique")),
-            "uq_users__email_unique"
-        );
-    }
-
-    #[test]
-    fn test_build_unique_constraint_name_without_key() {
-        assert_eq!(
-            build_unique_constraint_name("users", &["email"], None),
-            "uq_users__email"
-        );
-    }
-
-    #[test]
-    fn test_build_unique_constraint_name_multiple_columns_is_deterministic() {
-        assert_eq!(
-            build_unique_constraint_name("users", &["last_name", "first_name"], None),
-            build_unique_constraint_name("users", &["first_name", "last_name"], None)
-        );
-    }
-
-    #[test]
-    fn test_build_unique_constraint_name_sorts_columns_for_deterministic_name() {
-        let columns = vec!["product_id".to_string(), "order_id".to_string()];
-        let reversed = vec!["order_id".to_string(), "product_id".to_string()];
-
-        assert_eq!(
-            build_unique_constraint_name("order_items", &columns, None),
-            build_unique_constraint_name("order_items", &reversed, None)
-        );
-    }
-
-    #[test]
-    fn test_build_foreign_key_name_with_key() {
-        assert_eq!(
-            build_foreign_key_name("posts", &["user_id"], Some("fk_user")),
-            "fk_posts__fk_user"
-        );
-    }
-
-    #[test]
-    fn test_build_foreign_key_name_without_key() {
-        assert_eq!(
-            build_foreign_key_name("posts", &["user_id"], None),
-            "fk_posts__user_id"
-        );
-    }
-
-    #[test]
-    fn test_build_foreign_key_name_multiple_columns_is_deterministic() {
-        assert_eq!(
-            build_foreign_key_name("posts", &["tenant_id", "user_id"], None),
-            build_foreign_key_name("posts", &["user_id", "tenant_id"], None)
-        );
-    }
-
-    #[test]
-    fn test_build_foreign_key_name_sorts_columns_for_deterministic_name() {
-        let columns = vec!["tenant_id".to_string(), "account_id".to_string()];
-        let reversed = vec!["account_id".to_string(), "tenant_id".to_string()];
-
-        assert_eq!(
-            build_foreign_key_name("memberships", &columns, None),
-            build_foreign_key_name("memberships", &reversed, None)
-        );
+    /// The three builders monomorphized for `&[&str]` columns.
+    /// Column strings are `'static` so the generic builders (whose column
+    /// lifetime is fixed at instantiation) can coerce to a single fn pointer.
+    type BuildFn = fn(&str, &[&'static str], Option<&str>) -> String;
+    /// The three builders monomorphized for `&[String]` columns.
+    type BuildStringFn = fn(&str, &[String], Option<&str>) -> String;
+
+    #[rstest]
+    #[case::index_with_key(build_index_name, "users", &["email"][..], Some("email_idx"), "ix_users__email_idx")]
+    #[case::index_without_key(build_index_name, "users", &["email"][..], None, "ix_users__email")]
+    #[case::index_multiple_columns(build_index_name, "users", &["first_name", "last_name"][..], None, "ix_users__first_name_last_name")]
+    #[case::unique_with_key(build_unique_constraint_name, "users", &["email"][..], Some("email_unique"), "uq_users__email_unique")]
+    #[case::unique_without_key(build_unique_constraint_name, "users", &["email"][..], None, "uq_users__email")]
+    #[case::foreign_key_with_key(build_foreign_key_name, "posts", &["user_id"][..], Some("fk_user"), "fk_posts__fk_user")]
+    #[case::foreign_key_without_key(build_foreign_key_name, "posts", &["user_id"][..], None, "fk_posts__user_id")]
+    fn constraint_name_builders_render_expected(
+        #[case] build: BuildFn,
+        #[case] table: &str,
+        #[case] columns: &[&'static str],
+        #[case] key: Option<&str>,
+        #[case] expected: &str,
+    ) {
+        assert_eq!(build(table, columns, key), expected);
+    }
+
+    /// Column order must not affect the generated name (`&[&str]` instantiation).
+    #[rstest]
+    #[case::index(build_index_name, "users", &["last_name", "first_name"][..])]
+    #[case::unique(build_unique_constraint_name, "users", &["last_name", "first_name"][..])]
+    #[case::foreign_key(build_foreign_key_name, "posts", &["tenant_id", "user_id"][..])]
+    fn constraint_name_builders_sort_str_columns_for_deterministic_name(
+        #[case] build: BuildFn,
+        #[case] table: &str,
+        #[case] columns: &[&'static str],
+    ) {
+        let mut reversed = columns.to_vec();
+        reversed.reverse();
+        assert_eq!(build(table, columns, None), build(table, &reversed, None));
+    }
+
+    /// Column order must not affect the generated name (`&[String]` instantiation).
+    #[rstest]
+    #[case::index(build_index_name, "users", &["last_name", "first_name"][..])]
+    #[case::unique(build_unique_constraint_name, "order_items", &["product_id", "order_id"][..])]
+    #[case::foreign_key(build_foreign_key_name, "memberships", &["tenant_id", "account_id"][..])]
+    fn constraint_name_builders_sort_string_columns_for_deterministic_name(
+        #[case] build: BuildStringFn,
+        #[case] table: &str,
+        #[case] columns: &[&str],
+    ) {
+        let columns: Vec = columns.iter().map(ToString::to_string).collect();
+        let mut reversed = columns.clone();
+        reversed.reverse();
+        assert_eq!(build(table, &columns, None), build(table, &reversed, None));
     }
 
     #[test]
diff --git a/crates/vespertide-query/src/sql/create_table.rs b/crates/vespertide-query/src/sql/create_table.rs
index 3ebe1074..656f86ce 100644
--- a/crates/vespertide-query/src/sql/create_table.rs
+++ b/crates/vespertide-query/src/sql/create_table.rs
@@ -3,10 +3,10 @@ use sea_query::{Alias, ForeignKey, Index, Table, TableCreateStatement};
 use vespertide_core::{ColumnDef, ColumnType, ComplexColumnType, TableConstraint};
 
 use super::helpers::{
-    build_create_enum_type_sql, build_schema_statement, build_sea_column_def_with_table,
+    build_create_enum_type_sql, build_create_with_checks, build_sea_column_def_with_table,
     collect_sqlite_enum_check_clauses, to_sea_fk_action,
 };
-use super::types::{BuiltQuery, DatabaseBackend, RawSql};
+use super::types::{BuiltQuery, DatabaseBackend};
 use crate::error::QueryError;
 
 pub(crate) fn build_create_table_for_backend(
@@ -253,14 +253,13 @@ pub fn build_create_table(
         if enum_check_clauses.is_empty() {
             queries.push(BuiltQuery::CreateTable(Box::new(create_table_stmt)));
         } else {
-            // Embed CHECK constraints into CREATE TABLE statement
-            let base_sql = build_schema_statement(&create_table_stmt, backend);
-            let mut modified_sql = base_sql;
-            if let Some(pos) = modified_sql.rfind(')') {
-                let check_sql = enum_check_clauses.join(", ");
-                modified_sql.insert_str(pos, &format!(", {check_sql}"));
-            }
-            queries.push(BuiltQuery::Raw(RawSql::uniform(modified_sql)));
+            // Embed CHECK constraints into the CREATE TABLE statement via the
+            // single source of truth for CHECK injection.
+            queries.push(build_create_with_checks(
+                backend,
+                &create_table_stmt,
+                &enum_check_clauses,
+            ));
         }
     } else {
         queries.push(BuiltQuery::CreateTable(Box::new(create_table_stmt)));
diff --git a/crates/vespertide-query/src/sql/delete_column/mod.rs b/crates/vespertide-query/src/sql/delete_column/mod.rs
index f5aa7e39..c966cc0b 100644
--- a/crates/vespertide-query/src/sql/delete_column/mod.rs
+++ b/crates/vespertide-query/src/sql/delete_column/mod.rs
@@ -74,7 +74,9 @@ fn sqlite_constraint_handling(
             // SQLite can't DROP COLUMN if a CHECK references it — use temp table.
             TableConstraint::Check { expr, .. } => {
                 // Check if the expression references the column (e.g. "status" IN (...)).
-                if expr.contains(&format!("\"{column}\"")) || expr.contains(column) {
+                // Deliberately conservative bare-substring heuristic: it also matches the
+                // quoted form `"col"`, so false positives only cost an unnecessary rebuild.
+                if expr.contains(column) {
                     return Some(build_delete_column_sqlite_temp_table(
                         table,
                         column,
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index 98721e1d..bc6ebedb 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -16,12 +16,12 @@ use super::types::{BuiltQuery, DatabaseBackend, RawSql};
 /// from the caller — no allocation ever happens, so the `Cow` wrapper was
 /// purely ceremonial.
 #[must_use]
-pub fn normalize_fill_with(fill_with: Option<&str>) -> Option<&str> {
+pub(crate) fn normalize_fill_with(fill_with: Option<&str>) -> Option<&str> {
     fill_with.map(|s| if s.is_empty() { "''" } else { s })
 }
 
 /// Helper function to convert a schema statement to SQL for a specific backend
-pub fn build_schema_statement(
+pub(crate) fn build_schema_statement(
     stmt: &T,
     backend: DatabaseBackend,
 ) -> String {
@@ -33,7 +33,7 @@ pub fn build_schema_statement(
 }
 
 /// Helper function to convert a query statement (INSERT, SELECT, etc.) to SQL for a specific backend
-pub fn build_query_statement(
+pub(crate) fn build_query_statement(
     stmt: &T,
     backend: DatabaseBackend,
 ) -> String {
@@ -45,7 +45,7 @@ pub fn build_query_statement(
 }
 
 /// Apply vespertide `ColumnType` to `sea_query` `ColumnDef` with table-aware enum type naming
-pub fn apply_column_type_with_table(
+pub(crate) fn apply_column_type_with_table(
     col: &mut SeaColumnDef,
     ty: &ColumnType,
     table: &str,
@@ -210,7 +210,7 @@ fn apply_numeric_type(
 }
 
 /// Convert vespertide `ReferenceAction` to `sea_query` `ForeignKeyAction`
-pub fn to_sea_fk_action(action: &ReferenceAction) -> ForeignKeyAction {
+pub(crate) fn to_sea_fk_action(action: &ReferenceAction) -> ForeignKeyAction {
     match action {
         ReferenceAction::Cascade => ForeignKeyAction::Cascade,
         ReferenceAction::Restrict => ForeignKeyAction::Restrict,
@@ -221,8 +221,12 @@ pub fn to_sea_fk_action(action: &ReferenceAction) -> ForeignKeyAction {
     }
 }
 
-/// Convert vespertide `ReferenceAction` to SQL string
-pub fn reference_action_sql(action: &ReferenceAction) -> &'static str {
+/// Convert vespertide `ReferenceAction` to SQL string.
+///
+/// Test oracle: production paths go through [`to_sea_fk_action`]; this
+/// string-level mapping is only asserted against in unit tests.
+#[cfg(test)]
+pub(crate) fn reference_action_sql(action: &ReferenceAction) -> &'static str {
     match action {
         ReferenceAction::Cascade => "CASCADE",
         ReferenceAction::Restrict => "RESTRICT",
@@ -234,7 +238,7 @@ pub fn reference_action_sql(action: &ReferenceAction) -> &'static str {
 }
 
 /// Convert a default value string to the appropriate backend-specific expression
-pub fn convert_default_for_backend(default: &str, backend: DatabaseBackend) -> String {
+pub(crate) fn convert_default_for_backend(default: &str, backend: DatabaseBackend) -> String {
     // UUID generation functions (case-insensitive match against ASCII literals
     // — avoids the per-call `String` allocation that `to_lowercase()` would
     // incur, mirroring the convention `needs_quoting` already uses below).
@@ -260,7 +264,7 @@ pub fn convert_default_for_backend(default: &str, backend: DatabaseBackend) -> S
 
     // PostgreSQL-style type casts: 'value'::type or expr::type
     if let Some((value, cast_type)) = parse_pg_type_cast(default) {
-        return convert_type_cast(&value, &cast_type, backend);
+        return convert_type_cast(value, &cast_type, backend);
     }
 
     default.to_string()
@@ -268,7 +272,11 @@ pub fn convert_default_for_backend(default: &str, backend: DatabaseBackend) -> S
 
 /// Parse a PostgreSQL-style type cast expression (e.g., `'[]'::json`, `0::boolean`)
 /// Returns `(value, type)` if parsed, or None if not a type cast.
-pub(super) fn parse_pg_type_cast(expr: &str) -> Option<(String, String)> {
+///
+/// The value borrows `expr` (both arms return a contiguous slice of the
+/// input — quotes included for the quoted arm); only `cast_type` is owned
+/// because of the `to_lowercase()` normalisation.
+pub(super) fn parse_pg_type_cast(expr: &str) -> Option<(&str, String)> {
     let trimmed = expr.trim();
 
     // Handle quoted values: 'value'::type
@@ -287,7 +295,10 @@ pub(super) fn parse_pg_type_cast(expr: &str) -> Option<(String, String)> {
                 if let Some(stripped) = rest.strip_prefix("::") {
                     let cast_type = stripped.trim().to_lowercase();
                     if !cast_type.is_empty() {
-                        let value = format!("'{}'", after_open.get(..i)?);
+                        // Opening quote + verbatim content (incl. doubled
+                        // quotes) + closing quote is exactly the contiguous
+                        // input slice `trimmed[..i + 2]` — no allocation.
+                        let value = trimmed.get(..i + 1 + ch.len_utf8())?;
                         return Some((value, cast_type));
                     }
                 }
@@ -299,7 +310,7 @@ pub(super) fn parse_pg_type_cast(expr: &str) -> Option<(String, String)> {
 
     // Handle unquoted values: expr::type (e.g., 0::boolean, NULL::json)
     if let Some((value, cast_type)) = trimmed.split_once("::") {
-        let value = value.trim().to_string();
+        let value = value.trim();
         let cast_type = cast_type.trim().to_lowercase();
         if !value.is_empty() && !cast_type.is_empty() {
             return Some((value, cast_type));
@@ -352,7 +363,7 @@ pub(super) fn is_enum_type(column_type: &ColumnType) -> bool {
 
 /// Normalize a default value for enum columns - add quotes if needed
 /// This is used for SQL expressions (INSERT, UPDATE) where enum values need quoting
-pub fn normalize_enum_default(column_type: &ColumnType, value: &str) -> String {
+pub(crate) fn normalize_enum_default(column_type: &ColumnType, value: &str) -> String {
     if is_enum_type(column_type) && needs_quoting(value) {
         format!("'{value}'")
     } else {
@@ -390,7 +401,7 @@ pub(super) fn needs_quoting(default_str: &str) -> bool {
 }
 
 /// Build `sea_query` `ColumnDef` from vespertide `ColumnDef` for a specific backend with table-aware enum naming
-pub fn build_sea_column_def_with_table(
+pub(crate) fn build_sea_column_def_with_table(
     backend: DatabaseBackend,
     table: &str,
     column: &ColumnDef,
@@ -438,7 +449,7 @@ pub fn build_sea_column_def_with_table(
 ///
 /// The enum type name will be prefixed with the table name to avoid conflicts
 /// across tables using the same enum name (e.g., "status", "gender").
-pub fn build_create_enum_type_sql(
+pub(crate) fn build_create_enum_type_sql(
     table: &str,
     column_type: &ColumnType,
 ) -> Option {
@@ -469,7 +480,7 @@ pub fn build_create_enum_type_sql(
 /// Returns None for non-PostgreSQL backends or non-enum types
 ///
 /// The enum type name will be prefixed with the table name to match the CREATE TYPE.
-pub fn build_drop_enum_type_sql(
+pub(crate) fn build_drop_enum_type_sql(
     table: &str,
     column_type: &ColumnType,
 ) -> Option {
@@ -489,14 +500,14 @@ pub fn build_drop_enum_type_sql(
 }
 
 // Re-export naming functions from vespertide-naming
-pub use vespertide_naming::{
+pub(crate) use vespertide_naming::{
     build_check_constraint_name, build_enum_type_name, build_foreign_key_name, build_index_name,
     build_unique_constraint_name,
 };
 
 /// Generate CHECK constraint expression for `SQLite` enum column
 /// Returns the constraint clause like: CONSTRAINT "`chk_table_col`" CHECK (col IN ('val1', 'val2'))
-pub fn build_sqlite_enum_check_clause(
+pub(crate) fn build_sqlite_enum_check_clause(
     table: &str,
     column: &str,
     column_type: &ColumnType,
@@ -515,7 +526,7 @@ pub fn build_sqlite_enum_check_clause(
 }
 
 /// Collect all CHECK constraints for enum columns in a table (for `SQLite`)
-pub fn collect_sqlite_enum_check_clauses(table: &str, columns: &[ColumnDef]) -> Vec {
+pub(crate) fn collect_sqlite_enum_check_clauses(table: &str, columns: &[ColumnDef]) -> Vec {
     columns
         .iter()
         .filter_map(|col| build_sqlite_enum_check_clause(table, &col.name, &col.r#type))
@@ -524,7 +535,7 @@ pub fn collect_sqlite_enum_check_clauses(table: &str, columns: &[ColumnDef]) ->
 
 /// Extract CHECK constraint clauses from a list of table constraints.
 /// Returns SQL fragments like: `CONSTRAINT "chk_name" CHECK (expr)`
-pub fn extract_check_clauses(constraints: &[TableConstraint]) -> Vec {
+pub(crate) fn extract_check_clauses(constraints: &[TableConstraint]) -> Vec {
     constraints
         .iter()
         .filter_map(|c| {
@@ -544,7 +555,7 @@ pub fn extract_check_clauses(constraints: &[TableConstraint]) -> Vec {
 /// - Explicit CHECK constraints (from `TableConstraint::Check`)
 ///
 /// Returns deduplicated union of both.
-pub fn collect_all_check_clauses(
+pub(crate) fn collect_all_check_clauses(
     table: &str,
     columns: &[ColumnDef],
     constraints: &[TableConstraint],
@@ -562,7 +573,7 @@ pub fn collect_all_check_clauses(
 /// Build CREATE TABLE query with CHECK constraints properly embedded.
 /// sea-query doesn't support CHECK constraints natively, so we inject them
 /// by modifying the generated SQL string.
-pub fn build_create_with_checks(
+pub(crate) fn build_create_with_checks(
     backend: DatabaseBackend,
     create_stmt: &sea_query::TableCreateStatement,
     check_clauses: &[String],
@@ -585,7 +596,7 @@ pub fn build_create_with_checks(
 ///
 /// `table` is the ORIGINAL table name (used for constraint naming).
 /// `temp_table` is the temporary table name.
-pub fn build_sqlite_temp_table_create(
+pub(crate) fn build_sqlite_temp_table_create(
     backend: DatabaseBackend,
     temp_table: &str,
     table: &str,
@@ -640,7 +651,7 @@ pub(super) fn build_copy_into_temp_table(
 /// `pending_constraints` are constraints that exist in the logical schema but haven't been
 /// physically created yet (e.g., promoted from inline column definitions by `AddColumn` normalization).
 /// These will be created by separate `AddConstraint` actions later, so we must NOT recreate them here.
-pub fn recreate_indexes_after_rebuild(
+pub(crate) fn recreate_indexes_after_rebuild(
     table: &str,
     constraints: &[TableConstraint],
     pending_constraints: &[TableConstraint],
@@ -748,8 +759,11 @@ pub(super) fn build_sqlite_table_rebuild(
     queries
 }
 
-/// Extract enum name from column type if it's an enum
-pub fn get_enum_name(column_type: &ColumnType) -> Option<&str> {
+/// Extract enum name from column type if it's an enum.
+///
+/// Test oracle: only unit tests consume this accessor today.
+#[cfg(test)]
+pub(crate) fn get_enum_name(column_type: &ColumnType) -> Option<&str> {
     if let ColumnType::Complex(ComplexColumnType::Enum { name, .. }) = column_type {
         Some(name.as_str())
     } else {
diff --git a/crates/vespertide-query/src/sql/mod.rs b/crates/vespertide-query/src/sql/mod.rs
index 5ede24de..181651dc 100644
--- a/crates/vespertide-query/src/sql/mod.rs
+++ b/crates/vespertide-query/src/sql/mod.rs
@@ -16,7 +16,7 @@ pub mod rename_table;
 pub mod replace_constraint;
 pub mod types;
 
-pub use helpers::*;
+pub use helpers::{quote_ident, quote_idents};
 pub use types::{BuiltQuery, DatabaseBackend, RawSql};
 
 use crate::error::QueryError;
diff --git a/crates/vespertide-query/src/sql/tests/helpers.rs b/crates/vespertide-query/src/sql/tests/helpers.rs
index d2c2a4f8..cfe4e438 100644
--- a/crates/vespertide-query/src/sql/tests/helpers.rs
+++ b/crates/vespertide-query/src/sql/tests/helpers.rs
@@ -1,4 +1,8 @@
-use super::super::helpers::{is_enum_type, needs_quoting, parse_pg_type_cast};
+use super::super::helpers::{
+    apply_column_type_with_table, build_create_enum_type_sql, convert_default_for_backend,
+    get_enum_name, is_enum_type, needs_quoting, parse_pg_type_cast, recreate_indexes_after_rebuild,
+    reference_action_sql, to_sea_fk_action,
+};
 use super::*;
 use proptest::prelude::*;
 use sea_query::{Alias, ColumnDef as SeaColumnDef, ForeignKeyAction};

From b6614a003455b6c775bbe4e499101369744001d9 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 03:42:08 +0900
Subject: [PATCH 057/109] retry-now#0056: batch - shared-slice semantic-token
 encode, ColumnDef::new compaction in cli/loader tests, const-match sql_blob,
 loader PathBuf cleanup, fix two stale doc comments (5 kept)

---
 .../src/commands/revision/tests/branches.rs   | 327 +++---------------
 crates/vespertide-loader/src/models.rs        | 142 +++-----
 .../vespertide-lsp/src/diagnostics/locator.rs |   6 +-
 .../src/semantic_tokens/encode.rs             |  42 +--
 .../src/semantic_tokens/handler.rs            |   8 +-
 crates/vespertide-schema-gen/src/main.rs      |   6 +-
 crates/vespertide/src/runtime.rs              |  12 +-
 7 files changed, 143 insertions(+), 400 deletions(-)

diff --git a/crates/vespertide-cli/src/commands/revision/tests/branches.rs b/crates/vespertide-cli/src/commands/revision/tests/branches.rs
index 90ab2127..4a3898d9 100644
--- a/crates/vespertide-cli/src/commands/revision/tests/branches.rs
+++ b/crates/vespertide-cli/src/commands/revision/tests/branches.rs
@@ -266,17 +266,11 @@ async fn cmd_revision_core_dangling_fk_drop_returns_hard_error() {
         actions: vec![
             MigrationAction::CreateTable {
                 table: "users".into(),
-                columns: vec![ColumnDef {
-                    name: "id".into(),
-                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                    nullable: false,
-                    default: None,
-                    comment: None,
-                    primary_key: None,
-                    unique: None,
-                    index: None,
-                    foreign_key: None,
-                }],
+                columns: vec![ColumnDef::new(
+                    "id",
+                    ColumnType::Simple(SimpleColumnType::Integer),
+                    false,
+                )],
                 constraints: vec![TableConstraint::PrimaryKey {
                     auto_increment: false,
                     columns: vec!["id".into()],
@@ -286,28 +280,12 @@ async fn cmd_revision_core_dangling_fk_drop_returns_hard_error() {
             MigrationAction::CreateTable {
                 table: "posts".into(),
                 columns: vec![
-                    ColumnDef {
-                        name: "id".into(),
-                        r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                        nullable: false,
-                        default: None,
-                        comment: None,
-                        primary_key: None,
-                        unique: None,
-                        index: None,
-                        foreign_key: None,
-                    },
-                    ColumnDef {
-                        name: "user_id".into(),
-                        r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                        nullable: true,
-                        default: None,
-                        comment: None,
-                        primary_key: None,
-                        unique: None,
-                        index: None,
-                        foreign_key: None,
-                    },
+                    ColumnDef::new("id", ColumnType::Simple(SimpleColumnType::Integer), false),
+                    ColumnDef::new(
+                        "user_id",
+                        ColumnType::Simple(SimpleColumnType::Integer),
+                        true,
+                    ),
                 ],
                 constraints: vec![
                     TableConstraint::PrimaryKey {
@@ -341,34 +319,19 @@ async fn cmd_revision_core_dangling_fk_drop_returns_hard_error() {
         name: "posts".into(),
         description: None,
         columns: vec![
-            ColumnDef {
-                name: "id".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            },
-            ColumnDef {
-                name: "user_id".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                nullable: true,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: Some(ForeignKeySyntax::Object(ForeignKeyDef {
-                    ref_table: "users".into(),
-                    ref_columns: vec!["id".into()],
-                    on_delete: None,
-                    on_update: None,
-                    orphan_strategy: ForeignKeyOrphanStrategy::default(),
-                })),
-            },
+            ColumnDef::new("id", ColumnType::Simple(SimpleColumnType::Integer), false),
+            ColumnDef::new(
+                "user_id",
+                ColumnType::Simple(SimpleColumnType::Integer),
+                true,
+            )
+            .foreign_key(ForeignKeySyntax::Object(ForeignKeyDef {
+                ref_table: "users".into(),
+                ref_columns: vec!["id".into()],
+                on_delete: None,
+                on_update: None,
+                orphan_strategy: ForeignKeyOrphanStrategy::default(),
+            })),
         ],
         constraints: vec![TableConstraint::PrimaryKey {
             auto_increment: false,
@@ -417,28 +380,8 @@ async fn cmd_revision_core_drop_resolution_cancel_aborts() {
         actions: vec![MigrationAction::CreateTable {
             table: "users".into(),
             columns: vec![
-                ColumnDef {
-                    name: "id".into(),
-                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                    nullable: false,
-                    default: None,
-                    comment: None,
-                    primary_key: None,
-                    unique: None,
-                    index: None,
-                    foreign_key: None,
-                },
-                ColumnDef {
-                    name: "email".into(),
-                    r#type: ColumnType::Simple(SimpleColumnType::Text),
-                    nullable: true,
-                    default: None,
-                    comment: None,
-                    primary_key: None,
-                    unique: None,
-                    index: None,
-                    foreign_key: None,
-                },
+                ColumnDef::new("id", ColumnType::Simple(SimpleColumnType::Integer), false),
+                ColumnDef::new("email", ColumnType::Simple(SimpleColumnType::Text), true),
             ],
             constraints: vec![TableConstraint::PrimaryKey {
                 auto_increment: false,
@@ -459,17 +402,11 @@ async fn cmd_revision_core_drop_resolution_cancel_aborts() {
     let model = TableDef {
         name: "users".into(),
         description: None,
-        columns: vec![ColumnDef {
-            name: "id".into(),
-            r#type: ColumnType::Simple(SimpleColumnType::Integer),
-            nullable: false,
-            default: None,
-            comment: None,
-            primary_key: None,
-            unique: None,
-            index: None,
-            foreign_key: None,
-        }],
+        columns: vec![ColumnDef::new(
+            "id",
+            ColumnType::Simple(SimpleColumnType::Integer),
+            false,
+        )],
         constraints: vec![TableConstraint::PrimaryKey {
             auto_increment: false,
             columns: vec!["id".into()],
@@ -514,28 +451,8 @@ async fn cmd_revision_core_drop_resolution_accept_drop_writes_migration() {
         actions: vec![MigrationAction::CreateTable {
             table: "users".into(),
             columns: vec![
-                ColumnDef {
-                    name: "id".into(),
-                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                    nullable: false,
-                    default: None,
-                    comment: None,
-                    primary_key: None,
-                    unique: None,
-                    index: None,
-                    foreign_key: None,
-                },
-                ColumnDef {
-                    name: "email".into(),
-                    r#type: ColumnType::Simple(SimpleColumnType::Text),
-                    nullable: true,
-                    default: None,
-                    comment: None,
-                    primary_key: None,
-                    unique: None,
-                    index: None,
-                    foreign_key: None,
-                },
+                ColumnDef::new("id", ColumnType::Simple(SimpleColumnType::Integer), false),
+                ColumnDef::new("email", ColumnType::Simple(SimpleColumnType::Text), true),
             ],
             constraints: vec![TableConstraint::PrimaryKey {
                 auto_increment: false,
@@ -555,17 +472,11 @@ async fn cmd_revision_core_drop_resolution_accept_drop_writes_migration() {
     let model = TableDef {
         name: "users".into(),
         description: None,
-        columns: vec![ColumnDef {
-            name: "id".into(),
-            r#type: ColumnType::Simple(SimpleColumnType::Integer),
-            nullable: false,
-            default: None,
-            comment: None,
-            primary_key: None,
-            unique: None,
-            index: None,
-            foreign_key: None,
-        }],
+        columns: vec![ColumnDef::new(
+            "id",
+            ColumnType::Simple(SimpleColumnType::Integer),
+            false,
+        )],
         constraints: vec![TableConstraint::PrimaryKey {
             auto_increment: false,
             columns: vec!["id".into()],
@@ -612,28 +523,9 @@ async fn cmd_revision_core_default_change_backfill_path() {
         actions: vec![MigrationAction::CreateTable {
             table: "users".into(),
             columns: vec![
-                ColumnDef {
-                    name: "id".into(),
-                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                    nullable: false,
-                    default: None,
-                    comment: None,
-                    primary_key: None,
-                    unique: None,
-                    index: None,
-                    foreign_key: None,
-                },
-                ColumnDef {
-                    name: "status".into(),
-                    r#type: ColumnType::Simple(SimpleColumnType::Text),
-                    nullable: false,
-                    default: Some("'pending'".into()),
-                    comment: None,
-                    primary_key: None,
-                    unique: None,
-                    index: None,
-                    foreign_key: None,
-                },
+                ColumnDef::new("id", ColumnType::Simple(SimpleColumnType::Integer), false),
+                ColumnDef::new("status", ColumnType::Simple(SimpleColumnType::Text), false)
+                    .default("'pending'".into()),
             ],
             constraints: vec![TableConstraint::PrimaryKey {
                 auto_increment: false,
@@ -655,28 +547,9 @@ async fn cmd_revision_core_default_change_backfill_path() {
         name: "users".into(),
         description: None,
         columns: vec![
-            ColumnDef {
-                name: "id".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            },
-            ColumnDef {
-                name: "status".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Text),
-                nullable: false,
-                default: Some("'active'".into()),
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            },
+            ColumnDef::new("id", ColumnType::Simple(SimpleColumnType::Integer), false),
+            ColumnDef::new("status", ColumnType::Simple(SimpleColumnType::Text), false)
+                .default("'active'".into()),
         ],
         constraints: vec![TableConstraint::PrimaryKey {
             auto_increment: false,
@@ -736,28 +609,9 @@ async fn cmd_revision_core_default_change_cancel_aborts() {
         actions: vec![MigrationAction::CreateTable {
             table: "users".into(),
             columns: vec![
-                ColumnDef {
-                    name: "id".into(),
-                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                    nullable: false,
-                    default: None,
-                    comment: None,
-                    primary_key: None,
-                    unique: None,
-                    index: None,
-                    foreign_key: None,
-                },
-                ColumnDef {
-                    name: "status".into(),
-                    r#type: ColumnType::Simple(SimpleColumnType::Text),
-                    nullable: false,
-                    default: Some("'pending'".into()),
-                    comment: None,
-                    primary_key: None,
-                    unique: None,
-                    index: None,
-                    foreign_key: None,
-                },
+                ColumnDef::new("id", ColumnType::Simple(SimpleColumnType::Integer), false),
+                ColumnDef::new("status", ColumnType::Simple(SimpleColumnType::Text), false)
+                    .default("'pending'".into()),
             ],
             constraints: vec![TableConstraint::PrimaryKey {
                 auto_increment: false,
@@ -778,28 +632,9 @@ async fn cmd_revision_core_default_change_cancel_aborts() {
         name: "users".into(),
         description: None,
         columns: vec![
-            ColumnDef {
-                name: "id".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            },
-            ColumnDef {
-                name: "status".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Text),
-                nullable: false,
-                default: Some("'active'".into()),
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            },
+            ColumnDef::new("id", ColumnType::Simple(SimpleColumnType::Integer), false),
+            ColumnDef::new("status", ColumnType::Simple(SimpleColumnType::Text), false)
+                .default("'active'".into()),
         ],
         constraints: vec![TableConstraint::PrimaryKey {
             auto_increment: false,
@@ -847,28 +682,9 @@ async fn cmd_revision_core_default_change_skip_writes_v2_without_backfill() {
         actions: vec![MigrationAction::CreateTable {
             table: "users".into(),
             columns: vec![
-                ColumnDef {
-                    name: "id".into(),
-                    r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                    nullable: false,
-                    default: None,
-                    comment: None,
-                    primary_key: None,
-                    unique: None,
-                    index: None,
-                    foreign_key: None,
-                },
-                ColumnDef {
-                    name: "status".into(),
-                    r#type: ColumnType::Simple(SimpleColumnType::Text),
-                    nullable: false,
-                    default: Some("'pending'".into()),
-                    comment: None,
-                    primary_key: None,
-                    unique: None,
-                    index: None,
-                    foreign_key: None,
-                },
+                ColumnDef::new("id", ColumnType::Simple(SimpleColumnType::Integer), false),
+                ColumnDef::new("status", ColumnType::Simple(SimpleColumnType::Text), false)
+                    .default("'pending'".into()),
             ],
             constraints: vec![TableConstraint::PrimaryKey {
                 auto_increment: false,
@@ -889,28 +705,9 @@ async fn cmd_revision_core_default_change_skip_writes_v2_without_backfill() {
         name: "users".into(),
         description: None,
         columns: vec![
-            ColumnDef {
-                name: "id".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            },
-            ColumnDef {
-                name: "status".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Text),
-                nullable: false,
-                default: Some("'active'".into()),
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            },
+            ColumnDef::new("id", ColumnType::Simple(SimpleColumnType::Integer), false),
+            ColumnDef::new("status", ColumnType::Simple(SimpleColumnType::Text), false)
+                .default("'active'".into()),
         ],
         constraints: vec![TableConstraint::PrimaryKey {
             auto_increment: false,
@@ -941,17 +738,7 @@ async fn cmd_revision_core_default_change_skip_writes_v2_without_backfill() {
 }
 
 pub(super) fn col(name: &str, ty: ColumnType, nullable: bool) -> ColumnDef {
-    ColumnDef {
-        name: name.into(),
-        r#type: ty,
-        nullable,
-        default: None,
-        comment: None,
-        primary_key: None,
-        unique: None,
-        index: None,
-        foreign_key: None,
-    }
+    ColumnDef::new(name, ty, nullable)
 }
 
 pub(super) fn int_col(name: &str, nullable: bool) -> ColumnDef {
diff --git a/crates/vespertide-loader/src/models.rs b/crates/vespertide-loader/src/models.rs
index 79689487..4bcea7d7 100644
--- a/crates/vespertide-loader/src/models.rs
+++ b/crates/vespertide-loader/src/models.rs
@@ -89,16 +89,14 @@ fn load_model_file(path: &Path) -> Result {
 
 /// Load models from a specific directory (for compile-time use in macros).
 pub fn load_models_from_dir(
-    project_root: Option,
+    project_root: Option,
 ) -> Result, Box> {
-    use std::env;
-
     // Locate project root from CARGO_MANIFEST_DIR or use provided path
     let project_root = if let Some(root) = project_root {
         root
     } else {
-        std::path::PathBuf::from(
-            env::var("CARGO_MANIFEST_DIR")
+        PathBuf::from(
+            std::env::var("CARGO_MANIFEST_DIR")
                 .context("CARGO_MANIFEST_DIR environment variable not set")?,
         )
     };
@@ -189,17 +187,11 @@ mod tests {
         let table = TableDef {
             name: "users".into(),
             description: None,
-            columns: vec![ColumnDef {
-                name: "id".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }],
+            columns: vec![ColumnDef::new(
+                "id",
+                ColumnType::Simple(SimpleColumnType::Integer),
+                false,
+            )],
             constraints: vec![TableConstraint::PrimaryKey {
                 auto_increment: false,
                 columns: vec!["id".into()],
@@ -226,17 +218,11 @@ mod tests {
         let table = TableDef {
             name: "subtable".into(),
             description: None,
-            columns: vec![ColumnDef {
-                name: "id".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }],
+            columns: vec![ColumnDef::new(
+                "id",
+                ColumnType::Simple(SimpleColumnType::Integer),
+                false,
+            )],
             constraints: vec![TableConstraint::PrimaryKey {
                 auto_increment: false,
                 columns: vec!["id".into()],
@@ -264,18 +250,15 @@ mod tests {
         let table = TableDef {
             name: "orders".into(),
             description: None,
-            columns: vec![ColumnDef {
-                name: "user_id".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
+            columns: vec![
                 // Invalid FK format: should be "table.column" but missing the dot
-                foreign_key: Some(ForeignKeySyntax::String("invalid_format".into())),
-            }],
+                ColumnDef::new(
+                    "user_id",
+                    ColumnType::Simple(SimpleColumnType::Integer),
+                    false,
+                )
+                .foreign_key(ForeignKeySyntax::String("invalid_format".into())),
+            ],
             constraints: vec![],
         };
         fs::write(
@@ -333,17 +316,11 @@ mod tests {
         let table = TableDef {
             name: "users".into(),
             description: None,
-            columns: vec![ColumnDef {
-                name: "id".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }],
+            columns: vec![ColumnDef::new(
+                "id",
+                ColumnType::Simple(SimpleColumnType::Integer),
+                false,
+            )],
             constraints: vec![],
         };
         fs::write(
@@ -407,17 +384,11 @@ mod tests {
         let table = TableDef {
             name: "users".into(),
             description: None,
-            columns: vec![ColumnDef {
-                name: "id".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }],
+            columns: vec![ColumnDef::new(
+                "id",
+                ColumnType::Simple(SimpleColumnType::Integer),
+                false,
+            )],
             constraints: vec![],
         };
         fs::write(
@@ -443,17 +414,11 @@ mod tests {
         let table = TableDef {
             name: "users".into(),
             description: None,
-            columns: vec![ColumnDef {
-                name: "id".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }],
+            columns: vec![ColumnDef::new(
+                "id",
+                ColumnType::Simple(SimpleColumnType::Integer),
+                false,
+            )],
             constraints: vec![],
         };
         fs::write(
@@ -480,17 +445,11 @@ mod tests {
         let table = TableDef {
             name: "subtable".into(),
             description: None,
-            columns: vec![ColumnDef {
-                name: "id".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: None,
-            }],
+            columns: vec![ColumnDef::new(
+                "id",
+                ColumnType::Simple(SimpleColumnType::Integer),
+                false,
+            )],
             constraints: vec![],
         };
         fs::write(
@@ -547,17 +506,14 @@ mod tests {
         let table = TableDef {
             name: "orders".into(),
             description: None,
-            columns: vec![ColumnDef {
-                name: "user_id".into(),
-                r#type: ColumnType::Simple(SimpleColumnType::Integer),
-                nullable: false,
-                default: None,
-                comment: None,
-                primary_key: None,
-                unique: None,
-                index: None,
-                foreign_key: Some(ForeignKeySyntax::String("invalid_format".into())),
-            }],
+            columns: vec![
+                ColumnDef::new(
+                    "user_id",
+                    ColumnType::Simple(SimpleColumnType::Integer),
+                    false,
+                )
+                .foreign_key(ForeignKeySyntax::String("invalid_format".into())),
+            ],
             constraints: vec![],
         };
         fs::write(
diff --git a/crates/vespertide-lsp/src/diagnostics/locator.rs b/crates/vespertide-lsp/src/diagnostics/locator.rs
index f7e45248..b9d1a817 100644
--- a/crates/vespertide-lsp/src/diagnostics/locator.rs
+++ b/crates/vespertide-lsp/src/diagnostics/locator.rs
@@ -56,9 +56,9 @@ impl ErrorLocation {
             // Batched validation errors carry several independent violations.
             // Locator returns *one* location per call, so we recurse into the
             // first nested error to pick a sensible anchor. Per-violation
-            // diagnostics belong to the publisher (see TODO in
-            // `diagnostics::validation`): when it iterates `find_*_violations`
-            // each inner error is presented directly and this arm is bypassed.
+            // diagnostics are handled by the publisher in
+            // `diagnostics::validation`: it iterates `find_*_violations` and
+            // presents each inner error directly, so this arm is bypassed.
             Multiple(batch) => batch.0.first().and_then(Self::from_planner_error),
             // F9 dangling FK after drop: anchor on the *dropped* target so the
             // squiggle lands on the column/table the user removed. When only
diff --git a/crates/vespertide-lsp/src/semantic_tokens/encode.rs b/crates/vespertide-lsp/src/semantic_tokens/encode.rs
index 2292491d..6d641e57 100644
--- a/crates/vespertide-lsp/src/semantic_tokens/encode.rs
+++ b/crates/vespertide-lsp/src/semantic_tokens/encode.rs
@@ -20,7 +20,7 @@ use super::RawToken;
 ///   * Token lengths are measured in UTF-16 code units, not bytes —
 ///     `lsp-textdocument` handles that conversion.
 #[must_use]
-pub fn encode(tokens: &mut [RawToken], doc: &FullTextDocument) -> Vec {
+pub fn encode(tokens: &[RawToken], doc: &FullTextDocument) -> Vec {
     // Resolve every byte range to (line, start_utf16, length_utf16) once.
     // Drops zero-length tokens and multi-line spans up front.
     let mut resolved: Vec = tokens
@@ -112,8 +112,8 @@ mod tests {
 
     #[test]
     fn empty_input_yields_empty_output() {
-        let mut tokens: Vec = vec![];
-        let encoded = encode(&mut tokens, &doc("{}"));
+        let tokens: Vec = vec![];
+        let encoded = encode(&tokens, &doc("{}"));
         assert!(encoded.is_empty());
     }
 
@@ -135,8 +135,8 @@ mod tests {
             token_type: 1,
             token_modifiers: 0,
         };
-        let mut tokens = vec![a, i];
-        let encoded = encode(&mut tokens, &doc);
+        let tokens = vec![a, i];
+        let encoded = encode(&tokens, &doc);
         assert_eq!(encoded.len(), 2);
         assert_eq!(encoded[0].delta_line, 0);
         assert_eq!(encoded[0].delta_start, 8);
@@ -162,8 +162,8 @@ mod tests {
             token_type: 1,
             token_modifiers: 0,
         };
-        let mut tokens = vec![first, second];
-        let encoded = encode(&mut tokens, &doc);
+        let tokens = vec![first, second];
+        let encoded = encode(&tokens, &doc);
         assert_eq!(encoded.len(), 2);
         assert_eq!(encoded[1].delta_line, 1);
         assert_eq!(encoded[1].delta_start, 2, "absolute when line changes");
@@ -178,8 +178,8 @@ mod tests {
             token_type: 0,
             token_modifiers: 0,
         };
-        let mut tokens = vec![span];
-        let encoded = encode(&mut tokens, &doc);
+        let tokens = vec![span];
+        let encoded = encode(&tokens, &doc);
         assert!(encoded.is_empty(), "multi-line tokens must be dropped");
     }
 
@@ -198,8 +198,8 @@ mod tests {
             token_type: 0,
             token_modifiers: 0,
         };
-        let mut tokens = vec![later, earlier];
-        let encoded = encode(&mut tokens, &doc);
+        let tokens = vec![later, earlier];
+        let encoded = encode(&tokens, &doc);
         assert_eq!(encoded.len(), 2);
         // Earlier token reported first; later is delta from it.
         assert_eq!(encoded[0].delta_start, 1);
@@ -215,8 +215,8 @@ mod tests {
             token_type: 0,
             token_modifiers: 0,
         };
-        let mut tokens = vec![twice.clone(), twice];
-        let encoded = encode(&mut tokens, &doc);
+        let tokens = vec![twice.clone(), twice];
+        let encoded = encode(&tokens, &doc);
         assert_eq!(encoded.len(), 1);
     }
 
@@ -230,8 +230,8 @@ mod tests {
             token_type: 0,
             token_modifiers: 0,
         };
-        let mut tokens = vec![token];
-        let encoded = encode(&mut tokens, &doc);
+        let tokens = vec![token];
+        let encoded = encode(&tokens, &doc);
         assert_eq!(encoded.len(), 1);
         assert_eq!(encoded[0].length, 2, "length must be in UTF-16 code units");
     }
@@ -239,13 +239,13 @@ mod tests {
     #[test]
     fn raw_token_resolves_accented_byte_range_to_utf16_position() {
         let doc = doc("éx");
-        let mut tokens = vec![RawToken {
+        let tokens = vec![RawToken {
             byte_range: 0..2,
             token_type: 2,
             token_modifiers: 0,
         }];
 
-        let encoded = encode(&mut tokens, &doc);
+        let encoded = encode(&tokens, &doc);
 
         assert_eq!(encoded.len(), 1);
         assert_eq!(encoded[0].delta_line, 0);
@@ -258,24 +258,24 @@ mod tests {
 
     #[test]
     fn zero_length_tokens_are_rejected_before_encoding() {
-        let mut tokens = vec![RawToken {
+        let tokens = vec![RawToken {
             byte_range: 1..1,
             token_type: 0,
             token_modifiers: 0,
         }];
 
-        assert!(encode(&mut tokens, &doc("abc")).is_empty());
+        assert!(encode(&tokens, &doc("abc")).is_empty());
     }
 
     #[test]
     fn sub_character_ranges_that_map_to_zero_utf16_width_are_rejected() {
         let text = "🚀";
-        let mut tokens = vec![RawToken {
+        let tokens = vec![RawToken {
             byte_range: 1..2,
             token_type: 0,
             token_modifiers: 0,
         }];
 
-        assert!(encode(&mut tokens, &doc(text)).is_empty());
+        assert!(encode(&tokens, &doc(text)).is_empty());
     }
 }
diff --git a/crates/vespertide-lsp/src/semantic_tokens/handler.rs b/crates/vespertide-lsp/src/semantic_tokens/handler.rs
index 782450ec..c2f81c6a 100644
--- a/crates/vespertide-lsp/src/semantic_tokens/handler.rs
+++ b/crates/vespertide-lsp/src/semantic_tokens/handler.rs
@@ -22,8 +22,8 @@ pub fn compute_full(
 
     let data = store.docs_iter_for_uri(uri, |state| {
         let text = state.text();
-        let mut raw = super::classify(text, format, state.tree.as_ref());
-        super::encode(&mut raw, &state.doc)
+        let raw = super::classify(text, format, state.tree.as_ref());
+        super::encode(&raw, &state.doc)
     })?;
     let token_count = data.len();
     let uri_text = uri.as_str();
@@ -57,8 +57,8 @@ pub fn compute_range(
         let start = crate::position::lsp_position_to_byte(&state.doc, lsp_range.start);
         let end = crate::position::lsp_position_to_byte(&state.doc, lsp_range.end);
         let raw = super::classify(text, format, state.tree.as_ref());
-        let mut filtered = super::filter_range(raw, start..end);
-        super::encode(&mut filtered, &state.doc)
+        let filtered = super::filter_range(raw, start..end);
+        super::encode(&filtered, &state.doc)
     })?;
     let token_count = data.len();
     let uri_text = uri.as_str();
diff --git a/crates/vespertide-schema-gen/src/main.rs b/crates/vespertide-schema-gen/src/main.rs
index 9e1c5e9b..421f2ee7 100644
--- a/crates/vespertide-schema-gen/src/main.rs
+++ b/crates/vespertide-schema-gen/src/main.rs
@@ -82,9 +82,9 @@ fn run(out: &Path) -> Result<()> {
 const MIGRATION_ONLY_FIELDS: &[&str] = &["strategy", "orphan_strategy"];
 
 /// Recursively walk a JSON Schema document and remove every occurrence
-/// of `MIGRATION_ONLY_FIELDS` from `properties`, `required`, and
-/// `default` blocks. Operates on the schemars output as plain
-/// `serde_json::Value` so it survives schemars major-version changes.
+/// of `MIGRATION_ONLY_FIELDS` from `properties` and `required` blocks.
+/// Operates on the schemars output as plain `serde_json::Value` so it
+/// survives schemars major-version changes.
 fn strip_migration_fields(value: &mut serde_json::Value) {
     match value {
         serde_json::Value::Object(map) => {
diff --git a/crates/vespertide/src/runtime.rs b/crates/vespertide/src/runtime.rs
index 6f375d70..c36d3f31 100644
--- a/crates/vespertide/src/runtime.rs
+++ b/crates/vespertide/src/runtime.rs
@@ -57,12 +57,12 @@ impl EmbeddedMigration {
     }
 
     pub const fn sql_blob(self, backend: DatabaseBackend) -> &'static str {
-        if matches!(backend, DatabaseBackend::MySql) {
-            self.mysql_sql_blob
-        } else if matches!(backend, DatabaseBackend::Sqlite) {
-            self.sqlite_sql_blob
-        } else {
-            self.postgres_sql_blob
+        match backend {
+            DatabaseBackend::MySql => self.mysql_sql_blob,
+            DatabaseBackend::Sqlite => self.sqlite_sql_blob,
+            // Postgres today, plus any future `#[non_exhaustive]` backend
+            // variants — preserves the historical Postgres fallback.
+            _ => self.postgres_sql_blob,
         }
     }
 }

From c39aabb976051162a720c4e7244e7698d39b4ec8 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 04:09:53 +0900
Subject: [PATCH 058/109] retry-now#0057: batch - hoist CHECK type lookup per
 predicate, drop tokenize double-collect, defer contradiction label
 formatting, short-circuit strengthening parse (4 kept)

---
 .../src/validate/check_expr_parser.rs         | 14 +--
 .../validate/check_expr_parser/tests/mod.rs   |  2 +-
 .../src/validate/check_self_contradiction.rs  | 66 ++++++-------
 .../src/validate/check_strengthening.rs       |  8 +-
 .../src/validate/check_type_mismatch.rs       | 98 +++++++++++--------
 5 files changed, 103 insertions(+), 85 deletions(-)

diff --git a/crates/vespertide-planner/src/validate/check_expr_parser.rs b/crates/vespertide-planner/src/validate/check_expr_parser.rs
index 7bc10012..00d922a7 100644
--- a/crates/vespertide-planner/src/validate/check_expr_parser.rs
+++ b/crates/vespertide-planner/src/validate/check_expr_parser.rs
@@ -271,7 +271,7 @@ pub fn parse(expr: &str) -> CheckExpr {
     if trimmed.is_empty() {
         return CheckExpr::Unparseable;
     }
-    let Some(tokens) = tokenize(trimmed) else {
+    let Some(tokens) = tokenize_spanned(trimmed) else {
         return CheckExpr::Unparseable;
     };
     let mut parser = Parser {
@@ -490,10 +490,6 @@ fn tokenize_spanned(input: &str) -> Option> {
     Some(out)
 }
 
-fn tokenize(input: &str) -> Option> {
-    tokenize_spanned(input).map(|tokens| tokens.into_iter().map(|st| st.token).collect())
-}
-
 /// True when the next token sits in a position that may legally
 /// hold a literal (start of input, after an operator, after `(`,
 /// after `,`, after `BETWEEN`/`AND`/`OR`/`NOT`/`IN`/`IS`). Used to
@@ -545,7 +541,11 @@ fn classify_word(word: &str) -> Token {
 // -- Parser ----------------------------------------------------------------
 
 struct Parser {
-    tokens: Vec,
+    /// Spanned tokens straight from [`tokenize_spanned`] — the parser
+    /// only ever inspects `.token` (via [`Parser::peek`]), so holding the
+    /// spanned form avoids re-collecting a second span-stripped `Vec` on
+    /// every parse.
+    tokens: Vec,
     pos: usize,
     depth: usize,
 }
@@ -556,7 +556,7 @@ impl Parser {
     }
 
     fn peek(&self) -> Option<&Token> {
-        self.tokens.get(self.pos)
+        self.tokens.get(self.pos).map(|st| &st.token)
     }
 
     fn eat_keyword(&mut self, kw: Keyword) -> bool {
diff --git a/crates/vespertide-planner/src/validate/check_expr_parser/tests/mod.rs b/crates/vespertide-planner/src/validate/check_expr_parser/tests/mod.rs
index 6868ab00..80513353 100644
--- a/crates/vespertide-planner/src/validate/check_expr_parser/tests/mod.rs
+++ b/crates/vespertide-planner/src/validate/check_expr_parser/tests/mod.rs
@@ -945,7 +945,7 @@ fn classify_word_and_keyword_direct_branch() {
 #[test]
 fn eat_keyword_false_branch_leaves_position_unchanged() {
     let mut parser = Parser {
-        tokens: vec![Token::Keyword(Keyword::And)],
+        tokens: vec![spanned(Token::Keyword(Keyword::And))],
         pos: 0,
         depth: 0,
     };
diff --git a/crates/vespertide-planner/src/validate/check_self_contradiction.rs b/crates/vespertide-planner/src/validate/check_self_contradiction.rs
index 0c245256..7c3c693d 100644
--- a/crates/vespertide-planner/src/validate/check_self_contradiction.rs
+++ b/crates/vespertide-planner/src/validate/check_self_contradiction.rs
@@ -225,17 +225,18 @@ fn check_pair(column: &str, a: &CheckExpr, b: &CheckExpr) -> Option Option<(bool, String, String)> {
+/// `Compare`, return the structural facts: the IsNull's `negated`
+/// flag plus the Compare's op and literal. Returns `None` otherwise.
+/// The caller formats display labels only after a contradiction is
+/// confirmed, so the common healthy `IS NOT NULL AND col > X` shape
+/// allocates nothing here.
+fn is_null_vs_other<'a>(a: &'a CheckExpr, b: &'a CheckExpr) -> Option<(bool, Op, &'a Literal)> {
     // Normalise to (IsNull, Compare) ordering so the body is written once.
-    let (negated, op, value) = match (a, b) {
+    match (a, b) {
         (CheckExpr::IsNull { negated, .. }, CheckExpr::Compare { op, value, .. })
         | (CheckExpr::Compare { op, value, .. }, CheckExpr::IsNull { negated, .. }) => {
-            (*negated, *op, value)
+            Some((*negated, *op, value))
         }
-        _ => return None,
-    };
-    Some((
-        negated,
-        format_is_null(column, negated),
-        format_compare(column, op, &value.display_value()),
-    ))
+        _ => None,
+    }
 }
 
 fn format_compare(column: &str, op: Op, value_text: &str) -> String {
@@ -858,11 +857,10 @@ mod tests {
         );
     }
 
-    /// L254 / L257 `return None;` `let-else` guards inside
-    /// is_null_vs_other are provably unreachable: the outer match at
-    /// L248-252 already restricts (a, b) to (IsNull, Compare) or
-    /// (Compare, IsNull). After normalisation, the destructuring let
-    /// patterns cannot fail. Direct unit-test pins this invariant.
+    /// is_null_vs_other normalises (a, b) to (IsNull, Compare) ordering
+    /// and returns the structural facts (negated flag, op, literal)
+    /// without formatting. Direct unit-test pins both orderings and the
+    /// `_ => None` fallthrough.
     #[test]
     fn is_null_vs_other_normalises_both_orderings() {
         let isnull = CheckExpr::IsNull {
@@ -875,18 +873,18 @@ mod tests {
             value: Literal::Integer(5),
         };
         // (IsNull, Compare) order
-        let res = is_null_vs_other("x", &isnull, &compare);
-        assert!(res.is_some());
-        // (Compare, IsNull) order — swap arm at L250
-        let res = is_null_vs_other("x", &compare, &isnull);
-        assert!(res.is_some());
+        let res = is_null_vs_other(&isnull, &compare);
+        assert_eq!(res, Some((false, Op::Eq, &Literal::Integer(5))));
+        // (Compare, IsNull) order — swap arm
+        let res = is_null_vs_other(&compare, &isnull);
+        assert_eq!(res, Some((false, Op::Eq, &Literal::Integer(5))));
         // Neither is IsNull/Compare combo — None via the `_ => None` arm
         let in_expr = CheckExpr::In {
             column: "x".into(),
             values: vec![Literal::Integer(1)],
             negated: false,
         };
-        assert!(is_null_vs_other("x", &compare, &in_expr).is_none());
+        assert!(is_null_vs_other(&compare, &in_expr).is_none());
     }
 
     // ── Coverage-closure: defensive arms in find_contradiction & friends ──
diff --git a/crates/vespertide-planner/src/validate/check_strengthening.rs b/crates/vespertide-planner/src/validate/check_strengthening.rs
index 74f37db9..a3bdd52a 100644
--- a/crates/vespertide-planner/src/validate/check_strengthening.rs
+++ b/crates/vespertide-planner/src/validate/check_strengthening.rs
@@ -227,9 +227,15 @@ fn classify_strengthening(
     if old_expr_str.trim() == new_expr_str.trim() {
         return None;
     }
+    // Parse old first and short-circuit: when the old CHECK is outside
+    // the recognized grammar the classification is `None` regardless of
+    // the new expression, so its lex+parse is skipped entirely.
     let old = parse(old_expr_str);
+    if matches!(old, CheckExpr::Unparseable) {
+        return None;
+    }
     let new = parse(new_expr_str);
-    if matches!(old, CheckExpr::Unparseable) || matches!(new, CheckExpr::Unparseable) {
+    if matches!(new, CheckExpr::Unparseable) {
         return None;
     }
     if old == new {
diff --git a/crates/vespertide-planner/src/validate/check_type_mismatch.rs b/crates/vespertide-planner/src/validate/check_type_mismatch.rs
index 363d43bc..b347cafc 100644
--- a/crates/vespertide-planner/src/validate/check_type_mismatch.rs
+++ b/crates/vespertide-planner/src/validate/check_type_mismatch.rs
@@ -157,55 +157,56 @@ fn walk_check_expr(
     out: &mut Vec,
 ) {
     match expr_node {
+        // For each predicate node the column type is resolved ONCE here;
+        // unknown columns skip silently (other validators diagnose them),
+        // and `check_one` becomes a pure literal-vs-type check.
         CheckExpr::Compare { column, value, .. } => {
-            check_one(
-                action_index,
-                table,
-                check_name,
-                column,
-                value,
-                expr_text,
-                type_map,
-                out,
-            );
-        }
-        CheckExpr::In { column, values, .. } => {
-            for v in values {
+            if let Some(&col_type) = type_map.get(&(table, column.as_str())) {
                 check_one(
                     action_index,
                     table,
                     check_name,
                     column,
-                    v,
+                    col_type,
+                    value,
                     expr_text,
-                    type_map,
                     out,
                 );
             }
         }
+        CheckExpr::In { column, values, .. } => {
+            if let Some(&col_type) = type_map.get(&(table, column.as_str())) {
+                for v in values {
+                    check_one(
+                        action_index,
+                        table,
+                        check_name,
+                        column,
+                        col_type,
+                        v,
+                        expr_text,
+                        out,
+                    );
+                }
+            }
+        }
         CheckExpr::Between {
             column, low, high, ..
         } => {
-            check_one(
-                action_index,
-                table,
-                check_name,
-                column,
-                low,
-                expr_text,
-                type_map,
-                out,
-            );
-            check_one(
-                action_index,
-                table,
-                check_name,
-                column,
-                high,
-                expr_text,
-                type_map,
-                out,
-            );
+            if let Some(&col_type) = type_map.get(&(table, column.as_str())) {
+                for boundary in [low, high] {
+                    check_one(
+                        action_index,
+                        table,
+                        check_name,
+                        column,
+                        col_type,
+                        boundary,
+                        expr_text,
+                        out,
+                    );
+                }
+            }
         }
         CheckExpr::And(parts) | CheckExpr::Or(parts) => {
             for p in parts {
@@ -229,21 +230,18 @@ fn walk_check_expr(
 
 #[expect(
     clippy::too_many_arguments,
-    reason = "F-novel-4 per-literal checker threads context (table / check_name / column / literal / expr / type_map / out) through a single call; bundling into a struct would scatter the call sites without aiding clarity"
+    reason = "F-novel-4 per-literal checker threads context (table / check_name / column / col_type / literal / expr / out) through a single call; bundling into a struct would scatter the call sites without aiding clarity"
 )]
 fn check_one(
     action_index: usize,
     table: &str,
     check_name: &str,
     column: &str,
+    col_type: &ColumnType,
     literal: &Literal,
     expr_text: &str,
-    type_map: &HashMap<(&str, &str), &ColumnType>,
     out: &mut Vec,
 ) {
-    let Some(&col_type) = type_map.get(&(table, column)) else {
-        return; // unknown column - other validators handle it
-    };
     if !is_definitely_mismatch(col_type, literal) {
         return;
     }
@@ -260,8 +258,9 @@ fn check_one(
 }
 
 /// Borrow-keyed lookup map: `(table, column) -> column type`, borrowing
-/// from `plan` + `baseline` so per-literal lookups in [`check_one`] are
-/// allocation-free. Owned strings are produced only when a warning fires.
+/// from `plan` + `baseline` so per-predicate lookups in
+/// [`walk_check_expr`] are allocation-free. Owned strings are produced
+/// only when a warning fires.
 fn build_column_type_map<'a>(
     plan: &'a MigrationPlan,
     baseline: &'a [TableDef],
@@ -705,6 +704,21 @@ mod tests {
         assert!(find_check_type_mismatches(&p, &baseline).is_empty());
     }
 
+    /// Unknown-column skip is decided ONCE per predicate node — pin the
+    /// `In` and `Between` arms of `walk_check_expr` (not just `Compare`).
+    #[test]
+    fn unknown_column_in_list_and_between_silently_pass() {
+        let baseline = vec![baseline_table(
+            "t",
+            vec![col("age", ColumnType::Simple(SimpleColumnType::Integer))],
+        )];
+        let p = plan(vec![
+            add_check("t", "chk_in", "missing_col IN ('a', 'b')"),
+            add_check("t", "chk_between", "missing_col BETWEEN 'x' AND 'y'"),
+        ]);
+        assert!(find_check_type_mismatches(&p, &baseline).is_empty());
+    }
+
     #[test]
     fn unparseable_check_silently_passes() {
         let baseline = vec![baseline_table(

From cf4d6ada87023b63e1f837439fed0c723481b7f5 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 06:24:46 +0900
Subject: [PATCH 059/109] retry-now#0058: batch - dedupe seaorm PK scan via
 constraint_scan, loader read_config helper, allocation-light action Display
 (Cow truncate_comment), single-buffer to_pascal_case, borrow-based
 drop-resolution table diff (5 kept)

---
 crates/vespertide-core/src/action/display.rs  | 33 +++++++++++--------
 .../vespertide-exporter/src/python_naming.rs  | 18 +++++-----
 .../vespertide-exporter/src/seaorm/render.rs  | 13 ++------
 crates/vespertide-loader/src/config.rs        | 24 +++++++-------
 .../vespertide-planner/src/drop_resolution.rs | 28 +++++++---------
 5 files changed, 55 insertions(+), 61 deletions(-)

diff --git a/crates/vespertide-core/src/action/display.rs b/crates/vespertide-core/src/action/display.rs
index 358b171c..45082b08 100644
--- a/crates/vespertide-core/src/action/display.rs
+++ b/crates/vespertide-core/src/action/display.rs
@@ -1,5 +1,6 @@
 use super::MigrationAction;
 use crate::schema::TableConstraint;
+use std::borrow::Cow;
 use std::fmt;
 
 impl fmt::Display for MigrationAction {
@@ -57,12 +58,14 @@ fn write_migration_action(f: &mut fmt::Formatter<'_>, action: &MigrationAction)
             column,
             mapping,
         } => {
-            let summary = mapping
-                .iter()
-                .map(|(old, new)| format!("{old}->{new}"))
-                .collect::>()
-                .join(", ");
-            write!(f, "RemapEnumValues: {table}.{column} [{summary}]")
+            write!(f, "RemapEnumValues: {table}.{column} [")?;
+            for (i, (old, new)) in mapping.iter().enumerate() {
+                if i > 0 {
+                    write!(f, ", ")?;
+                }
+                write!(f, "{old}->{new}")?;
+            }
+            write!(f, "]")
         }
     }
 }
@@ -111,11 +114,13 @@ fn write_comment_action(
 /// shared by [`MigrationAction`]'s `Display` impl and the CLI diff formatter
 /// so the two renderings can never drift apart.
 #[must_use]
-pub fn truncate_comment(comment: &str) -> String {
-    if comment.chars().count() > 30 {
-        format!("{}...", truncate_chars(comment, 27))
+pub fn truncate_comment(comment: &str) -> Cow<'_, str> {
+    // A 31st char existing (index 30) means the comment exceeds the budget;
+    // probing one index avoids a full chars().count() scan on long comments.
+    if comment.char_indices().nth(30).is_some() {
+        Cow::Owned(format!("{}...", truncate_chars(comment, 27)))
     } else {
-        comment.to_string()
+        Cow::Borrowed(comment)
     }
 }
 
@@ -141,14 +146,14 @@ fn write_constraint_action(
     match constraint {
         TableConstraint::PrimaryKey { .. } => write!(f, "{action}: {table}.PRIMARY KEY"),
         TableConstraint::Unique { name, .. } => {
-            write_named_constraint(f, action, table, name.as_ref(), "UNIQUE")
+            write_named_constraint(f, action, table, name.as_deref(), "UNIQUE")
         }
         TableConstraint::ForeignKey { name, .. } => {
-            write_named_constraint(f, action, table, name.as_ref(), "FOREIGN KEY")
+            write_named_constraint(f, action, table, name.as_deref(), "FOREIGN KEY")
         }
         TableConstraint::Check { name, .. } => write!(f, "{action}: {table}.{name} (CHECK)"),
         TableConstraint::Index { name, .. } => {
-            write_named_constraint(f, action, table, name.as_ref(), "INDEX")
+            write_named_constraint(f, action, table, name.as_deref(), "INDEX")
         }
     }
 }
@@ -157,7 +162,7 @@ fn write_named_constraint(
     f: &mut fmt::Formatter<'_>,
     action: &str,
     table: &str,
-    name: Option<&String>,
+    name: Option<&str>,
     fallback: &str,
 ) -> fmt::Result {
     if let Some(name) = name {
diff --git a/crates/vespertide-exporter/src/python_naming.rs b/crates/vespertide-exporter/src/python_naming.rs
index 9bb33e74..c47982c8 100644
--- a/crates/vespertide-exporter/src/python_naming.rs
+++ b/crates/vespertide-exporter/src/python_naming.rs
@@ -15,15 +15,15 @@
 /// PascalCase semantics for JPA filename derivation without keeping a
 /// duplicate private implementation.
 pub fn to_pascal_case(s: &str) -> String {
-    s.split('_')
-        .map(|word| {
-            let mut chars = word.chars();
-            match chars.next() {
-                None => String::new(),
-                Some(first) => first.to_uppercase().chain(chars).collect(),
-            }
-        })
-        .collect()
+    let mut result = String::with_capacity(s.len());
+    for word in s.split('_') {
+        let mut chars = word.chars();
+        if let Some(first) = chars.next() {
+            result.extend(first.to_uppercase());
+            result.push_str(chars.as_str());
+        }
+    }
+    result
 }
 
 /// Convert any input to SCREAMING_SNAKE_CASE: inserts `_` before interior
diff --git a/crates/vespertide-exporter/src/seaorm/render.rs b/crates/vespertide-exporter/src/seaorm/render.rs
index 23071bc5..a9383e44 100644
--- a/crates/vespertide-exporter/src/seaorm/render.rs
+++ b/crates/vespertide-exporter/src/seaorm/render.rs
@@ -219,7 +219,7 @@ pub(super) fn render_column(
         }
         // JSONB custom type should use Json rust type
         ColumnType::Complex(ComplexColumnType::Custom { custom_type })
-            if custom_type.to_uppercase() == "JSONB" =>
+            if custom_type.eq_ignore_ascii_case("JSONB") =>
         {
             if column.nullable {
                 "Option".to_string()
@@ -234,16 +234,9 @@ pub(super) fn render_column(
 }
 pub(super) fn primary_key_columns(table: &TableDef) -> HashSet {
     use vespertide_core::schema::primary_key::PrimaryKeySyntax;
-    let mut keys = HashSet::new();
 
-    // First, check table-level constraints
-    for constraint in &table.constraints {
-        if let TableConstraint::PrimaryKey { columns, .. } = constraint {
-            for col in columns {
-                keys.insert(col.to_string());
-            }
-        }
-    }
+    // Table-level constraints via the shared scan (single source of truth).
+    let mut keys = crate::constraint_scan::primary_key_columns(&table.constraints);
 
     // Then, check inline primary_key on columns
     // This handles cases where primary_key is defined inline but not yet normalized
diff --git a/crates/vespertide-loader/src/config.rs b/crates/vespertide-loader/src/config.rs
index b5e57273..0381bc16 100644
--- a/crates/vespertide-loader/src/config.rs
+++ b/crates/vespertide-loader/src/config.rs
@@ -1,9 +1,17 @@
 use std::fs;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 
 use anyhow::{Context, Result};
 use vespertide_config::VespertideConfig;
 
+/// Read and parse a vespertide.json file that is known to exist.
+fn read_config(path: &Path) -> Result {
+    let content = fs::read_to_string(path).context("read vespertide.json")?;
+    let config: VespertideConfig =
+        serde_json::from_str(&content).context("parse vespertide.json")?;
+    Ok(config)
+}
+
 /// Load vespertide.json config from current directory.
 pub fn load_config() -> Result {
     let path = PathBuf::from("vespertide.json");
@@ -11,23 +19,17 @@ pub fn load_config() -> Result {
         anyhow::bail!("vespertide.json not found. Run 'vespertide init' first.");
     }
 
-    let content = fs::read_to_string(&path).context("read vespertide.json")?;
-    let config: VespertideConfig =
-        serde_json::from_str(&content).context("parse vespertide.json")?;
-    Ok(config)
+    read_config(&path)
 }
 
 /// Load config from a specific path.
-pub fn load_config_from_path(path: PathBuf) -> Result {
-    let path = path.into_boxed_path();
+pub fn load_config_from_path(path: impl AsRef) -> Result {
+    let path = path.as_ref();
     if !path.exists() {
         anyhow::bail!("vespertide.json not found at: {}", path.display());
     }
 
-    let content = fs::read_to_string(&path).context("read vespertide.json")?;
-    let config: VespertideConfig =
-        serde_json::from_str(&content).context("parse vespertide.json")?;
-    Ok(config)
+    read_config(path)
 }
 
 /// Load config from project root, with fallback to defaults.
diff --git a/crates/vespertide-planner/src/drop_resolution.rs b/crates/vespertide-planner/src/drop_resolution.rs
index 2cf7a658..cae81b00 100644
--- a/crates/vespertide-planner/src/drop_resolution.rs
+++ b/crates/vespertide-planner/src/drop_resolution.rs
@@ -215,10 +215,10 @@ fn resolve_table_drop(
     plan: &MigrationPlan,
     baseline: &[TableDef],
 ) -> DropResolution {
-    let baseline_columns: Vec = baseline
+    let baseline_columns: Vec<&str> = baseline
         .iter()
         .find(|t| t.name == table)
-        .map(|t| t.columns.iter().map(|c| c.name.to_string()).collect())
+        .map(|t| t.columns.iter().map(|c| c.name.as_str()).collect())
         .unwrap_or_default();
 
     let mut candidates: Vec = plan
@@ -300,31 +300,25 @@ fn column_candidate(dropped: Option<&ColumnDef>, added: &ColumnDef) -> RenameCan
 }
 
 fn table_candidate(
-    baseline_columns: &[String],
+    baseline_columns: &[&str],
     new_name: &str,
     new_columns: &[ColumnDef],
 ) -> RenameCandidate {
-    let added_names: Vec = new_columns.iter().map(|c| c.name.to_string()).collect();
-
-    let baseline_set: std::collections::HashSet<&str> =
-        baseline_columns.iter().map(String::as_str).collect();
+    let baseline_set: std::collections::HashSet<&str> = baseline_columns.iter().copied().collect();
     let added_set: std::collections::HashSet<&str> =
-        added_names.iter().map(String::as_str).collect();
+        new_columns.iter().map(|c| c.name.as_str()).collect();
 
-    let only_in_baseline: Vec<&&str> = baseline_set.difference(&added_set).collect();
-    let only_in_new: Vec<&&str> = added_set.difference(&baseline_set).collect();
+    let mut only_in_baseline: Vec<&str> = baseline_set.difference(&added_set).copied().collect();
+    let mut only_in_new: Vec<&str> = added_set.difference(&baseline_set).copied().collect();
 
     let mut differences = Vec::new();
     if !only_in_baseline.is_empty() {
-        let mut names: Vec =
-            vespertide_core::schema::names::names_to_strings(&only_in_baseline);
-        names.sort_unstable();
-        differences.push(format!("removed columns: {}", names.join(", ")));
+        only_in_baseline.sort_unstable();
+        differences.push(format!("removed columns: {}", only_in_baseline.join(", ")));
     }
     if !only_in_new.is_empty() {
-        let mut names: Vec = vespertide_core::schema::names::names_to_strings(&only_in_new);
-        names.sort_unstable();
-        differences.push(format!("added columns: {}", names.join(", ")));
+        only_in_new.sort_unstable();
+        differences.push(format!("added columns: {}", only_in_new.join(", ")));
     }
 
     let match_quality = if differences.is_empty() {

From e8d4c68336d74cc5367c42d6fd29671fde2a474d Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 09:11:24 +0900
Subject: [PATCH 060/109] =?UTF-8?q?retry-now#0059:=20batch=20=E2=80=94=20d?=
 =?UTF-8?q?rop=20RawSql=20Display=20alloc,=20BTreeSet=20constraint-diff=20?=
 =?UTF-8?q?membership=20(2=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 crates/vespertide-core/src/action/display.rs      | 10 +++++-----
 crates/vespertide-planner/src/diff/constraints.rs | 10 ++++++++--
 2 files changed, 13 insertions(+), 7 deletions(-)

diff --git a/crates/vespertide-core/src/action/display.rs b/crates/vespertide-core/src/action/display.rs
index 45082b08..32aa0aa9 100644
--- a/crates/vespertide-core/src/action/display.rs
+++ b/crates/vespertide-core/src/action/display.rs
@@ -129,12 +129,12 @@ fn truncate_chars(s: &str, max_chars: usize) -> String {
 }
 
 fn write_raw_sql_action(f: &mut fmt::Formatter<'_>, sql: &str) -> fmt::Result {
-    let display_sql = if sql.chars().count() > 50 {
-        format!("{}...", truncate_chars(sql, 47))
+    if sql.chars().count() > 50 {
+        let head = truncate_chars(sql, 47);
+        write!(f, "RawSql: {head}...")
     } else {
-        sql.to_string()
-    };
-    write!(f, "RawSql: {display_sql}")
+        write!(f, "RawSql: {sql}")
+    }
 }
 
 fn write_constraint_action(
diff --git a/crates/vespertide-planner/src/diff/constraints.rs b/crates/vespertide-planner/src/diff/constraints.rs
index d7733f1e..eaf830ca 100644
--- a/crates/vespertide-planner/src/diff/constraints.rs
+++ b/crates/vespertide-planner/src/diff/constraints.rs
@@ -69,6 +69,12 @@ pub(super) fn diff_constraints(
         &mut replaced_from,
         &mut replaced_to,
     );
+    // Promote the "already handled by a replacement" index lists to sets so the
+    // per-constraint membership tests in the removed/added passes are O(log n)
+    // instead of a linear `Vec::contains` scan (quadratic on wide tables).
+    let replaced_from: BTreeSet = replaced_from.into_iter().collect();
+    let replaced_to: BTreeSet = replaced_to.into_iter().collect();
+
     diff_removed_constraints(
         actions,
         table_name,
@@ -130,7 +136,7 @@ fn diff_removed_constraints(
     from_tbl: &TableDef,
     to_tbl: &TableDef,
     deleted_columns: &BTreeSet,
-    replaced_from: &[usize],
+    replaced_from: &BTreeSet,
 ) {
     for (fi, from_constraint) in from_tbl.constraints.iter().enumerate() {
         if to_tbl.constraints.contains(from_constraint) || replaced_from.contains(&fi) {
@@ -156,7 +162,7 @@ fn diff_added_constraints(
     table_name: &str,
     from_tbl: &TableDef,
     to_tbl: &TableDef,
-    replaced_to: &[usize],
+    replaced_to: &BTreeSet,
 ) {
     for (ti, to_constraint) in to_tbl.constraints.iter().enumerate() {
         if from_tbl.constraints.contains(to_constraint) || replaced_to.contains(&ti) {

From f48e63f80d9b5ca72c55c0e8eae65b1189890215 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 09:27:27 +0900
Subject: [PATCH 061/109] retry-now#0060: batch - dedupe/alloc micro-wins in
 create_table, constraints, check-clauses, ordering (4 kept)

---
 .../src/diff/constraints.rs                   | 32 ++++++++++++-------
 .../vespertide-planner/src/diff/ordering.rs   |  6 +++-
 .../vespertide-query/src/sql/create_table.rs  |  4 +--
 crates/vespertide-query/src/sql/helpers.rs    |  7 +++-
 4 files changed, 33 insertions(+), 16 deletions(-)

diff --git a/crates/vespertide-planner/src/diff/constraints.rs b/crates/vespertide-planner/src/diff/constraints.rs
index eaf830ca..e4a0410a 100644
--- a/crates/vespertide-planner/src/diff/constraints.rs
+++ b/crates/vespertide-planner/src/diff/constraints.rs
@@ -61,13 +61,21 @@ pub(super) fn diff_constraints(
     let mut replaced_from: Vec = Vec::new();
     let mut replaced_to: Vec = Vec::new();
 
+    // Build the exact-match constraint sets ONCE and share them across the
+    // removed/added passes so every "does the other table contain this exact
+    // constraint?" test is an O(log n) `BTreeSet::contains` rather than a linear
+    // `Vec::contains` scan (quadratic on wide tables).
+    let exact_from: BTreeSet<&TableConstraint> = from_tbl.constraints.iter().collect();
+    let exact_to: BTreeSet<&TableConstraint> = to_tbl.constraints.iter().collect();
+
     diff_replaced_constraints(
         actions,
         table_name,
         from_tbl,
         to_tbl,
-        &mut replaced_from,
-        &mut replaced_to,
+        &exact_from,
+        &exact_to,
+        (&mut replaced_from, &mut replaced_to),
     );
     // Promote the "already handled by a replacement" index lists to sets so the
     // per-constraint membership tests in the removed/added passes are O(log n)
@@ -79,11 +87,11 @@ pub(super) fn diff_constraints(
         actions,
         table_name,
         from_tbl,
-        to_tbl,
+        &exact_to,
         deleted_columns,
         &replaced_from,
     );
-    diff_added_constraints(actions, table_name, from_tbl, to_tbl, &replaced_to);
+    diff_added_constraints(actions, table_name, to_tbl, &exact_from, &replaced_to);
 }
 
 fn diff_replaced_constraints(
@@ -91,11 +99,11 @@ fn diff_replaced_constraints(
     table_name: &str,
     from_tbl: &TableDef,
     to_tbl: &TableDef,
-    replaced_from: &mut Vec,
-    replaced_to: &mut Vec,
+    exact_from: &BTreeSet<&TableConstraint>,
+    exact_to: &BTreeSet<&TableConstraint>,
+    replaced: (&mut Vec, &mut Vec),
 ) {
-    let exact_from: BTreeSet<&TableConstraint> = from_tbl.constraints.iter().collect();
-    let exact_to: BTreeSet<&TableConstraint> = to_tbl.constraints.iter().collect();
+    let (replaced_from, replaced_to) = replaced;
     let mut to_index: BTreeMap, Vec> = BTreeMap::new();
 
     for (ti, to_constraint) in to_tbl.constraints.iter().enumerate() {
@@ -134,12 +142,12 @@ fn diff_removed_constraints(
     actions: &mut Vec,
     table_name: &str,
     from_tbl: &TableDef,
-    to_tbl: &TableDef,
+    exact_to: &BTreeSet<&TableConstraint>,
     deleted_columns: &BTreeSet,
     replaced_from: &BTreeSet,
 ) {
     for (fi, from_constraint) in from_tbl.constraints.iter().enumerate() {
-        if to_tbl.constraints.contains(from_constraint) || replaced_from.contains(&fi) {
+        if exact_to.contains(from_constraint) || replaced_from.contains(&fi) {
             continue;
         }
         let constraint_columns = from_constraint.columns();
@@ -160,12 +168,12 @@ fn diff_removed_constraints(
 fn diff_added_constraints(
     actions: &mut Vec,
     table_name: &str,
-    from_tbl: &TableDef,
     to_tbl: &TableDef,
+    exact_from: &BTreeSet<&TableConstraint>,
     replaced_to: &BTreeSet,
 ) {
     for (ti, to_constraint) in to_tbl.constraints.iter().enumerate() {
-        if from_tbl.constraints.contains(to_constraint) || replaced_to.contains(&ti) {
+        if exact_from.contains(to_constraint) || replaced_to.contains(&ti) {
             continue;
         }
         actions.push(MigrationAction::AddConstraint {
diff --git a/crates/vespertide-planner/src/diff/ordering.rs b/crates/vespertide-planner/src/diff/ordering.rs
index a85874cd..343020ce 100644
--- a/crates/vespertide-planner/src/diff/ordering.rs
+++ b/crates/vespertide-planner/src/diff/ordering.rs
@@ -93,10 +93,14 @@ pub(super) fn topological_sort_tables<'a>(
 
     // Check for cycles
     if result.len() != tables.len() {
+        // Collect the already-placed table names once so the `remaining` filter
+        // is an O(log n) set lookup instead of a nested `result.iter().any(...)`
+        // rescan per table. Cold error path, so this is a clarity/complexity win.
+        let placed: BTreeSet<&str> = result.iter().map(|t| t.name.as_str()).collect();
         let remaining: Vec<&str> = tables
             .iter()
             .map(|t| t.name.as_str())
-            .filter(|name| !result.iter().any(|t| t.name.as_str() == *name))
+            .filter(|name| !placed.contains(name))
             .collect();
         return Err(PlannerError::TableValidation(format!(
             "Circular foreign key dependency detected among tables: {remaining:?}"
diff --git a/crates/vespertide-query/src/sql/create_table.rs b/crates/vespertide-query/src/sql/create_table.rs
index 656f86ce..65123aeb 100644
--- a/crates/vespertide-query/src/sql/create_table.rs
+++ b/crates/vespertide-query/src/sql/create_table.rs
@@ -218,10 +218,10 @@ pub fn build_create_table(
 
     // Create enum types first (PostgreSQL only)
     // Collect unique enum types to avoid duplicates
-    let mut created_enums = std::collections::HashSet::new();
+    let mut created_enums: std::collections::HashSet<&str> = std::collections::HashSet::new();
     for column in columns {
         if let ColumnType::Complex(ComplexColumnType::Enum { name, .. }) = &column.r#type
-            && created_enums.insert(name.clone())
+            && created_enums.insert(name.as_str())
             && let Some(create_type_sql) = build_create_enum_type_sql(table, &column.r#type)
         {
             queries.push(BuiltQuery::Raw(create_type_sql));
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index bc6ebedb..447b80a3 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -561,9 +561,14 @@ pub(crate) fn collect_all_check_clauses(
     constraints: &[TableConstraint],
 ) -> Vec {
     let mut clauses = collect_sqlite_enum_check_clauses(table, columns);
+    // Track membership in a set so each explicit clause is a single O(log n)
+    // lookup rather than a linear `Vec::contains` rescan (quadratic on tables
+    // with many CHECK constraints). Insertion order is preserved: enum clauses
+    // first, then explicit clauses in source order.
+    let mut seen: std::collections::BTreeSet = clauses.iter().cloned().collect();
     let explicit = extract_check_clauses(constraints);
     for clause in explicit {
-        if !clauses.contains(&clause) {
+        if seen.insert(clause.clone()) {
             clauses.push(clause);
         }
     }

From 6f4d63dd755da895a17c49a7066502e68326d715 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 09:41:26 +0900
Subject: [PATCH 062/109] =?UTF-8?q?retry-now#0061:=20batch=20=E2=80=94=20e?=
 =?UTF-8?q?lide=20allocations=20in=20query=20CREATE=20TABLE=20/=20add-colu?=
 =?UTF-8?q?mn=20(3=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 crates/vespertide-query/src/sql/add_column.rs | 15 +++++------
 .../vespertide-query/src/sql/create_table.rs  | 25 +++++++++----------
 2 files changed, 20 insertions(+), 20 deletions(-)

diff --git a/crates/vespertide-query/src/sql/add_column.rs b/crates/vespertide-query/src/sql/add_column.rs
index be7bb0c9..ab67d3f2 100644
--- a/crates/vespertide-query/src/sql/add_column.rs
+++ b/crates/vespertide-query/src/sql/add_column.rs
@@ -65,10 +65,16 @@ pub fn build_add_column(
             &table_def.constraints,
         );
 
-        // Copy existing data, filling new column
+        // Copy existing data, filling new column. Build the existing-column
+        // aliases once and reuse them for both the SELECT column list and the
+        // INSERT column list (the new column's alias is appended for the INSERT
+        // only, since its value comes from `expr_as(fill_expr, ...)`).
+        let mut columns_alias: Vec = Vec::with_capacity(table_def.columns.len() + 1);
         let mut select_query = Query::select();
         for col in &table_def.columns {
-            select_query.column(Alias::new(&col.name));
+            let alias = Alias::new(&col.name);
+            select_query.column(alias.clone());
+            columns_alias.push(alias);
         }
         let fill_expr = if let Some(fill) = normalize_fill_with(fill_with) {
             let converted = convert_default_for_backend(fill, backend);
@@ -83,11 +89,6 @@ pub fn build_add_column(
             .expr_as(fill_expr, Alias::new(&column.name))
             .from(Alias::new(table));
 
-        let mut columns_alias: Vec = table_def
-            .columns
-            .iter()
-            .map(|c| Alias::new(&c.name))
-            .collect();
         columns_alias.push(Alias::new(&column.name));
         let insert_stmt = Query::insert()
             .into_table(Alias::new(&temp_table))
diff --git a/crates/vespertide-query/src/sql/create_table.rs b/crates/vespertide-query/src/sql/create_table.rs
index 65123aeb..5b46a87b 100644
--- a/crates/vespertide-query/src/sql/create_table.rs
+++ b/crates/vespertide-query/src/sql/create_table.rs
@@ -62,7 +62,7 @@ fn collect_auto_increment_columns(
                 ..
             } = c
             {
-                Some(pk_cols.iter().map(AsRef::as_ref).collect::>())
+                Some(pk_cols.iter().map(AsRef::as_ref))
             } else {
                 None
             }
@@ -228,22 +228,21 @@ pub fn build_create_table(
         }
     }
 
-    // Separate unique constraints for Postgres and SQLite (they need separate CREATE INDEX statements)
-    // For MySQL, unique constraints are added directly in CREATE TABLE via build_create_table_for_backend
-    let (table_constraints, unique_constraints): (Vec<&TableConstraint>, Vec<&TableConstraint>) =
-        constraints
-            .iter()
-            .partition(|c| !matches!(c, TableConstraint::Unique { .. }));
-
     // Build CREATE TABLE
-    // For MySQL, include unique constraints in CREATE TABLE
-    // For Postgres and SQLite, exclude them (will be added as separate CREATE INDEX statements)
+    // For MySQL, include unique constraints in CREATE TABLE (no partition needed).
+    // For Postgres and SQLite, exclude them here — they are emitted below as
+    // separate CREATE UNIQUE INDEX statements, so partition off the uniques only
+    // in that branch (the MySQL path discards both halves, so allocating them
+    // there is pure waste).
     let create_table_stmt = if matches!(backend, DatabaseBackend::MySql) {
         build_create_table_for_backend(backend, table, columns, constraints)
     } else {
         // Convert references to owned values for build_create_table_for_backend
-        let table_constraints_owned: Vec =
-            table_constraints.iter().copied().cloned().collect();
+        let table_constraints_owned: Vec = constraints
+            .iter()
+            .filter(|c| !matches!(c, TableConstraint::Unique { .. }))
+            .cloned()
+            .collect();
         build_create_table_for_backend(backend, table, columns, &table_constraints_owned)
     };
 
@@ -267,7 +266,7 @@ pub fn build_create_table(
 
     // For Postgres and SQLite, add unique constraints as separate CREATE UNIQUE INDEX statements
     if matches!(backend, DatabaseBackend::Postgres | DatabaseBackend::Sqlite) {
-        for constraint in unique_constraints {
+        for constraint in constraints {
             if let TableConstraint::Unique {
                 name,
                 columns: unique_cols,

From 2ebd5bb273c49ce1903c4077190151ce07c8020c Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 09:58:20 +0900
Subject: [PATCH 063/109] =?UTF-8?q?retry-now#0062:=20batch=20=E2=80=94=20r?=
 =?UTF-8?q?elocate=20single=5Fcolumn=5Findexes=20to=20constraint=5Fscan,?=
 =?UTF-8?q?=20short-circuit=20identical=20column=20defaults=20in=20diff=20?=
 =?UTF-8?q?(2=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/constraint_scan.rs                    | 24 +++++++++++++++----
 .../vespertide-exporter/src/seaorm/render.rs  | 15 +-----------
 crates/vespertide-planner/src/diff/columns.rs |  9 +++++++
 3 files changed, 30 insertions(+), 18 deletions(-)

diff --git a/crates/vespertide-exporter/src/constraint_scan.rs b/crates/vespertide-exporter/src/constraint_scan.rs
index 412b8176..a0c30a88 100644
--- a/crates/vespertide-exporter/src/constraint_scan.rs
+++ b/crates/vespertide-exporter/src/constraint_scan.rs
@@ -1,9 +1,10 @@
 //! Shared constraint-scan helpers used by the ORM renderers.
 //!
-//! Every backend needs the same two lookup sets when rendering a table:
-//! the columns covered by table-level primary keys and the columns that
-//! carry a single-column unique constraint. Centralising the scans keeps
-//! the four renderers from drifting apart.
+//! Every backend needs the same lookup sets when rendering a table:
+//! the columns covered by table-level primary keys, the columns that
+//! carry a single-column unique constraint, and the columns that carry
+//! a single-column index. Centralising the scans keeps the four
+//! renderers from drifting apart.
 
 use std::collections::HashSet;
 
@@ -38,3 +39,18 @@ pub(crate) fn single_column_uniques(constraints: &[TableConstraint]) -> HashSet<
     }
     unique_cols
 }
+
+/// Collect the column names that carry a single-column `Index` constraint.
+///
+/// Lookup-only, ordering unused.
+pub(crate) fn single_column_indexes(constraints: &[TableConstraint]) -> HashSet {
+    let mut indexed_cols = HashSet::new();
+    for constraint in constraints {
+        if let TableConstraint::Index { columns, .. } = constraint
+            && columns.len() == 1
+        {
+            indexed_cols.insert(columns[0].to_string());
+        }
+    }
+    indexed_cols
+}
diff --git a/crates/vespertide-exporter/src/seaorm/render.rs b/crates/vespertide-exporter/src/seaorm/render.rs
index a9383e44..4adfd1f9 100644
--- a/crates/vespertide-exporter/src/seaorm/render.rs
+++ b/crates/vespertide-exporter/src/seaorm/render.rs
@@ -27,7 +27,7 @@ pub fn render_entity_with_config_and_paths(
 
     // Build sets of columns with single-column unique constraints and indexes
     let unique_columns = crate::constraint_scan::single_column_uniques(&table.constraints);
-    let indexed_columns = single_column_index_set(&table.constraints);
+    let indexed_columns = crate::constraint_scan::single_column_indexes(&table.constraints);
 
     // Check if any columns use enum types (enums derive Serialize/Deserialize)
     let has_enums = table.columns.iter().any(|c| {
@@ -136,19 +136,6 @@ pub fn render_entity_with_config_and_paths(
     lines.join("\n")
 }
 
-/// Build a set of column names that have single-column indexes from constraints.
-pub(super) fn single_column_index_set(constraints: &[TableConstraint]) -> HashSet {
-    let mut indexed_cols = HashSet::new();
-    for constraint in constraints {
-        if let TableConstraint::Index { columns, .. } = constraint
-            && columns.len() == 1
-        {
-            indexed_cols.insert(columns[0].to_string());
-        }
-    }
-    indexed_cols
-}
-
 pub(super) fn render_column(
     lines: &mut Vec,
     column: &ColumnDef,
diff --git a/crates/vespertide-planner/src/diff/columns.rs b/crates/vespertide-planner/src/diff/columns.rs
index 0357ac79..c2ed3455 100644
--- a/crates/vespertide-planner/src/diff/columns.rs
+++ b/crates/vespertide-planner/src/diff/columns.rs
@@ -197,6 +197,15 @@ fn diff_column_defaults(
 ) {
     for (col, to_def) in to_cols {
         if let Some(from_def) = from_cols.get(col) {
+            // Fast path: byte-identical raw defaults can never differ once
+            // lowered to SQL, so skip the two `to_sql()` allocations for the
+            // overwhelmingly common unchanged-column case. Structurally
+            // distinct defaults still fall through to the SQL-level compare
+            // below, so no `ModifyColumnDefault` is ever spuriously added or
+            // dropped.
+            if from_def.default == to_def.default {
+                continue;
+            }
             let from_default = from_def
                 .default
                 .as_ref()

From 6663ed587c9ab952c151fe60c490a954c1c3e104 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 10:16:35 +0900
Subject: [PATCH 064/109] =?UTF-8?q?retry-now#0063:=20batch=20=E2=80=94=20s?=
 =?UTF-8?q?kip=20auto-inc=20HashSet=20alloc,=20direct-build=20stripped=20A?=
 =?UTF-8?q?ddColumn,=20any()=20idiom,=20drop=20enum=20Vec<&str>=20(4=20kep?=
 =?UTF-8?q?t)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 crates/vespertide-planner/src/diff/columns.rs | 22 ++++++++++++++-----
 .../vespertide-query/src/sql/create_table.rs  | 19 +++++++++++-----
 crates/vespertide-query/src/sql/helpers.rs    | 21 ++++++++++++------
 3 files changed, 45 insertions(+), 17 deletions(-)

diff --git a/crates/vespertide-planner/src/diff/columns.rs b/crates/vespertide-planner/src/diff/columns.rs
index c2ed3455..43c014ab 100644
--- a/crates/vespertide-planner/src/diff/columns.rs
+++ b/crates/vespertide-planner/src/diff/columns.rs
@@ -253,11 +253,23 @@ fn diff_added_columns(
 ) {
     for (col, def) in to_cols {
         if !from_cols.contains_key(col) {
-            let mut col_def = (*def).clone();
-            col_def.primary_key = None;
-            col_def.unique = None;
-            col_def.index = None;
-            col_def.foreign_key = None;
+            // Build the stripped column directly instead of cloning the full
+            // `ColumnDef` then clearing the four inline-constraint fields — the
+            // clone would deep-copy those `Option` payloads (usually `Some(..)`
+            // on freshly-authored columns) only to discard them. Emitted payload
+            // is byte-identical. An exhaustive struct literal keeps this in sync
+            // with `ColumnDef` (a new field fails to compile until handled here).
+            let col_def = ColumnDef {
+                name: def.name.clone(),
+                r#type: def.r#type.clone(),
+                nullable: def.nullable,
+                default: def.default.clone(),
+                comment: def.comment.clone(),
+                primary_key: None,
+                unique: None,
+                index: None,
+                foreign_key: None,
+            };
             actions.push(MigrationAction::AddColumn {
                 table: table_name.into(),
                 column: Box::new(col_def),
diff --git a/crates/vespertide-query/src/sql/create_table.rs b/crates/vespertide-query/src/sql/create_table.rs
index 5b46a87b..2bc3e838 100644
--- a/crates/vespertide-query/src/sql/create_table.rs
+++ b/crates/vespertide-query/src/sql/create_table.rs
@@ -31,7 +31,17 @@ fn add_create_table_columns(
     let has_table_primary_key = constraints
         .iter()
         .any(|c| matches!(c, TableConstraint::PrimaryKey { .. }));
-    let auto_increment_columns = collect_auto_increment_columns(constraints);
+    // The vast majority of tables have no `PrimaryKey { auto_increment: true }`
+    // constraint, so skip the `HashSet` allocation + constraint scan entirely in
+    // that common case; an empty set produces byte-identical output.
+    let auto_increment_columns = if constraints
+        .iter()
+        .any(|c| matches!(c, TableConstraint::PrimaryKey { auto_increment: true, .. }))
+    {
+        collect_auto_increment_columns(constraints)
+    } else {
+        std::collections::HashSet::new()
+    };
 
     for column in columns {
         let mut col = build_sea_column_def_with_table(backend, table, column);
@@ -131,10 +141,9 @@ fn should_skip_sqlite_auto_increment_pk(
     matches!(backend, DatabaseBackend::Sqlite)
         && auto_increment
         && pk_cols.iter().all(|col_name| {
-            columns
-                .iter()
-                .find(|c| c.name == col_name.as_ref())
-                .is_some_and(|c| c.r#type.supports_auto_increment())
+            columns.iter().any(|c| {
+                c.name == col_name.as_ref() && c.r#type.supports_auto_increment()
+            })
         })
 }
 
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index 447b80a3..5ff0a1f0 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -4,8 +4,8 @@ use sea_query::{
 };
 
 use vespertide_core::{
-    ColumnDef, ColumnType, ComplexColumnType, ReferenceAction, SimpleColumnType, TableConstraint,
-    TableDef,
+    ColumnDef, ColumnType, ComplexColumnType, EnumValues, ReferenceAction, SimpleColumnType,
+    TableConstraint, TableDef,
 };
 
 use super::create_table::build_create_table_for_backend;
@@ -175,11 +175,9 @@ fn apply_complex_column_type(
             } else {
                 // Use table-prefixed enum type name to avoid conflicts
                 let type_name = build_enum_type_name(table, name);
-                let variants = values
-                    .variant_names()
-                    .into_iter()
-                    .map(Alias::new)
-                    .collect::>();
+                // Map each variant name straight into `Alias::new`, skipping the
+                // intermediate `Vec<&str>` that `variant_names()` would allocate.
+                let variants = enum_variant_aliases(values);
                 col.enumeration(Alias::new(&type_name), variants);
             }
         }
@@ -187,6 +185,15 @@ fn apply_complex_column_type(
     }
 }
 
+/// Build the `sea_query` `Alias` list for a string enum's variants without the
+/// intermediate `Vec<&str>` that `EnumValues::variant_names()` would allocate.
+fn enum_variant_aliases(values: &EnumValues) -> Vec {
+    match values {
+        EnumValues::String(variants) => variants.iter().map(Alias::new).collect(),
+        EnumValues::Integer(variants) => variants.iter().map(|v| Alias::new(&v.name)).collect(),
+    }
+}
+
 fn apply_numeric_type(
     col: &mut SeaColumnDef,
     precision: u32,

From 47fe5a8017a3f92eb991387628342046994e9199 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 10:36:26 +0900
Subject: [PATCH 065/109] retry-now#0064: batch - dedupe normalize group-key
 allocs, drop throwaway Vec in fk_attr_value, merge dead CHECK arm (4 kept)

---
 .../src/schema/table/normalize.rs             | 44 +++++++++++--------
 .../src/seaorm/relations/naming.rs            | 21 ++++++---
 .../vespertide-query/src/sql/create_table.rs  |  7 ++-
 3 files changed, 43 insertions(+), 29 deletions(-)

diff --git a/crates/vespertide-core/src/schema/table/normalize.rs b/crates/vespertide-core/src/schema/table/normalize.rs
index 24c39e56..48101cf1 100644
--- a/crates/vespertide-core/src/schema/table/normalize.rs
+++ b/crates/vespertide-core/src/schema/table/normalize.rs
@@ -114,20 +114,20 @@ fn collect_unique_groups(table: &TableDef) -> (ColumnGroups, ColumnGroupOrder) {
         if let Some(ref unique_val) = col.unique {
             match unique_val {
                 StrOrBoolOrArray::Str(name) => {
-                    push_grouped_column(&mut groups, &mut order, name, &col.name);
+                    push_grouped_column(&mut groups, &mut order, name.clone(), &col.name);
                 }
                 StrOrBoolOrArray::Bool(true) => {
                     push_grouped_column(
                         &mut groups,
                         &mut order,
-                        &format!("__auto_{}", col.name),
+                        format!("__auto_{}", col.name),
                         &col.name,
                     );
                 }
                 StrOrBoolOrArray::Bool(false) => {}
                 StrOrBoolOrArray::Array(names) => {
                     for unique_name in names {
-                        push_grouped_column(&mut groups, &mut order, unique_name, &col.name);
+                        push_grouped_column(&mut groups, &mut order, unique_name.clone(), &col.name);
                     }
                 }
             }
@@ -140,17 +140,23 @@ fn collect_unique_groups(table: &TableDef) -> (ColumnGroups, ColumnGroupOrder) {
 fn push_grouped_column(
     groups: &mut ColumnGroups,
     order: &mut Vec,
-    group_name: &str,
+    group_name: String,
     column_name: &str,
 ) {
-    // Single map lookup: groups never store empty vectors (a group exists
-    // only after a push below), so a Vec just created by `or_default()` is
-    // exactly the "first sighting" that must register in `order`.
-    let cols = groups.entry(group_name.to_string()).or_default();
-    if cols.is_empty() {
-        order.push(group_name.to_string());
+    // Own the key exactly once: `or_default()` needs it as the map key, and a
+    // first sighting also needs it in `order`. `groups` never stores empty
+    // vectors (a group exists only after a push below), so a Vec just created
+    // by `or_default()` is exactly the first sighting that must register in
+    // `order`. To avoid a second `.to_string()`, register in `order` before
+    // moving the key into the map on that first sighting.
+    let first_sighting = !groups.contains_key(&group_name);
+    if first_sighting {
+        order.push(group_name.clone());
     }
-    cols.push(column_name.into());
+    groups
+        .entry(group_name)
+        .or_default()
+        .push(column_name.into());
 }
 
 fn generated_name_to_constraint_name(name: &str) -> Option {
@@ -318,13 +324,13 @@ fn collect_column_indexes(
 ) -> Result<(), TableValidationError> {
     match index_val {
         StrOrBoolOrArray::Str(name) => {
-            push_checked_index(groups, order, tracker, name, column_name)
+            push_checked_index(groups, order, tracker, name.clone(), column_name)
         }
         StrOrBoolOrArray::Bool(true) => push_checked_index(
             groups,
             order,
             tracker,
-            &format!("__auto_{column_name}"),
+            format!("__auto_{column_name}"),
             column_name,
         ),
         StrOrBoolOrArray::Bool(false) => Ok(()),
@@ -347,7 +353,7 @@ fn push_index_array(
             return Err(duplicate_index(index_name, column_name));
         }
         seen_in_array.insert(index_name.clone());
-        push_checked_index(groups, order, tracker, index_name, column_name)?;
+        push_checked_index(groups, order, tracker, index_name.clone(), column_name)?;
     }
     Ok(())
 }
@@ -356,18 +362,18 @@ fn push_checked_index(
     groups: &mut ColumnGroups,
     order: &mut Vec,
     tracker: &mut BTreeMap>,
-    index_name: &str,
+    index_name: String,
     column_name: &str,
 ) -> Result<(), TableValidationError> {
-    if let Some(columns) = tracker.get(index_name)
+    if let Some(columns) = tracker.get(index_name.as_str())
         && columns.contains(column_name)
     {
-        return Err(duplicate_index(index_name, column_name));
+        return Err(duplicate_index(&index_name, column_name));
     }
 
-    push_grouped_column(groups, order, index_name, column_name);
+    push_grouped_column(groups, order, index_name.clone(), column_name);
     tracker
-        .entry(index_name.to_string())
+        .entry(index_name)
         .or_default()
         .insert(column_name.to_string());
     Ok(())
diff --git a/crates/vespertide-exporter/src/seaorm/relations/naming.rs b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
index 4dc40b16..89c26019 100644
--- a/crates/vespertide-exporter/src/seaorm/relations/naming.rs
+++ b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
@@ -120,11 +120,20 @@ pub(super) fn fk_attr_value>(cols: &[T]) -> String {
     if cols.len() == 1 {
         cols[0].as_ref().to_string()
     } else {
-        let joined = cols
-            .iter()
-            .map(AsRef::as_ref)
-            .collect::>()
-            .join(", ");
-        format!("({joined})")
+        // Write straight into one pre-sized buffer instead of allocating a
+        // throwaway `Vec<&str>` just to `join(", ")` it (mirrors the
+        // `quote_idents` idiom in vespertide-query). Output is byte-identical:
+        // `(a, b, c)`.
+        let content_len: usize = cols.iter().map(|c| c.as_ref().len()).sum();
+        let mut out = String::with_capacity(content_len + 2 * cols.len());
+        out.push('(');
+        for (i, c) in cols.iter().enumerate() {
+            if i > 0 {
+                out.push_str(", ");
+            }
+            out.push_str(c.as_ref());
+        }
+        out.push(')');
+        out
     }
 }
diff --git a/crates/vespertide-query/src/sql/create_table.rs b/crates/vespertide-query/src/sql/create_table.rs
index 2bc3e838..5d4bd823 100644
--- a/crates/vespertide-query/src/sql/create_table.rs
+++ b/crates/vespertide-query/src/sql/create_table.rs
@@ -103,10 +103,9 @@ fn add_create_table_constraints(
             TableConstraint::ForeignKey { .. } => {
                 add_foreign_key_constraint(stmt, table, constraint);
             }
-            TableConstraint::Check { name, expr, .. } => {
-                let _ = (name, expr);
-            }
-            TableConstraint::Index { .. } => {}
+            // CHECK is emitted elsewhere; indexes are created separately. Both
+            // are no-ops during CREATE TABLE constraint assembly.
+            TableConstraint::Check { .. } | TableConstraint::Index { .. } => {}
             _ => {
                 unreachable!("TableConstraint is #[non_exhaustive]; all variants are matched above")
             }

From 44d2063f4d7c32d1c274108deecc3b340126b80a Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 10:50:49 +0900
Subject: [PATCH 066/109] =?UTF-8?q?retry-now#0065:=20batch=20=E2=80=94=20g?=
 =?UTF-8?q?ate=20create=5Ftable=20enum=20HashSet,=20pre-size=20queries=20V?=
 =?UTF-8?q?ec,=20skip=20seaorm=20enum=20loop=20when=20enum-free=20(3=20kep?=
 =?UTF-8?q?t)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../vespertide-exporter/src/seaorm/render.rs  | 21 +++++---
 .../vespertide-query/src/sql/create_table.rs  | 52 +++++++++++++------
 2 files changed, 49 insertions(+), 24 deletions(-)

diff --git a/crates/vespertide-exporter/src/seaorm/render.rs b/crates/vespertide-exporter/src/seaorm/render.rs
index 4adfd1f9..402f76c0 100644
--- a/crates/vespertide-exporter/src/seaorm/render.rs
+++ b/crates/vespertide-exporter/src/seaorm/render.rs
@@ -44,14 +44,19 @@ pub fn render_entity_with_config_and_paths(
     }
     lines.push(String::new());
 
-    // Generate Enum definitions first
-    let mut processed_enums = HashSet::new();
-    for column in &table.columns {
-        if let ColumnType::Complex(ComplexColumnType::Enum { name, values }) = &column.r#type {
-            // Avoid duplicate enum definitions if multiple columns use the same enum
-            if !processed_enums.contains(name) {
-                render_enum(&mut lines, &table.name, name, values, config);
-                processed_enums.insert(name.clone());
+    // Generate Enum definitions first. Reuse the already-computed `has_enums`
+    // flag: enum-free tables (the common case) skip the `HashSet` allocation
+    // and the second column walk entirely. Output is byte-identical — with no
+    // enum columns the loop body never fires and no lines are pushed.
+    if has_enums {
+        let mut processed_enums = HashSet::new();
+        for column in &table.columns {
+            if let ColumnType::Complex(ComplexColumnType::Enum { name, values }) = &column.r#type {
+                // Avoid duplicate enum definitions if multiple columns use the same enum
+                if !processed_enums.contains(name) {
+                    render_enum(&mut lines, &table.name, name, values, config);
+                    processed_enums.insert(name.clone());
+                }
             }
         }
     }
diff --git a/crates/vespertide-query/src/sql/create_table.rs b/crates/vespertide-query/src/sql/create_table.rs
index 5d4bd823..a6cd81c6 100644
--- a/crates/vespertide-query/src/sql/create_table.rs
+++ b/crates/vespertide-query/src/sql/create_table.rs
@@ -34,10 +34,15 @@ fn add_create_table_columns(
     // The vast majority of tables have no `PrimaryKey { auto_increment: true }`
     // constraint, so skip the `HashSet` allocation + constraint scan entirely in
     // that common case; an empty set produces byte-identical output.
-    let auto_increment_columns = if constraints
-        .iter()
-        .any(|c| matches!(c, TableConstraint::PrimaryKey { auto_increment: true, .. }))
-    {
+    let auto_increment_columns = if constraints.iter().any(|c| {
+        matches!(
+            c,
+            TableConstraint::PrimaryKey {
+                auto_increment: true,
+                ..
+            }
+        )
+    }) {
         collect_auto_increment_columns(constraints)
     } else {
         std::collections::HashSet::new()
@@ -140,9 +145,9 @@ fn should_skip_sqlite_auto_increment_pk(
     matches!(backend, DatabaseBackend::Sqlite)
         && auto_increment
         && pk_cols.iter().all(|col_name| {
-            columns.iter().any(|c| {
-                c.name == col_name.as_ref() && c.r#type.supports_auto_increment()
-            })
+            columns
+                .iter()
+                .any(|c| c.name == col_name.as_ref() && c.r#type.supports_auto_increment())
         })
 }
 
@@ -222,17 +227,32 @@ pub fn build_create_table(
     let columns = &normalized.columns;
     let constraints = &normalized.constraints;
 
-    let mut queries = Vec::new();
+    // Pre-size for the CREATE TABLE statement plus up to one CREATE INDEX per
+    // constraint (uniques + indexes); enum-type `Raw`s are rare and bounded by
+    // the enum-column count, so a small over/under-estimate costs at most one
+    // realloc, never correctness.
+    let mut queries = Vec::with_capacity(1 + constraints.len());
 
     // Create enum types first (PostgreSQL only)
-    // Collect unique enum types to avoid duplicates
-    let mut created_enums: std::collections::HashSet<&str> = std::collections::HashSet::new();
-    for column in columns {
-        if let ColumnType::Complex(ComplexColumnType::Enum { name, .. }) = &column.r#type
-            && created_enums.insert(name.as_str())
-            && let Some(create_type_sql) = build_create_enum_type_sql(table, &column.r#type)
-        {
-            queries.push(BuiltQuery::Raw(create_type_sql));
+    // Collect unique enum types to avoid duplicates.
+    // The vast majority of tables have no enum columns, so skip the `HashSet`
+    // allocation + column scan entirely in that common case; the loop pushes
+    // nothing when there are no enum columns, so the output is byte-identical.
+    let has_enum_columns = columns.iter().any(|c| {
+        matches!(
+            c.r#type,
+            ColumnType::Complex(ComplexColumnType::Enum { .. })
+        )
+    });
+    if has_enum_columns {
+        let mut created_enums: std::collections::HashSet<&str> = std::collections::HashSet::new();
+        for column in columns {
+            if let ColumnType::Complex(ComplexColumnType::Enum { name, .. }) = &column.r#type
+                && created_enums.insert(name.as_str())
+                && let Some(create_type_sql) = build_create_enum_type_sql(table, &column.r#type)
+            {
+                queries.push(BuiltQuery::Raw(create_type_sql));
+            }
         }
     }
 

From dd9e1de907be1830a409956889d042a4a724ddfb Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 11:01:49 +0900
Subject: [PATCH 067/109] =?UTF-8?q?retry-now#0066:=20batch=20=E2=80=94=20h?=
 =?UTF-8?q?oist=20pascal=20invariant,=20pre-size=204=20string=20builders,?=
 =?UTF-8?q?=20dedupe=20single-col=20scanners,=20simplify=20pluralize=20(5?=
 =?UTF-8?q?=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/constraint_scan.rs                    | 48 +++++++++++--------
 .../vespertide-exporter/src/seaorm/imports.rs | 11 +++--
 .../src/seaorm/relations/naming.rs            | 35 +++++++++-----
 crates/vespertide-naming/src/lib.rs           | 10 +++-
 4 files changed, 68 insertions(+), 36 deletions(-)

diff --git a/crates/vespertide-exporter/src/constraint_scan.rs b/crates/vespertide-exporter/src/constraint_scan.rs
index a0c30a88..82b31d1c 100644
--- a/crates/vespertide-exporter/src/constraint_scan.rs
+++ b/crates/vespertide-exporter/src/constraint_scan.rs
@@ -8,7 +8,27 @@
 
 use std::collections::HashSet;
 
-use vespertide_core::TableConstraint;
+use vespertide_core::{ColumnName, TableConstraint};
+
+/// Collect the column names from every single-column constraint that `extract`
+/// matches. Shared body for [`single_column_uniques`] and
+/// [`single_column_indexes`], which differ only in the matched
+/// `TableConstraint` variant. `extract` returns the constraint's column slice
+/// for the variant it cares about, or `None` for every other variant.
+fn single_column_scan(
+    constraints: &[TableConstraint],
+    extract: impl Fn(&TableConstraint) -> Option<&[ColumnName]>,
+) -> HashSet {
+    let mut cols = HashSet::new();
+    for constraint in constraints {
+        if let Some(columns) = extract(constraint)
+            && columns.len() == 1
+        {
+            cols.insert(columns[0].to_string());
+        }
+    }
+    cols
+}
 
 /// Collect the column names covered by table-level `PrimaryKey` constraints.
 ///
@@ -29,28 +49,18 @@ pub(crate) fn primary_key_columns(constraints: &[TableConstraint]) -> HashSet HashSet {
-    let mut unique_cols = HashSet::new();
-    for constraint in constraints {
-        if let TableConstraint::Unique { columns, .. } = constraint
-            && columns.len() == 1
-        {
-            unique_cols.insert(columns[0].to_string());
-        }
-    }
-    unique_cols
+    single_column_scan(constraints, |c| match c {
+        TableConstraint::Unique { columns, .. } => Some(columns.as_slice()),
+        _ => None,
+    })
 }
 
 /// Collect the column names that carry a single-column `Index` constraint.
 ///
 /// Lookup-only, ordering unused.
 pub(crate) fn single_column_indexes(constraints: &[TableConstraint]) -> HashSet {
-    let mut indexed_cols = HashSet::new();
-    for constraint in constraints {
-        if let TableConstraint::Index { columns, .. } = constraint
-            && columns.len() == 1
-        {
-            indexed_cols.insert(columns[0].to_string());
-        }
-    }
-    indexed_cols
+    single_column_scan(constraints, |c| match c {
+        TableConstraint::Index { columns, .. } => Some(columns.as_slice()),
+        _ => None,
+    })
 }
diff --git a/crates/vespertide-exporter/src/seaorm/imports.rs b/crates/vespertide-exporter/src/seaorm/imports.rs
index 21baa9f7..c4e0689d 100644
--- a/crates/vespertide-exporter/src/seaorm/imports.rs
+++ b/crates/vespertide-exporter/src/seaorm/imports.rs
@@ -65,7 +65,9 @@ pub(super) const RUST_KEYWORDS: &[&str] = &[
 ];
 
 pub(super) fn sanitize_field_name(name: &str) -> String {
-    let mut result = String::new();
+    // Output is bounded by `name.len()` (+1 only in the leading-digit case);
+    // pre-size to collapse the small-string growth ladder into one allocation.
+    let mut result = String::with_capacity(name.len() + 1);
 
     for (idx, ch) in name.chars().enumerate() {
         if (ch.is_ascii_alphanumeric() && (idx > 0 || ch.is_ascii_alphabetic())) || ch == '_' {
@@ -98,7 +100,8 @@ pub(super) fn unique_name(base: &str, used: &mut HashSet) -> String {
     name
 }
 pub(super) fn to_pascal_case(s: &str) -> String {
-    let mut result = String::new();
+    // Separators are dropped, so the output never exceeds `s.len()`.
+    let mut result = String::with_capacity(s.len());
     let mut capitalize = true;
     for c in s.chars() {
         let is_separator = c == '_' || c == '-';
@@ -120,7 +123,9 @@ pub(super) fn to_pascal_case(s: &str) -> String {
 /// Convert `PascalCase` to `snake_case`.
 /// For "`CreatorUser`", generates "`creator_user`".
 pub(super) fn to_snake_case(s: &str) -> String {
-    let mut result = String::new();
+    // Each uppercase run inserts one `_`; a small slack over `s.len()` avoids a
+    // realloc for typical PascalCase input without over-sizing.
+    let mut result = String::with_capacity(s.len() + 4);
     for (i, c) in s.chars().enumerate() {
         if i > 0 && c.is_ascii_uppercase() {
             result.push('_');
diff --git a/crates/vespertide-exporter/src/seaorm/relations/naming.rs b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
index 89c26019..557869db 100644
--- a/crates/vespertide-exporter/src/seaorm/relations/naming.rs
+++ b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
@@ -29,19 +29,18 @@ pub(in crate::seaorm) fn unique_relation_enum_name(
         return preferred;
     }
 
-    let source_prefixed = format!("{}{}", to_pascal_case(source_table), base_relation_enum);
+    // `to_pascal_case(source_table)` is a loop-invariant: compute it once and
+    // reuse it for the source-prefixed candidate and every indexed candidate,
+    // instead of re-allocating the identical pascal string per attempt.
+    let source_pascal = to_pascal_case(source_table);
+    let source_prefixed = format!("{source_pascal}{base_relation_enum}");
     if !used_relation_enums.contains(&source_prefixed) {
         return source_prefixed;
     }
 
     let mut index = 2;
     loop {
-        let candidate = format!(
-            "{}{}{}",
-            to_pascal_case(source_table),
-            base_relation_enum,
-            index
-        );
+        let candidate = format!("{source_pascal}{base_relation_enum}{index}");
         if !used_relation_enums.contains(&candidate) {
             return candidate;
         }
@@ -104,18 +103,28 @@ pub(in crate::seaorm) fn infer_field_name_from_fk_column(
 pub(in crate::seaorm) fn pluralize(name: &str) -> String {
     if name.ends_with('s') {
         name.to_string()
-    } else if name.ends_with('y')
-        && !name.ends_with("ay")
-        && !name.ends_with("ey")
-        && !name.ends_with("oy")
-        && !name.ends_with("uy")
-    {
+    } else if name.ends_with('y') && !ends_with_vowel_y(name) {
         format!("{}ies", name.strip_suffix('y').unwrap_or(name))
     } else {
         format!("{name}s")
     }
 }
 
+/// True when `name` ends in a vowel immediately followed by `y` (e.g. `day`,
+/// `key`, `boy`, `guy`). Answers the `y`→`ies` guard with a single lookup of
+/// the character before the trailing `y` instead of four separate 2-byte
+/// suffix scans. The vowel set is exactly `{a, e, o, u}` (not `i`), so `iy`
+/// still pluralizes to `ies`; a bare `"y"` (no preceding char) is treated as a
+/// consonant-`y` and pluralizes to `ys`. Byte-identical to the prior chain.
+fn ends_with_vowel_y(name: &str) -> bool {
+    name.ends_with('y')
+        && name
+            .chars()
+            .rev()
+            .nth(1)
+            .is_some_and(|c| matches!(c, 'a' | 'e' | 'o' | 'u'))
+}
+
 pub(super) fn fk_attr_value>(cols: &[T]) -> String {
     if cols.len() == 1 {
         cols[0].as_ref().to_string()
diff --git a/crates/vespertide-naming/src/lib.rs b/crates/vespertide-naming/src/lib.rs
index fae44e7d..5b1f0118 100644
--- a/crates/vespertide-naming/src/lib.rs
+++ b/crates/vespertide-naming/src/lib.rs
@@ -95,7 +95,15 @@ fn write_sorted_columns>(out: &mut String, columns: &[T]) {
 /// Uses double underscore to separate table name from the rest.
 /// Format: chk_{table}__{column}
 pub fn build_check_constraint_name(table: &str, column: &str) -> String {
-    format!("chk_{table}__{column}")
+    // Build in one exact-size allocation like the sibling constraint-name
+    // builders above, instead of routing through `format!`. Output is
+    // byte-identical: `chk_{table}__{column}` (`"chk_"` = 4 bytes, `"__"` = 2).
+    let mut out = String::with_capacity(4 + table.len() + 2 + column.len());
+    out.push_str("chk_");
+    out.push_str(table);
+    out.push_str("__");
+    out.push_str(column);
+    out
 }
 
 /// Generate enum type name with table prefix to avoid conflicts.

From 5ad879b8122ee0cb87d854f74a3004eb09611ded Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 11:16:32 +0900
Subject: [PATCH 068/109] retry-now#0067: batch - skip no-unique constraint
 clone, pre-size enum type name, alloc-free FK natural-key compare (3 kept)

---
 .../src/seaorm/relations/naming.rs                    |  9 ++++++++-
 crates/vespertide-naming/src/lib.rs                   |  9 ++++++++-
 crates/vespertide-query/src/sql/create_table.rs       | 11 +++++++++--
 3 files changed, 25 insertions(+), 4 deletions(-)

diff --git a/crates/vespertide-exporter/src/seaorm/relations/naming.rs b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
index 557869db..f818eded 100644
--- a/crates/vespertide-exporter/src/seaorm/relations/naming.rs
+++ b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
@@ -85,7 +85,14 @@ pub(in crate::seaorm) fn infer_field_name_from_fk_column(
     // If the FK column exactly matches the referenced column name, treat it as a natural-key
     // relation and expose the target entity name instead of the raw column name.
     // Also handle compact forms like `username` for `user.name`.
-    if sanitized_lower == to_lower || sanitized_lower == format!("{table_lower}{to_lower}") {
+    // The second disjunct checks whether `sanitized_lower` equals `table_lower`
+    // concatenated with `to_lower` (e.g. "username" for table "user", col "name").
+    // Compare allocation-free via a prefix strip instead of building a joined
+    // `String` on every call: it is exactly equal iff `sanitized_lower` starts
+    // with `table_lower` and the remainder is `to_lower`.
+    if sanitized_lower == to_lower
+        || sanitized_lower.strip_prefix(table_lower.as_str()) == Some(to_lower.as_str())
+    {
         return sanitize_field_name(table_name);
     }
 
diff --git a/crates/vespertide-naming/src/lib.rs b/crates/vespertide-naming/src/lib.rs
index 5b1f0118..4fe6cb17 100644
--- a/crates/vespertide-naming/src/lib.rs
+++ b/crates/vespertide-naming/src/lib.rs
@@ -113,7 +113,14 @@ pub fn build_check_constraint_name(table: &str, column: &str) -> String {
 /// This prevents conflicts when multiple tables use the same enum name
 /// (e.g., "status" or "gender") with potentially different values.
 pub fn build_enum_type_name(table: &str, enum_name: &str) -> String {
-    format!("{table}_{enum_name}")
+    // Build in one exact-size allocation like the sibling constraint-name
+    // builders above, instead of routing through `format!`. Output is
+    // byte-identical: `{table}_{enum_name}` (`'_'` = 1 byte).
+    let mut out = String::with_capacity(table.len() + 1 + enum_name.len());
+    out.push_str(table);
+    out.push('_');
+    out.push_str(enum_name);
+    out
 }
 
 #[cfg(test)]
diff --git a/crates/vespertide-query/src/sql/create_table.rs b/crates/vespertide-query/src/sql/create_table.rs
index a6cd81c6..22ba0940 100644
--- a/crates/vespertide-query/src/sql/create_table.rs
+++ b/crates/vespertide-query/src/sql/create_table.rs
@@ -262,10 +262,17 @@ pub fn build_create_table(
     // separate CREATE UNIQUE INDEX statements, so partition off the uniques only
     // in that branch (the MySQL path discards both halves, so allocating them
     // there is pure waste).
-    let create_table_stmt = if matches!(backend, DatabaseBackend::MySql) {
+    let has_unique_constraints = constraints
+        .iter()
+        .any(|c| matches!(c, TableConstraint::Unique { .. }));
+    let create_table_stmt = if matches!(backend, DatabaseBackend::MySql) || !has_unique_constraints {
+        // MySQL keeps uniques inline; the PG/SQLite path emits them as separate
+        // CREATE UNIQUE INDEX statements below, so when there are no uniques to
+        // strip the constraint slice can pass straight through with no clone.
         build_create_table_for_backend(backend, table, columns, constraints)
     } else {
-        // Convert references to owned values for build_create_table_for_backend
+        // PG/SQLite with at least one unique: partition off the uniques into an
+        // owned Vec so they are excluded from the inline CREATE TABLE.
         let table_constraints_owned: Vec = constraints
             .iter()
             .filter(|c| !matches!(c, TableConstraint::Unique { .. }))

From 8f48dd9fa01b6d92650db5b3a4ff08a79623c577 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 11:28:49 +0900
Subject: [PATCH 069/109] =?UTF-8?q?retry-now#0068:=20batch=20=E2=80=94=20C?=
 =?UTF-8?q?ow=20borrowed=20fast-path=20in=203=20ERD=20escape=20helpers=20(?=
 =?UTF-8?q?3=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 crates/vespertide-cli/src/commands/erd/dot.rs       | 12 ++++++++----
 crates/vespertide-cli/src/commands/erd/mermaid.rs   |  7 +++++--
 .../vespertide-cli/src/commands/erd/svg/render.rs   | 13 ++++++++-----
 crates/vespertide-cli/src/commands/erd/svg/util.rs  |  7 +++++--
 4 files changed, 26 insertions(+), 13 deletions(-)

diff --git a/crates/vespertide-cli/src/commands/erd/dot.rs b/crates/vespertide-cli/src/commands/erd/dot.rs
index a4982829..c5c4e48b 100644
--- a/crates/vespertide-cli/src/commands/erd/dot.rs
+++ b/crates/vespertide-cli/src/commands/erd/dot.rs
@@ -43,8 +43,8 @@ pub fn render_dot(tables: &[TableDef]) -> String {
 }
 
 fn record_label(table: &TableDef) -> String {
-    let mut fields = Vec::with_capacity(table.columns.len() + 1);
-    fields.push(escape_record_field(&table.name));
+    let mut fields: Vec = Vec::with_capacity(table.columns.len() + 1);
+    fields.push(escape_record_field(&table.name).into_owned());
 
     for column in &table.columns {
         fields.push(column_record_field(table, column));
@@ -71,7 +71,11 @@ fn relationship_label(relation: &ForeignKeyRelation) -> String {
     )
 }
 
-fn escape_record_field(value: &str) -> String {
+fn escape_record_field(value: &str) -> std::borrow::Cow<'_, str> {
+    if !value.contains(['\\', '{', '}', '|', '<', '>', '"']) {
+        return std::borrow::Cow::Borrowed(value);
+    }
+
     let mut escaped = String::with_capacity(value.len());
 
     for ch in value.chars() {
@@ -85,7 +89,7 @@ fn escape_record_field(value: &str) -> String {
         }
     }
 
-    escaped
+    std::borrow::Cow::Owned(escaped)
 }
 
 #[cfg(test)]
diff --git a/crates/vespertide-cli/src/commands/erd/mermaid.rs b/crates/vespertide-cli/src/commands/erd/mermaid.rs
index 843ac7d3..9472f2e4 100644
--- a/crates/vespertide-cli/src/commands/erd/mermaid.rs
+++ b/crates/vespertide-cli/src/commands/erd/mermaid.rs
@@ -98,8 +98,11 @@ fn complex_column_type_to_mermaid(column_type: &ComplexColumnType) -> &'static s
     }
 }
 
-fn escape_mermaid_label(label: &str) -> String {
-    label.replace('\\', "\\\\").replace('"', "\\\"")
+fn escape_mermaid_label(label: &str) -> std::borrow::Cow<'_, str> {
+    if !label.contains(['\\', '"']) {
+        return std::borrow::Cow::Borrowed(label);
+    }
+    std::borrow::Cow::Owned(label.replace('\\', "\\\\").replace('"', "\\\""))
 }
 
 #[cfg(test)]
diff --git a/crates/vespertide-cli/src/commands/erd/svg/render.rs b/crates/vespertide-cli/src/commands/erd/svg/render.rs
index bd3446e3..115840f0 100644
--- a/crates/vespertide-cli/src/commands/erd/svg/render.rs
+++ b/crates/vespertide-cli/src/commands/erd/svg/render.rs
@@ -267,11 +267,14 @@ fn render_row(out: &mut String, bx: &TableBox, idx: usize, row: &RowSpec) {
         name = escape_xml(&row.name),
     );
 
-    // Type, right-aligned in monospace.
-    let type_display = if row.nullable {
-        format!("{}?", row.type_str)
+    // Type, right-aligned in monospace. The nullable branch needs an owned
+    // `String` (it appends `?`, then escapes it); the non-nullable branch
+    // escapes the borrowed `type_str` directly via `Cow`, so no clone is paid
+    // on the common path.
+    let type_display: std::borrow::Cow<'_, str> = if row.nullable {
+        std::borrow::Cow::Owned(escape_xml(&format!("{}?", row.type_str)).into_owned())
     } else {
-        row.type_str.clone()
+        escape_xml(&row.type_str)
     };
     let _ = writeln!(
         out,
@@ -282,7 +285,7 @@ fn render_row(out: &mut String, bx: &TableBox, idx: usize, row: &RowSpec) {
         fg = ROW_FG_MUTED,
         fs = TYPE_FS,
         ff = MONO_FAMILY,
-        t = escape_xml(&type_display),
+        t = type_display,
     );
 }
 
diff --git a/crates/vespertide-cli/src/commands/erd/svg/util.rs b/crates/vespertide-cli/src/commands/erd/svg/util.rs
index 78cc92a6..11176a3a 100644
--- a/crates/vespertide-cli/src/commands/erd/svg/util.rs
+++ b/crates/vespertide-cli/src/commands/erd/svg/util.rs
@@ -20,7 +20,10 @@ pub(super) fn render_empty() -> String {
     )
 }
 
-pub(super) fn escape_xml(input: &str) -> String {
+pub(super) fn escape_xml(input: &str) -> std::borrow::Cow<'_, str> {
+    if !input.contains(['&', '<', '>', '"', '\'']) {
+        return std::borrow::Cow::Borrowed(input);
+    }
     let mut out = String::with_capacity(input.len());
     for ch in input.chars() {
         match ch {
@@ -32,7 +35,7 @@ pub(super) fn escape_xml(input: &str) -> String {
             _ => out.push(ch),
         }
     }
-    out
+    std::borrow::Cow::Owned(out)
 }
 
 #[cfg(test)]

From 25202abb0fa74faebff39e2f8c81cf1ab29659e7 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 11:40:11 +0900
Subject: [PATCH 070/109] retry-now#0069: batch - alloc-free FK-ref split_once,
 borrow M2M junction FKs (2 kept)

---
 crates/vespertide-core/src/schema/table/normalize.rs | 12 ++++++------
 .../src/seaorm/relations/reverse.rs                  | 12 ++++++------
 2 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/crates/vespertide-core/src/schema/table/normalize.rs b/crates/vespertide-core/src/schema/table/normalize.rs
index 48101cf1..a0a3bc25 100644
--- a/crates/vespertide-core/src/schema/table/normalize.rs
+++ b/crates/vespertide-core/src/schema/table/normalize.rs
@@ -246,15 +246,15 @@ fn parse_foreign_key_reference(
     column_name: &str,
     reference: &str,
 ) -> Result<(TableName, Vec), TableValidationError> {
-    let parts: Vec<&str> = reference.split('.').collect();
-    if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
-        return Err(TableValidationError::InvalidForeignKeyFormat {
+    let (head, tail) = reference
+        .split_once('.')
+        .filter(|(head, tail)| !head.is_empty() && !tail.is_empty() && !tail.contains('.'))
+        .ok_or_else(|| TableValidationError::InvalidForeignKeyFormat {
             column_name: column_name.to_string(),
             value: reference.to_string(),
-        });
-    }
+        })?;
 
-    Ok((parts[0].into(), vec![parts[1].into()]))
+    Ok((head.into(), vec![tail.into()]))
 }
 
 fn foreign_key_constraint_exists(constraints: &[TableConstraint], column_name: &str) -> bool {
diff --git a/crates/vespertide-exporter/src/seaorm/relations/reverse.rs b/crates/vespertide-exporter/src/seaorm/relations/reverse.rs
index 7548a46f..310c9344 100644
--- a/crates/vespertide-exporter/src/seaorm/relations/reverse.rs
+++ b/crates/vespertide-exporter/src/seaorm/relations/reverse.rs
@@ -6,7 +6,7 @@
 
 use std::collections::{BTreeMap, HashMap, HashSet};
 
-use vespertide_core::{TableConstraint, TableDef};
+use vespertide_core::{ColumnName, TableConstraint, TableDef, TableName};
 
 use super::super::imports::{
     resolve_relation_entity_module_path, sanitize_field_name, to_pascal_case, to_snake_case,
@@ -85,7 +85,7 @@ fn collect_many_to_many_targets(
         return None;
     }
 
-    let fks: Vec<_> = junction_table
+    let fks: Vec<(&[ColumnName], &TableName)> = junction_table
         .constraints
         .iter()
         .filter_map(|c| {
@@ -93,7 +93,7 @@ fn collect_many_to_many_targets(
                 columns, ref_table, ..
             } = c
             {
-                Some((columns.clone(), ref_table.clone()))
+                Some((columns.as_slice(), ref_table))
             } else {
                 None
             }
@@ -113,7 +113,7 @@ fn collect_many_to_many_targets(
     }
 
     fks.iter()
-        .find(|(_, ref_table)| ref_table == ¤t_table.name)?;
+        .find(|(_, ref_table)| **ref_table == current_table.name)?;
 
     let mut targets = Vec::new();
 
@@ -122,10 +122,10 @@ fn collect_many_to_many_targets(
 
     // Target tables via M2M
     for (_, ref_table) in &fks {
-        if ref_table == ¤t_table.name {
+        if **ref_table == current_table.name {
             continue;
         }
-        let target_exists = schema.iter().any(|t| &t.name == ref_table);
+        let target_exists = schema.iter().any(|t| &t.name == *ref_table);
         if target_exists {
             targets.push(ref_table.to_string());
         }

From c1113f51927d451177de22df56bbec5e602006da Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 11:53:45 +0900
Subject: [PATCH 071/109] =?UTF-8?q?retry-now#0070:=20batch=20=E2=80=94=20d?=
 =?UTF-8?q?edupe=20reverse-relation=20enum=20recompute,=20hoist=20junction?=
 =?UTF-8?q?=20pascal,=20borrow=20display=5Fstr=20(3=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 crates/vespertide-core/src/schema/column.rs   | 52 +++++++++++--------
 .../src/seaorm/relations/reverse.rs           | 13 ++---
 2 files changed, 36 insertions(+), 29 deletions(-)

diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs
index bbcdd2c7..5a4e774c 100644
--- a/crates/vespertide-core/src/schema/column.rs
+++ b/crates/vespertide-core/src/schema/column.rs
@@ -386,31 +386,41 @@ impl SimpleColumnType {
         )
     }
 
-    /// Convert to human-readable display string
-    pub fn to_display_string(&self) -> String {
+    /// Borrow the human-readable display label as a `&'static str`.
+    ///
+    /// Prefer this over [`Self::to_display_string`] when the caller only needs
+    /// to read the label — it avoids an allocation for a compile-time constant.
+    #[must_use]
+    pub fn display_str(&self) -> &'static str {
         match self {
-            SimpleColumnType::SmallInt => "smallint".to_string(),
-            SimpleColumnType::Integer => "integer".to_string(),
-            SimpleColumnType::BigInt => "bigint".to_string(),
-            SimpleColumnType::Real => "real".to_string(),
-            SimpleColumnType::DoublePrecision => "double precision".to_string(),
-            SimpleColumnType::Text => "text".to_string(),
-            SimpleColumnType::Boolean => "boolean".to_string(),
-            SimpleColumnType::Date => "date".to_string(),
-            SimpleColumnType::Time => "time".to_string(),
-            SimpleColumnType::Timestamp => "timestamp".to_string(),
-            SimpleColumnType::Timestamptz => "timestamptz".to_string(),
-            SimpleColumnType::Interval => "interval".to_string(),
-            SimpleColumnType::Bytea => "bytea".to_string(),
-            SimpleColumnType::Uuid => "uuid".to_string(),
-            SimpleColumnType::Json => "json".to_string(),
-            SimpleColumnType::Inet => "inet".to_string(),
-            SimpleColumnType::Cidr => "cidr".to_string(),
-            SimpleColumnType::Macaddr => "macaddr".to_string(),
-            SimpleColumnType::Xml => "xml".to_string(),
+            SimpleColumnType::SmallInt => "smallint",
+            SimpleColumnType::Integer => "integer",
+            SimpleColumnType::BigInt => "bigint",
+            SimpleColumnType::Real => "real",
+            SimpleColumnType::DoublePrecision => "double precision",
+            SimpleColumnType::Text => "text",
+            SimpleColumnType::Boolean => "boolean",
+            SimpleColumnType::Date => "date",
+            SimpleColumnType::Time => "time",
+            SimpleColumnType::Timestamp => "timestamp",
+            SimpleColumnType::Timestamptz => "timestamptz",
+            SimpleColumnType::Interval => "interval",
+            SimpleColumnType::Bytea => "bytea",
+            SimpleColumnType::Uuid => "uuid",
+            SimpleColumnType::Json => "json",
+            SimpleColumnType::Inet => "inet",
+            SimpleColumnType::Cidr => "cidr",
+            SimpleColumnType::Macaddr => "macaddr",
+            SimpleColumnType::Xml => "xml",
         }
     }
 
+    /// Convert to human-readable display string
+    #[must_use]
+    pub fn to_display_string(&self) -> String {
+        self.display_str().to_string()
+    }
+
     /// Get the default fill value for this type
     /// Returns None if no sensible default exists
     pub fn default_fill_value(&self) -> &'static str {
diff --git a/crates/vespertide-exporter/src/seaorm/relations/reverse.rs b/crates/vespertide-exporter/src/seaorm/relations/reverse.rs
index 310c9344..979b01a0 100644
--- a/crates/vespertide-exporter/src/seaorm/relations/reverse.rs
+++ b/crates/vespertide-exporter/src/seaorm/relations/reverse.rs
@@ -259,11 +259,11 @@ fn reverse_relation_field_defs_inner(ctx: ReverseRelationFieldCtx<'_>) -> Vec
Date: Fri, 3 Jul 2026 12:13:18 +0900
Subject: [PATCH 072/109] =?UTF-8?q?retry-now#0071:=20exporter/core=20alloc?=
 =?UTF-8?q?+dedupe=20batch=20(5=20kept)=20=E2=80=94=20join=5Fqualified=5Fr?=
 =?UTF-8?q?efs=20helper,=201-alloc=20to=5Fcamel=5Fcase,=20EnumValues::vari?=
 =?UTF-8?q?ant=5Fnames=5Fjoined,=20inline=20JPA=20@JoinColumn=20attrs,=20p?=
 =?UTF-8?q?ush=5Fattr=20buffer=20builder?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 crates/vespertide-core/src/schema/column.rs   | 91 ++++++++++++++-----
 crates/vespertide-exporter/src/jpa/mod.rs     |  1 +
 crates/vespertide-exporter/src/jpa/render.rs  | 60 +++++++-----
 .../src/sqlalchemy/render.rs                  | 46 +++++-----
 .../src/sqlmodel/render.rs                    |  9 +-
 .../vespertide-exporter/src/utils/common.rs   | 76 ++++++++++++++++
 .../vespertide-planner/src/validate/enums.rs  |  2 +-
 7 files changed, 210 insertions(+), 75 deletions(-)

diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs
index 5a4e774c..18b86871 100644
--- a/crates/vespertide-core/src/schema/column.rs
+++ b/crates/vespertide-core/src/schema/column.rs
@@ -101,41 +101,41 @@ impl ColumnType {
 
     /// Convert column type to Rust type string (for `SeaORM` entity generation)
     pub fn to_rust_type(&self, nullable: bool) -> String {
-        let base = match self {
+        let base: &'static str = match self {
             ColumnType::Simple(ty) => match ty {
-                SimpleColumnType::SmallInt => "i16".to_string(),
-                SimpleColumnType::Integer => "i32".to_string(),
-                SimpleColumnType::BigInt => "i64".to_string(),
-                SimpleColumnType::Real => "f32".to_string(),
-                SimpleColumnType::DoublePrecision => "f64".to_string(),
+                SimpleColumnType::SmallInt => "i16",
+                SimpleColumnType::Integer => "i32",
+                SimpleColumnType::BigInt => "i64",
+                SimpleColumnType::Real => "f32",
+                SimpleColumnType::DoublePrecision => "f64",
                 SimpleColumnType::Text
                 | SimpleColumnType::Interval
                 | SimpleColumnType::Inet
                 | SimpleColumnType::Cidr
                 | SimpleColumnType::Macaddr
-                | SimpleColumnType::Xml => "String".to_string(),
-                SimpleColumnType::Boolean => "bool".to_string(),
-                SimpleColumnType::Date => "Date".to_string(),
-                SimpleColumnType::Time => "Time".to_string(),
-                SimpleColumnType::Timestamp => "DateTime".to_string(),
-                SimpleColumnType::Timestamptz => "DateTimeWithTimeZone".to_string(),
-                SimpleColumnType::Bytea => "Vec".to_string(),
-                SimpleColumnType::Uuid => "Uuid".to_string(),
-                SimpleColumnType::Json => "Json".to_string(),
+                | SimpleColumnType::Xml => "String",
+                SimpleColumnType::Boolean => "bool",
+                SimpleColumnType::Date => "Date",
+                SimpleColumnType::Time => "Time",
+                SimpleColumnType::Timestamp => "DateTime",
+                SimpleColumnType::Timestamptz => "DateTimeWithTimeZone",
+                SimpleColumnType::Bytea => "Vec",
+                SimpleColumnType::Uuid => "Uuid",
+                SimpleColumnType::Json => "Json",
             },
             ColumnType::Complex(ty) => match ty {
-                ComplexColumnType::Numeric { .. } => "Decimal".to_string(),
+                ComplexColumnType::Numeric { .. } => "Decimal",
                 ComplexColumnType::Varchar { .. }
                 | ComplexColumnType::Char { .. }
                 | ComplexColumnType::Custom { .. }
-                | ComplexColumnType::Enum { .. } => "String".to_string(),
+                | ComplexColumnType::Enum { .. } => "String",
             },
         };
 
         if nullable {
             format!("Option<{base}>")
         } else {
-            base
+            base.to_string()
         }
     }
 
@@ -497,6 +497,53 @@ impl EnumValues {
         }
     }
 
+    /// Join every variant *name* with `separator`, writing straight into one
+    /// buffer — no intermediate `Vec<&str>` allocation.
+    ///
+    /// Unlike [`Self::sql_values_joined`] (which emits SQL literals: quoted
+    /// strings / integer values), this emits the human-readable variant
+    /// *names* for diagnostics such as "allowed values are: a, b, c". For an
+    /// integer enum the member names are used, matching `variant_names()`.
+    ///
+    /// ```rust
+    /// use vespertide_core::{EnumValues, NumValue};
+    ///
+    /// let s = EnumValues::String(vec!["active".into(), "inactive".into()]);
+    /// assert_eq!(s.variant_names_joined(", "), "active, inactive");
+    ///
+    /// let i = EnumValues::Integer(vec![
+    ///     NumValue { name: "low".into(),  value: 0  },
+    ///     NumValue { name: "high".into(), value: 10 },
+    /// ]);
+    /// assert_eq!(i.variant_names_joined(", "), "low, high");
+    ///
+    /// let empty = EnumValues::String(vec![]);
+    /// assert_eq!(empty.variant_names_joined(", "), "");
+    /// ```
+    #[must_use]
+    pub fn variant_names_joined(&self, separator: &str) -> String {
+        let mut out = String::new();
+        match self {
+            EnumValues::String(values) => {
+                for (i, s) in values.iter().enumerate() {
+                    if i > 0 {
+                        out.push_str(separator);
+                    }
+                    out.push_str(s);
+                }
+            }
+            EnumValues::Integer(values) => {
+                for (i, v) in values.iter().enumerate() {
+                    if i > 0 {
+                        out.push_str(separator);
+                    }
+                    out.push_str(&v.name);
+                }
+            }
+        }
+        out
+    }
+
     /// Get the number of variants
     pub fn len(&self) -> usize {
         match self {
@@ -516,8 +563,8 @@ impl EnumValues {
     /// Returns `true` when `value` matches any variant of this enum.
     ///
     /// For string enums the comparison is exact-string against each variant.
-    /// For integer enums the value is first parsed as `i32` (matching the
-    /// underlying DB representation): successful parses match against
+    /// For integer enums the value is first parsed as `i64` (matching the
+    /// `NumValue::value` storage type): successful parses match against
     /// `NumValue::value`, failed parses fall back to matching against
     /// `NumValue::name`. Mirrors the loose JSON-default behaviour expected by
     /// the planner validator so a model author can write either the numeric
@@ -527,9 +574,9 @@ impl EnumValues {
     pub fn contains_value(&self, value: &str) -> bool {
         match self {
             EnumValues::String(variants) => variants.iter().any(|v| v == value),
-            EnumValues::Integer(variants) => value.parse::().map_or_else(
+            EnumValues::Integer(variants) => value.parse::().map_or_else(
                 |_| variants.iter().any(|v| v.name == value),
-                |n| variants.iter().any(|v| v.value == i64::from(n)),
+                |n| variants.iter().any(|v| v.value == n),
             ),
         }
     }
diff --git a/crates/vespertide-exporter/src/jpa/mod.rs b/crates/vespertide-exporter/src/jpa/mod.rs
index 5f992e1b..78e21a71 100644
--- a/crates/vespertide-exporter/src/jpa/mod.rs
+++ b/crates/vespertide-exporter/src/jpa/mod.rs
@@ -96,6 +96,7 @@ mod tests {
 
     #[rstest::rstest]
     #[case("created_at", "createdAt")]
+    #[case("user_id", "userId")]
     #[case("id", "id")]
     #[case("user_profile_image", "userProfileImage")]
     #[case("", "")]
diff --git a/crates/vespertide-exporter/src/jpa/render.rs b/crates/vespertide-exporter/src/jpa/render.rs
index 9435d464..780c66aa 100644
--- a/crates/vespertide-exporter/src/jpa/render.rs
+++ b/crates/vespertide-exporter/src/jpa/render.rs
@@ -8,6 +8,7 @@ use vespertide_core::schema::constraint::TableConstraint;
 use vespertide_core::{ColumnDef, TableDef};
 
 use crate::jpa::types::{UsedImports, java_type_for_column};
+use crate::utils::common::push_attr;
 
 pub(super) fn render_entity_inner(table: &TableDef) -> String {
     let mut lines: Vec = Vec::new();
@@ -352,12 +353,13 @@ fn render_fk_field(
     // @ManyToOne
     lines.push("    @ManyToOne(fetch = FetchType.LAZY)".into());
 
-    // @JoinColumn
-    let mut join_attrs: Vec = vec![format!("name = \"{}\"", col.name)];
+    // @JoinColumn — at most two attrs (`name` always, `nullable` optionally),
+    // built inline into one buffer instead of a throwaway `Vec` + join.
+    let mut join_attrs = format!("name = \"{}\"", col.name);
     if !col.nullable {
-        join_attrs.push("nullable = false".into());
+        join_attrs.push_str(", nullable = false");
     }
-    lines.push(format!("    @JoinColumn({})", join_attrs.join(", ")));
+    lines.push(format!("    @JoinColumn({join_attrs})"));
 
     // Field declaration
     lines.push(format!("    private {entity_type} {field_name};"));
@@ -368,16 +370,20 @@ fn render_fk_field(
 // ---------------------------------------------------------------------------
 
 fn build_column_attrs(col: &ColumnDef, is_pk: bool, is_unique: bool) -> String {
-    let mut attrs: Vec = vec![format!("name = \"{}\"", col.name)];
+    // Build the comma-separated attribute list directly into one buffer
+    // (preserving the exact fragment order) instead of collecting a
+    // `Vec` + `.join(", ")`.
+    let mut attrs = String::new();
+    push_attr(&mut attrs, &format!("name = \"{}\"", col.name));
 
     // nullable (skip for PK — always not-null)
     if !is_pk && !col.nullable {
-        attrs.push("nullable = false".into());
+        push_attr(&mut attrs, "nullable = false");
     }
 
     // unique (skip for PK)
     if is_unique && !is_pk {
-        attrs.push("unique = true".into());
+        push_attr(&mut attrs, "unique = true");
     }
 
     // Type-specific attributes
@@ -385,31 +391,31 @@ fn build_column_attrs(col: &ColumnDef, is_pk: bool, is_unique: bool) -> String {
         ColumnType::Complex(
             ComplexColumnType::Varchar { length } | ComplexColumnType::Char { length },
         ) => {
-            attrs.push(format!("length = {length}"));
+            push_attr(&mut attrs, &format!("length = {length}"));
         }
         ColumnType::Complex(ComplexColumnType::Numeric { precision, scale }) => {
-            attrs.push(format!("precision = {precision}"));
-            attrs.push(format!("scale = {scale}"));
+            push_attr(&mut attrs, &format!("precision = {precision}"));
+            push_attr(&mut attrs, &format!("scale = {scale}"));
         }
         ColumnType::Simple(SimpleColumnType::Text | SimpleColumnType::Xml) => {
-            attrs.push("columnDefinition = \"TEXT\"".into());
+            push_attr(&mut attrs, "columnDefinition = \"TEXT\"");
         }
         ColumnType::Simple(SimpleColumnType::Json) => {
-            attrs.push("columnDefinition = \"JSON\"".into());
+            push_attr(&mut attrs, "columnDefinition = \"JSON\"");
         }
         ColumnType::Simple(SimpleColumnType::Bytea) => {
-            attrs.push("columnDefinition = \"BYTEA\"".into());
+            push_attr(&mut attrs, "columnDefinition = \"BYTEA\"");
         }
         ColumnType::Simple(SimpleColumnType::Interval) => {
-            attrs.push("columnDefinition = \"INTERVAL\"".into());
+            push_attr(&mut attrs, "columnDefinition = \"INTERVAL\"");
         }
         ColumnType::Complex(ComplexColumnType::Custom { custom_type }) => {
-            attrs.push(format!("columnDefinition = \"{custom_type}\""));
+            push_attr(&mut attrs, &format!("columnDefinition = \"{custom_type}\""));
         }
         _ => {}
     }
 
-    attrs.join(", ")
+    attrs
 }
 
 // ---------------------------------------------------------------------------
@@ -460,15 +466,21 @@ fn build_default_initializer(col: &ColumnDef) -> Option {
 pub(super) use crate::python_naming::to_pascal_case;
 
 pub(super) fn to_camel_case(s: &str) -> String {
-    let pascal = to_pascal_case(s);
-    let mut chars = pascal.chars();
-    match chars.next() {
-        None => String::new(),
-        Some(first) => {
-            let lower: String = first.to_lowercase().collect();
-            format!("{lower}{}", chars.collect::())
-        }
+    let mut pascal = to_pascal_case(s);
+    // Lowercase only the leading character in place, preserving the exact
+    // `char::to_lowercase()` semantics of the previous implementation (Unicode
+    // multi-char lowercase mappings included) without the extra
+    // `chars.collect::()` + `format!` allocations.
+    let Some(first) = pascal.chars().next() else {
+        return pascal;
+    };
+    let lower: String = first.to_lowercase().collect();
+    // Fast path: single-char, unchanged leading char needs no rebuild.
+    if lower.len() == first.len_utf8() && lower.starts_with(first) {
+        return pascal;
     }
+    pascal.replace_range(0..first.len_utf8(), &lower);
+    pascal
 }
 
 pub(super) fn infer_fk_field_name(column_name: &str) -> String {
diff --git a/crates/vespertide-exporter/src/sqlalchemy/render.rs b/crates/vespertide-exporter/src/sqlalchemy/render.rs
index 13e4105b..183a14ac 100644
--- a/crates/vespertide-exporter/src/sqlalchemy/render.rs
+++ b/crates/vespertide-exporter/src/sqlalchemy/render.rs
@@ -5,7 +5,7 @@ use super::types::{UsedTypes, column_type_to_python, column_type_to_sqlalchemy};
 use crate::parallel_config::{
     PYTHON_EXPORT_PAR_TABLE_MIN_LEN, SQLALCHEMY_EXPORT_PAR_TABLE_THRESHOLD,
 };
-use crate::utils::common::join_quoted;
+use crate::utils::common::{join_qualified_refs, join_quoted, push_attr};
 use crate::utils::python::collect_composite_fks;
 use rayon::prelude::*;
 use vespertide_core::schema::column::{ColumnType, ComplexColumnType, EnumValues};
@@ -231,12 +231,7 @@ fn render_entity_part(table: &TableDef, used_types: &mut UsedTypes<'static>) ->
 
         for fk in &composite_fks {
             let local_cols = join_quoted(&fk.local_cols);
-            let ref_cols = fk
-                .ref_cols
-                .iter()
-                .map(|col| format!("\"{}.{}\"", fk.ref_table, col))
-                .collect::>()
-                .join(", ");
+            let ref_cols = join_qualified_refs(fk.ref_table, &fk.ref_cols);
             lines.push(format!(
                 "        ForeignKeyConstraint([{local_cols}], [{ref_cols}]),"
             ));
@@ -309,32 +304,42 @@ fn render_column(
     let python_type = column_type_to_python(&col.r#type, col.nullable);
     let sa_type = column_type_to_sqlalchemy(&col.r#type);
 
-    let mut attrs: Vec = Vec::new();
+    // Build the comma-separated attribute list directly into one buffer
+    // (preserving the exact fragment order) instead of collecting a
+    // `Vec` + `.join(", ")`.
+    let mut attrs = String::new();
 
     // Add SQLAlchemy type
-    attrs.push(sa_type);
+    push_attr(&mut attrs, &sa_type);
 
     // Foreign key
     if let Some((ref_table, ref_col)) = fk_info {
-        attrs.push(format!("ForeignKey(\"{ref_table}.{ref_col}\")"));
+        push_attr(
+            &mut attrs,
+            &format!("ForeignKey(\"{ref_table}.{ref_col}\")"),
+        );
     }
 
     // Primary key
     if is_pk {
-        attrs.push("primary_key=True".into());
+        push_attr(&mut attrs, "primary_key=True");
     }
 
     // Nullable
     if !is_pk {
-        attrs.push(format!(
-            "nullable={}",
-            if col.nullable { "True" } else { "False" }
-        ));
+        push_attr(
+            &mut attrs,
+            if col.nullable {
+                "nullable=True"
+            } else {
+                "nullable=False"
+            },
+        );
     }
 
     // Unique
     if is_unique && !is_pk {
-        attrs.push("unique=True".into());
+        push_attr(&mut attrs, "unique=True");
     }
 
     // Default value
@@ -344,18 +349,17 @@ fn render_column(
         let escaped = default_str.replace('"', "\\\"");
         // Check if it's a function call or literal
         if default_str.contains('(') {
-            attrs.push(format!("server_default=text(\"{escaped}\")"));
+            push_attr(&mut attrs, &format!("server_default=text(\"{escaped}\")"));
         } else if default_str.starts_with('\'') || default_str.starts_with('"') {
-            attrs.push(format!("server_default={default_str}"));
+            push_attr(&mut attrs, &format!("server_default={default_str}"));
         } else {
-            attrs.push(format!("server_default=\"{escaped}\""));
+            push_attr(&mut attrs, &format!("server_default=\"{escaped}\""));
         }
     }
 
-    let attrs_str = attrs.join(", ");
     lines.push(format!(
         "    {}: Mapped[{}] = mapped_column({})",
-        col.name, python_type, attrs_str
+        col.name, python_type, attrs
     ));
 }
 
diff --git a/crates/vespertide-exporter/src/sqlmodel/render.rs b/crates/vespertide-exporter/src/sqlmodel/render.rs
index 7f1a84e9..a4104494 100644
--- a/crates/vespertide-exporter/src/sqlmodel/render.rs
+++ b/crates/vespertide-exporter/src/sqlmodel/render.rs
@@ -3,7 +3,7 @@ use rayon::prelude::*;
 use crate::parallel_config::{
     PYTHON_EXPORT_PAR_TABLE_MIN_LEN, SQLMODEL_EXPORT_PAR_TABLE_THRESHOLD,
 };
-use crate::utils::common::join_quoted;
+use crate::utils::common::{join_qualified_refs, join_quoted};
 use crate::utils::python::{CompositeFk, collect_composite_fks};
 use vespertide_core::schema::column::{ColumnType, ComplexColumnType, EnumValues};
 use vespertide_core::schema::constraint::TableConstraint;
@@ -364,12 +364,7 @@ fn render_entity_body(table: &TableDef, composite_fks: &[CompositeFk<'_>]) -> Ve
 
         for fk in composite_fks {
             let local_cols = join_quoted(&fk.local_cols);
-            let ref_cols = fk
-                .ref_cols
-                .iter()
-                .map(|col| format!("\"{}.{}\"", fk.ref_table, col))
-                .collect::>()
-                .join(", ");
+            let ref_cols = join_qualified_refs(fk.ref_table, &fk.ref_cols);
             lines.push(format!(
                 "        ForeignKeyConstraint([{local_cols}], [{ref_cols}]),"
             ));
diff --git a/crates/vespertide-exporter/src/utils/common.rs b/crates/vespertide-exporter/src/utils/common.rs
index 2b17540c..b311c771 100644
--- a/crates/vespertide-exporter/src/utils/common.rs
+++ b/crates/vespertide-exporter/src/utils/common.rs
@@ -19,6 +19,43 @@ pub(crate) fn join_quoted>(items: &[T]) -> String {
     out
 }
 
+/// Append one comma-separated attribute `fragment` into `buf`, inserting a
+/// `", "` separator before every fragment except the first.
+///
+/// Replaces the `Vec` + `.join(", ")` pattern used to assemble ORM
+/// column-attribute lists (`@Column(name = ..., nullable = false)`,
+/// `mapped_column(String, primary_key=True)`): fragments are written straight
+/// into a single buffer with no intermediate `Vec` or per-fragment `String`
+/// allocations. Output is byte-identical to `fragments.join(", ")`.
+pub(crate) fn push_attr(buf: &mut String, fragment: &str) {
+    if !buf.is_empty() {
+        buf.push_str(", ");
+    }
+    buf.push_str(fragment);
+}
+
+/// Join FK-target columns as a double-quoted, `ref_table`-qualified,
+/// comma-separated list: `"tbl.col1", "tbl.col2"`.
+///
+/// Consolidates the identical `ForeignKeyConstraint([...], [...])` target
+/// rendering previously copy-pasted across the `SQLAlchemy` and `SQLModel`
+/// renderers, and builds the result in a single buffer instead of collecting an
+/// intermediate `Vec` (one `String` per column) per call site.
+pub(crate) fn join_qualified_refs(ref_table: &str, ref_cols: &[&str]) -> String {
+    let mut out = String::new();
+    for col in ref_cols {
+        if !out.is_empty() {
+            out.push_str(", ");
+        }
+        out.push('"');
+        out.push_str(ref_table);
+        out.push('.');
+        out.push_str(col);
+        out.push('"');
+    }
+    out
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -37,4 +74,43 @@ mod tests {
     fn multiple_items_are_comma_separated() {
         assert_eq!(join_quoted(&["a", "b", "c"]), "\"a\", \"b\", \"c\"");
     }
+
+    #[test]
+    fn qualified_refs_empty_slice_yields_empty_string() {
+        assert_eq!(join_qualified_refs("user", &[]), "");
+    }
+
+    #[test]
+    fn qualified_refs_single_column_is_table_qualified() {
+        assert_eq!(join_qualified_refs("user", &["id"]), "\"user.id\"");
+    }
+
+    #[test]
+    fn qualified_refs_multiple_columns_are_comma_separated() {
+        assert_eq!(
+            join_qualified_refs("account", &["tenant_id", "id"]),
+            "\"account.tenant_id\", \"account.id\""
+        );
+    }
+
+    #[test]
+    fn push_attr_matches_join_semantics() {
+        let mut buf = String::new();
+        push_attr(&mut buf, "name = \"id\"");
+        assert_eq!(buf, "name = \"id\"");
+        push_attr(&mut buf, "nullable = false");
+        push_attr(&mut buf, "unique = true");
+        assert_eq!(buf, "name = \"id\", nullable = false, unique = true");
+        assert_eq!(
+            buf,
+            ["name = \"id\"", "nullable = false", "unique = true"].join(", ")
+        );
+    }
+
+    #[test]
+    fn push_attr_first_fragment_has_no_leading_separator() {
+        let mut buf = String::new();
+        push_attr(&mut buf, "String");
+        assert_eq!(buf, "String");
+    }
 }
diff --git a/crates/vespertide-planner/src/validate/enums.rs b/crates/vespertide-planner/src/validate/enums.rs
index c45427ee..d08e0d60 100644
--- a/crates/vespertide-planner/src/validate/enums.rs
+++ b/crates/vespertide-planner/src/validate/enums.rs
@@ -49,7 +49,7 @@ pub(super) fn validate_enum_value(
     let is_valid = enum_values.contains_value(extracted);
 
     if !is_valid {
-        let allowed = enum_values.variant_names().join(", ");
+        let allowed = enum_values.variant_names_joined(", ");
         return Err(Box::new(InvalidEnumDefaultError {
             enum_name: enum_name.to_string(),
             table_name: table_name.to_string(),

From f46b8f5daeef5bb7f7de7ea91aeed165f0a6e432 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 12:38:51 +0900
Subject: [PATCH 073/109] retry-now#0072: batch - drop dead
 EnumValues::variant_names, route SeaORM index-comment join through
 join_column_names (2 kept)

---
 crates/vespertide-core/src/schema/column.rs   | 10 +-----
 crates/vespertide-core/src/schema/mod.rs      | 32 ++++++-------------
 .../vespertide-exporter/src/seaorm/render.rs  |  2 +-
 3 files changed, 12 insertions(+), 32 deletions(-)

diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs
index 18b86871..85411c1b 100644
--- a/crates/vespertide-core/src/schema/column.rs
+++ b/crates/vespertide-core/src/schema/column.rs
@@ -489,21 +489,13 @@ impl EnumValues {
         matches!(self, EnumValues::Integer(_))
     }
 
-    /// Get all variant names
-    pub fn variant_names(&self) -> Vec<&str> {
-        match self {
-            EnumValues::String(values) => values.iter().map(std::string::String::as_str).collect(),
-            EnumValues::Integer(values) => values.iter().map(|v| v.name.as_str()).collect(),
-        }
-    }
-
     /// Join every variant *name* with `separator`, writing straight into one
     /// buffer — no intermediate `Vec<&str>` allocation.
     ///
     /// Unlike [`Self::sql_values_joined`] (which emits SQL literals: quoted
     /// strings / integer values), this emits the human-readable variant
     /// *names* for diagnostics such as "allowed values are: a, b, c". For an
-    /// integer enum the member names are used, matching `variant_names()`.
+    /// integer enum the member names are used.
     ///
     /// ```rust
     /// use vespertide_core::{EnumValues, NumValue};
diff --git a/crates/vespertide-core/src/schema/mod.rs b/crates/vespertide-core/src/schema/mod.rs
index c9e8bc15..3e4ca6b3 100644
--- a/crates/vespertide-core/src/schema/mod.rs
+++ b/crates/vespertide-core/src/schema/mod.rs
@@ -220,27 +220,6 @@ mod tests {
             assert!(int_vals.is_integer());
         }
 
-        #[test]
-        fn test_enum_values_variant_names_string() {
-            let vals = EnumValues::String(vec!["pending".into(), "active".into()]);
-            assert_eq!(vals.variant_names(), vec!["pending", "active"]);
-        }
-
-        #[test]
-        fn test_enum_values_variant_names_integer() {
-            let vals = EnumValues::Integer(vec![
-                NumValue {
-                    name: "Low".into(),
-                    value: 0,
-                },
-                NumValue {
-                    name: "High".into(),
-                    value: 10,
-                },
-            ]);
-            assert_eq!(vals.variant_names(), vec!["Low", "High"]);
-        }
-
         #[rstest]
         // String match: exact-string check against each variant.
         #[case::string_match(
@@ -253,7 +232,7 @@ mod tests {
             "banned",
             false,
         )]
-        // Integer enum, numeric input: parses as i32 then matches `NumValue::value`.
+        // Integer enum, numeric input: parses as i64 then matches `NumValue::value`.
         #[case::integer_numeric_match(
             EnumValues::Integer(vec![
                 NumValue { name: "Low".into(),  value: 0  },
@@ -270,6 +249,15 @@ mod tests {
             "99",
             false,
         )]
+        // Integer enum, value beyond i32::MAX: must still parse (i64) and match.
+        #[case::integer_numeric_beyond_i32(
+            EnumValues::Integer(vec![
+                NumValue { name: "small".into(), value: 1             },
+                NumValue { name: "big".into(),   value: 3_000_000_000 },
+            ]),
+            "3000000000",
+            true,
+        )]
         // Integer enum, non-numeric input: falls back to matching `NumValue::name`.
         #[case::integer_name_match(
             EnumValues::Integer(vec![
diff --git a/crates/vespertide-exporter/src/seaorm/render.rs b/crates/vespertide-exporter/src/seaorm/render.rs
index 402f76c0..fa6978f4 100644
--- a/crates/vespertide-exporter/src/seaorm/render.rs
+++ b/crates/vespertide-exporter/src/seaorm/render.rs
@@ -276,7 +276,7 @@ pub(super) fn render_indexes_and_uniques(lines: &mut Vec, constraints: &
     if !index_constraints.is_empty() {
         lines.push("// Index definitions (SeaORM uses Statement builders externally)".into());
         for (name, columns) in index_constraints {
-            let cols = columns.join(", ");
+            let cols = vespertide_core::schema::names::join_column_names(columns, ", ");
             let idx_name = name.clone().unwrap_or_else(|| "(unnamed)".to_string());
             lines.push(format!("// {idx_name} on [{cols}]"));
         }

From 66cb48da0163091261c317cc2eb4e0e1db019481 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 12:47:51 +0900
Subject: [PATCH 074/109] retry-now#0073: dedupe single-column FK-info maps
 into constraint_scan helper (1 kept)

---
 .../src/constraint_scan.rs                    | 30 ++++++++++++++++++-
 .../src/sqlalchemy/render.rs                  | 27 +----------------
 .../src/sqlmodel/render.rs                    | 25 +---------------
 3 files changed, 31 insertions(+), 51 deletions(-)

diff --git a/crates/vespertide-exporter/src/constraint_scan.rs b/crates/vespertide-exporter/src/constraint_scan.rs
index 82b31d1c..b786ee81 100644
--- a/crates/vespertide-exporter/src/constraint_scan.rs
+++ b/crates/vespertide-exporter/src/constraint_scan.rs
@@ -6,7 +6,7 @@
 //! a single-column index. Centralising the scans keeps the four
 //! renderers from drifting apart.
 
-use std::collections::HashSet;
+use std::collections::{HashMap, HashSet};
 
 use vespertide_core::{ColumnName, TableConstraint};
 
@@ -64,3 +64,31 @@ pub(crate) fn single_column_indexes(constraints: &[TableConstraint]) -> HashSet<
         _ => None,
     })
 }
+
+/// Map each single-column foreign key's column name to its
+/// `(ref_table, ref_col)` target. Only foreign keys with exactly one owning
+/// column and one referenced column are included; composite FKs are skipped.
+///
+/// Lookup-only, ordering unused.
+pub(crate) fn single_column_fk_targets(
+    constraints: &[TableConstraint],
+) -> HashMap {
+    let mut map = HashMap::new();
+    for constraint in constraints {
+        if let TableConstraint::ForeignKey {
+            columns,
+            ref_table,
+            ref_columns,
+            ..
+        } = constraint
+            && columns.len() == 1
+            && ref_columns.len() == 1
+        {
+            map.insert(
+                columns[0].to_string(),
+                (ref_table.to_string(), ref_columns[0].to_string()),
+            );
+        }
+    }
+    map
+}
diff --git a/crates/vespertide-exporter/src/sqlalchemy/render.rs b/crates/vespertide-exporter/src/sqlalchemy/render.rs
index 183a14ac..65d7109b 100644
--- a/crates/vespertide-exporter/src/sqlalchemy/render.rs
+++ b/crates/vespertide-exporter/src/sqlalchemy/render.rs
@@ -1,5 +1,3 @@
-use std::collections::HashMap;
-
 use super::enums::render_enum;
 use super::types::{UsedTypes, column_type_to_python, column_type_to_sqlalchemy};
 use crate::parallel_config::{
@@ -139,30 +137,7 @@ fn render_entity_part(table: &TableDef, used_types: &mut UsedTypes<'static>) ->
     let unique_columns = crate::constraint_scan::single_column_uniques(&table.constraints);
 
     // Collect foreign key info; lookup-only, ordering unused.
-    let fk_info: HashMap = table
-        .constraints
-        .iter()
-        .filter_map(|c| {
-            if let TableConstraint::ForeignKey {
-                columns,
-                ref_table,
-                ref_columns,
-                ..
-            } = c
-            {
-                if columns.len() == 1 && ref_columns.len() == 1 {
-                    Some((
-                        columns[0].to_string(),
-                        (ref_table.to_string(), ref_columns[0].to_string()),
-                    ))
-                } else {
-                    None
-                }
-            } else {
-                None
-            }
-        })
-        .collect();
+    let fk_info = crate::constraint_scan::single_column_fk_targets(&table.constraints);
 
     // Render columns
     for col in &table.columns {
diff --git a/crates/vespertide-exporter/src/sqlmodel/render.rs b/crates/vespertide-exporter/src/sqlmodel/render.rs
index a4104494..7c153d33 100644
--- a/crates/vespertide-exporter/src/sqlmodel/render.rs
+++ b/crates/vespertide-exporter/src/sqlmodel/render.rs
@@ -268,30 +268,7 @@ fn render_entity_body(table: &TableDef, composite_fks: &[CompositeFk<'_>]) -> Ve
         .collect();
 
     // Collect foreign key info; lookup-only, ordering unused.
-    let fk_info: std::collections::HashMap = table
-        .constraints
-        .iter()
-        .filter_map(|c| {
-            if let TableConstraint::ForeignKey {
-                columns,
-                ref_table,
-                ref_columns,
-                ..
-            } = c
-            {
-                if columns.len() == 1 && ref_columns.len() == 1 {
-                    Some((
-                        columns[0].to_string(),
-                        (ref_table.to_string(), ref_columns[0].to_string()),
-                    ))
-                } else {
-                    None
-                }
-            } else {
-                None
-            }
-        })
-        .collect();
+    let fk_info = crate::constraint_scan::single_column_fk_targets(&table.constraints);
 
     // Render columns
     for col in &table.columns {

From 2bba8c8b0206af281bc76105809422364e8fa5a8 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 12:59:27 +0900
Subject: [PATCH 075/109] =?UTF-8?q?retry-now#0074:=20batch=20=E2=80=94=20u?=
 =?UTF-8?q?nify=20Python=20exporter=20column=5Ftype=5Fto=5Fpython=20+=20re?=
 =?UTF-8?q?nder=5Fenum=20into=20utils::python=20(2=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/sqlalchemy/enums.rs                   | 26 +-----
 .../vespertide-exporter/src/sqlalchemy/mod.rs |  2 +-
 .../src/sqlalchemy/render.rs                  |  2 +-
 .../src/sqlalchemy/types.rs                   | 54 ++----------
 .../vespertide-exporter/src/sqlmodel/enums.rs | 28 ++-----
 .../vespertide-exporter/src/sqlmodel/mod.rs   |  2 +-
 .../vespertide-exporter/src/sqlmodel/types.rs | 54 ++----------
 .../vespertide-exporter/src/utils/python.rs   | 83 +++++++++++++++++++
 8 files changed, 106 insertions(+), 145 deletions(-)

diff --git a/crates/vespertide-exporter/src/sqlalchemy/enums.rs b/crates/vespertide-exporter/src/sqlalchemy/enums.rs
index 5cbfa88a..3ccab993 100644
--- a/crates/vespertide-exporter/src/sqlalchemy/enums.rs
+++ b/crates/vespertide-exporter/src/sqlalchemy/enums.rs
@@ -1,22 +1,4 @@
-use super::render::{to_pascal_case, to_screaming_snake_case};
-use vespertide_core::schema::column::EnumValues;
-
-pub(super) fn render_enum(lines: &mut Vec, name: &str, values: &EnumValues) {
-    let class_name = to_pascal_case(name);
-
-    match values {
-        EnumValues::String(vals) => {
-            lines.push(format!("class {class_name}(str, enum.Enum):"));
-            for val in vals {
-                let variant_name = to_screaming_snake_case(val);
-                lines.push(format!("    {variant_name} = \"{val}\""));
-            }
-        }
-        EnumValues::Integer(vals) => {
-            lines.push(format!("class {class_name}(enum.IntEnum):"));
-            for val in vals {
-                lines.push(format!("    {} = {}", val.name, val.value));
-            }
-        }
-    }
-}
+// Shared verbatim with the SQLModel backend — both Python ORMs emit an
+// identical Python `enum` class, so the single implementation lives in
+// `crate::utils::python`.
+pub(super) use crate::utils::python::render_enum;
diff --git a/crates/vespertide-exporter/src/sqlalchemy/mod.rs b/crates/vespertide-exporter/src/sqlalchemy/mod.rs
index 3e3ea667..5eae2948 100644
--- a/crates/vespertide-exporter/src/sqlalchemy/mod.rs
+++ b/crates/vespertide-exporter/src/sqlalchemy/mod.rs
@@ -27,8 +27,8 @@ pub(crate) fn to_pascal_case_for_tests(s: &str) -> String {
 
 #[cfg(test)]
 mod tests {
-    use super::render::to_screaming_snake_case;
     use super::types::UsedTypes;
+    use crate::python_naming::to_screaming_snake_case;
     use vespertide_core::schema::column::{
         ColumnType, ComplexColumnType, EnumValues, NumValue, SimpleColumnType,
     };
diff --git a/crates/vespertide-exporter/src/sqlalchemy/render.rs b/crates/vespertide-exporter/src/sqlalchemy/render.rs
index 65d7109b..a8f5d979 100644
--- a/crates/vespertide-exporter/src/sqlalchemy/render.rs
+++ b/crates/vespertide-exporter/src/sqlalchemy/render.rs
@@ -343,4 +343,4 @@ fn render_column(
 // SCREAMING_SNAKE_CASE enum member names, so the implementation lives in
 // `crate::python_naming` and we re-export it here to keep every existing
 // `super::render::to_*` path working without churn.
-pub(super) use crate::python_naming::{to_pascal_case, to_screaming_snake_case};
+pub(super) use crate::python_naming::to_pascal_case;
diff --git a/crates/vespertide-exporter/src/sqlalchemy/types.rs b/crates/vespertide-exporter/src/sqlalchemy/types.rs
index 9918efb1..766c6898 100644
--- a/crates/vespertide-exporter/src/sqlalchemy/types.rs
+++ b/crates/vespertide-exporter/src/sqlalchemy/types.rs
@@ -5,6 +5,11 @@ use vespertide_core::schema::column::{
     ColumnType, ComplexColumnType, EnumValues, SimpleColumnType,
 };
 
+// Shared verbatim with the SQLModel backend — both Python ORMs map a
+// `ColumnType` to an identical Python type annotation, so the single
+// implementation lives in `crate::utils::python`.
+pub(super) use crate::utils::python::column_type_to_python;
+
 /// Track which types are actually used to generate minimal imports
 #[derive(Default)]
 pub(super) struct UsedTypes<'a> {
@@ -114,55 +119,6 @@ impl UsedTypes<'_> {
     }
 }
 
-pub(super) fn column_type_to_python(col_type: &ColumnType, nullable: bool) -> String {
-    let base = match col_type {
-        ColumnType::Simple(ty) => match ty {
-            SimpleColumnType::SmallInt | SimpleColumnType::Integer | SimpleColumnType::BigInt => {
-                "int"
-            }
-            SimpleColumnType::Real | SimpleColumnType::DoublePrecision => "float",
-            SimpleColumnType::Text
-            | SimpleColumnType::Interval
-            | SimpleColumnType::Inet
-            | SimpleColumnType::Cidr
-            | SimpleColumnType::Macaddr
-            | SimpleColumnType::Xml => "str",
-            SimpleColumnType::Boolean => "bool",
-            SimpleColumnType::Date => "date",
-            SimpleColumnType::Time => "time",
-            SimpleColumnType::Timestamp | SimpleColumnType::Timestamptz => "datetime",
-            SimpleColumnType::Bytea => "bytes",
-            SimpleColumnType::Uuid => "UUID",
-            SimpleColumnType::Json => "dict",
-            _ => unreachable!(
-                "SimpleColumnType is #[non_exhaustive]; all variants are matched above"
-            ),
-        },
-        ColumnType::Complex(ty) => match ty {
-            ComplexColumnType::Numeric { .. } => "Decimal",
-            ComplexColumnType::Varchar { .. }
-            | ComplexColumnType::Char { .. }
-            | ComplexColumnType::Custom { .. } => "str",
-            ComplexColumnType::Enum { name, .. } => {
-                return if nullable {
-                    format!("Optional[{}]", to_pascal_case(name))
-                } else {
-                    to_pascal_case(name)
-                };
-            }
-            _ => unreachable!(
-                "ComplexColumnType is #[non_exhaustive]; all variants are matched above"
-            ),
-        },
-    };
-
-    if nullable {
-        format!("Optional[{base}]")
-    } else {
-        base.to_string()
-    }
-}
-
 pub(super) fn column_type_to_sqlalchemy(col_type: &ColumnType) -> String {
     match col_type {
         ColumnType::Simple(ty) => match ty {
diff --git a/crates/vespertide-exporter/src/sqlmodel/enums.rs b/crates/vespertide-exporter/src/sqlmodel/enums.rs
index 8c032ba6..3bcb0c99 100644
--- a/crates/vespertide-exporter/src/sqlmodel/enums.rs
+++ b/crates/vespertide-exporter/src/sqlmodel/enums.rs
@@ -1,28 +1,12 @@
-use vespertide_core::schema::column::EnumValues;
-
 // Naming helpers shared with the `SQLAlchemy` exporter — both Python ORMs
 // produce identical PascalCase class names and identical
 // SCREAMING_SNAKE_CASE enum member names, so the implementation lives in
 // `crate::python_naming` and we re-export it here to keep every existing
 // `super::enums::to_*` path working without churn.
-pub(super) use crate::python_naming::{to_pascal_case, to_screaming_snake_case};
-
-pub(super) fn render_enum(lines: &mut Vec, name: &str, values: &EnumValues) {
-    let class_name = to_pascal_case(name);
+pub(super) use crate::python_naming::to_pascal_case;
 
-    match values {
-        EnumValues::String(vals) => {
-            lines.push(format!("class {class_name}(str, enum.Enum):"));
-            for val in vals {
-                let variant_name = to_screaming_snake_case(val);
-                lines.push(format!("    {variant_name} = \"{val}\""));
-            }
-        }
-        EnumValues::Integer(vals) => {
-            lines.push(format!("class {class_name}(enum.IntEnum):"));
-            for val in vals {
-                lines.push(format!("    {} = {}", val.name, val.value));
-            }
-        }
-    }
-}
+// `render_enum` is shared verbatim with the SQLAlchemy backend — both Python
+// ORMs emit an identical Python `enum` class, so the single implementation
+// lives in `crate::utils::python`. The naming re-export above stays because
+// `sqlmodel/render.rs` still resolves `super::enums::to_pascal_case`.
+pub(super) use crate::utils::python::render_enum;
diff --git a/crates/vespertide-exporter/src/sqlmodel/mod.rs b/crates/vespertide-exporter/src/sqlmodel/mod.rs
index 6655c2eb..f5115359 100644
--- a/crates/vespertide-exporter/src/sqlmodel/mod.rs
+++ b/crates/vespertide-exporter/src/sqlmodel/mod.rs
@@ -27,8 +27,8 @@ pub(crate) fn to_pascal_case_for_tests(s: &str) -> String {
 
 #[cfg(test)]
 mod tests {
-    use super::enums::to_screaming_snake_case;
     use super::types::UsedTypes;
+    use crate::python_naming::to_screaming_snake_case;
     use vespertide_core::{ColumnType, ComplexColumnType, SimpleColumnType};
 
     #[rstest::rstest]
diff --git a/crates/vespertide-exporter/src/sqlmodel/types.rs b/crates/vespertide-exporter/src/sqlmodel/types.rs
index 22a49a23..6cc802d2 100644
--- a/crates/vespertide-exporter/src/sqlmodel/types.rs
+++ b/crates/vespertide-exporter/src/sqlmodel/types.rs
@@ -1,8 +1,12 @@
 use std::collections::BTreeSet;
 
-use super::enums::to_pascal_case;
 use vespertide_core::schema::column::{ColumnType, ComplexColumnType, SimpleColumnType};
 
+// Shared verbatim with the SQLAlchemy backend — both Python ORMs map a
+// `ColumnType` to an identical Python type annotation, so the single
+// implementation lives in `crate::utils::python`.
+pub(super) use crate::utils::python::column_type_to_python;
+
 /// Track which types are actually used to generate minimal imports
 #[derive(Default)]
 #[expect(
@@ -62,51 +66,3 @@ impl UsedTypes<'_> {
     }
 }
 
-pub(super) fn column_type_to_python(col_type: &ColumnType, nullable: bool) -> String {
-    let base = match col_type {
-        ColumnType::Simple(ty) => match ty {
-            SimpleColumnType::SmallInt | SimpleColumnType::Integer | SimpleColumnType::BigInt => {
-                "int"
-            }
-            SimpleColumnType::Real | SimpleColumnType::DoublePrecision => "float",
-            SimpleColumnType::Text
-            | SimpleColumnType::Interval
-            | SimpleColumnType::Inet
-            | SimpleColumnType::Cidr
-            | SimpleColumnType::Macaddr
-            | SimpleColumnType::Xml => "str",
-            SimpleColumnType::Boolean => "bool",
-            SimpleColumnType::Date => "date",
-            SimpleColumnType::Time => "time",
-            SimpleColumnType::Timestamp | SimpleColumnType::Timestamptz => "datetime",
-            SimpleColumnType::Bytea => "bytes",
-            SimpleColumnType::Uuid => "UUID",
-            SimpleColumnType::Json => "dict",
-            _ => unreachable!(
-                "SimpleColumnType is #[non_exhaustive]; all variants are matched above"
-            ),
-        },
-        ColumnType::Complex(ty) => match ty {
-            ComplexColumnType::Numeric { .. } => "Decimal",
-            ComplexColumnType::Varchar { .. }
-            | ComplexColumnType::Char { .. }
-            | ComplexColumnType::Custom { .. } => "str",
-            ComplexColumnType::Enum { name, .. } => {
-                return if nullable {
-                    format!("Optional[{}]", to_pascal_case(name))
-                } else {
-                    to_pascal_case(name)
-                };
-            }
-            _ => unreachable!(
-                "ComplexColumnType is #[non_exhaustive]; all variants are matched above"
-            ),
-        },
-    };
-
-    if nullable {
-        format!("Optional[{base}]")
-    } else {
-        base.to_string()
-    }
-}
diff --git a/crates/vespertide-exporter/src/utils/python.rs b/crates/vespertide-exporter/src/utils/python.rs
index fb104753..d082e5f8 100644
--- a/crates/vespertide-exporter/src/utils/python.rs
+++ b/crates/vespertide-exporter/src/utils/python.rs
@@ -1,6 +1,89 @@
 use vespertide_core::TableDef;
+use vespertide_core::schema::column::{
+    ColumnType, ComplexColumnType, EnumValues, SimpleColumnType,
+};
 use vespertide_core::schema::constraint::TableConstraint;
 
+use crate::python_naming::{to_pascal_case, to_screaming_snake_case};
+
+/// Emit a Python `enum` class definition shared verbatim by the SQLAlchemy and
+/// SQLModel backends: `class {Pascal}(str, enum.Enum)` for string enums (members
+/// via `to_screaming_snake_case`) and `class {Pascal}(enum.IntEnum)` for integer
+/// enums. Both Python ORMs produce byte-identical enum classes.
+pub(crate) fn render_enum(lines: &mut Vec, name: &str, values: &EnumValues) {
+    let class_name = to_pascal_case(name);
+
+    match values {
+        EnumValues::String(vals) => {
+            lines.push(format!("class {class_name}(str, enum.Enum):"));
+            for val in vals {
+                let variant_name = to_screaming_snake_case(val);
+                lines.push(format!("    {variant_name} = \"{val}\""));
+            }
+        }
+        EnumValues::Integer(vals) => {
+            lines.push(format!("class {class_name}(enum.IntEnum):"));
+            for val in vals {
+                lines.push(format!("    {} = {}", val.name, val.value));
+            }
+        }
+    }
+}
+
+/// Map a `ColumnType` to its Python type annotation string, shared verbatim by
+/// the SQLAlchemy and SQLModel backends (both produce identical
+/// `int`/`float`/`str`/`datetime`/`Decimal`/`Optional[...]`/enum-PascalCase
+/// annotations). A single home means a future `SimpleColumnType` /
+/// `ComplexColumnType` variant is mapped in exactly one place.
+pub(crate) fn column_type_to_python(col_type: &ColumnType, nullable: bool) -> String {
+    let base = match col_type {
+        ColumnType::Simple(ty) => match ty {
+            SimpleColumnType::SmallInt | SimpleColumnType::Integer | SimpleColumnType::BigInt => {
+                "int"
+            }
+            SimpleColumnType::Real | SimpleColumnType::DoublePrecision => "float",
+            SimpleColumnType::Text
+            | SimpleColumnType::Interval
+            | SimpleColumnType::Inet
+            | SimpleColumnType::Cidr
+            | SimpleColumnType::Macaddr
+            | SimpleColumnType::Xml => "str",
+            SimpleColumnType::Boolean => "bool",
+            SimpleColumnType::Date => "date",
+            SimpleColumnType::Time => "time",
+            SimpleColumnType::Timestamp | SimpleColumnType::Timestamptz => "datetime",
+            SimpleColumnType::Bytea => "bytes",
+            SimpleColumnType::Uuid => "UUID",
+            SimpleColumnType::Json => "dict",
+            _ => unreachable!(
+                "SimpleColumnType is #[non_exhaustive]; all variants are matched above"
+            ),
+        },
+        ColumnType::Complex(ty) => match ty {
+            ComplexColumnType::Numeric { .. } => "Decimal",
+            ComplexColumnType::Varchar { .. }
+            | ComplexColumnType::Char { .. }
+            | ComplexColumnType::Custom { .. } => "str",
+            ComplexColumnType::Enum { name, .. } => {
+                return if nullable {
+                    format!("Optional[{}]", to_pascal_case(name))
+                } else {
+                    to_pascal_case(name)
+                };
+            }
+            _ => unreachable!(
+                "ComplexColumnType is #[non_exhaustive]; all variants are matched above"
+            ),
+        },
+    };
+
+    if nullable {
+        format!("Optional[{base}]")
+    } else {
+        base.to_string()
+    }
+}
+
 pub(crate) struct CompositeFk<'a> {
     pub local_cols: Vec<&'a str>,
     pub ref_table: &'a str,

From 2d5aa38938963c41d4ee97f88b2d8446a8f44ffe Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 13:12:10 +0900
Subject: [PATCH 076/109] retry-now#0075: batch - borrow constraint_scan lookup
 sets & FK map as &str, drop per-column String allocs (2 kept)

---
 .../src/constraint_scan.rs                    | 24 +++++++++----------
 .../src/seaorm/relations/reverse.rs           |  4 ++--
 .../src/seaorm/relations/self_ref.rs          |  2 +-
 .../vespertide-exporter/src/seaorm/render.rs  | 10 ++++----
 .../src/sqlalchemy/render.rs                  |  2 +-
 .../src/sqlmodel/render.rs                    |  2 +-
 6 files changed, 22 insertions(+), 22 deletions(-)

diff --git a/crates/vespertide-exporter/src/constraint_scan.rs b/crates/vespertide-exporter/src/constraint_scan.rs
index b786ee81..fec30e0c 100644
--- a/crates/vespertide-exporter/src/constraint_scan.rs
+++ b/crates/vespertide-exporter/src/constraint_scan.rs
@@ -15,16 +15,16 @@ use vespertide_core::{ColumnName, TableConstraint};
 /// [`single_column_indexes`], which differ only in the matched
 /// `TableConstraint` variant. `extract` returns the constraint's column slice
 /// for the variant it cares about, or `None` for every other variant.
-fn single_column_scan(
-    constraints: &[TableConstraint],
-    extract: impl Fn(&TableConstraint) -> Option<&[ColumnName]>,
-) -> HashSet {
+fn single_column_scan<'a>(
+    constraints: &'a [TableConstraint],
+    extract: impl Fn(&'a TableConstraint) -> Option<&'a [ColumnName]>,
+) -> HashSet<&'a str> {
     let mut cols = HashSet::new();
     for constraint in constraints {
         if let Some(columns) = extract(constraint)
             && columns.len() == 1
         {
-            cols.insert(columns[0].to_string());
+            cols.insert(columns[0].as_str());
         }
     }
     cols
@@ -33,12 +33,12 @@ fn single_column_scan(
 /// Collect the column names covered by table-level `PrimaryKey` constraints.
 ///
 /// Lookup-only, ordering unused.
-pub(crate) fn primary_key_columns(constraints: &[TableConstraint]) -> HashSet {
+pub(crate) fn primary_key_columns(constraints: &[TableConstraint]) -> HashSet<&str> {
     let mut keys = HashSet::new();
     for constraint in constraints {
         if let TableConstraint::PrimaryKey { columns, .. } = constraint {
             for col in columns {
-                keys.insert(col.to_string());
+                keys.insert(col.as_str());
             }
         }
     }
@@ -48,7 +48,7 @@ pub(crate) fn primary_key_columns(constraints: &[TableConstraint]) -> HashSet HashSet {
+pub(crate) fn single_column_uniques(constraints: &[TableConstraint]) -> HashSet<&str> {
     single_column_scan(constraints, |c| match c {
         TableConstraint::Unique { columns, .. } => Some(columns.as_slice()),
         _ => None,
@@ -58,7 +58,7 @@ pub(crate) fn single_column_uniques(constraints: &[TableConstraint]) -> HashSet<
 /// Collect the column names that carry a single-column `Index` constraint.
 ///
 /// Lookup-only, ordering unused.
-pub(crate) fn single_column_indexes(constraints: &[TableConstraint]) -> HashSet {
+pub(crate) fn single_column_indexes(constraints: &[TableConstraint]) -> HashSet<&str> {
     single_column_scan(constraints, |c| match c {
         TableConstraint::Index { columns, .. } => Some(columns.as_slice()),
         _ => None,
@@ -72,7 +72,7 @@ pub(crate) fn single_column_indexes(constraints: &[TableConstraint]) -> HashSet<
 /// Lookup-only, ordering unused.
 pub(crate) fn single_column_fk_targets(
     constraints: &[TableConstraint],
-) -> HashMap {
+) -> HashMap<&str, (&str, &str)> {
     let mut map = HashMap::new();
     for constraint in constraints {
         if let TableConstraint::ForeignKey {
@@ -85,8 +85,8 @@ pub(crate) fn single_column_fk_targets(
             && ref_columns.len() == 1
         {
             map.insert(
-                columns[0].to_string(),
-                (ref_table.to_string(), ref_columns[0].to_string()),
+                columns[0].as_str(),
+                (ref_table.as_str(), ref_columns[0].as_str()),
             );
         }
     }
diff --git a/crates/vespertide-exporter/src/seaorm/relations/reverse.rs b/crates/vespertide-exporter/src/seaorm/relations/reverse.rs
index 979b01a0..a77192af 100644
--- a/crates/vespertide-exporter/src/seaorm/relations/reverse.rs
+++ b/crates/vespertide-exporter/src/seaorm/relations/reverse.rs
@@ -78,7 +78,7 @@ pub(super) fn collect_reverse_relation_targets(
 fn collect_many_to_many_targets(
     current_table: &TableDef,
     junction_table: &TableDef,
-    junction_pk: &HashSet,
+    junction_pk: &HashSet<&str>,
     schema: &[TableDef],
 ) -> Option> {
     if junction_pk.len() < 2 {
@@ -357,7 +357,7 @@ fn reverse_relation_field_defs_inner(ctx: ReverseRelationFieldCtx<'_>) -> Vec,
+    junction_pk: &HashSet<&str>,
     schema: &[TableDef],
 ) -> Option> {
     // Junction table must have composite PK (2+ columns)
diff --git a/crates/vespertide-exporter/src/seaorm/relations/self_ref.rs b/crates/vespertide-exporter/src/seaorm/relations/self_ref.rs
index 3e8cb51e..411e7c80 100644
--- a/crates/vespertide-exporter/src/seaorm/relations/self_ref.rs
+++ b/crates/vespertide-exporter/src/seaorm/relations/self_ref.rs
@@ -24,7 +24,7 @@ pub(super) struct SelfRefJunction {
 pub(super) fn collect_self_ref_junction(
     current_table: &TableDef,
     junction_table: &TableDef,
-    junction_pk: &HashSet,
+    junction_pk: &HashSet<&str>,
 ) -> Option {
     if junction_pk.len() < 2 {
         return None;
diff --git a/crates/vespertide-exporter/src/seaorm/render.rs b/crates/vespertide-exporter/src/seaorm/render.rs
index fa6978f4..5d12abdc 100644
--- a/crates/vespertide-exporter/src/seaorm/render.rs
+++ b/crates/vespertide-exporter/src/seaorm/render.rs
@@ -144,10 +144,10 @@ pub fn render_entity_with_config_and_paths(
 pub(super) fn render_column(
     lines: &mut Vec,
     column: &ColumnDef,
-    primary_keys: &HashSet,
+    primary_keys: &HashSet<&str>,
     composite_pk: bool,
-    unique_columns: &HashSet,
-    indexed_columns: &HashSet,
+    unique_columns: &HashSet<&str>,
+    indexed_columns: &HashSet<&str>,
 ) {
     let is_pk = primary_keys.contains(column.name.as_str());
     let is_unique = unique_columns.contains(column.name.as_str());
@@ -224,7 +224,7 @@ pub(super) fn render_column(
 
     lines.push(format!("    pub {field_name}: {ty},"));
 }
-pub(super) fn primary_key_columns(table: &TableDef) -> HashSet {
+pub(super) fn primary_key_columns(table: &TableDef) -> HashSet<&str> {
     use vespertide_core::schema::primary_key::PrimaryKeySyntax;
 
     // Table-level constraints via the shared scan (single source of truth).
@@ -236,7 +236,7 @@ pub(super) fn primary_key_columns(table: &TableDef) -> HashSet {
         if let Some(PrimaryKeySyntax::Bool(true) | PrimaryKeySyntax::Object(_)) =
             &column.primary_key
         {
-            keys.insert(column.name.to_string());
+            keys.insert(column.name.as_str());
         }
     }
 
diff --git a/crates/vespertide-exporter/src/sqlalchemy/render.rs b/crates/vespertide-exporter/src/sqlalchemy/render.rs
index a8f5d979..0d1fe9b8 100644
--- a/crates/vespertide-exporter/src/sqlalchemy/render.rs
+++ b/crates/vespertide-exporter/src/sqlalchemy/render.rs
@@ -269,7 +269,7 @@ fn render_column(
     col: &ColumnDef,
     is_pk: bool,
     is_unique: bool,
-    fk_info: Option<&(String, String)>,
+    fk_info: Option<&(&str, &str)>,
 ) {
     // Add column comment
     if let Some(ref comment) = col.comment {
diff --git a/crates/vespertide-exporter/src/sqlmodel/render.rs b/crates/vespertide-exporter/src/sqlmodel/render.rs
index 7c153d33..36f1ced6 100644
--- a/crates/vespertide-exporter/src/sqlmodel/render.rs
+++ b/crates/vespertide-exporter/src/sqlmodel/render.rs
@@ -361,7 +361,7 @@ pub(super) fn render_column(
     is_pk: bool,
     is_unique: bool,
     is_indexed: bool,
-    fk_info: Option<&(String, String)>,
+    fk_info: Option<&(&str, &str)>,
 ) {
     // Add column comment
     if let Some(ref comment) = col.comment {

From 599e9aa434e02a66601048ca070957fe4b2656d5 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 13:26:01 +0900
Subject: [PATCH 077/109] =?UTF-8?q?retry-now#0076:=20batch=20=E2=80=94=20i?=
 =?UTF-8?q?n-place=20DeleteTable=20sort=20(no=20clone-Vec),=20inline=20sin?=
 =?UTF-8?q?gle-caller=20comment=20wrapper=20(2=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../vespertide-planner/src/diff/ordering.rs   | 44 ++++++++++++-------
 crates/vespertide-query/src/sql/mod.rs        | 20 +--------
 2 files changed, 30 insertions(+), 34 deletions(-)

diff --git a/crates/vespertide-planner/src/diff/ordering.rs b/crates/vespertide-planner/src/diff/ordering.rs
index 343020ce..95a02325 100644
--- a/crates/vespertide-planner/src/diff/ordering.rs
+++ b/crates/vespertide-planner/src/diff/ordering.rs
@@ -182,24 +182,36 @@ pub(super) fn sort_delete_tables(
         .map(|(idx, &name)| (name, idx))
         .collect();
 
-    // Reorder the DeleteTable actions according to sorted order
-    let mut delete_actions: Vec =
-        delete_indices.iter().map(|&i| actions[i].clone()).collect();
-
-    delete_actions.sort_by(|a, b| {
-        let a_name = extract_delete_table_name(a);
-        let b_name = extract_delete_table_name(b);
-
-        let a_pos = sorted_positions.get(a_name).copied().unwrap_or(0);
-        let b_pos = sorted_positions.get(b_name).copied().unwrap_or(0);
-        a_pos.cmp(&b_pos)
+    // Reorder the DeleteTable actions among their existing slots according to
+    // `sorted_positions`, WITHOUT cloning each action. We reorder the *slot
+    // indices* (cheap `usize` copies) by each slot's table position in
+    // `sorted_positions`, then apply that permutation to the `DeleteTable`
+    // entries in place with `slice::swap` — moving each action's payload rather
+    // than deep-cloning it.
+    //
+    // `delete_indices` is ascending (it comes from `.enumerate()`). `order[k]`
+    // holds the ORIGINAL slot whose action belongs at the k-th delete slot; the
+    // stable sort keeps equal-position actions in their original relative order,
+    // matching the previous stable `sort_by` byte-for-byte.
+    let mut order: Vec = delete_indices.clone();
+    order.sort_by_key(|&i| {
+        let name = extract_delete_table_name(&actions[i]);
+        sorted_positions.get(name).copied().unwrap_or(0)
     });
 
-    // Put them back (move-back instead of clone-back — `delete_actions` is owned
-    // and unused afterwards, so each `MigrationAction` is moved into place
-    // without an extra deep clone of its enum payload).
-    for (idx, action) in delete_indices.iter().zip(delete_actions) {
-        actions[*idx] = action;
+    // Apply the permutation `order` onto the delete slots via selection-style
+    // swaps: this writes each action into its destination with owned moves and
+    // no clone. `where_now[rank]` tracks which original slot currently occupies
+    // the rank-th delete slot so swaps stay consistent as we go.
+    let mut where_now: Vec = delete_indices.clone();
+    for dst_rank in 0..delete_indices.len() {
+        let want_slot = order[dst_rank];
+        // Current rank holding the wanted action.
+        let src_rank = where_now.iter().position(|&s| s == want_slot).unwrap();
+        if src_rank != dst_rank {
+            actions.swap(delete_indices[dst_rank], delete_indices[src_rank]);
+            where_now.swap(dst_rank, src_rank);
+        }
     }
 }
 
diff --git a/crates/vespertide-query/src/sql/mod.rs b/crates/vespertide-query/src/sql/mod.rs
index 181651dc..61fa3876 100644
--- a/crates/vespertide-query/src/sql/mod.rs
+++ b/crates/vespertide-query/src/sql/mod.rs
@@ -155,11 +155,11 @@ pub fn build_action_queries_with_pending(
             table,
             column,
             new_comment,
-        } => build_comment_action_queries(
+        } => build_modify_column_comment(
             backend,
             table,
             column,
-            new_comment.as_ref(),
+            new_comment.as_deref(),
             current_schema,
         ),
 
@@ -183,22 +183,6 @@ pub fn build_action_queries_with_pending(
     }
 }
 
-fn build_comment_action_queries(
-    backend: DatabaseBackend,
-    table: &str,
-    column: &str,
-    new_comment: Option<&String>,
-    current_schema: &[TableDef],
-) -> Result, QueryError> {
-    build_modify_column_comment(
-        backend,
-        table,
-        column,
-        new_comment.map(String::as_str),
-        current_schema,
-    )
-}
-
 fn build_constraint_action_queries(
     backend: DatabaseBackend,
     action: &MigrationAction,

From b8b6902b5b2517455fb1d30bfd97a59ae4b66e05 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 13:37:08 +0900
Subject: [PATCH 078/109] retry-now#0077: simplify WorkspaceIndex::is_empty to
 BTreeMap::is_empty (1 kept)

---
 crates/vespertide-lsp/src/workspace_index.rs | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/crates/vespertide-lsp/src/workspace_index.rs b/crates/vespertide-lsp/src/workspace_index.rs
index 4030476c..9ff6b346 100644
--- a/crates/vespertide-lsp/src/workspace_index.rs
+++ b/crates/vespertide-lsp/src/workspace_index.rs
@@ -127,9 +127,12 @@ impl WorkspaceIndex {
     }
 
     /// `true` if no tables are indexed.
+    ///
+    /// # Panics
+    /// Panics if the internal lock is poisoned.
     #[must_use]
     pub fn is_empty(&self) -> bool {
-        self.len() == 0
+        self.read_inner().by_name.is_empty()
     }
 }
 
@@ -313,6 +316,19 @@ mod tests {
         assert!(idx.is_empty());
     }
 
+    #[test]
+    fn is_empty_tracks_population() {
+        let idx = WorkspaceIndex::new();
+        assert!(idx.is_empty());
+
+        let pool = ParserPool::new();
+        let src = r#"{"name":"user"}"#;
+        let tree = pool.parse(src, DocumentFormat::Json).unwrap();
+        idx.upsert(&uri("user.json"), src, &tree);
+
+        assert!(!idx.is_empty());
+    }
+
     #[test]
     fn tables_returns_sorted_names() {
         let idx = WorkspaceIndex::new();

From 512924e6f9e2b3cce8c495b95634fa38479d6d40 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 13:56:51 +0900
Subject: [PATCH 079/109] =?UTF-8?q?retry-now#0078:=20batch=20=E2=80=94=20d?=
 =?UTF-8?q?rop=20redundant=20y-guard,=20skip=201-col=20sort,=20single-pass?=
 =?UTF-8?q?=20deleted-cols=20(3=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/seaorm/relations/naming.rs            | 23 +++++++++----------
 crates/vespertide-planner/src/diff/columns.rs | 16 ++++++-------
 .../src/diff/constraints.rs                   |  6 ++++-
 3 files changed, 24 insertions(+), 21 deletions(-)

diff --git a/crates/vespertide-exporter/src/seaorm/relations/naming.rs b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
index f818eded..32d36be2 100644
--- a/crates/vespertide-exporter/src/seaorm/relations/naming.rs
+++ b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
@@ -117,19 +117,18 @@ pub(in crate::seaorm) fn pluralize(name: &str) -> String {
     }
 }
 
-/// True when `name` ends in a vowel immediately followed by `y` (e.g. `day`,
-/// `key`, `boy`, `guy`). Answers the `y`→`ies` guard with a single lookup of
-/// the character before the trailing `y` instead of four separate 2-byte
-/// suffix scans. The vowel set is exactly `{a, e, o, u}` (not `i`), so `iy`
-/// still pluralizes to `ies`; a bare `"y"` (no preceding char) is treated as a
-/// consonant-`y` and pluralizes to `ys`. Byte-identical to the prior chain.
+/// True when the character *before* the trailing `y` is a vowel (e.g. `day`,
+/// `key`, `boy`, `guy`). The sole caller (`pluralize`) already gates this on
+/// `name.ends_with('y')`, so this helper owns only the "preceding char is a
+/// vowel" test — a single lookup of the character before the trailing `y`. The
+/// vowel set is exactly `{a, e, o, u}` (not `i`), so `iy` still pluralizes to
+/// `ies`; a bare `"y"` (no preceding char) is treated as a consonant-`y` and
+/// pluralizes to `ys`. Byte-identical to the prior chain.
 fn ends_with_vowel_y(name: &str) -> bool {
-    name.ends_with('y')
-        && name
-            .chars()
-            .rev()
-            .nth(1)
-            .is_some_and(|c| matches!(c, 'a' | 'e' | 'o' | 'u'))
+    name.chars()
+        .rev()
+        .nth(1)
+        .is_some_and(|c| matches!(c, 'a' | 'e' | 'o' | 'u'))
 }
 
 pub(super) fn fk_attr_value>(cols: &[T]) -> String {
diff --git a/crates/vespertide-planner/src/diff/columns.rs b/crates/vespertide-planner/src/diff/columns.rs
index 43c014ab..eaefba72 100644
--- a/crates/vespertide-planner/src/diff/columns.rs
+++ b/crates/vespertide-planner/src/diff/columns.rs
@@ -108,17 +108,17 @@ fn diff_deleted_columns(
     from_cols: &BTreeMap<&str, &ColumnDef>,
     to_cols: &BTreeMap<&str, &ColumnDef>,
 ) -> BTreeSet {
-    let deleted_columns: BTreeSet = from_cols
-        .keys()
-        .filter(|col| !to_cols.contains_key(*col))
-        .map(|col| (*col).to_string())
-        .collect();
-
-    for col in &deleted_columns {
+    // `from_cols` is a `BTreeMap`, so its keys are already in sorted order —
+    // iterating them once yields the same order a `BTreeSet` would, letting us
+    // emit each `DeleteColumn` action and populate the returned set in a single
+    // pass without the extra `col.clone().into()` per deleted column.
+    let mut deleted_columns = BTreeSet::new();
+    for col in from_cols.keys().filter(|col| !to_cols.contains_key(*col)) {
         actions.push(MigrationAction::DeleteColumn {
             table: table_name.into(),
-            column: col.clone().into(),
+            column: (*col).into(),
         });
+        deleted_columns.insert((*col).to_string());
     }
 
     deleted_columns
diff --git a/crates/vespertide-planner/src/diff/constraints.rs b/crates/vespertide-planner/src/diff/constraints.rs
index e4a0410a..9505d2bc 100644
--- a/crates/vespertide-planner/src/diff/constraints.rs
+++ b/crates/vespertide-planner/src/diff/constraints.rs
@@ -23,7 +23,11 @@ enum ConstraintIdentityKey<'a> {
 
 fn sorted_column_refs>(columns: &[T]) -> Vec<&str> {
     let mut columns: Vec<&str> = columns.iter().map(AsRef::as_ref).collect();
-    columns.sort_unstable();
+    // A 0- or 1-element slice is already sorted; skip the sort call for the
+    // overwhelmingly common single-column PK/FK/Unique/Index case.
+    if columns.len() > 1 {
+        columns.sort_unstable();
+    }
     columns
 }
 

From 5fb7c1b78540abbc39bc194b7d70f62925f7febe Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 14:06:09 +0900
Subject: [PATCH 080/109] retry-now#0079: return borrowed &str from LSP
 reference-search sibling_value/outer_table_name (1 kept)

---
 crates/vespertide-lsp/src/references/search.rs | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/crates/vespertide-lsp/src/references/search.rs b/crates/vespertide-lsp/src/references/search.rs
index 3efa944f..ea01c981 100644
--- a/crates/vespertide-lsp/src/references/search.rs
+++ b/crates/vespertide-lsp/src/references/search.rs
@@ -219,7 +219,11 @@ fn check_owning_table_matches(
 }
 
 /// Look up a sibling pair's scalar value within the same constraint object.
-fn sibling_value(source: &[u8], pair: tree_sitter::Node<'_>, target_key: &str) -> Option {
+fn sibling_value<'a>(
+    source: &'a [u8],
+    pair: tree_sitter::Node<'_>,
+    target_key: &str,
+) -> Option<&'a str> {
     let object_raw = pair.parent()?;
     let object = match object_raw.kind() {
         "flow_node" | "block_node" => object_raw.named_child(0)?,
@@ -242,14 +246,14 @@ fn sibling_value(source: &[u8], pair: tree_sitter::Node<'_>, target_key: &str) -
             let text = source
                 .get(actual.byte_range())
                 .and_then(|bytes| std::str::from_utf8(bytes).ok())?;
-            return Some(strip_quotes(text).to_string());
+            return Some(strip_quotes(text));
         }
     }
     None
 }
 
 /// Walk up to the document's outermost mapping and return its `name` value.
-fn outer_table_name(source: &[u8], node: tree_sitter::Node<'_>) -> Option {
+fn outer_table_name<'a>(source: &'a [u8], node: tree_sitter::Node<'_>) -> Option<&'a str> {
     let mut current = node.parent();
     let mut outer = None;
     while let Some(candidate) = current {
@@ -279,7 +283,7 @@ fn outer_table_name(source: &[u8], node: tree_sitter::Node<'_>) -> Option
Date: Fri, 3 Jul 2026 14:28:12 +0900
Subject: [PATCH 081/109] =?UTF-8?q?retry-now#0080:=20batch=20=E2=80=94=20d?=
 =?UTF-8?q?edupe=20LSP=20direct-child=20scans,=20fast-path=20int-enum=20re?=
 =?UTF-8?q?map,=20borrow-set=20CHECK=20dedup,=20O(k)=20delete-table=20perm?=
 =?UTF-8?q?ute=20(4=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../vespertide-lsp/src/references/search.rs   | 98 +++++++------------
 crates/vespertide-planner/src/diff/columns.rs |  5 +
 .../vespertide-planner/src/diff/ordering.rs   | 33 +++++--
 crates/vespertide-query/src/sql/helpers.rs    | 29 ++++--
 4 files changed, 84 insertions(+), 81 deletions(-)

diff --git a/crates/vespertide-lsp/src/references/search.rs b/crates/vespertide-lsp/src/references/search.rs
index ea01c981..427ceccb 100644
--- a/crates/vespertide-lsp/src/references/search.rs
+++ b/crates/vespertide-lsp/src/references/search.rs
@@ -218,25 +218,27 @@ fn check_owning_table_matches(
     outer_table_name(source, expr_pair).is_some_and(|name| name == expected_table)
 }
 
-/// Look up a sibling pair's scalar value within the same constraint object.
-fn sibling_value<'a>(
+/// Scan a mapping node's DIRECT child pairs (non-recursive) for the pair
+/// whose key equals `key`, then return its value's scalar text with the
+/// `flow_node`/`block_node` wrapper peeled and surrounding quotes stripped.
+///
+/// Shared by `sibling_value`, `outer_table_name`, `sibling_ref_table_matches`,
+/// and `is_column_pair`, which each open-coded this exact walk before. The
+/// crate's `tree_util::find_pair_with_key` is the *recursive* variant; this is
+/// the *direct-child* one.
+fn direct_child_scalar<'a>(
+    object: tree_sitter::Node<'_>,
     source: &'a [u8],
-    pair: tree_sitter::Node<'_>,
-    target_key: &str,
+    key: &str,
 ) -> Option<&'a str> {
-    let object_raw = pair.parent()?;
-    let object = match object_raw.kind() {
-        "flow_node" | "block_node" => object_raw.named_child(0)?,
-        _ => object_raw,
-    };
     let mut cursor = object.walk();
     for child in object.children(&mut cursor) {
         if matches!(child.kind(), "pair" | "block_mapping_pair")
-            && let Some(key) = child.named_child(0)
+            && let Some(key_node) = child.named_child(0)
             && let Some(key_text) = source
-                .get(key.byte_range())
+                .get(key_node.byte_range())
                 .and_then(|bytes| std::str::from_utf8(bytes).ok())
-            && strip_quotes(key_text) == target_key
+            && strip_quotes(key_text) == key
         {
             let value = child.named_child(1)?;
             let actual = match value.kind() {
@@ -252,6 +254,20 @@ fn sibling_value<'a>(
     None
 }
 
+/// Look up a sibling pair's scalar value within the same constraint object.
+fn sibling_value<'a>(
+    source: &'a [u8],
+    pair: tree_sitter::Node<'_>,
+    target_key: &str,
+) -> Option<&'a str> {
+    let object_raw = pair.parent()?;
+    let object = match object_raw.kind() {
+        "flow_node" | "block_node" => object_raw.named_child(0)?,
+        _ => object_raw,
+    };
+    direct_child_scalar(object, source, target_key)
+}
+
 /// Walk up to the document's outermost mapping and return its `name` value.
 fn outer_table_name<'a>(source: &'a [u8], node: tree_sitter::Node<'_>) -> Option<&'a str> {
     let mut current = node.parent();
@@ -266,27 +282,7 @@ fn outer_table_name<'a>(source: &'a [u8], node: tree_sitter::Node<'_>) -> Option
         current = candidate.parent();
     }
     let outer = outer?;
-    let mut cursor = outer.walk();
-    for child in outer.children(&mut cursor) {
-        if matches!(child.kind(), "pair" | "block_mapping_pair")
-            && let Some(key) = child.named_child(0)
-            && let Some(key_text) = source
-                .get(key.byte_range())
-                .and_then(|bytes| std::str::from_utf8(bytes).ok())
-            && strip_quotes(key_text) == "name"
-        {
-            let value = child.named_child(1)?;
-            let actual = match value.kind() {
-                "flow_node" | "block_node" => value.named_child(0).unwrap_or(value),
-                _ => value,
-            };
-            let text = source
-                .get(actual.byte_range())
-                .and_then(|bytes| std::str::from_utf8(bytes).ok())?;
-            return Some(strip_quotes(text));
-        }
-    }
-    None
+    direct_child_scalar(outer, source, "name")
 }
 
 /// Lex the CHECK expression in `value` and push a reference for every bare
@@ -326,23 +322,9 @@ fn sibling_ref_table_matches(
         "flow_node" | "block_node" => raw.named_child(0),
         _ => Some(raw),
     });
-    if let Some(fk_object) = fk_object {
-        let mut cursor = fk_object.walk();
-        for child in fk_object.children(&mut cursor) {
-            if matches!(child.kind(), "pair" | "block_mapping_pair")
-                && let Some(key) = child.named_child(0)
-                && let Some(key_text) = source
-                    .get(key.byte_range())
-                    .and_then(|bytes| std::str::from_utf8(bytes).ok())
-                && strip_quotes(key_text) == "ref_table"
-            {
-                return child
-                    .named_child(1)
-                    .is_some_and(|value| value_matches(source, value, table_name));
-            }
-        }
-    }
-    false
+    fk_object
+        .and_then(|obj| direct_child_scalar(obj, source, "ref_table"))
+        .is_some_and(|ref_table| ref_table == table_name)
 }
 
 fn push_array_matches(
@@ -476,20 +458,8 @@ fn is_column_pair(name_pair: tree_sitter::Node<'_>, source: &[u8], expected_tabl
         if let Some(outer) = outer
             && outer.id() != column_object.id()
         {
-            let mut cursor = outer.walk();
-            for child in outer.children(&mut cursor) {
-                if matches!(child.kind(), "pair" | "block_mapping_pair")
-                    && let Some(key) = child.named_child(0)
-                    && let Some(key_text) = source
-                        .get(key.byte_range())
-                        .and_then(|bytes| std::str::from_utf8(bytes).ok())
-                    && strip_quotes(key_text) == "name"
-                {
-                    return child
-                        .named_child(1)
-                        .is_some_and(|value| value_matches(source, value, expected_table));
-                }
-            }
+            return direct_child_scalar(outer, source, "name")
+                .is_some_and(|name| name == expected_table);
         }
     }
     false
diff --git a/crates/vespertide-planner/src/diff/columns.rs b/crates/vespertide-planner/src/diff/columns.rs
index eaefba72..ea105a84 100644
--- a/crates/vespertide-planner/src/diff/columns.rs
+++ b/crates/vespertide-planner/src/diff/columns.rs
@@ -87,6 +87,11 @@ fn compute_integer_enum_remapping(from: &ColumnType, to: &ColumnType) -> BTreeMa
     else {
         return BTreeMap::new();
     };
+    // Fast path: identical variant lists can never remap — skip the map build.
+    // This is the overwhelmingly common case (unchanged integer enum).
+    if from_items == to_items {
+        return BTreeMap::new();
+    }
     let to_by_name: BTreeMap<&str, i64> = to_items
         .iter()
         .map(|nv| (nv.name.as_str(), nv.value))
diff --git a/crates/vespertide-planner/src/diff/ordering.rs b/crates/vespertide-planner/src/diff/ordering.rs
index 95a02325..60ef8119 100644
--- a/crates/vespertide-planner/src/diff/ordering.rs
+++ b/crates/vespertide-planner/src/diff/ordering.rs
@@ -183,11 +183,7 @@ pub(super) fn sort_delete_tables(
         .collect();
 
     // Reorder the DeleteTable actions among their existing slots according to
-    // `sorted_positions`, WITHOUT cloning each action. We reorder the *slot
-    // indices* (cheap `usize` copies) by each slot's table position in
-    // `sorted_positions`, then apply that permutation to the `DeleteTable`
-    // entries in place with `slice::swap` — moving each action's payload rather
-    // than deep-cloning it.
+    // `sorted_positions`, WITHOUT cloning each action.
     //
     // `delete_indices` is ascending (it comes from `.enumerate()`). `order[k]`
     // holds the ORIGINAL slot whose action belongs at the k-th delete slot; the
@@ -200,17 +196,34 @@ pub(super) fn sort_delete_tables(
     });
 
     // Apply the permutation `order` onto the delete slots via selection-style
-    // swaps: this writes each action into its destination with owned moves and
-    // no clone. `where_now[rank]` tracks which original slot currently occupies
-    // the rank-th delete slot so swaps stay consistent as we go.
+    // swaps, moving each action into its destination with owned moves and no
+    // clone. We keep two mutually-inverse index arrays and update BOTH in O(1)
+    // per swap, so resolving "which rank currently holds the wanted action" is a
+    // direct array read instead of the previous inner `where_now.position(..)`
+    // linear rescan — making the apply O(k) instead of O(k²):
+    //   * `where_now[rank]`  — original slot currently occupying that delete rank
+    //   * `slot_at_rank`     — its inverse, keyed by rank offset within the run
+    // The reordered `DeleteTable` payloads stay byte-identical to before.
+    //
+    // Delete slots are ascending, so `delete_indices[0]` is the lowest slot;
+    // `rank_base` lets us index the inverse array by `slot - rank_base` cheaply.
     let mut where_now: Vec = delete_indices.clone();
+    let rank_base = delete_indices[0];
+    let span = delete_indices[delete_indices.len() - 1] - rank_base + 1;
+    let mut slot_to_rank = vec![usize::MAX; span];
+    for (rank, &slot) in delete_indices.iter().enumerate() {
+        slot_to_rank[slot - rank_base] = rank;
+    }
     for dst_rank in 0..delete_indices.len() {
         let want_slot = order[dst_rank];
-        // Current rank holding the wanted action.
-        let src_rank = where_now.iter().position(|&s| s == want_slot).unwrap();
+        let src_rank = slot_to_rank[want_slot - rank_base];
         if src_rank != dst_rank {
             actions.swap(delete_indices[dst_rank], delete_indices[src_rank]);
+            // Swap the two slots' bookkeeping so both arrays stay consistent.
+            let displaced_slot = where_now[dst_rank];
             where_now.swap(dst_rank, src_rank);
+            slot_to_rank[want_slot - rank_base] = dst_rank;
+            slot_to_rank[displaced_slot - rank_base] = src_rank;
         }
     }
 }
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index 5ff0a1f0..1845ecaa 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -568,17 +568,32 @@ pub(crate) fn collect_all_check_clauses(
     constraints: &[TableConstraint],
 ) -> Vec {
     let mut clauses = collect_sqlite_enum_check_clauses(table, columns);
-    // Track membership in a set so each explicit clause is a single O(log n)
-    // lookup rather than a linear `Vec::contains` rescan (quadratic on tables
-    // with many CHECK constraints). Insertion order is preserved: enum clauses
+    // Enum clauses are already unique among themselves (one per column), so the
+    // membership set only needs to reject explicit clauses that collide with an
+    // enum clause or with an earlier-kept explicit clause. Build the dedup set
+    // from borrowed `&str` views of the enum clauses (no per-enum-clause `String`
+    // clone) plus each kept explicit clause's `&str`, filtering the explicit list
+    // into `retained` BEFORE touching `clauses` so no borrow into `clauses`
+    // overlaps its later mutation. Insertion order is preserved: enum clauses
     // first, then explicit clauses in source order.
-    let mut seen: std::collections::BTreeSet = clauses.iter().cloned().collect();
     let explicit = extract_check_clauses(constraints);
-    for clause in explicit {
-        if seen.insert(clause.clone()) {
-            clauses.push(clause);
+    let mut retained: Vec = Vec::with_capacity(explicit.len());
+    {
+        // `seen` borrows the enum clauses out of `clauses`, so keep it in an
+        // inner scope whose borrows end before `clauses` is mutated below. This
+        // seeds the dedup set from borrowed `&str` — no per-enum-clause `String`
+        // clone (the win) — and interns each kept explicit clause's `&str` so
+        // explicit-vs-explicit dedup stays a single O(log n) lookup. Only the
+        // explicit clauses actually kept are moved into `retained`.
+        let mut seen: std::collections::BTreeSet<&str> =
+            clauses.iter().map(String::as_str).collect();
+        for clause in &explicit {
+            if seen.insert(clause.as_str()) {
+                retained.push(clause.clone());
+            }
         }
     }
+    clauses.extend(retained);
     clauses
 }
 

From 7f0d5a82e37e965cf5c5fc7e1a649d0e21f96816 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 14:40:16 +0900
Subject: [PATCH 082/109] retry-now#0081: exporter dedup - reuse
 constraint_scan helpers in sqlmodel/sqlalchemy render (2 kept)

---
 .../vespertide-exporter/src/sqlalchemy/render.rs | 10 +++++-----
 .../vespertide-exporter/src/sqlmodel/render.rs   | 16 +---------------
 2 files changed, 6 insertions(+), 20 deletions(-)

diff --git a/crates/vespertide-exporter/src/sqlalchemy/render.rs b/crates/vespertide-exporter/src/sqlalchemy/render.rs
index 0d1fe9b8..87a0c273 100644
--- a/crates/vespertide-exporter/src/sqlalchemy/render.rs
+++ b/crates/vespertide-exporter/src/sqlalchemy/render.rs
@@ -73,9 +73,12 @@ fn render_entity_part(table: &TableDef, used_types: &mut UsedTypes<'static>) ->
 
     let composite_fks = collect_composite_fks(table);
 
+    // Collect single-column foreign key targets once; the import flag below and
+    // the per-column render lookups both read from this single scan.
+    let fk_info = crate::constraint_scan::single_column_fk_targets(&table.constraints);
+
     // Check for single-column foreign keys
-    let has_single_fk = table.constraints.iter().any(|c| matches!(c, TableConstraint::ForeignKey { columns, ref_columns, .. } if columns.len() == 1 && ref_columns.len() == 1));
-    if has_single_fk {
+    if !fk_info.is_empty() {
         used_types.sa_types.insert("ForeignKey");
     }
 
@@ -136,9 +139,6 @@ fn render_entity_part(table: &TableDef, used_types: &mut UsedTypes<'static>) ->
     // Collect unique columns (single-column unique constraints); lookup-only, ordering unused.
     let unique_columns = crate::constraint_scan::single_column_uniques(&table.constraints);
 
-    // Collect foreign key info; lookup-only, ordering unused.
-    let fk_info = crate::constraint_scan::single_column_fk_targets(&table.constraints);
-
     // Render columns
     for col in &table.columns {
         render_column(
diff --git a/crates/vespertide-exporter/src/sqlmodel/render.rs b/crates/vespertide-exporter/src/sqlmodel/render.rs
index 36f1ced6..731bcedf 100644
--- a/crates/vespertide-exporter/src/sqlmodel/render.rs
+++ b/crates/vespertide-exporter/src/sqlmodel/render.rs
@@ -251,21 +251,7 @@ fn render_entity_body(table: &TableDef, composite_fks: &[CompositeFk<'_>]) -> Ve
     let unique_columns = crate::constraint_scan::single_column_uniques(&table.constraints);
 
     // Collect indexed columns (single-column indexes); lookup-only, ordering unused.
-    let indexed_columns: std::collections::HashSet = table
-        .constraints
-        .iter()
-        .filter_map(|c| {
-            if let TableConstraint::Index { columns, .. } = c {
-                if columns.len() == 1 {
-                    Some(columns[0].to_string())
-                } else {
-                    None
-                }
-            } else {
-                None
-            }
-        })
-        .collect();
+    let indexed_columns = crate::constraint_scan::single_column_indexes(&table.constraints);
 
     // Collect foreign key info; lookup-only, ordering unused.
     let fk_info = crate::constraint_scan::single_column_fk_targets(&table.constraints);

From 842031e59763fe4800aae70ae96f2e6e50967c86 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 14:56:58 +0900
Subject: [PATCH 083/109] =?UTF-8?q?retry-now#0082:=20batch=20=E2=80=94=20r?=
 =?UTF-8?q?euse=20buffers=20in=20seaorm=20naming=20+=20borrow=20ERD=20BFS?=
 =?UTF-8?q?=20frontier=20(4=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 crates/vespertide-cli/src/commands/erd/mod.rs      | 14 ++++++++++----
 crates/vespertide-exporter/src/seaorm/imports.rs   | 13 +++++++++++--
 .../src/seaorm/relations/naming.rs                 | 11 ++++++++++-
 3 files changed, 31 insertions(+), 7 deletions(-)

diff --git a/crates/vespertide-cli/src/commands/erd/mod.rs b/crates/vespertide-cli/src/commands/erd/mod.rs
index bcf6eec9..af734fe1 100644
--- a/crates/vespertide-cli/src/commands/erd/mod.rs
+++ b/crates/vespertide-cli/src/commands/erd/mod.rs
@@ -110,17 +110,23 @@ pub(super) fn filter_tables_with_warnings(
     // Layered BFS: each round expands only the names discovered in the
     // previous round instead of re-scanning (and re-cloning) all of `kept`,
     // so every node's adjacency is walked at most once regardless of depth.
-    let mut frontier: Vec = kept.iter().cloned().collect();
+    // The discovered-neighbor frontier borrows `&str` into the owned `String`s
+    // that live in `adjacency`, so only the `kept` set membership needs an
+    // owned clone (the frontier itself no longer clones every neighbor name).
+    // The seed must be a snapshot detached from `kept` so the frontier borrow
+    // does not collide with the `kept.insert` below; `seed_names` owns it.
+    let seed_names: Vec = kept.iter().cloned().collect();
+    let mut frontier: Vec<&str> = seed_names.iter().map(String::as_str).collect();
     for _ in 0..depth {
         if frontier.is_empty() {
             break;
         }
-        let mut next = Vec::new();
+        let mut next: Vec<&str> = Vec::new();
         for name in frontier {
-            if let Some(neighbors) = adjacency.get(&name) {
+            if let Some(neighbors) = adjacency.get(name) {
                 for neighbor in neighbors {
                     if all_names.contains(neighbor) && kept.insert(neighbor.clone()) {
-                        next.push(neighbor.clone());
+                        next.push(neighbor.as_str());
                     }
                 }
             }
diff --git a/crates/vespertide-exporter/src/seaorm/imports.rs b/crates/vespertide-exporter/src/seaorm/imports.rs
index c4e0689d..70ce0e9b 100644
--- a/crates/vespertide-exporter/src/seaorm/imports.rs
+++ b/crates/vespertide-exporter/src/seaorm/imports.rs
@@ -7,7 +7,12 @@ use std::collections::{HashMap, HashSet};
 ///
 /// Returns a path like `crate::models::admin::admin`.
 pub(super) fn absolute_module_path(crate_prefix: &str, to_module: &[String]) -> String {
-    let mut path = crate_prefix.to_string();
+    // Pre-size to the exact final length (`crate_prefix` + `"::" + seg` per
+    // segment) so multi-segment paths allocate once instead of climbing the
+    // doubling ladder. Output byte-identical.
+    let cap = crate_prefix.len() + to_module.iter().map(|s| s.len() + 2).sum::();
+    let mut path = String::with_capacity(cap);
+    path.push_str(crate_prefix);
     for seg in to_module {
         path.push_str("::");
         path.push_str(seg);
@@ -83,7 +88,11 @@ pub(super) fn sanitize_field_name(name: &str) -> String {
     if result.is_empty() {
         "_col".into()
     } else if RUST_KEYWORDS.contains(&result.as_str()) {
-        format!("r#{result}")
+        // Reuse the already-allocated `result` buffer instead of allocating a
+        // second `String` via `format!`. The pre-sized capacity (`name.len() + 1`)
+        // usually holds the 2-byte `"r#"` prefix with a single memmove and no realloc.
+        result.insert_str(0, "r#");
+        result
     } else {
         result
     }
diff --git a/crates/vespertide-exporter/src/seaorm/relations/naming.rs b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
index 32d36be2..61d2181b 100644
--- a/crates/vespertide-exporter/src/seaorm/relations/naming.rs
+++ b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
@@ -111,7 +111,16 @@ pub(in crate::seaorm) fn pluralize(name: &str) -> String {
     if name.ends_with('s') {
         name.to_string()
     } else if name.ends_with('y') && !ends_with_vowel_y(name) {
-        format!("{}ies", name.strip_suffix('y').unwrap_or(name))
+        // Build `ies` directly into one exact-size buffer instead of
+        // routing through the `format!` formatter machinery (mirrors the
+        // buffer-push idiom used by `fk_attr_value` / `quote_idents`). The
+        // `strip_suffix('y')` is guarded by `ends_with('y')` above, so
+        // `unwrap_or(name)` is dead-defensive but harmless. Byte-identical.
+        let stem = name.strip_suffix('y').unwrap_or(name);
+        let mut out = String::with_capacity(stem.len() + 3);
+        out.push_str(stem);
+        out.push_str("ies");
+        out
     } else {
         format!("{name}s")
     }

From 9df4d957d47694454926842b32bcda8abdb1e86c Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 15:10:48 +0900
Subject: [PATCH 084/109] =?UTF-8?q?retry-now#0083:=20batch=20=E2=80=94=20s?=
 =?UTF-8?q?implify=20generate=5Frelation=5Fenum=5Fname=20to=20strip=5Fsuff?=
 =?UTF-8?q?ix().unwrap=5For()=20(1=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 crates/vespertide-exporter/src/seaorm/relations/naming.rs | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/crates/vespertide-exporter/src/seaorm/relations/naming.rs b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
index 61d2181b..dd4c2b28 100644
--- a/crates/vespertide-exporter/src/seaorm/relations/naming.rs
+++ b/crates/vespertide-exporter/src/seaorm/relations/naming.rs
@@ -10,11 +10,7 @@ use super::super::imports::{sanitize_field_name, to_pascal_case};
 pub(in crate::seaorm) fn generate_relation_enum_name>(columns: &[T]) -> String {
     // Take the first column and remove common FK suffixes like "_id"
     let first_col = columns[0].as_ref();
-    let without_id = if let Some(stripped) = first_col.strip_suffix("_id") {
-        stripped
-    } else {
-        first_col
-    };
+    let without_id = first_col.strip_suffix("_id").unwrap_or(first_col);
 
     to_pascal_case(without_id)
 }

From 1df6e2b685346a194df7dcf9dbf98e4943353869 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 15:22:28 +0900
Subject: [PATCH 085/109] =?UTF-8?q?retry-now#0084:=20batch=20=E2=80=94=20b?=
 =?UTF-8?q?orrow=20constraint=20index/unique=20in=20SQLAlchemy/SQLModel/JP?=
 =?UTF-8?q?A=20render=20+=20drop=20ERD=20PK=20clone=20(4=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 crates/vespertide-cli/src/commands/erd/mod.rs       | 4 ++--
 crates/vespertide-exporter/src/jpa/render.rs        | 8 ++++----
 crates/vespertide-exporter/src/sqlalchemy/render.rs | 8 ++++----
 crates/vespertide-exporter/src/sqlmodel/render.rs   | 8 ++++----
 4 files changed, 14 insertions(+), 14 deletions(-)

diff --git a/crates/vespertide-cli/src/commands/erd/mod.rs b/crates/vespertide-cli/src/commands/erd/mod.rs
index af734fe1..7c09ea87 100644
--- a/crates/vespertide-cli/src/commands/erd/mod.rs
+++ b/crates/vespertide-cli/src/commands/erd/mod.rs
@@ -438,12 +438,12 @@ fn are_columns_unique(table: &TableDef, columns: &[String]) -> bool {
 fn primary_key_columns(table: &TableDef) -> Vec {
     if let Some(columns) = table.constraints.iter().find_map(|constraint| {
         if let TableConstraint::PrimaryKey { columns, .. } = constraint {
-            Some(columns.clone())
+            Some(columns.as_slice())
         } else {
             None
         }
     }) {
-        return names_to_strings(&columns);
+        return names_to_strings(columns);
     }
 
     table
diff --git a/crates/vespertide-exporter/src/jpa/render.rs b/crates/vespertide-exporter/src/jpa/render.rs
index 780c66aa..82c6beb2 100644
--- a/crates/vespertide-exporter/src/jpa/render.rs
+++ b/crates/vespertide-exporter/src/jpa/render.rs
@@ -163,7 +163,7 @@ fn render_table_annotation(
         .iter()
         .filter_map(|c| {
             if let TableConstraint::Index { name, columns } = c {
-                Some((name.clone(), columns.clone()))
+                Some((name, columns))
             } else {
                 None
             }
@@ -175,7 +175,7 @@ fn render_table_annotation(
         .filter_map(|c| {
             if let TableConstraint::Unique { name, columns, .. } = c {
                 if columns.len() > 1 {
-                    Some((name.clone(), columns.clone()))
+                    Some((name, columns))
                 } else {
                     None
                 }
@@ -194,7 +194,7 @@ fn render_table_annotation(
 
     if !indexes.is_empty() {
         annotation.push_str(", indexes = {\n");
-        for (i, (name, columns)) in indexes.iter().enumerate() {
+        for (i, &(name, columns)) in indexes.iter().enumerate() {
             let col_list = columns.join(", ");
             let comma = if i < indexes.len() - 1 { "," } else { "" };
             if let Some(idx_name) = name {
@@ -211,7 +211,7 @@ fn render_table_annotation(
 
     if !unique_constraints.is_empty() {
         annotation.push_str(", uniqueConstraints = {\n");
-        for (i, (name, columns)) in unique_constraints.iter().enumerate() {
+        for (i, &(name, columns)) in unique_constraints.iter().enumerate() {
             let cols = crate::utils::common::join_quoted(columns);
             let comma = if i < unique_constraints.len() - 1 {
                 ","
diff --git a/crates/vespertide-exporter/src/sqlalchemy/render.rs b/crates/vespertide-exporter/src/sqlalchemy/render.rs
index 87a0c273..97d43ac1 100644
--- a/crates/vespertide-exporter/src/sqlalchemy/render.rs
+++ b/crates/vespertide-exporter/src/sqlalchemy/render.rs
@@ -156,7 +156,7 @@ fn render_entity_part(table: &TableDef, used_types: &mut UsedTypes<'static>) ->
         .iter()
         .filter_map(|c| {
             if let TableConstraint::Index { name, columns } = c {
-                Some((name.clone(), columns.clone()))
+                Some((name, columns))
             } else {
                 None
             }
@@ -170,7 +170,7 @@ fn render_entity_part(table: &TableDef, used_types: &mut UsedTypes<'static>) ->
         .filter_map(|c| {
             if let TableConstraint::Unique { name, columns, .. } = c {
                 if columns.len() > 1 {
-                    Some((name.clone(), columns.clone()))
+                    Some((name, columns))
                 } else {
                     None
                 }
@@ -184,7 +184,7 @@ fn render_entity_part(table: &TableDef, used_types: &mut UsedTypes<'static>) ->
         lines.push(String::new());
         lines.push("    __table_args__ = (".into());
 
-        for (name, columns) in &indexes {
+        for &(name, columns) in &indexes {
             let cols_str = join_quoted(columns);
             if let Some(idx_name) = name {
                 lines.push(format!("        Index(\"{idx_name}\", {cols_str}),"));
@@ -193,7 +193,7 @@ fn render_entity_part(table: &TableDef, used_types: &mut UsedTypes<'static>) ->
             }
         }
 
-        for (name, columns) in &composite_uniques {
+        for &(name, columns) in &composite_uniques {
             let cols_str = join_quoted(columns);
             if let Some(uq_name) = name {
                 lines.push(format!(
diff --git a/crates/vespertide-exporter/src/sqlmodel/render.rs b/crates/vespertide-exporter/src/sqlmodel/render.rs
index 731bcedf..100b4870 100644
--- a/crates/vespertide-exporter/src/sqlmodel/render.rs
+++ b/crates/vespertide-exporter/src/sqlmodel/render.rs
@@ -275,7 +275,7 @@ fn render_entity_body(table: &TableDef, composite_fks: &[CompositeFk<'_>]) -> Ve
         .filter_map(|c| {
             if let TableConstraint::Index { name, columns } = c {
                 if columns.len() > 1 {
-                    Some((name.clone(), columns.clone()))
+                    Some((name, columns))
                 } else {
                     None
                 }
@@ -291,7 +291,7 @@ fn render_entity_body(table: &TableDef, composite_fks: &[CompositeFk<'_>]) -> Ve
         .filter_map(|c| {
             if let TableConstraint::Unique { name, columns, .. } = c {
                 if columns.len() > 1 {
-                    Some((name.clone(), columns.clone()))
+                    Some((name, columns))
                 } else {
                     None
                 }
@@ -305,7 +305,7 @@ fn render_entity_body(table: &TableDef, composite_fks: &[CompositeFk<'_>]) -> Ve
         lines.push(String::new());
         lines.push("    __table_args__ = (".into());
 
-        for (name, columns) in &composite_indexes {
+        for &(name, columns) in &composite_indexes {
             let cols_str = join_quoted(columns);
             if let Some(idx_name) = name {
                 lines.push(format!("        Index(\"{idx_name}\", {cols_str}),"));
@@ -314,7 +314,7 @@ fn render_entity_body(table: &TableDef, composite_fks: &[CompositeFk<'_>]) -> Ve
             }
         }
 
-        for (name, columns) in &composite_uniques {
+        for &(name, columns) in &composite_uniques {
             let cols_str = join_quoted(columns);
             if let Some(uq_name) = name {
                 lines.push(format!(

From 0b1d193a42afbfc71d4d79c9b2623e9d643e2d44 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 15:42:06 +0900
Subject: [PATCH 086/109] retry-now#0086: remove redundant TableCreateStatement
 clones in CREATE TABLE assembly (1 kept)

---
 crates/vespertide-query/src/sql/create_table.rs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/crates/vespertide-query/src/sql/create_table.rs b/crates/vespertide-query/src/sql/create_table.rs
index 22ba0940..3b412239 100644
--- a/crates/vespertide-query/src/sql/create_table.rs
+++ b/crates/vespertide-query/src/sql/create_table.rs
@@ -61,7 +61,7 @@ fn add_create_table_columns(
             }
             col.auto_increment();
         }
-        *stmt = stmt.col(col).to_owned();
+        stmt.col(col);
     }
 }
 
@@ -133,7 +133,7 @@ fn add_primary_key_constraint(
     for c in pk_cols {
         pk_idx.col(Alias::new(c.as_ref()));
     }
-    *stmt = stmt.primary_key(&mut pk_idx).to_owned();
+    stmt.primary_key(&mut pk_idx);
 }
 
 fn should_skip_sqlite_auto_increment_pk(
@@ -167,7 +167,7 @@ fn add_mysql_unique_constraint(
     for col in unique_cols {
         idx.col(Alias::new(col.as_ref()));
     }
-    *stmt = stmt.index(&mut idx).to_owned();
+    stmt.index(&mut idx);
 }
 
 fn add_foreign_key_constraint(
@@ -201,7 +201,7 @@ fn add_foreign_key_constraint(
         if let Some(action) = on_update {
             fk.on_update(to_sea_fk_action(action));
         }
-        *stmt = stmt.foreign_key(&mut fk).to_owned();
+        stmt.foreign_key(&mut fk);
     }
 }
 

From a9f6d3376063a3b9ccd8734057d7d305967f2d6e Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 16:09:40 +0900
Subject: [PATCH 087/109] =?UTF-8?q?retry-now#0087:=20batch=20=E2=80=94=20p?=
 =?UTF-8?q?re-size=20exporter/core=20join=20buffers,=20drop=20throwaway=20?=
 =?UTF-8?q?Vecs=20(4=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 crates/vespertide-core/src/schema/names.rs    |  8 ++-
 .../vespertide-exporter/src/seaorm/render.rs  | 68 +++++++++----------
 .../vespertide-exporter/src/utils/common.rs   | 19 +++++-
 3 files changed, 55 insertions(+), 40 deletions(-)

diff --git a/crates/vespertide-core/src/schema/names.rs b/crates/vespertide-core/src/schema/names.rs
index 2d5bfda3..708c14eb 100644
--- a/crates/vespertide-core/src/schema/names.rs
+++ b/crates/vespertide-core/src/schema/names.rs
@@ -263,7 +263,13 @@ pub fn names_to_strings(names: &[T]) -> Vec {
 /// ```
 #[must_use]
 pub fn join_column_names(columns: &[ColumnName], separator: &str) -> String {
-    let mut out = String::new();
+    // Pre-size exactly: sum of column-name lengths plus one separator between
+    // each adjacent pair. Finishes matching the buffer-push pattern this
+    // helper's doc-comment claims to follow — `quote_ident` already pre-sizes
+    // via `out.reserve(name.len() + 2)`. Output stays byte-identical.
+    let cap = columns.iter().map(|c| c.as_str().len()).sum::()
+        + separator.len() * columns.len().saturating_sub(1);
+    let mut out = String::with_capacity(cap);
     for (i, c) in columns.iter().enumerate() {
         if i > 0 {
             out.push_str(separator);
diff --git a/crates/vespertide-exporter/src/seaorm/render.rs b/crates/vespertide-exporter/src/seaorm/render.rs
index 5d12abdc..4e3f0cbb 100644
--- a/crates/vespertide-exporter/src/seaorm/render.rs
+++ b/crates/vespertide-exporter/src/seaorm/render.rs
@@ -161,41 +161,43 @@ pub(super) fn render_column(
         }
     }
 
-    // Build attribute parts
-    let mut attrs: Vec = Vec::new();
+    // Build attribute parts in a single buffer via the shared `push_attr`
+    // helper (comma-separated, no intermediate `Vec`). Output is
+    // byte-identical to the previous `Vec` + `.join(", ")`.
+    let mut attrs = String::new();
 
     if is_pk {
-        attrs.push("primary_key".into());
+        crate::utils::common::push_attr(&mut attrs, "primary_key");
         // Only show auto_increment = false for integer types that support auto_increment
         if composite_pk && column.r#type.supports_auto_increment() {
-            attrs.push("auto_increment = false".into());
+            crate::utils::common::push_attr(&mut attrs, "auto_increment = false");
         }
     }
 
     if is_unique && !is_pk {
         // unique is redundant if it's already a primary key
-        attrs.push("unique".into());
+        crate::utils::common::push_attr(&mut attrs, "unique");
     }
 
     if is_indexed && !is_pk && !is_unique {
         // indexed is redundant if it's already a primary key or unique
-        attrs.push("indexed".into());
+        crate::utils::common::push_attr(&mut attrs, "indexed");
     }
 
     if has_default && let Some(ref default_val) = column.default {
         // Format the default value for SeaORM
         let formatted = format_default_value(default_val, &column.r#type);
-        attrs.push(formatted);
+        crate::utils::common::push_attr(&mut attrs, &formatted);
     }
 
     // For custom types, add column_type attribute with the custom type value
     if let ColumnType::Complex(ComplexColumnType::Custom { custom_type }) = &column.r#type {
-        attrs.push(format!("column_type = \"{custom_type}\""));
+        crate::utils::common::push_attr(&mut attrs, &format!("column_type = \"{custom_type}\""));
     }
 
     // Output attribute if any
     if !attrs.is_empty() {
-        lines.push(format!("    #[sea_orm({})]", attrs.join(", ")));
+        lines.push(format!("    #[sea_orm({attrs})]"));
     }
 
     let field_name = sanitize_field_name(&column.name);
@@ -243,53 +245,45 @@ pub(super) fn primary_key_columns(table: &TableDef) -> HashSet<&str> {
     keys
 }
 pub(super) fn render_indexes_and_uniques(lines: &mut Vec, constraints: &[TableConstraint]) {
-    let index_constraints: Vec<_> = constraints
+    // Classify without materializing throwaway `Vec`s: the two flags drive the
+    // section headers and the inline `filter_map` loops below iterate the
+    // constraints directly. A composite unique is one with more than one column.
+    let has_index = constraints
         .iter()
-        .filter_map(|c| {
-            if let TableConstraint::Index { name, columns } = c {
-                Some((name, columns))
-            } else {
-                None
-            }
-        })
-        .collect();
-
-    let composite_uniques: Vec<_> = constraints
+        .any(|c| matches!(c, TableConstraint::Index { .. }));
+    let has_composite_unique = constraints
         .iter()
-        .filter_map(|c| {
-            if let TableConstraint::Unique { name, columns, .. } = c {
-                if columns.len() > 1 {
-                    Some((name, columns))
-                } else {
-                    None
-                }
-            } else {
-                None
-            }
-        })
-        .collect();
+        .any(|c| matches!(c, TableConstraint::Unique { columns, .. } if columns.len() > 1));
 
-    if index_constraints.is_empty() && composite_uniques.is_empty() {
+    if !has_index && !has_composite_unique {
         return;
     }
 
-    if !index_constraints.is_empty() {
+    if has_index {
         lines.push("// Index definitions (SeaORM uses Statement builders externally)".into());
-        for (name, columns) in index_constraints {
+        for (name, columns) in constraints.iter().filter_map(|c| match c {
+            TableConstraint::Index { name, columns } => Some((name, columns)),
+            _ => None,
+        }) {
             let cols = vespertide_core::schema::names::join_column_names(columns, ", ");
             let idx_name = name.clone().unwrap_or_else(|| "(unnamed)".to_string());
             lines.push(format!("// {idx_name} on [{cols}]"));
         }
     }
 
-    if !composite_uniques.is_empty() {
+    if has_composite_unique {
         lines.push(String::new());
         lines.push(
             "/// Composite unique constraints — declare in migrations or use Statement builder."
                 .into(),
         );
         lines.push("pub const COMPOSITE_UNIQUES: &[&[&str]] = &[".into());
-        for (name, columns) in composite_uniques {
+        for (name, columns) in constraints.iter().filter_map(|c| match c {
+            TableConstraint::Unique { name, columns, .. } if columns.len() > 1 => {
+                Some((name, columns))
+            }
+            _ => None,
+        }) {
             let cols_str = crate::utils::common::join_quoted(columns);
             let comment = name
                 .as_deref()
diff --git a/crates/vespertide-exporter/src/utils/common.rs b/crates/vespertide-exporter/src/utils/common.rs
index b311c771..daca4b59 100644
--- a/crates/vespertide-exporter/src/utils/common.rs
+++ b/crates/vespertide-exporter/src/utils/common.rs
@@ -7,7 +7,14 @@
 /// result in a single buffer instead of collecting an intermediate
 /// `Vec` per call site.
 pub(crate) fn join_quoted>(items: &[T]) -> String {
-    let mut out = String::new();
+    // Pre-size exactly: each item contributes 2 quotes + its own length, and
+    // every item after the first adds a 2-byte ", " separator. Mirrors the
+    // `String::with_capacity` + buffer-push convention used by the sibling
+    // helpers in `query/helpers.rs`, `vespertide-naming`, and
+    // `seaorm/relations/naming.rs`. Output stays byte-identical.
+    let content_len: usize = items.iter().map(|i| i.as_ref().len()).sum();
+    let capacity = content_len + 2 * items.len() + 2 * items.len().saturating_sub(1);
+    let mut out = String::with_capacity(capacity);
     for item in items {
         if !out.is_empty() {
             out.push_str(", ");
@@ -42,7 +49,15 @@ pub(crate) fn push_attr(buf: &mut String, fragment: &str) {
 /// renderers, and builds the result in a single buffer instead of collecting an
 /// intermediate `Vec` (one `String` per column) per call site.
 pub(crate) fn join_qualified_refs(ref_table: &str, ref_cols: &[&str]) -> String {
-    let mut out = String::new();
+    // Pre-size exactly: each column renders as `"."` — 2 quotes
+    // + `ref_table.len()` + 1 dot + `col.len()` — and every column after the
+    // first adds a 2-byte ", " separator. Matches the buffer-push pre-sizing
+    // convention used across the workspace. Output stays byte-identical.
+    let cols_len: usize = ref_cols.iter().map(|c| c.len()).sum();
+    let per_col_fixed = 2 + ref_table.len() + 1;
+    let capacity =
+        per_col_fixed * ref_cols.len() + cols_len + 2 * ref_cols.len().saturating_sub(1);
+    let mut out = String::with_capacity(capacity);
     for col in ref_cols {
         if !out.is_empty() {
             out.push_str(", ");

From 219a1f5ce94fa99e670450e804d46a387170e1a0 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 16:23:29 +0900
Subject: [PATCH 088/109] =?UTF-8?q?retry-now#0087:=20batch=20=E2=80=94=20d?=
 =?UTF-8?q?edup=20LSP=20tree-walk=20helpers=20(3=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- tree_util::outermost_ancestor_mapping replaces 3 upward ancestor-mapping walks
- collapse references::search private trim_one_byte_each_side into tree_util
- tree_util::is_top_level_pair replaces 2 top-level-pair checks

All byte-identical behaviour; vespertide-lsp tests + clippy green.
---
 .../vespertide-lsp/src/references/resolver.rs | 44 +-----------
 .../vespertide-lsp/src/references/search.rs   | 63 +++--------------
 crates/vespertide-lsp/src/tree_util.rs        | 70 +++++++++++++++++--
 3 files changed, 77 insertions(+), 100 deletions(-)

diff --git a/crates/vespertide-lsp/src/references/resolver.rs b/crates/vespertide-lsp/src/references/resolver.rs
index 67da5693..7e961abb 100644
--- a/crates/vespertide-lsp/src/references/resolver.rs
+++ b/crates/vespertide-lsp/src/references/resolver.rs
@@ -2,7 +2,8 @@
 
 use crate::text_util::strip_quotes;
 use crate::tree_util::{
-    ancestor_pair, direct_child_value, enclosing_string, node_at_byte, skip_yaml_wrappers,
+    ancestor_pair, direct_child_value, enclosing_string, is_top_level_pair, node_at_byte,
+    outermost_ancestor_mapping, skip_yaml_wrappers,
 };
 
 use super::ReferenceSymbol;
@@ -106,50 +107,11 @@ fn resolve_check_expr_column(
     })
 }
 
-/// A pair is "top level" when its direct ancestor mapping is itself the
-/// outermost mapping of the document. This is how we distinguish the
-/// table's own `name: ...` from a column's `name: ...`.
-fn is_top_level_pair(pair: tree_sitter::Node<'_>) -> bool {
-    let Some(parent_mapping) = pair.parent() else {
-        return false;
-    };
-    if !matches!(
-        parent_mapping.kind(),
-        "object" | "block_mapping" | "flow_mapping"
-    ) {
-        return false;
-    }
-    // Walk above the parent mapping; if we encounter another mapping with
-    // the same kind, we are nested and therefore not top level.
-    let mut current = parent_mapping.parent();
-    while let Some(candidate) = current {
-        if matches!(
-            candidate.kind(),
-            "object" | "block_mapping" | "flow_mapping"
-        ) {
-            return false;
-        }
-        current = candidate.parent();
-    }
-    true
-}
-
 /// Given a column's `name` pair, find the owning table's top-level name.
 fn enclosing_table_name(name_pair: tree_sitter::Node<'_>, source: &str) -> Option {
     // The pair we got is inside a column object. Walk up to the document's
     // outermost mapping and return its direct `name` value.
-    let mut current = name_pair.parent();
-    let mut outer = None;
-    while let Some(candidate) = current {
-        if matches!(
-            candidate.kind(),
-            "object" | "block_mapping" | "flow_mapping"
-        ) {
-            outer = Some(candidate);
-        }
-        current = candidate.parent();
-    }
-    let outer = outer?;
+    let outer = outermost_ancestor_mapping(name_pair)?;
 
     let mut cursor = outer.walk();
     for child in outer.children(&mut cursor) {
diff --git a/crates/vespertide-lsp/src/references/search.rs b/crates/vespertide-lsp/src/references/search.rs
index 427ceccb..2be0f111 100644
--- a/crates/vespertide-lsp/src/references/search.rs
+++ b/crates/vespertide-lsp/src/references/search.rs
@@ -270,18 +270,7 @@ fn sibling_value<'a>(
 
 /// Walk up to the document's outermost mapping and return its `name` value.
 fn outer_table_name<'a>(source: &'a [u8], node: tree_sitter::Node<'_>) -> Option<&'a str> {
-    let mut current = node.parent();
-    let mut outer = None;
-    while let Some(candidate) = current {
-        if matches!(
-            candidate.kind(),
-            "object" | "block_mapping" | "flow_mapping"
-        ) {
-            outer = Some(candidate);
-        }
-        current = candidate.parent();
-    }
-    let outer = outer?;
+    let outer = crate::tree_util::outermost_ancestor_mapping(node)?;
     direct_child_scalar(outer, source, "name")
 }
 
@@ -388,46 +377,22 @@ fn inner_content_range(node: tree_sitter::Node<'_>) -> std::ops::Range {
         // tree-sitter-json: `string` is `"…"`. Its first named child is
         // `string_content` (absent when the literal is empty).
         "string" => node.named_child(0).map_or_else(
-            || trim_one_byte_each_side(node.byte_range()),
+            || crate::tree_util::trim_one_byte_each_side(&node.byte_range()),
             |inner| inner.byte_range(),
         ),
         // tree-sitter-yaml quoted scalars include their delimiters; trim
         // one byte on each side.
-        "double_quote_scalar" | "single_quote_scalar" => trim_one_byte_each_side(node.byte_range()),
+        "double_quote_scalar" | "single_quote_scalar" => {
+            crate::tree_util::trim_one_byte_each_side(&node.byte_range())
+        }
         // Unquoted scalars (YAML plain / string_scalar, or anything else)
         // have no delimiters — the full range is the identifier.
         _ => node.byte_range(),
     }
 }
 
-fn trim_one_byte_each_side(range: std::ops::Range) -> std::ops::Range {
-    if range.end.saturating_sub(range.start) >= 2 {
-        (range.start + 1)..(range.end - 1)
-    } else {
-        range
-    }
-}
-
 fn is_top_level(pair: tree_sitter::Node<'_>) -> bool {
-    if let Some(parent) = pair.parent() {
-        if matches!(parent.kind(), "object" | "block_mapping" | "flow_mapping") {
-            let mut current = parent.parent();
-            while let Some(candidate) = current {
-                if matches!(
-                    candidate.kind(),
-                    "object" | "block_mapping" | "flow_mapping"
-                ) {
-                    return false;
-                }
-                current = candidate.parent();
-            }
-            true
-        } else {
-            false
-        }
-    } else {
-        false
-    }
+    crate::tree_util::is_top_level_pair(pair)
 }
 
 /// Check that this `name` pair lives directly inside a column object whose
@@ -443,19 +408,7 @@ fn is_column_pair(name_pair: tree_sitter::Node<'_>, source: &[u8], expected_tabl
     {
         // The column object is not allowed to be the outermost mapping — that's
         // the table itself.
-        let mut current = column_object.parent();
-        let mut outer = None;
-        while let Some(candidate) = current {
-            if matches!(
-                candidate.kind(),
-                "object" | "block_mapping" | "flow_mapping"
-            ) {
-                outer = Some(candidate);
-            }
-            current = candidate.parent();
-        }
-
-        if let Some(outer) = outer
+        if let Some(outer) = crate::tree_util::outermost_ancestor_mapping(column_object)
             && outer.id() != column_object.id()
         {
             return direct_child_scalar(outer, source, "name")
@@ -604,7 +557,7 @@ mod tests {
 
     #[test]
     fn range_and_top_level_helpers_cover_defensive_branches() {
-        assert_eq!(trim_one_byte_each_side(4..5), 4..5);
+        assert_eq!(crate::tree_util::trim_one_byte_each_side(&(4..5)), 4..5);
 
         let src = r#"{"name":"user","columns":[{"name":"id","type":"integer"}]}"#;
         let tree = parse_json(src);
diff --git a/crates/vespertide-lsp/src/tree_util.rs b/crates/vespertide-lsp/src/tree_util.rs
index 141801e5..4429d404 100644
--- a/crates/vespertide-lsp/src/tree_util.rs
+++ b/crates/vespertide-lsp/src/tree_util.rs
@@ -87,10 +87,10 @@ pub(crate) fn unwrap_yaml_node(node: tree_sitter::Node<'_>) -> tree_sitter::Node
 /// `single_quote_scalar` byte ranges (and JSON `string` ranges when the
 /// `string_content` named child is absent — the empty-string case).
 ///
-/// Centralises the three byte-identical private copies that lived in
-/// `file_features`, `rename`, and `symbols` pre-this-hoist. The
-/// `references::search::trim_one_byte_each_side` variant takes `Range`
-/// by value and stays put; its signature change is a separate decision.
+/// Centralises the four byte-identical private copies that lived in
+/// `file_features`, `rename`, `symbols`, and `references::search`
+/// pre-this-hoist (the last took `Range` by value; call sites now pass a
+/// reference).
 pub(crate) fn trim_one_byte_each_side(range: &std::ops::Range) -> std::ops::Range {
     if range.end.saturating_sub(range.start) >= 2 {
         (range.start + 1)..(range.end - 1)
@@ -289,6 +289,68 @@ pub(crate) fn find_pair_with_key<'tree>(
     })
 }
 
+/// A pair is "top level" when its direct ancestor mapping is itself the
+/// outermost mapping of the document — this is how an LSP feature tells the
+/// table's own `name: ...` apart from a column's `name: ...`. The `pair`'s
+/// parent must be a mapping kind (`object` / `block_mapping` /
+/// `flow_mapping`); walking above that parent must encounter NO further
+/// mapping ancestor.
+///
+/// Centralises the two byte-equivalent copies that lived in
+/// `references::search::is_top_level` and
+/// `references::resolver::is_top_level_pair` pre-this-hoist.
+pub(crate) fn is_top_level_pair(pair: tree_sitter::Node<'_>) -> bool {
+    let Some(parent_mapping) = pair.parent() else {
+        return false;
+    };
+    if !matches!(
+        parent_mapping.kind(),
+        "object" | "block_mapping" | "flow_mapping"
+    ) {
+        return false;
+    }
+    // Walk above the parent mapping; if we encounter another mapping with
+    // the same kind, we are nested and therefore not top level.
+    let mut current = parent_mapping.parent();
+    while let Some(candidate) = current {
+        if matches!(
+            candidate.kind(),
+            "object" | "block_mapping" | "flow_mapping"
+        ) {
+            return false;
+        }
+        current = candidate.parent();
+    }
+    true
+}
+
+/// Walk strictly the PARENT chain of `node` and return the OUTERMOST
+/// ancestor whose kind is a mapping (`object` / `block_mapping` /
+/// `flow_mapping`) — i.e. the last mapping-kind node encountered before the
+/// parent chain runs out. Returns `None` when no ancestor is a mapping.
+///
+/// This is the UPWARD counterpart to [`find_outer_mapping`], which descends
+/// DFS to find the topmost mapping. Centralises the three byte-identical
+/// upward walks that lived in `references::search::outer_table_name`,
+/// `references::search::is_column_pair`, and
+/// `references::resolver::enclosing_table_name` pre-this-hoist.
+pub(crate) fn outermost_ancestor_mapping(
+    node: tree_sitter::Node<'_>,
+) -> Option> {
+    let mut current = node.parent();
+    let mut outer = None;
+    while let Some(candidate) = current {
+        if matches!(
+            candidate.kind(),
+            "object" | "block_mapping" | "flow_mapping"
+        ) {
+            outer = Some(candidate);
+        }
+        current = candidate.parent();
+    }
+    outer
+}
+
 /// True when any ancestor pair of `node` has key `"constraints"`. Shared LSP
 /// predicate for "cursor is inside a `constraints` array" — used by both the
 /// CHECK-expression hover (`hover::check_expr`) and the CHECK-expression

From 5f34c886404263cbc9d8cd2117be283898f07d5f Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 16:47:12 +0900
Subject: [PATCH 089/109] =?UTF-8?q?retry-now#0088:=20seaorm=20relations=20?=
 =?UTF-8?q?=E2=80=94=20borrow=20FK=20tuples=20in=20self-ref=20+=20M2M=20co?=
 =?UTF-8?q?llectors,=20&str=20keys=20for=20fk=5Fcount=5Fper=5Ftable=20(3?=
 =?UTF-8?q?=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/seaorm/relations/reverse.rs           | 26 +++++++++----------
 .../src/seaorm/relations/self_ref.rs          |  8 +++---
 2 files changed, 16 insertions(+), 18 deletions(-)

diff --git a/crates/vespertide-exporter/src/seaorm/relations/reverse.rs b/crates/vespertide-exporter/src/seaorm/relations/reverse.rs
index a77192af..74bdf1ad 100644
--- a/crates/vespertide-exporter/src/seaorm/relations/reverse.rs
+++ b/crates/vespertide-exporter/src/seaorm/relations/reverse.rs
@@ -179,8 +179,7 @@ fn reverse_relation_field_defs_inner(ctx: ReverseRelationFieldCtx<'_>) -> Vec = Vec::new();
 
     // Count how many FKs from each table reference this table
-    let mut fk_count_per_table: std::collections::HashMap =
-        std::collections::HashMap::new();
+    let mut fk_count_per_table: HashMap<&str, usize> = HashMap::new();
     for other_table in schema {
         if other_table.name == table.name {
             continue;
@@ -190,7 +189,7 @@ fn reverse_relation_field_defs_inner(ctx: ReverseRelationFieldCtx<'_>) -> Vec = junction_table
+    let fks: Vec<(&[ColumnName], &TableName)> = junction_table
         .constraints
         .iter()
         .filter_map(|c| {
@@ -374,7 +373,7 @@ fn collect_many_to_many_relations(
                 columns, ref_table, ..
             } = c
             {
-                Some((columns.clone(), ref_table.clone()))
+                Some((columns.as_slice(), ref_table))
             } else {
                 None
             }
@@ -397,17 +396,16 @@ fn collect_many_to_many_relations(
 
     // Find which FK references the current table
     fks.iter()
-        .find(|(_, ref_table)| ref_table == ¤t_table.name)?;
+        .find(|(_, ref_table)| **ref_table == current_table.name)?;
 
     let mut relations = Vec::new();
 
-    let self_ref_fks: Vec<_> = fks
+    // All FKs point back at the current table ⇒ pure self-ref junction, not an
+    // M2M junction linking two distinct tables.
+    if fks
         .iter()
-        .filter(|(_, ref_table)| ref_table == ¤t_table.name)
-        .cloned()
-        .collect();
-
-    if self_ref_fks.len() == fks.len() {
+        .all(|(_, ref_table)| **ref_table == current_table.name)
+    {
         return None;
     }
 
@@ -428,11 +426,11 @@ fn collect_many_to_many_relations(
 
     // Then add has_many with via for the target tables (M2M relations)
     for (_columns, ref_table) in &fks {
-        if ref_table == ¤t_table.name {
+        if **ref_table == current_table.name {
             continue;
         }
 
-        let target_exists = schema.iter().any(|t| &t.name == ref_table);
+        let target_exists = schema.iter().any(|t| &t.name == *ref_table);
         if !target_exists {
             continue;
         }
diff --git a/crates/vespertide-exporter/src/seaorm/relations/self_ref.rs b/crates/vespertide-exporter/src/seaorm/relations/self_ref.rs
index 411e7c80..ada52cc3 100644
--- a/crates/vespertide-exporter/src/seaorm/relations/self_ref.rs
+++ b/crates/vespertide-exporter/src/seaorm/relations/self_ref.rs
@@ -7,7 +7,7 @@
 
 use std::collections::{HashMap, HashSet};
 
-use vespertide_core::{TableConstraint, TableDef};
+use vespertide_core::{ColumnName, TableConstraint, TableDef, TableName};
 
 use super::super::imports::{
     absolute_module_path, sanitize_field_name, to_pascal_case, unique_name,
@@ -30,7 +30,7 @@ pub(super) fn collect_self_ref_junction(
         return None;
     }
 
-    let fks: Vec<_> = junction_table
+    let fks: Vec<(&[ColumnName], &TableName)> = junction_table
         .constraints
         .iter()
         .filter_map(|c| {
@@ -38,7 +38,7 @@ pub(super) fn collect_self_ref_junction(
                 columns, ref_table, ..
             } = c
             {
-                Some((columns.clone(), ref_table.clone()))
+                Some((columns.as_slice(), ref_table))
             } else {
                 None
             }
@@ -58,7 +58,7 @@ pub(super) fn collect_self_ref_junction(
 
     if !fks
         .iter()
-        .all(|(_, ref_table)| ref_table == ¤t_table.name)
+        .all(|(_, ref_table)| **ref_table == current_table.name)
     {
         return None;
     }

From f777f261c01bbf4b5a42430f941b5d725bb25ed9 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 16:55:07 +0900
Subject: [PATCH 090/109] retry-now#0089: replace to_uppercase() allocation in
 is_function_expr with eq_ignore_ascii_case (1 kept)

---
 .../src/validate/default_changes.rs           | 22 +++++++++----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/crates/vespertide-planner/src/validate/default_changes.rs b/crates/vespertide-planner/src/validate/default_changes.rs
index e9ed2968..0c5f9c22 100644
--- a/crates/vespertide-planner/src/validate/default_changes.rs
+++ b/crates/vespertide-planner/src/validate/default_changes.rs
@@ -234,17 +234,17 @@ fn is_function_expr(s: &str) -> bool {
     if trimmed.contains('(') {
         return true;
     }
-    let upper = trimmed.to_uppercase();
-    matches!(
-        upper.as_str(),
-        "CURRENT_TIMESTAMP"
-            | "CURRENT_DATE"
-            | "CURRENT_TIME"
-            | "LOCALTIMESTAMP"
-            | "LOCALTIME"
-            | "CURRENT_USER"
-            | "SESSION_USER"
-    )
+    // ASCII-case-insensitive compares against the fixed keyword set avoid the
+    // per-call `String` allocation that `to_uppercase()` would incur (mirrors
+    // the allocation-free convention in `helpers.rs::needs_quoting`). All
+    // keywords are pure ASCII, so the result is byte-identical.
+    trimmed.eq_ignore_ascii_case("CURRENT_TIMESTAMP")
+        || trimmed.eq_ignore_ascii_case("CURRENT_DATE")
+        || trimmed.eq_ignore_ascii_case("CURRENT_TIME")
+        || trimmed.eq_ignore_ascii_case("LOCALTIMESTAMP")
+        || trimmed.eq_ignore_ascii_case("LOCALTIME")
+        || trimmed.eq_ignore_ascii_case("CURRENT_USER")
+        || trimmed.eq_ignore_ascii_case("SESSION_USER")
 }
 
 #[cfg(test)]

From fc9bde9b63331c90e5ac73b90affa8f6931f5752 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 17:07:51 +0900
Subject: [PATCH 091/109] =?UTF-8?q?retry-now#0090:=20batch=20=E2=80=94=20d?=
 =?UTF-8?q?rop=20redundant=20sanitize=5Ffilename=20clone=20x3,=20short-cir?=
 =?UTF-8?q?cuit=20RawSql=20len=20check=20(2=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 crates/vespertide-cli/src/commands/export.rs | 6 +++---
 crates/vespertide-core/src/action/display.rs | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/crates/vespertide-cli/src/commands/export.rs b/crates/vespertide-cli/src/commands/export.rs
index 3de79363..c284e219 100644
--- a/crates/vespertide-cli/src/commands/export.rs
+++ b/crates/vespertide-cli/src/commands/export.rs
@@ -202,7 +202,7 @@ fn rel_path_to_module_segments(rel_path: &Path) -> Vec {
             if let std::path::Component::Normal(name) = component
                 && let Some(s) = name.to_str()
             {
-                segments.push(sanitize_filename(s).clone());
+                segments.push(sanitize_filename(s));
             }
         }
     }
@@ -215,7 +215,7 @@ fn rel_path_to_module_segments(rel_path: &Path) -> Vec {
             (file_name, "")
         };
         let stem = stem.strip_suffix(".vespertide").unwrap_or(stem);
-        segments.push(sanitize_filename(stem).clone());
+        segments.push(sanitize_filename(stem));
     }
 
     segments
@@ -380,7 +380,7 @@ async fn ensure_mod_chain(root: &Path, rel_path: &Path) -> Result<()> {
     };
     let mut comps: Vec = path_stripped
         .components()
-        .filter_map(|c| c.as_os_str().to_str().map(|s| sanitize_filename(s).clone()))
+        .filter_map(|c| c.as_os_str().to_str().map(sanitize_filename))
         .collect();
     if comps.is_empty() {
         return Ok(());
diff --git a/crates/vespertide-core/src/action/display.rs b/crates/vespertide-core/src/action/display.rs
index 32aa0aa9..5175e125 100644
--- a/crates/vespertide-core/src/action/display.rs
+++ b/crates/vespertide-core/src/action/display.rs
@@ -129,7 +129,7 @@ fn truncate_chars(s: &str, max_chars: usize) -> String {
 }
 
 fn write_raw_sql_action(f: &mut fmt::Formatter<'_>, sql: &str) -> fmt::Result {
-    if sql.chars().count() > 50 {
+    if sql.chars().nth(50).is_some() {
         let head = truncate_chars(sql, 47);
         write!(f, "RawSql: {head}...")
     } else {

From 34f73aeb2ae53203d61e93312ae557ce7fe70093 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 17:21:25 +0900
Subject: [PATCH 092/109] =?UTF-8?q?retry-now#0091:=20batch=20=E2=80=94=20d?=
 =?UTF-8?q?rop=20throwaway=20format!=20in=20build=5Fcreate=5Fwith=5Fchecks?=
 =?UTF-8?q?,=20single=20file=5Ftype()=20fast=20path=20in=20collect=5Fmodel?=
 =?UTF-8?q?=5Fpaths=20(2=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 crates/vespertide-loader/src/models.rs     | 13 +++++++++++--
 crates/vespertide-query/src/sql/helpers.rs |  6 +++++-
 2 files changed, 16 insertions(+), 3 deletions(-)

diff --git a/crates/vespertide-loader/src/models.rs b/crates/vespertide-loader/src/models.rs
index 4bcea7d7..74047ca6 100644
--- a/crates/vespertide-loader/src/models.rs
+++ b/crates/vespertide-loader/src/models.rs
@@ -57,9 +57,18 @@ fn collect_model_paths(dir: &Path) -> Result> {
         let entry = entry.context("read directory entry")?;
         let path = entry.path();
 
-        if path.is_dir() {
+        // `DirEntry::file_type()` answers dir/file from the single `readdir`
+        // result on most platforms (no extra stat). It does NOT follow
+        // symlinks, whereas `Path::is_dir`/`is_file` DO — so for symlinked
+        // entries we fall back to the path checks to keep behavior identical.
+        let ft = entry.file_type().ok();
+        let is_symlink = ft.is_some_and(|t| t.is_symlink());
+
+        if ft.is_some_and(|t| t.is_dir()) || (is_symlink && path.is_dir()) {
             paths.extend(collect_model_paths(&path)?);
-        } else if path.is_file() && crate::has_supported_extension(&path) {
+        } else if (ft.is_some_and(|t| t.is_file()) || (is_symlink && path.is_file()))
+            && crate::has_supported_extension(&path)
+        {
             paths.push(path);
         }
     }
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index 1845ecaa..2f12dcec 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -612,7 +612,11 @@ pub(crate) fn build_create_with_checks(
         let mut modified_sql = base_sql;
         if let Some(pos) = modified_sql.rfind(')') {
             let check_sql = check_clauses.join(", ");
-            modified_sql.insert_str(pos, &format!(", {check_sql}"));
+            // Insert `", " + check_sql` at `pos` without an extra `format!`
+            // allocation: the joined text goes in first, then the separator is
+            // inserted BEFORE it at the same `pos`, yielding `", `.
+            modified_sql.insert_str(pos, &check_sql);
+            modified_sql.insert_str(pos, ", ");
         }
         BuiltQuery::Raw(RawSql::uniform(modified_sql))
     }

From 247cc6084b8b565ff5cad8c83e4bd5fe0df3e03d Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 17:38:55 +0900
Subject: [PATCH 093/109] =?UTF-8?q?retry-now#0093:=20batch=20=E2=80=94=20d?=
 =?UTF-8?q?rop=20per-token=20classify=5Fword=20alloc,=20hoist=20BETWEEN-sw?=
 =?UTF-8?q?ap=20title=20const=20(2=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 crates/vespertide-lsp/src/code_actions.rs     |  7 +++-
 .../src/validate/check_expr_parser.rs         | 34 +++++++++++++------
 2 files changed, 29 insertions(+), 12 deletions(-)

diff --git a/crates/vespertide-lsp/src/code_actions.rs b/crates/vespertide-lsp/src/code_actions.rs
index d4318e62..ae85a5a5 100644
--- a/crates/vespertide-lsp/src/code_actions.rs
+++ b/crates/vespertide-lsp/src/code_actions.rs
@@ -95,6 +95,11 @@ fn check_expr_actions(
 /// lexicographically, anything mixed or non-orderable (bool/null) skipped.
 /// `NOT BETWEEN` is skipped — a reversed `NOT BETWEEN` is always-true and
 /// therefore harmless.
+/// Fixed title for the reversed-`BETWEEN` swap refactor. Hoisted to a
+/// module-scope constant so the string literal is not re-materialised on each
+/// match found by the scan loop below.
+const SWAP_REVERSED_BETWEEN_TITLE: &str = "Swap reversed BETWEEN bounds";
+
 fn swap_reversed_between(expr_text: &str, base: usize) -> Vec {
     use vespertide_planner::{CheckTokenKind, lex_check_expr};
 
@@ -129,7 +134,7 @@ fn swap_reversed_between(expr_text: &str, base: usize) -> Vec
             && literal_greater(low_text, high_text)
         {
             out.push(DomainCodeAction {
-                title: "Swap reversed BETWEEN bounds".to_string(),
+                title: SWAP_REVERSED_BETWEEN_TITLE.to_string(),
                 kind: CodeActionKind::Refactor,
                 edits: vec![
                     DomainTextEdit {
diff --git a/crates/vespertide-planner/src/validate/check_expr_parser.rs b/crates/vespertide-planner/src/validate/check_expr_parser.rs
index 00d922a7..a4895c5b 100644
--- a/crates/vespertide-planner/src/validate/check_expr_parser.rs
+++ b/crates/vespertide-planner/src/validate/check_expr_parser.rs
@@ -524,17 +524,29 @@ fn parse_number_token(raw: &str) -> Option {
 }
 
 fn classify_word(word: &str) -> Token {
-    match word.to_ascii_uppercase().as_str() {
-        "AND" => Token::Keyword(Keyword::And),
-        "OR" => Token::Keyword(Keyword::Or),
-        "NOT" => Token::Keyword(Keyword::Not),
-        "IN" => Token::Keyword(Keyword::In),
-        "BETWEEN" => Token::Keyword(Keyword::Between),
-        "IS" => Token::Keyword(Keyword::Is),
-        "NULL" => Token::Keyword(Keyword::Null),
-        "TRUE" => Token::Keyword(Keyword::True),
-        "FALSE" => Token::Keyword(Keyword::False),
-        _ => Token::Ident(word.to_string()),
+    // Case-insensitive keyword recognition without allocating: identifiers
+    // (the common token) fall straight through to the `Token::Ident` arm with
+    // no `to_ascii_uppercase` String churn on the hot path.
+    if word.eq_ignore_ascii_case("AND") {
+        Token::Keyword(Keyword::And)
+    } else if word.eq_ignore_ascii_case("OR") {
+        Token::Keyword(Keyword::Or)
+    } else if word.eq_ignore_ascii_case("NOT") {
+        Token::Keyword(Keyword::Not)
+    } else if word.eq_ignore_ascii_case("IN") {
+        Token::Keyword(Keyword::In)
+    } else if word.eq_ignore_ascii_case("BETWEEN") {
+        Token::Keyword(Keyword::Between)
+    } else if word.eq_ignore_ascii_case("IS") {
+        Token::Keyword(Keyword::Is)
+    } else if word.eq_ignore_ascii_case("NULL") {
+        Token::Keyword(Keyword::Null)
+    } else if word.eq_ignore_ascii_case("TRUE") {
+        Token::Keyword(Keyword::True)
+    } else if word.eq_ignore_ascii_case("FALSE") {
+        Token::Keyword(Keyword::False)
+    } else {
+        Token::Ident(word.to_string())
     }
 }
 

From d93f0a8df94bf32ee9b767fe18651110afa9fb1a Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 17:51:05 +0900
Subject: [PATCH 094/109] =?UTF-8?q?retry-now#0094:=20batch=20=E2=80=94=20a?=
 =?UTF-8?q?lloc-free=20column=5Fmarkers=20match,=20same=5Fcolumn=5Fset=20l?=
 =?UTF-8?q?ength=20early-out=20(2=20kept)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 crates/vespertide-cli/src/commands/erd/mod.rs | 28 +++++++++++--------
 1 file changed, 16 insertions(+), 12 deletions(-)

diff --git a/crates/vespertide-cli/src/commands/erd/mod.rs b/crates/vespertide-cli/src/commands/erd/mod.rs
index 7c09ea87..80432910 100644
--- a/crates/vespertide-cli/src/commands/erd/mod.rs
+++ b/crates/vespertide-cli/src/commands/erd/mod.rs
@@ -261,18 +261,17 @@ pub(super) fn is_foreign_key_column(table: &TableDef, column_name: &str) -> bool
 }
 
 pub(super) fn column_markers(table: &TableDef, column: &ColumnDef) -> String {
-    let mut markers = Vec::new();
-    if is_primary_key_column(table, &column.name) {
-        markers.push("PK");
-    }
-    if is_foreign_key_column(table, &column.name) {
-        markers.push("FK");
-    }
-
-    if markers.is_empty() {
-        String::new()
-    } else {
-        format!(" ({})", markers.join(", "))
+    // The marker set is at most two fixed strings ("PK", "FK"), so the output is
+    // exactly one of four values. A direct match returning a &'static str for the
+    // three non-empty cases removes the Vec + join + format! allocations.
+    match (
+        is_primary_key_column(table, &column.name),
+        is_foreign_key_column(table, &column.name),
+    ) {
+        (true, true) => " (PK, FK)".to_string(),
+        (true, false) => " (PK)".to_string(),
+        (false, true) => " (FK)".to_string(),
+        (false, false) => String::new(),
     }
 }
 
@@ -529,6 +528,11 @@ fn is_nullable_column(table: &TableDef, column_name: &str) -> bool {
 }
 
 fn same_column_set, U: AsRef>(left: &[T], right: &[U]) -> bool {
+    // Differing lengths can never yield equal sets, so skip building both
+    // BTreeSets on the common mismatched-arity case (e.g. single-col vs composite).
+    if left.len() != right.len() {
+        return false;
+    }
     let left: BTreeSet<&str> = left.iter().map(AsRef::as_ref).collect();
     let right: BTreeSet<&str> = right.iter().map(AsRef::as_ref).collect();
     left == right

From 351e194790e5d40d897bd04989831538f95d86e3 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 18:05:10 +0900
Subject: [PATCH 095/109] retry-now#0095: borrow deleted-column names in diff
 (BTreeSet<&str>) to drop a String alloc per deleted column (1 kept)

---
 crates/vespertide-planner/src/diff/columns.rs  | 18 ++++++++++--------
 .../vespertide-planner/src/diff/constraints.rs |  4 ++--
 2 files changed, 12 insertions(+), 10 deletions(-)

diff --git a/crates/vespertide-planner/src/diff/columns.rs b/crates/vespertide-planner/src/diff/columns.rs
index ea105a84..f50a3805 100644
--- a/crates/vespertide-planner/src/diff/columns.rs
+++ b/crates/vespertide-planner/src/diff/columns.rs
@@ -4,12 +4,12 @@ use vespertide_core::{
     ColumnDef, ColumnType, ComplexColumnType, EnumValues, MigrationAction, TableDef,
 };
 
-pub(super) fn diff_columns(
+pub(super) fn diff_columns<'a>(
     actions: &mut Vec,
     table_name: &str,
-    from_tbl: &TableDef,
+    from_tbl: &'a TableDef,
     to_tbl: &TableDef,
-) -> BTreeSet {
+) -> BTreeSet<&'a str> {
     // Columns - use BTreeMap for consistent ordering
     let from_cols: BTreeMap<_, _> = from_tbl
         .columns
@@ -107,23 +107,25 @@ fn compute_integer_enum_remapping(from: &ColumnType, to: &ColumnType) -> BTreeMa
         .collect()
 }
 
-fn diff_deleted_columns(
+fn diff_deleted_columns<'a>(
     actions: &mut Vec,
     table_name: &str,
-    from_cols: &BTreeMap<&str, &ColumnDef>,
+    from_cols: &BTreeMap<&'a str, &ColumnDef>,
     to_cols: &BTreeMap<&str, &ColumnDef>,
-) -> BTreeSet {
+) -> BTreeSet<&'a str> {
     // `from_cols` is a `BTreeMap`, so its keys are already in sorted order —
     // iterating them once yields the same order a `BTreeSet` would, letting us
     // emit each `DeleteColumn` action and populate the returned set in a single
-    // pass without the extra `col.clone().into()` per deleted column.
+    // pass. The set borrows the column names straight from the normalized
+    // `TableDef` (they outlive the whole diff), so no `String` allocation per
+    // deleted column is needed — the only consumer looks each name up by `&str`.
     let mut deleted_columns = BTreeSet::new();
     for col in from_cols.keys().filter(|col| !to_cols.contains_key(*col)) {
         actions.push(MigrationAction::DeleteColumn {
             table: table_name.into(),
             column: (*col).into(),
         });
-        deleted_columns.insert((*col).to_string());
+        deleted_columns.insert(*col);
     }
 
     deleted_columns
diff --git a/crates/vespertide-planner/src/diff/constraints.rs b/crates/vespertide-planner/src/diff/constraints.rs
index 9505d2bc..a2068e9e 100644
--- a/crates/vespertide-planner/src/diff/constraints.rs
+++ b/crates/vespertide-planner/src/diff/constraints.rs
@@ -60,7 +60,7 @@ pub(super) fn diff_constraints(
     table_name: &str,
     from_tbl: &TableDef,
     to_tbl: &TableDef,
-    deleted_columns: &BTreeSet,
+    deleted_columns: &BTreeSet<&str>,
 ) {
     let mut replaced_from: Vec = Vec::new();
     let mut replaced_to: Vec = Vec::new();
@@ -147,7 +147,7 @@ fn diff_removed_constraints(
     table_name: &str,
     from_tbl: &TableDef,
     exact_to: &BTreeSet<&TableConstraint>,
-    deleted_columns: &BTreeSet,
+    deleted_columns: &BTreeSet<&str>,
     replaced_from: &BTreeSet,
 ) {
     for (fi, from_constraint) in from_tbl.constraints.iter().enumerate() {

From 99de7599db0a06f1a9cfae466db86d1b7a5c851b Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Fri, 3 Jul 2026 18:18:54 +0900
Subject: [PATCH 096/109] retry-now#0096: skip to_by_name build when either
 integer-enum variant list is empty in compute_integer_enum_remapping (1 kept)

---
 crates/vespertide-planner/src/diff/columns.rs |  6 +++++
 .../src/diff/tests/enum_remap.rs              | 22 +++++++++++++++++++
 2 files changed, 28 insertions(+)

diff --git a/crates/vespertide-planner/src/diff/columns.rs b/crates/vespertide-planner/src/diff/columns.rs
index f50a3805..3b92c7fa 100644
--- a/crates/vespertide-planner/src/diff/columns.rs
+++ b/crates/vespertide-planner/src/diff/columns.rs
@@ -92,6 +92,12 @@ fn compute_integer_enum_remapping(from: &ColumnType, to: &ColumnType) -> BTreeMa
     if from_items == to_items {
         return BTreeMap::new();
     }
+    // Fast path: a remap pairs a `from` variant with a same-named `to` variant
+    // whose value shifted. If either side is empty there can be no shared name,
+    // so the result is always empty — skip building `to_by_name` entirely.
+    if from_items.is_empty() || to_items.is_empty() {
+        return BTreeMap::new();
+    }
     let to_by_name: BTreeMap<&str, i64> = to_items
         .iter()
         .map(|nv| (nv.name.as_str(), nv.value))
diff --git a/crates/vespertide-planner/src/diff/tests/enum_remap.rs b/crates/vespertide-planner/src/diff/tests/enum_remap.rs
index fe3916b6..45c3cceb 100644
--- a/crates/vespertide-planner/src/diff/tests/enum_remap.rs
+++ b/crates/vespertide-planner/src/diff/tests/enum_remap.rs
@@ -253,6 +253,28 @@ fn variant_renamed_with_same_value_is_not_a_remap() {
     assert!(remap_actions(&actions).is_empty());
 }
 
+#[test]
+fn empty_from_integer_enum_emits_no_remap() {
+    // A column whose integer enum starts with *no* variants and gains some can
+    // never remap an existing value (there is no shared name to shift). The
+    // empty-`from` fast-path guard must short-circuit to an empty mapping, so
+    // no `RemapEnumValues` action is emitted for this transition.
+    let from = table("tickets", enum_col("priority", "ticket_priority", vec![]));
+    let to = table(
+        "tickets",
+        enum_col(
+            "priority",
+            "ticket_priority",
+            vec![("low", 0), ("medium", 5)],
+        ),
+    );
+    let actions = diff_pair(from, to);
+    assert!(
+        remap_actions(&actions).is_empty(),
+        "empty baseline integer enum cannot remap any value — no RemapEnumValues"
+    );
+}
+
 #[test]
 fn string_enum_is_ignored_by_this_pass() {
     use vespertide_core::EnumValues;

From 3cb1a1dfc4fe066e360585d4a850e8663cbde2f0 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 20 Aug 2026 11:15:48 +0900
Subject: [PATCH 097/109] Update lib

---
 .github/workflows/CI.yml              |  36 +-
 .github/workflows/bench.yml           |   4 +-
 .github/workflows/deploy-pages.yml    |   8 +-
 .github/workflows/fuzz.yml            |   6 +-
 .github/workflows/mutants.yml         |   4 +-
 Cargo.lock                            | 814 ++++++++++----------------
 apps/landing/package.json             |  10 +-
 apps/vscode-extension/package.json    |  14 +-
 bun.lock                              | 756 ++++++++++++++----------
 crates/vespertide-cli/Cargo.toml      |   4 +-
 crates/vespertide-core/Cargo.toml     |   2 +-
 crates/vespertide-exporter/Cargo.toml |   2 +-
 crates/vespertide-loader/Cargo.toml   |   2 +-
 crates/vespertide-lsp/Cargo.toml      |   2 +-
 crates/vespertide-macro/Cargo.toml    |   8 +-
 crates/vespertide-planner/Cargo.toml  |   4 +-
 crates/vespertide-query/Cargo.toml    |   6 +-
 crates/vespertide/Cargo.toml          |   4 +-
 examples/app/Cargo.toml               |   2 +-
 package.json                          |   4 +-
 tests/runtime-sqlite/Cargo.toml       |   2 +-
 21 files changed, 826 insertions(+), 868 deletions(-)

diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index f2e7943a..f4419a3d 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -33,7 +33,7 @@ jobs:
     name: fmt
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
       - uses: actions-rust-lang/setup-rust-toolchain@v1
         with:
           components: rustfmt
@@ -44,7 +44,7 @@ jobs:
     name: clippy
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
       - uses: actions-rust-lang/setup-rust-toolchain@v1
         with:
           components: clippy
@@ -55,7 +55,7 @@ jobs:
     name: test
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
       - uses: actions-rust-lang/setup-rust-toolchain@v1
       - name: Test
         run: cargo test --workspace --all-features
@@ -73,7 +73,7 @@ jobs:
       matrix:
         threads: ["1", "4"]
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
       - uses: actions-rust-lang/setup-rust-toolchain@v1
       - name: Test with Rayon thread count
         run: cargo test --workspace --all-features --exclude vespertide-fuzz
@@ -89,7 +89,7 @@ jobs:
     name: SQL validity (daemon-free)
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
       - uses: actions-rust-lang/setup-rust-toolchain@v1
       - name: Install build-essential for pg_query (PG C parser)
         run: sudo apt-get update && sudo apt-get install -y build-essential libreadline-dev zlib1g-dev flex bison
@@ -105,7 +105,7 @@ jobs:
     name: cargo-deny
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
       - uses: actions-rust-lang/setup-rust-toolchain@v1
       - uses: EmbarkStudios/cargo-deny-action@v2
         with:
@@ -116,7 +116,7 @@ jobs:
     runs-on: ubuntu-latest
     if: github.event_name == 'pull_request'
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
         with:
           fetch-depth: 0
       - uses: actions-rust-lang/setup-rust-toolchain@v1
@@ -196,7 +196,7 @@ jobs:
       changepacks: ${{ steps.changepacks.outputs.changepacks }}
       release_assets_urls: ${{ steps.changepacks.outputs.release_assets_urls }}
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
       # changepacks/action@main: project-internal action intentionally tracks main.
       # `publish: true` runs `cargo publish` for every Cargo.toml changed in this
       # release wave. `release_assets_urls` output is consumed by the downstream
@@ -258,7 +258,7 @@ jobs:
             archive: zip
             use_cross: false
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
 
       - uses: actions-rust-lang/setup-rust-toolchain@v1
         with:
@@ -266,7 +266,7 @@ jobs:
 
       - name: Install cross (aarch64-unknown-linux-gnu only)
         if: matrix.use_cross
-        uses: taiki-e/install-action@v2.81.6
+        uses: taiki-e/install-action@v2.86.3
         with:
           tool: cross
 
@@ -354,7 +354,7 @@ jobs:
             binary_dir_name: win32-x64
             archive_format: zip
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
 
       - uses: oven-sh/setup-bun@v2
         with:
@@ -485,7 +485,7 @@ jobs:
     permissions:
       contents: write
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
         with:
           fetch-depth: 0
 
@@ -548,7 +548,7 @@ jobs:
     name: doc
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
       - uses: actions-rust-lang/setup-rust-toolchain@v1
       - name: Check docs
         run: RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace
@@ -557,7 +557,7 @@ jobs:
     name: schema-drift
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
       - uses: actions-rust-lang/setup-rust-toolchain@v1
       - name: Regenerate schemas
         run: cargo run -p vespertide-schema-gen -- --out _tmp_schemas
@@ -568,7 +568,7 @@ jobs:
     name: insta-pending
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
       - uses: actions-rust-lang/setup-rust-toolchain@v1
       - name: Run exporter snapshots
         run: cargo test -p vespertide-exporter
@@ -584,7 +584,7 @@ jobs:
     name: line-budget
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
       - name: Check Rust line budget
         run: sh scripts/check-line-budget.sh
 
@@ -595,7 +595,7 @@ jobs:
       image: xd009642/tarpaulin:develop-nightly
       options: --security-opt seccomp=unconfined
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
       - uses: oven-sh/setup-bun@v2
         with:
           bun-version: latest
@@ -632,7 +632,7 @@ jobs:
           cargo fmt
           cargo tarpaulin --engine llvm --out Lcov Stdout --workspace --exclude app --fail-under 100
       - name: Upload to codecov.io
-        uses: codecov/codecov-action@v6
+        uses: codecov/codecov-action@v7
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
           fail_ci_if_error: true
diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml
index 045e6eb0..4da80605 100644
--- a/.github/workflows/bench.yml
+++ b/.github/workflows/bench.yml
@@ -32,7 +32,7 @@ jobs:
     runs-on: ubuntu-latest
     timeout-minutes: 30
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
 
       - uses: actions-rust-lang/setup-rust-toolchain@v1
 
@@ -53,7 +53,7 @@ jobs:
     runs-on: ubuntu-latest
     timeout-minutes: 15
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
 
       - uses: actions-rust-lang/setup-rust-toolchain@v1
 
diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml
index d2a29b9f..21d13494 100644
--- a/.github/workflows/deploy-pages.yml
+++ b/.github/workflows/deploy-pages.yml
@@ -23,13 +23,13 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v6
+        uses: actions/checkout@v7
 
       - name: Install bun
         uses: oven-sh/setup-bun@v2
 
       - name: Cache bun dependencies
-        uses: actions/cache@v5
+        uses: actions/cache@v6
         with:
           path: ~/.bun/install/cache
           key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
@@ -37,12 +37,12 @@ jobs:
             bun-${{ runner.os }}-
 
       - name: Install Node.js
-        uses: actions/setup-node@v6
+        uses: actions/setup-node@v7
         with:
           node-version: 22
 
       - name: Cache Next.js build
-        uses: actions/cache@v5
+        uses: actions/cache@v6
         with:
           path: |
             apps/landing/.next/cache
diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml
index 3554c37a..1f73d9b2 100644
--- a/.github/workflows/fuzz.yml
+++ b/.github/workflows/fuzz.yml
@@ -40,7 +40,7 @@ jobs:
           - fuzz_migration_apply
           - fuzz_lsp_request
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
 
       - uses: actions-rust-lang/setup-rust-toolchain@v1
         with:
@@ -51,12 +51,12 @@ jobs:
           # crt-static libc is incompatible with `-Zsanitizer=address`.
           target: x86_64-unknown-linux-gnu
 
-      - uses: taiki-e/install-action@v2.81.6
+      - uses: taiki-e/install-action@v2.86.3
         with:
           tool: cargo-fuzz
 
       - name: Cache corpus
-        uses: actions/cache@v5
+        uses: actions/cache@v6
         with:
           path: fuzz/corpus/${{ matrix.target }}
           key: fuzz-corpus-${{ matrix.target }}-${{ github.sha }}
diff --git a/.github/workflows/mutants.yml b/.github/workflows/mutants.yml
index e604b4ae..3906fcde 100644
--- a/.github/workflows/mutants.yml
+++ b/.github/workflows/mutants.yml
@@ -27,13 +27,13 @@ jobs:
         shard:
           [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
         with:
           fetch-depth: 0
 
       - uses: actions-rust-lang/setup-rust-toolchain@v1
 
-      - uses: taiki-e/install-action@v2.81.6
+      - uses: taiki-e/install-action@v2.86.3
         with:
           tool: cargo-mutants
 
diff --git a/Cargo.lock b/Cargo.lock
index 1e530443..a8fad2c3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,19 +4,13 @@ version = 4
 
 [[package]]
 name = "aho-corasick"
-version = "1.1.4"
+version = "1.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
 dependencies = [
  "memchr",
 ]
 
-[[package]]
-name = "aliasable"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
-
 [[package]]
 name = "alloca"
 version = "0.4.0"
@@ -28,9 +22,9 @@ dependencies = [
 
 [[package]]
 name = "android_system_properties"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
 dependencies = [
  "libc",
 ]
@@ -93,15 +87,15 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.102"
+version = "1.0.104"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
 
 [[package]]
 name = "ar_archive_writer"
-version = "0.5.1"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b"
+checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6"
 dependencies = [
  "object",
 ]
@@ -138,7 +132,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -160,18 +154,18 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
 name = "async-trait"
-version = "0.1.89"
+version = "0.1.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
@@ -186,7 +180,7 @@ version = "0.66.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags 2.13.1",
  "cexpr",
  "clang-sys",
  "lazy_static",
@@ -199,7 +193,7 @@ dependencies = [
  "regex",
  "rustc-hash 1.1.0",
  "shlex 1.3.0",
- "syn",
+ "syn 2.0.119",
  "which",
 ]
 
@@ -226,9 +220,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
 name = "bitflags"
-version = "2.13.0"
+version = "2.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
 
 [[package]]
 name = "borrow-or-share"
@@ -238,13 +232,13 @@ checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
 
 [[package]]
 name = "bstr"
-version = "1.12.1"
+version = "1.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f"
 dependencies = [
  "memchr",
  "regex-automata",
- "serde",
+ "serde_core",
 ]
 
 [[package]]
@@ -255,9 +249,9 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
 
 [[package]]
 name = "bytes"
-version = "1.11.1"
+version = "1.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
 
 [[package]]
 name = "cast"
@@ -267,9 +261,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
 
 [[package]]
 name = "cc"
-version = "1.2.63"
+version = "1.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
+checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
 dependencies = [
  "find-msvc-tools",
  "jobserver",
@@ -333,9 +327,9 @@ dependencies = [
 
 [[package]]
 name = "clang-sys"
-version = "1.8.1"
+version = "1.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
 dependencies = [
  "glob",
  "libc",
@@ -344,9 +338,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.6.1"
+version = "4.6.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -354,9 +348,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.6.0"
+version = "4.6.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
 dependencies = [
  "anstream",
  "anstyle",
@@ -366,14 +360,14 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.6.1"
+version = "4.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
 dependencies = [
  "heck 0.5.0",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
@@ -399,9 +393,9 @@ dependencies = [
 
 [[package]]
 name = "console"
-version = "0.16.3"
+version = "0.16.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
+checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
 dependencies = [
  "encode_unicode",
  "libc",
@@ -452,9 +446,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-deque"
-version = "0.8.6"
+version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
 dependencies = [
  "crossbeam-epoch",
  "crossbeam-utils",
@@ -462,18 +456,18 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.9.18"
+version = "0.9.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
 dependencies = [
  "crossbeam-utils",
 ]
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.21"
+version = "0.8.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
 
 [[package]]
 name = "crunchy"
@@ -501,7 +495,7 @@ dependencies = [
  "ident_case",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -512,7 +506,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
 dependencies = [
  "darling_core",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -529,6 +523,17 @@ dependencies = [
  "parking_lot_core",
 ]
 
+[[package]]
+name = "derive-where"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
 [[package]]
 name = "derive_arbitrary"
 version = "1.4.2"
@@ -537,7 +542,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -558,7 +563,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustc_version",
- "syn",
+ "syn 2.0.119",
  "unicode-xid",
 ]
 
@@ -582,13 +587,13 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
 
 [[package]]
 name = "displaydoc"
-version = "0.2.6"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
@@ -611,9 +616,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
 
 [[package]]
 name = "either"
-version = "1.16.0"
+version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
 
 [[package]]
 name = "encode_unicode"
@@ -651,15 +656,15 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
 
 [[package]]
 name = "fastrand"
-version = "2.4.1"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
 
 [[package]]
 name = "find-msvc-tools"
-version = "0.1.9"
+version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
 
 [[package]]
 name = "fixedbitset"
@@ -702,12 +707,6 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
-[[package]]
-name = "foldhash"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
-
 [[package]]
 name = "foldhash"
 version = "0.2.0"
@@ -731,9 +730,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
 
 [[package]]
 name = "futures"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -746,9 +745,9 @@ dependencies = [
 
 [[package]]
 name = "futures-channel"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -756,15 +755,15 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -773,32 +772,32 @@ dependencies = [
 
 [[package]]
 name = "futures-io"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
 
 [[package]]
 name = "futures-macro"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
 name = "futures-sink"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
 
 [[package]]
 name = "futures-task"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
 
 [[package]]
 name = "futures-timer"
@@ -808,9 +807,9 @@ checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
 
 [[package]]
 name = "futures-util"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -837,22 +836,20 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.4.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
 dependencies = [
  "cfg-if",
  "libc",
  "r-efi 6.0.0",
- "wasip2",
- "wasip3",
 ]
 
 [[package]]
 name = "glob"
-version = "0.3.3"
+version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
 
 [[package]]
 name = "half"
@@ -871,22 +868,13 @@ version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
 
-[[package]]
-name = "hashbrown"
-version = "0.15.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
-dependencies = [
- "foldhash 0.1.5",
-]
-
 [[package]]
 name = "hashbrown"
 version = "0.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
 dependencies = [
- "foldhash 0.2.0",
+ "foldhash",
 ]
 
 [[package]]
@@ -895,14 +883,14 @@ version = "0.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
 dependencies = [
- "foldhash 0.2.0",
+ "foldhash",
 ]
 
 [[package]]
 name = "hashlink"
-version = "0.12.0"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5081f264ed7adee96ea4b4778b6bb9da0a7228b084587aa3bd3ff05da7c5a3b"
+checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248"
 dependencies = [
  "hashbrown 0.17.1",
 ]
@@ -960,9 +948,9 @@ dependencies = [
 
 [[package]]
 name = "icu_collections"
-version = "2.2.0"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
 dependencies = [
  "displaydoc",
  "potential_utf",
@@ -974,9 +962,9 @@ dependencies = [
 
 [[package]]
 name = "icu_locale_core"
-version = "2.2.0"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
 dependencies = [
  "displaydoc",
  "litemap",
@@ -987,9 +975,9 @@ dependencies = [
 
 [[package]]
 name = "icu_normalizer"
-version = "2.2.0"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
 dependencies = [
  "icu_collections",
  "icu_normalizer_data",
@@ -1001,16 +989,17 @@ dependencies = [
 
 [[package]]
 name = "icu_normalizer_data"
-version = "2.2.0"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
 
 [[package]]
 name = "icu_properties"
-version = "2.2.0"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
 dependencies = [
+ "displaydoc",
  "icu_collections",
  "icu_locale_core",
  "icu_properties_data",
@@ -1021,15 +1010,15 @@ dependencies = [
 
 [[package]]
 name = "icu_properties_data"
-version = "2.2.0"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
 
 [[package]]
 name = "icu_provider"
-version = "2.2.0"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428"
 dependencies = [
  "displaydoc",
  "icu_locale_core",
@@ -1040,12 +1029,6 @@ dependencies = [
  "zerovec",
 ]
 
-[[package]]
-name = "id-arena"
-version = "2.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
-
 [[package]]
 name = "ident_case"
 version = "1.0.1"
@@ -1081,15 +1064,13 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
 dependencies = [
  "equivalent",
  "hashbrown 0.17.1",
- "serde",
- "serde_core",
 ]
 
 [[package]]
 name = "insta"
-version = "1.47.2"
+version = "1.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e"
+checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82"
 dependencies = [
  "console",
  "once_cell",
@@ -1139,23 +1120,22 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
 
 [[package]]
 name = "jobserver"
-version = "0.1.34"
+version = "0.1.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
 dependencies = [
- "getrandom 0.3.4",
+ "getrandom 0.4.3",
  "libc",
 ]
 
 [[package]]
 name = "js-sys"
-version = "0.3.99"
+version = "0.3.104"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
+checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
 dependencies = [
  "cfg-if",
  "futures-util",
- "once_cell",
  "wasm-bindgen",
 ]
 
@@ -1171,17 +1151,11 @@ version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
 
-[[package]]
-name = "leb128fmt"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
-
 [[package]]
 name = "libc"
-version = "0.2.186"
+version = "0.2.189"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
 
 [[package]]
 name = "libfuzzer-sys"
@@ -1205,9 +1179,9 @@ dependencies = [
 
 [[package]]
 name = "libsqlite3-sys"
-version = "0.38.1"
+version = "0.38.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6c19a05435c21ac299d71b6a9c13db3e3f47c520517d58990a462a1397a61db"
+checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8"
 dependencies = [
  "cc",
  "pkg-config",
@@ -1228,9 +1202,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
 
 [[package]]
 name = "litemap"
-version = "0.8.2"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
 
 [[package]]
 name = "lock_api"
@@ -1243,9 +1217,9 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.32"
+version = "0.4.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
 
 [[package]]
 name = "ls-types"
@@ -1253,7 +1227,7 @@ version = "0.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "896e16b8e17d8732b9efe4d5b66cb0cc162b3023a2d8122f2aea6f7f185e0a67"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags 2.13.1",
  "fluent-uri 0.4.1",
  "percent-encoding",
  "serde",
@@ -1294,9 +1268,9 @@ dependencies = [
 
 [[package]]
 name = "memchr"
-version = "2.8.1"
+version = "2.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
 
 [[package]]
 name = "minimal-lexical"
@@ -1346,9 +1320,9 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.37.3"
+version = "0.39.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
+checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b"
 dependencies = [
  "memchr",
 ]
@@ -1380,30 +1354,6 @@ dependencies = [
  "num-traits",
 ]
 
-[[package]]
-name = "ouroboros"
-version = "0.18.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59"
-dependencies = [
- "aliasable",
- "ouroboros_macro",
- "static_assertions",
-]
-
-[[package]]
-name = "ouroboros_macro"
-version = "0.18.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0"
-dependencies = [
- "heck 0.4.1",
- "proc-macro2",
- "proc-macro2-diagnostics",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "page_size"
 version = "0.6.0"
@@ -1461,9 +1411,9 @@ dependencies = [
 
 [[package]]
 name = "pg_query"
-version = "6.1.1"
+version = "6.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ca6fdb8f9d32182abf17328789f87f305dd8c8ce5bf48c5aa2b5cffc94e1c04"
+checksum = "42a66643c4aa9c929b68b2c9e30d6c4d955bfd1e87e255ce6c5d0c026082436c"
 dependencies = [
  "bindgen",
  "cc",
@@ -1485,9 +1435,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
 
 [[package]]
 name = "pkg-config"
-version = "0.3.33"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
 
 [[package]]
 name = "plotters"
@@ -1529,9 +1479,9 @@ dependencies = [
 
 [[package]]
 name = "potential_utf"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
 dependencies = [
  "zerovec",
 ]
@@ -1582,7 +1532,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
 dependencies = [
  "proc-macro2",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -1596,26 +1546,13 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.106"
+version = "1.0.107"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
 dependencies = [
  "unicode-ident",
 ]
 
-[[package]]
-name = "proc-macro2-diagnostics"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
- "version_check",
- "yansi",
-]
-
 [[package]]
 name = "proptest"
 version = "1.11.0"
@@ -1624,7 +1561,7 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
 dependencies = [
  "bit-set",
  "bit-vec",
- "bitflags 2.13.0",
+ "bitflags 2.13.1",
  "num-traits",
  "rand",
  "rand_chacha",
@@ -1661,7 +1598,7 @@ dependencies = [
  "prost",
  "prost-types",
  "regex",
- "syn",
+ "syn 2.0.119",
  "tempfile",
 ]
 
@@ -1675,7 +1612,7 @@ dependencies = [
  "itertools 0.14.0",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -1689,9 +1626,9 @@ dependencies = [
 
 [[package]]
 name = "psm"
-version = "0.1.31"
+version = "0.1.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea"
+checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622"
 dependencies = [
  "ar_archive_writer",
  "cc",
@@ -1705,9 +1642,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
 
 [[package]]
 name = "quote"
-version = "1.0.45"
+version = "1.0.47"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
 dependencies = [
  "proc-macro2",
 ]
@@ -1726,9 +1663,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
 
 [[package]]
 name = "rand"
-version = "0.9.4"
+version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
 dependencies = [
  "rand_chacha",
  "rand_core",
@@ -1799,7 +1736,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b"
 dependencies = [
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -1808,34 +1745,34 @@ version = "0.5.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags 2.13.1",
 ]
 
 [[package]]
 name = "ref-cast"
-version = "1.0.25"
+version = "1.0.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
+checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
 dependencies = [
  "ref-cast-impl",
 ]
 
 [[package]]
 name = "ref-cast-impl"
-version = "1.0.25"
+version = "1.0.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
+checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
 name = "regex"
-version = "1.12.3"
+version = "1.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -1845,9 +1782,9 @@ dependencies = [
 
 [[package]]
 name = "regex-automata"
-version = "0.4.14"
+version = "0.4.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -1856,9 +1793,9 @@ dependencies = [
 
 [[package]]
 name = "regex-syntax"
-version = "0.8.10"
+version = "0.8.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
 
 [[package]]
 name = "relative-path"
@@ -1873,7 +1810,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
 dependencies = [
  "hashbrown 0.16.1",
- "thiserror 2.0.18",
+ "thiserror 2.0.20",
 ]
 
 [[package]]
@@ -1901,7 +1838,7 @@ dependencies = [
  "regex",
  "relative-path",
  "rustc_version",
- "syn",
+ "syn 2.0.119",
  "unicode-ident",
 ]
 
@@ -1913,16 +1850,16 @@ checksum = "34bef7b9430b9f9e666d930202e1344765b623203affe2f779bcd1f269384248"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
 name = "rusqlite"
-version = "0.40.1"
+version = "0.40.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323"
+checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags 2.13.1",
  "fallible-iterator",
  "fallible-streaming-iterator",
  "hashlink",
@@ -1939,9 +1876,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
 [[package]]
 name = "rustc-hash"
-version = "2.1.2"
+version = "2.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
 
 [[package]]
 name = "rustc_version"
@@ -1958,7 +1895,7 @@ version = "0.38.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags 2.13.1",
  "errno",
  "libc",
  "linux-raw-sys 0.4.15",
@@ -1971,7 +1908,7 @@ version = "1.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags 2.13.1",
  "errno",
  "libc",
  "linux-raw-sys 0.12.1",
@@ -1980,9 +1917,9 @@ dependencies = [
 
 [[package]]
 name = "rustversion"
-version = "1.0.22"
+version = "1.0.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
 
 [[package]]
 name = "rusty-fork"
@@ -2013,9 +1950,9 @@ dependencies = [
 
 [[package]]
 name = "schemars"
-version = "1.2.1"
+version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
+checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
 dependencies = [
  "dyn-clone",
  "ref-cast",
@@ -2026,14 +1963,14 @@ dependencies = [
 
 [[package]]
 name = "schemars_derive"
-version = "1.2.1"
+version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f"
+checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba"
 dependencies = [
  "proc-macro2",
  "quote",
  "serde_derive_internals",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
@@ -2044,47 +1981,48 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
 [[package]]
 name = "sea-orm"
-version = "2.0.0-rc.40"
+version = "2.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "628c3b6acb53ca9942f7f151431ed49db92dafa14d15976a1b9db9d4bd06431c"
+checksum = "a334e83ced3ae3ee44db0f84d1fcf8d2087a1ad9bb9036f00f9f6067156ea197"
 dependencies = [
  "async-stream",
  "async-trait",
+ "derive-where",
  "derive_more",
  "futures-util",
  "itertools 0.14.0",
  "log",
- "ouroboros",
  "sea-orm-macros",
  "sea-query",
  "serde",
  "strum",
- "thiserror 2.0.18",
+ "thiserror 2.0.20",
  "tracing",
  "url",
+ "uuid",
  "web-time",
 ]
 
 [[package]]
 name = "sea-orm-macros"
-version = "2.0.0-rc.40"
+version = "2.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68a91def07bceb98aab308f7dd16c27496b76a6b7b92b94a61b309b5043d93d5"
+checksum = "4039a86f9acc4d3b52747508b347dddc6fd725bbc429902ebeb6d26225fc2528"
 dependencies = [
  "heck 0.5.0",
  "itertools 0.14.0",
  "pluralizer",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
  "unicode-ident",
 ]
 
 [[package]]
 name = "sea-query"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d190cfb3bcceb8a8d7d04dee5a0c77f60c7627979cdcb47fdcb8934f009badf"
+checksum = "546040c653a705e60ec65ecd3191a809603734bebbc225775916dea9ae409b31"
 dependencies = [
  "itoa",
  "ordered-float",
@@ -2101,8 +2039,8 @@ dependencies = [
  "heck 0.4.1",
  "proc-macro2",
  "quote",
- "syn",
- "thiserror 2.0.18",
+ "syn 2.0.119",
+ "thiserror 2.0.20",
 ]
 
 [[package]]
@@ -2113,9 +2051,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
 
 [[package]]
 name = "serde"
-version = "1.0.228"
+version = "1.0.229"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
 dependencies = [
  "serde_core",
  "serde_derive",
@@ -2123,40 +2061,40 @@ dependencies = [
 
 [[package]]
 name = "serde_core"
-version = "1.0.228"
+version = "1.0.229"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.228"
+version = "1.0.229"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
 name = "serde_derive_internals"
-version = "0.29.1"
+version = "0.30.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.150"
+version = "1.0.151"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
 dependencies = [
  "indexmap",
  "itoa",
@@ -2168,13 +2106,13 @@ dependencies = [
 
 [[package]]
 name = "serde_repr"
-version = "0.1.20"
+version = "0.1.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
@@ -2201,9 +2139,9 @@ dependencies = [
 
 [[package]]
 name = "serial_test"
-version = "3.5.0"
+version = "4.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d"
+checksum = "a6df5ed973ad8d834e09f824f9e9f449af6b9a3745f78dec7cc752770bd3bf11"
 dependencies = [
  "futures-executor",
  "futures-util",
@@ -2215,13 +2153,13 @@ dependencies = [
 
 [[package]]
 name = "serial_test_derive"
-version = "3.5.0"
+version = "4.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c"
+checksum = "a22144e767da4ddd8416dbf383700542ffd8a5dc493dfecedfe1fe3ad03c98ae"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
@@ -2265,9 +2203,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
 
 [[package]]
 name = "smallvec"
-version = "1.15.1"
+version = "1.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
 
 [[package]]
 name = "sqlite-wasm-rs"
@@ -2299,9 +2237,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
 
 [[package]]
 name = "stacker"
-version = "0.1.24"
+version = "0.1.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190"
+checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967"
 dependencies = [
  "cc",
  "cfg-if",
@@ -2310,12 +2248,6 @@ dependencies = [
  "windows-sys 0.61.2",
 ]
 
-[[package]]
-name = "static_assertions"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
-
 [[package]]
 name = "streaming-iterator"
 version = "0.1.9"
@@ -2336,9 +2268,20 @@ checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
 
 [[package]]
 name = "syn"
-version = "2.0.117"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2359,14 +2302,14 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
 name = "target-triple"
-version = "1.0.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b"
+checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171"
 
 [[package]]
 name = "tempfile"
@@ -2375,7 +2318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
 dependencies = [
  "fastrand",
- "getrandom 0.4.2",
+ "getrandom 0.4.3",
  "once_cell",
  "rustix 1.1.4",
  "windows-sys 0.61.2",
@@ -2407,11 +2350,11 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "2.0.18"
+version = "2.0.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
 dependencies = [
- "thiserror-impl 2.0.18",
+ "thiserror-impl 2.0.20",
 ]
 
 [[package]]
@@ -2422,34 +2365,34 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "2.0.18"
+version = "2.0.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
 name = "thread_local"
-version = "1.1.9"
+version = "1.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
 dependencies = [
  "cfg-if",
 ]
 
 [[package]]
 name = "tinystr"
-version = "0.8.3"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
 dependencies = [
  "displaydoc",
  "zerovec",
@@ -2467,9 +2410,9 @@ dependencies = [
 
 [[package]]
 name = "tokio"
-version = "1.52.3"
+version = "1.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
 dependencies = [
  "pin-project-lite",
  "tokio-macros",
@@ -2477,33 +2420,34 @@ dependencies = [
 
 [[package]]
 name = "tokio-macros"
-version = "2.7.0"
+version = "2.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
 name = "tokio-util"
-version = "0.7.18"
+version = "0.7.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
 dependencies = [
  "bytes",
  "futures-core",
  "futures-sink",
+ "libc",
  "pin-project-lite",
  "tokio",
 ]
 
 [[package]]
 name = "toml"
-version = "1.1.2+spec-1.1.0"
+version = "1.1.4+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
+checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
 dependencies = [
  "indexmap",
  "serde_core",
@@ -2525,9 +2469,9 @@ dependencies = [
 
 [[package]]
 name = "toml_edit"
-version = "0.25.12+spec-1.1.0"
+version = "0.25.13+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
+checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
 dependencies = [
  "indexmap",
  "toml_datetime",
@@ -2537,18 +2481,18 @@ dependencies = [
 
 [[package]]
 name = "toml_parser"
-version = "1.1.2+spec-1.1.0"
+version = "1.1.3+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
 dependencies = [
  "winnow",
 ]
 
 [[package]]
 name = "toml_writer"
-version = "1.1.1+spec-1.1.0"
+version = "1.1.2+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
+checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
 
 [[package]]
 name = "tower"
@@ -2616,7 +2560,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -2660,9 +2604,9 @@ dependencies = [
 
 [[package]]
 name = "tree-sitter"
-version = "0.26.9"
+version = "0.26.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dab76d0b724ba557954125188cf0633a1ca43199ced82d95c7b9c32cc3de1f3"
+checksum = "83c567a8e18ae93f20982c90370b16fd24023aeaf52f6052b96957ab253a0fec"
 dependencies = [
  "cc",
  "regex",
@@ -2700,9 +2644,9 @@ dependencies = [
 
 [[package]]
 name = "trybuild"
-version = "1.0.116"
+version = "1.0.120"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47c635f0191bd3a2941013e5062667100969f8c4e9cd787c14f977265d73616e"
+checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4"
 dependencies = [
  "dissimilar",
  "glob",
@@ -2769,12 +2713,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
 name = "uuid"
-version = "1.23.2"
+version = "1.24.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7"
+checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9"
 dependencies = [
- "getrandom 0.4.2",
+ "getrandom 0.4.3",
  "js-sys",
+ "serde_core",
  "wasm-bindgen",
 ]
 
@@ -2790,12 +2735,6 @@ version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
-[[package]]
-name = "version_check"
-version = "0.9.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
-
 [[package]]
 name = "vespertide"
 version = "0.2.1"
@@ -2860,7 +2799,7 @@ dependencies = [
  "sea-orm",
  "serde",
  "serde_json",
- "thiserror 2.0.18",
+ "thiserror 2.0.20",
  "vespertide-naming",
 ]
 
@@ -2873,7 +2812,7 @@ dependencies = [
  "proptest",
  "rayon",
  "rstest",
- "thiserror 2.0.18",
+ "thiserror 2.0.20",
  "vespertide-config",
  "vespertide-core",
  "vespertide-naming",
@@ -2921,7 +2860,7 @@ dependencies = [
  "lsp-types",
  "proptest",
  "rstest",
- "rustc-hash 2.1.2",
+ "rustc-hash 2.1.3",
  "serde",
  "serde_json",
  "serde_yaml",
@@ -2949,7 +2888,7 @@ dependencies = [
  "quote",
  "runtime-macros",
  "serial_test",
- "syn",
+ "syn 3.0.3",
  "tempfile",
  "trybuild",
  "vespertide-config",
@@ -2978,7 +2917,7 @@ dependencies = [
  "rayon",
  "rstest",
  "serial_test",
- "thiserror 2.0.18",
+ "thiserror 2.0.20",
  "vespertide-core",
  "vespertide-naming",
 ]
@@ -2997,7 +2936,7 @@ dependencies = [
  "sea-query",
  "serial_test",
  "sqlparser",
- "thiserror 2.0.18",
+ "thiserror 2.0.20",
  "vespertide-core",
  "vespertide-naming",
  "vespertide-planner",
@@ -3039,27 +2978,18 @@ dependencies = [
 
 [[package]]
 name = "wasip2"
-version = "1.0.3+wasi-0.2.9"
+version = "1.0.4+wasi-0.2.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
 dependencies = [
- "wit-bindgen 0.57.1",
-]
-
-[[package]]
-name = "wasip3"
-version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
-dependencies = [
- "wit-bindgen 0.51.0",
+ "wit-bindgen",
 ]
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.122"
+version = "0.2.127"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
+checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
 dependencies = [
  "cfg-if",
  "once_cell",
@@ -3070,9 +3000,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.122"
+version = "0.2.127"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
+checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -3080,65 +3010,31 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.122"
+version = "0.2.127"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
+checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
 dependencies = [
  "bumpalo",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.122"
+version = "0.2.127"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
+checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
 dependencies = [
  "unicode-ident",
 ]
 
-[[package]]
-name = "wasm-encoder"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
-dependencies = [
- "leb128fmt",
- "wasmparser",
-]
-
-[[package]]
-name = "wasm-metadata"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
-dependencies = [
- "anyhow",
- "indexmap",
- "wasm-encoder",
- "wasmparser",
-]
-
-[[package]]
-name = "wasmparser"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
-dependencies = [
- "bitflags 2.13.0",
- "hashbrown 0.15.5",
- "indexmap",
- "semver",
-]
-
 [[package]]
 name = "web-sys"
-version = "0.3.99"
+version = "0.3.104"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436"
+checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -3218,7 +3114,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -3229,7 +3125,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -3340,118 +3236,24 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
 name = "winnow"
-version = "1.0.3"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
 dependencies = [
  "memchr",
 ]
 
-[[package]]
-name = "wit-bindgen"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
-dependencies = [
- "wit-bindgen-rust-macro",
-]
-
 [[package]]
 name = "wit-bindgen"
 version = "0.57.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
 
-[[package]]
-name = "wit-bindgen-core"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
-dependencies = [
- "anyhow",
- "heck 0.5.0",
- "wit-parser",
-]
-
-[[package]]
-name = "wit-bindgen-rust"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
-dependencies = [
- "anyhow",
- "heck 0.5.0",
- "indexmap",
- "prettyplease",
- "syn",
- "wasm-metadata",
- "wit-bindgen-core",
- "wit-component",
-]
-
-[[package]]
-name = "wit-bindgen-rust-macro"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
-dependencies = [
- "anyhow",
- "prettyplease",
- "proc-macro2",
- "quote",
- "syn",
- "wit-bindgen-core",
- "wit-bindgen-rust",
-]
-
-[[package]]
-name = "wit-component"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
-dependencies = [
- "anyhow",
- "bitflags 2.13.0",
- "indexmap",
- "log",
- "serde",
- "serde_derive",
- "serde_json",
- "wasm-encoder",
- "wasm-metadata",
- "wasmparser",
- "wit-parser",
-]
-
-[[package]]
-name = "wit-parser"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
-dependencies = [
- "anyhow",
- "id-arena",
- "indexmap",
- "log",
- "semver",
- "serde",
- "serde_derive",
- "serde_json",
- "unicode-xid",
- "wasmparser",
-]
-
 [[package]]
 name = "writeable"
-version = "0.6.3"
+version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
-
-[[package]]
-name = "yansi"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
 
 [[package]]
 name = "yoke"
@@ -3472,28 +3274,28 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
  "synstructure",
 ]
 
 [[package]]
 name = "zerocopy"
-version = "0.8.50"
+version = "0.8.56"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
+checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
 dependencies = [
  "zerocopy-derive",
 ]
 
 [[package]]
 name = "zerocopy-derive"
-version = "0.8.50"
+version = "0.8.56"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
+checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -3513,21 +3315,21 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
  "synstructure",
 ]
 
 [[package]]
 name = "zeroize"
-version = "1.8.2"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
 
 [[package]]
 name = "zerotrie"
-version = "0.2.4"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
 dependencies = [
  "displaydoc",
  "yoke",
@@ -3536,9 +3338,9 @@ dependencies = [
 
 [[package]]
 name = "zerovec"
-version = "0.11.6"
+version = "0.11.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
 dependencies = [
  "yoke",
  "zerofrom",
@@ -3547,17 +3349,17 @@ dependencies = [
 
 [[package]]
 name = "zerovec-derive"
-version = "0.11.3"
+version = "0.11.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
 name = "zmij"
-version = "1.0.21"
+version = "1.0.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/apps/landing/package.json b/apps/landing/package.json
index e5adda5f..58c6a118 100644
--- a/apps/landing/package.json
+++ b/apps/landing/package.json
@@ -18,27 +18,27 @@
     "@devup-ui/reset-css": "^1",
     "@mdx-js/loader": "^3.1.1",
     "@mdx-js/react": "^3.1.1",
-    "@next/mdx": "^16.2.7",
+    "@next/mdx": "^16.3.1",
     "clsx": "^2.1.1",
     "next": "^16",
     "react": "^19",
-    "rehype-pretty-code": "^0.14.3",
+    "rehype-pretty-code": "^0.14.5",
     "rehype-sanitize": "^6.0.0",
     "rehype-slug": "^6.0.0",
     "rehype-stringify": "^10.0.1",
     "remark-parse": "^11.0.0",
     "remark-rehype": "^11.1.2",
-    "shiki": "^4.2.0",
+    "shiki": "^4.4.3",
     "unified": "^11.0.5"
   },
   "devDependencies": {
     "@types/mdx": "^2.0.14",
     "@devup-api/next-plugin": "^0.1",
     "@devup-ui/next-plugin": "^1",
-    "@types/node": "^25",
+    "@types/node": "^26",
     "@types/react": "^19",
     "@types/react-syntax-highlighter": "^15.5.13",
     "babel-plugin-react-compiler": "^1.0.0",
-    "typescript": "^6.0.3"
+    "typescript": "^7.0.2"
   }
 }
\ No newline at end of file
diff --git a/apps/vscode-extension/package.json b/apps/vscode-extension/package.json
index ad961744..1f2cbcfb 100644
--- a/apps/vscode-extension/package.json
+++ b/apps/vscode-extension/package.json
@@ -175,16 +175,16 @@
     "test": "vscode-test"
   },
   "dependencies": {
-    "vscode-languageclient": "^10.0.0"
+    "vscode-languageclient": "^10.1.0"
   },
   "devDependencies": {
-    "@types/node": "^25.9.2",
-    "@types/vscode": "^1.120.0",
-    "@vscode/test-cli": "^0.0.12",
+    "@types/node": "^26.2.0",
+    "@types/vscode": "^1.134.0",
+    "@vscode/test-cli": "^0.0.15",
     "@vscode/vsce": "^3.9.2",
-    "esbuild": "^0.28.0",
-    "ovsx": "^1.0.0",
-    "typescript": "^6.0.3"
+    "esbuild": "^0.28.2",
+    "ovsx": "^1.1.1",
+    "typescript": "^7.0.2"
   },
   "vsce": {
     "dependencies": false
diff --git a/bun.lock b/bun.lock
index 2be0ba3c..65950845 100644
--- a/bun.lock
+++ b/bun.lock
@@ -8,9 +8,9 @@
         "@devup-ui/bun-plugin": "^1.0",
         "@types/bun": "^1.3",
         "bun-test-env-dom": "^1.0",
-        "eslint-plugin-devup": "^2.0.19",
+        "eslint-plugin-devup": "^2.0.20",
         "husky": "^9.1",
-        "oxlint": "^1.68.0",
+        "oxlint": "^1.79.0",
       },
     },
     "apps/landing": {
@@ -24,44 +24,44 @@
         "@devup-ui/reset-css": "^1",
         "@mdx-js/loader": "^3.1.1",
         "@mdx-js/react": "^3.1.1",
-        "@next/mdx": "^16.2.7",
+        "@next/mdx": "^16.3.1",
         "clsx": "^2.1.1",
         "next": "^16",
         "react": "^19",
-        "rehype-pretty-code": "^0.14.3",
+        "rehype-pretty-code": "^0.14.5",
         "rehype-sanitize": "^6.0.0",
         "rehype-slug": "^6.0.0",
         "rehype-stringify": "^10.0.1",
         "remark-parse": "^11.0.0",
         "remark-rehype": "^11.1.2",
-        "shiki": "^4.2.0",
+        "shiki": "^4.4.3",
         "unified": "^11.0.5",
       },
       "devDependencies": {
         "@devup-api/next-plugin": "^0.1",
         "@devup-ui/next-plugin": "^1",
         "@types/mdx": "^2.0.14",
-        "@types/node": "^25",
+        "@types/node": "^26",
         "@types/react": "^19",
         "@types/react-syntax-highlighter": "^15.5.13",
         "babel-plugin-react-compiler": "^1.0.0",
-        "typescript": "^6.0.3",
+        "typescript": "^7.0.2",
       },
     },
     "apps/vscode-extension": {
       "name": "vespertide",
-      "version": "0.1.0",
+      "version": "0.1.1",
       "dependencies": {
-        "vscode-languageclient": "^10.0.0",
+        "vscode-languageclient": "^10.1.0",
       },
       "devDependencies": {
-        "@types/node": "^25.9.2",
-        "@types/vscode": "^1.120.0",
-        "@vscode/test-cli": "^0.0.12",
+        "@types/node": "^26.2.0",
+        "@types/vscode": "^1.134.0",
+        "@vscode/test-cli": "^0.0.15",
         "@vscode/vsce": "^3.9.2",
-        "esbuild": "^0.28.0",
-        "ovsx": "^1.0.0",
-        "typescript": "^6.0.3",
+        "esbuild": "^0.28.2",
+        "ovsx": "^1.1.1",
+        "typescript": "^7.0.2",
       },
     },
   },
@@ -72,27 +72,29 @@
 
     "@azu/style-format": ["@azu/style-format@1.0.1", "", { "dependencies": { "@azu/format-text": "^1.0.1" } }, "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g=="],
 
-    "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="],
+    "@azure/abort-controller": ["@azure/abort-controller@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg=="],
+
+    "@azure/core-auth": ["@azure/core-auth@1.11.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg=="],
 
-    "@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="],
+    "@azure/core-client": ["@azure/core-client@1.11.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-JjQWO6akOck45PH/XBrxzsQGAiKrfFl4m5iggJ0ItMIz5omRufOXWpqCPpdjKN3vKDzlSUvFjaMb7Zwf0gvAdA=="],
 
-    "@azure/core-client": ["@azure/core-client@1.10.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-1D2LpsU7y9xrqKjdIbsB7PlrRePw0xsVV8p+AKTlzITrWmscajryfJCdDJB/oGwvDI5HmRo04eMMADB67uwAwQ=="],
+    "@azure/core-process": ["@azure/core-process@1.0.0", "", {}, "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg=="],
 
-    "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.24.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.4", "tslib": "^2.6.2" } }, "sha512-PpLsoDQ3AMmKZ0VU+0GrmqMxgp/sExjlVm4R+nLWngeoEGAzOIPVifaxKGU5gMv+nWELUoHfvrolWD+ZS/nFJg=="],
+    "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.25.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.4", "tslib": "^2.6.2" } }, "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA=="],
 
-    "@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="],
+    "@azure/core-tracing": ["@azure/core-tracing@1.4.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ=="],
 
-    "@azure/core-util": ["@azure/core-util@1.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A=="],
+    "@azure/core-util": ["@azure/core-util@1.14.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw=="],
 
-    "@azure/identity": ["@azure/identity@4.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", "@azure/msal-node": "^5.1.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw=="],
+    "@azure/identity": ["@azure/identity@4.13.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-process": "^1.0.0", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", "@azure/msal-node": "^5.1.5", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-NXL2/pCJctLxgw8bvrwwgge743kEq8LBT+O1pmV0vyUwetzFPH9auP6jhkU/cgZCPPtWoewAe3ncaGCgPo07fA=="],
 
-    "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="],
+    "@azure/logger": ["@azure/logger@1.4.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA=="],
 
-    "@azure/msal-browser": ["@azure/msal-browser@5.12.0", "", { "dependencies": { "@azure/msal-common": "16.7.0" } }, "sha512-eNf2aqx1C6I0yT1GEu5ukblFrmaBXGfe1bivpmlfqvK7giPZvoXLa404C8EfeHVsy6EIryfQuPRzuW1fPxWlHg=="],
+    "@azure/msal-browser": ["@azure/msal-browser@5.19.0", "", { "dependencies": { "@azure/msal-common": "16.13.0" } }, "sha512-DHe9iRcyByGJuLPkl0K31a1JjOdRY2zX38Q07mQpSbR8zOj1EIgsWfTXhSQVyyijUxlcofVy/br7qWJbrMwVXQ=="],
 
-    "@azure/msal-common": ["@azure/msal-common@16.7.0", "", {}, "sha512-Jb8Y7pX6KM42SIT7KWP6YbY3+vLbwB5b5m+tpiiOzMU1QeyelQzs9lO8jv1e7/Uj9r7tg7VjPvW4T0KB1jF3UQ=="],
+    "@azure/msal-common": ["@azure/msal-common@16.13.0", "", {}, "sha512-rOAy0KUcyBbdwVJ+f3uPpthXatFLLZN+/KWAsTLzk1aB23Xl9DRmmXYwSvBFOZyXj4jUQQ5FKxxRkhAFW1fOow=="],
 
-    "@azure/msal-node": ["@azure/msal-node@5.2.3", "", { "dependencies": { "@azure/msal-common": "16.7.0", "jsonwebtoken": "^9.0.0" } }, "sha512-YYX4TchEVddVBiybKvKhV9QO/q22jgewP+BVxKG7Uh115voPcviGlypbKERDsqQdAiSTJrwi80gcWFjYKdo8+Q=="],
+    "@azure/msal-node": ["@azure/msal-node@5.6.0", "", { "dependencies": { "@azure/msal-common": "16.13.0", "jsonwebtoken": "^9.0.0" } }, "sha512-uFY9NxrWHw8PwZx7gAX6PDn+9vdfS05+levc/kwkx77IkjfaldnQbbcQzzDIZ5Hq5Zdr6/z92oAIoRWKp6MnOA=="],
 
     "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
 
@@ -100,7 +102,7 @@
 
     "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
 
-    "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="],
+    "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
 
     "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
 
@@ -118,31 +120,31 @@
 
     "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
 
-    "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
+    "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
 
     "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
 
     "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
 
-    "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
+    "@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
 
-    "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
+    "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
 
     "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
 
-    "@devup-api/core": ["@devup-api/core@0.1.18", "", {}, "sha512-x4vaAT4zqBMq2nwXEY8AK2Qdx7+vUmDmziR0UHxE7D+8ODqX4H3iZo46a0/aXihiS7zqK3UznZOZCygiQGNNPQ=="],
+    "@devup-api/core": ["@devup-api/core@0.1.19", "", {}, "sha512-sIrq8tHJwScuo3kHRSaRLUEYZDkZ5uxlx5h1hez4vRqGlHKAJuyD9UVLLe+aVshqGqiwc3/obpoiiwdrirvIfw=="],
 
-    "@devup-api/fetch": ["@devup-api/fetch@0.1.21", "", { "dependencies": { "@devup-api/core": "^0.1.18" } }, "sha512-Ify9XIH9qoX3AJot/IYZPjewp6vKXiD7s3nbEoeUGWzfXiCCvXCNKvAYsCgZi1c44/lgFNZPnx9vbqTXkh4jng=="],
+    "@devup-api/fetch": ["@devup-api/fetch@0.1.22", "", { "dependencies": { "@devup-api/core": "^0.1.19" } }, "sha512-Pk6YskC+G/XRfCUBqfxpi9NYOxsq5sRTfDy4n5B9anO3X3V6IWxeJSAekwDV+5Rtukn9fGRmizcN08zCarXcHQ=="],
 
-    "@devup-api/generator": ["@devup-api/generator@0.1.24", "", { "dependencies": { "@devup-api/core": "^0.1.18", "@devup-api/utils": "^0.1.10" } }, "sha512-BOCYyMfqT5cMmUNS5oHkzglPN3N55ILkR9uMRjAMMS7BuKuYFMfdUAwf0EGNzvNUePoBloYeW5ztGZNlDjDQjA=="],
+    "@devup-api/generator": ["@devup-api/generator@0.1.25", "", { "dependencies": { "@devup-api/core": "^0.1.19", "@devup-api/utils": "^0.1.11" } }, "sha512-EY7XYT2aWo2FpYtu5Y08ZWpAvGp6KpHD6+caYwNt/PL8uv0dDUx8CjqPYFZFQ1ebAJssSguQRIgoYdPj9HhaHQ=="],
 
-    "@devup-api/next-plugin": ["@devup-api/next-plugin@0.1.13", "", { "dependencies": { "@devup-api/core": "^0.1.18", "@devup-api/generator": "^0.1.24", "@devup-api/utils": "^0.1.10", "@devup-api/webpack-plugin": "^0.1.13" }, "peerDependencies": { "next": "*" } }, "sha512-CffM3tJCZLbD3rYrWRNqAKv97VjLIEe2hHCBeZ/iyd5wWWBiac3I/8uWM12SlZ/BJ3yP0cH7ZdqHZvcWcSFrvw=="],
+    "@devup-api/next-plugin": ["@devup-api/next-plugin@0.1.14", "", { "dependencies": { "@devup-api/core": "^0.1.19", "@devup-api/generator": "^0.1.25", "@devup-api/utils": "^0.1.11", "@devup-api/webpack-plugin": "^0.1.14" }, "peerDependencies": { "next": "*" } }, "sha512-DZrPZeHQ7bYK92EwecGoaxziZaYSiOZfjx1J2U1etQ7ajlu9iNFCnTkzTJDjUpvUc5ouCm5kc/7TJXnDxGpb5Q=="],
 
-    "@devup-api/react-query": ["@devup-api/react-query@0.1.13", "", { "dependencies": { "@devup-api/fetch": "^0.1.20", "@tanstack/react-query": ">=5.96" }, "peerDependencies": { "react": "*" } }, "sha512-hhZqiKbqdlHB2qUOqvhgEN6vkED+VZ4Rzt6HoPw26bOx98rve6AoafWY/meYKRFCE9oHwmNpsz2ARyLboWWVVg=="],
+    "@devup-api/react-query": ["@devup-api/react-query@0.1.16", "", { "dependencies": { "@devup-api/fetch": "^0.1.22" }, "peerDependencies": { "@tanstack/react-query": ">=5.0.0", "react": "*" } }, "sha512-mba1ImWScyyMG0Clg/2s9djP8vJXBjTH7VRNMve7LpiEFdf1AvWxGFC481dNdXhwvECsB0+zByFLpqC3uzIYmQ=="],
 
-    "@devup-api/utils": ["@devup-api/utils@0.1.10", "", {}, "sha512-FT5YCVdq0W2ftTavg+iQou5viJFgBV0/SNOalgUu4/lvaflP6qwP1C2ECbJiW6u+I3PtwloKyXLuu2CFYpIT1g=="],
+    "@devup-api/utils": ["@devup-api/utils@0.1.11", "", {}, "sha512-+GkCGAEfe3oV9gjgeINpayN63WLKKbFYuzRdXB/k2dR0u2zoMYfPXSSMGp+2MKR1+rvzbboNzoQNOn2bJf/e5g=="],
 
-    "@devup-api/webpack-plugin": ["@devup-api/webpack-plugin@0.1.13", "", { "dependencies": { "@devup-api/core": "^0.1.18", "@devup-api/generator": "^0.1.24", "@devup-api/utils": "^0.1.10" } }, "sha512-dQMqcMMdNUtzUHdaVYm29aIAU2S3+1EXLnWI3zsbVfF8X8isWqLlmwPS5aioY7iGDIYW4nL3C4gkIrhvT2pgpA=="],
+    "@devup-api/webpack-plugin": ["@devup-api/webpack-plugin@0.1.14", "", { "dependencies": { "@devup-api/core": "^0.1.19", "@devup-api/generator": "^0.1.25", "@devup-api/utils": "^0.1.11" } }, "sha512-dnxFmwByMuQzdP2sWkYcASFSH+7B3oasdgMsF79iOjHrEF0xeL4CkGaZsHx2lVx79LuwQIhX1XV2VWXQ61QM4A=="],
 
     "@devup-ui/bun-plugin": ["@devup-ui/bun-plugin@1.0.10", "", { "dependencies": { "@devup-ui/plugin-utils": "^1.0.8", "@devup-ui/wasm": "^1.0.75" } }, "sha512-GvCtLyCtS6FMXM6rg+s34N4XRFLfOdtzcMuLe61vsCloGhWn/XChWmQtnKJ0wDU7XfFUrgJCRW5BhsnV10hKOA=="],
 
@@ -162,71 +164,67 @@
 
     "@devup-ui/webpack-plugin": ["@devup-ui/webpack-plugin@1.0.61", "", { "dependencies": { "@devup-ui/plugin-utils": "^1.0.8", "@devup-ui/wasm": "^1.0.75" } }, "sha512-YbGiXC0MxQ52cnO0Uw4EUJJoyHAf+f031hoHyn0IhetpN/wPEbOy/g4Uv+b4sZxWUlMrO2RL6TZsLfPIw7w+rQ=="],
 
-    "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
-
-    "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
-
-    "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
+    "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
 
-    "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="],
+    "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
 
-    "@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="],
+    "@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
 
-    "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="],
+    "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
 
-    "@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="],
+    "@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
 
-    "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="],
+    "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
 
-    "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="],
+    "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
 
-    "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="],
+    "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
 
-    "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="],
+    "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
 
-    "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="],
+    "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
 
-    "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="],
+    "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
 
-    "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="],
+    "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
 
-    "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="],
+    "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
 
-    "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="],
+    "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
 
-    "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="],
+    "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
 
-    "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="],
+    "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
 
-    "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="],
+    "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
 
-    "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="],
+    "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
 
-    "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="],
+    "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
 
-    "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="],
+    "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
 
-    "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="],
+    "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
 
-    "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="],
+    "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
 
-    "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="],
+    "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
 
-    "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="],
+    "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
 
-    "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="],
+    "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
 
-    "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="],
+    "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
 
-    "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="],
+    "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
 
-    "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
+    "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="],
 
     "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
 
     "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
 
-    "@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="],
+    "@eslint/config-helpers": ["@eslint/config-helpers@0.7.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw=="],
 
     "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
 
@@ -236,7 +234,7 @@
 
     "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="],
 
-    "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.10.1", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.10.1" } }, "sha512-nIT1Jdsar1KnLuqX+q3oMKwUIDcSJ4GnGMBnCtXLLL+lIpqaBBACPIo9By3NeF15XDPLDLIou8aQd3/xqZoSkQ=="],
+    "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.11.6", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.11.6" } }, "sha512-ZQ47qUTeNbGhHkCGExJ1oZhruoxKRaxO44RgFETl3T4c1rRxIBlAOnM3SAH4XHu7Ue2owJXP+jOx1vOuKuxcSg=="],
 
     "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
 
@@ -250,53 +248,89 @@
 
     "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
 
-    "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
+    "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="],
+
+    "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="],
+
+    "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "os": "freebsd" }, "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg=="],
+
+    "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="],
+
+    "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="],
+
+    "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="],
+
+    "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="],
+
+    "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw=="],
+
+    "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.2", "", { "os": "linux", "cpu": "none" }, "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w=="],
+
+    "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="],
+
+    "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="],
 
-    "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
+    "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="],
 
-    "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
+    "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="],
 
-    "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
+    "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="],
 
-    "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
+    "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="],
 
-    "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
+    "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.2" }, "os": "linux", "cpu": "ppc64" }, "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA=="],
 
-    "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
+    "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.2" }, "os": "linux", "cpu": "none" }, "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ=="],
 
-    "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
+    "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="],
 
-    "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
+    "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="],
 
-    "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
+    "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="],
 
-    "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
+    "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="],
 
-    "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
+    "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="],
 
-    "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
+    "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "cpu": "none" }, "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q=="],
 
-    "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
+    "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w=="],
 
-    "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
+    "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="],
 
-    "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
+    "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="],
 
-    "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
+    "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="],
 
-    "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
+    "@inquirer/checkbox": ["@inquirer/checkbox@4.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA=="],
 
-    "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
+    "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="],
 
-    "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
+    "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="],
 
-    "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
+    "@inquirer/editor": ["@inquirer/editor@4.2.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/external-editor": "^1.0.3", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ=="],
 
-    "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
+    "@inquirer/expand": ["@inquirer/expand@4.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew=="],
 
-    "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
+    "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="],
 
-    "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
+    "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="],
+
+    "@inquirer/input": ["@inquirer/input@4.3.1", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g=="],
+
+    "@inquirer/number": ["@inquirer/number@3.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg=="],
+
+    "@inquirer/password": ["@inquirer/password@4.0.23", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA=="],
+
+    "@inquirer/prompts": ["@inquirer/prompts@7.10.1", "", { "dependencies": { "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", "@inquirer/editor": "^4.2.23", "@inquirer/expand": "^4.0.23", "@inquirer/input": "^4.3.1", "@inquirer/number": "^3.0.23", "@inquirer/password": "^4.0.23", "@inquirer/rawlist": "^4.1.11", "@inquirer/search": "^3.2.2", "@inquirer/select": "^4.4.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg=="],
+
+    "@inquirer/rawlist": ["@inquirer/rawlist@4.1.11", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw=="],
+
+    "@inquirer/search": ["@inquirer/search@3.2.2", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA=="],
+
+    "@inquirer/select": ["@inquirer/select@4.4.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w=="],
+
+    "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
 
     "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
 
@@ -318,57 +352,79 @@
 
     "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
 
-    "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
+    "@napi-rs/keyring": ["@napi-rs/keyring@1.3.0", "", { "optionalDependencies": { "@napi-rs/keyring-darwin-arm64": "1.3.0", "@napi-rs/keyring-darwin-x64": "1.3.0", "@napi-rs/keyring-freebsd-x64": "1.3.0", "@napi-rs/keyring-linux-arm-gnueabihf": "1.3.0", "@napi-rs/keyring-linux-arm64-gnu": "1.3.0", "@napi-rs/keyring-linux-arm64-musl": "1.3.0", "@napi-rs/keyring-linux-riscv64-gnu": "1.3.0", "@napi-rs/keyring-linux-x64-gnu": "1.3.0", "@napi-rs/keyring-linux-x64-musl": "1.3.0", "@napi-rs/keyring-win32-arm64-msvc": "1.3.0", "@napi-rs/keyring-win32-ia32-msvc": "1.3.0", "@napi-rs/keyring-win32-x64-msvc": "1.3.0" } }, "sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA=="],
+
+    "@napi-rs/keyring-darwin-arm64": ["@napi-rs/keyring-darwin-arm64@1.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pl76hJvdYUBn6I24bXiOBMA9nbDapo3I5B+f3OorjDU4dUMSypXeKbOVehJe8fhgTiH24flMyTS3aAIy43xegQ=="],
+
+    "@napi-rs/keyring-darwin-x64": ["@napi-rs/keyring-darwin-x64@1.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-YcJtEV5LA3cvA4z3BurgxH5IhTsW1JfIvcAAcqcecwk06Si9F9NqkxbZVIfDwQ8oRHgaBmT3zZJnLAotCrVahw=="],
+
+    "@napi-rs/keyring-freebsd-x64": ["@napi-rs/keyring-freebsd-x64@1.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vlLf31TGhfRAaxLDBhg8b89ss0HHD/lyNmL5F3UjSaz5CUXElsJmKYq9fqA/B+cZKUEUcLHHGhF0I/CqcFdaVw=="],
+
+    "@napi-rs/keyring-linux-arm-gnueabihf": ["@napi-rs/keyring-linux-arm-gnueabihf@1.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-KiWdMMu/Inz/bHHIAGrnF7r54FZDYXuHO6UFF/rhIrshUsxbMG1Rl9lEymNtqqsVo927G0VYcb02FzWQ3iBQRQ=="],
+
+    "@napi-rs/keyring-linux-arm64-gnu": ["@napi-rs/keyring-linux-arm64-gnu@1.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-eyKGpY40lm9Jvs1aD294XRH4y7+TlJM0YVAryZeXA6TX0mb4gMkxVXwSQv7MCwgah7raeUd0dKUb4BPAYIgcMg=="],
 
-    "@next/env": ["@next/env@16.2.7", "", {}, "sha512-tMJizPlj6ZYpBMMdK8S0LJufrP4QTdR6pcv9KQ/bVETPAmg0j1mlHE9G2c38UyGHxoBapgwuj7XjbGJ2RcDFOg=="],
+    "@napi-rs/keyring-linux-arm64-musl": ["@napi-rs/keyring-linux-arm64-musl@1.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-iIK6JWHXAJqDrEyLY3TmswwloVyt2vj+04TZnew+uSJ9gnDO8EwRbp3/iw3LpWaXiDO7VomGO6y8I0Id8uBZSw=="],
 
-    "@next/mdx": ["@next/mdx@16.2.7", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-4RmM0KISxvfHr37/cn9TAGD2oy1nvTQ+ycgknz2xpd8IrY980N7XDU3CXhfKOXPhIVgbshxFF9HQEQC32ZVa9A=="],
+    "@napi-rs/keyring-linux-riscv64-gnu": ["@napi-rs/keyring-linux-riscv64-gnu@1.3.0", "", { "os": "linux", "cpu": "none" }, "sha512-/PGqrwn6EwgtK6vccASSXJRfOSP4vN1F4ASsIQ+7MdrK6hNvAJ1FZPrIuD5gGGdxezo3F++To2Wq7DbuGIeuNQ=="],
 
-    "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vm1EDI/pVaBNNiychmxk3fft+OhQPVD9cIM/tReLZIQ3TfQ4kqI9DwKk00dzuS1ulC7icbrzCFrmRRlk9PfNdw=="],
+    "@napi-rs/keyring-linux-x64-gnu": ["@napi-rs/keyring-linux-x64-gnu@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2PDK1WKWTu9lBGq9VvNEkSlQD3O7YwVpmnyN2M3cy4v7NJ/8gDMd9GXv3G+FVXN13uhp4gnnPBS+ScefmEeD2A=="],
 
-    "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-O3IRSv1ZBL1zs0WrIgefTEcTKFVn+ryxBNe54erJ6KsD+2f/Mmt7g2jOYh8PSBdUwPtKQJuCsTMlZ7tIu2AcsQ=="],
+    "@napi-rs/keyring-linux-x64-musl": ["@napi-rs/keyring-linux-x64-musl@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-oJ2HkX8YUo46QBkn0pG+HuIKQNqr523q6vBobCn+P95s4C4K6/kLBqHY/1bg5J4ap31DzsznhnFKcfBNBsjCnw=="],
 
-    "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-Re6PZtjBDd0aMU+VcZcC/PrIvj4WhrjDYtMhhCVQamWN4L90EVP0pcEOBQD25prSlw7OzNw5QpHLWMilRLsRNw=="],
+    "@napi-rs/keyring-win32-arm64-msvc": ["@napi-rs/keyring-win32-arm64-msvc@1.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-tOd3c/uAaeoE4ycVlmAdSvygz0Zt3zdca6Y7gokBeIbaRDWpjDIUOpU3MvML59XAaqyuKGsVVu0F/DZb1lHPmw=="],
 
-    "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-qyogG9QtBzWxgJfeGBvOEHI3851gTfCF3wLZ5RDLTBJGAmE9p1qDwKCOdrBrvBzRvYDT+gUDp72pzlSEfAXgNA=="],
+    "@napi-rs/keyring-win32-ia32-msvc": ["@napi-rs/keyring-win32-ia32-msvc@1.3.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-sPSqeAFZMGqP1R++M2JTza7GQJJ/TpCo6JU6Vcd4jnebvOaEDs9b7eipakU1PJdSvhpC2yXMCNRk9gXfrhuwHQ=="],
 
-    "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.7", "", { "os": "linux", "cpu": "x64" }, "sha512-Vhe4ZDuBpmMogrGi5D4R2Kq4JAQlj6+wvgaFYy31zfES0zPmt6TLA+cuYpM/OLrPZjo2MYQTHVqNUSCR6+fDZQ=="],
+    "@napi-rs/keyring-win32-x64-msvc": ["@napi-rs/keyring-win32-x64-msvc@1.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-4DnCWXwDc0HRKwyRlG5y0VhKZW2tNRQfKKfyj6IX/KWfDNyq9hn4n+GL1auyDcOO/v8PwnhmYo2+rOOqCkvvOg=="],
 
-    "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.7", "", { "os": "linux", "cpu": "x64" }, "sha512-srvian89JahFLw1YLBEuhvPJ0DO5lpUeJQMXy4xYo7g628ZlNgXdNkqoxSAv9OYrBfByh6vxISMwW/mRbzCY+g=="],
+    "@next/env": ["@next/env@16.3.1", "", {}, "sha512-35G3xwkQUb2oETSDjFXGrVugknoayLFBh7vSE+yAcl9IP2zT9wyGwq7297AYHR11kJld807t5f8AJBs6WBzXsQ=="],
 
-    "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-GX3wvLpULFuRFJzwHaKfm7QZJ18F4ZSuxlPJ96BoBglCzBmdSjyeBKF+ZhWhvL/ckxNfLnNa7bsObO2ipYpszw=="],
+    "@next/mdx": ["@next/mdx@16.3.1", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-H0oFxC5i9MH2I20eedDvHlJ61UuQ6BizOmD6QGWxtb6g27TDg1Vf4F/XegkNCnuSPfyX9U4KOHn7UQc2oYYFWQ=="],
 
-    "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.7", "", { "os": "win32", "cpu": "x64" }, "sha512-J4WlM72NMk076Qsg0jTdK3SNXatlSdnjW7L7oNGLst1tAGjHrJh/FYi+pw9wyIjEtGRKDNzD0zuiY16oWYWVaw=="],
+    "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ABMIu2zQ7cnNIHm5ivKGwZwUrm0pAai3yiJ/gK/rF1c1VP9UOnj7XECbMKFdVKp9I9eMYq9NoDs1WXOoowxzJw=="],
 
-    "@node-rs/crc32": ["@node-rs/crc32@1.10.6", "", { "optionalDependencies": { "@node-rs/crc32-android-arm-eabi": "1.10.6", "@node-rs/crc32-android-arm64": "1.10.6", "@node-rs/crc32-darwin-arm64": "1.10.6", "@node-rs/crc32-darwin-x64": "1.10.6", "@node-rs/crc32-freebsd-x64": "1.10.6", "@node-rs/crc32-linux-arm-gnueabihf": "1.10.6", "@node-rs/crc32-linux-arm64-gnu": "1.10.6", "@node-rs/crc32-linux-arm64-musl": "1.10.6", "@node-rs/crc32-linux-x64-gnu": "1.10.6", "@node-rs/crc32-linux-x64-musl": "1.10.6", "@node-rs/crc32-wasm32-wasi": "1.10.6", "@node-rs/crc32-win32-arm64-msvc": "1.10.6", "@node-rs/crc32-win32-ia32-msvc": "1.10.6", "@node-rs/crc32-win32-x64-msvc": "1.10.6" } }, "sha512-+llXfqt+UzgoDzT9of5vPQPGqTAVCohU74I9zIBkNo5TH6s2P31DFJOGsJQKN207f0GHnYv5pV3wh3BCY/un/A=="],
+    "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gNG21e/UnrroeScbY/QndUEdl0mF1FRibW7BBeYUz/5ABCepjqDdEdgr592vpzMtCn/m7FTjYq3TN4TpyDnutw=="],
 
-    "@node-rs/crc32-android-arm-eabi": ["@node-rs/crc32-android-arm-eabi@1.10.6", "", { "os": "android", "cpu": "arm" }, "sha512-vZAMuJXm3TpWPOkkhxdrofWDv+Q+I2oO7ucLRbXyAPmXFNDhHtBxbO1rk9Qzz+M3eep8ieS4/+jCL1Q0zacNMQ=="],
+    "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-6B6Lw016iwNUQuaJoraMMTLh6TwHzFUtxipSScD1F3YyymcrRWkobodRS2ftIOkF5vrs4zNlyUrTC5YZQ9Lz5w=="],
 
-    "@node-rs/crc32-android-arm64": ["@node-rs/crc32-android-arm64@1.10.6", "", { "os": "android", "cpu": "arm64" }, "sha512-Vl/JbjCinCw/H9gEpZveWCMjxjcEChDcDBM8S4hKay5yyoRCUHJPuKr4sjVDBeOm+1nwU3oOm6Ca8dyblwp4/w=="],
+    "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JUiPXZKK9wOhjf4MgDiH29GZLxfqOesbLtHq2pDxwH/WwscTRV2ToymnOTh1egzaZf0ueUf8T2+CeYTGHjW0Iw=="],
 
-    "@node-rs/crc32-darwin-arm64": ["@node-rs/crc32-darwin-arm64@1.10.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kARYANp5GnmsQiViA5Qu74weYQ3phOHSYQf0G+U5wB3NB5JmBHnZcOc46Ig21tTypWtdv7u63TaltJQE41noyg=="],
+    "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Uog9jsrmIRIL/lfvIp9htmskSNC7JcQsMVucXL2V2YY1y/D9IUN3LPEafqy0zRJ2cIU1SQ0V6F6TlffQ+pLAGg=="],
 
-    "@node-rs/crc32-darwin-x64": ["@node-rs/crc32-darwin-x64@1.10.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-Q99bevJVMfLTISpkpKBlXgtPUItrvTWKFyiqoKH5IvscZmLV++NH4V13Pa17GTBmv9n18OwzgQY4/SRq6PQNVA=="],
+    "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-6yy3FT13KgUFOj5H8bl8w/6nKiJwHIvbtwh1V+1acsu+7y4tJjnemSa6mhsh53BeoVrlozE+fMgZhXH46WmjMA=="],
 
-    "@node-rs/crc32-freebsd-x64": ["@node-rs/crc32-freebsd-x64@1.10.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-66hpawbNjrgnS9EDMErta/lpaqOMrL6a6ee+nlI2viduVOmRZWm9Rg9XdGTK/+c4bQLdtC6jOd+Kp4EyGRYkAg=="],
+    "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-iOoN1QecUoGNZik536U/vtK43YwgyrCsGIkth52yIkl612n+0C9MjSnJbQAikISpb+WYRooBVhaDlUW7iZoKog=="],
 
-    "@node-rs/crc32-linux-arm-gnueabihf": ["@node-rs/crc32-linux-arm-gnueabihf@1.10.6", "", { "os": "linux", "cpu": "arm" }, "sha512-E8Z0WChH7X6ankbVm8J/Yym19Cq3otx6l4NFPS6JW/cWdjv7iw+Sps2huSug+TBprjbcEA+s4TvEwfDI1KScjg=="],
+    "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d/k+PpAriUPaeMJJOG7HUSdqfEX46FEPWU1p3/nm2ACmXhj9hFEWdFODUBIpkuijXYkfL90qZzTqVPRp4BW/hw=="],
 
-    "@node-rs/crc32-linux-arm64-gnu": ["@node-rs/crc32-linux-arm64-gnu@1.10.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-LmWcfDbqAvypX0bQjQVPmQGazh4dLiVklkgHxpV4P0TcQ1DT86H/SWpMBMs/ncF8DGuCQ05cNyMv1iddUDugoQ=="],
+    "@node-rs/crc32": ["@node-rs/crc32@1.10.7", "", { "optionalDependencies": { "@node-rs/crc32-android-arm-eabi": "1.10.7", "@node-rs/crc32-android-arm64": "1.10.7", "@node-rs/crc32-darwin-arm64": "1.10.7", "@node-rs/crc32-darwin-x64": "1.10.7", "@node-rs/crc32-freebsd-x64": "1.10.7", "@node-rs/crc32-linux-arm-gnueabihf": "1.10.7", "@node-rs/crc32-linux-arm64-gnu": "1.10.7", "@node-rs/crc32-linux-arm64-musl": "1.10.7", "@node-rs/crc32-linux-x64-gnu": "1.10.7", "@node-rs/crc32-linux-x64-musl": "1.10.7", "@node-rs/crc32-win32-arm64-msvc": "1.10.7", "@node-rs/crc32-win32-ia32-msvc": "1.10.7", "@node-rs/crc32-win32-x64-msvc": "1.10.7" } }, "sha512-OwuyRAe9Lj0GoFVBFzTS6bTAV4i+Xd68sYbNUmLnI1GVkQIqVb9mzNrpKSZkBFiQD73CdMB/uKYsvk2bxq/eZg=="],
 
-    "@node-rs/crc32-linux-arm64-musl": ["@node-rs/crc32-linux-arm64-musl@1.10.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-k8ra/bmg0hwRrIEE8JL1p32WfaN9gDlUUpQRWsbxd1WhjqvXea7kKO6K4DwVxyxlPhBS9Gkb5Urq7Y4mXANzaw=="],
+    "@node-rs/crc32-android-arm-eabi": ["@node-rs/crc32-android-arm-eabi@1.10.7", "", { "os": "android", "cpu": "arm" }, "sha512-mWNghDkwgoc5uJhhPx/LbgLoNAxnq6Sfz4pTxi4NWG7s5l+dPe7K+L4kXb0oOfwl3Yq3Ro7CNPGLrU4cmTXhaA=="],
 
-    "@node-rs/crc32-linux-x64-gnu": ["@node-rs/crc32-linux-x64-gnu@1.10.6", "", { "os": "linux", "cpu": "x64" }, "sha512-IfjtqcuFK7JrSZ9mlAFhb83xgium30PguvRjIMI45C3FJwu18bnLk1oR619IYb/zetQT82MObgmqfKOtgemEKw=="],
+    "@node-rs/crc32-android-arm64": ["@node-rs/crc32-android-arm64@1.10.7", "", { "os": "android", "cpu": "arm64" }, "sha512-lLUVm2H5HrSm/g2379JBRsSr5RCDqobP6I5sxZd+wSq6WBH2b6goilNYhxbss8gYQZLCJA09EvyFkZFFU9VuAQ=="],
 
-    "@node-rs/crc32-linux-x64-musl": ["@node-rs/crc32-linux-x64-musl@1.10.6", "", { "os": "linux", "cpu": "x64" }, "sha512-LbFYsA5M9pNunOweSt6uhxenYQF94v3bHDAQRPTQ3rnjn+mK6IC7YTAYoBjvoJP8lVzcvk9hRj8wp4Jyh6Y80g=="],
+    "@node-rs/crc32-darwin-arm64": ["@node-rs/crc32-darwin-arm64@1.10.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8slhmrRa3B3/z+MnLgST+lR5T0Oic60YTQ3S9OIHP259GiktlmdYyebhAvy0bBcyCuScLDcW0EB3sXxWw1TCbQ=="],
 
-    "@node-rs/crc32-wasm32-wasi": ["@node-rs/crc32-wasm32-wasi@1.10.6", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.5" }, "cpu": "none" }, "sha512-KaejdLgHMPsRaxnM+OG9L9XdWL2TabNx80HLdsCOoX9BVhEkfh39OeahBo8lBmidylKbLGMQoGfIKDjq0YMStw=="],
+    "@node-rs/crc32-darwin-x64": ["@node-rs/crc32-darwin-x64@1.10.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-+RkQ2+jSWco0WqSPq7GWJlJwmJpIGA79OoToGTSAsqgypkls4i3PlexVTPrfepuHn3whpa4GrmjvLbRm5RXgCQ=="],
 
-    "@node-rs/crc32-win32-arm64-msvc": ["@node-rs/crc32-win32-arm64-msvc@1.10.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-x50AXiSxn5Ccn+dCjLf1T7ZpdBiV1Sp5aC+H2ijhJO4alwznvXgWbopPRVhbp2nj0i+Gb6kkDUEyU+508KAdGQ=="],
+    "@node-rs/crc32-freebsd-x64": ["@node-rs/crc32-freebsd-x64@1.10.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yqtsf23JCO1gYt3/Rkk5aoVn1rINk/J7//A9EKwGstNpBBlN3t0JKD3QpGzEnqc1vS72lEvIeO9sO3XiH+hJWw=="],
 
-    "@node-rs/crc32-win32-ia32-msvc": ["@node-rs/crc32-win32-ia32-msvc@1.10.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-DpDxQLaErJF9l36aghe1Mx+cOnYLKYo6qVPqPL9ukJ5rAGLtCdU0C+Zoi3gs9ySm8zmbFgazq/LvmsZYU42aBw=="],
+    "@node-rs/crc32-linux-arm-gnueabihf": ["@node-rs/crc32-linux-arm-gnueabihf@1.10.7", "", { "os": "linux", "cpu": "arm" }, "sha512-AB/I1GO9LDIIHiur2bexqPy30740AmBjEhO8ij9k9desVO41dN5/ddTU2+xbf9TYt2XXvku1qYbtf5YHAAVFAw=="],
 
-    "@node-rs/crc32-win32-x64-msvc": ["@node-rs/crc32-win32-x64-msvc@1.10.6", "", { "os": "win32", "cpu": "x64" }, "sha512-5B1vXosIIBw1m2Rcnw62IIfH7W9s9f7H7Ma0rRuhT8HR4Xh8QCgw6NJSI2S2MCngsGktYnAhyUvs81b7efTyQw=="],
+    "@node-rs/crc32-linux-arm64-gnu": ["@node-rs/crc32-linux-arm64-gnu@1.10.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-0lPFuudkW+bpeXUZAqz6FSml6Z2JvF52UGSALvnDS8Qyg3wKBNWjcXy78Pw1wiIGao/AAFFr74I3OwCMzYujuQ=="],
+
+    "@node-rs/crc32-linux-arm64-musl": ["@node-rs/crc32-linux-arm64-musl@1.10.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-lx5m7iCmdXPGigG7AI5NiawWsiJ1HwR/w6PV8XN48HNsgzdjar0ISB8TSLtJpq+jFcj10Wcz3Rt43RA72652Vg=="],
+
+    "@node-rs/crc32-linux-x64-gnu": ["@node-rs/crc32-linux-x64-gnu@1.10.7", "", { "os": "linux", "cpu": "x64" }, "sha512-If3ogA9D2XKKb8vHttu+jl3BTWT7UCTgmrXmUAwQABH45iK32AZ9OoMcrg2rWKKXgGpG6x3IslFBgzdOkObDnA=="],
+
+    "@node-rs/crc32-linux-x64-musl": ["@node-rs/crc32-linux-x64-musl@1.10.7", "", { "os": "linux", "cpu": "x64" }, "sha512-XxuxZYJhQBlo2rRnyOOummZSyBdC+jHpXWM9XSWs1LWfsgc36IauKY+XXRAJwKwefx8lwmMjqp6T/Zq9frU87w=="],
+
+    "@node-rs/crc32-win32-arm64-msvc": ["@node-rs/crc32-win32-arm64-msvc@1.10.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-Rs8TGuvxgpkteSLhPqAb3DOqqRO1jiYghdMxozl+P3Ftep1hTWL8+gwYF5dtu1BtdOJpz23A+8GehSuwl96MrQ=="],
+
+    "@node-rs/crc32-win32-ia32-msvc": ["@node-rs/crc32-win32-ia32-msvc@1.10.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-G/xNcmblMH6Z7KBX02XJPJj2UGo6eTgFa+r5ZU17O8V5P9iQLrKqs/T7Sz40otDxazD1maSXT/zA8Q7rBu2/3g=="],
+
+    "@node-rs/crc32-win32-x64-msvc": ["@node-rs/crc32-win32-x64-msvc@1.10.7", "", { "os": "win32", "cpu": "x64" }, "sha512-1uXrMu17uz12WPdii84NXmoQsoPdmSQViyCMJPu0KQuwnhErQCA4PoGK+dih9jY0mrcLuzR1r5wXVaE049fJ6w=="],
 
     "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
 
@@ -388,43 +444,43 @@
 
     "@npmcli/promise-spawn": ["@npmcli/promise-spawn@7.0.2", "", { "dependencies": { "which": "^4.0.0" } }, "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ=="],
 
-    "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.68.0", "", { "os": "android", "cpu": "arm" }, "sha512-wEdsIspexXLLMCPAEOcCuFLMt6aE3AzTuA/nQKLPRnoJ+EQTturmGheDkhHuuVHx0GbutjQ3JKmEn+Gz6Ag28Q=="],
+    "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.79.0", "", { "os": "android", "cpu": "arm" }, "sha512-TebFaaMklO/RXzTv7PucaCq9l3X6D1gA+C8H6K4njtjFOV+zWE9MKLpulcJZN9bzytbUbQIY0mZuz12nQ5Kv4Q=="],
 
-    "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.68.0", "", { "os": "android", "cpu": "arm64" }, "sha512-6aZRNNXQTsYtgaus8HTb9nuCcsrQTlKXGnktwvwW0n/SooRWNxNb3925grDkC63aEYZuCIyOVLV16IdYIoC2aQ=="],
+    "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.79.0", "", { "os": "android", "cpu": "arm64" }, "sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg=="],
 
-    "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.68.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lVTbsE3kO4bLpZELgjRZuAJc8kP98wb83yMXWH8gaPaFZ+cM2IDeZto4ByoUAYj0Mxv2rvw+A1ssZequSepVSg=="],
+    "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.79.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BVC2nsMzqQzRDPc5RhixkZ+m1p7iH4bxRRvqkbwDXX0PlQKm1BPy8J8cRjnAFafOq2QzI+BfO3vE8w2GZ3CBag=="],
 
-    "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.68.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-nCmw2XrmQskjBUh/sfP5yKs93V68LijQgjd1cuuZ/q4SCARngLYs60/qqyzuMsg8QQ9KArDI98hxs/RDGE4KRQ=="],
+    "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.79.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-p6Lm+snmhGuLKL1+CpCV8L6ijkE/qJzK2H2jG9+eKJT0n31RbY4FLsdhexekgP3bLpw4Kgde+9DZuDZQ4yIInA=="],
 
-    "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.68.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TI4ovQJliYE9V6e06cEv+qEI9uj7Ao65fmif4er4HD+aouyYyh0P31q2jh3KtqsOHHcQqv2PZ61TjJFLpBDGWQ=="],
+    "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.79.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-qDMm0dXZnoHyRqSL4N4xUq82T4sqK5cbKSjvd/dF/YbMUXc2R1wEPf+vmA5S0qUmi0nwXfNbjXBtZaIqzQLIMg=="],
 
-    "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.68.0", "", { "os": "linux", "cpu": "arm" }, "sha512-LcNnEi9g71Cmry5ZpLbKT+oVv+/zYG3hYVAbBBB5X85nOQZSk8l92CnDkxJMcxUg0NCnMCOFZuaVDlMyv4tYJw=="],
+    "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.79.0", "", { "os": "linux", "cpu": "arm" }, "sha512-2od7s0nuKPzqyUZAWk9KkCyGg7eI9dwFPZg+20lB15fKFkVZ0c9ZFxqPfiBAyDTlTkh9stPI0t+JlPCqMbItVA=="],
 
-    "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.68.0", "", { "os": "linux", "cpu": "arm" }, "sha512-OovHahL3FX4UaK+hgSf11llUx2vszqjSdQQ61Ck9InOEI/ptZoC4XSQJurITqItVvd53JSlmkLMeaNjM1PoQew=="],
+    "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.79.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZOQUjkzDnvlhSE3+tWC3YXx94MMl+sYMlwH+u1+YGApGHOJP/YAc8ZBRFOXZ6eOBmxtXAWuS/fBcdZr8qqNO1A=="],
 
-    "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.68.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-YbzTglnHLzzi9zv5or8Ztz5fykAoZE8W9iM42/bOrF4HBSB6rJTqdLQWuoP76EHQw9DuKl76K1QmFlG29sPJXQ=="],
+    "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.79.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-lu158FR4nGqGeRS3BQvtG85wRgU/Fy4MD5Cxp1hzJXizGiLo6u2742wJSCDKh8cFcZntvX7fcxlq4mMmfryH1g=="],
 
-    "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.68.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qVKtCZNic+OoNnOr/hCQAu22HSQzflI7Fsq/Blzkw02SnLuv163k3kfmrVpZjSBlUHgsRKj6WgQiw30d3SX02Q=="],
+    "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.79.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-mbpKQeE2aflTjddaHK7MP8KP/OFbUM++lt5M635ENM8IyIdK0jm2t9pb+2v9mVVIvhF6TqA4l7F79Pll1mi+uw=="],
 
-    "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.68.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zExyZ8ZOUuAyQ0y9jpTcyjKUz62YY9JhKPyVxzvjTpXzZ3ujdqiVwfPWDdnA1SsIOrxdtxHn7KErDHLWskFjXg=="],
+    "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.79.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WpGNua7gaxaHnpSDeog2ji8IDHn/QLPl9LPzwkR/FvVv58vT5BcXjRXnU+wbu3N75cpeha8CdC7ho/U2OIsB4g=="],
 
-    "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.68.0", "", { "os": "linux", "cpu": "none" }, "sha512-6C4MPuwewyDavA7sxM14wzgRi5GGL68HPIxRCdVyS75U4MDbpFVYzKO9WNR6KLKTMPq2pcz3THwo1sK2uiqngw=="],
+    "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.79.0", "", { "os": "linux", "cpu": "none" }, "sha512-tK1E93A5LVzISg4ngpKJnfTs7EqtIUceGI7MQ4GyDjJiLi8wPCkEyKlj2xkyKWZ1yzkDJyLHTBJ5/iFWRdnJvg=="],
 
-    "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.68.0", "", { "os": "linux", "cpu": "none" }, "sha512-bnZooVeHAcvA+dH0EDLgx+7HY/DRi6e0hFszg3P+OBatuUjV6EvfIyNIzWOusmqAVh4L6r21GGTZtiKE4iqM4Q=="],
+    "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.79.0", "", { "os": "linux", "cpu": "none" }, "sha512-qhQvUIrngXivA2A9pQ+xPCychztn/5qUv7yS3gDwXv3w7Rag+eTeeXWmRyx+t7XsW5x6LuY/8AsTq36UgFIblg=="],
 
-    "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.68.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-dIqnZnJSmHCMOUpUcWQOiV14o3DDPVx1DSsMaSzvdhNjC1tB1iEPZbdiMSCIEYbkgbsYznHXWqFdKL8WUB3F8g=="],
+    "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.79.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-sv6AaVgU/eE6u+6WFiQVDcPPwTxP6IJMSB9k701W2r/r6Tx465e8vPvVyRxquNH4Vy6KwRNu90mVbxXJN8+5gg=="],
 
-    "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.68.0", "", { "os": "linux", "cpu": "x64" }, "sha512-zc9lEnfV/HreDTY6gdMlZe+irkwHSxQ4/B1pS9GyK7RVaA5LxhoZY/w6/o2vIwLLEYiXQ5ujGxOM1ZazeFAAIA=="],
+    "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.79.0", "", { "os": "linux", "cpu": "x64" }, "sha512-iFZL02deziHslb3jEX9KdqlAkYoo4fGyotchKDzdfK1f5mxlIBeiQeHhvK3iFpuEJSB4ma/qeFn9oxPiwnhUPQ=="],
 
-    "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.68.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dl5QEX0TCo/40Cdh1o1JdPS//+YiWqjC+Hrrya5OQmStZZr4svAFtdlqcpCrU9yq2Mo3vRVyO9B3h0dzD8s36Q=="],
+    "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.79.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3DtZR2raqObnh7wXZoFYFd0Fw7skBvcb3f7A+/lkEiDuh8hrE6vv9b/62Qxao1a9/OeHLw/FcXlXzgsW9wTRFg=="],
 
-    "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.68.0", "", { "os": "none", "cpu": "arm64" }, "sha512-/qy6dOvi4S3/LeXq0l5BT5pRKPYA7oj3uKwJOAZOr5HRLL+HK6jdBynvWuXIA2wwfE01RzNYmbBdM7vwYx00sA=="],
+    "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.79.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Oatt4GuA1WJkqzk2ozx4HrWROOi7opV3AKDw/U8qDIqeTqzsjn5K2x3REJMNjU3/KU/Bkq96Zi3CknaiDTaC/Q=="],
 
-    "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.68.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-fHNtVqPHSYE7UFDSLVFUjxQjnSVXxseNJmRW+XuP4pXXDwePdPda43NL7/BBCFTxHjycOc44JNDaOPtFDNui9A=="],
+    "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.79.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-NAgZr9Qp8nIA9rpo0JEvwiabTF/2UVqBNnupBG9X4kxXcQoScJUTi+qHhvabb9s/thgj5wQ4XcIaJvb+ZMgoKw=="],
 
-    "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.68.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-NnKXr4Wgo4nps3erhrE0f8shBvBPZMHg72nDsvX0JyrRvsNiP3f1JNvbCKh+A6VFvpF7ZoJxu904P3cKMhvZnA=="],
+    "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.79.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+KyXjIvcpaXmWW/j9NNY5yWjrIVxaX18VyIheQy3jwc2GSYgpCr7MGI/HxIGQ/shAL5IWEKbhsqoMpAO5Stiog=="],
 
-    "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.68.0", "", { "os": "win32", "cpu": "x64" }, "sha512-zg5pA+84AlU6XHJ3ruiRxziO71QTrz8nLsk6u01JGS5+tL9/bnlakFiklFrcy4R1/V7ktWtaNitN3JZWmKnf6g=="],
+    "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.79.0", "", { "os": "win32", "cpu": "x64" }, "sha512-mEelcCMMBS57sIXh2veGMNy+pQwuGtcMxHxGIZWQ5Ba9pJ5jCCUFOZB9E2JhBaxGsURe+WGe0zJp4RVre52gpQ=="],
 
     "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
 
@@ -454,51 +510,49 @@
 
     "@secretlint/types": ["@secretlint/types@10.2.2", "", {}, "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg=="],
 
-    "@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="],
+    "@shikijs/core": ["@shikijs/core@4.4.3", "", { "dependencies": { "@shikijs/primitive": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" } }, "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg=="],
 
-    "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og=="],
+    "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ=="],
 
-    "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g=="],
+    "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w=="],
 
-    "@shikijs/langs": ["@shikijs/langs@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0" } }, "sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ=="],
+    "@shikijs/langs": ["@shikijs/langs@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A=="],
 
-    "@shikijs/primitive": ["@shikijs/primitive@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA=="],
+    "@shikijs/primitive": ["@shikijs/primitive@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ=="],
 
-    "@shikijs/themes": ["@shikijs/themes@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0" } }, "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w=="],
+    "@shikijs/themes": ["@shikijs/themes@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw=="],
 
-    "@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
+    "@shikijs/types": ["@shikijs/types@4.4.3", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g=="],
 
     "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
 
     "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="],
 
-    "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
+    "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="],
 
-    "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.101.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "^5.4.0 || ^6.0.0" }, "optionalPeers": ["typescript"] }, "sha512-wsfg821y4yw21J7nKI2oM5yyGSz3vASXqgWbmWCXZpnyY9ObLrBCcXivwZKj4YHF2fUWiqoOIRX2pbE79cf6gQ=="],
+    "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.101.4", "", { "dependencies": { "@typescript-eslint/utils": "^8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "^5.4.0 || ^6.0.0" }, "optionalPeers": ["typescript"] }, "sha512-jqAZJWXGd5PthJYH9wmC2O5Ry5xAN2/7zxi9qcANQ+Xix8V5JkqNRjZ8Fh+rYzqzVYGiHqbrHekt+uh38OZVpw=="],
 
-    "@tanstack/query-core": ["@tanstack/query-core@5.101.0", "", {}, "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow=="],
+    "@tanstack/query-core": ["@tanstack/query-core@5.101.4", "", {}, "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw=="],
 
-    "@tanstack/react-query": ["@tanstack/react-query@5.101.0", "", { "dependencies": { "@tanstack/query-core": "5.101.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg=="],
+    "@tanstack/react-query": ["@tanstack/react-query@5.101.4", "", { "dependencies": { "@tanstack/query-core": "5.101.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA=="],
 
     "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
 
-    "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="],
+    "@testing-library/jest-dom": ["@testing-library/jest-dom@7.0.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" }, "peerDependencies": { "@testing-library/dom": ">=10 <11", "vitest": ">= 0.32" }, "optionalPeers": ["vitest"] }, "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw=="],
 
     "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
 
-    "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="],
-
-    "@textlint/ast-node-types": ["@textlint/ast-node-types@15.7.1", "", {}, "sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA=="],
+    "@testing-library/user-event": ["@testing-library/user-event@14.6.5", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-FhqjldLTpteueBaKflhNFlMT3+PM0O5fiBUivht6b9CZ1eesJyy7+g3Jr7XwJzt/Hip3ZG5hWwK1MX1FuDiE4w=="],
 
-    "@textlint/linter-formatter": ["@textlint/linter-formatter@15.7.1", "", { "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", "@textlint/module-interop": "15.7.1", "@textlint/resolver": "15.7.1", "@textlint/types": "15.7.1", "chalk": "^4.1.2", "debug": "^4.4.3", "js-yaml": "^4.1.1", "lodash": "^4.18.1", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "table": "^6.9.0", "text-table": "^0.2.0" } }, "sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA=="],
+    "@textlint/ast-node-types": ["@textlint/ast-node-types@15.8.0", "", {}, "sha512-5CiH9COYmovWmExQgs7763DzX6Gy9zjkjJ7JxCC95wyTcjwQn/8poNF6fv3qzRlmx8CRRde8DHr9FcgAAiPzgw=="],
 
-    "@textlint/module-interop": ["@textlint/module-interop@15.7.1", "", {}, "sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g=="],
+    "@textlint/linter-formatter": ["@textlint/linter-formatter@15.8.0", "", { "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", "@textlint/module-interop": "15.8.0", "@textlint/resolver": "15.8.0", "@textlint/types": "15.8.0", "debug": "^4.4.3", "js-yaml": "^4.3.0", "lodash": "^4.18.1", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "table": "^6.9.0", "text-table": "^0.2.0" } }, "sha512-+oU3A235NATv6Lzi4xa4kJ65PuNJlIxesaO4AvDhDWA9FWm7y4XKWaoQCW1esgaQQ6dwnUiFKArQ8TcJ86mC4w=="],
 
-    "@textlint/resolver": ["@textlint/resolver@15.7.1", "", {}, "sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g=="],
+    "@textlint/module-interop": ["@textlint/module-interop@15.8.0", "", {}, "sha512-rt+OR1WYGoLOY8HkA/aBPrqufF6yUUEsKEAh7XohTsT3lp9IyZFT6zOIbjul9P4FAzsmSPkcrYjVx3Bz/IUfkg=="],
 
-    "@textlint/types": ["@textlint/types@15.7.1", "", { "dependencies": { "@textlint/ast-node-types": "15.7.1" } }, "sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A=="],
+    "@textlint/resolver": ["@textlint/resolver@15.8.0", "", {}, "sha512-E88tzfX3K8Jykk+38aJ9cy8RquD8ABVOPTO2rFEESq0wcg8x6/ypdAS8ZgR7OKiGqlRF0hkO/m5PbQwVfKM3VA=="],
 
-    "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
+    "@textlint/types": ["@textlint/types@15.8.0", "", { "dependencies": { "@textlint/ast-node-types": "15.8.0" } }, "sha512-Anhc6y5736YIsvqae0U6k0YmB2M/QVHkEeOv2aydAn/WIkdI69dCOiDbe3/+RagS3qstFTSFWJzNRA2lUjv19w=="],
 
     "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
 
@@ -514,7 +568,7 @@
 
     "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
 
-    "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
+    "@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],
 
     "@types/is-empty": ["@types/is-empty@1.2.3", "", {}, "sha512-4J1l5d79hoIvsrKh5VUKVRA1aIdsOb10Hu5j3J2VfP/msDnfTdGPmNp2E1Wg+vs97Bktzo+MZePFFXSGoykYJw=="],
 
@@ -530,11 +584,11 @@
 
     "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
 
-    "@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="],
+    "@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="],
 
     "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="],
 
-    "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
+    "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="],
 
     "@types/react-syntax-highlighter": ["@types/react-syntax-highlighter@15.5.13", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="],
 
@@ -544,41 +598,81 @@
 
     "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
 
-    "@types/vscode": ["@types/vscode@1.120.0", "", {}, "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA=="],
+    "@types/vscode": ["@types/vscode@1.134.0", "", {}, "sha512-NDEu0hg4sF7+vvFsADsktqUJ6f80LHSZvVK2Ovo1XiQ0/VHck1O3zst+ZZyVA/uvz6vo6LcuoqU2q48YMqOwWw=="],
 
     "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
 
     "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
 
-    "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.60.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/type-utils": "8.60.1", "@typescript-eslint/utils": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.60.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg=="],
+    "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.67.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.67.0", "@typescript-eslint/type-utils": "8.67.0", "@typescript-eslint/utils": "8.67.0", "@typescript-eslint/visitor-keys": "8.67.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.67.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ=="],
+
+    "@typescript-eslint/parser": ["@typescript-eslint/parser@8.67.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.67.0", "@typescript-eslint/types": "8.67.0", "@typescript-eslint/typescript-estree": "8.67.0", "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w=="],
+
+    "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.67.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.67.0", "@typescript-eslint/types": "^8.67.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw=="],
+
+    "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "@typescript-eslint/visitor-keys": "8.67.0" } }, "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg=="],
+
+    "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.67.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg=="],
+
+    "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "@typescript-eslint/typescript-estree": "8.67.0", "@typescript-eslint/utils": "8.67.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q=="],
+
+    "@typescript-eslint/types": ["@typescript-eslint/types@8.67.0", "", {}, "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww=="],
+
+    "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.67.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.67.0", "@typescript-eslint/tsconfig-utils": "8.67.0", "@typescript-eslint/types": "8.67.0", "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw=="],
+
+    "@typescript-eslint/utils": ["@typescript-eslint/utils@8.67.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.67.0", "@typescript-eslint/types": "8.67.0", "@typescript-eslint/typescript-estree": "8.67.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A=="],
+
+    "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA=="],
+
+    "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
+
+    "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],
+
+    "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="],
+
+    "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="],
+
+    "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="],
+
+    "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="],
+
+    "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="],
+
+    "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="],
+
+    "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="],
+
+    "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="],
+
+    "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="],
 
-    "@typescript-eslint/parser": ["@typescript-eslint/parser@8.60.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA=="],
+    "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="],
 
-    "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.60.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.60.1", "@typescript-eslint/types": "^8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw=="],
+    "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="],
 
-    "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1" } }, "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w=="],
+    "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="],
 
-    "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.60.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA=="],
+    "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="],
 
-    "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A=="],
+    "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="],
 
-    "@typescript-eslint/types": ["@typescript-eslint/types@8.60.1", "", {}, "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w=="],
+    "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="],
 
-    "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.60.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.60.1", "@typescript-eslint/tsconfig-utils": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew=="],
+    "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="],
 
-    "@typescript-eslint/utils": ["@typescript-eslint/utils@8.60.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg=="],
+    "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="],
 
-    "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag=="],
+    "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
 
-    "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.6", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og=="],
+    "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.8", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-bLMpVcWZNzq6lYOybwFwOAR1IXKcHnhUNqYeHjl1bET/qE3jFPFH+p8Wrh3rU4xwdnifPxmKNESBYnvnmc75aA=="],
 
-    "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="],
+    "@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="],
 
-    "@vscode/test-cli": ["@vscode/test-cli@0.0.12", "", { "dependencies": { "@types/mocha": "^10.0.10", "c8": "^10.1.3", "chokidar": "^3.6.0", "enhanced-resolve": "^5.18.3", "glob": "^10.3.10", "minimatch": "^9.0.3", "mocha": "^11.7.4", "supports-color": "^10.2.2", "yargs": "^17.7.2" }, "bin": { "vscode-test": "out/bin.mjs" } }, "sha512-iYN0fDg29+a2Xelle/Y56Xvv7Nc8Thzq4VwpzAF/SIE6918rDicqfsQxV6w1ttr2+SOm+10laGuY9FG2ptEKsQ=="],
+    "@vscode/test-cli": ["@vscode/test-cli@0.0.15", "", { "dependencies": { "@types/mocha": "^10.0.10", "c8": "^11.0.0", "chokidar": "^5.0.0", "enhanced-resolve": "^5.24.0", "glob": "^13.0.6", "minimatch": "^10.2.5", "mocha": "^11.7.6", "supports-color": "^10.2.2", "yargs": "^18.0.0" }, "bin": { "vscode-test": "out/bin.mjs" } }, "sha512-nAxk2X79wuXS7aOhyFFhFcCqd7EBUoMesu7ZgsYE/4eFjyBMuyIweVE94BxdKH1RieN8eOz2SIrljrZt6Lk9fQ=="],
 
     "@vscode/vsce": ["@vscode/vsce@3.9.2", "", { "dependencies": { "@azure/identity": "^4.1.0", "@secretlint/node": "^10.1.2", "@secretlint/secretlint-formatter-sarif": "^10.1.2", "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", "@vscode/vsce-sign": "^2.0.0", "azure-devops-node-api": "^12.5.0", "chalk": "^4.1.2", "cheerio": "^1.0.0-rc.9", "cockatiel": "^3.1.2", "commander": "^12.1.0", "form-data": "^4.0.0", "glob": "^13.0.6", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", "leven": "^3.1.0", "markdown-it": "^14.1.0", "mime": "^1.3.4", "minimatch": "^10.2.2", "parse-semver": "^1.1.1", "read": "^1.0.7", "secretlint": "^10.1.2", "semver": "^7.5.2", "tmp": "^0.2.3", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", "xml2js": "^0.5.0", "yauzl": "^3.2.1", "yazl": "^2.2.2" }, "optionalDependencies": { "keytar": "^7.7.0" }, "bin": { "vsce": "vsce" } }, "sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA=="],
 
-    "@vscode/vsce-sign": ["@vscode/vsce-sign@2.0.9", "", { "optionalDependencies": { "@vscode/vsce-sign-alpine-arm64": "2.0.6", "@vscode/vsce-sign-alpine-x64": "2.0.6", "@vscode/vsce-sign-darwin-arm64": "2.0.6", "@vscode/vsce-sign-darwin-x64": "2.0.6", "@vscode/vsce-sign-linux-arm": "2.0.6", "@vscode/vsce-sign-linux-arm64": "2.0.6", "@vscode/vsce-sign-linux-x64": "2.0.6", "@vscode/vsce-sign-win32-arm64": "2.0.6", "@vscode/vsce-sign-win32-x64": "2.0.6" } }, "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g=="],
+    "@vscode/vsce-sign": ["@vscode/vsce-sign@2.1.0", "", { "optionalDependencies": { "@vscode/vsce-sign-alpine-arm64": "2.0.6", "@vscode/vsce-sign-alpine-x64": "2.0.6", "@vscode/vsce-sign-darwin-arm64": "2.0.6", "@vscode/vsce-sign-darwin-x64": "2.0.6", "@vscode/vsce-sign-linux-arm": "2.0.6", "@vscode/vsce-sign-linux-arm64": "2.0.6", "@vscode/vsce-sign-linux-x64": "2.0.6", "@vscode/vsce-sign-win32-arm64": "2.0.6", "@vscode/vsce-sign-win32-x64": "2.0.6" } }, "sha512-9AQrqazrBgTgRSuwleLVXUrIUphY02/SFCh2TKYoLV/xifJAdblhdmEmw5gUrYSPQ3sRwNs9iyCMD14sATEE6g=="],
 
     "@vscode/vsce-sign-alpine-arm64": ["@vscode/vsce-sign-alpine-arm64@2.0.6", "", { "os": "none", "cpu": "arm64" }, "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q=="],
 
@@ -600,7 +694,7 @@
 
     "abbrev": ["abbrev@2.0.0", "", {}, "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ=="],
 
-    "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
+    "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
 
     "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
 
@@ -614,8 +708,6 @@
 
     "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
 
-    "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
-
     "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
 
     "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
@@ -650,13 +742,11 @@
 
     "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
 
-    "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+    "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
 
     "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
 
-    "baseline-browser-mapping": ["baseline-browser-mapping@2.10.34", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw=="],
-
-    "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
+    "baseline-browser-mapping": ["baseline-browser-mapping@2.11.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA=="],
 
     "binaryextensions": ["binaryextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw=="],
 
@@ -666,13 +756,13 @@
 
     "boundary": ["boundary@2.0.0", "", {}, "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA=="],
 
-    "brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
+    "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
 
     "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
 
     "browser-stdout": ["browser-stdout@1.3.1", "", {}, "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="],
 
-    "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
+    "browserslist": ["browserslist@4.28.8", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="],
 
     "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
 
@@ -690,7 +780,7 @@
 
     "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
 
-    "c8": ["c8@10.1.3", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.1", "@istanbuljs/schema": "^0.1.3", "find-up": "^5.0.0", "foreground-child": "^3.1.1", "istanbul-lib-coverage": "^3.2.0", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.1.6", "test-exclude": "^7.0.1", "v8-to-istanbul": "^9.0.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1" }, "peerDependencies": { "monocart-coverage-reports": "^2" }, "optionalPeers": ["monocart-coverage-reports"], "bin": { "c8": "bin/c8.js" } }, "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA=="],
+    "c8": ["c8@11.0.0", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.1", "@istanbuljs/schema": "^0.1.3", "find-up": "^5.0.0", "foreground-child": "^3.1.1", "istanbul-lib-coverage": "^3.2.0", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.1.6", "test-exclude": "^8.0.0", "v8-to-istanbul": "^9.0.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1" }, "peerDependencies": { "monocart-coverage-reports": "^2" }, "optionalPeers": ["monocart-coverage-reports"], "bin": { "c8": "bin/c8.js" } }, "sha512-e/uRViGHSVIJv7zsaDKM7VRn2390TgHXqUSvYwPHBQaU6L7E9L0n9JbdkwdYPvshDT0KymBmmlwSpms3yBaMNg=="],
 
     "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
 
@@ -700,7 +790,7 @@
 
     "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
 
-    "caniuse-lite": ["caniuse-lite@1.0.30001797", "", {}, "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w=="],
+    "caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="],
 
     "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
 
@@ -714,19 +804,23 @@
 
     "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
 
+    "chardet": ["chardet@2.2.0", "", {}, "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA=="],
+
     "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="],
 
     "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="],
 
-    "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
+    "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
 
     "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
 
     "ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="],
 
+    "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
+
     "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
 
-    "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
+    "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
 
     "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
 
@@ -750,6 +844,8 @@
 
     "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
 
+    "cross-keychain": ["cross-keychain@1.1.0", "", { "dependencies": { "@inquirer/prompts": "^7.8.6", "meow": "^14.0.0" }, "optionalDependencies": { "@napi-rs/keyring": "^1.2.0" }, "bin": { "cross-keychain": "dist/cli.js" } }, "sha512-244DWNdGepLKD5vEn3reZqwzZFiE/LD4U+XV9IaXQbtIXKvQkf0VkRaOj/9vPYauPdR12PSGB3U0cE7jJi3WTQ=="],
+
     "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
 
     "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
@@ -760,7 +856,7 @@
 
     "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
 
-    "csstype-extra": ["csstype-extra@0.1.29", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-9y4phbWzHTetVUxRlx2Lm6WULf/ciwtZ0AmaQnI8pwtEHQMw6BXkXLXBnehGJGSFsZ4zXc6MOoBCfzPbHroMMQ=="],
+    "csstype-extra": ["csstype-extra@0.1.35", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-pEJ+ZP1nE0TT5E5XXRuNBiU44O2CCwgm1zd0ogXOX/We8GxX++T7KhThrQaSO2V2Yvdmh2OsqBWLH0+OQTUXKA=="],
 
     "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
 
@@ -780,7 +876,7 @@
 
     "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
 
-    "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
+    "default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="],
 
     "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
 
@@ -820,7 +916,7 @@
 
     "editions": ["editions@6.22.0", "", { "dependencies": { "version-range": "^4.15.0" } }, "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ=="],
 
-    "electron-to-chromium": ["electron-to-chromium@1.5.368", "", {}, "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw=="],
+    "electron-to-chromium": ["electron-to-chromium@1.5.411", "", {}, "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg=="],
 
     "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
 
@@ -828,7 +924,7 @@
 
     "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
 
-    "enhanced-resolve": ["enhanced-resolve@5.23.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA=="],
+    "enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
 
     "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
 
@@ -840,11 +936,13 @@
 
     "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="],
 
+    "es-abstract-get": ["es-abstract-get@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "es-object-atoms": "^1.1.2", "is-callable": "^1.2.7", "object-inspect": "^1.13.4" } }, "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg=="],
+
     "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
 
     "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
 
-    "es-iterator-helpers": ["es-iterator-helpers@1.3.2", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0" } }, "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw=="],
+    "es-iterator-helpers": ["es-iterator-helpers@1.4.0", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0" } }, "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q=="],
 
     "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
 
@@ -852,25 +950,25 @@
 
     "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="],
 
-    "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
+    "es-to-primitive": ["es-to-primitive@1.3.4", "", { "dependencies": { "es-abstract-get": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "is-callable": "^1.2.7", "is-date-object": "^1.1.0", "is-symbol": "^1.1.1" } }, "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw=="],
 
     "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
 
     "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
 
-    "esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="],
+    "esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
 
     "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
 
     "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
 
-    "eslint": ["eslint@10.4.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw=="],
+    "eslint": ["eslint@10.8.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ=="],
 
     "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
 
     "eslint-mdx": ["eslint-mdx@3.8.1", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "espree": "^9.6.1 || ^10.4.0 || ^11.2.0", "estree-util-visit": "^2.0.0", "remark-mdx": "^3.1.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "synckit": "^0.11.8", "unified": "^11.0.5", "unified-engine": "^11.2.2", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "eslint": ">=8.0.0", "remark-lint-file-extension": "*" }, "optionalPeers": ["remark-lint-file-extension"] }, "sha512-hnsqWwMOHqUANwxWEGt8XbwABPEr5sTOolAzqyUDFdlERpqjFE/icylb+mJl60VICL+kLbbvXWbnFLWZdTqJ2g=="],
 
-    "eslint-plugin-devup": ["eslint-plugin-devup@2.0.19", "", { "dependencies": { "@devup-ui/eslint-plugin": ">=1.0.14", "@eslint/js": ">=10.0", "@tanstack/eslint-plugin-query": ">=5.100.6", "eslint": ">=10.2", "eslint-config-prettier": ">=10", "eslint-plugin-mdx": ">=3.7.0", "eslint-plugin-prettier": ">=5.5.5", "eslint-plugin-react": ">=7.37.5", "eslint-plugin-react-hooks": ">=7", "eslint-plugin-simple-import-sort": ">=13.0.0", "eslint-plugin-unused-imports": ">=4.4.1", "prettier": ">=3", "typescript-eslint": ">=8.59" } }, "sha512-E1CwZp4kjy/py/xztR1cXOF/FuzEuGc2GaYEK3cCaAtVna0rTT9TwxPKcTpGQIJvjlZHNxEl5BoeJdARC8GGPQ=="],
+    "eslint-plugin-devup": ["eslint-plugin-devup@2.0.20", "", { "dependencies": { "@devup-ui/eslint-plugin": ">=1.0.16", "@eslint/js": ">=10.0", "@tanstack/eslint-plugin-query": ">=5.101.2", "eslint": ">=10.7", "eslint-config-prettier": ">=10", "eslint-plugin-mdx": ">=3.8.1", "eslint-plugin-prettier": ">=5.5.6", "eslint-plugin-react": ">=7.37.5", "eslint-plugin-react-hooks": ">=7", "eslint-plugin-simple-import-sort": ">=13.0.0", "eslint-plugin-unused-imports": ">=4.4.1", "prettier": ">=3", "typescript-eslint": ">=8.64" } }, "sha512-qLHUI5K8ftJyZoO1XrzFexbRz6p1uRkhaCu1EANB1Itx69LYPbfw3U1DMNpByL1hjYf5ZCFrmNc8+Dc4e2JgrA=="],
 
     "eslint-plugin-mdx": ["eslint-plugin-mdx@3.8.1", "", { "dependencies": { "eslint-mdx": "^3.8.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0", "remark-mdx": "^3.1.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "synckit": "^0.11.8", "unified": "^11.0.5", "vfile": "^6.0.3" }, "peerDependencies": { "eslint": ">=8.0.0" } }, "sha512-4OLgotfBxUDc1f6ihXSagT/1+JCCUABA/2r6Kzl6gqFftg4dCV0wBfdwFo6X6UO/FzTHr3g6mVt+6prRXffc/Q=="],
 
@@ -880,7 +978,7 @@
 
     "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
 
-    "eslint-plugin-simple-import-sort": ["eslint-plugin-simple-import-sort@13.0.0", "", { "peerDependencies": { "eslint": ">=5.0.0" } }, "sha512-McAc+/Nlvcg4byY/CABGH8kqnefWBj8s3JA2okEtz8ixbECQgU46p0HkTUKa4YS7wvgGceimlc34p1nXqbWqtA=="],
+    "eslint-plugin-simple-import-sort": ["eslint-plugin-simple-import-sort@14.0.0", "", { "peerDependencies": { "eslint": ">=5.0.0" } }, "sha512-NUJO0+XFCkk+o5EsAJruTgnfMEpeWrPWeJS15UVF60GgXmqz1BJ9/3hzlvG7lkL8Bubzos5cCLptThbFfPnSMQ=="],
 
     "eslint-plugin-unused-imports": ["eslint-plugin-unused-imports@4.4.1", "", { "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "eslint": "^10.0.0 || ^9.0.0 || ^8.0.0" }, "optionalPeers": ["@typescript-eslint/eslint-plugin"] }, "sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ=="],
 
@@ -926,7 +1024,7 @@
 
     "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
 
-    "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
+    "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="],
 
     "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
 
@@ -942,25 +1040,23 @@
 
     "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
 
-    "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
+    "flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="],
 
-    "follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
+    "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
 
     "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
 
     "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
 
-    "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
+    "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="],
 
     "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
 
-    "fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
-
-    "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+    "fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="],
 
     "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
 
-    "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
+    "function.prototype.name": ["function.prototype.name@1.2.0", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2", "hasown": "^2.0.4", "is-callable": "^1.2.7", "is-document.all": "^1.0.0" } }, "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew=="],
 
     "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
 
@@ -970,6 +1066,8 @@
 
     "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
 
+    "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
+
     "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
 
     "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
@@ -980,7 +1078,7 @@
 
     "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
 
-    "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
+    "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
 
     "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
 
@@ -992,7 +1090,7 @@
 
     "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
 
-    "happy-dom": ["happy-dom@20.10.1", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-awPoqPjx8CgjapJllyDlgzgVHjBExcitKK5ZJkxwhQJyQpHFkyS2bEcqCm7IeW20cQvuCI0cz2Ifq79CJKqtiw=="],
+    "happy-dom": ["happy-dom@20.11.6", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-Hldbg8AdAa5a5oDcZpjqnGitp7JB0hqWmfv/8qr+kft4vzSD8BHsbdRfzYvL/0QcbKcURC/yyoygbeDQarPvYg=="],
 
     "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
 
@@ -1084,8 +1182,6 @@
 
     "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
 
-    "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
-
     "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
 
     "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
@@ -1102,6 +1198,8 @@
 
     "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
 
+    "is-document.all": ["is-document.all@1.0.0", "", { "dependencies": { "call-bound": "^1.0.4" } }, "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g=="],
+
     "is-empty": ["is-empty@1.2.0", "", {}, "sha512-F2FnH/otLNJv0J6wc73A5Xo7oHLNnqplYqZhUu01tD54DIPvxIRSTSLkrUB/M0nHO4vo1O9PDfN4KoTxCzLh/w=="],
 
     "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
@@ -1172,7 +1270,7 @@
 
     "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
 
-    "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="],
+    "js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
 
     "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
 
@@ -1210,7 +1308,7 @@
 
     "lines-and-columns": ["lines-and-columns@2.0.4", "", {}, "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A=="],
 
-    "linkify-it": ["linkify-it@5.0.1", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg=="],
+    "linkify-it": ["linkify-it@5.0.2", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q=="],
 
     "load-plugin": ["load-plugin@6.0.3", "", { "dependencies": { "@npmcli/config": "^8.0.0", "import-meta-resolve": "^4.0.0" } }, "sha512-kc0X2FEUZr145odl68frm+lMJuQ23+rTXYmR6TImqPtbpmXC4vVXbWKDQ9IzndA0HfyQamWfKLhzsqGSTxE63w=="],
 
@@ -1248,7 +1346,7 @@
 
     "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
 
-    "markdown-it": ["markdown-it@14.2.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ=="],
+    "markdown-it": ["markdown-it@14.3.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.5.0", "linkify-it": "^5.0.2", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw=="],
 
     "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
 
@@ -1270,7 +1368,9 @@
 
     "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
 
-    "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
+    "mdurl": ["mdurl@2.1.0", "", {}, "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg=="],
+
+    "meow": ["meow@14.1.0", "", {}, "sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw=="],
 
     "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
 
@@ -1342,7 +1442,7 @@
 
     "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
 
-    "minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
+    "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
 
     "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
 
@@ -1350,27 +1450,27 @@
 
     "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
 
-    "mocha": ["mocha@11.7.6", "", { "dependencies": { "browser-stdout": "^1.3.1", "chokidar": "^4.0.1", "debug": "^4.3.5", "diff": "^7.0.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", "minimatch": "^9.0.5", "ms": "^2.1.3", "picocolors": "^1.1.1", "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", "workerpool": "^9.2.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "bin": { "mocha": "bin/mocha.js", "_mocha": "bin/_mocha" } }, "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA=="],
+    "mocha": ["mocha@11.8.0", "", { "dependencies": { "browser-stdout": "^1.3.1", "chokidar": "^4.0.1", "debug": "^4.3.5", "diff": "^7.0.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", "minimatch": "^9.0.5", "ms": "^2.1.3", "picocolors": "^1.1.1", "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", "workerpool": "^9.2.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "bin": { "mocha": "bin/mocha.js", "_mocha": "bin/_mocha" } }, "sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw=="],
 
     "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
 
     "mute-stream": ["mute-stream@0.0.8", "", {}, "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="],
 
-    "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
+    "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
 
     "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
 
     "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
 
-    "next": ["next@16.2.7", "", { "dependencies": { "@next/env": "16.2.7", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.7", "@next/swc-darwin-x64": "16.2.7", "@next/swc-linux-arm64-gnu": "16.2.7", "@next/swc-linux-arm64-musl": "16.2.7", "@next/swc-linux-x64-gnu": "16.2.7", "@next/swc-linux-x64-musl": "16.2.7", "@next/swc-win32-arm64-msvc": "16.2.7", "@next/swc-win32-x64-msvc": "16.2.7", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-eMJxgjRzBaj3olkP4cBamHDXL79A8FC6u1GcsO1D1Tsx8bw/LLXUJCaoajVxtnhD3A1IJqIT8IcRJjgBIPJq4w=="],
+    "next": ["next@16.3.1", "", { "dependencies": { "@next/env": "16.3.1", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.1", "@next/swc-darwin-x64": "16.3.1", "@next/swc-linux-arm64-gnu": "16.3.1", "@next/swc-linux-arm64-musl": "16.3.1", "@next/swc-linux-x64-gnu": "16.3.1", "@next/swc-linux-x64-musl": "16.3.1", "@next/swc-win32-arm64-msvc": "16.3.1", "@next/swc-win32-x64-msvc": "16.3.1", "sharp": "^0.35.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hsAp0i7Rh+/dhe7DGIeN2YlpLM1DP4MNxti9EtDMtqcO612X81MvvEj388/oTce9U1EcEIOWDlGq0zRwrBKvuA=="],
 
-    "node-abi": ["node-abi@3.92.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ=="],
+    "node-abi": ["node-abi@3.94.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g=="],
 
     "node-addon-api": ["node-addon-api@4.3.0", "", {}, "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="],
 
-    "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
+    "node-exports-info": ["node-exports-info@1.6.2", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag=="],
 
-    "node-releases": ["node-releases@2.0.47", "", {}, "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og=="],
+    "node-releases": ["node-releases@2.0.53", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="],
 
     "node-sarif-builder": ["node-sarif-builder@3.4.0", "", { "dependencies": { "@types/sarif": "^2.1.7", "fs-extra": "^11.1.1" } }, "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg=="],
 
@@ -1378,8 +1478,6 @@
 
     "normalize-package-data": ["normalize-package-data@6.0.2", "", { "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g=="],
 
-    "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
-
     "npm-install-checks": ["npm-install-checks@6.3.0", "", { "dependencies": { "semver": "^7.1.1" } }, "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw=="],
 
     "npm-normalize-package-bin": ["npm-normalize-package-bin@3.0.1", "", {}, "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ=="],
@@ -1414,17 +1512,17 @@
 
     "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
 
-    "ovsx": ["ovsx@1.0.0", "", { "dependencies": { "@vscode/vsce": "^3.7.1", "commander": "^6.2.1", "follow-redirects": "^1.16.0", "is-ci": "^2.0.0", "leven": "^3.1.0", "semver": "^7.6.0", "tmp": "^0.2.3", "yauzl-promise": "^4.0.0" }, "bin": { "ovsx": "bin/ovsx" } }, "sha512-bDxwb55DNbybe10chWlnofj6ZJuER1LjFaQKUtUL9SW8l9P5Vqn7LfI23IFtmtuSJ82pYX0R+MisomXPKNGR9A=="],
+    "ovsx": ["ovsx@1.1.1", "", { "dependencies": { "@inquirer/prompts": "^7.10.1", "@vscode/vsce": "^3.7.1", "commander": "^6.2.1", "cross-keychain": "^1.1.0", "follow-redirects": "^1.16.0", "is-ci": "^2.0.0", "leven": "^3.1.0", "semver": "^7.6.0", "tmp": "^0.2.3", "yauzl-promise": "^4.0.0" }, "bin": { "ovsx": "bin/ovsx" } }, "sha512-tklsCzvGVWKlM91Vc9U8tNnaQ+XacPJ12SWHjDaHGUJB49oMhoAULsJGeefhHebPvvckbcWbKqKIXODMZah5SA=="],
 
-    "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
+    "own-keys": ["own-keys@1.0.2", "", { "dependencies": { "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg=="],
 
-    "oxlint": ["oxlint@1.68.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.68.0", "@oxlint/binding-android-arm64": "1.68.0", "@oxlint/binding-darwin-arm64": "1.68.0", "@oxlint/binding-darwin-x64": "1.68.0", "@oxlint/binding-freebsd-x64": "1.68.0", "@oxlint/binding-linux-arm-gnueabihf": "1.68.0", "@oxlint/binding-linux-arm-musleabihf": "1.68.0", "@oxlint/binding-linux-arm64-gnu": "1.68.0", "@oxlint/binding-linux-arm64-musl": "1.68.0", "@oxlint/binding-linux-ppc64-gnu": "1.68.0", "@oxlint/binding-linux-riscv64-gnu": "1.68.0", "@oxlint/binding-linux-riscv64-musl": "1.68.0", "@oxlint/binding-linux-s390x-gnu": "1.68.0", "@oxlint/binding-linux-x64-gnu": "1.68.0", "@oxlint/binding-linux-x64-musl": "1.68.0", "@oxlint/binding-openharmony-arm64": "1.68.0", "@oxlint/binding-win32-arm64-msvc": "1.68.0", "@oxlint/binding-win32-ia32-msvc": "1.68.0", "@oxlint/binding-win32-x64-msvc": "1.68.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-dXcbq+xsmLrMy6T8d0euf3IYUfLmjHIE11pOxiUSi5LHkFZaYPv568R6sEjcavVpUxoaQe66UBuK4HEi74NxpA=="],
+    "oxlint": ["oxlint@1.79.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.79.0", "@oxlint/binding-android-arm64": "1.79.0", "@oxlint/binding-darwin-arm64": "1.79.0", "@oxlint/binding-darwin-x64": "1.79.0", "@oxlint/binding-freebsd-x64": "1.79.0", "@oxlint/binding-linux-arm-gnueabihf": "1.79.0", "@oxlint/binding-linux-arm-musleabihf": "1.79.0", "@oxlint/binding-linux-arm64-gnu": "1.79.0", "@oxlint/binding-linux-arm64-musl": "1.79.0", "@oxlint/binding-linux-ppc64-gnu": "1.79.0", "@oxlint/binding-linux-riscv64-gnu": "1.79.0", "@oxlint/binding-linux-riscv64-musl": "1.79.0", "@oxlint/binding-linux-s390x-gnu": "1.79.0", "@oxlint/binding-linux-x64-gnu": "1.79.0", "@oxlint/binding-linux-x64-musl": "1.79.0", "@oxlint/binding-openharmony-arm64": "1.79.0", "@oxlint/binding-win32-arm64-msvc": "1.79.0", "@oxlint/binding-win32-ia32-msvc": "1.79.0", "@oxlint/binding-win32-x64-msvc": "1.79.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg=="],
 
     "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
 
     "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
 
-    "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],
+    "p-map": ["p-map@7.0.6", "", {}, "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg=="],
 
     "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
 
@@ -1448,7 +1546,7 @@
 
     "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
 
-    "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
+    "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
 
     "path-type": ["path-type@6.0.0", "", {}, "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ=="],
 
@@ -1456,19 +1554,19 @@
 
     "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
 
-    "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
+    "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
 
     "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
 
     "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
 
-    "postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
+    "postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="],
 
     "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
 
     "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
 
-    "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
+    "prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="],
 
     "prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="],
 
@@ -1490,7 +1588,7 @@
 
     "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
 
-    "qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="],
+    "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="],
 
     "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
 
@@ -1500,9 +1598,9 @@
 
     "rc-config-loader": ["rc-config-loader@4.1.4", "", { "dependencies": { "debug": "^4.4.3", "js-yaml": "^4.1.1", "json5": "^2.2.3", "require-from-string": "^2.0.2" } }, "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ=="],
 
-    "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
+    "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
 
-    "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
+    "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
 
     "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
 
@@ -1514,7 +1612,7 @@
 
     "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
 
-    "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
+    "readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="],
 
     "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
 
@@ -1538,7 +1636,7 @@
 
     "rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="],
 
-    "rehype-pretty-code": ["rehype-pretty-code@0.14.3", "", { "dependencies": { "@types/hast": "^3.0.4", "hast-util-to-string": "^3.0.0", "parse-numeric-range": "^1.3.0", "rehype-parse": "^9.0.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "shiki": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" } }, "sha512-Cz692FeYusTjT5cfFWLc4r7JhgC3/JlJptgUh4iffBxWxUnWW1oqbWFi7jGCeq00DYUm8yzoTnvpocaYa5x82g=="],
+    "rehype-pretty-code": ["rehype-pretty-code@0.14.5", "", { "dependencies": { "@types/hast": "^3.0.4", "hast-util-to-string": "^3.0.0", "parse-numeric-range": "^1.3.0", "rehype-parse": "^9.0.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "shiki": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" } }, "sha512-+DhMMNMgOwmE0xu2RmagN63nR0GRObPIQ9TuoNdUYYIDcCr8nWXU1CJUG6yca7vg99Gp5d+62A/+ZvksmsQXjA=="],
 
     "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="],
 
@@ -1580,13 +1678,13 @@
 
     "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
 
-    "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
+    "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
 
     "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
 
     "secretlint": ["secretlint@10.2.2", "", { "dependencies": { "@secretlint/config-creator": "^10.2.2", "@secretlint/formatter": "^10.2.2", "@secretlint/node": "^10.2.2", "@secretlint/profiler": "^10.2.2", "debug": "^4.4.1", "globby": "^14.1.0", "read-pkg": "^9.0.1" }, "bin": "./bin/secretlint.js" }, "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg=="],
 
-    "semver": ["semver@7.8.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ=="],
+    "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
 
     "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="],
 
@@ -1596,15 +1694,15 @@
 
     "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
 
-    "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
+    "sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" } }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="],
 
     "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
 
     "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
 
-    "shiki": ["shiki@4.2.0", "", { "dependencies": { "@shikijs/core": "4.2.0", "@shikijs/engine-javascript": "4.2.0", "@shikijs/engine-oniguruma": "4.2.0", "@shikijs/langs": "4.2.0", "@shikijs/themes": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ=="],
+    "shiki": ["shiki@4.4.3", "", { "dependencies": { "@shikijs/core": "4.4.3", "@shikijs/engine-javascript": "4.4.3", "@shikijs/engine-oniguruma": "4.4.3", "@shikijs/langs": "4.4.3", "@shikijs/themes": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g=="],
 
-    "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
+    "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
 
     "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
 
@@ -1640,7 +1738,7 @@
 
     "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
 
-    "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+    "string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
 
     "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
 
@@ -1658,7 +1756,7 @@
 
     "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
 
-    "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+    "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
 
     "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
 
@@ -1686,13 +1784,13 @@
 
     "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
 
-    "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
+    "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="],
 
     "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
 
     "terminal-link": ["terminal-link@4.0.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "supports-hyperlinks": "^3.2.0" } }, "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA=="],
 
-    "test-exclude": ["test-exclude@7.0.2", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^10.2.2" } }, "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw=="],
+    "test-exclude": ["test-exclude@8.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^13.0.6", "minimatch": "^10.2.2" } }, "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ=="],
 
     "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
 
@@ -1732,9 +1830,9 @@
 
     "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
 
-    "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
+    "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
 
-    "typescript-eslint": ["typescript-eslint@8.60.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.60.1", "@typescript-eslint/parser": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA=="],
+    "typescript-eslint": ["typescript-eslint@8.67.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.67.0", "@typescript-eslint/parser": "8.67.0", "@typescript-eslint/typescript-estree": "8.67.0", "@typescript-eslint/utils": "8.67.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg=="],
 
     "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
 
@@ -1742,9 +1840,9 @@
 
     "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="],
 
-    "undici": ["undici@7.27.2", "", {}, "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA=="],
+    "undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
 
-    "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
+    "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
 
     "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
 
@@ -1768,7 +1866,7 @@
 
     "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
 
-    "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
+    "update-browserslist-db": ["update-browserslist-db@1.3.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ=="],
 
     "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
 
@@ -1798,11 +1896,11 @@
 
     "vfile-statistics": ["vfile-statistics@3.0.0", "", { "dependencies": { "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-/qlwqwWBWFOmpXujL/20P+Iuydil0rZZNglR+VNm6J0gpLHwuVM5s7g2TfVoswbXjZ4HuIhLMySEyIw5i7/D8w=="],
 
-    "vscode-jsonrpc": ["vscode-jsonrpc@9.0.0", "", {}, "sha512-+VvMmQPJhtvJ+8O+zu2JKIRiLxXF8NW7krWgyMGeOHrp4Cn23T5hc0v2LknNeopDOB70wghHAds7mKtcZ0I4Sg=="],
+    "vscode-jsonrpc": ["vscode-jsonrpc@9.0.1", "", {}, "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw=="],
 
-    "vscode-languageclient": ["vscode-languageclient@10.0.0", "", { "dependencies": { "minimatch": "^10.2.5", "semver": "^7.8.1", "vscode-languageserver-protocol": "3.18.0", "vscode-languageserver-textdocument": "1.0.13" } }, "sha512-3yRHFkktZQCCg8ehHnD2Z4DZ4mZ17FNo8bxM4OFt8wtpxNBAOZGHmpbIflZSkicvCxi+ozuWntbdeWiY0gP77w=="],
+    "vscode-languageclient": ["vscode-languageclient@10.1.0", "", { "dependencies": { "minimatch": "^10.2.5", "semver": "^7.8.1", "vscode-languageserver-protocol": "3.18.2", "vscode-languageserver-textdocument": "1.0.13" } }, "sha512-XXRx6lqVitQy/oOLr9MfNYRG+MbQkhXkDaxbQMiKxEm8zZNfheRFUKNb8UYNh2stn9btl2wQM5wZFJjJvoc+jA=="],
 
-    "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.18.0", "", { "dependencies": { "vscode-jsonrpc": "9.0.0", "vscode-languageserver-types": "3.18.0" } }, "sha512-Zdz+kJ12Iz6tc11xfZyEo501bBATHXrCjmMfnaR3pMnf1CoqZBKIynba3P+/bi9VEdrMbNtAVKYpKhbODvqy+Q=="],
+    "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.18.2", "", { "dependencies": { "vscode-jsonrpc": "9.0.1", "vscode-languageserver-types": "3.18.0" } }, "sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg=="],
 
     "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.13", "", {}, "sha512-nx0ZHwMGIsVkzFG3/VLeJYBLTaFBRuNdGDvevvjuoayU5EOS2fEYazOhtCM3PI9ClMMg5igc0uwXtAq4tJj+Dw=="],
 
@@ -1830,13 +1928,13 @@
 
     "workerpool": ["workerpool@9.3.4", "", {}, "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg=="],
 
-    "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+    "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
 
     "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
 
     "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
 
-    "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
+    "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
 
     "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
 
@@ -1850,13 +1948,13 @@
 
     "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
 
-    "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
+    "yargs": ["yargs@18.1.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg=="],
 
     "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
 
     "yargs-unparser": ["yargs-unparser@2.0.0", "", { "dependencies": { "camelcase": "^6.0.0", "decamelize": "^4.0.0", "flat": "^5.0.2", "is-plain-obj": "^2.1.0" } }, "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA=="],
 
-    "yauzl": ["yauzl@3.3.2", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-Md9ankxxN23wncAN8s7+Tn3Co52zLUPMtnrLAbVCnfG5d2tKBFfmygYSgXlqFgXObtzIgqkx7aNgDBpso9+4qA=="],
+    "yauzl": ["yauzl@3.4.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw=="],
 
     "yauzl-promise": ["yauzl-promise@4.0.0", "", { "dependencies": { "@node-rs/crc32": "^1.7.0", "is-it-type": "^5.1.2", "simple-invariant": "^2.0.1" } }, "sha512-/HCXpyHXJQQHvFq9noqrjfa/WpQC2XYs3vI7tBiAi4QiIU1knvYhZGaO1QPjwIVMdqflxbmwgMXtYeaRiAE0CA=="],
 
@@ -1864,6 +1962,8 @@
 
     "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
 
+    "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
+
     "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
 
     "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
@@ -1878,11 +1978,13 @@
 
     "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
 
-    "@eslint/config-array/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
+    "@inquirer/core/mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
 
-    "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
+    "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
 
-    "@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
+    "@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="],
+
+    "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
 
     "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
 
@@ -1896,6 +1998,12 @@
 
     "@npmcli/git/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
 
+    "@npmcli/map-workspaces/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
+
+    "@npmcli/map-workspaces/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
+
+    "@npmcli/package-json/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
+
     "@npmcli/package-json/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="],
 
     "@npmcli/promise-spawn/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
@@ -1904,27 +2012,23 @@
 
     "@secretlint/formatter/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
 
-    "@secretlint/formatter/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
-
     "@testing-library/jest-dom/aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
 
     "@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
 
     "@textlint/linter-formatter/pluralize": ["pluralize@2.0.0", "", {}, "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw=="],
 
-    "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
+    "@textlint/linter-formatter/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
 
-    "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
+    "@textlint/linter-formatter/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
 
-    "@vscode/vsce/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
+    "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="],
 
-    "@vscode/vsce/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
+    "c8/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="],
 
     "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
 
-    "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
-
-    "eslint/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
+    "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
 
     "eslint-plugin-react/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
 
@@ -1932,7 +2036,7 @@
 
     "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
 
-    "globby/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
+    "globby/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="],
 
     "happy-dom/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
 
@@ -1942,10 +2046,18 @@
 
     "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
 
+    "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
+
     "mocha/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
 
+    "mocha/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
+
+    "mocha/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
+
     "mocha/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
 
+    "mocha/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="],
+
     "node-exports-info/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
 
     "normalize-package-data/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="],
@@ -1960,7 +2072,7 @@
 
     "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
 
-    "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
+    "path-scurry/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
 
     "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
 
@@ -1970,15 +2082,21 @@
 
     "read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="],
 
+    "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+    "strip-ansi/ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
+
     "supports-hyperlinks/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
 
     "table/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
 
-    "test-exclude/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
+    "table/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
 
-    "tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+    "table/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
 
-    "unified-engine/@types/node": ["@types/node@22.19.20", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw=="],
+    "unified-engine/@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="],
+
+    "unified-engine/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
 
     "unified-engine/ignore": ["ignore@6.0.2", "", {}, "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A=="],
 
@@ -1988,41 +2106,61 @@
 
     "vfile-reporter/supports-color": ["supports-color@9.4.0", "", {}, "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw=="],
 
-    "vscode-languageclient/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
+    "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
+
+    "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
+
+    "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+    "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+    "yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
 
     "yargs-unparser/is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="],
 
     "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
 
-    "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
+    "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
 
-    "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
+    "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
 
-    "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+    "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
 
     "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
 
     "@npmcli/git/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
 
+    "@npmcli/map-workspaces/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
+
+    "@npmcli/map-workspaces/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
+
+    "@npmcli/package-json/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
+
+    "@npmcli/package-json/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
+
     "@npmcli/package-json/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
 
     "@npmcli/promise-spawn/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
 
     "@secretlint/config-loader/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
 
-    "@secretlint/formatter/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+    "c8/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
 
-    "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
+    "c8/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
 
-    "@vscode/vsce/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
+    "cliui/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
 
-    "@vscode/vsce/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
+    "eslint-plugin-react/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="],
 
-    "eslint-plugin-react/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="],
+    "mocha/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
 
-    "eslint/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
+    "mocha/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
 
-    "mocha/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
+    "mocha/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
+
+    "mocha/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
+
+    "mocha/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
 
     "normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
 
@@ -2030,32 +2168,50 @@
 
     "table/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
 
-    "test-exclude/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
-
     "unified-engine/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
 
+    "unified-engine/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
+
+    "unified-engine/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
+
     "unified-engine/parse-json/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
 
     "vfile-reporter/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
 
-    "vfile-reporter/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
+    "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
+
+    "@npmcli/map-workspaces/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
+
+    "@npmcli/map-workspaces/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+    "@npmcli/package-json/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
+
+    "@npmcli/package-json/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
+
+    "c8/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+    "c8/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+    "c8/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+    "eslint-plugin-react/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
 
-    "vscode-languageclient/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
+    "mocha/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
 
-    "@eslint/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+    "mocha/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
 
-    "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+    "mocha/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
 
-    "@vscode/vsce/glob/path-scurry/lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="],
+    "mocha/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
 
-    "@vscode/vsce/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+    "mocha/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
 
-    "eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+    "unified-engine/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
 
-    "test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+    "unified-engine/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
 
-    "vfile-reporter/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+    "@npmcli/package-json/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
 
-    "vscode-languageclient/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+    "unified-engine/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
   }
 }
diff --git a/crates/vespertide-cli/Cargo.toml b/crates/vespertide-cli/Cargo.toml
index 3c41dd5d..ff23f478 100644
--- a/crates/vespertide-cli/Cargo.toml
+++ b/crates/vespertide-cli/Cargo.toml
@@ -37,11 +37,11 @@ vespertide-exporter = { workspace = true }
 
 [dev-dependencies]
 tempfile = "3"
-serial_test = "3"
+serial_test = "4"
 rstest = "0.26"
 assert_cmd = "2"
 predicates = "3"
-insta = "1.47"
+insta = "1.48"
 
 [[bin]]
 name = "vespertide"
diff --git a/crates/vespertide-core/Cargo.toml b/crates/vespertide-core/Cargo.toml
index 798a6514..b24637d8 100644
--- a/crates/vespertide-core/Cargo.toml
+++ b/crates/vespertide-core/Cargo.toml
@@ -14,7 +14,7 @@ readme = "../../README.md"
 [dependencies]
 proptest = { version = "1", optional = true, default-features = false, features = ["std", "bit-set"] }
 serde = { version = "1", features = ["derive"] }
-sea-orm = { version = "2.0.0-rc.40", default-features = false }
+sea-orm = { version = "2.0.2", default-features = false }
 schemars = { version = "1.2", optional = true }
 thiserror = "2"
 vespertide-naming = { workspace = true }
diff --git a/crates/vespertide-exporter/Cargo.toml b/crates/vespertide-exporter/Cargo.toml
index 36c2e0c0..c2c9ca0e 100644
--- a/crates/vespertide-exporter/Cargo.toml
+++ b/crates/vespertide-exporter/Cargo.toml
@@ -21,7 +21,7 @@ thiserror = "2"
 [dev-dependencies]
 criterion = { version = "0.8", features = ["html_reports"] }
 rstest = "0.26"
-insta = { version = "1.47", features = ["yaml"] }
+insta = { version = "1.48", features = ["yaml"] }
 proptest = "1"
 vespertide-core = { workspace = true, features = ["arbitrary"] }
 
diff --git a/crates/vespertide-loader/Cargo.toml b/crates/vespertide-loader/Cargo.toml
index b67046b2..26f1bb1f 100644
--- a/crates/vespertide-loader/Cargo.toml
+++ b/crates/vespertide-loader/Cargo.toml
@@ -23,7 +23,7 @@ serde_yaml = "0.9"
 [dev-dependencies]
 tempfile = "3"
 rstest = "0.26"
-serial_test = "3.5"
+serial_test = "4.0"
 
 [lints]
 workspace = true
diff --git a/crates/vespertide-lsp/Cargo.toml b/crates/vespertide-lsp/Cargo.toml
index 3723ecf9..a58a5166 100644
--- a/crates/vespertide-lsp/Cargo.toml
+++ b/crates/vespertide-lsp/Cargo.toml
@@ -60,7 +60,7 @@ vespertide-planner = { workspace = true }
 vespertide-loader = { workspace = true }
 
 [dev-dependencies]
-insta = { version = "1.47", features = ["json", "yaml"] }
+insta = { version = "1.48", features = ["json", "yaml"] }
 proptest = "1"
 rstest = "0.26"
 tempfile = "3"
diff --git a/crates/vespertide-macro/Cargo.toml b/crates/vespertide-macro/Cargo.toml
index 959f829b..4cc93b41 100644
--- a/crates/vespertide-macro/Cargo.toml
+++ b/crates/vespertide-macro/Cargo.toml
@@ -22,7 +22,7 @@ vespertide-query = { workspace = true }
 vespertide-planner = { workspace = true }
 # Only LitStr/Ident/Token/parse are used (no Expr/Item/Stmt), so the heavy
 # "full" syntax-tree feature is intentionally omitted to cut macro-crate build time.
-syn = { version = "2.0", features = ["parsing", "proc-macro"] }
+syn = { version = "3.0", features = ["parsing", "proc-macro"] }
 proc-macro2 = "1.0"
 quote = "1"
 proc-macro-crate = "3.5.0"
@@ -33,15 +33,15 @@ tempfile = "3"
 # and needs the full syntax tree + visitor. Supplying them here keeps the
 # production `syn` dependency lean (parsing+proc-macro only) while satisfying
 # the dev/test build via feature unification.
-syn = { version = "2.0", features = ["full", "visit"] }
+syn = { version = "3.0", features = ["full", "visit"] }
 runtime-macros = "1.1"
-trybuild = { version = "1.0.116", features = ["diff"] }
+trybuild = { version = "1.0.120", features = ["diff"] }
 # `serial_test::serial` for tests that mutate the process-global
 # `CARGO_MANIFEST_DIR` env var (4 tests in src/tests/mod.rs). Without this,
 # cargo's parallel test threads race on the env var and tests intermittently
 # observe a missing/wrong path — surfaced by the `Parallelism (RAYON_NUM_THREADS=1)`
 # CI job because rayon=1 changes the timing window enough to expose the race.
-serial_test = "3.5"
+serial_test = "4.0"
 
 [lints]
 workspace = true
diff --git a/crates/vespertide-planner/Cargo.toml b/crates/vespertide-planner/Cargo.toml
index 3f6ea481..db1bec11 100644
--- a/crates/vespertide-planner/Cargo.toml
+++ b/crates/vespertide-planner/Cargo.toml
@@ -20,9 +20,9 @@ thiserror = "2"
 [dev-dependencies]
 criterion = { version = "0.8", features = ["html_reports"] }
 rstest = "0.26"
-insta = "1.47"
+insta = "1.48"
 proptest = "1"
-serial_test = "3.5"
+serial_test = "4.0"
 vespertide-core = { workspace = true, features = ["arbitrary"] }
 
 [[bench]]
diff --git a/crates/vespertide-query/Cargo.toml b/crates/vespertide-query/Cargo.toml
index 9dd60b5e..96286c09 100644
--- a/crates/vespertide-query/Cargo.toml
+++ b/crates/vespertide-query/Cargo.toml
@@ -21,14 +21,14 @@ thiserror = "2"
 # (`=1.0.0-rc.33`). sea-orm is the primary dependency, so vespertide-query
 # tracks its sea-query line to keep a single unified version in the graph.
 # The caret form still permits stable 1.0.0 once sea-orm upgrades past the rc.
-sea-query = { version = "1.0.1", features = ["thread-safe"] }
+sea-query = { version = "1.0.2", features = ["thread-safe"] }
 
 [dev-dependencies]
 criterion = { version = "0.8", features = ["html_reports"] }
 rstest = "0.26"
-insta = "1.47"
+insta = "1.48"
 proptest = "1"
-serial_test = "3.5"
+serial_test = "4.0"
 vespertide-core = { workspace = true, features = ["arbitrary"] }
 # Daemon-free SQL validation:
 # - rusqlite (bundled): in-memory SQLite for real execution checks (no system libsqlite needed)
diff --git a/crates/vespertide/Cargo.toml b/crates/vespertide/Cargo.toml
index 6008610b..d5fd42ae 100644
--- a/crates/vespertide/Cargo.toml
+++ b/crates/vespertide/Cargo.toml
@@ -14,14 +14,14 @@ readme = "../../README.md"
 [dependencies]
 vespertide-macro = { workspace = true }
 vespertide-core = { workspace = true }
-sea-orm = { version = "2.0.0-rc.40", default-features = false }
+sea-orm = { version = "2.0.2", default-features = false }
 
 # Note: the sea-orm `sqlx-sqlite` test (runtime_database_error.rs) was moved to
 # the workspace-excluded crate `tests/runtime-sqlite/` because sqlx's libsqlite3-sys 0.30
 # conflicts on `links = "sqlite3"` with vespertide-query's rusqlite 0.39 (libsqlite3-sys 0.37).
 # Keeping both on latest required isolating the sqlx-sqlite test crate.
 [dev-dependencies]
-sea-orm = { version = "2.0.0-rc.40", default-features = false, features = ["proxy"] }
+sea-orm = { version = "2.0.2", default-features = false, features = ["proxy"] }
 tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
 
 [lints]
diff --git a/examples/app/Cargo.toml b/examples/app/Cargo.toml
index 23de54ea..a036bdc6 100644
--- a/examples/app/Cargo.toml
+++ b/examples/app/Cargo.toml
@@ -7,7 +7,7 @@ publish = false
 [dependencies]
 vespertide = { path = "../../crates/vespertide" }
 tokio = { version = "1", features = ["full"] }
-sea-orm = { version = "2.0.0-rc.40", features = ["sqlx-sqlite", "sqlx-postgres", "runtime-tokio-native-tls", "macros"] }
+sea-orm = { version = "2.0.2", features = ["sqlx-sqlite", "sqlx-postgres", "runtime-tokio-native-tls", "macros"] }
 anyhow = "1"
 serde = { version = "1", features = ["derive"] }
 vespera = "0.2.0"
diff --git a/package.json b/package.json
index cc0d9265..b0c69046 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,8 @@
   "description": "**FastAPI-like developer experience for Rust.** Zero-config OpenAPI 3.1 generation for Axum.",
   "author": "devfive",
   "devDependencies": {
-    "eslint-plugin-devup": "^2.0.19",
-    "oxlint": "^1.68.0",
+    "eslint-plugin-devup": "^2.0.20",
+    "oxlint": "^1.79.0",
     "husky": "^9.1",
     "bun-test-env-dom": "^1.0",
     "@devup-ui/bun-plugin": "^1.0",
diff --git a/tests/runtime-sqlite/Cargo.toml b/tests/runtime-sqlite/Cargo.toml
index 2c321e49..6fa6cd43 100644
--- a/tests/runtime-sqlite/Cargo.toml
+++ b/tests/runtime-sqlite/Cargo.toml
@@ -20,7 +20,7 @@ path = "src/lib.rs"
 
 [dev-dependencies]
 tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
-sea-orm = { version = "2.0.0-rc.40", default-features = false, features = ["runtime-tokio-native-tls", "sqlx-sqlite"] }
+sea-orm = { version = "2.0.2", default-features = false, features = ["runtime-tokio-native-tls", "sqlx-sqlite"] }
 vespertide = { path = "../../crates/vespertide" }
 
 # Mirror the workspace lint policy explicitly — `[lints] workspace = true`

From 4992fae6606c967ce7a4a7e4697faab03366e989 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 20 Aug 2026 12:04:47 +0900
Subject: [PATCH 098/109] =?UTF-8?q?fix(deps):=20typescript=EB=A5=BC=207.0.?=
 =?UTF-8?q?2=20=ED=94=84=EB=A6=AC=EB=B7=B0=EC=97=90=EC=84=9C=206.0.3?=
 =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=90=98=EB=8F=8C=EB=A0=A4=20landing=20?=
 =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EB=B3=B5=EA=B5=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

typescript 7.x는 컴파일러 본체를 `@typescript/typescript-` optional
dependency로 분리하는데, bun 1.3.9가 이 플랫폼 패키지를 어느 OS에서도 설치하지
않는다. lockfile에는 20개 항목이 모두 기록돼 있지만 `node_modules`에는 하나도
풀리지 않아, `next build`의 타입 체크 단계에서 `tsc`가

  Error: Unable to resolve @typescript/typescript-linux-x64

로 죽는다(Deploy to GitHub Pages / build 잡 실패). Windows 로컬에서도 동일하게
`node_modules/.bun/typescript@7.0.2`만 있고 `@typescript/*`는 비어 있어
플랫폼 한정 문제가 아니다.

main이 쓰는 6.0.3은 컴파일러가 패키지 안에 들어 있어 이 분리 자체가 없다.
landing과 vscode-extension 모두 6.0.3으로 되돌린다. 나머지 JS 의존성
최신화(@next/mdx, shiki, rehype-pretty-code, @types/node, esbuild,
vscode-languageclient 등)는 그대로 유지한다.

검증: `bun install` 후 `bun run --filter landing build`가 CI와 동일한 명령으로
통과(TypeScript 9.4s, 정적 페이지 21개 생성, exit 0).
---
 apps/landing/package.json          | 2 +-
 apps/vscode-extension/package.json | 2 +-
 bun.lock                           | 8 +++++---
 3 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/apps/landing/package.json b/apps/landing/package.json
index 58c6a118..4a3fd6a7 100644
--- a/apps/landing/package.json
+++ b/apps/landing/package.json
@@ -39,6 +39,6 @@
     "@types/react": "^19",
     "@types/react-syntax-highlighter": "^15.5.13",
     "babel-plugin-react-compiler": "^1.0.0",
-    "typescript": "^7.0.2"
+    "typescript": "^6.0.3"
   }
 }
\ No newline at end of file
diff --git a/apps/vscode-extension/package.json b/apps/vscode-extension/package.json
index 1f2cbcfb..3cba1e9e 100644
--- a/apps/vscode-extension/package.json
+++ b/apps/vscode-extension/package.json
@@ -184,7 +184,7 @@
     "@vscode/vsce": "^3.9.2",
     "esbuild": "^0.28.2",
     "ovsx": "^1.1.1",
-    "typescript": "^7.0.2"
+    "typescript": "^6.0.3"
   },
   "vsce": {
     "dependencies": false
diff --git a/bun.lock b/bun.lock
index 65950845..695139da 100644
--- a/bun.lock
+++ b/bun.lock
@@ -45,7 +45,7 @@
         "@types/react": "^19",
         "@types/react-syntax-highlighter": "^15.5.13",
         "babel-plugin-react-compiler": "^1.0.0",
-        "typescript": "^7.0.2",
+        "typescript": "^6.0.3",
       },
     },
     "apps/vscode-extension": {
@@ -61,7 +61,7 @@
         "@vscode/vsce": "^3.9.2",
         "esbuild": "^0.28.2",
         "ovsx": "^1.1.1",
-        "typescript": "^7.0.2",
+        "typescript": "^6.0.3",
       },
     },
   },
@@ -1830,7 +1830,7 @@
 
     "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
 
-    "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
+    "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
 
     "typescript-eslint": ["typescript-eslint@8.67.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.67.0", "@typescript-eslint/parser": "8.67.0", "@typescript-eslint/typescript-estree": "8.67.0", "@typescript-eslint/utils": "8.67.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg=="],
 
@@ -2030,6 +2030,8 @@
 
     "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
 
+    "csstype-extra/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
+
     "eslint-plugin-react/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
 
     "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],

From 04b224b5e2e81c014c55d9e6af243ae55008b178 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 20 Aug 2026 12:04:48 +0900
Subject: [PATCH 099/109] =?UTF-8?q?chore(changepacks):=20=EC=84=B1?=
 =?UTF-8?q?=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94=20=EC=9B=A8=EC=9D=B4?=
 =?UTF-8?q?=EB=B8=8C=EC=9D=98=20breaking=20=EB=B3=80=EA=B2=BD=EC=9D=84=20?=
 =?UTF-8?q?=EC=84=A0=EC=96=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

cargo-semver-checks 게이트는 PR이 도입한 changepack에서만 release-type을 도출한다
(base에 이미 있는 descriptor는 게이트를 완화하지 않는다). 이 PR에는 descriptor가
없어 게이트가 Cargo.toml 버전(0.2.1, 미변경)만 보고 아래 5건을 breaking으로
차단하고 있었다.

- vespertide-config: NameCase::is_snake / is_camel / is_pascal 제거
- vespertide-core: EnumValues::variant_names / to_sql_values 제거
- vespertide-exporter: 비-non_exhaustive enum Orm에 Prisma 변형 추가
- vespertide-planner: find_primary_key_removals 인자 2 -> 1
- vespertide-query: sql::helpers의 build_schema_statement /
  build_query_statement / apply_column_type_with_table /
  build_sqlite_enum_check_clause / extract_check_clauses 제거

전부 의도된 변경이므로 되돌리지 않고 0.x 기준 breaking(Minor)으로 선언한다.
0.x에서는 Minor가 cargo-semver-checks의 major에 대응하므로 게이트가 열린다.
미선언 크레이트는 changepacks가 patch로 자동 승계한다(#177 참고).

검증: `cargo semver-checks --release-type major`로 9개 크레이트 전부
"no semver update required" (exit 0).
---
 .changepacks/changepack_log_mKQhxic2ssoZF-e_s_8GZ.json | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 .changepacks/changepack_log_mKQhxic2ssoZF-e_s_8GZ.json

diff --git a/.changepacks/changepack_log_mKQhxic2ssoZF-e_s_8GZ.json b/.changepacks/changepack_log_mKQhxic2ssoZF-e_s_8GZ.json
new file mode 100644
index 00000000..bb5a2af3
--- /dev/null
+++ b/.changepacks/changepack_log_mKQhxic2ssoZF-e_s_8GZ.json
@@ -0,0 +1 @@
+{"changes":{"crates/vespertide-config/Cargo.toml":"Minor","crates/vespertide-core/Cargo.toml":"Minor","crates/vespertide-exporter/Cargo.toml":"Minor","crates/vespertide-planner/Cargo.toml":"Minor","crates/vespertide-query/Cargo.toml":"Minor"},"note":"성능 최적화 웨이브: 미사용 public API 정리(NameCase::is_*, EnumValues::variant_names/to_sql_values, sql::helpers 일부, find_primary_key_removals 시그니처)와 Orm::Prisma 변형 추가로 0.x 기준 breaking","date":"2026-08-20T03:03:26.0689134Z"}

From dab4b36aadbdceae5a3e463c0a6288a2207a01d7 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 20 Aug 2026 12:31:33 +0900
Subject: [PATCH 100/109] =?UTF-8?q?test:=20tarpaulin=20100%=20=EA=B2=8C?=
 =?UTF-8?q?=EC=9D=B4=ED=8A=B8=EA=B0=80=20=EC=A7=80=EC=A0=81=ED=95=9C=20?=
 =?UTF-8?q?=EB=AF=B8=EC=BB=A4=EB=B2=84=20=EA=B2=BD=EB=A1=9C=EC=97=90=20?=
 =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

CI coverage 잡이 99.91%(11562/11572)로 --fail-under 100에 걸린다. 이 커밋은
그중 doctest로만 덮여 있던 경로와 분기가 통째로 빠져 있던 경로를 메운다.

vespertide-core/schema/names.rs
- `into_inner`: 기존 테스트는 전부 `String::from`(별도의 `From<$ty> for String`
  impl)을 지나가서 이 본문에 도달하지 못했다. 세 뉴타입 모두에서 직접 호출.
- `TableName::with_prefix`: 빈 접두사 조기 반환과 실제 prepend 경로. 지금까지
  doctest만 덮고 있었는데 tarpaulin은 doctest를 실행하지 않는다.

vespertide-cli/utils.rs — `render_migration_name`의 placeholder match
기존 케이스는 `%04v` / `%v` / `%m`만 지나가서 나머지 세 갈래가 비어 있었다.
- `%z_%v`: 알 수 없는 placeholder(`_ => i += 1`)
- `%03x-%m-tail`: 숫자 뒤가 `v`가 아닌 `b'0'` 갈래의 else
- `%v%`: 끝에 남은 홑 `%`(`i + 1 >= bytes.len()` 가드)
이로써 utils.rs는 86/88 -> 88/88.

vespertide-cli/commands/erd — `parse_reference`
`collect_foreign_key_relations`를 통해 간접적으로만 호출돼서 accept/reject 갈래가
각자의 region을 갖지 못했다. 6개 rstest 케이스로 직접 호출한다(정상,
3-세그먼트, 빈 테이블, 빈 컬럼, 구분자 없음, 빈 입력).

검증: cargo test -p vespertide-cli --bins 474 passed,
-p vespertide-core --all-features --lib 430 passed, clippy 무경고, fmt clean,
check-line-budget.sh 통과(erd/tests/mod.rs 1167 <= 1200).

남은 미커버 줄은 워크스페이스 전체 tarpaulin 실행에서만 재현된다. 패키지 단위
로컬 실행과 CI의 워크스페이스 실행이 서로 다른 줄을 지목해서(예: erd/mod.rs가
로컬 327 vs CI 356) 로컬만으로는 조준할 수 없다. CI 재측정 결과를 보고 잇는다.
---
 .../src/commands/erd/tests/mod.rs             | 19 +++++++++++++
 crates/vespertide-cli/src/utils.rs            | 17 ++++++++++-
 crates/vespertide-core/src/schema/names.rs    | 28 +++++++++++++++++++
 3 files changed, 63 insertions(+), 1 deletion(-)

diff --git a/crates/vespertide-cli/src/commands/erd/tests/mod.rs b/crates/vespertide-cli/src/commands/erd/tests/mod.rs
index 535e1ea5..7c50005e 100644
--- a/crates/vespertide-cli/src/commands/erd/tests/mod.rs
+++ b/crates/vespertide-cli/src/commands/erd/tests/mod.rs
@@ -1145,3 +1145,22 @@ fn foreign_key_column_groups_pushes_new_inline_group_after_table_constraint() {
         ]
     );
 }
+
+/// `parse_reference` is only reached indirectly (through
+/// `collect_foreign_key_relations`), which leaves its accept/reject arms
+/// attributed to a region the workspace-wide and single-package tarpaulin runs
+/// disagree about. Calling it directly pins every branch to its own region.
+#[rstest::rstest]
+#[case::table_and_column("users.id", Some(("users", "id")))]
+#[case::three_parts("a.b.c", None)]
+#[case::empty_table(".id", None)]
+#[case::empty_column("users.", None)]
+#[case::no_separator("users", None)]
+#[case::empty_input("", None)]
+fn parse_reference_accepts_only_table_dot_column(
+    #[case] input: &str,
+    #[case] expected: Option<(&str, &str)>,
+) {
+    let expected = expected.map(|(table, column)| (table.to_string(), vec![column.to_string()]));
+    assert_eq!(parse_reference(input), expected);
+}
diff --git a/crates/vespertide-cli/src/utils.rs b/crates/vespertide-cli/src/utils.rs
index 61710cb8..8f84be48 100644
--- a/crates/vespertide-cli/src/utils.rs
+++ b/crates/vespertide-cli/src/utils.rs
@@ -330,7 +330,22 @@ mod tests {
         FileFormat::Yaml,
         "한_%v_%m",
         "한_0004_añadir_tabla.vespertide.yaml"
-    )] // UTF-8 pattern + comment lock the byte-scanner and one-pass sanitize
+    )]
+    // UTF-8 pattern + comment lock the byte-scanner and one-pass sanitize
+    // The three cases below close the remaining arms of `render_migration_name`'s
+    // placeholder match. Each arm is a distinct `i` advance, and LLVM attributes
+    // the region to a different line depending on how the crate is built (the
+    // workspace-wide tarpaulin run and a single-package run disagree), so every
+    // arm needs its own case rather than relying on one representative pattern.
+    #[case(9, None, FileFormat::Json, "%z_%v", "%z_0009.vespertide.json")] // `_ => i += 1`: unknown placeholder is copied verbatim, scan resumes
+    #[case(
+        9,
+        Some("Fix Bug"),
+        FileFormat::Json,
+        "%03x-%m-tail",
+        "%03x-fix_bug-tail.vespertide.json"
+    )] // `b'0'` arm's else: digits not terminated by `v` fall through untouched
+    #[case(9, None, FileFormat::Json, "%v%", "0009%.vespertide.json")] // `i + 1 >= bytes.len()`: a trailing bare `%` is not a placeholder
     fn migration_filename_with_format_and_pattern_tests(
         #[case] version: u32,
         #[case] comment: Option<&str>,
diff --git a/crates/vespertide-core/src/schema/names.rs b/crates/vespertide-core/src/schema/names.rs
index 708c14eb..5f57735f 100644
--- a/crates/vespertide-core/src/schema/names.rs
+++ b/crates/vespertide-core/src/schema/names.rs
@@ -333,6 +333,34 @@ mod tests {
         assert_eq!(s, "ix_orders__id");
     }
 
+    #[test]
+    fn into_inner_consumes_newtype_back_into_string() {
+        // Covers the `into_inner` macro-expansion lines. `String::from` goes
+        // through the separate `From<$ty> for String` impl, so the tests above
+        // do not reach this body; call it explicitly on each newtype.
+        assert_eq!(TableName::new("user").into_inner(), "user");
+        assert_eq!(ColumnName::new("email").into_inner(), "email");
+        assert_eq!(
+            IndexName::new("ix_user__email").into_inner(),
+            "ix_user__email"
+        );
+    }
+
+    #[test]
+    fn with_prefix_empty_is_a_no_op() {
+        // Covers the `prefix.is_empty()` early return in `TableName::with_prefix`.
+        // Only the doctest exercised it, and doctests do not run under tarpaulin.
+        let unchanged = TableName::new("user").with_prefix("");
+        assert_eq!(unchanged.as_str(), "user");
+    }
+
+    #[test]
+    fn with_prefix_prepends_in_place() {
+        // Sibling of the empty case: the non-empty branch must actually prepend.
+        let prefixed = TableName::new("user").with_prefix("tenant_");
+        assert_eq!(prefixed.as_str(), "tenant_user");
+    }
+
     #[test]
     fn join_column_names_empty_returns_empty_string() {
         // Empty-slice path: `for ... in []` never runs, returns the

From 8e0dca88e36846d1f11e185fd5e92ac5bbc77bbb Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 20 Aug 2026 13:51:25 +0900
Subject: [PATCH 101/109] =?UTF-8?q?test:=20coverage=20=EA=B2=8C=EC=9D=B4?=
 =?UTF-8?q?=ED=8A=B8=EA=B0=80=20=EB=82=A8=EA=B8=B4=20=EB=AF=B8=EC=8B=A4?=
 =?UTF-8?q?=ED=96=89=20=EB=B6=84=EA=B8=B0=204=EA=B3=B3=EC=9D=84=20?=
 =?UTF-8?q?=EB=8D=AE=EB=8A=94=EB=8B=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

CI coverage가 99.96%(11567/11572)로 5줄 부족하다. 지목된 줄만 보면 닫는
중괄호나 함수 파라미터처럼 실행 대상이 아닌 위치라 오해하기 쉬운데, 실제로는
그 줄이 속한 함수 안에 한 번도 실행되지 않는 분기가 있고 tarpaulin이 대표 줄
하나만 보고하는 형태였다. 각 함수의 빠진 분기를 찾아 덮는다.

vespertide-query/sql/tests/helpers.rs — 3-backend 정책 위반 시정
`apply_numeric_type`은 `Postgres | MySql`(decimal_len)과 `Sqlite`(double)로
갈리는데, Numeric을 지나는 테스트가 전부 `DatabaseBackend::Postgres` 하나였다.
crates/vespertide-query/AGENTS.md의 N-BACKEND TRIPLE 정책이 명시적으로 금지하는
단일 백엔드 테스트다. 세 type-mapping 테스트에 `#[values(Postgres, MySql,
Sqlite)]` 축을 추가해 SQLite 갈래를 실행시킨다. 아울러
- precision 40 / scale 35 케이스로 `precision.min(28)` 클램프를 지나게 한다
  (기존 케이스는 전부 범위 안이라 클램프가 통과 경로였다)
- 정수 enum 케이스로 `values.is_integer()` 참 갈래를 덮는다

vespertide-lsp/diagnostics/validation/visitors.rs — YAML 갈래
`walk_columns_for_complex_type`는 `"object"`(JSON)와 `"block_mapping"`(YAML)을
함께 받는데 기존 `collect_complex_type_violations` 테스트 5개가 전부 JSON이라
YAML 갈래가 한 번도 실행되지 않았다. 모델은 YAML도 1급 포맷이므로
`test_support::parse_yaml`로 block_mapping 트리를 통과시킨다.

vespertide-cli/commands/erd — is_junction_table의 마지막 갈래
기존 픽스처는 두 길이 가드(PK<2, FK그룹<2)에서 걸러지거나 마지막
`primary_key_columns.iter().all(..)`를 통과해 true가 되는 경우뿐이었다. PK 2개 +
FK 그룹 2개를 모두 통과하지만 PK 하나(`seq`)가 FK가 아니라 `all(..)`이 false가
되는 경로 - 함수 끝까지 가서 거절하는 유일한 경로 - 를 추가하고,
`detect_cardinality`를 통해 M:N으로 분류되지 않음을 공개 경로에서 확인한다.

vespertide-lsp/references/search.rs — CHECK 식별자 비일치
`push_check_expr_matches`의 유일한 테스트가 `age > 0`(식별자 1개, 항상 일치)이라
`ident == column`의 거짓 갈래가 비어 있었다. `age > 0 AND score > age`로 일치·
비일치를 한 번에 지나게 한다.

검증: cargo test -p vespertide-{lsp,query,cli,core} --all-features 전부 통과
(0 failed), clippy --workspace --all-targets --all-features 무경고, fmt clean,
check-line-budget.sh 통과.

참고: 로컬 Windows에서 pg_query 링크가 막혀 있던 문제는
`RUSTFLAGS=-C link-arg=oldnames.lib`로 해소된다(MSVC가 strdup을 _strdup로만
제공해서 생긴 문제). 덕분에 sql_pg_query 타깃까지 로컬에서 돌렸다.
---
 .../src/commands/erd/tests/svg_coverage.rs    | 49 ++++++++++++++++
 .../src/diagnostics/validation/visitors.rs    | 19 ++++++
 .../vespertide-lsp/src/references/search.rs   | 37 ++++++++++++
 .../vespertide-query/src/sql/tests/helpers.rs | 58 +++++++++++++------
 4 files changed, 146 insertions(+), 17 deletions(-)

diff --git a/crates/vespertide-cli/src/commands/erd/tests/svg_coverage.rs b/crates/vespertide-cli/src/commands/erd/tests/svg_coverage.rs
index 47cf2f61..4f2a9747 100644
--- a/crates/vespertide-cli/src/commands/erd/tests/svg_coverage.rs
+++ b/crates/vespertide-cli/src/commands/erd/tests/svg_coverage.rs
@@ -26,6 +26,55 @@ fn is_junction_table_three_way_junction_returns_true() {
     );
 }
 
+// The two length guards above short-circuit before `is_junction_table`'s final
+// `primary_key_columns.iter().all(..)` check, and every other junction fixture
+// makes that check succeed. This table clears both guards (2 PK columns, 2 FK
+// groups) but one PK column (`seq`) is NOT a foreign key, so `all(..)` returns
+// false — the only path that reaches the end of the function and still rejects.
+#[test]
+fn is_junction_table_pk_column_outside_fk_set_returns_false() {
+    let not_a_junction = table(
+        "audit_entry",
+        vec![
+            primary_key("user_id", integer())
+                .foreign_key(ForeignKeySyntax::String("user.id".into())),
+            primary_key("seq", integer()),
+            foreign_key("tag_id", "tag.id"),
+        ],
+    );
+    assert!(
+        !is_junction_table(¬_a_junction),
+        "a PK column that is not part of any FK group disqualifies the table"
+    );
+}
+
+// `detect_cardinality` classifies through `is_junction_table` first. Driving the
+// false case above through the public relation collector proves the junction
+// guard is evaluated (and rejected) rather than short-circuited away.
+#[test]
+fn detect_cardinality_skips_many_to_many_when_pk_is_not_all_fk() {
+    let user = normalize(&table("user", vec![primary_key("id", integer())]));
+    let tag = normalize(&table("tag", vec![primary_key("id", integer())]));
+    let audit = normalize(&table(
+        "audit_entry",
+        vec![
+            primary_key("user_id", integer())
+                .foreign_key(ForeignKeySyntax::String("user.id".into())),
+            primary_key("seq", integer()),
+            foreign_key("tag_id", "tag.id"),
+        ],
+    ));
+
+    let relations = collect_foreign_key_relations(&[user, tag, audit]);
+    assert!(
+        relations
+            .iter()
+            .filter(|r| r.child_table == "audit_entry")
+            .all(|r| r.cardinality != Cardinality::ManyToMany),
+        "non-junction table must not classify as M:N, got: {relations:?}"
+    );
+}
+
 // Deterministic SVG fixture — pins bezier path coords (1 decimal) and
 // cardinality labels to kill ~400 coord-arithmetic mutants in svg/edges.rs.
 // Fixture includes: parallel edges (2 FKs child→parent), curved edges,
diff --git a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
index f67107ec..b1df406e 100644
--- a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
+++ b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
@@ -732,6 +732,25 @@ mod tests {
         );
     }
 
+    /// Every other `collect_complex_type_violations` test above feeds JSON, so
+    /// `walk_columns_for_complex_type` only ever matched the `"object"` arm and
+    /// its `"block_mapping"` sibling — the YAML shape — was never taken. Models
+    /// are a first-class YAML format, so the walker has to be exercised on a
+    /// `block_mapping` tree too.
+    #[test]
+    fn complex_type_walk_reaches_yaml_block_mapping_columns() {
+        let src = "name: u\ncolumns:\n  - name: c\n    type:\n      kind: custom\n";
+        let tree = crate::test_support::parse_yaml(src);
+        let mut out = Vec::new();
+
+        collect_complex_type_violations(&tree, src, &mut out);
+
+        assert!(
+            out.iter().any(|diag| diag.message.contains("custom_type")),
+            "YAML block_mapping column should reach inspect_complex_type; got: {out:?}"
+        );
+    }
+
     #[test]
     fn collect_syntax_errors_recurses_into_children() {
         let src = r#"{"columns":[{"name":"id",}]}"#;
diff --git a/crates/vespertide-lsp/src/references/search.rs b/crates/vespertide-lsp/src/references/search.rs
index 2be0f111..a44eb681 100644
--- a/crates/vespertide-lsp/src/references/search.rs
+++ b/crates/vespertide-lsp/src/references/search.rs
@@ -653,4 +653,41 @@ mod tests {
             "declaration and CHECK expression should both be found: {out:?}"
         );
     }
+
+    /// The test above only ever lexes a CHECK expression whose single column
+    /// identifier matches the symbol, so `push_check_expr_matches` never took
+    /// the `ident == column` false path. A predicate naming two columns forces
+    /// both: `age` is pushed, `score` is skipped.
+    #[test]
+    fn check_expr_matches_skip_identifiers_for_other_columns() {
+        let src = r#"{"name":"user","columns":[{"name":"age","type":"integer"},{"name":"score","type":"integer"}],"constraints":[{"type":"check","name":"c","expr":"age > 0 AND score > age"}]}"#;
+        let tree = parse_json(src);
+        let mut out = Vec::new();
+
+        collect_in_document(
+            &ReferenceSymbol::Column {
+                table: "user".to_string(),
+                column: "age".to_string(),
+            },
+            &uri("user.json"),
+            src,
+            &tree,
+            false,
+            &mut out,
+        );
+
+        let hits: Vec<&str> = out
+            .iter()
+            .map(|reference| &src[reference.byte_range.clone()])
+            .collect();
+        assert!(
+            hits.iter().all(|hit| *hit == "age"),
+            "only `age` identifiers may be reported, got: {hits:?}"
+        );
+        assert_eq!(
+            hits.len(),
+            2,
+            "both `age` occurrences in the CHECK predicate are references: {hits:?}"
+        );
+    }
 }
diff --git a/crates/vespertide-query/src/sql/tests/helpers.rs b/crates/vespertide-query/src/sql/tests/helpers.rs
index cfe4e438..78b27d82 100644
--- a/crates/vespertide-query/src/sql/tests/helpers.rs
+++ b/crates/vespertide-query/src/sql/tests/helpers.rs
@@ -6,8 +6,13 @@ use super::super::helpers::{
 use super::*;
 use proptest::prelude::*;
 use sea_query::{Alias, ColumnDef as SeaColumnDef, ForeignKeyAction};
-use vespertide_core::{ComplexColumnType, EnumValues};
+use vespertide_core::{ComplexColumnType, EnumValues, NumValue};
 
+// The `#[values(Postgres, MySql, Sqlite)]` axis on the three type-mapping tests
+// below is the N-BACKEND TRIPLE policy from `crates/vespertide-query/AGENTS.md`:
+// a type-mapping test that only runs Postgres hides every backend-specific arm.
+// Concretely, `apply_numeric_type` splits `Postgres | MySql` (decimal_len) from
+// `Sqlite` (double), so a Postgres-only Numeric case never reached the SQLite arm.
 #[rstest]
 #[case(ColumnType::Simple(SimpleColumnType::Integer))]
 #[case(ColumnType::Simple(SimpleColumnType::BigInt))]
@@ -17,10 +22,18 @@ use vespertide_core::{ComplexColumnType, EnumValues};
 #[case(ColumnType::Simple(SimpleColumnType::Uuid))]
 #[case(ColumnType::Complex(ComplexColumnType::Varchar { length: 255 }))]
 #[case(ColumnType::Complex(ComplexColumnType::Numeric { precision: 10, scale: 2 }))]
-fn test_column_type_conversion(#[case] ty: ColumnType) {
+fn test_column_type_conversion(
+    #[case] ty: ColumnType,
+    #[values(
+        DatabaseBackend::Postgres,
+        DatabaseBackend::MySql,
+        DatabaseBackend::Sqlite
+    )]
+    backend: DatabaseBackend,
+) {
     // Just ensure no panic - test by creating a column with this type
     let mut col = SeaColumnDef::new(Alias::new("test"));
-    apply_column_type_with_table(&mut col, &ty, "test_table", DatabaseBackend::Postgres);
+    apply_column_type_with_table(&mut col, &ty, "test_table", backend);
 }
 
 #[rstest]
@@ -43,30 +56,41 @@ fn test_column_type_conversion(#[case] ty: ColumnType) {
 #[case(SimpleColumnType::Cidr)]
 #[case(SimpleColumnType::Macaddr)]
 #[case(SimpleColumnType::Xml)]
-fn test_all_simple_types_cover_branches(#[case] ty: SimpleColumnType) {
-    let mut col = SeaColumnDef::new(Alias::new("t"));
-    apply_column_type_with_table(
-        &mut col,
-        &ColumnType::Simple(ty),
-        "test_table",
+fn test_all_simple_types_cover_branches(
+    #[case] ty: SimpleColumnType,
+    #[values(
         DatabaseBackend::Postgres,
-    );
+        DatabaseBackend::MySql,
+        DatabaseBackend::Sqlite
+    )]
+    backend: DatabaseBackend,
+) {
+    let mut col = SeaColumnDef::new(Alias::new("t"));
+    apply_column_type_with_table(&mut col, &ColumnType::Simple(ty), "test_table", backend);
 }
 
 #[rstest]
 #[case(ComplexColumnType::Varchar { length: 42 })]
 #[case(ComplexColumnType::Numeric { precision: 8, scale: 3 })]
+// Precision beyond the 28-digit ceiling exercises `apply_numeric_type`'s
+// `precision.min(28)` / `scale.min(safe_precision)` clamp, which a
+// within-range case leaves as a straight pass-through.
+#[case(ComplexColumnType::Numeric { precision: 40, scale: 35 })]
 #[case(ComplexColumnType::Char { length: 3 })]
 #[case(ComplexColumnType::Custom { custom_type: "GEOGRAPHY".into() })]
 #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec!["active".into(), "inactive".into()]) })]
-fn test_all_complex_types_cover_branches(#[case] ty: ComplexColumnType) {
-    let mut col = SeaColumnDef::new(Alias::new("t"));
-    apply_column_type_with_table(
-        &mut col,
-        &ColumnType::Complex(ty),
-        "test_table",
+#[case(ComplexColumnType::Enum { name: "level".into(), values: EnumValues::Integer(vec![NumValue { name: "low".into(), value: 0 }]) })]
+fn test_all_complex_types_cover_branches(
+    #[case] ty: ComplexColumnType,
+    #[values(
         DatabaseBackend::Postgres,
-    );
+        DatabaseBackend::MySql,
+        DatabaseBackend::Sqlite
+    )]
+    backend: DatabaseBackend,
+) {
+    let mut col = SeaColumnDef::new(Alias::new("t"));
+    apply_column_type_with_table(&mut col, &ColumnType::Complex(ty), "test_table", backend);
 }
 
 #[rstest]

From 43b9fc3dd525297b931f3d5353917a0c093bbc92 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 20 Aug 2026 14:04:52 +0900
Subject: [PATCH 102/109] =?UTF-8?q?refactor:=20tarpaulin=EC=9D=B4=20?=
 =?UTF-8?q?=EC=A0=91=EC=96=B4=EB=B2=84=EB=A6=AC=EB=8D=98=204=EA=B0=9C=20?=
 =?UTF-8?q?=EA=B5=AC=EB=AC=B8=EC=9D=84=20=EB=8B=A8=EC=9D=BC=20region?=
 =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=8E=B8=EB=8B=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

앞선 커밋으로 coverage가 99.97%(11568/11572)까지 올랐고, 남은 4줄은 전부 같은
성격이다. 지목된 줄은 닫는 중괄호, 함수 파라미터 행, 호출 인자 행처럼 그 자체로
실행 대상이 아닌 위치이고, 정작 그 줄이 속한 함수의 본문은 covered로 잡힌다.
tarpaulin --engine llvm이 다중 행 구문의 region을 접으면서 대표 행 하나를
미실행으로 남기는 형태다. 테스트로는 못 메우므로 구문을 한 행/한 region으로
접는다. 동작은 넷 다 그대로다.

query/sql/helpers.rs — 단일 호출 헬퍼를 호출부로 흡수
`apply_numeric_type`은 호출부가 한 곳뿐인데, 형제 match arm들이 전부 `col`의
메서드를 직접 부르는 것과 달리 자유 함수를 거쳤다. 그 호출 행만 callee 쪽으로
접혀 미실행으로 잡혔다. 본문을 Numeric arm 안으로 되돌려 호출 행 자체를 없앤다.

lsp/references/search.rs — 5-파라미터 시그니처를 메서드로
`push_check_expr_matches`의 파라미터 목록이 5행으로 감기면서 첫 행이 접혔다.
문서 단위 인자 셋(source/column/uri)을 `CheckExprScan`으로 묶고 메서드로 바꿔
시그니처를 한 행에 담는다. 내부의 `if let` 체인도 early-return + continue로 펴서
각 분기가 자기 region을 갖게 한다.

lsp/diagnostics/validation/visitors.rs — 재귀를 명시적 worklist로
`walk_columns_for_complex_type`은 루프 본문 끝이 자기 호출이라 루프 latch 행이
접혔다. `Vec` worklist를 쓰는 반복 형태로 바꾼다. 방문 집합은 동일하다.

query/sql/add_constraint/mod.rs — 두 table-not-found 테스트를 rstest로
PrimaryKey와 Check 두 케이스가 거의 같은 `#[test]`로 중복돼 있었고, 다중 행
인자 목록의 한 행이 접혔다. 크레이트 rstest 정책대로 하나의 파라메트릭 테스트로
합치고 호출을 한 행에 담는다(빈 스키마는 `&[]`로 직접 전달).

검증: cargo test -p vespertide-{lsp,query,cli} --all-features 전부 통과
(0 failed), clippy --workspace --all-targets --all-features 무경고, fmt clean.
---
 .../src/diagnostics/validation/visitors.rs    | 17 ++++--
 .../vespertide-lsp/src/references/search.rs   | 59 +++++++++++-------
 .../src/sql/add_constraint/mod.rs             | 60 ++++++++-----------
 crates/vespertide-query/src/sql/helpers.rs    | 44 +++++++-------
 4 files changed, 95 insertions(+), 85 deletions(-)

diff --git a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
index b1df406e..78dbd931 100644
--- a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
+++ b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
@@ -298,18 +298,25 @@ pub(in crate::diagnostics) fn collect_complex_type_violations(
     walk_columns_for_complex_type(columns, source_bytes, out);
 }
 
+/// Iterative rather than recursive: the recursive form ended each loop body
+/// with the self-call, and `tarpaulin --engine llvm` folded the loop's latch
+/// row out of the executed region even though the walk demonstrably runs. An
+/// explicit worklist keeps every row inside a single straight-line region.
 #[cfg(test)]
 fn walk_columns_for_complex_type(
     node: tree_sitter::Node<'_>,
     source: &[u8],
     out: &mut Vec,
 ) {
-    let mut cursor = node.walk();
-    for child in node.children(&mut cursor) {
-        if matches!(child.kind(), "object" | "block_mapping") {
-            inspect_complex_type(child, source, out);
+    let mut pending = vec![node];
+    while let Some(current) = pending.pop() {
+        let mut cursor = current.walk();
+        for child in current.children(&mut cursor) {
+            if matches!(child.kind(), "object" | "block_mapping") {
+                inspect_complex_type(child, source, out);
+            }
+            pending.push(child);
         }
-        walk_columns_for_complex_type(child, source, out);
     }
 }
 
diff --git a/crates/vespertide-lsp/src/references/search.rs b/crates/vespertide-lsp/src/references/search.rs
index a44eb681..14f883d4 100644
--- a/crates/vespertide-lsp/src/references/search.rs
+++ b/crates/vespertide-lsp/src/references/search.rs
@@ -195,7 +195,12 @@ fn inspect_pair(
                 if is_check_constraint_pair(source, pair)
                     && check_owning_table_matches(source, pair, table) =>
             {
-                push_check_expr_matches(value, source, column, uri, out);
+                CheckExprScan {
+                    source,
+                    column,
+                    uri,
+                }
+                .push_matches(value, out);
             }
             _ => {}
         }
@@ -274,30 +279,42 @@ fn outer_table_name<'a>(source: &'a [u8], node: tree_sitter::Node<'_>) -> Option
     direct_child_scalar(outer, source, "name")
 }
 
-/// Lex the CHECK expression in `value` and push a reference for every bare
-/// identifier matching `column`, with byte ranges absolute to the document.
-fn push_check_expr_matches(
-    value: tree_sitter::Node<'_>,
-    source: &[u8],
-    column: &str,
-    uri: &Uri,
-    out: &mut Vec,
-) {
-    if let Some(inner) = crate::check_expr_range::expr_inner_range(value)
-        && let Some(expr_text) = source
+/// Everything `push_check_expr_matches` needs about the document it is
+/// scanning. Grouping the three document-level arguments keeps the signature on
+/// one line: the previous five-parameter list wrapped across rows, and
+/// `tarpaulin --engine llvm` folded the first parameter row out of the
+/// function's executed region.
+struct CheckExprScan<'a> {
+    source: &'a [u8],
+    column: &'a str,
+    uri: &'a Uri,
+}
+
+impl CheckExprScan<'_> {
+    /// Lex the CHECK expression in `value` and push a reference for every bare
+    /// identifier matching `self.column`, byte ranges absolute to the document.
+    fn push_matches(&self, value: tree_sitter::Node<'_>, out: &mut Vec) {
+        let Some(inner) = crate::check_expr_range::expr_inner_range(value) else {
+            return;
+        };
+        let Some(expr_text) = self
+            .source
             .get(inner.clone())
             .and_then(|bytes| std::str::from_utf8(bytes).ok())
-    {
+        else {
+            return;
+        };
         for token in vespertide_planner::lex_check_expr(expr_text) {
-            if token.kind == vespertide_planner::CheckTokenKind::Column
-                && let Some(ident) = expr_text.get(token.span.clone())
-                && ident == column
-            {
-                out.push(DomainReference {
-                    uri: uri.clone(),
-                    byte_range: (inner.start + token.span.start)..(inner.start + token.span.end),
-                });
+            if token.kind != vespertide_planner::CheckTokenKind::Column {
+                continue;
+            }
+            if expr_text.get(token.span.clone()) != Some(self.column) {
+                continue;
             }
+            out.push(DomainReference {
+                uri: self.uri.clone(),
+                byte_range: (inner.start + token.span.start)..(inner.start + token.span.end),
+            });
         }
     }
 }
diff --git a/crates/vespertide-query/src/sql/add_constraint/mod.rs b/crates/vespertide-query/src/sql/add_constraint/mod.rs
index a3068740..f3210b59 100644
--- a/crates/vespertide-query/src/sql/add_constraint/mod.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/mod.rs
@@ -185,24 +185,31 @@ mod tests {
             assert_snapshot!(joined_sql(backend, &result));
         });
     }
-    #[test]
-    fn test_add_constraint_primary_key_sqlite_table_not_found() {
-        let constraint = TableConstraint::PrimaryKey {
-            columns: vec!["id".into()],
-            auto_increment: false,
-            strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
-        };
-        let current_schema = vec![]; // Empty schema - table not found
-        let result = build_add_constraint(
-            DatabaseBackend::Sqlite,
-            "users",
-            &constraint,
-            ¤t_schema,
-            &[],
+    /// PrimaryKey and Check are the two constraint kinds that route through the
+    /// SQLite table rebuild, so both must reject a missing table with the same
+    /// message. Parameterized rather than two near-identical `#[test]`s per the
+    /// crate's rstest policy; the call is kept on one line because the
+    /// multi-line argument list left an argument row that `tarpaulin --engine
+    /// llvm` folded out of the executed region.
+    #[rstest]
+    #[case::primary_key(TableConstraint::PrimaryKey {
+        columns: vec!["id".into()],
+        auto_increment: false,
+        strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
+    })]
+    #[case::check(TableConstraint::Check {
+        name: "chk_age".into(),
+        expr: "age > 0".into(),
+        strategy: vespertide_core::CheckViolationStrategy::default(),
+    })]
+    fn add_constraint_on_sqlite_requires_table_in_schema(#[case] constraint: TableConstraint) {
+        let r = build_add_constraint(DatabaseBackend::Sqlite, "users", &constraint, &[], &[]);
+        let err = r.expect_err("missing table must be rejected");
+        assert!(
+            err.to_string()
+                .contains("Table 'users' not found in current schema"),
+            "got: {err}"
         );
-        assert!(result.is_err());
-        let err_msg = result.unwrap_err().to_string();
-        assert!(err_msg.contains("Table 'users' not found in current schema"));
     }
 
     #[test]
@@ -393,25 +400,6 @@ mod tests {
         assert!(sql.contains("CREATE TABLE"));
     }
     #[test]
-    fn test_add_constraint_check_sqlite_table_not_found() {
-        let constraint = TableConstraint::Check {
-            name: "chk_age".into(),
-            expr: "age > 0".into(),
-            strategy: vespertide_core::CheckViolationStrategy::default(),
-        };
-        let current_schema = vec![]; // Empty schema - table not found
-        let result = build_add_constraint(
-            DatabaseBackend::Sqlite,
-            "users",
-            &constraint,
-            ¤t_schema,
-            &[],
-        );
-        assert!(result.is_err());
-        let err_msg = result.unwrap_err().to_string();
-        assert!(err_msg.contains("Table 'users' not found in current schema"));
-    }
-    #[test]
     fn test_add_constraint_check_sqlite_without_existing_check() {
         let constraint = TableConstraint::Check {
             name: "chk_age".into(),
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index 2f12dcec..9ce95cf3 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -159,8 +159,28 @@ fn apply_complex_column_type(
         ComplexColumnType::Varchar { length } => {
             col.string_len(*length);
         }
+        // Folded in from the former single-use `apply_numeric_type` helper: the
+        // sibling arms all call a `col` method directly, and routing this one
+        // through a free function left its call site as a region `tarpaulin
+        // --engine llvm` folded into the callee, reporting an executed line as
+        // uncovered. Behaviour is unchanged.
         ComplexColumnType::Numeric { precision, scale } => {
-            apply_numeric_type(col, *precision, *scale, backend);
+            debug_assert!(
+                *scale <= *precision,
+                "numeric scale ({scale}) must be <= precision ({precision}); schema validation should reject this before SQL generation"
+            );
+            // `decimal_len` rejects oversized precision, and SQLite has no
+            // fixed-point type at all, so clamp first and fan out per backend.
+            let safe_precision = (*precision).min(28);
+            let safe_scale = (*scale).min(safe_precision);
+            match backend {
+                DatabaseBackend::Postgres | DatabaseBackend::MySql => {
+                    col.decimal_len(safe_precision, safe_scale);
+                }
+                DatabaseBackend::Sqlite => {
+                    col.double();
+                }
+            }
         }
         ComplexColumnType::Char { length } => {
             col.char_len(*length);
@@ -194,28 +214,6 @@ fn enum_variant_aliases(values: &EnumValues) -> Vec {
     }
 }
 
-fn apply_numeric_type(
-    col: &mut SeaColumnDef,
-    precision: u32,
-    scale: u32,
-    backend: DatabaseBackend,
-) {
-    debug_assert!(
-        scale <= precision,
-        "numeric scale ({scale}) must be <= precision ({precision}); schema validation should reject this before SQL generation"
-    );
-    let safe_precision = precision.min(28);
-    let safe_scale = scale.min(safe_precision);
-    match backend {
-        DatabaseBackend::Postgres | DatabaseBackend::MySql => {
-            col.decimal_len(safe_precision, safe_scale);
-        }
-        DatabaseBackend::Sqlite => {
-            col.double();
-        }
-    }
-}
-
 /// Convert vespertide `ReferenceAction` to `sea_query` `ForeignKeyAction`
 pub(crate) fn to_sea_fk_action(action: &ReferenceAction) -> ForeignKeyAction {
     match action {

From 519974627eaf02aadf032fbe680c979638595015 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 20 Aug 2026 14:14:02 +0900
Subject: [PATCH 103/109] =?UTF-8?q?Revert=20"refactor:=20tarpaulin?=
 =?UTF-8?q?=EC=9D=B4=20=EC=A0=91=EC=96=B4=EB=B2=84=EB=A6=AC=EB=8D=98=204?=
 =?UTF-8?q?=EA=B0=9C=20=EA=B5=AC=EB=AC=B8=EC=9D=84=20=EB=8B=A8=EC=9D=BC=20?=
 =?UTF-8?q?region=EC=9C=BC=EB=A1=9C=20=ED=8E=B8=EB=8B=A4"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This reverts commit 43b9fc3dd525297b931f3d5353917a0c093bbc92.
---
 .../src/diagnostics/validation/visitors.rs    | 17 ++----
 .../vespertide-lsp/src/references/search.rs   | 59 +++++++-----------
 .../src/sql/add_constraint/mod.rs             | 60 +++++++++++--------
 crates/vespertide-query/src/sql/helpers.rs    | 44 +++++++-------
 4 files changed, 85 insertions(+), 95 deletions(-)

diff --git a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
index 78dbd931..b1df406e 100644
--- a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
+++ b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
@@ -298,25 +298,18 @@ pub(in crate::diagnostics) fn collect_complex_type_violations(
     walk_columns_for_complex_type(columns, source_bytes, out);
 }
 
-/// Iterative rather than recursive: the recursive form ended each loop body
-/// with the self-call, and `tarpaulin --engine llvm` folded the loop's latch
-/// row out of the executed region even though the walk demonstrably runs. An
-/// explicit worklist keeps every row inside a single straight-line region.
 #[cfg(test)]
 fn walk_columns_for_complex_type(
     node: tree_sitter::Node<'_>,
     source: &[u8],
     out: &mut Vec,
 ) {
-    let mut pending = vec![node];
-    while let Some(current) = pending.pop() {
-        let mut cursor = current.walk();
-        for child in current.children(&mut cursor) {
-            if matches!(child.kind(), "object" | "block_mapping") {
-                inspect_complex_type(child, source, out);
-            }
-            pending.push(child);
+    let mut cursor = node.walk();
+    for child in node.children(&mut cursor) {
+        if matches!(child.kind(), "object" | "block_mapping") {
+            inspect_complex_type(child, source, out);
         }
+        walk_columns_for_complex_type(child, source, out);
     }
 }
 
diff --git a/crates/vespertide-lsp/src/references/search.rs b/crates/vespertide-lsp/src/references/search.rs
index 14f883d4..a44eb681 100644
--- a/crates/vespertide-lsp/src/references/search.rs
+++ b/crates/vespertide-lsp/src/references/search.rs
@@ -195,12 +195,7 @@ fn inspect_pair(
                 if is_check_constraint_pair(source, pair)
                     && check_owning_table_matches(source, pair, table) =>
             {
-                CheckExprScan {
-                    source,
-                    column,
-                    uri,
-                }
-                .push_matches(value, out);
+                push_check_expr_matches(value, source, column, uri, out);
             }
             _ => {}
         }
@@ -279,42 +274,30 @@ fn outer_table_name<'a>(source: &'a [u8], node: tree_sitter::Node<'_>) -> Option
     direct_child_scalar(outer, source, "name")
 }
 
-/// Everything `push_check_expr_matches` needs about the document it is
-/// scanning. Grouping the three document-level arguments keeps the signature on
-/// one line: the previous five-parameter list wrapped across rows, and
-/// `tarpaulin --engine llvm` folded the first parameter row out of the
-/// function's executed region.
-struct CheckExprScan<'a> {
-    source: &'a [u8],
-    column: &'a str,
-    uri: &'a Uri,
-}
-
-impl CheckExprScan<'_> {
-    /// Lex the CHECK expression in `value` and push a reference for every bare
-    /// identifier matching `self.column`, byte ranges absolute to the document.
-    fn push_matches(&self, value: tree_sitter::Node<'_>, out: &mut Vec) {
-        let Some(inner) = crate::check_expr_range::expr_inner_range(value) else {
-            return;
-        };
-        let Some(expr_text) = self
-            .source
+/// Lex the CHECK expression in `value` and push a reference for every bare
+/// identifier matching `column`, with byte ranges absolute to the document.
+fn push_check_expr_matches(
+    value: tree_sitter::Node<'_>,
+    source: &[u8],
+    column: &str,
+    uri: &Uri,
+    out: &mut Vec,
+) {
+    if let Some(inner) = crate::check_expr_range::expr_inner_range(value)
+        && let Some(expr_text) = source
             .get(inner.clone())
             .and_then(|bytes| std::str::from_utf8(bytes).ok())
-        else {
-            return;
-        };
+    {
         for token in vespertide_planner::lex_check_expr(expr_text) {
-            if token.kind != vespertide_planner::CheckTokenKind::Column {
-                continue;
-            }
-            if expr_text.get(token.span.clone()) != Some(self.column) {
-                continue;
+            if token.kind == vespertide_planner::CheckTokenKind::Column
+                && let Some(ident) = expr_text.get(token.span.clone())
+                && ident == column
+            {
+                out.push(DomainReference {
+                    uri: uri.clone(),
+                    byte_range: (inner.start + token.span.start)..(inner.start + token.span.end),
+                });
             }
-            out.push(DomainReference {
-                uri: self.uri.clone(),
-                byte_range: (inner.start + token.span.start)..(inner.start + token.span.end),
-            });
         }
     }
 }
diff --git a/crates/vespertide-query/src/sql/add_constraint/mod.rs b/crates/vespertide-query/src/sql/add_constraint/mod.rs
index f3210b59..a3068740 100644
--- a/crates/vespertide-query/src/sql/add_constraint/mod.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/mod.rs
@@ -185,31 +185,24 @@ mod tests {
             assert_snapshot!(joined_sql(backend, &result));
         });
     }
-    /// PrimaryKey and Check are the two constraint kinds that route through the
-    /// SQLite table rebuild, so both must reject a missing table with the same
-    /// message. Parameterized rather than two near-identical `#[test]`s per the
-    /// crate's rstest policy; the call is kept on one line because the
-    /// multi-line argument list left an argument row that `tarpaulin --engine
-    /// llvm` folded out of the executed region.
-    #[rstest]
-    #[case::primary_key(TableConstraint::PrimaryKey {
-        columns: vec!["id".into()],
-        auto_increment: false,
-        strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
-    })]
-    #[case::check(TableConstraint::Check {
-        name: "chk_age".into(),
-        expr: "age > 0".into(),
-        strategy: vespertide_core::CheckViolationStrategy::default(),
-    })]
-    fn add_constraint_on_sqlite_requires_table_in_schema(#[case] constraint: TableConstraint) {
-        let r = build_add_constraint(DatabaseBackend::Sqlite, "users", &constraint, &[], &[]);
-        let err = r.expect_err("missing table must be rejected");
-        assert!(
-            err.to_string()
-                .contains("Table 'users' not found in current schema"),
-            "got: {err}"
+    #[test]
+    fn test_add_constraint_primary_key_sqlite_table_not_found() {
+        let constraint = TableConstraint::PrimaryKey {
+            columns: vec!["id".into()],
+            auto_increment: false,
+            strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
+        };
+        let current_schema = vec![]; // Empty schema - table not found
+        let result = build_add_constraint(
+            DatabaseBackend::Sqlite,
+            "users",
+            &constraint,
+            ¤t_schema,
+            &[],
         );
+        assert!(result.is_err());
+        let err_msg = result.unwrap_err().to_string();
+        assert!(err_msg.contains("Table 'users' not found in current schema"));
     }
 
     #[test]
@@ -400,6 +393,25 @@ mod tests {
         assert!(sql.contains("CREATE TABLE"));
     }
     #[test]
+    fn test_add_constraint_check_sqlite_table_not_found() {
+        let constraint = TableConstraint::Check {
+            name: "chk_age".into(),
+            expr: "age > 0".into(),
+            strategy: vespertide_core::CheckViolationStrategy::default(),
+        };
+        let current_schema = vec![]; // Empty schema - table not found
+        let result = build_add_constraint(
+            DatabaseBackend::Sqlite,
+            "users",
+            &constraint,
+            ¤t_schema,
+            &[],
+        );
+        assert!(result.is_err());
+        let err_msg = result.unwrap_err().to_string();
+        assert!(err_msg.contains("Table 'users' not found in current schema"));
+    }
+    #[test]
     fn test_add_constraint_check_sqlite_without_existing_check() {
         let constraint = TableConstraint::Check {
             name: "chk_age".into(),
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index 9ce95cf3..2f12dcec 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -159,28 +159,8 @@ fn apply_complex_column_type(
         ComplexColumnType::Varchar { length } => {
             col.string_len(*length);
         }
-        // Folded in from the former single-use `apply_numeric_type` helper: the
-        // sibling arms all call a `col` method directly, and routing this one
-        // through a free function left its call site as a region `tarpaulin
-        // --engine llvm` folded into the callee, reporting an executed line as
-        // uncovered. Behaviour is unchanged.
         ComplexColumnType::Numeric { precision, scale } => {
-            debug_assert!(
-                *scale <= *precision,
-                "numeric scale ({scale}) must be <= precision ({precision}); schema validation should reject this before SQL generation"
-            );
-            // `decimal_len` rejects oversized precision, and SQLite has no
-            // fixed-point type at all, so clamp first and fan out per backend.
-            let safe_precision = (*precision).min(28);
-            let safe_scale = (*scale).min(safe_precision);
-            match backend {
-                DatabaseBackend::Postgres | DatabaseBackend::MySql => {
-                    col.decimal_len(safe_precision, safe_scale);
-                }
-                DatabaseBackend::Sqlite => {
-                    col.double();
-                }
-            }
+            apply_numeric_type(col, *precision, *scale, backend);
         }
         ComplexColumnType::Char { length } => {
             col.char_len(*length);
@@ -214,6 +194,28 @@ fn enum_variant_aliases(values: &EnumValues) -> Vec {
     }
 }
 
+fn apply_numeric_type(
+    col: &mut SeaColumnDef,
+    precision: u32,
+    scale: u32,
+    backend: DatabaseBackend,
+) {
+    debug_assert!(
+        scale <= precision,
+        "numeric scale ({scale}) must be <= precision ({precision}); schema validation should reject this before SQL generation"
+    );
+    let safe_precision = precision.min(28);
+    let safe_scale = scale.min(safe_precision);
+    match backend {
+        DatabaseBackend::Postgres | DatabaseBackend::MySql => {
+            col.decimal_len(safe_precision, safe_scale);
+        }
+        DatabaseBackend::Sqlite => {
+            col.double();
+        }
+    }
+}
+
 /// Convert vespertide `ReferenceAction` to `sea_query` `ForeignKeyAction`
 pub(crate) fn to_sea_fk_action(action: &ReferenceAction) -> ForeignKeyAction {
     match action {

From f217e921a00c5d85c5a44015c6d0aeeef1de776e Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 20 Aug 2026 14:26:17 +0900
Subject: [PATCH 104/109] =?UTF-8?q?refactor:=20coverage=EA=B0=80=20?=
 =?UTF-8?q?=EC=A0=91=EC=96=B4=EB=B2=84=EB=A6=AC=EB=8D=98=204=EA=B0=9C=20?=
 =?UTF-8?q?=EC=A7=80=EC=A0=90=EC=9D=84=20=EB=8D=94=20=EC=9E=91=EC=9D=80=20?=
 =?UTF-8?q?=ED=95=A8=EC=88=98=EB=A1=9C=20=EB=B6=84=ED=95=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

앞선 시도는 방향이 반대였다. 다중 행 구문을 한 행으로 "합치면" 접히는 anchor가
옮겨가거나 새 구문이 새 anchor를 만들어 오히려 4줄 -> 6줄로 늘었다(되돌림).
반대로 "쪼개면" 각 조각이 자기 함수 본문이 되어 region을 확보한다.

query/sql/helpers.rs
- `apply_complex_column_type`의 Enum arm 본문(if/else + 지역 변수 2개)을
  `apply_enum_column_type`으로 분리해 dispatch match를 arm당 한 문장으로 만든다.
- `apply_numeric_type`을 `clamp_numeric_precision`(클램프 산술) +
  `apply_numeric_for_backend`(백엔드 분기)로 쪼갠다. 잎 함수 하나가 통째로
  inline되면서 호출 행이 callee 쪽으로 흡수되던 것을 끊는다.

lsp/diagnostics/validation/visitors.rs
- `walk_columns_for_complex_type`의 루프 본문을 `visit_complex_type_child`로
  분리해 루프가 한 문장이 되게 한다.

lsp/references/search.rs
- `push_check_expr_matches`를 셋으로 쪼갠다: `check_expr_text`(두 None 탈출),
  `matching_column_spans`(토큰 필터), `push_absolute_spans`(문서 좌표 변환).
  결합된 `if let` 체인 대신 각 단계가 자기 함수가 된다.

query/sql/add_constraint/mod.rs
- 두 `*_table_not_found` 테스트가 공유하던 5-인자 호출을
  `sqlite_missing_table_error` 헬퍼로 뽑아 각 테스트 본문을 한 문장으로 만든다.

동작은 넷 다 그대로다. 검증: cargo test -p vespertide-{lsp,query} --all-features
전부 통과(0 failed), clippy --workspace --all-targets --all-features 무경고,
fmt clean.
---
 .../src/diagnostics/validation/visitors.rs    | 19 ++++--
 .../vespertide-lsp/src/references/search.rs   | 63 ++++++++++++++-----
 .../src/sql/add_constraint/mod.rs             | 31 ++++-----
 crates/vespertide-query/src/sql/helpers.rs    | 49 +++++++++++----
 4 files changed, 111 insertions(+), 51 deletions(-)

diff --git a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
index b1df406e..e4713e1f 100644
--- a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
+++ b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
@@ -306,11 +306,22 @@ fn walk_columns_for_complex_type(
 ) {
     let mut cursor = node.walk();
     for child in node.children(&mut cursor) {
-        if matches!(child.kind(), "object" | "block_mapping") {
-            inspect_complex_type(child, source, out);
-        }
-        walk_columns_for_complex_type(child, source, out);
+        visit_complex_type_child(child, source, out);
+    }
+}
+
+/// Body of [`walk_columns_for_complex_type`]'s loop, split out so the loop is a
+/// single statement.
+#[cfg(test)]
+fn visit_complex_type_child(
+    child: tree_sitter::Node<'_>,
+    source: &[u8],
+    out: &mut Vec,
+) {
+    if matches!(child.kind(), "object" | "block_mapping") {
+        inspect_complex_type(child, source, out);
     }
+    walk_columns_for_complex_type(child, source, out);
 }
 
 fn inspect_complex_type(
diff --git a/crates/vespertide-lsp/src/references/search.rs b/crates/vespertide-lsp/src/references/search.rs
index a44eb681..02b16425 100644
--- a/crates/vespertide-lsp/src/references/search.rs
+++ b/crates/vespertide-lsp/src/references/search.rs
@@ -283,22 +283,55 @@ fn push_check_expr_matches(
     uri: &Uri,
     out: &mut Vec,
 ) {
-    if let Some(inner) = crate::check_expr_range::expr_inner_range(value)
-        && let Some(expr_text) = source
-            .get(inner.clone())
-            .and_then(|bytes| std::str::from_utf8(bytes).ok())
-    {
-        for token in vespertide_planner::lex_check_expr(expr_text) {
-            if token.kind == vespertide_planner::CheckTokenKind::Column
-                && let Some(ident) = expr_text.get(token.span.clone())
-                && ident == column
-            {
-                out.push(DomainReference {
-                    uri: uri.clone(),
-                    byte_range: (inner.start + token.span.start)..(inner.start + token.span.end),
-                });
-            }
+    let Some((inner, expr_text)) = check_expr_text(value, source) else {
+        return;
+    };
+    let spans = matching_column_spans(expr_text, column);
+    push_absolute_spans(spans, inner.start, uri, out);
+}
+
+/// Byte range of the CHECK predicate inside `value`, plus the predicate text.
+/// Split from [`push_check_expr_matches`] so the two `None` exits are their own
+/// unit rather than arms of a combined `if let` chain.
+fn check_expr_text<'a>(
+    value: tree_sitter::Node<'_>,
+    source: &'a [u8],
+) -> Option<(std::ops::Range, &'a str)> {
+    let inner = crate::check_expr_range::expr_inner_range(value)?;
+    let bytes = source.get(inner.clone())?;
+    let text = std::str::from_utf8(bytes).ok()?;
+    Some((inner, text))
+}
+
+/// Spans of every bare column identifier in `expr_text` that names `column`,
+/// relative to `expr_text`.
+fn matching_column_spans(expr_text: &str, column: &str) -> Vec> {
+    let mut spans = Vec::new();
+    for token in vespertide_planner::lex_check_expr(expr_text) {
+        if token.kind != vespertide_planner::CheckTokenKind::Column {
+            continue;
         }
+        if expr_text.get(token.span.clone()) != Some(column) {
+            continue;
+        }
+        spans.push(token.span);
+    }
+    spans
+}
+
+/// Shift `spans` by `offset` into document coordinates and record each as a
+/// reference.
+fn push_absolute_spans(
+    spans: Vec>,
+    offset: usize,
+    uri: &Uri,
+    out: &mut Vec,
+) {
+    for span in spans {
+        out.push(DomainReference {
+            uri: uri.clone(),
+            byte_range: (offset + span.start)..(offset + span.end),
+        });
     }
 }
 
diff --git a/crates/vespertide-query/src/sql/add_constraint/mod.rs b/crates/vespertide-query/src/sql/add_constraint/mod.rs
index a3068740..bc2356fb 100644
--- a/crates/vespertide-query/src/sql/add_constraint/mod.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/mod.rs
@@ -192,16 +192,7 @@ mod tests {
             auto_increment: false,
             strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
         };
-        let current_schema = vec![]; // Empty schema - table not found
-        let result = build_add_constraint(
-            DatabaseBackend::Sqlite,
-            "users",
-            &constraint,
-            ¤t_schema,
-            &[],
-        );
-        assert!(result.is_err());
-        let err_msg = result.unwrap_err().to_string();
+        let err_msg = sqlite_missing_table_error(&constraint);
         assert!(err_msg.contains("Table 'users' not found in current schema"));
     }
 
@@ -392,6 +383,15 @@ mod tests {
         let sql = joined_sql(DatabaseBackend::Sqlite, &queries);
         assert!(sql.contains("CREATE TABLE"));
     }
+    /// Adds `constraint` to `users` on SQLite against an empty schema and
+    /// returns the resulting error message. Split out of the two
+    /// `*_table_not_found` tests so each keeps a single-statement body.
+    fn sqlite_missing_table_error(constraint: &TableConstraint) -> String {
+        build_add_constraint(DatabaseBackend::Sqlite, "users", constraint, &[], &[])
+            .expect_err("empty schema must be rejected")
+            .to_string()
+    }
+
     #[test]
     fn test_add_constraint_check_sqlite_table_not_found() {
         let constraint = TableConstraint::Check {
@@ -399,16 +399,7 @@ mod tests {
             expr: "age > 0".into(),
             strategy: vespertide_core::CheckViolationStrategy::default(),
         };
-        let current_schema = vec![]; // Empty schema - table not found
-        let result = build_add_constraint(
-            DatabaseBackend::Sqlite,
-            "users",
-            &constraint,
-            ¤t_schema,
-            &[],
-        );
-        assert!(result.is_err());
-        let err_msg = result.unwrap_err().to_string();
+        let err_msg = sqlite_missing_table_error(&constraint);
         assert!(err_msg.contains("Table 'users' not found in current schema"));
     }
     #[test]
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index 2f12dcec..31bb4316 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -169,22 +169,28 @@ fn apply_complex_column_type(
             col.custom(Alias::new(custom_type));
         }
         ComplexColumnType::Enum { name, values } => {
-            // For integer enums, use INTEGER type instead of ENUM
-            if values.is_integer() {
-                col.integer();
-            } else {
-                // Use table-prefixed enum type name to avoid conflicts
-                let type_name = build_enum_type_name(table, name);
-                // Map each variant name straight into `Alias::new`, skipping the
-                // intermediate `Vec<&str>` that `variant_names()` would allocate.
-                let variants = enum_variant_aliases(values);
-                col.enumeration(Alias::new(&type_name), variants);
-            }
+            apply_enum_column_type(col, name, values, table);
         }
         _ => unreachable!("ComplexColumnType is #[non_exhaustive]; all variants are matched above"),
     }
 }
 
+/// Enum arm of [`apply_complex_column_type`], split out so the dispatch match
+/// stays one statement per arm.
+fn apply_enum_column_type(col: &mut SeaColumnDef, name: &str, values: &EnumValues, table: &str) {
+    // For integer enums, use INTEGER type instead of ENUM
+    if values.is_integer() {
+        col.integer();
+        return;
+    }
+    // Use table-prefixed enum type name to avoid conflicts
+    let type_name = build_enum_type_name(table, name);
+    // Map each variant name straight into `Alias::new`, skipping the
+    // intermediate `Vec<&str>` that `variant_names()` would allocate.
+    let variants = enum_variant_aliases(values);
+    col.enumeration(Alias::new(&type_name), variants);
+}
+
 /// Build the `sea_query` `Alias` list for a string enum's variants without the
 /// intermediate `Vec<&str>` that `EnumValues::variant_names()` would allocate.
 fn enum_variant_aliases(values: &EnumValues) -> Vec {
@@ -200,15 +206,34 @@ fn apply_numeric_type(
     scale: u32,
     backend: DatabaseBackend,
 ) {
+    let (safe_precision, safe_scale) = clamp_numeric_precision(precision, scale);
+    apply_numeric_for_backend(col, safe_precision, safe_scale, backend);
+}
+
+/// `decimal_len` rejects oversized precision, so clamp to the widest value every
+/// backend accepts. Split from [`apply_numeric_type`] so the clamp arithmetic is
+/// its own unit.
+fn clamp_numeric_precision(precision: u32, scale: u32) -> (u32, u32) {
     debug_assert!(
         scale <= precision,
         "numeric scale ({scale}) must be <= precision ({precision}); schema validation should reject this before SQL generation"
     );
     let safe_precision = precision.min(28);
     let safe_scale = scale.min(safe_precision);
+    (safe_precision, safe_scale)
+}
+
+/// SQLite has no fixed-point type, so it falls back to `double`; the other two
+/// backends take the clamped `DECIMAL(p, s)`.
+fn apply_numeric_for_backend(
+    col: &mut SeaColumnDef,
+    precision: u32,
+    scale: u32,
+    backend: DatabaseBackend,
+) {
     match backend {
         DatabaseBackend::Postgres | DatabaseBackend::MySql => {
-            col.decimal_len(safe_precision, safe_scale);
+            col.decimal_len(precision, scale);
         }
         DatabaseBackend::Sqlite => {
             col.double();

From 9659357791556d9a5093268daff46c39ae732216 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 20 Aug 2026 14:35:59 +0900
Subject: [PATCH 105/109] =?UTF-8?q?Revert=20"refactor:=20coverage=EA=B0=80?=
 =?UTF-8?q?=20=EC=A0=91=EC=96=B4=EB=B2=84=EB=A6=AC=EB=8D=98=204=EA=B0=9C?=
 =?UTF-8?q?=20=EC=A7=80=EC=A0=90=EC=9D=84=20=EB=8D=94=20=EC=9E=91=EC=9D=80?=
 =?UTF-8?q?=20=ED=95=A8=EC=88=98=EB=A1=9C=20=EB=B6=84=ED=95=B4"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This reverts commit f217e921a00c5d85c5a44015c6d0aeeef1de776e.
---
 .../src/diagnostics/validation/visitors.rs    | 19 ++----
 .../vespertide-lsp/src/references/search.rs   | 63 +++++--------------
 .../src/sql/add_constraint/mod.rs             | 31 +++++----
 crates/vespertide-query/src/sql/helpers.rs    | 49 ++++-----------
 4 files changed, 51 insertions(+), 111 deletions(-)

diff --git a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
index e4713e1f..b1df406e 100644
--- a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
+++ b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
@@ -306,22 +306,11 @@ fn walk_columns_for_complex_type(
 ) {
     let mut cursor = node.walk();
     for child in node.children(&mut cursor) {
-        visit_complex_type_child(child, source, out);
-    }
-}
-
-/// Body of [`walk_columns_for_complex_type`]'s loop, split out so the loop is a
-/// single statement.
-#[cfg(test)]
-fn visit_complex_type_child(
-    child: tree_sitter::Node<'_>,
-    source: &[u8],
-    out: &mut Vec,
-) {
-    if matches!(child.kind(), "object" | "block_mapping") {
-        inspect_complex_type(child, source, out);
+        if matches!(child.kind(), "object" | "block_mapping") {
+            inspect_complex_type(child, source, out);
+        }
+        walk_columns_for_complex_type(child, source, out);
     }
-    walk_columns_for_complex_type(child, source, out);
 }
 
 fn inspect_complex_type(
diff --git a/crates/vespertide-lsp/src/references/search.rs b/crates/vespertide-lsp/src/references/search.rs
index 02b16425..a44eb681 100644
--- a/crates/vespertide-lsp/src/references/search.rs
+++ b/crates/vespertide-lsp/src/references/search.rs
@@ -283,55 +283,22 @@ fn push_check_expr_matches(
     uri: &Uri,
     out: &mut Vec,
 ) {
-    let Some((inner, expr_text)) = check_expr_text(value, source) else {
-        return;
-    };
-    let spans = matching_column_spans(expr_text, column);
-    push_absolute_spans(spans, inner.start, uri, out);
-}
-
-/// Byte range of the CHECK predicate inside `value`, plus the predicate text.
-/// Split from [`push_check_expr_matches`] so the two `None` exits are their own
-/// unit rather than arms of a combined `if let` chain.
-fn check_expr_text<'a>(
-    value: tree_sitter::Node<'_>,
-    source: &'a [u8],
-) -> Option<(std::ops::Range, &'a str)> {
-    let inner = crate::check_expr_range::expr_inner_range(value)?;
-    let bytes = source.get(inner.clone())?;
-    let text = std::str::from_utf8(bytes).ok()?;
-    Some((inner, text))
-}
-
-/// Spans of every bare column identifier in `expr_text` that names `column`,
-/// relative to `expr_text`.
-fn matching_column_spans(expr_text: &str, column: &str) -> Vec> {
-    let mut spans = Vec::new();
-    for token in vespertide_planner::lex_check_expr(expr_text) {
-        if token.kind != vespertide_planner::CheckTokenKind::Column {
-            continue;
-        }
-        if expr_text.get(token.span.clone()) != Some(column) {
-            continue;
+    if let Some(inner) = crate::check_expr_range::expr_inner_range(value)
+        && let Some(expr_text) = source
+            .get(inner.clone())
+            .and_then(|bytes| std::str::from_utf8(bytes).ok())
+    {
+        for token in vespertide_planner::lex_check_expr(expr_text) {
+            if token.kind == vespertide_planner::CheckTokenKind::Column
+                && let Some(ident) = expr_text.get(token.span.clone())
+                && ident == column
+            {
+                out.push(DomainReference {
+                    uri: uri.clone(),
+                    byte_range: (inner.start + token.span.start)..(inner.start + token.span.end),
+                });
+            }
         }
-        spans.push(token.span);
-    }
-    spans
-}
-
-/// Shift `spans` by `offset` into document coordinates and record each as a
-/// reference.
-fn push_absolute_spans(
-    spans: Vec>,
-    offset: usize,
-    uri: &Uri,
-    out: &mut Vec,
-) {
-    for span in spans {
-        out.push(DomainReference {
-            uri: uri.clone(),
-            byte_range: (offset + span.start)..(offset + span.end),
-        });
     }
 }
 
diff --git a/crates/vespertide-query/src/sql/add_constraint/mod.rs b/crates/vespertide-query/src/sql/add_constraint/mod.rs
index bc2356fb..a3068740 100644
--- a/crates/vespertide-query/src/sql/add_constraint/mod.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/mod.rs
@@ -192,7 +192,16 @@ mod tests {
             auto_increment: false,
             strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
         };
-        let err_msg = sqlite_missing_table_error(&constraint);
+        let current_schema = vec![]; // Empty schema - table not found
+        let result = build_add_constraint(
+            DatabaseBackend::Sqlite,
+            "users",
+            &constraint,
+            ¤t_schema,
+            &[],
+        );
+        assert!(result.is_err());
+        let err_msg = result.unwrap_err().to_string();
         assert!(err_msg.contains("Table 'users' not found in current schema"));
     }
 
@@ -383,15 +392,6 @@ mod tests {
         let sql = joined_sql(DatabaseBackend::Sqlite, &queries);
         assert!(sql.contains("CREATE TABLE"));
     }
-    /// Adds `constraint` to `users` on SQLite against an empty schema and
-    /// returns the resulting error message. Split out of the two
-    /// `*_table_not_found` tests so each keeps a single-statement body.
-    fn sqlite_missing_table_error(constraint: &TableConstraint) -> String {
-        build_add_constraint(DatabaseBackend::Sqlite, "users", constraint, &[], &[])
-            .expect_err("empty schema must be rejected")
-            .to_string()
-    }
-
     #[test]
     fn test_add_constraint_check_sqlite_table_not_found() {
         let constraint = TableConstraint::Check {
@@ -399,7 +399,16 @@ mod tests {
             expr: "age > 0".into(),
             strategy: vespertide_core::CheckViolationStrategy::default(),
         };
-        let err_msg = sqlite_missing_table_error(&constraint);
+        let current_schema = vec![]; // Empty schema - table not found
+        let result = build_add_constraint(
+            DatabaseBackend::Sqlite,
+            "users",
+            &constraint,
+            ¤t_schema,
+            &[],
+        );
+        assert!(result.is_err());
+        let err_msg = result.unwrap_err().to_string();
         assert!(err_msg.contains("Table 'users' not found in current schema"));
     }
     #[test]
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index 31bb4316..2f12dcec 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -169,28 +169,22 @@ fn apply_complex_column_type(
             col.custom(Alias::new(custom_type));
         }
         ComplexColumnType::Enum { name, values } => {
-            apply_enum_column_type(col, name, values, table);
+            // For integer enums, use INTEGER type instead of ENUM
+            if values.is_integer() {
+                col.integer();
+            } else {
+                // Use table-prefixed enum type name to avoid conflicts
+                let type_name = build_enum_type_name(table, name);
+                // Map each variant name straight into `Alias::new`, skipping the
+                // intermediate `Vec<&str>` that `variant_names()` would allocate.
+                let variants = enum_variant_aliases(values);
+                col.enumeration(Alias::new(&type_name), variants);
+            }
         }
         _ => unreachable!("ComplexColumnType is #[non_exhaustive]; all variants are matched above"),
     }
 }
 
-/// Enum arm of [`apply_complex_column_type`], split out so the dispatch match
-/// stays one statement per arm.
-fn apply_enum_column_type(col: &mut SeaColumnDef, name: &str, values: &EnumValues, table: &str) {
-    // For integer enums, use INTEGER type instead of ENUM
-    if values.is_integer() {
-        col.integer();
-        return;
-    }
-    // Use table-prefixed enum type name to avoid conflicts
-    let type_name = build_enum_type_name(table, name);
-    // Map each variant name straight into `Alias::new`, skipping the
-    // intermediate `Vec<&str>` that `variant_names()` would allocate.
-    let variants = enum_variant_aliases(values);
-    col.enumeration(Alias::new(&type_name), variants);
-}
-
 /// Build the `sea_query` `Alias` list for a string enum's variants without the
 /// intermediate `Vec<&str>` that `EnumValues::variant_names()` would allocate.
 fn enum_variant_aliases(values: &EnumValues) -> Vec {
@@ -206,34 +200,15 @@ fn apply_numeric_type(
     scale: u32,
     backend: DatabaseBackend,
 ) {
-    let (safe_precision, safe_scale) = clamp_numeric_precision(precision, scale);
-    apply_numeric_for_backend(col, safe_precision, safe_scale, backend);
-}
-
-/// `decimal_len` rejects oversized precision, so clamp to the widest value every
-/// backend accepts. Split from [`apply_numeric_type`] so the clamp arithmetic is
-/// its own unit.
-fn clamp_numeric_precision(precision: u32, scale: u32) -> (u32, u32) {
     debug_assert!(
         scale <= precision,
         "numeric scale ({scale}) must be <= precision ({precision}); schema validation should reject this before SQL generation"
     );
     let safe_precision = precision.min(28);
     let safe_scale = scale.min(safe_precision);
-    (safe_precision, safe_scale)
-}
-
-/// SQLite has no fixed-point type, so it falls back to `double`; the other two
-/// backends take the clamped `DECIMAL(p, s)`.
-fn apply_numeric_for_backend(
-    col: &mut SeaColumnDef,
-    precision: u32,
-    scale: u32,
-    backend: DatabaseBackend,
-) {
     match backend {
         DatabaseBackend::Postgres | DatabaseBackend::MySql => {
-            col.decimal_len(precision, scale);
+            col.decimal_len(safe_precision, safe_scale);
         }
         DatabaseBackend::Sqlite => {
             col.double();

From 3a469afecde19d501098fe0b7b37dd5d70d46791 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 20 Aug 2026 14:46:59 +0900
Subject: [PATCH 106/109] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=9C=EB=8D=95?=
 =?UTF-8?q?=EC=85=98=20=ED=8C=8C=EC=9D=BC=EC=97=90=20=ED=9D=A9=EC=96=B4?=
 =?UTF-8?q?=EC=A0=B8=20=EC=9E=88=EB=8D=98=20test=20=EC=A0=84=EC=9A=A9=20?=
 =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A5=BC=20test=20=EB=AA=A8=EB=93=88?=
 =?UTF-8?q?=EB=A1=9C=20=EB=AA=A8=EC=9D=80=EB=8B=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

coverage가 지목하는 4줄은 세 파일에 공통된 배치 문제를 갖고 있었다. test 전용
코드가 `#[cfg(test)] mod tests` 밖 module-level에 있거나, test 모듈이 프로덕션
코드 위에 놓여 있어서 두 종류의 코드가 한 파일에서 뒤섞인다. AGENTS.md의
test-file placement 정책도 "inline `#[cfg(test)] mod tests`를 파일 하단에"를
기본값으로 못박고 있다.

lsp/diagnostics/validation/visitors.rs
module-level `#[cfg(test)]` 항목이 7개 있었다 - collect_syntax_errors,
collect_unknown_column_types, walk_column_objects, collect_duplicate_column_names,
collect_complex_type_violations, walk_columns_for_complex_type, walk_for_errors.
전부 `mod tests` 안으로 옮긴다. 네 개는 `fused_walk_matches_unfused_pipeline`
(diagnostics/mod.rs)이 쓰는 오라클이라 `mod tests`를 `pub(in crate::diagnostics)`
로 열고 validation/mod.rs의 재수출 경로를 `visitors::tests::{..}`로 바꾼다.
프로덕션 항목의 가시성은 그대로다.

query/sql/helpers.rs
`reference_action_sql`과 `get_enum_name`은 `#[cfg(test)]`인데 프로덕션 파일에
있었고 소비자는 sql/tests/helpers.rs 하나뿐이다. 그 테스트 파일로 옮겨
프로덕션 파일에서 test 전용 코드를 걷어낸다.

query/sql/add_constraint/mod.rs
`#[cfg(test)] mod tests`가 파일 최상단(5~927줄)에 있고 프로덕션 코드가 그 아래에
있었다. 정책대로 tests 블록을 파일 하단으로 옮긴다. 내용 변경은 없다.

검증: cargo test -p vespertide-{lsp,query} --all-features 전부 통과(0 failed),
clippy --workspace --all-targets --all-features 무경고, fmt clean,
check-line-budget.sh 통과.
---
 .../src/diagnostics/validation/mod.rs         |   2 +-
 .../src/diagnostics/validation/visitors.rs    | 263 +++++------
 .../src/sql/add_constraint/mod.rs             | 422 +++++++++---------
 crates/vespertide-query/src/sql/helpers.rs    |  28 --
 .../vespertide-query/src/sql/tests/helpers.rs |  32 +-
 5 files changed, 374 insertions(+), 373 deletions(-)

diff --git a/crates/vespertide-lsp/src/diagnostics/validation/mod.rs b/crates/vespertide-lsp/src/diagnostics/validation/mod.rs
index 7d4eb6f1..9867c38d 100644
--- a/crates/vespertide-lsp/src/diagnostics/validation/mod.rs
+++ b/crates/vespertide-lsp/src/diagnostics/validation/mod.rs
@@ -16,7 +16,7 @@ pub(super) use visitors::collect_all;
 // `fused_walk_matches_unfused_pipeline` (see diagnostics/mod.rs). Production
 // uses the fused `collect_all` path exclusively.
 #[cfg(test)]
-pub(super) use visitors::{
+pub(super) use visitors::tests::{
     collect_complex_type_violations, collect_duplicate_column_names, collect_syntax_errors,
     collect_unknown_column_types,
 };
diff --git a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
index b1df406e..10582899 100644
--- a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
+++ b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
@@ -105,50 +105,6 @@ impl<'source, 'tree> FusedCollector<'source, 'tree> {
     }
 }
 
-#[cfg(test)]
-pub(in crate::diagnostics) fn collect_syntax_errors(
-    tree: &tree_sitter::Tree,
-    out: &mut Vec,
-) {
-    let root = tree.root_node();
-    if root.has_error() {
-        walk_for_errors(root, out);
-    }
-}
-
-/// Tree-sitter-based pre-pass that flags unknown column types with a
-/// precise byte range pointing at the offending `type` value. Runs before
-/// serde so users see the squiggle on the right line even when serde's
-/// untagged-enum error reports a misleading position.
-#[cfg(test)]
-pub(in crate::diagnostics) fn collect_unknown_column_types(
-    tree: &tree_sitter::Tree,
-    source: &str,
-    out: &mut Vec,
-) {
-    let source_bytes = source.as_bytes();
-    let Some(columns) = find_value_for_key(tree.root_node(), source_bytes, "columns") else {
-        return;
-    };
-
-    walk_column_objects(columns, source_bytes, out);
-}
-
-#[cfg(test)]
-fn walk_column_objects(
-    node: tree_sitter::Node<'_>,
-    source: &[u8],
-    out: &mut Vec,
-) {
-    let mut cursor = node.walk();
-    for child in node.children(&mut cursor) {
-        if matches!(child.kind(), "object" | "block_mapping") {
-            inspect_column_type(child, source, out);
-        }
-        walk_column_objects(child, source, out);
-    }
-}
-
 fn inspect_column_type(
     column: tree_sitter::Node<'_>,
     source: &[u8],
@@ -188,31 +144,6 @@ fn inspect_column_type(
     }
 }
 
-/// Tree-sitter-based pre-pass that flags two columns sharing a `name`.
-/// Pinpoints the SECOND (and later) occurrence so the user sees the
-/// squiggle on the offending column, not on the table.
-///
-/// Critically, we visit ONLY the direct elements of the `columns` array.
-/// A naive recursive walk would dive into nested objects (e.g. integer
-/// enum members like `{"name":"low","value":0}` inside `type.values`) and
-/// mistakenly compare their `name` against the column names.
-#[cfg(test)]
-pub(in crate::diagnostics) fn collect_duplicate_column_names(
-    tree: &tree_sitter::Tree,
-    source: &str,
-    out: &mut Vec,
-) {
-    let source_bytes = source.as_bytes();
-    let Some(columns_raw) = find_value_for_key(tree.root_node(), source_bytes, "columns") else {
-        return;
-    };
-
-    let mut seen: std::collections::BTreeSet = std::collections::BTreeSet::new();
-    for column in direct_column_objects(columns_raw) {
-        inspect_column_name(column, source_bytes, &mut seen, out);
-    }
-}
-
 /// Resolve `columns: [...]` value to the direct list of column mapping
 /// nodes — peeling through tree-sitter-yaml's wrappers and skipping
 /// punctuation / comments.
@@ -272,47 +203,6 @@ fn inspect_column_name(
     }
 }
 
-/// Tree-sitter-based pre-pass for COMPLEX (object-form) column types.
-///
-/// Catches things serde either silently allows or reports at a misleading
-/// byte position:
-///   * `kind` is missing / empty / unknown.
-///   * `varchar` / `char` without `length`.
-///   * `numeric` without `precision` or `scale`.
-///   * `enum` without `name`, without `values`, with an empty `values`, or
-///     with duplicate string variants / duplicate integer variant names.
-///   * `custom` without `custom_type`.
-///
-/// Each diagnostic gets a precise byte range covering the offending pair so
-/// the squiggle lands on the right line.
-#[cfg(test)]
-pub(in crate::diagnostics) fn collect_complex_type_violations(
-    tree: &tree_sitter::Tree,
-    source: &str,
-    out: &mut Vec,
-) {
-    let source_bytes = source.as_bytes();
-    let Some(columns) = find_value_for_key(tree.root_node(), source_bytes, "columns") else {
-        return;
-    };
-    walk_columns_for_complex_type(columns, source_bytes, out);
-}
-
-#[cfg(test)]
-fn walk_columns_for_complex_type(
-    node: tree_sitter::Node<'_>,
-    source: &[u8],
-    out: &mut Vec,
-) {
-    let mut cursor = node.walk();
-    for child in node.children(&mut cursor) {
-        if matches!(child.kind(), "object" | "block_mapping") {
-            inspect_complex_type(child, source, out);
-        }
-        walk_columns_for_complex_type(child, source, out);
-    }
-}
-
 fn inspect_complex_type(
     column: tree_sitter::Node<'_>,
     source: &[u8],
@@ -538,31 +428,142 @@ fn push_complex(
 }
 
 #[cfg(test)]
-fn walk_for_errors(node: tree_sitter::Node<'_>, out: &mut Vec) {
-    if node.is_error() || node.is_missing() {
-        out.push(DomainDiagnostic {
-            byte_range: node.byte_range(),
-            severity: Severity::Error,
-            message: if node.is_missing() {
-                format!("Missing {}", node.kind())
-            } else {
-                "Syntax error".to_string()
-            },
-            code: "syntax-error".to_string(),
-        });
-        return;
+pub(in crate::diagnostics) mod tests {
+    use super::*;
+    use crate::test_support::parse_json as parse;
+
+    #[cfg(test)]
+    pub(in crate::diagnostics) fn collect_syntax_errors(
+        tree: &tree_sitter::Tree,
+        out: &mut Vec,
+    ) {
+        let root = tree.root_node();
+        if root.has_error() {
+            walk_for_errors(root, out);
+        }
     }
 
-    let mut cursor = node.walk();
-    for child in node.children(&mut cursor) {
-        walk_for_errors(child, out);
+    /// Tree-sitter-based pre-pass that flags unknown column types with a
+    /// precise byte range pointing at the offending `type` value. Runs before
+    /// serde so users see the squiggle on the right line even when serde's
+    /// untagged-enum error reports a misleading position.
+    #[cfg(test)]
+    pub(in crate::diagnostics) fn collect_unknown_column_types(
+        tree: &tree_sitter::Tree,
+        source: &str,
+        out: &mut Vec,
+    ) {
+        let source_bytes = source.as_bytes();
+        let Some(columns) = find_value_for_key(tree.root_node(), source_bytes, "columns") else {
+            return;
+        };
+
+        walk_column_objects(columns, source_bytes, out);
     }
-}
 
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::test_support::parse_json as parse;
+    #[cfg(test)]
+    fn walk_column_objects(
+        node: tree_sitter::Node<'_>,
+        source: &[u8],
+        out: &mut Vec,
+    ) {
+        let mut cursor = node.walk();
+        for child in node.children(&mut cursor) {
+            if matches!(child.kind(), "object" | "block_mapping") {
+                inspect_column_type(child, source, out);
+            }
+            walk_column_objects(child, source, out);
+        }
+    }
+
+    /// Tree-sitter-based pre-pass that flags two columns sharing a `name`.
+    /// Pinpoints the SECOND (and later) occurrence so the user sees the
+    /// squiggle on the offending column, not on the table.
+    ///
+    /// Critically, we visit ONLY the direct elements of the `columns` array.
+    /// A naive recursive walk would dive into nested objects (e.g. integer
+    /// enum members like `{"name":"low","value":0}` inside `type.values`) and
+    /// mistakenly compare their `name` against the column names.
+    #[cfg(test)]
+    pub(in crate::diagnostics) fn collect_duplicate_column_names(
+        tree: &tree_sitter::Tree,
+        source: &str,
+        out: &mut Vec,
+    ) {
+        let source_bytes = source.as_bytes();
+        let Some(columns_raw) = find_value_for_key(tree.root_node(), source_bytes, "columns")
+        else {
+            return;
+        };
+
+        let mut seen: std::collections::BTreeSet = std::collections::BTreeSet::new();
+        for column in direct_column_objects(columns_raw) {
+            inspect_column_name(column, source_bytes, &mut seen, out);
+        }
+    }
+
+    /// Tree-sitter-based pre-pass for COMPLEX (object-form) column types.
+    ///
+    /// Catches things serde either silently allows or reports at a misleading
+    /// byte position:
+    ///   * `kind` is missing / empty / unknown.
+    ///   * `varchar` / `char` without `length`.
+    ///   * `numeric` without `precision` or `scale`.
+    ///   * `enum` without `name`, without `values`, with an empty `values`, or
+    ///     with duplicate string variants / duplicate integer variant names.
+    ///   * `custom` without `custom_type`.
+    ///
+    /// Each diagnostic gets a precise byte range covering the offending pair so
+    /// the squiggle lands on the right line.
+    #[cfg(test)]
+    pub(in crate::diagnostics) fn collect_complex_type_violations(
+        tree: &tree_sitter::Tree,
+        source: &str,
+        out: &mut Vec,
+    ) {
+        let source_bytes = source.as_bytes();
+        let Some(columns) = find_value_for_key(tree.root_node(), source_bytes, "columns") else {
+            return;
+        };
+        walk_columns_for_complex_type(columns, source_bytes, out);
+    }
+
+    #[cfg(test)]
+    fn walk_columns_for_complex_type(
+        node: tree_sitter::Node<'_>,
+        source: &[u8],
+        out: &mut Vec,
+    ) {
+        let mut cursor = node.walk();
+        for child in node.children(&mut cursor) {
+            if matches!(child.kind(), "object" | "block_mapping") {
+                inspect_complex_type(child, source, out);
+            }
+            walk_columns_for_complex_type(child, source, out);
+        }
+    }
+
+    #[cfg(test)]
+    fn walk_for_errors(node: tree_sitter::Node<'_>, out: &mut Vec) {
+        if node.is_error() || node.is_missing() {
+            out.push(DomainDiagnostic {
+                byte_range: node.byte_range(),
+                severity: Severity::Error,
+                message: if node.is_missing() {
+                    format!("Missing {}", node.kind())
+                } else {
+                    "Syntax error".to_string()
+                },
+                code: "syntax-error".to_string(),
+            });
+            return;
+        }
+
+        let mut cursor = node.walk();
+        for child in node.children(&mut cursor) {
+            walk_for_errors(child, out);
+        }
+    }
 
     fn first_column<'tree>(
         tree: &'tree tree_sitter::Tree,
diff --git a/crates/vespertide-query/src/sql/add_constraint/mod.rs b/crates/vespertide-query/src/sql/add_constraint/mod.rs
index a3068740..9e5e28d3 100644
--- a/crates/vespertide-query/src/sql/add_constraint/mod.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/mod.rs
@@ -2,6 +2,217 @@ mod check;
 mod foreign_key;
 mod index;
 mod primary_key;
+mod unique;
+
+use vespertide_core::{TableConstraint, TableDef};
+
+use super::helpers::{build_sqlite_table_rebuild, require_table_in_schema};
+use super::types::{BuiltQuery, DatabaseBackend};
+use crate::error::QueryError;
+
+/// Locate the single-column PRIMARY KEY for `table` in `current_schema`,
+/// returning `None` whenever the table has no usable PK (missing, composite,
+/// or the PK column appears inside `cols` — the last case would turn a
+/// `NOT IN (SELECT MIN(pk) ... GROUP BY pk)` cleanup into a tautology).
+///
+/// Shared by [`primary_key::build_primary_key`]'s pre-cleanup and
+/// [`unique::build_unique`]'s pre-cleanup; co-locating the resolver here
+/// eliminates the drift risk of two byte-identical private copies.
+pub(super) fn try_resolve_single_pk_column>(
+    table: &str,
+    current_schema: &[TableDef],
+    cols: &[T],
+) -> Option {
+    let table_def = current_schema.iter().find(|t| t.name.as_str() == table)?;
+
+    // Prefer the first table-level PRIMARY KEY constraint; fall back to
+    // inline `primary_key: true` columns only when no table-level PK exists.
+    // Either way the resolver fires only when exactly one column is the PK
+    // (composite PK → `None`).
+    let pk_column = if let Some(columns) = table_def.constraints.iter().find_map(|c| match c {
+        TableConstraint::PrimaryKey { columns, .. } => Some(columns),
+        _ => None,
+    }) {
+        if columns.len() != 1 {
+            return None;
+        }
+        columns[0].to_string()
+    } else {
+        let mut inline = table_def
+            .columns
+            .iter()
+            .filter(|col| col.primary_key.is_some());
+        let first = inline.next()?;
+        if inline.next().is_some() {
+            return None;
+        }
+        first.name.to_string()
+    };
+
+    if cols.iter().any(|c| c.as_ref() == pk_column) {
+        return None;
+    }
+    Some(pk_column)
+}
+
+pub fn build_add_constraint(
+    backend: DatabaseBackend,
+    table: &str,
+    constraint: &TableConstraint,
+    current_schema: &[TableDef],
+    pending_constraints: &[TableConstraint],
+) -> Result, QueryError> {
+    if let TableConstraint::PrimaryKey {
+        columns, strategy, ..
+    } = constraint
+    {
+        return primary_key::build_primary_key(
+            backend,
+            table,
+            columns,
+            strategy,
+            constraint,
+            current_schema,
+            pending_constraints,
+        );
+    }
+
+    match constraint {
+        TableConstraint::Unique {
+            name,
+            columns,
+            strategy,
+        } => unique::build_unique(
+            backend,
+            table,
+            name.as_deref(),
+            columns,
+            strategy,
+            current_schema,
+        ),
+        TableConstraint::ForeignKey {
+            name,
+            columns,
+            ref_table,
+            ref_columns,
+            on_delete,
+            on_update,
+            orphan_strategy,
+        } => foreign_key::build_foreign_key(
+            backend,
+            table,
+            name.as_deref(),
+            columns,
+            ref_table,
+            ref_columns,
+            on_delete.as_ref(),
+            on_update.as_ref(),
+            *orphan_strategy,
+            constraint,
+            current_schema,
+            pending_constraints,
+        ),
+        TableConstraint::Index { name, columns } => {
+            Ok(index::build_index(table, name.as_deref(), columns))
+        }
+        TableConstraint::Check {
+            name,
+            expr,
+            strategy,
+        } => check::build_check(
+            backend,
+            table,
+            name,
+            expr,
+            strategy,
+            constraint,
+            current_schema,
+            pending_constraints,
+        ),
+        _ => unreachable!("TableConstraint is #[non_exhaustive]; all variants are matched above"),
+    }
+}
+
+pub(super) fn merge_constraint(
+    existing: &[TableConstraint],
+    constraint: &TableConstraint,
+) -> Vec {
+    let mut out = Vec::with_capacity(existing.len() + 1);
+    let mut replaced = false;
+    for c in existing {
+        if constraints_overlap(c, constraint) {
+            if !replaced {
+                out.push(constraint.clone());
+                replaced = true;
+            }
+        } else {
+            out.push(c.clone());
+        }
+    }
+    if !replaced {
+        out.push(constraint.clone());
+    }
+    out
+}
+
+pub(super) fn constraints_overlap(a: &TableConstraint, b: &TableConstraint) -> bool {
+    match (a, b) {
+        (
+            TableConstraint::ForeignKey {
+                columns: a_cols, ..
+            },
+            TableConstraint::ForeignKey {
+                columns: b_cols, ..
+            },
+        )
+        | (
+            TableConstraint::PrimaryKey {
+                columns: a_cols, ..
+            },
+            TableConstraint::PrimaryKey {
+                columns: b_cols, ..
+            },
+        ) => a_cols == b_cols,
+        (
+            TableConstraint::Check {
+                name: a_name,
+                expr: a_expr,
+                ..
+            },
+            TableConstraint::Check {
+                name: b_name,
+                expr: b_expr,
+                ..
+            },
+        ) => a_name == b_name && a_expr == b_expr,
+        _ => false,
+    }
+}
+
+pub(super) fn rebuild_sqlite_table_with_added_constraint(
+    backend: DatabaseBackend,
+    table: &str,
+    constraint: &TableConstraint,
+    current_schema: &[TableDef],
+    pending_constraints: &[TableConstraint],
+) -> Result, QueryError> {
+    let table_def = require_table_in_schema(
+        current_schema,
+        table,
+        "SQLite requires current schema information to add constraints",
+    )?;
+    let new_constraints = merge_constraint(&table_def.constraints, constraint);
+    Ok(build_sqlite_table_rebuild(
+        backend,
+        table,
+        &table_def.columns,
+        &new_constraints,
+        &table_def.columns,
+        &table_def.constraints,
+        pending_constraints,
+    ))
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -925,214 +1136,3 @@ mod tests {
         assert!(clauses.is_empty());
     }
 }
-
-mod unique;
-
-use vespertide_core::{TableConstraint, TableDef};
-
-use super::helpers::{build_sqlite_table_rebuild, require_table_in_schema};
-use super::types::{BuiltQuery, DatabaseBackend};
-use crate::error::QueryError;
-
-/// Locate the single-column PRIMARY KEY for `table` in `current_schema`,
-/// returning `None` whenever the table has no usable PK (missing, composite,
-/// or the PK column appears inside `cols` — the last case would turn a
-/// `NOT IN (SELECT MIN(pk) ... GROUP BY pk)` cleanup into a tautology).
-///
-/// Shared by [`primary_key::build_primary_key`]'s pre-cleanup and
-/// [`unique::build_unique`]'s pre-cleanup; co-locating the resolver here
-/// eliminates the drift risk of two byte-identical private copies.
-pub(super) fn try_resolve_single_pk_column>(
-    table: &str,
-    current_schema: &[TableDef],
-    cols: &[T],
-) -> Option {
-    let table_def = current_schema.iter().find(|t| t.name.as_str() == table)?;
-
-    // Prefer the first table-level PRIMARY KEY constraint; fall back to
-    // inline `primary_key: true` columns only when no table-level PK exists.
-    // Either way the resolver fires only when exactly one column is the PK
-    // (composite PK → `None`).
-    let pk_column = if let Some(columns) = table_def.constraints.iter().find_map(|c| match c {
-        TableConstraint::PrimaryKey { columns, .. } => Some(columns),
-        _ => None,
-    }) {
-        if columns.len() != 1 {
-            return None;
-        }
-        columns[0].to_string()
-    } else {
-        let mut inline = table_def
-            .columns
-            .iter()
-            .filter(|col| col.primary_key.is_some());
-        let first = inline.next()?;
-        if inline.next().is_some() {
-            return None;
-        }
-        first.name.to_string()
-    };
-
-    if cols.iter().any(|c| c.as_ref() == pk_column) {
-        return None;
-    }
-    Some(pk_column)
-}
-
-pub fn build_add_constraint(
-    backend: DatabaseBackend,
-    table: &str,
-    constraint: &TableConstraint,
-    current_schema: &[TableDef],
-    pending_constraints: &[TableConstraint],
-) -> Result, QueryError> {
-    if let TableConstraint::PrimaryKey {
-        columns, strategy, ..
-    } = constraint
-    {
-        return primary_key::build_primary_key(
-            backend,
-            table,
-            columns,
-            strategy,
-            constraint,
-            current_schema,
-            pending_constraints,
-        );
-    }
-
-    match constraint {
-        TableConstraint::Unique {
-            name,
-            columns,
-            strategy,
-        } => unique::build_unique(
-            backend,
-            table,
-            name.as_deref(),
-            columns,
-            strategy,
-            current_schema,
-        ),
-        TableConstraint::ForeignKey {
-            name,
-            columns,
-            ref_table,
-            ref_columns,
-            on_delete,
-            on_update,
-            orphan_strategy,
-        } => foreign_key::build_foreign_key(
-            backend,
-            table,
-            name.as_deref(),
-            columns,
-            ref_table,
-            ref_columns,
-            on_delete.as_ref(),
-            on_update.as_ref(),
-            *orphan_strategy,
-            constraint,
-            current_schema,
-            pending_constraints,
-        ),
-        TableConstraint::Index { name, columns } => {
-            Ok(index::build_index(table, name.as_deref(), columns))
-        }
-        TableConstraint::Check {
-            name,
-            expr,
-            strategy,
-        } => check::build_check(
-            backend,
-            table,
-            name,
-            expr,
-            strategy,
-            constraint,
-            current_schema,
-            pending_constraints,
-        ),
-        _ => unreachable!("TableConstraint is #[non_exhaustive]; all variants are matched above"),
-    }
-}
-
-pub(super) fn merge_constraint(
-    existing: &[TableConstraint],
-    constraint: &TableConstraint,
-) -> Vec {
-    let mut out = Vec::with_capacity(existing.len() + 1);
-    let mut replaced = false;
-    for c in existing {
-        if constraints_overlap(c, constraint) {
-            if !replaced {
-                out.push(constraint.clone());
-                replaced = true;
-            }
-        } else {
-            out.push(c.clone());
-        }
-    }
-    if !replaced {
-        out.push(constraint.clone());
-    }
-    out
-}
-
-pub(super) fn constraints_overlap(a: &TableConstraint, b: &TableConstraint) -> bool {
-    match (a, b) {
-        (
-            TableConstraint::ForeignKey {
-                columns: a_cols, ..
-            },
-            TableConstraint::ForeignKey {
-                columns: b_cols, ..
-            },
-        )
-        | (
-            TableConstraint::PrimaryKey {
-                columns: a_cols, ..
-            },
-            TableConstraint::PrimaryKey {
-                columns: b_cols, ..
-            },
-        ) => a_cols == b_cols,
-        (
-            TableConstraint::Check {
-                name: a_name,
-                expr: a_expr,
-                ..
-            },
-            TableConstraint::Check {
-                name: b_name,
-                expr: b_expr,
-                ..
-            },
-        ) => a_name == b_name && a_expr == b_expr,
-        _ => false,
-    }
-}
-
-pub(super) fn rebuild_sqlite_table_with_added_constraint(
-    backend: DatabaseBackend,
-    table: &str,
-    constraint: &TableConstraint,
-    current_schema: &[TableDef],
-    pending_constraints: &[TableConstraint],
-) -> Result, QueryError> {
-    let table_def = require_table_in_schema(
-        current_schema,
-        table,
-        "SQLite requires current schema information to add constraints",
-    )?;
-    let new_constraints = merge_constraint(&table_def.constraints, constraint);
-    Ok(build_sqlite_table_rebuild(
-        backend,
-        table,
-        &table_def.columns,
-        &new_constraints,
-        &table_def.columns,
-        &table_def.constraints,
-        pending_constraints,
-    ))
-}
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index 2f12dcec..80ef3cb4 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -228,22 +228,6 @@ pub(crate) fn to_sea_fk_action(action: &ReferenceAction) -> ForeignKeyAction {
     }
 }
 
-/// Convert vespertide `ReferenceAction` to SQL string.
-///
-/// Test oracle: production paths go through [`to_sea_fk_action`]; this
-/// string-level mapping is only asserted against in unit tests.
-#[cfg(test)]
-pub(crate) fn reference_action_sql(action: &ReferenceAction) -> &'static str {
-    match action {
-        ReferenceAction::Cascade => "CASCADE",
-        ReferenceAction::Restrict => "RESTRICT",
-        ReferenceAction::SetNull => "SET NULL",
-        ReferenceAction::SetDefault => "SET DEFAULT",
-        ReferenceAction::NoAction => "NO ACTION",
-        _ => unreachable!("ReferenceAction is #[non_exhaustive]; all variants are matched above"),
-    }
-}
-
 /// Convert a default value string to the appropriate backend-specific expression
 pub(crate) fn convert_default_for_backend(default: &str, backend: DatabaseBackend) -> String {
     // UUID generation functions (case-insensitive match against ASCII literals
@@ -790,18 +774,6 @@ pub(super) fn build_sqlite_table_rebuild(
     queries
 }
 
-/// Extract enum name from column type if it's an enum.
-///
-/// Test oracle: only unit tests consume this accessor today.
-#[cfg(test)]
-pub(crate) fn get_enum_name(column_type: &ColumnType) -> Option<&str> {
-    if let ColumnType::Complex(ComplexColumnType::Enum { name, .. }) = column_type {
-        Some(name.as_str())
-    } else {
-        None
-    }
-}
-
 /// Quote an identifier (table name, column name, constraint name) for the given backend.
 ///
 /// Escapes any quote characters within the identifier to prevent SQL injection
diff --git a/crates/vespertide-query/src/sql/tests/helpers.rs b/crates/vespertide-query/src/sql/tests/helpers.rs
index 78b27d82..f409f5b0 100644
--- a/crates/vespertide-query/src/sql/tests/helpers.rs
+++ b/crates/vespertide-query/src/sql/tests/helpers.rs
@@ -1,8 +1,36 @@
 use super::super::helpers::{
     apply_column_type_with_table, build_create_enum_type_sql, convert_default_for_backend,
-    get_enum_name, is_enum_type, needs_quoting, parse_pg_type_cast, recreate_indexes_after_rebuild,
-    reference_action_sql, to_sea_fk_action,
+    is_enum_type, needs_quoting, parse_pg_type_cast, recreate_indexes_after_rebuild,
+    to_sea_fk_action,
 };
+
+/// Convert vespertide `ReferenceAction` to SQL string.
+///
+/// Test oracle: production paths go through [`to_sea_fk_action`]; this
+/// string-level mapping is only asserted against here, so it lives in the test
+/// module rather than as a `#[cfg(test)]` item inside the production file.
+fn reference_action_sql(action: &ReferenceAction) -> &'static str {
+    match action {
+        ReferenceAction::Cascade => "CASCADE",
+        ReferenceAction::Restrict => "RESTRICT",
+        ReferenceAction::SetNull => "SET NULL",
+        ReferenceAction::SetDefault => "SET DEFAULT",
+        ReferenceAction::NoAction => "NO ACTION",
+        _ => unreachable!("ReferenceAction is #[non_exhaustive]; all variants are matched above"),
+    }
+}
+
+/// Extract enum name from column type if it's an enum.
+///
+/// Test oracle: only these unit tests consume this accessor, so it lives here
+/// rather than as a `#[cfg(test)]` item inside the production file.
+fn get_enum_name(column_type: &ColumnType) -> Option<&str> {
+    if let ColumnType::Complex(ComplexColumnType::Enum { name, .. }) = column_type {
+        Some(name.as_str())
+    } else {
+        None
+    }
+}
 use super::*;
 use proptest::prelude::*;
 use sea_query::{Alias, ColumnDef as SeaColumnDef, ForeignKeyAction};

From 85645b0325216b783b55936a7ced06317c3420f9 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 20 Aug 2026 15:15:55 +0900
Subject: [PATCH 107/109] =?UTF-8?q?test:=20coverage=EA=B0=80=20=EC=A7=80?=
 =?UTF-8?q?=EB=AA=A9=ED=95=98=EB=8D=98=20=EB=A7=88=EC=A7=80=EB=A7=89=204?=
 =?UTF-8?q?=EC=A4=84=EC=9D=98=20=EC=8B=A4=EC=A0=9C=20=EB=AF=B8=EC=8B=A4?=
 =?UTF-8?q?=ED=96=89=20=EB=B6=84=EA=B8=B0=EB=A5=BC=20=ED=95=B4=EC=86=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

지금까지 오독하고 있었다. CI coverage 잡은 tarpaulin 실행 직전에 `.rustfmt.toml`을
덮어쓰고(`max_width = 100000`, `fn_call_width`/`chain_width` 동일,
`fn_params_layout = "Compressed"`) `cargo fmt`를 돌린다(CI.yml의 "rust coverage
issue" 주석). 즉 보고되는 줄 번호는 **재포맷된 파일** 기준이고, 저장소 원본에서
같은 번호를 읽으면 전혀 다른 코드가 보인다. 같은 rustfmt 설정으로 로컬 재포맷해
대조한 결과 네 줄 모두 실제로 실행되지 않는 분기였다.

lsp/diagnostics/validation/visitors.rs - 도달 불가한 let-else
`let Some(values_pair) = values_pair else { return; };`. 바로 위에서
`values_pair.is_none()`이면 `missing`에 "values"를 넣고 `!missing.is_empty()`에서
이미 반환하므로 이 `else`는 도달할 수 없다. 뒤따르는
`if let Some(values_value_raw) = values_pair.named_child(1)`와 let-chain으로 합쳐
죽은 갈래를 없앤다. 들여쓰기 변화 없음.

query/sql/helpers.rs - 도달 불가한 EnumValues::Integer arm
`enum_variant_aliases`의 Integer arm은 호출부가 `if values.is_integer()`로 이미
걸러내서 실행될 수 없었다. 헬퍼를 없애고 `values`를 직접 match 한다 -
`EnumValues::Integer(_) => col.integer()`, `String(variants) => col.enumeration(..)`.
predicate 대신 변형으로 분기하므로 죽은 arm이 생기지 않는다.

lsp/references/search.rs - is_column_pair의 trailing false
테이블 자신의 `name` pair는 최외곽 매핑에 직접 놓여서
`outer.id() != column_object.id()`가 거짓이 되고, 두 `if let`을 모두 건너뛰어
마지막 `false`에 도달한다. 기존 테스트는 "outer에 name 키가 없는" 경로만 덮고
있었다. 테이블 레벨 `name`이 컬럼 선언으로 오인되지 않음을 고정한다.

query/sql/add_constraint/primary_key.rs - 인라인 복합 PK
`try_resolve_single_pk_column`의 인라인 갈래에 있는 두 번째 가드
(`inline.next().is_some()`)는 테이블 레벨 PRIMARY KEY 없이 `primary_key: true`가
두 컬럼에 달린 경우에만 실행된다. 기존 복합 PK 테스트는 테이블 레벨 갈래로
들어가서 이 지점에 닿지 못했다.

검증: cargo test -p vespertide-{lsp,query} --all-features 전부 통과(0 failed),
clippy --workspace --all-targets --all-features 무경고, fmt clean,
check-line-budget.sh 통과, insta 스냅샷 변동 없음.
---
 .../src/diagnostics/validation/visitors.rs    | 10 ++++---
 .../vespertide-lsp/src/references/search.rs   | 17 ++++++++++++
 .../src/sql/add_constraint/primary_key.rs     | 24 +++++++++++++++++
 crates/vespertide-query/src/sql/helpers.rs    | 27 ++++++++-----------
 4 files changed, 58 insertions(+), 20 deletions(-)

diff --git a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
index 10582899..7114b4e2 100644
--- a/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
+++ b/crates/vespertide-lsp/src/diagnostics/validation/visitors.rs
@@ -330,10 +330,12 @@ fn check_enum_shape(
         return;
     }
 
-    let Some(values_pair) = values_pair else {
-        return;
-    };
-    if let Some(values_value_raw) = values_pair.named_child(1) {
+    // `missing` is empty here, so the `values_pair.is_none()` branch above did
+    // not fire and the pair is present. Folded into the `named_child` let-chain
+    // rather than a `let ... else`, whose `else` arm would be unreachable.
+    if let Some(values_pair) = values_pair
+        && let Some(values_value_raw) = values_pair.named_child(1)
+    {
         let values_value = unwrap_yaml_node(values_value_raw);
         if !matches!(
             values_value.kind(),
diff --git a/crates/vespertide-lsp/src/references/search.rs b/crates/vespertide-lsp/src/references/search.rs
index a44eb681..b0c367c9 100644
--- a/crates/vespertide-lsp/src/references/search.rs
+++ b/crates/vespertide-lsp/src/references/search.rs
@@ -626,6 +626,23 @@ mod tests {
         assert!(!is_column_pair(nested_name, src.as_bytes(), "user"));
     }
 
+    /// The table's own `name` pair sits directly in the outermost mapping, so
+    /// `outer.id() != column_object.id()` is false and the function reaches its
+    /// trailing `false` — the only path that skips both inner `if let`s. Without
+    /// this, a table-level `name` could be mistaken for a column declaration.
+    #[test]
+    fn is_column_pair_returns_false_for_the_tables_own_name_pair() {
+        let src = r#"{"name":"user","columns":[{"name":"id","type":"integer"}]}"#;
+        let tree = parse_json(src);
+        let root_name =
+            find_pair_with_key(tree.root_node(), src.as_bytes(), "name").expect("root name pair");
+
+        assert!(
+            !is_column_pair(root_name, src.as_bytes(), "user"),
+            "the table's own `name` is not a column declaration"
+        );
+    }
+
     #[test]
     fn collect_in_document_keeps_table_and_check_references_distinct() {
         let src = r#"{"name":"user","columns":[{"name":"age","type":"integer"}],"constraints":[{"type":"check","name":"c","expr":"age > 0"}]}"#;
diff --git a/crates/vespertide-query/src/sql/add_constraint/primary_key.rs b/crates/vespertide-query/src/sql/add_constraint/primary_key.rs
index a72f7eae..e43da862 100644
--- a/crates/vespertide-query/src/sql/add_constraint/primary_key.rs
+++ b/crates/vespertide-query/src/sql/add_constraint/primary_key.rs
@@ -207,4 +207,28 @@ mod tests {
         let resolved = try_resolve_single_pk_column("logs", &schema, &["id"]);
         assert!(resolved.is_none());
     }
+
+    /// Composite PK declared *inline* (`primary_key: true` on two columns) with
+    /// no table-level PRIMARY KEY constraint. The composite test above goes
+    /// through the table-level branch, so only this one reaches the inline
+    /// branch's second `inline.next().is_some()` guard.
+    #[test]
+    fn try_resolve_single_pk_column_returns_none_for_inline_composite_pk() {
+        let pk = |name: &str| {
+            ColumnDef::new(name, ColumnType::Simple(SimpleColumnType::Integer), false)
+                .primary_key(vespertide_core::schema::primary_key::PrimaryKeySyntax::Bool(true))
+        };
+        let schema = vec![TableDef {
+            name: "memberships".into(),
+            description: None,
+            columns: vec![pk("user_id"), pk("group_id")],
+            constraints: vec![],
+        }];
+
+        let resolved = try_resolve_single_pk_column("memberships", &schema, &["joined_at"]);
+        assert!(
+            resolved.is_none(),
+            "two inline PK columns are a composite PK, not a single-column PK"
+        );
+    }
 }
diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs
index 80ef3cb4..5173c846 100644
--- a/crates/vespertide-query/src/sql/helpers.rs
+++ b/crates/vespertide-query/src/sql/helpers.rs
@@ -168,32 +168,27 @@ fn apply_complex_column_type(
         ComplexColumnType::Custom { custom_type } => {
             col.custom(Alias::new(custom_type));
         }
-        ComplexColumnType::Enum { name, values } => {
-            // For integer enums, use INTEGER type instead of ENUM
-            if values.is_integer() {
+        // Matched on the variant rather than `values.is_integer()`: the former
+        // `enum_variant_aliases` helper carried an `EnumValues::Integer` arm
+        // that the `is_integer()` guard made unreachable.
+        ComplexColumnType::Enum { name, values } => match values {
+            // Integer enums are stored as INTEGER; no native enum type is emitted.
+            EnumValues::Integer(_) => {
                 col.integer();
-            } else {
+            }
+            EnumValues::String(variants) => {
                 // Use table-prefixed enum type name to avoid conflicts
                 let type_name = build_enum_type_name(table, name);
                 // Map each variant name straight into `Alias::new`, skipping the
                 // intermediate `Vec<&str>` that `variant_names()` would allocate.
-                let variants = enum_variant_aliases(values);
-                col.enumeration(Alias::new(&type_name), variants);
+                let aliases: Vec = variants.iter().map(Alias::new).collect();
+                col.enumeration(Alias::new(&type_name), aliases);
             }
-        }
+        },
         _ => unreachable!("ComplexColumnType is #[non_exhaustive]; all variants are matched above"),
     }
 }
 
-/// Build the `sea_query` `Alias` list for a string enum's variants without the
-/// intermediate `Vec<&str>` that `EnumValues::variant_names()` would allocate.
-fn enum_variant_aliases(values: &EnumValues) -> Vec {
-    match values {
-        EnumValues::String(variants) => variants.iter().map(Alias::new).collect(),
-        EnumValues::Integer(variants) => variants.iter().map(|v| Alias::new(&v.name)).collect(),
-    }
-}
-
 fn apply_numeric_type(
     col: &mut SeaColumnDef,
     precision: u32,

From ad6101afe54fa5fd600f334e334858d73e18ab02 Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 20 Aug 2026 16:12:41 +0900
Subject: [PATCH 108/109] =?UTF-8?q?test(mutants):=20=EB=82=A1=EC=9D=80=20?=
 =?UTF-8?q?=EC=A0=9C=EC=99=B8=20=EA=B7=9C=EC=B9=99=EC=9D=84=20=EC=A4=84=20?=
 =?UTF-8?q?=EB=B9=84=EC=9D=98=EC=A1=B4=EC=9C=BC=EB=A1=9C=20=EA=B3=A0?=
 =?UTF-8?q?=EC=B9=98=EA=B3=A0=20=EC=82=B4=EC=95=84=EB=82=A8=EC=9D=80=20?=
 =?UTF-8?q?=EB=AE=A4=ED=84=B4=ED=8A=B8=EB=A5=BC=20=EC=B2=98=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

cargo-mutants 12/16 shard가 실패했고 살아남은 뮤턴트는 37건이었다. 원인의 절반은
테스트 공백이 아니라 `.cargo/mutants.toml`의 규칙 자체가 낡은 것이었다.

줄 번호 고정 규칙 19건을 줄 비의존으로 전환
E1~E18 규칙 다수가 `ordering[.]rs:410(:[0-9]+)?:`처럼 줄 번호에 고정돼 있었는데,
이 브랜치의 리팩터로 대상 함수가 전부 이동해 규칙이 아무것도 매치하지 못했다
(ordering 410 -> 실제 344, diff/mod 49 -> 20, builder/mod 63 -> 99,
check_type_mismatch 366 -> 304, clear_index_fields는 파일에서 사라짐). 파일명 +
뮤테이션 + 함수명이면 이미 고유하므로 줄 번호를 `[0-9]+:[0-9]+`로 바꿔 다음
리팩터에도 썩지 않게 한다.

이 과정에서 `check_expr_parser ... .* in tokenize_spanned` 하나는 줄 고정을 풀면
스캐너의 모든 뮤턴트를 가려버리므로, 비종료를 유발하는 루프 경계 뮤테이션
(`replace < with (<=|==|>)`)으로 좁혔다.

신규 등가 뮤턴트 5건 선언 (E19~E23)
- E19 capacity/reserve 산술: build_constraint_name / join_column_names /
  quote_ident_into. 버퍼 사전 크기만 바뀌고 push 순서·내용이 같아 반환값이
  바이트 동일하다.
- E20 map_paths_with_threshold: 성능 웨이브가 추출한 loader의 seq/parallel
  디스패치. E1/E7/E9/E14와 같은 범주.
- E21 sorted_column_refs: 0·1원소 슬라이스의 정렬 생략 가드. 어느 쪽이든 순서 동일.
- E22 compute_integer_enum_remapping의 빈 쪽 fast path: `&&`로 바꿔 통과시켜도
  빈 from은 0회 순회, 빈 to는 빈 to_by_name이라 결과가 빈 맵으로 같다.
- E23 build_create_table의 할당 전략 가드: Unique는 add_mysql_unique_constraint로
  가고 PG/SQLite에선 no-op이라 슬라이스를 넘기든 거르든 CREATE TABLE이 같다.
  같은 파일의 필터 `!`는 게이트에 남겨야 하므로 열 번호로 좁혔다.

실제 테스트 공백 보강
- create_table: unique 단독 픽스처는 strip/keep 차이를 못 본다. PRIMARY KEY를
  함께 둔 3-backend 케이스를 추가해 필터의 `!` 삭제(= 유니크만 남기고 PK 유실)를
  잡는다. 스냅샷 3개 신규.
- is_function_expr / convert_default_for_backend: 키워드 `||` 체인은 앞쪽이
  단락 평가되면 뒤쪽이 검증되지 않는다. 각 체인의 마지막 대안
  (CURRENT_TIME, LOCALTIME, CURRENT_USER, SESSION_USER,
  lower(hex(randomblob(16))), getdate())을 케이스로 추가.
- ColumnType/ComplexColumnType::display_label: 직접 단언이 하나도 없어 본문을
  상수로 바꿔도 통과했다. 5개 복합 변형 + 3개 단순 타입을 고정.
- render_migration_name: 끝까지 이어지는 숫자(`%012`)로 `%0N` 스캔의 두 경계를,
  기본 4자리와 다른 폭(`%06v`)으로 `pattern[i + 2..j]` 슬라이스를 검증.

검증: cargo test --workspace --all-features 전부 통과(0 failed),
clippy --workspace --all-targets --all-features 무경고, fmt clean,
check-line-budget.sh 통과.
---
 .cargo/mutants.toml                           | 81 ++++++++++++++-----
 crates/vespertide-cli/src/utils.rs            | 10 ++-
 crates/vespertide-core/src/schema/column.rs   | 43 ++++++++++
 .../src/validate/default_changes.rs           |  7 ++
 .../vespertide-query/src/sql/create_table.rs  | 44 ++++++++++
 ...le_unique_alongside_primary_key_MySql.snap |  5 ++
 ...unique_alongside_primary_key_Postgres.snap |  6 ++
 ...e_unique_alongside_primary_key_Sqlite.snap |  6 ++
 .../vespertide-query/src/sql/tests/helpers.rs | 15 ++++
 9 files changed, 197 insertions(+), 20 deletions(-)
 create mode 100644 crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_unique_alongside_primary_key@create_table_unique_alongside_primary_key_MySql.snap
 create mode 100644 crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_unique_alongside_primary_key@create_table_unique_alongside_primary_key_Postgres.snap
 create mode 100644 crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_unique_alongside_primary_key@create_table_unique_alongside_primary_key_Sqlite.snap

diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml
index d5c31e74..e4dca401 100644
--- a/.cargo/mutants.toml
+++ b/.cargo/mutants.toml
@@ -75,7 +75,7 @@ exclude_re = [
     # column x-range rather than sitting side-by-side. Therefore both mutants
     # are byte-identical for every renderer-reachable layout while the other
     # pick_anchors arithmetic remains in scope.
-    "edges[.]rs:152(:[0-9]+)?: replace > with >= in pick_anchors",
+    "edges[.]rs:[0-9]+:[0-9]+: replace > with >= in pick_anchors",
     # E6: classify_strengthening's `||` guard
     # (`old==Unparseable || new==Unparseable -> return None`) is equivalent
     # under `&&`. When exactly one side is Unparseable the `&&` mutant skips
@@ -83,7 +83,7 @@ exclude_re = [
     # has no matching arm and falls to `_ => None` ? identical observable
     # result for every input. The two-unparseable case is also caught earlier
     # by `if old == new { return None }`. No test can distinguish the mutant.
-    "check_strengthening[.]rs:233(:[0-9]+)?: replace [|][|] with && in classify_strengthening",
+    "check_strengthening[.]rs:[0-9]+:[0-9]+: replace [|][|] with && in classify_strengthening",
     # E7: the Rayon `*_par_*_threshold()` functions in parallel_config.rs
     # (vespertide-planner + vespertide-query) are perf-only dispatch knobs.
     # Replacing the returned threshold with 0 or 1 only changes WHEN the
@@ -97,51 +97,51 @@ exclude_re = [
     # ModifyColumnDefault for the same column), so they can never be equal.
     # `< -> <=` therefore has no reachable input that distinguishes it ?
     # equivalent. The `&&` operator on the same line remains in the gate.
-    "ordering[.]rs:410(:[0-9]+)?: replace < with <= in sort_enum_default_dependencies",
+    "ordering[.]rs:[0-9]+:[0-9]+: replace < with <= in sort_enum_default_dependencies",
     # E9: the `len() < *_par_*_threshold()` seq-vs-parallel DISPATCH comparisons.
     # Mutating `<` to ==/>/<= only changes which execution path is taken; the
     # sequential and Rayon paths produce identical results (covered by the
     # validate/diff/build result tests). Perf-only ? same category as E1.
-    "validate/plan[.]rs:46(:[0-9]+)?: replace < with (==|>|<=) in find_plan_violations",
-    "validate/plan[.]rs:167(:[0-9]+)?: replace < with (==|>|<=) in find_missing_fill_with",
-    "validate/plan[.]rs:280(:[0-9]+)?: replace < with (==|>|<=) in find_missing_enum_fill_with",
-    "validate/schema[.]rs:77(:[0-9]+)?: replace < with (==|>|<=) in find_schema_violations",
-    "diff/mod[.]rs:49(:[0-9]+)?: replace < with (==|>|<=) in diff_schemas",
-    "builder/mod[.]rs:63(:[0-9]+)?: replace < with (==|>|<=) in build_plan_queries",
+    "validate/plan[.]rs:[0-9]+:[0-9]+: replace < with (==|>|<=) in find_plan_violations",
+    "validate/plan[.]rs:[0-9]+:[0-9]+: replace < with (==|>|<=) in find_missing_fill_with",
+    "validate/plan[.]rs:[0-9]+:[0-9]+: replace < with (==|>|<=) in find_missing_enum_fill_with",
+    "validate/schema[.]rs:[0-9]+:[0-9]+: replace < with (==|>|<=) in find_schema_violations",
+    "diff/mod[.]rs:[0-9]+:[0-9]+: replace < with (==|>|<=) in diff_schemas",
+    "builder/mod[.]rs:[0-9]+:[0-9]+: replace < with (==|>|<=) in build_plan_queries",
     # E10: the pairwise contradiction loop `for j in (i + 1)..preds.len()`.
     # The `+ -> *` mutant yields `i * 1 == i`, so the inner loop additionally
     # visits the (i, i) self-pair `check_pair(col, p, p)`. A predicate never
     # contradicts itself (two identical Compare/In/Between/IsNull predicates
     # are always mutually satisfiable), so every added self-pair returns None
     # and the observable result is unchanged ? equivalent.
-    "check_self_contradiction[.]rs:120(:[0-9]+)?: replace [+] with [*] in find_contradiction",
+    "check_self_contradiction[.]rs:[0-9]+:[0-9]+: replace [+] with [*] in find_contradiction",
     # E11: is_definitely_mismatch's `(Json, _) => false` and
     # `(Custom { .. }, _) => false` arms return exactly the same value as the
     # match's `_ => false` catch-all. Deleting either arm makes that input
     # fall straight through to `_ => false` ? identical observable result,
     # equivalent.
-    "check_type_mismatch[.]rs:366(:[0-9]+)?: delete match arm .* in is_definitely_mismatch",
-    "check_type_mismatch[.]rs:395(:[0-9]+)?: delete match arm .* in is_definitely_mismatch",
+    "check_type_mismatch[.]rs:[0-9]+:[0-9]+: delete match arm .* in is_definitely_mismatch",
+    "check_type_mismatch[.]rs:[0-9]+:[0-9]+: delete match arm .* in is_definitely_mismatch",
     # E12: in find_primary_key_additions, `n_existing == 0 => NewColumns`.
     # The function acts ONLY on `ExistingColumns`; both NewColumns and the
     # `_ => Mixed` fallback hit `if !matches!(kind, ExistingColumns) continue;`.
     # Deleting the `0` arm sends n_existing==0 to Mixed (PK columns always
     # number >=1, so `0 == len` is unreachable) ? same `continue`, equivalent.
-    "pk_additions[.]rs:115(:[0-9]+)?: delete match arm 0 in find_primary_key_additions",
+    "pk_additions[.]rs:[0-9]+:[0-9]+: delete match arm 0 in find_primary_key_additions",
     # E13: in clear_index_fields, `filtered.len() < names.len()` gates whether
     # the multi-index array is rewritten after removing a constraint name. When
     # the name is absent, `filtered` is a clone of `names` (same elements, same
     # order), so the `<= ` branch would set `col.index = Some(Array(filtered))`
     # to a value byte-identical to the untouched original ? no observable
     # difference, equivalent.
-    "constraint_ops[.]rs:148(:[0-9]+)?: replace < with <= in clear_index_fields",
+    "constraint_ops[.]rs:[0-9]+:[0-9]+: replace < with <= in clear_index_fields",
     # E14: the loader's `paths.len() < LOAD_FILES_PAR_THRESHOLD` seq-vs-parallel
     # dispatch. Mutating `<` only changes whether files are loaded sequentially
     # or via Rayon; the loaded result is identical. Perf-only, same as E1/E9.
-    "loader/src/migrations[.]rs:21(:[0-9]+)?: replace < with (==|>|<=) in load_migrations",
-    "loader/src/migrations[.]rs:65(:[0-9]+)?: replace < with (==|>|<=) in load_migrations_from_dir",
-    "loader/src/models[.]rs:43(:[0-9]+)?: replace < with (==|>|<=) in load_models_recursive",
-    "loader/src/models[.]rs:145(:[0-9]+)?: replace < with (==|>|<=) in load_models_recursive_internal",
+    "loader/src/migrations[.]rs:[0-9]+:[0-9]+: replace < with (==|>|<=) in load_migrations",
+    "loader/src/migrations[.]rs:[0-9]+:[0-9]+: replace < with (==|>|<=) in load_migrations_from_dir",
+    "loader/src/models[.]rs:[0-9]+:[0-9]+: replace < with (==|>|<=) in load_models_recursive",
+    "loader/src/models[.]rs:[0-9]+:[0-9]+: replace < with (==|>|<=) in load_models_recursive_internal",
     # E15: pure `println!` display helpers (revision fill_with header/footer and
     # narrowing strategy descriptions). No return value or logic; replacing the
     # body with () or deleting a match arm only changes stdout decoration. Same
@@ -175,9 +175,51 @@ exclude_re = [
     # scanner's exponent loop condition. (Caught/missed mutants in these
     # functions stay in the gate; only the non-terminating ones are excluded.)
     "check_expr_parser[.]rs:[0-9]+:[0-9]+: replace [+]= with (-=|[*]=|/=) in tokenize_spanned",
-    "check_expr_parser[.]rs:408(:[0-9]+)?: .* in tokenize_spanned",
+    # The scanner's `while i < bytes.len() && ..` bounds guards. Relaxing the
+    # bound is the same non-advancing class as the `+=` mutants above: the
+    # cursor stops being the loop's decreasing measure and the scan never
+    # terminates. Narrowed from a `.*`-in-tokenize_spanned wildcard so any
+    # OTHER mutation in the scanner still has to be killed by a test.
+    "check_expr_parser[.]rs:[0-9]+:[0-9]+: replace < with (<=|==|>) in tokenize_spanned",
     "check_expr_parser[.]rs:[0-9]+:[0-9]+: replace [+]= with (-=|[*]=|/=) in Parser::parse_predicate",
     "layout[.]rs:[0-9]+:[0-9]+: replace [+]= with (-=|[*]=|/=) in rebalance_groups",
+    # E19: `String::with_capacity` / `Vec::reserve` sizing arithmetic. These
+    # mutants change only how much the buffer pre-allocates; every one of these
+    # functions then pushes the same bytes in the same order, so the returned
+    # value is byte-identical and no assertion can distinguish them. (The
+    # buffer-push helpers themselves stay in the gate - only the capacity
+    # expression is excluded.)
+    "naming/src/lib[.]rs:[0-9]+:[0-9]+: replace [+] with (-|[*]) in build_constraint_name",
+    "schema/names[.]rs:[0-9]+:[0-9]+: replace ([+]|[*]) with ([*]|[+]) in join_column_names",
+    "sql/helpers[.]rs:[0-9]+:[0-9]+: replace [+] with (-|[*]) in quote_ident_into",
+    # E20: the loader's generic `paths.len() < LOAD_FILES_PAR_THRESHOLD`
+    # seq-vs-parallel dispatch, extracted into `map_paths_with_threshold` by the
+    # performance wave. Same perf-only category as E1/E7/E9/E14 - both arms map
+    # the same closure over the same paths and collect the same result.
+    "loader/src/parallel_config[.]rs:[0-9]+:[0-9]+: replace < with (==|>|<=) in map_paths_with_threshold",
+    # E21: `sorted_column_refs` skips the sort for 0- or 1-element slices, which
+    # are already sorted. Every mutant of that guard only changes WHETHER
+    # `sort_unstable` runs on a slice too short to reorder, so the returned
+    # order is identical either way.
+    "diff/constraints[.]rs:[0-9]+:[0-9]+: replace > with (<|==|>=) in sorted_column_refs",
+    # E22: `compute_integer_enum_remapping`'s empty-side fast path
+    # (`from_items.is_empty() || to_items.is_empty()`). Under `&&` only the
+    # both-empty case short-circuits, but the fall-through still yields an empty
+    # map either way: an empty `from_items` iterates zero times, and an empty
+    # `to_items` produces an empty `to_by_name` that matches no name. The guard
+    # skips work, it does not change the result.
+    "diff/columns[.]rs:[0-9]+:[0-9]+: replace [|][|] with && in compute_integer_enum_remapping",
+    # E23: `build_create_table`'s `matches!(backend, MySql) || !has_unique_constraints`
+    # picks between passing the constraint slice through by reference and
+    # cloning a unique-free copy. `TableConstraint::Unique` dispatches to
+    # `add_mysql_unique_constraint`, which no-ops on PG/SQLite, so both arms emit
+    # the same CREATE TABLE - the guard only decides whether the clone is paid
+    # for. (The `!` inside the partitioning filter on the next lines stays in the
+    # gate: dropping it would keep only the uniques and lose the PK, which
+    # `test_create_table_unique_alongside_primary_key` asserts against.) Anchored
+    # on the COLUMN of the guard's `!` so the filter's `!` a few lines below is
+    # not swept up - the two are indistinguishable by mutation text alone.
+    "sql/create_table[.]rs:[0-9]+:77: delete ! in build_create_table",
 ]
 
 # Focus on logic-heavy crates. Exclude:
@@ -198,3 +240,4 @@ exclude_globs = [
 
 # 3× baseline test time per mutant (handles proptest variance)
 timeout_multiplier = 3.0
+
diff --git a/crates/vespertide-cli/src/utils.rs b/crates/vespertide-cli/src/utils.rs
index 8f84be48..37cb3389 100644
--- a/crates/vespertide-cli/src/utils.rs
+++ b/crates/vespertide-cli/src/utils.rs
@@ -345,7 +345,15 @@ mod tests {
         "%03x-%m-tail",
         "%03x-fix_bug-tail.vespertide.json"
     )] // `b'0'` arm's else: digits not terminated by `v` fall through untouched
-    #[case(9, None, FileFormat::Json, "%v%", "0009%.vespertide.json")] // `i + 1 >= bytes.len()`: a trailing bare `%` is not a placeholder
+    #[case(9, None, FileFormat::Json, "%v%", "0009%.vespertide.json")]
+    // `i + 1 >= bytes.len()`: a trailing bare `%` is not a placeholder
+    // Digits running to the very end exercise both `j < bytes.len()` bounds in
+    // the `%0N` scan; relaxing either to `<=` indexes one past the slice.
+    #[case(9, None, FileFormat::Json, "%012", "%012.vespertide.json")]
+    // A width that differs from the 4-digit default proves the width really
+    // comes from `pattern[i + 2..j]`: with `%04v` the padded and default
+    // renderings coincide, so the slice bounds go unchecked.
+    #[case(9, None, FileFormat::Json, "%06v", "000009.vespertide.json")]
     fn migration_filename_with_format_and_pattern_tests(
         #[case] version: u32,
         #[case] comment: Option<&str>,
diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs
index 85411c1b..9bdcb6e5 100644
--- a/crates/vespertide-core/src/schema/column.rs
+++ b/crates/vespertide-core/src/schema/column.rs
@@ -732,3 +732,46 @@ impl ComplexColumnType {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use rstest::rstest;
+
+    /// `display_label` is the wire-format spelling shown in LSP drift messages,
+    /// planner type-mismatch diagnostics and schema violations. Every consumer
+    /// only forwards the string, so without a direct assertion the whole body
+    /// could return a constant and nothing would notice.
+    #[rstest]
+    #[case::varchar(ComplexColumnType::Varchar { length: 32 }, "varchar(32)")]
+    #[case::char(ComplexColumnType::Char { length: 2 }, "char(2)")]
+    #[case::numeric(ComplexColumnType::Numeric { precision: 10, scale: 2 }, "numeric(10, 2)")]
+    #[case::custom(ComplexColumnType::Custom { custom_type: "TSVECTOR".into() }, "custom(TSVECTOR)")]
+    #[case::enum_type(
+        ComplexColumnType::Enum {
+            name: "status".into(),
+            values: EnumValues::String(vec!["active".into()]),
+        },
+        "enum(status)"
+    )]
+    fn complex_display_label_renders_wire_format(
+        #[case] complex: ComplexColumnType,
+        #[case] expected: &str,
+    ) {
+        assert_eq!(complex.display_label(), expected);
+        // The `ColumnType` wrapper must forward to the same string rather than
+        // rendering its own spelling.
+        assert_eq!(ColumnType::Complex(complex).display_label(), expected);
+    }
+
+    #[rstest]
+    #[case::integer(SimpleColumnType::Integer, "integer")]
+    #[case::big_int(SimpleColumnType::BigInt, "big_int")]
+    #[case::timestamptz(SimpleColumnType::Timestamptz, "timestamptz")]
+    fn simple_display_label_uses_the_model_name(
+        #[case] simple: SimpleColumnType,
+        #[case] expected: &str,
+    ) {
+        assert_eq!(ColumnType::Simple(simple).display_label(), expected);
+    }
+}
diff --git a/crates/vespertide-planner/src/validate/default_changes.rs b/crates/vespertide-planner/src/validate/default_changes.rs
index 0c5f9c22..d5c1a49e 100644
--- a/crates/vespertide-planner/src/validate/default_changes.rs
+++ b/crates/vespertide-planner/src/validate/default_changes.rs
@@ -350,6 +350,13 @@ mod tests {
         assert!(is_function_expr("current_timestamp"));
         assert!(is_function_expr("CURRENT_DATE"));
         assert!(is_function_expr("LocalTimestamp"));
+        // The keyword list is one `||` chain, so only an input that reaches the
+        // FINAL alternatives proves those `||`s are disjunctions: an earlier
+        // match short-circuits and leaves the tail untested.
+        assert!(is_function_expr("CURRENT_TIME"));
+        assert!(is_function_expr("LOCALTIME"));
+        assert!(is_function_expr("CURRENT_USER"));
+        assert!(is_function_expr("session_user"));
     }
 
     #[test]
diff --git a/crates/vespertide-query/src/sql/create_table.rs b/crates/vespertide-query/src/sql/create_table.rs
index 96e68c98..48027438 100644
--- a/crates/vespertide-query/src/sql/create_table.rs
+++ b/crates/vespertide-query/src/sql/create_table.rs
@@ -491,6 +491,50 @@ mod tests {
         });
     }
 
+    /// The unique-only fixture above cannot see whether the PG/SQLite branch
+    /// *strips* uniques or *keeps only* uniques - both leave the same effective
+    /// constraint set when the unique is the sole entry. Pairing the unique with
+    /// a PRIMARY KEY makes the two behaviours diverge: keeping only uniques
+    /// would drop the PK from the emitted CREATE TABLE.
+    #[rstest]
+    #[case::unique_beside_pk_postgres(DatabaseBackend::Postgres)]
+    #[case::unique_beside_pk_mysql(DatabaseBackend::MySql)]
+    #[case::unique_beside_pk_sqlite(DatabaseBackend::Sqlite)]
+    fn test_create_table_unique_alongside_primary_key(#[case] backend: DatabaseBackend) {
+        let result = build_create_table(
+            backend,
+            "users",
+            &[
+                col("id", ColumnType::Simple(SimpleColumnType::Integer)),
+                col("email", ColumnType::Simple(SimpleColumnType::Text)),
+            ],
+            &[
+                TableConstraint::PrimaryKey {
+                    columns: vec!["id".into()],
+                    auto_increment: false,
+                    strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
+                },
+                TableConstraint::Unique {
+                    name: Some("uq_email".into()),
+                    columns: vec!["email".into()],
+                    strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
+                        keep: vespertide_core::KeepPolicy::First,
+                    },
+                },
+            ],
+        )
+        .unwrap();
+        let sql = join_queries(&result, backend, "\n");
+
+        assert!(
+            sql.contains("PRIMARY KEY"),
+            "the PK must survive the unique partitioning: {sql}"
+        );
+        with_settings!({ snapshot_suffix => format!("create_table_unique_alongside_primary_key_{:?}", backend) }, {
+            assert_snapshot!(sql);
+        });
+    }
+
     #[rstest]
     #[case::table_level_unique_no_name_postgres(DatabaseBackend::Postgres)]
     #[case::table_level_unique_no_name_mysql(DatabaseBackend::MySql)]
diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_unique_alongside_primary_key@create_table_unique_alongside_primary_key_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_unique_alongside_primary_key@create_table_unique_alongside_primary_key_MySql.snap
new file mode 100644
index 00000000..38381998
--- /dev/null
+++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_unique_alongside_primary_key@create_table_unique_alongside_primary_key_MySql.snap
@@ -0,0 +1,5 @@
+---
+source: crates/vespertide-query/src/sql/create_table.rs
+expression: sql
+---
+CREATE TABLE `users` ( `id` int, `email` text, PRIMARY KEY (`id`), UNIQUE KEY `uq_users__uq_email` (`email`) )
diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_unique_alongside_primary_key@create_table_unique_alongside_primary_key_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_unique_alongside_primary_key@create_table_unique_alongside_primary_key_Postgres.snap
new file mode 100644
index 00000000..7603d139
--- /dev/null
+++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_unique_alongside_primary_key@create_table_unique_alongside_primary_key_Postgres.snap
@@ -0,0 +1,6 @@
+---
+source: crates/vespertide-query/src/sql/create_table.rs
+expression: sql
+---
+CREATE TABLE "users" ( "id" integer, "email" text, PRIMARY KEY ("id") )
+CREATE UNIQUE INDEX "uq_users__uq_email" ON "users" ("email")
diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_unique_alongside_primary_key@create_table_unique_alongside_primary_key_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_unique_alongside_primary_key@create_table_unique_alongside_primary_key_Sqlite.snap
new file mode 100644
index 00000000..7603d139
--- /dev/null
+++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_unique_alongside_primary_key@create_table_unique_alongside_primary_key_Sqlite.snap
@@ -0,0 +1,6 @@
+---
+source: crates/vespertide-query/src/sql/create_table.rs
+expression: sql
+---
+CREATE TABLE "users" ( "id" integer, "email" text, PRIMARY KEY ("id") )
+CREATE UNIQUE INDEX "uq_users__uq_email" ON "users" ("email")
diff --git a/crates/vespertide-query/src/sql/tests/helpers.rs b/crates/vespertide-query/src/sql/tests/helpers.rs
index f409f5b0..88aae126 100644
--- a/crates/vespertide-query/src/sql/tests/helpers.rs
+++ b/crates/vespertide-query/src/sql/tests/helpers.rs
@@ -172,6 +172,21 @@ fn test_reference_action_sql_all_variants(#[case] action: ReferenceAction, #[cas
     DatabaseBackend::Sqlite,
     "CURRENT_TIMESTAMP"
 )]
+// The UUID and timestamp keyword lists are `||` chains, so an input matching an
+// early alternative short-circuits and leaves the tail alternatives untested.
+// These two reach the LAST alternative of each chain.
+#[case::sqlite_uuid_spelling_postgres(
+    "lower(hex(randomblob(16)))",
+    DatabaseBackend::Postgres,
+    "gen_random_uuid()"
+)]
+#[case::sqlite_uuid_spelling_sqlite(
+    "lower(hex(randomblob(16)))",
+    DatabaseBackend::Sqlite,
+    "lower(hex(randomblob(16)))"
+)]
+#[case::getdate_postgres("getdate()", DatabaseBackend::Postgres, "CURRENT_TIMESTAMP")]
+#[case::getdate_mysql("GetDate()", DatabaseBackend::MySql, "CURRENT_TIMESTAMP")]
 #[case::now_postgres("now()", DatabaseBackend::Postgres, "CURRENT_TIMESTAMP")]
 #[case::now_mysql("now()", DatabaseBackend::MySql, "CURRENT_TIMESTAMP")]
 #[case::now_sqlite("now()", DatabaseBackend::Sqlite, "CURRENT_TIMESTAMP")]

From dcc4eb63b81d2f64e5b93136740d48112572398b Mon Sep 17 00:00:00 2001
From: owjs3901 
Date: Thu, 20 Aug 2026 16:47:58 +0900
Subject: [PATCH 109/109] =?UTF-8?q?test:=20=EC=9E=94=EC=97=AC=20=EB=AE=A4?=
 =?UTF-8?q?=ED=84=B4=ED=8A=B8=20=EC=A0=95=EB=A6=AC=20(rank=20=EA=B3=B5?=
 =?UTF-8?q?=EA=B0=84=20=EC=B9=98=ED=99=98=20+=20=EC=8B=AC=EB=A7=81?=
 =?UTF-8?q?=ED=81=AC/PK=20=ED=85=8C=EC=8A=A4=ED=8A=B8)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

cargo-mutants 5개 shard에 남아 있던 미검출 뮤턴트를 정리한다.

- sort_delete_tables: slot 공간 대신 rank 공간(0..k)에서 치환을 적용하도록
  재작성. delete_indices는 오름차순이라 rank r과 slot delete_indices[r]이
  순서 동형이므로 `slot - rank_base` 변환과 delete slot 사이의 gap까지
  포괄하던 희소 역인덱스가 통째로 불필요해진다. 이 산술을 겨냥하던 뮤턴트
  5건이 구조적으로 사라지며, 출력은 이전과 바이트 동일하다. 비연속·비-0
  시작 delete run 3건을 permute하는 테스트로 재작성분을 고정했다.

- string_enum_value_removed: 옛 default가 신·구 enum 어디에도 속하지 않으면
  재정렬이 일어나지 않아야 함을 고정. `any(|v| v == needle)`이 `!=`로 뒤집히면
  모든 비-멤버 default를 "제거된 값"으로 오판해 두 액션을 뒤바꾼다.

- collect_model_paths: models/ 안의 파일 심링크는 따라가고 끊어진 심링크는
  건너뛰는 #[cfg(unix)] 테스트 추가. `is_symlink && ...` 두 연접이 `||`로
  바뀌면 각각 파일을 read_dir 하거나 없는 경로를 read_to_string 해 Err가 된다.

- should_skip_sqlite_auto_increment_pk: auto-increment를 지원하지 않는 타입
  (TEXT)에 auto_increment PK를 걸면 SQLite도 명시적 PRIMARY KEY 절을 유지해야
  함을 3-백엔드 스냅샷으로 고정.

- render_migration_name의 `+=` 뮤턴트는 기존 E18(비종료) 범주에 편입.
  `*= 1` / `/= 1`은 항등이라 커서가 루프의 감소 측도이길 멈춰 스캔이
  bytes.len()에 도달하지 못한다.

검증: 위 4개 뮤턴트를 수동 적용해 CAUGHT 확인(심링크 2건은 WSL Linux에서
실행), fmt / test / clippy -D warnings / line-budget 모두 통과.
---
 .cargo/mutants.toml                           |   7 ++
 crates/vespertide-loader/src/models.rs        |  61 ++++++++++
 .../vespertide-planner/src/diff/ordering.rs   |  65 +++++------
 .../src/diff/tests/ordering_sort.rs           | 109 ++++++++++++++++++
 .../vespertide-query/src/sql/create_table.rs  |  31 +++++
 ...rement_pk_on_non_integer_column_MySql.snap |   5 +
 ...ent_pk_on_non_integer_column_Postgres.snap |   5 +
 ...ement_pk_on_non_integer_column_Sqlite.snap |   5 +
 8 files changed, 255 insertions(+), 33 deletions(-)
 create mode 100644 crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_auto_increment_pk_on_non_integer_column@create_table_auto_increment_pk_on_non_integer_column_MySql.snap
 create mode 100644 crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_auto_increment_pk_on_non_integer_column@create_table_auto_increment_pk_on_non_integer_column_Postgres.snap
 create mode 100644 crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_auto_increment_pk_on_non_integer_column@create_table_auto_increment_pk_on_non_integer_column_Sqlite.snap

diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml
index e4dca401..9b8e0155 100644
--- a/.cargo/mutants.toml
+++ b/.cargo/mutants.toml
@@ -183,6 +183,13 @@ exclude_re = [
     "check_expr_parser[.]rs:[0-9]+:[0-9]+: replace < with (<=|==|>) in tokenize_spanned",
     "check_expr_parser[.]rs:[0-9]+:[0-9]+: replace [+]= with (-=|[*]=|/=) in Parser::parse_predicate",
     "layout[.]rs:[0-9]+:[0-9]+: replace [+]= with (-=|[*]=|/=) in rebalance_groups",
+    # Same non-termination class in the migration-filename byte scanner:
+    # `render_migration_name` walks `i` (and the inner width cursor `j`) forward
+    # by 1 or 2 per step. `*= 1` is the identity, and `-=` walks backwards into
+    # an underflow, so either way the cursor stops being the loop's decreasing
+    # measure and the scan never reaches `bytes.len()`. The scanner's comparison
+    # and slice-bound mutants stay in the gate - `%012` and `%06v` cover those.
+    "cli/src/utils[.]rs:[0-9]+:[0-9]+: replace [+]= with (-=|[*]=|/=) in render_migration_name",
     # E19: `String::with_capacity` / `Vec::reserve` sizing arithmetic. These
     # mutants change only how much the buffer pre-allocates; every one of these
     # functions then pushes the same bytes in the same order, so the returned
diff --git a/crates/vespertide-loader/src/models.rs b/crates/vespertide-loader/src/models.rs
index 74047ca6..3f219192 100644
--- a/crates/vespertide-loader/src/models.rs
+++ b/crates/vespertide-loader/src/models.rs
@@ -214,6 +214,67 @@ mod tests {
         assert_eq!(models[0].name, "users");
     }
 
+    // `DirEntry::file_type()` describes the LINK, never its target, so symlinked
+    // entries fall back to `Path::is_dir` / `Path::is_file`. Three shapes must be
+    // told apart inside `models/`:
+    //   * `order.json`   -> a real file OUTSIDE models/  => loaded
+    //   * `nowhere.json` -> a missing target (dangling)  => skipped
+    //   * `user.json`    -> a plain file                 => loaded
+    // Misreading a file symlink as a directory would `read_dir` a file, and
+    // misreading a dangling symlink as a file would `read_to_string` a path that
+    // does not exist. Both surface as `Err`, so the successful two-table load
+    // pins each `is_symlink && ...` conjunction.
+    #[cfg(unix)]
+    #[test]
+    #[serial]
+    fn load_models_follows_file_symlinks_and_skips_dangling_ones() {
+        use std::os::unix::fs::symlink;
+
+        let tmp = tempdir().unwrap();
+        let _guard = CwdGuard::new(tmp.path());
+        write_default_config("vespertide.json");
+
+        fs::create_dir_all("models").unwrap();
+        fs::create_dir_all("external").unwrap();
+
+        let write_model = |path: &str, name: &str| {
+            let table = TableDef {
+                name: name.into(),
+                description: None,
+                columns: vec![ColumnDef::new(
+                    "id",
+                    ColumnType::Simple(SimpleColumnType::Integer),
+                    false,
+                )],
+                constraints: vec![TableConstraint::PrimaryKey {
+                    auto_increment: false,
+                    columns: vec!["id".into()],
+                    strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
+                }],
+            };
+            fs::write(path, serde_json::to_string(&table).unwrap()).unwrap();
+        };
+
+        write_model("models/user.json", "users");
+        write_model("external/order.json", "orders");
+
+        symlink(tmp.path().join("external/order.json"), "models/order.json").unwrap();
+        symlink(
+            tmp.path().join("external/nowhere.json"),
+            "models/nowhere.json",
+        )
+        .unwrap();
+
+        let mut names: Vec = load_models(&VespertideConfig::default())
+            .unwrap()
+            .into_iter()
+            .map(|t| t.name.into_inner())
+            .collect();
+        names.sort();
+
+        assert_eq!(names, ["orders", "users"]);
+    }
+
     #[test]
     #[serial]
     fn load_models_recursive_processes_subdirectories() {
diff --git a/crates/vespertide-planner/src/diff/ordering.rs b/crates/vespertide-planner/src/diff/ordering.rs
index 60ef8119..cc19f675 100644
--- a/crates/vespertide-planner/src/diff/ordering.rs
+++ b/crates/vespertide-planner/src/diff/ordering.rs
@@ -185,45 +185,44 @@ pub(super) fn sort_delete_tables(
     // Reorder the DeleteTable actions among their existing slots according to
     // `sorted_positions`, WITHOUT cloning each action.
     //
-    // `delete_indices` is ascending (it comes from `.enumerate()`). `order[k]`
-    // holds the ORIGINAL slot whose action belongs at the k-th delete slot; the
-    // stable sort keeps equal-position actions in their original relative order,
-    // matching the previous stable `sort_by` byte-for-byte.
-    let mut order: Vec = delete_indices.clone();
-    order.sort_by_key(|&i| {
-        let name = extract_delete_table_name(&actions[i]);
+    // Everything below works in RANK space (`0..k` over the delete run) rather
+    // than in raw slot space. `delete_indices` is ascending (it comes from
+    // `.enumerate()`), so rank `r` and slot `delete_indices[r]` are
+    // order-isomorphic — ranks index the bookkeeping arrays directly, with no
+    // `slot - base` translation and no sparse side table spanning the gaps
+    // between delete slots.
+    //
+    // `order[dst]` holds the ORIGINAL rank whose action belongs at rank `dst`;
+    // the stable sort keeps equal-position actions in their original relative
+    // order, matching the previous stable `sort_by` byte-for-byte.
+    let k = delete_indices.len();
+    let mut order: Vec = (0..k).collect();
+    order.sort_by_key(|&rank| {
+        let name = extract_delete_table_name(&actions[delete_indices[rank]]);
         sorted_positions.get(name).copied().unwrap_or(0)
     });
 
     // Apply the permutation `order` onto the delete slots via selection-style
     // swaps, moving each action into its destination with owned moves and no
-    // clone. We keep two mutually-inverse index arrays and update BOTH in O(1)
-    // per swap, so resolving "which rank currently holds the wanted action" is a
-    // direct array read instead of the previous inner `where_now.position(..)`
-    // linear rescan — making the apply O(k) instead of O(k²):
-    //   * `where_now[rank]`  — original slot currently occupying that delete rank
-    //   * `slot_at_rank`     — its inverse, keyed by rank offset within the run
+    // clone. We keep two mutually-inverse rank arrays and update BOTH in O(1)
+    // per swap, so resolving "which position currently holds the wanted action"
+    // is a direct array read instead of an inner `.position(..)` linear rescan
+    // — making the apply O(k) instead of O(k²):
+    //   * `origin_at[pos]`  — original rank of the action now sitting at `pos`
+    //   * `pos_of[origin]`  — its inverse
     // The reordered `DeleteTable` payloads stay byte-identical to before.
-    //
-    // Delete slots are ascending, so `delete_indices[0]` is the lowest slot;
-    // `rank_base` lets us index the inverse array by `slot - rank_base` cheaply.
-    let mut where_now: Vec = delete_indices.clone();
-    let rank_base = delete_indices[0];
-    let span = delete_indices[delete_indices.len() - 1] - rank_base + 1;
-    let mut slot_to_rank = vec![usize::MAX; span];
-    for (rank, &slot) in delete_indices.iter().enumerate() {
-        slot_to_rank[slot - rank_base] = rank;
-    }
-    for dst_rank in 0..delete_indices.len() {
-        let want_slot = order[dst_rank];
-        let src_rank = slot_to_rank[want_slot - rank_base];
-        if src_rank != dst_rank {
-            actions.swap(delete_indices[dst_rank], delete_indices[src_rank]);
-            // Swap the two slots' bookkeeping so both arrays stay consistent.
-            let displaced_slot = where_now[dst_rank];
-            where_now.swap(dst_rank, src_rank);
-            slot_to_rank[want_slot - rank_base] = dst_rank;
-            slot_to_rank[displaced_slot - rank_base] = src_rank;
+    let mut origin_at: Vec = (0..k).collect();
+    let mut pos_of: Vec = (0..k).collect();
+    for dst in 0..k {
+        let want = order[dst];
+        let src = pos_of[want];
+        if src != dst {
+            actions.swap(delete_indices[dst], delete_indices[src]);
+            // Swap the two positions' bookkeeping so both arrays stay consistent.
+            let displaced = origin_at[dst];
+            origin_at.swap(dst, src);
+            pos_of[want] = dst;
+            pos_of[displaced] = src;
         }
     }
 }
diff --git a/crates/vespertide-planner/src/diff/tests/ordering_sort.rs b/crates/vespertide-planner/src/diff/tests/ordering_sort.rs
index 8c71ccd0..391da9b8 100644
--- a/crates/vespertide-planner/src/diff/tests/ordering_sort.rs
+++ b/crates/vespertide-planner/src/diff/tests/ordering_sort.rs
@@ -228,6 +228,17 @@ mod topo_and_delete_sort_tests {
         MigrationAction::DeleteTable { table: name.into() }
     }
 
+    // An action `sort_delete_tables` must never touch, tagged by column name so
+    // the assertions can prove it stayed in its original slot.
+    fn marker(column: &str) -> MigrationAction {
+        MigrationAction::ModifyColumnDefault {
+            table: "unrelated".into(),
+            column: column.into(),
+            new_default: None,
+            backfill: None,
+        }
+    }
+
     // A standalone table `a` plus a `b <-> c` FK cycle. The error must list
     // ONLY the cyclic tables (b, c), proving the `!result.iter().any(name ==
     // t.name)` "not-yet-placed" filter. The `delete !` mutant and the `== ->
@@ -329,4 +340,102 @@ mod topo_and_delete_sort_tests {
             MigrationAction::ModifyColumnDefault { .. }
         ));
     }
+
+    // Three FK-chained deletes (c -> b -> a) that neither start at slot 0 nor
+    // occupy contiguous slots: inert actions sit before and between them. The
+    // delete payloads must be permuted among exactly their own slots (1, 3, 4)
+    // into dependent-first order while the inert actions stay put. Pins the
+    // rank-space permutation apply — any confusion between a delete's rank
+    // (0..3) and its slot (1, 3, 4) either displaces an inert action or drops
+    // a delete on the floor.
+    #[test]
+    fn sort_delete_permutes_only_delete_slots_in_a_three_table_chain() {
+        let a = pk_table("a", None);
+        let b = pk_table("b", Some("a"));
+        let c = pk_table("c", Some("b"));
+        let mut all: BTreeMap<&str, &TableDef> = BTreeMap::new();
+        all.insert("a", &a);
+        all.insert("b", &b);
+        all.insert("c", &c);
+
+        let mut actions = vec![
+            marker("first"),
+            delete("a"),
+            marker("middle"),
+            delete("b"),
+            delete("c"),
+        ];
+        sort_delete_tables(&mut actions, &all);
+
+        // Dependents first: c (FK-> b), then b (FK-> a), then a.
+        assert_eq!(extract_delete_table_name(&actions[1]), "c");
+        assert_eq!(extract_delete_table_name(&actions[3]), "b");
+        assert_eq!(extract_delete_table_name(&actions[4]), "a");
+
+        // The non-delete actions never move.
+        assert!(matches!(
+            &actions[0],
+            MigrationAction::ModifyColumnDefault { column, .. } if column.as_str() == "first"
+        ));
+        assert!(matches!(
+            &actions[2],
+            MigrationAction::ModifyColumnDefault { column, .. } if column.as_str() == "middle"
+        ));
+    }
+
+    // The old default ('archived') belongs to NEITHER the old nor the new enum
+    // — a stale default left behind by a hand edit. Dropping 'inactive' does
+    // not remove the *default's* value, so the default change carries no
+    // ordering dependency and the actions must stay as-is. Pins the
+    // `from_values` membership half of `string_enum_value_removed`: an
+    // `any(|v| v != needle)` mutant classifies every non-member default as
+    // removed and reorders the pair.
+    #[test]
+    fn enum_default_reorder_skips_when_old_default_is_not_an_enum_member() {
+        use crate::diff::ordering::sort_enum_default_dependencies;
+        use vespertide_core::{ComplexColumnType, DefaultValue, EnumValues};
+
+        let old_enum = ColumnType::Complex(ComplexColumnType::Enum {
+            name: "e".into(),
+            values: EnumValues::String(vec!["active".into(), "inactive".into()]),
+        });
+        let new_enum = ColumnType::Complex(ComplexColumnType::Enum {
+            name: "e".into(),
+            values: EnumValues::String(vec!["active".into()]),
+        });
+
+        let mut from_col = col("status", old_enum);
+        from_col.default = Some(DefaultValue::String("'archived'".into()));
+        let from_table = table("t", vec![from_col], vec![]);
+        let mut from_map: BTreeMap<&str, &TableDef> = BTreeMap::new();
+        from_map.insert("t", &from_table);
+
+        let mut actions = vec![
+            MigrationAction::ModifyColumnType {
+                table: "t".into(),
+                column: "status".into(),
+                new_type: new_enum,
+                fill_with: None,
+                narrowing_strategy: None,
+                timezone: None,
+            },
+            MigrationAction::ModifyColumnDefault {
+                table: "t".into(),
+                column: "status".into(),
+                new_default: Some("'active'".into()),
+                backfill: None,
+            },
+        ];
+
+        sort_enum_default_dependencies(&mut actions, &from_map);
+
+        assert!(
+            matches!(&actions[0], MigrationAction::ModifyColumnType { .. }),
+            "type change must stay first when the old default is not an enum member"
+        );
+        assert!(matches!(
+            &actions[1],
+            MigrationAction::ModifyColumnDefault { .. }
+        ));
+    }
 }
diff --git a/crates/vespertide-query/src/sql/create_table.rs b/crates/vespertide-query/src/sql/create_table.rs
index 48027438..f9c954d1 100644
--- a/crates/vespertide-query/src/sql/create_table.rs
+++ b/crates/vespertide-query/src/sql/create_table.rs
@@ -695,6 +695,37 @@ mod tests {
         });
     }
 
+    #[rstest]
+    #[case::auto_increment_unsupported_type_postgres(DatabaseBackend::Postgres)]
+    #[case::auto_increment_unsupported_type_mysql(DatabaseBackend::MySql)]
+    #[case::auto_increment_unsupported_type_sqlite(DatabaseBackend::Sqlite)]
+    fn test_create_table_auto_increment_pk_on_non_integer_column(#[case] backend: DatabaseBackend) {
+        // SQLite folds an auto-increment PK into the column definition
+        // (`INTEGER PRIMARY KEY AUTOINCREMENT`) and therefore SKIPS the separate
+        // PRIMARY KEY clause — but only when the PK column's type can actually
+        // carry AUTOINCREMENT. TEXT cannot, so nothing is folded into the column
+        // and the explicit PRIMARY KEY clause must survive on every backend.
+        let columns = vec![col("id", ColumnType::Simple(SimpleColumnType::Text))];
+        let constraints = vec![TableConstraint::PrimaryKey {
+            auto_increment: true,
+            columns: vec!["id".into()],
+            strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
+        }];
+
+        let queries = build_create_table(backend, "users", &columns, &constraints).unwrap();
+        let sql = join_queries(&queries, backend, ";\n");
+
+        assert!(
+            sql.to_uppercase().contains("PRIMARY KEY"),
+            "auto-increment PK on a type without auto-increment support must still \
+             emit an explicit PRIMARY KEY, got: {sql}"
+        );
+
+        with_settings!({ snapshot_suffix => format!("create_table_auto_increment_pk_on_non_integer_column_{:?}", backend) }, {
+            assert_snapshot!(sql);
+        });
+    }
+
     #[rstest]
     #[case::inline_auto_increment_postgres(DatabaseBackend::Postgres)]
     #[case::inline_auto_increment_mysql(DatabaseBackend::MySql)]
diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_auto_increment_pk_on_non_integer_column@create_table_auto_increment_pk_on_non_integer_column_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_auto_increment_pk_on_non_integer_column@create_table_auto_increment_pk_on_non_integer_column_MySql.snap
new file mode 100644
index 00000000..5e7adabf
--- /dev/null
+++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_auto_increment_pk_on_non_integer_column@create_table_auto_increment_pk_on_non_integer_column_MySql.snap
@@ -0,0 +1,5 @@
+---
+source: crates/vespertide-query/src/sql/create_table.rs
+expression: sql
+---
+CREATE TABLE `users` ( `id` text, PRIMARY KEY (`id`) )
diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_auto_increment_pk_on_non_integer_column@create_table_auto_increment_pk_on_non_integer_column_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_auto_increment_pk_on_non_integer_column@create_table_auto_increment_pk_on_non_integer_column_Postgres.snap
new file mode 100644
index 00000000..14e29fb3
--- /dev/null
+++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_auto_increment_pk_on_non_integer_column@create_table_auto_increment_pk_on_non_integer_column_Postgres.snap
@@ -0,0 +1,5 @@
+---
+source: crates/vespertide-query/src/sql/create_table.rs
+expression: sql
+---
+CREATE TABLE "users" ( "id" text, PRIMARY KEY ("id") )
diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_auto_increment_pk_on_non_integer_column@create_table_auto_increment_pk_on_non_integer_column_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_auto_increment_pk_on_non_integer_column@create_table_auto_increment_pk_on_non_integer_column_Sqlite.snap
new file mode 100644
index 00000000..14e29fb3
--- /dev/null
+++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_auto_increment_pk_on_non_integer_column@create_table_auto_increment_pk_on_non_integer_column_Sqlite.snap
@@ -0,0 +1,5 @@
+---
+source: crates/vespertide-query/src/sql/create_table.rs
+expression: sql
+---
+CREATE TABLE "users" ( "id" text, PRIMARY KEY ("id") )