diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..722d5e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode diff --git a/oracle/common.go b/oracle/common.go index 42571e6..3f730ab 100644 --- a/oracle/common.go +++ b/oracle/common.go @@ -448,7 +448,7 @@ func writeQuotedIdentifier(builder *strings.Builder, identifier string) { // - plsqlBuilder: The builder to write the PL/SQL code into. // - dbNames: The slice containing the column names. // - table: The table name -func writeTableRecordCollectionDecl(plsqlBuilder *strings.Builder, dbNames []string, table string) { +func writeTableRecordCollectionDecl(db *gorm.DB, plsqlBuilder *strings.Builder, dbNames []string, table string) { // Declare a record where each element has the same structure as a row from the given table plsqlBuilder.WriteString(" TYPE t_record IS RECORD (\n") for i, field := range dbNames { @@ -456,11 +456,11 @@ func writeTableRecordCollectionDecl(plsqlBuilder *strings.Builder, dbNames []str plsqlBuilder.WriteString(",\n") } plsqlBuilder.WriteString(" ") - writeQuotedIdentifier(plsqlBuilder, field) + db.QuoteTo(plsqlBuilder, field) plsqlBuilder.WriteString(" ") - writeQuotedIdentifier(plsqlBuilder, table) + db.QuoteTo(plsqlBuilder, table) plsqlBuilder.WriteString(".") - writeQuotedIdentifier(plsqlBuilder, field) + db.QuoteTo(plsqlBuilder, field) plsqlBuilder.WriteString("%TYPE") } plsqlBuilder.WriteString("\n") diff --git a/oracle/create.go b/oracle/create.go index c1415ae..b700863 100644 --- a/oracle/create.go +++ b/oracle/create.go @@ -289,7 +289,7 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau // Start PL/SQL block plsqlBuilder.WriteString("DECLARE\n") - writeTableRecordCollectionDecl(&plsqlBuilder, stmt.Schema.DBNames, stmt.Table) + writeTableRecordCollectionDecl(db, &plsqlBuilder, stmt.Schema.DBNames, stmt.Table) plsqlBuilder.WriteString(" l_affected_records t_records;\n") // Create array types and variables for each column @@ -323,7 +323,7 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau // FORALL with MERGE and RETURNING BULK COLLECT INTO plsqlBuilder.WriteString(fmt.Sprintf(" FORALL i IN 1..%d\n", len(createValues.Values))) plsqlBuilder.WriteString(" MERGE INTO ") - writeQuotedIdentifier(&plsqlBuilder, stmt.Table) + db.QuoteTo(&plsqlBuilder, stmt.Table) plsqlBuilder.WriteString(" t\n") // Build USING clause plsqlBuilder.WriteString(" USING (SELECT ") @@ -332,7 +332,7 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau plsqlBuilder.WriteString(", ") } plsqlBuilder.WriteString(fmt.Sprintf("l_col_%d_array(i) AS ", idx)) - writeQuotedIdentifier(&plsqlBuilder, column.Name) + db.QuoteTo(&plsqlBuilder, column.Name) } plsqlBuilder.WriteString(" FROM DUAL) s\n") @@ -344,9 +344,9 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau plsqlBuilder.WriteString(" AND ") } plsqlBuilder.WriteString("t.") - writeQuotedIdentifier(&plsqlBuilder, conflictCol.Name) + db.QuoteTo(&plsqlBuilder, conflictCol.Name) plsqlBuilder.WriteString(" = s.") - writeQuotedIdentifier(&plsqlBuilder, conflictCol.Name) + db.QuoteTo(&plsqlBuilder, conflictCol.Name) } plsqlBuilder.WriteString(")\n") @@ -371,9 +371,9 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau plsqlBuilder.WriteString(", ") } plsqlBuilder.WriteString("t.") - writeQuotedIdentifier(&plsqlBuilder, column.Name) + db.QuoteTo(&plsqlBuilder, column.Name) plsqlBuilder.WriteString(" = s.") - writeQuotedIdentifier(&plsqlBuilder, column.Name) + db.QuoteTo(&plsqlBuilder, column.Name) updateCount++ } } @@ -405,9 +405,9 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau plsqlBuilder.WriteString(", ") } plsqlBuilder.WriteString("t.") - writeQuotedIdentifier(&plsqlBuilder, column.Name) + db.QuoteTo(&plsqlBuilder, column.Name) plsqlBuilder.WriteString(" = s.") - writeQuotedIdentifier(&plsqlBuilder, column.Name) + db.QuoteTo(&plsqlBuilder, column.Name) updateCount++ } } @@ -427,9 +427,9 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau } } plsqlBuilder.WriteString(" WHEN MATCHED THEN UPDATE SET t.") - writeQuotedIdentifier(&plsqlBuilder, noopCol) + db.QuoteTo(&plsqlBuilder, noopCol) plsqlBuilder.WriteString(" = s.") - writeQuotedIdentifier(&plsqlBuilder, noopCol) + db.QuoteTo(&plsqlBuilder, noopCol) plsqlBuilder.WriteString("\n") } @@ -444,7 +444,7 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau if insertCount > 0 { plsqlBuilder.WriteString(", ") } - writeQuotedIdentifier(&plsqlBuilder, column.Name) + db.QuoteTo(&plsqlBuilder, column.Name) insertCount++ } } @@ -459,7 +459,7 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau plsqlBuilder.WriteString(", ") } plsqlBuilder.WriteString("s.") - writeQuotedIdentifier(&plsqlBuilder, column.Name) + db.QuoteTo(&plsqlBuilder, column.Name) insertCount++ } } @@ -475,7 +475,7 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau if insertCount > 0 { plsqlBuilder.WriteString(", ") } - writeQuotedIdentifier(&plsqlBuilder, column.Name) + db.QuoteTo(&plsqlBuilder, column.Name) insertCount++ } } @@ -489,7 +489,7 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau plsqlBuilder.WriteString(", ") } plsqlBuilder.WriteString("s.") - writeQuotedIdentifier(&plsqlBuilder, column.Name) + db.QuoteTo(&plsqlBuilder, column.Name) insertCount++ } } @@ -503,7 +503,7 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau if i > 0 { plsqlBuilder.WriteString(", ") } - writeQuotedIdentifier(&plsqlBuilder, column) + db.QuoteTo(&plsqlBuilder, column) } plsqlBuilder.WriteString("\n BULK COLLECT INTO l_affected_records;\n") @@ -514,7 +514,7 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau if field := findFieldByDBName(schema, column); field != nil { stmt.Vars = append(stmt.Vars, sql.Out{Dest: createTypedDestination(field)}) plsqlBuilder.WriteString(fmt.Sprintf(" IF l_affected_records.COUNT > %d THEN :%d := l_affected_records(%d).", rowIdx, outParamIndex+1, rowIdx+1)) - writeQuotedIdentifier(&plsqlBuilder, column) + db.QuoteTo(&plsqlBuilder, column) plsqlBuilder.WriteString("; END IF;\n") outParamIndex++ } @@ -548,7 +548,7 @@ func buildBulkInsertOnlyPLSQL(db *gorm.DB, createValues clause.Values) { // Start PL/SQL block plsqlBuilder.WriteString("DECLARE\n") - writeTableRecordCollectionDecl(&plsqlBuilder, stmt.Schema.DBNames, stmt.Table) + writeTableRecordCollectionDecl(db, &plsqlBuilder, stmt.Schema.DBNames, stmt.Table) plsqlBuilder.WriteString(" l_inserted_records t_records;\n") // Create array types and variables for each column @@ -582,14 +582,14 @@ func buildBulkInsertOnlyPLSQL(db *gorm.DB, createValues clause.Values) { // FORALL with RETURNING BULK COLLECT INTO plsqlBuilder.WriteString(fmt.Sprintf(" FORALL i IN 1..%d\n", len(createValues.Values))) plsqlBuilder.WriteString(" INSERT INTO ") - writeQuotedIdentifier(&plsqlBuilder, stmt.Table) + db.QuoteTo(&plsqlBuilder, stmt.Table) plsqlBuilder.WriteString(" (") // Add column names for i, column := range createValues.Columns { if i > 0 { plsqlBuilder.WriteString(", ") } - writeQuotedIdentifier(&plsqlBuilder, column.Name) + db.QuoteTo(&plsqlBuilder, column.Name) } plsqlBuilder.WriteString(") VALUES (") @@ -609,7 +609,7 @@ func buildBulkInsertOnlyPLSQL(db *gorm.DB, createValues clause.Values) { if i > 0 { plsqlBuilder.WriteString(", ") } - writeQuotedIdentifier(&plsqlBuilder, column) + db.QuoteTo(&plsqlBuilder, column) } plsqlBuilder.WriteString("\n BULK COLLECT INTO l_inserted_records;\n") @@ -618,7 +618,7 @@ func buildBulkInsertOnlyPLSQL(db *gorm.DB, createValues clause.Values) { for rowIdx := 0; rowIdx < len(createValues.Values); rowIdx++ { for _, column := range allColumns { var columnBuilder strings.Builder - writeQuotedIdentifier(&columnBuilder, column) + db.QuoteTo(&columnBuilder, column) quotedColumn := columnBuilder.String() if field := findFieldByDBName(schema, column); field != nil { diff --git a/oracle/delete.go b/oracle/delete.go index 9d90bfc..817f69a 100644 --- a/oracle/delete.go +++ b/oracle/delete.go @@ -255,13 +255,13 @@ func buildBulkDeletePLSQL(db *gorm.DB) { // Start PL/SQL block plsqlBuilder.WriteString("DECLARE\n") - writeTableRecordCollectionDecl(&plsqlBuilder, stmt.Schema.DBNames, stmt.Table) + writeTableRecordCollectionDecl(db, &plsqlBuilder, stmt.Schema.DBNames, stmt.Table) plsqlBuilder.WriteString(" l_deleted_records t_records;\n") plsqlBuilder.WriteString("BEGIN\n") // Build DELETE statement plsqlBuilder.WriteString(" DELETE FROM ") - writeQuotedIdentifier(&plsqlBuilder, stmt.Table) + db.QuoteTo(&plsqlBuilder, stmt.Table) // Add WHERE clause if it exists if whereClause, hasWhere := stmt.Clauses["WHERE"]; hasWhere { @@ -278,7 +278,7 @@ func buildBulkDeletePLSQL(db *gorm.DB) { if i > 0 { plsqlBuilder.WriteString(", ") } - writeQuotedIdentifier(&plsqlBuilder, column) + db.QuoteTo(&plsqlBuilder, column) } plsqlBuilder.WriteString("\n BULK COLLECT INTO l_deleted_records;\n") @@ -297,7 +297,7 @@ func buildBulkDeletePLSQL(db *gorm.DB) { plsqlBuilder.WriteString(fmt.Sprintf(" IF l_deleted_records.COUNT > %d THEN\n", rowIdx)) plsqlBuilder.WriteString(fmt.Sprintf(" :%d := l_deleted_records(%d).", outParamIndex+1, rowIdx+1)) - writeQuotedIdentifier(&plsqlBuilder, column) + db.QuoteTo(&plsqlBuilder, column) plsqlBuilder.WriteString(";\n") plsqlBuilder.WriteString(" END IF;\n") outParamIndex++ @@ -324,9 +324,9 @@ func buildWhereClause(db *gorm.DB, plsqlBuilder *strings.Builder, expressions [] case clause.Eq: // Write the column name if columnName, ok := e.Column.(string); ok { - writeQuotedIdentifier(plsqlBuilder, columnName) + db.QuoteTo(plsqlBuilder, columnName) } else if columnExpr, ok := e.Column.(clause.Column); ok { - writeQuotedIdentifier(plsqlBuilder, columnExpr.Name) + db.QuoteTo(plsqlBuilder, columnExpr.Name) } else { plsqlBuilder.WriteString(fmt.Sprintf("%v", e.Column)) } @@ -342,9 +342,9 @@ func buildWhereClause(db *gorm.DB, plsqlBuilder *strings.Builder, expressions [] case clause.IN: if columnName, ok := e.Column.(string); ok { - writeQuotedIdentifier(plsqlBuilder, columnName) + db.QuoteTo(plsqlBuilder, columnName) } else if columnExpr, ok := e.Column.(clause.Column); ok { - writeQuotedIdentifier(plsqlBuilder, columnExpr.Name) + db.QuoteTo(plsqlBuilder, columnExpr.Name) } else { plsqlBuilder.WriteString(fmt.Sprintf("%v", e.Column)) } diff --git a/oracle/oracle.go b/oracle/oracle.go index ed70a86..922450e 100644 --- a/oracle/oracle.go +++ b/oracle/oracle.go @@ -62,10 +62,11 @@ import ( ) type Config struct { - DriverName string - DataSourceName string - Conn *sql.DB - DefaultStringSize uint + DriverName string + DataSourceName string + Conn *sql.DB + DefaultStringSize uint + SkipQuoteIdentifiers bool } type Dialector struct { @@ -104,6 +105,18 @@ func (d Dialector) Initialize(db *gorm.DB) (err error) { callback.Update().Replace("gorm:update", Update) callback.Query().Before("gorm:query").Register("oracle:before_query", BeforeQuery) + if d.SkipQuoteIdentifiers { + // When identifiers are not quoted, columns are returned by Oracle in uppercase. + // Fields in the models may be lower case for compatibility with other databases. + // Match them up with the fields using the column mapping. + oracleCaseHandler := "oracle:case_handler" + if callback.Query().Get(oracleCaseHandler) == nil { + if err := callback.Query().Before("gorm:query").Register(oracleCaseHandler, MismatchedCaseHandler); err != nil { + return err + } + } + } + maps.Copy(db.ClauseBuilders, OracleClauseBuilders()) if d.Conn == nil { @@ -237,9 +250,13 @@ func (d Dialector) BindVarTo(writer clause.Writer, stmt *gorm.Statement, v inter // Manages quoting of identifiers func (d Dialector) QuoteTo(writer clause.Writer, str string) { - var builder strings.Builder - writeQuotedIdentifier(&builder, str) - writer.WriteString(builder.String()) + out := str + if !d.SkipQuoteIdentifiers { + var builder strings.Builder + writeQuotedIdentifier(&builder, str) + out = builder.String() + } + _, _ = writer.WriteString(out) } var numericPlaceholder = regexp.MustCompile(`:(\d+)`) diff --git a/oracle/query.go b/oracle/query.go index 5a45c26..e21690a 100644 --- a/oracle/query.go +++ b/oracle/query.go @@ -39,9 +39,10 @@ package oracle import ( - "gorm.io/gorm" "regexp" "strings" + + "gorm.io/gorm" ) // Identifies the table name alias provided as @@ -65,3 +66,19 @@ func BeforeQuery(db *gorm.DB) { } return } + +// MismatchedCaseHandler handles Oracle Case Insensitivity. +// When identifiers are not quoted, columns are returned by Oracle in uppercase. +// Fields in the models may be lower case for compatibility with other databases. +// Match them up with the fields using the column mapping. +func MismatchedCaseHandler(gormDB *gorm.DB) { + if gormDB.Statement == nil || gormDB.Statement.Schema == nil { + return + } + if len(gormDB.Statement.Schema.Fields) > 0 && gormDB.Statement.ColumnMapping == nil { + gormDB.Statement.ColumnMapping = map[string]string{} + } + for _, field := range gormDB.Statement.Schema.Fields { + gormDB.Statement.ColumnMapping[strings.ToUpper(field.DBName)] = field.Name + } +} diff --git a/oracle/update.go b/oracle/update.go index 0a5b653..2a2ee19 100644 --- a/oracle/update.go +++ b/oracle/update.go @@ -476,13 +476,13 @@ func buildUpdatePLSQL(db *gorm.DB) { // Start PL/SQL block plsqlBuilder.WriteString("DECLARE\n") - writeTableRecordCollectionDecl(&plsqlBuilder, stmt.Schema.DBNames, stmt.Table) + writeTableRecordCollectionDecl(db, &plsqlBuilder, stmt.Schema.DBNames, stmt.Table) plsqlBuilder.WriteString(" l_updated_records t_records;\n") plsqlBuilder.WriteString("BEGIN\n") // Build UPDATE statement plsqlBuilder.WriteString(" UPDATE ") - writeQuotedIdentifier(&plsqlBuilder, stmt.Table) + db.QuoteTo(&plsqlBuilder, stmt.Table) plsqlBuilder.WriteString(" SET ") // Add SET assignments - handle both regular values and expressions @@ -490,7 +490,7 @@ func buildUpdatePLSQL(db *gorm.DB) { if i > 0 { plsqlBuilder.WriteString(", ") } - writeQuotedIdentifier(&plsqlBuilder, assignment.Column.Name) + db.QuoteTo(&plsqlBuilder, assignment.Column.Name) plsqlBuilder.WriteString(" = ") // Check if the value is a clause.Expr (like gorm.Expr) @@ -528,7 +528,7 @@ func buildUpdatePLSQL(db *gorm.DB) { if i > 0 { plsqlBuilder.WriteString(", ") } - writeQuotedIdentifier(&plsqlBuilder, column) + db.QuoteTo(&plsqlBuilder, column) } plsqlBuilder.WriteString("\n BULK COLLECT INTO l_updated_records;\n") @@ -559,7 +559,7 @@ func buildUpdatePLSQL(db *gorm.DB) { // Add the assignment to PL/SQL with correct parameter reference plsqlBuilder.WriteString(fmt.Sprintf(" IF l_updated_records.COUNT > %d THEN\n", rowIdx)) plsqlBuilder.WriteString(fmt.Sprintf(" :%d := l_updated_records(%d).", paramIndex, rowIdx+1)) - writeQuotedIdentifier(&plsqlBuilder, column) + db.QuoteTo(&plsqlBuilder, column) plsqlBuilder.WriteString(";\n") plsqlBuilder.WriteString(" END IF;\n") } diff --git a/tests/.gitignore b/tests/.gitignore index c14a297..9a71212 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1 +1 @@ -go.sum \ No newline at end of file +passed-tests.txt.new diff --git a/tests/go.sum b/tests/go.sum new file mode 100644 index 0000000..f7e9277 --- /dev/null +++ b/tests/go.sum @@ -0,0 +1,58 @@ +github.com/UNO-SOFT/zlog v0.8.1 h1:TEFkGJHtUfTRgMkLZiAjLSHALjwSBdw6/zByMC5GJt4= +github.com/UNO-SOFT/zlog v0.8.1/go.mod h1:yqFOjn3OhvJ4j7ArJqQNA+9V+u6t9zSAyIZdWdMweWc= +github.com/VictoriaMetrics/easyproto v0.1.4 h1:r8cNvo8o6sR4QShBXQd1bKw/VVLSQma/V2KhTBPf+Sc= +github.com/VictoriaMetrics/easyproto v0.1.4/go.mod h1:QlGlzaJnDfFd8Lk6Ci/fuLxfTo3/GThPs2KH23mv710= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/godror/godror v0.49.0 h1:oXAzPOm7bAGdFGkTaePCRXv3dfyr6Agni++4XhaRcC8= +github.com/godror/godror v0.49.0/go.mod h1:D4gKled+sJVcagT1HWibkBsO9PcLn2Nu96FCr1RtnzI= +github.com/godror/knownpb v0.3.0 h1:+caUdy8hTtl7X05aPl3tdL540TvCcaQA6woZQroLZMw= +github.com/godror/knownpb v0.3.0/go.mod h1:PpTyfJwiOEAzQl7NtVCM8kdPCnp3uhxsZYIzZ5PV4zU= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc= +github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA= +golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=