From 2ad7bd7534da3725307f228e4410d02467dd8531 Mon Sep 17 00:00:00 2001 From: pshapovalov Date: Thu, 7 Mar 2024 12:31:51 +0000 Subject: [PATCH] init commit --- .github/workflows/go.yaml | 65 ++++++++++++++++++++ .gitignore | 78 ++++++++++++++++++++++++ .golangci.pipeline.yaml | 53 ++++++++++++++++ Makefile | 9 +++ README.md | 1 + go.mod | 19 ++++++ go.sum | 42 +++++++++++++ pkg/dbclient/driver.go | 107 +++++++++++++++++++++++++++++++++ pkg/dbclient/port.go | 53 ++++++++++++++++ pkg/dbclient/service.go | 26 ++++++++ pkg/pgtxmanager/transaction.go | 20 ++++++ pkg/pgtxmanager/transactor.go | 88 +++++++++++++++++++++++++++ pkg/prettier/prettier.go | 36 +++++++++++ 13 files changed, 597 insertions(+) create mode 100644 .github/workflows/go.yaml create mode 100644 .gitignore create mode 100644 .golangci.pipeline.yaml create mode 100644 Makefile create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/dbclient/driver.go create mode 100644 pkg/dbclient/port.go create mode 100644 pkg/dbclient/service.go create mode 100644 pkg/pgtxmanager/transaction.go create mode 100644 pkg/pgtxmanager/transactor.go create mode 100644 pkg/prettier/prettier.go diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml new file mode 100644 index 0000000..8e991d7 --- /dev/null +++ b/.github/workflows/go.yaml @@ -0,0 +1,65 @@ +name: Go + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +env: + REGISTRY: "cr.selcloud.ru/practice" + IMAGE_NAME: "auth-service" + CONTAINER_NAME: "auth-service-container" + REGISTRY_USER: "token" + GRPC_PORT: "50051" + + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.22' + cache-dependency-path: go.sum + - name: Build + run: go build -o ./bin/ -v ./... + - name: Test + run: go test -v ./... + + linter: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: '1.22' + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + # Require: The version of golangci-lint to use. + # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. + # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. + version: v1.53 + # Optional: working directory, useful for monorepos + # working-directory: somedir + # Optional: golangci-lint command line arguments. + # + # Note: By default, the `.golangci.yml` file should be at the root of the repository. + # The location of the configuration file can be changed by using `--config=` + args: --timeout=30m --config=./.golangci.pipeline.yaml --issues-exit-code=0 + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true + # Optional: if set to true, then all caching functionality will be completely disabled, + # takes precedence over all other caching options. + # skip-cache: true + # Optional: if set to true, then the action won't cache or restore ~/go/pkg. + # skip-pkg-cache: true + # Optional: if set to true, then the action won't cache or restore ~/.cache/go-build. + # skip-build-cache: true + # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'. + # install-mode: "goinstall" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8ff130 --- /dev/null +++ b/.gitignore @@ -0,0 +1,78 @@ +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,godot,synology +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,godot,synology + +### Godot ### +# Godot 4+ specific ignores +.godot/ + +# Godot-specific ignores +.import/ +export.cfg +export_presets.cfg + +# Imported translations (automatically generated from CSV files) +*.translation + +# Mono-specific ignores +.mono/ +data_*/ +mono_crash.*.json + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Synology ### +# Thumbnails +@eaDir +# Recycle bin +\#recycle + +### VisualStudioCode ### +.vscode/* + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,godot,synology + +/bin/ +.devcontainer/ + diff --git a/.golangci.pipeline.yaml b/.golangci.pipeline.yaml new file mode 100644 index 0000000..29db6e8 --- /dev/null +++ b/.golangci.pipeline.yaml @@ -0,0 +1,53 @@ +# More info on config here: https://golangci-lint.run/usage/configuration/#config-file +run: + concurrency: 8 + timeout: 10m + issues-exit-code: 1 + tests: true + skip-dirs: + - bin + - vendor + - var + - tmp + - .cache + skip-files: + - \.pb\.go$ + - \.pb\.gw\.go$ + +output: + format: colored-line-number + print-issued-lines: true + print-linter-name: true + +linters-settings: + govet: + check-shadowing: true + dupl: + threshold: 100 + goconst: + min-len: 2 + min-occurrences: 2 + +linters: + disable-all: true + enable: + - errcheck + - goconst + - goimports + - gosec + - govet + - ineffassign + - megacheck + - revive + - typecheck + - unused + +issues: + exclude-use-default: false + exclude: + # _ instead of err checks + - G104 + - exported func .* returns unexported type .*, which can be annoying to use + - should have a package comment + - don't use an underscore in package name + - or be unexported \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..54cccf4 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +LOCAL_BIN:=$(CURDIR)/bin + +install-lint: + GOBIN=$(LOCAL_BIN) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.53.3 + GOBIN=$(LOCAL_BIN) go install golang.org/x/vuln/cmd/govulncheck@latest + +lint: + GOBIN=$(LOCAL_BIN) golangci-lint run ./... --config .golangci.pipeline.yaml + GOBIN=$(LOCAL_BIN) govulncheck ./... \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..02ddf20 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# platform \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4c46272 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module github.com/pillarion/practice-platform + +go 1.22.0 + +require github.com/jackc/pgx/v5 v5.5.4 + +require ( + github.com/jackc/puddle/v2 v2.2.1 // indirect + golang.org/x/sync v0.1.0 // indirect +) + +require ( + github.com/georgysavva/scany/v2 v2.1.0 + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/pkg/errors v0.9.1 + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fd7ef85 --- /dev/null +++ b/go.sum @@ -0,0 +1,42 @@ +github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs= +github.com/cockroachdb/cockroach-go/v2 v2.2.0/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/georgysavva/scany/v2 v2.1.0 h1:jEAX+yPQ2AAtnv0WJzAYlgsM/KzvwbD6BjSjLIyDxfc= +github.com/georgysavva/scany/v2 v2.1.0/go.mod h1:fqp9yHZzM/PFVa3/rYEC57VmDx+KDch0LoqrJzkvtos= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= +github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/dbclient/driver.go b/pkg/dbclient/driver.go new file mode 100644 index 0000000..b084a03 --- /dev/null +++ b/pkg/dbclient/driver.go @@ -0,0 +1,107 @@ +package dbclient + +import ( + "context" + "fmt" + "log" + + "github.com/georgysavva/scany/v2/pgxscan" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgxpool" + + txmanager "github.com/pillarion/practice-platform/pkg/pgtxmanager" + "github.com/pillarion/practice-platform/pkg/prettier" + "github.com/pkg/errors" +) + +type pg struct { + dbc *pgxpool.Pool +} + +// NewDB initializes a new DB driver using the provided database configuration. +func NewDB(ctx context.Context, dsn string) (DB, error) { + dbc, err := pgxpool.New(ctx, dsn) + if err != nil { + return nil, errors.Errorf("failed to connect to db: %v", err) + } + return &pg{ + dbc: dbc, + }, nil +} + +func (p *pg) ScanOneContext(ctx context.Context, dest interface{}, q Query, args ...interface{}) error { + logQuery(ctx, q, args...) + + row, err := p.QueryContext(ctx, q, args...) + if err != nil { + return err + } + + return pgxscan.ScanOne(dest, row) +} + +func (p *pg) ScanAllContext(ctx context.Context, dest interface{}, q Query, args ...interface{}) error { + logQuery(ctx, q, args...) + + rows, err := p.QueryContext(ctx, q, args...) + if err != nil { + return err + } + + return pgxscan.ScanAll(dest, rows) +} + +func (p *pg) ExecContext(ctx context.Context, q Query, args ...interface{}) (pgconn.CommandTag, error) { + logQuery(ctx, q, args...) + + tx, ok := ctx.Value(txmanager.TxKey).(pgx.Tx) + if ok { + return tx.Exec(ctx, q.QueryRaw, args...) + } + + return p.dbc.Exec(ctx, q.QueryRaw, args...) +} + +func (p *pg) QueryContext(ctx context.Context, q Query, args ...interface{}) (pgx.Rows, error) { + logQuery(ctx, q, args...) + + tx, ok := ctx.Value(txmanager.TxKey).(pgx.Tx) + if ok { + return tx.Query(ctx, q.QueryRaw, args...) + } + + return p.dbc.Query(ctx, q.QueryRaw, args...) +} + +func (p *pg) QueryRowContext(ctx context.Context, q Query, args ...interface{}) pgx.Row { + logQuery(ctx, q, args...) + + tx, ok := ctx.Value(txmanager.TxKey).(pgx.Tx) + if ok { + return tx.QueryRow(ctx, q.QueryRaw, args...) + } + + return p.dbc.QueryRow(ctx, q.QueryRaw, args...) +} + +func (p *pg) BeginTx(ctx context.Context, txOptions pgx.TxOptions) (pgx.Tx, error) { + return p.dbc.BeginTx(ctx, txOptions) +} + +func (p *pg) Ping(ctx context.Context) error { + return p.dbc.Ping(ctx) +} + +func (p *pg) Close() { + p.dbc.Close() +} + +func logQuery(ctx context.Context, q Query, args ...interface{}) { + prettyQuery := prettier.Pretty(q.QueryRaw, prettier.PlaceholderDollar, args...) + log.Println( + ctx, + fmt.Sprintf("sql: %s", q.Name), + fmt.Sprintf("query: %s", prettyQuery), + ) +} diff --git a/pkg/dbclient/port.go b/pkg/dbclient/port.go new file mode 100644 index 0000000..0b2e0b3 --- /dev/null +++ b/pkg/dbclient/port.go @@ -0,0 +1,53 @@ +package dbclient + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + pgtxmanager "github.com/pillarion/practice-platform/pkg/pgtxmanager" +) + +// Client represents a client for DB +type Client interface { + DB() DB + Close() error +} + +// DB represents a database +type DB interface { + SQLExecer + pgtxmanager.Transactor + Pinger + Close() +} + +// Pinger represents a pinger +type Pinger interface { + Ping(ctx context.Context) error +} + +// SQLExecer represents an executor +type SQLExecer interface { + NamedExecer + QueryExecer +} + +// NamedExecer represents a named query scanner +type NamedExecer interface { + ScanOneContext(ctx context.Context, dest interface{}, q Query, args ...interface{}) error + ScanAllContext(ctx context.Context, dest interface{}, q Query, args ...interface{}) error +} + +// QueryExecer represents a query executor +type QueryExecer interface { + ExecContext(ctx context.Context, q Query, args ...interface{}) (pgconn.CommandTag, error) + QueryContext(ctx context.Context, q Query, args ...interface{}) (pgx.Rows, error) + QueryRowContext(ctx context.Context, q Query, args ...interface{}) pgx.Row +} + +// Query represents a query wrapper +type Query struct { + Name string + QueryRaw string +} diff --git a/pkg/dbclient/service.go b/pkg/dbclient/service.go new file mode 100644 index 0000000..358595f --- /dev/null +++ b/pkg/dbclient/service.go @@ -0,0 +1,26 @@ +package dbclient + +type pgClient struct { + masterDBC DB +} + +// New initializes a new user repository using the provided database configuration. +func New(dbc DB) (Client, error) { + return &pgClient{ + masterDBC: dbc, + }, nil +} + +// DB returns the database client +func (c *pgClient) DB() DB { + return c.masterDBC +} + +// Close closes the database client +func (c *pgClient) Close() error { + if c.masterDBC != nil { + c.masterDBC.Close() + } + + return nil +} diff --git a/pkg/pgtxmanager/transaction.go b/pkg/pgtxmanager/transaction.go new file mode 100644 index 0000000..6f34b64 --- /dev/null +++ b/pkg/pgtxmanager/transaction.go @@ -0,0 +1,20 @@ +package pgtxmanager + +import ( + "context" + + "github.com/jackc/pgx/v5" +) + +// TxManager represents a transaction manager +type TxManager interface { + ReadCommitted(ctx context.Context, f Handler) error +} + +// Handler represents a handler for a transaction +type Handler func(ctx context.Context) error + +// Transactor represents a transactor +type Transactor interface { + BeginTx(ctx context.Context, txOptions pgx.TxOptions) (pgx.Tx, error) +} diff --git a/pkg/pgtxmanager/transactor.go b/pkg/pgtxmanager/transactor.go new file mode 100644 index 0000000..27eff1e --- /dev/null +++ b/pkg/pgtxmanager/transactor.go @@ -0,0 +1,88 @@ +package pgtxmanager + +import ( + "context" + + "github.com/jackc/pgx/v5" + + "github.com/pkg/errors" +) + +type key string + +const ( + // TxKey is the key for the transaction in the context + TxKey key = "tx" +) + +type manager struct { + db Transactor +} + +// NewTransactionManager represents a transaction manager +func NewTransactionManager(db Transactor) TxManager { + return &manager{ + db: db, + } +} + +func (m *manager) ReadCommitted(ctx context.Context, f Handler) error { + txOpts := pgx.TxOptions{IsoLevel: pgx.ReadCommitted} + return m.transaction(ctx, txOpts, f) +} + +func (m *manager) transaction(ctx context.Context, opts pgx.TxOptions, fn Handler) (err error) { + // Если это вложенная транзакция, пропускаем инициацию новой транзакции и выполняем обработчик. + tx, ok := ctx.Value(TxKey).(pgx.Tx) + if ok { + return fn(ctx) + } + + // Стартуем новую транзакцию. + tx, err = m.db.BeginTx(ctx, opts) + if err != nil { + return errors.Wrap(err, "can't begin transaction") + } + + // Кладем транзакцию в контекст. + ctx = MakeContextTx(ctx, tx) + + // Настраиваем функцию отсрочки для отката или коммита транзакции. + defer func() { + // восстанавливаемся после паники + if r := recover(); r != nil { + err = errors.Errorf("panic recovered: %v", r) + } + + // откатываем транзакцию, если произошла ошибка + if err != nil { + if errRollback := tx.Rollback(ctx); errRollback != nil { + err = errors.Wrapf(err, "errRollback: %v", errRollback) + } + + return + } + + // если ошибок не было, коммитим транзакцию + if nil == err { + err = tx.Commit(ctx) + if err != nil { + err = errors.Wrap(err, "tx commit failed") + } + } + }() + + // Выполните код внутри транзакции. + // Если функция терпит неудачу, возвращаем ошибку, и функция отсрочки выполняет откат + // или в противном случае транзакция коммитится. + if err = fn(ctx); err != nil { + err = errors.Wrap(err, "failed executing code inside transaction") + } + + return err +} + +// MakeContextTx adds tx to context +func MakeContextTx(ctx context.Context, tx pgx.Tx) context.Context { + return context.WithValue(ctx, TxKey, tx) +} diff --git a/pkg/prettier/prettier.go b/pkg/prettier/prettier.go new file mode 100644 index 0000000..8a369c6 --- /dev/null +++ b/pkg/prettier/prettier.go @@ -0,0 +1,36 @@ +package prettier + +import ( + "fmt" + "strconv" + "strings" +) + +const ( + // PlaceholderDollar is a placeholder for dollar signs + PlaceholderDollar = "$" + // PlaceholderQuestion is a placeholder for question marks + PlaceholderQuestion = "?" +) + +// Pretty returns a prettier query +func Pretty(query string, placeholder string, args ...any) string { + for i, param := range args { + var value string + switch v := param.(type) { + case string: + value = fmt.Sprintf("%q", v) + case []byte: + value = fmt.Sprintf("%q", string(v)) + default: + value = fmt.Sprintf("%v", v) + } + + query = strings.Replace(query, fmt.Sprintf("%s%s", placeholder, strconv.Itoa(i+1)), value, -1) + } + + query = strings.ReplaceAll(query, "\t", "") + query = strings.ReplaceAll(query, "\n", " ") + + return strings.TrimSpace(query) +}