fix(assets): improve error reporting for asset failures
fix(embed): update embedDirectives to return result directly fix(executor): clean up import order and formatting fix(syntax_test): change file permission syntax to octal fix(mysql): update file permission syntax to octal fix(pgsql): update file permission syntax to octal fix(sqlex): simplify Walk function parameter handling fix(transform): adjust identifier comparison logic fix(template): clean up function formatting and error handling
This commit is contained in:
@@ -203,7 +203,8 @@ func runAssetsExecute(cmd *cobra.Command, args []string) error {
|
||||
if len(failures) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "Failed: %d\n", len(failures))
|
||||
fmt.Fprintf(os.Stderr, "\n⚠ Failed Entries Summary (%d failed):\n", len(failures))
|
||||
for i, f := range failures {
|
||||
for i := range failures {
|
||||
f := &failures[i]
|
||||
fmt.Fprintf(os.Stderr, " %d. %s (Priority=%d, Sequence=%d)\n Error: %v\n",
|
||||
i+1, filepath.Base(f.item.Entry.File), f.item.Priority, f.item.Sequence, f.err)
|
||||
}
|
||||
|
||||
@@ -42,10 +42,9 @@ func ProcessEmbedDirectives(sqlPath, sql string) (string, error) {
|
||||
return ProcessEmbedDirectivesWithOptions(sqlPath, sql, EmbedOptions{})
|
||||
}
|
||||
|
||||
func embedDirectives(sql string) (string, []string) {
|
||||
var directives []string
|
||||
func embedDirectives(sql string) (result string, directives []string) {
|
||||
// Only standalone line comments are directives, and only in SQL code.
|
||||
result := sqllex.Walk(sql, nil, func(comment string) string {
|
||||
result = sqllex.Walk(sql, nil, func(comment string) string {
|
||||
if match := embedDirectivePattern.FindStringSubmatch(comment); match != nil {
|
||||
directives = append(directives, match[1])
|
||||
return ""
|
||||
@@ -54,6 +53,7 @@ func embedDirectives(sql string) (string, []string) {
|
||||
})
|
||||
return result, directives
|
||||
}
|
||||
|
||||
func HasEmbedDirectives(sql string) bool {
|
||||
_, directives := embedDirectives(sql)
|
||||
return len(directives) > 0
|
||||
|
||||
@@ -3,9 +3,10 @@ package assetloader
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/sqllex"
|
||||
"path/filepath"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/sqllex"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
@@ -22,7 +23,6 @@ import (
|
||||
// placeholder (not built-in and not in staticParams) returns an error.
|
||||
// PostgreSQL cast syntax (::type) is left untouched.
|
||||
func BuildQuery(call string, fileBytes []byte, filename string, staticParams map[string]string) (query string, args []any, err error) {
|
||||
|
||||
paramIndex := map[string]int{} // name → 1-based position
|
||||
var firstErr error
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ func TestEmbedLiteralOnePassAndSQLContexts(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
data := "price $1 ${2} $HOME $$ :b 'quote' \\path"
|
||||
for file, text := range map[string]string{"a.txt": data, "b.txt": "Second"} {
|
||||
if err := os.WriteFile(filepath.Join(dir, file), []byte(text), 0600); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(dir, file), []byte(text), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ func columnDefaults(create string) map[string]string {
|
||||
depth--
|
||||
if depth == 0 {
|
||||
definitions = append(definitions, create[start:i])
|
||||
break
|
||||
}
|
||||
}
|
||||
if ch == ')' && depth == 0 {
|
||||
@@ -177,8 +176,9 @@ func autoIncrementValue(create string) string {
|
||||
}
|
||||
return "1"
|
||||
}
|
||||
|
||||
func unsupportedNativeOptions(create string) []string {
|
||||
words := strings.FieldsFunc(strings.ToUpper(unquotedSQL(create)), func(r rune) bool { return !(r >= 'A' && r <= 'Z' || r == '_') })
|
||||
words := strings.FieldsFunc(strings.ToUpper(unquotedSQL(create)), func(r rune) bool { return (r < 'A' || r > 'Z') && r != '_' })
|
||||
var out []string
|
||||
for i, w := range words {
|
||||
switch w {
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ func ident(c byte) bool { return start(c) || c >= '0' && c <= '9' || c >= 128 }
|
||||
// Walk visits named parameters and standalone line comments in executable SQL. Quoted
|
||||
// strings/identifiers, dollar bodies, casts and nested block comments are opaque.
|
||||
// Returning replacement text is literal: it is never scanned again.
|
||||
func Walk(sql string, parameter func(string) string, lineComment func(string) string) string {
|
||||
func Walk(sql string, parameter, lineComment func(string) string) string {
|
||||
var out strings.Builder
|
||||
for i := 0; i < len(sql); {
|
||||
begin := i
|
||||
|
||||
@@ -53,7 +53,7 @@ func LowercaseIdentifiers(db *models.Database) (*models.Database, error) {
|
||||
out.Field(i).SetString(strings.ToLower(value.String()))
|
||||
continue
|
||||
}
|
||||
if !(v.Type() == reflect.TypeOf(models.Index{}) && field == "Columns") && listFields[field] && value.Kind() == reflect.Slice && value.Type().Elem().Kind() == reflect.String {
|
||||
if (v.Type() != reflect.TypeOf(models.Index{}) || field != "Columns") && listFields[field] && value.Kind() == reflect.Slice && value.Type().Elem().Kind() == reflect.String {
|
||||
list := reflect.MakeSlice(value.Type(), value.Len(), value.Len())
|
||||
for j := 0; j < value.Len(); j++ {
|
||||
list.Index(j).SetString(strings.ToLower(value.Index(j).String()))
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"testing"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
func TestLowercaseIdentifiersReferencesAndData(t *testing.T) {
|
||||
@@ -35,6 +36,7 @@ func TestLowercaseIdentifiersReferencesAndData(t *testing.T) {
|
||||
t.Fatal("input mutated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLowercaseIdentifierCollisions(t *testing.T) {
|
||||
db := models.InitDatabase("db")
|
||||
s := models.InitSchema("app")
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestStrictConversionPreflightsAllObjects(t *testing.T) {
|
||||
unsupported.Indexes[index.Name] = index
|
||||
db.Schemas[0].Tables = append(db.Schemas[0].Tables, unsupported)
|
||||
out := filepath.Join(t.TempDir(), "existing.sql")
|
||||
if err := os.WriteFile(out, []byte("keep existing contents"), 0600); err != nil {
|
||||
if err := os.WriteFile(out, []byte("keep existing contents"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
opts := &writers.WriterOptions{StrictConversion: true, OutputPath: out}
|
||||
|
||||
@@ -3,14 +3,16 @@ package mysql_test
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
driver "github.com/go-sql-driver/mysql"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
driver "github.com/go-sql-driver/mysql"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
||||
rmysql "git.warky.dev/wdevs/relspecgo/pkg/readers/mysql"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
@@ -50,7 +52,7 @@ func TestLiveUnsupportedObjectsDiagnosed(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "schema.sql")
|
||||
if err := os.WriteFile(path, []byte("original"), 0600); err != nil {
|
||||
if err := os.WriteFile(path, []byte("original"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = wmysql.NewWriter(&writers.WriterOptions{StrictConversion: true, OutputPath: path}).WriteDatabase(model)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
@@ -12,12 +11,15 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
driver "github.com/go-sql-driver/mysql"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
||||
rmysql "git.warky.dev/wdevs/relspecgo/pkg/readers/mysql"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
wmysql "git.warky.dev/wdevs/relspecgo/pkg/writers/mysql"
|
||||
driver "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func TestLiveSchemaRoundTrip(t *testing.T) {
|
||||
|
||||
@@ -10,11 +10,12 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/mariadb"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/transform"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
type Writer struct {
|
||||
@@ -53,6 +54,7 @@ func (w *Writer) Statements(db *models.Database) ([]string, error) {
|
||||
}
|
||||
return databaseStatements(db), nil
|
||||
}
|
||||
|
||||
func (w *Writer) WriteDatabase(db *models.Database) error {
|
||||
statements, err := w.Statements(db)
|
||||
if err != nil {
|
||||
@@ -78,6 +80,7 @@ func (w *Writer) WriteDatabase(db *models.Database) error {
|
||||
}
|
||||
return closeErr
|
||||
}
|
||||
|
||||
func writeStatements(out io.Writer, statements []string, name string) error {
|
||||
name = strings.NewReplacer("\n", " ", "\r", " ").Replace(name)
|
||||
if _, err := fmt.Fprintf(out, "-- Database: %s\n", name); err != nil {
|
||||
@@ -93,9 +96,11 @@ func writeStatements(out io.Writer, statements []string, name string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Writer) WriteSchema(s *models.Schema) error {
|
||||
return w.WriteDatabase(&models.Database{Schemas: []*models.Schema{s}})
|
||||
}
|
||||
|
||||
func (w *Writer) WriteTable(t *models.Table) error {
|
||||
return w.WriteSchema(&models.Schema{Name: "", Tables: []*models.Table{t}})
|
||||
}
|
||||
@@ -106,6 +111,7 @@ func qualified(schema, name string) string {
|
||||
}
|
||||
return quote(schema) + "." + quote(name)
|
||||
}
|
||||
|
||||
func keys[T any](m map[string]T) []string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
@@ -122,9 +128,11 @@ func metaBool(m map[string]any, key string, def bool) bool {
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func signature(c *models.Column) string {
|
||||
return fmt.Sprintf("%s/%d/%d/%d", c.Type, c.Length, c.Precision, c.Scale)
|
||||
}
|
||||
|
||||
func columnType(c *models.Column) string {
|
||||
if native := metaString(c.Metadata, "mysql_type"); native != "" && metaString(c.Metadata, "mysql_type_signature") == signature(c) {
|
||||
return native
|
||||
@@ -152,6 +160,7 @@ func columnType(c *models.Column) string {
|
||||
func literal(s string) string {
|
||||
return "'" + strings.ReplaceAll(strings.ReplaceAll(s, "\\", "\\\\"), "'", "''") + "'"
|
||||
}
|
||||
|
||||
func defaultSQL(c *models.Column) string {
|
||||
value := fmt.Sprint(c.Default)
|
||||
if native := metaString(c.Metadata, "mysql_default_sql"); native != "" && metaString(c.Metadata, "mysql_default_value") == value {
|
||||
@@ -185,6 +194,7 @@ func defaultSQL(c *models.Column) string {
|
||||
}
|
||||
return literal(value)
|
||||
}
|
||||
|
||||
func indexParts(i *models.Index) string {
|
||||
if len(i.KeyParts) == 0 {
|
||||
return quoted(i.Columns)
|
||||
@@ -205,6 +215,7 @@ func indexParts(i *models.Index) string {
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
func tableStatement(s *models.Schema, t *models.Table) string {
|
||||
schema := ""
|
||||
if s != nil {
|
||||
@@ -346,6 +357,7 @@ func tableStatement(s *models.Schema, t *models.Table) string {
|
||||
}
|
||||
return "CREATE TABLE IF NOT EXISTS " + qualified(schema, t.Name) + " (\n" + strings.Join(defs, ",\n") + "\n)" + tail + ";\n\n"
|
||||
}
|
||||
|
||||
func databaseStatements(db *models.Database) []string {
|
||||
var statements []string
|
||||
for _, s := range db.Schemas {
|
||||
@@ -378,6 +390,7 @@ func databaseStatements(db *models.Database) []string {
|
||||
}
|
||||
return statements
|
||||
}
|
||||
|
||||
func (w *Writer) execute(statements []string, conn string) error {
|
||||
if w.options.AtomicExecution {
|
||||
return fmt.Errorf("MySQL DDL implicitly commits; atomic execution is unsupported")
|
||||
|
||||
@@ -848,7 +848,7 @@ func adoptableUniqueIndex(table *models.Table, constraint *models.Constraint) st
|
||||
return ""
|
||||
}
|
||||
for i, col := range index.Columns {
|
||||
if !(col == constraint.Columns[i]) {
|
||||
if col != constraint.Columns[i] {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -1239,21 +1239,21 @@ func constraintsEqual(c1, c2 *models.Constraint) bool {
|
||||
return false
|
||||
}
|
||||
for i, col := range c1.Columns {
|
||||
if !(col == c2.Columns[i]) {
|
||||
if col != c2.Columns[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// For foreign keys, also compare referenced table and columns
|
||||
if c1.Type == models.ForeignKeyConstraint {
|
||||
if !(c1.ReferencedTable == c2.ReferencedTable) {
|
||||
if c1.ReferencedTable != c2.ReferencedTable {
|
||||
return false
|
||||
}
|
||||
if len(c1.ReferencedColumns) != len(c2.ReferencedColumns) {
|
||||
return false
|
||||
}
|
||||
for i, col := range c1.ReferencedColumns {
|
||||
if !(col == c2.ReferencedColumns[i]) {
|
||||
if col != c2.ReferencedColumns[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1283,7 +1283,7 @@ func indexesEqual(idx1, idx2 *models.Index) bool {
|
||||
return false
|
||||
}
|
||||
for i, col := range idx1.Columns {
|
||||
if !(col == idx2.Columns[i]) {
|
||||
if col != idx2.Columns[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1365,7 +1365,7 @@ func primaryKeyColumnsMatch(modelTable *models.Table, current *models.Constraint
|
||||
return false
|
||||
}
|
||||
for i, col := range modelCols {
|
||||
if !(col == current.Columns[i]) {
|
||||
if col != current.Columns[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestQuotedNamesAndLowercaseOption(t *testing.T) {
|
||||
t.Fatal("input changed")
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "out.sql")
|
||||
if err := os.WriteFile(path, []byte("keep"), 0600); err != nil {
|
||||
if err := os.WriteFile(path, []byte("keep"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
col.Name = "bad\x00name"
|
||||
|
||||
@@ -29,11 +29,13 @@ type Writer struct {
|
||||
}
|
||||
|
||||
// Shared execution results retain the PostgreSQL package's public type names.
|
||||
type ExecutionReport = writers.ExecutionReport
|
||||
type ExecutionFailure = writers.ExecutionFailure
|
||||
type SchemaReport = writers.SchemaReport
|
||||
type TableReport = writers.TableReport
|
||||
type ExecutionError = writers.ExecutionError
|
||||
type (
|
||||
ExecutionReport = writers.ExecutionReport
|
||||
ExecutionFailure = writers.ExecutionFailure
|
||||
SchemaReport = writers.SchemaReport
|
||||
TableReport = writers.TableReport
|
||||
ExecutionError = writers.ExecutionError
|
||||
)
|
||||
|
||||
// NewWriter creates a new PostgreSQL SQL writer
|
||||
func NewWriter(options *writers.WriterOptions) *Writer {
|
||||
@@ -2114,7 +2116,6 @@ func extractSequenceName(defaultExpr string) string {
|
||||
name.WriteByte(defaultExpr[i])
|
||||
}
|
||||
return ""
|
||||
|
||||
}
|
||||
|
||||
// executeDatabaseSQL applies db to a PostgreSQL database. By default it reads the live schema
|
||||
@@ -2497,7 +2498,6 @@ func parseQualifiedIdent(s string) (schema, name string) {
|
||||
}
|
||||
}
|
||||
return "", stripQuotes(token)
|
||||
|
||||
}
|
||||
|
||||
// firstBareIdent returns the first whitespace/delimiter-terminated token, stripping outer quotes.
|
||||
@@ -2520,7 +2520,6 @@ func firstBareIdent(s string) string {
|
||||
}
|
||||
}
|
||||
return s
|
||||
|
||||
}
|
||||
|
||||
// firstIdentAfterKeyword returns the first identifier token after keyword (matched case-insensitively via upperStmt).
|
||||
|
||||
@@ -46,9 +46,9 @@ func (w *Writer) WriteDatabase(db *models.Database) error {
|
||||
var scripts []*models.Script
|
||||
for _, schema := range db.Schemas {
|
||||
for _, script := range sortedScripts(schema.Scripts) {
|
||||
copy := *script
|
||||
copy.Name = schema.Name + "." + script.Name
|
||||
scripts = append(scripts, ©)
|
||||
scriptCopy := *script
|
||||
scriptCopy.Name = schema.Name + "." + script.Name
|
||||
scripts = append(scripts, &scriptCopy)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@ func (w *Writer) executeSchemaMode(db *models.Database) error {
|
||||
}
|
||||
return w.executeFiles(data)
|
||||
}
|
||||
|
||||
func (w *Writer) executeDomainMode(db *models.Database) error {
|
||||
var data []*TemplateData
|
||||
for _, domain := range db.Domains {
|
||||
@@ -144,6 +145,7 @@ func (w *Writer) executeDomainMode(db *models.Database) error {
|
||||
}
|
||||
return w.executeFiles(data)
|
||||
}
|
||||
|
||||
func (w *Writer) executeScriptMode(db *models.Database) error {
|
||||
var data []*TemplateData
|
||||
for _, schema := range db.Schemas {
|
||||
@@ -153,6 +155,7 @@ func (w *Writer) executeScriptMode(db *models.Database) error {
|
||||
}
|
||||
return w.executeFiles(data)
|
||||
}
|
||||
|
||||
func (w *Writer) executeTableMode(db *models.Database) error {
|
||||
var data []*TemplateData
|
||||
for _, schema := range db.Schemas {
|
||||
@@ -246,7 +249,10 @@ func (w *Writer) writeArtifact(root *os.Root, file artifact) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer root.Remove(temporary)
|
||||
defer func() {
|
||||
// Best-effort cleanup; a successful rename already removes the staging path.
|
||||
_ = root.Remove(temporary)
|
||||
}()
|
||||
_, writeErr := staged.WriteString(file.content)
|
||||
if err := errors.Join(writeErr, staged.Close()); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user