diff --git a/database/query_builder_test.go b/database/query_builder_test.go new file mode 100644 index 0000000..318b408 --- /dev/null +++ b/database/query_builder_test.go @@ -0,0 +1,337 @@ +package database + +import ( + "github.com/icinga/icinga-go-library/testutils" + "testing" +) + +type MockEntity struct { + Entity + Id int + Name string + Age int + Email string +} + +func TestInsertStatement(t *testing.T) { + tests := []testutils.TestCase[string, testutils.InsertStatementTestData]{ + { + Name: "NoColumnsSet", + Expected: `INSERT INTO "mock_entity" ("age", "email", "id", "name") VALUES (:age, :email, :id, :name)`, + }, + { + Name: "ColumnsSet", + Expected: `INSERT INTO "mock_entity" ("email", "id", "name") VALUES (:email, :id, :name)`, + Data: testutils.InsertStatementTestData{ + Columns: []string{"id", "name", "email"}, + }, + }, + { + Name: "ExcludedColumnsSet", + Expected: `INSERT INTO "mock_entity" ("age", "id", "name") VALUES (:age, :id, :name)`, + Data: testutils.InsertStatementTestData{ + ExcludedColumns: []string{"email"}, + }, + }, + { + Name: "ColumnsAndExcludedColumnsSet", + Expected: `INSERT INTO "mock_entity" ("id", "name") VALUES (:id, :name)`, + Data: testutils.InsertStatementTestData{ + Columns: []string{"id", "name", "email"}, + ExcludedColumns: []string{"email"}, + }, + }, + { + Name: "OverrideTableName", + Expected: `INSERT INTO "custom_table_name" ("email", "id", "name") VALUES (:email, :id, :name)`, + Data: testutils.InsertStatementTestData{ + Table: "custom_table_name", + Columns: []string{"id", "name", "email"}, + }, + }, + //{ + // Name: "InvalidColumnName", + // Data: testutils.InsertStatementTestData{ + // Columns: []string{"id", "name", "email", "invalid_column"}, + // ExcludedColumns: nil, + // }, + // Error: testutils.ErrorIs(ErrInvalidColumnName), + //}, + //{ + } + + for _, tst := range tests { + t.Run(tst.Name, tst.F(func(data testutils.InsertStatementTestData) (string, error) { + var actual string + var err error + + stmt := NewInsertStatement(&MockEntity{}). + SetColumns(data.Columns...). + SetExcludedColumns(data.ExcludedColumns...) + + if data.Table != "" { + stmt.Into(data.Table) + } + + qb := NewTestQueryBuilder(MySQL) + actual = qb.InsertStatement(stmt) + + return actual, err + + })) + } +} + +func TestInsertIgnoreStatement(t *testing.T) { + tests := []testutils.TestCase[string, testutils.InsertIgnoreStatementTestData]{ + { + Name: "NoColumnsSet_MySQL", + Expected: `INSERT IGNORE INTO "mock_entity" ("age", "email", "id", "name") VALUES (:age, :email, :id, :name)`, + Data: testutils.InsertIgnoreStatementTestData{ + Driver: MySQL, + }, + }, + { + Name: "ColumnsSet_MySQL", + Expected: `INSERT IGNORE INTO "mock_entity" ("email", "id", "name") VALUES (:email, :id, :name)`, + Data: testutils.InsertIgnoreStatementTestData{ + Driver: MySQL, + Columns: []string{"id", "name", "email"}, + }, + }, + { + Name: "ExcludedColumnsSet_MySQL", + Expected: `INSERT IGNORE INTO "mock_entity" ("age", "id", "name") VALUES (:age, :id, :name)`, + Data: testutils.InsertIgnoreStatementTestData{ + Driver: MySQL, + ExcludedColumns: []string{"email"}, + }, + }, + { + Name: "ColumnsAndExcludedColumnsSet_MySQL", + Expected: `INSERT IGNORE INTO "mock_entity" ("id", "name") VALUES (:id, :name)`, + Data: testutils.InsertIgnoreStatementTestData{ + Driver: MySQL, + Columns: []string{"id", "name", "email"}, + ExcludedColumns: []string{"email"}, + }, + }, + { + Name: "OverrideTableName_MySQL", + Expected: `INSERT IGNORE INTO "custom_table_name" ("email", "id", "name") VALUES (:email, :id, :name)`, + Data: testutils.InsertIgnoreStatementTestData{ + Driver: MySQL, + Table: "custom_table_name", + Columns: []string{"id", "name", "email"}, + }, + }, + { + Name: "NoColumnsSet_PostgreSQL", + Expected: `INSERT INTO "mock_entity" ("age", "email", "id", "name") VALUES (:age, :email, :id, :name) ON CONFLICT DO NOTHING`, + Data: testutils.InsertIgnoreStatementTestData{ + Driver: PostgreSQL, + }, + }, + { + Name: "ColumnsSet_PostgreSQL", + Expected: `INSERT INTO "mock_entity" ("email", "id", "name") VALUES (:email, :id, :name) ON CONFLICT DO NOTHING`, + Data: testutils.InsertIgnoreStatementTestData{ + Driver: PostgreSQL, + Columns: []string{"id", "name", "email"}, + }, + }, + { + Name: "ExcludedColumnsSet_PostgreSQL", + Expected: `INSERT INTO "mock_entity" ("age", "id", "name") VALUES (:age, :id, :name) ON CONFLICT DO NOTHING`, + Data: testutils.InsertIgnoreStatementTestData{ + Driver: PostgreSQL, + ExcludedColumns: []string{"email"}, + }, + }, + { + Name: "ColumnsAndExcludedColumnsSet_PostgreSQL", + Expected: `INSERT INTO "mock_entity" ("id", "name") VALUES (:id, :name) ON CONFLICT DO NOTHING`, + Data: testutils.InsertIgnoreStatementTestData{ + Driver: PostgreSQL, + Columns: []string{"id", "name", "email"}, + ExcludedColumns: []string{"email"}, + }, + }, + { + Name: "OverrideTableName_PostgreSQL", + Expected: `INSERT INTO "custom_table_name" ("email", "id", "name") VALUES (:email, :id, :name) ON CONFLICT DO NOTHING`, + Data: testutils.InsertIgnoreStatementTestData{ + Driver: PostgreSQL, + Table: "custom_table_name", + Columns: []string{"id", "name", "email"}, + ExcludedColumns: nil, + }, + }, + { + Name: "UnsupportedDriver", + Error: testutils.ErrorIs(ErrUnsupportedDriver), + Data: testutils.InsertIgnoreStatementTestData{ + Driver: "abcxyz", // Unsupported driver + Columns: []string{"id", "name", "email"}, + ExcludedColumns: nil, + }, + }, + } + + for _, tst := range tests { + t.Run(tst.Name, tst.F(func(data testutils.InsertIgnoreStatementTestData) (string, error) { + var actual string + var err error + + stmt := NewInsertStatement(&MockEntity{}). + SetColumns(data.Columns...). + SetExcludedColumns(data.ExcludedColumns...) + + if data.Table != "" { + stmt.Into(data.Table) + } + + qb := NewTestQueryBuilder(data.Driver) + actual, err = qb.InsertIgnoreStatement(stmt) + + return actual, err + + })) + } +} + +func TestInsertSelectStatement(t *testing.T) { + tests := []testutils.TestCase[string, testutils.InsertSelectStatementTestData]{ + { + Name: "ColumnsSet", + Expected: `INSERT INTO "mock_entity" ("email", "id", "name") SELECT "email", "id", "name" FROM "mock_entity" WHERE id = :id`, + Data: testutils.InsertSelectStatementTestData{ + Columns: []string{"id", "name", "email"}, + Select: NewSelectStatement(&MockEntity{}).SetColumns("id", "name", "email").SetWhere("id = :id"), + }, + }, + { + Name: "ExcludedColumnsSet", + Expected: `INSERT INTO "mock_entity" ("age", "id", "name") SELECT "age", "id", "name" FROM "mock_entity" WHERE id = :id`, + Data: testutils.InsertSelectStatementTestData{ + ExcludedColumns: []string{"email"}, + Select: NewSelectStatement(&MockEntity{}).SetExcludedColumns("email").SetWhere("id = :id"), + }, + }, + { + Name: "ColumnsAndExcludedColumnsSet", + Expected: `INSERT INTO "mock_entity" ("id", "name") SELECT "id", "name" FROM "mock_entity" WHERE id = :id`, + Data: testutils.InsertSelectStatementTestData{ + Columns: []string{"id", "name", "email"}, + ExcludedColumns: []string{"email"}, + Select: NewSelectStatement(&MockEntity{}).SetColumns("id", "name", "email").SetExcludedColumns("email").SetWhere("id = :id"), + }, + }, + { + Name: "OverrideTableName", + Expected: `INSERT INTO "custom_table_name" ("email", "id", "name") SELECT "email", "id", "name" FROM "mock_entity" WHERE id = :id`, + Data: testutils.InsertSelectStatementTestData{ + Table: "custom_table_name", + Columns: []string{"id", "name", "email"}, + Select: NewSelectStatement(&MockEntity{}).SetColumns("id", "name", "email").SetWhere("id = :id"), + }, + }, + { + Name: "SelectStatementMissing", + Error: testutils.ErrorIs(ErrMissingStatementPart), + Data: testutils.InsertSelectStatementTestData{}, + }, + //{ + // Name: "InvalidColumnName", + // Data: testutils.InsertStatementTestData{ + // Columns: []string{"id", "name", "email", "invalid_column"}, + // ExcludedColumns: nil, + // }, + // Error: testutils.ErrorIs(ErrInvalidColumnName), + //}, + } + + for _, tst := range tests { + t.Run(tst.Name, tst.F(func(data testutils.InsertSelectStatementTestData) (string, error) { + var actual string + var err error + + stmt := NewInsertSelectStatement(&MockEntity{}). + SetColumns(data.Columns...). + SetExcludedColumns(data.ExcludedColumns...) + + if data.Select != nil { + stmt.SetSelect(data.Select.(SelectStatement)) + } + + if data.Table != "" { + stmt.Into(data.Table) + } + + qb := NewTestQueryBuilder(MySQL) + actual, err = qb.InsertSelectStatement(stmt) + + return actual, err + + })) + } +} + +func TestUpdateStatement(t *testing.T) { + tests := []testutils.TestCase[string, testutils.UpdateStatementTestData]{ + { + Name: "NoWhereSet", + Error: testutils.ErrorIs(ErrMissingStatementPart), + }, + { + Name: "ColumnsSet", + Expected: `UPDATE "mock_entity" SET "email" = :email, "name" = :name WHERE id = :id`, + Data: testutils.UpdateStatementTestData{ + Columns: []string{"name", "email"}, + Where: "id = :id", + }, + }, + { + Name: "ExcludedColumnsSet", + Expected: `UPDATE "mock_entity" SET "email" = :email, "name" = :name WHERE id = :id`, + Data: testutils.UpdateStatementTestData{ + ExcludedColumns: []string{"id", "age"}, + Where: "id = :id", + }, + }, + { + Name: "OverrideTableName", + Expected: `UPDATE "custom_table_name" SET "email" = :email, "id" = :id, "name" = :name WHERE id = :id`, + Data: testutils.UpdateStatementTestData{ + Table: "custom_table_name", + Columns: []string{"id", "name", "email"}, + Where: "id = :id", + }, + }, + } + + for _, tst := range tests { + t.Run(tst.Name, tst.F(func(data testutils.UpdateStatementTestData) (string, error) { + var actual string + var err error + + stmt := NewUpdateStatement(&MockEntity{}). + SetColumns(data.Columns...). + SetExcludedColumns(data.ExcludedColumns...) + + if data.Where != "" { + stmt.SetWhere(data.Where) + } + + if data.Table != "" { + stmt.SetTable(data.Table) + } + + qb := NewTestQueryBuilder(MySQL) + actual, err = qb.UpdateStatement(stmt) + + return actual, err + + })) + } +} diff --git a/testutils/testutils.go b/testutils/testutils.go index 6fa4cf2..1b30343 100644 --- a/testutils/testutils.go +++ b/testutils/testutils.go @@ -51,6 +51,54 @@ type ConfigTestData struct { Env map[string]string } +type InsertStatementTestData struct { + Table string + Columns []string + ExcludedColumns []string +} + +type InsertIgnoreStatementTestData struct { + Driver string + Table string + Columns []string + ExcludedColumns []string +} + +type InsertSelectStatementTestData struct { + Table string + Columns []string + ExcludedColumns []string + + // Should be SelectStatement but cannot because of import cycle + Select any +} + +type UpdateStatementTestData struct { + Table string + Columns []string + ExcludedColumns []string + Where string +} + +type UpsertStatementTestData struct { + Driver string + Table string + Columns []string + ExcludedColumns []string +} + +type DeleteStatementTestData struct { + Table string + Where string +} + +type SelectStatementTestData struct { + Table string + Columns []string + ExcludedColumns []string + Where string +} + // ErrorAs returns a function that checks if the error is of a specific type T. // This is useful for verifying that an error matches a particular interface or concrete type. func ErrorAs[T error]() func(t *testing.T, err error) {