Write some nice aspirational package docs
This commit is contained in:
parent
b9a4cb2361
commit
a2917b98c0
121
src/db/db.go
121
src/db/db.go
|
@ -21,40 +21,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Values of these kinds are ok to query even if they are not directly understood by pgtype.
|
A general error to be used when no results are found. This is the error returned
|
||||||
This is common for custom types like:
|
by QueryOne, and can generally be used by other database helpers that fetch a single
|
||||||
|
result but find nothing.
|
||||||
type ThreadType int
|
|
||||||
*/
|
*/
|
||||||
var queryableKinds = []reflect.Kind{
|
var NotFound = errors.New("not found")
|
||||||
reflect.Int,
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Checks if we are able to handle a particular type in a database query. This applies only to
|
|
||||||
primitive types and not structs, since the database only returns individual primitive types
|
|
||||||
and it is our job to stitch them back together into structs later.
|
|
||||||
*/
|
|
||||||
func typeIsQueryable(t reflect.Type) bool {
|
|
||||||
_, isRecognizedByPgtype := connInfo.DataTypeForValue(reflect.New(t).Elem().Interface()) // if pgtype recognizes it, we don't need to dig in further for more `db` tags
|
|
||||||
// NOTE: boy it would be nice if we didn't have to do reflect.New here, considering that pgtype is just doing reflection on the value anyway
|
|
||||||
|
|
||||||
if isRecognizedByPgtype {
|
|
||||||
return true
|
|
||||||
} else if t == reflect.TypeOf(uuid.UUID{}) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// pgtype doesn't recognize it, but maybe it's a primitive type we can deal with
|
|
||||||
k := t.Kind()
|
|
||||||
for _, qk := range queryableKinds {
|
|
||||||
if k == qk {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// This interface should match both a direct pgx connection or a pgx transaction.
|
// This interface should match both a direct pgx connection or a pgx transaction.
|
||||||
type ConnOrTx interface {
|
type ConnOrTx interface {
|
||||||
|
@ -102,14 +73,14 @@ type columnName []string
|
||||||
// corresponds to a field index for use with Field on a reflect.Type or reflect.Value.
|
// corresponds to a field index for use with Field on a reflect.Type or reflect.Value.
|
||||||
type fieldPath []int
|
type fieldPath []int
|
||||||
|
|
||||||
type StructQueryIterator[T any] struct {
|
type ResultIterator[T any] struct {
|
||||||
fieldPaths []fieldPath
|
fieldPaths []fieldPath
|
||||||
rows pgx.Rows
|
rows pgx.Rows
|
||||||
destType reflect.Type
|
destType reflect.Type
|
||||||
closed chan struct{}
|
closed chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (it *StructQueryIterator[T]) Next() (*T, bool) {
|
func (it *ResultIterator[T]) Next() (*T, bool) {
|
||||||
hasNext := it.rows.Next()
|
hasNext := it.rows.Next()
|
||||||
if !hasNext {
|
if !hasNext {
|
||||||
it.Close()
|
it.Close()
|
||||||
|
@ -182,7 +153,7 @@ func (it *StructQueryIterator[T]) Next() (*T, bool) {
|
||||||
return result.Interface().(*T), true
|
return result.Interface().(*T), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (it *StructQueryIterator[any]) Close() {
|
func (it *ResultIterator[any]) Close() {
|
||||||
it.rows.Close()
|
it.rows.Close()
|
||||||
select {
|
select {
|
||||||
case it.closed <- struct{}{}:
|
case it.closed <- struct{}{}:
|
||||||
|
@ -190,7 +161,7 @@ func (it *StructQueryIterator[any]) Close() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (it *StructQueryIterator[T]) ToSlice() []*T {
|
func (it *ResultIterator[T]) ToSlice() []*T {
|
||||||
defer it.Close()
|
defer it.Close()
|
||||||
var result []*T
|
var result []*T
|
||||||
for {
|
for {
|
||||||
|
@ -238,6 +209,11 @@ func followPathThroughStructs(structPtrVal reflect.Value, path []int) (reflect.V
|
||||||
return val, field
|
return val, field
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Performs a SQL query and returns a slice of all the result rows. The query is just plain SQL, but make sure to read the package documentation for details. You must explicitly provide the type argument - this is how it knows what Go type to map the results to, and it cannot be inferred.
|
||||||
|
|
||||||
|
Any SQL query may be performed, including INSERT and UPDATE - as long as it returns a result set, you can use this. If the query does not return a result set, or you simply do not care about the result set, call Exec directly on your pgx connection.
|
||||||
|
*/
|
||||||
func Query[T any](ctx context.Context, conn ConnOrTx, query string, args ...interface{}) ([]*T, error) {
|
func Query[T any](ctx context.Context, conn ConnOrTx, query string, args ...interface{}) ([]*T, error) {
|
||||||
it, err := QueryIterator[T](ctx, conn, query, args...)
|
it, err := QueryIterator[T](ctx, conn, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -247,7 +223,7 @@ func Query[T any](ctx context.Context, conn ConnOrTx, query string, args ...inte
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func QueryIterator[T any](ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (*StructQueryIterator[T], error) {
|
func QueryIterator[T any](ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (*ResultIterator[T], error) {
|
||||||
var destExample T
|
var destExample T
|
||||||
destType := reflect.TypeOf(destExample)
|
destType := reflect.TypeOf(destExample)
|
||||||
|
|
||||||
|
@ -261,7 +237,7 @@ func QueryIterator[T any](ctx context.Context, conn ConnOrTx, query string, args
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
it := &StructQueryIterator[T]{
|
it := &ResultIterator[T]{
|
||||||
fieldPaths: compiled.fieldPaths,
|
fieldPaths: compiled.fieldPaths,
|
||||||
rows: rows,
|
rows: rows,
|
||||||
destType: compiled.destType,
|
destType: compiled.destType,
|
||||||
|
@ -285,6 +261,23 @@ func QueryIterator[T any](ctx context.Context, conn ConnOrTx, query string, args
|
||||||
return it, nil
|
return it, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func QueryOne[T any](ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (*T, error) {
|
||||||
|
rows, err := QueryIterator[T](ctx, conn, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
result, hasRow := rows.Next()
|
||||||
|
if !hasRow {
|
||||||
|
return nil, NotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: QueryFunc?
|
||||||
|
|
||||||
type compiledQuery struct {
|
type compiledQuery struct {
|
||||||
query string
|
query string
|
||||||
destType reflect.Type
|
destType reflect.Type
|
||||||
|
@ -408,27 +401,42 @@ func getColumnNamesAndPaths(destType reflect.Type, pathSoFar []int, prefix []str
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
A general error to be used when no results are found. This is the error returned
|
Values of these kinds are ok to query even if they are not directly understood by pgtype.
|
||||||
by QueryOne, and can generally be used by other database helpers that fetch a single
|
This is common for custom types like:
|
||||||
result but find nothing.
|
|
||||||
|
type ThreadType int
|
||||||
*/
|
*/
|
||||||
var NotFound = errors.New("not found")
|
var queryableKinds = []reflect.Kind{
|
||||||
|
reflect.Int,
|
||||||
func QueryOne[T any](ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (*T, error) {
|
|
||||||
rows, err := QueryIterator[T](ctx, conn, query, args...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
result, hasRow := rows.Next()
|
|
||||||
if !hasRow {
|
|
||||||
return nil, NotFound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
/*
|
||||||
|
Checks if we are able to handle a particular type in a database query. This applies only to
|
||||||
|
primitive types and not structs, since the database only returns individual primitive types
|
||||||
|
and it is our job to stitch them back together into structs later.
|
||||||
|
*/
|
||||||
|
func typeIsQueryable(t reflect.Type) bool {
|
||||||
|
_, isRecognizedByPgtype := connInfo.DataTypeForValue(reflect.New(t).Elem().Interface()) // if pgtype recognizes it, we don't need to dig in further for more `db` tags
|
||||||
|
// NOTE: boy it would be nice if we didn't have to do reflect.New here, considering that pgtype is just doing reflection on the value anyway
|
||||||
|
|
||||||
|
if isRecognizedByPgtype {
|
||||||
|
return true
|
||||||
|
} else if t == reflect.TypeOf(uuid.UUID{}) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pgtype doesn't recognize it, but maybe it's a primitive type we can deal with
|
||||||
|
k := t.Kind()
|
||||||
|
for _, qk := range queryableKinds {
|
||||||
|
if k == qk {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Delete in favor of `QueryOne`
|
||||||
func QueryScalar(ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (interface{}, error) {
|
func QueryScalar(ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (interface{}, error) {
|
||||||
rows, err := conn.Query(ctx, query, args...)
|
rows, err := conn.Query(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -452,6 +460,7 @@ func QueryScalar(ctx context.Context, conn ConnOrTx, query string, args ...inter
|
||||||
return nil, NotFound
|
return nil, NotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Delete in favor of `QueryOne[string]`
|
||||||
func QueryString(ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (string, error) {
|
func QueryString(ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (string, error) {
|
||||||
result, err := QueryScalar(ctx, conn, query, args...)
|
result, err := QueryScalar(ctx, conn, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -466,6 +475,7 @@ func QueryString(ctx context.Context, conn ConnOrTx, query string, args ...inter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Delete in favor of `QueryOne[int]`
|
||||||
func QueryInt(ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (int, error) {
|
func QueryInt(ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (int, error) {
|
||||||
result, err := QueryScalar(ctx, conn, query, args...)
|
result, err := QueryScalar(ctx, conn, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -484,6 +494,7 @@ func QueryInt(ctx context.Context, conn ConnOrTx, query string, args ...interfac
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Delete in favor of `QueryOne[bool]`
|
||||||
func QueryBool(ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (bool, error) {
|
func QueryBool(ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (bool, error) {
|
||||||
result, err := QueryScalar(ctx, conn, query, args...)
|
result, err := QueryScalar(ctx, conn, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,4 +1,59 @@
|
||||||
/*
|
/*
|
||||||
Wow so dobument
|
This package contains lowish-level APIs for making database queries to our Postgres database. It streamlines the process of mapping query results to Go types, while allowing you to write arbitrary SQL queries.
|
||||||
|
|
||||||
|
The primary functions are Query and QueryIterator. See the package and function examples for detailed usage.
|
||||||
|
|
||||||
|
Query syntax
|
||||||
|
|
||||||
|
This package allows a few small extensions to SQL syntax to streamline the interaction between Go and Postgres.
|
||||||
|
|
||||||
|
Arguments can be provided using placeholders like $1, $2, etc. All arguments will be safely escaped and mapped from their Go type to the correct Postgres type. (This is a direct proxy to pgx.)
|
||||||
|
|
||||||
|
projectIDs, err := db.Query[int](ctx, conn,
|
||||||
|
`
|
||||||
|
SELECT id
|
||||||
|
FROM handmade_project
|
||||||
|
WHERE
|
||||||
|
slug = ANY($1)
|
||||||
|
AND hidden = $2
|
||||||
|
`,
|
||||||
|
[]string{"4coder", "metadesk"},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
(This also demonstrates a useful tip: if you want to use a slice in your query, use Postgres arrays instead of IN.)
|
||||||
|
|
||||||
|
When querying individual fields, you can simply select the field like so:
|
||||||
|
|
||||||
|
ids, err := db.Query[int](ctx, conn, `SELECT id FROM handmade_project`)
|
||||||
|
|
||||||
|
To query multiple columns at once, you may use a struct type with `db:"column_name"` tags, and the special $columns placeholder:
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Slug string `db:"slug"`
|
||||||
|
DateCreated time.Time `db:"date_created"`
|
||||||
|
}
|
||||||
|
projects, err := db.Query[Project](ctx, conn, `SELECT $columns FROM ...`)
|
||||||
|
// Resulting query:
|
||||||
|
// SELECT id, slug, date_created FROM ...
|
||||||
|
|
||||||
|
Sometimes a table name prefix is required on each column to disambiguate between column names, especially when performing a JOIN. In those situations, you can include the prefix in the $columns placeholder like $columns{prefix}:
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Slug string `db:"slug"`
|
||||||
|
DateCreated time.Time `db:"date_created"`
|
||||||
|
}
|
||||||
|
orphanedProjects, err := db.Query[Project](ctx, conn, `
|
||||||
|
SELECT $columns{projects}
|
||||||
|
FROM
|
||||||
|
handmade_project AS projects
|
||||||
|
LEFT JOIN handmade_user_projects AS uproj
|
||||||
|
WHERE
|
||||||
|
uproj.user_id IS NULL
|
||||||
|
`)
|
||||||
|
// Resulting query:
|
||||||
|
// SELECT projects.id, projects.slug, projects.date_created FROM ...
|
||||||
*/
|
*/
|
||||||
package db
|
package db
|
||||||
|
|
Loading…
Reference in New Issue