diff --git a/shows.go b/shows.go index 0458630..349edb9 100644 --- a/shows.go +++ b/shows.go @@ -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 +} diff --git a/shows_test.go b/shows_test.go index f6b59b9..fe6816d 100644 --- a/shows_test.go +++ b/shows_test.go @@ -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 = ` Marvel's Jessica Jones @@ -32,6 +29,43 @@ const showNFO1 = ` ` +const showNFO1E1 = ` + + AKA Ladies Night + Marvel's Jessica Jones + 1 + 1 + 5311261 + 2015-11-20 + 2015-11-20 + Jessica Jones is hired to find a pretty NYU student who's vanished, but it turns out to be more than a simple missing persons case. + 60 + http://thetvdb.com/banners/episodes/284190/5311261.jpg + 7.5 + tt2357547 + 284190 + tt4162058 + +` +const showNFO1E2 = ` + + AKA Crush Syndrome + Marvel's Jessica Jones + 1 + 2 + 5311262 + 2015-11-20 + 2015-11-20 + Jessica vows to prove Hope's innocence, even though it means tracking down a terrifying figure from her own past. + 60 + http://thetvdb.com/banners/episodes/284190/5311262.jpg + 7.7 + tt2357547 + 284190 + tt4162062 + +` + 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 { diff --git a/sqly/sqly.go b/sqly/sqly.go new file mode 100644 index 0000000..35eb820 --- /dev/null +++ b/sqly/sqly.go @@ -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) +} diff --git a/users/users.go b/users/users.go index 5b15b0e..4673480 100644 --- a/users/users.go +++ b/users/users.go @@ -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 } diff --git a/users/users_test.go b/users/users_test.go index 18fd064..49b20c4 100644 --- a/users/users_test.go +++ b/users/users_test.go @@ -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"