Skip to content

Improve performance - #181

Merged
owjs3901 merged 110 commits into
mainfrom
improve-performance
Aug 20, 2026
Merged

Improve performance#181
owjs3901 merged 110 commits into
mainfrom
improve-performance

Conversation

@owjs3901

Copy link
Copy Markdown
Contributor

No description provided.

owjs3901 added 30 commits June 23, 2026 22:25
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::<Vec<_>>().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).
…sh 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<String> 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.
…e-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).
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<Uri> 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).
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.
…fter_rebuild loop and dedupe Index/Unique arms
…{postgres,mysql}.rs - switch to RawSql::uniform
…_backend

Replace let lower = default.to_lowercase(); lower == \<ascii-literal>\` with direct default.eq_ignore_ascii_case(\<ascii-literal>\) 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.
…espertide-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.
…s_for_action, extract builder test_support, drop nn_col shadow (3 kept)
…pport (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.
…dup planner+exporter proptest strategies via core::arbitrary (3 kept)
…alues::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.
…e 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<Path>.
- 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.
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::<Vec<_>>().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.
…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.
…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('\\'').
…are 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).
…tes, extract build_drop_index_query helper (2 kept)
…names_to_strings helper, refresh AGENTS.md docs (3 kept)
…spertide-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.
owjs3901 added 22 commits July 3, 2026 16:09
- 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.
… collectors, &str keys for fk_count_per_table (3 kept)
…hecks, single file_type() fast path in collect_model_paths (2 kept)
…to drop a String alloc per deleted column (1 kept)
…t list is empty in compute_integer_enum_remapping (1 kept)
Prisma exporter(#166)를 성능 최적화 브랜치에 통합한다. 충돌 16건은 모두
"main의 동작 + improve-performance의 할당 최적화" 방향으로 해소했다.

동작은 main을 채택
- vespertide-naming: relation-naming 헬퍼를 되돌리고 sanitize_identifier /
  IdentifierStart / seaorm_module_name / to_screaming_snake_case를 받는다.
  브랜치가 exporter 내부로 옮겼던 헬퍼를 Prisma 포함 5개 백엔드가 의존한다.
- Python enum 멤버명을 vespertide_naming::to_screaming_snake_case로 통일.
  브랜치의 위치 기반 변환은 ERROR_LEVEL을 E_R_R_O_R__L_E_V_E_L로 폭발시켰다.
- sanitize_field_name / fk_attr_value / unique_relation_enum_name / M2M
  relation enum의 식별자 정규화를 그대로 받는다.

성능은 improve-performance를 유지
- build_constraint_name / write_sorted_columns 단일 버퍼 빌더 유지.
- push_attr / join_quoted / join_qualified_refs 누적 방식 유지. main이
  Vec::insert / Vec::join으로 추가한 지점은 insert_str / push_attr로 재작성했고
  출력은 바이트 동일하다.
- fk_attr_value는 sanitize를 적용하면서 Vec<String> 중간 할당 없이 유지.
- unique_relation_enum_name / M2M enum의 PascalCase prefix 루프 불변식 유지.
- CLI export의 to_pascal_case 중복 제거(python_naming 재사용) 유지.

그 외
- vespertide-macro: syn 3.0 승격 이후 runtime-macros(syn 2.x)가 `full` 피처를
  받지 못해 dev 빌드가 깨지던 문제를 수정. 재명명 dev-dependency로 2.x 쪽
  피처만 통일하고 프로덕션 syn은 parsing+proc-macro로 유지한다.
- seaorm/render.rs의 조용한 오병합(String 버퍼에 Vec::push) 수정.
- AGENTS.md 3종: 5-ORM 표기, export/ 경로, 라인 예산 표, 테스트 수 갱신.

검증: clippy(--all-targets --all-features) 무경고, 테스트 4232 passed /
0 failed / 3 ignored, fmt clean, check-line-budget.sh 통과, 스키마 재생성
diff 0. 로컬 Windows에서 pg_query(libpg_query)의 C 빌드/링크가 불가해
vespertide-query의 sql_pg_query 타깃만 제외했다(환경 한정, Linux CI 무관).
typescript 7.x는 컴파일러 본체를 `@typescript/typescript-<platform>` 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).
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).
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 재측정 결과를 보고 잇는다.
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 타깃까지 로컬에서 돌렸다.
앞선 커밋으로 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.
앞선 시도는 방향이 반대였다. 다중 행 구문을 한 행으로 "합치면" 접히는 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.
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 통과.
지금까지 오독하고 있었다. 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 스냅샷 변동 없음.
@github-actions

Copy link
Copy Markdown

Changepacks

vespertide@0.1.1 - apps/vscode-extension/package.json

Maybe you forgot to write the following files to the latest version

vespertide@0.2.1 → 0.2.2 - crates/vespertide/Cargo.toml

Patch

  • Auto-update: depends on 'vespertide-core' via a local workspace dependency

vespertide-cli@0.2.1 → 0.3.0 - crates/vespertide-cli/Cargo.toml

Minor

  • Prisma ORM exporter 추가

vespertide-config@0.2.1 → 0.3.0 - crates/vespertide-config/Cargo.toml

Minor

  • 성능 최적화 웨이브: 미사용 public API 정리(NameCase::is_*, EnumValues::variant_names/to_sql_values, sql::helpers 일부, find_primary_key_removals 시그니처)와 Orm::Prisma 변형 추가로 0.x 기준 breaking

vespertide-core@0.2.1 → 0.3.0 - crates/vespertide-core/Cargo.toml

Minor

  • 성능 최적화 웨이브: 미사용 public API 정리(NameCase::is_*, EnumValues::variant_names/to_sql_values, sql::helpers 일부, find_primary_key_removals 시그니처)와 Orm::Prisma 변형 추가로 0.x 기준 breaking

vespertide-exporter@0.2.1 → 0.3.0 - crates/vespertide-exporter/Cargo.toml

Minor

  • 성능 최적화 웨이브: 미사용 public API 정리(NameCase::is_*, EnumValues::variant_names/to_sql_values, sql::helpers 일부, find_primary_key_removals 시그니처)와 Orm::Prisma 변형 추가로 0.x 기준 breaking
  • Prisma ORM exporter 추가

vespertide-loader@0.2.1 → 0.2.2 - crates/vespertide-loader/Cargo.toml

Patch

  • Auto-update: depends on 'vespertide-config' via a local workspace dependency

vespertide-lsp@0.2.1 → 0.2.2 - crates/vespertide-lsp/Cargo.toml

Patch

  • Auto-update: depends on 'vespertide-config' via a local workspace dependency

vespertide-macro@0.2.1 → 0.2.2 - crates/vespertide-macro/Cargo.toml

Patch

  • Auto-update: depends on 'vespertide-config' via a local workspace dependency

vespertide-naming@0.2.1 → 0.3.0 - crates/vespertide-naming/Cargo.toml

Minor

  • Prisma ORM exporter 추가

vespertide-planner@0.2.1 → 0.3.0 - crates/vespertide-planner/Cargo.toml

Minor

  • 성능 최적화 웨이브: 미사용 public API 정리(NameCase::is_*, EnumValues::variant_names/to_sql_values, sql::helpers 일부, find_primary_key_removals 시그니처)와 Orm::Prisma 변형 추가로 0.x 기준 breaking

vespertide-query@0.2.1 → 0.3.0 - crates/vespertide-query/Cargo.toml

Minor

  • 성능 최적화 웨이브: 미사용 public API 정리(NameCase::is_*, EnumValues::variant_names/to_sql_values, sql::helpers 일부, find_primary_key_removals 시그니처)와 Orm::Prisma 변형 추가로 0.x 기준 breaking

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 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 모두 통과.
@owjs3901
owjs3901 merged commit e8a091a into main Aug 20, 2026
37 checks passed
@owjs3901
owjs3901 deleted the improve-performance branch August 20, 2026 08:07
owjs3901 pushed a commit that referenced this pull request Aug 20, 2026
…ray BOM

The hand-rolled byte cursors in quoted_literal_end /
ind_last_top_level_cast / contains_composite_keyword advanced with
i += 1 / i += 2 / quote + end. cargo-mutants turns each of those into
-= or *=, which stops the cursor advancing and hangs the scan, so the
mutants surfaced as TIMEOUT (plus one genuine MISSED on i *= 2).

Drive the scan from �ytes().enumerate() instead: the cursor is monotonic by
construction, so the whole non-termination mutant class disappears rather than
needing a mutants.toml exclusion. Toggling in_quote on every ' handles
the SQL '' escape for free, which removes quoted_literal_end entirely.

contains_composite_keyword no longer needs its own quote-skipping loop: with
the short label-like keywords (�nd, or,
ot, in, like) dropped from
the list, a plain whole-word split keeps 'not_started'::user_status and
'in progress' on the convertible path.

Also strips a UTF-8 BOM accidentally written into add_column.rs, which made
cargo-mutants skip that file wholesale, and raises the changepack to Minor:
CI derives the cargo-semver-checks release-type only from Major/Minor, so a
Patch descriptor left the gate deriving from the (unbumped) Cargo.toml
version and failing on the API removals already queued from #181.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant