1
0
Fork 0

Compare commits

...

26 Commits

Author SHA1 Message Date
Alexander Andreev 8c8c1f2617
Added some more comments. 2023-05-22 02:37:37 +04:00
Alexander Andreev 16e5ececf5
Added a check for a website field's length. 2023-05-22 02:35:39 +04:00
Alexander Andreev 141e1215a3
Added comments. 2023-05-22 02:35:07 +04:00
Alexander Andreev 5d56b56882
Oh, changed email in LICENSE. 2023-05-22 02:12:13 +04:00
Alexander Andreev d272dada83
Let's delete README. 2023-05-22 01:01:43 +04:00
Alexander Andreev 84ff66b4f4
Fixed package name. 2023-05-22 01:01:10 +04:00
Alexander Andreev 9a93ad9a3a
Structure was simplified. 2023-05-22 00:50:11 +04:00
Alexander Andreev fe47f60581
Version incremented in a README. 2023-05-09 23:32:06 +04:00
Alexander Andreev 5a72fbecb4
Found why Edit*() methods didn't work, for UPDATE query Exec() method must be used instead of QueryRow(). 2023-05-09 23:31:39 +04:00
Alexander Andreev cb623e8087
Fixed TestSqliteEditEntry test. 2023-05-09 23:30:18 +04:00
Alexander Andreev 1d6a05d15c
Version in a README was incremented. 2023-05-09 21:45:51 +04:00
Alexander Andreev 761b7626c9
Date now stored as Unix time (int64). No NULL is being returned so no such check is needed. 2023-05-09 21:32:01 +04:00
Alexander Andreev 6cfe0a7d8b
Return both website and hide_website fields (for use on an admin page). Using COALESCE to return 0 and an empty string if there is no reply to not deal with NULL. 2023-05-09 21:30:17 +04:00
Alexander Andreev 613f0c1616
PRIMARY KEY moved out on a separate line. Use INTEGER (Unix time) for created dates to save space. 2023-05-09 21:27:52 +04:00
Alexander Andreev c658b798aa
Changed version in README.md. 2023-05-06 22:24:46 +04:00
Alexander Andreev 1c424e1ec9
Removed unnecessary braces around CASE ... END statement. 2023-05-06 22:22:40 +04:00
Alexander Andreev b9963c9521
Do not change created field in replyUpdate.sql. 2023-05-06 22:22:08 +04:00
Alexander Andreev 9d7088f2cd
Changed formatting for entryCount.sql. 2023-05-06 22:21:47 +04:00
Alexander Andreev f5631bf3d0
Modified tests accordingly to all changes. 2023-05-06 22:21:23 +04:00
Alexander Andreev 2cc91b351c
Don't modify reply's date when edit. 2023-05-06 22:20:58 +04:00
Alexander Andreev 9347a16fca
Do not return number of affected rows in Delete*() methods. 2023-05-06 22:20:11 +04:00
Alexander Andreev bea8df31cb
Return Guestbook interface instead of SQLiteDatabase instance. 2023-05-06 22:19:04 +04:00
Alexander Andreev f602dddff3
Replaced Update*() with Edit*(). 2023-05-06 22:18:20 +04:00
Alexander Andreev 32c1fa6aa1
Reorder fields of Entry struct. 2023-05-06 22:16:40 +04:00
Alexander Andreev 723d2d4772
Make DeleteReply() return count of affected rows. 2023-03-23 17:03:23 +04:00
Alexander Andreev 506a010b23
Module's min Go version was set down to 1.16. 2023-03-23 16:56:28 +04:00
24 changed files with 476 additions and 454 deletions

View File

@ -1,6 +1,6 @@
The MIT License
Copyright (c) 2022,2023 Alexander "Arav" Andreev <me@arav.top>
Copyright (c) 2022,2023 Alexander "Arav" Andreev <me@arav.su>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

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

View File

@ -1,319 +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) (*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)
}

View File

@ -1,2 +0,0 @@
SELECT COUNT(`entry_id`) AS `total`
FROM `entry`;

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,5 +0,0 @@
UPDATE OR REPLACE `reply`
SET
`created` = ?,
`message` = ?
WHERE `entry_id` = ?;

View File

@ -1,20 +0,0 @@
-- SQLite3
CREATE TABLE IF NOT EXISTS `entry` (
`entry_id` INTEGER PRIMARY KEY NOT NULL,
`created` TEXT NOT NULL,
`name` TEXT NOT NULL,
`message` TEXT NOT NULL,
`website` TEXT NOT NULL,
`hide_website` INTEGER NOT NULL DEFAULT TRUE);
CREATE INDEX IF NOT EXISTS `entry_created_idx`
ON `entry` (`created`);
CREATE TABLE IF NOT EXISTS `reply` (
`entry_id` INTEGER PRIMARY KEY NOT NULL,
`created` TEXT NOT NULL,
`message` TEXT NOT NULL,
FOREIGN KEY (`entry_id`)
REFERENCES `entry` (`entry_id`)
ON DELETE CASCADE
ON UPDATE CASCADE );

307
db_sqlite.go Normal file
View File

@ -0,0 +1,307 @@
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)
}

View File

@ -1,10 +1,9 @@
package sqlite_test
package justguestbook_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"
)
@ -13,8 +12,8 @@ const (
testRMsg = "lol"
)
func genTestDB() (db *sqlite.SQLiteDatabase, err error) {
db, err = sqlite.New(":memory:")
func genTestDB() (db guestbook.Guestbook, err error) {
db, err = guestbook.NewSQLiteDB(":memory:")
if err != nil {
return nil, errors.Wrap(err, "failed to init DB")
}
@ -65,7 +64,7 @@ func TestSqliteInsertAndGetAll(t *testing.T) {
}
}
func TestSqliteUpdateEntry(t *testing.T) {
func TestSqliteEditEntry(t *testing.T) {
db, err := genTestDB()
if err != nil {
t.Error(err)
@ -83,16 +82,20 @@ func TestSqliteUpdateEntry(t *testing.T) {
me := es0[0]
me.ID = es0[0].ID
me.Name = "NotSoAnonymous"
err = db.UpdateEntry(me)
err = db.EditEntry(me)
if err != nil {
t.Error(err)
}
es0, err = db.Entries(1, 30)
if es0[0].Name != me.Name {
t.Error("name wasn't changed", es0[0].Name, "!=", me.Name, es0[0].ID, me.ID)
}
}
func TestSqliteUpdateReply(t *testing.T) {
func TestSqliteEditReply(t *testing.T) {
db, err := genTestDB()
if err != nil {
t.Error(err)
@ -113,7 +116,7 @@ func TestSqliteUpdateReply(t *testing.T) {
mr.ID = es0[0].ID
mr.Message = "bur"
err = db.UpdateReply(mr)
err = db.EditReply(mr)
if err != nil {
t.Error(err)
}
@ -130,13 +133,8 @@ func TestSqliteDeleteEntry(t *testing.T) {
}
}()
dc, err := db.DeleteEntry(1)
if err != nil {
t.Error("cannot delete entry", err, dc)
}
if c, err := db.Count(); err != nil || c != 0 {
t.Error(c, err, dc)
if err = db.DeleteEntry(1); err != nil {
t.Error("cannot delete entry", err)
}
}

2
go.mod
View File

@ -1,6 +1,6 @@
module git.arav.su/Arav/justguestbook
go 1.19
go 1.16
require (
github.com/mattn/go-sqlite3 v1.14.16

108
guestbook.go Normal file
View File

@ -0,0 +1,108 @@
package justguestbook
import (
"errors"
"fmt"
"time"
)
const (
// DateFormat is a format of the date and time used in a guestbook.
DateFormat = "2006-01-02 15:04:05"
// entryWebsiteMaxLength is a maximum length for a Website field of an Entry.
entryWebsiteMaxLength = 255
)
// Reply holds a reply for a guestbook's entry.
type Reply struct {
// ID of a guestbook's entry is used when update of a reply is performed.
//
// Not present in JSON because a reply comes within its corresponding entry.
ID int64 `json:"-"`
// Created holds a date and time when a reply was created.
Created time.Time `json:"created,omitempty"`
// Message holds a reply's message.
Message string `json:"message"`
}
// NewReply creates a new reply and, before that, verifies that a passed message
// is not empty.
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
}
// Entry holds a guestbook entry.
type Entry struct {
// ID of an entry.
ID int64 `json:"entry_id"`
// Created holds the date and time when an entry was created.
Created time.Time `json:"created"`
// Name holds a nick-/name of a guest who posted an entry.
Name string `json:"name"`
// Website of a guest. Can be empty.
Website string `json:"website,omitempty"`
// HideWebsite tells wether to hide or show guest's website.
HideWebsite bool `json:"hide_website,omitempty"`
// Message that a guest wrote.
Message string `json:"message"`
// Reply holds a reply for this entry.
Reply *Reply `json:"reply,omitempty"`
}
// NewEntry creates a new Entry and, before that, verifies if the name and message
// aren't empty and the website field do not exceed the limit.
func NewEntry(name, message, website string, hideWebsite bool) (*Entry, error) {
if name == "" || message == "" {
return nil, errors.New("name and message field are required")
}
if len(website) > entryWebsiteMaxLength {
return nil, errors.New(fmt.Sprint("website field's max length is",
entryWebsiteMaxLength, "but", len(website), "was given"))
}
return &Entry{
Created: time.Now().UTC(),
Name: name,
Website: website,
HideWebsite: hideWebsite,
Message: message}, nil
}
// Guestbook is an interface that should be implemented by a DB.
type Guestbook interface {
// Entries returns a slice of guestbook entries.
Entries(page, pageSize int64) ([]*Entry, error)
// Count returns how much entries are in a DB.
Count() (int64, error)
// NewEntry inserts a given Entry into a DB.
NewEntry(entry *Entry) error
// EditEntry modifies the fields of an existing entry.
EditEntry(entry *Entry) error
// DeleteEntry removes an Entry with a given ID.
DeleteEntry(entryID int64) error
// NewReply inserts a given Reply into a DB.
NewReply(reply *Reply) error
// EditReply modifies a message field of a given Reply.
EditReply(reply *Reply) error
// DeleteReply removes a Reply with a given ID of an Entry.
DeleteReply(entryID int64) error
// Close closes a DB.
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
UpdateEntry(entry *Entry) error
DeleteEntry(entryID int64) error
NewReply(reply *Reply) error
UpdateReply(reply *Reply) error
DeleteReply(entryID int64) error
Close() error
}

View File

@ -1,29 +0,0 @@
package guestbook
import (
"errors"
"time"
)
type Entry struct {
ID int64 `json:"entry_id"`
Created time.Time `json:"created"`
Name string `json:"name"`
Website string `json:"website,omitempty"`
Message string `json:"message"`
HideWebsite bool `json:"hide_website,omitempty"`
Reply *Reply `json:"reply,omitempty"`
}
func NewEntry(name, message, website string, hideWebsite bool) (*Entry, error) {
if name == "" || message == "" {
return nil, errors.New("name and message field are required")
}
return &Entry{
Created: time.Now().UTC(),
Name: name,
Website: website,
HideWebsite: hideWebsite,
Message: message}, nil
}

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,3 @@
SELECT
COUNT(`entry_id`) AS `total`
FROM `entry`;

View File

@ -3,13 +3,10 @@ SELECT
`entry`.`created`,
`entry`.`name`,
`entry`.`message`,
(CASE
WHEN `entry`.`hide_website` IS FALSE
THEN `entry`.`website`
ELSE ''
END) AS `website`,
`reply`.`created` AS `reply_created`,
`reply`.`message` AS `reply_message`
`entry`.`website`,
`entry`.`hide_website`,
COALESCE(`reply`.`created`, 0) AS `reply_created`,
COALESCE(`reply`.`message`, '') AS `reply_message`
FROM `entry`
LEFT JOIN `reply`
ON `entry`.`entry_id` = `reply`.`entry_id`

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