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