111 lines
4.2 KiB
C#
111 lines
4.2 KiB
C#
using Microsoft.Data.Sqlite;
|
||
|
||
namespace Wiki.Api;
|
||
|
||
/// <summary>Подключения к SQLite и схема. Файл базы лежит на постоянном томе (Storage:DataDir).</summary>
|
||
public sealed class Db
|
||
{
|
||
public string DataDir { get; }
|
||
public string ImagesDir { get; }
|
||
private readonly string _cs;
|
||
|
||
public Db(IConfiguration cfg)
|
||
{
|
||
DataDir = Path.GetFullPath(cfg["Storage:DataDir"] ?? "data");
|
||
ImagesDir = Path.Combine(DataDir, "images");
|
||
Directory.CreateDirectory(ImagesDir);
|
||
_cs = new SqliteConnectionStringBuilder
|
||
{
|
||
DataSource = Path.Combine(DataDir, "wiki.db"),
|
||
Cache = SqliteCacheMode.Shared,
|
||
ForeignKeys = true,
|
||
}.ToString();
|
||
}
|
||
|
||
public SqliteConnection Open()
|
||
{
|
||
var c = new SqliteConnection(_cs);
|
||
c.Open();
|
||
using var cmd = c.CreateCommand();
|
||
cmd.CommandText = "PRAGMA busy_timeout=5000;";
|
||
cmd.ExecuteNonQuery();
|
||
return c;
|
||
}
|
||
|
||
public void Init()
|
||
{
|
||
using var c = Open();
|
||
Exec(c, """
|
||
PRAGMA journal_mode=WAL;
|
||
|
||
CREATE TABLE IF NOT EXISTS meta(
|
||
key TEXT PRIMARY KEY,
|
||
value TEXT NOT NULL
|
||
);
|
||
INSERT OR IGNORE INTO meta(key, value) VALUES ('created_at', strftime('%Y-%m-%dT%H:%M:%fZ','now'));
|
||
INSERT OR IGNORE INTO meta(key, value) VALUES ('schema', '1');
|
||
|
||
-- Папки и страницы одной таблицей: kind = 'folder' | 'page'.
|
||
CREATE TABLE IF NOT EXISTS nodes(
|
||
id TEXT PRIMARY KEY,
|
||
workspace_id TEXT NOT NULL,
|
||
parent_id TEXT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
||
kind TEXT NOT NULL CHECK (kind IN ('folder','page')),
|
||
title TEXT NOT NULL,
|
||
content TEXT NOT NULL DEFAULT '',
|
||
version INTEGER NOT NULL DEFAULT 1,
|
||
created_at TEXT NOT NULL,
|
||
created_by TEXT NOT NULL,
|
||
updated_at TEXT NOT NULL,
|
||
updated_by TEXT NOT NULL,
|
||
updated_by_name TEXT NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS ix_nodes_ws_parent ON nodes(workspace_id, parent_id);
|
||
|
||
CREATE TABLE IF NOT EXISTS images(
|
||
id TEXT PRIMARY KEY,
|
||
workspace_id TEXT NOT NULL,
|
||
page_id TEXT NULL,
|
||
mime TEXT NOT NULL,
|
||
size INTEGER NOT NULL,
|
||
file_name TEXT NOT NULL,
|
||
created_at TEXT NOT NULL,
|
||
created_by TEXT NOT NULL
|
||
);
|
||
|
||
-- Полнотекстовый поиск по названию и тексту (external content на nodes).
|
||
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
|
||
title, content,
|
||
content='nodes', content_rowid='rowid',
|
||
tokenize='unicode61 remove_diacritics 2'
|
||
);
|
||
CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes BEGIN
|
||
INSERT INTO nodes_fts(rowid, title, content) VALUES (new.rowid, new.title, new.content);
|
||
END;
|
||
CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes BEGIN
|
||
INSERT INTO nodes_fts(nodes_fts, rowid, title, content) VALUES ('delete', old.rowid, old.title, old.content);
|
||
END;
|
||
CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE OF title, content ON nodes BEGIN
|
||
INSERT INTO nodes_fts(nodes_fts, rowid, title, content) VALUES ('delete', old.rowid, old.title, old.content);
|
||
INSERT INTO nodes_fts(rowid, title, content) VALUES (new.rowid, new.title, new.content);
|
||
END;
|
||
""");
|
||
}
|
||
|
||
public string? Meta(string key)
|
||
{
|
||
using var c = Open();
|
||
using var cmd = c.CreateCommand();
|
||
cmd.CommandText = "SELECT value FROM meta WHERE key=$k";
|
||
cmd.Parameters.AddWithValue("$k", key);
|
||
return cmd.ExecuteScalar() as string;
|
||
}
|
||
|
||
public static void Exec(SqliteConnection c, string sql)
|
||
{
|
||
using var cmd = c.CreateCommand();
|
||
cmd.CommandText = sql;
|
||
cmd.ExecuteNonQuery();
|
||
}
|
||
}
|