diff --git a/Makefile b/Makefile index 1631eba..55b99ac 100755 --- a/Makefile +++ b/Makefile @@ -2,9 +2,10 @@ test: fmt GORACE="atexit_sleep_ms=50" go test -timeout=1s -race -covermode=atomic ./... + GORACE="atexit_sleep_ms=50" go test -count=1 github.com/smarty/sqldb/integration fmt: - go fmt ./... + go mod tidy && go fmt ./... compile: go build ./... diff --git a/README.md b/README.md index bf503a0..879e530 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,56 @@ [![Code Coverage](https://codecov.io/gh/smarty/sqldb/branch/master/graph/badge.svg)](https://codecov.io/gh/smarty/sqldb) [![Go Report Card](https://goreportcard.com/badge/github.com/smarty/sqldb)](https://goreportcard.com/report/github.com/smarty/sqldb) [![GoDoc](https://godoc.org/github.com/smarty/sqldb?status.svg)](http://godoc.org/github.com/smarty/sqldb) + + +## Upgrading from v2 to v3 + +Those upgrading from v2 to v3 may be interested in the following code, which can be used to adapt write operations ('Scripts') to v3 while still using a v2-style interface: + +```go +package mysql + +import ( + "context" + + "github.com/smarty/sqldb/v3" +) + +// Deprecated +type LegacyExecutor interface { + Execute(context.Context, string, ...any) (uint64, error) +} + +// Deprecated +func newLegacyExecutor(handle sqldb.Handle) LegacyExecutor { + return &legacyExecutor{handle: handle} +} + +// Deprecated +type legacyExecutor struct { + handle sqldb.Handle +} + +// Deprecated +func (this *legacyExecutor) Execute(ctx context.Context, statement string, args ...any) (uint64, error) { + script := &rowCountScript{ + BaseScript: sqldb.BaseScript{ + Text: statement, + Args: args, + }, + } + err := this.handle.Execute(ctx, script) + return script.rowsAffectedCount, err +} + +// Deprecated +type rowCountScript struct { + sqldb.BaseScript + rowsAffectedCount uint64 +} + +// Deprecated +func (this *rowCountScript) RowsAffected(rowCount uint64) { + this.rowsAffectedCount += rowCount +} +``` \ No newline at end of file diff --git a/base.go b/base.go new file mode 100644 index 0000000..4ca0205 --- /dev/null +++ b/base.go @@ -0,0 +1,29 @@ +package sqldb + +// BaseScript is a bare-minimum implementation of Script. +type BaseScript struct { + Text string + Args []any +} + +func (this BaseScript) Statements() string { + return this.Text +} +func (this BaseScript) Parameters() []any { + return this.Args +} + +// BaseQuery is a bare-minium, partial implementation of Query. +// Users are invited to embed it on types that define a Scan method, +// thus completing the Query implementation. +type BaseQuery struct { + Text string + Args []any +} + +func (this BaseQuery) Statement() string { + return this.Text +} +func (this BaseQuery) Parameters() []any { + return this.Args +} diff --git a/binding_connection_pool_adapter.go b/binding_connection_pool_adapter.go deleted file mode 100644 index b872eb7..0000000 --- a/binding_connection_pool_adapter.go +++ /dev/null @@ -1,41 +0,0 @@ -package sqldb - -import ( - "context" -) - -type BindingConnectionPoolAdapter struct { - inner ConnectionPool - selector BindingSelector - panicOnBindError bool -} - -func NewBindingConnectionPoolAdapter(actual ConnectionPool, panicOnBindError bool) *BindingConnectionPoolAdapter { - return &BindingConnectionPoolAdapter{ - inner: actual, - selector: NewBindingSelectorAdapter(actual, panicOnBindError), - panicOnBindError: panicOnBindError, - } -} - -func (this *BindingConnectionPoolAdapter) Ping(ctx context.Context) error { - return this.inner.Ping(ctx) -} -func (this *BindingConnectionPoolAdapter) BeginTransaction(ctx context.Context) (BindingTransaction, error) { - if tx, err := this.inner.BeginTransaction(ctx); err == nil { - return NewBindingTransactionAdapter(tx, this.panicOnBindError), nil - } else { - return nil, err - } -} -func (this *BindingConnectionPoolAdapter) Close() error { - return this.inner.Close() -} - -func (this *BindingConnectionPoolAdapter) Execute(ctx context.Context, statement string, parameters ...any) (uint64, error) { - return this.inner.Execute(ctx, statement, parameters...) -} - -func (this *BindingConnectionPoolAdapter) BindSelect(ctx context.Context, binder Binder, statement string, parameters ...any) error { - return this.selector.BindSelect(ctx, binder, statement, parameters...) -} diff --git a/binding_connection_pool_adapter_test.go b/binding_connection_pool_adapter_test.go deleted file mode 100644 index 359e616..0000000 --- a/binding_connection_pool_adapter_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package sqldb - -import ( - "context" - "errors" - "reflect" - "testing" - - "github.com/smarty/assertions/should" - "github.com/smarty/gunit" -) - -func TestBindingConnectionPoolAdapterFixture(t *testing.T) { - gunit.Run(new(BindingConnectionPoolAdapterFixture), t) -} - -type BindingConnectionPoolAdapterFixture struct { - *gunit.Fixture - - inner *FakeConnectionPool - pool *BindingConnectionPoolAdapter -} - -func (this *BindingConnectionPoolAdapterFixture) Setup() { - this.inner = &FakeConnectionPool{} - this.pool = NewBindingConnectionPoolAdapter(this.inner, false) -} - -/////////////////////////////////////////////////////////////// - -func (this *BindingConnectionPoolAdapterFixture) TestPing() { - this.inner.pingError = errors.New("") - - err := this.pool.Ping(context.Background()) - - this.So(err, should.Equal, this.inner.pingError) - this.So(this.inner.pingCalls, should.Equal, 1) -} - -func (this *BindingConnectionPoolAdapterFixture) TestBeginTransaction() { - transaction, err := this.pool.BeginTransaction(context.Background()) - - this.So(transaction, should.NotBeNil) - this.So(reflect.TypeOf(transaction), should.Equal, reflect.TypeOf(&BindingTransactionAdapter{})) - this.So(err, should.BeNil) -} - -func (this *BindingConnectionPoolAdapterFixture) TestBeginFailedTransaction() { - this.inner.transactionError = errors.New("") - - transaction, err := this.pool.BeginTransaction(context.Background()) - - this.So(transaction, should.BeNil) - this.So(err, should.Equal, this.inner.transactionError) -} - -func (this *BindingConnectionPoolAdapterFixture) TestClose() { - this.inner.closeError = errors.New("") - - err := this.pool.Close() - - this.So(err, should.Equal, this.inner.closeError) - this.So(this.inner.closeCalls, should.Equal, 1) -} - -func (this *BindingConnectionPoolAdapterFixture) TestExecute() { - this.inner.executeResult = 42 - this.inner.executeError = errors.New("") - - affected, err := this.pool.Execute(context.Background(), "statement") - - this.So(affected, should.Equal, this.inner.executeResult) - this.So(err, should.Equal, this.inner.executeError) - this.So(this.inner.executeStatement, should.Equal, "statement") - this.So(this.inner.executeCalls, should.Equal, 1) -} - -func (this *BindingConnectionPoolAdapterFixture) TestBindSelect() { - this.inner.selectError = errors.New("") - - err := this.pool.BindSelect(context.Background(), nil, "query", 1, 2, 3) - - this.So(err, should.Equal, this.inner.selectError) - this.So(this.inner.selectCalls, should.Equal, 1) - this.So(this.inner.selectStatement, should.Equal, "query") - this.So(this.inner.selectParameters, should.Resemble, []any{1, 2, 3}) -} diff --git a/binding_selector_adapter.go b/binding_selector_adapter.go deleted file mode 100644 index 03d872d..0000000 --- a/binding_selector_adapter.go +++ /dev/null @@ -1,37 +0,0 @@ -package sqldb - -import "context" - -type BindingSelectorAdapter struct { - selector Selector - panicOnBindError bool -} - -func NewBindingSelectorAdapter(selector Selector, panicOnBindError bool) *BindingSelectorAdapter { - return &BindingSelectorAdapter{selector: selector, panicOnBindError: panicOnBindError} -} - -func (this *BindingSelectorAdapter) BindSelect(ctx context.Context, binder Binder, statement string, parameters ...any) error { - result, err := this.selector.Select(ctx, statement, parameters...) - if err != nil { - return err - } - - for result.Next() { - if err := result.Err(); err != nil { - _ = result.Close() - return err - } - - if err := binder(result); err != nil { - _ = result.Close() - if this.panicOnBindError { - panic(err) - } else { - return err - } - } - } - - return result.Close() -} diff --git a/binding_selector_adapter_test.go b/binding_selector_adapter_test.go deleted file mode 100644 index 70684f4..0000000 --- a/binding_selector_adapter_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package sqldb - -import ( - "context" - "errors" - "testing" - - "github.com/smarty/assertions/should" - "github.com/smarty/gunit" -) - -func TestBindingSelectorAdapterFixture(t *testing.T) { - gunit.Run(new(BindingSelectorAdapterFixture), t) -} - -type BindingSelectorAdapterFixture struct { - *gunit.Fixture - - fakeResult *FakeSelectResult - fakeInnerSelector *FakeSelector - selector *BindingSelectorAdapter -} - -func (this *BindingSelectorAdapterFixture) Setup() { - this.fakeResult = &FakeSelectResult{} - this.fakeInnerSelector = &FakeSelector{selectResult: this.fakeResult} - this.selector = NewBindingSelectorAdapter(this.fakeInnerSelector, false) -} - -/////////////////////////////////////////////////////////////// - -func (this *BindingSelectorAdapterFixture) TestFailedSelectReturnsError() { - this.fakeInnerSelector.selectError = errors.New("") - - err := this.selector.BindSelect(context.Background(), nil, "query", 1, 2, 3) - - this.So(err, should.Equal, this.fakeInnerSelector.selectError) - this.So(this.fakeInnerSelector.selects, should.Equal, 1) - this.So(this.fakeInnerSelector.statement, should.Equal, "query") - this.So(this.fakeInnerSelector.parameters, should.Resemble, []any{1, 2, 3}) -} - -func (this *BindingSelectorAdapterFixture) TestEmptyResult() { - err := this.selector.BindSelect(context.Background(), nil, "query", 1, 2, 3) - this.So(err, should.BeNil) - this.So(this.fakeInnerSelector.selects, should.Equal, 1) - this.So(this.fakeResult.nextCalls, should.Equal, 1) - this.So(this.fakeResult.closeCalls, should.Equal, 1) -} - -func (this *BindingSelectorAdapterFixture) TestResultErrorClosesAndReturnsError() { - this.fakeResult.iterations = 1 - this.fakeResult.errError = errors.New("") - - err := this.selector.BindSelect(context.Background(), nil, "query", 1, 2, 3) - this.So(err, should.Equal, this.fakeResult.errError) - this.So(this.fakeInnerSelector.selects, should.Equal, 1) - this.So(this.fakeResult.nextCalls, should.Equal, 1) - this.So(this.fakeResult.errCalls, should.Equal, 1) - this.So(this.fakeResult.closeCalls, should.Equal, 1) -} - -func (this *BindingSelectorAdapterFixture) TestScanErrorClosesAndReturnsError() { - this.fakeResult.iterations = 1 - this.fakeResult.scanError = errors.New("") - - err := this.selector.BindSelect(context.Background(), func(source Scanner) error { - return source.Scan() - }, "query", 1, 2, 3) - - this.So(err, should.Equal, this.fakeResult.scanError) - this.So(this.fakeInnerSelector.selects, should.Equal, 1) - this.So(this.fakeResult.nextCalls, should.Equal, 1) - this.So(this.fakeResult.errCalls, should.Equal, 1) - this.So(this.fakeResult.scanCalls, should.Equal, 1) - this.So(this.fakeResult.closeCalls, should.Equal, 1) -} - -func (this *BindingSelectorAdapterFixture) TestScanErrorClosesAndPanicsWhenConfigured() { - this.selector.panicOnBindError = true - this.fakeResult.iterations = 1 - this.fakeResult.scanError = errors.New("") - - this.So(func() { - this.selector.BindSelect(context.Background(), func(source Scanner) error { - return source.Scan() - }, "query", 1, 2, 3) - }, should.Panic) -} - -/////////////////////////////////////////////////////////////// - -type FakeSelector struct { - selects int - statement string - parameters []any - selectResult *FakeSelectResult - selectError error -} - -func (this *FakeSelector) Select(_ context.Context, statement string, parameters ...any) (SelectResult, error) { - this.selects++ - this.statement = statement - this.parameters = parameters - return this.selectResult, this.selectError -} diff --git a/binding_transaction_adapter.go b/binding_transaction_adapter.go deleted file mode 100644 index fd0bb10..0000000 --- a/binding_transaction_adapter.go +++ /dev/null @@ -1,19 +0,0 @@ -package sqldb - -import "context" - -type BindingTransactionAdapter struct { - Transaction - selector BindingSelector -} - -func NewBindingTransactionAdapter(actual Transaction, panicOnBindError bool) *BindingTransactionAdapter { - return &BindingTransactionAdapter{ - Transaction: actual, - selector: NewBindingSelectorAdapter(actual, panicOnBindError), - } -} - -func (this *BindingTransactionAdapter) BindSelect(ctx context.Context, binder Binder, statement string, parameters ...any) error { - return this.selector.BindSelect(ctx, binder, statement, parameters...) -} diff --git a/binding_transaction_adapter_test.go b/binding_transaction_adapter_test.go deleted file mode 100644 index 138a204..0000000 --- a/binding_transaction_adapter_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package sqldb - -import ( - "context" - "errors" - "testing" - - "github.com/smarty/assertions/should" - "github.com/smarty/gunit" -) - -func TestBindingTransactionAdapterFixture(t *testing.T) { - gunit.Run(new(BindingTransactionAdapterFixture), t) -} - -type BindingTransactionAdapterFixture struct { - *gunit.Fixture - - inner *FakeTransaction - transaction *BindingTransactionAdapter -} - -func (this *BindingTransactionAdapterFixture) Setup() { - this.inner = &FakeTransaction{} - this.transaction = NewBindingTransactionAdapter(this.inner, false) -} - -/////////////////////////////////////////////////////////////// - -func (this *BindingTransactionAdapterFixture) TestCommit() { - this.inner.commitError = errors.New("") - - err := this.transaction.Commit() - - this.So(err, should.Equal, this.inner.commitError) - this.So(this.inner.commitCalls, should.Equal, 1) -} - -func (this *BindingTransactionAdapterFixture) TestRollback() { - this.inner.rollbackError = errors.New("") - - err := this.transaction.Rollback() - - this.So(err, should.Equal, this.inner.rollbackError) - this.So(this.inner.rollbackCalls, should.Equal, 1) -} - -func (this *BindingTransactionAdapterFixture) TestExecute() { - this.inner.executeResult = 42 - this.inner.executeError = errors.New("") - - affected, err := this.transaction.Execute(context.Background(), "statement") - - this.So(affected, should.Equal, this.inner.executeResult) - this.So(err, should.Equal, this.inner.executeError) - this.So(this.inner.executeStatement, should.Equal, "statement") - this.So(this.inner.executeCalls, should.Equal, 1) -} - -func (this *BindingTransactionAdapterFixture) TestBindSelect() { - this.inner.selectError = errors.New("") - - err := this.transaction.BindSelect(context.Background(), nil, "query", 1, 2, 3) - - this.So(err, should.Equal, this.inner.selectError) - this.So(this.inner.selectCalls, should.Equal, 1) - this.So(this.inner.selectStatement, should.Equal, "query") - this.So(this.inner.selectParameters, should.Resemble, []any{1, 2, 3}) -} diff --git a/config.go b/config.go index 3272d7e..1576b78 100644 --- a/config.go +++ b/config.go @@ -1,102 +1,40 @@ package sqldb -import ( - "database/sql" - "time" -) - type configuration struct { - txOptions *sql.TxOptions - splitStatement bool - panicOnBindError bool - stackTraceOnError bool - parameterPrefix string - retrySleep time.Duration -} - -func NewPool(handle *sql.DB, options ...option) ConnectionPool { - var config configuration - Options.apply(options...)(&config) - return newPool(handle, config) -} -func NewBindingPool(handle *sql.DB, options ...option) BindingConnectionPool { - var config configuration - Options.apply(options...)(&config) - return newBindingPool(handle, config) -} -func newPool(handle *sql.DB, config configuration) ConnectionPool { - var pool ConnectionPool = NewLibraryConnectionPoolAdapter(handle, config.txOptions) - pool = NewNormalizeContextCancellationConnectionPool(pool) - - if config.splitStatement { - pool = NewSplitStatementConnectionPool(pool, config.parameterPrefix) - } - - if config.stackTraceOnError { - pool = NewStackTraceConnectionPool(pool) - } - - return pool -} -func newBindingPool(handle *sql.DB, config configuration) BindingConnectionPool { - inner := newPool(handle, config) - var pool BindingConnectionPool = NewBindingConnectionPoolAdapter(inner, config.panicOnBindError) - - if config.retrySleep > 0 { - pool = NewRetryBindingConnectionPool(pool, config.retrySleep) - } - - return pool + logger logger + threshold int } +type option func(*configuration) var Options singleton type singleton struct{} -type option func(*configuration) - -func (singleton) TxOptions(value *sql.TxOptions) option { - return func(this *configuration) { this.txOptions = value } -} -func (singleton) PanicOnBindError(value bool) option { - return func(this *configuration) { this.panicOnBindError = value } -} -func (singleton) MySQL() option { - return func(this *configuration) { this.splitStatement = true; this.parameterPrefix = "?" } -} -func (singleton) ParameterPrefix(value string) option { - return func(this *configuration) { this.parameterPrefix = value } -} -func (singleton) SplitStatement(value bool) option { - return func(this *configuration) { this.splitStatement = value } -} -func (singleton) RetrySleep(value time.Duration) option { - return func(this *configuration) { this.retrySleep = value } -} -func (singleton) StackTraceErrDiagnostics(value bool) option { - return func(this *configuration) { this.stackTraceOnError = value } -} func (singleton) apply(options ...option) option { return func(this *configuration) { - for _, option := range Options.defaults(options...) { - option(this) + for _, item := range Options.defaults(options...) { + item(this) } } } func (singleton) defaults(options ...option) []option { - var defaultTxOptions = &sql.TxOptions{Isolation: sql.LevelReadCommitted} - const defaultStackTraceErrDiagnostics = true - const defaultPanicOnBindError = true - const defaultSplitStatement = true - const defaultParameterPrefix = "?" - const defaultRetrySleep = 0 - return append([]option{ - Options.TxOptions(defaultTxOptions), - Options.PanicOnBindError(defaultPanicOnBindError), - Options.StackTraceErrDiagnostics(defaultStackTraceErrDiagnostics), - Options.ParameterPrefix(defaultParameterPrefix), - Options.SplitStatement(defaultSplitStatement), - Options.RetrySleep(defaultRetrySleep), + Options.Logger(&nop{}), + Options.PreparationThreshold(1), }, options...) } + +func (singleton) Logger(logger logger) option { + return func(this *configuration) { this.logger = logger } +} + +// PreparationThreshold specifies the number of times a give sql statement can be +// executed before it will be transitioned to a prepared statement. Passing a negative +// value will disable any use of prepared statements. +func (singleton) PreparationThreshold(n int) option { + return func(this *configuration) { this.threshold = n } +} + +type nop struct{} + +func (nop) Printf(string, ...any) {} diff --git a/config_tx.go b/config_tx.go deleted file mode 100644 index c32bf9b..0000000 --- a/config_tx.go +++ /dev/null @@ -1,82 +0,0 @@ -package sqldb - -import ( - "database/sql" -) - -type txConfig struct { - splitStatement bool - panicOnBindError bool - stackTraceOnError bool - parameterPrefix string -} - -func NewTransaction(handle *sql.Tx, options ...txOption) Transaction { - var config txConfig - TxOptions.apply(options...)(&config) - return newTx(handle, config) -} -func NewBindingTransaction(handle *sql.Tx, options ...txOption) BindingTransaction { - var config txConfig - TxOptions.apply(options...)(&config) - return newBindingTx(handle, config) -} -func newTx(handle *sql.Tx, config txConfig) Transaction { - var tx Transaction = NewLibraryTransactionAdapter(handle) - - if config.splitStatement { - tx = NewSplitStatementTransaction(tx, config.parameterPrefix) - } - - if config.stackTraceOnError { - tx = NewStackTraceTransaction(tx) - } - - return tx -} -func newBindingTx(handle *sql.Tx, config txConfig) BindingTransaction { - inner := newTx(handle, config) - return NewBindingTransactionAdapter(inner, config.panicOnBindError) -} - -var TxOptions txSingleton - -type txSingleton struct{} -type txOption func(*txConfig) - -func (txSingleton) PanicOnBindError(value bool) txOption { - return func(this *txConfig) { this.panicOnBindError = value } -} -func (txSingleton) MySQL() txOption { - return func(this *txConfig) { this.splitStatement = true; this.parameterPrefix = "?" } -} -func (txSingleton) ParameterPrefix(value string) txOption { - return func(this *txConfig) { this.parameterPrefix = value } -} -func (txSingleton) SplitStatement(value bool) txOption { - return func(this *txConfig) { this.splitStatement = value } -} -func (txSingleton) StackTraceErrDiagnostics(value bool) txOption { - return func(this *txConfig) { this.stackTraceOnError = value } -} - -func (txSingleton) apply(txOptions ...txOption) txOption { - return func(this *txConfig) { - for _, txOption := range TxOptions.defaults(txOptions...) { - txOption(this) - } - } -} -func (txSingleton) defaults(txOptions ...txOption) []txOption { - const defaultStackTraceErrDiagnostics = true - const defaultPanicOnBindError = true - const defaultSplitStatement = true - const defaultParameterPrefix = "?" - - return append([]txOption{ - TxOptions.PanicOnBindError(defaultPanicOnBindError), - TxOptions.StackTraceErrDiagnostics(defaultStackTraceErrDiagnostics), - TxOptions.ParameterPrefix(defaultParameterPrefix), - TxOptions.SplitStatement(defaultSplitStatement), - }, txOptions...) -} diff --git a/contracts.go b/contracts.go new file mode 100644 index 0000000..b868ce3 --- /dev/null +++ b/contracts.go @@ -0,0 +1,68 @@ +package sqldb + +import ( + "context" + "database/sql" + "errors" +) + +var ErrParameterCountMismatch = errors.New("the number of parameters supplied does not match the statement") + +type ( + logger interface { + Printf(string, ...any) + } + + // Pool is a common subset of methods implemented by *sql.DB and *sql.Tx. + // The name is a nod to that fact that a *sql.DB implements a Pool of connections. + Pool interface { + PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row + } + + // Handle is a high level approach to common database operations, where each operation implements either + // the Query or Script interface. + Handle interface { + Execute(context.Context, ...Script) error + Populate(context.Context, ...Query) error + PopulateRow(context.Context, ...Query) error + } + + // Script represents SQL statements that aren't expected to provide rows as a result. + Script interface { + // Statements returns a string containing 1 or more SQL statements, separated by `;`. + // This means that the ';' character should NOT be used within any of the statements. + Statements() string + + // Parameters returns a slice of the parameters to be interleaved across all SQL + // returned from Statements(). + Parameters() []any + } + + // RowsAffected provides an (optional) hook for a type implementing Script to receive + // the number of rows affected by executing a statement provided by a Script. It is + // called for each statement that doesn't result in an error. + RowsAffected interface { + RowsAffected(uint64) + } + + // Query represents a SQL statement that is expected to provide rows as a result. + // Rows are provided to the Scan method. + Query interface { + // Statement returns a string containing a single SQL query. + Statement() string + + // Parameters returns a slice of the parameters to be used in the query. + Parameters() []any + + // Scan specifies a callback to be provided the *sql.Rows for each retrieved record. + Scan(Scanner) error + } + + // Scanner is implemented by *sql.Row and *sql.Rows. + Scanner interface { + Scan(...any) error + } +) diff --git a/db.go b/db.go new file mode 100644 index 0000000..a7a92ef --- /dev/null +++ b/db.go @@ -0,0 +1,181 @@ +package sqldb + +import ( + "context" + "database/sql" + "errors" + "fmt" + "hash/fnv" + "iter" + "runtime/debug" + "strings" +) + +type defaultHandle struct { + pool Pool + logger logger + threshold int + counts map[uint64]int // map[sql-statement-checksum]count + prepared map[uint64]*sql.Stmt // map[sql-statement-checksum]stmt +} + +func New(handle Pool, options ...option) Handle { + var config configuration + Options.apply(options...)(&config) + return &defaultHandle{ + pool: handle, + logger: config.logger, + threshold: config.threshold, + counts: make(map[uint64]int), + prepared: make(map[uint64]*sql.Stmt), + } +} + +func (this *defaultHandle) prepare(ctx context.Context, rawStatement string) (*sql.Stmt, error) { + if this.threshold < 0 { + return nil, nil + } + if len(this.counts) > 1024*64 { // Put some kind of cap on how many statements we will track. + return nil, nil + } + checksum := checksum([]byte(rawStatement)) + if this.counts[checksum] < this.threshold { + this.counts[checksum]++ + return nil, nil + } + statement, ok := this.prepared[checksum] + if ok { + return statement, nil + } + statement, err := this.pool.PrepareContext(ctx, rawStatement) + if err != nil { + return nil, err + } + this.prepared[checksum] = statement + return statement, nil +} +func checksum(x []byte) (hash uint64) { + h := fnv.New64a() + _, _ = h.Write(x) + return h.Sum64() +} + +func (this *defaultHandle) Execute(ctx context.Context, scripts ...Script) (err error) { + defer func() { err = normalizeErr(err) }() + for _, script := range scripts { + statements := script.Statements() + parameters := script.Parameters() + placeholderCount := strings.Count(statements, "?") + if placeholderCount != len(parameters) { + return fmt.Errorf("%w: Expected: %d, received %d", ErrParameterCountMismatch, placeholderCount, len(parameters)) + } + for statement, params := range interleaveParameters(statements, parameters...) { + prepared, err := this.prepare(ctx, statement) + if err != nil { + return err + } + var result sql.Result + if prepared != nil { + result, err = prepared.ExecContext(ctx, params...) + } else { + result, err = this.pool.ExecContext(ctx, statement, params...) + } + if err != nil { + return err + } + if rows, ok := script.(RowsAffected); ok { + if affected, err := result.RowsAffected(); err == nil { + rows.RowsAffected(uint64(affected)) + } + } + } + } + return nil +} +func (this *defaultHandle) Populate(ctx context.Context, queries ...Query) (err error) { + defer func() { err = normalizeErr(err) }() + for _, query := range queries { + statement := query.Statement() + prepared, err := this.prepare(ctx, statement) + if err != nil { + return err + } + parameters := query.Parameters() + var rows *sql.Rows + if prepared != nil { + rows, err = prepared.QueryContext(ctx, parameters...) + } else { + rows, err = this.pool.QueryContext(ctx, statement, parameters...) + } + if err != nil { + return err + } + for rows.Next() { + err = query.Scan(rows) + if err != nil { + _ = rows.Close() + return err + } + } + _ = rows.Close() + } + return nil +} +func (this *defaultHandle) PopulateRow(ctx context.Context, queries ...Query) (err error) { + defer func() { err = normalizeErr(err) }() + for _, query := range queries { + statement := query.Statement() + prepared, err := this.prepare(ctx, statement) + if err != nil { + return err + } + parameters := query.Parameters() + var row *sql.Row + if prepared != nil { + row = prepared.QueryRowContext(ctx, parameters...) + } else { + row = this.pool.QueryRowContext(ctx, statement, parameters...) + } + err = query.Scan(row) + if err == nil { + continue + } + if errors.Is(err, sql.ErrNoRows) { + continue + } + return err + } + return nil +} + +// interleaveParameters splits the statements (on ';') and yields each with its corresponding parameters. +func interleaveParameters(statements string, parameters ...any) iter.Seq2[string, []any] { + return func(yield func(string, []any) bool) { + index := 0 + for statement := range strings.SplitSeq(statements, ";") { + if len(strings.TrimSpace(statement)) == 0 { + continue + } + statement += ";" // terminate the statement + indexOffset := strings.Count(statement, "?") + params := parameters[index : index+indexOffset] + index += indexOffset + if !yield(statement, params) { + return + } + } + } +} + +// normalizeErr attaches a stack trace to non-nil errors and also normalizes errors that are +// semantically equal to context.Canceled. At present we are unaware whether this is still a +// commonly encountered scenario. +func normalizeErr(err error) error { + if err == nil { + return nil + } + if strings.Contains(err.Error(), "operation was canceled") { + return fmt.Errorf("%w: %w", context.Canceled, err) + } + return fmt.Errorf("%w\nStack Trace:\n%s", err, string(debug.Stack())) +} diff --git a/db_test.go b/db_test.go new file mode 100644 index 0000000..a6513b1 --- /dev/null +++ b/db_test.go @@ -0,0 +1,32 @@ +package sqldb + +import ( + "reflect" + "testing" +) + +func TestInterleaveParameters(t *testing.T) { + actual := make(map[string][]any) + for statement, args := range interleaveParameters("?,?;"+"?;;;"+"?,?,?", 1, 2, 3, 4, 5, 6) { + actual[statement] = args + } + expected := map[string][]any{ + "?,?;": {1, 2}, + "?;": {3}, + "?,?,?;": {4, 5, 6}, + } + assertEqual(t, expected, actual) +} + +func assertEqual(t *testing.T, expected, actual any) { + if reflect.DeepEqual(expected, actual) { + return + } + t.Helper() + t.Errorf("\n"+ + "expected: %v\n"+ + "actual: %v", + expected, + actual, + ) +} diff --git a/doc_test.go b/doc_test.go deleted file mode 100644 index af89360..0000000 --- a/doc_test.go +++ /dev/null @@ -1,241 +0,0 @@ -package sqldb - -import ( - "context" - "strings" -) - -/////////////////////////////////////////////////////////////// - -type FakeConnectionPool struct { - pingCalls int - pingError error - - transactionCalls int - transaction *FakeTransaction - transactionError error - - closeCalls int - closeError error - - selectCalls int - selectStatement string - selectParameters []any - selectResult *FakeSelectResult - selectError error - - executeCalls int - executeStatement string - executeParameters []any - executeResult uint64 - executeError error -} - -func (this *FakeConnectionPool) Ping(_ context.Context) error { - this.pingCalls++ - return this.pingError -} - -func (this *FakeConnectionPool) BeginTransaction(_ context.Context) (Transaction, error) { - this.transactionCalls++ - return this.transaction, this.transactionError -} - -func (this *FakeConnectionPool) Close() error { - this.closeCalls++ - return this.closeError -} - -func (this *FakeConnectionPool) Execute(_ context.Context, statement string, parameters ...any) (uint64, error) { - this.executeCalls++ - this.executeStatement = statement - this.executeParameters = parameters - return this.executeResult, this.executeError -} - -func (this *FakeConnectionPool) Select(_ context.Context, statement string, parameters ...any) (SelectResult, error) { - this.selectCalls++ - this.selectStatement = statement - this.selectParameters = parameters - return this.selectResult, this.selectError -} - -/////////////////////////////////////////////////////////////// - -type FakeTransaction struct { - commitCalls int - commitError error - - rollbackCalls int - rollbackError error - - selectCalls int - selectStatement string - selectParameters []any - selectResult *FakeSelectResult - selectError error - - executeCalls int - executeStatement string - executeParameters []any - executeResult uint64 - executeError error -} - -func (this *FakeTransaction) Commit() error { - this.commitCalls++ - return this.commitError -} - -func (this *FakeTransaction) Rollback() error { - this.rollbackCalls++ - return this.rollbackError -} - -func (this *FakeTransaction) Execute(_ context.Context, statement string, parameters ...any) (uint64, error) { - this.executeCalls++ - this.executeStatement = statement - this.executeParameters = parameters - return this.executeResult, this.executeError -} - -func (this *FakeTransaction) Select(_ context.Context, statement string, parameters ...any) (SelectResult, error) { - this.selectCalls++ - this.selectStatement = statement - this.selectParameters = parameters - return this.selectResult, this.selectError -} - -/////////////////////////////////////////////////////////////// - -type FakeSelectResult struct { - nextCalls int - errCalls int - closeCalls int - scanCalls int - iterations int - - errError error - closeError error - scanError error -} - -func (this *FakeSelectResult) Next() bool { - this.nextCalls++ - return this.iterations >= this.nextCalls -} - -func (this *FakeSelectResult) Err() error { - this.errCalls++ - return this.errError -} - -func (this *FakeSelectResult) Close() error { - this.closeCalls++ - return this.closeError -} - -func (this *FakeSelectResult) Scan(_ ...any) error { - this.scanCalls++ - return this.scanError -} - -/////////////////////////////////////////////////////////////// - -type FakeExecutor struct { - affected uint64 - errorsToReturn []error - statements []string - parameters [][]any -} - -func (this *FakeExecutor) Execute(_ context.Context, statement string, parameters ...any) (uint64, error) { - this.statements = append(this.statements, strings.TrimSpace(statement)) - this.parameters = append(this.parameters, parameters) - - if len(this.statements) <= len(this.errorsToReturn) { - return this.affected, this.errorsToReturn[len(this.statements)-1] - } - - return this.affected, nil -} - -/////////////////////////////////////////////////////////////// - -type FakeBindingConnectionPool struct { - pingCalls int - pingError error - - transactionCalls int - transaction *FakeBindingTransaction - transactionError error - - closeCalls int - closeError error - - selectCalls int - selectBinder Binder - selectStatement string - selectParameters []any - selectResult *FakeSelectResult - selectError error - - executeCalls int - executeStatement string - executeParameters []any - executeResult uint64 - executeError error -} - -func (this *FakeBindingConnectionPool) Ping(_ context.Context) error { - this.pingCalls++ - return this.pingError -} - -func (this *FakeBindingConnectionPool) BeginTransaction(_ context.Context) (BindingTransaction, error) { - this.transactionCalls++ - return this.transaction, this.transactionError -} - -func (this *FakeBindingConnectionPool) Close() error { - this.closeCalls++ - return this.closeError -} - -func (this *FakeBindingConnectionPool) Execute(_ context.Context, statement string, parameters ...any) (uint64, error) { - this.executeCalls++ - this.executeStatement = statement - this.executeParameters = parameters - return this.executeResult, this.executeError -} - -func (this *FakeBindingConnectionPool) BindSelect(_ context.Context, binder Binder, statement string, parameters ...any) error { - this.selectCalls++ - this.selectBinder = binder - this.selectStatement = statement - this.selectParameters = parameters - return this.selectError -} - -/////////////////////////////////////////////////////////////// - -type FakeBindingTransaction struct { -} - -func (this *FakeBindingTransaction) Commit() error { - panic("Not called") -} - -func (this *FakeBindingTransaction) Rollback() error { - panic("Not called") -} - -func (this *FakeBindingTransaction) Execute(_ context.Context, _ string, _ ...any) (uint64, error) { - panic("Not called") -} - -func (this *FakeBindingTransaction) BindSelect(_ context.Context, _ Binder, _ string, _ ...any) error { - panic("Not called") -} - -/////////////////////////////////////////////////////////////// diff --git a/go.mod b/go.mod index 27e2979..92fe81c 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,3 @@ -module github.com/smarty/sqldb/v2 +module github.com/smarty/sqldb/v3 -go 1.22 - -require ( - github.com/smarty/assertions v1.16.0 - github.com/smarty/gunit v1.5.0 -) +go 1.24 diff --git a/go.sum b/go.sum index 0855eb3..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +0,0 @@ -github.com/smarty/assertions v1.16.0 h1:EvHNkdRA4QHMrn75NZSoUQ/mAUXAYWfatfB01yTCzfY= -github.com/smarty/assertions v1.16.0/go.mod h1:duaaFdCS0K9dnoM50iyek/eYINOZ64gbh1Xlf6LG7AI= -github.com/smarty/gunit v1.5.0 h1:OmG6a/rgi7qCjlQis6VjXbvx/WqZ8I6xSlbfN4YB5MY= -github.com/smarty/gunit v1.5.0/go.mod h1:uAeNibUD292KZRcg5OTy7lb6WR5++UC0BQOzNuiRzpU= diff --git a/go.work b/go.work new file mode 100644 index 0000000..5225fef --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.25 + +use ( + . + integration +) diff --git a/integration/db_test.go b/integration/db_test.go new file mode 100644 index 0000000..8322aef --- /dev/null +++ b/integration/db_test.go @@ -0,0 +1,147 @@ +package integration + +import ( + "database/sql" + "log" + "testing" + + "github.com/smarty/gunit/v2" + "github.com/smarty/gunit/v2/better" + "github.com/smarty/gunit/v2/should" + "github.com/smarty/sqldb/v3" + + _ "github.com/mattn/go-sqlite3" +) + +func TestFixture(t *testing.T) { + gunit.Run(new(Fixture), t) +} + +type Fixture struct { + *gunit.Fixture + db *sql.DB + tx *sql.Tx + DB sqldb.Handle +} + +func (this *Fixture) Setup() { + var err error + this.db, err = sql.Open("sqlite3", ":memory:") + this.So(err, better.BeNil) + + this.tx, err = this.db.BeginTx(this.Context(), nil) + this.So(err, better.BeNil) + + this.DB = sqldb.New(this.db, + sqldb.Options.Logger(log.New(this.Output(), this.Name()+": ", 0)), + sqldb.Options.PreparationThreshold(5), + ) + ddl := &DDL{} + err = this.DB.Execute(this.Context(), ddl) + this.So(err, better.BeNil) + this.So(ddl.totalRows, should.Equal, 4) +} +func (this *Fixture) Teardown() { + this.So(this.tx.Rollback(), better.BeNil) + this.So(this.db.Close(), better.BeNil) +} + +func (this *Fixture) TestQuery() { + for range 10 { // should transition to prepared statements + query := &SelectAll{Result: make(map[int]string)} + err := this.DB.Populate(this.Context(), query) + this.So(err, better.BeNil) + this.So(query.Result, should.Equal, map[int]string{ + 1: "a", + 2: "b", + 3: "c", + 4: "d", + }) + } +} +func (this *Fixture) TestQueryRow() { + query := &SelectRow{id: 1} + err := this.DB.PopulateRow(this.Context(), query) + this.So(err, better.BeNil) + this.So(query.value, should.Equal, "a") +} +func (this *Fixture) TestQueryQueryRow_NoResult() { + query := &SelectRow{id: 5} + err := this.DB.PopulateRow(this.Context(), query) + this.So(err, better.BeNil) + this.So(query.value, should.BeEmpty) +} + +/////////////////////////////////////////////// + +type DDL struct { + totalRows uint64 +} + +func (this *DDL) Statements() string { + return ` + DROP TABLE IF EXISTS sqldb_integration_test; + + CREATE TABLE sqldb_integration_test ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL + ); + + INSERT INTO sqldb_integration_test (name) VALUES (?); + INSERT INTO sqldb_integration_test (name) VALUES (?); + INSERT INTO sqldb_integration_test (name) VALUES (?); + INSERT INTO sqldb_integration_test (name) VALUES (?);` +} +func (this *DDL) Parameters() []any { + return []any{ + "a", + "b", + "c", + "d", + } +} +func (this *DDL) RowsAffected(rows uint64) { + this.totalRows += rows +} + +/////////////////////////////////////////////// + +type SelectAll struct { + Result map[int]string +} + +func (this *SelectAll) Statement() string { + return ` + SELECT id, name + FROM sqldb_integration_test;` +} + +func (this *SelectAll) Parameters() []any { + return nil +} + +func (this *SelectAll) Scan(scanner sqldb.Scanner) error { + var id int + var name string + defer func() { this.Result[id] = name }() + return scanner.Scan(&id, &name) +} + +/////////////////////////////////////////////// + +type SelectRow struct { + id int + value string +} + +func (this *SelectRow) Statement() string { + return `SELECT name FROM sqldb_integration_test WHERE id = ?;` +} +func (this *SelectRow) Parameters() []any { + return []any{this.id} +} +func (this *SelectRow) Scan(scanner sqldb.Scanner) error { + return scanner.Scan(&this.value) +} + +/////////////////////////////////////////////// diff --git a/integration/go.mod b/integration/go.mod new file mode 100644 index 0000000..a343efe --- /dev/null +++ b/integration/go.mod @@ -0,0 +1,11 @@ +module github.com/smarty/sqldb/integration + +go 1.25 + +require ( + github.com/mattn/go-sqlite3 v1.14.32 + github.com/smarty/gunit/v2 v2.0.0-20250910224800-b24d6a1628bf + github.com/smarty/sqldb/v3 v3.0.0 +) + +replace github.com/smarty/sqldb/v3 => ../ diff --git a/integration/go.sum b/integration/go.sum new file mode 100644 index 0000000..039e6e5 --- /dev/null +++ b/integration/go.sum @@ -0,0 +1,4 @@ +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/smarty/gunit/v2 v2.0.0-20250910224800-b24d6a1628bf h1:2z0AVf/sIEX+34bp8O/elAQwPoPwMuC5pXjZNqgbuuY= +github.com/smarty/gunit/v2 v2.0.0-20250910224800-b24d6a1628bf/go.mod h1:8zgLMlQPLDFO5SVz7TrnUS1ADFiHHMChB8zPIQ4R4Ys= diff --git a/interfaces.go b/interfaces.go deleted file mode 100644 index bbcfaee..0000000 --- a/interfaces.go +++ /dev/null @@ -1,72 +0,0 @@ -package sqldb - -import "context" - -type ( - ConnectionPool interface { - Ping(context.Context) error - BeginTransaction(context.Context) (Transaction, error) - Close() error - Executor - Selector - } - - Transaction interface { - Commit() error - Rollback() error - Executor - Selector - } - - Executor interface { - Execute(context.Context, string, ...any) (uint64, error) - } - - Selector interface { - Select(context.Context, string, ...any) (SelectResult, error) - } - - SelectExecutor interface { - Selector - Executor - } - - SelectResult interface { - Next() bool - Err() error - Close() error - Scanner - } - - Scanner interface { - Scan(...any) error - } -) - -type ( - BindingConnectionPool interface { - Ping(context.Context) error - BeginTransaction(context.Context) (BindingTransaction, error) - Close() error - Executor - BindingSelector - } - - BindingTransaction interface { - Commit() error - Rollback() error - Executor - BindingSelector - } - - BindingSelector interface { - BindSelect(context.Context, Binder, string, ...any) error - } - - Binder func(Scanner) error - - BindingSelectExecutor interface { - BindingSelector - Executor - } -) diff --git a/library_connection_pool_adapter.go b/library_connection_pool_adapter.go deleted file mode 100644 index ffafa4d..0000000 --- a/library_connection_pool_adapter.go +++ /dev/null @@ -1,40 +0,0 @@ -package sqldb - -import ( - "context" - "database/sql" -) - -type LibraryConnectionPoolAdapter struct { - *sql.DB - txOptions *sql.TxOptions -} - -func NewLibraryConnectionPoolAdapter(actual *sql.DB, txOptions *sql.TxOptions) *LibraryConnectionPoolAdapter { - return &LibraryConnectionPoolAdapter{DB: actual, txOptions: txOptions} -} - -func (this *LibraryConnectionPoolAdapter) Ping(ctx context.Context) error { - return this.DB.PingContext(ctx) -} - -func (this *LibraryConnectionPoolAdapter) BeginTransaction(ctx context.Context) (Transaction, error) { - if tx, err := this.DB.BeginTx(ctx, this.txOptions); err == nil { - return NewLibraryTransactionAdapter(tx), nil - } else { - return nil, err - } -} - -func (this *LibraryConnectionPoolAdapter) Execute(ctx context.Context, query string, parameters ...any) (uint64, error) { - if result, err := this.DB.ExecContext(ctx, query, parameters...); err != nil { - return 0, err - } else { - count, _ := result.RowsAffected() - return uint64(count), nil - } -} - -func (this *LibraryConnectionPoolAdapter) Select(ctx context.Context, query string, parameters ...any) (SelectResult, error) { - return this.DB.QueryContext(ctx, query, parameters...) -} diff --git a/library_transaction_adapter.go b/library_transaction_adapter.go deleted file mode 100644 index e60f0ed..0000000 --- a/library_transaction_adapter.go +++ /dev/null @@ -1,27 +0,0 @@ -package sqldb - -import ( - "context" - "database/sql" -) - -type LibraryTransactionAdapter struct { - *sql.Tx -} - -func NewLibraryTransactionAdapter(actual *sql.Tx) *LibraryTransactionAdapter { - return &LibraryTransactionAdapter{Tx: actual} -} - -func (this *LibraryTransactionAdapter) Execute(ctx context.Context, query string, parameters ...any) (uint64, error) { - if result, err := this.Tx.ExecContext(ctx, query, parameters...); err != nil { - return 0, err - } else { - count, _ := result.RowsAffected() - return uint64(count), nil - } -} - -func (this *LibraryTransactionAdapter) Select(ctx context.Context, query string, parameters ...any) (SelectResult, error) { - return this.Tx.QueryContext(ctx, query, parameters...) -} diff --git a/normalize_context_cancellation_connection_pool.go b/normalize_context_cancellation_connection_pool.go deleted file mode 100644 index 2cf080f..0000000 --- a/normalize_context_cancellation_connection_pool.go +++ /dev/null @@ -1,55 +0,0 @@ -package sqldb - -import ( - "context" - "fmt" - "strings" -) - -type NormalizeContextCancellationConnectionPool struct { - inner ConnectionPool -} - -func NewNormalizeContextCancellationConnectionPool(inner ConnectionPool) *NormalizeContextCancellationConnectionPool { - return &NormalizeContextCancellationConnectionPool{inner: inner} -} - -func (this *NormalizeContextCancellationConnectionPool) Ping(ctx context.Context) error { - return this.normalizeContextCancellationError(this.inner.Ping(ctx)) -} - -func (this *NormalizeContextCancellationConnectionPool) BeginTransaction(ctx context.Context) (Transaction, error) { - if tx, err := this.inner.BeginTransaction(ctx); err == nil { - return NewStackTraceTransaction(tx), nil - } else { - return nil, this.normalizeContextCancellationError(err) - } -} - -func (this *NormalizeContextCancellationConnectionPool) Close() error { - return this.normalizeContextCancellationError(this.inner.Close()) -} - -func (this *NormalizeContextCancellationConnectionPool) Execute(ctx context.Context, statement string, parameters ...any) (uint64, error) { - affected, err := this.inner.Execute(ctx, statement, parameters...) - return affected, this.normalizeContextCancellationError(err) -} - -func (this *NormalizeContextCancellationConnectionPool) Select(ctx context.Context, query string, parameters ...any) (SelectResult, error) { - result, err := this.inner.Select(ctx, query, parameters...) - return result, this.normalizeContextCancellationError(err) -} - -// TODO remove manual check of "use of closed network connection" with release of https://github.com/go-sql-driver/mysql/pull/1615 -func (this *NormalizeContextCancellationConnectionPool) normalizeContextCancellationError(err error) error { - if err == nil { - return nil - } - if strings.Contains(err.Error(), "operation was canceled") { - return fmt.Errorf("%w: %w", context.Canceled, err) - } - if strings.Contains(err.Error(), "use of closed network connection") { - return fmt.Errorf("%w: %w", context.Canceled, err) - } - return err -} diff --git a/normalize_context_cancellation_connection_pool_test.go b/normalize_context_cancellation_connection_pool_test.go deleted file mode 100644 index 5e1455e..0000000 --- a/normalize_context_cancellation_connection_pool_test.go +++ /dev/null @@ -1,206 +0,0 @@ -package sqldb - -import ( - "context" - "errors" - "testing" - - "github.com/smarty/assertions/should" - "github.com/smarty/gunit" -) - -func TestNormalizeContextCancellationConnectionPoolFixture(t *testing.T) { - gunit.Run(new(NormalizeContextCancellationConnectionPoolFixture), t) -} - -type NormalizeContextCancellationConnectionPoolFixture struct { - *gunit.Fixture - - inner *FakeConnectionPool - adapter *NormalizeContextCancellationConnectionPool -} - -func (this *NormalizeContextCancellationConnectionPoolFixture) Setup() { - this.inner = &FakeConnectionPool{} - this.adapter = NewNormalizeContextCancellationConnectionPool(this.inner) -} - -func (this *NormalizeContextCancellationConnectionPoolFixture) TestPing_Successful() { - err := this.adapter.Ping(context.Background()) - - this.So(err, should.BeNil) - this.So(this.inner.pingCalls, should.Equal, 1) -} -func (this *NormalizeContextCancellationConnectionPoolFixture) TestPing_Failed() { - pingErr := errors.New("PING ERROR") - this.inner.pingError = pingErr - - err := this.adapter.Ping(context.Background()) - - this.So(this.inner.pingCalls, should.Equal, 1) - this.So(err, should.Equal, pingErr) -} -func (this *NormalizeContextCancellationConnectionPoolFixture) TestPing_AdaptContextCancelled() { - this.inner.pingError = operationCanceledErr - - err := this.adapter.Ping(context.Background()) - - this.So(this.inner.pingCalls, should.Equal, 1) - this.So(errors.Is(err, operationCanceledErr), should.BeTrue) - this.So(errors.Is(err, context.Canceled), should.BeTrue) -} - -func (this *NormalizeContextCancellationConnectionPoolFixture) TestBeginTransaction_Successful() { - transaction := new(FakeTransaction) - this.inner.transaction = transaction - - tx, err := this.adapter.BeginTransaction(context.Background()) - - this.So(err, should.BeNil) - this.So(this.inner.transactionCalls, should.Equal, 1) - this.So(tx, should.NotBeNil) -} -func (this *NormalizeContextCancellationConnectionPoolFixture) TestBeginTransaction_Failed() { - transactionErr := errors.New("BEGIN TRANSACTION ERROR") - this.inner.transactionError = transactionErr - - tx, err := this.adapter.BeginTransaction(context.Background()) - - this.So(tx, should.BeNil) - this.So(err, should.Equal, transactionErr) -} -func (this *NormalizeContextCancellationConnectionPoolFixture) TestBeginTransaction_AdaptContextCancelled() { - this.inner.transactionError = operationCanceledErr - - tx, err := this.adapter.BeginTransaction(context.Background()) - - this.So(tx, should.BeNil) - this.So(errors.Is(err, operationCanceledErr), should.BeTrue) - this.So(errors.Is(err, context.Canceled), should.BeTrue) -} - -func (this *NormalizeContextCancellationConnectionPoolFixture) TestClose_Successful() { - err := this.adapter.Close() - - this.So(err, should.BeNil) - this.So(this.inner.closeCalls, should.Equal, 1) -} -func (this *NormalizeContextCancellationConnectionPoolFixture) TestClose_Failed() { - closeErr := errors.New("CLOSE ERROR") - this.inner.closeError = closeErr - - err := this.adapter.Close() - - this.So(this.inner.closeCalls, should.Equal, 1) - this.So(err, should.Equal, closeErr) -} -func (this *NormalizeContextCancellationConnectionPoolFixture) TestClose_AdaptContextCancelled() { - this.inner.closeError = operationCanceledErr - - err := this.adapter.Close() - - this.So(this.inner.closeCalls, should.Equal, 1) - this.So(errors.Is(err, operationCanceledErr), should.BeTrue) - this.So(errors.Is(err, context.Canceled), should.BeTrue) -} - -func (this *NormalizeContextCancellationConnectionPoolFixture) TestExecute_Successful() { - this.inner.executeResult = 42 - - result, err := this.adapter.Execute(context.Background(), "statement") - - this.So(result, should.Equal, 42) - this.So(err, should.BeNil) - this.So(this.inner.executeCalls, should.Equal, 1) - this.So(this.inner.executeStatement, should.Equal, "statement") -} -func (this *NormalizeContextCancellationConnectionPoolFixture) TestExecute_Failed() { - this.inner.executeResult = 42 - executeErr := errors.New("EXECUTE ERROR") - this.inner.executeError = executeErr - - result, err := this.adapter.Execute(context.Background(), "statement") - - this.So(result, should.Equal, 42) - this.So(err, should.Equal, executeErr) - this.So(this.inner.executeCalls, should.Equal, 1) - this.So(this.inner.executeStatement, should.Equal, "statement") -} -func (this *NormalizeContextCancellationConnectionPoolFixture) TestExecute_AdaptContextCancelled() { - this.inner.executeResult = 42 - this.inner.executeError = operationCanceledErr - - result, err := this.adapter.Execute(context.Background(), "statement") - - this.So(result, should.Equal, 42) - this.So(this.inner.executeCalls, should.Equal, 1) - this.So(this.inner.executeStatement, should.Equal, "statement") - this.So(errors.Is(err, operationCanceledErr), should.BeTrue) - this.So(errors.Is(err, context.Canceled), should.BeTrue) -} - -func (this *NormalizeContextCancellationConnectionPoolFixture) TestSelect_Successful() { - expectedResult := new(FakeSelectResult) - this.inner.selectResult = expectedResult - - result, err := this.adapter.Select(context.Background(), "query", 1, 2, 3) - - this.So(result, should.Equal, expectedResult) - this.So(err, should.BeNil) - this.So(this.inner.selectCalls, should.Equal, 1) - this.So(this.inner.selectStatement, should.Equal, "query") - this.So(this.inner.selectParameters, should.Equal, []any{1, 2, 3}) -} -func (this *NormalizeContextCancellationConnectionPoolFixture) TestSelect_Failed() { - expectedResult := new(FakeSelectResult) - this.inner.selectResult = expectedResult - selectErr := errors.New("SELECT ERROR") - this.inner.selectError = selectErr - - result, err := this.adapter.Select(context.Background(), "query", 1, 2, 3) - - this.So(result, should.Equal, expectedResult) - this.So(err, should.Equal, selectErr) - this.So(this.inner.selectCalls, should.Equal, 1) - this.So(this.inner.selectStatement, should.Equal, "query") - this.So(this.inner.selectParameters, should.Equal, []any{1, 2, 3}) -} -func (this *NormalizeContextCancellationConnectionPoolFixture) TestSelect_AdaptContextCancelled() { - expectedResult := new(FakeSelectResult) - this.inner.selectResult = expectedResult - this.inner.selectError = operationCanceledErr - - result, err := this.adapter.Select(context.Background(), "query", 1, 2, 3) - - this.So(result, should.Equal, expectedResult) - this.So(this.inner.selectCalls, should.Equal, 1) - this.So(this.inner.selectStatement, should.Equal, "query") - this.So(this.inner.selectParameters, should.Equal, []any{1, 2, 3}) - this.So(errors.Is(err, operationCanceledErr), should.BeTrue) - this.So(errors.Is(err, context.Canceled), should.BeTrue) -} - -func (this *NormalizeContextCancellationConnectionPoolFixture) TestContextCancellationErrorAdapter_NilError() { - err := this.adapter.normalizeContextCancellationError(nil) - this.So(err, should.BeNil) -} -func (this *NormalizeContextCancellationConnectionPoolFixture) TestContextCancellationErrorAdapter_GenericError() { - genericErr := errors.New("generic error") - err := this.adapter.normalizeContextCancellationError(genericErr) - this.So(err, should.Equal, genericErr) -} -func (this *NormalizeContextCancellationConnectionPoolFixture) TestContextCancellationErrorAdapter_OperationCanceledError() { - err := this.adapter.normalizeContextCancellationError(operationCanceledErr) - this.So(errors.Is(err, operationCanceledErr), should.BeTrue) - this.So(errors.Is(err, context.Canceled), should.BeTrue) -} -func (this *NormalizeContextCancellationConnectionPoolFixture) TestContextCancellationErrorAdapter_ClosedConnectionError() { - err := this.adapter.normalizeContextCancellationError(closedNetworkConnectionErr) - this.So(errors.Is(err, closedNetworkConnectionErr), should.BeTrue) - this.So(errors.Is(err, context.Canceled), should.BeTrue) -} - -var ( - operationCanceledErr = errors.New("operation was canceled") - closedNetworkConnectionErr = errors.New("use of closed network connection") -) diff --git a/retry_binding_connection_pool.go b/retry_binding_connection_pool.go deleted file mode 100644 index 30843d9..0000000 --- a/retry_binding_connection_pool.go +++ /dev/null @@ -1,25 +0,0 @@ -package sqldb - -import ( - "context" - "time" -) - -type RetryBindingConnectionPool struct { - BindingConnectionPool - selector *RetryBindingSelector -} - -func NewRetryBindingConnectionPool(inner BindingConnectionPool, sleep time.Duration) *RetryBindingConnectionPool { - return &RetryBindingConnectionPool{ - BindingConnectionPool: inner, - selector: NewRetryBindingSelector(inner, sleep), - } -} - -// This does not implement an override of BeginTransaction because retry makes no sense within the concept of -// a transaction. If the tx fails, it's done. - -func (this *RetryBindingConnectionPool) BindSelect(ctx context.Context, binder Binder, statement string, parameters ...any) error { - return this.selector.BindSelect(ctx, binder, statement, parameters...) -} diff --git a/retry_binding_connection_pool_test.go b/retry_binding_connection_pool_test.go deleted file mode 100644 index b7081db..0000000 --- a/retry_binding_connection_pool_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package sqldb - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/smarty/assertions/should" - "github.com/smarty/gunit" -) - -func TestRetryBindingConnectionPoolFixture(t *testing.T) { - gunit.Run(new(RetryBindingConnectionPoolFixture), t) -} - -type RetryBindingConnectionPoolFixture struct { - *gunit.Fixture - - inner *FakeBindingConnectionPool - pool *RetryBindingConnectionPool -} - -func (this *RetryBindingConnectionPoolFixture) Setup() { - this.inner = &FakeBindingConnectionPool{} - this.pool = NewRetryBindingConnectionPool(this.inner, time.Second) -} - -/////////////////////////////////////////////////////////////// - -func (this *RetryBindingConnectionPoolFixture) TestPing() { - this.inner.pingError = errors.New("") - - err := this.pool.Ping(context.Background()) - - this.So(err, should.Equal, this.inner.pingError) - this.So(this.inner.pingCalls, should.Equal, 1) -} - -func (this *RetryBindingConnectionPoolFixture) TestBeginTransaction() { - this.inner.transaction = &FakeBindingTransaction{} - - transaction, err := this.pool.BeginTransaction(context.Background()) - - this.So(transaction, should.Equal, this.inner.transaction) - this.So(err, should.BeNil) - this.So(this.inner.transactionCalls, should.Equal, 1) -} - -func (this *RetryBindingConnectionPoolFixture) TestClose() { - this.inner.closeError = errors.New("") - - err := this.pool.Close() - - this.So(err, should.Equal, this.inner.closeError) - this.So(this.inner.closeCalls, should.Equal, 1) -} - -func (this *RetryBindingConnectionPoolFixture) TestExecute() { - this.inner.executeResult = 42 - this.inner.executeError = errors.New("") - - affected, err := this.pool.Execute(context.Background(), "statement", 1, 2, 3) - - this.So(affected, should.Equal, 42) - this.So(err, should.Equal, this.inner.executeError) - this.So(this.inner.executeCalls, should.Equal, 1) - this.So(this.inner.executeParameters, should.Resemble, []any{1, 2, 3}) -} - -func (this *RetryBindingConnectionPoolFixture) TestBindSelect() { - this.inner.selectResult = &FakeSelectResult{} - - err := this.pool.BindSelect(context.Background(), func(Scanner) error { - return nil - }, "query", 1, 2, 3) - - this.So(err, should.BeNil) - this.So(this.inner.selectBinder, should.NotBeNil) - this.So(this.inner.selectCalls, should.Equal, 1) - this.So(this.inner.selectStatement, should.Equal, "query") - this.So(this.inner.selectParameters, should.Resemble, []any{1, 2, 3}) -} diff --git a/retry_binding_selector.go b/retry_binding_selector.go deleted file mode 100644 index 5d7af85..0000000 --- a/retry_binding_selector.go +++ /dev/null @@ -1,25 +0,0 @@ -package sqldb - -import ( - "context" - "time" -) - -type RetryBindingSelector struct { - BindingSelector - duration time.Duration -} - -func NewRetryBindingSelector(actual BindingConnectionPool, duration time.Duration) *RetryBindingSelector { - return &RetryBindingSelector{BindingSelector: actual, duration: duration} -} - -func (this *RetryBindingSelector) BindSelect(ctx context.Context, binder Binder, statement string, parameters ...any) error { - for { - if this.BindingSelector.BindSelect(ctx, binder, statement, parameters...) == nil { - return nil - } - - time.Sleep(this.duration) // TODO: context.WithTimeout() - } -} diff --git a/retry_binding_selector_test.go b/retry_binding_selector_test.go deleted file mode 100644 index e44c4a0..0000000 --- a/retry_binding_selector_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package sqldb - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/smarty/assertions" - "github.com/smarty/assertions/should" - "github.com/smarty/gunit" -) - -func TestRetryBindingSelectorFixture(t *testing.T) { - gunit.Run(new(RetryBindingSelectorFixture), t) -} - -type RetryBindingSelectorFixture struct { - *gunit.Fixture - - inner *FakeRetrySelector - selector *RetryBindingSelector -} - -func (this *RetryBindingSelectorFixture) Setup() { - this.inner = &FakeRetrySelector{} - this.selector = NewRetryBindingSelector(this.inner, time.Millisecond*3) -} - -/////////////////////////////////////////////////////////////// - -func (this *RetryBindingSelectorFixture) TestSelectWithoutErrors() { - err := this.selector.BindSelect(context.Background(), nil, "statement", 1, 2, 3) - - this.So(err, should.Equal, err) - this.So(this.inner.count, should.Equal, 1) - this.So(this.inner.statement, should.Equal, "statement") - this.So(this.inner.parameters, should.Resemble, []any{1, 2, 3}) -} - -func (this *RetryBindingSelectorFixture) TestRetryUntilSuccess() { - this.inner.errorCount = 4 - - started := time.Now().UTC() - err := this.selector.BindSelect(context.Background(), nil, "statement", 1, 2, 3) - - this.So(err, should.Equal, err) - this.So(this.inner.count, should.Equal, 5) // last attempt is successful - this.So(time.Since(started), should.BeGreaterThan, time.Millisecond*3*4) // 3ms * 4 sleeps -} - -/////////////////////////////////////////////////////////////// - -type FakeRetrySelector struct { - count int - errorCount int - binder Binder - statement string - parameters []any -} - -func (this *FakeRetrySelector) Ping(_ context.Context) error { - panic("Should not be called.") -} - -func (this *FakeRetrySelector) BeginTransaction(_ context.Context) (BindingTransaction, error) { - panic("Should not be called.") -} - -func (this *FakeRetrySelector) Close() error { - panic("Should not be called.") -} - -func (this *FakeRetrySelector) BindSelect(_ context.Context, binder Binder, statement string, parameters ...any) error { - if this.binder == nil { - this.binder = binder - } else { - assertions.So(this.binder, should.Equal, binder) - } - - if this.statement == "" { - this.statement = statement - } else { - assertions.So(this.statement, should.Equal, statement) - } - - if len(this.parameters) == 0 { - this.parameters = parameters - } else { - assertions.So(this.parameters, should.Resemble, parameters) - } - - this.count++ - if this.errorCount > 0 && this.count <= this.errorCount { - return errors.New("") - } else { - return nil - } -} - -func (this *FakeRetrySelector) Execute(_ context.Context, _ string, _ ...any) (uint64, error) { - panic("Should not be called.") -} diff --git a/split_statement_connection_pool.go b/split_statement_connection_pool.go deleted file mode 100644 index 061587c..0000000 --- a/split_statement_connection_pool.go +++ /dev/null @@ -1,29 +0,0 @@ -package sqldb - -import "context" - -type SplitStatementConnectionPool struct { - ConnectionPool - delimiter string - executor *SplitStatementExecutor -} - -func NewSplitStatementConnectionPool(inner ConnectionPool, delimiter string) *SplitStatementConnectionPool { - return &SplitStatementConnectionPool{ - ConnectionPool: inner, - delimiter: delimiter, - executor: NewSplitStatementExecutor(inner, delimiter), - } -} - -func (this *SplitStatementConnectionPool) BeginTransaction(ctx context.Context) (Transaction, error) { - if transaction, err := this.ConnectionPool.BeginTransaction(ctx); err == nil { - return NewSplitStatementTransaction(transaction, this.delimiter), nil - } else { - return nil, err - } -} - -func (this *SplitStatementConnectionPool) Execute(ctx context.Context, statement string, parameters ...any) (uint64, error) { - return this.executor.Execute(ctx, statement, parameters...) -} diff --git a/split_statement_connection_pool_test.go b/split_statement_connection_pool_test.go deleted file mode 100644 index bbc09eb..0000000 --- a/split_statement_connection_pool_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package sqldb - -import ( - "context" - "errors" - "reflect" - "testing" - - "github.com/smarty/assertions/should" - "github.com/smarty/gunit" -) - -func TestSplitStatementConnectionPoolFixture(t *testing.T) { - gunit.Run(new(SplitStatementConnectionPoolFixture), t) -} - -type SplitStatementConnectionPoolFixture struct { - *gunit.Fixture - - inner *FakeConnectionPool - pool *SplitStatementConnectionPool -} - -func (this *SplitStatementConnectionPoolFixture) Setup() { - this.inner = &FakeConnectionPool{} - this.pool = NewSplitStatementConnectionPool(this.inner, "?") -} - -/////////////////////////////////////////////////////////////// - -func (this *SplitStatementConnectionPoolFixture) TestPing() { - this.inner.pingError = errors.New("") - - err := this.pool.Ping(context.Background()) - - this.So(err, should.Equal, this.inner.pingError) - this.So(this.inner.pingCalls, should.Equal, 1) -} - -func (this *SplitStatementConnectionPoolFixture) TestBeginTransactionFails() { - this.inner.transactionError = errors.New("") - - transaction, err := this.pool.BeginTransaction(context.Background()) - - this.So(transaction, should.BeNil) - this.So(err, should.Equal, this.inner.transactionError) - this.So(this.inner.transactionCalls, should.Equal, 1) -} - -func (this *SplitStatementConnectionPoolFixture) TestBeginTransactionSucceeds() { - this.inner.transaction = &FakeTransaction{} - - transaction, err := this.pool.BeginTransaction(context.Background()) - - this.So(reflect.TypeOf(transaction), should.Equal, reflect.TypeOf(&SplitStatementTransaction{})) - this.So(err, should.BeNil) - this.So(this.inner.transactionCalls, should.Equal, 1) -} - -func (this *SplitStatementConnectionPoolFixture) TestClose() { - this.inner.closeError = errors.New("") - - err := this.pool.Close() - - this.So(err, should.Equal, this.inner.closeError) - this.So(this.inner.closeCalls, should.Equal, 1) -} - -func (this *SplitStatementConnectionPoolFixture) TestExecute() { - this.inner.executeResult = 5 - - affected, err := this.pool.Execute(context.Background(), "statement1 ?; statement2 ? ?;", 1, 2, 3) - - this.So(affected, should.Equal, 10) - this.So(err, should.BeNil) - this.So(this.inner.executeCalls, should.Equal, 2) - this.So(this.inner.executeParameters, should.Resemble, []any{2, 3}) // last two parameters -} - -func (this *SplitStatementConnectionPoolFixture) TestSelect() { - this.inner.selectError = errors.New("") - this.inner.selectResult = &FakeSelectResult{} - - result, err := this.pool.Select(context.Background(), "query", 1, 2, 3) - - this.So(result, should.Equal, this.inner.selectResult) - this.So(err, should.Equal, this.inner.selectError) - this.So(this.inner.selectCalls, should.Equal, 1) - this.So(this.inner.selectStatement, should.Equal, "query") - this.So(this.inner.selectParameters, should.Resemble, []any{1, 2, 3}) -} diff --git a/split_statement_executor.go b/split_statement_executor.go deleted file mode 100644 index 3a58682..0000000 --- a/split_statement_executor.go +++ /dev/null @@ -1,45 +0,0 @@ -package sqldb - -import ( - "context" - "errors" - "fmt" - "strings" -) - -type SplitStatementExecutor struct { - Executor - delimiter string -} - -func NewSplitStatementExecutor(actual Executor, delimiter string) *SplitStatementExecutor { - return &SplitStatementExecutor{Executor: actual, delimiter: delimiter} -} - -func (this *SplitStatementExecutor) Execute(ctx context.Context, statement string, parameters ...any) (uint64, error) { - if argumentCount := strings.Count(statement, this.delimiter); argumentCount != len(parameters) { - return 0, fmt.Errorf("%w: Expected: %d, received %d", ErrArgumentCountMismatch, argumentCount, len(parameters)) - } - - var count uint64 - index := 0 - for _, statement = range strings.Split(statement, ";") { - if len(strings.TrimSpace(statement)) == 0 { - continue - } - - statement += ";" // terminate the statement - indexOffset := strings.Count(statement, this.delimiter) - if affected, err := this.Executor.Execute(ctx, statement, parameters[index:index+indexOffset]...); err != nil { - return 0, err - } else { - count += affected - } - - index += indexOffset - } - - return count, nil -} - -var ErrArgumentCountMismatch = errors.New("the number of arguments supplied does not match the statement") diff --git a/split_statement_executor_test.go b/split_statement_executor_test.go deleted file mode 100644 index d7bc57c..0000000 --- a/split_statement_executor_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package sqldb - -import ( - "context" - "errors" - "testing" - - "github.com/smarty/assertions/should" - "github.com/smarty/gunit" -) - -func TestSplitStatementExecutorFixture(t *testing.T) { - gunit.Run(new(SplitStatementExecutorFixture), t) -} - -type SplitStatementExecutorFixture struct { - *gunit.Fixture - - fakeInner *FakeExecutor - executor *SplitStatementExecutor -} - -func (this *SplitStatementExecutorFixture) Setup() { - this.fakeInner = &FakeExecutor{} - this.executor = NewSplitStatementExecutor(this.fakeInner, "?") -} - -/////////////////////////////////////////////////////////////// - -func (this *SplitStatementExecutorFixture) TestStatementAndParameterCountsDoNotMatch() { - this.fakeInner.affected = 1 - - affected, err := this.executor.Execute(context.Background(), "? ? ?") - - this.So(affected, should.Equal, 0) - this.So(err, should.NotBeNil) - this.So(this.fakeInner.statements, should.BeEmpty) -} - -func (this *SplitStatementExecutorFixture) TestSingleStatement() { - this.fakeInner.affected = 1 - affected, err := this.executor.Execute(context.Background(), "statement ? ?", 1, 2) - - this.So(affected, should.Equal, this.fakeInner.affected) - this.So(err, should.BeNil) - this.So(this.fakeInner.statements, should.Resemble, []string{"statement ? ?;"}) - this.So(this.fakeInner.parameters, should.Resemble, [][]any{{1, 2}}) -} - -func (this *SplitStatementExecutorFixture) TestEmptyStatementsAreSkipped() { - affected, err := this.executor.Execute(context.Background(), ";;;;") - - this.So(affected, should.Equal, 0) - this.So(err, should.BeNil) - this.So(this.fakeInner.statements, should.BeEmpty) - this.So(this.fakeInner.parameters, should.BeEmpty) -} - -func (this *SplitStatementExecutorFixture) TestMultipleStatements() { - this.fakeInner.affected = 2 - - affected, err := this.executor.Execute(context.Background(), "1 ?; 2 ? ?; 3 ? ? ?", 1, 2, 3, 4, 5, 6) - - this.So(affected, should.Equal, this.fakeInner.affected*3) - this.So(err, should.BeNil) - this.So(this.fakeInner.statements, should.Resemble, []string{ - "1 ?;", - "2 ? ?;", - "3 ? ? ?;", - }) - this.So(this.fakeInner.parameters, should.Resemble, [][]any{ - {1}, - {2, 3}, - {4, 5, 6}, - }) -} - -func (this *SplitStatementExecutorFixture) TestFailureAbortsAdditionalStatements() { - this.fakeInner.affected = 10 - this.fakeInner.errorsToReturn = []error{nil, errors.New("")} - - affected, err := this.executor.Execute(context.Background(), "1;2;3") - - this.So(affected, should.Equal, 0) - this.So(err, should.Equal, this.fakeInner.errorsToReturn[1]) - this.So(this.fakeInner.statements, should.Resemble, []string{"1;", "2;"}) -} diff --git a/split_statement_transaction.go b/split_statement_transaction.go deleted file mode 100644 index 47be7f6..0000000 --- a/split_statement_transaction.go +++ /dev/null @@ -1,19 +0,0 @@ -package sqldb - -import "context" - -type SplitStatementTransaction struct { - Transaction - executor *SplitStatementExecutor -} - -func NewSplitStatementTransaction(inner Transaction, delimiter string) *SplitStatementTransaction { - return &SplitStatementTransaction{ - Transaction: inner, - executor: NewSplitStatementExecutor(inner, delimiter), - } -} - -func (this *SplitStatementTransaction) Execute(ctx context.Context, statement string, parameters ...any) (uint64, error) { - return this.executor.Execute(ctx, statement, parameters...) -} diff --git a/split_statement_transaction_test.go b/split_statement_transaction_test.go deleted file mode 100644 index ec0fca4..0000000 --- a/split_statement_transaction_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package sqldb - -import ( - "context" - "errors" - "testing" - - "github.com/smarty/assertions/should" - "github.com/smarty/gunit" -) - -func TestSplitStatementTransactionFixture(t *testing.T) { - gunit.Run(new(SplitStatementTransactionFixture), t) -} - -type SplitStatementTransactionFixture struct { - *gunit.Fixture - - inner *FakeTransaction - transaction *SplitStatementTransaction -} - -func (this *SplitStatementTransactionFixture) Setup() { - this.inner = &FakeTransaction{} - this.transaction = NewSplitStatementTransaction(this.inner, "?") -} - -/////////////////////////////////////////////////////////////// - -func (this *SplitStatementTransactionFixture) TestCommit() { - this.inner.commitError = errors.New("") - - err := this.transaction.Commit() - - this.So(err, should.Equal, this.inner.commitError) - this.So(this.inner.commitCalls, should.Equal, 1) -} - -func (this *SplitStatementTransactionFixture) TestRollback() { - this.inner.rollbackError = errors.New("") - - err := this.transaction.Rollback() - - this.So(err, should.Equal, this.inner.rollbackError) - this.So(this.inner.rollbackCalls, should.Equal, 1) -} - -func (this *SplitStatementTransactionFixture) TestSelect() { - this.inner.selectError = errors.New("") - this.inner.selectResult = &FakeSelectResult{} - - result, err := this.transaction.Select(context.Background(), "query", 1, 2, 3) - - this.So(result, should.Equal, this.inner.selectResult) - this.So(err, should.Equal, this.inner.selectError) - this.So(this.inner.selectCalls, should.Equal, 1) - this.So(this.inner.selectStatement, should.Equal, "query") - this.So(this.inner.selectParameters, should.Resemble, []any{1, 2, 3}) -} - -func (this *SplitStatementTransactionFixture) TestExecute() { - this.inner.executeResult = 5 - - affected, err := this.transaction.Execute(context.Background(), "statement1 ?; statement2 ? ?;", 1, 2, 3) - - this.So(affected, should.Equal, 10) - this.So(err, should.BeNil) - this.So(this.inner.executeCalls, should.Equal, 2) - this.So(this.inner.executeParameters, should.Resemble, []any{2, 3}) // last two parameters -} diff --git a/stack_trace.go b/stack_trace.go deleted file mode 100644 index f2eb79f..0000000 --- a/stack_trace.go +++ /dev/null @@ -1,30 +0,0 @@ -package sqldb - -import ( - "fmt" - "runtime/debug" -) - -// StackTrace performs when used as a nil pointer struct field. When non-nil, it returns a preset value. -// This is useful during testing when asserting on simple, deterministic values is helpful. -type StackTrace struct { - trace string -} - -func ContrivedStackTrace(trace string) *StackTrace { - return &StackTrace{trace: trace} -} - -func (this *StackTrace) Wrap(err error) error { - if err != nil { - err = fmt.Errorf("%w\nStack Trace:\n%s", err, this.StackTrace()) - } - return err -} - -func (this *StackTrace) StackTrace() string { - if this == nil { - return string(debug.Stack()) - } - return this.trace -} diff --git a/stack_trace_connection_pool.go b/stack_trace_connection_pool.go deleted file mode 100644 index 4f66375..0000000 --- a/stack_trace_connection_pool.go +++ /dev/null @@ -1,38 +0,0 @@ -package sqldb - -import "context" - -type StackTraceConnectionPool struct { - inner ConnectionPool - *StackTrace -} - -func NewStackTraceConnectionPool(inner ConnectionPool) *StackTraceConnectionPool { - return &StackTraceConnectionPool{inner: inner} -} - -func (this *StackTraceConnectionPool) Ping(ctx context.Context) error { - return this.Wrap(this.inner.Ping(ctx)) -} - -func (this *StackTraceConnectionPool) BeginTransaction(ctx context.Context) (Transaction, error) { - if tx, err := this.inner.BeginTransaction(ctx); err == nil { - return NewStackTraceTransaction(tx), nil - } else { - return nil, this.Wrap(err) - } -} - -func (this *StackTraceConnectionPool) Close() error { - return this.Wrap(this.inner.Close()) -} - -func (this *StackTraceConnectionPool) Execute(ctx context.Context, statement string, parameters ...any) (uint64, error) { - affected, err := this.inner.Execute(ctx, statement, parameters...) - return affected, this.Wrap(err) -} - -func (this *StackTraceConnectionPool) Select(ctx context.Context, query string, parameters ...any) (SelectResult, error) { - result, err := this.inner.Select(ctx, query, parameters...) - return result, this.Wrap(err) -} diff --git a/stack_trace_connection_pool_test.go b/stack_trace_connection_pool_test.go deleted file mode 100644 index 352e9a6..0000000 --- a/stack_trace_connection_pool_test.go +++ /dev/null @@ -1,139 +0,0 @@ -package sqldb - -import ( - "context" - "errors" - "testing" - - "github.com/smarty/assertions/should" - "github.com/smarty/gunit" -) - -func TestStackTraceConnectionPoolFixture(t *testing.T) { - gunit.Run(new(StackTraceConnectionPoolFixture), t) -} - -type StackTraceConnectionPoolFixture struct { - *gunit.Fixture - - pool *FakeConnectionPool - adapter *StackTraceConnectionPool -} - -func (this *StackTraceConnectionPoolFixture) Setup() { - this.pool = &FakeConnectionPool{} - this.adapter = NewStackTraceConnectionPool(this.pool) - this.adapter.StackTrace = ContrivedStackTrace("HELLO, WORLD!") -} - -func (this *StackTraceConnectionPoolFixture) TestPing_WhenSuccessful_NoStackTraceIncluded() { - err := this.adapter.Ping(context.Background()) - - this.So(err, should.BeNil) - this.So(this.pool.pingCalls, should.Equal, 1) -} - -func (this *StackTraceConnectionPoolFixture) TestPing_WhenFails_StackTraceAppendedToErr() { - this.pool.pingError = errors.New("PING ERROR") - - err := this.adapter.Ping(context.Background()) - - this.So(this.pool.pingCalls, should.Equal, 1) - this.So(err, should.NotBeNil) - this.So(err.Error(), should.Equal, "PING ERROR\nStack Trace:\nHELLO, WORLD!") -} - -func (this *StackTraceConnectionPoolFixture) TestClose_WhenSuccessful_NoStackTraceIncluded() { - err := this.adapter.Close() - - this.So(err, should.BeNil) - this.So(this.pool.closeCalls, should.Equal, 1) -} - -func (this *StackTraceConnectionPoolFixture) TestClose_WhenFails_StackTraceAppendedToErr() { - this.pool.closeError = errors.New("CLOSE ERROR") - - err := this.adapter.Close() - - this.So(this.pool.closeCalls, should.Equal, 1) - this.So(err, should.NotBeNil) - this.So(err.Error(), should.Equal, "CLOSE ERROR\nStack Trace:\nHELLO, WORLD!") -} - -func (this *StackTraceConnectionPoolFixture) TestBeginTransaction_WhenSuccessful_NoStackTraceIncluded() { - transaction := new(FakeTransaction) - this.pool.transaction = transaction - - tx, err := this.adapter.BeginTransaction(context.Background()) - - this.So(err, should.BeNil) - this.So(this.pool.transactionCalls, should.Equal, 1) - this.So(tx.(*StackTraceTransaction).inner, should.Equal, transaction) -} - -func (this *StackTraceConnectionPoolFixture) TestBeginTransaction_WhenFails_StackTraceAppendedToErr() { - transaction := new(FakeTransaction) - this.pool.transaction = transaction - this.pool.transactionError = errors.New("TX ERROR") - - tx, err := this.adapter.BeginTransaction(context.Background()) - - this.So(this.pool.transactionCalls, should.Equal, 1) - this.So(tx, should.BeNil) - this.So(err, should.NotBeNil) - this.So(err.Error(), should.Equal, "TX ERROR\nStack Trace:\nHELLO, WORLD!") -} - -func (this *StackTraceConnectionPoolFixture) TestExecute_WhenSuccessful_NoStackTraceIncluded() { - this.pool.executeResult = 42 - - result, err := this.adapter.Execute(context.Background(), "QUERY", 1, 2, 3) - - this.So(result, should.Equal, 42) - this.So(err, should.BeNil) - this.So(this.pool.executeCalls, should.Equal, 1) - this.So(this.pool.executeStatement, should.Equal, "QUERY") - this.So(this.pool.executeParameters, should.Resemble, []any{1, 2, 3}) -} - -func (this *StackTraceConnectionPoolFixture) TestExecute_WhenFails_StackTraceAppendedToErr() { - this.pool.executeError = errors.New("EXECUTE ERROR") - this.pool.executeResult = 42 - - result, err := this.adapter.Execute(context.Background(), "QUERY", 1, 2, 3) - - this.So(result, should.Equal, 42) - this.So(err, should.NotBeNil) - this.So(err.Error(), should.Equal, "EXECUTE ERROR\nStack Trace:\nHELLO, WORLD!") - this.So(this.pool.executeCalls, should.Equal, 1) - this.So(this.pool.executeStatement, should.Equal, "QUERY") - this.So(this.pool.executeParameters, should.Resemble, []any{1, 2, 3}) -} - -func (this *StackTraceConnectionPoolFixture) TestSelect_WhenSuccessful_NoStackTraceIncluded() { - expectedResult := new(FakeSelectResult) - this.pool.selectResult = expectedResult - - result, err := this.adapter.Select(context.Background(), "QUERY", 1, 2, 3) - - this.So(result, should.Equal, expectedResult) - this.So(err, should.BeNil) - this.So(this.pool.selectCalls, should.Equal, 1) - this.So(this.pool.selectStatement, should.Equal, "QUERY") - this.So(this.pool.selectParameters, should.Resemble, []any{1, 2, 3}) -} - -func (this *StackTraceConnectionPoolFixture) TestSelect_WhenFails_StackTraceAppendedToErr() { - expectedResult := new(FakeSelectResult) - this.pool.selectResult = expectedResult - this.pool.selectError = errors.New("SELECT ERROR") - - result, err := this.adapter.Select(context.Background(), "QUERY", 1, 2, 3) - - this.So(result, should.Equal, expectedResult) - this.So(err, should.NotBeNil) - this.So(err.Error(), should.Equal, "SELECT ERROR\nStack Trace:\nHELLO, WORLD!") - this.So(this.pool.selectCalls, should.Equal, 1) - this.So(this.pool.selectStatement, should.Equal, "QUERY") - this.So(this.pool.selectParameters, should.Resemble, []any{1, 2, 3}) -} diff --git a/stack_trace_test.go b/stack_trace_test.go deleted file mode 100644 index b4274c4..0000000 --- a/stack_trace_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package sqldb - -import ( - "errors" - "runtime/debug" - "testing" - - "github.com/smarty/assertions/should" - "github.com/smarty/gunit" -) - -func TestStackTraceFixture(t *testing.T) { - gunit.Run(new(StackTraceFixture), t) -} - -type StackTraceFixture struct { - *gunit.Fixture - - stack *StackTrace -} - -func (this *StackTraceFixture) TestWhenNil_ReturnsActualStackTrace() { - actual := this.stack.StackTrace() - expected := string(debug.Stack()) - - actual = actual[len(actual)-860:] // last 860 characters - expected = expected[len(expected)-860:] // last 860 characters - - this.So(actual, should.Equal, expected) -} - -func (this *StackTraceFixture) TestWhenNonNil_ReturnsPreSetMessage() { - this.stack = ContrivedStackTrace("HELLO") - this.So(this.stack.StackTrace(), should.Equal, "HELLO") -} - -func (this *StackTraceFixture) TestWrap_NilErrorReturned() { - var err error - err = this.stack.Wrap(err) - this.So(err, should.BeNil) -} - -func (this *StackTraceFixture) TestWrap_NonNilErrorDecorated() { - this.stack = ContrivedStackTrace("GOPHER STACK") - err := errors.New("HELLO") - err = this.stack.Wrap(err) - this.So(err.Error(), should.Equal, "HELLO\nStack Trace:\nGOPHER STACK") -} diff --git a/stack_trace_transaction.go b/stack_trace_transaction.go deleted file mode 100644 index 4eecbea..0000000 --- a/stack_trace_transaction.go +++ /dev/null @@ -1,30 +0,0 @@ -package sqldb - -import "context" - -type StackTraceTransaction struct { - *StackTrace - inner Transaction -} - -func NewStackTraceTransaction(inner Transaction) *StackTraceTransaction { - return &StackTraceTransaction{inner: inner} -} - -func (this *StackTraceTransaction) Commit() error { - return this.Wrap(this.inner.Commit()) -} - -func (this *StackTraceTransaction) Rollback() error { - return this.Wrap(this.inner.Rollback()) -} - -func (this *StackTraceTransaction) Execute(ctx context.Context, statement string, parameters ...any) (uint64, error) { - affected, err := this.inner.Execute(ctx, statement, parameters...) - return affected, this.Wrap(err) -} - -func (this *StackTraceTransaction) Select(ctx context.Context, statement string, args ...any) (SelectResult, error) { - result, err := this.inner.Select(ctx, statement, args...) - return result, this.Wrap(err) -} diff --git a/stack_trace_transaction_test.go b/stack_trace_transaction_test.go deleted file mode 100644 index 868fd18..0000000 --- a/stack_trace_transaction_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package sqldb - -import ( - "context" - "errors" - "testing" - - "github.com/smarty/assertions/should" - "github.com/smarty/gunit" -) - -func TestStackTraceTransactionFixture(t *testing.T) { - gunit.Run(new(StackTraceTransactionFixture), t) -} - -type StackTraceTransactionFixture struct { - *gunit.Fixture - - inner *FakeTransaction - transaction *StackTraceTransaction -} - -func (this *StackTraceTransactionFixture) Setup() { - this.inner = new(FakeTransaction) - this.transaction = NewStackTraceTransaction(this.inner) - this.transaction.StackTrace = ContrivedStackTrace("STACK") -} - -func (this *StackTraceTransactionFixture) TestCommit() { - this.inner.commitError = errors.New("ERROR") - - err := this.transaction.Commit() - - this.So(err.Error(), should.Equal, "ERROR\nStack Trace:\nSTACK") - this.So(this.inner.commitCalls, should.Equal, 1) -} - -func (this *StackTraceTransactionFixture) TestRollback() { - this.inner.rollbackError = errors.New("ERROR") - - err := this.transaction.Rollback() - - this.So(err.Error(), should.Equal, "ERROR\nStack Trace:\nSTACK") - this.So(this.inner.rollbackCalls, should.Equal, 1) -} - -func (this *StackTraceTransactionFixture) TestExecute() { - this.inner.executeError = errors.New("ERROR") - this.inner.executeResult = 42 - - rows, err := this.transaction.Execute(context.Background(), "STATEMENT", 1, 2, 3) - - this.So(rows, should.Equal, 42) - this.So(err.Error(), should.Equal, "ERROR\nStack Trace:\nSTACK") - this.So(this.inner.executeCalls, should.Equal, 1) - this.So(this.inner.executeStatement, should.Equal, "STATEMENT") - this.So(this.inner.executeParameters, should.Resemble, []any{1, 2, 3}) -} - -func (this *StackTraceTransactionFixture) TestSelect() { - expectedResult := new(FakeSelectResult) - this.inner.selectResult = expectedResult - this.inner.selectError = errors.New("ERROR") - - result, err := this.transaction.Select(context.Background(), "STATEMENT", 1, 2, 3) - - this.So(result, should.Equal, expectedResult) - this.So(err.Error(), should.Equal, "ERROR\nStack Trace:\nSTACK") - this.So(this.inner.selectCalls, should.Equal, 1) - this.So(this.inner.selectStatement, should.Equal, "STATEMENT") - this.So(this.inner.selectParameters, should.Resemble, []any{1, 2, 3}) -}