Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix: solve issue with schema builder default values comparison#11995

Open
gioboa wants to merge 2 commits intotypeorm:masterfrom
gioboa:fix/1729
Open

fix: solve issue with schema builder default values comparison#11995
gioboa wants to merge 2 commits intotypeorm:masterfrom
gioboa:fix/1729

Conversation

@gioboa
Copy link
Collaborator

@gioboa gioboa commented Feb 16, 2026

Close #1729

Description of change

Pull-Request Checklist

  • Code is up-to-date with the master branch
  • This pull request links relevant issues as Fixes #00000
  • There are new or updated tests validating the change (tests/**.test.ts)
  • Documentation has been updated to reflect this change (docs/docs/**.md)

@qodo-free-for-open-source-projects

User description

Close #1729

Description of change

Pull-Request Checklist

  • Code is up-to-date with the master branch
  • This pull request links relevant issues as Fixes #00000
  • There are new or updated tests validating the change (tests/**.test.ts)
  • Documentation has been updated to reflect this change (docs/docs/**.md)

PR Type

Bug fix


Description

  • Fix schema builder default value comparison for PostgreSQL

  • Handle type casting in default values when comparing columns

  • Remove unnecessary type casting removal from query runner

  • Add comprehensive JSDoc parameters to driver methods


Diagram Walkthrough

flowchart LR
  A["PostgreSQL Default Values"] --> B["Query Runner Loads DB Schema"]
  B --> C["Driver Compares Column Defaults"]
  C --> D["Strip Type Casting for Comparison"]
  D --> E["Determine if Column Changed"]
  E --> F["Generate Migration Queries"]
Loading

File Walkthrough

Relevant files
Bug fix
PostgresDriver.ts
Fix default value comparison with type casting                     

src/driver/postgres/PostgresDriver.ts

  • Enhanced defaultEqual() method to compare default values by stripping
    type casting (e.g., ::text, ::integer) before comparison
  • Added logic to handle cases where PostgreSQL type casting differs
    between schema definition and actual database state
  • Added missing JSDoc @param annotations for multiple methods
  • Removed unnecessary blank line in JSDoc comment for supportedDataTypes
+51/-2   
PostgresQueryRunner.ts
Simplify default value handling in query runner                   

src/driver/postgres/PostgresQueryRunner.ts

  • Removed type casting removal logic from column_default processing in
    schema loading
  • Simplified default value assignment to preserve original database
    value
  • Moved type casting comparison responsibility to the driver's
    defaultEqual() method
+2/-3     
Tests
change-default-value.test.ts
Add test for default value comparison                                       

test/functional/schema-builder/change-default-value.test.ts

+34/-0   
EntityWithDefaultValues.ts
Create test entity with default values                                     

test/functional/schema-builder/entity/EntityWithDefaultValues.ts

  • Created test entity with function-based default values
  • Includes columns with md5((now())::text) and now() defaults
  • Used to validate schema builder behavior with PostgreSQL function
    defaults
+13/-0   

@qodo-free-for-open-source-projects
Copy link

qodo-free-for-open-source-projects bot commented Feb 16, 2026

PR Code Suggestions ✨

Latest suggestions up to be0755e

CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle escaped quotes correctly

Refactor stripTypeCastsOutsideQuotes to use a state machine for correctly
handling escaped single quotes ('') in SQL literals. The current split("'")
approach can fail, leading to incorrect default value comparisons.

src/driver/postgres/PostgresDriver.ts [1848-1860]

 stripTypeCastsOutsideQuotes(expr: string): string {
-    return expr
-        .split(`'`)
-        .map((v, i) => {
-            return i % 2 === 0
-                ? v.replace(
-                      /::[\w\s.[\]\-"]+(?:\([^)]*\))?[\w\s.[\]\-"]*/g,
-                      "",
-                  )
-                : v
-        })
-        .join(`'`)
+    let out = ""
+    let inSingleQuote = false
+
+    for (let i = 0; i < expr.length; i++) {
+        const ch = expr[i]
+
+        if (ch === "'") {
+            // handle escaped quote inside string literal: ''
+            if (inSingleQuote && expr[i + 1] === "'") {
+                out += "''"
+                i++
+                continue
+            }
+
+            inSingleQuote = !inSingleQuote
+            out += ch
+            continue
+        }
+
+        if (!inSingleQuote && ch === ":" && expr[i + 1] === ":") {
+            // skip '::' and following type/cast spec
+            i += 2
+
+            // consume optional whitespace
+            while (i < expr.length && /\s/.test(expr[i])) i++
+
+            // consume type identifier / schema-qualified / quoted identifiers / array brackets
+            while (
+                i < expr.length &&
+                /[\w.[\]" -]/.test(expr[i])
+            ) {
+                i++
+            }
+
+            // consume optional type params: (..)
+            if (expr[i] === "(") {
+                let depth = 1
+                i++
+                while (i < expr.length && depth > 0) {
+                    if (expr[i] === "(") depth++
+                    else if (expr[i] === ")") depth--
+                    i++
+                }
+            }
+
+            i-- // for-loop will i++
+            continue
+        }
+
+        out += ch
+    }
+
+    return out
 }
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a flaw in the stripTypeCastsOutsideQuotes implementation where escaped single quotes are not handled, and provides a more robust state-machine-based implementation that correctly handles this edge case, preventing incorrect schema diffs.

High
  • More

Previous suggestions

Suggestions up to commit 486a336
CategorySuggestion                                                                                                                                    Impact
Possible issue
Correctly handle escaped quotes

Improve the stripTypeCastsOutsideQuotes function to correctly handle escaped
single quotes ('') in SQL strings by replacing the split logic with a more
robust character-by-character scanner.

src/driver/postgres/PostgresDriver.ts [1848-1860]

 stripTypeCastsOutsideQuotes(expr: string): string {
-    return expr
-        .split(`'`)
-        .map((v, i) => {
-            return i % 2 === 0
-                ? v.replace(
-                      /::[\w\s.[\]\-"]+(?:\([^)]*\))?[\w\s.[\]\-"]*/g,
-                      "",
-                  )
-                : v
-        })
-        .join(`'`)
+    let out = ""
+    let inQuote = false
+
+    for (let i = 0; i < expr.length; i++) {
+        const ch = expr[i]
+
+        if (ch === "'") {
+            // handle escaped quote inside a string: ''
+            if (inQuote && expr[i + 1] === "'") {
+                out += "''"
+                i++
+                continue
+            }
+            inQuote = !inQuote
+            out += ch
+            continue
+        }
+
+        if (!inQuote && ch === ":" && expr[i + 1] === ":") {
+            // skip ::type-cast token outside quotes
+            i += 2
+            while (i < expr.length) {
+                const c = expr[i]
+                // stop when we likely reached an operator / delimiter
+                if (c === "'" || c === "," || c === ")" || c === ";" || c === " ") break
+                i++
+            }
+            i-- // compensate for loop increment
+            continue
+        }
+
+        out += ch
+    }
+
+    return out
 }
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug in the new stripTypeCastsOutsideQuotes function where it fails to handle escaped single quotes in SQL strings, and the proposed fix using a scanner is more robust and correct.

Medium
Suggestions up to commit dde101a
CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle quoted strings correctly

Refactor stripTypeCastsOutsideQuotes to use a state machine for correctly
handling escaped single quotes and dollar-quoted strings, preventing incorrect
parsing of SQL default values.

src/driver/postgres/PostgresDriver.ts [1848-1860]

 stripTypeCastsOutsideQuotes(expr: string): string {
-    return expr
-        .split(`'`)
-        .map((v, i) => {
-            return i % 2 === 0
-                ? v.replace(
-                      /::[\w\s.[\]\-"]+(?:\([^)]*\))?[\w\s.[\]\-"]*/g,
-                      "",
-                  )
-                : v
-        })
-        .join(`'`)
+    let out = ""
+    let i = 0
+    let inSingle = false
+    let dollarTag: string | null = null
+
+    const tryReadDollarTag = (s: string, from: number): string | null => {
+        if (s[from] !== "$") return null
+        const next = s.indexOf("$", from + 1)
+        if (next === -1) return null
+        const tag = s.slice(from, next + 1) // includes trailing $
+        // must be $tag$ with tag consisting of [A-Za-z0-9_]* (empty allowed)
+        if (!/^\$[A-Za-z0-9_]*\$$/.test(tag)) return null
+        return tag
+    }
+
+    while (i < expr.length) {
+        // handle dollar-quoted strings
+        if (!inSingle) {
+            if (dollarTag) {
+                if (expr.startsWith(dollarTag, i)) {
+                    out += dollarTag
+                    i += dollarTag.length
+                    dollarTag = null
+                    continue
+                }
+                out += expr[i++]
+                continue
+            } else {
+                const tag = tryReadDollarTag(expr, i)
+                if (tag) {
+                    out += tag
+                    i += tag.length
+                    dollarTag = tag
+                    continue
+                }
+            }
+        }
+
+        const ch = expr[i]
+
+        // handle single-quoted strings (with escaped quotes '')
+        if (!dollarTag && ch === "'") {
+            out += ch
+            i++
+            if (inSingle && expr[i] === "'") {
+                // escaped quote inside string
+                out += "'"
+                i++
+                continue
+            }
+            inSingle = !inSingle
+            continue
+        }
+
+        // remove ::type casts only when not inside quotes
+        if (!inSingle && !dollarTag && expr.startsWith("::", i)) {
+            i += 2
+            // skip optional whitespace
+            while (i < expr.length && /\s/.test(expr[i])) i++
+            // skip type name + optional modifiers (e.g. schema."MyType"(...)[])
+            while (
+                i < expr.length &&
+                /[A-Za-z0-9_\s.\[\]\-"]/g.test(expr[i])
+            ) {
+                i++
+            }
+            if (expr[i] === "(") {
+                let depth = 0
+                while (i < expr.length) {
+                    if (expr[i] === "(") depth++
+                    else if (expr[i] === ")") {
+                        depth--
+                        if (depth === 0) {
+                            i++
+                            break
+                        }
+                    }
+                    i++
+                }
+                while (
+                    i < expr.length &&
+                    /[A-Za-z0-9_\s.\[\]\-"]/g.test(expr[i])
+                ) {
+                    i++
+                }
+            }
+            continue
+        }
+
+        out += ch
+        i++
+    }
+
+    return out
 }
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a significant flaw in the new stripTypeCastsOutsideQuotes function, which would fail on common SQL patterns like escaped quotes or dollar-quoting, and provides a robust fix.

High
Normalize parenthesized JSON defaults

Before parsing a JSON default value, trim whitespace and strip any surrounding
parentheses to handle cases where Postgres wraps the default, such as
('{"a":1}'::jsonb).

src/driver/postgres/PostgresDriver.ts [1225-1243]

 let tableColumnDefault = tableColumn.default
 if (typeof tableColumnDefault === "string") {
-    tableColumnDefault =
-        this.stripTypeCastsOutsideQuotes(tableColumnDefault)
+    tableColumnDefault = this.stripTypeCastsOutsideQuotes(tableColumnDefault).trim()
+
+    // strip redundant surrounding parentheses: ( ... ), (( ... )), etc.
+    while (
+        tableColumnDefault.startsWith("(") &&
+        tableColumnDefault.endsWith(")")
+    ) {
+        const inner = tableColumnDefault.slice(1, -1).trim()
+        // stop if parentheses are not just wrapping the whole expression
+        if (inner.length === tableColumnDefault.length - 2) {
+            tableColumnDefault = inner
+        } else {
+            break
+        }
+    }
+
     if (
         tableColumnDefault.startsWith("'") &&
         tableColumnDefault.endsWith("'")
     ) {
         tableColumnDefault = tableColumnDefault.substring(
             1,
             tableColumnDefault.length - 1,
         )
     }
+
     try {
         tableColumnDefault = JSON.parse(tableColumnDefault)
     } catch (e) {
         // if it's not a valid JSON, we just leave it as it is
     }
 }
Suggestion importance[1-10]: 8

__

Why: The suggestion addresses a valid edge case where parenthesized default values from Postgres would cause incorrect schema comparisons for JSON types, and the proposed fix is correct and improves robustness.

Medium
✅ Suggestions up to commit 628a4d3
CategorySuggestion                                                                                                                                    Impact
Possible issue
Correctly strip casts outside strings

Refactor stripTypeCastsOutsideQuotes to use a state machine instead of split()
to correctly handle escaped quotes within string literals when stripping type
casts.

src/driver/postgres/PostgresDriver.ts [1848-1860]

 stripTypeCastsOutsideQuotes(expr: string): string {
-    return expr
-        .split(`'`)
-        .map((v, i) => {
-            return i % 2 === 0
-                ? v.replace(
-                      /::[\w\s.[\]\-"]+(?:\([^)]*\))?[\w\s.[\]\-"]*/g,
-                      "",
-                  )
-                : v
-        })
-        .join(`'`)
+    let out = ""
+    let i = 0
+    let inSingleQuote = false
+
+    const castRegex =
+        /^::[\w\s.[\]\-"]+(?:\([^)]*\))?[\w\s.[\]\-"]*/
+
+    while (i < expr.length) {
+        const ch = expr[i]
+
+        if (ch === "'") {
+            // handle escaped single-quote inside string literal: ''
+            if (inSingleQuote && expr[i + 1] === "'") {
+                out += "''"
+                i += 2
+                continue
+            }
+
+            inSingleQuote = !inSingleQuote
+            out += ch
+            i += 1
+            continue
+        }
+
+        if (!inSingleQuote && ch === ":" && expr[i + 1] === ":") {
+            const match = expr.slice(i).match(castRegex)
+            if (match) {
+                i += match[0].length
+                continue
+            }
+        }
+
+        out += ch
+        i += 1
+    }
+
+    return out
 }
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug in the new stripTypeCastsOutsideQuotes function where escaped quotes are not handled, and proposes a robust state-machine based fix that correctly handles this edge case.

Medium
Normalize JSON defaults before comparing
Suggestion Impact:The commit updated JSON/JSONB default handling to normalize by stripping type casts outside quotes before unquoting and JSON.parse, and added a reusable stripTypeCastsOutsideQuotes helper (also used in other default comparisons). It did not implement the suggested trim/parentheses unwrapping or the JSON.stringify fallback comparison on parse failure.

code diff:

@@ -1222,15 +1222,25 @@
             ["json", "jsonb"].includes(columnMetadata.type as string) &&
             !["function", "undefined"].includes(typeof columnMetadata.default)
         ) {
-            const tableColumnDefault =
-                typeof tableColumn.default === "string"
-                    ? JSON.parse(
-                          tableColumn.default.substring(
-                              1,
-                              tableColumn.default.length - 1,
-                          ),
-                      )
-                    : tableColumn.default
+            let tableColumnDefault = tableColumn.default
+            if (typeof tableColumnDefault === "string") {
+                tableColumnDefault =
+                    this.stripTypeCastsOutsideQuotes(tableColumnDefault)
+                if (
+                    tableColumnDefault.startsWith("'") &&
+                    tableColumnDefault.endsWith("'")
+                ) {
+                    tableColumnDefault = tableColumnDefault.substring(
+                        1,
+                        tableColumnDefault.length - 1,
+                    )
+                }
+                try {
+                    tableColumnDefault = JSON.parse(tableColumnDefault)
+                } catch (e) {
+                    // if it's not a valid JSON, we just leave it as it is
+                }
+            }
 
             return OrmUtils.deepCompare(
                 columnMetadata.default,
@@ -1247,8 +1257,8 @@
         if (
             columnDefault &&
             tableColumn.default &&
-            columnDefault.replace(/::[\w\s.[\]\-"]+/g, "") ===
-                tableColumn.default.replace(/::[\w\s.[\]\-"]+/g, "")
+            this.stripTypeCastsOutsideQuotes(columnDefault) ===
+                this.stripTypeCastsOutsideQuotes(tableColumn.default)
         ) {
             return true
         }
@@ -1830,5 +1840,23 @@
 
         return comment
     }
+
+    /**
+     * Strips type casts from a given expression, but only if they are outside of quotes.
+     * @param expr
+     */
+    stripTypeCastsOutsideQuotes(expr: string): string {
+        return expr
+            .split(`'`)
+            .map((v, i) => {
+                return i % 2 === 0
+                    ? v.replace(
+                          /::[\w\s.[\]\-"]+(?:\([^)]*\))?[\w\s.[\]\-"]*/g,
+                          "",
+                      )
+                    : v
+            })
+            .join(`'`)
+    }

In defaultEqual, normalize the tableColumnDefault string for JSON/JSONB types by
trimming whitespace and removing surrounding parentheses before parsing to
handle variations in PostgreSQL's output.

src/driver/postgres/PostgresDriver.ts [1226-1248]

 if (typeof tableColumnDefault === "string") {
-    tableColumnDefault =
-        this.stripTypeCastsOutsideQuotes(tableColumnDefault)
+    tableColumnDefault = this.stripTypeCastsOutsideQuotes(tableColumnDefault).trim()
+
+    // unwrap a single layer of parentheses often present in pg_get_expr / column_default
+    if (tableColumnDefault.startsWith("(") && tableColumnDefault.endsWith(")")) {
+        tableColumnDefault = tableColumnDefault.slice(1, -1).trim()
+    }
+
     if (
         tableColumnDefault.startsWith("'") &&
         tableColumnDefault.endsWith("'")
     ) {
         tableColumnDefault = tableColumnDefault.substring(
             1,
             tableColumnDefault.length - 1,
         )
     }
+
     try {
         tableColumnDefault = JSON.parse(tableColumnDefault)
     } catch (e) {
-        // if it's not a valid JSON, we just leave it as it is
+        return (
+            JSON.stringify(columnMetadata.default) ===
+            JSON.stringify(tableColumnDefault)
+        )
     }
 }
 
 return OrmUtils.deepCompare(
     columnMetadata.default,
     tableColumnDefault,
 )
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that default expressions from PostgreSQL can include extra whitespace and parentheses, which the current logic doesn't handle, and proposes a good normalization step to improve comparison robustness.

Medium
General
Normalize expressions before comparison

In defaultEqual, normalize non-JSON default expressions by trimming whitespace
and removing surrounding parentheses before comparison to avoid false change
detection.

src/driver/postgres/PostgresDriver.ts [1257-1264]

-if (
-    columnDefault &&
-    tableColumn.default &&
-    this.stripTypeCastsOutsideQuotes(columnDefault) ===
-        this.stripTypeCastsOutsideQuotes(tableColumn.default)
-) {
-    return true
+if (columnDefault && tableColumn.default) {
+    const normalizeExpr = (v: string) => {
+        let s = this.stripTypeCastsOutsideQuotes(v).trim()
+        if (s.startsWith("(") && s.endsWith(")")) s = s.slice(1, -1).trim()
+        return s
+    }
+
+    if (normalizeExpr(columnDefault) === normalizeExpr(tableColumn.default)) {
+        return true
+    }
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion improves the comparison of non-JSON default values by normalizing them, which prevents false positives in schema change detection due to formatting differences like whitespace or parentheses.

Medium
✅ Suggestions up to commit 1b20b27
CategorySuggestion                                                                                                                                    Impact
Possible issue
Avoid stripping casts inside literals

Improve the stripTypeCastsOutsideQuotes function by replacing the naive string
split with a state machine that correctly handles escaped single-quotes and
dollar-quoted strings to avoid incorrectly stripping type casts from within
string literals.

src/driver/postgres/PostgresDriver.ts [1850-1862]

 stripTypeCastsOutsideQuotes(expr: string): string {
-    return expr
-        .split(`'`)
-        .map((v, i) => {
-            return i % 2 === 0
-                ? v.replace(
-                      /::[\w\s.[\]\-"]+(?:\([^)]*\))?[\w\s.[\]\-"]*/g,
-                      "",
-                  )
-                : v
-        })
-        .join(`'`)
+    const castRegex = /::[\w\s.[\]\-"]+(?:\([^)]*\))?[\w\s.[\]\-"]*/g
+
+    let out = ""
+    let i = 0
+    let inSingle = false
+    let dollarTag: string | null = null
+
+    while (i < expr.length) {
+        // enter/exit dollar-quoted string: $tag$...$tag$
+        if (!inSingle) {
+            const m = expr.slice(i).match(/^\$[A-Za-z_0-9]*\$/)
+            if (m) {
+                const tag = m[0]
+                if (dollarTag === null) {
+                    dollarTag = tag
+                } else if (dollarTag === tag) {
+                    dollarTag = null
+                }
+                out += tag
+                i += tag.length
+                continue
+            }
+        }
+
+        const ch = expr[i]
+
+        if (dollarTag !== null) {
+            out += ch
+            i++
+            continue
+        }
+
+        if (ch === "'") {
+            out += ch
+            // handle escaped quote inside single-quoted literal: ''
+            if (inSingle && expr[i + 1] === "'") {
+                out += "'"
+                i += 2
+                continue
+            }
+            inSingle = !inSingle
+            i++
+            continue
+        }
+
+        if (!inSingle) {
+            const rest = expr.slice(i)
+            const castMatch = rest.match(/^::[\w\s.[\]\-"]+(?:\([^)]*\))?[\w\s.[\]\-"]*/)
+            if (castMatch) {
+                // skip cast outside quotes
+                i += castMatch[0].length
+                continue
+            }
+        }
+
+        out += ch
+        i++
+    }
+
+    // also strip any remaining casts outside quotes that weren't matched by the incremental matcher
+    return out.replace(castRegex, "")
 }
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical flaw in the new stripTypeCastsOutsideQuotes function, which fails on valid SQL literals with escaped quotes or dollar-quoting, and provides a much more robust implementation.

High
General
Compare stripped expressions more broadly
Suggestion Impact:The commit removed the condition requiring different '::' presence between column and table defaults, allowing the stripped comparison to apply even when both defaults include type casts.

code diff:

         if (
             columnDefault &&
             tableColumn.default &&
-            columnDefault.includes("::") !==
-                tableColumn.default.includes("::") &&
             this.stripTypeCastsOutsideQuotes(columnDefault) ===
                 this.stripTypeCastsOutsideQuotes(tableColumn.default)

Modify the defaultEqual method to perform a cast-insensitive comparison whenever
either the column default or table default contains a type cast (::), not just
when their cast presence differs.

src/driver/postgres/PostgresDriver.ts [1257-1266]

-if (
-    columnDefault &&
-    tableColumn.default &&
-    columnDefault.includes("::") !==
-        tableColumn.default.includes("::") &&
-    this.stripTypeCastsOutsideQuotes(columnDefault) ===
+if (columnDefault && tableColumn.default) {
+    const strippedColumnDefault =
+        this.stripTypeCastsOutsideQuotes(columnDefault)
+    const strippedTableDefault =
         this.stripTypeCastsOutsideQuotes(tableColumn.default)
-) {
-    return true
+
+    if (
+        (columnDefault.includes("::") || tableColumn.default.includes("::")) &&
+        strippedColumnDefault === strippedTableDefault
+    ) {
+        return true
+    }
 }
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a limitation in the default value comparison logic that can cause unnecessary migrations when both defaults have different type casts, and proposes a more comprehensive check.

Medium
Add fallback when JSON parsing fails

In defaultEqual for json/jsonb types, add a fallback comparison for when
JSON.parse fails. This involves stringifying the metadata's default value and
comparing it to the database's default string value to handle more variations.

src/driver/postgres/PostgresDriver.ts [1225-1248]

 let tableColumnDefault = tableColumn.default
+let parsed = false
+
 if (typeof tableColumnDefault === "string") {
     tableColumnDefault =
         this.stripTypeCastsOutsideQuotes(tableColumnDefault)
+
     if (
         tableColumnDefault.startsWith("'") &&
         tableColumnDefault.endsWith("'")
     ) {
         tableColumnDefault = tableColumnDefault.substring(
             1,
             tableColumnDefault.length - 1,
         )
     }
+
     try {
         tableColumnDefault = JSON.parse(tableColumnDefault)
+        parsed = true
     } catch (e) {
-        // if it's not a valid JSON, we just leave it as it is
+        parsed = false
     }
 }
 
-return OrmUtils.deepCompare(
-    columnMetadata.default,
-    tableColumnDefault,
-)
+if (parsed) {
+    return OrmUtils.deepCompare(
+        columnMetadata.default,
+        tableColumnDefault,
+    )
+}
 
+// fallback: compare normalized JSON text
+if (typeof tableColumnDefault === "string") {
+    const metaJson = JSON.stringify(columnMetadata.default)
+    return metaJson === tableColumnDefault
+}
+
+return OrmUtils.deepCompare(columnMetadata.default, tableColumnDefault)
+
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that the json/jsonb default comparison logic is incomplete if JSON.parse fails, and proposes a reasonable fallback mechanism to improve robustness.

Medium
✅ Suggestions up to commit d543450
CategorySuggestion                                                                                                                                    Impact
Possible issue
Make cast stripping SQL-safe
Suggestion Impact:The commit modified stripTypeCastsOutsideQuotes in the same area to improve cast stripping by expanding the regex to handle additional cast patterns (e.g., with parentheses), but it did not implement the suggested scanner approach for escaped quotes or dollar-quoted strings.

code diff:

@@ -1851,7 +1849,12 @@
         return expr
             .split(`'`)
             .map((v, i) => {
-                return i % 2 === 0 ? v.replace(/::[\w\s.[\]\-"]+/g, "") : v
+                return i % 2 === 0
+                    ? v.replace(
+                          /::[\w\s.[\]\-"]+(?:\([^)]*\))?[\w\s.[\]\-"]*/g,
+                          "",
+                      )
+                    : v
             })
             .join(`'`)

Refactor stripTypeCastsOutsideQuotes to correctly handle escaped single quotes
and dollar-quoted strings by using a small scanner instead of split("'").

src/driver/postgres/PostgresDriver.ts [1850-1857]

 stripTypeCastsOutsideQuotes(expr: string): string {
-    return expr
-        .split(`'`)
-        .map((v, i) => {
-            return i % 2 === 0 ? v.replace(/::[\w\s.[\]\-"]+/g, "") : v
-        })
-        .join(`'`)
+    let out = ""
+    let i = 0
+    let inSingleQuote = false
+    let dollarTag: string | null = null
+
+    while (i < expr.length) {
+        // enter/exit dollar-quoted strings: $$...$$ or $tag$...$tag$
+        if (!inSingleQuote) {
+            if (dollarTag) {
+                if (expr.startsWith(dollarTag, i)) {
+                    out += dollarTag
+                    i += dollarTag.length
+                    dollarTag = null
+                    continue
+                }
+            } else if (expr[i] === "$") {
+                const end = expr.indexOf("$", i + 1)
+                if (end !== -1) {
+                    const tag = expr.slice(i, end + 1) // includes both '$'
+                    // basic validation: $...$ with no spaces
+                    if (!/\s/.test(tag)) {
+                        dollarTag = tag
+                        out += tag
+                        i += tag.length
+                        continue
+                    }
+                }
+            }
+        }
+
+        // inside dollar-quoted string: copy verbatim
+        if (dollarTag) {
+            out += expr[i++]
+            continue
+        }
+
+        // toggle single-quoted string, handling escaped ''
+        if (expr[i] === "'") {
+            if (inSingleQuote && expr[i + 1] === "'") {
+                out += "''"
+                i += 2
+                continue
+            }
+            inSingleQuote = !inSingleQuote
+            out += expr[i++]
+            continue
+        }
+
+        // strip ::type casts only when outside of quotes
+        if (!inSingleQuote && expr[i] === ":" && expr[i + 1] === ":") {
+            i += 2
+            while (i < expr.length && /[\w\s.[\]\-"]/.test(expr[i])) i++
+            continue
+        }
+
+        out += expr[i++]
+    }
+
+    return out
 }
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies that the stripTypeCastsOutsideQuotes implementation is not robust enough for complex SQL strings, which could cause incorrect schema diffs.

High
Compare defaults ignoring type casts
Suggestion Impact:The commit removed the restrictive `includes("::")` mismatch check, so equality is now determined by comparing the cast-stripped defaults whenever both defaults exist. It also enhanced the cast-stripping regex to handle more cast forms (e.g., with parentheses).

code diff:

@@ -1257,8 +1257,6 @@
         if (
             columnDefault &&
             tableColumn.default &&
-            columnDefault.includes("::") !==
-                tableColumn.default.includes("::") &&
             this.stripTypeCastsOutsideQuotes(columnDefault) ===
                 this.stripTypeCastsOutsideQuotes(tableColumn.default)
         ) {
@@ -1851,7 +1849,12 @@
         return expr
             .split(`'`)
             .map((v, i) => {
-                return i % 2 === 0 ? v.replace(/::[\w\s.[\]\-"]+/g, "") : v
+                return i % 2 === 0
+                    ? v.replace(
+                          /::[\w\s.[\]\-"]+(?:\([^)]*\))?[\w\s.[\]\-"]*/g,
+                          "",
+                      )
+                    : v
             })
             .join(`'`)

Modify the default value comparison to always check the cast-stripped versions
if the raw strings differ, not just when one contains a cast.

src/driver/postgres/PostgresDriver.ts [1251-1268]

 const columnDefault = this.lowerDefaultValueIfNecessary(
     this.normalizeDefault(columnMetadata),
 )
 
 if (columnDefault === tableColumn.default) return true
 
-if (
-    columnDefault &&
-    tableColumn.default &&
-    columnDefault.includes("::") !==
-        tableColumn.default.includes("::") &&
-    this.stripTypeCastsOutsideQuotes(columnDefault) ===
-        this.stripTypeCastsOutsideQuotes(tableColumn.default)
-) {
-    return true
+if (columnDefault && tableColumn.default) {
+    const strippedColumn = this.stripTypeCastsOutsideQuotes(columnDefault)
+    const strippedTable = this.stripTypeCastsOutsideQuotes(tableColumn.default)
+
+    if (strippedColumn === strippedTable) return true
 }
 
 return false
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out a logic flaw where cast-stripped comparison is too restrictive, potentially causing false change detections for default values.

Medium
Make JSON default parsing robust

Improve JSON default value parsing by handling the E'...' prefix and only
attempting to parse if the string starts with { or [.

src/driver/postgres/PostgresDriver.ts [1226-1243]

 if (typeof tableColumnDefault === "string") {
-    tableColumnDefault =
-        this.stripTypeCastsOutsideQuotes(tableColumnDefault)
+    tableColumnDefault = this.stripTypeCastsOutsideQuotes(tableColumnDefault)
+
+    // handle Postgres escaped string literal prefix: E'...'
     if (
+        tableColumnDefault.startsWith("E'") &&
+        tableColumnDefault.endsWith("'")
+    ) {
+        tableColumnDefault = tableColumnDefault.substring(
+            2,
+            tableColumnDefault.length - 1,
+        )
+    } else if (
         tableColumnDefault.startsWith("'") &&
         tableColumnDefault.endsWith("'")
     ) {
         tableColumnDefault = tableColumnDefault.substring(
             1,
             tableColumnDefault.length - 1,
         )
     }
-    try {
-        tableColumnDefault = JSON.parse(tableColumnDefault)
-    } catch (e) {
-        // if it's not a valid JSON, we just leave it as it is
+
+    const trimmed = tableColumnDefault.trim()
+    if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
+        tableColumnDefault = JSON.parse(trimmed)
     }
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion improves the robustness of parsing JSON default values from the database by handling more string literal formats, preventing some false schema differences.

Medium
✅ Suggestions up to commit a3728f5
CategorySuggestion                                                                                                                                    Impact
Possible issue
Normalize expressions before comparing
Suggestion Impact:The commit refactored default-expression comparison to strip PostgreSQL type casts via a shared helper (stripTypeCastsOutsideQuotes) and used it in the relevant comparison paths, improving normalization of defaults with casts (though it did not add whitespace normalization or the exact suggested comparison structure).

code diff:

@@ -1224,10 +1224,8 @@
         ) {
             let tableColumnDefault = tableColumn.default
             if (typeof tableColumnDefault === "string") {
-                tableColumnDefault = tableColumnDefault.replace(
-                    /::[\w\s.[\]\-"]+/g,
-                    "",
-                )
+                tableColumnDefault =
+                    this.stripTypeCastsOutsideQuotes(tableColumnDefault)
                 if (
                     tableColumnDefault.startsWith("'") &&
                     tableColumnDefault.endsWith("'")
@@ -1261,8 +1259,8 @@
             tableColumn.default &&
             columnDefault.includes("::") !==
                 tableColumn.default.includes("::") &&
-            columnDefault.replace(/::[\w\s.[\]\-"]+/g, "") ===
-                tableColumn.default.replace(/::[\w\s.[\]\-"]+/g, "")
+            this.stripTypeCastsOutsideQuotes(columnDefault) ===
+                this.stripTypeCastsOutsideQuotes(tableColumn.default)
         ) {
             return true
         }
@@ -1844,5 +1842,18 @@
 
         return comment
     }
+
+    /**
+     * Strips type casts from a given expression, but only if they are outside of quotes.
+     * @param expr
+     */
+    stripTypeCastsOutsideQuotes(expr: string): string {
+        return expr
+            .split(`'`)
+            .map((v, i) => {
+                return i % 2 === 0 ? v.replace(/::[\w\s.[\]\-"]+/g, "") : v
+            })
+            .join(`'`)
+    }

Normalize both default value expressions by stripping type casts and extra
whitespace before comparison to prevent incorrect schema diffs.

src/driver/postgres/PostgresDriver.ts [1253-1270]

 const columnDefault = this.lowerDefaultValueIfNecessary(
     this.normalizeDefault(columnMetadata),
 )
 
 if (columnDefault === tableColumn.default) return true
 
-if (
-    columnDefault &&
-    tableColumn.default &&
-    columnDefault.includes("::") !==
-        tableColumn.default.includes("::") &&
-    columnDefault.replace(/::[\w\s.[\]\-"]+/g, "") ===
-        tableColumn.default.replace(/::[\w\s.[\]\-"]+/g, "")
-) {
-    return true
-}
+const normalizeDefaultExpression = (v?: string) =>
+    v
+        ? v
+              .replace(/::(?:[\w"]+)(?:\s*\[\])*/g, "")
+              .replace(/\s+/g, " ")
+              .trim()
+        : v
 
-return false
+return (
+    normalizeDefaultExpression(columnDefault) ===
+    normalizeDefaultExpression(tableColumn.default)
+)
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a flaw in the PR's logic for comparing default values with type casts, which could cause unnecessary schema migrations, and provides a more robust solution.

Medium
Avoid mismatched JSON comparisons
Suggestion Impact:The commit adopted the "normalize by stripping Postgres type casts" part of the suggestion by introducing a dedicated helper (stripTypeCastsOutsideQuotes) and using it in the relevant comparisons, but it did not implement the suggested JSON.parse failure fallback to a normalized string comparison.

code diff:

             if (typeof tableColumnDefault === "string") {
-                tableColumnDefault = tableColumnDefault.replace(
-                    /::[\w\s.[\]\-"]+/g,
-                    "",
-                )
+                tableColumnDefault =
+                    this.stripTypeCastsOutsideQuotes(tableColumnDefault)
                 if (
                     tableColumnDefault.startsWith("'") &&
                     tableColumnDefault.endsWith("'")
@@ -1261,8 +1259,8 @@
             tableColumn.default &&
             columnDefault.includes("::") !==
                 tableColumn.default.includes("::") &&
-            columnDefault.replace(/::[\w\s.[\]\-"]+/g, "") ===
-                tableColumn.default.replace(/::[\w\s.[\]\-"]+/g, "")
+            this.stripTypeCastsOutsideQuotes(columnDefault) ===
+                this.stripTypeCastsOutsideQuotes(tableColumn.default)
         ) {
             return true
         }
@@ -1844,5 +1842,23 @@
 
         return comment
     }
+
+    /**
+     * Strips type casts from a given expression, but only if they are outside of quotes.
+     * @param expr
+     */
+    stripTypeCastsOutsideQuotes(expr: string): string {
+        return expr
+            .split(`'`)
+            .map((v, i) => {
+                return i % 2 === 0
+                    ? v.replace(
+                          /::[\w\s.[\]\-"]+(?:\([^)]*\))?[\w\s.[\]\-"]*/g,
+                          "",
+                      )
+                    : v
+            })
+            .join(`'`)
+    }

When JSON.parse fails for a database default value, fall back to a normalized
string comparison instead of a deep comparison between an object and a string.

src/driver/postgres/PostgresDriver.ts [1225-1250]

 let tableColumnDefault = tableColumn.default
 if (typeof tableColumnDefault === "string") {
-    tableColumnDefault = tableColumnDefault.replace(
-        /::[\w\s.[\]\-"]+/g,
-        "",
-    )
-    if (
-        tableColumnDefault.startsWith("'") &&
-        tableColumnDefault.endsWith("'")
-    ) {
-        tableColumnDefault = tableColumnDefault.substring(
-            1,
-            tableColumnDefault.length - 1,
+    const normalized = tableColumnDefault
+        .replace(/::(?:[\w"]+)(?:\s*\[\])*/g, "")
+        .trim()
+
+    const unquoted =
+        normalized.startsWith("'") && normalized.endsWith("'")
+            ? normalized.substring(1, normalized.length - 1)
+            : normalized
+
+    try {
+        tableColumnDefault = JSON.parse(unquoted)
+    } catch {
+        // fall back to normalized string comparison to avoid object-vs-string mismatch
+        return (
+            typeof columnMetadata.default === "string" &&
+            columnMetadata.default.trim() === unquoted
         )
-    }
-    try {
-        tableColumnDefault = JSON.parse(tableColumnDefault)
-    } catch (e) {
-        // if it's not a valid JSON, we just leave it as it is
     }
 }
 
-return OrmUtils.deepCompare(
-    columnMetadata.default,
-    tableColumnDefault,
-)
+return OrmUtils.deepCompare(columnMetadata.default, tableColumnDefault)
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a potential issue where a failed JSON parse could lead to a type mismatch during comparison, and proposes a reasonable fallback to a string comparison.

Medium
General
Tighten cast-stripping pattern
Suggestion Impact:Instead of tightening the regex itself, the commit refactored cast-stripping into a helper that only removes casts outside of quoted strings, reducing unintended stripping; it still uses the original regex.

code diff:

             let tableColumnDefault = tableColumn.default
             if (typeof tableColumnDefault === "string") {
-                tableColumnDefault = tableColumnDefault.replace(
-                    /::[\w\s.[\]\-"]+/g,
-                    "",
-                )
+                tableColumnDefault =
+                    this.stripTypeCastsOutsideQuotes(tableColumnDefault)
                 if (
                     tableColumnDefault.startsWith("'") &&
                     tableColumnDefault.endsWith("'")
@@ -1261,8 +1259,8 @@
             tableColumn.default &&
             columnDefault.includes("::") !==
                 tableColumn.default.includes("::") &&
-            columnDefault.replace(/::[\w\s.[\]\-"]+/g, "") ===
-                tableColumn.default.replace(/::[\w\s.[\]\-"]+/g, "")
+            this.stripTypeCastsOutsideQuotes(columnDefault) ===
+                this.stripTypeCastsOutsideQuotes(tableColumn.default)
         ) {
             return true
         }
@@ -1844,5 +1842,18 @@
 
         return comment
     }
+
+    /**
+     * Strips type casts from a given expression, but only if they are outside of quotes.
+     * @param expr
+     */
+    stripTypeCastsOutsideQuotes(expr: string): string {
+        return expr
+            .split(`'`)
+            .map((v, i) => {
+                return i % 2 === 0 ? v.replace(/::[\w\s.[\]\-"]+/g, "") : v
+            })
+            .join(`'`)
+    }

Refine the regular expression used for stripping type casts from default values
to be more specific, preventing it from removing unintended parts of an
expression.

src/driver/postgres/PostgresDriver.ts [1227-1230]

 tableColumnDefault = tableColumnDefault.replace(
-    /::[\w\s.[\]\-"]+/g,
+    /::(?:[\w"]+)(?:\s*\[\])*/g,
     "",
 )
Suggestion importance[1-10]: 6

__

Why: The suggestion improves the regex for stripping type casts, making it more precise and less likely to cause incorrect comparisons, which enhances the robustness of the default value comparison logic.

Low
✅ Suggestions up to commit bbc9ee3
CategorySuggestion                                                                                                                                    Impact
Possible issue
Preserve numeric default normalization
Suggestion Impact:The commit reordered the .replace calls so the ::type cast is removed before wrapping numeric defaults in quotes, preserving prior numeric default normalization behavior.

code diff:

@@ -3997,8 +3997,8 @@
                                     tableColumn.default = dbColumn[
                                         "column_default"
                                     ]
+                                        .replace(/::[\w\s.[\]\-"]+/g, "")
                                         .replace(/^(-?\d+)$/, "'$1'")
-                                        .replace(/::[\w\s.[\]\-"]+/g, "")
                                 }

Reorder the replace calls to first strip type casts and then normalize numeric
defaults to preserve the original behavior.

src/driver/postgres/PostgresQueryRunner.ts [3999-4001]

 ]
+    .replace(/::[\w\s.[\]\-"]+/g, "")
     .replace(/^(-?\d+)$/, "'$1'")
-    .replace(/::[\w\s.[\]\-"]+/g, "")
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a regression in default value normalization for numeric types with casts, which could cause unnecessary schema migrations.

Medium
Avoid over-stripping type casts
Suggestion Impact:Replaced direct global regex-based stripping with a helper (stripTypeCastsOutsideQuotes) that removes type casts only outside quoted string segments, preventing incorrect modifications inside string literals.

code diff:

-                tableColumnDefault = tableColumnDefault.replace(
-                    /::[\w\s.[\]\-"]+/g,
-                    "",
-                )
+                tableColumnDefault =
+                    this.stripTypeCastsOutsideQuotes(tableColumnDefault)
                 if (
                     tableColumnDefault.startsWith("'") &&
                     tableColumnDefault.endsWith("'")
@@ -1261,8 +1259,8 @@
             tableColumn.default &&
             columnDefault.includes("::") !==
                 tableColumn.default.includes("::") &&
-            columnDefault.replace(/::[\w\s.[\]\-"]+/g, "") ===
-                tableColumn.default.replace(/::[\w\s.[\]\-"]+/g, "")
+            this.stripTypeCastsOutsideQuotes(columnDefault) ===
+                this.stripTypeCastsOutsideQuotes(tableColumn.default)
         ) {
             return true
         }
@@ -1844,5 +1842,18 @@
 
         return comment
     }
+
+    /**
+     * Strips type casts from a given expression, but only if they are outside of quotes.
+     * @param expr
+     */
+    stripTypeCastsOutsideQuotes(expr: string): string {
+        return expr
+            .split(`'`)
+            .map((v, i) => {
+                return i % 2 === 0 ? v.replace(/::[\w\s.[\]\-"]+/g, "") : v
+            })
+            .join(`'`)
+    }

Change the regex for stripping type casts to only match at the end of the string
to avoid incorrectly modifying string literals that contain ::.

src/driver/postgres/PostgresDriver.ts [1227-1230]

 tableColumnDefault = tableColumnDefault.replace(
-    /::[\w\s.[\]\-"]+/g,
+    /::[\w\s.[\]\-"]+$/g,
     "",
 )
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that globally replacing :: is too aggressive and can corrupt string literals, proposing a safer alternative by only matching trailing casts.

Medium
General
Reduce false-positive default equality
Suggestion Impact:Updated default comparison logic to strip type casts via a dedicated helper (that ignores casts inside quotes) and used it for both table and column defaults, improving normalization before equality checks.

code diff:

@@ -1224,10 +1224,8 @@
         ) {
             let tableColumnDefault = tableColumn.default
             if (typeof tableColumnDefault === "string") {
-                tableColumnDefault = tableColumnDefault.replace(
-                    /::[\w\s.[\]\-"]+/g,
-                    "",
-                )
+                tableColumnDefault =
+                    this.stripTypeCastsOutsideQuotes(tableColumnDefault)
                 if (
                     tableColumnDefault.startsWith("'") &&
                     tableColumnDefault.endsWith("'")
@@ -1261,8 +1259,8 @@
             tableColumn.default &&
             columnDefault.includes("::") !==
                 tableColumn.default.includes("::") &&
-            columnDefault.replace(/::[\w\s.[\]\-"]+/g, "") ===
-                tableColumn.default.replace(/::[\w\s.[\]\-"]+/g, "")
+            this.stripTypeCastsOutsideQuotes(columnDefault) ===
+                this.stripTypeCastsOutsideQuotes(tableColumn.default)
         ) {
             return true
         }
@@ -1844,5 +1842,18 @@
 
         return comment
     }
+
+    /**
+     * Strips type casts from a given expression, but only if they are outside of quotes.
+     * @param expr
+     */
+    stripTypeCastsOutsideQuotes(expr: string): string {
+        return expr
+            .split(`'`)
+            .map((v, i) => {
+                return i % 2 === 0 ? v.replace(/::[\w\s.[\]\-"]+/g, "") : v
+            })
+            .join(`'`)
+    }

Refactor the default value comparison to more safely handle type casts by
normalizing both values before comparison, reducing the chance of incorrect
equality checks.

src/driver/postgres/PostgresDriver.ts [1259-1268]

-if (
-    columnDefault &&
-    tableColumn.default &&
-    columnDefault.includes("::") !==
-        tableColumn.default.includes("::") &&
-    columnDefault.replace(/::[\w\s.[\]\-"]+/g, "") ===
-        tableColumn.default.replace(/::[\w\s.[\]\-"]+/g, "")
-) {
-    return true
+if (columnDefault && tableColumn.default) {
+    const normalizedColumnDefault = columnDefault.replace(
+        /::[\w\s.[\]\-"]+$/g,
+        "",
+    )
+    const normalizedTableDefault = tableColumn.default.replace(
+        /::[\w\s.[\]\-"]+$/g,
+        "",
+    )
+
+    if (
+        normalizedColumnDefault === normalizedTableDefault &&
+        normalizedColumnDefault !== columnDefault
+    ) {
+        return true
+    }
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion improves the logic for comparing default values by using a more robust method for handling type casts, which prevents false positives and makes the comparison more accurate.

Medium
✅ Suggestions up to commit f24280f
CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix numeric default normalization order
Suggestion Impact:The commit moved the type-cast stripping `.replace(/::.../g, "")` to occur before wrapping numeric defaults in quotes, matching the suggested normalization fix.

code diff:

                                     tableColumn.default = dbColumn[
                                         "column_default"
                                     ]
+                                        .replace(/::[\w\s.[\]\-"]+/g, "")
                                         .replace(/^(-?\d+)$/, "'$1'")
-                                        .replace(/::[\w\s.[\]\-"]+/g, "")

Reverse the order of the replace calls to first strip type casts and then wrap
numeric defaults in quotes, fixing an issue with defaults like '42::integer'.

src/driver/postgres/PostgresQueryRunner.ts [3997-4001]

-tableColumn.default = dbColumn[
-    "column_default"
-]
+tableColumn.default = dbColumn["column_default"]
+    .replace(/::[\w\s.[\]\-"]+/g, "")
     .replace(/^(-?\d+)$/, "'$1'")
-    .replace(/::[\w\s.[\]\-"]+/g, "")
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a logic error in the order of replace operations, which would cause incorrect schema comparisons for numeric defaults with type casts.

Medium
Handle parentheses in default parsing

Add logic to trim whitespace and remove surrounding parentheses from default
values before parsing to correctly handle formats like ('{"a":1}'::jsonb).

src/driver/postgres/PostgresDriver.ts [1225-1245]

 let tableColumnDefault = tableColumn.default
 if (typeof tableColumnDefault === "string") {
+    tableColumnDefault = tableColumnDefault.trim()
+
+    while (
+        tableColumnDefault.startsWith("(") &&
+        tableColumnDefault.endsWith(")")
+    ) {
+        tableColumnDefault = tableColumnDefault
+            .substring(1, tableColumnDefault.length - 1)
+            .trim()
+    }
+
     tableColumnDefault = tableColumnDefault.replace(
         /::[\w\s.[\]\-"]+/g,
         "",
     )
+
     if (
         tableColumnDefault.startsWith("'") &&
         tableColumnDefault.endsWith("'")
     ) {
         tableColumnDefault = tableColumnDefault.substring(
             1,
             tableColumnDefault.length - 1,
         )
     }
+
     try {
         tableColumnDefault = JSON.parse(tableColumnDefault)
     } catch (e) {
         // if it's not a valid JSON, we just leave it as it is
     }
 }
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that Postgres default values can be wrapped in parentheses, which the current code does not handle, leading to incorrect schema comparisons.

Medium
✅ Suggestions up to commit 148b87e
CategorySuggestion                                                                                                                                   

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 16, 2026

commit: be0755e

@qodo-free-for-open-source-projects
Copy link

qodo-free-for-open-source-projects bot commented Feb 16, 2026

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (3) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Casts hide real default changes 🐞 Bug ✓ Correctness
Description
defaultEqual now falls back to comparing defaults after stripping all ::type casts outside
quotes on both sides. This can collapse semantically different defaults (e.g., different datetime
function normalizations/types) into the same string and prevent schema builder from detecting a real
default change.
Code

src/driver/postgres/PostgresDriver.ts[R1255-1266]

+        if (columnDefault === tableColumn.default) return true
+
+        if (
+            columnDefault &&
+            tableColumn.default &&
+            this.stripTypeCastsOutsideQuotes(columnDefault) ===
+                this.stripTypeCastsOutsideQuotes(tableColumn.default)
+        ) {
+            return true
+        }
+
+        return false
Evidence
normalizeDatetimeFunction produces distinct default expressions for different datetime defaults
(notably differing by casts/type). The new fallback in defaultEqual strips those casts via
stripTypeCastsOutsideQuotes, which removes ::date, ::timestamp ..., etc., making different
defaults compare equal and therefore treated as “unchanged” by schema diffing.

src/driver/postgres/PostgresDriver.ts[1794-1826]
src/driver/postgres/PostgresDriver.ts[1251-1267]
src/driver/postgres/PostgresDriver.ts[1844-1860]
src/driver/postgres/PostgresDriver.ts[1429-1451]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`defaultEqual` uses `stripTypeCastsOutsideQuotes` as a fallback equality check. Because the stripper removes *all* `::type` casts (outside quotes) across the whole expression, semantically different defaults that rely on casts/type distinctions can be treated as equal, preventing schema diffs/migrations.
### Issue Context
- `normalizeDatetimeFunction` intentionally returns different expressions for `CURRENT_DATE` vs `LOCALTIMESTAMP`, differing primarily by casts/type.
- The fallback equality removes those casts, making them comparable as identical strings.
### Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[1251-1267]
- src/driver/postgres/PostgresDriver.ts[1794-1826]
- src/driver/postgres/PostgresDriver.ts[1844-1860]
### Suggested direction
- Consider stripping casts only from `tableColumn.default` (database side), and only those casts that Postgres injects around literals / at the end of the expression (e.g., `&amp;#x27;&amp;lt;literal&amp;gt;&amp;#x27;::type`, or an outermost trailing cast), instead of stripping every cast everywhere.
- Add a regression test that changes a column default between two different datetime defaults (e.g. CURRENT_DATE vs LOCALTIMESTAMP) and ensure a schema diff is produced.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


✅ 2. JSON default parse crash 🐞 Bug ⛯ Reliability
Description
PostgresQueryRunner now preserves type casts in tableColumn.default (e.g. ::jsonb), but
PostgresDriver.defaultEqual's json/jsonb branch still JSON.parse()s by stripping only the first/last
character, assuming the value ends with a quote. If the DB returns a casted JSON default, schema
diff/sync can throw at runtime while comparing defaults.
Code

src/driver/postgres/PostgresQueryRunner.ts[R3997-4001]

+                                    tableColumn.default =
+                                        dbColumn["column_default"]
                           tableColumn.default =
                               tableColumn.default.replace(
                                   /^(-?\d+)$/,
Evidence
The PR change stops removing ::type casts from dbColumn["column_default"] when populating
tableColumn.default. Separately, PostgresDriver.defaultEqual has a special json/jsonb path that
parses tableColumn.default by taking substring(1, len-1), which only works when the string literally
ends with a closing single-quote. The query-runner code itself demonstrates that information_schema
defaults can contain casts (e.g. "'now'::text"), so casted defaults are expected in practice; with
casts preserved, the JSON.parse path becomes unsafe. This comparison runs during synchronize() which
is used broadly in tests and in schema sync flows.

src/driver/postgres/PostgresQueryRunner.ts[3988-4004]
src/driver/postgres/PostgresDriver.ts[1221-1233]
test/utils/test-utils.ts[504-508]
test/functional/json/basic-jsonb/entity/Record.ts[17-25]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PostgresQueryRunner` now assigns `tableColumn.default` directly from `information_schema.columns.column_default` without stripping `::type` casts, but `PostgresDriver.defaultEqual()`&amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;s JSON/JSONB branch still assumes `tableColumn.default` ends with a `&amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;` and uses `substring(1, len-1)` before `JSON.parse()`. If the default includes a cast suffix (e.g. `...&amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;::jsonb`), parsing will fail.
### Issue Context
- The query runner already expects casts in defaults (e.g. it checks for `&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;now&amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;::text&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;`), so casted defaults are realistic.
- JSONB defaults are used in the test suite and schema synchronization flows.
### Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[1221-1239]
- src/driver/postgres/PostgresQueryRunner.ts[3988-4004]
### Suggested approach
1. Add a small helper to strip `::type` casts **only outside single-quoted literals** (or at minimum strip only a trailing cast suffix).
2. In the JSON/JSONB branch, apply that helper to `tableColumn.default` before attempting `substring`/`JSON.parse`.
3. Add/extend a functional test covering a JSONB column with an object default and verifying schema diff does not throw.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


✅ 3. JSON default parse crash 🐞 Bug ⛯ Reliability
Description
PostgresQueryRunner now preserves type casts in tableColumn.default (e.g. ::jsonb), but
PostgresDriver.defaultEqual's json/jsonb branch still JSON.parse()s by stripping only the first/last
character, assuming the value ends with a quote. If the DB returns a casted JSON default, schema
diff/sync can throw at runtime while comparing defaults.
Code

src/driver/postgres/PostgresQueryRunner.ts[R3997-4001]

+                                    tableColumn.default =
+                                        dbColumn["column_default"]
                          tableColumn.default =
                              tableColumn.default.replace(
                                  /^(-?\d+)$/,
Evidence
The PR change stops removing ::type casts from dbColumn["column_default"] when populating
tableColumn.default. Separately, PostgresDriver.defaultEqual has a special json/jsonb path that
parses tableColumn.default by taking substring(1, len-1), which only works when the string literally
ends with a closing single-quote. The query-runner code itself demonstrates that information_schema
defaults can contain casts (e.g. "'now'::text"), so casted defaults are expected in practice; with
casts preserved, the JSON.parse path becomes unsafe. This comparison runs during synchronize() which
is used broadly in tests and in schema sync flows.

src/driver/postgres/PostgresQueryRunner.ts[3988-4004]
src/driver/postgres/PostgresDriver.ts[1221-1233]
test/utils/test-utils.ts[504-508]
test/functional/json/basic-jsonb/entity/Record.ts[17-25]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PostgresQueryRunner` now assigns `tableColumn.default` directly from `information_schema.columns.column_default` without stripping `::type` casts, but `PostgresDriver.defaultEqual()`&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;s JSON/JSONB branch still assumes `tableColumn.default` ends with a `&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;` and uses `substring(1, len-1)` before `JSON.parse()`. If the default includes a cast suffix (e.g. `...&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;::jsonb`), parsing will fail.
### Issue Context
- The query runner already expects casts in defaults (e.g. it checks for `&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;now&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;::text&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;`), so casted defaults are realistic.
- JSONB defaults are used in the test suite and schema synchronization flows.
### Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[1221-1239]
- src/driver/postgres/PostgresQueryRunner.ts[3988-4004]
### Suggested approach
1. Add a small helper to strip `::type` casts **only outside single-quoted literals** (or at minimum strip only a trailing cast suffix).
2. In the JSON/JSONB branch, apply that helper to `tableColumn.default` before attempting `substring`/`JSON.parse`.
3. Add/extend a functional test covering a JSONB column with an object default and verifying schema diff does not throw.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Naive cast stripping 🐞 Bug ✓ Correctness ⭐ New
Description
stripTypeCastsOutsideQuotes only tracks single-quote literals via split("'") and ignores other valid
PostgreSQL literal forms (e.g., dollar-quoted strings, escape strings), so it can strip casts from
inside literals or corrupt expressions. This can lead to false schema diffs (unnecessary/missed
migrations) and potentially malformed down-migration SQL that reuses the loaded default.
Code

src/driver/postgres/PostgresDriver.ts[R1848-1860]

+    stripTypeCastsOutsideQuotes(expr: string): string {
+        return expr
+            .split(`'`)
+            .map((v, i) => {
+                return i % 2 === 0
+                    ? v.replace(
+                          /::[\w\s.[\]\-"]+(?:\([^)]*\))?[\w\s.[\]\-"]*/g,
+                          "",
+                      )
+                    : v
+            })
+            .join(`'`)
+    }
Evidence
The new helper removes casts by splitting on single quotes and applying a regex to “outside-quote”
segments, but this does not model PostgreSQL’s full quoting rules. The helper is used both when
reading defaults from the database and when comparing defaults for schema diffs; those diffs
directly drive generated ALTER TABLE ... SET DEFAULT statements (including down queries), so any
corruption/false equality affects migration correctness.

src/driver/postgres/PostgresDriver.ts[1844-1860]
src/driver/postgres/PostgresQueryRunner.ts[3951-4002]
src/driver/postgres/PostgresDriver.ts[1217-1266]
src/driver/postgres/PostgresDriver.ts[1429-1451]
src/driver/postgres/PostgresQueryRunner.ts[1713-1737]
src/driver/postgres/PostgresDriver.ts[1198-1202]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`stripTypeCastsOutsideQuotes` attempts to remove Postgres `::type` casts while preserving casts inside string literals, but it only models single-quoted literals by doing `expr.split(&quot;&#x27;&quot;)`. This does not cover other valid Postgres literal forms (notably dollar-quoted strings and escape strings), and can lead to corrupted defaults and incorrect schema diff decisions.

### Issue Context
This helper is used both when reading `information_schema.columns.column_default` into `TableColumn.default` and when comparing defaults in `defaultEqual()`, which drives schema sync/migration generation.

### Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[1844-1860]
- src/driver/postgres/PostgresDriver.ts[1217-1266]
- src/driver/postgres/PostgresQueryRunner.ts[3951-4002]
- test/functional/schema-builder/change-default-value.test.ts[1-58]

### Notes
Add new regression tests for default expressions that use:
- dollar-quoted strings (`$$...$$` / `$tag$...$tag$`) containing `::`
- escape strings (`E&#x27;...&#x27;`) with escaped quotes/backslashes containing `::`
so `stripTypeCastsOutsideQuotes` does not strip within those literals and schema diff remains stable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Empty @param JSDoc tags 📘 Rule violation ✓ Correctness
Description
Multiple new JSDoc @param tags were added without any descriptions, which adds comment noise and
is stylistically inconsistent. This may reduce readability and violate the project’s expectation to
avoid AI-like filler comments.
Code

src/driver/postgres/PostgresDriver.ts[R715-718]

/**
* Creates a query runner used to execute database queries.
+     * @param mode
*/
Evidence
Compliance ID 4 requires avoiding AI-generated noise and extra comments; the added JSDoc @param
lines provide no descriptive value and appear to be filler. The cited lines show an added @param
tag with no description.

Rule 4: Remove AI-generated noise
src/driver/postgres/PostgresDriver.ts[715-718]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New JSDoc `@param` tags were added without any descriptive text (e.g., `* @param mode`). This is comment noise and can be interpreted as AI-generated filler.
## Issue Context
The compliance checklist item &amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;Remove AI-generated noise&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot; requires avoiding extra comments and style inconsistencies.
## Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[715-727]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Swallowed JSON.parse error 📘 Rule violation ⛯ Reliability
Description
The new logic wraps JSON.parse in a try/catch that silently ignores failures, which is an added
defensive construct that can hide unexpected formats and make debugging schema-diff issues harder.
This may be considered abnormal defensive code under the noise policy.
Code

src/driver/postgres/PostgresDriver.ts[R1238-1242]

+                try {
+                    tableColumnDefault = JSON.parse(tableColumnDefault)
+                } catch (e) {
+                    // if it's not a valid JSON, we just leave it as it is
+                }
Evidence
Compliance ID 4 disallows abnormal defensive try/catch blocks and low-value comments; this change
adds a swallowed exception path (catch with no handling) during default comparison.

Rule 4: Remove AI-generated noise
src/driver/postgres/PostgresDriver.ts[1238-1242]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`JSON.parse` errors are caught and ignored, which introduces a silent failure path and an added defensive try/catch block.
## Issue Context
This occurs in `defaultEqual()` while normalizing/comparing `json`/`jsonb` defaults.
## Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[1225-1243]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
7. Cast stripping corrupts literals 🐞 Bug ✓ Correctness
Description
defaultEqual strips /::.../g across the entire default expression without respecting single-quoted
string literals. If a string literal itself contains "::" (e.g. IPv6/text tokens), the comparison
can remove part of the literal and treat different defaults as equal, skipping required schema
changes.
Code

src/driver/postgres/PostgresDriver.ts[R1247-1253]

+        if (
+            columnDefault &&
+            tableColumn.default &&
+            columnDefault.replace(/::[\w\s.[\]\-"]+/g, "") ===
+                tableColumn.default.replace(/::[\w\s.[\]\-"]+/g, "")
+        ) {
+            return true
Evidence
normalizeDefault wraps string defaults in single quotes, so user data can legitimately include "::"
inside a quoted literal. The new comparison removes ::... anywhere in the string, including inside
those literals, because it does not split by quotes. The codebase already has a pattern
(lowerDefaultValueIfNecessary) that avoids mutating quoted segments by splitting on ',
highlighting that this comparison should likely be quote-aware too.

src/driver/postgres/PostgresDriver.ts[1184-1192]
src/driver/postgres/PostgresDriver.ts[1245-1254]
src/driver/postgres/PostgresDriver.ts[1564-1575]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new default comparison removes `::type` patterns using a regex across the full SQL default expression. Because it is not quote-aware, it can match `::` inside single-quoted literals and remove user data, potentially making different defaults compare as equal.
### Issue Context
`normalizeDefault()` wraps string/number defaults in single quotes, so defaults can legitimately contain `::` as part of the value. The codebase already demonstrates quote-aware string processing in `lowerDefaultValueIfNecessary()`.
### Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[1184-1192]
- src/driver/postgres/PostgresDriver.ts[1245-1254]
- src/driver/postgres/PostgresDriver.ts[1564-1575]
### Suggested approach
1. Implement a helper like `stripTypeCastsOutsideQuotes(expr: string): string` using the same `split(&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;)` technique to only apply the `::type` removal on even (outside-quote) segments.
2. Use this helper in `defaultEqual()` instead of calling `.replace(/::.../g, &amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;)` directly.
3. Add a functional test with a string column default containing `::` (e.g. `default: &amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;2001:db8::1&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;`) and verify schema diff behaves correctly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Empty @param JSDoc tags 📘 Rule violation ✓ Correctness
Description
Multiple new JSDoc @param tags were added without any descriptions, which adds comment noise and
is stylistically inconsistent. This may reduce readability and violate the project’s expectation to
avoid AI-like filler comments.
Code

src/driver/postgres/PostgresDriver.ts[R715-718]

/**
* Creates a query runner used to execute database queries.
+     * @param mode
*/
Evidence
Compliance ID 4 requires avoiding AI-generated noise and extra comments; the added JSDoc @param
lines provide no descriptive value and appear to be filler. The cited lines show an added @param
tag with no description.

Rule 4: Remove AI-generated noise
src/driver/postgres/PostgresDriver.ts[715-718]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New JSDoc `@param` tags were added without any descriptive text (e.g., `* @param mode`). This is comment noise and can be interpreted as AI-generated filler.
## Issue Context
The compliance checklist item &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;Remove AI-generated noise&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot; requires avoiding extra comments and style inconsistencies.
## Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[715-727]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Cast stripping corrupts literals 🐞 Bug ✓ Correctness
Description
defaultEqual strips /::.../g across the entire default expression without respecting single-quoted
string literals. If a string literal itself contains "::" (e.g. IPv6/text tokens), the comparison
can remove part of the literal and treat different defaults as equal, skipping required schema
changes.
Code

src/driver/postgres/PostgresDriver.ts[R1247-1253]

+        if (
+            columnDefault &&
+            tableColumn.default &&
+            columnDefault.replace(/::[\w\s.[\]\-"]+/g, "") ===
+                tableColumn.default.replace(/::[\w\s.[\]\-"]+/g, "")
+        ) {
+            return true
Evidence
normalizeDefault wraps string defaults in single quotes, so user data can legitimately include "::"
inside a quoted literal. The new comparison removes ::... anywhere in the string, including inside
those literals, because it does not split by quotes. The codebase already has a pattern
(lowerDefaultValueIfNecessary) that avoids mutating quoted segments by splitting on ',
highlighting that this comparison should likely be quote-aware too.

src/driver/postgres/PostgresDriver.ts[1184-1192]
src/driver/postgres/PostgresDriver.ts[1245-1254]
src/driver/postgres/PostgresDriver.ts[1564-1575]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new default comparison removes `::type` patterns using a regex across the full SQL default expression. Because it is not quote-aware, it can match `::` inside single-quoted literals and remove user data, potentially making different defaults compare as equal.
### Issue Context
`normalizeDefault()` wraps string/number defaults in single quotes, so defaults can legitimately contain `::` as part of the value. The codebase already demonstrates quote-aware string processing in `lowerDefaultValueIfNecessary()`.
### Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[1184-1192]
- src/driver/postgres/PostgresDriver.ts[1245-1254]
- src/driver/postgres/PostgresDriver.ts[1564-1575]
### Suggested approach
1. Implement a helper like `stripTypeCastsOutsideQuotes(expr: string): string` using the same `split(&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;)` technique to only apply the `::type` removal on even (outside-quote) segments.
2. Use this helper in `defaultEqual()` instead of calling `.replace(/::.../g, &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;)` directly.
3. Add a functional test with a string column default containing `::` (e.g. `default: &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;2001:db8::1&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;`) and verify schema diff behaves correctly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Grey Divider

Previous review results

Review updated until commit be0755e

Results up to commit 486a336


🐞 Bugs (3) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider
Action required
1. Casts hide real default changes 🐞 Bug ✓ Correctness ⭐ New
Description
defaultEqual now falls back to comparing defaults after stripping all ::type casts outside
quotes on both sides. This can collapse semantically different defaults (e.g., different datetime
function normalizations/types) into the same string and prevent schema builder from detecting a real
default change.
Code

src/driver/postgres/PostgresDriver.ts[R1255-1266]

+        if (columnDefault === tableColumn.default) return true
+
+        if (
+            columnDefault &&
+            tableColumn.default &&
+            this.stripTypeCastsOutsideQuotes(columnDefault) ===
+                this.stripTypeCastsOutsideQuotes(tableColumn.default)
+        ) {
+            return true
+        }
+
+        return false
Evidence
normalizeDatetimeFunction produces distinct default expressions for different datetime defaults
(notably differing by casts/type). The new fallback in defaultEqual strips those casts via
stripTypeCastsOutsideQuotes, which removes ::date, ::timestamp ..., etc., making different
defaults compare equal and therefore treated as “unchanged” by schema diffing.

src/driver/postgres/PostgresDriver.ts[1794-1826]
src/driver/postgres/PostgresDriver.ts[1251-1267]
src/driver/postgres/PostgresDriver.ts[1844-1860]
src/driver/postgres/PostgresDriver.ts[1429-1451]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`defaultEqual` uses `stripTypeCastsOutsideQuotes` as a fallback equality check. Because the stripper removes *all* `::type` casts (outside quotes) across the whole expression, semantically different defaults that rely on casts/type distinctions can be treated as equal, preventing schema diffs/migrations.

### Issue Context
- `normalizeDatetimeFunction` intentionally returns different expressions for `CURRENT_DATE` vs `LOCALTIMESTAMP`, differing primarily by casts/type.
- The fallback equality removes those casts, making them comparable as identical strings.

### Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[1251-1267]
- src/driver/postgres/PostgresDriver.ts[1794-1826]
- src/driver/postgres/PostgresDriver.ts[1844-1860]

### Suggested direction
- Consider stripping casts only from `tableColumn.default` (database side), and only those casts that Postgres injects around literals / at the end of the expression (e.g., `&#x27;&lt;literal&gt;&#x27;::type`, or an outermost trailing cast), instead of stripping every cast everywhere.
- Add a regression test that changes a column default between two different datetime defaults (e.g. CURRENT_DATE vs LOCALTIMESTAMP) and ensure a schema diff is produced.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


✅ 2. JSON default parse crash 🐞 Bug ⛯ Reliability
Description
PostgresQueryRunner now preserves type casts in tableColumn.default (e.g. ::jsonb), but
PostgresDriver.defaultEqual's json/jsonb branch still JSON.parse()s by stripping only the first/last
character, assuming the value ends with a quote. If the DB returns a casted JSON default, schema
diff/sync can throw at runtime while comparing defaults.
Code

src/driver/postgres/PostgresQueryRunner.ts[R3997-4001]

+                                    tableColumn.default =
+                                        dbColumn["column_default"]
                           tableColumn.default =
                               tableColumn.default.replace(
                                   /^(-?\d+)$/,
Evidence
The PR change stops removing ::type casts from dbColumn["column_default"] when populating
tableColumn.default. Separately, PostgresDriver.defaultEqual has a special json/jsonb path that
parses tableColumn.default by taking substring(1, len-1), which only works when the string literally
ends with a closing single-quote. The query-runner code itself demonstrates that information_schema
defaults can contain casts (e.g. "'now'::text"), so casted defaults are expected in practice; with
casts preserved, the JSON.parse path becomes unsafe. This comparison runs during synchronize() which
is used broadly in tests and in schema sync flows.

src/driver/postgres/PostgresQueryRunner.ts[3988-4004]
src/driver/postgres/PostgresDriver.ts[1221-1233]
test/utils/test-utils.ts[504-508]
test/functional/json/basic-jsonb/entity/Record.ts[17-25]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PostgresQueryRunner` now assigns `tableColumn.default` directly from `information_schema.columns.column_default` without stripping `::type` casts, but `PostgresDriver.defaultEqual()`&amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;s JSON/JSONB branch still assumes `tableColumn.default` ends with a `&amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;` and uses `substring(1, len-1)` before `JSON.parse()`. If the default includes a cast suffix (e.g. `...&amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;::jsonb`), parsing will fail.
### Issue Context
- The query runner already expects casts in defaults (e.g. it checks for `&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;now&amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;::text&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;`), so casted defaults are realistic.
- JSONB defaults are used in the test suite and schema synchronization flows.
### Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[1221-1239]
- src/driver/postgres/PostgresQueryRunner.ts[3988-4004]
### Suggested approach
1. Add a small helper to strip `::type` casts **only outside single-quoted literals** (or at minimum strip only a trailing cast suffix).
2. In the JSON/JSONB branch, apply that helper to `tableColumn.default` before attempting `substring`/`JSON.parse`.
3. Add/extend a functional test covering a JSONB column with an object default and verifying schema diff does not throw.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
3. Swallowed JSON.parse error 📘 Rule violation ⛯ Reliability ⭐ New
Description
The new logic wraps JSON.parse in a try/catch that silently ignores failures, which is an added
defensive construct that can hide unexpected formats and make debugging schema-diff issues harder.
This may be considered abnormal defensive code under the noise policy.
Code

src/driver/postgres/PostgresDriver.ts[R1238-1242]

+                try {
+                    tableColumnDefault = JSON.parse(tableColumnDefault)
+                } catch (e) {
+                    // if it's not a valid JSON, we just leave it as it is
+                }
Evidence
Compliance ID 4 disallows abnormal defensive try/catch blocks and low-value comments; this change
adds a swallowed exception path (catch with no handling) during default comparison.

Rule 4: Remove AI-generated noise
src/driver/postgres/PostgresDriver.ts[1238-1242]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`JSON.parse` errors are caught and ignored, which introduces a silent failure path and an added defensive try/catch block.

## Issue Context
This occurs in `defaultEqual()` while normalizing/comparing `json`/`jsonb` defaults.

## Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[1225-1243]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Empty @param JSDoc tags 📘 Rule violation ✓ Correctness
Description
Multiple new JSDoc @param tags were added without any descriptions, which adds comment noise and
is stylistically inconsistent. This may reduce readability and violate the project’s expectation to
avoid AI-like filler comments.
Code

src/driver/postgres/PostgresDriver.ts[R715-718]

/**
* Creates a query runner used to execute database queries.
+     * @param mode
*/
Evidence
Compliance ID 4 requires avoiding AI-generated noise and extra comments; the added JSDoc @param
lines provide no descriptive value and appear to be filler. The cited lines show an added @param
tag with no description.

Rule 4: Remove AI-generated noise
src/driver/postgres/PostgresDriver.ts[715-718]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New JSDoc `@param` tags were added without any descriptive text (e.g., `* @param mode`). This is comment noise and can be interpreted as AI-generated filler.
## Issue Context
The compliance checklist item &amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;Remove AI-generated noise&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot; requires avoiding extra comments and style inconsistencies.
## Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[715-727]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Cast stripping corrupts literals 🐞 Bug ✓ Correctness
Description
defaultEqual strips /::.../g across the entire default expression without respecting single-quoted
string literals. If a string literal itself contains "::" (e.g. IPv6/text tokens), the comparison
can remove part of the literal and treat different defaults as equal, skipping required schema
changes.
Code

src/driver/postgres/PostgresDriver.ts[R1247-1253]

+        if (
+            columnDefault &&
+            tableColumn.default &&
+            columnDefault.replace(/::[\w\s.[\]\-"]+/g, "") ===
+                tableColumn.default.replace(/::[\w\s.[\]\-"]+/g, "")
+        ) {
+            return true
Evidence
normalizeDefault wraps string defaults in single quotes, so user data can legitimately include "::"
inside a quoted literal. The new comparison removes ::... anywhere in the string, including inside
those literals, because it does not split by quotes. The codebase already has a pattern
(lowerDefaultValueIfNecessary) that avoids mutating quoted segments by splitting on ',
highlighting that this comparison should likely be quote-aware too.

src/driver/postgres/PostgresDriver.ts[1184-1192]
src/driver/postgres/PostgresDriver.ts[1245-1254]
src/driver/postgres/PostgresDriver.ts[1564-1575]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new default comparison removes `::type` patterns using a regex across the full SQL default expression. Because it is not quote-aware, it can match `::` inside single-quoted literals and remove user data, potentially making different defaults compare as equal.
### Issue Context
`normalizeDefault()` wraps string/number defaults in single quotes, so defaults can legitimately contain `::` as part of the value. The codebase already demonstrates quote-aware string processing in `lowerDefaultValueIfNecessary()`.
### Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[1184-1192]
- src/driver/postgres/PostgresDriver.ts[1245-1254]
- src/driver/postgres/PostgresDriver.ts[1564-1575]
### Suggested approach
1. Implement a helper like `stripTypeCastsOutsideQuotes(expr: string): string` using the same `split(&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;)` technique to only apply the `::type` removal on even (outside-quote) segments.
2. Use this helper in `defaultEqual()` instead of calling `.replace(/::.../g, &amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;)` directly.
3. Add a functional test with a string column default containing `::` (e.g. `default: &amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;2001:db8::1&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;`) and verify schema diff behaves correctly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider Grey Divider
Results up to commit N/A


🐞 Bugs (2) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider
Action required
✅ 1. JSON default parse crash 🐞 Bug ⛯ Reliability
Description
PostgresQueryRunner now preserves type casts in tableColumn.default (e.g. ::jsonb), but
PostgresDriver.defaultEqual's json/jsonb branch still JSON.parse()s by stripping only the first/last
character, assuming the value ends with a quote. If the DB returns a casted JSON default, schema
diff/sync can throw at runtime while comparing defaults.
Code

src/driver/postgres/PostgresQueryRunner.ts[R3997-4001]

+                                    tableColumn.default =
+                                        dbColumn["column_default"]
                            tableColumn.default =
                                tableColumn.default.replace(
                                    /^(-?\d+)$/,
Evidence
The PR change stops removing ::type casts from dbColumn["column_default"] when populating
tableColumn.default. Separately, PostgresDriver.defaultEqual has a special json/jsonb path that
parses tableColumn.default by taking substring(1, len-1), which only works when the string literally
ends with a closing single-quote. The query-runner code itself demonstrates that information_schema
defaults can contain casts (e.g. "'now'::text"), so casted defaults are expected in practice; with
casts preserved, the JSON.parse path becomes unsafe. This comparison runs during synchronize() which
is used broadly in tests and in schema sync flows.

src/driver/postgres/PostgresQueryRunner.ts[3988-4004]
src/driver/postgres/PostgresDriver.ts[1221-1233]
test/utils/test-utils.ts[504-508]
test/functional/json/basic-jsonb/entity/Record.ts[17-25]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PostgresQueryRunner` now assigns `tableColumn.default` directly from `information_schema.columns.column_default` without stripping `::type` casts, but `PostgresDriver.defaultEqual()`&amp;amp;amp;amp;amp;amp;amp;amp;#x27;s JSON/JSONB branch still assumes `tableColumn.default` ends with a `&amp;amp;amp;amp;amp;amp;amp;amp;#x27;` and uses `substring(1, len-1)` before `JSON.parse()`. If the default includes a cast suffix (e.g. `...&amp;amp;amp;amp;amp;amp;amp;amp;#x27;::jsonb`), parsing will fail.
### Issue Context
- The query runner already expects casts in defaults (e.g. it checks for `&amp;amp;amp;amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;amp;amp;#x27;now&amp;amp;amp;amp;amp;amp;amp;amp;#x27;::text&amp;amp;amp;amp;amp;amp;amp;amp;quot;`), so casted defaults are realistic.
- JSONB defaults are used in the test suite and schema synchronization flows.
### Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[1221-1239]
- src/driver/postgres/PostgresQueryRunner.ts[3988-4004]
### Suggested approach
1. Add a small helper to strip `::type` casts **only outside single-quoted literals** (or at minimum strip only a trailing cast suffix).
2. In the JSON/JSONB branch, apply that helper to `tableColumn.default` before attempting `substring`/`JSON.parse`.
3. Add/extend a functional test covering a JSONB column with an object default and verifying schema diff does not throw.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
2. Empty @param JSDoc tags 📘 Rule violation ✓ Correctness
Description
Multiple new JSDoc @param tags were added without any descriptions, which adds comment noise and
is stylistically inconsistent. This may reduce readability and violate the project’s expectation to
avoid AI-like filler comments.
Code

src/driver/postgres/PostgresDriver.ts[R715-718]

/**
* Creates a query runner used to execute database queries.
+     * @param mode
*/
Evidence
Compliance ID 4 requires avoiding AI-generated noise and extra comments; the added JSDoc @param
lines provide no descriptive value and appear to be filler. The cited lines show an added @param
tag with no description.

Rule 4: Remove AI-generated noise
src/driver/postgres/PostgresDriver.ts[715-718]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New JSDoc `@param` tags were added without any descriptive text (e.g., `* @param mode`). This is comment noise and can be interpreted as AI-generated filler.
## Issue Context
The compliance checklist item &amp;amp;amp;amp;amp;amp;amp;amp;quot;Remove AI-generated noise&amp;amp;amp;amp;amp;amp;amp;amp;quot; requires avoiding extra comments and style inconsistencies.
## Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[715-727]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Cast stripping corrupts literals 🐞 Bug ✓ Correctness
Description
defaultEqual strips /::.../g across the entire default expression without respecting single-quoted
string literals. If a string literal itself contains "::" (e.g. IPv6/text tokens), the comparison
can remove part of the literal and treat different defaults as equal, skipping required schema
changes.
Code

src/driver/postgres/PostgresDriver.ts[R1247-1253]

+        if (
+            columnDefault &&
+            tableColumn.default &&
+            columnDefault.replace(/::[\w\s.[\]\-"]+/g, "") ===
+                tableColumn.default.replace(/::[\w\s.[\]\-"]+/g, "")
+        ) {
+            return true
Evidence
normalizeDefault wraps string defaults in single quotes, so user data can legitimately include "::"
inside a quoted literal. The new comparison removes ::... anywhere in the string, including inside
those literals, because it does not split by quotes. The codebase already has a pattern
(lowerDefaultValueIfNecessary) that avoids mutating quoted segments by splitting on ',
highlighting that this comparison should likely be quote-aware too.

src/driver/postgres/PostgresDriver.ts[1184-1192]
src/driver/postgres/PostgresDriver.ts[1245-1254]
src/driver/postgres/PostgresDriver.ts[1564-1575]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new default comparison removes `::type` patterns using a regex across the full SQL default expression. Because it is not quote-aware, it can match `::` inside single-quoted literals and remove user data, potentially making different defaults compare as equal.
### Issue Context
`normalizeDefault()` wraps string/number defaults in single quotes, so defaults can legitimately contain `::` as part of the value. The codebase already demonstrates quote-aware string processing in `lowerDefaultValueIfNecessary()`.
### Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[1184-1192]
- src/driver/postgres/PostgresDriver.ts[1245-1254]
- src/driver/postgres/PostgresDriver.ts[1564-1575]
### Suggested approach
1. Implement a helper like `stripTypeCastsOutsideQuotes(expr: string): string` using the same `split(&amp;amp;amp;amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;amp;amp;#x27;&amp;amp;amp;amp;amp;amp;amp;amp;quot;)` technique to only apply the `::type` removal on even (outside-quote) segments.
2. Use this helper in `defaultEqual()` instead of calling `.replace(/::.../g, &amp;amp;amp;amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;amp;amp;quot;)` directly.
3. Add a functional test with a string column default containing `::` (e.g. `default: &amp;amp;amp;amp;amp;amp;amp;amp;quot;2001:db8::1&amp;amp;amp;amp;amp;amp;amp;amp;quot;`) and verify schema diff behaves correctly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider Grey Divider
Qodo Logo

@qodo-free-for-open-source-projects

Persistent review updated to latest commit 148b87e

@qodo-free-for-open-source-projects

Persistent review updated to latest commit c7961ad

@qodo-free-for-open-source-projects

Persistent review updated to latest commit f24280f

@qodo-free-for-open-source-projects

Persistent review updated to latest commit bbc9ee3

@coveralls
Copy link

coveralls commented Feb 16, 2026

Coverage Status

coverage: 81.17% (+0.01%) from 81.16%
when pulling be0755e on gioboa:fix/1729
into 960a59f on typeorm:master.

@qodo-free-for-open-source-projects

Persistent review updated to latest commit a3728f5

@qodo-free-for-open-source-projects

Persistent review updated to latest commit d543450

@qodo-free-for-open-source-projects

Persistent review updated to latest commit 1b20b27

@qodo-free-for-open-source-projects

Persistent review updated to latest commit dde101a

@qodo-free-for-open-source-projects

Persistent review updated to latest commit 628a4d3

@qodo-free-for-open-source-projects

Persistent review updated to latest commit dde101a

@qodo-free-for-open-source-projects

Persistent review updated to latest commit 486a336

Comment on lines +1255 to +1266
if (columnDefault === tableColumn.default) return true

if (
columnDefault &&
tableColumn.default &&
this.stripTypeCastsOutsideQuotes(columnDefault) ===
this.stripTypeCastsOutsideQuotes(tableColumn.default)
) {
return true
}

return false

Choose a reason for hiding this comment

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

Action required

1. Casts hide real default changes 🐞 Bug ✓ Correctness

defaultEqual now falls back to comparing defaults after stripping all ::type casts outside
quotes on both sides. This can collapse semantically different defaults (e.g., different datetime
function normalizations/types) into the same string and prevent schema builder from detecting a real
default change.
Agent Prompt
### Issue description
`defaultEqual` uses `stripTypeCastsOutsideQuotes` as a fallback equality check. Because the stripper removes *all* `::type` casts (outside quotes) across the whole expression, semantically different defaults that rely on casts/type distinctions can be treated as equal, preventing schema diffs/migrations.

### Issue Context
- `normalizeDatetimeFunction` intentionally returns different expressions for `CURRENT_DATE` vs `LOCALTIMESTAMP`, differing primarily by casts/type.
- The fallback equality removes those casts, making them comparable as identical strings.

### Fix Focus Areas
- src/driver/postgres/PostgresDriver.ts[1251-1267]
- src/driver/postgres/PostgresDriver.ts[1794-1826]
- src/driver/postgres/PostgresDriver.ts[1844-1860]

### Suggested direction
- Consider stripping casts only from `tableColumn.default` (database side), and only those casts that Postgres injects around literals / at the end of the expression (e.g., `'<literal>'::type`, or an outermost trailing cast), instead of stripping every cast everywhere.
- Add a regression test that changes a column default between two different datetime defaults (e.g. CURRENT_DATE vs LOCALTIMESTAMP) and ensure a schema diff is produced.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@gioboa gioboa requested review from alumni and naorpeled February 17, 2026 07:52
@gioboa gioboa requested a review from G0maa February 17, 2026 07:52
@qodo-free-for-open-source-projects

Persistent review updated to latest commit be0755e

Copy link
Collaborator

@alumni alumni left a comment

Choose a reason for hiding this comment

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

We should probably do some things similar in CockroachDB.

Probably in the long term we should find a way to reuse code, so we can benefit from the fixes (maybe we put a halt on the dialect task and think about it a bit better).

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.

Queries generated with function as DEFAULT still generated later on migrations

3 participants