refactor: store migrator

This commit is contained in:
Steven 2024-08-16 08:07:30 +08:00
parent 1ae3afc0ba
commit 6e901fc940
82 changed files with 1494 additions and 402 deletions

View file

@ -58,16 +58,11 @@ var (
slog.Error("failed to create db driver", "error", err)
return
}
if err := dbDriver.Migrate(ctx); err != nil {
cancel()
slog.Error("failed to migrate database", "error", err)
return
}
storeInstance := store.New(dbDriver, instanceProfile)
if err := storeInstance.MigrateManually(ctx); err != nil {
if err := storeInstance.Migrate(ctx); err != nil {
cancel()
slog.Error("failed to migrate manually", "error", err)
slog.Error("failed to migrate", "error", err)
return
}

View file

@ -2,7 +2,7 @@ root = "."
tmp_dir = ".air"
[build]
bin = "./.air/memos --mode dev --public true"
bin = "./.air/memos --mode demo --public true"
cmd = "go build -o ./.air/memos ./bin/memos/main.go"
delay = 1000
exclude_dir = [".air", "web", "build"]

View file

@ -1,167 +0,0 @@
package mysql
import (
"context"
"embed"
"fmt"
"io/fs"
"regexp"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/usememos/memos/server/version"
"github.com/usememos/memos/store"
)
//go:embed migration
var migrationFS embed.FS
const (
latestSchemaFileName = "LATEST__SCHEMA.sql"
)
func (d *DB) Migrate(ctx context.Context) error {
if d.profile.IsDev() {
return d.nonProdMigrate(ctx)
}
return d.prodMigrate(ctx)
}
func (d *DB) nonProdMigrate(ctx context.Context) error {
rows, err := d.db.QueryContext(ctx, "SHOW TABLES")
if err != nil {
return errors.Errorf("failed to query database tables: %s", err)
}
if rows.Err() != nil {
return errors.Errorf("failed to query database tables: %s", err)
}
defer rows.Close()
var tables []string
for rows.Next() {
var table string
err := rows.Scan(&table)
if err != nil {
return errors.Errorf("failed to scan table name: %s", err)
}
tables = append(tables, table)
}
if len(tables) != 0 {
return nil
}
buf, err := migrationFS.ReadFile("migration/dev/" + latestSchemaFileName)
if err != nil {
return errors.Errorf("failed to read latest schema file: %s", err)
}
stmt := string(buf)
if _, err := d.db.ExecContext(ctx, stmt); err != nil {
return errors.Errorf("failed to exec SQL %s: %s", stmt, err)
}
return nil
}
func (d *DB) prodMigrate(ctx context.Context) error {
currentVersion := version.GetCurrentVersion(d.profile.Mode)
migrationHistoryList, err := d.FindMigrationHistoryList(ctx, &store.FindMigrationHistory{})
// If there is no migration history, we should apply the latest schema.
if err != nil || len(migrationHistoryList) == 0 {
buf, err := migrationFS.ReadFile("migration/prod/" + latestSchemaFileName)
if err != nil {
return errors.Errorf("failed to read latest schema file: %s", err)
}
if _, err := d.db.ExecContext(ctx, string(buf)); err != nil {
return errors.Errorf("failed to exec latest schema: %s", err)
}
if _, err := d.UpsertMigrationHistory(ctx, &store.UpsertMigrationHistory{
Version: currentVersion,
}); err != nil {
return errors.Wrap(err, "failed to upsert migration history")
}
return nil
}
migrationHistoryVersionList := []string{}
for _, migrationHistory := range migrationHistoryList {
migrationHistoryVersionList = append(migrationHistoryVersionList, migrationHistory.Version)
}
sort.Sort(version.SortVersion(migrationHistoryVersionList))
latestMigrationHistoryVersion := migrationHistoryVersionList[len(migrationHistoryVersionList)-1]
if !version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), latestMigrationHistoryVersion) {
return nil
}
fmt.Println("start to migrate database schema")
for _, minorVersion := range getMinorVersionList() {
normalizedVersion := minorVersion + ".0"
if version.IsVersionGreaterThan(normalizedVersion, latestMigrationHistoryVersion) && version.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
fmt.Println("applying migration of", normalizedVersion)
if err := d.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
return errors.Wrap(err, "failed to apply minor version migration")
}
}
}
fmt.Println("end migrate")
return nil
}
func (d *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion string) error {
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("migration/prod/%s/*.sql", minorVersion))
if err != nil {
return errors.Wrap(err, "failed to read ddl files")
}
sort.Strings(filenames)
// Loop over all migration files and execute them in order.
for _, filename := range filenames {
buf, err := migrationFS.ReadFile(filename)
if err != nil {
return errors.Wrapf(err, "failed to read minor version migration file, filename=%s", filename)
}
for _, stmt := range strings.Split(string(buf), ";") {
if strings.TrimSpace(stmt) == "" {
continue
}
if _, err := d.db.ExecContext(ctx, stmt); err != nil {
return errors.Wrapf(err, "migrate error: %s", stmt)
}
}
}
// Upsert the newest version to migration_history.
version := minorVersion + ".0"
if _, err = d.UpsertMigrationHistory(ctx, &store.UpsertMigrationHistory{Version: version}); err != nil {
return errors.Wrapf(err, "failed to upsert migration history with version: %s", version)
}
return nil
}
// minorDirRegexp is a regular expression for minor version directory.
var minorDirRegexp = regexp.MustCompile(`^migration/prod/[0-9]+\.[0-9]+$`)
func getMinorVersionList() []string {
minorVersionList := []string{}
if err := fs.WalkDir(migrationFS, "migration", func(path string, file fs.DirEntry, err error) error {
if err != nil {
return err
}
if file.IsDir() && minorDirRegexp.MatchString(path) {
minorVersionList = append(minorVersionList, file.Name())
}
return nil
}); err != nil {
panic(err)
}
sort.Sort(version.SortVersion(minorVersionList))
return minorVersionList
}

View file

@ -1,9 +1,7 @@
package mysql
import (
"context"
"database/sql"
"log/slog"
"github.com/go-sql-driver/mysql"
"github.com/pkg/errors"
@ -45,30 +43,8 @@ func (d *DB) GetDB() *sql.DB {
return d.db
}
func (d *DB) GetCurrentDBSize(ctx context.Context) (int64, error) {
query := "SELECT SUM(`data_length` + `index_length`) AS `size` " +
" FROM information_schema.TABLES" +
" WHERE `table_schema` = ?" +
" GROUP BY `table_schema`"
rows, err := d.db.QueryContext(ctx, query, d.config.DBName)
if err != nil {
slog.Error("Query db size error, make sure you have enough privilege", "error", err)
return 0, err
}
defer rows.Close()
var size int64
for rows.Next() {
if err := rows.Scan(&size); err != nil {
return 0, err
}
}
if rows.Err() != nil {
return 0, rows.Err()
}
return size, nil
func (d *DB) Type() string {
return "mysql"
}
func (d *DB) Close() error {

View file

@ -1,170 +0,0 @@
package postgres
import (
"context"
"embed"
"fmt"
"io/fs"
"regexp"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/usememos/memos/server/version"
"github.com/usememos/memos/store"
)
//go:embed migration
var migrationFS embed.FS
const (
latestSchemaFileName = "LATEST__SCHEMA.sql"
)
func (d *DB) Migrate(ctx context.Context) error {
if d.profile.IsDev() {
return d.nonProdMigrate(ctx)
}
return d.prodMigrate(ctx)
}
func (d *DB) nonProdMigrate(ctx context.Context) error {
rows, err := d.db.QueryContext(ctx, "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema';")
if err != nil {
return errors.Errorf("failed to query database tables: %s", err)
}
if rows.Err() != nil {
return errors.Errorf("failed to query database tables: %s", err)
}
defer rows.Close()
var tables []string
for rows.Next() {
var table string
err := rows.Scan(&table)
if err != nil {
return errors.Errorf("failed to scan table name: %s", err)
}
tables = append(tables, table)
}
if len(tables) != 0 {
return nil
}
buf, err := migrationFS.ReadFile("migration/dev/" + latestSchemaFileName)
if err != nil {
return errors.Errorf("failed to read latest schema file: %s", err)
}
stmt := string(buf)
if _, err := d.db.ExecContext(ctx, stmt); err != nil {
return errors.Errorf("failed to exec SQL %s: %s", stmt, err)
}
return nil
}
func (d *DB) prodMigrate(ctx context.Context) error {
currentVersion := version.GetCurrentVersion(d.profile.Mode)
migrationHistoryList, err := d.FindMigrationHistoryList(ctx, &store.FindMigrationHistory{})
// If there is no migration history, we should apply the latest schema.
if err != nil || len(migrationHistoryList) == 0 {
buf, err := migrationFS.ReadFile("migration/prod/" + latestSchemaFileName)
if err != nil {
return errors.Errorf("failed to read latest schema file: %s", err)
}
stmt := string(buf)
if _, err := d.db.ExecContext(ctx, stmt); err != nil {
return errors.Errorf("failed to exec SQL %s: %s", stmt, err)
}
if _, err := d.UpsertMigrationHistory(ctx, &store.UpsertMigrationHistory{
Version: currentVersion,
}); err != nil {
return errors.Wrap(err, "failed to upsert migration history")
}
return nil
}
migrationHistoryVersionList := []string{}
for _, migrationHistory := range migrationHistoryList {
migrationHistoryVersionList = append(migrationHistoryVersionList, migrationHistory.Version)
}
sort.Sort(version.SortVersion(migrationHistoryVersionList))
latestMigrationHistoryVersion := migrationHistoryVersionList[len(migrationHistoryVersionList)-1]
if !version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), latestMigrationHistoryVersion) {
return nil
}
fmt.Println("start migrate")
for _, minorVersion := range getMinorVersionList() {
normalizedVersion := minorVersion + ".0"
if version.IsVersionGreaterThan(normalizedVersion, latestMigrationHistoryVersion) && version.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
fmt.Println("applying migration for", normalizedVersion)
if err := d.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
return errors.Wrap(err, "failed to apply minor version migration")
}
}
}
fmt.Println("end migrate")
return nil
}
func (d *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion string) error {
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("migration/prod/%s/*.sql", minorVersion))
if err != nil {
return errors.Wrap(err, "failed to read ddl files")
}
sort.Strings(filenames)
// Loop over all migration files and execute them in order.
for _, filename := range filenames {
buf, err := migrationFS.ReadFile(filename)
if err != nil {
return errors.Wrapf(err, "failed to read minor version migration file, filename=%s", filename)
}
for _, stmt := range strings.Split(string(buf), ";") {
if strings.TrimSpace(stmt) == "" {
continue
}
if _, err := d.db.ExecContext(ctx, stmt); err != nil {
return errors.Wrapf(err, "migrate error: %s", stmt)
}
}
}
// Upsert the newest version to migration_history.
version := minorVersion + ".0"
if _, err = d.UpsertMigrationHistory(ctx, &store.UpsertMigrationHistory{Version: version}); err != nil {
return errors.Wrapf(err, "failed to upsert migration history with version: %s", version)
}
return nil
}
// minorDirRegexp is a regular expression for minor version directory.
var minorDirRegexp = regexp.MustCompile(`^migration/prod/[0-9]+\.[0-9]+$`)
func getMinorVersionList() []string {
minorVersionList := []string{}
if err := fs.WalkDir(migrationFS, "migration", func(path string, file fs.DirEntry, err error) error {
if err != nil {
return err
}
if file.IsDir() && minorDirRegexp.MatchString(path) {
minorVersionList = append(minorVersionList, file.Name())
}
return nil
}); err != nil {
panic(err)
}
sort.Sort(version.SortVersion(minorVersionList))
return minorVersionList
}

View file

@ -1,7 +1,6 @@
package postgres
import (
"context"
"database/sql"
"log"
@ -44,8 +43,8 @@ func (d *DB) GetDB() *sql.DB {
return d.db
}
func (*DB) GetCurrentDBSize(context.Context) (int64, error) {
return 0, errors.New("unimplemented")
func (d *DB) Type() string {
return "postgres"
}
func (d *DB) Close() error {

View file

@ -1,13 +1,9 @@
package sqlite
import (
"context"
"database/sql"
"os"
"github.com/pkg/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
// Import the SQLite driver.
_ "modernc.org/sqlite"
@ -58,13 +54,8 @@ func (d *DB) GetDB() *sql.DB {
return d.db
}
func (d *DB) GetCurrentDBSize(context.Context) (int64, error) {
fi, err := os.Stat(d.profile.DSN)
if err != nil {
return 0, status.Errorf(codes.Internal, "failed to get file info: %v", err)
}
return fi.Size(), nil
func (d *DB) Type() string {
return "sqlite"
}
func (d *DB) Close() error {

View file

@ -11,10 +11,9 @@ type Driver interface {
GetDB() *sql.DB
Close() error
Migrate(ctx context.Context) error
// current file is driver
GetCurrentDBSize(ctx context.Context) (int64, error)
// Type returns the type of the driver.
// Supported types are: sqlite, mysql, postgres.
Type() string
// MigrationHistory model related methods.
FindMigrationHistoryList(ctx context.Context, find *FindMigrationHistory) ([]*MigrationHistory, error)

View file

@ -0,0 +1,146 @@
-- migration_history
CREATE TABLE migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
-- system_setting
CREATE TABLE system_setting (
name TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
UNIQUE(name)
);
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
avatar_url TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT ''
);
CREATE INDEX idx_user_username ON user (username);
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE(user_id, key)
);
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid TEXT NOT NULL UNIQUE,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
tags TEXT NOT NULL DEFAULT '[]',
payload TEXT NOT NULL DEFAULT '{}'
);
CREATE INDEX idx_memo_creator_id ON memo (creator_id);
CREATE INDEX idx_memo_content ON memo (content);
CREATE INDEX idx_memo_visibility ON memo (visibility);
CREATE INDEX idx_memo_tags ON memo (tags);
-- memo_organizer
CREATE TABLE memo_organizer (
memo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
UNIQUE(memo_id, user_id)
);
-- memo_relation
CREATE TABLE memo_relation (
memo_id INTEGER NOT NULL,
related_memo_id INTEGER NOT NULL,
type TEXT NOT NULL,
UNIQUE(memo_id, related_memo_id, type)
);
-- resource
CREATE TABLE resource (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid TEXT NOT NULL UNIQUE,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
filename TEXT NOT NULL DEFAULT '',
blob BLOB DEFAULT NULL,
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
memo_id INTEGER,
storage_type TEXT NOT NULL DEFAULT '',
reference TEXT NOT NULL DEFAULT '',
payload TEXT NOT NULL DEFAULT '{}'
);
CREATE INDEX idx_resource_creator_id ON resource (creator_id);
CREATE INDEX idx_resource_memo_id ON resource (memo_id);
-- activity
CREATE TABLE activity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
type TEXT NOT NULL DEFAULT '',
level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',
payload TEXT NOT NULL DEFAULT '{}'
);
-- idp
CREATE TABLE idp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
identifier_filter TEXT NOT NULL DEFAULT '',
config TEXT NOT NULL DEFAULT '{}'
);
-- inbox
CREATE TABLE inbox (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
sender_id INTEGER NOT NULL,
receiver_id INTEGER NOT NULL,
status TEXT NOT NULL,
message TEXT NOT NULL DEFAULT '{}'
);
-- webhook
CREATE TABLE webhook (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
creator_id INTEGER NOT NULL,
name TEXT NOT NULL,
url TEXT NOT NULL
);
CREATE INDEX idx_webhook_creator_id ON webhook (creator_id);
-- reaction
CREATE TABLE reaction (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
creator_id INTEGER NOT NULL,
content_id TEXT NOT NULL,
reaction_type TEXT NOT NULL,
UNIQUE(creator_id, content_id, reaction_type)
);

View file

@ -0,0 +1,9 @@
-- activity
CREATE TABLE activity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
type TEXT NOT NULL DEFAULT '',
level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',
payload TEXT NOT NULL DEFAULT '{}'
);

View file

@ -0,0 +1,4 @@
ALTER TABLE
user
ADD
COLUMN avatar_url TEXT NOT NULL DEFAULT '';

View file

@ -0,0 +1,8 @@
-- idp
CREATE TABLE idp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
identifier_filter TEXT NOT NULL DEFAULT '',
config TEXT NOT NULL DEFAULT '{}'
);

View file

@ -0,0 +1,7 @@
-- storage
CREATE TABLE storage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
config TEXT NOT NULL DEFAULT '{}'
);

View file

@ -0,0 +1,6 @@
UPDATE
user_setting
SET
key = 'memo-visibility'
WHERE
key = 'memoVisibility';

View file

@ -0,0 +1,69 @@
UPDATE
system_setting
SET
name = 'server-id'
WHERE
name = 'serverId';
UPDATE
system_setting
SET
name = 'secret-session'
WHERE
name = 'secretSessionName';
UPDATE
system_setting
SET
name = 'allow-signup'
WHERE
name = 'allowSignUp';
UPDATE
system_setting
SET
name = 'disable-public-memos'
WHERE
name = 'disablePublicMemos';
UPDATE
system_setting
SET
name = 'additional-style'
WHERE
name = 'additionalStyle';
UPDATE
system_setting
SET
name = 'additional-script'
WHERE
name = 'additionalScript';
UPDATE
system_setting
SET
name = 'customized-profile'
WHERE
name = 'customizedProfile';
UPDATE
system_setting
SET
name = 'storage-service-id'
WHERE
name = 'storageServiceId';
UPDATE
system_setting
SET
name = 'local-storage-path'
WHERE
name = 'localStoragePath';
UPDATE
system_setting
SET
name = 'openai-config'
WHERE
name = 'openAIConfig';

View file

@ -0,0 +1,4 @@
ALTER TABLE
resource
ADD
COLUMN internal_path TEXT NOT NULL DEFAULT '';

View file

@ -0,0 +1,18 @@
ALTER TABLE
resource
ADD
COLUMN public_id TEXT NOT NULL DEFAULT '';
CREATE UNIQUE INDEX resource_id_public_id_unique_index ON resource (id, public_id);
UPDATE
resource
SET
public_id = printf (
'%s-%s-%s-%s-%s',
lower(hex(randomblob(4))),
lower(hex(randomblob(2))),
lower(hex(randomblob(2))),
lower(hex(randomblob(2))),
lower(hex(randomblob(6)))
);

View file

@ -0,0 +1,7 @@
-- memo_relation
CREATE TABLE memo_relation (
memo_id INTEGER NOT NULL,
related_memo_id INTEGER NOT NULL,
type TEXT NOT NULL,
UNIQUE(memo_id, related_memo_id, type)
);

View file

@ -0,0 +1,22 @@
DROP TABLE IF EXISTS memo_organizer_temp;
CREATE TABLE memo_organizer_temp (
memo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
UNIQUE(memo_id, user_id)
);
INSERT INTO
memo_organizer_temp (memo_id, user_id, pinned)
SELECT
memo_id,
user_id,
pinned
FROM
memo_organizer;
DROP TABLE memo_organizer;
ALTER TABLE
memo_organizer_temp RENAME TO memo_organizer;

View file

@ -0,0 +1,25 @@
DROP TABLE IF EXISTS resource_temp;
CREATE TABLE resource_temp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
filename TEXT NOT NULL DEFAULT '',
blob BLOB DEFAULT NULL,
external_link TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
internal_path TEXT NOT NULL DEFAULT ''
);
INSERT INTO
resource_temp (id, creator_id, created_ts, updated_ts, filename, blob, external_link, type, size, internal_path)
SELECT
id, creator_id, created_ts, updated_ts, filename, blob, external_link, type, size, internal_path
FROM
resource;
DROP TABLE resource;
ALTER TABLE resource_temp RENAME TO resource;

View file

@ -0,0 +1,5 @@
CREATE INDEX IF NOT EXISTS idx_user_username ON user (username);
CREATE INDEX IF NOT EXISTS idx_memo_creator_id ON memo (creator_id);
CREATE INDEX IF NOT EXISTS idx_memo_content ON memo (content);
CREATE INDEX IF NOT EXISTS idx_memo_visibility ON memo (visibility);
CREATE INDEX IF NOT EXISTS idx_resource_creator_id ON resource (creator_id);

View file

@ -0,0 +1,25 @@
DROP TABLE IF EXISTS user_temp;
CREATE TABLE user_temp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
avatar_url TEXT NOT NULL DEFAULT ''
);
INSERT INTO
user_temp (id, created_ts, updated_ts, row_status, username, role, email, nickname, password_hash, avatar_url)
SELECT
id, created_ts, updated_ts, row_status, username, role, email, nickname, password_hash, avatar_url
FROM
user;
DROP TABLE user;
ALTER TABLE user_temp RENAME TO user;

View file

@ -0,0 +1,13 @@
ALTER TABLE resource ADD COLUMN memo_id INTEGER;
UPDATE resource
SET memo_id = (
SELECT memo_id
FROM memo_resource
WHERE resource.id = memo_resource.resource_id
LIMIT 1
);
CREATE INDEX idx_resource_memo_id ON resource (memo_id);
DROP TABLE IF EXISTS memo_resource;

View file

@ -0,0 +1 @@
DROP TABLE IF EXISTS shortcut;

View file

@ -0,0 +1,9 @@
-- inbox
CREATE TABLE inbox (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
sender_id INTEGER NOT NULL,
receiver_id INTEGER NOT NULL,
status TEXT NOT NULL,
message TEXT NOT NULL DEFAULT '{}'
);

View file

@ -0,0 +1 @@
DELETE FROM activity;

View file

@ -0,0 +1,12 @@
-- webhook
CREATE TABLE webhook (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
creator_id INTEGER NOT NULL,
name TEXT NOT NULL,
url TEXT NOT NULL
);
CREATE INDEX idx_webhook_creator_id ON webhook (creator_id);

View file

@ -0,0 +1,4 @@
UPDATE user_setting SET key = 'USER_SETTING_LOCALE', value = REPLACE(value, '"', '') WHERE key = 'locale';
UPDATE user_setting SET key = 'USER_SETTING_APPEARANCE', value = REPLACE(value, '"', '') WHERE key = 'appearance';
UPDATE user_setting SET key = 'USER_SETTING_MEMO_VISIBILITY', value = REPLACE(value, '"', '') WHERE key = 'memo-visibility';
UPDATE user_setting SET key = 'USER_SETTING_TELEGRAM_USER_ID', value = REPLACE(value, '"', '') WHERE key = 'telegram-user-id';

View file

@ -0,0 +1,11 @@
ALTER TABLE memo ADD COLUMN resource_name TEXT NOT NULL DEFAULT "";
UPDATE memo SET resource_name = lower(hex(randomblob(8)));
CREATE UNIQUE INDEX idx_memo_resource_name ON memo (resource_name);
ALTER TABLE resource ADD COLUMN resource_name TEXT NOT NULL DEFAULT "";
UPDATE resource SET resource_name = lower(hex(randomblob(8)));
CREATE UNIQUE INDEX idx_resource_resource_name ON resource (resource_name);

View file

@ -0,0 +1,60 @@
-- change user role field from "OWNER"/"USER" to "HOST"/"USER".
PRAGMA foreign_keys = off;
DROP TABLE IF EXISTS _user_old;
ALTER TABLE
user RENAME TO _user_old;
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
INSERT INTO
user (
id,
created_ts,
updated_ts,
row_status,
email,
name,
password_hash,
open_id
)
SELECT
id,
created_ts,
updated_ts,
row_status,
email,
name,
password_hash,
open_id
FROM
_user_old;
UPDATE
user
SET
role = 'HOST'
WHERE
id IN (
SELECT
id
FROM
_user_old
WHERE
role = 'OWNER'
);
DROP TABLE IF EXISTS _user_old;
PRAGMA foreign_keys = on;

View file

@ -0,0 +1,4 @@
ALTER TABLE
memo
ADD
COLUMN visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PRIVATE')) DEFAULT 'PRIVATE';

View file

@ -0,0 +1,9 @@
-- reaction
CREATE TABLE reaction (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
creator_id INTEGER NOT NULL,
content_id TEXT NOT NULL,
reaction_type TEXT NOT NULL,
UNIQUE(creator_id, content_id, reaction_type)
);

View file

@ -0,0 +1 @@
ALTER TABLE user ADD COLUMN description TEXT NOT NULL DEFAULT "";

View file

@ -0,0 +1,3 @@
ALTER TABLE memo RENAME COLUMN resource_name TO uid;
ALTER TABLE resource RENAME COLUMN resource_name TO uid;

View file

@ -0,0 +1,17 @@
ALTER TABLE resource ADD COLUMN storage_type TEXT NOT NULL DEFAULT '';
ALTER TABLE resource ADD COLUMN reference TEXT NOT NULL DEFAULT '';
ALTER TABLE resource ADD COLUMN payload TEXT NOT NULL DEFAULT '{}';
UPDATE resource
SET storage_type = 'LOCAL', reference = internal_path
WHERE internal_path IS NOT NULL AND internal_path != '';
UPDATE resource
SET storage_type = 'EXTERNAL', reference = external_link
WHERE external_link IS NOT NULL AND external_link != '';
ALTER TABLE resource DROP COLUMN internal_path;
ALTER TABLE resource DROP COLUMN external_link;

View file

@ -0,0 +1,3 @@
ALTER TABLE memo ADD COLUMN tags TEXT NOT NULL DEFAULT '[]';
CREATE INDEX idx_memo_tags ON memo (tags);

View file

@ -0,0 +1 @@
ALTER TABLE memo ADD COLUMN payload TEXT NOT NULL DEFAULT '{}';

View file

@ -0,0 +1 @@
DROP TABLE tag;

View file

@ -0,0 +1,43 @@
-- change memo visibility field from "PRIVATE"/"PUBLIC" to "PRIVATE"/"PROTECTED"/"PUBLIC".
PRAGMA foreign_keys = off;
DROP TABLE IF EXISTS _memo_old;
ALTER TABLE
memo RENAME TO _memo_old;
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO
memo (
id,
creator_id,
created_ts,
updated_ts,
row_status,
content,
visibility
)
SELECT
id,
creator_id,
created_ts,
updated_ts,
row_status,
content,
visibility
FROM
_memo_old;
DROP TABLE IF EXISTS _memo_old;
PRAGMA foreign_keys = on;

View file

@ -0,0 +1,9 @@
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id);

View file

@ -0,0 +1,217 @@
PRAGMA foreign_keys = off;
DROP TABLE IF EXISTS _user_old;
ALTER TABLE
user RENAME TO _user_old;
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
INSERT INTO
user
SELECT
*
FROM
_user_old;
DROP TABLE IF EXISTS _user_old;
DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_user_modification_time`
AFTER
UPDATE
ON `user` FOR EACH ROW BEGIN
UPDATE
`user`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _memo_old;
ALTER TABLE
memo RENAME TO _memo_old;
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO
memo
SELECT
*
FROM
_memo_old;
DROP TABLE IF EXISTS _memo_old;
DROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_memo_modification_time`
AFTER
UPDATE
ON `memo` FOR EACH ROW BEGIN
UPDATE
`memo`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _memo_organizer_old;
ALTER TABLE
memo_organizer RENAME TO _memo_organizer_old;
-- memo_organizer
CREATE TABLE memo_organizer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
FOREIGN KEY(memo_id) REFERENCES memo(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE,
UNIQUE(memo_id, user_id)
);
INSERT INTO
memo_organizer
SELECT
*
FROM
_memo_organizer_old;
DROP TABLE IF EXISTS _memo_organizer_old;
DROP TABLE IF EXISTS _shortcut_old;
ALTER TABLE
shortcut RENAME TO _shortcut_old;
-- shortcut
CREATE TABLE shortcut (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
title TEXT NOT NULL DEFAULT '',
payload TEXT NOT NULL DEFAULT '{}',
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO
shortcut
SELECT
*
FROM
_shortcut_old;
DROP TABLE IF EXISTS _shortcut_old;
DROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_shortcut_modification_time`
AFTER
UPDATE
ON `shortcut` FOR EACH ROW BEGIN
UPDATE
`shortcut`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _resource_old;
ALTER TABLE
resource RENAME TO _resource_old;
-- resource
CREATE TABLE resource (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
filename TEXT NOT NULL DEFAULT '',
blob BLOB DEFAULT NULL,
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO
resource
SELECT
*
FROM
_resource_old;
DROP TABLE IF EXISTS _resource_old;
DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_resource_modification_time`
AFTER
UPDATE
ON `resource` FOR EACH ROW BEGIN
UPDATE
`resource`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _user_setting_old;
ALTER TABLE
user_setting RENAME TO _user_setting_old;
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE,
UNIQUE(user_id, key)
);
INSERT INTO
user_setting
SELECT
*
FROM
_user_setting_old;
DROP TABLE IF EXISTS _user_setting_old;
PRAGMA foreign_keys = on;

View file

@ -0,0 +1,10 @@
-- memo_resource
CREATE TABLE memo_resource (
memo_id INTEGER NOT NULL,
resource_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
FOREIGN KEY(memo_id) REFERENCES memo(id) ON DELETE CASCADE,
FOREIGN KEY(resource_id) REFERENCES resource(id) ON DELETE CASCADE,
UNIQUE(memo_id, resource_id)
);

View file

@ -0,0 +1,7 @@
-- system_setting
CREATE TABLE system_setting (
name TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
UNIQUE(name)
);

View file

@ -0,0 +1,4 @@
ALTER TABLE
resource
ADD
COLUMN external_link TEXT NOT NULL DEFAULT '';

View file

@ -0,0 +1,59 @@
DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_user_modification_time`
AFTER
UPDATE
ON `user` FOR EACH ROW BEGIN
UPDATE
`user`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_memo_modification_time`
AFTER
UPDATE
ON `memo` FOR EACH ROW BEGIN
UPDATE
`memo`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_shortcut_modification_time`
AFTER
UPDATE
ON `shortcut` FOR EACH ROW BEGIN
UPDATE
`shortcut`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_resource_modification_time`
AFTER
UPDATE
ON `resource` FOR EACH ROW BEGIN
UPDATE
`resource`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;

View file

@ -0,0 +1,191 @@
PRAGMA foreign_keys = off;
DROP TABLE IF EXISTS _user_old;
ALTER TABLE
user RENAME TO _user_old;
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
INSERT INTO
user
SELECT
*
FROM
_user_old;
DROP TABLE IF EXISTS _user_old;
DROP TABLE IF EXISTS _memo_old;
ALTER TABLE
memo RENAME TO _memo_old;
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE'
);
INSERT INTO
memo
SELECT
*
FROM
_memo_old;
DROP TABLE IF EXISTS _memo_old;
DROP TABLE IF EXISTS _memo_organizer_old;
ALTER TABLE
memo_organizer RENAME TO _memo_organizer_old;
-- memo_organizer
CREATE TABLE memo_organizer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
UNIQUE(memo_id, user_id)
);
INSERT INTO
memo_organizer
SELECT
*
FROM
_memo_organizer_old;
DROP TABLE IF EXISTS _memo_organizer_old;
DROP TABLE IF EXISTS _shortcut_old;
ALTER TABLE
shortcut RENAME TO _shortcut_old;
-- shortcut
CREATE TABLE shortcut (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
title TEXT NOT NULL DEFAULT '',
payload TEXT NOT NULL DEFAULT '{}'
);
INSERT INTO
shortcut
SELECT
*
FROM
_shortcut_old;
DROP TABLE IF EXISTS _shortcut_old;
DROP TABLE IF EXISTS _resource_old;
ALTER TABLE
resource RENAME TO _resource_old;
-- resource
CREATE TABLE resource (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
filename TEXT NOT NULL DEFAULT '',
blob BLOB DEFAULT NULL,
external_link TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0
);
INSERT INTO
resource (
id,
creator_id,
created_ts,
updated_ts,
filename,
blob,
external_link,
type,
size
)
SELECT
id,
creator_id,
created_ts,
updated_ts,
filename,
blob,
external_link,
type,
size
FROM
_resource_old;
DROP TABLE IF EXISTS _resource_old;
DROP TABLE IF EXISTS _user_setting_old;
ALTER TABLE
user_setting RENAME TO _user_setting_old;
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE(user_id, key)
);
INSERT INTO
user_setting
SELECT
*
FROM
_user_setting_old;
DROP TABLE IF EXISTS _user_setting_old;
DROP TABLE IF EXISTS _memo_resource_old;
ALTER TABLE
memo_resource RENAME TO _memo_resource_old;
-- memo_resource
CREATE TABLE memo_resource (
memo_id INTEGER NOT NULL,
resource_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(memo_id, resource_id)
);
INSERT INTO
memo_resource
SELECT
*
FROM
_memo_resource_old;
DROP TABLE IF EXISTS _memo_resource_old;

View file

@ -0,0 +1,7 @@
DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`;
DROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`;
DROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`;
DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`;

View file

@ -0,0 +1,5 @@
-- migration_history
CREATE TABLE IF NOT EXISTS migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);

View file

@ -0,0 +1,50 @@
-- add column username TEXT NOT NULL UNIQUE
-- rename column name to nickname
-- add role `ADMIN`
DROP TABLE IF EXISTS _user_old;
ALTER TABLE
user RENAME TO _user_old;
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
INSERT INTO
user (
id,
created_ts,
updated_ts,
row_status,
username,
role,
email,
nickname,
password_hash,
open_id
)
SELECT
id,
created_ts,
updated_ts,
row_status,
email,
role,
email,
name,
password_hash,
open_id
FROM
_user_old;
DROP TABLE IF EXISTS _user_old;

View file

@ -0,0 +1,6 @@
-- tag
CREATE TABLE tag (
name TEXT NOT NULL,
creator_id INTEGER NOT NULL,
UNIQUE(name, creator_id)
);

View file

@ -0,0 +1,146 @@
-- migration_history
CREATE TABLE migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
-- system_setting
CREATE TABLE system_setting (
name TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
UNIQUE(name)
);
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
avatar_url TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT ''
);
CREATE INDEX idx_user_username ON user (username);
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE(user_id, key)
);
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid TEXT NOT NULL UNIQUE,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
tags TEXT NOT NULL DEFAULT '[]',
payload TEXT NOT NULL DEFAULT '{}'
);
CREATE INDEX idx_memo_creator_id ON memo (creator_id);
CREATE INDEX idx_memo_content ON memo (content);
CREATE INDEX idx_memo_visibility ON memo (visibility);
CREATE INDEX idx_memo_tags ON memo (tags);
-- memo_organizer
CREATE TABLE memo_organizer (
memo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
UNIQUE(memo_id, user_id)
);
-- memo_relation
CREATE TABLE memo_relation (
memo_id INTEGER NOT NULL,
related_memo_id INTEGER NOT NULL,
type TEXT NOT NULL,
UNIQUE(memo_id, related_memo_id, type)
);
-- resource
CREATE TABLE resource (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid TEXT NOT NULL UNIQUE,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
filename TEXT NOT NULL DEFAULT '',
blob BLOB DEFAULT NULL,
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
memo_id INTEGER,
storage_type TEXT NOT NULL DEFAULT '',
reference TEXT NOT NULL DEFAULT '',
payload TEXT NOT NULL DEFAULT '{}'
);
CREATE INDEX idx_resource_creator_id ON resource (creator_id);
CREATE INDEX idx_resource_memo_id ON resource (memo_id);
-- activity
CREATE TABLE activity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
type TEXT NOT NULL DEFAULT '',
level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',
payload TEXT NOT NULL DEFAULT '{}'
);
-- idp
CREATE TABLE idp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
identifier_filter TEXT NOT NULL DEFAULT '',
config TEXT NOT NULL DEFAULT '{}'
);
-- inbox
CREATE TABLE inbox (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
sender_id INTEGER NOT NULL,
receiver_id INTEGER NOT NULL,
status TEXT NOT NULL,
message TEXT NOT NULL DEFAULT '{}'
);
-- webhook
CREATE TABLE webhook (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
creator_id INTEGER NOT NULL,
name TEXT NOT NULL,
url TEXT NOT NULL
);
CREATE INDEX idx_webhook_creator_id ON webhook (creator_id);
-- reaction
CREATE TABLE reaction (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
creator_id INTEGER NOT NULL,
content_id TEXT NOT NULL,
reaction_type TEXT NOT NULL,
UNIQUE(creator_id, content_id, reaction_type)
);

View file

@ -1 +1,198 @@
package store
import (
"context"
"embed"
"fmt"
"io/fs"
"log/slog"
"regexp"
"sort"
"github.com/pkg/errors"
"github.com/usememos/memos/server/version"
)
//go:embed migration
var migrationFS embed.FS
//go:embed seed
var seedFS embed.FS
// Migrate applies the latest schema to the database.
func (s *Store) Migrate(ctx context.Context) error {
if err := s.preMigrate(ctx); err != nil {
return errors.Wrap(err, "failed to pre-migrate")
}
if s.Profile.Mode == "prod" {
migrationHistoryList, err := s.driver.FindMigrationHistoryList(ctx, &FindMigrationHistory{})
if err != nil {
return errors.Wrap(err, "failed to find migration history")
}
if len(migrationHistoryList) == 0 {
return errors.Errorf("no migration history found")
}
migrationHistoryVersions := []string{}
for _, migrationHistory := range migrationHistoryList {
migrationHistoryVersions = append(migrationHistoryVersions, migrationHistory.Version)
}
sort.Sort(version.SortVersion(migrationHistoryVersions))
latestMigrationHistoryVersion := migrationHistoryVersions[len(migrationHistoryVersions)-1]
currentVersion := version.GetCurrentVersion(s.Profile.Mode)
if version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), latestMigrationHistoryVersion) {
minorVersionList := s.getMinorVersionList()
fmt.Println("start migration")
for _, minorVersion := range minorVersionList {
normalizedVersion := minorVersion + ".0"
if version.IsVersionGreaterThan(normalizedVersion, latestMigrationHistoryVersion) && version.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
fmt.Println("applying migration for", normalizedVersion)
if err := s.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
return errors.Wrap(err, "failed to apply minor version migration")
}
}
}
fmt.Println("end migrate")
}
} else {
// In demo mode, we should seed the database.
if s.Profile.Mode == "demo" {
if err := s.seed(ctx); err != nil {
return errors.Wrap(err, "failed to seed")
}
}
}
return nil
}
func (s *Store) preMigrate(ctx context.Context) error {
migrationHistoryList, err := s.driver.FindMigrationHistoryList(ctx, &FindMigrationHistory{})
// If there is no migration history, we should apply the latest schema.
if err != nil || len(migrationHistoryList) == 0 {
if err != nil {
slog.Error("failed to find migration history", "error", err)
}
fileName := s.getMigrationBasePath() + latestSchemaFileName
bytes, err := migrationFS.ReadFile(fileName)
if err != nil {
return errors.Errorf("failed to read latest schema file: %s", err)
}
if err := s.execute(ctx, string(bytes)); err != nil {
return errors.Errorf("failed to exec SQL file %s, err %s", fileName, err)
}
if _, err := s.driver.UpsertMigrationHistory(ctx, &UpsertMigrationHistory{
Version: version.GetCurrentVersion(s.Profile.Mode),
}); err != nil {
return errors.Wrap(err, "failed to upsert migration history")
}
}
return nil
}
func (s *Store) getMigrationBasePath() string {
mode := "dev"
if s.Profile.Mode == "prod" {
mode = "prod"
}
return fmt.Sprintf("migration/%s/%s/", s.driver.Type(), mode)
}
func (s *Store) getSeedBasePath() string {
return fmt.Sprintf("seed/%s/", s.driver.Type())
}
const (
latestSchemaFileName = "LATEST__SCHEMA.sql"
)
func (s *Store) applyMigrationForMinorVersion(ctx context.Context, minorVersion string) error {
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("%s%s/*.sql", s.getMigrationBasePath(), minorVersion))
if err != nil {
return errors.Wrap(err, "failed to read migration files")
}
sort.Strings(filenames)
migrationStmt := ""
// Loop over all migration files and execute them in order.
for _, filename := range filenames {
buf, err := migrationFS.ReadFile(filename)
if err != nil {
return errors.Wrapf(err, "failed to read minor version migration file, filename=%s", filename)
}
stmt := string(buf)
migrationStmt += stmt
if err := s.execute(ctx, stmt); err != nil {
return errors.Wrapf(err, "migrate error: %s", stmt)
}
}
// Upsert the newest version to migration_history.
version := minorVersion + ".0"
if _, err = s.driver.UpsertMigrationHistory(ctx, &UpsertMigrationHistory{
Version: version,
}); err != nil {
return errors.Wrapf(err, "failed to upsert migration history with version: %s", version)
}
return nil
}
func (s *Store) seed(ctx context.Context) error {
// Only seed for SQLite.
if s.driver.Type() != "sqlite" {
return nil
}
filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s*.sql", s.getSeedBasePath()))
if err != nil {
return errors.Wrap(err, "failed to read seed files")
}
sort.Strings(filenames)
// Loop over all seed files and execute them in order.
for _, filename := range filenames {
bytes, err := seedFS.ReadFile(filename)
if err != nil {
return errors.Wrapf(err, "failed to read seed file, filename=%s", filename)
}
if err := s.execute(ctx, string(bytes)); err != nil {
return errors.Wrapf(err, "seed error: %s", filename)
}
}
return nil
}
// execute runs a single SQL statement within a transaction.
func (s *Store) execute(ctx context.Context, stmt string) error {
tx, err := s.driver.GetDB().Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, stmt); err != nil {
return errors.Wrap(err, "failed to execute statement")
}
return tx.Commit()
}
func (s *Store) getMinorVersionList() []string {
var minorDirRegexp = regexp.MustCompile(fmt.Sprintf(`^%s[0-9]+\.[0-9]+$`, s.getMigrationBasePath()))
minorVersionList := []string{}
if err := fs.WalkDir(migrationFS, "migration", func(path string, file fs.DirEntry, err error) error {
if err != nil {
return err
}
if file.IsDir() && minorDirRegexp.MatchString(path) {
minorVersionList = append(minorVersionList, file.Name())
}
return nil
}); err != nil {
panic(err)
}
sort.Sort(version.SortVersion(minorVersionList))
return minorVersionList
}

View file

@ -147,7 +147,7 @@ func (s *Store) DeleteResource(ctx context.Context, delete *DeleteResource) erro
}
return nil
}(); err != nil {
slog.Warn("Failed to delete s3 object", err)
slog.Warn("Failed to delete s3 object", slog.Any("err", err))
}
}

View file

@ -0,0 +1,12 @@
DELETE FROM system_setting;
DELETE FROM user;
DELETE FROM user_setting;
DELETE FROM memo;
DELETE FROM memo_organizer;
DELETE FROM memo_relation;
DELETE FROM resource;
DELETE FROM activity;
DELETE FROM idp;
DELETE FROM inbox;
DELETE FROM webhook;
DELETE FROM reaction;

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,6 @@
package store
import (
"context"
"sync"
"github.com/usememos/memos/server/profile"
@ -25,14 +24,6 @@ func New(driver Driver, profile *profile.Profile) *Store {
}
}
func (*Store) MigrateManually(context.Context) error {
return nil
}
func (s *Store) Close() error {
return s.driver.Close()
}
func (s *Store) GetCurrentDBSize(ctx context.Context) (int64, error) {
return s.driver.GetCurrentDBSize(ctx)
}

View file

@ -21,11 +21,11 @@ func NewTestingStore(ctx context.Context, t *testing.T) *store.Store {
fmt.Printf("failed to create db driver, error: %+v\n", err)
}
resetTestingDB(ctx, profile, dbDriver)
if err := dbDriver.Migrate(ctx); err != nil {
fmt.Printf("failed to migrate db, error: %+v\n", err)
}
store := store.New(dbDriver, profile)
if err := store.Migrate(ctx); err != nil {
fmt.Printf("failed to migrate db, error: %+v\n", err)
}
return store
}