From 87cf5221c2ea37cd91c57de7cd786d5c2954fc97 Mon Sep 17 00:00:00 2001 From: Rafael Dantas <36964299+dantasrafael@users.noreply.github.com> Date: Tue, 14 May 2024 11:56:34 -0300 Subject: [PATCH] feat: increase database module (#86) * feat: increase database module * feat: isodate and isotime tests --- development-environment/database/add-dogs.sql | 3 - .../database/clear-database.sql | 2 +- development-environment/database/schema.sql | 11 +- .../database/sql-tx/clear.sql | 1 - .../database/sql-tx/schema.sql | 5 - pkg/base/transaction/mock_transaction.go | 14 +- pkg/base/transaction/mock_transaction_test.go | 7 +- pkg/base/transaction/transaction.go | 3 +- pkg/base/types/iso_date.go | 43 +- pkg/base/types/iso_date_test.go | 36 ++ pkg/base/types/iso_time.go | 41 +- pkg/base/types/iso_time_test.go | 36 ++ pkg/base/validator/validator.go | 53 ++- pkg/database/cacheDB/cache_test.go | 77 ++-- pkg/database/sqlDB/migrations.go | 53 ++- pkg/database/sqlDB/migrations_test.go | 18 +- pkg/database/sqlDB/page_query.go | 92 ++++- pkg/database/sqlDB/page_query_test.go | 67 +--- pkg/database/sqlDB/query.go | 91 ++++- pkg/database/sqlDB/query_test.go | 368 ++++++------------ pkg/database/sqlDB/sql_db.go | 88 +++-- pkg/database/sqlDB/sql_db_test.go | 19 + pkg/database/sqlDB/sql_transaction.go | 71 +++- pkg/database/sqlDB/sql_transaction_test.go | 115 ++---- pkg/database/sqlDB/statement.go | 38 +- pkg/database/sqlDB/statement_test.go | 57 +-- 26 files changed, 801 insertions(+), 608 deletions(-) delete mode 100644 development-environment/database/add-dogs.sql delete mode 100644 development-environment/database/sql-tx/clear.sql delete mode 100644 development-environment/database/sql-tx/schema.sql diff --git a/development-environment/database/add-dogs.sql b/development-environment/database/add-dogs.sql deleted file mode 100644 index f88ae55..0000000 --- a/development-environment/database/add-dogs.sql +++ /dev/null @@ -1,3 +0,0 @@ -insert into dog (name, characteristics) -values ('Pitty', '{"mad","destructive"}'), - ('Stella', '{"cute"}'); diff --git a/development-environment/database/clear-database.sql b/development-environment/database/clear-database.sql index 5e65c7f..b200197 100644 --- a/development-environment/database/clear-database.sql +++ b/development-environment/database/clear-database.sql @@ -1,3 +1,3 @@ delete from users; delete from profiles; -delete from dog; +delete from contacts; diff --git a/development-environment/database/schema.sql b/development-environment/database/schema.sql index 0be4b78..8006f25 100644 --- a/development-environment/database/schema.sql +++ b/development-environment/database/schema.sql @@ -11,9 +11,8 @@ CREATE TABLE IF NOT EXISTS users ( CONSTRAINT fk_users_profiles FOREIGN KEY (profile_id) REFERENCES profiles (id) ); -CREATE TABLE IF NOT EXISTS dog -( - id SERIAL PRIMARY KEY, - name TEXT NOT NULL, - characteristics TEXT[] -); +CREATE TABLE IF NOT EXISTS contacts ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + email TEXT UNIQUE +); \ No newline at end of file diff --git a/development-environment/database/sql-tx/clear.sql b/development-environment/database/sql-tx/clear.sql deleted file mode 100644 index 101cb62..0000000 --- a/development-environment/database/sql-tx/clear.sql +++ /dev/null @@ -1 +0,0 @@ -delete from contacts; diff --git a/development-environment/database/sql-tx/schema.sql b/development-environment/database/sql-tx/schema.sql deleted file mode 100644 index cb4072b..0000000 --- a/development-environment/database/sql-tx/schema.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE IF NOT EXISTS contacts ( - id SERIAL PRIMARY KEY, - name TEXT NOT NULL, - email TEXT UNIQUE -); diff --git a/pkg/base/transaction/mock_transaction.go b/pkg/base/transaction/mock_transaction.go index 1f377a0..ce23b9a 100644 --- a/pkg/base/transaction/mock_transaction.go +++ b/pkg/base/transaction/mock_transaction.go @@ -2,15 +2,23 @@ package transaction import "context" -// mockSqlTransaction struct with implements Transaction for tests purpose +// mockSqlTransaction implements a transaction.Transaction type mockSqlTransaction struct { } -// NewMockTransaction create a mock transaction for tests purpose +// NewMockTransaction returns a new mockSqlTransaction. +// +// No parameters. +// Returns a Transaction. func NewMockTransaction() Transaction { return mockSqlTransaction{} } -func (m mockSqlTransaction) ExecTx(ctx context.Context, fn func(ctx context.Context) error) error { +// Execute executes a mockSqlTransaction. +// +// ctx: The context for the transaction. +// fn: The function to be executed. +// Returns an error. +func (m mockSqlTransaction) Execute(ctx context.Context, fn func(ctx context.Context) error) error { return fn(ctx) } diff --git a/pkg/base/transaction/mock_transaction_test.go b/pkg/base/transaction/mock_transaction_test.go index 23adb51..727e498 100644 --- a/pkg/base/transaction/mock_transaction_test.go +++ b/pkg/base/transaction/mock_transaction_test.go @@ -4,8 +4,9 @@ import ( "context" "errors" "fmt" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestMockTransaction(t *testing.T) { @@ -13,7 +14,7 @@ func TestMockTransaction(t *testing.T) { f := func(ctx context.Context) error { return nil } m := NewMockTransaction() - assert.NoError(t, m.ExecTx(context.Background(), f)) + assert.NoError(t, m.Execute(context.Background(), f)) }) t.Run("execute with error", func(t *testing.T) { @@ -23,6 +24,6 @@ func TestMockTransaction(t *testing.T) { } m := NewMockTransaction() - assert.ErrorIs(t, m.ExecTx(context.Background(), f), expectedErr) + assert.ErrorIs(t, m.Execute(context.Background(), f), expectedErr) }) } diff --git a/pkg/base/transaction/transaction.go b/pkg/base/transaction/transaction.go index 18e3deb..1b797bd 100644 --- a/pkg/base/transaction/transaction.go +++ b/pkg/base/transaction/transaction.go @@ -4,6 +4,7 @@ import ( "context" ) +// Transaction defines the interface for a transaction type Transaction interface { - ExecTx(context.Context, func(ctx context.Context) error) error + Execute(context.Context, func(ctx context.Context) error) error } diff --git a/pkg/base/types/iso_date.go b/pkg/base/types/iso_date.go index 8ec7054..b7d5503 100644 --- a/pkg/base/types/iso_date.go +++ b/pkg/base/types/iso_date.go @@ -9,17 +9,54 @@ import ( // IsoDate struct type IsoDate time.Time -// Value converts iso date to sql driver value +// ParseIsoDate converts string to iso date. +// +// It takes a string value as input. +// Returns IsoDate and an error. +func ParseIsoDate(value string) (IsoDate, error) { + parsedDate, err := time.Parse(time.DateOnly, value) + if err != nil { + return IsoDate{}, err + } + + return IsoDate(parsedDate), nil +} + +// Value converts iso date to sql driver value. +// +// Returns driver.Value and an error. func (t IsoDate) Value() (driver.Value, error) { return time.Time(t), nil } -// MarshalJSON converts iso date to json string format +// String returns the iso date formatted using the format string. +// +// No parameters. +// Returns a string. +func (t IsoDate) String() string { + return time.Time(t).Format(time.DateOnly) +} + +// GoString returns the iso date in Go source code format string. +// +// No parameters. +// Returns a string. +func (t IsoDate) GoString() string { + return time.Time(t).GoString() +} + +// MarshalJSON converts iso date to json string format. +// +// No parameters. +// Returns a byte slice and an error. func (t IsoDate) MarshalJSON() ([]byte, error) { return json.Marshal(time.Time(t).Format(time.DateOnly)) } -// UnmarshalJSON converts json string to iso date +// UnmarshalJSON converts json string to iso date. +// +// It takes a byte slice as the input data. +// Returns an error. func (t *IsoDate) UnmarshalJSON(data []byte) error { var ptr *string if err := json.Unmarshal(data, &ptr); err != nil { diff --git a/pkg/base/types/iso_date_test.go b/pkg/base/types/iso_date_test.go index f1a9c37..dcf604f 100644 --- a/pkg/base/types/iso_date_test.go +++ b/pkg/base/types/iso_date_test.go @@ -8,6 +8,42 @@ import ( ) func TestIsoDate(t *testing.T) { + t.Run("Should get parsed iso date", func(t *testing.T) { + expected := IsoDate(time.Date(2022, time.January, 30, 0, 0, 0, 0, time.UTC)) + + result, err := ParseIsoDate("2022-01-30") + + assert.Nil(t, err) + assert.NotNil(t, result) + assert.Equal(t, expected, result) + }) + + t.Run("Should return error when parse with a invalid string", func(t *testing.T) { + result, err := ParseIsoDate("invalid") + + assert.NotNil(t, err) + assert.NotNil(t, result) + assert.Equal(t, IsoDate(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)), result) + }) + + t.Run("Should get string iso date", func(t *testing.T) { + expected := "2022-01-30" + + result := IsoDate(time.Date(2022, time.January, 30, 0, 0, 0, 0, time.UTC)).String() + + assert.NotNil(t, result) + assert.Equal(t, expected, result) + }) + + t.Run("Should get go string iso date", func(t *testing.T) { + expected := "time.Date(2022, time.January, 30, 0, 0, 0, 0, time.UTC)" + + result := IsoDate(time.Date(2022, time.January, 30, 0, 0, 0, 0, time.UTC)).GoString() + + assert.NotNil(t, result) + assert.Equal(t, expected, result) + }) + t.Run("Should get value with a valid value", func(t *testing.T) { expected := time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC) diff --git a/pkg/base/types/iso_time.go b/pkg/base/types/iso_time.go index 53440fc..cce105b 100644 --- a/pkg/base/types/iso_time.go +++ b/pkg/base/types/iso_time.go @@ -9,17 +9,54 @@ import ( // IsoTime struct type IsoTime time.Time -// Value converts iso time to sql driver value +// ParseIsoTime converts string to iso time. +// +// It takes a string value as input. +// Returns IsoTime and an error. +func ParseIsoTime(value string) (IsoTime, error) { + parsedTime, err := time.Parse(time.TimeOnly, value) + if err != nil { + return IsoTime{}, err + } + + return IsoTime(parsedTime), nil +} + +// Value converts iso time to sql driver value. +// +// Returns driver.Value and an error. func (t IsoTime) Value() (driver.Value, error) { return time.Time(t), nil } -// MarshalJSON converts iso time to json string format +// String returns the iso time formatted using the format string +// +// No parameters. +// Returns a string. +func (t IsoTime) String() string { + return time.Time(t).Format(time.TimeOnly) +} + +// GoString returns the iso time in Go source code format string. +// +// No parameters. +// Returns a string. +func (t IsoTime) GoString() string { + return time.Time(t).GoString() +} + +// MarshalJSON converts iso time to json string format. +// +// No parameters. +// Returns a byte slice and an error. func (t IsoTime) MarshalJSON() ([]byte, error) { return json.Marshal(time.Time(t).Format(time.TimeOnly)) } // UnmarshalJSON converts json string to iso time +// +// It takes a byte slice as the input data. +// Returns an error. func (t *IsoTime) UnmarshalJSON(data []byte) error { var ptr *string if err := json.Unmarshal(data, &ptr); err != nil { diff --git a/pkg/base/types/iso_time_test.go b/pkg/base/types/iso_time_test.go index c80aa1a..c629c98 100644 --- a/pkg/base/types/iso_time_test.go +++ b/pkg/base/types/iso_time_test.go @@ -8,6 +8,42 @@ import ( ) func TestIsoTime(t *testing.T) { + t.Run("Should get parsed iso time", func(t *testing.T) { + expected := IsoTime(time.Date(0, time.January, 1, 10, 20, 30, 0, time.UTC)) + + result, err := ParseIsoTime("10:20:30") + + assert.Nil(t, err) + assert.NotNil(t, result) + assert.Equal(t, expected, result) + }) + + t.Run("Should return error when parse with a invalid string", func(t *testing.T) { + result, err := ParseIsoTime("invalid") + + assert.NotNil(t, err) + assert.NotNil(t, result) + assert.Equal(t, IsoTime(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)), result) + }) + + t.Run("Should get string iso time", func(t *testing.T) { + expected := "10:20:30" + + result := IsoTime(time.Date(0, time.January, 1, 10, 20, 30, 0, time.UTC)).String() + + assert.NotNil(t, result) + assert.Equal(t, expected, result) + }) + + t.Run("Should get go string iso time", func(t *testing.T) { + expected := "time.Date(0, time.January, 1, 10, 20, 30, 0, time.UTC)" + + result := IsoTime(time.Date(0, time.January, 1, 10, 20, 30, 0, time.UTC)).GoString() + + assert.NotNil(t, result) + assert.Equal(t, expected, result) + }) + t.Run("Should get value with a valid value", func(t *testing.T) { expected := time.Date(1, time.January, 1, 10, 20, 30, 0, time.UTC) diff --git a/pkg/base/validator/validator.go b/pkg/base/validator/validator.go index b1c609c..94337d3 100644 --- a/pkg/base/validator/validator.go +++ b/pkg/base/validator/validator.go @@ -1,6 +1,7 @@ package validator import ( + "github.com/colibri-project-io/colibri-sdk-go/pkg/base/types" form "github.com/go-playground/form/v4" playValidator "github.com/go-playground/validator/v10" "github.com/google/uuid" @@ -13,25 +14,63 @@ type Validator struct { var instance *Validator +// Initialize initializes the Validator instance with playValidator and formDecoder, then registers custom types. +// +// No parameters. +// No return values. func Initialize() { instance = &Validator{ validator: playValidator.New(), formDecoder: form.NewDecoder(), } - registerCustomTypes() + registerUUIDCustomType() + registerIsoDateCustomType() + registerIsoTimeCustomType() } +// registerUUIDCustomType registers a custom type function for UUID parsing. +// +// It takes an array of strings as input parameters and returns an any type and an error. +func registerUUIDCustomType() { + instance.formDecoder.RegisterCustomTypeFunc(func(vals []string) (any, error) { + return uuid.Parse(vals[0]) + }, uuid.UUID{}) +} + +// registerIsoDateCustomType registers a custom type function for ISO date parsing. +// +// It takes an array of strings as input parameters and returns an interface{} and an error. +func registerIsoDateCustomType() { + instance.formDecoder.RegisterCustomTypeFunc(func(vals []string) (interface{}, error) { + return types.ParseIsoDate(vals[0]) + }, types.IsoDate{}) +} + +// registerIsoTimeCustomType registers a custom type function for ISO time parsing. +// +// It takes an array of strings as input parameters and returns an interface{} and an error. +func registerIsoTimeCustomType() { + instance.formDecoder.RegisterCustomTypeFunc(func(vals []string) (interface{}, error) { + return types.ParseIsoTime(vals[0]) + }, types.IsoTime{}) +} + +// Struct performs validation on the provided object using the validator instance. +// +// Parameter: +// - object: the object to be validated +// Return type: error func Struct(object any) error { return instance.validator.Struct(object) } +// FormDecode decodes the values from the map[string][]string into the provided object using the formDecoder instance. +// +// Parameters: +// - object: the object to be decoded +// - values: the map containing the values to be decoded +// Return type: error func FormDecode(object any, values map[string][]string) error { return instance.formDecoder.Decode(object, values) } - -func registerCustomTypes() { - instance.formDecoder.RegisterCustomTypeFunc(func(vals []string) (any, error) { - return uuid.Parse(vals[0]) - }, uuid.UUID{}) -} diff --git a/pkg/database/cacheDB/cache_test.go b/pkg/database/cacheDB/cache_test.go index ad262f9..20d62fb 100644 --- a/pkg/database/cacheDB/cache_test.go +++ b/pkg/database/cacheDB/cache_test.go @@ -15,77 +15,80 @@ type userCached struct { } func TestCacheNotInitializedValidation(t *testing.T) { + instance = nil + t.Run("Should return error when cacheDB is not initialized", func(t *testing.T) { - instance = nil cache := NewCache[userCached]("cache-test", time.Hour) - _, err := cache.Many(context.Background()) + result, err := cache.Many(context.Background()) + assert.NotNil(t, err) + assert.Nil(t, result) }) } func TestCache(t *testing.T) { + test.InitializeCacheDBTest() + Initialize() + ctx := context.Background() + cacheWithEmptyName := Cache[userCached]{} + cache := NewCache[userCached]("cache-test", time.Hour) expected := []userCached{ {Id: 1, Name: "User 1"}, {Id: 2, Name: "User 2"}, {Id: 3, Name: "User 3"}, } - test.InitializeCacheDBTest() - Initialize() t.Run("Should return a new point of cache", func(t *testing.T) { result := NewCache[userCached]("cache-test", time.Hour) + assert.NotNil(t, result) assert.NotNil(t, result.name) assert.NotNil(t, result.ttl) }) t.Run("Should return error when cache name is empty value on call many", func(t *testing.T) { - cache := Cache[userCached]{} + result, err := cacheWithEmptyName.Many(ctx) - _, err := cache.Many(ctx) assert.NotNil(t, err) + assert.Nil(t, result) }) t.Run("Should return error when cache name is empty value on call one", func(t *testing.T) { - cache := Cache[userCached]{} + result, err := cacheWithEmptyName.One(ctx) - _, err := cache.One(ctx) assert.NotNil(t, err) + assert.Nil(t, result) }) t.Run("Should return error when cache name is empty value on call set", func(t *testing.T) { - cache := Cache[userCached]{} + err := cacheWithEmptyName.Set(ctx, cacheWithEmptyName) - err := cache.Set(ctx, cache) assert.NotNil(t, err) }) t.Run("Should return error when cache name is empty value on call del", func(t *testing.T) { - cache := Cache[userCached]{} + err := cacheWithEmptyName.Del(ctx) - err := cache.Del(ctx) assert.NotNil(t, err) }) t.Run("Should return error when occurred error in json unmarshal on set data in cache", func(t *testing.T) { - cache := NewCache[userCached]("cache-test", time.Hour) invalid := map[string]interface{}{ "invalid": make(chan int), } err := cache.Set(ctx, invalid) + assert.NotNil(t, err) }) t.Run("Should set many data in cache", func(t *testing.T) { - cache := NewCache[userCached]("cache-test", time.Hour) - - err := cache.Set(ctx, expected) - assert.Nil(t, err) - + setErr := cache.Set(ctx, expected) result, err := cache.Many(ctx) + + assert.Nil(t, setErr) assert.Nil(t, err) assert.NotNil(t, result) assert.Len(t, result, 3) @@ -93,34 +96,28 @@ func TestCache(t *testing.T) { }) t.Run("Should set one data in cache", func(t *testing.T) { - cache := NewCache[userCached]("cache-test", time.Hour) - - err := cache.Set(ctx, expected[0]) - assert.Nil(t, err) - + setErr := cache.Set(ctx, expected[0]) result, err := cache.One(ctx) + + assert.Nil(t, setErr) assert.Nil(t, err) assert.NotNil(t, result) assert.Equal(t, expected[0], *result) }) t.Run("Should del data in cache", func(t *testing.T) { - cache := NewCache[userCached]("cache-test", time.Hour) - - err := cache.Set(ctx, expected) - assert.Nil(t, err) - - result, err := cache.Many(ctx) - assert.Nil(t, err) - assert.NotNil(t, result) - assert.Len(t, result, 3) - assert.Equal(t, expected, result) - - err = cache.Del(ctx) - assert.Nil(t, err) - - result, err = cache.Many(ctx) - assert.Nil(t, err) - assert.Nil(t, result) + setErr := cache.Set(ctx, expected) + manyInitialResult, manyInitialErr := cache.Many(ctx) + delErr := cache.Del(ctx) + manyFinalResult, manyFinalErr := cache.Many(ctx) + + assert.NoError(t, setErr) + assert.NoError(t, manyInitialErr) + assert.NotNil(t, manyInitialResult) + assert.Len(t, manyInitialResult, 3) + assert.Equal(t, expected, manyInitialResult) + assert.NoError(t, delErr) + assert.NoError(t, manyFinalErr) + assert.Nil(t, manyFinalResult) }) } diff --git a/pkg/database/sqlDB/migrations.go b/pkg/database/sqlDB/migrations.go index 6de92d0..50fe8bc 100644 --- a/pkg/database/sqlDB/migrations.go +++ b/pkg/database/sqlDB/migrations.go @@ -1,8 +1,8 @@ package sqlDB import ( + "database/sql" "errors" - "fmt" "os" "github.com/colibri-project-io/colibri-sdk-go/pkg/base/config" @@ -12,36 +12,53 @@ import ( _ "github.com/golang-migrate/migrate/v4/source/file" ) -func migrations() error { +const ( + migrationSourceURLEnv string = "MIGRATION_SOURCE_URL" + migrationWithPwdDefaultPath string = "${PWD}/migrations" + migrationDefaultPath string = "./migrations" + + migrationIgnoringMsg string = "Ignoring migration because env variable SQL_DB_MIGRATION is set to false" + migrationEnvNotSetUsingDefaultMsg string = "Migration env variable %s is not set, using default value %s" + migrationStartingMsg string = "Starting migration execution" + migrationCouldNotConnectDBMsg string = "Could not connect to database for migration: %v" + migrationExecutingInfoMsg string = "Executing migration on path: %s" + migrationExecutionWithErrorMsg string = "An error when executing database migration: %v" + migrationFinalizedMsg string = "Migration finalized successfully" +) + +// executeDatabaseMigration performs database migrations based on the provided source URL. +// +// It checks if the SQL_DB_MIGRATION environment variable is set to true before proceeding. +// It uses the MIGRATION_SOURCE_URL environment variable for migration source. If not set, it defaults to "./migrations". +// Returns an error if there is a failure during migration execution. +func executeDatabaseMigration(instance *sql.DB) error { if !config.SQL_DB_MIGRATION { - logging.Info("Ignoring migration because env variable SQL_DB_MIGRATION is set to false") + logging.Info(migrationIgnoringMsg) return nil } - sourceUrl := os.Getenv("MIGRATION_SOURCE_URL") + sourceUrl := os.Getenv(migrationSourceURLEnv) if sourceUrl == "" { - logging.Warn("Migration env variable MIGRATION_SOURCE_URL is empty, using default value ${PWD}/migrations") - sourceUrl = "./migrations" + logging.Warn(migrationEnvNotSetUsingDefaultMsg, migrationSourceURLEnv, migrationWithPwdDefaultPath) + sourceUrl = migrationDefaultPath } - logging.Info("Starting migration execution") + logging.Info(migrationStartingMsg) driver, err := postgres.WithInstance(instance, &postgres.Config{}) if err != nil { - return fmt.Errorf("database: could not create migration connection: %v", err) + logging.Error(migrationCouldNotConnectDBMsg, err) + return err } - logging.Info("Executing migrations on path: %s", sourceUrl) - m, _ := migrate.NewWithDatabaseInstance( - "file://"+sourceUrl, - config.SQL_DB_NAME, driver, - ) - - if m != nil { - if err = m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { - return fmt.Errorf("database: error when executing database migration: %v", err) + logging.Info(migrationExecutingInfoMsg, sourceUrl) + migrateDatabaseInstance, _ := migrate.NewWithDatabaseInstance("file://"+sourceUrl, config.SQL_DB_NAME, driver) + if migrateDatabaseInstance != nil { + if err = migrateDatabaseInstance.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { + logging.Error(migrationExecutionWithErrorMsg, err) + return err } } - logging.Info("Finalized migrations execution.") + logging.Info(migrationFinalizedMsg) return nil } diff --git a/pkg/database/sqlDB/migrations_test.go b/pkg/database/sqlDB/migrations_test.go index e0a75d1..3a3d64a 100644 --- a/pkg/database/sqlDB/migrations_test.go +++ b/pkg/database/sqlDB/migrations_test.go @@ -12,21 +12,15 @@ import ( ) func TestMigrations(t *testing.T) { - basePath := test.MountAbsolutPath(test.DATABASE_ENVIRONMENT_PATH) - assert.NoError(t, os.Setenv(config.ENV_SQL_DB_MIGRATION, "true")) - - t.Run("should execute migration successfully and find users", func(t *testing.T) { - assert.NoError(t, os.Setenv("MIGRATION_SOURCE_URL", fmt.Sprintf("%smigrations", test.DATABASE_ENVIRONMENT_PATH))) - - test.InitializeSqlDBTest() - pc := test.UsePostgresContainer() - - Initialize() - - assert.NoError(t, pc.Dataset(basePath, "clear-database.sql", "add-users.sql")) + InitializeSqlDBTest() + os.Setenv(config.ENV_SQL_DB_MIGRATION, "true") + os.Setenv("MIGRATION_SOURCE_URL", fmt.Sprintf("%smigrations", test.DATABASE_ENVIRONMENT_PATH)) + t.Run("Should execute migration successfully and find users", func(t *testing.T) { const query = "SELECT u.id, u.name, u.birthday, p.id, p.name FROM users u INNER JOIN profiles p ON p.id = u.profile_id" + result, err := NewQuery[User](context.Background(), query).Many() + assert.NoError(t, err) assert.NotNil(t, result) assert.Len(t, result, 2) diff --git a/pkg/database/sqlDB/page_query.go b/pkg/database/sqlDB/page_query.go index be36568..8edb4f8 100644 --- a/pkg/database/sqlDB/page_query.go +++ b/pkg/database/sqlDB/page_query.go @@ -2,13 +2,19 @@ package sqlDB import ( "context" + "database/sql" "errors" "fmt" "github.com/colibri-project-io/colibri-sdk-go/pkg/base/types" ) -// PageQuery struct +const ( + pageTotalPostgresQuery string = "SELECT COUNT(tb.*) FROM (%s) tb" + pageDataPostgresQuery string = "%s ORDER BY %s LIMIT %d OFFSET %d" +) + +// PageQuery is a struct for sql page query type PageQuery[T any] struct { ctx context.Context page *types.PageRequest @@ -16,40 +22,68 @@ type PageQuery[T any] struct { args []interface{} } -// NewPageQuery create a new pointer to PageQuery struct +// NewPageQuery creates a new pointer to PageQuery struct. +// +// ctx: the context.Context for the query +// page: the types.PageRequest for the query +// query: the query string to execute +// params: variadic interface{} for additional parameters +// Returns a pointer to PageQuery struct func NewPageQuery[T any](ctx context.Context, page *types.PageRequest, query string, params ...interface{}) *PageQuery[T] { return &PageQuery[T]{ctx, page, query, params} } -// Execute returns a pointer of page type with slice of T data +// Execute returns a pointer of page type with slice of T data. +// +// No parameters. +// Returns a pointer to PageQuery struct and an error. func (q *PageQuery[T]) Execute() (*types.Page[T], error) { - if err := q.validate(); err != nil { + return q.ExecuteInInstance(sqlDBInstance) +} + +// ExecuteInInstance executes the page query in the given database instance. +// +// Parameters: +// - instance: the database instance to execute the query in. +// Returns a Page of type T and an error. +func (q *PageQuery[T]) ExecuteInInstance(instance *sql.DB) (*types.Page[T], error) { + if err := q.validate(instance); err != nil { return nil, err } var result types.Page[T] var err error - result.TotalElements, err = q.pageTotal() + result.TotalElements, err = q.pageTotal(instance) if err != nil { return nil, err } - result.Content, err = q.pageData() + result.Content, err = q.pageData(instance) return &result, err } -func (q *PageQuery[T]) pageTotal() (uint64, error) { - query := fmt.Sprintf("SELECT COUNT(tb.*) FROM (%s) tb", q.query) +// pageTotal calculates the total number of records in the query result. +// +// Parameters: +// - instance: the database instance to execute the query in. +// Returns a uint64 representing the total number of records and an error. +func (q *PageQuery[T]) pageTotal(instance *sql.DB) (uint64, error) { + query := fmt.Sprintf(pageTotalPostgresQuery, q.query) var result uint64 - err := instance.QueryRowContext(q.ctx, query, q.args...).Scan(&result) + err := q.queryRowContext(instance, query).Scan(&result) return result, err } -func (q *PageQuery[T]) pageData() ([]T, error) { - query := fmt.Sprintf("%s ORDER BY %s LIMIT %d OFFSET %d", q.query, q.page.GetOrder(), q.page.Size, ((q.page.Page - 1) * q.page.Size)) +// pageData retrieves data for the page query from the given database instance. +// +// Parameters: +// - instance: the database instance to retrieve data from. +// Returns a slice of type T and an error. +func (q *PageQuery[T]) pageData(instance *sql.DB) ([]T, error) { + query := fmt.Sprintf(pageDataPostgresQuery, q.query, q.page.GetOrder(), q.page.Size, ((q.page.Page - 1) * q.page.Size)) - rows, err := instance.QueryContext(q.ctx, query, q.args...) + rows, err := q.queryContext(instance, query) if err != nil { return nil, err } @@ -58,7 +92,11 @@ func (q *PageQuery[T]) pageData() ([]T, error) { return getDataList[T](rows) } -func (q *PageQuery[T]) validate() error { +// validate checks if the PageQuery instance is initialized, if the page is empty, and if the query is empty. +// +// instance: the database instance to validate against +// Returns an error. +func (q *PageQuery[T]) validate(instance *sql.DB) error { if instance == nil { return errors.New(db_not_initialized_error) } @@ -73,3 +111,31 @@ func (q *PageQuery[T]) validate() error { return nil } + +// queryContext executes a query on the provided SQL instance. +// +// Parameters: +// - instance: The *sql.DB instance to execute the query. +// - query: The SQL query string to execute. +// Returns the resulting rows and an error. +func (q *PageQuery[T]) queryContext(instance *sql.DB, query string) (*sql.Rows, error) { + if tx := q.ctx.Value(SqlTxContext); tx != nil { + return tx.(*sql.Tx).QueryContext(q.ctx, query, q.args...) + } + + return instance.QueryContext(q.ctx, query, q.args...) +} + +// queryRowContext executes a query on the provided SQL instance and returns a single row. +// +// Parameters: +// - instance: The *sql.DB instance to execute the query. +// - query: The SQL query string to execute. +// Returns the resulting row. +func (q *PageQuery[T]) queryRowContext(instance *sql.DB, query string) *sql.Row { + if tx := q.ctx.Value(SqlTxContext); tx != nil { + return tx.(*sql.Tx).QueryRowContext(q.ctx, query, q.args...) + } + + return instance.QueryRowContext(q.ctx, query, q.args...) +} diff --git a/pkg/database/sqlDB/page_query_test.go b/pkg/database/sqlDB/page_query_test.go index 7d51f07..6037111 100644 --- a/pkg/database/sqlDB/page_query_test.go +++ b/pkg/database/sqlDB/page_query_test.go @@ -4,80 +4,51 @@ import ( "context" "testing" - "github.com/colibri-project-io/colibri-sdk-go/pkg/base/logging" - "github.com/colibri-project-io/colibri-sdk-go/pkg/base/test" "github.com/colibri-project-io/colibri-sdk-go/pkg/base/types" "github.com/stretchr/testify/assert" ) func TestPageQueryWithoutInitialize(t *testing.T) { - basePath := test.MountAbsolutPath(test.DATABASE_ENVIRONMENT_PATH) + sqlDBInstance = nil - test.InitializeSqlDBTest() - pc := test.UsePostgresContainer() - - if err := pc.Dataset(basePath, "schema.sql"); err != nil { - logging.Fatal(err.Error()) - } - - instance = nil - - t.Run("Page", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) - - orders := []types.Sort{ + t.Run("Should return error when execute page query with db not initialized error", func(t *testing.T) { + page := types.NewPageRequest(1, 1, []types.Sort{ {Direction: types.DESC, Field: "name"}, {Direction: types.ASC, Field: "birthday"}, - } - page := types.NewPageRequest(1, 1, orders) - _, err = NewPageQuery[User](context.Background(), page, query_base).Execute() + }) + + result, err := NewPageQuery[User](context.Background(), page, query_base).Execute() + assert.Error(t, err, db_not_initialized_error) + assert.Nil(t, result) }) } func TestPageQuery(t *testing.T) { + InitializeSqlDBTest() ctx := context.Background() - orders := []types.Sort{ + page := types.NewPageRequest(1, 1, []types.Sort{ {Direction: types.DESC, Field: "u.name"}, {Direction: types.ASC, Field: "u.birthday"}, - } - page := types.NewPageRequest(1, 1, orders) - - basePath := test.MountAbsolutPath(test.DATABASE_ENVIRONMENT_PATH) - test.InitializeSqlDBTest() - pc := test.UsePostgresContainer() - if err := pc.Dataset(basePath, "schema.sql"); err != nil { - logging.Fatal(err.Error()) - } - - Initialize() + }) - t.Run("Page without page info", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) + t.Run("Should return error when execute page query without page info", func(t *testing.T) { + result, err := NewPageQuery[User](ctx, nil, query_base).Execute() - _, err = NewPageQuery[User](ctx, nil, query_base).Execute() assert.Error(t, err, page_is_empty_error) + assert.Nil(t, result) }) - t.Run("Page without query", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) + t.Run("Should return error when execute page query without query", func(t *testing.T) { + result, err := NewPageQuery[User](ctx, page, "").Execute() - _, err = NewPageQuery[User](ctx, page, "").Execute() assert.Error(t, err, query_is_empty_error) + assert.Nil(t, result) }) - t.Run("Page", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) - + t.Run("Should execute page query", func(t *testing.T) { result, err := NewPageQuery[User](ctx, page, query_base).Execute() + assert.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, "OTHER USER", result.Content[0].Name) diff --git a/pkg/database/sqlDB/query.go b/pkg/database/sqlDB/query.go index 9fa757c..870fcf4 100644 --- a/pkg/database/sqlDB/query.go +++ b/pkg/database/sqlDB/query.go @@ -4,10 +4,11 @@ import ( "context" "database/sql" "errors" + "github.com/colibri-project-io/colibri-sdk-go/pkg/database/cacheDB" ) -// Query struct +// Query is a struct for sql query type Query[T any] struct { ctx context.Context cache *cacheDB.Cache[T] @@ -15,35 +16,60 @@ type Query[T any] struct { args []any } -// NewQuery create a new pointer to Query struct +// NewQuery create a new pointer to Query struct. +// +// ctx: the context.Context for the query +// query: the query string to execute +// params: variadic interface{} for additional parameters +// Returns a pointer to Query struct func NewQuery[T any](ctx context.Context, query string, params ...any) *Query[T] { return &Query[T]{ctx, nil, query, params} } -// NewCachedQuery create a new pointer to Query struct with cache +// NewCachedQuery create a new pointer to Query struct with cache. +// +// ctx: the context.Context for the query +// cache: the cacheDB.Cache to store the query result +// query: the query string to execute +// params: variadic interface{} for additional parameters +// Returns a pointer to Query struct func NewCachedQuery[T any](ctx context.Context, cache *cacheDB.Cache[T], query string, params ...any) (q *Query[T]) { return &Query[T]{ctx, cache, query, params} } -// Many returns a slice of T value +// Many returns a slice of T value. +// +// No parameters are required. Returns a slice of T value and an error. func (q *Query[T]) Many() ([]T, error) { - if err := q.validate(); err != nil { + return q.ManyInInstance(sqlDBInstance) +} + +// ManyInInstance retrieves multiple items of type T for the given SQL instance. +// +// instance: The *sql.DB instance to execute the query. +// Returns a slice of retrieved items of type T and an error. +func (q *Query[T]) ManyInInstance(instance *sql.DB) ([]T, error) { + if err := q.validate(instance); err != nil { return nil, err } if q.cache == nil { - return q.fetchMany() + return q.fetchMany(instance) } result, err := q.cache.Many(q.ctx) - if err != nil { - return q.fetchMany() + if result == nil || err != nil { + return q.fetchMany(instance) } return result, nil } -func (q *Query[T]) fetchMany() ([]T, error) { - rows, err := q.queryContext() +// fetchMany retrieves multiple items of type T for the given SQL instance. +// +// instance: The *sql.DB instance to execute the query. +// Returns a slice of retrieved items of type T and an error. +func (q *Query[T]) fetchMany(instance *sql.DB) ([]T, error) { + rows, err := q.queryContext(instance) if err != nil { return nil, err } @@ -62,25 +88,40 @@ func (q *Query[T]) fetchMany() ([]T, error) { } // One return a pointer of T value +// +// No parameters. +// Returns a pointer of T and an error. func (q *Query[T]) One() (*T, error) { - if err := q.validate(); err != nil { + return q.OneInInstance(sqlDBInstance) +} + +// OneInInstance retrieves a single item of type T for the given SQL instance. +// +// instance: The *sql.DB instance to execute the query. +// Returns a pointer of T and an error. +func (q *Query[T]) OneInInstance(instance *sql.DB) (*T, error) { + if err := q.validate(instance); err != nil { return nil, err } if q.cache == nil { - return q.fetchOne() + return q.fetchOne(instance) } result, err := q.cache.One(q.ctx) - if err != nil { - return q.fetchOne() + if result == nil || err != nil { + return q.fetchOne(instance) } return result, nil } -func (q *Query[T]) fetchOne() (*T, error) { +// fetchOne retrieves a single item of type T for the given SQL instance. +// +// instance: The *sql.DB instance to execute the query. +// Returns a pointer of T and an error. +func (q *Query[T]) fetchOne(instance *sql.DB) (*T, error) { model := new(T) - if err := q.queryRowContext().Scan(reflectCols(model)...); err != nil && err != sql.ErrNoRows { + if err := q.queryRowContext(instance).Scan(reflectCols(model)...); err != nil && err != sql.ErrNoRows { return nil, err } else if err == sql.ErrNoRows { return nil, nil @@ -93,7 +134,11 @@ func (q *Query[T]) fetchOne() (*T, error) { return model, nil } -func (q *Query[T]) validate() error { +// validate checks if the Query instance is initialized and if the query is empty. +// +// instance: The *sql.DB instance to execute the query. +// Returns an error. +func (q *Query[T]) validate(instance *sql.DB) error { if instance == nil { return errors.New(db_not_initialized_error) } @@ -105,7 +150,11 @@ func (q *Query[T]) validate() error { return nil } -func (q *Query[T]) queryContext() (*sql.Rows, error) { +// queryContext executes a query on the provided SQL instance. +// +// instance: The *sql.DB instance to execute the query. +// Returns the resulting rows and an error. +func (q *Query[T]) queryContext(instance *sql.DB) (*sql.Rows, error) { if tx := q.ctx.Value(SqlTxContext); tx != nil { return tx.(*sql.Tx).QueryContext(q.ctx, q.query, q.args...) } @@ -113,7 +162,11 @@ func (q *Query[T]) queryContext() (*sql.Rows, error) { return instance.QueryContext(q.ctx, q.query, q.args...) } -func (q *Query[T]) queryRowContext() *sql.Row { +// queryRowContext executes a query on the provided SQL instance and returns a single row. +// +// instance: The *sql.DB instance to execute the query. +// Returns the resulting row. +func (q *Query[T]) queryRowContext(instance *sql.DB) *sql.Row { if tx := q.ctx.Value(SqlTxContext); tx != nil { return tx.(*sql.Tx).QueryRowContext(q.ctx, q.query, q.args...) } diff --git a/pkg/database/sqlDB/query_test.go b/pkg/database/sqlDB/query_test.go index 273f1e0..4f3617c 100644 --- a/pkg/database/sqlDB/query_test.go +++ b/pkg/database/sqlDB/query_test.go @@ -5,388 +5,248 @@ import ( "testing" "time" - "github.com/colibri-project-io/colibri-sdk-go/pkg/base/logging" - "github.com/colibri-project-io/colibri-sdk-go/pkg/base/test" "github.com/stretchr/testify/assert" + "github.com/colibri-project-io/colibri-sdk-go/pkg/base/test" "github.com/colibri-project-io/colibri-sdk-go/pkg/database/cacheDB" ) func TestQueryWithoutInitialize(t *testing.T) { - basePath := test.MountAbsolutPath(test.DATABASE_ENVIRONMENT_PATH) - - test.InitializeSqlDBTest() - pc := test.UsePostgresContainer() + ctx := context.Background() + sqlDBInstance = nil - if err := pc.Dataset(basePath, "schema.sql"); err != nil { - logging.Fatal(err.Error()) - } - - instance = nil - - t.Run("One without params", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) + t.Run("Should return error when execute query one without params with db not initialized error", func(t *testing.T) { + result, err := NewQuery[User](ctx, query_base+" LIMIT 1").One() - _, err = NewQuery[User](context.Background(), query_base+" LIMIT 1").One() assert.Error(t, err, db_not_initialized_error) + assert.Nil(t, result) }) - t.Run("One with params", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) + t.Run("Should return error when execute query one with params with db not initialized error", func(t *testing.T) { + result, err := NewQuery[User](ctx, query_base+" WHERE u.id = $1", 1).One() - _, err = NewQuery[User](context.Background(), query_base+" WHERE u.id = $1", 1).One() assert.Error(t, err, db_not_initialized_error) + assert.Nil(t, result) }) - t.Run("Many without params", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) + t.Run("Should return error when execute query many without params with db not initialized error", func(t *testing.T) { + result, err := NewQuery[User](ctx, query_base).Many() - _, err = NewQuery[User](context.Background(), query_base).Many() assert.Error(t, err, db_not_initialized_error) + assert.Nil(t, result) }) - t.Run("Many with params", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) + t.Run("Should return error when execute query many with params with db not initialized error", func(t *testing.T) { + result, err := NewQuery[User](ctx, query_base+" WHERE u.name = $1", "ADMIN USER").Many() - _, err = NewQuery[User](context.Background(), query_base+" WHERE u.name = $1", "ADMIN USER").Many() assert.Error(t, err, db_not_initialized_error) + assert.Nil(t, result) }) } func TestQuery(t *testing.T) { - basePath := test.MountAbsolutPath(test.DATABASE_ENVIRONMENT_PATH) + ctx := context.Background() + InitializeSqlDBTest() - test.InitializeSqlDBTest() - pc := test.UsePostgresContainer() + t.Run("Should return error when execute one without query", func(t *testing.T) { + result, err := NewQuery[User](ctx, "").One() - if err := pc.Dataset(basePath, "schema.sql"); err != nil { - logging.Fatal(err.Error()) - } - - Initialize() - - t.Run("One without query", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) - - ctx := context.Background() - _, err = NewQuery[User](ctx, "").One() assert.Error(t, err, query_is_empty_error) - + assert.Nil(t, result) }) - t.Run("One without params", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) - - ctx := context.Background() + t.Run("Should execute one without params", func(t *testing.T) { result, err := NewQuery[User](ctx, query_base+" LIMIT 1").One() + assert.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, "ADMIN USER", result.Name) }) - t.Run("One with params", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) - - ctx := context.Background() + t.Run("Should execute one with params", func(t *testing.T) { result, err := NewQuery[User](ctx, query_base+" WHERE u.name = $1", "ADMIN USER").One() + assert.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, "ADMIN USER", result.Name) - }) - t.Run("Many without query", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) + t.Run("Should return error when execute many without query", func(t *testing.T) { + result, err := NewQuery[User](ctx, "").Many() - ctx := context.Background() - _, err = NewQuery[User](ctx, "").Many() assert.Error(t, err, query_is_empty_error) + assert.Nil(t, result) }) - t.Run("Many without params", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) - - ctx := context.Background() + t.Run("Should execute many without params", func(t *testing.T) { result, err := NewQuery[User](ctx, query_base).Many() + assert.NoError(t, err) assert.NotNil(t, result) assert.Len(t, result, 2) assert.Equal(t, "ADMIN USER", result[0].Name) - }) - t.Run("Many with params", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) - - ctx := context.Background() + t.Run("Should execute many with params", func(t *testing.T) { result, err := NewQuery[User](ctx, query_base+" WHERE u.name = $1", "ADMIN USER").Many() + assert.NoError(t, err) assert.NotNil(t, result) assert.Len(t, result, 1) assert.Equal(t, "ADMIN USER", result[0].Name) }) - - t.Run("Many Dogs", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-dogs.sql"} - assert.NoError(t, pc.Dataset(basePath, datasets...)) - query := "SELECT id, name, characteristics FROM dog ORDER BY name" - dogs, err := NewQuery[Dog](context.Background(), query).Many() - assert.NoError(t, err) - assert.Len(t, dogs, 2) - assert.Equal(t, dogs[0].Name, "Pitty") - assert.Len(t, dogs[0].Characteristics, 2) - assert.Equal(t, dogs[0].Characteristics, []string{"mad", "destructive"}) - assert.Equal(t, dogs[1].Name, "Stella") - assert.Len(t, dogs[1].Characteristics, 1) - assert.Equal(t, dogs[1].Characteristics, []string{"cute"}) - }) } func TestQueryWithoutCacheDBInitialize(t *testing.T) { - basePath := test.MountAbsolutPath(test.DATABASE_ENVIRONMENT_PATH) - - test.InitializeSqlDBTest() - pc := test.UsePostgresContainer() + cache := cacheDB.NewCache[User]("TestQueryWithoutCacheDBInitialize", time.Hour) + ctx := context.Background() + InitializeSqlDBTest() - if err := pc.Dataset(basePath, "schema.sql"); err != nil { - logging.Fatal(err.Error()) - } + t.Run("Should return error when one without params with cache", func(t *testing.T) { + dbResult, dbErr := NewCachedQuery(ctx, cache, query_base+" LIMIT 1").One() + cacheResult, cacheErr := cache.One(ctx) - Initialize() - - t.Run("One without params with cache", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) - - ctx := context.Background() - cache := cacheDB.NewCache[User]("DbTestsOneWithoutParams", time.Hour) - result, err := NewCachedQuery(ctx, cache, query_base+" LIMIT 1").One() - assert.NoError(t, err) - assert.NotNil(t, result) - assert.Equal(t, "ADMIN USER", result.Name) - - _, err = cache.One(ctx) - assert.Error(t, err, "Cache not initialized") + assert.NoError(t, dbErr) + assert.NotNil(t, dbResult) + assert.Equal(t, "ADMIN USER", dbResult.Name) + assert.Error(t, cacheErr, "Cache not initialized") + assert.Nil(t, cacheResult) }) - t.Run("One with params with cache", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) - - ctx := context.Background() - cache := cacheDB.NewCache[User]("DbTestsOneWithoutParams", time.Hour) - result, err := NewCachedQuery(ctx, cache, query_base+" WHERE u.name = $1", "ADMIN USER").One() - assert.NoError(t, err) - assert.NotNil(t, result) - assert.Equal(t, "ADMIN USER", result.Name) + t.Run("Should return error when one with params with cache", func(t *testing.T) { + dbResult, dbErr := NewCachedQuery(ctx, cache, query_base+" WHERE u.name = $1", "ADMIN USER").One() + cacheResult, cacheErr := cache.One(ctx) - _, err = cache.One(ctx) - assert.Error(t, err, "Cache not initialized") + assert.NoError(t, dbErr) + assert.NotNil(t, dbResult) + assert.Equal(t, "ADMIN USER", dbResult.Name) + assert.Error(t, cacheErr, "Cache not initialized") + assert.Nil(t, cacheResult) }) - t.Run("Many without params with cache", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) - - ctx := context.Background() - cache := cacheDB.NewCache[User]("DbTestsManyWithoutParams", time.Hour) - result, err := NewCachedQuery(ctx, cache, query_base).Many() - assert.NoError(t, err) - assert.NotNil(t, result) - assert.Len(t, result, 2) - assert.Equal(t, "ADMIN USER", result[0].Name) + t.Run("Should return error when many without params with cache", func(t *testing.T) { + dbResult, dbErr := NewCachedQuery(ctx, cache, query_base).Many() + cacheResult, cacheErr := cache.Many(ctx) - _, err = cache.Many(ctx) - assert.Error(t, err, "Cache not initialized") + assert.NoError(t, dbErr) + assert.NotNil(t, dbResult) + assert.Len(t, dbResult, 2) + assert.Equal(t, "ADMIN USER", dbResult[0].Name) + assert.Error(t, cacheErr, "Cache not initialized") + assert.Nil(t, cacheResult) }) - t.Run("Many with params with cache", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) - - ctx := context.Background() - cache := cacheDB.NewCache[User]("DbTestsManyWithoutParams", time.Hour) - result, err := NewCachedQuery(ctx, cache, query_base+" WHERE u.name = $1", "ADMIN USER").Many() - assert.NoError(t, err) - assert.NotNil(t, result) - assert.Len(t, result, 1) - assert.Equal(t, "ADMIN USER", result[0].Name) + t.Run("Should return error when many with params with cache", func(t *testing.T) { + dbResult, dbErr := NewCachedQuery(ctx, cache, query_base+" WHERE u.name = $1", "ADMIN USER").Many() + cacheResult, cacheErr := cache.Many(ctx) - _, err = cache.Many(ctx) - assert.Error(t, err, "Cache not initialized") + assert.NoError(t, dbErr) + assert.NotNil(t, dbResult) + assert.Len(t, dbResult, 1) + assert.Equal(t, "ADMIN USER", dbResult[0].Name) + assert.Error(t, cacheErr, "Cache not initialized") + assert.Nil(t, cacheResult) }) } func TestCachedQuery(t *testing.T) { - basePath := test.MountAbsolutPath(test.DATABASE_ENVIRONMENT_PATH) - + InitializeSqlDBTest() test.InitializeCacheDBTest() - test.InitializeSqlDBTest() - pc := test.UsePostgresContainer() - - if err := pc.Dataset(basePath, "schema.sql"); err != nil { - logging.Fatal(err.Error()) - } - cacheDB.Initialize() - Initialize() - t.Run("One without query with cache", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) + cache := cacheDB.NewCache[User]("TestCachedQuery", time.Hour) + ctx := context.Background() - ctx := context.Background() - cache := cacheDB.NewCache[User]("DbTestsOneWithoutQuery", time.Hour) - cacheInitialData, err := cache.One(ctx) - assert.NotNil(t, err) - assert.Nil(t, cacheInitialData) + t.Run("Should return error when one without query with cache", func(t *testing.T) { + cacheInitialData, cacheInitialErr := cache.One(ctx) + result, err := NewCachedQuery(ctx, cache, "").One() - _, err = NewCachedQuery(ctx, cache, "").One() + assert.NoError(t, cacheInitialErr) assert.Error(t, err, query_is_empty_error) + assert.Nil(t, cacheInitialData) + assert.Nil(t, result) }) - t.Run("One without params with cache", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) + t.Run("Should execute one without params with cache", func(t *testing.T) { + cacheInitialData, cacheInitialErr := cache.One(ctx) + result, err := NewCachedQuery(ctx, cache, query_base+" LIMIT 1").One() + cacheFinalData, cacheFinalErr := cache.One(ctx) + cacheDelErr := cache.Del(ctx) - ctx := context.Background() - cache := cacheDB.NewCache[User]("DbTestsOneWithoutParams", time.Hour) - cacheInitialData, err := cache.One(ctx) - assert.NotNil(t, err) + assert.NoError(t, cacheInitialErr) assert.Nil(t, cacheInitialData) - - result, err := NewCachedQuery(ctx, cache, query_base+" LIMIT 1").One() assert.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, "ADMIN USER", result.Name) - - cacheFinalData, err := cache.One(ctx) - assert.Nil(t, err) + assert.NoError(t, cacheFinalErr) assert.NotNil(t, cacheFinalData) assert.Equal(t, "ADMIN USER", cacheFinalData.Name) - - err = cache.Del(ctx) - assert.Nil(t, err) + assert.NoError(t, cacheDelErr) }) - t.Run("One with params with cache", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) + t.Run("Should execute one with params with cache", func(t *testing.T) { + cacheInitialData, cacheInitialErr := cache.One(ctx) + result, err := NewCachedQuery(ctx, cache, query_base+" WHERE u.name = $1", "ADMIN USER").One() + cacheFinalData, cacheFinalErr := cache.One(ctx) + cacheDelErr := cache.Del(ctx) - ctx := context.Background() - cache := cacheDB.NewCache[User]("DbTestsOneWithoutParams", time.Hour) - cacheInitialData, err := cache.One(ctx) - assert.NotNil(t, err) + assert.NoError(t, cacheInitialErr) assert.Nil(t, cacheInitialData) - - result, err := NewCachedQuery(ctx, cache, query_base+" WHERE u.name = $1", "ADMIN USER").One() assert.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, "ADMIN USER", result.Name) - - cacheFinalData, err := cache.One(ctx) - assert.Nil(t, err) + assert.NoError(t, cacheFinalErr) assert.NotNil(t, cacheFinalData) assert.Equal(t, "ADMIN USER", cacheFinalData.Name) - - err = cache.Del(ctx) - assert.Nil(t, err) + assert.NoError(t, cacheDelErr) }) - t.Run("Many without query with cache", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) + t.Run("Should return error when many without query with cache", func(t *testing.T) { + cacheInitialData, cacheInitialErr := cache.One(ctx) + result, err := NewCachedQuery(ctx, cache, "").Many() - ctx := context.Background() - cache := cacheDB.NewCache[User]("DbTestsManyWithoutQuery", time.Hour) - cacheInitialData, err := cache.One(ctx) - assert.NotNil(t, err) + assert.NoError(t, cacheInitialErr) assert.Nil(t, cacheInitialData) - - _, err = NewCachedQuery(ctx, cache, "").Many() assert.Error(t, err, query_is_empty_error) + assert.Nil(t, result) }) - t.Run("Many without params with cache", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) + t.Run("Should execute many without params with cache", func(t *testing.T) { + cacheInitialData, cacheInitialErr := cache.One(ctx) + result, err := NewCachedQuery(ctx, cache, query_base).Many() + cacheFinalData, cacheFinalErr := cache.Many(ctx) + cacheDelErr := cache.Del(ctx) - ctx := context.Background() - cache := cacheDB.NewCache[User]("DbTestsManyWithoutParams", time.Hour) - cacheInitialData, err := cache.One(ctx) - assert.NotNil(t, err) + assert.NoError(t, cacheInitialErr) assert.Nil(t, cacheInitialData) - - result, err := NewCachedQuery(ctx, cache, query_base).Many() assert.NoError(t, err) assert.NotNil(t, result) assert.Len(t, result, 2) assert.Equal(t, "ADMIN USER", result[0].Name) - - cacheFinalData, err := cache.Many(ctx) - assert.Nil(t, err) + assert.NoError(t, cacheFinalErr) assert.NotNil(t, cacheFinalData) assert.Len(t, result, 2) assert.Equal(t, "ADMIN USER", result[0].Name) - - err = cache.Del(ctx) - assert.Nil(t, err) + assert.NoError(t, cacheDelErr) }) - t.Run("Many with params with cache", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) + t.Run("Should execute many with params with cache", func(t *testing.T) { + cacheInitialData, cacheInitialErr := cache.One(ctx) + result, err := NewCachedQuery(ctx, cache, query_base+" WHERE u.name = $1", "ADMIN USER").Many() + cacheFinalData, cacheFinalErr := cache.Many(ctx) + cacheDelErr := cache.Del(ctx) - ctx := context.Background() - cache := cacheDB.NewCache[User]("DbTestsManyWithoutParams", time.Hour) - cacheInitialData, err := cache.One(ctx) - assert.NotNil(t, err) + assert.NoError(t, cacheInitialErr) assert.Nil(t, cacheInitialData) - - result, err := NewCachedQuery(ctx, cache, query_base+" WHERE u.name = $1", "ADMIN USER").Many() assert.NoError(t, err) assert.NotNil(t, result) assert.Len(t, result, 1) assert.Equal(t, "ADMIN USER", result[0].Name) - - cacheFinalData, err := cache.Many(ctx) - assert.Nil(t, err) + assert.NoError(t, cacheFinalErr) assert.NotNil(t, cacheFinalData) assert.Len(t, result, 1) assert.Equal(t, "ADMIN USER", result[0].Name) - - err = cache.Del(ctx) - assert.Nil(t, err) + assert.NoError(t, cacheDelErr) }) } diff --git a/pkg/database/sqlDB/sql_db.go b/pkg/database/sqlDB/sql_db.go index b9aefe2..c0b46bc 100644 --- a/pkg/database/sqlDB/sql_db.go +++ b/pkg/database/sqlDB/sql_db.go @@ -18,52 +18,78 @@ import ( ) const ( - db_connection_success string = "SQL database connected" - db_connection_error string = "An error occurred while trying to connect to the database. Error: %s" + db_connection_success string = "%s database connected" + db_connection_error string = "An error occurred while trying to connect to the %s database. Error: %s" db_migration_error string = "An error occurred when validate database migrations: %v" db_not_initialized_error string = "database not initialized" query_is_empty_error string = "query is empty" page_is_empty_error string = "page is empty" ) -type sqlDBObserver struct{} +// sqlDBObserver is a struct for sql database observer. +type sqlDBObserver struct { + name string + instance *sql.DB +} -// Close finalize sql database connection -func (o sqlDBObserver) Close() { - logging.Info("waiting to safely close the sql database connection") - if observer.WaitRunningTimeout() { - logging.Warn("WaitGroup timed out, forcing close the sql database connection") - } +// sqlDBInstance is a pointer to sql.DB +var sqlDBInstance *sql.DB - logging.Info("closing sql database connection") - if err := instance.Close(); err != nil { - logging.Error("error when closing sql database connection: %+v", err) +// Initialize start connection with sql database and execute migration. +// +// No parameters. +// No return values. +func Initialize() { + sqlDB := NewSQLDatabaseInstance("SQL", config.SQL_DB_CONNECTION_URI) + sqlDB.SetMaxOpenConns(config.SQL_DB_MAX_OPEN_CONNS) + sqlDB.SetMaxIdleConns(config.SQL_DB_MAX_IDLE_CONNS) + + if err := executeDatabaseMigration(sqlDB); err != nil { + logging.Fatal(db_migration_error, err) } -} -var instance *sql.DB + sqlDBInstance = sqlDB +} -// Initialize start connection with sql database and execute migration -func Initialize() { - sqlDB, err := sql.Open(monitoring.GetSQLDBDriverName(), config.SQL_DB_CONNECTION_URI) +// NewSQLDatabaseInstance creates a new SQL database instance. +// +// Parameters: +// - name: a string representing the name of the database. +// - databaseURL: a string representing the URL of the database. +// Returns a pointer to sql.DB. +func NewSQLDatabaseInstance(name, databaseURL string) *sql.DB { + sqlDB, err := sql.Open(monitoring.GetSQLDBDriverName(), databaseURL) if err != nil { - logging.Fatal(db_connection_error, err) + logging.Fatal(db_connection_error, name, err) } if err = sqlDB.Ping(); err != nil { - logging.Fatal(db_connection_error, err) + logging.Fatal(db_connection_error, name, err) } - instance = sqlDB + logging.Info(db_connection_success, name) + observer.Attach(sqlDBObserver{name, sqlDB}) + return sqlDB +} - if err = migrations(); err != nil { - logging.Fatal(db_migration_error, err) +// Close finalize sql database connection +// +// No parameters. +// No return values. +func (o sqlDBObserver) Close() { + logging.Info("waiting to safely close the %s database connection", o.name) + if observer.WaitRunningTimeout() { + logging.Warn("WaitGroup timed out, forcing close the %s database connection", o.name) + } + logging.Info("closing %s database connection", o.name) + if err := o.instance.Close(); err != nil { + logging.Error("error when closing %s database connection: %+v", o.name, err) } - - observer.Attach(sqlDBObserver{}) - logging.Info(db_connection_success) } +// getDataList retrieves a list of items from the given sql.Rows object. +// +// It takes a sql.Rows object as input and returns a list of items of type T and an error. func getDataList[T any](rows *sql.Rows) ([]T, error) { list := make([]T, 0) for rows.Next() { @@ -79,6 +105,10 @@ func getDataList[T any](rows *sql.Rows) ([]T, error) { return list, nil } +// reflectCols generates a list of column values from the provided model. +// +// model: the model to reflect columns from +// []any: a list of column values func reflectCols(model any) (cols []any) { typeOf := reflect.TypeOf(model).Elem() valueOf := reflect.ValueOf(model).Elem() @@ -107,6 +137,10 @@ func reflectCols(model any) (cols []any) { return cols } +// reflectValueValidations validates the type of the provided value. +// +// value: the value to validate +// (isStruct, isTime, isNull, isSlice) : returns booleans indicating if the value is a struct, time type, null type, or a slice. func reflectValueValidations(value reflect.Value) (isStruct, isTime, isNull, isSlice bool) { isStruct = value.Kind() == reflect.Struct isTime = slices.Contains([]string{"time.Time", "types.IsoDate", "types.IsoTime"}, value.Type().String()) @@ -115,6 +149,10 @@ func reflectValueValidations(value reflect.Value) (isStruct, isTime, isNull, isS return } +// closer closes the provided io.Closer interface and logs an error if closing fails. +// +// o: the io.Closer interface to be closed +// Error: returns any error encountered during closing. func closer(o io.Closer) { if err := o.Close(); err != nil { logging.Error("could not close statement: %v", err) diff --git a/pkg/database/sqlDB/sql_db_test.go b/pkg/database/sqlDB/sql_db_test.go index 859c2e4..5fbb8c1 100644 --- a/pkg/database/sqlDB/sql_db_test.go +++ b/pkg/database/sqlDB/sql_db_test.go @@ -2,6 +2,9 @@ package sqlDB import ( "time" + + "github.com/colibri-project-io/colibri-sdk-go/pkg/base/logging" + "github.com/colibri-project-io/colibri-sdk-go/pkg/base/test" ) const ( @@ -25,3 +28,19 @@ type Dog struct { Name string Characteristics []string } + +func InitializeSqlDBTest() { + basePath := test.MountAbsolutPath(test.DATABASE_ENVIRONMENT_PATH) + + test.InitializeSqlDBTest() + pc := test.UsePostgresContainer() + + if err := pc.Dataset(basePath, "schema.sql"); err != nil { + logging.Fatal(err.Error()) + } + + datasets := []string{"clear-database.sql", "add-users.sql", "add-contacts.sql", "add-dogs.sql"} + pc.Dataset(basePath, datasets...) + + Initialize() +} diff --git a/pkg/database/sqlDB/sql_transaction.go b/pkg/database/sqlDB/sql_transaction.go index 0bcb559..6e9509a 100644 --- a/pkg/database/sqlDB/sql_transaction.go +++ b/pkg/database/sqlDB/sql_transaction.go @@ -4,70 +4,103 @@ import ( "context" "database/sql" "fmt" + "github.com/colibri-project-io/colibri-sdk-go/pkg/base/logging" "github.com/colibri-project-io/colibri-sdk-go/pkg/base/transaction" ) -const SqlTxContext = "SqlTxContext" +// SqlTxContextKey is the type of the context key for the transaction. +type SqlTxContextKey string + +const ( + SqlTxContext SqlTxContextKey = "SqlTxContext" + + transactionIsolationWarnMsg string = "transaction isolation just use first parameter, others will be ignored" + transactionRollbackErrorMsg string = "error when executing transaction rollback: %v: %w" + transactionCommitErrorMsg string = "could not commit transaction: %w" + transactionStartErrorMsg string = "could not start database transaction: %v" +) +// sqlTransaction implements a transaction.Transaction type sqlTransaction struct { isolation sql.IsolationLevel } -// NewTransaction creates new sqlTransaction with implements a transaction.Transaction +// NewTransaction creates a new sqlTransaction implementing the transaction.Transaction interface. +// +// It takes an optional variable number of sql.IsolationLevel parameters and returns a transaction.Transaction. func NewTransaction(isolation ...sql.IsolationLevel) transaction.Transaction { isolationLevel := sql.LevelDefault if len(isolation) == 1 { isolationLevel = isolation[0] } else if len(isolation) > 1 { isolationLevel = isolation[0] - logging.Warn("transaction isolation just use first parameter, others will be ignored") + logging.Warn(transactionIsolationWarnMsg) } return &sqlTransaction{isolation: isolationLevel} } -// ExecTx execute a transactional sql -func (t *sqlTransaction) ExecTx(ctx context.Context, fn func(ctx context.Context) error) error { - tx, txCh, err := t.beginTx(ctx) +// Execute executes a transactional SQL. +// +// ctx: The context for the transaction. +// fn: The function to be executed. +// Returns an error. +func (t *sqlTransaction) Execute(ctx context.Context, fn func(ctx context.Context) error) error { + return t.ExecuteInInstance(ctx, sqlDBInstance, fn) +} + +// ExecuteInInstance executes a transaction in a specific database instance. +// +// ctx: The context for the transaction. +// instance: The specific database instance where the transaction will be executed. +// fn: The function to be executed as part of the transaction. +// Returns an error. +func (t *sqlTransaction) ExecuteInInstance(ctx context.Context, instance *sql.DB, fn func(ctx context.Context) error) error { + transaction, transactionChannel, err := t.beginTransaction(ctx, instance) if err != nil { return err } - defer close(txCh) + defer close(transactionChannel) - ctx = context.WithValue(ctx, SqlTxContext, tx) + ctx = context.WithValue(ctx, SqlTxContext, transaction) if err = fn(ctx); err != nil { - if rbErr := tx.Rollback(); rbErr != nil { - fErr := fmt.Errorf("error when executing transaction rollback: %v: %w", err, rbErr) + if rbErr := transaction.Rollback(); rbErr != nil { + fErr := fmt.Errorf(transactionRollbackErrorMsg, err, rbErr) logging.Error("%v", fErr) - txCh <- fErr + transactionChannel <- fErr return fErr } logging.Error("%v", err) - txCh <- err + transactionChannel <- err return err } - if err = tx.Commit(); err != nil { - fErr := fmt.Errorf("could not commit transaction: %w", err) + if err = transaction.Commit(); err != nil { + fErr := fmt.Errorf(transactionCommitErrorMsg, err) logging.Error("%v", fErr) - txCh <- fErr + transactionChannel <- fErr return fErr } return nil } -func (t *sqlTransaction) beginTx(ctx context.Context) (*sql.Tx, chan error, error) { - tx, err := instance.BeginTx(ctx, &sql.TxOptions{Isolation: t.isolation}) +// beginTransaction starts a new database transaction. +// +// ctx: The context for the transaction. +// instance: The specific database instance for the transaction. +// Returns the transaction, a channel for errors, and an error. +func (t *sqlTransaction) beginTransaction(ctx context.Context, instance *sql.DB) (*sql.Tx, chan error, error) { + transaction, err := instance.BeginTx(ctx, &sql.TxOptions{Isolation: t.isolation}) if err != nil { - fErr := fmt.Errorf("could not start database transaction: %v", err) + fErr := fmt.Errorf(transactionStartErrorMsg, err) logging.Error("%v", fErr) return nil, nil, fErr } - return tx, make(chan error, 1), nil + return transaction, make(chan error, 1), nil } diff --git a/pkg/database/sqlDB/sql_transaction_test.go b/pkg/database/sqlDB/sql_transaction_test.go index d2bed5a..a0c35c6 100644 --- a/pkg/database/sqlDB/sql_transaction_test.go +++ b/pkg/database/sqlDB/sql_transaction_test.go @@ -3,29 +3,20 @@ package sqlDB import ( "context" "database/sql" - "github.com/colibri-project-io/colibri-sdk-go/pkg/base/logging" - "github.com/colibri-project-io/colibri-sdk-go/pkg/base/test" "testing" "github.com/stretchr/testify/assert" ) func TestSqlTransactionWithoutInitialize(t *testing.T) { - basePath := test.MountAbsolutPath("../../../development-environment/database/sql-tx/") + sqlDBInstance = nil + ctx := context.Background() - test.InitializeSqlDBTest() - pc := test.UsePostgresContainer() + t.Run("Should return error when instance is nil", func(t *testing.T) { + err := NewStatement(ctx, "", "Contact Name 1", "em@il.com").Execute() - if err := pc.Dataset(basePath, "schema.sql"); err != nil { - logging.Fatal(err.Error()) - } - - instance = nil - - stmt1 := NewStatement(context.Background(), "", "Contact Name 1", "em@il.com") - err := stmt1.Execute() - - assert.Error(t, err, db_not_initialized_error) + assert.Error(t, err, db_not_initialized_error) + }) } func TestSqlTransaction(t *testing.T) { @@ -34,114 +25,90 @@ func TestSqlTransaction(t *testing.T) { Email string } - basePath := test.MountAbsolutPath("../../../development-environment/database/sql-tx/") - - test.InitializeSqlDBTest() - pc := test.UsePostgresContainer() - - Initialize() - - if err := pc.Dataset(basePath, "schema.sql"); err != nil { - logging.Fatal(err.Error()) - } + ctx := context.Background() + InitializeSqlDBTest() t.Run("Should return error when query is nil", func(t *testing.T) { - if err := pc.Dataset(basePath, "clear.sql"); err != nil { - logging.Fatal(err.Error()) - } - - stmt := NewStatement(context.Background(), "", "Contact Name 1", "em@il.com") - err := stmt.Execute() + err := NewStatement(ctx, "", "Contact Name 1", "em@il.com").Execute() assert.Error(t, err, query_is_empty_error) }) - t.Run("commit ok", func(t *testing.T) { - if err := pc.Dataset(basePath, "clear.sql"); err != nil { - logging.Fatal(err.Error()) - } - - tx := NewTransaction() - err := tx.ExecTx(context.Background(), func(ctx context.Context) error { + t.Run("Should execute transaction and commit", func(t *testing.T) { + transaction := NewTransaction() + err := transaction.Execute(ctx, func(ctx context.Context) error { insertContact1 := "INSERT INTO contacts (name, email) VALUES ($1, $2) " - stmt1 := NewStatement(ctx, insertContact1, "Contact Name 1", "em@il.com") + stmt1 := NewStatement(ctx, insertContact1, "Contact Name 1 with Commit", "email1@email.com") if err := stmt1.Execute(); err != nil { return err } insertContact2 := "INSERT INTO contacts (name, email) VALUES ($1, $2) " - stmt2 := NewStatement(ctx, insertContact2, "Contact Name 2", "2em@il.com") + stmt2 := NewStatement(ctx, insertContact2, "Contact Name 2 with commit", "email2@email.com") if err := stmt2.Execute(); err != nil { return err } return nil }) - assert.NoError(t, err) + query1Result, query1Err := NewQuery[contact](ctx, "SELECT name, email FROM contacts WHERE email = $1", "email1@email.com").One() + query2Result, query2Err := NewQuery[contact](ctx, "SELECT name, email FROM contacts WHERE email = $1", "email2@email.com").One() - q1 := NewQuery[contact](context.Background(), "SELECT name, email FROM contacts WHERE email = $1", "em@il.com") - c1, err := q1.One() assert.NoError(t, err) - assert.NotNil(t, c1) - assert.Equal(t, "em@il.com", c1.Email) - - q2 := NewQuery[contact](context.Background(), "SELECT name, email FROM contacts WHERE email = $1", "2em@il.com") - c2, err := q2.One() - assert.NoError(t, err) - assert.NotNil(t, c2) - assert.Equal(t, "2em@il.com", c2.Email) + assert.NoError(t, query1Err) + assert.NotNil(t, query1Result) + assert.Equal(t, "email1@email.com", query1Result.Email) + assert.NoError(t, query2Err) + assert.NotNil(t, query2Result) + assert.Equal(t, "email2@email.com", query2Result.Email) }) - t.Run("fail with rollback", func(t *testing.T) { - if err := pc.Dataset(basePath, "clear.sql"); err != nil { - logging.Fatal(err.Error()) - } - - q1 := NewQuery[contact](context.Background(), "SELECT name, email FROM contacts WHERE email = $1", "em@il.com") - c1, err := q1.One() - assert.NoError(t, err) - assert.Nil(t, c1) - - tx := NewTransaction() - err = tx.ExecTx(context.Background(), func(ctx context.Context) error { + t.Run("Should execute transaction with fail and rollback", func(t *testing.T) { + transaction := NewTransaction() + query1Result, query1Err := NewQuery[contact](ctx, "SELECT name, email FROM contacts WHERE email = $1", "email2@email.com").One() + err := transaction.Execute(ctx, func(ctx context.Context) error { insertContact1 := "INSERT INTO contacts (name, email) VALUES ($1, $2) " - stmt1 := NewStatement(ctx, insertContact1, "Contact Name 1", "em@il.com") + stmt1 := NewStatement(ctx, insertContact1, "Contact Name 1 with fail", "email1-with-fail@email.com") if err := stmt1.Execute(); err != nil { return err } insertContact2 := "INSERT INTO contacts (name, email) VALUES ($1, $2) " - stmt2 := NewStatement(ctx, insertContact2, "Contact Name 2", "em@il.com") + stmt2 := NewStatement(ctx, insertContact2, "Contact Name 2 with fail", "email2@email.com") if err := stmt2.Execute(); err != nil { return err } return nil }) - assert.Error(t, err) + query2Result, query2Err := NewQuery[contact](ctx, "SELECT name, email FROM contacts WHERE email = $1", "email1-with-fail@email.com").One() - q1 = NewQuery[contact](context.Background(), "SELECT name, email FROM contacts WHERE email = $1", "em@il.com") - c1, err = q1.One() - assert.NoError(t, err) - assert.Nil(t, c1) + assert.NoError(t, query1Err) + assert.NotNil(t, query1Result) + assert.Error(t, err) + assert.NoError(t, query2Err) + assert.Nil(t, query2Result) }) } -func TestSqlTransaction_isolationLevel(t *testing.T) { - t.Run("isolation level default", func(t *testing.T) { +func TestSqlTransactionIsolationLevel(t *testing.T) { + t.Run("Should return isolation level default", func(t *testing.T) { tx := NewTransaction() + assert.NotNil(t, tx) assert.Equal(t, sql.LevelDefault, tx.(*sqlTransaction).isolation) }) - t.Run("isolation level serializable", func(t *testing.T) { + t.Run("Should return isolation level serializable", func(t *testing.T) { tx := NewTransaction(sql.LevelSerializable) + assert.NotNil(t, tx) assert.Equal(t, sql.LevelSerializable, tx.(*sqlTransaction).isolation) }) - t.Run("multiple isolations, only first is used", func(t *testing.T) { + t.Run("Should return multiple isolations, only first is used", func(t *testing.T) { tx := NewTransaction(sql.LevelLinearizable, sql.LevelSerializable) + assert.NotNil(t, tx) assert.Equal(t, sql.LevelLinearizable, tx.(*sqlTransaction).isolation) }) diff --git a/pkg/database/sqlDB/statement.go b/pkg/database/sqlDB/statement.go index 216f131..fe0208e 100644 --- a/pkg/database/sqlDB/statement.go +++ b/pkg/database/sqlDB/statement.go @@ -6,25 +6,41 @@ import ( "errors" ) -// Statement struct +// Statement is a struct for sql statement type Statement struct { ctx context.Context query string args []interface{} } -// NewStatement create a new pointer to Statement struct +// NewStatement creates a new pointer to Statement struct. +// +// ctx: the context.Context for the statement +// query: the query string for the statement +// params: variadic interface{} for additional parameters +// Returns a pointer to Statement struct func NewStatement(ctx context.Context, query string, params ...interface{}) *Statement { return &Statement{ctx, query, params} } -// Execute apply statement in database +// Execute applies the statement in the database. +// +// No parameters. +// Returns an error. func (s *Statement) Execute() error { - if err := s.validate(); err != nil { + return s.ExecuteInInstance(sqlDBInstance) +} + +// ExecuteInInstance executes the statement in the provided database instance. +// +// instance: the sql database instance to execute the statement in. +// Returns an error. +func (s *Statement) ExecuteInInstance(instance *sql.DB) error { + if err := s.validate(instance); err != nil { return err } - stmt, err := s.createStatement() + stmt, err := s.createStatement(instance) if err != nil { return err } @@ -37,7 +53,11 @@ func (s *Statement) Execute() error { return nil } -func (s *Statement) validate() error { +// validate checks if the Statement instance is initialized and if the query is empty. +// +// No parameters. +// Returns an error. +func (s *Statement) validate(instance *sql.DB) error { if instance == nil { return errors.New(db_not_initialized_error) } @@ -49,7 +69,11 @@ func (s *Statement) validate() error { return nil } -func (s *Statement) createStatement() (*sql.Stmt, error) { +// createStatement creates a SQL statement for execution. +// +// No parameters. +// Returns a pointer to sql.Stmt and an error. +func (s *Statement) createStatement(instance *sql.DB) (*sql.Stmt, error) { if tx := s.ctx.Value(SqlTxContext); tx != nil { return tx.(*sql.Tx).PrepareContext(s.ctx, s.query) } diff --git a/pkg/database/sqlDB/statement_test.go b/pkg/database/sqlDB/statement_test.go index 51225c2..60df0e4 100644 --- a/pkg/database/sqlDB/statement_test.go +++ b/pkg/database/sqlDB/statement_test.go @@ -5,72 +5,41 @@ import ( "testing" "time" - "github.com/colibri-project-io/colibri-sdk-go/pkg/base/test" - - "github.com/colibri-project-io/colibri-sdk-go/pkg/base/logging" "github.com/stretchr/testify/assert" ) func TestStatementWithoutInitialize(t *testing.T) { - basePath := test.MountAbsolutPath(test.DATABASE_ENVIRONMENT_PATH) - - test.InitializeSqlDBTest() - pc := test.UsePostgresContainer() - - if err := pc.Dataset(basePath, "schema.sql"); err != nil { - logging.Fatal(err.Error()) - } - - instance = nil + ctx := context.Background() + sqlDBInstance = nil - t.Run("Statement", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) - - ctx := context.Background() + t.Run("Should return error when execute statement", func(t *testing.T) { birth, _ := time.Parse("2006-01-02", "2021-11-22") user := User{123, "Usuário teste stmt", birth, Profile{100, "ADMIN"}} - err = NewStatement(ctx, "INSERT INTO users VALUES ($1, $2, $3, $4)", user.Id, user.Name, user.Birthday, user.Profile.Id).Execute() + + err := NewStatement(ctx, "INSERT INTO users VALUES ($1, $2, $3, $4)", user.Id, user.Name, user.Birthday, user.Profile.Id).Execute() + assert.Error(t, err, db_not_initialized_error) }) } func TestStatement(t *testing.T) { - basePath := test.MountAbsolutPath(test.DATABASE_ENVIRONMENT_PATH) + InitializeSqlDBTest() + ctx := context.Background() - test.InitializeSqlDBTest() - pc := test.UsePostgresContainer() + t.Run("Should return error when execute statement without query", func(t *testing.T) { + err := NewStatement(ctx, "").Execute() - if err := pc.Dataset(basePath, "schema.sql"); err != nil { - logging.Fatal(err.Error()) - } - - Initialize() - - t.Run("Statement without query", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) - - ctx := context.Background() - err = NewStatement(ctx, "").Execute() assert.Error(t, err, query_is_empty_error) }) - t.Run("Statement", func(t *testing.T) { - datasets := []string{"clear-database.sql", "add-users.sql"} - err := pc.Dataset(basePath, datasets...) - assert.NoError(t, err) - - ctx := context.Background() + t.Run("Should execute statement", func(t *testing.T) { birth, _ := time.Parse("2006-01-02", "2021-11-22") user := User{123, "Usuário teste stmt", birth, Profile{100, "ADMIN"}} - err = NewStatement(ctx, "INSERT INTO users VALUES ($1, $2, $3, $4)", user.Id, user.Name, user.Birthday, user.Profile.Id).Execute() - assert.NoError(t, err) + statementErr := NewStatement(ctx, "INSERT INTO users VALUES ($1, $2, $3, $4)", user.Id, user.Name, user.Birthday, user.Profile.Id).Execute() result, err := NewQuery[User](ctx, query_base+" WHERE u.id = $1", user.Id).One() + assert.NoError(t, statementErr) assert.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, user.Id, result.Id)