Write some nice aspirational package docs

This commit is contained in:
Ben Visness 2022-04-16 13:57:41 -05:00
parent b9a4cb2361
commit a2917b98c0
2 changed files with 124 additions and 58 deletions

View File

@ -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 {

View File

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