Rewrite to use embed.FS and sqlx.
This commit is contained in:
parent
ff130af80a
commit
cae890c983
11
README.md
11
README.md
@ -1,2 +1,13 @@
|
||||
# simplesql
|
||||
simplesql is a Go library to simplify using prepared statements. It is built on top of [sqlx](https://jmoiron.github.io/sqlx/).
|
||||
|
||||
## Use
|
||||
To use simplesql, embed a directory of files in a Go program. Call simplesql.Prepare with the embedded FS.
|
||||
|
||||
* The embedded directory is expected to be named "sqlq".
|
||||
* Files must have the extension ".sql".
|
||||
* Queries must be written using [named parameters](https://jmoiron.github.io/sqlx/#namedParams).
|
||||
|
||||
After calling Prepare successfully, a Stmts type is returned. This maps the file name to the prepared statement in the file.
|
||||
|
||||
See [the test package](./simplesql_test) for a working example.
|
||||
|
7
go.mod
7
go.mod
@ -1,3 +1,8 @@
|
||||
module git.simplesystems.tech/bjj-gym-management/simplesql
|
||||
|
||||
go 1.14
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/jmoiron/sqlx v1.3.4 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.10 // indirect
|
||||
)
|
||||
|
7
go.sum
Normal file
7
go.sum
Normal file
@ -0,0 +1,7 @@
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w=
|
||||
github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.10 h1:MLn+5bFRlWMGoSRmJour3CL1w/qL96mvipqpwQW/Sfk=
|
||||
github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
62
simplesql.go
62
simplesql.go
@ -2,39 +2,67 @@ package simplesql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// StmtKey is the simplest map key to use for mapping to a sql.Stmt, or a string statement.
|
||||
// It is up to the user to properly map StmtKeys to the right statement.
|
||||
type StmtKey int
|
||||
|
||||
// Stmts is the type that will be used to prepare and close sql.Stmts.
|
||||
type Stmts map[StmtKey]*sql.Stmt
|
||||
// Stmts is a map of named statements
|
||||
type Stmts map[string]*sqlx.NamedStmt
|
||||
|
||||
// Close will close all prepared statements.
|
||||
// This should be called after all statements are done.
|
||||
// This should be called after all statements are no longer needed.
|
||||
func (s *Stmts) Close() {
|
||||
for _, stmt := range *s {
|
||||
_ = stmt.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// New will create a Stmts from the pass queries. Each string query will be prepared
|
||||
// and mapped to the same key.
|
||||
func New(ctx context.Context, db *sql.DB, sqlStmts map[StmtKey]string) (Stmts, error) {
|
||||
s := Stmts{}
|
||||
// Prepare will create a Stmts from an embed.FS. The file name is the key.
|
||||
func Prepare(ctx context.Context, db *sqlx.DB, queries embed.FS) (Stmts, error) {
|
||||
// Read entries from sqlq dir
|
||||
entries, err := queries.ReadDir("sqlq")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading sqlq dir: %w", err)
|
||||
}
|
||||
|
||||
// Iterate through SQL statements and prepare them
|
||||
for key, q := range sqlStmts {
|
||||
ps, err := db.PrepareContext(ctx, q)
|
||||
// Get file content per entry and map to file name as prepared statement
|
||||
s := Stmts{}
|
||||
for _, entry := range entries {
|
||||
// Get file content
|
||||
fp := filepath.Join("sqlq", entry.Name())
|
||||
qb, err := queries.ReadFile(fp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("preparing statement: %w", err)
|
||||
return nil, fmt.Errorf("reading file %q from sqlq: %w", fp, err)
|
||||
}
|
||||
|
||||
s[key] = ps
|
||||
// Cleanse input query
|
||||
qs := cleanseQuery(string(qb))
|
||||
|
||||
// Prepare statement
|
||||
stmt, err := db.PrepareNamedContext(ctx, qs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("preparing statement %q: %w", qs, err)
|
||||
}
|
||||
|
||||
// Map to file name, without the extension, which is assumed to be .sql
|
||||
s[strings.TrimSuffix(entry.Name(), ".sql")] = stmt
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func cleanseQuery(s string) string {
|
||||
var cleansed []string
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
line := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, "--") {
|
||||
cleansed = append(cleansed, line)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(cleansed, "\n")
|
||||
}
|
||||
|
62
simplesql_test/simplesql_test.go
Normal file
62
simplesql_test/simplesql_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
package simplesql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"testing"
|
||||
|
||||
ssql "git.simplesystems.tech/bjj-gym-management/simplesql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
//go:embed sqlq
|
||||
var sqlQueries embed.FS
|
||||
|
||||
type user struct {
|
||||
Username string `db:"username"`
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
// Create in-memory db for test
|
||||
db, err := sqlx.Open("sqlite3", "file:temp.db?mode=memory")
|
||||
if err != nil {
|
||||
t.Fatalf("opening db: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Create table
|
||||
if _, err := db.Exec("CREATE TABLE users(id INT, username VARCHAR)"); err != nil {
|
||||
t.Fatalf("creating table: %v", err)
|
||||
}
|
||||
|
||||
// Add users
|
||||
if _, err := db.Exec("INSERT INTO users (id, username) VALUES (1, 'u1'), (2, 'u2')"); err != nil {
|
||||
t.Fatalf("adding users: %v", err)
|
||||
}
|
||||
|
||||
// Prepare statements
|
||||
stmts, err := ssql.Prepare(context.TODO(), db, sqlQueries)
|
||||
if err != nil {
|
||||
t.Fatalf("preparing statements: %v", err)
|
||||
}
|
||||
defer stmts.Close()
|
||||
|
||||
if len(stmts) != 1 {
|
||||
t.Errorf("expecting 1 statement, but got %d", len(stmts))
|
||||
}
|
||||
|
||||
if _, ok := stmts["get_user"]; !ok {
|
||||
t.Error("expecting get_user to be in stmts, but it is not")
|
||||
}
|
||||
|
||||
// Get a user from the table, using the prepared statement
|
||||
var res user
|
||||
if err := stmts["get_user"].Get(&res, map[string]interface{}{"user_id": "2"}); err != nil {
|
||||
t.Fatalf("getting users: %v", err)
|
||||
}
|
||||
|
||||
if res.Username != "u2" {
|
||||
t.Errorf("expecting u2, but got %s", res.Username)
|
||||
}
|
||||
}
|
2
simplesql_test/sqlq/get_user.sql
Normal file
2
simplesql_test/sqlq/get_user.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- Can use SQL comments here!
|
||||
SELECT username FROM users WHERE id = :user_id;
|
Loading…
Reference in New Issue
Block a user