mirror of
https://github.com/siyuan-note/siyuan.git
synced 2026-07-03 14:09:06 +02:00
af920e871f
Signed-off-by: Daniel <845765@qq.com>
783 lines
21 KiB
Go
783 lines
21 KiB
Go
// SiYuan - Refactor your thinking
|
|
// Copyright (c) 2020-present, b3log.org
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package treenode
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/88250/gulu"
|
|
"github.com/88250/lute/ast"
|
|
"github.com/88250/lute/parse"
|
|
"github.com/siyuan-note/logging"
|
|
"github.com/siyuan-note/siyuan/kernel/util"
|
|
)
|
|
|
|
type BlockTree struct {
|
|
ID string // 块 ID
|
|
RootID string // 根 ID
|
|
ParentID string // 父 ID
|
|
BoxID string // 笔记本 ID
|
|
Path string // 文档数据路径
|
|
HPath string // 文档可读路径
|
|
Updated string // 更新时间
|
|
Type string // 类型
|
|
}
|
|
|
|
var (
|
|
db *sql.DB
|
|
|
|
initDatabaseLock = sync.RWMutex{}
|
|
)
|
|
|
|
func initDatabase(forceRebuild bool) {
|
|
initDatabaseLock.Lock()
|
|
defer initDatabaseLock.Unlock()
|
|
|
|
initDBConnection()
|
|
|
|
if !forceRebuild {
|
|
if !gulu.File.IsExist(util.BlockTreeDBPath) {
|
|
forceRebuild = true
|
|
}
|
|
}
|
|
if !forceRebuild {
|
|
// 校验块树表是否可用,避免因上次重建被中断导致数据库文件存在但表缺失
|
|
var table string
|
|
if err := db.QueryRow("SELECT name FROM sqlite_master WHERE type = ? AND name = ?", "table", "blocktrees").Scan(&table); nil != err {
|
|
logging.LogWarnf("blocktrees table missing or unreadable [%s], will rebuild blocktree database", err)
|
|
forceRebuild = true
|
|
}
|
|
}
|
|
if !forceRebuild {
|
|
return
|
|
}
|
|
|
|
initDBTables()
|
|
vacuum()
|
|
|
|
logging.LogInfof("reinitialized database [%s]", util.BlockTreeDBPath)
|
|
}
|
|
|
|
func initDBTables() {
|
|
_, err := db.Exec("DROP TABLE IF EXISTS blocktrees")
|
|
if err != nil {
|
|
logging.LogFatalf(logging.ExitCodeUnavailableDatabase, "drop table [blocks] failed: %s", err)
|
|
}
|
|
_, err = db.Exec("CREATE TABLE blocktrees (id, root_id, parent_id, box_id, path, hpath, updated, type)")
|
|
if err != nil {
|
|
logging.LogFatalf(logging.ExitCodeUnavailableDatabase, "create table [blocktrees] failed: %s", err)
|
|
}
|
|
|
|
_, err = db.Exec("CREATE INDEX idx_blocktrees_id ON blocktrees(id)")
|
|
if err != nil {
|
|
logging.LogFatalf(logging.ExitCodeUnavailableDatabase, "create index [idx_blocktrees_id] failed: %s", err)
|
|
}
|
|
|
|
_, err = db.Exec("CREATE INDEX idx_blocktrees_root_id ON blocktrees(root_id)")
|
|
if err != nil {
|
|
logging.LogFatalf(logging.ExitCodeUnavailableDatabase, "create index [idx_blocktrees_root_id] failed: %s", err)
|
|
}
|
|
}
|
|
|
|
func initDBConnection() {
|
|
closeDatabase()
|
|
|
|
util.LogDatabaseSize(util.BlockTreeDBPath)
|
|
dsn := util.BlockTreeDBPath + "?_journal_mode=WAL" +
|
|
"&_synchronous=OFF" +
|
|
"&_mmap_size=4294967296" +
|
|
"&_secure_delete=OFF" +
|
|
"&_cache_size=-128000" +
|
|
"&_page_size=32768" +
|
|
"&_busy_timeout=7000" +
|
|
"&_ignore_check_constraints=ON" +
|
|
"&_temp_store=MEMORY" +
|
|
"&_case_sensitive_like=OFF"
|
|
var err error
|
|
db, err = sql.Open("sqlite3_extended", dsn)
|
|
if err != nil {
|
|
logging.LogFatalf(logging.ExitCodeUnavailableDatabase, "create database failed: %s", err)
|
|
}
|
|
db.SetMaxIdleConns(7)
|
|
db.SetMaxOpenConns(7)
|
|
db.SetConnMaxLifetime(365 * 24 * time.Hour)
|
|
}
|
|
|
|
func CloseDatabase() {
|
|
closeDatabase()
|
|
}
|
|
|
|
func closeDatabase() {
|
|
if nil == db {
|
|
return
|
|
}
|
|
|
|
if err := db.Close(); err != nil {
|
|
logging.LogErrorf("close database failed: %s", err)
|
|
}
|
|
debug.FreeOSMemory()
|
|
db = nil
|
|
runtime.GC()
|
|
return
|
|
}
|
|
|
|
func GetBlockTreesByType(typ string) (ret []*BlockTree) {
|
|
sqlStmt := "SELECT * FROM blocktrees WHERE type = ?"
|
|
rows, err := query(sqlStmt, typ)
|
|
if err != nil {
|
|
logging.LogErrorf("sql query [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var block BlockTree
|
|
if err = rows.Scan(&block.ID, &block.RootID, &block.ParentID, &block.BoxID, &block.Path, &block.HPath, &block.Updated, &block.Type); err != nil {
|
|
logging.LogErrorf("query scan field failed: %s", err)
|
|
return
|
|
}
|
|
ret = append(ret, &block)
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetBlockTreeByBoxPath(boxID, path string) (ret *BlockTree) {
|
|
ret = &BlockTree{}
|
|
sqlStmt := "SELECT * FROM blocktrees WHERE box_id = ? AND path = ?"
|
|
err := queryRow(sqlStmt, boxID, path).Scan(&ret.ID, &ret.RootID, &ret.ParentID, &ret.BoxID, &ret.Path, &ret.HPath, &ret.Updated, &ret.Type)
|
|
if err != nil {
|
|
ret = nil
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return
|
|
}
|
|
logging.LogErrorf("sql query [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func CountTrees() (ret int) {
|
|
sqlStmt := "SELECT COUNT(*) FROM blocktrees WHERE type = 'd'"
|
|
err := queryRow(sqlStmt).Scan(&ret)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return 0
|
|
}
|
|
logging.LogErrorf("sql query [%s] failed: %s", sqlStmt, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
func CountBlocks() (ret int) {
|
|
sqlStmt := "SELECT COUNT(*) FROM blocktrees"
|
|
err := queryRow(sqlStmt).Scan(&ret)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return 0
|
|
}
|
|
logging.LogErrorf("sql query [%s] failed: %s", sqlStmt, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetBlockTreeRootByPath(boxID, path string) (ret *BlockTree) {
|
|
ret = &BlockTree{}
|
|
sqlStmt := "SELECT * FROM blocktrees WHERE box_id = ? AND path = ? AND type = 'd'"
|
|
err := queryRow(sqlStmt, boxID, path).Scan(&ret.ID, &ret.RootID, &ret.ParentID, &ret.BoxID, &ret.Path, &ret.HPath, &ret.Updated, &ret.Type)
|
|
if err != nil {
|
|
ret = nil
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return
|
|
}
|
|
logging.LogErrorf("sql query [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetBlockTreeRootByHPath(boxID, hPath string) (ret *BlockTree) {
|
|
ret = &BlockTree{}
|
|
hPath = gulu.Str.RemoveInvisible(hPath)
|
|
sqlStmt := "SELECT * FROM blocktrees WHERE box_id = ? AND hpath = ? AND type = 'd'"
|
|
err := queryRow(sqlStmt, boxID, hPath).Scan(&ret.ID, &ret.RootID, &ret.ParentID, &ret.BoxID, &ret.Path, &ret.HPath, &ret.Updated, &ret.Type)
|
|
if err != nil {
|
|
ret = nil
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return
|
|
}
|
|
logging.LogErrorf("sql query [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetBlockTreeRootsByHPath(boxID, hPath string) (ret []*BlockTree) {
|
|
hPath = gulu.Str.RemoveInvisible(hPath)
|
|
sqlStmt := "SELECT * FROM blocktrees WHERE box_id = ? AND hpath = ? AND type = 'd'"
|
|
rows, err := query(sqlStmt, boxID, hPath)
|
|
if err != nil {
|
|
logging.LogErrorf("sql query [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var block BlockTree
|
|
if err = rows.Scan(&block.ID, &block.RootID, &block.ParentID, &block.BoxID, &block.Path, &block.HPath, &block.Updated, &block.Type); err != nil {
|
|
logging.LogErrorf("query scan field failed: %s", err)
|
|
return
|
|
}
|
|
ret = append(ret, &block)
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetBlockTreeByHPathPreferredParentID(boxID, hPath, preferredParentID string) (ret *BlockTree) {
|
|
hPath = gulu.Str.RemoveInvisible(hPath)
|
|
var roots []*BlockTree
|
|
sqlStmt := "SELECT * FROM blocktrees WHERE box_id = ? AND hpath = ? AND parent_id = ? LIMIT 1"
|
|
rows, err := query(sqlStmt, boxID, hPath, preferredParentID)
|
|
if err != nil {
|
|
logging.LogErrorf("sql query [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var block BlockTree
|
|
if err = rows.Scan(&block.ID, &block.RootID, &block.ParentID, &block.BoxID, &block.Path, &block.HPath, &block.Updated, &block.Type); err != nil {
|
|
logging.LogErrorf("query scan field failed: %s", err)
|
|
return
|
|
}
|
|
if "" == preferredParentID {
|
|
ret = &block
|
|
return
|
|
}
|
|
roots = append(roots, &block)
|
|
}
|
|
|
|
if 1 > len(roots) {
|
|
return
|
|
}
|
|
|
|
for _, root := range roots {
|
|
if root.ID == preferredParentID {
|
|
ret = root
|
|
return
|
|
}
|
|
}
|
|
ret = roots[0]
|
|
return
|
|
}
|
|
|
|
func ExistBlockTree(id string) bool {
|
|
sqlStmt := "SELECT COUNT(*) FROM blocktrees WHERE id = ?"
|
|
var count int
|
|
err := queryRow(sqlStmt, id).Scan(&count)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return false
|
|
}
|
|
logging.LogErrorf("sql query [%s] failed: %s", sqlStmt, err)
|
|
return false
|
|
}
|
|
return 0 < count
|
|
}
|
|
|
|
func ExistBlockTrees(ids []string) (ret map[string]bool) {
|
|
ret = map[string]bool{}
|
|
if 1 > len(ids) {
|
|
return
|
|
}
|
|
|
|
for _, id := range ids {
|
|
ret[id] = false
|
|
}
|
|
|
|
sqlStmt := "SELECT id FROM blocktrees WHERE id IN ('" + strings.Join(ids, "','") + "')"
|
|
rows, err := query(sqlStmt)
|
|
if err != nil {
|
|
logging.LogErrorf("sql query [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var id string
|
|
if err = rows.Scan(&id); err != nil {
|
|
logging.LogErrorf("query scan field failed: %s", err)
|
|
return
|
|
}
|
|
ret[id] = true
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetBlockTrees(ids []string) (ret map[string]*BlockTree) {
|
|
ret = map[string]*BlockTree{}
|
|
if 1 > len(ids) {
|
|
return
|
|
}
|
|
|
|
stmtBuf := bytes.Buffer{}
|
|
stmtBuf.WriteString("SELECT * FROM blocktrees WHERE id IN (")
|
|
for i := range ids {
|
|
stmtBuf.WriteString("?")
|
|
if i == len(ids)-1 {
|
|
stmtBuf.WriteString(")")
|
|
} else {
|
|
stmtBuf.WriteString(",")
|
|
}
|
|
}
|
|
var args []any
|
|
for _, id := range ids {
|
|
args = append(args, id)
|
|
}
|
|
stmt := stmtBuf.String()
|
|
rows, err := query(stmt, args...)
|
|
if err != nil {
|
|
logging.LogErrorf("sql query [%s] failed: %s", stmt, err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var block BlockTree
|
|
if err = rows.Scan(&block.ID, &block.RootID, &block.ParentID, &block.BoxID, &block.Path, &block.HPath, &block.Updated, &block.Type); err != nil {
|
|
logging.LogErrorf("query scan field failed: %s", err)
|
|
return
|
|
}
|
|
ret[block.ID] = &block
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetBlockTree(id string) (ret *BlockTree) {
|
|
if "" == id {
|
|
return
|
|
}
|
|
|
|
ret = &BlockTree{}
|
|
sqlStmt := "SELECT * FROM blocktrees WHERE id = ?"
|
|
err := queryRow(sqlStmt, id).Scan(&ret.ID, &ret.RootID, &ret.ParentID, &ret.BoxID, &ret.Path, &ret.HPath, &ret.Updated, &ret.Type)
|
|
if err != nil {
|
|
ret = nil
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return
|
|
}
|
|
logging.LogErrorf("sql query [%s] failed: %v\n\t%s", sqlStmt, err, logging.ShortStack())
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// IsContainerType 按主类型缩写判断块是否为容器块(可合法接收子块)。
|
|
// 入参 abbrType 对应 BlockTree.Type(如 "d"/"h"/"p"),由 TypeAbbr 写入。
|
|
func IsContainerType(abbrType string) bool {
|
|
switch abbrType {
|
|
case "d", "b", "l", "i", "s", "callout":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CheckContainerParent 校验 parentID 指向的块是否允许接收子块。
|
|
// 仅在“通过 parentID 定位插入/移动目标”(即不依赖 previousID/nextID)的场景下调用,
|
|
// 因为一旦带 previousID/nextID,事务层走的是兄弟级 InsertAfter/InsertBefore,天然合法。
|
|
// 返回 nil 表示合法;返回 error 时调用方应拒绝本次操作。
|
|
func CheckContainerParent(parentID string) error {
|
|
bt := GetBlockTree(parentID)
|
|
if nil == bt {
|
|
return fmt.Errorf("parent block not found: %s", parentID)
|
|
}
|
|
if IsContainerType(bt.Type) {
|
|
return nil
|
|
}
|
|
if "h" == bt.Type {
|
|
// 标题是叶子块,其“子内容”在数据结构上实为后续兄弟节点(由 HeadingChildren 按层级推算)。
|
|
// 把块挂成标题的 AST 子节点属于非法嵌套,应改用 previousID 定位。
|
|
return fmt.Errorf("heading [%s] is a leaf block and cannot have children; to place a block below this heading, pass previousID=<heading id> or previousID=<last block below the heading> instead of parentID", parentID)
|
|
}
|
|
return fmt.Errorf("block [%s] type %q is a leaf block and cannot have children; use previousID to place the block as its sibling instead", parentID, bt.Type)
|
|
}
|
|
|
|
// CheckListItemNesting 校验 parentID 和 childID 是否形成“列表项直含列表项”的非法嵌套。
|
|
// 嵌套列表的正确结构是 ListItem > List > ListItem,列表项不能直接作为另一个列表项的子块。
|
|
// 仅在 move 场景调用(源和目标类型均已知)。
|
|
func CheckListItemNesting(parentID, childID string) error {
|
|
parentBt := GetBlockTree(parentID)
|
|
childBt := GetBlockTree(childID)
|
|
if nil == parentBt || nil == childBt {
|
|
return nil // 查不到就放行,不阻塞未知场景
|
|
}
|
|
if "i" == parentBt.Type && "i" == childBt.Type {
|
|
return fmt.Errorf("a list-item cannot directly contain another list-item; to nest, first create a list (NodeList) under the outer list-item, then add the inner list-items to that list")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func SetBlockTreePath(tree *parse.Tree) {
|
|
RemoveBlockTreesByRootID(tree.ID)
|
|
IndexBlockTree(tree)
|
|
}
|
|
|
|
func RemoveBlockTreesByRootID(rootID string) {
|
|
sqlStmt := "DELETE FROM blocktrees WHERE root_id = ?"
|
|
_, err := exec(sqlStmt, rootID)
|
|
if err != nil {
|
|
logging.LogErrorf("sql exec [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func CountBlockTreesByPathPrefix(pathPrefix string) (ret int) {
|
|
sqlStmt := "SELECT COUNT(*) FROM blocktrees WHERE path LIKE ?"
|
|
err := queryRow(sqlStmt, pathPrefix+"%").Scan(&ret)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return 0
|
|
}
|
|
logging.LogErrorf("sql query [%s] failed: %s", sqlStmt, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetBlockTreesByPathPrefix(pathPrefix string) (ret []*BlockTree) {
|
|
sqlStmt := "SELECT * FROM blocktrees WHERE path LIKE ?"
|
|
rows, err := query(sqlStmt, pathPrefix+"%")
|
|
if err != nil {
|
|
logging.LogErrorf("sql query [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var block BlockTree
|
|
if err = rows.Scan(&block.ID, &block.RootID, &block.ParentID, &block.BoxID, &block.Path, &block.HPath, &block.Updated, &block.Type); err != nil {
|
|
logging.LogErrorf("query scan field failed: %s", err)
|
|
return
|
|
}
|
|
ret = append(ret, &block)
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetBlockTreesByRootID(rootID string) (ret []*BlockTree) {
|
|
sqlStmt := "SELECT * FROM blocktrees WHERE root_id = ?"
|
|
rows, err := query(sqlStmt, rootID)
|
|
if err != nil {
|
|
logging.LogErrorf("sql query [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var block BlockTree
|
|
if err = rows.Scan(&block.ID, &block.RootID, &block.ParentID, &block.BoxID, &block.Path, &block.HPath, &block.Updated, &block.Type); err != nil {
|
|
logging.LogErrorf("query scan field failed: %s", err)
|
|
return
|
|
}
|
|
ret = append(ret, &block)
|
|
}
|
|
return
|
|
}
|
|
|
|
func RemoveBlockTreesByPathPrefix(pathPrefix string) {
|
|
sqlStmt := "DELETE FROM blocktrees WHERE path LIKE ?"
|
|
_, err := exec(sqlStmt, pathPrefix+"%")
|
|
if err != nil {
|
|
logging.LogErrorf("sql exec [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func GetBlockTreesByBoxID(boxID string) (ret []*BlockTree) {
|
|
sqlStmt := "SELECT * FROM blocktrees WHERE box_id = ?"
|
|
rows, err := query(sqlStmt, boxID)
|
|
if err != nil {
|
|
logging.LogErrorf("sql query [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var block BlockTree
|
|
if err = rows.Scan(&block.ID, &block.RootID, &block.ParentID, &block.BoxID, &block.Path, &block.HPath, &block.Updated, &block.Type); err != nil {
|
|
logging.LogErrorf("query scan field failed: %s", err)
|
|
return
|
|
}
|
|
ret = append(ret, &block)
|
|
}
|
|
return
|
|
}
|
|
|
|
func RemoveBlockTreesByBoxID(boxID string) (ids []string) {
|
|
sqlStmt := "SELECT id FROM blocktrees WHERE box_id = ?"
|
|
rows, err := query(sqlStmt, boxID)
|
|
if err != nil {
|
|
logging.LogErrorf("sql query [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var id string
|
|
if err = rows.Scan(&id); err != nil {
|
|
logging.LogErrorf("query scan field failed: %s", err)
|
|
return
|
|
}
|
|
ids = append(ids, id)
|
|
}
|
|
|
|
sqlStmt = "DELETE FROM blocktrees WHERE box_id = ?"
|
|
_, err = exec(sqlStmt, boxID)
|
|
if err != nil {
|
|
logging.LogErrorf("sql exec [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func RemoveBlockTreesByIDs(ids []string) {
|
|
if 1 > len(ids) {
|
|
return
|
|
}
|
|
|
|
sqlStmt := "DELETE FROM blocktrees WHERE id IN ('" + strings.Join(ids, "','") + "')"
|
|
_, err := exec(sqlStmt)
|
|
if err != nil {
|
|
logging.LogErrorf("sql exec [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func RemoveBlockTree(id string) {
|
|
sqlStmt := "DELETE FROM blocktrees WHERE id = ?"
|
|
_, err := exec(sqlStmt, id)
|
|
if err != nil {
|
|
logging.LogErrorf("sql exec [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
var indexBlockTreeLock = sync.Mutex{}
|
|
|
|
func IndexBlockTree(tree *parse.Tree) {
|
|
var changedNodes []*ast.Node
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering || !n.IsBlock() || "" == n.ID {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
changedNodes = append(changedNodes, n)
|
|
return ast.WalkContinue
|
|
})
|
|
|
|
if 1 > len(changedNodes) {
|
|
return
|
|
}
|
|
|
|
indexBlockTreeLock.Lock()
|
|
defer indexBlockTreeLock.Unlock()
|
|
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
logging.LogErrorf("begin transaction failed: %s", err)
|
|
return
|
|
}
|
|
|
|
execInsertBlocktrees(tx, tree, changedNodes)
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
logging.LogErrorf("commit transaction failed: %s", err)
|
|
}
|
|
}
|
|
|
|
func UpsertBlockTree(tree *parse.Tree) {
|
|
oldBts := map[string]*BlockTree{}
|
|
bts := GetBlockTreesByRootID(tree.ID)
|
|
for _, bt := range bts {
|
|
oldBts[bt.ID] = bt
|
|
}
|
|
|
|
var changedNodes []*ast.Node
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering || !n.IsBlock() || "" == n.ID {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
if oldBt, found := oldBts[n.ID]; found {
|
|
if oldBt.Updated != n.IALAttr("updated") || oldBt.Type != TypeAbbr(n.Type.String()) || oldBt.Path != tree.Path || oldBt.BoxID != tree.Box || oldBt.HPath != tree.HPath {
|
|
children := ChildBlockNodes(n) // 需要考虑子块,因为一些操作(比如移动块)后需要同时更新子块
|
|
changedNodes = append(changedNodes, children...)
|
|
}
|
|
} else {
|
|
children := ChildBlockNodes(n)
|
|
changedNodes = append(changedNodes, children...)
|
|
}
|
|
return ast.WalkContinue
|
|
})
|
|
|
|
if 1 > len(changedNodes) {
|
|
return
|
|
}
|
|
|
|
ids := bytes.Buffer{}
|
|
for i, n := range changedNodes {
|
|
ids.WriteString("'")
|
|
ids.WriteString(n.ID)
|
|
ids.WriteString("'")
|
|
if i < len(changedNodes)-1 {
|
|
ids.WriteString(",")
|
|
}
|
|
}
|
|
|
|
indexBlockTreeLock.Lock()
|
|
defer indexBlockTreeLock.Unlock()
|
|
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
logging.LogErrorf("begin transaction failed: %s", err)
|
|
return
|
|
}
|
|
|
|
sqlStmt := "DELETE FROM blocktrees WHERE id IN (" + ids.String() + ")"
|
|
_, err = tx.Exec(sqlStmt)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
logging.LogErrorf("sql exec [%s] failed: %s", sqlStmt, err)
|
|
return
|
|
}
|
|
|
|
execInsertBlocktrees(tx, tree, changedNodes)
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
logging.LogErrorf("commit transaction failed: %s", err)
|
|
}
|
|
}
|
|
|
|
func execInsertBlocktrees(tx *sql.Tx, tree *parse.Tree, changedNodes []*ast.Node) {
|
|
sqlStmt := "INSERT INTO blocktrees (id, root_id, parent_id, box_id, path, hpath, updated, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
stmt, err := tx.Prepare(sqlStmt)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
logging.LogErrorf("exec database stmt [%s] failed: %s\n %s", sqlStmt, err, logging.ShortStack())
|
|
|
|
if strings.Contains(err.Error(), "database disk image is malformed") {
|
|
closeDatabase()
|
|
util.RemoveDatabaseFile(util.BlockTreeDBPath)
|
|
initDatabase(true)
|
|
logging.LogFatalf(logging.ExitCodeUnavailableDatabase, "database disk image [%s] is malformed, please restart SiYuan kernel to rebuild it\n\t%s", util.BlockTreeDBPath, err)
|
|
}
|
|
return
|
|
}
|
|
defer stmt.Close()
|
|
|
|
for _, n := range changedNodes {
|
|
var parentID string
|
|
if nil != n.Parent {
|
|
parentID = n.Parent.ID
|
|
}
|
|
if _, err = tx.Exec(sqlStmt, n.ID, tree.ID, parentID, tree.Box, tree.Path, tree.HPath, n.IALAttr("updated"), TypeAbbr(n.Type.String())); err != nil {
|
|
tx.Rollback()
|
|
logging.LogErrorf("exec database stmt [%s] failed: %s\n %s", sqlStmt, err, logging.ShortStack())
|
|
|
|
if strings.Contains(err.Error(), "database disk image is malformed") {
|
|
closeDatabase()
|
|
util.RemoveDatabaseFile(util.BlockTreeDBPath)
|
|
initDatabase(true)
|
|
logging.LogFatalf(logging.ExitCodeUnavailableDatabase, "database disk image [%s] is malformed, please restart SiYuan kernel to rebuild it\n\t%s", util.BlockTreeDBPath, err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func InitBlockTree(force bool) {
|
|
initDatabase(force)
|
|
}
|
|
|
|
func CeilTreeCount(count int) int {
|
|
if 100 > count {
|
|
return 100
|
|
}
|
|
|
|
for i := 1; i < 40; i++ {
|
|
if count < i*500 {
|
|
return i * 500
|
|
}
|
|
}
|
|
return 500*40 + 1
|
|
}
|
|
|
|
func CeilBlockCount(count int) int {
|
|
if 5000 > count {
|
|
return 5000
|
|
}
|
|
|
|
for i := 1; i < 100; i++ {
|
|
if count < i*10000 {
|
|
return i * 10000
|
|
}
|
|
}
|
|
return 10000*100 + 1
|
|
}
|
|
|
|
func queryRow(query string, args ...any) *sql.Row {
|
|
query = strings.TrimSpace(query)
|
|
if "" == query {
|
|
logging.LogErrorf("statement is empty")
|
|
return nil
|
|
}
|
|
|
|
if nil == db {
|
|
return nil
|
|
}
|
|
return db.QueryRow(query, args...)
|
|
}
|
|
|
|
func query(query string, args ...any) (*sql.Rows, error) {
|
|
query = strings.TrimSpace(query)
|
|
if "" == query {
|
|
return nil, errors.New("statement is empty")
|
|
}
|
|
|
|
if nil == db {
|
|
return nil, errors.New("database is nil")
|
|
}
|
|
return db.Query(query, args...)
|
|
}
|
|
|
|
func exec(stmt string, args ...any) (sql.Result, error) {
|
|
stmt = strings.TrimSpace(stmt)
|
|
if "" == stmt {
|
|
return nil, errors.New("statement is empty")
|
|
}
|
|
|
|
if nil == db {
|
|
return nil, errors.New("database is nil")
|
|
}
|
|
return db.Exec(stmt, args...)
|
|
}
|
|
|
|
func vacuum() {
|
|
if nil != db {
|
|
if _, err := db.Exec("VACUUM"); nil != err {
|
|
logging.LogErrorf("vacuum database failed: %s", err)
|
|
}
|
|
}
|
|
}
|