2017-07-12 06:43:11 +00:00
package yatwiki
2017-07-08 23:13:36 +00:00
import (
"database/sql"
"fmt"
2018-04-02 04:19:28 +00:00
"log"
2017-10-29 00:19:24 +00:00
"strings"
2017-07-08 23:13:36 +00:00
"time"
_ "github.com/mattn/go-sqlite3"
)
type WikiDB struct {
2017-07-09 06:45:28 +00:00
db * sql . DB
compressionLevel int
2017-07-08 23:13:36 +00:00
}
2017-07-09 06:45:28 +00:00
func NewWikiDB ( dbFilePath string , compressionLevel int ) ( * WikiDB , error ) {
2017-07-08 23:13:36 +00:00
db , err := sql . Open ( "sqlite3" , dbFilePath )
if err != nil {
return nil , err
}
2017-07-09 06:45:28 +00:00
wdb := WikiDB {
db : db ,
compressionLevel : compressionLevel ,
}
2017-07-08 23:13:36 +00:00
err = wdb . assertSchema ( )
if err != nil {
return nil , fmt . Errorf ( "assertSchema: %s" , err . Error ( ) )
}
return & wdb , nil
}
2018-04-02 04:19:28 +00:00
func ( this * WikiDB ) multiTx ( stmts ... string ) ( err error ) {
tx , err := this . db . Begin ( )
2017-07-08 23:13:36 +00:00
if err != nil {
return err
}
2018-04-02 04:19:28 +00:00
for _ , stmt := range stmts {
_ , err := tx . Exec ( stmt )
if err != nil {
tx . Rollback ( )
return err
}
}
return tx . Commit ( )
}
func ( this * WikiDB ) assertSchema ( ) error {
_ , err := this . db . Exec ( `
CREATE TABLE IF NOT EXISTS schema (
id INTEGER PRIMARY KEY
2017-07-08 23:13:36 +00:00
) ; ` )
if err != nil {
return err
}
2018-04-02 04:19:28 +00:00
// Look up current value from schema table
schemaLookup := this . db . QueryRow ( ` SELECT MAX(id) mid FROM schema ` )
currentSchema := int64 ( 0 )
if err = schemaLookup . Scan ( & currentSchema ) ; err == nil {
// That's fine
2017-07-08 23:13:36 +00:00
}
2018-04-02 04:19:28 +00:00
log . Printf ( "Found DB version %d\n" , currentSchema )
2018-04-02 04:53:55 +00:00
//
2018-04-02 04:19:28 +00:00
if currentSchema == 0 {
2018-04-02 04:53:55 +00:00
// Schema 0 ==> Schema 1
2018-04-02 04:19:28 +00:00
log . Println ( "Upgrading to DB version 1" )
err := this . multiTx (
` CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY ,
article INTEGER ,
modified INTEGER ,
body BLOB ,
author TEXT
) ; ` ,
` CREATE TABLE IF NOT EXISTS titles (
id INTEGER PRIMARY KEY ,
title TEXT
) ; ` ,
` CREATE INDEX IF NOT EXISTS articles_modified_index ON articles (modified) ` ,
` CREATE INDEX IF NOT EXISTS articles_title_index ON articles (article) ` ,
` INSERT INTO schema (id) VALUES (1); ` ,
)
if err != nil {
return err
}
currentSchema = 1
2017-07-08 23:13:36 +00:00
}
2018-04-02 04:53:55 +00:00
//
if currentSchema == 1 {
log . Println ( "Upgrading to DB version 2" )
err := this . multiTx (
` ALTER TABLE titles ADD COLUMN is_deleted INTEGER DEFAULT 0; ` ,
` INSERT INTO schema (id) VALUES (2); ` ,
)
if err != nil {
return err
}
currentSchema = 1
}
//
2017-07-08 23:13:36 +00:00
return nil
}
type Article struct {
2017-07-09 06:45:28 +00:00
ID int64
TitleID int64
2017-07-08 23:13:36 +00:00
Modified int64
Body [ ] byte
Author string
2017-07-09 06:05:03 +00:00
Title string
2017-07-08 23:13:36 +00:00
}
func ( this * WikiDB ) GetArticleById ( articleId int ) ( * Article , error ) {
row := this . db . QueryRow ( ` SELECT articles.* FROM articles WHERE id = ? ` , articleId )
return this . parseArticle ( row )
}
2017-07-09 06:05:03 +00:00
func ( this * WikiDB ) GetRevision ( revId int ) ( * Article , error ) {
2017-07-09 01:18:18 +00:00
row := this . db . QueryRow ( ` SELECT articles.*, titles.title FROM articles JOIN titles ON articles.article=titles.id WHERE articles.id = ? ` , revId )
return this . parseArticleWithTitle ( row )
}
2017-07-08 23:13:36 +00:00
func ( this * WikiDB ) GetLatestVersion ( title string ) ( * Article , error ) {
2017-10-29 02:09:53 +00:00
row := this . db . QueryRow ( ` SELECT articles.* FROM articles WHERE article = (SELECT id FROM titles WHERE title = ?) ORDER BY modified DESC LIMIT 1 ` , this . normaliseTitle ( title ) )
2017-07-08 23:13:36 +00:00
return this . parseArticle ( row )
}
2017-07-09 06:45:28 +00:00
type ArticleAlteredError struct {
got , expected int64
}
func ( aae ArticleAlteredError ) Error ( ) string {
return fmt . Sprintf ( "Warning: Your changes were not based on the most recent version of the page (r%d ≠ r%d). No changes were saved." , aae . got , aae . expected )
}
2017-10-29 00:19:24 +00:00
func ( this * WikiDB ) normaliseTitle ( title string ) string {
return strings . ToLower ( strings . Trim ( title , " \r\n\t" ) )
}
2017-07-09 06:45:28 +00:00
func ( this * WikiDB ) SaveArticle ( title , author , body string , expectBaseRev int64 ) error {
isNewArticle := false
a , err := this . GetLatestVersion ( title )
if err != nil {
if err == sql . ErrNoRows {
isNewArticle = true
} else {
return fmt . Errorf ( "Couldn't check for existing article title: %s" , err . Error ( ) )
}
}
if ! isNewArticle && a . ID != expectBaseRev {
return ArticleAlteredError { got : expectBaseRev , expected : a . ID }
}
zBody , err := gzdeflate ( [ ] byte ( body ) , this . compressionLevel )
if err != nil {
return err
}
var titleId int64
if isNewArticle {
2017-10-29 00:19:24 +00:00
titleInsert , err := this . db . Exec ( ` INSERT INTO titles (title) VALUES (?) ` , this . normaliseTitle ( title ) )
2017-07-09 06:45:28 +00:00
if err != nil {
return err
}
titleId , err = titleInsert . LastInsertId ( )
if err != nil {
return err
}
} else {
titleId = a . TitleID
}
_ , err = this . db . Exec ( ` INSERT INTO articles (article, modified, body, author) VALUES (?, ?, ?, ?) ` , titleId , time . Now ( ) . Unix ( ) , zBody , author )
if err != nil {
return err
}
return nil
}
2017-07-09 06:05:03 +00:00
func ( this * WikiDB ) GetRevisionHistory ( title string ) ( [ ] Article , error ) {
2017-10-29 00:19:24 +00:00
rows , err := this . db . Query (
` SELECT articles.id, articles.modified, articles.author FROM articles WHERE article = (SELECT id FROM titles WHERE title = ?) ORDER BY modified DESC ` ,
this . normaliseTitle ( title ) ,
)
2017-07-09 06:05:03 +00:00
if err != nil {
return nil , err
}
defer rows . Close ( )
ret := make ( [ ] Article , 0 )
for rows . Next ( ) {
a := Article { }
err := rows . Scan ( & a . ID , & a . Modified , & a . Author )
if err != nil {
return nil , err
}
ret = append ( ret , a )
}
return ret , nil
}
2017-07-11 07:19:42 +00:00
func ( this * WikiDB ) GetNextOldestRevision ( revision int ) ( int , error ) {
row := this . db . QueryRow (
` SELECT articles.id FROM articles WHERE articles.article = (SELECT article FROM articles WHERE id = ?) AND id < ? ORDER BY id DESC LIMIT 1 ` ,
revision , revision ,
)
var ret int
err := row . Scan ( & ret )
if err != nil {
return 0 , err
}
return ret , nil
}
2017-07-09 06:05:03 +00:00
func ( this * WikiDB ) GetRecentChanges ( offset int , limit int ) ( [ ] Article , error ) {
rows , err := this . db . Query (
` SELECT articles.id, articles.modified, articles.author, titles.title FROM articles JOIN titles ON articles.article=titles.id ORDER BY modified DESC ` +
fmt . Sprintf ( ` LIMIT %d OFFSET %d ` , limit , offset ) ,
)
if err != nil {
return nil , err
}
defer rows . Close ( )
ret := make ( [ ] Article , 0 , limit )
for rows . Next ( ) {
a := Article { }
err := rows . Scan ( & a . ID , & a . Modified , & a . Author , & a . Title )
if err != nil {
return nil , err
}
ret = append ( ret , a )
}
return ret , nil
}
2017-07-09 01:00:26 +00:00
func ( this * WikiDB ) TotalRevisions ( ) ( int64 , error ) {
row := this . db . QueryRow ( ` SELECT COUNT(*) c FROM articles ` )
var ret int64
err := row . Scan ( & ret )
if err != nil {
return 0 , err
}
return ret , nil
}
2017-07-08 23:13:36 +00:00
func ( this * WikiDB ) ListTitles ( ) ( [ ] string , error ) {
rows , err := this . db . Query ( ` SELECT title FROM titles ORDER BY title ASC ` )
if err != nil {
return nil , err
}
defer rows . Close ( )
2017-07-09 06:05:03 +00:00
2017-07-08 23:13:36 +00:00
ret := make ( [ ] string , 0 )
for rows . Next ( ) {
var title string
err = rows . Scan ( & title )
if err != nil {
return nil , err
}
ret = append ( ret , title )
}
return ret , nil
}
func ( this * WikiDB ) parseArticle ( row * sql . Row ) ( * Article , error ) {
a := Article { }
2017-07-09 00:13:43 +00:00
var gzBody [ ] byte
err := row . Scan ( & a . ID , & a . TitleID , & a . Modified , & gzBody , & a . Author )
if err != nil {
return nil , err
}
2017-07-09 06:45:28 +00:00
decompressed , err := gzinflate ( gzBody )
2017-07-08 23:13:36 +00:00
if err != nil {
return nil , err
}
2017-07-09 00:13:43 +00:00
a . Body = decompressed
2017-07-08 23:13:36 +00:00
return & a , nil
}
2017-07-09 06:05:03 +00:00
func ( this * WikiDB ) parseArticleWithTitle ( row * sql . Row ) ( * Article , error ) {
a := Article { }
2017-07-09 00:13:43 +00:00
var gzBody [ ] byte
err := row . Scan ( & a . ID , & a . TitleID , & a . Modified , & gzBody , & a . Author , & a . Title )
2017-07-08 23:13:36 +00:00
if err != nil {
return nil , err
}
2017-07-09 06:45:28 +00:00
decompressed , err := gzinflate ( gzBody )
2017-07-09 00:13:43 +00:00
if err != nil {
return nil , err
}
a . Body = decompressed
2017-07-08 23:13:36 +00:00
return & a , nil
}
func ( this * WikiDB ) Close ( ) {
this . db . Close ( )
}