Skip to content

fix(query): treat modify_column_type.fill_with replacements as bare enum labels - #182

Merged
owjs3901 merged 2 commits into
mainfrom
fix/modify-column-type-fill-with-quoting
Aug 20, 2026
Merged

fix(query): treat modify_column_type.fill_with replacements as bare enum labels#182
owjs3901 merged 2 commits into
mainfrom
fix/modify-column-type-fill-with-quoting

Conversation

@owjs3901

@owjs3901 owjs3901 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Reproduction

schemas/migration.schema.json documented modify_column_type.fill_with as taking a pre-quoted replacement:

"e.g., `{\"cancelled\": \"'pending'\"}` generates an `UPDATE` before the type change."

A migration written exactly as documented ??shrinking a plan.sheet_policy enum from
[FIXED, NEGOTIATION, OVER_500] down to [FIXED, NEGOTIATION] and remapping the removed label:

{
  "type": "modify_column_type",
  "table": "plan",
  "column": "sheet_policy",
  "new_type": { "kind": "enum", "name": "sheet_policy", "values": ["FIXED", "NEGOTIATION"] },
  "fill_with": { "OVER_500": "'FIXED'" }
}

emitted this on vespertide log --backend postgres (verified against the real CLI binary with the fix bypassed):

UPDATE "plan" SET "sheet_policy" = E'\'FIXED\'' WHERE "sheet_policy" = 'OVER_500'

The literal's content is the 7-character token 'FIXED', not the 5-character label FIXED.
PostgreSQL rejects it with invalid input value for enum, and the migration fails.

Root cause

crates/vespertide-query/src/sql/modify_column_type/mod.rs:162 (pre-change):

.value(Alias::new(column), Expr::val(replacement.as_str()))

Expr::val binds the replacement as a data value, so sea-query escapes it and adds its own
quoting. The implementation therefore expects a bare label, while the schema documentation
showed a pre-quoted one. The two contradicted each other; the existing
test_modify_column_type_with_fill_with test happened to use a bare value, so the mismatch was
never caught.

The WHERE side (.and_where(Expr::col(...).eq(removed_value.as_str())), line 163) is fine ??an
unknown-type literal coerces to the enum type in Postgres.

What changed

Contract: BARE. A fill_with replacement is a plain enum label with no SQL quotes.
Expr::val binding is kept, so replacements stay injection-safe.

  • crates/vespertide-query/src/sql/modify_column_type/fill_with.rs (new) ??build_fill_with_updates
    and extend_fill_with_updates moved out of mod.rs into their own module. This is the layout
    crates/vespertide-query/AGENTS.md already describes (modify_column_type/ = direct / sqlite_rebuild / fill_with),
    and it keeps mod.rs (1140 lines) from crossing the 1200-line tier ceiling once the new tests land.
    Call sites in direct.rs / sqlite_rebuild.rs are unchanged.
  • strip_legacy_outer_quotes ??backward-compatibility path. When a replacement both starts and
    ends with a single quote, exactly one outer layer is stripped, a one-time warning
    (std::sync::Once) goes to stderr, and the build proceeds. Migration files in the wild that follow
    the old documented form keep working instead of starting to fail.
  • crates/vespertide-core/src/action/mod.rs ??rustdoc on MigrationAction::ModifyColumnType.fill_with
    now states the bare contract and mentions the legacy fallback.
  • schemas/migration.schema.json ??regenerated from that rustdoc. One line changed
    (the modify_column_type.fill_with description); model.schema.json and config.schema.json
    are byte-identical. Kept deliberately minimal ??see "Overlap with sibling PRs" below.
  • crates/vespertide-cli/src/commands/revision/prompts/fill_with.rs ??every value collected by
    collect_enum_fill_with_values now passes through strip_enum_quotes, so the interactive
    vespertide revision prompt cannot reintroduce a quoted value regardless of which prompt fn is
    injected. Previously the guarantee lived only in prompt_enum_value_bare, one wiring line away
    from being lost.

Test evidence

New tests in fill_with.rs, all fanned out across the mandatory {Postgres, MySQL, SQLite} backend
triple per crates/vespertide-query/AGENTS.md:

Test Asserts
bare_replacement_gets_exactly_one_quote_layer SQL contains = 'FIXED' and not '''FIXED'''; 3 snapshots
quoted_replacement_matches_bare_replacement {"OVER_500": "'FIXED'"} produces SQL byte-identical to {"OVER_500": "FIXED"}
strip_legacy_outer_quotes_removes_at_most_one_layer ''FIXED''??'FIXED', 'FIXED?뭫nchanged, FIXED'?뭫nchanged, '?뭫nchanged, ''??"", FIXED?뭫nchanged
multiple_mappings_are_deterministically_ordered 3 mappings inserted in ascending and descending order produce identical SQL in BTreeMap key order; 3 snapshots
absent_fill_with_emits_nothing None contributes no statements

Plus test_collect_enum_fill_with_values_strips_quotes_from_prompt_result in
crates/vespertide-cli/src/commands/revision/tests/prompts.rs.

Snapshot (Postgres, multi-mapping):

UPDATE "plan" SET "sheet_policy" = 'FIXED' WHERE "sheet_policy" = 'OVER_500';
UPDATE "plan" SET "sheet_policy" = 'NEGOTIATION' WHERE "sheet_policy" = 'PER_SHEET';
UPDATE "plan" SET "sheet_policy" = 'FIXED' WHERE "sheet_policy" = 'UNDER_100'

Gates

cargo test --workspace --all-features   -> exit 0; 4364 passed, 0 failed, 3 documented #[ignore]
cargo clippy --workspace --all-targets --all-features -- -D warnings -> exit 0
cargo fmt --all --check                 -> exit 0
sh scripts/check-line-budget.sh         -> All tracked Rust files are within budget
schema drift (regen into _tmp_schemas + git diff --no-index) -> empty

End-to-end verification against the real CLI

Built vespertide.exe and ran vespertide log --backend postgres on a scratch project containing
the reproduction above.

Legacy pre-quoted form ??correct SQL, warning emitted once on stderr:

1-1. UPDATE "plan" SET "sheet_policy" = 'FIXED' WHERE "sheet_policy" = 'OVER_500'
...
vespertide: warning: modify_column_type.fill_with replacement 'FIXED' for sheet_policy.OVER_500 is
wrapped in SQL single quotes. fill_with values are bare enum labels; the quotes were stripped for
compatibility. Rewrite the migration to use FIXED.

Bare form (the newly documented contract) ??identical SQL, no warning:

1-1. UPDATE "plan" SET "sheet_policy" = 'FIXED' WHERE "sheet_policy" = 'OVER_500'

Overlap with sibling PRs

Two sibling PRs are in flight on this repo (add_column.fill_with lowercasing; a data_migration
action). Both may also touch schemas/migration.schema.json and the MigrationAction rustdoc, so
those edits were kept as small as possible here:

  • schemas/migration.schema.json: 1 line ??the modify_column_type.fill_with description only.
    add_column.fill_with and modify_column_nullable.fill_with are untouched.
  • crates/vespertide-core/src/action/mod.rs: 1 hunk ??the doc comment on the
    ModifyColumnType.fill_with field only. No variant added, removed, or reordered.

Changepack

.changepacks/changepack_log_A6HBSMdx7cre8I-RyNhPx.json declares Minor for
vespertide-core, vespertide-query, and vespertide-cli.

Minor rather than Patch is deliberate. The semver-checks job derives its release-type from the
PR-introduced descriptor (.github/workflows/CI.yml:141-155), and that script has no Patch
branch
— a Patch-only descriptor leaves RT="", which makes the action derive strictly from the
un-bumped Cargo.toml version. Against the current main (which already carries the breaking
changes from #181) that fails, exactly as it did on the first push of this PR before the descriptor
was added. On a 0.x crate the script maps Minor to release-type: major, which is the same
setting under which #181 passed.

The three crates already carry pending Minor entries in this wave, so the descriptor does not
change the computed version bump (changepacks takes the max per package) — it only makes the
semver-checks gate evaluate this PR under the correct release model.

devfive added 2 commits August 20, 2026 19:56
…num labels

The schema docs for `modify_column_type.fill_with` showed the replacement
already wrapped in SQL single quotes (`{"cancelled": "'pending'"}`), but
`build_fill_with_updates` binds it with `Expr::val`, which escapes the value
and adds its own quoting. A migration written exactly as documented emitted
`SET "col" = E'\'FIXED\''`, storing the 7-character token `'FIXED'` instead of
the 5-character label `FIXED`, and PostgreSQL rejected it with
`invalid input value for enum`.

Settle on the bare form as the documented contract and keep `Expr::val`
binding, so replacements stay injection-safe:

- Move the fill_with emission out of `modify_column_type/mod.rs` into a new
  `modify_column_type/fill_with.rs` (matches the layout already described in
  `crates/vespertide-query/AGENTS.md`).
- Add `strip_legacy_outer_quotes`: when a replacement both starts and ends with
  a single quote, strip exactly one outer layer, warn once, and proceed, so
  existing migration files written against the old docs keep working.
- Document the bare contract on `MigrationAction::ModifyColumnType.fill_with`
  and regenerate `schemas/migration.schema.json` (one description line).
- Normalise every collected value through `strip_enum_quotes` in
  `collect_enum_fill_with_values`, so the revision prompt cannot reintroduce
  quoted values regardless of the injected prompt fn.

Tests cover all three backends: a bare label gets exactly one quote layer, a
pre-quoted label produces byte-identical SQL, and multiple mappings keep the
BTreeMap key ordering.
@github-actions

Copy link
Copy Markdown

Changepacks

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

  • modify_column_type.fill_with 를 bare enum 라벨로 확정 (스키마 문서와 구현 불일치로 enum 축소 마이그레이션이 invalid input value for enum 으로 실패하던 문제). 따옴표가 이미 붙은 기존 값은 한 겹 제거 + 1회 경고로 하위호환 유지
  • 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

  • modify_column_type.fill_with 를 bare enum 라벨로 확정 (스키마 문서와 구현 불일치로 enum 축소 마이그레이션이 invalid input value for enum 으로 실패하던 문제). 따옴표가 이미 붙은 기존 값은 한 겹 제거 + 1회 경고로 하위호환 유지
  • 성능 최적화 웨이브: 미사용 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

  • modify_column_type.fill_with 를 bare enum 라벨로 확정 (스키마 문서와 구현 불일치로 enum 축소 마이그레이션이 invalid input value for enum 으로 실패하던 문제). 따옴표가 이미 붙은 기존 값은 한 겹 제거 + 1회 경고로 하위호환 유지
  • 성능 최적화 웨이브: 미사용 public API 정리(NameCase::is_*, EnumValues::variant_names/to_sql_values, sql::helpers 일부, find_primary_key_removals 시그니처)와 Orm::Prisma 변형 추가로 0.x 기준 breaking

@owjs3901
owjs3901 merged commit 2ea7c32 into main Aug 20, 2026
37 checks passed
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