regresql

package
v0.0.0-...-048667c Latest Latest
Warning

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

Go to latest
Published: Nov 7, 2023 License: PostgreSQL Imports: 15 Imported by: 0

Documentation

Overview

regresql package implements the RegreSQL commands.

The main entry point of this package is a Suite data structure instance, which can be obtained with the Walk() function:

func List(dir string) {
	suite := Walk(dir)
	suite.Println()
}

That's the simplest you can do with a suite instance, and that's the whole implementation of the exported List function too.

One you have a test suite instance, for more interesting things you usually want to read regresql configuration which is created by the Init() command and stores the PostgreSQL connection string, in the format expected by the github.com/lib/pq library:

suite := Walk(root)
config, err := suite.readConfig()

if err != nil {
	fmt.Printf(err.Error())
	os.Exit(3)
}

if err := TestConnectionString(config.PgUri); err != nil {
	fmt.Printf(err.Error())
	os.Exit(2)
}

Now that you have a test suite and a valid PostgreSQL connection string, it's possible to run the SQL queries. A query is typically as in the following example:

-- name: list-albums-by-artist
-- List the album titles and duration of a given artist
  select album.title as album,
         sum(milliseconds) * interval '1 ms' as duration
    from album
         join artist using(artistid)
         left join track using(albumid)
   where artist.name = :name
group by album
order by album;

This is parsed to find out the parameters, spelled in a `psql` compatible way as documented in https://www.postgresql.org/docs/9.6/static/app-psql.html#APP-PSQL-VARIABLES.

To be able to run regression tests against such a query with parameters, we need parameter values. That's to be found in a Plan file. A plan file is a YAML file associated with a query, such as the following:

"1":
  name: "Red Hot Chili Peppers"

In this file we find a single implementation of the query parameters, named "1" (that's automatically filled in by the Init() function). Our user edited the file to fill in "Red Hot Chili Peppers" from an empty string, as created by the Init() function.

The Init() function created a YAML plan file for each query, using the https://github.com/spf13/viper library. The user is expected to edit the YAML files. Once the parameters are edited it's possible to run the queries.

Update() runs the queries and stores their results in an expected file.

Test() runs the queries, stores their results in an out file (the actual output) and compares this actual result set with the expected one.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DiffFiles

func DiffFiles(a string, b string, c int) (string, error)

DiffFiles returns a unified diff result with c lines of context

func DiffLines

func DiffLines(from string, to string, a []string, b []string, c int) string

DiffLines compares two lists of strings and reports differences in the unified format. from and to are the files to report in the diff output.

func Init

func Init(root string, pguri string)

Init initializes a code repository for RegreSQL processing.

That means creating the ./regresql/ directory, walking the code repository in search of *.sql files, and creating the associated empty plan files. If the plan files already exists, we simply skip them, thus allowing to run init again on an existing repository to create missing plan files.

func List

func List(dir string)

List walks a repository, builds a Suite instance and pretty prints it.

func PlanQueries

func PlanQueries(root string)

PlanQueries create query plans for queries found in the root repository

func Test

func Test(root string)

Test runs the queries and compare their results to the previously created expected files (see Update()), reporting a TAP output to standard output.

func TestConnectionString

func TestConnectionString(pguri string) error

TestConnectionString connects to PostgreSQL with pguri and issue a single query (select 1"), because some errors (such as missing SSL certificates) only happen at query time.

func Update

func Update(root string)

Update updates the expected files from the queries and their parameters.

Types

type Folder

type Folder struct {
	Dir   string
	Files []string
}

Folder implements a directory from the source repository wherein we found some SQL files. Folder are only implemented as part of a Suite instance.

type Plan

type Plan struct {
	Query      *Query
	Path       string // the file path where we read the Plan from
	Names      []string
	Bindings   []map[string]string
	ResultSets []ResultSet
}

A query plan associates a Query parsed from a Path (name of the file on disk) and a list of set of parameters used to run the query. Each set of parameters as a name in Names[i] and a list of bindings in Bindings[i]. When the query is executed we store its output in ResultSets[i].

func (*Plan) CompareResultSets

func (p *Plan) CompareResultSets(regressDir string, expectedDir string, t *tap.T)

CompareResultsSets load the expected result set and compares it with the given Plan's ResultSet, and fills in a tap.T test output.

The test is considered passed when the diff is empty.

Rather than returning an error in case something wrong happens, we register a diagnostic against the tap output and fail the test case.

func (*Plan) Execute

func (p *Plan) Execute(db *sql.DB) error

Executes a plan and returns the filepath where the output has been written, for later comparing

func (*Plan) Write

func (p *Plan) Write()

Write a plan to disk in YAML format, thanks to Viper.

func (*Plan) WriteResultSets

func (p *Plan) WriteResultSets(dir string) error

WriteResultSets serialize the result of running a query, as a Pretty Printed output (comparable to a simplified `psql` output)

type Query

type Query struct {
	Path   string
	Text   string   // original query text
	Query  string   // "normalized" SQL query for lib/pq
	Vars   []string // variable names used in the query text
	Params []string // ordered list of params used in the query
}

A query instances represents an SQL query, read from Path filename and stored raw as the Text slot. The query text is “parsed” into the Query slot, and parameters are extracted into both the Vars slot and the Params slot.

SELECT * FROM foo WHERE a = :a and b between :a and :b;

In the previous query, we would have Vars = [a b] and Params = [a a b].

func (*Query) CreateEmptyPlan

func (q *Query) CreateEmptyPlan(dir string) (*Plan, error)

CreateEmptyPlan creates a YAML file where to store the set of parameters associated with a query.

func (*Query) GetPlan

func (q *Query) GetPlan(planDir string) (*Plan, error)

GetPlan instanciates a Plan from a Query, parsing a set of actual parameters when it exists.

func (*Query) Prepare

func (q *Query) Prepare(bindings map[string]string) (string, []interface{})

Prepare an args... interface{} for Query from given bindings

type ResultSet

type ResultSet struct {
	Cols     []string
	Rows     [][]interface{}
	Filename string
}

A ResultSet stores the result of a Query in Filename, with Cols and Rows separated.

func QueryDB

func QueryDB(db *sql.DB, query string, args ...interface{}) (*ResultSet, error)

QueryDB runs the query against the db database connection, and returns a ResultSet

func (*ResultSet) PrettyPrint

func (r *ResultSet) PrettyPrint() string

PrettyPrint pretty prints a result set and returns it as a string

func (*ResultSet) Println

func (r *ResultSet) Println()

Println outputs to standard output a Pretty Printed result set.

func (*ResultSet) Write

func (r *ResultSet) Write(filename string, overwrite bool) error

Writes the Result Set r to filename, overwriting it if already exists when overwrite is true

type Suite

type Suite struct {
	Root        string
	RegressDir  string
	Dirs        []Folder
	PlanDir     string
	ExpectedDir string
	OutDir      string
}

Suite implements a test suite, which is found in the Root directory and contains a list of Dirs folders, each containing a list of SQL query files. The RegressDir slot contains the directory where regresql stores its files: the query plans with bound parameters, their expected outputs and the actual results obtained when running `regresql test`.

Rather than handling a fully recursive data structure, which isn't necessary for our endeavours, we maintain a fixed two-levels data structure. The Printf() method dipatched on a Suite method is callable from the main command and shows our structure organisation:

$ regresql list
.
  src/sql/
    album-by-artist.sql
    album-tracks.sql
    artist.sql
    genre-topn.sql
    genre-tracks.sql

func Walk

func Walk(root string) *Suite

Walk walks the root directory recursively in search of *.sql files and returns a Suite instance representing the traversal.

func (*Suite) Println

func (s *Suite) Println()

Println(Suite) pretty prints the Suite instance to standard out.

Jump to

Keyboard shortcuts

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