preql

package module
v0.0.0-...-c7b10eb Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 26, 2020 License: MIT Imports: 14 Imported by: 0

README

preql

A simple compile time query builder.

preql will search a package for sql queryies and generate simple helper functions that perform that query at compile time so the runtime does not have to parse queries anymore.

In addition preql can generate SQL scanners for the types in your code.

Installation

go get -u github.com/romeovs/preql

Usage

preql will not modify your code in any way, but can generate code that can assist you in setting up sql queries and scanners.

Scanners for a type

To generate scanners for a type, add sql struct tags to that type. For example

package user

type User struct {
  Username string `sql:"username"`
  Email    string `sql:"email"`
}

will generate the following functions in preql.go

// Scan scans the columns of the current row of rows into u.
// Expects rows.Next to be called at least once already.
func (u *User) Scan (rows *sql.Rows) error {
  // Omitted
}

// ScanOne scans one row from rows into u.
func (u *User) ScanOne (rows *sql.Rows) error {
  // Omitted
}

// toUser tries to scan the first row of rows into a new User.
// It checks err first to see if there were sql errors first and passes on the error.
// If err is sql.ErrNoRows, toUser returns nil, nil.
// This is a convenient helper to wrap preql query functions, eg.
//
//     u, err := toUser(preqlFindUser(r.sql, ctx))
//
func toUser(rows *sql.Rows, err error) (*User, error) {
  // Omitted
}

These functions are written to be about as optimal as you can get. If you add another field to the User struct definition, just run preql again and that field will be scanned as well.

You can omit fields from being scanned by not setting the sql struct tag on those fields.

Bound queries

Say we have a UserRepository that wraps the sql.DB and will implement a helper FindUserByUsername that can fetch a user from the database based on theri username.

package users

import (
  "database/sql"
)

type UserRepository struct {
  db *sql.DB
}

// FindUserByUsername finds a user by their username.
func (r *UserRepository) FindUserByUsername(ctx context.Context, username string) (*User, error) {
  // TODO: implement
  return nil, nil
}

You can hardcode the SQL query in a r.db.QueryContext call, but this can become tedious because you need to remember to correctly number the args and pass them in that order. This becomes really hard with big queries.

Instead we add a the SQL query to the comment like so:

// FindUserByUsername finds a user by their username.
//
// sql:
//   SELECT * FROM users
//   WHERE lower(username) = lower(:username)
//   LIMIT 1
//
func (r *UserRepository) FindUserByUsername(ctx context.Context, username string) (*User, error) {
  // TODO: implement
  return nil, nil
}

If you run preql the user package now, preql.go will contain a function preqlFindUserByUsername:

// preqlFindUserByUsername implements the query associated with FindUserByUsername in it's doc comment.
func preqlFindUserByUsername(sql sql.Interface, ctx context.Context, username string) (*sql.Rows, error) {
  // Omitted
}

You can call this function and it will perform the provided query with the args all in the correct order:

func (r *UserRepository) FindUserByUsername(ctx context.Context, username string) (*User, error) {
  rows, err := preqlFindUserByUsername(r.db, ctx, username)
  // handle errors, scan user etc.
}

This makes writing sql queries a bit less tedious and using them a bit more type-safe.

context.Context

preql detects wether or not the functions it generates need a context.Context argument by looking at the orignal functions arguments. Functions that don't have a context.Context argument will be using context.Background internally.

Exec

If you have a query that does not return results, use exec: in the comment instread of sql:. For example:

// CreateUser will create a new user in the system
//
// exec:
//   INSERT INTO users (
//     username,
//     email
//   ) VALUES (
//     lower(:username),
//     email
//   )
func (r *UserRepository) CreateUser(ctx context.Context, username string, email string) error {
  // Omitted
}

This will generate preqlCreateUser that uses exec under the hood.

Binding arguments

The sql query for a function can use all the arguments of the function by their referencing name prefixed with :.

context.Context arguments are not passed to the query.

You can index structs by the name they have defined in the sql struct tag. The User type above can be indexed by .username and .email, for example.

For example:

  • func(ctx context.Context, name string, limit uin64, offset uint64) can use :name, :limit, and :offset
  • func(user *User) can use :user.username and :user.email.
sql.Interface

The generated preql* functions do not accept a database/sql.DB but instead a github.com/romeovs/preql/sql.Interface so they can be used in the global database context as well as with transactions.

You will need to update the UserRepository as well:

package users

import (
  "github.com/romeovs/preql/sql"
)

type UserRepository struct {
  db sql.Interface
}

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Arg

type Arg struct {
	// Name is the name of the argument.
	Name string

	// TypeName is the name of the argument type.
	TypeName string
}

Arg is a description of a function argument.

type Package

type Package struct {
	Name           string
	Path           string
	Dir            string
	ScannableTypes []*ScannableType
	Queries        []*Query
}

Package contains the information

func Load

func Load(pkgpath string) (*Package, error)

Load loads the package at pkgpath and returns the relevant types and functions for preql.

func (Package) Generate

func (pkg Package) Generate() error

type Query

type Query struct {
	// Name is the name of the function or method.
	Name string

	// QueryString is the associated sql query.
	QueryString string

	// BindArgs are the arguments to the function query.
	BindArgs map[string]interface{}

	// Args are the arguments of the function.
	Args []Arg

	// Type is either "query" or "exec"
	Type string
}

Query holds information about methods that have an associated sql query.

func (Query) Arguments

func (q Query) Arguments() string

func (Query) Params

func (q Query) Params() (string, error)

type ScannableType

type ScannableType struct {
	// Name is the name of the type.
	Name string

	// Fields are the types field names, indexed by sql name (as defined in the sql struct tag).
	Fields map[string]string
}

ScannableType holds information about a type that is scannable.

func (ScannableType) Receiver

func (t ScannableType) Receiver() string

Receiver returns the receiver name of the scannable type.

Directories

Path Synopsis
examples
Package sql package provides a uniform interface for sql operations on sql databases.
Package sql package provides a uniform interface for sql operations on sql databases.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL