From 8de266b4a7391145e962918abb3a9705c13fd2c8 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 1 Nov 2021 17:08:54 +0800 Subject: [PATCH] Add ToSQL support to generate SQL string. (#4787) * Add db.ToSQL method for generate SQL string. * Improve sql builder test for all dialects. Improve assertEqualSQL test helper for ignore quotes in SQL. --- gorm.go | 15 +++++ tests/sql_builder_test.go | 135 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) diff --git a/gorm.go b/gorm.go index 71cd01e82..fc70f6845 100644 --- a/gorm.go +++ b/gorm.go @@ -441,3 +441,18 @@ func (db *DB) Use(plugin Plugin) error { db.Plugins[name] = plugin return nil } + +// ToSQL for generate SQL string. +// +// db.ToSQL(func(tx *gorm.DB) *gorm.DB { +// return tx.Model(&User{}).Where(&User{Name: "foo", Age: 20}) +// .Limit(10).Offset(5) +// .Order("name ASC") +// .First(&User{}) +// }) +func (db *DB) ToSQL(queryFn func(tx *DB) *DB) string { + tx := queryFn(db.Session(&Session{DryRun: true})) + stmt := tx.Statement + + return db.Dialector.Explain(stmt.SQL.String(), stmt.Vars...) +} diff --git a/tests/sql_builder_test.go b/tests/sql_builder_test.go index 081b96c92..2f9fd8dad 100644 --- a/tests/sql_builder_test.go +++ b/tests/sql_builder_test.go @@ -8,6 +8,8 @@ import ( "gorm.io/gorm" "gorm.io/gorm/clause" . "gorm.io/gorm/utils/tests" + + "time" ) func TestRow(t *testing.T) { @@ -287,3 +289,136 @@ func TestFromWithJoins(t *testing.T) { t.Errorf("The first join condition is over written instead of combining") } } + +func TestToSQL(t *testing.T) { + // By default DB.DryRun should false + if DB.DryRun { + t.Fatal("Failed expect DB.DryRun to be false") + } + + if DB.Dialector.Name() == "sqlserver" { + t.Skip("Skip SQL Server for this test, because it too difference with other dialects.") + } + + date, _ := time.Parse("2006-01-02", "2021-10-18") + + // find + sql := DB.ToSQL(func(tx *gorm.DB) *gorm.DB { + return tx.Model(&User{}).Where("id = ?", 100).Limit(10).Order("age desc").Find(&[]User{}) + }) + assertEqualSQL(t, `SELECT * FROM "users" WHERE id = 100 AND "users"."deleted_at" IS NULL ORDER BY age desc LIMIT 10`, sql) + + // after model chagned + if DB.Statement.DryRun || DB.DryRun { + t.Fatal("Failed expect DB.DryRun and DB.Statement.ToSQL to be false") + } + + if DB.Statement.SQL.String() != "" { + t.Fatal("Failed expect DB.Statement.SQL to be empty") + } + + // first + sql = DB.ToSQL(func(tx *gorm.DB) *gorm.DB { + return tx.Model(&User{}).Where(&User{Name: "foo", Age: 20}).Limit(10).Offset(5).Order("name ASC").First(&User{}) + }) + assertEqualSQL(t, `SELECT * FROM "users" WHERE "users"."name" = 'foo' AND "users"."age" = 20 AND "users"."deleted_at" IS NULL ORDER BY name ASC,"users"."id" LIMIT 1 OFFSET 5`, sql) + + // last and unscoped + sql = DB.ToSQL(func(tx *gorm.DB) *gorm.DB { + return tx.Model(&User{}).Unscoped().Where(&User{Name: "bar", Age: 12}).Limit(10).Offset(5).Order("name ASC").Last(&User{}) + }) + assertEqualSQL(t, `SELECT * FROM "users" WHERE "users"."name" = 'bar' AND "users"."age" = 12 ORDER BY name ASC,"users"."id" DESC LIMIT 1 OFFSET 5`, sql) + + // create + user := &User{Name: "foo", Age: 20} + user.CreatedAt = date + user.UpdatedAt = date + sql = DB.ToSQL(func(tx *gorm.DB) *gorm.DB { + return tx.Model(&User{}).Create(user) + }) + assertEqualSQL(t, `INSERT INTO "users" ("created_at","updated_at","deleted_at","name","age","birthday","company_id","manager_id","active") VALUES ('2021-10-18 00:00:00','2021-10-18 00:00:00',NULL,'foo',20,NULL,NULL,NULL,false) RETURNING "id"`, sql) + + // save + user = &User{Name: "foo", Age: 20} + user.CreatedAt = date + user.UpdatedAt = date + sql = DB.ToSQL(func(tx *gorm.DB) *gorm.DB { + return tx.Model(&User{}).Save(user) + }) + assertEqualSQL(t, `INSERT INTO "users" ("created_at","updated_at","deleted_at","name","age","birthday","company_id","manager_id","active") VALUES ('2021-10-18 00:00:00','2021-10-18 00:00:00',NULL,'foo',20,NULL,NULL,NULL,false) RETURNING "id"`, sql) + + // updates + user = &User{Name: "bar", Age: 22} + user.CreatedAt = date + user.UpdatedAt = date + sql = DB.ToSQL(func(tx *gorm.DB) *gorm.DB { + return tx.Model(&User{}).Where("id = ?", 100).Updates(user) + }) + assertEqualSQL(t, `UPDATE "users" SET "created_at"='2021-10-18 00:00:00',"updated_at"='2021-10-18 19:50:09.438',"name"='bar',"age"=22 WHERE id = 100 AND "users"."deleted_at" IS NULL`, sql) + + // update + sql = DB.ToSQL(func(tx *gorm.DB) *gorm.DB { + return tx.Model(&User{}).Where("id = ?", 100).Update("name", "Foo bar") + }) + assertEqualSQL(t, `UPDATE "users" SET "name"='Foo bar',"updated_at"='2021-10-18 19:50:09.438' WHERE id = 100 AND "users"."deleted_at" IS NULL`, sql) + + // UpdateColumn + sql = DB.ToSQL(func(tx *gorm.DB) *gorm.DB { + return tx.Model(&User{}).Where("id = ?", 100).UpdateColumn("name", "Foo bar") + }) + assertEqualSQL(t, `UPDATE "users" SET "name"='Foo bar' WHERE id = 100 AND "users"."deleted_at" IS NULL`, sql) + + // UpdateColumns + sql = DB.ToSQL(func(tx *gorm.DB) *gorm.DB { + return tx.Model(&User{}).Where("id = ?", 100).UpdateColumns(User{Name: "Foo", Age: 100}) + }) + assertEqualSQL(t, `UPDATE "users" SET "name"='Foo',"age"=100 WHERE id = 100 AND "users"."deleted_at" IS NULL`, sql) + + // after model chagned + if DB.Statement.DryRun || DB.DryRun { + t.Fatal("Failed expect DB.DryRun and DB.Statement.ToSQL to be false") + } +} + +// assertEqualSQL for assert that the sql is equal, this method will ignore quote, and dialect speicals. +func assertEqualSQL(t *testing.T, expected string, actually string) { + t.Helper() + + // replace SQL quote, convert into postgresql like "" + expected = replaceQuoteInSQL(expected) + actually = replaceQuoteInSQL(actually) + + // ignore updated_at value, becase it's generated in Gorm inernal, can't to mock value on update. + var updatedAtRe = regexp.MustCompile(`(?i)"updated_at"=".+?"`) + actually = updatedAtRe.ReplaceAllString(actually, `"updated_at"=?`) + expected = updatedAtRe.ReplaceAllString(expected, `"updated_at"=?`) + + // ignore RETURNING "id" (only in PostgreSQL) + var returningRe = regexp.MustCompile(`(?i)RETURNING "id"`) + actually = returningRe.ReplaceAllString(actually, ``) + expected = returningRe.ReplaceAllString(expected, ``) + + actually = strings.TrimSpace(actually) + expected = strings.TrimSpace(expected) + + if actually != expected { + t.Fatalf("\nexpected: %s\nactually: %s", expected, actually) + } +} + +func replaceQuoteInSQL(sql string) string { + // convert single quote into double quote + sql = strings.Replace(sql, `'`, `"`, -1) + + // convert dialect speical quote into double quote + switch DB.Dialector.Name() { + case "postgres": + sql = strings.Replace(sql, `"`, `"`, -1) + case "mysql", "sqlite": + sql = strings.Replace(sql, "`", `"`, -1) + case "sqlserver": + sql = strings.Replace(sql, `'`, `"`, -1) + } + + return sql +}