Write some nice aspirational package docs
This commit is contained in:
parent
b9a4cb2361
commit
a2917b98c0
125
src/db/db.go
125
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.
|
||||
This is common for custom types like:
|
||||
|
||||
type ThreadType int
|
||||
A general error to be used when no results are found. This is the error returned
|
||||
by QueryOne, and can generally be used by other database helpers that fetch a single
|
||||
result but find nothing.
|
||||
*/
|
||||
var queryableKinds = []reflect.Kind{
|
||||
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
|
||||
}
|
||||
var NotFound = errors.New("not found")
|
||||
|
||||
// This interface should match both a direct pgx connection or a pgx transaction.
|
||||
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.
|
||||
type fieldPath []int
|
||||
|
||||
type StructQueryIterator[T any] struct {
|
||||
type ResultIterator[T any] struct {
|
||||
fieldPaths []fieldPath
|
||||
rows pgx.Rows
|
||||
destType reflect.Type
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
func (it *StructQueryIterator[T]) Next() (*T, bool) {
|
||||
func (it *ResultIterator[T]) Next() (*T, bool) {
|
||||
hasNext := it.rows.Next()
|
||||
if !hasNext {
|
||||
it.Close()
|
||||
|
@ -182,7 +153,7 @@ func (it *StructQueryIterator[T]) Next() (*T, bool) {
|
|||
return result.Interface().(*T), true
|
||||
}
|
||||
|
||||
func (it *StructQueryIterator[any]) Close() {
|
||||
func (it *ResultIterator[any]) Close() {
|
||||
it.rows.Close()
|
||||
select {
|
||||
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()
|
||||
var result []*T
|
||||
for {
|
||||
|
@ -238,6 +209,11 @@ func followPathThroughStructs(structPtrVal reflect.Value, path []int) (reflect.V
|
|||
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) {
|
||||
it, err := QueryIterator[T](ctx, conn, query, args...)
|
||||
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
|
||||
destType := reflect.TypeOf(destExample)
|
||||
|
||||
|
@ -261,7 +237,7 @@ func QueryIterator[T any](ctx context.Context, conn ConnOrTx, query string, args
|
|||
return nil, err
|
||||
}
|
||||
|
||||
it := &StructQueryIterator[T]{
|
||||
it := &ResultIterator[T]{
|
||||
fieldPaths: compiled.fieldPaths,
|
||||
rows: rows,
|
||||
destType: compiled.destType,
|
||||
|
@ -285,6 +261,23 @@ func QueryIterator[T any](ctx context.Context, conn ConnOrTx, query string, args
|
|||
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 {
|
||||
query string
|
||||
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
|
||||
by QueryOne, and can generally be used by other database helpers that fetch a single
|
||||
result but find nothing.
|
||||
Values of these kinds are ok to query even if they are not directly understood by pgtype.
|
||||
This is common for custom types like:
|
||||
|
||||
type ThreadType int
|
||||
*/
|
||||
var NotFound = errors.New("not found")
|
||||
|
||||
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
|
||||
var queryableKinds = []reflect.Kind{
|
||||
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
|
||||
}
|
||||
|
||||
// TODO: Delete in favor of `QueryOne`
|
||||
func QueryScalar(ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (interface{}, error) {
|
||||
rows, err := conn.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
|
@ -452,6 +460,7 @@ func QueryScalar(ctx context.Context, conn ConnOrTx, query string, args ...inter
|
|||
return nil, NotFound
|
||||
}
|
||||
|
||||
// TODO: Delete in favor of `QueryOne[string]`
|
||||
func QueryString(ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (string, error) {
|
||||
result, err := QueryScalar(ctx, conn, query, args...)
|
||||
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) {
|
||||
result, err := QueryScalar(ctx, conn, query, args...)
|
||||
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) {
|
||||
result, err := QueryScalar(ctx, conn, query, args...)
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue