1
0

Structure was simplified.

This commit is contained in:
Alexander Andreev 2023-05-22 00:50:11 +04:00
parent fe47f60581
commit 9a93ad9a3a
Signed by: Arav
GPG Key ID: D22A817D95815393
20 changed files with 376 additions and 385 deletions

View File

@ -1,4 +1,4 @@
justguestbook ver. 1.2.1
justguestbook ver. 1.3.0
========================
A library implementing simple guestbook with replies.

View File

@ -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)
}

View File

@ -1,4 +0,0 @@
INSERT INTO `entry`
(`created`, `name`, `message`, `website`, `hide_website`)
VALUES
(?, ?, ?, ?, ?);

View File

@ -1,7 +0,0 @@
UPDATE OR REPLACE `entry`
SET
`name` = ?,
`message` = ?,
`website` = ?,
`hide_website` = ?
WHERE `entry_id` = ?;

View File

@ -1,4 +0,0 @@
UPDATE OR REPLACE `reply`
SET
`message` = ?
WHERE `entry_id` = ?;

View File

@ -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 );

304
db_sqlite.go Normal file
View File

@ -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)
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,4 @@
INSERT INTO `entry`
(`created`, `name`, `message`, `website`, `hide_website`)
VALUES
(?, ?, ?, ?, ?);

View File

@ -0,0 +1,7 @@
UPDATE OR REPLACE `entry`
SET
`name` = ?,
`message` = ?,
`website` = ?,
`hide_website` = ?
WHERE `entry_id` = ?;

View File

@ -0,0 +1,4 @@
UPDATE OR REPLACE `reply`
SET
`message` = ?
WHERE `entry_id` = ?;

22
sqlite_queries/schema.sql Normal file
View File

@ -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 );