diff --git a/README.md b/README.md index 7792c37..7d74930 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -justguestbook ver. 1.2.1 +justguestbook ver. 1.3.0 ======================== A library implementing simple guestbook with replies. \ No newline at end of file diff --git a/database/sqlite/database.go b/database/sqlite/database.go deleted file mode 100644 index 16b45be..0000000 --- a/database/sqlite/database.go +++ /dev/null @@ -1,305 +0,0 @@ -package sqlite - -import ( - "database/sql" - _ "embed" - "fmt" - "time" - - "git.arav.su/Arav/justguestbook/guestbook" - _ "github.com/mattn/go-sqlite3" - "github.com/pkg/errors" -) - -var ( - //go:embed queries/schema.sql - querySchema string - - //go:embed queries/entryGetAll.sql - queryGetAll string - //go:embed queries/entryCount.sql - queryCount string - - //go:embed queries/entryNew.sql - queryNewEntry string - //go:embed queries/entryUpdate.sql - queryUpdateEntry string - //go:embed queries/entryDelete.sql - queryDeleteEntry string - //go:embed queries/replyNew.sql - queryNewReply string - //go:embed queries/replyUpdate.sql - queryUpdateReply string - //go:embed queries/replyDelete.sql - queryDeleteReply string -) - -var ( - stmtGetAll *sql.Stmt - stmtCount *sql.Stmt - stmtNewEntry *sql.Stmt - stmtUpdateEntry *sql.Stmt - stmtDeleteEntry *sql.Stmt - stmtNewReply *sql.Stmt - stmtUpdateReply *sql.Stmt - stmtDeleteReply *sql.Stmt -) - -func initDBStatements(db *sql.DB) error { - db.Exec("PRAGMA foreign_keys = ON;") - - _, err := db.Exec(querySchema) - if err != nil { - return errors.Wrap(err, "failed to init schema") - } - - stmtGetAll, err = db.Prepare(queryGetAll) - if err != nil { - return errors.Wrap(err, "failed to prepare queryGetAll") - } - - stmtCount, err = db.Prepare(queryCount) - if err != nil { - return errors.Wrap(err, "failed to prepare queryCount") - } - - stmtNewEntry, err = db.Prepare(queryNewEntry) - if err != nil { - return errors.Wrap(err, "failed to prepare queryNewEntry") - } - - stmtUpdateEntry, err = db.Prepare(queryUpdateEntry) - if err != nil { - return errors.Wrap(err, "failed to prepare queryUpdateEntry") - } - - stmtDeleteEntry, err = db.Prepare(queryDeleteEntry) - if err != nil { - return errors.Wrap(err, "failed to prepare queryDeleteEntry") - } - - stmtNewReply, err = db.Prepare(queryNewReply) - if err != nil { - return errors.Wrap(err, "failed to prepare queryNewReply") - } - - stmtUpdateReply, err = db.Prepare(queryUpdateReply) - if err != nil { - return errors.Wrap(err, "failed to prepare queryUpdateReply") - } - - stmtDeleteReply, err = db.Prepare(queryDeleteReply) - if err != nil { - return errors.Wrap(err, "failed to prepare queryDeleteReply") - } - - return nil -} - -type SQLiteDatabase struct { - db *sql.DB -} - -func New(filePath string) (guestbook.Guestbook, error) { - db, err := sql.Open("sqlite3", dsn(filePath)) - if err != nil { - return nil, err - } - - if err := initDBStatements(db); err != nil { - return nil, err - } - - return &SQLiteDatabase{db: db}, nil -} - -func (d *SQLiteDatabase) Entries(page, pageSize int64) (entries []*guestbook.Entry, err error) { - tx, err := d.db.Begin() - if err != nil { - return - } - defer tx.Rollback() - - rows, err := tx.Stmt(stmtGetAll).Query(pageSize, (page-1)*pageSize) - if err != nil { - return - } - defer rows.Close() - - for rows.Next() { - var entry guestbook.Entry - var entry_created int64 - var reply_created int64 - var reply_message string - if err = rows.Scan( - &entry.ID, &entry_created, &entry.Name, - &entry.Message, &entry.Website, &entry.HideWebsite, - &reply_created, &reply_message); err != nil { - return - } - - entry.Created = time.Unix(entry_created, 0) - if err != nil { - return - } - - if reply_message != "" { - if err != nil { - return nil, err - } - - entry.Reply = &guestbook.Reply{ - ID: entry.ID, - // Created: date, - Created: time.Unix(reply_created, 0), - Message: reply_message} - } - - entries = append(entries, &entry) - } - - tx.Commit() - - return -} - -// Count returns how much entries are in an `entry` table. -func (d *SQLiteDatabase) Count() (count int64, err error) { - tx, err := d.db.Begin() - if err != nil { - return -1, err - } - defer tx.Rollback() - - err = tx.Stmt(stmtCount).QueryRow().Scan(&count) - if err != nil { - return -1, err - } - - tx.Commit() - - return count, nil -} - -// NewEntry inserts a passed Entry struct and fills its ID field if successful. -func (d *SQLiteDatabase) NewEntry(entry *guestbook.Entry) error { - tx, err := d.db.Begin() - if err != nil { - return err - } - defer tx.Rollback() - - r, err := tx.Stmt(stmtNewEntry).Exec(entry.Created.UTC().Unix(), entry.Name, entry.Message, - entry.Website, entry.HideWebsite) - if err != nil { - return err - } - - entry.ID, err = r.LastInsertId() - if err != nil { - return err - } - - tx.Commit() - - return nil -} - -func (d *SQLiteDatabase) EditEntry(entry *guestbook.Entry) error { - tx, err := d.db.Begin() - if err != nil { - return err - } - defer tx.Rollback() - - _, err = tx.Stmt(stmtUpdateEntry).Exec(entry.Name, entry.Message, entry.Website, entry.HideWebsite, entry.ID) - if err != nil { - return err - } - tx.Commit() - - return nil -} - -func (d *SQLiteDatabase) DeleteEntry(entryID int64) error { - tx, err := d.db.Begin() - if err != nil { - return err - } - defer tx.Rollback() - - if _, err = tx.Stmt(stmtDeleteEntry).Exec(entryID); err != nil { - return err - } - - tx.Commit() - - return nil -} - -func (d *SQLiteDatabase) NewReply(reply *guestbook.Reply) error { - tx, err := d.db.Begin() - if err != nil { - return err - } - defer tx.Rollback() - - _, err = tx.Stmt(stmtNewReply).Exec(reply.ID, reply.Created.UTC().Unix(), reply.Message) - if err != nil { - return err - } - - tx.Commit() - - return nil -} - -func (d *SQLiteDatabase) EditReply(reply *guestbook.Reply) error { - tx, err := d.db.Begin() - if err != nil { - return err - } - defer tx.Rollback() - - _, err = tx.Stmt(stmtUpdateReply).Exec(reply.Message, reply.ID) - if err != nil { - return err - } - - tx.Commit() - - return nil -} - -func (d *SQLiteDatabase) DeleteReply(entryID int64) error { - tx, err := d.db.Begin() - if err != nil { - return err - } - defer tx.Rollback() - - if _, err = tx.Stmt(stmtDeleteReply).Exec(entryID); err != nil { - return err - } - - tx.Commit() - - return nil -} - -func (d *SQLiteDatabase) Close() error { - stmtCount.Close() - stmtDeleteEntry.Close() - stmtDeleteReply.Close() - stmtGetAll.Close() - stmtNewEntry.Close() - stmtNewReply.Close() - stmtUpdateEntry.Close() - stmtUpdateReply.Close() - - return d.db.Close() -} - -func dsn(filePath string) string { - return fmt.Sprintf("file:%s?_journal=WAL&_mutex=full", filePath) -} diff --git a/database/sqlite/queries/entryNew.sql b/database/sqlite/queries/entryNew.sql deleted file mode 100644 index d2e3f0d..0000000 --- a/database/sqlite/queries/entryNew.sql +++ /dev/null @@ -1,4 +0,0 @@ -INSERT INTO `entry` - (`created`, `name`, `message`, `website`, `hide_website`) -VALUES - (?, ?, ?, ?, ?); \ No newline at end of file diff --git a/database/sqlite/queries/entryUpdate.sql b/database/sqlite/queries/entryUpdate.sql deleted file mode 100644 index dc4bea6..0000000 --- a/database/sqlite/queries/entryUpdate.sql +++ /dev/null @@ -1,7 +0,0 @@ - UPDATE OR REPLACE `entry` - SET - `name` = ?, - `message` = ?, - `website` = ?, - `hide_website` = ? - WHERE `entry_id` = ?; \ No newline at end of file diff --git a/database/sqlite/queries/replyUpdate.sql b/database/sqlite/queries/replyUpdate.sql deleted file mode 100644 index 09706cd..0000000 --- a/database/sqlite/queries/replyUpdate.sql +++ /dev/null @@ -1,4 +0,0 @@ - UPDATE OR REPLACE `reply` - SET - `message` = ? - WHERE `entry_id` = ?; \ No newline at end of file diff --git a/database/sqlite/queries/schema.sql b/database/sqlite/queries/schema.sql deleted file mode 100644 index abad9a7..0000000 --- a/database/sqlite/queries/schema.sql +++ /dev/null @@ -1,22 +0,0 @@ --- SQLite3 -CREATE TABLE IF NOT EXISTS `entry` ( - `entry_id` INTEGER NOT NULL, - `created` INTEGER NOT NULL, - `name` TEXT NOT NULL, - `message` TEXT NOT NULL, - `website` TEXT NOT NULL, - `hide_website` INTEGER NOT NULL DEFAULT TRUE, - PRIMARY KEY (`entry_id`) ); - -CREATE INDEX IF NOT EXISTS `entry_created_idx` - ON `entry` (`created`); - -CREATE TABLE IF NOT EXISTS `reply` ( - `entry_id` INTEGER NOT NULL, - `created` INTEGER NOT NULL, - `message` TEXT NOT NULL, - PRIMARY KEY (`entry_id`), - FOREIGN KEY (`entry_id`) - REFERENCES `entry` (`entry_id`) - ON DELETE CASCADE - ON UPDATE CASCADE ); \ No newline at end of file diff --git a/db_sqlite.go b/db_sqlite.go new file mode 100644 index 0000000..0665dfb --- /dev/null +++ b/db_sqlite.go @@ -0,0 +1,304 @@ +package guestbook + +import ( + "database/sql" + _ "embed" + "fmt" + "time" + + _ "github.com/mattn/go-sqlite3" + "github.com/pkg/errors" +) + +var ( + //go:embed sqlite_queries/schema.sql + sqlQuerySchema string + + //go:embed sqlite_queries/entryGetAll.sql + sqlQueryGetAll string + //go:embed sqlite_queries/entryCount.sql + sqlQueryCount string + + //go:embed sqlite_queries/entryNew.sql + sqlQueryNewEntry string + //go:embed sqlite_queries/entryUpdate.sql + sqlQueryUpdateEntry string + //go:embed sqlite_queries/entryDelete.sql + sqlQueryDeleteEntry string + //go:embed sqlite_queries/replyNew.sql + sqlQueryNewReply string + //go:embed sqlite_queries/replyUpdate.sql + sqlQueryUpdateReply string + //go:embed sqlite_queries/replyDelete.sql + sqlQueryDeleteReply string +) + +var ( + sqlStmtGetAll *sql.Stmt + sqlStmtCount *sql.Stmt + sqlStmtNewEntry *sql.Stmt + sqlStmtUpdateEntry *sql.Stmt + sqlStmtDeleteEntry *sql.Stmt + sqlStmtNewReply *sql.Stmt + sqlStmtUpdateReply *sql.Stmt + sqlStmtDeleteReply *sql.Stmt +) + +func initSQLiteStatements(db *sql.DB) error { + db.Exec("PRAGMA foreign_keys = ON;") + + _, err := db.Exec(sqlQuerySchema) + if err != nil { + return errors.Wrap(err, "failed to init schema") + } + + sqlStmtGetAll, err = db.Prepare(sqlQueryGetAll) + if err != nil { + return errors.Wrap(err, "failed to prepare sqlQueryGetAll") + } + + sqlStmtCount, err = db.Prepare(sqlQueryCount) + if err != nil { + return errors.Wrap(err, "failed to prepare sqlQueryCount") + } + + sqlStmtNewEntry, err = db.Prepare(sqlQueryNewEntry) + if err != nil { + return errors.Wrap(err, "failed to prepare sqlQueryNewEntry") + } + + sqlStmtUpdateEntry, err = db.Prepare(sqlQueryUpdateEntry) + if err != nil { + return errors.Wrap(err, "failed to prepare sqlQueryUpdateEntry") + } + + sqlStmtDeleteEntry, err = db.Prepare(sqlQueryDeleteEntry) + if err != nil { + return errors.Wrap(err, "failed to prepare sqlQueryDeleteEntry") + } + + sqlStmtNewReply, err = db.Prepare(sqlQueryNewReply) + if err != nil { + return errors.Wrap(err, "failed to prepare sqlQueryNewReply") + } + + sqlStmtUpdateReply, err = db.Prepare(sqlQueryUpdateReply) + if err != nil { + return errors.Wrap(err, "failed to prepare sqlQueryUpdateReply") + } + + sqlStmtDeleteReply, err = db.Prepare(sqlQueryDeleteReply) + if err != nil { + return errors.Wrap(err, "failed to prepare sqlQueryDeleteReply") + } + + return nil +} + +type SQLiteDatabase struct { + db *sql.DB +} + +func NewSQLiteDB(filePath string) (Guestbook, error) { + db, err := sql.Open("sqlite3", sqliteDSN(filePath)) + if err != nil { + return nil, err + } + + if err := initSQLiteStatements(db); err != nil { + return nil, err + } + + return &SQLiteDatabase{db: db}, nil +} + +func (d *SQLiteDatabase) Entries(page, pageSize int64) (entries []*Entry, err error) { + tx, err := d.db.Begin() + if err != nil { + return + } + defer tx.Rollback() + + rows, err := tx.Stmt(sqlStmtGetAll).Query(pageSize, (page-1)*pageSize) + if err != nil { + return + } + defer rows.Close() + + for rows.Next() { + var entry Entry + var entry_created int64 + var reply_created int64 + var reply_message string + if err = rows.Scan( + &entry.ID, &entry_created, &entry.Name, + &entry.Message, &entry.Website, &entry.HideWebsite, + &reply_created, &reply_message); err != nil { + return + } + + entry.Created = time.Unix(entry_created, 0) + if err != nil { + return + } + + if reply_message != "" { + if err != nil { + return nil, err + } + + entry.Reply = &Reply{ + ID: entry.ID, + // Created: date, + Created: time.Unix(reply_created, 0), + Message: reply_message} + } + + entries = append(entries, &entry) + } + + tx.Commit() + + return +} + +// Count returns how much entries are in an `entry` table. +func (d *SQLiteDatabase) Count() (count int64, err error) { + tx, err := d.db.Begin() + if err != nil { + return -1, err + } + defer tx.Rollback() + + err = tx.Stmt(sqlStmtCount).QueryRow().Scan(&count) + if err != nil { + return -1, err + } + + tx.Commit() + + return count, nil +} + +// NewEntry inserts a passed Entry struct and fills its ID field if successful. +func (d *SQLiteDatabase) NewEntry(entry *Entry) error { + tx, err := d.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + r, err := tx.Stmt(sqlStmtNewEntry).Exec(entry.Created.UTC().Unix(), entry.Name, entry.Message, + entry.Website, entry.HideWebsite) + if err != nil { + return err + } + + entry.ID, err = r.LastInsertId() + if err != nil { + return err + } + + tx.Commit() + + return nil +} + +func (d *SQLiteDatabase) EditEntry(entry *Entry) error { + tx, err := d.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + _, err = tx.Stmt(sqlStmtUpdateEntry).Exec(entry.Name, entry.Message, entry.Website, entry.HideWebsite, entry.ID) + if err != nil { + return err + } + tx.Commit() + + return nil +} + +func (d *SQLiteDatabase) DeleteEntry(entryID int64) error { + tx, err := d.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + if _, err = tx.Stmt(sqlStmtDeleteEntry).Exec(entryID); err != nil { + return err + } + + tx.Commit() + + return nil +} + +func (d *SQLiteDatabase) NewReply(reply *Reply) error { + tx, err := d.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + _, err = tx.Stmt(sqlStmtNewReply).Exec(reply.ID, reply.Created.UTC().Unix(), reply.Message) + if err != nil { + return err + } + + tx.Commit() + + return nil +} + +func (d *SQLiteDatabase) EditReply(reply *Reply) error { + tx, err := d.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + _, err = tx.Stmt(sqlStmtUpdateReply).Exec(reply.Message, reply.ID) + if err != nil { + return err + } + + tx.Commit() + + return nil +} + +func (d *SQLiteDatabase) DeleteReply(entryID int64) error { + tx, err := d.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + if _, err = tx.Stmt(sqlStmtDeleteReply).Exec(entryID); err != nil { + return err + } + + tx.Commit() + + return nil +} + +func (d *SQLiteDatabase) Close() error { + sqlStmtCount.Close() + sqlStmtDeleteEntry.Close() + sqlStmtDeleteReply.Close() + sqlStmtGetAll.Close() + sqlStmtNewEntry.Close() + sqlStmtNewReply.Close() + sqlStmtUpdateEntry.Close() + sqlStmtUpdateReply.Close() + + return d.db.Close() +} + +func sqliteDSN(filePath string) string { + return fmt.Sprintf("file:%s?_journal=WAL&_mutex=full", filePath) +} diff --git a/database/sqlite/database_test.go b/db_sqlite_test.go similarity index 94% rename from database/sqlite/database_test.go rename to db_sqlite_test.go index 6fe2e39..6855276 100644 --- a/database/sqlite/database_test.go +++ b/db_sqlite_test.go @@ -1,10 +1,9 @@ -package sqlite_test +package guestbook_test import ( "testing" - "git.arav.su/Arav/justguestbook/database/sqlite" - "git.arav.su/Arav/justguestbook/guestbook" + guestbook "git.arav.su/Arav/justguestbook" "github.com/pkg/errors" ) @@ -14,7 +13,7 @@ const ( ) func genTestDB() (db guestbook.Guestbook, err error) { - db, err = sqlite.New(":memory:") + db, err = guestbook.NewSQLiteDB(":memory:") if err != nil { return nil, errors.Wrap(err, "failed to init DB") } diff --git a/guestbook/entry.go b/guestbook.go similarity index 50% rename from guestbook/entry.go rename to guestbook.go index b1df1e9..7545aad 100644 --- a/guestbook/entry.go +++ b/guestbook.go @@ -5,6 +5,25 @@ import ( "time" ) +const DateFormat = "2006-01-02 15:04:05" + +type Reply struct { + ID int64 `json:"-"` + Created time.Time `json:"created,omitempty"` + Message string `json:"message"` +} + +func NewReply(entryID int64, message string) (*Reply, error) { + if message == "" { + return nil, errors.New("empty message field") + } + + return &Reply{ + ID: entryID, + Created: time.Now().UTC(), + Message: message}, nil +} + type Entry struct { ID int64 `json:"entry_id"` Created time.Time `json:"created"` @@ -27,3 +46,15 @@ func NewEntry(name, message, website string, hideWebsite bool) (*Entry, error) { HideWebsite: hideWebsite, Message: message}, nil } + +type Guestbook interface { + Entries(page, pageSize int64) ([]*Entry, error) + Count() (int64, error) + NewEntry(entry *Entry) error + EditEntry(entry *Entry) error + DeleteEntry(entryID int64) error + NewReply(reply *Reply) error + EditReply(reply *Reply) error + DeleteReply(entryID int64) error + Close() error +} diff --git a/guestbook/database.go b/guestbook/database.go deleted file mode 100644 index 92e4110..0000000 --- a/guestbook/database.go +++ /dev/null @@ -1,15 +0,0 @@ -package guestbook - -const DateFormat = "2006-01-02 15:04:05" - -type Guestbook interface { - Entries(page, pageSize int64) ([]*Entry, error) - Count() (int64, error) - NewEntry(entry *Entry) error - EditEntry(entry *Entry) error - DeleteEntry(entryID int64) error - NewReply(reply *Reply) error - EditReply(reply *Reply) error - DeleteReply(entryID int64) error - Close() error -} diff --git a/guestbook/reply.go b/guestbook/reply.go deleted file mode 100644 index e8cce6c..0000000 --- a/guestbook/reply.go +++ /dev/null @@ -1,23 +0,0 @@ -package guestbook - -import ( - "errors" - "time" -) - -type Reply struct { - ID int64 `json:"-"` - Created time.Time `json:"created,omitempty"` - Message string `json:"message"` -} - -func NewReply(entryID int64, message string) (*Reply, error) { - if message == "" { - return nil, errors.New("empty reply field") - } - - return &Reply{ - ID: entryID, - Created: time.Now().UTC(), - Message: message}, nil -} diff --git a/database/sqlite/queries/entryCount.sql b/sqlite_queries/entryCount.sql similarity index 100% rename from database/sqlite/queries/entryCount.sql rename to sqlite_queries/entryCount.sql diff --git a/database/sqlite/queries/entryDelete.sql b/sqlite_queries/entryDelete.sql similarity index 100% rename from database/sqlite/queries/entryDelete.sql rename to sqlite_queries/entryDelete.sql diff --git a/database/sqlite/queries/entryGetAll.sql b/sqlite_queries/entryGetAll.sql similarity index 100% rename from database/sqlite/queries/entryGetAll.sql rename to sqlite_queries/entryGetAll.sql diff --git a/sqlite_queries/entryNew.sql b/sqlite_queries/entryNew.sql new file mode 100644 index 0000000..92edb9c --- /dev/null +++ b/sqlite_queries/entryNew.sql @@ -0,0 +1,4 @@ +INSERT INTO `entry` + (`created`, `name`, `message`, `website`, `hide_website`) +VALUES + (?, ?, ?, ?, ?); \ No newline at end of file diff --git a/sqlite_queries/entryUpdate.sql b/sqlite_queries/entryUpdate.sql new file mode 100644 index 0000000..ef77a2a --- /dev/null +++ b/sqlite_queries/entryUpdate.sql @@ -0,0 +1,7 @@ +UPDATE OR REPLACE `entry` + SET + `name` = ?, + `message` = ?, + `website` = ?, + `hide_website` = ? + WHERE `entry_id` = ?; \ No newline at end of file diff --git a/database/sqlite/queries/replyDelete.sql b/sqlite_queries/replyDelete.sql similarity index 100% rename from database/sqlite/queries/replyDelete.sql rename to sqlite_queries/replyDelete.sql diff --git a/database/sqlite/queries/replyNew.sql b/sqlite_queries/replyNew.sql similarity index 100% rename from database/sqlite/queries/replyNew.sql rename to sqlite_queries/replyNew.sql diff --git a/sqlite_queries/replyUpdate.sql b/sqlite_queries/replyUpdate.sql new file mode 100644 index 0000000..c990a25 --- /dev/null +++ b/sqlite_queries/replyUpdate.sql @@ -0,0 +1,4 @@ +UPDATE OR REPLACE `reply` + SET + `message` = ? + WHERE `entry_id` = ?; \ No newline at end of file diff --git a/sqlite_queries/schema.sql b/sqlite_queries/schema.sql new file mode 100644 index 0000000..607b7f9 --- /dev/null +++ b/sqlite_queries/schema.sql @@ -0,0 +1,22 @@ +-- SQLite3 +CREATE TABLE IF NOT EXISTS `entry` ( + `entry_id` INTEGER NOT NULL, + `created` INTEGER NOT NULL, + `name` TEXT NOT NULL, + `message` TEXT NOT NULL, + `website` TEXT NOT NULL, + `hide_website` INTEGER NOT NULL DEFAULT TRUE, + PRIMARY KEY (`entry_id`) ); + +CREATE INDEX IF NOT EXISTS `entry_created_idx` + ON `entry` (`created`); + +CREATE TABLE IF NOT EXISTS `reply` ( + `entry_id` INTEGER NOT NULL, + `created` INTEGER NOT NULL, + `message` TEXT NOT NULL, + PRIMARY KEY (`entry_id`), + FOREIGN KEY (`entry_id`) + REFERENCES `entry` (`entry_id`) + ON DELETE CASCADE + ON UPDATE CASCADE ); \ No newline at end of file