Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for multiple queries per SQL file #3

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 32 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ The `regresql` tool implement a regression testing facility for SQL queries,
and supports the PostgreSQL database system. A regression test allows to
ensure known results when the code is edited. To enable that we need:

- some code to test, here SQL queries, each in its own file,
- some code to test, here SQL queries
- a known result set for each SQL query,
- a regression driver that runs queries again and check their result
against the known expected result set.

The RegreSQL tool is that regression driver. It helps with creating the
expected result set for each query and then running query files again to
check that the results are still the same.
Expand All @@ -21,7 +21,7 @@ against a known PostgreSQL database content.
The `regresql` tool is written in Go, so:

go get github.com/dimitri/regresql

This command will compile and install the command in your `$GOPATH/bin`,
which defaults to `~/go/bin`. See <https://golang.org/doc/install> if you're
new to the Go language.
Expand Down Expand Up @@ -75,6 +75,17 @@ about
[psql variables](https://www.postgresql.org/docs/current/static/app-psql.html#APP-PSQL-VARIABLES) and
their usage syntax and quoting rules: `:foo`, `:'foo'` and `:"foo"`.

RegreSQL supports either single query per SQL file, or multiple queries in file. In latter case you have
to tag/name the queries to enable the support.

Example

```
-- name: my-sample-query
SELECT count(*) FROM users
```


## Test Suites

By default a Test Suite is a source directory.
Expand All @@ -92,25 +103,28 @@ RegreSQL needs the following files and directories to run:
running the regression tests and the top level directory where to find
the SQL files to test against.

- `./regresql/expected/path/to/query.yaml`
- `./regresql/expected/path/to/file_query-name.yaml`

For each file *query.sql* found in your source tree, RegreSQL creates a
subpath in `./regresql/plans` with a *query.yaml* file. This YAML file
For each file *file.sql* found in your source tree, RegreSQL creates a
subpath in `./regresql/plans` with a *file_query-name.yaml* file. This YAML file
contains query plans: that's a list of SQL parameters values to use when
testing.

- `./regresql/expected/path/to/query.out`
- `./regresql/expected/path/to/file_query-name.out`

For each file *query.sql* found in your source tree, RegreSQL creates a
subpath in `./regresql/expected` directory and stores in *query.out* the
subpath in `./regresql/expected` directory and stores in *file_query-name.out* the
expected result set of the query,

- `./regresql/out/path/to/query.sql`
- `./regresql/out/path/to/file_query-name.sql`

The result of running the query in *query.sql* is stored in *query.out*
The result of running the query in *file_query-name.sql* is stored in *query.out*
in the `regresql/out` directory subpath for it, so that it is possible
to compare this result to the expected one in `regresql/expected`.

In all cases `query_name` is replaced by the tagged query name. If not present, name
`default` is used.

## Example

In a small local application the command `regresql list` returns the
Expand Down Expand Up @@ -141,7 +155,7 @@ Now we have to edit the YAML plan files to add bindings, here's an example
for a query using a single parameter, `:name`:

```
$ cat src/sql/album-by-artist.sql
$ cat src/sql/album_by_artist.sql
-- name: list-albums-by-artist
-- List the album titles and duration of a given artist
select album.title as album,
Expand All @@ -153,7 +167,7 @@ $ cat src/sql/album-by-artist.sql
group by album
order by album;

$ cat regresql/plans/src/sql/album-by-artist.yaml
$ cat regresql/plans/src/sql/album_by_artist_album-by-artist.yaml
"1":
name: "Red Hot Chili Peppers"
```
Expand All @@ -164,12 +178,12 @@ And we can now run the tests:
$ regresql test
Connecting to 'postgres:///chinook?sslmode=disable'… ✓
TAP version 13
ok 1 - src/sql/album-by-artist.1.out
ok 2 - src/sql/album-tracks.1.out
ok 3 - src/sql/artist.1.out
ok 4 - src/sql/genre-topn.top-3.out
ok 5 - src/sql/genre-topn.top-1.out
ok 6 - src/sql/genre-tracks.out
ok 1 - src/sql/album-by-artist_album-by-artist.1.out
ok 2 - src/sql/album-tracks_album-tracks.1.out
ok 3 - src/sql/artist_top-artists-by-album.1.out
ok 4 - src/sql/genre-topn_genre-top-n.top-3.out
ok 5 - src/sql/genre-topn.genre-top-n.top-1.out
ok 6 - src/sql/genre-tracks_tracks-by-genre.1.out
```

We can see the following files have been created by the RegreSQL tool:
Expand Down
1 change: 1 addition & 0 deletions regresql/plans.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ func (p *Plan) Write() {
func getPlanPath(q *Query, targetdir string) string {
planPath := filepath.Join(targetdir, filepath.Base(q.Path))
planPath = strings.TrimSuffix(planPath, path.Ext(planPath))
planPath = planPath + "_" + q.Name
planPath = planPath + ".yaml"

return planPath
Expand Down
20 changes: 10 additions & 10 deletions regresql/regresql.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ func Init(root string, pguri string) {
suite.Println()

fmt.Println("")
fmt.Printf(`Empty test plans have been created in '%s'.\n
Edit the plans to add query binding values, then run\n
\n
regresql update\n
\n
to create the expected regression files for your test plans. Plans are\n
simple YAML files containing multiple set of query parameter bindings. The\n
default plan files contain a single entry named "1", you can rename the test\n
case and add a value for each parameter.\n `,
fmt.Printf(`Empty test plans have been created in '%s'.
Edit the plans to add query binding values, then run

regresql update

to create the expected regression files for your test plans. Plans are
simple YAML files containing multiple set of query parameter bindings. The
default plan files contain a single entry named "1", you can rename the test
case and add a value for each parameter.`,
suite.PlanDir)
}

Expand Down Expand Up @@ -71,7 +71,7 @@ func PlanQueries(root string) {
suite.Println()

fmt.Println("")
fmt.Println(`Empty test plans have been created.
fmt.Printf(`Empty test plans have been created.
Edit the plans to add query binding values, then run

regresql update
Expand Down
71 changes: 71 additions & 0 deletions regresql/scanner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Credits to https://github.com/gchaincl/dotsql
package regresql

import (
"bufio"
"regexp"
"strings"
)

type Scanner struct {
line string
queries map[string]string
current string
}

type stateFn func(*Scanner) stateFn

func getTag(line string) string {
re := regexp.MustCompile("^\\s*--\\s*name:\\s*(\\S+)")
matches := re.FindStringSubmatch(line)
if matches == nil {
return ""
}
return matches[1]
}

func initialState(s *Scanner) stateFn {
if tag := getTag(s.line); len(tag) > 0 {
s.current = tag
return queryState
} else {
s.appendQueryLine()
}
return initialState
}

func queryState(s *Scanner) stateFn {
if tag := getTag(s.line); len(tag) > 0 {
s.current = tag
} else {
s.appendQueryLine()
}
return queryState
}

func (s *Scanner) appendQueryLine() {
current := s.queries[s.current]
line := strings.Trim(s.line, " \t")
if len(line) == 0 {
return
}

if len(current) > 0 {
current = current + "\n"
}

current = current + line
s.queries[s.current] = current
}

func (s *Scanner) Run(io *bufio.Scanner) map[string]string {
s.queries = make(map[string]string)
s.current = "default"

for state := initialState; io.Scan(); {
s.line = io.Text()
state = state(s)
}

return s.queries
}
37 changes: 22 additions & 15 deletions regresql/sql.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package regresql

import (
"bufio"
"fmt"
"io/ioutil"
"os"
"regexp"
)

Expand All @@ -25,27 +26,33 @@ In the previous query, we would have Vars = [a b] and Params = [a a b].
*/
type Query struct {
Path string
Name 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
}

// Parse a SQL file and returns a Query instance, with variables used in the
// query separated in the Query.Vars map.
func parseQueryFile(queryPath string) (*Query, error) {
sqlbytes, err := ioutil.ReadFile(queryPath)
// Parse a SQL file and returns map of Queries instances, with variables
// used in the query separated in the Query.Vars map.
func parseQueryFile(queryPath string) (map[string]*Query, error) {
f, err := os.Open(queryPath)
if err != nil {
var q *Query
e := fmt.Errorf(
"Failed to parse query file '%s': %s\n",
queryPath,
err)
return q, e
return nil, fmt.Errorf("Failed to open query file '%s: %s\n", queryPath, err.Error())
}
queryString := string(sqlbytes)

return parseQueryString(queryPath, queryString), nil
scanner := &Scanner{}
newQueries := scanner.Run(bufio.NewScanner(f))

queries := make(map[string]*Query)

for name, query := range newQueries {
query := parseQueryString(queryPath, name, query)

queries[name] = query
}

return queries, nil
}

// let's consider as an example the following SQL query:
Expand All @@ -61,7 +68,7 @@ func parseQueryFile(queryPath string) (*Query, error) {
//
// the idea is that then we can replace the param names by their values
// thanks to the plan test bindings given by the user (see p.Execute)
func parseQueryString(queryPath string, queryString string) *Query {
func parseQueryString(queryPath, queryName, queryString string) *Query {
// find a uses of variables in the SQL query text, and put then in a
// map so that we get each of them only once, even when used several
// times in the same query
Expand Down Expand Up @@ -95,7 +102,7 @@ func parseQueryString(queryPath string, queryString string) *Query {
}

// now build and return our Query
return &Query{queryPath, queryString, sql, vars, params}
return &Query{queryPath, queryName, queryString, sql, vars, params}
}

// Prepare an args... interface{} for Query from given bindings
Expand Down
8 changes: 4 additions & 4 deletions regresql/sql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

func TestParseQueryString(t *testing.T) {
queryString := `select * from foo where id = :user_id`
q := parseQueryString("no/path", queryString)
q := parseQueryString("no/path", "default", queryString)

if len(q.Vars) != 1 || q.Vars[0] != "user_id" {
t.Error("Expected [\"user_id\"], got ", q.Vars)
Expand All @@ -15,7 +15,7 @@ func TestParseQueryString(t *testing.T) {

func TestParseQueryStringWithTypeCast(t *testing.T) {
queryString := `select name::text from foo where id = :user_id`
q := parseQueryString("no/path", queryString)
q := parseQueryString("no/path", "default", queryString)

if len(q.Vars) != 1 || q.Vars[0] != "user_id" {
t.Error("Expected only [\"user_id\"], got ", q.Vars)
Expand All @@ -24,7 +24,7 @@ func TestParseQueryStringWithTypeCast(t *testing.T) {

func TestPrepareOneParam(t *testing.T) {
queryString := `select * from foo where id = :id`
q := parseQueryString("no/path", queryString)
q := parseQueryString("no/path", "default", queryString)
b := make(map[string]string)
b["id"] = "1"

Expand All @@ -42,7 +42,7 @@ func TestPrepareOneParam(t *testing.T) {

func TestPrepareTwoParams(t *testing.T) {
queryString := `select * from foo where a = :a and b between :a and :b`
q := parseQueryString("no/path", queryString)
q := parseQueryString("no/path", "default", queryString)
b := make(map[string]string)
b["a"] = "a"
b["b"] = "b"
Expand Down
Loading