Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae0efdc008 | ||
|
|
be08c8199f | ||
|
|
19b592820c | ||
|
|
b158a98acc |
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Maintainer: Hein (Warky Devs) <hein@warky.dev>
|
||||
pkgname=relspec
|
||||
pkgver=1.0.68
|
||||
pkgver=1.0.70
|
||||
pkgrel=1
|
||||
pkgdesc="RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs."
|
||||
arch=('x86_64' 'aarch64')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: relspec
|
||||
Version: 1.0.68
|
||||
Version: 1.0.70
|
||||
Release: 1%{?dist}
|
||||
Summary: RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs.
|
||||
|
||||
|
||||
+6
-1
@@ -492,7 +492,12 @@ func extractTypeParts(col *models.Column) (baseType string, length, precision, s
|
||||
}
|
||||
}
|
||||
|
||||
typeName = pgsql.NormalizePGType(typeName)
|
||||
// serial/bigserial/smallserial are sugar over an integer column plus a
|
||||
// sequence default; PostgreSQL itself reports the underlying integer
|
||||
// type back for such columns, so treat them as equivalent here to avoid
|
||||
// spurious conflicts between a DBML "bigserial" source and a live-read
|
||||
// "bigint" target (or vice versa).
|
||||
typeName = pgsql.SerialUnderlyingType(typeName)
|
||||
|
||||
return typeName, length, precision, scale
|
||||
}
|
||||
|
||||
@@ -196,6 +196,50 @@ func TestMergeColumns_TypeConflictIsDetected(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeColumns_SerialVsUnderlyingIntegerIsNotAConflict(t *testing.T) {
|
||||
target := &models.Database{
|
||||
Schemas: []*models.Schema{
|
||||
{
|
||||
Name: "public",
|
||||
Tables: []*models.Table{
|
||||
{
|
||||
Name: "users",
|
||||
Schema: "public",
|
||||
Columns: map[string]*models.Column{
|
||||
// As reported back by a live PostgreSQL read of an
|
||||
// existing serial primary key column.
|
||||
"id": {Name: "id", Type: "bigint"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
source := &models.Database{
|
||||
Schemas: []*models.Schema{
|
||||
{
|
||||
Name: "public",
|
||||
Tables: []*models.Table{
|
||||
{
|
||||
Name: "users",
|
||||
Schema: "public",
|
||||
Columns: map[string]*models.Column{
|
||||
// As declared in a DBML source spec.
|
||||
"id": {Name: "id", Type: "bigserial"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := MergeDatabases(target, source, nil)
|
||||
|
||||
if len(result.TypeConflicts) != 0 {
|
||||
t.Fatalf("Expected no type conflicts for bigserial vs bigint, got %d: %+v", len(result.TypeConflicts), result.TypeConflicts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeConstraints_NewConstraint(t *testing.T) {
|
||||
target := &models.Database{
|
||||
Schemas: []*models.Schema{
|
||||
|
||||
@@ -193,6 +193,28 @@ func IsKnownPGBaseType(baseType string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// serialUnderlyingType maps each serial pseudo-type to the integer type
|
||||
// PostgreSQL actually stores the column as. serial/bigserial/smallserial are
|
||||
// not real types: they are sugar for an integer column plus a sequence
|
||||
// default, and pg_catalog (and information_schema) always reports the
|
||||
// underlying integer type back for such columns.
|
||||
var serialUnderlyingType = map[string]string{
|
||||
"serial": "integer",
|
||||
"bigserial": "bigint",
|
||||
"smallserial": "smallint",
|
||||
}
|
||||
|
||||
// SerialUnderlyingType returns the underlying integer type for a serial
|
||||
// pseudo-type (e.g. "bigserial" -> "bigint"). If baseType (after
|
||||
// NormalizePGType) is not a serial type, it is returned unchanged.
|
||||
func SerialUnderlyingType(baseType string) string {
|
||||
normalized := NormalizePGType(baseType)
|
||||
if underlying, ok := serialUnderlyingType[normalized]; ok {
|
||||
return underlying
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func IsGoType(pTypeName string) bool {
|
||||
for k := range GoToStdTypes {
|
||||
if strings.EqualFold(pTypeName, k) {
|
||||
|
||||
@@ -460,7 +460,7 @@ func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, model
|
||||
}
|
||||
|
||||
// Check default value changes
|
||||
if fmt.Sprintf("%v", modelCol.Default) != fmt.Sprintf("%v", currentCol.Default) {
|
||||
if !columnDefaultsEqual(modelCol.Default, currentCol.Default) {
|
||||
setDefault, defaultVal := formatColumnDefaultSQL(modelCol)
|
||||
|
||||
sql, err := w.executor.ExecuteAlterColumnDefaultWithCheck(AlterColumnDefaultWithCheckData{
|
||||
@@ -956,7 +956,24 @@ func columnsEqual(col1, col2 *models.Column) bool {
|
||||
}
|
||||
return columnTypesEqual(col1, col2) &&
|
||||
col1.NotNull == col2.NotNull &&
|
||||
fmt.Sprintf("%v", col1.Default) == fmt.Sprintf("%v", col2.Default)
|
||||
columnDefaultsEqual(col1.Default, col2.Default)
|
||||
}
|
||||
|
||||
// columnDefaultsEqual compares column defaults for drift detection, stripping
|
||||
// MySQL-style backticks (e.g. from GORM tags) so a model default of
|
||||
// "`now()`" is recognised as equal to a live default of "now()".
|
||||
func columnDefaultsEqual(default1, default2 interface{}) bool {
|
||||
return normalizeDefaultForCompare(default1) == normalizeDefaultForCompare(default2)
|
||||
}
|
||||
|
||||
func normalizeDefaultForCompare(value interface{}) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
if s, ok := value.(string); ok {
|
||||
return strings.TrimSpace(stripBackticks(s))
|
||||
}
|
||||
return fmt.Sprintf("%v", value)
|
||||
}
|
||||
|
||||
func columnTypesEqual(col1, col2 *models.Column) bool {
|
||||
|
||||
@@ -530,7 +530,7 @@ func BuildCreateTableData(schemaName string, table *models.Table) CreateTableDat
|
||||
}
|
||||
if col.Default != nil {
|
||||
if value, ok := col.Default.(string); ok {
|
||||
colData.Default = writers.QuoteDefaultValue(value, col.Type)
|
||||
colData.Default = writers.QuoteDefaultValue(stripBackticks(value), col.Type)
|
||||
} else {
|
||||
colData.Default = fmt.Sprintf("%v", col.Default)
|
||||
}
|
||||
|
||||
@@ -512,7 +512,7 @@ func formatColumnDefaultSQL(col *models.Column) (setDefault bool, defaultVal str
|
||||
return false, ""
|
||||
}
|
||||
if value, ok := col.Default.(string); ok {
|
||||
return true, writers.QuoteDefaultValue(value, col.Type)
|
||||
return true, writers.QuoteDefaultValue(stripBackticks(value), col.Type)
|
||||
}
|
||||
return true, fmt.Sprintf("%v", col.Default)
|
||||
}
|
||||
|
||||
@@ -1140,6 +1140,37 @@ func TestWriteSchema_EmitsGuardedAlterColumnDefaultStatements(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSchema_AlterColumnDefaultStripsBackticksFromFunctionExpression(t *testing.T) {
|
||||
db := models.InitDatabase("testdb")
|
||||
schema := models.InitSchema("public")
|
||||
|
||||
table := models.InitTable("agent_skills", "public")
|
||||
|
||||
updatedAtCol := models.InitColumn("updatedat", "agent_skills", "public")
|
||||
updatedAtCol.Type = "timestamp"
|
||||
updatedAtCol.Default = "`now()`"
|
||||
table.Columns["updatedat"] = updatedAtCol
|
||||
|
||||
schema.Tables = append(schema.Tables, table)
|
||||
db.Schemas = append(db.Schemas, schema)
|
||||
|
||||
var buf bytes.Buffer
|
||||
writer := NewWriter(&writers.WriterOptions{})
|
||||
writer.writer = &buf
|
||||
|
||||
if err := writer.WriteDatabase(db); err != nil {
|
||||
t.Fatalf("WriteDatabase failed: %v", err)
|
||||
}
|
||||
|
||||
output := buf.String()
|
||||
if strings.Contains(output, "`") {
|
||||
t.Fatalf("expected no backticks in generated SQL, got:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(output, "ALTER COLUMN updatedat SET DEFAULT now()") {
|
||||
t.Fatalf("expected guarded SET DEFAULT now() without backticks, got:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSchema_GuardedAlterColumnTypeFallsBackOnConversionFailure(t *testing.T) {
|
||||
db := models.InitDatabase("testdb")
|
||||
schema := models.InitSchema("public")
|
||||
|
||||
@@ -148,6 +148,26 @@ func SanitizeFilename(name string) string {
|
||||
// Examples (boolean): "true" → "true"
|
||||
// Examples (bigint): "0" → "0"
|
||||
// Examples (timestamp): "now()" → "now()" (function call – never quoted)
|
||||
// bareKeywordDefaults are PostgreSQL default-value keywords that are
|
||||
// expressions, not string literals, even though they contain no
|
||||
// parentheses (e.g. "CURRENT_DATE" rather than "now()"). They must never be
|
||||
// wrapped in quotes.
|
||||
var bareKeywordDefaults = map[string]bool{
|
||||
"current_date": true,
|
||||
"current_time": true,
|
||||
"current_timestamp": true,
|
||||
"localtime": true,
|
||||
"localtimestamp": true,
|
||||
"current_user": true,
|
||||
"session_user": true,
|
||||
"current_role": true,
|
||||
"current_catalog": true,
|
||||
"current_schema": true,
|
||||
"null": true,
|
||||
"true": true,
|
||||
"false": true,
|
||||
}
|
||||
|
||||
func QuoteDefaultValue(value, sqlType string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
|
||||
@@ -158,6 +178,12 @@ func QuoteDefaultValue(value, sqlType string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
// Bare keyword expressions (e.g. CURRENT_DATE) are never quoted,
|
||||
// regardless of column type.
|
||||
if bareKeywordDefaults[strings.ToLower(value)] {
|
||||
return value
|
||||
}
|
||||
|
||||
// Normalise the SQL type: lowercase, strip length/precision suffix.
|
||||
baseType := strings.ToLower(strings.TrimSpace(sqlType))
|
||||
if idx := strings.Index(baseType, "("); idx > 0 {
|
||||
|
||||
@@ -41,6 +41,24 @@ func TestQuoteDefaultValue(t *testing.T) {
|
||||
sqlType: "timestamptz",
|
||||
want: "now()",
|
||||
},
|
||||
{
|
||||
name: "bare keyword default CURRENT_DATE is not quoted",
|
||||
value: "CURRENT_DATE",
|
||||
sqlType: "date",
|
||||
want: "CURRENT_DATE",
|
||||
},
|
||||
{
|
||||
name: "bare keyword default is case insensitive",
|
||||
value: "current_timestamp",
|
||||
sqlType: "timestamptz",
|
||||
want: "current_timestamp",
|
||||
},
|
||||
{
|
||||
name: "bare keyword default localtime is not quoted",
|
||||
value: "LOCALTIME",
|
||||
sqlType: "time",
|
||||
want: "LOCALTIME",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
Reference in New Issue
Block a user