Compare commits

...
Author SHA1 Message Date
warkanum 1c217b546c Merge pull request 'feat(scripts): support external file embedding' (#12) from issue-6-external-file-embedding into master
Reviewed-on: #12
Reviewed-by: Warky <2+warkanum@noreply@warky.dev>
2026-07-20 11:09:39 +00:00
sgcommand 5c31deb630 Merge pull request #11: fix deterministic template table index ordering 2026-07-19 14:11:11 +00:00
SG Command c2def00bcf fix(template): make map helper ordering deterministic 2026-07-19 15:19:33 +02:00
warkanum 784dc1f0da chore(release): update package version to 1.0.63
Release / test (push) Successful in 52s
Release / release (push) Successful in 1m45s
Release / pkg-aur (push) Successful in 1m1s
Release / pkg-deb (push) Successful in 2m48s
Release / pkg-rpm (push) Successful in 2m49s
2026-07-18 22:41:30 +02:00
warkanum 7d93bee4bd chore: Fixed linitng issues 2026-07-18 22:41:23 +02:00
6 changed files with 126 additions and 13 deletions
+1 -1
View File
@@ -393,7 +393,7 @@ func writeDatabase(db *models.Database, dbType, outputPath, packageName, schemaF
writerOpts := newWriterOptions(outputPath, packageName, flattenSchema, nullableTypes, continueOnError) writerOpts := newWriterOptions(outputPath, packageName, flattenSchema, nullableTypes, continueOnError)
if extraFields != "" { if extraFields != "" {
if strings.ToLower(dbType) != "bun" { if !strings.EqualFold(dbType, "bun") {
return fmt.Errorf("--extra-fields is only supported for Bun output") return fmt.Errorf("--extra-fields is only supported for Bun output")
} }
extraFieldsJSON, err := os.ReadFile(extraFields) extraFieldsJSON, err := os.ReadFile(extraFields)
+1 -1
View File
@@ -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.62 pkgver=1.0.63
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 -1
View File
@@ -1,5 +1,5 @@
Name: relspec Name: relspec
Version: 1.0.62 Version: 1.0.63
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.
+4 -5
View File
@@ -29,15 +29,14 @@ const pgCastMarker = "\x00PGCAST\x00"
// A placeholder that appears more than once maps to the same $N. An unknown // A placeholder that appears more than once maps to the same $N. An unknown
// placeholder (not built-in and not in staticParams) returns an error. // placeholder (not built-in and not in staticParams) returns an error.
// PostgreSQL cast syntax (::type) is left untouched. // PostgreSQL cast syntax (::type) is left untouched.
func BuildQuery(call string, fileBytes []byte, filename string, staticParams map[string]string) (string, []any, error) { func BuildQuery(call string, fileBytes []byte, filename string, staticParams map[string]string) (query string, args []any, err error) {
// Protect :: casts before running the placeholder regex. // Protect :: casts before running the placeholder regex.
protected := strings.ReplaceAll(call, "::", pgCastMarker) protected := strings.ReplaceAll(call, "::", pgCastMarker)
paramIndex := map[string]int{} // name → 1-based position paramIndex := map[string]int{} // name → 1-based position
var args []any
var firstErr error var firstErr error
result := namedPlaceholder.ReplaceAllStringFunc(protected, func(match string) string { query = namedPlaceholder.ReplaceAllStringFunc(protected, func(match string) string {
if firstErr != nil { if firstErr != nil {
return match return match
} }
@@ -78,9 +77,9 @@ func BuildQuery(call string, fileBytes []byte, filename string, staticParams map
} }
// Restore :: casts. // Restore :: casts.
result = strings.ReplaceAll(result, pgCastMarker, "::") query = strings.ReplaceAll(query, pgCastMarker, "::")
return result, args, nil return query, args, nil
} }
// ExecuteItem reads the asset file referenced by item.Entry.File (which is the // ExecuteItem reads the asset file referenced by item.Entry.File (which is the
+32 -5
View File
@@ -1,7 +1,9 @@
package reflectutil package reflectutil
import ( import (
"fmt"
"reflect" "reflect"
"sort"
"strings" "strings"
) )
@@ -134,7 +136,7 @@ func MapKeys(i interface{}) []interface{} {
return []interface{}{} return []interface{}{}
} }
keys := v.MapKeys() keys := sortedMapKeys(v)
result := make([]interface{}, len(keys)) result := make([]interface{}, len(keys))
for i, key := range keys { for i, key := range keys {
result[i] = key.Interface() result[i] = key.Interface()
@@ -155,14 +157,39 @@ func MapValues(i interface{}) []interface{} {
return []interface{}{} return []interface{}{}
} }
result := make([]interface{}, 0, v.Len()) keys := sortedMapKeys(v)
iter := v.MapRange() result := make([]interface{}, 0, len(keys))
for iter.Next() { for _, key := range keys {
result = append(result, iter.Value().Interface()) result = append(result, v.MapIndex(key).Interface())
} }
return result return result
} }
func sortedMapKeys(v reflect.Value) []reflect.Value {
keys := v.MapKeys()
sort.SliceStable(keys, func(i, j int) bool {
return mapKeyLess(keys[i], keys[j])
})
return keys
}
func mapKeyLess(a, b reflect.Value) bool {
switch a.Kind() {
case reflect.String:
return a.String() < b.String()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return a.Int() < b.Int()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return a.Uint() < b.Uint()
case reflect.Float32, reflect.Float64:
return a.Float() < b.Float()
case reflect.Bool:
return !a.Bool() && b.Bool()
default:
return fmt.Sprint(a.Interface()) < fmt.Sprint(b.Interface())
}
}
// MapGet safely gets a value from a map by key // MapGet safely gets a value from a map by key
// Returns nil if key doesn't exist or not a map // Returns nil if key doesn't exist or not a map
func MapGet(m interface{}, key interface{}) interface{} { func MapGet(m interface{}, key interface{}) interface{} {
+87
View File
@@ -0,0 +1,87 @@
package template
import (
"os"
"path/filepath"
"strings"
"testing"
"git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/writers"
)
func TestWriterTableIndexValuesDeterministic(t *testing.T) {
dir := t.TempDir()
templatePath := filepath.Join(dir, "indexes.tmpl")
outputDir := filepath.Join(dir, "out")
outputPath := filepath.Join(outputDir, "accounts.txt")
templateBody := "{{range values .Table.Indexes}}{{.Name}}:{{join .Columns \",\"}}\n{{end}}"
if err := os.MkdirAll(outputDir, 0755); err != nil {
t.Fatalf("create output dir: %v", err)
}
if err := os.WriteFile(templatePath, []byte(templateBody), 0644); err != nil {
t.Fatalf("write template: %v", err)
}
db := databaseWithMultipleIndexes()
var first []byte
const runs = 100
for i := 0; i < runs; i++ {
writer, err := NewWriter(&writers.WriterOptions{
OutputPath: outputDir,
Metadata: map[string]interface{}{
"template_path": templatePath,
"mode": string(TableMode),
},
})
if err != nil {
t.Fatalf("new writer: %v", err)
}
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("write database run %d: %v", i, err)
}
got, err := os.ReadFile(outputPath)
if err != nil {
t.Fatalf("read output run %d: %v", i, err)
}
if i == 0 {
first = got
continue
}
if string(got) != string(first) {
t.Fatalf("run %d output differed from first run\nfirst:\n%s\nrun %d:\n%s", i, first, i, got)
}
}
want := strings.Join([]string{
"idx_accounts_email:email",
"idx_accounts_last_login:last_login",
"idx_accounts_name:name",
"idx_accounts_status:status",
"idx_accounts_tenant:tenant_id",
"",
}, "\n")
if string(first) != want {
t.Fatalf("unexpected index order\nwant:\n%s\ngot:\n%s", want, first)
}
}
func databaseWithMultipleIndexes() *models.Database {
db := models.InitDatabase("test")
schema := models.InitSchema("public")
table := models.InitTable("accounts", "public")
table.Indexes["idx_accounts_status"] = &models.Index{Name: "idx_accounts_status", Table: table.Name, Schema: schema.Name, Columns: []string{"status"}}
table.Indexes["idx_accounts_email"] = &models.Index{Name: "idx_accounts_email", Table: table.Name, Schema: schema.Name, Columns: []string{"email"}}
table.Indexes["idx_accounts_tenant"] = &models.Index{Name: "idx_accounts_tenant", Table: table.Name, Schema: schema.Name, Columns: []string{"tenant_id"}}
table.Indexes["idx_accounts_name"] = &models.Index{Name: "idx_accounts_name", Table: table.Name, Schema: schema.Name, Columns: []string{"name"}}
table.Indexes["idx_accounts_last_login"] = &models.Index{Name: "idx_accounts_last_login", Table: table.Name, Schema: schema.Name, Columns: []string{"last_login"}}
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
return db
}