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) (*SQLiteDatabase, 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 string var reply_created sql.NullString var reply_message sql.NullString if err = rows.Scan( &entry.ID, &entry_created, &entry.Name, &entry.Message, &entry.Website, &reply_created, &reply_message); err != nil { return } entry.Created, err = time.Parse(guestbook.DateFormat, entry_created) if err != nil { return } if reply_message.Valid /* reply_created is also valid if reply is */ { date, err := time.Parse(guestbook.DateFormat, reply_created.String) if err != nil { return nil, err } entry.Reply = &guestbook.Reply{ ID: entry.ID, Created: date, Message: reply_message.String} } 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.Format(guestbook.DateFormat), 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 } // UpdateEntry func (d *SQLiteDatabase) UpdateEntry(entry *guestbook.Entry) error { tx, err := d.db.Begin() if err != nil { return err } defer tx.Rollback() row := tx.Stmt(stmtUpdateEntry).QueryRow(entry.Name, entry.Message, entry.Website, entry.HideWebsite, entry.ID) if row.Err() != nil { return row.Err() } tx.Commit() return nil } func (d *SQLiteDatabase) DeleteEntry(entryID int64) (int64, error) { tx, err := d.db.Begin() if err != nil { return -1, err } defer tx.Rollback() res, err := tx.Stmt(stmtDeleteEntry).Exec(entryID) if err != nil { return -1, err } c, err := res.RowsAffected() if err != nil { return -1, err } tx.Commit() return c, 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.Format(guestbook.DateFormat), reply.Message) if err != nil { return err } tx.Commit() return nil } func (d *SQLiteDatabase) UpdateReply(reply *guestbook.Reply) error { tx, err := d.db.Begin() if err != nil { return err } defer tx.Rollback() row := tx.Stmt(stmtUpdateReply).QueryRow( reply.Created.Format(guestbook.DateFormat), reply.Message, reply.ID) if row.Err() != nil { return row.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() res, err := tx.Stmt(stmtDeleteReply).Exec(entryID) if err != nil { return err } _, err = res.RowsAffected() if 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) }