Add sqly for handle sql dependency between package

This commit is contained in:
Nicolas Duhamel 2016-02-14 18:59:53 +01:00
parent 9212318c23
commit 51c3ab87d9
5 changed files with 301 additions and 75 deletions

View File

@ -2,38 +2,63 @@ package shows
import (
"fmt"
"time"
"gitlab.quimbo.fr/odwrtw/canape-sql/sqly"
"gitlab.quimbo.fr/odwrtw/canape-sql/users"
"github.com/jmoiron/sqlx"
"github.com/odwrtw/polochon/lib"
)
const showsCreate = `
CREATE TABLE shows (
id SERIAL PRIMARY KEY,
imdbid text NOT NULL UNIQUE,
title text NOT NULL,
updated timestamp DEFAULT current_timestamp,
created timestamp DEFAULT current_timestamp
);
`
var Schema = sqly.Schema{
Require: []sqly.Schema{
users.Schema,
},
Tables: []sqly.SchemaTable{
sqly.SchemaTable{
Name: "shows",
Sql: `
CREATE TABLE shows (
id SERIAL PRIMARY KEY,
imdbid text NOT NULL UNIQUE,
title text NOT NULL,
updated timestamp DEFAULT current_timestamp,
created timestamp DEFAULT current_timestamp
);
`},
sqly.SchemaTable{
Name: "episodes",
Sql: `
CREATE TABLE episodes (
id SERIAL PRIMARY KEY,
shows_id integer REFERENCES shows (id) ON DELETE CASCADE,
title text NOT NULL,
season integer NOT NULL,
episode integer NOT NULL,
updated timestamp DEFAULT current_timestamp,
created timestamp DEFAULT current_timestamp
);
`},
},
Drop: `
DROP TABLE episodes;
DROP TABLE shows;
`,
}
const (
addShowQuery = `INSERT INTO shows (imdbid, title) VALUES (:imdbid, :title) RETURNING id;`
getShowQuery = `SELECT * FROM shows WHERE imdbid=$1;`
deleteShowQuery = `DELETE FROM shows WHERE id=$1;`
addEpisodeQuery = `INSERT INTO episodes (shows_id, title, season, episode) VALUES ($1,$2,$3,$4);`
getEpisodesQuery = `SELECT title, season, episode FROM episodes WHERE shows_id=$1;`
)
// BaseTable have to be embeded in all your struct which reflect a table
type BaseTable struct {
Updated time.Time
Created time.Time
}
type Show struct {
ID int
sqly.BaseTable
polochon.Show
BaseTable
Episodes []*Episode
}
func Get(db *sqlx.DB, imdbID string) (*Show, error) {
@ -55,6 +80,23 @@ func (s *Show) Add(db *sqlx.DB) error {
r.Scan(&id)
}
s.ID = id
// When add a show to database use polochon episode details
// so s.Show.Episodes
for _, pEp := range s.Show.Episodes {
err = s.addEpisode(db, pEp)
if err != nil {
return err
}
}
return nil
}
func (s *Show) addEpisode(db *sqlx.DB, pEpisode *polochon.ShowEpisode) error {
_, err := db.Exec(addEpisodeQuery, s.ID, pEpisode.Title, pEpisode.Season, pEpisode.Episode)
if err != nil {
return err
}
return nil
}
@ -69,3 +111,19 @@ func (s *Show) Delete(db *sqlx.DB) error {
}
return nil
}
func (s *Show) GetEpisodes(db *sqlx.DB) error {
// When retrive episode's info from database populate the s.Episodes member
// and not s.Show.Episodes
err := db.Select(&s.Episodes, getEpisodesQuery, s.ID)
if err != nil {
return err
}
return nil
}
type Episode struct {
sqly.BaseTable
polochon.ShowEpisode
}

View File

@ -1,22 +1,19 @@
package shows
import (
"database/sql"
"fmt"
"os"
"strings"
"testing"
"gitlab.quimbo.fr/odwrtw/canape-sql/sqltest"
"gitlab.quimbo.fr/odwrtw/canape-sql/sqly"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"github.com/odwrtw/polochon/lib"
)
const drop = `
DROP TABLE shows;
`
const showNFO1 = `
<tvshow>
<title>Marvel&#39;s Jessica Jones</title>
@ -32,6 +29,43 @@ const showNFO1 = `
</tvshow>
`
const showNFO1E1 = `
<episodedetails>
<title>AKA Ladies Night</title>
<showtitle>Marvel&#39;s Jessica Jones</showtitle>
<season>1</season>
<episode>1</episode>
<uniqueid>5311261</uniqueid>
<aired>2015-11-20</aired>
<premiered>2015-11-20</premiered>
<plot>Jessica Jones is hired to find a pretty NYU student who&#39;s vanished, but it turns out to be more than a simple missing persons case.</plot>
<runtime>60</runtime>
<thumb>http://thetvdb.com/banners/episodes/284190/5311261.jpg</thumb>
<rating>7.5</rating>
<showimdbid>tt2357547</showimdbid>
<showtvdbid>284190</showtvdbid>
<episodeimdbid>tt4162058</episodeimdbid>
</episodedetails>
`
const showNFO1E2 = `
<episodedetails>
<title>AKA Crush Syndrome </title>
<showtitle>Marvel&#39;s Jessica Jones</showtitle>
<season>1</season>
<episode>2</episode>
<uniqueid>5311262</uniqueid>
<aired>2015-11-20</aired>
<premiered>2015-11-20</premiered>
<plot>Jessica vows to prove Hope&#39;s innocence, even though it means tracking down a terrifying figure from her own past.</plot>
<runtime>60</runtime>
<thumb>http://thetvdb.com/banners/episodes/284190/5311262.jpg</thumb>
<rating>7.7</rating>
<showimdbid>tt2357547</showimdbid>
<showtvdbid>284190</showtvdbid>
<episodeimdbid>tt4162062</episodeimdbid>
</episodedetails>
`
var db *sqlx.DB
func init() {
@ -44,20 +78,34 @@ func init() {
fmt.Printf("Unavailable PG tests:\n %v\n", err)
os.Exit(1)
}
err = sqly.InitDB(db)
if err != nil {
fmt.Printf("Unavailable PG tests:\n %v\n", err)
os.Exit(1)
}
}
func TestAddRemoveShow(t *testing.T) {
sqltest.RunWithSchema(db, showsCreate, drop, t, func(db *sqlx.DB, t *testing.T) {
sqly.RunWithSchema(db, Schema, t, func(db *sqlx.DB, t *testing.T) {
nfo := strings.NewReader(showNFO1)
s := &polochon.Show{}
polochon.ReadNFO(nfo, s)
nfo = strings.NewReader(showNFO1E1)
ep1 := &polochon.ShowEpisode{}
polochon.ReadNFO(nfo, ep1)
nfo = strings.NewReader(showNFO1E2)
ep2 := &polochon.ShowEpisode{}
polochon.ReadNFO(nfo, ep2)
s.Episodes = append(s.Episodes, ep1)
s.Episodes = append(s.Episodes, ep2)
err := polochon.ReadNFO(nfo, s)
if err != nil {
t.Fatal(err)
}
show := Show{Show: *s}
err = show.Add(db)
err := show.Add(db)
if err != nil {
t.Fatal(err)
}
@ -67,6 +115,14 @@ func TestAddRemoveShow(t *testing.T) {
t.Fatal(err)
}
err = show1.GetEpisodes(db)
if err != nil {
t.Fatal(err)
}
if len(show1.Episodes) != 2 {
t.Fatalf("Unexpected number of episodes: %d", len(show1.Episodes))
}
err = show1.Delete(db)
if err != nil {
t.Fatal(err)
@ -76,7 +132,7 @@ func TestAddRemoveShow(t *testing.T) {
if err == nil {
t.Fatal("We expect an error here, the show didn't exist anymore")
}
if err.Error() != "sql: no rows in result set" {
if err != sql.ErrNoRows {
t.Fatalf("Unexpected error: %q", err)
}
if show2 != nil {

115
sqly/sqly.go Normal file
View File

@ -0,0 +1,115 @@
package sqly
import (
"fmt"
"strings"
"testing"
"time"
"github.com/jmoiron/sqlx"
)
const initDBQuery = `
CREATE OR REPLACE FUNCTION update_modified_column()
RETURNS TRIGGER AS $$
BEGIN NEW.updated = now(); RETURN NEW; END; $$ language 'plpgsql';
`
// InitDB add some function to the database and template
func InitDB(db *sqlx.DB) error {
err := MultiExec(db, initDBQuery)
if err != nil {
return err
}
return nil
}
// BaseTable have to be embeded in all your struct which reflect a table
type BaseTable struct {
ID int
Updated time.Time
Created time.Time
}
type SchemaTable struct {
Name string
Sql string
}
type Schema struct {
// Create contains all tables, the key is the name and the
// value the sql
Tables []SchemaTable
// Drop contains the drop sql
Drop string
// Require add schema before create and delete after
Require []Schema
}
func (s Schema) Create(db *sqlx.DB) error {
for _, sch := range s.Require {
err := sch.Create(db)
if err != nil {
return err
}
}
for _, table := range s.Tables {
_, err := db.Exec(table.Sql)
if err != nil {
return fmt.Errorf("%s\n%s", err, table.Sql)
}
trigger := fmt.Sprintf("CREATE TRIGGER update_%s BEFORE UPDATE ON %s FOR EACH ROW EXECUTE PROCEDURE update_modified_column();", table.Name, table.Name)
_, err = db.Exec(trigger)
if err != nil {
return fmt.Errorf("%s\n%s", err, trigger)
}
}
return nil
}
func (s Schema) Delete(db *sqlx.DB) error {
for _, sch := range s.Require {
err := sch.Delete(db)
if err != nil {
return err
}
}
err := MultiExec(db, s.Drop)
if err != nil {
return err
}
return nil
}
func MultiExec(e sqlx.Execer, query string) error {
stmts := strings.Split(query, ";\n")
if len(strings.Trim(stmts[len(stmts)-1], " \n\t\r")) == 0 {
stmts = stmts[:len(stmts)-1]
}
for _, s := range stmts {
_, err := e.Exec(s)
if err != nil {
return fmt.Errorf("%s\n%s", err, s)
}
}
return nil
}
func RunWithSchema(db *sqlx.DB, schema Schema, t *testing.T, test func(db *sqlx.DB, t *testing.T)) {
defer func() {
err := schema.Delete(db)
if err != nil {
t.Fatalf("%s", err.Error())
}
}()
err := schema.Create(db)
if err != nil {
t.Fatalf("%s", err.Error())
}
test(db, t)
}

View File

@ -2,35 +2,41 @@ package users
import (
"fmt"
"time"
"github.com/jmoiron/sqlx"
"gitlab.quimbo.fr/odwrtw/canape-sql/random"
"gitlab.quimbo.fr/odwrtw/canape-sql/sqly"
)
const usersCreate = `
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name text NOT NULL UNIQUE,
updated timestamp DEFAULT current_timestamp,
created timestamp DEFAULT current_timestamp
);
var Schema = sqly.Schema{
Tables: []sqly.SchemaTable{
sqly.SchemaTable{
Name: "users",
Sql: `
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name text NOT NULL UNIQUE,
updated timestamp DEFAULT current_timestamp,
created timestamp DEFAULT current_timestamp
);`},
sqly.SchemaTable{
Name: "tokens",
Sql: `
CREATE TABLE tokens (
id SERIAL,
value text NOT NULL UNIQUE,
users_id integer REFERENCES users (id) ON DELETE CASCADE,
updated timestamp DEFAULT current_timestamp,
created timestamp DEFAULT current_timestamp
);
`},
},
Drop: `
DROP TABLE tokens;
DROP TABLE users;
`,
}
CREATE TABLE tokens (
id SERIAL,
value text NOT NULL UNIQUE,
users_id integer REFERENCES users (id) ON DELETE CASCADE,
updated timestamp DEFAULT current_timestamp,
created timestamp DEFAULT current_timestamp
);
CREATE OR REPLACE FUNCTION update_modified_column()
RETURNS TRIGGER AS $$
BEGIN NEW.updated = now(); RETURN NEW; END; $$ language 'plpgsql';
CREATE TRIGGER update_users BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE update_modified_column();
CREATE TRIGGER update_tokens BEFORE UPDATE ON tokens FOR EACH ROW EXECUTE PROCEDURE update_modified_column();
`
const (
addUserQuery = `INSERT INTO users (name) VALUES ($1) RETURNING id;`
getUserQuery = `SELECT * FROM users WHERE name=$1;`
@ -43,23 +49,15 @@ const (
deleteTokenQuery = `DELETE FROM tokens WHERE users_id=$1 AND value=$2;`
)
// BaseTable have to be embeded in all your struct which reflect a table
type BaseTable struct {
Updated time.Time
Created time.Time
}
// User represents an user
type User struct {
BaseTable
ID int
sqly.BaseTable
Name string
}
// Token represents a token
type Token struct {
BaseTable
ID int
sqly.BaseTable
Value string
}

View File

@ -5,19 +5,12 @@ import (
"os"
"testing"
"gitlab.quimbo.fr/odwrtw/canape-sql/sqltest"
"gitlab.quimbo.fr/odwrtw/canape-sql/sqly"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
const drop = `
DROP TABLE tokens;
DROP TABLE users;
DROP FUNCTION update_modified_column();
`
var db *sqlx.DB
func init() {
@ -30,10 +23,16 @@ func init() {
fmt.Printf("Unavailable PG tests:\n %v\n", err)
os.Exit(1)
}
err = sqly.InitDB(db)
if err != nil {
fmt.Printf("Unavailable PG tests:\n %v\n", err)
os.Exit(1)
}
}
func TestUser(t *testing.T) {
sqltest.RunWithSchema(db, usersCreate, drop, t, func(db *sqlx.DB, t *testing.T) {
sqly.RunWithSchema(db, Schema, t, func(db *sqlx.DB, t *testing.T) {
// Add a new user
u := &User{Name: "plop"}
@ -101,7 +100,7 @@ func TestUser(t *testing.T) {
}
func TestTokenAddDelete(t *testing.T) {
sqltest.RunWithSchema(db, usersCreate, drop, t, func(db *sqlx.DB, t *testing.T) {
sqly.RunWithSchema(db, Schema, t, func(db *sqlx.DB, t *testing.T) {
// Add a new user
u := &User{Name: "plop"}
err := u.Add(db)
@ -161,7 +160,7 @@ func TestTokenAddDelete(t *testing.T) {
}
func TestTokenCheck(t *testing.T) {
sqltest.RunWithSchema(db, usersCreate, drop, t, func(db *sqlx.DB, t *testing.T) {
sqly.RunWithSchema(db, Schema, t, func(db *sqlx.DB, t *testing.T) {
u := &User{Name: "plop"}
u.Add(db)
token, err := u.NewToken(db)
@ -187,7 +186,7 @@ func TestTokenCheck(t *testing.T) {
}
func TestAutoUpdateCols(t *testing.T) {
sqltest.RunWithSchema(db, usersCreate, drop, t, func(db *sqlx.DB, t *testing.T) {
sqly.RunWithSchema(db, Schema, t, func(db *sqlx.DB, t *testing.T) {
u := &User{Name: "plop"}
u.Add(db)
u.Name = "toto"