package sq import ( "context" "database/sql" "github.com/jmoiron/sqlx" "gogs.mikescher.com/BlackForestBytes/goext/exerr" "gogs.mikescher.com/BlackForestBytes/goext/langext" "sync" "time" ) type DB interface { Queryable Ping(ctx context.Context) error BeginTransaction(ctx context.Context, iso sql.IsolationLevel) (Tx, error) AddListener(listener Listener) Exit() error RegisterConverter(DBTypeConverter) } type DBOptions struct { RegisterDefaultConverter *bool RegisterCommentTrimmer *bool } type database struct { db *sqlx.DB txctr uint16 lock sync.Mutex lstr []Listener conv []DBTypeConverter } func NewDB(db *sqlx.DB, opt DBOptions) DB { sqdb := &database{ db: db, txctr: 0, lock: sync.Mutex{}, lstr: make([]Listener, 0), } if langext.Coalesce(opt.RegisterDefaultConverter, true) { sqdb.registerDefaultConverter() } if langext.Coalesce(opt.RegisterCommentTrimmer, true) { sqdb.AddListener(CommentTrimmer) } return sqdb } func (db *database) AddListener(listener Listener) { db.lstr = append(db.lstr, listener) } func (db *database) Exec(ctx context.Context, sqlstr string, prep PP) (sql.Result, error) { origsql := sqlstr t0 := time.Now() preMeta := PreExecMeta{Context: ctx, TransactionConstructorContext: nil} for _, v := range db.lstr { err := v.PreExec(ctx, nil, &sqlstr, &prep, preMeta) if err != nil { return nil, exerr.Wrap(err, "failed to call SQL pre-exec listener").Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build() } } t1 := time.Now() res, err := db.db.NamedExecContext(ctx, sqlstr, prep) postMeta := PostExecMeta{Context: ctx, TransactionConstructorContext: nil, Init: t0, Start: t1, End: time.Now()} for _, v := range db.lstr { v.PostExec(nil, origsql, sqlstr, prep, err, postMeta) } if err != nil { return nil, exerr.Wrap(err, "Failed to [exec] sql statement").Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build() } return res, nil } func (db *database) Query(ctx context.Context, sqlstr string, prep PP) (*sqlx.Rows, error) { origsql := sqlstr t0 := time.Now() preMeta := PreQueryMeta{Context: ctx, TransactionConstructorContext: nil} for _, v := range db.lstr { err := v.PreQuery(ctx, nil, &sqlstr, &prep, preMeta) if err != nil { return nil, exerr.Wrap(err, "failed to call SQL pre-query listener").Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build() } } t1 := time.Now() rows, err := sqlx.NamedQueryContext(ctx, db.db, sqlstr, prep) postMeta := PostQueryMeta{Context: ctx, TransactionConstructorContext: nil, Init: t0, Start: t1, End: time.Now()} for _, v := range db.lstr { v.PostQuery(nil, origsql, sqlstr, prep, err, postMeta) } if err != nil { return nil, exerr.Wrap(err, "Failed to [query] sql statement").Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build() } return rows, nil } func (db *database) Ping(ctx context.Context) error { t0 := time.Now() preMeta := PrePingMeta{Context: ctx} for _, v := range db.lstr { err := v.PrePing(ctx, preMeta) if err != nil { return err } } t1 := time.Now() err := db.db.PingContext(ctx) postMeta := PostPingMeta{Context: ctx, Init: t0, Start: t1, End: time.Now()} for _, v := range db.lstr { v.PostPing(err, postMeta) } if err != nil { return exerr.Wrap(err, "Failed to [ping] sql database").Build() } return nil } func (db *database) BeginTransaction(ctx context.Context, iso sql.IsolationLevel) (Tx, error) { t0 := time.Now() db.lock.Lock() txid := db.txctr db.txctr += 1 // with overflow ! db.lock.Unlock() preMeta := PreTxBeginMeta{Context: ctx} for _, v := range db.lstr { err := v.PreTxBegin(ctx, txid, preMeta) if err != nil { return nil, err } } t1 := time.Now() xtx, err := db.db.BeginTxx(ctx, &sql.TxOptions{Isolation: iso}) postMeta := PostTxBeginMeta{Context: ctx, Init: t0, Start: t1, End: time.Now()} for _, v := range db.lstr { v.PostTxBegin(txid, err, postMeta) } if err != nil { return nil, exerr.Wrap(err, "Failed to start sql transaction").Build() } return newTransaction(ctx, xtx, txid, db), nil } func (db *database) Exit() error { return db.db.Close() } func (db *database) ListConverter() []DBTypeConverter { return db.conv } func (db *database) RegisterConverter(conv DBTypeConverter) { db.conv = langext.ArrFilter(db.conv, func(v DBTypeConverter) bool { return v.ModelTypeString() != conv.ModelTypeString() }) db.conv = append(db.conv, conv) } func (db *database) registerDefaultConverter() { db.RegisterConverter(ConverterBoolToBit) db.RegisterConverter(ConverterTimeToUnixMillis) db.RegisterConverter(ConverterRFCUnixMilliTimeToUnixMillis) db.RegisterConverter(ConverterRFCUnixNanoTimeToUnixNanos) db.RegisterConverter(ConverterRFCUnixTimeToUnixSeconds) db.RegisterConverter(ConverterRFC339TimeToString) db.RegisterConverter(ConverterRFC339NanoTimeToString) db.RegisterConverter(ConverterRFCDateToString) db.RegisterConverter(ConverterRFCTimeToString) db.RegisterConverter(ConverterRFCSecondsF64ToString) db.RegisterConverter(ConverterJsonObjToString) db.RegisterConverter(ConverterJsonArrToString) db.RegisterConverter(ConverterExErrCategoryToString) db.RegisterConverter(ConverterExErrSeverityToString) db.RegisterConverter(ConverterExErrTypeToString) }