Unverified Commit 61cac96e authored by Kevin Lyda's avatar Kevin Lyda
Browse files

A bunch of sql changes

Creates the users, folders and co_owners tables.  Move user preferences
inside the main database.

Makes use of sqlite triggers and foreign key constraints.  Not a huge
fan of these, but they do fit the use case and this isn't a case where
scaling is a concern.
parent fcc3fb91
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -19,6 +19,8 @@ Look up how to run migrations on the db from embedded files.
Figure out readline completion.  Case sensitive - issue?  Building from
repl.commands?

sqlite trigger tracing: `.trace stdout --row --profile --stmt --expanded --plain --close`

## Things to do

  * Implement a better dclish parser.
+1 −18
Original line number Diff line number Diff line
@@ -2,15 +2,11 @@
package accounts

import (
	"database/sql"
	"errors"
	"fmt"
	"os"
	"path"
	"strings"

	"git.lyda.ie/kevin/bulletin/folders"
	"github.com/adrg/xdg"
	_ "modernc.org/sqlite" // Loads sqlite driver.
)

@@ -19,7 +15,6 @@ import (
type UserData struct {
	Account        string
	FullName       string
	pref           *sql.DB
	Folders        *folders.Store
	CurrentFolder  string
	CurrentMessage int
@@ -49,18 +44,7 @@ func Open(acc string) error {
		Account: acc,
	}

	prefdir := path.Join(xdg.ConfigHome, "BULLETIN")
	err = os.MkdirAll(prefdir, 0700)
	if err != nil {
		return errors.New("account preference directory problem")
	}
	User.pref, err = sql.Open("sqlite", path.Join(prefdir, fmt.Sprintf("%s.%s", acc, ".db")))
	if err != nil {
		return errors.New("account preference database problem")
	}
	// TODO: run prefs migration - move this to a storage module.

	User.Folders, err = folders.Open()
	User.Folders, err = folders.Open(acc)
	if err != nil {
		return err
	}
@@ -70,7 +54,6 @@ func Open(acc string) error {

// Close closes the resources open for the account.
func (u *UserData) Close() {
	u.pref.Close()
	u.Folders.Close()
}

+8 −8
Original line number Diff line number Diff line
@@ -5,7 +5,6 @@ import (
	"database/sql"
	"embed"
	"errors"
	"log"
	"os"
	"path"

@@ -23,11 +22,12 @@ var fs embed.FS

// Store is the store for folders.
type Store struct {
	user string
	db   *sql.DB
}

// Open opens the folders database.
func Open() (*Store, error) {
func Open(user string) (*Store, error) {
	fdir := path.Join(xdg.DataHome, "BULLETIN")
	err := os.MkdirAll(fdir, 0700)
	if err != nil {
@@ -39,18 +39,18 @@ func Open() (*Store, error) {
	if err != nil {
		return nil, err
	}
	m, err := migrate.NewWithSourceInstance("iofs", sqldir, "sqlite://"+fdb)
	m, err := migrate.NewWithSourceInstance("iofs", sqldir, "sqlite://"+fdb+"?_pragma=foreign_keys(1)")
	if err != nil {
		log.Fatal(err)
		return nil, err
	}
	err = m.Up()
	if err != nil {
	if err != nil && err != migrate.ErrNoChange {
		return nil, err
	}
	m.Close()

	store := &Store{}
	store.db, err = sql.Open("sqlite", fdb)
	store := &Store{user: user}
	store.db, err = sql.Open("sqlite", "file://"+fdb+"?_pragma=foreign_keys(1)")
	if err != nil {
		return nil, errors.New("bulletin database problem")
	}
+13 −0
Original line number Diff line number Diff line
--- Dropped in reverse order to deal with foreign keys.
DROP TRIGGER co_owners_after_update_update_at;
DROP TABLE co_owners;

DROP TRIGGER folders_before_delete_protect;
DROP TRIGGER folders_after_update_update_at;
DROP TRIGGER folders_before_update_validate;
DROP TRIGGER folders_before_insert_validate;
DROP TABLE folders;

DROP TRIGGER users_before_delete_protect;
DROP TRIGGER users_before_update_protect;
DROP TRIGGER users_after_update_update_at;
DROP TABLE users;
+90 −7
Original line number Diff line number Diff line
CREATE TABLE users (
  login       VARCHAR(25)  NOT NULL PRIMARY KEY,
  name        VARCHAR(53)  NOT NULL,
  admin       INT          DEFAULT 0,
  create_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
  update_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL
) WITHOUT ROWID;

CREATE TRIGGER users_after_update_update_at
  AFTER UPDATE ON users FOR EACH ROW
  WHEN NEW.update_at = OLD.update_at    --- avoid infinite loop
BEGIN
  UPDATE users SET update_at=CURRENT_TIMESTAMP WHERE login=NEW.login;
END;

INSERT INTO users (login, name, admin)
       VALUES ('SYSTEM', 'System User', 1);

CREATE TRIGGER users_before_update_protect
  AFTER UPDATE ON users FOR EACH ROW
  WHEN OLD.login = 'SYSTEM' AND (NEW.login != OLD.login OR NEW.admin != 1)
BEGIN
  SELECT RAISE (ABORT, 'SYSTEM user is protected');
END;

CREATE TRIGGER users_before_delete_protect
  BEFORE DELETE on users FOR EACH ROW
  WHEN OLD.login = 'SYSTEM'
BEGIN
  SELECT RAISE (ABORT, 'SYSTEM user is protected');
END;

CREATE TABLE folders (
  name        VARCHAR(25)  NOT NULL UNIQUE,
  name        VARCHAR(25)  NOT NULL PRIMARY KEY,
  always      INT          DEFAULT 0,
  brief       INT          DEFAULT 0,
  description VARCHAR(53),
  co_owners   TEXT,
  description VARCHAR(53)  NOT NULL,
  notify      INT          DEFAULT 0,
  owner       TEXT,
  owner       VARCHAR(25)  REFERENCES users(login) ON UPDATE CASCADE,
  readnew     INT          DEFAULT 0,
  shownew     INT          DEFAULT 0,
  system      INT          DEFAULT 0,
  expire      INT          DEFAULT 14,
  visibility  TEXT         DEFAULT "public"
);
  visibility  TEXT         DEFAULT 'public',
  create_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
  update_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL
) WITHOUT ROWID;

CREATE TRIGGER folders_before_insert_validate
  BEFORE INSERT on folders
BEGIN
  SELECT
    CASE
      WHEN NEW.name != UPPER(NEW.name) OR NEW.name GLOB '*[^A-Z0-9_-]*' THEN
        RAISE (ABORT, 'Invalid folder name')
    END;
END;

CREATE TRIGGER folders_before_update_validate
  BEFORE UPDATE on folders
BEGIN
  SELECT
    CASE
      WHEN NEW.name != UPPER(NEW.name) OR NEW.name GLOB '*[^A-Z0-9_-]*' THEN
        RAISE (ABORT, 'Invalid folder name')
      WHEN OLD.name = 'GENERAL' AND OLD.name != NEW.name THEN
        RAISE (ABORT, 'GENERAL folder is protected')
    END;
END;

CREATE TRIGGER folders_after_update_update_at
  AFTER UPDATE ON folders FOR EACH ROW
    WHEN NEW.update_at = OLD.update_at    --- avoid infinite loop
BEGIN
  UPDATE folders SET update_at=CURRENT_TIMESTAMP WHERE name=NEW.name;
END;

CREATE TRIGGER folders_before_delete_protect
  BEFORE DELETE on folders FOR EACH ROW
  WHEN OLD.name = 'GENERAL'
BEGIN
  SELECT RAISE (ABORT, 'GENERAL folder is protected');
END;

INSERT INTO folders (name, description, system, shownew, owner)
       VALUES ("GENERAL", "Default general bulletin folder.", 1, 1, "SYSTEM");
       VALUES ('GENERAL', 'Default general bulletin folder.', 1, 1, 'SYSTEM');

CREATE TABLE co_owners (
  folder      VARCHAR(25)  REFERENCES folders(name) ON UPDATE CASCADE,
  owner       VARCHAR(25)  REFERENCES users(login) ON UPDATE CASCADE,
  create_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
  update_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
  PRIMARY KEY (folder, owner)
) WITHOUT ROWID;

CREATE TRIGGER co_owners_after_update_update_at
  AFTER UPDATE ON co_owners FOR EACH ROW
    WHEN NEW.update_at = OLD.update_at    --- avoid infinite loop
BEGIN
  UPDATE co_owners SET update_at=CURRENT_TIMESTAMP WHERE folder=NEW.folder AND owner=NEW.owner;
END;
Loading