Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e828d48798 | |||
| 6e470a9239 | |||
| 096815fe49 | |||
| b8f60203cb | |||
|
|
15763f60cc |
@@ -1,6 +1,6 @@
|
|||||||
# Maintainer: Hein (Warky Devs) <hein@warky.dev>
|
# Maintainer: Hein (Warky Devs) <hein@warky.dev>
|
||||||
pkgname=relspec
|
pkgname=relspec
|
||||||
pkgver=1.0.50
|
pkgver=1.0.52
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs."
|
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')
|
arch=('x86_64' 'aarch64')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: relspec
|
Name: relspec
|
||||||
Version: 1.0.50
|
Version: 1.0.52
|
||||||
Release: 1%{?dist}
|
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.
|
Summary: RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs.
|
||||||
|
|
||||||
|
|||||||
@@ -311,10 +311,11 @@ func (tm *TypeMapper) BuildBunTag(column *models.Column, table *models.Table) st
|
|||||||
if column.Type != "" {
|
if column.Type != "" {
|
||||||
// Sanitize type to remove backticks
|
// Sanitize type to remove backticks
|
||||||
typeStr := writers.SanitizeStructTagValue(column.Type)
|
typeStr := writers.SanitizeStructTagValue(column.Type)
|
||||||
|
isArray := pgsql.IsArrayType(typeStr)
|
||||||
hasExplicitTypeModifier := pgsql.HasExplicitTypeModifier(typeStr)
|
hasExplicitTypeModifier := pgsql.HasExplicitTypeModifier(typeStr)
|
||||||
if !hasExplicitTypeModifier && column.Length > 0 {
|
if !hasExplicitTypeModifier && !isArray && column.Length > 0 {
|
||||||
typeStr = fmt.Sprintf("%s(%d)", typeStr, column.Length)
|
typeStr = fmt.Sprintf("%s(%d)", typeStr, column.Length)
|
||||||
} else if !hasExplicitTypeModifier && column.Precision > 0 {
|
} else if !hasExplicitTypeModifier && !isArray && column.Precision > 0 {
|
||||||
if column.Scale > 0 {
|
if column.Scale > 0 {
|
||||||
typeStr = fmt.Sprintf("%s(%d,%d)", typeStr, column.Precision, column.Scale)
|
typeStr = fmt.Sprintf("%s(%d,%d)", typeStr, column.Precision, column.Scale)
|
||||||
} else {
|
} else {
|
||||||
@@ -322,6 +323,9 @@ func (tm *TypeMapper) BuildBunTag(column *models.Column, table *models.Table) st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
parts = append(parts, fmt.Sprintf("type:%s", typeStr))
|
parts = append(parts, fmt.Sprintf("type:%s", typeStr))
|
||||||
|
if isArray && tm.typeStyle == writers.NullableTypeStdlib {
|
||||||
|
parts = append(parts, "array")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Primary key
|
// Primary key
|
||||||
|
|||||||
@@ -574,6 +574,10 @@ func TestTypeMapper_SQLTypeToGoType_Bun(t *testing.T) {
|
|||||||
{"boolean", false, "resolvespec_common.SqlBool"},
|
{"boolean", false, "resolvespec_common.SqlBool"},
|
||||||
{"uuid", false, "resolvespec_common.SqlUUID"},
|
{"uuid", false, "resolvespec_common.SqlUUID"},
|
||||||
{"jsonb", false, "resolvespec_common.SqlJSONB"},
|
{"jsonb", false, "resolvespec_common.SqlJSONB"},
|
||||||
|
{"text[]", true, "resolvespec_common.SqlStringArray"},
|
||||||
|
{"text[]", false, "resolvespec_common.SqlStringArray"},
|
||||||
|
{"integer[]", true, "resolvespec_common.SqlInt32Array"},
|
||||||
|
{"bigint[]", false, "resolvespec_common.SqlInt64Array"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -685,6 +689,24 @@ func TestTypeMapper_BuildBunTag(t *testing.T) {
|
|||||||
},
|
},
|
||||||
want: []string{"id,", "type:bigserial,", "pk,", "autoincrement,"},
|
want: []string{"id,", "type:bigserial,", "pk,", "autoincrement,"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "text array type",
|
||||||
|
column: &models.Column{
|
||||||
|
Name: "tags",
|
||||||
|
Type: "text[]",
|
||||||
|
NotNull: false,
|
||||||
|
},
|
||||||
|
want: []string{"tags,", "type:text[],"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "integer array type",
|
||||||
|
column: &models.Column{
|
||||||
|
Name: "scores",
|
||||||
|
Type: "integer[]",
|
||||||
|
NotNull: true,
|
||||||
|
},
|
||||||
|
want: []string{"scores,", "type:integer[],"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -695,6 +717,30 @@ func TestTypeMapper_BuildBunTag(t *testing.T) {
|
|||||||
t.Errorf("BuildBunTag() = %q, missing %q", result, part)
|
t.Errorf("BuildBunTag() = %q, missing %q", result, part)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// resolvespec mode must NOT add "array" — SqlXxxArray uses sql.Scanner
|
||||||
|
if strings.Contains(result, ",array,") || strings.HasSuffix(result, ",array,") {
|
||||||
|
t.Errorf("BuildBunTag() = %q, must not contain 'array' in resolvespec mode", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypeMapper_BuildBunTag_StdlibArrayHasArrayTag(t *testing.T) {
|
||||||
|
mapper := NewTypeMapper(writers.NullableTypeStdlib)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
column *models.Column
|
||||||
|
}{
|
||||||
|
{name: "text array", column: &models.Column{Name: "tags", Type: "text[]"}},
|
||||||
|
{name: "integer array", column: &models.Column{Name: "scores", Type: "integer[]", NotNull: true}},
|
||||||
|
}
|
||||||
|
for _, tt := range cases {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := mapper.BuildBunTag(tt.column, nil)
|
||||||
|
if !strings.Contains(result, "array") {
|
||||||
|
t.Errorf("BuildBunTag() = %q, expected 'array' in stdlib mode", result)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -658,6 +658,10 @@ func TestTypeMapper_SQLTypeToGoType(t *testing.T) {
|
|||||||
{"timestamp", false, "sql_types.SqlTimeStamp"},
|
{"timestamp", false, "sql_types.SqlTimeStamp"},
|
||||||
{"boolean", true, "bool"},
|
{"boolean", true, "bool"},
|
||||||
{"boolean", false, "sql_types.SqlBool"},
|
{"boolean", false, "sql_types.SqlBool"},
|
||||||
|
{"text[]", true, "sql_types.SqlStringArray"},
|
||||||
|
{"text[]", false, "sql_types.SqlStringArray"},
|
||||||
|
{"integer[]", true, "sql_types.SqlInt32Array"},
|
||||||
|
{"bigint[]", false, "sql_types.SqlInt64Array"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -670,6 +674,21 @@ func TestTypeMapper_SQLTypeToGoType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTypeMapper_BuildGormTag_ArrayType(t *testing.T) {
|
||||||
|
mapper := NewTypeMapper("")
|
||||||
|
|
||||||
|
col := &models.Column{
|
||||||
|
Name: "tags",
|
||||||
|
Type: "text[]",
|
||||||
|
NotNull: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := mapper.BuildGormTag(col, nil)
|
||||||
|
if !strings.Contains(tag, "type:text[]") {
|
||||||
|
t.Fatalf("expected array type to be preserved, got %q", tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTypeMapper_BuildGormTag_PreservesExplicitTypeModifiers(t *testing.T) {
|
func TestTypeMapper_BuildGormTag_PreservesExplicitTypeModifiers(t *testing.T) {
|
||||||
mapper := NewTypeMapper("")
|
mapper := NewTypeMapper("")
|
||||||
|
|
||||||
|
|||||||
@@ -1251,6 +1251,9 @@ func isIntegerType(colType string) bool {
|
|||||||
func isTextType(colType string) bool {
|
func isTextType(colType string) bool {
|
||||||
textTypes := []string{"text", "varchar", "character varying", "char", "character", "string"}
|
textTypes := []string{"text", "varchar", "character varying", "char", "character", "string"}
|
||||||
lowerType := strings.ToLower(colType)
|
lowerType := strings.ToLower(colType)
|
||||||
|
if strings.HasSuffix(lowerType, "[]") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
for _, t := range textTypes {
|
for _, t := range textTypes {
|
||||||
if strings.HasPrefix(lowerType, t) {
|
if strings.HasPrefix(lowerType, t) {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -87,6 +87,43 @@ func TestWriteDatabase(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWriteDatabase_GinIndexOnTextArrayDoesNotUseTrigramOperatorClass(t *testing.T) {
|
||||||
|
db := models.InitDatabase("testdb")
|
||||||
|
schema := models.InitSchema("public")
|
||||||
|
|
||||||
|
table := models.InitTable("plans", "public")
|
||||||
|
|
||||||
|
tagsCol := models.InitColumn("tags", "plans", "public")
|
||||||
|
tagsCol.Type = "text[]"
|
||||||
|
table.Columns["tags"] = tagsCol
|
||||||
|
|
||||||
|
index := &models.Index{
|
||||||
|
Name: "idx_plans_tags",
|
||||||
|
Type: "gin",
|
||||||
|
Columns: []string{"tags"},
|
||||||
|
}
|
||||||
|
table.Indexes[index.Name] = index
|
||||||
|
|
||||||
|
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, `USING gin (tags)`) {
|
||||||
|
t.Fatalf("expected GIN index on array column without explicit trigram opclass, got:\n%s", output)
|
||||||
|
}
|
||||||
|
if strings.Contains(output, "gin_trgm_ops") {
|
||||||
|
t.Fatalf("did not expect gin_trgm_ops for text[] column, got:\n%s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWriteForeignKeys(t *testing.T) {
|
func TestWriteForeignKeys(t *testing.T) {
|
||||||
// Create a test database with two related tables
|
// Create a test database with two related tables
|
||||||
db := models.InitDatabase("testdb")
|
db := models.InitDatabase("testdb")
|
||||||
|
|||||||
@@ -207,7 +207,8 @@ func quoteSQLLiteral(value string) string {
|
|||||||
// - Returns a clean identifier safe for use in struct tags and field names
|
// - Returns a clean identifier safe for use in struct tags and field names
|
||||||
func SanitizeStructTagValue(value string) string {
|
func SanitizeStructTagValue(value string) string {
|
||||||
// Remove DBML/DCTX style comments in brackets (e.g., [note: 'description'])
|
// Remove DBML/DCTX style comments in brackets (e.g., [note: 'description'])
|
||||||
commentRegex := regexp.MustCompile(`\s*\[.*?\]\s*`)
|
// Require at least one character inside brackets to avoid stripping PostgreSQL array suffix "[]"
|
||||||
|
commentRegex := regexp.MustCompile(`\s*\[[^\]]+\]\s*`)
|
||||||
value = commentRegex.ReplaceAllString(value, "")
|
value = commentRegex.ReplaceAllString(value, "")
|
||||||
|
|
||||||
// Trim whitespace
|
// Trim whitespace
|
||||||
|
|||||||
Reference in New Issue
Block a user