From ff7796de84c8b34e256801a2d3fa9d2feb56837f Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Fri, 17 Oct 2025 02:51:36 +0800 Subject: [PATCH] chore: do not strip type cast --- internal/diff/table.go | 17 +- ir/formatter.go | 139 +++++++++--- ir/normalize.go | 39 ++-- ir/parser.go | 211 ++++++++++-------- .../create_table/add_column_default/diff.sql | 1 + .../create_table/add_column_default/new.sql | 4 +- .../create_table/add_column_default/plan.json | 6 + .../create_table/add_column_default/plan.sql | 2 + .../create_table/add_column_default/plan.txt | 3 + .../diff/create_view/alter_view/plan.json | 2 +- testdata/dump/bytebase/pgschema.sql | 2 +- testdata/dump/employee/pgschema.sql | 2 +- testdata/dump/employee/raw.sql | 2 +- testdata/dump/sakila/pgschema.sql | 2 +- testdata/dump/tenant/pgschema.sql | 2 +- 15 files changed, 262 insertions(+), 172 deletions(-) diff --git a/internal/diff/table.go b/internal/diff/table.go index f8f8fbd8..1be87df7 100644 --- a/internal/diff/table.go +++ b/internal/diff/table.go @@ -2,7 +2,6 @@ package diff import ( "fmt" - "regexp" "sort" "strings" @@ -1104,9 +1103,7 @@ func buildColumnClauses(column *ir.Column, isPartOfAnyPK bool, tableSchema strin defaultValue = strings.ReplaceAll(defaultValue, schemaPrefix, "") } - // Strip type qualifiers from default values - defaultValue = stripTypeQualifiers(defaultValue) - + // Type casts are now preserved (from pg_query.Deparse) for canonical representation parts = append(parts, fmt.Sprintf("DEFAULT %s", defaultValue)) } @@ -1213,18 +1210,6 @@ func formatColumnDataTypeForCreate(column *ir.Column) string { return dataType } -// stripTypeQualifiers removes PostgreSQL type qualifiers from default values -func stripTypeQualifiers(defaultValue string) string { - // Use regex to match any type qualifier pattern (::typename) - // This handles both built-in types and user-defined types like enums - re := regexp.MustCompile(`(.*)::[a-zA-Z_][a-zA-Z0-9_\s]*(\[\])?$`) - matches := re.FindStringSubmatch(defaultValue) - if len(matches) > 1 { - return matches[1] - } - return defaultValue -} - // indexesStructurallyEqual compares two indexes for structural equality // excluding comments and other metadata that don't require index recreation func indexesStructurallyEqual(oldIndex, newIndex *ir.Index) bool { diff --git a/ir/formatter.go b/ir/formatter.go index d3a49382..16bf1ad0 100644 --- a/ir/formatter.go +++ b/ir/formatter.go @@ -464,6 +464,15 @@ func (f *postgreSQLFormatter) formatBoolExpr(boolExpr *pg_query.BoolExpr) { // formatTypeCast formats a type cast expression func (f *postgreSQLFormatter) formatTypeCast(typeCast *pg_query.TypeCast) { + // Check if this is a redundant cast that should be stripped for cleaner output + if f.isRedundantTypeCast(typeCast) { + // Just format the argument without the cast + if typeCast.Arg != nil { + f.formatExpression(typeCast.Arg) + } + return + } + // Special handling for INTERVAL type casts if typeCast.TypeName != nil && len(typeCast.TypeName.Names) > 0 { // Get the type name (last element in the names array) @@ -498,6 +507,101 @@ func (f *postgreSQLFormatter) formatTypeCast(typeCast *pg_query.TypeCast) { } } +// isRedundantTypeCast checks if a type cast is redundant and can be safely removed +// for cleaner view output. This includes: +// - Casts on string literals (e.g., 'value'::text, 'value'::varchar) +// - Casts on NULL (e.g., NULL::numeric) +// - Casts with pg_catalog schema qualifiers on basic types +// - Nested casts (e.g., 'value'::varchar::text) +func (f *postgreSQLFormatter) isRedundantTypeCast(typeCast *pg_query.TypeCast) bool { + if typeCast.Arg == nil || typeCast.TypeName == nil { + return false + } + + // Helper to check if we can find a constant at the bottom of nested casts + var findBaseConstant func(*pg_query.Node) *pg_query.A_Const + findBaseConstant = func(node *pg_query.Node) *pg_query.A_Const { + if aConst := node.GetAConst(); aConst != nil { + return aConst + } + // Check if this is a nested type cast + if nestedCast := node.GetTypeCast(); nestedCast != nil && nestedCast.Arg != nil { + return findBaseConstant(nestedCast.Arg) + } + return nil + } + + // Check if the argument is a constant value (possibly nested in casts) + if aConst := findBaseConstant(typeCast.Arg); aConst != nil { + // Get the type name to check if this is a text-like cast + typeName := "" + if len(typeCast.TypeName.Names) > 0 { + if len(typeCast.TypeName.Names) == 2 { + // Handle pg_catalog.typename + if schema := typeCast.TypeName.Names[0].GetString_(); schema != nil && schema.Sval == "pg_catalog" { + if typ := typeCast.TypeName.Names[1].GetString_(); typ != nil { + typeName = typ.Sval + } + } + } else if len(typeCast.TypeName.Names) == 1 { + if typ := typeCast.TypeName.Names[0].GetString_(); typ != nil { + typeName = typ.Sval + } + } + } + + // String literal casts to text-like types are redundant (e.g., 'text'::text, 'value'::varchar) + // But date/timestamp casts are NOT redundant (e.g., '2020-01-01'::date) + if aConst.GetSval() != nil { + // Only strip casts to text-like types + textLikeTypes := []string{"text", "varchar", "character varying", "char", "character", "bpchar"} + for _, t := range textLikeTypes { + if typeName == t { + return true + } + } + // Keep all other casts (date, timestamp, numeric, etc.) + return false + } + + // NULL casts are redundant (e.g., NULL::numeric → NULL) + if aConst.Isnull { + return true + } + } + + // Check if this is a redundant column cast (e.g., column::text where column is already text) + // For view formatting, we strip casts on column references to basic types + if typeCast.Arg.GetColumnRef() != nil && typeCast.TypeName != nil { + // Get the type name + if len(typeCast.TypeName.Names) > 0 { + typeName := "" + // Check if it's a pg_catalog qualified type + if len(typeCast.TypeName.Names) == 2 { + if schema := typeCast.TypeName.Names[0].GetString_(); schema != nil && schema.Sval == "pg_catalog" { + if typ := typeCast.TypeName.Names[1].GetString_(); typ != nil { + typeName = typ.Sval + } + } + } else if len(typeCast.TypeName.Names) == 1 { + if typ := typeCast.TypeName.Names[0].GetString_(); typ != nil { + typeName = typ.Sval + } + } + + // Common text-like types that are often redundantly cast + textLikeTypes := []string{"text", "varchar", "character varying", "char", "character", "bpchar"} + for _, t := range textLikeTypes { + if typeName == t { + return true + } + } + } + } + + return false +} + // formatTypeName formats a type name func (f *postgreSQLFormatter) formatTypeName(typeName *pg_query.TypeName) { for i, nameNode := range typeName.Names { @@ -558,14 +662,15 @@ func (f *postgreSQLFormatter) formatCaseExpr(caseExpr *pg_query.CaseExpr) { f.buffer.WriteString(" WHEN ") f.formatExpression(when.Expr) f.buffer.WriteString(" THEN ") - f.formatExpressionStripCast(when.Result) + // Format result expression - redundant type casts will be stripped by formatTypeCast + f.formatExpression(when.Result) } } - // Format ELSE clause, stripping unnecessary type casts from constants/NULL + // Format ELSE clause - redundant type casts will be stripped by formatTypeCast if caseExpr.Defresult != nil { f.buffer.WriteString(" ELSE ") - f.formatExpressionStripCast(caseExpr.Defresult) + f.formatExpression(caseExpr.Defresult) } f.buffer.WriteString(" END") @@ -647,26 +752,6 @@ func (f *postgreSQLFormatter) formatNullTest(nullTest *pg_query.NullTest) { } } -// formatExpressionStripCast formats an expression, stripping unnecessary type casts from constants and NULL -func (f *postgreSQLFormatter) formatExpressionStripCast(expr *pg_query.Node) { - // If this is a TypeCast of a constant or NULL, format just the value without the cast - if typeCast := expr.GetTypeCast(); typeCast != nil { - if typeCast.Arg != nil { - if aConst := typeCast.Arg.GetAConst(); aConst != nil { - // This is a typed constant, format just the constant value - f.formatAConst(aConst) - return - } - // For non-constant args, recursively strip casts - f.formatExpressionStripCast(typeCast.Arg) - return - } - } - - // Otherwise, format normally - f.formatExpression(expr) -} - // formatAArrayExpr formats array expressions (ARRAY[...]) func (f *postgreSQLFormatter) formatAArrayExpr(arrayExpr *pg_query.A_ArrayExpr) { f.buffer.WriteString("ARRAY[") @@ -682,17 +767,17 @@ func (f *postgreSQLFormatter) formatAArrayExpr(arrayExpr *pg_query.A_ArrayExpr) // formatArrayAsIN is a helper to format "column IN (values)" syntax // Used by both formatAExpr and formatScalarArrayOpExpr to convert "= ANY(ARRAY[...])" to "IN (...)" func (f *postgreSQLFormatter) formatArrayAsIN(leftExpr *pg_query.Node, arrayElements []*pg_query.Node) { - // Format left side (the column/expression) - f.formatExpressionStripCast(leftExpr) + // Format left side (the column/expression) - preserves type casts for canonical representation + f.formatExpression(leftExpr) f.buffer.WriteString(" IN (") - // Format array elements as comma-separated list, stripping unnecessary type casts + // Format array elements as comma-separated list - preserves type casts for canonical representation for i, elem := range arrayElements { if i > 0 { f.buffer.WriteString(", ") } - f.formatExpressionStripCast(elem) + f.formatExpression(elem) } f.buffer.WriteString(")") diff --git a/ir/normalize.go b/ir/normalize.go index 0f869b1a..d5552e5b 100644 --- a/ir/normalize.go +++ b/ir/normalize.go @@ -112,28 +112,21 @@ func normalizeDefaultValue(value string) string { } // Handle type casting - remove explicit type casts that are semantically equivalent - // Pattern: ''::text -> '' - // Pattern: '{}'::jsonb -> '{}' + // Use regex to properly handle type casts within complex expressions + // Pattern: 'literal'::type -> 'literal' (removes redundant casts from string literals) if strings.Contains(value, "::") { - // Find the cast and remove it for simple literal values - if strings.HasPrefix(value, "'") { - if idx := strings.Index(value, "'::"); idx != -1 { - // Find the closing quote - if closeIdx := strings.Index(value[1:], "'"); closeIdx != -1 { - literal := value[:closeIdx+2] // Include the closing quote - if literal == "''" || literal == "'{}'" { - value = literal - } - } - } - } - // Pattern: 'G'::schema.type_name -> 'G' - // Pattern: 'G'::type_name -> 'G' - if strings.Contains(value, "'::") { - if idx := strings.Index(value, "'::"); idx != -1 { - value = value[:idx+1] - } - } + // Use regex to match and remove type casts from string literals + // This handles: 'text'::text, 'utc'::text, '{}'::jsonb, '{}'::text[], etc. + // Also handles multi-word types like 'value'::character varying + // Pattern explanation: + // '([^']*)' - matches a quoted string literal (capturing the content) + // ::[a-zA-Z_][\w\s.]* - matches ::typename + // [a-zA-Z_] - type name must start with letter or underscore + // [\w\s.]* - followed by word chars, spaces, or dots (for "character varying" or "pg_catalog.text") + // (?:\[\])? - optionally followed by [] for array types (non-capturing group) + // (?:\b|(?=\[)|$) - followed by word boundary, opening bracket, or end of string + re := regexp.MustCompile(`'([^']*)'::(?:[a-zA-Z_][\w\s.]*)(?:\[\])?`) + value = re.ReplaceAllString(value, "'$1'") } return value @@ -891,7 +884,7 @@ func normalizeCheckClause(checkClause string) string { func normalizeExpressionWithPgQuery(expr string) string { // Create a dummy SELECT statement with the expression to parse it dummySQL := fmt.Sprintf("SELECT %s", expr) - + parseResult, err := pg_query.Parse(dummySQL) if err != nil { // If parsing fails, return empty string to trigger fallback @@ -935,7 +928,7 @@ func removeRedundantNumericCasts(expr string) string { re := regexp.MustCompile(pattern) result = re.ReplaceAllString(result, "$1") } - + return result } diff --git a/ir/parser.go b/ir/parser.go index 171a26e6..ddd3f452 100644 --- a/ir/parser.go +++ b/ir/parser.go @@ -812,22 +812,73 @@ func (p *Parser) deparseExpr(expr *pg_query.Node) string { return "" } - // Create a minimal statement to deparse the expression + // Wrap the expression in a SELECT statement to make it deparsable + // SELECT allows us to deparse any expression node + selectStmt := &pg_query.SelectStmt{ + TargetList: []*pg_query.Node{ + { + Node: &pg_query.Node_ResTarget{ + ResTarget: &pg_query.ResTarget{ + Val: expr, + }, + }, + }, + }, + } + stmt := &pg_query.RawStmt{ - Stmt: expr, + Stmt: &pg_query.Node{ + Node: &pg_query.Node_SelectStmt{ + SelectStmt: selectStmt, + }, + }, } + parseResult := &pg_query.ParseResult{ Stmts: []*pg_query.RawStmt{stmt}, } // Use pg_query's Deparse function if deparseResult, err := pg_query.Deparse(parseResult); err == nil { - return strings.TrimSpace(deparseResult) + // Extract just the expression part from "SELECT ;" + result := strings.TrimSpace(deparseResult) + result = strings.TrimPrefix(result, "SELECT") + result = strings.TrimSpace(result) + result = strings.TrimSuffix(result, ";") + return strings.TrimSpace(result) } return "" } +// uppercasePostgreSQLKeywords converts lowercase PostgreSQL keywords to uppercase +// to match the canonical format returned by pg_get_expr and other PostgreSQL functions. +// This is needed because pg_query.Deparse returns lowercase keywords. +func uppercasePostgreSQLKeywords(sql string) string { + // List of PostgreSQL keywords that should be uppercase + keywords := []string{ + "CURRENT_TIMESTAMP", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_USER", + "SESSION_USER", + "LOCALTIME", + "LOCALTIMESTAMP", + "NULL", + } + + result := sql + for _, keyword := range keywords { + // Use word boundary regex to avoid replacing keywords that are part of identifiers + // For example, avoid replacing "current_user" in "current_user_id" + lowercase := strings.ToLower(keyword) + pattern := regexp.MustCompile(`\b` + regexp.QuoteMeta(lowercase) + `\b`) + result = pattern.ReplaceAllString(result, keyword) + } + + return result +} + // parseInlineForeignKey parses an inline foreign key constraint from a column definition func (p *Parser) parseInlineForeignKey(constraint *pg_query.Constraint, columnName, schemaName, tableName string) *Constraint { // Generate constraint name (PostgreSQL convention: table_column_fkey) @@ -997,110 +1048,72 @@ func (p *Parser) extractTypeModifiers(typmods []*pg_query.Node) []int { return mods } -// extractDefaultValue extracts default value from expression +// extractDefaultValue extracts the default value expression from a pg_query node. +// Uses pg_query's Deparse to get PostgreSQL's canonical representation with type casts preserved. +// This ensures perfect round-trip consistency and handles all edge cases correctly. func (p *Parser) extractDefaultValue(expr *pg_query.Node) string { if expr == nil { return "" } - switch e := expr.Node.(type) { - case *pg_query.Node_AConst: - if e.AConst.Isnull { - return "NULL" - } - if e.AConst.Val != nil { - switch val := e.AConst.Val.(type) { - case *pg_query.A_Const_Sval: - return "'" + val.Sval.Sval + "'" - case *pg_query.A_Const_Ival: - return strconv.FormatInt(int64(val.Ival.Ival), 10) - case *pg_query.A_Const_Fval: - return val.Fval.Fval - case *pg_query.A_Const_Boolval: - if val.Boolval.Boolval { - return "true" - } - return "false" - case *pg_query.A_Const_Bsval: - return "B'" + val.Bsval.Bsval + "'" - } - } - case *pg_query.Node_FuncCall: - // Handle function calls like nextval() and schema-qualified functions - if len(e.FuncCall.Funcname) > 0 { - // Build full function name (handle schema.function) - var funcParts []string - for _, part := range e.FuncCall.Funcname { - if str := part.GetString_(); str != nil { - funcParts = append(funcParts, str.Sval) - } - } - funcName := strings.Join(funcParts, ".") - if len(e.FuncCall.Args) > 0 { - // Extract first argument (usually sequence name) - if arg := e.FuncCall.Args[0]; arg != nil { - if aConst := arg.GetAConst(); aConst != nil { - if strVal := aConst.GetSval(); strVal != nil { - return fmt.Sprintf("%s('%s'::regclass)", funcName, strVal.Sval) - } - } - } - } - return funcName + "()" - } - case *pg_query.Node_TypeCast: - // Handle type casts like CURRENT_TIMESTAMP - if e.TypeCast.Arg != nil { - return p.extractDefaultValue(e.TypeCast.Arg) - } - case *pg_query.Node_ColumnRef: - // Handle column references like CURRENT_TIMESTAMP, CURRENT_USER - if len(e.ColumnRef.Fields) > 0 { - if field := e.ColumnRef.Fields[0]; field != nil { - if str := field.GetString_(); str != nil { - return str.Sval - } - } - } - case *pg_query.Node_SqlvalueFunction: - // Handle SQL value functions based on their operation type - switch e.SqlvalueFunction.Op { - case pg_query.SQLValueFunctionOp_SVFOP_CURRENT_DATE: - return "CURRENT_DATE" - case pg_query.SQLValueFunctionOp_SVFOP_CURRENT_TIME: - return "CURRENT_TIME" - case pg_query.SQLValueFunctionOp_SVFOP_CURRENT_TIME_N: - return "CURRENT_TIME" - case pg_query.SQLValueFunctionOp_SVFOP_CURRENT_TIMESTAMP: - return "CURRENT_TIMESTAMP" - case pg_query.SQLValueFunctionOp_SVFOP_CURRENT_TIMESTAMP_N: - return "CURRENT_TIMESTAMP" - case pg_query.SQLValueFunctionOp_SVFOP_LOCALTIME: - return "LOCALTIME" - case pg_query.SQLValueFunctionOp_SVFOP_LOCALTIME_N: - return "LOCALTIME" - case pg_query.SQLValueFunctionOp_SVFOP_LOCALTIMESTAMP: - return "LOCALTIMESTAMP" - case pg_query.SQLValueFunctionOp_SVFOP_LOCALTIMESTAMP_N: - return "LOCALTIMESTAMP" - case pg_query.SQLValueFunctionOp_SVFOP_CURRENT_ROLE: - return "CURRENT_ROLE" - case pg_query.SQLValueFunctionOp_SVFOP_CURRENT_USER: - return "CURRENT_USER" - case pg_query.SQLValueFunctionOp_SVFOP_USER: - return "USER" - case pg_query.SQLValueFunctionOp_SVFOP_SESSION_USER: - return "SESSION_USER" - case pg_query.SQLValueFunctionOp_SVFOP_CURRENT_CATALOG: - return "CURRENT_CATALOG" - case pg_query.SQLValueFunctionOp_SVFOP_CURRENT_SCHEMA: - return "CURRENT_SCHEMA" - default: - return "CURRENT_TIMESTAMP" // fallback for unknown + // Use pg_query's Deparse - it handles ALL cases correctly including: + // - Type casts ('value'::type) + // - Arrays, jsonb, enums + // - Schema-qualified types + // - Complex expressions + result := p.deparseExpr(expr) + + // Uppercase PostgreSQL keywords for default values + // This is needed because pg_query.Deparse returns lowercase keywords + // but PostgreSQL's pg_get_expr returns uppercase + result = uppercasePostgreSQLKeywords(result) + + // Wrap in parentheses if the expression contains operators that require them in DEFAULT clause + // pg_query.Deparse strips outer parentheses, but PostgreSQL requires them for operator expressions + // Examples: (now() AT TIME ZONE 'utc'), (1 + 2), ('a' || 'b') + if needsParenthesesInDefault(result) { + result = "(" + result + ")" + } + + return result +} + +// needsParenthesesInDefault checks if a default value expression needs parentheses +// PostgreSQL requires parentheses around operator expressions in DEFAULT clauses +func needsParenthesesInDefault(expr string) bool { + upperExpr := strings.ToUpper(expr) + + // Operators that require parentheses in DEFAULT clause + operators := []string{ + " AT TIME ZONE ", + " + ", + " - ", + " * ", + " / ", + " % ", + " ^ ", + " || ", + " AND ", + " OR ", + " NOT ", + " IS ", + " BETWEEN ", + " LIKE ", + " ILIKE ", + " SIMILAR TO ", + " ~ ", + " !~ ", + " ~* ", + " !~* ", + } + + for _, op := range operators { + if strings.Contains(upperExpr, op) { + return true } } - return "" + return false } // extractGeneratedExpression extracts the expression from a generated column constraint diff --git a/testdata/diff/create_table/add_column_default/diff.sql b/testdata/diff/create_table/add_column_default/diff.sql index 89fc9dcc..99f72513 100644 --- a/testdata/diff/create_table/add_column_default/diff.sql +++ b/testdata/diff/create_table/add_column_default/diff.sql @@ -6,3 +6,4 @@ ALTER TABLE events ADD COLUMN created_at timestamp DEFAULT CURRENT_TIMESTAMP; ALTER TABLE events ADD COLUMN updated_at timestamp DEFAULT now(); ALTER TABLE events ADD COLUMN config jsonb DEFAULT '{}'; ALTER TABLE events ADD COLUMN tags text[] DEFAULT '{}'; +ALTER TABLE events ADD COLUMN created_at_utc timestamp DEFAULT (now() AT TIME ZONE 'utc') NOT NULL; diff --git a/testdata/diff/create_table/add_column_default/new.sql b/testdata/diff/create_table/add_column_default/new.sql index d0b7623d..bc9c9c68 100644 --- a/testdata/diff/create_table/add_column_default/new.sql +++ b/testdata/diff/create_table/add_column_default/new.sql @@ -13,5 +13,7 @@ CREATE TABLE public.events ( updated_at timestamp without time zone DEFAULT now(), -- Type cast default config jsonb DEFAULT '{}'::jsonb, - tags text[] DEFAULT '{}'::text[] + tags text[] DEFAULT '{}'::text[], + -- Complex expression with parentheses and AT TIME ZONE (issue #91) + created_at_utc timestamp without time zone DEFAULT (now() AT TIME ZONE 'utc') NOT NULL ); diff --git a/testdata/diff/create_table/add_column_default/plan.json b/testdata/diff/create_table/add_column_default/plan.json index 80a9e6e5..9af99e63 100644 --- a/testdata/diff/create_table/add_column_default/plan.json +++ b/testdata/diff/create_table/add_column_default/plan.json @@ -55,6 +55,12 @@ "type": "table.column", "operation": "create", "path": "public.events.tags" + }, + { + "sql": "ALTER TABLE events ADD COLUMN created_at_utc timestamp DEFAULT (now() AT TIME ZONE 'utc') NOT NULL;", + "type": "table.column", + "operation": "create", + "path": "public.events.created_at_utc" } ] } diff --git a/testdata/diff/create_table/add_column_default/plan.sql b/testdata/diff/create_table/add_column_default/plan.sql index d26fde84..e606b75b 100644 --- a/testdata/diff/create_table/add_column_default/plan.sql +++ b/testdata/diff/create_table/add_column_default/plan.sql @@ -13,3 +13,5 @@ ALTER TABLE events ADD COLUMN updated_at timestamp DEFAULT now(); ALTER TABLE events ADD COLUMN config jsonb DEFAULT '{}'; ALTER TABLE events ADD COLUMN tags text[] DEFAULT '{}'; + +ALTER TABLE events ADD COLUMN created_at_utc timestamp DEFAULT (now() AT TIME ZONE 'utc') NOT NULL; diff --git a/testdata/diff/create_table/add_column_default/plan.txt b/testdata/diff/create_table/add_column_default/plan.txt index 83ba76c7..2c8836d1 100644 --- a/testdata/diff/create_table/add_column_default/plan.txt +++ b/testdata/diff/create_table/add_column_default/plan.txt @@ -7,6 +7,7 @@ Tables: ~ events + config (column) + created_at (column) + + created_at_utc (column) + is_active (column) + priority (column) + score (column) @@ -32,3 +33,5 @@ ALTER TABLE events ADD COLUMN updated_at timestamp DEFAULT now(); ALTER TABLE events ADD COLUMN config jsonb DEFAULT '{}'; ALTER TABLE events ADD COLUMN tags text[] DEFAULT '{}'; + +ALTER TABLE events ADD COLUMN created_at_utc timestamp DEFAULT (now() AT TIME ZONE 'utc') NOT NULL; diff --git a/testdata/diff/create_view/alter_view/plan.json b/testdata/diff/create_view/alter_view/plan.json index 3685413c..8a8e632c 100644 --- a/testdata/diff/create_view/alter_view/plan.json +++ b/testdata/diff/create_view/alter_view/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.4.0", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "ac468edfbfd39b907f6e6304a4e67cf244d852ad990177f4b4dd316249f6269c" + "hash": "db68d37f10b08d2025dcc5a091e5611c9ee22205efa0ff3d28c99d4ffa399701" }, "groups": [ { diff --git a/testdata/dump/bytebase/pgschema.sql b/testdata/dump/bytebase/pgschema.sql index ff2ed787..d22c8fe8 100644 --- a/testdata/dump/bytebase/pgschema.sql +++ b/testdata/dump/bytebase/pgschema.sql @@ -3,7 +3,7 @@ -- -- Dumped from database version PostgreSQL 17.5 --- Dumped by pgschema version 1.3.0 +-- Dumped by pgschema version 1.4.0 -- diff --git a/testdata/dump/employee/pgschema.sql b/testdata/dump/employee/pgschema.sql index a13833cc..69a7b5f7 100644 --- a/testdata/dump/employee/pgschema.sql +++ b/testdata/dump/employee/pgschema.sql @@ -3,7 +3,7 @@ -- -- Dumped from database version PostgreSQL 17.5 --- Dumped by pgschema version 1.3.0 +-- Dumped by pgschema version 1.4.0 -- diff --git a/testdata/dump/employee/raw.sql b/testdata/dump/employee/raw.sql index 3c148080..83a4a293 100644 --- a/testdata/dump/employee/raw.sql +++ b/testdata/dump/employee/raw.sql @@ -84,7 +84,7 @@ ALTER TABLE audit ENABLE ROW LEVEL SECURITY; CREATE POLICY audit_user_isolation ON audit FOR ALL TO PUBLIC - USING (user_name = current_user); + USING (user_name = CURRENT_USER); -- Policy: Allow audit system to insert records (bypass RLS for service accounts) CREATE POLICY audit_insert_system ON audit diff --git a/testdata/dump/sakila/pgschema.sql b/testdata/dump/sakila/pgschema.sql index d8a0fcb0..d0ba9a1d 100644 --- a/testdata/dump/sakila/pgschema.sql +++ b/testdata/dump/sakila/pgschema.sql @@ -3,7 +3,7 @@ -- -- Dumped from database version PostgreSQL 17.5 --- Dumped by pgschema version 1.3.0 +-- Dumped by pgschema version 1.4.0 -- diff --git a/testdata/dump/tenant/pgschema.sql b/testdata/dump/tenant/pgschema.sql index afce6fe3..09a92de1 100644 --- a/testdata/dump/tenant/pgschema.sql +++ b/testdata/dump/tenant/pgschema.sql @@ -3,7 +3,7 @@ -- -- Dumped from database version PostgreSQL 17.5 --- Dumped by pgschema version 1.3.0 +-- Dumped by pgschema version 1.4.0 --