Rewrite to use embed.FS and sqlx.

This commit is contained in:
jeff 2022-01-02 19:46:54 -08:00
parent ff130af80a
commit cae890c983
6 changed files with 133 additions and 18 deletions

View File

@ -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
View File

@ -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
View 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=

View File

@ -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")
}

View 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)
}
}

View File

@ -0,0 +1,2 @@
-- Can use SQL comments here!
SELECT username FROM users WHERE id = :user_id;