Skip to content

Added support to change numeric precision#363

Open
marioloko wants to merge 1 commit intopgplex:mainfrom
marioloko:main
Open

Added support to change numeric precision#363
marioloko wants to merge 1 commit intopgplex:mainfrom
marioloko:main

Conversation

@marioloko
Copy link

Fixes #362

@greptile-apps
Copy link

greptile-apps bot commented Mar 22, 2026

Greptile Summary

This PR fixes issue #362 by teaching the diff engine to detect and emit migrations when a numeric/decimal column's precision or scale changes. Previously, precision-only changes were silently ignored because the comparison only looked at the bare DataType string (e.g. "numeric") without considering its modifiers.

Key changes:

  • generateColumnSQL now calls the new columnTypeWithModifiers helper to build the full type string (e.g. numeric(16,6)) before comparing old vs new, so a precision-only change correctly produces ALTER TABLE … ALTER COLUMN … TYPE numeric(16,6);
  • columnsEqual gains explicit Precision and Scale nil/value comparisons so the planner correctly marks these columns as needing migration
  • A new test fixture (alter_numeric_precision) validates the numeric(15,4)numeric(16,6) path
  • The needsUsingClause / IsBuiltInType call chain already handles type strings with parenthesised modifiers (strips them internally), so numeric(15,4)numeric(16,6) correctly avoids generating a spurious USING clause

Minor observations:

  • columnTypeWithModifiers duplicates the modifier-building logic already present in table.go's formatColumnDataType and formatColumnDataTypeForCreate; consolidating would reduce future drift risk
  • columnsEqual still compares bare DataType strings in its first check rather than using columnTypeWithModifiers, which is functionally correct today (precision/scale are checked separately) but creates an asymmetry worth documenting
  • Test coverage is focused on the one scenario from the issue; additional fixtures for adding/removing precision entirely and for decimal aliases would strengthen confidence

Confidence Score: 4/5

  • Safe to merge — the precision-change detection and SQL generation are correct; only minor code-quality improvements remain.
  • The core logic is sound: columnTypeWithModifiers correctly builds modifier-annotated type strings, IsBuiltInType already strips parenthesised modifiers so needsUsingClause works correctly, and columnsEqual now catches precision/scale differences. The two P2 comments (duplicated logic, asymmetry in columnsEqual) are non-blocking style issues. Test coverage is narrowly scoped to the reported issue scenario but is sufficient for merging.
  • Pay close attention to internal/diff/column.go — specifically the new columnTypeWithModifiers function and the asymmetry between how columnsEqual and generateColumnSQL compare types.

Important Files Changed

Filename Overview
internal/diff/column.go Core logic change: generateColumnSQL now builds type strings with precision/scale/length modifiers via the new columnTypeWithModifiers helper, and columnsEqual gains explicit Precision/Scale comparisons. The implementation is correct for the numeric/decimal case, but columnTypeWithModifiers duplicates modifier-building logic already present in table.go's formatColumnDataType.
testdata/diff/create_table/alter_numeric_precision/diff.sql Expected diff output — ALTER TABLE transactions ALTER COLUMN amount TYPE numeric(16,6); — is correct PostgreSQL DDL for a numeric precision change.
testdata/diff/create_table/alter_numeric_precision/old.sql Starting state with amount numeric(15,4). Representative but only covers the precision+scale→precision+scale case; adding/removing precision entirely is not covered.
testdata/diff/create_table/alter_numeric_precision/new.sql Desired state with amount numeric(16,6). Straightforward test fixture.
testdata/diff/create_table/alter_numeric_precision/plan.json Plan JSON output — single step of type table.column/alter with the correct SQL. No issues.
testdata/diff/create_table/alter_numeric_precision/plan.sql Plan SQL output matches diff.sql as expected.
testdata/diff/create_table/alter_numeric_precision/plan.txt Human-readable plan output correctly shows amount (column) as modified under transactions.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[columnsEqual called] --> B{DataType equal?}
    B -- No --> C[Return false: type change]
    B -- Yes --> D{Precision nil-ness equal?}
    D -- No --> E[Return false: precision changed]
    D -- Yes --> F{Precision values equal?}
    F -- No --> G[Return false: precision changed]
    F -- Yes --> H{Scale nil-ness equal?}
    H -- No --> I[Return false: scale changed]
    H -- Yes --> J{Scale values equal?}
    J -- No --> K[Return false: scale changed]
    J -- Yes --> L[Continue other checks...]

    M[generateColumnSQL called] --> N[columnTypeWithModifiers old\ne.g. numeric-15-4]
    M --> O[columnTypeWithModifiers new\ne.g. numeric-16-6]
    N --> P{oldType != newType?}
    O --> P
    P -- Yes --> Q[needsUsingClause?\nIsBuiltInType strips parens\nnumeric-15-4 → numeric → true]
    Q -- No USING --> R[ALTER TABLE t ALTER COLUMN c TYPE numeric-16-6]
    Q -- USING needed --> S[ALTER TABLE t ALTER COLUMN c\nTYPE newType USING c::newType]
Loading

Comments Outside Diff (1)

  1. internal/diff/column.go, line 141-152 (link)

    P2 columnsEqual DataType comparison does not include modifiers

    generateColumnSQL now uses columnTypeWithModifiers when computing whether a type change has occurred. columnsEqual, however, still compares only the bare DataType string (e.g. "numeric"), relying on the separate Precision/Scale checks added below.

    This is functionally correct today because Precision and Scale are always checked explicitly afterwards. It is worth noting the asymmetry, though: if a future type gains modifiers that are NOT captured in separate IR fields (e.g. timestamp(p) precision stored only in DataType), columnsEqual and generateColumnSQL could diverge — generateColumnSQL would detect the change and emit an ALTER TYPE, while columnsEqual would still return true (equal) and suppress the planned migration.

    Consider adding a comment documenting this intentional split, or aligning the comparison by also using columnTypeWithModifiers here.

Reviews (1): Last reviewed commit: "Added support to change numeric precisio..." | Re-trigger Greptile

Comment on lines +209 to +221
func columnTypeWithModifiers(col *ir.Column) string {
dt := col.DataType
if col.MaxLength != nil && (dt == "varchar" || dt == "character varying") {
return fmt.Sprintf("varchar(%d)", *col.MaxLength)
} else if col.MaxLength != nil && dt == "character" {
return fmt.Sprintf("character(%d)", *col.MaxLength)
} else if col.Precision != nil && col.Scale != nil && (dt == "numeric" || dt == "decimal") {
return fmt.Sprintf("%s(%d,%d)", dt, *col.Precision, *col.Scale)
} else if col.Precision != nil && (dt == "numeric" || dt == "decimal") {
return fmt.Sprintf("%s(%d)", dt, *col.Precision)
}
return dt
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicated modifier-formatting logic

columnTypeWithModifiers is essentially the same as formatColumnDataType (in table.go), which already handles the varchar/character/numeric/decimal cases. The only intentional difference is that columnTypeWithModifiers skips the serial → serial/bigserial rewrite, but formatColumnDataType can also return the raw integer type when the column is not a serial.

Reusing formatColumnDataType (or extracting the shared modifier-building block into a small unexported helper) would keep the logic in one place and reduce the risk of future drift — e.g., if bit(n) or timestamp(p) precision support is added later, it would need to be patched in both files.

Copy link
Contributor

@tianzhou tianzhou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. Please take a look at review comments

@@ -0,0 +1 @@
ALTER TABLE transactions ALTER COLUMN amount TYPE numeric(16,6);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for file name, pls prefix with issue_xxx

#352

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses #362 by ensuring column type diffs account for NUMERIC precision/scale changes (and similar typmod-style modifiers), so schema plans correctly emit ALTER TABLE ... ALTER COLUMN ... TYPE ... when only modifiers change.

Changes:

  • Include precision/scale (and length) modifiers when determining whether a column’s type has changed during SQL generation.
  • Extend column structural equality to consider numeric Precision and Scale.
  • Add a diff fixture covering numeric precision/scale modification planning.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/diff/column.go Updates column equality and type-change SQL generation to consider precision/scale/length modifiers.
testdata/diff/create_table/alter_numeric_precision/plan.txt Adds expected human-readable plan output for numeric precision/scale alteration.
testdata/diff/create_table/alter_numeric_precision/plan.sql Adds expected SQL plan output for numeric precision/scale alteration.
testdata/diff/create_table/alter_numeric_precision/plan.json Adds expected JSON plan output for numeric precision/scale alteration.
testdata/diff/create_table/alter_numeric_precision/old.sql Fixture: prior schema state with numeric typmod.
testdata/diff/create_table/alter_numeric_precision/new.sql Fixture: updated schema state with modified numeric typmod.
testdata/diff/create_table/alter_numeric_precision/diff.sql Fixture: expected diff SQL for the typmod change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +211 to +219
if col.MaxLength != nil && (dt == "varchar" || dt == "character varying") {
return fmt.Sprintf("varchar(%d)", *col.MaxLength)
} else if col.MaxLength != nil && dt == "character" {
return fmt.Sprintf("character(%d)", *col.MaxLength)
} else if col.Precision != nil && col.Scale != nil && (dt == "numeric" || dt == "decimal") {
return fmt.Sprintf("%s(%d,%d)", dt, *col.Precision, *col.Scale)
} else if col.Precision != nil && (dt == "numeric" || dt == "decimal") {
return fmt.Sprintf("%s(%d)", dt, *col.Precision)
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

columnTypeWithModifiers only applies MaxLength modifiers for varchar/character types. But columnsEqual compares MaxLength for all columns, and the inspector populates MaxLength from character_maximum_length, which also applies to types like bit / bit varying (varbit). If a bit-string length changes, it will be detected as a column change but generateColumnSQL won’t emit an ALTER ... TYPE bit(n) because oldType/newType will both remain the bare DataType. Consider extending columnTypeWithModifiers (or using a shared formatter) to include length modifiers for other length-typed columns where MaxLength is set.

Suggested change
if col.MaxLength != nil && (dt == "varchar" || dt == "character varying") {
return fmt.Sprintf("varchar(%d)", *col.MaxLength)
} else if col.MaxLength != nil && dt == "character" {
return fmt.Sprintf("character(%d)", *col.MaxLength)
} else if col.Precision != nil && col.Scale != nil && (dt == "numeric" || dt == "decimal") {
return fmt.Sprintf("%s(%d,%d)", dt, *col.Precision, *col.Scale)
} else if col.Precision != nil && (dt == "numeric" || dt == "decimal") {
return fmt.Sprintf("%s(%d)", dt, *col.Precision)
}
// Length-based modifiers (character and bit string types)
if col.MaxLength != nil {
switch dt {
case "varchar", "character varying":
// Normalize to varchar(n) for varying-length character types
return fmt.Sprintf("varchar(%d)", *col.MaxLength)
case "character":
return fmt.Sprintf("character(%d)", *col.MaxLength)
case "bit":
return fmt.Sprintf("bit(%d)", *col.MaxLength)
case "bit varying", "varbit":
// Use canonical "bit varying(n)" representation
return fmt.Sprintf("bit varying(%d)", *col.MaxLength)
}
}
// Numeric precision/scale modifiers
if col.Precision != nil && col.Scale != nil && (dt == "numeric" || dt == "decimal") {
return fmt.Sprintf("%s(%d,%d)", dt, *col.Precision, *col.Scale)
} else if col.Precision != nil && (dt == "numeric" || dt == "decimal") {
return fmt.Sprintf("%s(%d)", dt, *col.Precision)
}

Copilot uses AI. Check for mistakes.
Comment on lines +210 to +220
dt := col.DataType
if col.MaxLength != nil && (dt == "varchar" || dt == "character varying") {
return fmt.Sprintf("varchar(%d)", *col.MaxLength)
} else if col.MaxLength != nil && dt == "character" {
return fmt.Sprintf("character(%d)", *col.MaxLength)
} else if col.Precision != nil && col.Scale != nil && (dt == "numeric" || dt == "decimal") {
return fmt.Sprintf("%s(%d,%d)", dt, *col.Precision, *col.Scale)
} else if col.Precision != nil && (dt == "numeric" || dt == "decimal") {
return fmt.Sprintf("%s(%d)", dt, *col.Precision)
}
return dt
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

columnTypeWithModifiers duplicates the type/modifier formatting logic already implemented in internal/diff/table.go (e.g., formatColumnDataType / formatColumnDataTypeForCreate). To avoid these drifting out of sync (especially as more modifier-carrying types are added), consider refactoring to reuse a single formatter (possibly with an option to skip SERIAL transformation, since this helper mentions that requirement).

Suggested change
dt := col.DataType
if col.MaxLength != nil && (dt == "varchar" || dt == "character varying") {
return fmt.Sprintf("varchar(%d)", *col.MaxLength)
} else if col.MaxLength != nil && dt == "character" {
return fmt.Sprintf("character(%d)", *col.MaxLength)
} else if col.Precision != nil && col.Scale != nil && (dt == "numeric" || dt == "decimal") {
return fmt.Sprintf("%s(%d,%d)", dt, *col.Precision, *col.Scale)
} else if col.Precision != nil && (dt == "numeric" || dt == "decimal") {
return fmt.Sprintf("%s(%d)", dt, *col.Precision)
}
return dt
return formatColumnDataType(col)

Copilot uses AI. Check for mistakes.
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.

Bug: pgschema is not detecting changes in numeric precision

3 participants