package justguestbook 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 } // SQLiteDatabase implements a Guestbook that works with a SQLite DB // under the hood. type SQLiteDatabase struct { db *sql.DB } // NewSQLiteDB returns an instance of a SQLite Guestbook implementation. 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) }