From e6d56b373e842e1b7ba688892cc09c7da5bd70e5 Mon Sep 17 00:00:00 2001 From: Kenneth Shaw Date: Wed, 24 Feb 2016 14:17:09 +0700 Subject: [PATCH] Large rewrite to fix issues with index names --- examples/mysql/booktest.go | 2 +- examples/mysql/gen.sh | 2 +- examples/mysql/models/author.xo.go | 54 +- examples/mysql/models/book.xo.go | 85 ++- examples/mysql/models/booktype.xo.go | 6 +- examples/oracle/booktest.go | 182 ++++++ examples/oracle/gen.sh | 80 +++ examples/postgres/booktest.go | 2 +- examples/postgres/gen.sh | 2 +- examples/postgres/models/author.xo.go | 54 +- examples/postgres/models/book.xo.go | 86 +-- examples/postgres/models/booktype.xo.go | 6 +- examples/sqlite/booktest.go | 2 +- examples/sqlite/gen.sh | 2 +- examples/sqlite/models/author.xo.go | 54 +- examples/sqlite/models/book.xo.go | 46 +- gen.sh | 316 ++++++---- internal/argtype.go | 86 ++- internal/funcs.go | 97 ++- internal/loader.go | 776 ++++++++++++++++++++++++ internal/loaders.go | 85 --- internal/templates.go | 53 +- internal/types.go | 194 +++--- internal/util.go | 64 +- loaders/mysql.go | 579 +++--------------- loaders/oracle.go | 4 +- loaders/postgres.go | 527 ++-------------- loaders/sqlite.go | 569 ++--------------- models/column.xo.go | 137 ++--- models/enum.xo.go | 61 +- models/enumvalue.xo.go | 48 ++ models/foreignkey.xo.go | 75 ++- models/index.xo.go | 125 ++++ models/indexcolumn.xo.go | 121 ++++ models/myenum.xo.go | 49 -- models/myenumvalue.xo.go | 30 + models/pgcolorder.xo.go | 33 + models/proc.xo.go | 38 +- models/procparam.xo.go | 81 +++ models/sqcolumn.xo.go | 48 -- models/sqforeignkey.xo.go | 48 -- models/sqindex.xo.go | 45 -- models/sqindexinfo.xo.go | 43 -- models/sqtableinfo.xo.go | 48 -- models/table.xo.go | 117 ++++ models/xo_db.xo.go | 2 +- templates/mysql.fkey.go.tpl | 1 - templates/mysql.foreignkey.go.tpl | 1 + templates/mysql.idx.go.tpl | 1 - templates/mysql.index.go.tpl | 1 + templates/mysql.model.go.tpl | 128 ---- templates/mysql.querytype.go.tpl | 1 + templates/mysql.type.go.tpl | 128 ++++ templates/postgres.enum.go.tpl | 49 +- templates/postgres.fkey.go.tpl | 7 - templates/postgres.foreignkey.go.tpl | 7 + templates/postgres.idx.go.tpl | 71 --- templates/postgres.index.go.tpl | 56 ++ templates/postgres.model.go.tpl | 158 ----- templates/postgres.proc.go.tpl | 14 +- templates/postgres.query.go.tpl | 22 +- templates/postgres.querytype.go.tpl | 11 + templates/postgres.type.go.tpl | 158 +++++ templates/sqlite3.fkey.go.tpl | 1 - templates/sqlite3.foreignkey.go.tpl | 1 + templates/sqlite3.idx.go.tpl | 1 - templates/sqlite3.index.go.tpl | 1 + templates/sqlite3.model.go.tpl | 1 - templates/sqlite3.querytype.go.tpl | 1 + templates/sqlite3.type.go.tpl | 1 + templates/tpls.go | 301 +++++---- templates/xo_db.go.tpl | 2 +- xo.go | 111 ++-- 73 files changed, 3310 insertions(+), 3089 deletions(-) create mode 100644 examples/oracle/booktest.go create mode 100755 examples/oracle/gen.sh create mode 100644 internal/loader.go delete mode 100644 internal/loaders.go create mode 100644 models/enumvalue.xo.go create mode 100644 models/index.xo.go create mode 100644 models/indexcolumn.xo.go delete mode 100644 models/myenum.xo.go create mode 100644 models/myenumvalue.xo.go create mode 100644 models/pgcolorder.xo.go create mode 100644 models/procparam.xo.go delete mode 100644 models/sqcolumn.xo.go delete mode 100644 models/sqforeignkey.xo.go delete mode 100644 models/sqindex.xo.go delete mode 100644 models/sqindexinfo.xo.go delete mode 100644 models/sqtableinfo.xo.go create mode 100644 models/table.xo.go delete mode 120000 templates/mysql.fkey.go.tpl create mode 120000 templates/mysql.foreignkey.go.tpl delete mode 120000 templates/mysql.idx.go.tpl create mode 120000 templates/mysql.index.go.tpl delete mode 100644 templates/mysql.model.go.tpl create mode 120000 templates/mysql.querytype.go.tpl create mode 100644 templates/mysql.type.go.tpl delete mode 100644 templates/postgres.fkey.go.tpl create mode 100644 templates/postgres.foreignkey.go.tpl delete mode 100644 templates/postgres.idx.go.tpl create mode 100644 templates/postgres.index.go.tpl delete mode 100644 templates/postgres.model.go.tpl create mode 100644 templates/postgres.querytype.go.tpl create mode 100644 templates/postgres.type.go.tpl delete mode 120000 templates/sqlite3.fkey.go.tpl create mode 120000 templates/sqlite3.foreignkey.go.tpl delete mode 120000 templates/sqlite3.idx.go.tpl create mode 120000 templates/sqlite3.index.go.tpl delete mode 120000 templates/sqlite3.model.go.tpl create mode 120000 templates/sqlite3.querytype.go.tpl create mode 120000 templates/sqlite3.type.go.tpl diff --git a/examples/mysql/booktest.go b/examples/mysql/booktest.go index 176f1109..7740ba25 100644 --- a/examples/mysql/booktest.go +++ b/examples/mysql/booktest.go @@ -141,7 +141,7 @@ func main() { */ // retrieve first book - books0, err := models.BooksByTitle(db, "my book title", 2016) + books0, err := models.BooksByTitleYear(db, "my book title", 2016) if err != nil { log.Fatal(err) } diff --git a/examples/mysql/gen.sh b/examples/mysql/gen.sh index 3ea58d08..a2a6aa17 100755 --- a/examples/mysql/gen.sh +++ b/examples/mysql/gen.sh @@ -59,7 +59,7 @@ ENDSQL $XOBIN $DB -o $SRC/models $EXTRA -cat << ENDSQL | $XOBIN $DB -N -M -B -T AuthorBookResult --query-type-comment='AuthorBookResult is the result of a search.' -o $SRC/models $EXTRA +$XOBIN $DB -N -M -B -T AuthorBookResult --query-type-comment='AuthorBookResult is the result of a search.' -o $SRC/models $EXTRA << ENDSQL SELECT a.author_id AS author_id, a.name AS author_name, diff --git a/examples/mysql/models/author.xo.go b/examples/mysql/models/author.xo.go index a2d68010..44e5f595 100644 --- a/examples/mysql/models/author.xo.go +++ b/examples/mysql/models/author.xo.go @@ -124,7 +124,33 @@ func (a *Author) Delete(db XODB) error { return nil } -// AuthorsByName retrieves rows from booktest.authors, each as a Author. +// AuthorByAuthorID retrieves a row from booktest.authors as a Author. +// +// Looks up using index authors_author_id_pkey. +func AuthorByAuthorID(db XODB, authorID int) (*Author, error) { + var err error + + // sql query + const sqlstr = `SELECT ` + + `author_id, name ` + + `FROM booktest.authors ` + + `WHERE author_id = ?` + + // run query + XOLog(sqlstr, authorID) + a := Author{ + _exists: true, + } + + err = db.QueryRow(sqlstr, authorID).Scan(&a.AuthorID, &a.Name) + if err != nil { + return nil, err + } + + return &a, nil +} + +// AuthorsByName retrieves a row from booktest.authors as a Author. // // Looks up using index authors_name_idx. func AuthorsByName(db XODB, name string) ([]*Author, error) { @@ -162,29 +188,3 @@ func AuthorsByName(db XODB, name string) ([]*Author, error) { return res, nil } - -// AuthorByAuthorID retrieves a row from booktest.authors as a Author. -// -// Looks up using index author_id. -func AuthorByAuthorID(db XODB, authorID int) (*Author, error) { - var err error - - // sql query - const sqlstr = `SELECT ` + - `author_id, name ` + - `FROM booktest.authors ` + - `WHERE author_id = ?` - - a := Author{ - _exists: true, - } - - // run query - XOLog(sqlstr, authorID) - err = db.QueryRow(sqlstr, authorID).Scan(&a.AuthorID, &a.Name) - if err != nil { - return nil, err - } - - return &a, nil -} diff --git a/examples/mysql/models/book.xo.go b/examples/mysql/models/book.xo.go index 5b3a3bb5..95f3ad6d 100644 --- a/examples/mysql/models/book.xo.go +++ b/examples/mysql/models/book.xo.go @@ -133,25 +133,71 @@ func (b *Book) Delete(db XODB) error { return nil } -// BookByIsbn retrieves a row from booktest.books as a Book. +// Author returns the Author associated with the Book's AuthorID (author_id). // -// Looks up using index isbn. -func BookByIsbn(db XODB, isbn string) (*Book, error) { +// Generated from books_ibfk_1. +func (b *Book) Author(db XODB) (*Author, error) { + return AuthorByAuthorID(db, b.AuthorID) +} + +// BooksByAuthorID retrieves a row from booktest.books as a Book. +// +// Looks up using index author_id. +func BooksByAuthorID(db XODB, authorID int) ([]*Book, error) { var err error // sql query const sqlstr = `SELECT ` + `book_id, author_id, isbn, book_type, title, year, available, tags ` + `FROM booktest.books ` + - `WHERE isbn = ?` + `WHERE author_id = ?` + + // run query + XOLog(sqlstr, authorID) + q, err := db.Query(sqlstr, authorID) + if err != nil { + return nil, err + } + defer q.Close() + + // load results + res := []*Book{} + for q.Next() { + b := Book{ + _exists: true, + } + + // scan + err = q.Scan(&b.BookID, &b.AuthorID, &b.Isbn, &b.BookType, &b.Title, &b.Year, &b.Available, &b.Tags) + if err != nil { + return nil, err + } + + res = append(res, &b) + } + return res, nil +} + +// BookByBookID retrieves a row from booktest.books as a Book. +// +// Looks up using index books_book_id_pkey. +func BookByBookID(db XODB, bookID int) (*Book, error) { + var err error + + // sql query + const sqlstr = `SELECT ` + + `book_id, author_id, isbn, book_type, title, year, available, tags ` + + `FROM booktest.books ` + + `WHERE book_id = ?` + + // run query + XOLog(sqlstr, bookID) b := Book{ _exists: true, } - // run query - XOLog(sqlstr, isbn) - err = db.QueryRow(sqlstr, isbn).Scan(&b.BookID, &b.AuthorID, &b.Isbn, &b.BookType, &b.Title, &b.Year, &b.Available, &b.Tags) + err = db.QueryRow(sqlstr, bookID).Scan(&b.BookID, &b.AuthorID, &b.Isbn, &b.BookType, &b.Title, &b.Year, &b.Available, &b.Tags) if err != nil { return nil, err } @@ -159,10 +205,10 @@ func BookByIsbn(db XODB, isbn string) (*Book, error) { return &b, nil } -// BooksByTitle retrieves rows from booktest.books, each as a Book. +// BooksByTitleYear retrieves a row from booktest.books as a Book. // // Looks up using index books_title_idx. -func BooksByTitle(db XODB, title string, year int) ([]*Book, error) { +func BooksByTitleYear(db XODB, title string, year int) ([]*Book, error) { var err error // sql query @@ -198,35 +244,28 @@ func BooksByTitle(db XODB, title string, year int) ([]*Book, error) { return res, nil } -// BookByBookID retrieves a row from booktest.books as a Book. +// BookByIsbn retrieves a row from booktest.books as a Book. // -// Looks up using index book_id. -func BookByBookID(db XODB, bookID int) (*Book, error) { +// Looks up using index isbn. +func BookByIsbn(db XODB, isbn string) (*Book, error) { var err error // sql query const sqlstr = `SELECT ` + `book_id, author_id, isbn, book_type, title, year, available, tags ` + `FROM booktest.books ` + - `WHERE book_id = ?` + `WHERE isbn = ?` + // run query + XOLog(sqlstr, isbn) b := Book{ _exists: true, } - // run query - XOLog(sqlstr, bookID) - err = db.QueryRow(sqlstr, bookID).Scan(&b.BookID, &b.AuthorID, &b.Isbn, &b.BookType, &b.Title, &b.Year, &b.Available, &b.Tags) + err = db.QueryRow(sqlstr, isbn).Scan(&b.BookID, &b.AuthorID, &b.Isbn, &b.BookType, &b.Title, &b.Year, &b.Available, &b.Tags) if err != nil { return nil, err } return &b, nil } - -// Book returns the Author associated with the Book's AuthorID (author_id). -// -// Generated from books_ibfk_1. -func (b *Book) Author(db XODB) (*Author, error) { - return AuthorByAuthorID(db, b.AuthorID) -} diff --git a/examples/mysql/models/booktype.xo.go b/examples/mysql/models/booktype.xo.go index a0627060..e41fdef7 100644 --- a/examples/mysql/models/booktype.xo.go +++ b/examples/mysql/models/booktype.xo.go @@ -8,14 +8,14 @@ import ( "errors" ) -// BookType is the 'book_type' enum type. +// BookType is the 'book_type' enum type from booktest. type BookType uint16 const ( - // FictionBookType is the book_type for 'FICTION'. + // FictionBookType is the 'FICTION' BookType. FictionBookType = BookType(1) - // NonfictionBookType is the book_type for 'NONFICTION'. + // NonfictionBookType is the 'NONFICTION' BookType. NonfictionBookType = BookType(2) ) diff --git a/examples/oracle/booktest.go b/examples/oracle/booktest.go new file mode 100644 index 00000000..031611f8 --- /dev/null +++ b/examples/oracle/booktest.go @@ -0,0 +1,182 @@ +package main + +import ( + "database/sql" + "flag" + "fmt" + "log" + "time" + + _ "github.com/lib/pq" + + "github.com/knq/xo/examples/postgres/models" +) + +var flagVerbose = flag.Bool("v", false, "verbose") + +func main() { + var err error + + // set logging + flag.Parse() + if *flagVerbose { + models.XOLog = func(s string, p ...interface{}) { + fmt.Printf("> SQL: %s -- %v\n", s, p) + } + } + + // open database + db, err := sql.Open("postgres", "postgres://booktest:booktest@localhost/booktest") + if err != nil { + log.Fatal(err) + } + + // create an author + a := models.Author{ + Name: "Unknown Master", + } + + // save author to database + err = a.Save(db) + if err != nil { + log.Fatal(err) + } + + // create transaction + tx, err := db.Begin() + if err != nil { + log.Fatal(err) + } + + // save first book + now := time.Now() + b0 := models.Book{ + AuthorID: a.AuthorID, + Isbn: "1", + Title: "my book title", + Booktype: models.FictionBookType, + Year: 2016, + Available: &now, + } + err = b0.Save(db) + if err != nil { + log.Fatal(err) + } + + // save second book + b1 := models.Book{ + AuthorID: a.AuthorID, + Isbn: "2", + Title: "the second book", + Booktype: models.FictionBookType, + Year: 2016, + Available: &now, + Tags: models.StringSlice{"cool", "unique"}, + } + err = b1.Save(db) + if err != nil { + log.Fatal(err) + } + + // update the title and tags + b1.Title = "changed second title" + b1.Tags = models.StringSlice{"cool", "disastor"} + err = b1.Update(db) + if err != nil { + log.Fatal(err) + } + + // save third book + b2 := models.Book{ + AuthorID: a.AuthorID, + Isbn: "3", + Title: "the third book", + Booktype: models.FictionBookType, + Year: 2001, + Available: &now, + Tags: models.StringSlice{"cool"}, + } + err = b2.Save(db) + if err != nil { + log.Fatal(err) + } + + // save fourth book + b3 := models.Book{ + AuthorID: a.AuthorID, + Isbn: "4", + Title: "4th place finisher", + Booktype: models.NonfictionBookType, + Year: 2011, + Available: &now, + Tags: models.StringSlice{"other"}, + } + err = b3.Save(db) + if err != nil { + log.Fatal(err) + } + + // tx commit + err = tx.Commit() + if err != nil { + log.Fatal(err) + } + + // upsert, changing ISBN and title + b4 := models.Book{ + BookID: b3.BookID, + AuthorID: a.AuthorID, + Isbn: "NEW ISBN", + Booktype: b3.Booktype, + Title: "never ever gonna finish, a quatrain", + Year: b3.Year, + Available: b3.Available, + Tags: models.StringSlice{"someother"}, + } + err = b4.Upsert(db) + if err != nil { + log.Fatal(err) + } + + // retrieve first book + books0, err := models.BooksByTitleYear(db, "my book title", 2016) + if err != nil { + log.Fatal(err) + } + for _, book := range books0 { + fmt.Printf("Book %d (%s): %s available: %s\n", book.BookID, book.Booktype, book.Title, book.Available.Format(time.RFC822Z)) + author, err := book.Author(db) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Book %d author: %s\n", book.BookID, author.Name) + } + + // find a book with either "cool" or "other" tag + fmt.Printf("---------\nTag search results:\n") + res, err := models.AuthorBookResultsByTags(db, models.StringSlice{"cool", "other", "someother"}) + if err != nil { + log.Fatal(err) + } + for _, ab := range res { + fmt.Printf("Book %d: '%s', Author: '%s', ISBN: '%s' Tags: '%v'\n", ab.BookID, ab.BookTitle, ab.AuthorName, ab.BookIsbn, ab.BookTags) + } + + // call say_hello(varchar) + str, err := models.SayHello(db, "john") + if err != nil { + log.Fatal(err) + } + fmt.Printf("SayHello response: %s\n", str) + + // get book 4 and delete + b5, err := models.BookByBookID(db, 4) + if err != nil { + log.Fatal(err) + } + err = b5.Delete(db) + if err != nil { + log.Fatal(err) + } +} diff --git a/examples/oracle/gen.sh b/examples/oracle/gen.sh new file mode 100755 index 00000000..d41184fb --- /dev/null +++ b/examples/oracle/gen.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +DBUSER=system +DBPASS=manager +DBHOST=$(docker port orcl 1521) +DBNAME=orcl + +SP=$DBUSER/$DBPASS@$DBHOST/$DBNAME +DB=oracle://$DBUSER:$DBPASS@$DBHOST/$DBNAME + +EXTRA=$1 + +SRC=$(realpath $(cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd )) + +XOBIN=$(which xo) +if [ -e $SRC/../../xo ]; then + XOBIN=$SRC/../../xo +fi + +DEST=$SRC/models + +set -x + +mkdir -p $DEST +rm -f $DEST/*.go +rm -f $SRC/oracle + +sqlplus -S $SP <<< 'DROP INDEX books_title_idx;' +sqlplus -S $SP <<< 'DROP INDEX authors_name_idx;' +sqlplus -S $SP <<< 'DROP TABLE books;' +sqlplus -S $SP <<< 'DROP TABLE authors;' + +sqlplus -S $SP << 'ENDSQL' +CREATE TABLE authors ( + author_id integer GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY, + name nvarchar2(255) DEFAULT '' NOT NULL +); + +CREATE INDEX authors_name_idx ON authors(name); + +CREATE TABLE books ( + book_id integer GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY, + author_id integer REFERENCES authors(author_id) NOT NULL, + isbn nvarchar2(255) DEFAULT '' UNIQUE NOT NULL, + booktype book_type DEFAULT 'FICTION' NOT NULL, + title nvarchar2(255) DEFAULT '' NOT NULL, + year integer DEFAULT 2000 NOT NULL, + available timestamp with time zone DEFAULT 'NOW()' NOT NULL, + tags nvarchar2[] DEFAULT '{}' NOT NULL +); + +CREATE INDEX books_title_idx ON books(title, year); + +ENDSQL + +$XOBIN $DB -o $SRC/models $EXTRA + +exit + +$XOBIN $DB -N -M -B -T AuthorBookResult --query-type-comment='AuthorBookResult is the result of a search.' -o $SRC/models $EXTRA << ENDSQL +SELECT + a.author_id::integer AS author_id, + a.name::text AS author_name, + b.book_id::integer AS book_id, + b.isbn::text AS book_isbn, + b.title::text AS book_title, + b.tags::text[] AS book_tags +FROM books b +JOIN authors a ON a.author_id = b.author_id +WHERE b.tags && %%tags StringSlice%%::nvarchar2[] +ENDSQL + +pushd $SRC &> /dev/null + +go build +./oracle $EXTRA + +popd &> /dev/null + +sqlplus $SP <<< 'select * from books;' diff --git a/examples/postgres/booktest.go b/examples/postgres/booktest.go index 311a7fa9..031611f8 100644 --- a/examples/postgres/booktest.go +++ b/examples/postgres/booktest.go @@ -139,7 +139,7 @@ func main() { } // retrieve first book - books0, err := models.BooksByTitle(db, "my book title", 2016) + books0, err := models.BooksByTitleYear(db, "my book title", 2016) if err != nil { log.Fatal(err) } diff --git a/examples/postgres/gen.sh b/examples/postgres/gen.sh index 08a15442..2c0b1788 100755 --- a/examples/postgres/gen.sh +++ b/examples/postgres/gen.sh @@ -64,7 +64,7 @@ ENDSQL $XOBIN $DB -o $SRC/models $EXTRA -cat << ENDSQL | $XOBIN $DB -N -M -B -T AuthorBookResult --query-type-comment='AuthorBookResult is the result of a search.' -o $SRC/models $EXTRA +$XOBIN $DB -N -M -B -T AuthorBookResult --query-type-comment='AuthorBookResult is the result of a search.' -o $SRC/models $EXTRA << ENDSQL SELECT a.author_id::integer AS author_id, a.name::text AS author_name, diff --git a/examples/postgres/models/author.xo.go b/examples/postgres/models/author.xo.go index bf080dbc..225b9a35 100644 --- a/examples/postgres/models/author.xo.go +++ b/examples/postgres/models/author.xo.go @@ -154,33 +154,7 @@ func (a *Author) Delete(db XODB) error { return nil } -// AuthorByAuthorID retrieves a row from public.authors as a Author. -// -// Looks up using index authors_pkey. -func AuthorByAuthorID(db XODB, authorID int) (*Author, error) { - var err error - - // sql query - const sqlstr = `SELECT ` + - `author_id, name ` + - `FROM public.authors ` + - `WHERE author_id = $1` - - a := Author{ - _exists: true, - } - - // run query - XOLog(sqlstr, authorID) - err = db.QueryRow(sqlstr, authorID).Scan(&a.AuthorID, &a.Name) - if err != nil { - return nil, err - } - - return &a, nil -} - -// AuthorsByName retrieves rows from public.authors, each as a Author. +// AuthorsByName retrieves a row from public.authors as a Author. // // Looks up using index authors_name_idx. func AuthorsByName(db XODB, name string) ([]*Author, error) { @@ -218,3 +192,29 @@ func AuthorsByName(db XODB, name string) ([]*Author, error) { return res, nil } + +// AuthorByAuthorID retrieves a row from public.authors as a Author. +// +// Looks up using index authors_pkey. +func AuthorByAuthorID(db XODB, authorID int) (*Author, error) { + var err error + + // sql query + const sqlstr = `SELECT ` + + `author_id, name ` + + `FROM public.authors ` + + `WHERE author_id = $1` + + // run query + XOLog(sqlstr, authorID) + a := Author{ + _exists: true, + } + + err = db.QueryRow(sqlstr, authorID).Scan(&a.AuthorID, &a.Name) + if err != nil { + return nil, err + } + + return &a, nil +} diff --git a/examples/postgres/models/book.xo.go b/examples/postgres/models/book.xo.go index 137ef3b0..92565e8f 100644 --- a/examples/postgres/models/book.xo.go +++ b/examples/postgres/models/book.xo.go @@ -163,43 +163,37 @@ func (b *Book) Delete(db XODB) error { return nil } -// BooksByTitle retrieves rows from public.books, each as a Book. +// Author returns the Author associated with the Book's AuthorID (author_id). // -// Looks up using index books_title_idx. -func BooksByTitle(db XODB, title string, year int) ([]*Book, error) { +// Generated from books_author_id_fkey. +func (b *Book) Author(db XODB) (*Author, error) { + return AuthorByAuthorID(db, b.AuthorID) +} + +// BookByIsbn retrieves a row from public.books as a Book. +// +// Looks up using index books_isbn_key. +func BookByIsbn(db XODB, isbn string) (*Book, error) { var err error // sql query const sqlstr = `SELECT ` + `book_id, author_id, isbn, booktype, title, year, available, tags ` + `FROM public.books ` + - `WHERE title = $1 AND year = $2` + `WHERE isbn = $1` // run query - XOLog(sqlstr, title, year) - q, err := db.Query(sqlstr, title, year) - if err != nil { - return nil, err + XOLog(sqlstr, isbn) + b := Book{ + _exists: true, } - defer q.Close() - // load results - res := []*Book{} - for q.Next() { - b := Book{ - _exists: true, - } - - // scan - err = q.Scan(&b.BookID, &b.AuthorID, &b.Isbn, &b.Booktype, &b.Title, &b.Year, &b.Available, &b.Tags) - if err != nil { - return nil, err - } - - res = append(res, &b) + err = db.QueryRow(sqlstr, isbn).Scan(&b.BookID, &b.AuthorID, &b.Isbn, &b.Booktype, &b.Title, &b.Year, &b.Available, &b.Tags) + if err != nil { + return nil, err } - return res, nil + return &b, nil } // BookByBookID retrieves a row from public.books as a Book. @@ -214,12 +208,12 @@ func BookByBookID(db XODB, bookID int) (*Book, error) { `FROM public.books ` + `WHERE book_id = $1` + // run query + XOLog(sqlstr, bookID) b := Book{ _exists: true, } - // run query - XOLog(sqlstr, bookID) err = db.QueryRow(sqlstr, bookID).Scan(&b.BookID, &b.AuthorID, &b.Isbn, &b.Booktype, &b.Title, &b.Year, &b.Available, &b.Tags) if err != nil { return nil, err @@ -228,35 +222,41 @@ func BookByBookID(db XODB, bookID int) (*Book, error) { return &b, nil } -// BookByIsbn retrieves a row from public.books as a Book. +// BooksByTitleYear retrieves a row from public.books as a Book. // -// Looks up using index books_isbn_key. -func BookByIsbn(db XODB, isbn string) (*Book, error) { +// Looks up using index books_title_idx. +func BooksByTitleYear(db XODB, title string, year int) ([]*Book, error) { var err error // sql query const sqlstr = `SELECT ` + `book_id, author_id, isbn, booktype, title, year, available, tags ` + `FROM public.books ` + - `WHERE isbn = $1` - - b := Book{ - _exists: true, - } + `WHERE title = $1 AND year = $2` // run query - XOLog(sqlstr, isbn) - err = db.QueryRow(sqlstr, isbn).Scan(&b.BookID, &b.AuthorID, &b.Isbn, &b.Booktype, &b.Title, &b.Year, &b.Available, &b.Tags) + XOLog(sqlstr, title, year) + q, err := db.Query(sqlstr, title, year) if err != nil { return nil, err } + defer q.Close() - return &b, nil -} + // load results + res := []*Book{} + for q.Next() { + b := Book{ + _exists: true, + } -// Book returns the Author associated with the Book's AuthorID (author_id). -// -// Generated from books_author_id_fkey. -func (b *Book) Author(db XODB) (*Author, error) { - return AuthorByAuthorID(db, b.AuthorID) + // scan + err = q.Scan(&b.BookID, &b.AuthorID, &b.Isbn, &b.Booktype, &b.Title, &b.Year, &b.Available, &b.Tags) + if err != nil { + return nil, err + } + + res = append(res, &b) + } + + return res, nil } diff --git a/examples/postgres/models/booktype.xo.go b/examples/postgres/models/booktype.xo.go index de5db2e1..c092440c 100644 --- a/examples/postgres/models/booktype.xo.go +++ b/examples/postgres/models/booktype.xo.go @@ -8,14 +8,14 @@ import ( "errors" ) -// BookType is the 'book_type' enum type. +// BookType is the 'book_type' enum type from public. type BookType uint16 const ( - // FictionBookType is the book_type for 'FICTION'. + // FictionBookType is the 'FICTION' BookType. FictionBookType = BookType(1) - // NonfictionBookType is the book_type for 'NONFICTION'. + // NonfictionBookType is the 'NONFICTION' BookType. NonfictionBookType = BookType(2) ) diff --git a/examples/sqlite/booktest.go b/examples/sqlite/booktest.go index a23f0ae2..cc277eb9 100644 --- a/examples/sqlite/booktest.go +++ b/examples/sqlite/booktest.go @@ -136,7 +136,7 @@ func main() { */ // retrieve first book - books0, err := models.BooksByTitle(db, "my book title", 2016) + books0, err := models.BooksByTitleYear(db, "my book title", 2016) if err != nil { log.Fatal(err) } diff --git a/examples/sqlite/gen.sh b/examples/sqlite/gen.sh index 1c498ebe..2198c9e5 100755 --- a/examples/sqlite/gen.sh +++ b/examples/sqlite/gen.sh @@ -48,7 +48,7 @@ ENDSQL $XOBIN $DB -o $SRC/models $EXTRA -cat << ENDSQL | $XOBIN $DB -N -M -B -T AuthorBookResult --query-type-comment='AuthorBookResult is the result of a search.' -o $SRC/models $EXTRA +$XOBIN $DB -N -M -B -T AuthorBookResult --query-type-comment='AuthorBookResult is the result of a search.' -o $SRC/models $EXTRA << ENDSQL SELECT a.author_id, a.name AS author_name, diff --git a/examples/sqlite/models/author.xo.go b/examples/sqlite/models/author.xo.go index a7faf047..093c28cd 100644 --- a/examples/sqlite/models/author.xo.go +++ b/examples/sqlite/models/author.xo.go @@ -124,7 +124,33 @@ func (a *Author) Delete(db XODB) error { return nil } -// AuthorsByName retrieves rows from authors, each as a Author. +// AuthorByAuthorID retrieves a row from authors as a Author. +// +// Looks up using index authors_author_id_pkey. +func AuthorByAuthorID(db XODB, authorID int) (*Author, error) { + var err error + + // sql query + const sqlstr = `SELECT ` + + `author_id, name ` + + `FROM authors ` + + `WHERE author_id = ?` + + // run query + XOLog(sqlstr, authorID) + a := Author{ + _exists: true, + } + + err = db.QueryRow(sqlstr, authorID).Scan(&a.AuthorID, &a.Name) + if err != nil { + return nil, err + } + + return &a, nil +} + +// AuthorsByName retrieves a row from authors as a Author. // // Looks up using index authors_name_idx. func AuthorsByName(db XODB, name string) ([]*Author, error) { @@ -162,29 +188,3 @@ func AuthorsByName(db XODB, name string) ([]*Author, error) { return res, nil } - -// AuthorByAuthorID retrieves a row from authors as a Author. -// -// Looks up using index authors_author_id_pkey. -func AuthorByAuthorID(db XODB, authorID int) (*Author, error) { - var err error - - // sql query - const sqlstr = `SELECT ` + - `author_id, name ` + - `FROM authors ` + - `WHERE author_id = ?` - - a := Author{ - _exists: true, - } - - // run query - XOLog(sqlstr, authorID) - err = db.QueryRow(sqlstr, authorID).Scan(&a.AuthorID, &a.Name) - if err != nil { - return nil, err - } - - return &a, nil -} diff --git a/examples/sqlite/models/book.xo.go b/examples/sqlite/models/book.xo.go index adb18c96..909f024e 100644 --- a/examples/sqlite/models/book.xo.go +++ b/examples/sqlite/models/book.xo.go @@ -129,25 +129,32 @@ func (b *Book) Delete(db XODB) error { return nil } -// BookByIsbn retrieves a row from books as a Book. +// Author returns the Author associated with the Book's AuthorID (author_id). // -// Looks up using index sqlite_autoindex_books_1. -func BookByIsbn(db XODB, isbn string) (*Book, error) { +// Generated from books_author_id_fkey. +func (b *Book) Author(db XODB) (*Author, error) { + return AuthorByAuthorID(db, b.AuthorID) +} + +// BookByBookID retrieves a row from books as a Book. +// +// Looks up using index books_book_id_pkey. +func BookByBookID(db XODB, bookID int) (*Book, error) { var err error // sql query const sqlstr = `SELECT ` + `book_id, author_id, isbn, title, year, available, tags ` + `FROM books ` + - `WHERE isbn = ?` + `WHERE book_id = ?` + // run query + XOLog(sqlstr, bookID) b := Book{ _exists: true, } - // run query - XOLog(sqlstr, isbn) - err = db.QueryRow(sqlstr, isbn).Scan(&b.BookID, &b.AuthorID, &b.Isbn, &b.Title, &b.Year, &b.Available, &b.Tags) + err = db.QueryRow(sqlstr, bookID).Scan(&b.BookID, &b.AuthorID, &b.Isbn, &b.Title, &b.Year, &b.Available, &b.Tags) if err != nil { return nil, err } @@ -155,10 +162,10 @@ func BookByIsbn(db XODB, isbn string) (*Book, error) { return &b, nil } -// BooksByTitle retrieves rows from books, each as a Book. +// BooksByTitleYear retrieves a row from books as a Book. // // Looks up using index books_title_idx. -func BooksByTitle(db XODB, title string, year int) ([]*Book, error) { +func BooksByTitleYear(db XODB, title string, year int) ([]*Book, error) { var err error // sql query @@ -194,35 +201,28 @@ func BooksByTitle(db XODB, title string, year int) ([]*Book, error) { return res, nil } -// BookByBookID retrieves a row from books as a Book. +// BookByIsbn retrieves a row from books as a Book. // -// Looks up using index books_book_id_pkey. -func BookByBookID(db XODB, bookID int) (*Book, error) { +// Looks up using index sqlite_autoindex_books_1. +func BookByIsbn(db XODB, isbn string) (*Book, error) { var err error // sql query const sqlstr = `SELECT ` + `book_id, author_id, isbn, title, year, available, tags ` + `FROM books ` + - `WHERE book_id = ?` + `WHERE isbn = ?` + // run query + XOLog(sqlstr, isbn) b := Book{ _exists: true, } - // run query - XOLog(sqlstr, bookID) - err = db.QueryRow(sqlstr, bookID).Scan(&b.BookID, &b.AuthorID, &b.Isbn, &b.Title, &b.Year, &b.Available, &b.Tags) + err = db.QueryRow(sqlstr, isbn).Scan(&b.BookID, &b.AuthorID, &b.Isbn, &b.Title, &b.Year, &b.Available, &b.Tags) if err != nil { return nil, err } return &b, nil } - -// Book returns the Author associated with the Book's AuthorID (author_id). -// -// Generated from books_author_id_fkey. -func (b *Book) Author(db XODB) (*Author, error) { - return AuthorByAuthorID(db, b.AuthorID) -} diff --git a/gen.sh b/gen.sh index 9f4c6944..920d4009 100755 --- a/gen.sh +++ b/gen.sh @@ -1,9 +1,9 @@ #!/bin/bash -PGSQL=pgsql://xodb:xodb@localhost/xodb -MYSQL=mysql://xodb:xodb@localhost/xodb -SQLTE=file:xodb.sqlite3 -ORCLE=oracle://xodb:xodb@localhost/xodb +PGDB=pgsql://xodb:xodb@localhost/xodb +MYDB=mysql://xodb:xodb@localhost/xodb +ORDB=oracle://xodb:xodb@localhost/xodb +SQDB=file:xodb.sqlite3 DEST=$1 @@ -11,6 +11,8 @@ if [ -z "$DEST" ]; then DEST=x fi +EXTRA=$2 + XOBIN=$(which xo) if [ -e ./xo ]; then XOBIN=./xo @@ -22,78 +24,95 @@ mkdir -p $DEST rm -f *.sqlite3 rm -rf $DEST/*.xo.go -# postgresql enum query -cat << ENDSQL | $XOBIN $PGSQL -v -N -M -B -T Enum -F PgEnumsBySchema --query-type-comment='Enum represents a enum value.' -o $DEST +# postgres enum list query +COMMENT='Enum represents a enum.' +$XOBIN $PGDB -N -M -B -T Enum -F PgEnums --query-type-comment "$COMMENT" -o $DEST $EXTRA << ENDSQL +SELECT + t.typname::varchar AS enum_name +FROM pg_type t + JOIN ONLY pg_namespace n ON n.oid = t.typnamespace + JOIN ONLY pg_enum e ON t.oid = e.enumtypid +WHERE n.nspname = %%schema string%% +ENDSQL + +# postgres enum value list query +COMMENT='EnumValue represents a enum value.' +$XOBIN $PGDB -N -M -B -T EnumValue -F PgEnumValues --query-type-comment "$COMMENT" -o $DEST $EXTRA << ENDSQL SELECT - t.typname::varchar AS enum_type, e.enumlabel::varchar AS enum_value, - e.enumsortorder::integer AS const_value, - ''::varchar AS type, - ''::varchar AS value, - ''::varchar AS comment + e.enumsortorder::integer AS const_value FROM pg_type t - LEFT JOIN pg_namespace n ON n.oid = t.typnamespace - JOIN pg_enum e ON t.oid = e.enumtypid + JOIN ONLY pg_namespace n ON n.oid = t.typnamespace + LEFT JOIN pg_enum e ON t.oid = e.enumtypid +WHERE n.nspname = %%schema string%% AND t.typname = %%enum string%% +ENDSQL + +# postgres proc list query +COMMENT='Proc represents a stored procedure.' +$XOBIN $PGDB -N -M -B -T Proc -F PgProcs --query-type-comment "$COMMENT" -o $DEST $EXTRA << ENDSQL +SELECT + p.proname::varchar AS proc_name, + pg_get_function_result(p.oid)::varchar AS return_type +FROM pg_proc p + JOIN ONLY pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = %%schema string%% ENDSQL -# postgresql column query -cat << ENDSQL | $XOBIN $PGSQL -v -N -M -B -T Column -F PgColumnsByRelkindSchema --query-type-comment='Column represents class (ie, table, view, etc) attributes.' -o $DEST +# postgres proc parameter list query +COMMENT='ProcParam represents a stored procedure param.' +$XOBIN $PGDB -N -M -B -T ProcParam -F PgProcParams --query-type-comment "$COMMENT" -o $DEST $EXTRA << ENDSQL +SELECT + UNNEST(STRING_TO_ARRAY(oidvectortypes(p.proargtypes), ', '))::varchar AS param_type +FROM pg_proc p + JOIN ONLY pg_namespace n ON p.pronamespace = n.oid +WHERE n.nspname = %%schema string%% AND p.proname = %%proc string%% +ENDSQL + +# postgres table list query +COMMENT='Table represents table info.' +$XOBIN $PGDB -N -M -B -T Table -F PgTables --query-type-comment "$COMMENT" -o $DEST $EXTRA << ENDSQL +SELECT + c.relkind::varchar AS type, + c.relname::varchar AS table_name +FROM pg_class c + JOIN ONLY pg_namespace n ON n.oid = c.relnamespace +WHERE n.nspname = %%schema string%% AND c.relkind = %%relkind string%% +ENDSQL + +# postgres table column list query +FIELDS='FieldOrdinal int,ColumnName string,DataType string,NotNull bool,DefaultValue sql.NullString,IsPrimaryKey bool' +COMMENT='Column represents column info.' +$XOBIN $PGDB -N -M -B -T Column -F PgTableColumns -Z "$FIELDS" --query-type-comment "$COMMENT" -o $DEST $EXTRA << ENDSQL SELECT + a.attnum::integer AS field_ordinal, a.attname::varchar AS column_name, - c.relname::varchar AS table_name, format_type(a.atttypid, a.atttypmod)::varchar AS data_type, - a.attnum::integer AS field_ordinal, - (NOT a.attnotnull)::boolean AS is_nullable, - COALESCE(i.oid <> 0, false)::boolean AS is_index, - COALESCE(ct.contype = 'u' OR ct.contype = 'p', false)::boolean AS is_unique, - COALESCE(ct.contype = 'p', false)::boolean AS is_primary_key, - COALESCE(cf.contype = 'f', false)::boolean AS is_foreign_key, - COALESCE(i.relname, '')::varchar AS index_name, - COALESCE(cf.conname, '')::varchar AS foreign_index_name, - a.atthasdef::boolean AS has_default, + a.attnotnull::boolean AS not_null, COALESCE(pg_get_expr(ad.adbin, ad.adrelid), '')::varchar AS default_value, - ''::varchar AS field, - ''::varchar AS type, - ''::varchar AS nil_type, - ''::varchar AS tag, - 0::integer AS len, - ''::varchar AS comment + COALESCE(ct.contype = 'p', false)::boolean AS is_primary_key FROM pg_attribute a JOIN ONLY pg_class c ON c.oid = a.attrelid JOIN ONLY pg_namespace n ON n.oid = c.relnamespace LEFT JOIN pg_constraint ct ON ct.conrelid = c.oid AND a.attnum = ANY(ct.conkey) AND ct.contype IN('p', 'u') - LEFT JOIN pg_constraint cf ON cf.conrelid = c.oid AND a.attnum = ANY(cf.conkey) AND cf.contype IN('f') LEFT JOIN pg_attrdef ad ON ad.adrelid = c.oid AND ad.adnum = a.attnum - LEFT JOIN pg_index ix ON a.attnum = ANY(ix.indkey) AND c.oid = a.attrelid AND c.oid = ix.indrelid - LEFT JOIN pg_class i ON i.oid = ix.indexrelid -WHERE a.attisdropped = false AND c.relkind = %%relkind string%% AND a.attnum > 0 AND n.nspname = %%schema string%% -ORDER BY c.relname, a.attnum -ENDSQL - -# postgresql proc query -cat << ENDSQL | $XOBIN $PGSQL -v -N -M -B -T Proc -F PgProcsBySchema --query-type-comment='Proc represents a stored procedure.' -o $DEST -SELECT - p.proname::varchar AS proc_name, - oidvectortypes(p.proargtypes)::varchar AS parameter_types, - pg_get_function_result(p.oid)::varchar AS return_type, - ''::varchar AS comment -FROM pg_proc p - INNER JOIN pg_namespace n ON p.pronamespace = n.oid -WHERE n.nspname = %%schema string%% -ORDER BY p.proname +WHERE a.attisdropped = false AND a.attnum > 0 AND n.nspname = %%schema string%% AND c.relname = %%table string%% +ORDER BY a.attnum ENDSQL -# postgresql foreign key query -cat << ENDSQL | $XOBIN $PGSQL -v -N -M -B -T ForeignKey -F PgForeignKeysBySchema --query-type-comment='ForeignKey represents a foreign key.' -o $DEST +# postgres table foreign key list query +COMMENT='ForeignKey represents a foreign key.' +$XOBIN $PGDB -N -M -B -T ForeignKey -F PgTableForeignKeys --query-type-comment "$COMMENT" -o $DEST $EXTRA << ENDSQL SELECT r.conname::varchar AS foreign_key_name, - a.relname::varchar AS table_name, b.attname::varchar AS column_name, i.relname::varchar AS ref_index_name, c.relname::varchar AS ref_table_name, d.attname::varchar AS ref_column_name, - ''::varchar AS comment + 0::integer AS key_id, + 0::integer AS seq_no, + ''::varchar AS on_update, + ''::varchar AS on_delete, + ''::varchar AS match FROM pg_constraint r JOIN ONLY pg_class a ON a.oid = r.conrelid JOIN ONLY pg_attribute b ON b.attisdropped = false AND b.attnum = ANY(r.conkey) AND b.attrelid = r.conrelid @@ -101,103 +120,170 @@ FROM pg_constraint r JOIN ONLY pg_class c on c.oid = r.confrelid JOIN ONLY pg_attribute d ON d.attisdropped = false AND d.attnum = ANY(r.confkey) AND d.attrelid = r.confrelid JOIN ONLY pg_namespace n ON n.oid = r.connamespace -WHERE r.contype = 'f' AND n.nspname = %%schema string%% -ORDER BY r.conname, a.relname, b.attname +WHERE r.contype = 'f' AND n.nspname = %%schema string%% AND a.relname = %%table string%% +ORDER BY r.conname, b.attname ENDSQL -# mysql enum query -cat << ENDSQL | $XOBIN $MYSQL -v -N -M -B -T MyEnum --query-type-comment='MyEnum represents a MySQL enum.' -o $DEST +# postgres table index list query +COMMENT='Index represents an index.' +$XOBIN $PGDB -N -M -B -T Index -F PgTableIndexes --query-type-comment "$COMMENT" -o $DEST $EXTRA << ENDSQL SELECT - table_name AS table_name, - column_name AS enum_type, - SUBSTRING(column_type, 6, CHAR_LENGTH(column_type) - 6) AS enum_values + DISTINCT ic.relname::varchar AS index_name, + i.indisunique::boolean AS is_unique, + i.indisprimary::boolean AS is_primary, + 0::integer AS seq_no, + ''::varchar AS origin, + false::boolean AS is_partial +FROM pg_index i + JOIN ONLY pg_class c ON c.oid = i.indrelid + JOIN ONLY pg_namespace n ON n.oid = c.relnamespace + JOIN ONLY pg_class ic ON ic.oid = i.indexrelid +WHERE n.nspname = %%schema string%% AND c.relname = %%table string%% +ENDSQL + +# postgres index column list query +COMMENT='IndexColumn represents index column info.' +$XOBIN $PGDB -N -M -B -T IndexColumn -F PgIndexColumns --query-type-comment "$COMMENT" -o $DEST $EXTRA << ENDSQL +SELECT + (row_number() over())::integer AS seq_no, + a.attnum::integer AS cid, + a.attname::varchar AS column_name +FROM pg_index i + JOIN ONLY pg_class c ON c.oid = i.indrelid + JOIN ONLY pg_namespace n ON n.oid = c.relnamespace + JOIN ONLY pg_class ic ON ic.oid = i.indexrelid + LEFT JOIN pg_attribute a ON i.indrelid = a.attrelid AND a.attnum = ANY(i.indkey) AND a.attisdropped = false +WHERE n.nspname = %%schema string%% AND ic.relname = %%index string%% +ENDSQL + +# postgres index column order query +COMMENT='PgColOrder represents index column order.' +$XOBIN $PGDB -N -M -B -1 -T PgColOrder -F PgGetColOrder --query-type-comment "$COMMENT" -o $DEST $EXTRA << ENDSQL +SELECT + i.indkey::varchar AS ord +FROM pg_index i + JOIN ONLY pg_class c ON c.oid = i.indrelid + JOIN ONLY pg_namespace n ON n.oid = c.relnamespace + JOIN ONLY pg_class ic ON ic.oid = i.indexrelid +WHERE n.nspname = %%schema string%% AND ic.relname = %%index string%% +ENDSQL + +# mysql enum list query +$XOBIN $MYDB -a -N -M -B -T Enum -F MyEnums -o $DEST $EXTRA << ENDSQL +SELECT + DISTINCT column_name AS enum_name FROM information_schema.columns WHERE data_type = 'enum' AND table_schema = %%schema string%% -ORDER BY table_name, column_name -ENDSQL - -# mysql column query -cat << ENDSQL | $XOBIN $MYSQL -a -v -N -M -B -T Column -F MyColumnsByRelkindSchema -o $DEST -SELECT - c.column_name, - c.table_name, - IF(c.data_type = 'enum', c.column_name, c.column_type) AS data_type, - c.ordinal_position AS field_ordinal, - IF(c.is_nullable, true, false) AS is_nullable, - IF(c.column_key <> '', true, false) AS is_index, - IF(c.column_key IN('PRI', 'UNI'), true, false) AS is_unique, - IF(c.column_key = 'PRI', true, false) AS is_primary_key, - COALESCE((SELECT s.index_name - FROM information_schema.statistics s - WHERE s.table_schema = c.table_schema AND s.table_name = c.table_name AND s.column_name = c.column_name), '') AS index_name, - COALESCE((SELECT x.constraint_name - FROM information_schema.key_column_usage x - WHERE x.table_name = c.table_name AND x.column_name = c.column_name AND NOT x.referenced_table_name IS NULL), '') AS foreign_index_name, - COALESCE(IF(c.column_default IS NULL, true, false), false) AS has_default, - COALESCE(c.column_default, '') AS default_value, - COALESCE(c.column_comment, '') AS comment -FROM information_schema.columns c -LEFT JOIN information_schema.tables t ON t.table_schema = c.table_schema AND t.table_name = c.table_name -WHERE t.table_type = %%relkind string%% AND c.table_schema = %%schema string%% -ORDER BY c.table_name, c.ordinal_position -ENDSQL - -# mysql proc query -cat << ENDSQL | $XOBIN $MYSQL -a -v -N -M -B -T Proc -F MyProcsBySchema -o $DEST +ENDSQL + +# mysql enum value list query +$XOBIN $MYDB -N -M -B -1 -T MyEnumValue -F MyEnumValues -o $DEST $EXTRA << ENDSQL +SELECT + SUBSTRING(column_type, 6, CHAR_LENGTH(column_type) - 6) AS enum_values +FROM information_schema.columns +WHERE data_type = 'enum' AND table_schema = %%schema string%% AND column_name = %%enum string%% +ENDSQL + +# mysql proc list query +$XOBIN $MYDB -a -N -M -B -T Proc -F MyProcs -o $DEST $EXTRA << ENDSQL SELECT r.routine_name AS proc_name, - (SELECT GROUP_CONCAT(l.dtd_identifier SEPARATOR ', ') - FROM information_schema.parameters l - WHERE l.specific_schema = r.routine_schema AND l.specific_name = r.routine_name AND l.ordinal_position > 0 - ORDER BY l.ordinal_position) AS parameter_types, p.dtd_identifier AS return_type FROM information_schema.routines r -INNER JOIN information_schema.parameters p ON - p.specific_schema = r.routine_schema AND p.specific_name = r.routine_name AND p.ordinal_position = 0 +INNER JOIN information_schema.parameters p + ON p.specific_schema = r.routine_schema AND p.specific_name = r.routine_name AND p.ordinal_position = 0 WHERE r.routine_schema = %%schema string%% -ORDER BY r.specific_name ENDSQL -# mysql foreign key query -cat << ENDSQL | $XOBIN $MYSQL -a -v -N -M -B -T ForeignKey -F MyForeignKeysBySchema -o $DEST +# mysql proc parameter list query +$XOBIN $MYDB -a -N -M -B -T ProcParam -F MyProcParams -o $DEST $EXTRA << ENDSQL +SELECT + dtd_identifier AS param_type +FROM information_schema.parameters +WHERE ordinal_position > 0 AND specific_schema = %%schema string%% AND specific_name = %%proc string%% +ORDER BY ordinal_position +ENDSQL + +# mysql table list query +$XOBIN $MYDB -a -N -M -B -T Table -F MyTables -o $DEST $EXTRA << ENDSQL +SELECT + table_name +FROM information_schema.tables +WHERE table_schema = %%schema string%% AND table_type = %%relkind string%% +ENDSQL + +# mysql table column list query +$XOBIN $MYDB -a -N -M -B -T Column -F MyTableColumns -o $DEST $EXTRA << ENDSQL +SELECT + ordinal_position AS field_ordinal, + column_name, + IF(data_type = 'enum', column_name, column_type) AS data_type, + IF(is_nullable, false, true) AS not_null, + column_default AS default_value, + IF(column_key = 'PRI', true, false) AS is_primary_key +FROM information_schema.columns +WHERE table_schema = %%schema string%% AND table_name = %%table string%% +ORDER BY ordinal_position +ENDSQL + +# mysql table foreign key list query +$XOBIN $MYDB -a -N -M -B -T ForeignKey -F MyTableForeignKeys -o $DEST $EXTRA << ENDSQL SELECT constraint_name AS foreign_key_name, - table_name AS table_name, column_name AS column_name, - '' AS ref_index_name, referenced_table_name AS ref_table_name, referenced_column_name AS ref_column_name FROM information_schema.key_column_usage -WHERE referenced_table_name IS NOT NULL AND table_schema = %%schema string%% -ORDER BY table_name, constraint_name +WHERE referenced_table_name IS NOT NULL AND table_schema = %%schema string%% AND table_name = %%table string%% +ENDSQL + +# mysql table index list query +$XOBIN $MYDB -a -N -M -B -T Index -F MyTableIndexes -o $DEST $EXTRA << ENDSQL +SELECT + DISTINCT index_name, + NOT non_unique AS is_unique +FROM information_schema.statistics +WHERE index_name <> 'PRIMARY' AND index_schema = %%schema string%% AND table_name = %%table string%% +ENDSQL + +# mysql index column list query +$XOBIN $MYDB -a -N -M -B -T IndexColumn -F MyIndexColumns -o $DEST $EXTRA << ENDSQL +SELECT + seq_in_index AS seq_no, + column_name +FROM information_schema.statistics +WHERE index_schema = %%schema string%% AND table_name = %%table string%% AND index_name = %%index string%% +ORDER BY seq_in_index ENDSQL # sqlite table list query -cat << ENDSQL | $XOBIN $SQLTE -v -N -M -B -T SqTableinfo -o $DEST +$XOBIN $SQDB -a -N -M -B -T Table -F SqTables -o $DEST $EXTRA << ENDSQL SELECT - type, - name, tbl_name AS table_name FROM sqlite_master WHERE type = %%relkind string%% ENDSQL -# sqlite table info query -cat << ENDSQL | $XOBIN $SQLTE -v -I -N -M -B -T SqColumn -Z 'FieldOrdinal int,ColumnName string,DataType string,NotNull bool,DefaultValue sql.NullString,IsPrimaryKey bool' -o $DEST +# sqlite table column list query +FIELDS='FieldOrdinal int,ColumnName string,DataType string,NotNull bool,DefaultValue sql.NullString,IsPrimaryKey bool' +$XOBIN $SQDB -a -I -N -M -B -T Column -F SqTableColumns -Z "$FIELDS" -o $DEST $EXTRA << ENDSQL PRAGMA table_info(%%table string,interpolate%%) ENDSQL -# sqlite foreign key list query -cat << ENDSQL | $XOBIN $SQLTE -v -I -N -M -B -T SqForeignKey -Z 'ID int,Seq int,RefTableName string,ColumnName string,RefColumnName string,OnUpdate string,OnDelete string,Match string' -o $DEST +# sqlite table foreign key list query +FIELDS='KeyID int,SeqNo int,RefTableName string,ColumnName string,RefColumnName string,OnUpdate string,OnDelete string,Match string' +$XOBIN $SQDB -a -I -N -M -B -T ForeignKey -F SqTableForeignKeys -Z "$FIELDS" -o $DEST $EXTRA << ENDSQL PRAGMA foreign_key_list(%%table string,interpolate%%) ENDSQL -# sqlite index list query -cat << ENDSQL | $XOBIN $SQLTE -v -I -N -M -B -T SqIndex -Z 'Seq int,IndexName string,IsUnique bool,Origin string,IsPartial bool' -o $DEST +# sqlite table index list query +FIELDS='SeqNo int,IndexName string,IsUnique bool,Origin string,IsPartial bool' +$XOBIN $SQDB -a -I -N -M -B -T Index -F SqTableIndexes -Z "$FIELDS" -o $DEST $EXTRA << ENDSQL PRAGMA index_list(%%table string,interpolate%%) ENDSQL -# sqlite index info query -cat << ENDSQL | $XOBIN $SQLTE -v -I -N -M -B -T SqIndexinfo -Z 'SeqNo int,Cid int,ColumnName string' -o $DEST +# sqlite index column list query +FIELDS='SeqNo int,Cid int,ColumnName string' +$XOBIN $SQDB -a -I -N -M -B -T IndexColumn -F SqIndexColumns -Z "$FIELDS" -o $DEST $EXTRA << ENDSQL PRAGMA index_info(%%index string,interpolate%%) ENDSQL diff --git a/internal/argtype.go b/internal/argtype.go index ec7385bf..109ecfab 100644 --- a/internal/argtype.go +++ b/internal/argtype.go @@ -1,12 +1,6 @@ package internal -import ( - "io/ioutil" - "path" - "text/template" - - "github.com/knq/xo/templates" -) +import "database/sql" // ArgType is the type that specifies the command line arguments. type ArgType struct { @@ -99,9 +93,15 @@ type ArgType struct { // Filename is the output filename, as derived from Out. Filename string `arg:"-"` - // Loader is the schema loader + // LoaderType is the loader type. + LoaderType string `arg:"-"` + + // Loader is the schema loader. Loader Loader `arg:"-"` + // DB is the opened database handle. + DB *sql.DB `arg:"-"` + // templateSet is the set of templates to use for generating data. templateSet *TemplateSet `arg:"-"` @@ -116,25 +116,55 @@ type ArgType struct { ShortNameTypeMap map[string]string `arg:"-"` } -// UserTemplateLoader loads templates from the specified name -func (a *ArgType) TemplateLoader(name string) ([]byte, error) { - // no template path specified - if a.TemplatePath == "" { - return templates.Asset(name) - } - - return ioutil.ReadFile(path.Join(a.TemplatePath, name)) -} - -// TemplateSet retrieves the created template set. -func (a *ArgType) TemplateSet() *TemplateSet { - if a.templateSet == nil { - a.templateSet = &TemplateSet{ - funcs: a.NewTemplateFuncs(), - l: a.TemplateLoader, - tpls: map[string]*template.Template{}, - } +// NewDefaultArgs returns the default arguments. +func NewDefaultArgs() *ArgType { + return &ArgType{ + Suffix: ".xo.go", + Int32Type: "int", + Uint32Type: "uint", + QueryParamDelimiter: "%%", + + // KnownTypeMap is the collection of known Go types. + KnownTypeMap: map[string]bool{ + "bool": true, + "string": true, + "byte": true, + "rune": true, + "int": true, + "int16": true, + "int32": true, + "int64": true, + "uint": true, + "uint8": true, + "uint16": true, + "uint32": true, + "uint64": true, + "float32": true, + "float64": true, + "Slice": true, + "StringSlice": true, + }, + + // ShortNameTypeMap is the collection of Go style short names for types, mainly + // used for use with declaring a func receiver on a type. + ShortNameTypeMap: map[string]string{ + "bool": "b", + "string": "s", + "byte": "b", + "rune": "r", + "int": "i", + "int16": "i", + "int32": "i", + "int64": "i", + "uint": "u", + "uint8": "u", + "uint16": "u", + "uint32": "u", + "uint64": "u", + "float32": "f", + "float64": "f", + "Slice": "s", + "StringSlice": "ss", + }, } - - return a.templateSet } diff --git a/internal/funcs.go b/internal/funcs.go index e20d66e2..5d35e1d3 100644 --- a/internal/funcs.go +++ b/internal/funcs.go @@ -6,8 +6,6 @@ import ( "text/template" "github.com/serenize/snaker" - - "github.com/knq/xo/models" ) // NewTemplateFuncs returns a set of template funcs bound to the supplied args. @@ -107,28 +105,28 @@ func (a *ArgType) inc(i int) int { return i + 1 } -// colnames creates a list of the column names found in columns, excluding any -// FieldName in ignoreFields. +// colnames creates a list of the column names found in fields, excluding any +// Field.Name in ignoreNames. // // Used to present a comma separated list of column names, ie in a sql select, // or update. (ie, field_1, field_2, field_3, ...) -func (a *ArgType) colnames(columns []*models.Column, ignoreFields ...string) string { +func (a *ArgType) colnames(fields []*Field, ignoreNames ...string) string { ignore := map[string]bool{} - for _, n := range ignoreFields { + for _, n := range ignoreNames { ignore[n] = true } str := "" i := 0 - for _, col := range columns { - if ignore[col.Field] { + for _, f := range fields { + if ignore[f.Name] { continue } if i != 0 { str = str + ", " } - str = str + col.ColumnName + str = str + f.Col.ColumnName i++ } @@ -141,72 +139,72 @@ func (a *ArgType) colnames(columns []*models.Column, ignoreFields ...string) str // Used to create a sql query list of column names in a where clause (ie, // field_1 = $1 AND field_2 = $2 AND ... ) or in an update clause (ie, field = // $1, field = $2, ...) -func (a *ArgType) colnamesquery(columns []*models.Column, sep string, ignoreFields ...string) string { +func (a *ArgType) colnamesquery(fields []*Field, sep string, ignoreNames ...string) string { ignore := map[string]bool{} - for _, n := range ignoreFields { + for _, n := range ignoreNames { ignore[n] = true } str := "" i := 0 - for _, col := range columns { - if ignore[col.Field] { + for _, f := range fields { + if ignore[f.Name] { continue } if i != 0 { str = str + sep } - str = str + col.ColumnName + " = " + a.Loader.NthParam(i) + str = str + f.Col.ColumnName + " = " + a.Loader.NthParam(i) i++ } return str } -// colprefixnames creates a list of the column names found in columns with the -// supplied prefix, excluding any FieldName in ignoreFields. +// colprefixnames creates a list of the column names found in fields with the +// supplied prefix, excluding any FieldName in ignoreNames. // // Used to present a comma separated list of column names, with a prefix, ie in // a sql select, or update. (ie, t.field_1, t.field_2, t.field_3, ...) -func (a *ArgType) colprefixnames(columns []*models.Column, prefix string, ignoreFields ...string) string { +func (a *ArgType) colprefixnames(fields []*Field, prefix string, ignoreNames ...string) string { ignore := map[string]bool{} - for _, n := range ignoreFields { + for _, n := range ignoreNames { ignore[n] = true } str := "" i := 0 - for _, col := range columns { - if ignore[col.Field] { + for _, f := range fields { + if ignore[f.Name] { continue } if i != 0 { str = str + ", " } - str = str + prefix + "." + col.ColumnName + str = str + prefix + "." + f.Col.ColumnName i++ } return str } -// colvals creates a list of value place holders for the columns found in -// columns, excluding any FieldName in ignoreFields. +// colvals creates a list of value place holders for the fields found in +// fields, excluding any FieldName in ignoreNames. // // Used to present a comma separated list of column names, ie in a sql select, // or update. (ie, $1, $2, $3 ...) -func (a *ArgType) colvals(columns []*models.Column, ignoreFields ...string) string { +func (a *ArgType) colvals(fields []*Field, ignoreNames ...string) string { ignore := map[string]bool{} - for _, n := range ignoreFields { + for _, n := range ignoreNames { ignore[n] = true } str := "" i := 0 - for _, col := range columns { - if ignore[col.Field] { + for _, f := range fields { + if ignore[f.Name] { continue } @@ -221,48 +219,48 @@ func (a *ArgType) colvals(columns []*models.Column, ignoreFields ...string) stri } // fieldnames creates a list of field names from the field names of the -// provided columns, adding the prefix provided, and excluding any field name -// in ignoreFields. +// provided fields, adding the prefix provided, and excluding any field name +// in ignoreNames. // // Used to present a comma separated list of field names, ie in a Go statement // (ie, t.Field1, t.Field2, t.Field3 ...) -func (a *ArgType) fieldnames(columns []*models.Column, prefix string, ignoreFields ...string) string { +func (a *ArgType) fieldnames(fields []*Field, prefix string, ignoreNames ...string) string { ignore := map[string]bool{} - for _, n := range ignoreFields { + for _, n := range ignoreNames { ignore[n] = true } str := "" i := 0 - for _, col := range columns { - if ignore[col.Field] { + for _, f := range fields { + if ignore[f.Name] { continue } if i != 0 { str = str + ", " } - str = str + prefix + "." + col.Field + str = str + prefix + "." + f.Name i++ } return str } -// count returns the 1-based count of columns, excluding any field name in -// ignoreFields. +// count returns the 1-based count of fields, excluding any field name in +// ignoreNames. // -// Used to get the count of columns, and useful for specifying the last sql +// Used to get the count of fields, and useful for specifying the last sql // parameter. -func (a *ArgType) colcount(columns []*models.Column, ignoreFields ...string) int { +func (a *ArgType) colcount(fields []*Field, ignoreNames ...string) int { ignore := map[string]bool{} - for _, n := range ignoreFields { + for _, n := range ignoreNames { ignore[n] = true } i := 1 - for _, col := range columns { - if ignore[col.Field] { + for _, f := range fields { + if ignore[f.Name] { continue } @@ -272,28 +270,29 @@ func (a *ArgType) colcount(columns []*models.Column, ignoreFields ...string) int } // goparamlist converts a list of fields into their named Go parameters, -// skipping any field names in ignoreFields. -func (a *ArgType) goparamlist(columns []*models.Column, addType bool, ignoreFields ...string) string { +// skipping any field names in ignoreNames. +func (a *ArgType) goparamlist(fields []*Field, addType bool, ignoreNames ...string) string { ignore := map[string]bool{} - for _, n := range ignoreFields { + for _, n := range ignoreNames { ignore[n] = true } str := "" i := 0 - for _, col := range columns { - if ignore[col.Field] { + for _, f := range fields { + if ignore[f.Name] { continue } s := "v" + strconv.Itoa(i) - if len(col.Field) > 0 { - s = strings.ToLower(col.Field[:1]) + col.Field[1:] + if len(f.Name) > 0 { + n := strings.Split(snaker.CamelToSnake(f.Name), "_") + s = strings.ToLower(n[0]) + f.Name[len(n[0]):] } str = str + ", " + s if addType { - str = str + " " + a.retype(col.Type) + str = str + " " + a.retype(f.Type) } i++ diff --git a/internal/loader.go b/internal/loader.go new file mode 100644 index 00000000..05aadd87 --- /dev/null +++ b/internal/loader.go @@ -0,0 +1,776 @@ +package internal + +import ( + "errors" + "fmt" + "net/url" + "strings" + + "github.com/gedex/inflector" + "github.com/serenize/snaker" + + "github.com/knq/xo/models" +) + +// Loader is the common interface for database drivers that can generate code +// from a database schema. +type Loader interface { + // IsSupported processes the passed url.URL, returning the sql.Open + // driverName and dataSourceName and whether or not the Loader supports the + // scheme in the url. + IsSupported(*url.URL) (string, bool) + + // NthParam returns the 0-based Nth param for the Loader. + NthParam(i int) string + + // Mask returns the mask. + Mask() string + + // Relkind returns the schema's relkind identifier (ie, TABLE, VIEW, BASE TABLE, etc). + Relkind(RelType) string + + // SchemaName loads the active schema name from the database if not provided on the cli. + SchemaName(*ArgType) (string, error) + + // ParseQuery parses the ArgType.Query and writes any necessary type(s) to + // the ArgType from the opened database handle. + ParseQuery(*ArgType) error + + // LoadSchema loads the ArgType.Schema from the opened database handle, + // writing any generated types to ArgType. + LoadSchema(*ArgType) error +} + +// SchemaLoaders are the available schema loaders. +var SchemaLoaders = map[string]Loader{} + +// TypeLoader provides a common Loader implementation used by the built in +// schema/query loaders. +type TypeLoader struct { + Schemes []string + ProcessDSN func(*url.URL, string) string + ParamN func(int) string + MaskFunc func() string + ProcessRelkind func(RelType) string + Schema func(*ArgType) (string, error) + ParseType func(*ArgType, string, bool) (int, string, string) + EnumList func(models.XODB, string) ([]*models.Enum, error) + EnumValueList func(models.XODB, string, string) ([]*models.EnumValue, error) + ProcList func(models.XODB, string) ([]*models.Proc, error) + ProcParamList func(models.XODB, string, string) ([]*models.ProcParam, error) + TableList func(models.XODB, string, string) ([]*models.Table, error) + ColumnList func(models.XODB, string, string) ([]*models.Column, error) + ForeignKeyList func(models.XODB, string, string) ([]*models.ForeignKey, error) + IndexList func(models.XODB, string, string) ([]*models.Index, error) + IndexColumnList func(models.XODB, string, string, string) ([]*models.IndexColumn, error) + QueryStrip func([]string, []string) + QueryColumnList func(*ArgType, []string) ([]*models.Column, error) +} + +// IsSupported processes the passed url.URL, returning the sql.Open driverName +// and dataSourceName and whether or not the Loader supports the scheme in the +// url. +func (tl TypeLoader) IsSupported(u *url.URL) (string, bool) { + uscheme := strings.ToLower(u.Scheme) + protocol := "tcp" + + // check if +unix or whatever is in the scheme + if strings.Contains(uscheme, "+") { + p := strings.SplitN(uscheme, "+", 2) + uscheme = p[0] + protocol = p[1] + } + + // determine if this type loader works for the url scheme + var found bool + for _, s := range tl.Schemes { + if uscheme == strings.ToLower(s) { + found = true + break + } + } + + // bail if not found + if !found { + return "", false + } + + // fix scheme + u.Scheme = tl.Schemes[0] + + // process dsn if func is non-nil + if tl.ProcessDSN != nil { + return tl.ProcessDSN(u, protocol), true + } + + return u.String(), true +} + +// NthParam satisifies Loader's NthParam. +func (tl TypeLoader) NthParam(i int) string { + if tl.ParamN != nil { + return tl.ParamN(i) + } + + return fmt.Sprintf("$%d", i+1) +} + +// Mask returns the parameter mask +func (tl TypeLoader) Mask() string { + if tl.MaskFunc != nil { + return tl.MaskFunc() + } + + return "$%d" +} + +// Relkind satisfies Loader's Relkind. +func (tl TypeLoader) Relkind(rt RelType) string { + if tl.ProcessRelkind != nil { + return tl.ProcessRelkind(rt) + } + + return rt.String() +} + +// SchemaName returns the active schema name. +func (tl TypeLoader) SchemaName(args *ArgType) (string, error) { + if tl.Schema != nil { + return tl.Schema(args) + } + + return "", nil +} + +// ParseQuery satisfies Loader's ParseQuery. +func (tl TypeLoader) ParseQuery(args *ArgType) error { + var err error + + // parse supplied query + queryStr, params := args.ParseQuery(tl.Mask(), true) + inspectStr, _ := args.ParseQuery("NULL", false) + + // split up query and inspect based on lines + query := strings.Split(queryStr, "\n") + inspect := strings.Split(inspectStr, "\n") + + // query comment placeholder + queryComments := make([]string, len(query)+1) + + // trim whitespace if applicable + if args.QueryTrim { + for n, l := range query { + query[n] = strings.TrimSpace(l) + if n < len(query)-1 { + query[n] = query[n] + " " + } + } + + for n, l := range inspect { + inspect[n] = strings.TrimSpace(l) + if n < len(inspect)-1 { + inspect[n] = inspect[n] + " " + } + } + } + + // query strip + if args.QueryStrip && tl.QueryStrip != nil { + tl.QueryStrip(query, queryComments) + } + + // create template for query type + typeTpl := &Type{ + Name: args.QueryType, + RelType: Table, + Fields: []*Field{}, + Table: &models.Table{ + TableName: "[custom " + strings.ToLower(snaker.CamelToSnake(args.QueryType)) + "]", + }, + Comment: args.QueryTypeComment, + } + + if args.QueryFields == "" { + // if no query fields specified, then pass to inspector + colList, err := tl.QueryColumnList(args, inspect) + if err != nil { + return err + } + + // process columns + for _, c := range colList { + f := &Field{ + Name: SnakeToCamel(strings.ToLower(c.ColumnName)), + Col: c, + } + f.Len, f.NilType, f.Type = tl.ParseType(args, c.DataType, false) + typeTpl.Fields = append(typeTpl.Fields, f) + } + } else { + // extract fields from query fields + for _, qf := range strings.Split(args.QueryFields, ",") { + qf = strings.TrimSpace(qf) + colName := qf + colType := "string" + + i := strings.Index(qf, " ") + if i != -1 { + colName = qf[:i] + colType = qf[i+1:] + } + + typeTpl.Fields = append(typeTpl.Fields, &Field{ + Name: colName, + Type: colType, + Col: &models.Column{ + ColumnName: snaker.CamelToSnake(colName), + }, + }) + } + } + + // generate query type template + err = args.ExecuteTemplate(QueryTypeTemplate, args.QueryType, "", typeTpl) + if err != nil { + return err + } + + // build func name + funcName := args.QueryFunc + if funcName == "" { + // no func name specified, so generate based on type + if args.QueryOnlyOne { + funcName = args.QueryType + } else { + funcName = inflector.Pluralize(args.QueryType) + } + + // affix any params + if len(params) == 0 { + funcName = "Get" + funcName + } else { + funcName = funcName + "By" + for _, p := range params { + funcName = funcName + strings.ToUpper(p.Name[:1]) + p.Name[1:] + } + } + } + + // create func template + queryTpl := &Query{ + Name: funcName, + Query: query, + QueryComments: queryComments, + QueryParams: params, + OnlyOne: args.QueryOnlyOne, + Interpolate: args.QueryInterpolate, + Type: typeTpl, + Comment: args.QueryFuncComment, + } + + // generate template + err = args.ExecuteTemplate(QueryTemplate, args.QueryType, "", queryTpl) + if err != nil { + return err + } + + return nil +} + +// LoadSchema loads schema definitions. +func (tl TypeLoader) LoadSchema(args *ArgType) error { + var err error + + // load enums + _, err = tl.LoadEnums(args) + if err != nil { + return err + } + + // load procs + _, err = tl.LoadProcs(args) + if err != nil { + return err + } + + // load tables + tableMap, err := tl.LoadRelkind(args, Table) + if err != nil { + return err + } + + // load foreign keys + _, err = tl.LoadForeignKeys(args, tableMap) + if err != nil { + return err + } + + // load indexes + _, err = tl.LoadIndexes(args, tableMap) + if err != nil { + return err + } + + return nil +} + +// LoadEnums loads schema enums. +func (tl TypeLoader) LoadEnums(args *ArgType) (map[string]*Enum, error) { + var err error + + // not supplied, so bail + if tl.EnumList == nil { + return nil, nil + } + + // load enums + enumList, err := tl.EnumList(args.DB, args.Schema) + if err != nil { + return nil, err + } + + // process enums + enumMap := map[string]*Enum{} + for _, e := range enumList { + enumTpl := &Enum{ + Name: inflector.Singularize(SnakeToCamel(e.EnumName)), + Schema: args.Schema, + Values: []*EnumValue{}, + Enum: e, + } + + err = tl.LoadEnumValues(args, enumTpl) + if err != nil { + return nil, err + } + + enumMap[enumTpl.Name] = enumTpl + args.KnownTypeMap[enumTpl.Name] = true + } + + // generate enum templates + for _, e := range enumMap { + err = args.ExecuteTemplate(EnumTemplate, e.Name, "", e) + if err != nil { + return nil, err + } + } + + return enumMap, nil +} + +// LoadEnumValues loads schema enum values. +func (tl TypeLoader) LoadEnumValues(args *ArgType, enumTpl *Enum) error { + var err error + + // load enum values + enumValues, err := tl.EnumValueList(args.DB, args.Schema, enumTpl.Enum.EnumName) + if err != nil { + return err + } + + // process enum values + for _, ev := range enumValues { + // chop off redundant enum name if applicable + name := SnakeToCamel(strings.ToLower(ev.EnumValue)) + if strings.HasSuffix(strings.ToLower(name), strings.ToLower(enumTpl.Name)) { + n := name[:len(name)-len(enumTpl.Name)] + if len(n) > 0 { + name = n + } + } + + enumTpl.Values = append(enumTpl.Values, &EnumValue{ + Name: name, + Val: ev, + }) + } + + return nil +} + +// LoadProcs loads schema stored procedures definitions. +func (tl TypeLoader) LoadProcs(args *ArgType) (map[string]*Proc, error) { + var err error + + // not supplied, so bail + if tl.ProcList == nil { + return nil, nil + } + + // load procs + procList, err := tl.ProcList(args.DB, args.Schema) + if err != nil { + return nil, err + } + + // process procs + procMap := map[string]*Proc{} + for _, p := range procList { + // fix the name if it starts with one or more underscores + name := p.ProcName + for strings.HasPrefix(name, "_") { + name = name[1:] + } + + // create template + procTpl := &Proc{ + Name: SnakeToCamel(strings.ToLower(name)), + Schema: args.Schema, + Params: []*Field{}, + Return: &Field{}, + Proc: p, + } + + // parse return type into template + _, procTpl.Return.NilType, procTpl.Return.Type = tl.ParseType(args, p.ReturnType, false) + + // load proc parameters + err = tl.LoadProcParams(args, procTpl) + if err != nil { + return nil, err + } + + procMap[p.ProcName] = procTpl + } + + // generate proc templates + for _, p := range procMap { + err = args.ExecuteTemplate(ProcTemplate, "sp_"+p.Name, "", p) + if err != nil { + return nil, err + } + } + + return procMap, nil +} + +// LoadProcParams loads schema stored procedure parameters. +func (tl TypeLoader) LoadProcParams(args *ArgType, procTpl *Proc) error { + var err error + + // load proc params + paramList, err := tl.ProcParamList(args.DB, args.Schema, procTpl.Proc.ProcName) + if err != nil { + return err + } + + // process params + for i, p := range paramList { + // TODO: some databases support named parameters in procs (MySQL) + paramTpl := &Field{ + Name: fmt.Sprintf("v%d", i), + } + _, _, paramTpl.Type = tl.ParseType(args, strings.TrimSpace(p.ParamType), false) + + // add to proc params + if procTpl.ProcParams != "" { + procTpl.ProcParams = procTpl.ProcParams + ", " + } + procTpl.ProcParams = procTpl.ProcParams + p.ParamType + + procTpl.Params = append(procTpl.Params, paramTpl) + } + + return nil +} + +// LoadRelkind loads a schema table/view definition. +func (tl TypeLoader) LoadRelkind(args *ArgType, relType RelType) (map[string]*Type, error) { + var err error + + // load tables + tableList, err := tl.TableList(args.DB, args.Schema, tl.Relkind(relType)) + if err != nil { + return nil, err + } + + // tables + tableMap := make(map[string]*Type) + for _, ti := range tableList { + // create template + typeTpl := &Type{ + Name: inflector.Singularize(SnakeToCamel(strings.ToLower(ti.TableName))), + Schema: args.Schema, + RelType: relType, + Fields: []*Field{}, + Table: ti, + } + + // process columns + err = tl.LoadColumns(args, typeTpl) + if err != nil { + return nil, err + } + + tableMap[ti.TableName] = typeTpl + } + + // generate table templates + for _, t := range tableMap { + err = args.ExecuteTemplate(TypeTemplate, t.Name, "", t) + if err != nil { + return nil, err + } + } + + return tableMap, nil +} + +// LoadColumns loads schema table/view columns. +func (tl TypeLoader) LoadColumns(args *ArgType, typeTpl *Type) error { + var err error + + // load columns + columnList, err := tl.ColumnList(args.DB, args.Schema, typeTpl.Table.TableName) + if err != nil { + return err + } + + // process columns + for _, c := range columnList { + // set col info + f := &Field{ + Name: SnakeToCamel(strings.ToLower(c.ColumnName)), + Col: c, + } + f.Len, f.NilType, f.Type = tl.ParseType(args, c.DataType, !c.NotNull) + + // set primary key + if c.IsPrimaryKey { + typeTpl.PrimaryKey = f + } + + // append col to template fields + typeTpl.Fields = append(typeTpl.Fields, f) + } + + return nil +} + +// LoadForeignKeys loads foreign keys. +func (tl TypeLoader) LoadForeignKeys(args *ArgType, tableMap map[string]*Type) (map[string]*ForeignKey, error) { + var err error + + fkMap := map[string]*ForeignKey{} + for _, t := range tableMap { + // load keys per table + err = tl.LoadTableForeignKeys(args, tableMap, t, fkMap) + if err != nil { + return nil, err + } + } + + // generate templates + for _, fk := range fkMap { + err = args.ExecuteTemplate(ForeignKeyTemplate, fk.Type.Name, fk.ForeignKey.ForeignKeyName, fk) + if err != nil { + return nil, err + } + } + + return fkMap, nil +} + +// LoadTableForeignKeys loads schema foreign key definitions per table. +func (tl TypeLoader) LoadTableForeignKeys(args *ArgType, tableMap map[string]*Type, typeTpl *Type, fkMap map[string]*ForeignKey) error { + var err error + + // load foreign keys + foreignKeyList, err := tl.ForeignKeyList(args.DB, args.Schema, typeTpl.Table.TableName) + if err != nil { + return err + } + + // loop over foreign keys for table + for _, fk := range foreignKeyList { + var refTpl *Type + var col, refCol *Field + + colLoop: + // find column + for _, f := range typeTpl.Fields { + if f.Col.ColumnName == fk.ColumnName { + col = f + break colLoop + } + } + + refTplLoop: + // find ref table + for _, t := range tableMap { + if t.Table.TableName == fk.RefTableName { + refTpl = t + break refTplLoop + } + } + + refColLoop: + // find ref column + for _, f := range refTpl.Fields { + if f.Col.ColumnName == fk.RefColumnName { + refCol = f + break refColLoop + } + } + + // check everything was found + if col == nil || refTpl == nil || refCol == nil { + return errors.New("could not find col, refTpl, or refCol") + } + + // foreign key name + if fk.ForeignKeyName == "" { + fk.ForeignKeyName = typeTpl.Table.TableName + "_" + col.Col.ColumnName + "_fkey" + } + + // create foreign key template + fkMap[fk.ForeignKeyName] = &ForeignKey{ + Schema: args.Schema, + Type: typeTpl, + Field: col, + RefType: refTpl, + RefField: refCol, + ForeignKey: fk, + } + } + + return nil +} + +// LoadIndexes loads schema index definitions. +func (tl TypeLoader) LoadIndexes(args *ArgType, tableMap map[string]*Type) (map[string]*Index, error) { + var err error + + ixMap := map[string]*Index{} + for _, t := range tableMap { + // load table indexes + err = tl.LoadTableIndexes(args, t, ixMap) + if err != nil { + return nil, err + } + } + + // generate templates + for _, ix := range ixMap { + err = args.ExecuteTemplate(IndexTemplate, ix.Type.Name, ix.Index.IndexName, ix) + if err != nil { + return nil, err + } + } + + return ixMap, nil +} + +// LoadTableIndexes loads schema index definitions per table. +func (tl TypeLoader) LoadTableIndexes(args *ArgType, typeTpl *Type, ixMap map[string]*Index) error { + var err error + var priIxLoaded bool + + // load indexes + indexList, err := tl.IndexList(args.DB, args.Schema, typeTpl.Table.TableName) + if err != nil { + return err + } + + // process indexes + for _, ix := range indexList { + ixName := ix.IndexName + + // save that the primary key index was loaded + priIxLoaded = priIxLoaded || ix.IsPrimary + + // chop off tablename_ + if strings.HasPrefix(ixName, typeTpl.Table.TableName+"_") { + ixName = ixName[len(typeTpl.Table.TableName)+1:] + } + + // chop off _ix, _idx, or _index + switch { + case strings.HasSuffix(ixName, "_ix"): + ixName = ixName[:len(ixName)-len("_ix")] + case strings.HasSuffix(ixName, "_idx"): + ixName = ixName[:len(ixName)-len("_idx")] + case strings.HasSuffix(ixName, "_index"): + ixName = ixName[:len(ixName)-len("_index")] + } + + // determine the type name + typeName := typeTpl.Name + if !ix.IsUnique { + typeName = inflector.Pluralize(typeTpl.Name) + } + + // create index template + ixTpl := &Index{ + Name: SnakeToCamel(strings.ToLower(ixName)), + TypeName: typeName, + Schema: args.Schema, + Type: typeTpl, + Fields: []*Field{}, + Index: ix, + } + + // load index columns + err = tl.LoadIndexColumns(args, ixTpl) + if err != nil { + return err + } + + ixMap[ix.IndexName] = ixTpl + } + + // if no primary key index loaded, but a primary key column was defined in + // the type, then create the definition here. this is needed for sqlite, as + // sqlite doesn't define primary keys in its index list + if !priIxLoaded && typeTpl.PrimaryKey != nil { + ixName := typeTpl.Table.TableName + "_" + typeTpl.PrimaryKey.Col.ColumnName + "_pkey" + ixMap[ixName] = &Index{ + Name: SnakeToCamel(strings.ToLower(ixName)), + TypeName: typeTpl.Name, + Schema: args.Schema, + Type: typeTpl, + Fields: []*Field{typeTpl.PrimaryKey}, + Index: &models.Index{ + IndexName: ixName, + IsUnique: true, + IsPrimary: true, + }, + } + } + + return nil +} + +// LoadIndexColumns loads the index column information. +func (tl TypeLoader) LoadIndexColumns(args *ArgType, ixTpl *Index) error { + var err error + + // load index columns + indexCols, err := tl.IndexColumnList(args.DB, args.Schema, ixTpl.Type.Table.TableName, ixTpl.Index.IndexName) + if err != nil { + return err + } + + // process index columns + for _, ic := range indexCols { + var field *Field + + fieldLoop: + // find field + for _, f := range ixTpl.Type.Fields { + if f.Col.ColumnName == ic.ColumnName { + field = f + break fieldLoop + } + } + + if field == nil { + continue + } + + ixTpl.Fields = append(ixTpl.Fields, field) + } + + return nil +} diff --git a/internal/loaders.go b/internal/loaders.go deleted file mode 100644 index 41448104..00000000 --- a/internal/loaders.go +++ /dev/null @@ -1,85 +0,0 @@ -package internal - -import ( - "database/sql" - "net/url" - "strconv" - "strings" -) - -// Loader is the common interface for database drivers that can generate code -// from a database schema. -type Loader interface { - IsSupported(*url.URL) (string, bool) - ParseQuery(*ArgType, *sql.DB) error - LoadSchemaTypes(*ArgType, *sql.DB) error - NthParam(i int) string -} - -// TypeLoader provides a Loader. -type TypeLoader struct { - Schemes []string - ProcessDSN func(*url.URL, string) string - QueryFunc func(*ArgType, *sql.DB) error - LoadSchemaFunc func(*ArgType, *sql.DB) error - ParamN func(int) string -} - -// IsSupported returns whether or not the url is supported, and modifies the -// URL to have the correct scheme and returns a correct DSN string for -// sql.Open. -func (tl TypeLoader) IsSupported(u *url.URL) (string, bool) { - uscheme := strings.ToLower(u.Scheme) - protocol := "tcp" - - // check if +unix or whatever is in the scheme - if strings.Contains(uscheme, "+") { - p := strings.SplitN(uscheme, "+", 2) - uscheme = p[0] - protocol = p[1] - } - - var found bool - for _, s := range tl.Schemes { - if uscheme == strings.ToLower(s) { - found = true - break - } - } - - if !found { - return "", false - } - - // fix scheme - u.Scheme = tl.Schemes[0] - - // process dsn if func is non-nil - if tl.ProcessDSN != nil { - return tl.ProcessDSN(u, protocol), true - } - - return u.String(), true -} - -// ParseQuery satisfies Loader's ParseQuery. -func (tl TypeLoader) ParseQuery(args *ArgType, db *sql.DB) error { - return tl.QueryFunc(args, db) -} - -// LoadSchemaTypes satisfies Loader's LoadSchemaTypes. -func (tl TypeLoader) LoadSchemaTypes(args *ArgType, db *sql.DB) error { - return tl.LoadSchemaFunc(args, db) -} - -// NthParam satisifies Loader's NthParam. -func (tl TypeLoader) NthParam(i int) string { - if tl.ParamN != nil { - return tl.ParamN(i) - } - - return "$" + strconv.Itoa(i+1) -} - -// SchemaLoaders are the available schema loaders. -var SchemaLoaders = map[string]Loader{} diff --git a/internal/templates.go b/internal/templates.go index c7eafc69..f3983abf 100644 --- a/internal/templates.go +++ b/internal/templates.go @@ -1,16 +1,42 @@ -// Package templates contains the various Go code templates used by xo. package internal import ( "bytes" - "errors" + "fmt" "io" + "io/ioutil" + "path" "text/template" + + "github.com/knq/xo/templates" ) +// TemplateLoader loads templates from the specified name. +func (a *ArgType) TemplateLoader(name string) ([]byte, error) { + // no template path specified + if a.TemplatePath == "" { + return templates.Asset(name) + } + + return ioutil.ReadFile(path.Join(a.TemplatePath, name)) +} + +// TemplateSet retrieves the created template set. +func (a *ArgType) TemplateSet() *TemplateSet { + if a.templateSet == nil { + a.templateSet = &TemplateSet{ + funcs: a.NewTemplateFuncs(), + l: a.TemplateLoader, + tpls: map[string]*template.Template{}, + } + } + + return a.templateSet +} + // ExecuteTemplate loads and parses the supplied template with name and // executes it with obj as the context. -func (a *ArgType) ExecuteTemplate(tt TemplateType, name string, obj interface{}) error { +func (a *ArgType) ExecuteTemplate(tt TemplateType, name string, sub string, obj interface{}) error { var err error // setup generated @@ -20,23 +46,18 @@ func (a *ArgType) ExecuteTemplate(tt TemplateType, name string, obj interface{}) // create store v := TBuf{ - Type: tt, - Name: name, - Buf: new(bytes.Buffer), + TemplateType: tt, + Name: name, + Subname: sub, + Buf: new(bytes.Buffer), } // build template name - templateName := "" - if tt != XO { - // grab tl - tl, ok := a.Loader.(TypeLoader) - if !ok { - return errors.New("internal error") - } - - templateName = tl.Schemes[0] + "." + loaderType := "" + if tt != XOTemplate { + loaderType = a.LoaderType + "." } - templateName = templateName + tt.String() + ".go.tpl" + templateName := fmt.Sprintf("%s%s.go.tpl", loaderType, tt) // execute template err = a.TemplateSet().Execute(v.Buf, templateName, obj) diff --git a/internal/types.go b/internal/types.go index 4bb6ce9d..5afa429e 100644 --- a/internal/types.go +++ b/internal/types.go @@ -7,112 +7,156 @@ type TemplateType uint // the order here will be the alter the output order per file. const ( - XO TemplateType = iota - Enum - Model - Proc - Index - ForeignKey - QueryModel - Query + EnumTemplate TemplateType = iota + ProcTemplate + TypeTemplate + ForeignKeyTemplate + IndexTemplate + QueryTypeTemplate + QueryTemplate + + // always last + XOTemplate ) // String returns the name for the associated template type. func (tt TemplateType) String() string { var s string switch tt { - case XO: + case XOTemplate: s = "xo_db" - case Enum: + case EnumTemplate: s = "enum" - case Proc: + case ProcTemplate: s = "proc" - case Model: - s = "model" - case Index: - s = "idx" - case ForeignKey: - s = "fkey" - case QueryModel: - s = "model" - case Query: + case TypeTemplate: + s = "type" + case ForeignKeyTemplate: + s = "foreignkey" + case IndexTemplate: + s = "index" + case QueryTypeTemplate: + s = "querytype" + case QueryTemplate: s = "query" - default: panic("unknown TemplateType") } return s } -// EnumTemplate is a template item for a enum. -type EnumTemplate struct { - Type string - EnumType string - Comment string - Values []*models.Enum +// RelType represents the different types of relational storage (table/view). +type RelType uint + +const ( + // Table reltype + Table RelType = iota + + // View reltype + View +) + +// String provides the string representation of RelType. +func (rt RelType) String() string { + var s string + switch rt { + case Table: + s = "TABLE" + case View: + s = "VIEW" + default: + panic("unknown RelType") + } + return s } -// TableTemplate is a template item for a table. -type TableTemplate struct { - Type string - TableSchema string - TableName string - PrimaryKey string - PrimaryKeyField string - PrimaryKeyType string - Comment string - Fields []*models.Column +// EnumValue holds data for a single enum value. +type EnumValue struct { + Name string + Val *models.EnumValue + Comment string } -// ProcTemplate is a template item for a stored procedure. -type ProcTemplate struct { - Name string - ReturnType string - NilReturnType string - TableSchema string - ProcName string - ProcParameterNames string - ProcParameterTypes string - ProcReturnType string - Comment string - Parameters []*models.Column +// Enum is a template item for a enum. +type Enum struct { + Name string + Schema string + Values []*EnumValue + Enum *models.Enum + Comment string } -// ForeignKeyTemplate is a template item for a foreign relationship on a table. -type ForeignKeyTemplate struct { - Type string - ForeignKeyName string - ColumnName string - Field string - FieldType string - RefType string - RefField string - RefFieldType string +// Proc is a template item for a stored procedure. +type Proc struct { + Name string + Schema string + ProcParams string + Params []*Field + Return *Field + Proc *models.Proc + Comment string } -// IndexTemplate is a template item for a index into a table. -type IndexTemplate struct { - Type string +// Field contains field information. +type Field struct { + Name string + Type string + NilType string + Len int + Col *models.Column + Comment string +} + +// Type is a template item for a type (ie, table/view/custom query). +type Type struct { + Name string + Schema string + RelType RelType + PrimaryKey *Field + Fields []*Field + Table *models.Table + Comment string +} + +// ForeignKey is a template item for a foreign relationship on a table. +type ForeignKey struct { + /*Name string*/ + Schema string + Type *Type + Field *Field + RefType *Type + RefField *Field + ForeignKey *models.ForeignKey + Comment string +} + +// Index is a template item for a index into a table. +type Index struct { + Name string + TypeName string + Schema string + Type *Type + Fields []*Field + Index *models.Index + Comment string +} + +// QueryParam is a query parameter for a custom query. +type QueryParam struct { Name string - TableSchema string - TableName string - IndexName string - IsUnique bool - Plural string - Comment string - Fields []*models.Column - Table *TableTemplate + Type string + Interpolate bool } -// FuncTemplate is a template item for a custom query. -type QueryTemplate struct { +// Query is a template item for a custom query. +type Query struct { + Schema string Name string - Type string Query []string QueryComments []string - Parameters []QueryParameter + QueryParams []*QueryParam OnlyOne bool Interpolate bool + Type *Type Comment string - Table *TableTemplate } diff --git a/internal/util.go b/internal/util.go index 3ac85efe..cb530332 100644 --- a/internal/util.go +++ b/internal/util.go @@ -7,20 +7,15 @@ import ( "regexp" "strings" "time" -) -// QueryParameter is an extracted query parameter from a query. -type QueryParameter struct { - Name string - Type string - Interpolate bool -} + "github.com/serenize/snaker" +) // ParseQuery takes the query in args and looks for strings in the form of // "%% [,