diff --git a/README.md b/README.md index 71f0f90..1b50685 100644 --- a/README.md +++ b/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. diff --git a/go.mod b/go.mod index 4ad9e28..3087bd6 100644 --- a/go.mod +++ b/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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c7be50c --- /dev/null +++ b/go.sum @@ -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= diff --git a/simplesql.go b/simplesql.go index 6f20a72..484dbb0 100644 --- a/simplesql.go +++ b/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") +} diff --git a/simplesql_test/simplesql_test.go b/simplesql_test/simplesql_test.go new file mode 100644 index 0000000..ea4fa3e --- /dev/null +++ b/simplesql_test/simplesql_test.go @@ -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) + } +} diff --git a/simplesql_test/sqlq/get_user.sql b/simplesql_test/sqlq/get_user.sql new file mode 100644 index 0000000..c71617d --- /dev/null +++ b/simplesql_test/sqlq/get_user.sql @@ -0,0 +1,2 @@ +-- Can use SQL comments here! +SELECT username FROM users WHERE id = :user_id;