diff --git a/examples/booktest/mysql/db.go b/examples/booktest/mysql/db.go new file mode 100644 index 0000000000..c6f85ff8b8 --- /dev/null +++ b/examples/booktest/mysql/db.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. + +package booktest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/examples/booktest/mysql/models.go b/examples/booktest/mysql/models.go new file mode 100644 index 0000000000..6b8259dbaa --- /dev/null +++ b/examples/booktest/mysql/models.go @@ -0,0 +1,35 @@ +// Code generated by sqlc. DO NOT EDIT. + +package booktest + +import ( + "time" +) + +type BookTypeType string + +const ( + FICTION BookTypeType = "FICTION" + NONFICTION BookTypeType = "NONFICTION" +) + +func (e *BookTypeType) Scan(src interface{}) error { + *e = BookTypeType(src.([]byte)) + return nil +} + +type Author struct { + AuthorID int + Name string +} + +type Book struct { + BookID int + AuthorID int + Isbn string + BookType BookTypeType + Title string + Yr int + Available time.Time + Tags string +} diff --git a/examples/booktest/mysql/query.sql b/examples/booktest/mysql/query.sql new file mode 100644 index 0000000000..d3bcf4edd7 --- /dev/null +++ b/examples/booktest/mysql/query.sql @@ -0,0 +1,48 @@ +/* name: GetAuthor :one */ +SELECT * FROM authors +WHERE author_id = ?; + +/* name: GetBook :one */ +SELECT * FROM books +WHERE book_id = ?; + +/* name: DeleteBook :exec */ +DELETE FROM books +WHERE book_id = ?; + +/* name: BooksByTitleYear :many */ +SELECT * FROM books +WHERE title = ? AND yr = ?; + +/* name: CreateAuthor :exec */ +INSERT INTO authors (name) VALUES (?); + +/* name: CreateBook :exec */ +INSERT INTO books ( + author_id, + isbn, + booktype, + title, + yr, + available, + tags +) VALUES ( + ?, + ?, + ?, + ?, + ?, + ?, + ? +); + +/* name: UpdateBook :exec */ +UPDATE books +SET title = ?, tags = ? +WHERE book_id = ?; + +/* name: UpdateBookISBN :exec */ +UPDATE books +SET title = ?, tags = ?, isbn = ? +WHERE book_id = ?; + diff --git a/examples/booktest/mysql/query.sql.go b/examples/booktest/mysql/query.sql.go new file mode 100644 index 0000000000..6d3703ac9b --- /dev/null +++ b/examples/booktest/mysql/query.sql.go @@ -0,0 +1,189 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: query.sql + +package booktest + +import ( + "context" + "time" +) + +const booksByTitleYear = `-- name: BooksByTitleYear :many +select book_id, author_id, isbn, book_type, title, yr, available, tags from books where title = ? and yr = ? +` + +type BooksByTitleYearParams struct { + Title string + Yr int +} + +type BooksByTitleYearRow struct { + BookID int + AuthorID int + Isbn string + BookType BookTypeType + Title string + Yr int + Available time.Time + Tags string +} + +func (q *Queries) BooksByTitleYear(ctx context.Context, arg BooksByTitleYearParams) ([]BooksByTitleYearRow, error) { + rows, err := q.db.QueryContext(ctx, booksByTitleYear, arg.Title, arg.Yr) + if err != nil { + return nil, err + } + defer rows.Close() + var items []BooksByTitleYearRow + for rows.Next() { + var i BooksByTitleYearRow + if err := rows.Scan( + &i.BookID, + &i.AuthorID, + &i.Isbn, + &i.BookType, + &i.Title, + &i.Yr, + &i.Available, + &i.Tags, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const createAuthor = `-- name: CreateAuthor :exec +insert into authors(name) values (?) +` + +func (q *Queries) CreateAuthor(ctx context.Context, name string) error { + _, err := q.db.ExecContext(ctx, createAuthor, name) + return err +} + +const createBook = `-- name: CreateBook :exec +insert into books(author_id, isbn, booktype, title, yr, available, tags) values (?, ?, ?, ?, ?, ?, ?) +` + +type CreateBookParams struct { + AuthorID int + Isbn string + Unknown interface{} + Title string + Yr int + Available time.Time + Tags string +} + +func (q *Queries) CreateBook(ctx context.Context, arg CreateBookParams) error { + _, err := q.db.ExecContext(ctx, createBook, + arg.AuthorID, + arg.Isbn, + arg.Unknown, + arg.Title, + arg.Yr, + arg.Available, + arg.Tags, + ) + return err +} + +const deleteBook = `-- name: DeleteBook :exec +delete from books where book_id = ? +` + +func (q *Queries) DeleteBook(ctx context.Context, book_id int) error { + _, err := q.db.ExecContext(ctx, deleteBook, book_id) + return err +} + +const getAuthor = `-- name: GetAuthor :one +select author_id, name from authors where author_id = ? +` + +type GetAuthorRow struct { + AuthorID int + Name string +} + +func (q *Queries) GetAuthor(ctx context.Context, author_id int) (GetAuthorRow, error) { + row := q.db.QueryRowContext(ctx, getAuthor, author_id) + var i GetAuthorRow + err := row.Scan(&i.AuthorID, &i.Name) + return i, err +} + +const getBook = `-- name: GetBook :one +select book_id, author_id, isbn, book_type, title, yr, available, tags from books where book_id = ? +` + +type GetBookRow struct { + BookID int + AuthorID int + Isbn string + BookType BookTypeType + Title string + Yr int + Available time.Time + Tags string +} + +func (q *Queries) GetBook(ctx context.Context, book_id int) (GetBookRow, error) { + row := q.db.QueryRowContext(ctx, getBook, book_id) + var i GetBookRow + err := row.Scan( + &i.BookID, + &i.AuthorID, + &i.Isbn, + &i.BookType, + &i.Title, + &i.Yr, + &i.Available, + &i.Tags, + ) + return i, err +} + +const updateBook = `-- name: UpdateBook :exec +update books set title = ?, tags = ? where book_id = ? +` + +type UpdateBookParams struct { + Title string + Tags string + BookID int +} + +func (q *Queries) UpdateBook(ctx context.Context, arg UpdateBookParams) error { + _, err := q.db.ExecContext(ctx, updateBook, arg.Title, arg.Tags, arg.BookID) + return err +} + +const updateBookISBN = `-- name: UpdateBookISBN :exec +update books set title = ?, tags = ?, isbn = ? where book_id = ? +` + +type UpdateBookISBNParams struct { + Title string + Tags string + Isbn string + BookID int +} + +func (q *Queries) UpdateBookISBN(ctx context.Context, arg UpdateBookISBNParams) error { + _, err := q.db.ExecContext(ctx, updateBookISBN, + arg.Title, + arg.Tags, + arg.Isbn, + arg.BookID, + ) + return err +} diff --git a/examples/booktest/mysql/schema.sql b/examples/booktest/mysql/schema.sql new file mode 100644 index 0000000000..53406a41a7 --- /dev/null +++ b/examples/booktest/mysql/schema.sql @@ -0,0 +1,32 @@ +SET FOREIGN_KEY_CHECKS=0; +DROP TABLE IF EXISTS authors; +DROP TABLE IF EXISTS books; +-- DROP FUNCTION IF EXISTS say_hello; +SET FOREIGN_KEY_CHECKS=1; + +CREATE TABLE authors ( + author_id integer NOT NULL AUTO_INCREMENT PRIMARY KEY, + name text NOT NULL DEFAULT '' +) ENGINE=InnoDB; + +CREATE INDEX authors_name_idx ON authors(name(255)); + +CREATE TABLE books ( + book_id integer NOT NULL AUTO_INCREMENT PRIMARY KEY, + author_id integer NOT NULL, + isbn varchar(255) NOT NULL DEFAULT '' UNIQUE, + book_type ENUM('FICTION', 'NONFICTION') NOT NULL DEFAULT 'FICTION', + title text NOT NULL DEFAULT '', + yr integer NOT NULL DEFAULT 2000, + available datetime NOT NULL DEFAULT NOW(), + tags text NOT NULL DEFAULT '' + -- CONSTRAINT FOREIGN KEY (author_id) REFERENCES authors(author_id) +) ENGINE=InnoDB; + +CREATE INDEX books_title_idx ON books(title(255), yr); + +/* +CREATE FUNCTION say_hello(s text) RETURNS text + DETERMINISTIC + RETURN CONCAT('hello ', s); +*/ diff --git a/examples/booktest/postgresql/db.go b/examples/booktest/postgresql/db.go new file mode 100644 index 0000000000..c6f85ff8b8 --- /dev/null +++ b/examples/booktest/postgresql/db.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. + +package booktest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/examples/booktest/postgresql/db_test.go b/examples/booktest/postgresql/db_test.go new file mode 100644 index 0000000000..0461662d79 --- /dev/null +++ b/examples/booktest/postgresql/db_test.go @@ -0,0 +1,157 @@ +// +build examples + +package booktest + +import ( + "context" + "path/filepath" + "testing" + "time" + + "github.com/kyleconroy/sqlc/internal/sqltest" +) + +func TestBooks(t *testing.T) { + db, cleanup := sqltest.PostgreSQL(t, filepath.Join("schema.sql")) + defer cleanup() + + ctx := context.Background() + dq := New(db) + + // create an author + a, err := dq.CreateAuthor(ctx, "Unknown Master") + if err != nil { + t.Fatal(err) + } + + // create transaction + tx, err := db.Begin() + if err != nil { + t.Fatal(err) + } + + tq := dq.WithTx(tx) + + // save first book + now := time.Now() + _, err = tq.CreateBook(ctx, CreateBookParams{ + AuthorID: a.AuthorID, + Isbn: "1", + Title: "my book title", + Booktype: BookTypeFICTION, + Year: 2016, + Available: now, + Tags: []string{}, + }) + if err != nil { + t.Fatal(err) + } + + // save second book + b1, err := tq.CreateBook(ctx, CreateBookParams{ + AuthorID: a.AuthorID, + Isbn: "2", + Title: "the second book", + Booktype: BookTypeFICTION, + Year: 2016, + Available: now, + Tags: []string{"cool", "unique"}, + }) + if err != nil { + t.Fatal(err) + } + + // update the title and tags + err = tq.UpdateBook(ctx, UpdateBookParams{ + BookID: b1.BookID, + Title: "changed second title", + Tags: []string{"cool", "disastor"}, + }) + if err != nil { + t.Fatal(err) + } + + // save third book + _, err = tq.CreateBook(ctx, CreateBookParams{ + AuthorID: a.AuthorID, + Isbn: "3", + Title: "the third book", + Booktype: BookTypeFICTION, + Year: 2001, + Available: now, + Tags: []string{"cool"}, + }) + if err != nil { + t.Fatal(err) + } + + // save fourth book + b3, err := tq.CreateBook(ctx, CreateBookParams{ + AuthorID: a.AuthorID, + Isbn: "4", + Title: "4th place finisher", + Booktype: BookTypeNONFICTION, + Year: 2011, + Available: now, + Tags: []string{"other"}, + }) + if err != nil { + t.Fatal(err) + } + + // tx commit + err = tx.Commit() + if err != nil { + t.Fatal(err) + } + + // upsert, changing ISBN and title + err = dq.UpdateBookISBN(ctx, UpdateBookISBNParams{ + BookID: b3.BookID, + Isbn: "NEW ISBN", + Title: "never ever gonna finish, a quatrain", + Tags: []string{"someother"}, + }) + if err != nil { + t.Fatal(err) + } + + // retrieve first book + books0, err := dq.BooksByTitleYear(ctx, BooksByTitleYearParams{ + Title: "my book title", + Year: 2016, + }) + if err != nil { + t.Fatal(err) + } + for _, book := range books0 { + t.Logf("Book %d (%s): %s available: %s\n", book.BookID, book.Booktype, book.Title, book.Available.Format(time.RFC822Z)) + author, err := dq.GetAuthor(ctx, book.AuthorID) + if err != nil { + t.Fatal(err) + } + t.Logf("Book %d author: %s\n", book.BookID, author.Name) + } + + // find a book with either "cool" or "other" tag + t.Logf("---------\nTag search results:\n") + res, err := dq.BooksByTags(ctx, []string{"cool", "other", "someother"}) + if err != nil { + t.Fatal(err) + } + for _, ab := range res { + t.Logf("Book %d: '%s', Author: '%s', ISBN: '%s' Tags: '%v'\n", ab.BookID, ab.Title, ab.Name, ab.Isbn, ab.Tags) + } + + // TODO: call say_hello(varchar) + + // get book 4 and delete + b5, err := dq.GetBook(ctx, b3.BookID) + if err != nil { + t.Fatal(err) + } + if err := dq.DeleteBook(ctx, b5.BookID); err != nil { + t.Fatal(err) + } + +} diff --git a/examples/booktest/postgresql/models.go b/examples/booktest/postgresql/models.go new file mode 100644 index 0000000000..09c7b6c4e8 --- /dev/null +++ b/examples/booktest/postgresql/models.go @@ -0,0 +1,35 @@ +// Code generated by sqlc. DO NOT EDIT. + +package booktest + +import ( + "time" +) + +type BookType string + +const ( + BookTypeFICTION BookType = "FICTION" + BookTypeNONFICTION BookType = "NONFICTION" +) + +func (e *BookType) Scan(src interface{}) error { + *e = BookType(src.([]byte)) + return nil +} + +type Author struct { + AuthorID int32 + Name string +} + +type Book struct { + BookID int32 + AuthorID int32 + Isbn string + Booktype BookType + Title string + Year int32 + Available time.Time + Tags []string +} diff --git a/examples/booktest/postgresql/query.sql b/examples/booktest/postgresql/query.sql new file mode 100644 index 0000000000..f4537c603e --- /dev/null +++ b/examples/booktest/postgresql/query.sql @@ -0,0 +1,60 @@ +-- name: GetAuthor :one +SELECT * FROM authors +WHERE author_id = $1; + +-- name: GetBook :one +SELECT * FROM books +WHERE book_id = $1; + +-- name: DeleteBook :exec +DELETE FROM books +WHERE book_id = $1; + +-- name: BooksByTitleYear :many +SELECT * FROM books +WHERE title = $1 AND year = $2; + +-- name: BooksByTags :many +SELECT + book_id, + title, + name, + isbn, + tags +FROM books +LEFT JOIN authors ON books.author_id = authors.author_id +WHERE tags && $1::varchar[]; + +-- name: CreateAuthor :one +INSERT INTO authors (name) VALUES ($1) +RETURNING *; + +-- name: CreateBook :one +INSERT INTO books ( + author_id, + isbn, + booktype, + title, + year, + available, + tags +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7 +) +RETURNING *; + +-- name: UpdateBook :exec +UPDATE books +SET title = $1, tags = $2 +WHERE book_id = $3; + +-- name: UpdateBookISBN :exec +UPDATE books +SET title = $1, tags = $2, isbn = $4 +WHERE book_id = $3; diff --git a/examples/booktest/postgresql/query.sql.go b/examples/booktest/postgresql/query.sql.go new file mode 100644 index 0000000000..9d1566b33e --- /dev/null +++ b/examples/booktest/postgresql/query.sql.go @@ -0,0 +1,252 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: query.sql + +package booktest + +import ( + "context" + "time" + + "github.com/lib/pq" +) + +const booksByTags = `-- name: BooksByTags :many +SELECT + book_id, + title, + name, + isbn, + tags +FROM books +LEFT JOIN authors ON books.author_id = authors.author_id +WHERE tags && $1::varchar[] +` + +type BooksByTagsRow struct { + BookID int32 + Title string + Name string + Isbn string + Tags []string +} + +func (q *Queries) BooksByTags(ctx context.Context, dollar_1 []string) ([]BooksByTagsRow, error) { + rows, err := q.db.QueryContext(ctx, booksByTags, pq.Array(dollar_1)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []BooksByTagsRow + for rows.Next() { + var i BooksByTagsRow + if err := rows.Scan( + &i.BookID, + &i.Title, + &i.Name, + &i.Isbn, + pq.Array(&i.Tags), + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const booksByTitleYear = `-- name: BooksByTitleYear :many +SELECT book_id, author_id, isbn, booktype, title, year, available, tags FROM books +WHERE title = $1 AND year = $2 +` + +type BooksByTitleYearParams struct { + Title string + Year int32 +} + +func (q *Queries) BooksByTitleYear(ctx context.Context, arg BooksByTitleYearParams) ([]Book, error) { + rows, err := q.db.QueryContext(ctx, booksByTitleYear, arg.Title, arg.Year) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Book + for rows.Next() { + var i Book + if err := rows.Scan( + &i.BookID, + &i.AuthorID, + &i.Isbn, + &i.Booktype, + &i.Title, + &i.Year, + &i.Available, + pq.Array(&i.Tags), + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const createAuthor = `-- name: CreateAuthor :one +INSERT INTO authors (name) VALUES ($1) +RETURNING author_id, name +` + +func (q *Queries) CreateAuthor(ctx context.Context, name string) (Author, error) { + row := q.db.QueryRowContext(ctx, createAuthor, name) + var i Author + err := row.Scan(&i.AuthorID, &i.Name) + return i, err +} + +const createBook = `-- name: CreateBook :one +INSERT INTO books ( + author_id, + isbn, + booktype, + title, + year, + available, + tags +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7 +) +RETURNING book_id, author_id, isbn, booktype, title, year, available, tags +` + +type CreateBookParams struct { + AuthorID int32 + Isbn string + Booktype BookType + Title string + Year int32 + Available time.Time + Tags []string +} + +func (q *Queries) CreateBook(ctx context.Context, arg CreateBookParams) (Book, error) { + row := q.db.QueryRowContext(ctx, createBook, + arg.AuthorID, + arg.Isbn, + arg.Booktype, + arg.Title, + arg.Year, + arg.Available, + pq.Array(arg.Tags), + ) + var i Book + err := row.Scan( + &i.BookID, + &i.AuthorID, + &i.Isbn, + &i.Booktype, + &i.Title, + &i.Year, + &i.Available, + pq.Array(&i.Tags), + ) + return i, err +} + +const deleteBook = `-- name: DeleteBook :exec +DELETE FROM books +WHERE book_id = $1 +` + +func (q *Queries) DeleteBook(ctx context.Context, bookID int32) error { + _, err := q.db.ExecContext(ctx, deleteBook, bookID) + return err +} + +const getAuthor = `-- name: GetAuthor :one +SELECT author_id, name FROM authors +WHERE author_id = $1 +` + +func (q *Queries) GetAuthor(ctx context.Context, authorID int32) (Author, error) { + row := q.db.QueryRowContext(ctx, getAuthor, authorID) + var i Author + err := row.Scan(&i.AuthorID, &i.Name) + return i, err +} + +const getBook = `-- name: GetBook :one +SELECT book_id, author_id, isbn, booktype, title, year, available, tags FROM books +WHERE book_id = $1 +` + +func (q *Queries) GetBook(ctx context.Context, bookID int32) (Book, error) { + row := q.db.QueryRowContext(ctx, getBook, bookID) + var i Book + err := row.Scan( + &i.BookID, + &i.AuthorID, + &i.Isbn, + &i.Booktype, + &i.Title, + &i.Year, + &i.Available, + pq.Array(&i.Tags), + ) + return i, err +} + +const updateBook = `-- name: UpdateBook :exec +UPDATE books +SET title = $1, tags = $2 +WHERE book_id = $3 +` + +type UpdateBookParams struct { + Title string + Tags []string + BookID int32 +} + +func (q *Queries) UpdateBook(ctx context.Context, arg UpdateBookParams) error { + _, err := q.db.ExecContext(ctx, updateBook, arg.Title, pq.Array(arg.Tags), arg.BookID) + return err +} + +const updateBookISBN = `-- name: UpdateBookISBN :exec +UPDATE books +SET title = $1, tags = $2, isbn = $4 +WHERE book_id = $3 +` + +type UpdateBookISBNParams struct { + Title string + Tags []string + BookID int32 + Isbn string +} + +func (q *Queries) UpdateBookISBN(ctx context.Context, arg UpdateBookISBNParams) error { + _, err := q.db.ExecContext(ctx, updateBookISBN, + arg.Title, + pq.Array(arg.Tags), + arg.BookID, + arg.Isbn, + ) + return err +} diff --git a/examples/booktest/postgresql/schema.sql b/examples/booktest/postgresql/schema.sql new file mode 100644 index 0000000000..0816931a81 --- /dev/null +++ b/examples/booktest/postgresql/schema.sql @@ -0,0 +1,37 @@ +DROP TABLE IF EXISTS books CASCADE; +DROP TYPE IF EXISTS book_type CASCADE; +DROP TABLE IF EXISTS authors CASCADE; +DROP FUNCTION IF EXISTS say_hello(text) CASCADE; + +CREATE TABLE authors ( + author_id SERIAL PRIMARY KEY, + name text NOT NULL DEFAULT '' +); + +CREATE INDEX authors_name_idx ON authors(name); + +CREATE TYPE book_type AS ENUM ( + 'FICTION', + 'NONFICTION' +); + +CREATE TABLE books ( + book_id SERIAL PRIMARY KEY, + author_id integer NOT NULL REFERENCES authors(author_id), + isbn text NOT NULL DEFAULT '' UNIQUE, + booktype book_type NOT NULL DEFAULT 'FICTION', + title text NOT NULL DEFAULT '', + year integer NOT NULL DEFAULT 2000, + available timestamp with time zone NOT NULL DEFAULT 'NOW()', + tags varchar[] NOT NULL DEFAULT '{}' +); + +CREATE INDEX books_title_idx ON books(title, year); + +CREATE FUNCTION say_hello(text) RETURNS text AS $$ +BEGIN + RETURN CONCAT('hello ', $1); +END; +$$ LANGUAGE plpgsql; + +CREATE INDEX books_title_lower_idx ON books(title); diff --git a/examples/jets/README.md b/examples/jets/README.md new file mode 100644 index 0000000000..0f8d99572e --- /dev/null +++ b/examples/jets/README.md @@ -0,0 +1,3 @@ +This database schema and query selection is taken from the +[SQLBoiler](https://github.com/volatiletech/sqlboiler#features--examples) +README. diff --git a/examples/jets/db.go b/examples/jets/db.go new file mode 100644 index 0000000000..2a9cc0bc83 --- /dev/null +++ b/examples/jets/db.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. + +package jets + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/examples/jets/models.go b/examples/jets/models.go new file mode 100644 index 0000000000..7bc33b6bda --- /dev/null +++ b/examples/jets/models.go @@ -0,0 +1,28 @@ +// Code generated by sqlc. DO NOT EDIT. + +package jets + +import () + +type Jet struct { + ID int32 + PilotID int32 + Age int32 + Name string + Color string +} + +type Language struct { + ID int32 + Language string +} + +type Pilot struct { + ID int32 + Name string +} + +type PilotLanguage struct { + PilotID int32 + LanguageID int32 +} diff --git a/examples/jets/query-building.sql b/examples/jets/query-building.sql new file mode 100644 index 0000000000..ede8952367 --- /dev/null +++ b/examples/jets/query-building.sql @@ -0,0 +1,8 @@ +-- name: CountPilots :one +SELECT COUNT(*) FROM pilots; + +-- name: ListPilots :many +SELECT * FROM pilots LIMIT 5; + +-- name: DeletePilot :exec +DELETE FROM pilots WHERE id = $1; diff --git a/examples/jets/query-building.sql.go b/examples/jets/query-building.sql.go new file mode 100644 index 0000000000..5fd138f816 --- /dev/null +++ b/examples/jets/query-building.sql.go @@ -0,0 +1,55 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: query-building.sql + +package jets + +import ( + "context" +) + +const countPilots = `-- name: CountPilots :one +SELECT COUNT(*) FROM pilots +` + +func (q *Queries) CountPilots(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, countPilots) + var count int64 + err := row.Scan(&count) + return count, err +} + +const deletePilot = `-- name: DeletePilot :exec +DELETE FROM pilots WHERE id = $1 +` + +func (q *Queries) DeletePilot(ctx context.Context, id int32) error { + _, err := q.db.ExecContext(ctx, deletePilot, id) + return err +} + +const listPilots = `-- name: ListPilots :many +SELECT id, name FROM pilots LIMIT 5 +` + +func (q *Queries) ListPilots(ctx context.Context) ([]Pilot, error) { + rows, err := q.db.QueryContext(ctx, listPilots) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Pilot + for rows.Next() { + var i Pilot + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/examples/jets/schema.sql b/examples/jets/schema.sql new file mode 100644 index 0000000000..2cc4aca574 --- /dev/null +++ b/examples/jets/schema.sql @@ -0,0 +1,35 @@ +CREATE TABLE pilots ( + id integer NOT NULL, + name text NOT NULL +); + +ALTER TABLE pilots ADD CONSTRAINT pilot_pkey PRIMARY KEY (id); + +CREATE TABLE jets ( + id integer NOT NULL, + pilot_id integer NOT NULL, + age integer NOT NULL, + name text NOT NULL, + color text NOT NULL +); + +ALTER TABLE jets ADD CONSTRAINT jet_pkey PRIMARY KEY (id); +ALTER TABLE jets ADD CONSTRAINT jet_pilots_fkey FOREIGN KEY (pilot_id) REFERENCES pilots(id); + +CREATE TABLE languages ( + id integer NOT NULL, + language text NOT NULL +); + +ALTER TABLE languages ADD CONSTRAINT language_pkey PRIMARY KEY (id); + +-- Join table +CREATE TABLE pilot_languages ( + pilot_id integer NOT NULL, + language_id integer NOT NULL +); + +-- Composite primary key +ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_pkey PRIMARY KEY (pilot_id, language_id); +ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_pilots_fkey FOREIGN KEY (pilot_id) REFERENCES pilots(id); +ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_languages_fkey FOREIGN KEY (language_id) REFERENCES languages(id); diff --git a/examples/ondeck/db_test.go b/examples/ondeck/db_test.go index afa7ae0961..31ca17fa03 100644 --- a/examples/ondeck/db_test.go +++ b/examples/ondeck/db_test.go @@ -4,93 +4,14 @@ package ondeck import ( "context" - "database/sql" - "fmt" - "io/ioutil" - "math/rand" - "os" - "path/filepath" "testing" + "github.com/kyleconroy/sqlc/internal/sqltest" + "github.com/google/go-cmp/cmp" _ "github.com/lib/pq" ) -func id() string { - bytes := make([]byte, 10) - for i := 0; i < 10; i++ { - bytes[i] = byte(65 + rand.Intn(25)) // A=65 and Z = 65+25 - } - return string(bytes) -} - -func provision(t *testing.T) (*sql.DB, func()) { - t.Helper() - - pgUser := os.Getenv("PG_USER") - pgHost := os.Getenv("PG_HOST") - pgPort := os.Getenv("PG_PORT") - pgPass := os.Getenv("PG_PASSWORD") - pgDB := os.Getenv("PG_DATABASE") - - if pgUser == "" { - pgUser = "postgres" - } - - if pgPort == "" { - pgPort = "5432" - } - - if pgHost == "" { - pgHost = "127.0.0.1" - } - - if pgDB == "" { - pgDB = "dinotest" - } - - source := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", pgUser, pgPass, pgHost, pgPort, pgDB) - t.Logf("db: %s", source) - - db, err := sql.Open("postgres", source) - if err != nil { - t.Fatal(err) - } - - schema := "dinotest_" + id() - - // For each test, pick a new schema name at random. - // `foo` is used here only as an example - if _, err := db.Exec("CREATE SCHEMA " + schema); err != nil { - t.Fatal(err) - } - - sdb, err := sql.Open("postgres", source+"&search_path="+schema) - if err != nil { - t.Fatal(err) - } - - files, err := ioutil.ReadDir("schema") - if err != nil { - t.Fatal(err) - } - for _, f := range files { - blob, err := ioutil.ReadFile(filepath.Join("schema", f.Name())) - if err != nil { - t.Fatal(err) - } - if _, err := sdb.Exec(string(blob)); err != nil { - t.Fatalf("%s: %s", f.Name(), err) - } - } - - return sdb, func() { - if _, err := db.Exec("DROP SCHEMA " + schema + " CASCADE"); err != nil { - t.Fatal(err) - } - } -} - func runOnDeckQueries(t *testing.T, q *Queries) { ctx := context.Background() @@ -203,7 +124,7 @@ func runOnDeckQueries(t *testing.T, q *Queries) { func TestPrepared(t *testing.T) { t.Parallel() - sdb, cleanup := provision(t) + sdb, cleanup := sqltest.PostgreSQL(t, "schema") defer cleanup() q, err := Prepare(context.Background(), sdb) @@ -217,7 +138,7 @@ func TestPrepared(t *testing.T) { func TestQueries(t *testing.T) { t.Parallel() - sdb, cleanup := provision(t) + sdb, cleanup := sqltest.PostgreSQL(t, "schema") defer cleanup() runOnDeckQueries(t, New(sdb)) diff --git a/internal/dinosql/parser_test.go b/internal/dinosql/parser_test.go index 645118306d..617a0da299 100644 --- a/internal/dinosql/parser_test.go +++ b/internal/dinosql/parser_test.go @@ -1,13 +1,8 @@ package dinosql import ( - "io/ioutil" - "os" - "path/filepath" - "strings" "testing" - "github.com/google/go-cmp/cmp" pg "github.com/lfittl/pg_query_go" nodes "github.com/lfittl/pg_query_go/nodes" ) @@ -117,82 +112,6 @@ func TestExtractArgs(t *testing.T) { } } -func cmpDirectory(t *testing.T, dir string, actual map[string]string) { - t.Helper() - - files, err := ioutil.ReadDir(dir) - if err != nil { - t.Fatal(err) - } - - expected := map[string]string{} - - for _, file := range files { - if file.IsDir() { - continue - } - if !strings.HasSuffix(file.Name(), ".go") { - continue - } - if strings.HasSuffix(file.Name(), "_test.go") { - continue - } - blob, err := ioutil.ReadFile(filepath.Join(dir, file.Name())) - if err != nil { - t.Fatal(err) - } - expected[file.Name()] = string(blob) - } - - if !cmp.Equal(expected, actual) { - t.Errorf("%s contents differ", dir) - for name, contents := range expected { - if actual[name] == "" { - t.Errorf("%s is empty", name) - continue - } - if diff := cmp.Diff(contents, actual[name]); diff != "" { - t.Errorf("%s differed (-want +got):\n%s", name, diff) - } - } - } -} - -func TestParseSchema(t *testing.T) { - // Change to the top-level directory of the project - os.Chdir(filepath.Join("..", "..")) - - rd, err := os.Open("sqlc.json") - if err != nil { - t.Fatal(err) - } - - conf, err := ParseConfig(rd) - if err != nil { - t.Fatal(err) - } - - for pkg, s := range conf.PackageMap { - settings := s - t.Run(pkg, func(t *testing.T) { - c, err := ParseCatalog(settings.Schema) - if err != nil { - t.Fatal(err) - } - q, err := ParseQueries(c, settings) - if err != nil { - t.Fatal(err) - } - output, err := Generate(q, conf) - if err != nil { - t.Fatal(err) - } - cmpDirectory(t, settings.Path, output) - }) - } - -} - func TestParseMetadata(t *testing.T) { for _, query := range []string{ `-- name: CreateFoo, :one`, diff --git a/internal/endtoend/endtoend_test.go b/internal/endtoend/endtoend_test.go new file mode 100644 index 0000000000..bd5c8179ec --- /dev/null +++ b/internal/endtoend/endtoend_test.go @@ -0,0 +1,102 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/kyleconroy/sqlc/internal/dinosql" + "github.com/kyleconroy/sqlc/internal/mysql" +) + +func TestCodeGeneration(t *testing.T) { + // Change to the top-level directory of the project + os.Chdir(filepath.Join("..", "..")) + + rd, err := os.Open("sqlc.json") + if err != nil { + t.Fatal(err) + } + + conf, err := dinosql.ParseConfig(rd) + if err != nil { + t.Fatal(err) + } + + for n, s := range conf.PackageMap { + pkg := s + t.Run(n, func(t *testing.T) { + var result dinosql.Generateable + switch pkg.Engine { + case dinosql.EngineMySQL: + q, err := mysql.GeneratePkg(pkg.Name, pkg.Schema, pkg.Queries, conf) + if err != nil { + t.Fatal(err) + } + result = q + case dinosql.EnginePostgreSQL: + c, err := dinosql.ParseCatalog(pkg.Schema) + if err != nil { + fmt.Printf("%#v\n", err) + t.Fatal(err) + } + q, err := dinosql.ParseQueries(c, pkg) + if err != nil { + t.Fatal(err) + } + result = q + } + output, err := dinosql.Generate(result, conf) + if err != nil { + t.Fatal(err) + } + cmpDirectory(t, pkg.Path, output) + }) + } + +} + +func cmpDirectory(t *testing.T, dir string, actual map[string]string) { + t.Helper() + + files, err := ioutil.ReadDir(dir) + if err != nil { + t.Fatalf("error reading dir %s: %s", dir, err) + } + + expected := map[string]string{} + + for _, file := range files { + if file.IsDir() { + continue + } + if !strings.HasSuffix(file.Name(), ".go") { + continue + } + if strings.HasSuffix(file.Name(), "_test.go") { + continue + } + blob, err := ioutil.ReadFile(filepath.Join(dir, file.Name())) + if err != nil { + t.Fatal(err) + } + expected[file.Name()] = string(blob) + } + + if !cmp.Equal(expected, actual) { + t.Errorf("%s contents differ", dir) + for name, contents := range expected { + if actual[name] == "" { + t.Errorf("%s is empty", name) + continue + } + if diff := cmp.Diff(contents, actual[name]); diff != "" { + t.Errorf("%s differed (-want +got):\n%s", name, diff) + } + } + } +} diff --git a/internal/sqltest/postgres.go b/internal/sqltest/postgres.go new file mode 100644 index 0000000000..e11eb9769c --- /dev/null +++ b/internal/sqltest/postgres.go @@ -0,0 +1,90 @@ +package sqltest + +import ( + "database/sql" + "fmt" + "io/ioutil" + "math/rand" + "os" + "path/filepath" + "testing" + + "github.com/kyleconroy/sqlc/internal/dinosql" + + _ "github.com/lib/pq" +) + +func id() string { + bytes := make([]byte, 10) + for i := 0; i < 10; i++ { + bytes[i] = byte(65 + rand.Intn(25)) // A=65 and Z = 65+25 + } + return string(bytes) +} + +func PostgreSQL(t *testing.T, migrations string) (*sql.DB, func()) { + t.Helper() + + pgUser := os.Getenv("PG_USER") + pgHost := os.Getenv("PG_HOST") + pgPort := os.Getenv("PG_PORT") + pgPass := os.Getenv("PG_PASSWORD") + pgDB := os.Getenv("PG_DATABASE") + + if pgUser == "" { + pgUser = "postgres" + } + + if pgPort == "" { + pgPort = "5432" + } + + if pgHost == "" { + pgHost = "127.0.0.1" + } + + if pgDB == "" { + pgDB = "dinotest" + } + + source := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", pgUser, pgPass, pgHost, pgPort, pgDB) + t.Logf("db: %s", source) + + db, err := sql.Open("postgres", source) + if err != nil { + t.Fatal(err) + } + + schema := "dinotest_" + id() + + // For each test, pick a new schema name at random. + // `foo` is used here only as an example + if _, err := db.Exec("CREATE SCHEMA " + schema); err != nil { + t.Fatal(err) + } + + sdb, err := sql.Open("postgres", source+"&search_path="+schema) + if err != nil { + t.Fatal(err) + } + + files, err := dinosql.ReadSQLFiles(migrations) + if err != nil { + t.Fatal(err) + } + for _, f := range files { + blob, err := ioutil.ReadFile(f) + if err != nil { + t.Fatal(err) + } + if _, err := sdb.Exec(string(blob)); err != nil { + t.Fatalf("%s: %s", filepath.Base(f), err) + } + } + + return sdb, func() { + if _, err := db.Exec("DROP SCHEMA " + schema + " CASCADE"); err != nil { + t.Fatal(err) + } + } +} diff --git a/sqlc.json b/sqlc.json index 96282769d1..0a13f81d84 100644 --- a/sqlc.json +++ b/sqlc.json @@ -2,12 +2,32 @@ "version": "1", "packages": [ { - "name": "ondeck", "path": "examples/ondeck", "schema": "examples/ondeck/schema", "queries": "examples/ondeck/query", + "engine": "postgresql", "emit_json_tags": true, "emit_prepared_queries": true + }, + { + "path": "examples/jets", + "schema": "examples/jets/schema.sql", + "queries": "examples/jets/query-building.sql", + "engine": "postgresql" + }, + { + "name": "booktest", + "path": "examples/booktest/postgresql", + "schema": "examples/booktest/postgresql/schema.sql", + "queries": "examples/booktest/postgresql/query.sql", + "engine": "postgresql" + }, + { + "name": "booktest", + "path": "examples/booktest/mysql", + "schema": "examples/booktest/mysql/schema.sql", + "queries": "examples/booktest/mysql/query.sql", + "engine": "mysql" } ] }