Initial sqlite3 compatibility

This commit is contained in:
2018-11-13 00:58:44 +00:00
parent e15ca16d17
commit 80e66d2b32
7 changed files with 123 additions and 170 deletions

View File

@@ -7,7 +7,7 @@ import (
"strings"
"time"
"github.com/go-sql-driver/mysql"
"github.com/mattn/go-sqlite3"
"github.com/guregu/null"
"github.com/guregu/null/zero"
uuid "github.com/nu7hatch/gouuid"
@@ -22,10 +22,19 @@ import (
"github.com/writeas/writefreely/author"
)
const (
mySQLErrDuplicateKey = 1062
var (
sqlNow = `datetime('now')`
sqlExpires = `(expires IS NULL OR expires > `+sqlNow+`)`
)
func isConstraintError(err error) bool {
if sqliteErr, ok := err.(*sqlite3.Error); ok {
return sqliteErr.Code == sqlite3.ErrConstraint
}
return false
}
type writestore interface {
CreateUser(*User, string) error
UpdateUserEmail(keys *keychain, userID int64, email string) error
@@ -110,13 +119,11 @@ func (db *datastore) CreateUser(u *User, collectionTitle string) error {
// 1. Add to `users` table
// NOTE: Assumes User's Password is already hashed!
res, err := t.Exec("INSERT INTO users (username, password, email, created) VALUES (?, ?, ?, NOW())", u.Username, u.HashedPass, u.Email)
res, err := t.Exec("INSERT INTO users (username, password, email, created) VALUES (?, ?, ?, "+sqlNow+")", u.Username, u.HashedPass, u.Email)
if err != nil {
t.Rollback()
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mySQLErrDuplicateKey {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
if isConstraintError(err) {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
log.Error("Rolling back users INSERT: %v\n", err)
@@ -136,10 +143,8 @@ func (db *datastore) CreateUser(u *User, collectionTitle string) error {
res, err = t.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", u.Username, collectionTitle, "", CollUnlisted, u.ID, 0)
if err != nil {
t.Rollback()
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mySQLErrDuplicateKey {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
if isConstraintError(err) {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
log.Error("Rolling back collections INSERT: %v\n", err)
return err
@@ -208,10 +213,8 @@ func (db *datastore) CreateCollection(alias, title string, userID int64) (*Colle
// All good, so create new collection
res, err := db.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", alias, title, "", CollUnlisted, userID, 0)
if err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mySQLErrDuplicateKey {
return nil, impart.HTTPError{http.StatusConflict, "Collection already exists."}
}
if isConstraintError(err) {
return nil, impart.HTTPError{http.StatusConflict, "Collection already exists."}
}
log.Error("Couldn't add to collections: %v\n", err)
return nil, err
@@ -329,7 +332,7 @@ func (db *datastore) GetUserNameFromToken(accessToken string) (string, error) {
var oneTime bool
var username string
err := db.QueryRow("SELECT username, one_time FROM accesstokens LEFT JOIN users ON user_id = id WHERE token = ? AND (expires IS NULL OR expires > NOW())", t).Scan(&username, &oneTime)
err := db.QueryRow("SELECT username, one_time FROM accesstokens LEFT JOIN users ON user_id = id WHERE token = ? AND "+sqlExpires, t).Scan(&username, &oneTime)
switch {
case err == sql.ErrNoRows:
return "", ErrBadAccessToken
@@ -354,7 +357,7 @@ func (db *datastore) GetUserDataFromToken(accessToken string) (int64, string, er
var userID int64
var oneTime bool
var username string
err := db.QueryRow("SELECT user_id, username, one_time FROM accesstokens LEFT JOIN users ON user_id = id WHERE token = ? AND (expires IS NULL OR expires > NOW())", t).Scan(&userID, &username, &oneTime)
err := db.QueryRow("SELECT user_id, username, one_time FROM accesstokens LEFT JOIN users ON user_id = id WHERE token = ? AND "+sqlExpires, t).Scan(&userID, &username, &oneTime)
switch {
case err == sql.ErrNoRows:
return 0, "", ErrBadAccessToken
@@ -393,7 +396,7 @@ func (db *datastore) GetUserIDPrivilege(accessToken string) (userID int64, sudo
}
var oneTime bool
err := db.QueryRow("SELECT user_id, sudo, one_time FROM accesstokens WHERE token = ? AND (expires IS NULL OR expires > NOW())", t).Scan(&userID, &sudo, &oneTime)
err := db.QueryRow("SELECT user_id, sudo, one_time FROM accesstokens WHERE token = ? AND "+sqlExpires, t).Scan(&userID, &sudo, &oneTime)
switch {
case err == sql.ErrNoRows:
return -1, false
@@ -425,7 +428,7 @@ func (db *datastore) DeleteToken(accessToken []byte) error {
// userID.
func (db *datastore) FetchLastAccessToken(userID int64) string {
var t []byte
err := db.QueryRow("SELECT token FROM accesstokens WHERE user_id = ? AND (expires IS NULL OR expires > NOW()) ORDER BY created DESC LIMIT 1", userID).Scan(&t)
err := db.QueryRow("SELECT token FROM accesstokens WHERE user_id = ? AND "+sqlExpires+" ORDER BY created DESC LIMIT 1", userID).Scan(&t)
switch {
case err == sql.ErrNoRows:
return ""
@@ -468,12 +471,16 @@ func (db *datastore) GetTemporaryOneTimeAccessToken(userID int64, validSecs int,
// Insert UUID to `accesstokens`
binTok := u[:]
expirationVal := "NULL"
now := time.Now()
expirationVal := interface{}("NULL")
if validSecs > 0 {
expirationVal = fmt.Sprintf("DATE_ADD(NOW(), INTERVAL %d SECOND)", validSecs)
expirationVal = interface{}(now.Add(time.Duration(validSecs)*time.Second))
}
_, err = db.Exec("INSERT INTO accesstokens (token, user_id, created, one_time, expires) VALUES (?, ?, NOW(), ?, "+expirationVal+")", string(binTok), userID, oneTime)
_, err = db.Exec("INSERT INTO accesstokens (token, user_id, created, one_time, expires) VALUES (?, ?, ?, ?, ?)", string(binTok), userID, now, oneTime, expirationVal)
if err != nil {
log.Error("Couldn't INSERT accesstoken: %v", err)
return "", err
@@ -557,18 +564,16 @@ func (db *datastore) CreatePost(userID, collID int64, post *SubmittedPost) (*Pos
}
}
_, err = db.Exec("INSERT INTO posts (id, slug, title, content, text_appearance, language, rtl, owner_id, collection_id, updated, view_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?)", friendlyID, slug, post.Title, post.Content, appearance, post.Language, post.IsRTL, ownerID, ownerCollID, 0)
_, err = db.Exec("INSERT INTO posts (id, slug, title, content, text_appearance, language, rtl, owner_id, collection_id, updated, view_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, "+sqlNow+", ?)", friendlyID, slug, post.Title, post.Content, appearance, post.Language, post.IsRTL, ownerID, ownerCollID, 0)
if err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mySQLErrDuplicateKey {
// Duplicate entry error; try a new slug
// TODO: make this a little more robust
// TODO: reuse exact same db.Exec statement as above
slug = sql.NullString{id.GenSafeUniqueSlug(slug.String), true}
_, err = db.Exec("INSERT INTO posts (id, slug, title, content, text_appearance, language, rtl, owner_id, collection_id, updated, view_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?)", friendlyID, slug, post.Title, post.Content, appearance, post.Language, post.IsRTL, ownerID, ownerCollID, 0)
if err != nil {
return nil, handleFailedPostInsert(fmt.Errorf("Retried slug generation, still failed: %v", err))
}
if isConstraintError(err) {
// Duplicate entry error; try a new slug
// TODO: make this a little more robust
// TODO: reuse exact same db.Exec statement as above
slug = sql.NullString{id.GenSafeUniqueSlug(slug.String), true}
_, err = db.Exec("INSERT INTO posts (id, slug, title, content, text_appearance, language, rtl, owner_id, collection_id, updated, view_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, "+sqlNow+", ?)", friendlyID, slug, post.Title, post.Content, appearance, post.Language, post.IsRTL, ownerID, ownerCollID, 0)
if err != nil {
return nil, handleFailedPostInsert(fmt.Errorf("Retried slug generation, still failed: %v", err))
} else {
return nil, handleFailedPostInsert(err)
}
@@ -650,7 +655,7 @@ func (db *datastore) UpdateOwnedPost(post *AuthenticatedPost, userID int64) erro
return ErrPostNoUpdatableVals
}
queryUpdates += sep + "updated = NOW()"
queryUpdates += sep + "updated = " + sqlNow
res, err := db.Exec("UPDATE posts SET "+queryUpdates+" WHERE id = ? AND "+authCondition, params...)
if err != nil {
@@ -962,7 +967,7 @@ func (db *datastore) GetPostsCount(c *CollectionObj, includeFuture bool) {
var count int64
timeCondition := ""
if !includeFuture {
timeCondition = "AND created <= NOW()"
timeCondition = "AND created <= " + sqlNow
}
err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE collection_id = ? AND pinned_position IS NULL "+timeCondition, c.ID).Scan(&count)
switch {
@@ -1001,7 +1006,7 @@ func (db *datastore) GetPosts(c *Collection, page int, includeFuture bool) (*[]P
}
timeCondition := ""
if !includeFuture {
timeCondition = "AND created <= NOW()"
timeCondition = "AND created <= " + sqlNow
}
rows, err := db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? AND pinned_position IS NULL "+timeCondition+" ORDER BY created "+order+limitStr, collID)
if err != nil {
@@ -1058,7 +1063,7 @@ func (db *datastore) GetPostsTagged(c *Collection, tag string, page int, include
}
timeCondition := ""
if !includeFuture {
timeCondition = "AND created <= NOW()"
timeCondition = "AND created <= " + sqlNow
}
rows, err := db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? AND LOWER(content) RLIKE ? "+timeCondition+" ORDER BY created "+order+limitStr, collID, "#"+strings.ToLower(tag)+"[[:>:]]")
if err != nil {
@@ -1133,17 +1138,16 @@ func (db *datastore) CanCollect(cpr *ClaimPostRequest, userID int64) bool {
func (db *datastore) AttemptClaim(p *ClaimPostRequest, query string, params []interface{}, slugIdx int) (sql.Result, error) {
qRes, err := db.Exec(query, params...)
if err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mySQLErrDuplicateKey && slugIdx > -1 {
s := id.GenSafeUniqueSlug(p.Slug)
if s == p.Slug {
// Sanity check to prevent infinite recursion
return qRes, fmt.Errorf("GenSafeUniqueSlug generated nothing unique: %s", s)
}
p.Slug = s
params[slugIdx] = p.Slug
return db.AttemptClaim(p, query, params, slugIdx)
if isConstraintError(err) && slugIdx > -1 {
s := id.GenSafeUniqueSlug(p.Slug)
if s == p.Slug {
// Sanity check to prevent infinite recursion
return qRes, fmt.Errorf("GenSafeUniqueSlug generated nothing unique: %s", s)
}
p.Slug = s
params[slugIdx] = p.Slug
return db.AttemptClaim(p, query, params, slugIdx)
}
return qRes, fmt.Errorf("attemptClaim: %s", err)
}
@@ -1431,7 +1435,7 @@ func (db *datastore) GetLastPinnedPostPos(collID int64) int64 {
}
func (db *datastore) GetPinnedPosts(coll *CollectionObj) (*[]PublicPost, error) {
rows, err := db.Query("SELECT id, slug, title, LEFT(content, 80), pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL ORDER BY pinned_position ASC", coll.ID)
rows, err := db.Query("SELECT id, slug, title, SUBSTR(content, 80), pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL ORDER BY pinned_position ASC", coll.ID)
if err != nil {
log.Error("Failed selecting pinned posts: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve pinned posts."}
@@ -1694,10 +1698,8 @@ func (db *datastore) ChangeSettings(app *app, u *User, s *userSettings) error {
_, err = t.Exec("UPDATE users SET username = ? WHERE id = ?", newUsername, u.ID)
if err != nil {
t.Rollback()
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mySQLErrDuplicateKey {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
if isConstraintError(err) {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
log.Error("Unable to update users table: %v", err)
return ErrInternalGeneral
@@ -1706,10 +1708,8 @@ func (db *datastore) ChangeSettings(app *app, u *User, s *userSettings) error {
_, err = t.Exec("UPDATE collections SET alias = ? WHERE alias = ? AND owner_id = ?", newUsername, u.Username, u.ID)
if err != nil {
t.Rollback()
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mySQLErrDuplicateKey {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
if isConstraintError(err) {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
log.Error("Unable to update collection: %v", err)
return ErrInternalGeneral