mcpField
S
sense-bases

sense-bases

github.com/kmalakoff数据与数据库0166 次安装3 周前更新
代码仓库

关于 sense-bases

sense-bases 是一份参考技能,说明如何将 Obsidian Bases 的筛选器、公式和视图转换为 sense 的 SQL 查询。它面向需要将 Bases 数据选择迁移到 sense 查询接口的用户,并涵盖等价表达式与边缘情况。

主要功能

  1. 01将 Bases 属性映射为 SQL
  2. 02转换筛选条件与谓词
  3. 03将公式转换为 SQLite 表达式
  4. 04用排序和窗口函数表达视图
  5. 05说明转换注意事项

适用场景

  1. 01将 Bases 视图重建为 sense 查询
  2. 02转换标签、链接或列表筛选
  3. 03计算公式与分组汇总
  4. 04通过绑定路径处理当前笔记

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/kmalakoff/sensemaking --skill sense-bases

安装后在 Claude Code 中输入/sense-bases确认已加载。

附带文件EXAMPLES.md

详细介绍

sense-bases

.base 文件采用 YAML 格式:筛选器用于选择笔记,公式用于计算值,视图用于排序和分组。Obsidian 会根据自身的元数据缓存进行计算;sense 将相同的数据存储在 SQL 表中。所有用于选择或计算行的 Bases 结构都有对应的 SQL 写法。没有对应写法的是呈现方式(columnSize、卡片布局),它们只会改变像素,而不会改变行,因此翻译时忽略它们不会损失任何信息。

将一个视图转换为一条查询:将基础级别的 filters 与视图的 filters 进行 AND 运算,并将视图的 order/sort/limit 分别用作 SELECT 列表、ORDER BY 和 LIMIT。包含四个视图的基础会转换为四条查询;将它们以基础的名称保存在 sense.config.json 的 queries 块中,就可以通过 sense <name> 运行。通用查询接口请参阅 sense 技能;本文仅介绍如何将 Bases 结构映射到该接口。

属性

Basessense说明
note.field / 裸字段 fieldf.<field>Frontmatter 列,名称相同
file.namebasename(f.path, '.md')Name 列:不包含目录和扩展名
带扩展名的名称basename(f.path)后缀参数与 Unix basename 相同:会保留并剥除该后缀
file.folderf.path LIKE 'Folder/%'用于 inFolder("Folder")
file.ext'md'sense 仅索引 Markdown
file.sizef._size字节数
file.mtimedatetime(f._mtime / 1000, 'unixepoch')存储单位为毫秒
file.ctimedatetime(f._ctime / 1000, 'unixepoch')参见下文的注意事项
file.tagstags 表Frontmatter 标签和行内 #tags,已去重,粒度与 Obsidian 相同
file.linkslinks WHERE embed = 0每个不同的书写目标对应一行
file.embedslinks WHERE embed = 1
file.backlinkslinks WHERE dst = f.path
this绑定的 ?参见下文

file.ctime 是文件系统的创建时间。git clone 或复制操作会重置该时间,因此,对于通过检出获得的目录树,其中所有文件的 ctime 都会是检出日期;按作者信息计算的年龄公式会出错,但按文件系统信息计算则没有问题。如果修改时间呈现这种情况,map 会发出警告;创建时间也会有同样的问题。

谓词

Bases含义
== != > < >= <=相同
&& || !AND OR NOT
and: / or: / not: filter blocks带括号的 AND / OR / NOT
field.isEmpty()(f.field IS NULL OR f.field IN ('', '[]', '[null]'))
file.hasTag("book")EXISTS (SELECT 1 FROM tags WHERE tags.path = f.path AND (tag = 'book' OR tag LIKE 'book/%'))
file.hasLink("Note")EXISTS (SELECT 1 FROM links WHERE src = f.path AND dst = 'Note.md')
list.contains(x)EXISTS (SELECT 1 FROM json_each(f.list) WHERE value = x)
string.contains(x)instr(f.string, x) > 0
list.containsAny(...)使用 value IN (...) 的 json_each EXISTS
/regex/.matches(x)SQLite 不支持正则表达式;LIKE/GLOB 可处理锚定和通配符模式
value.isType("object")json_each 自带的 type 列:... FROM json_each(f.field) j WHERE j.type = 'object'

树中没有笔记声明的字段根本没有对应列,因此筛选器引用它会报错,提示 no such column,而不是将每一行都视为空值。Obsidian 的求值器会将未知属性返回为空;SQL 则不会。 删除该子句与错误所表达的意思相同,而 SELECT name FROM pragma_table_info('frontmatter') 会列出已有的字段。

isEmpty() 有四种为真的情况,因为空值有三种存储方式:NULL(键缺失)、'' (空字符串值)、'[]'(写作 [] 的列表)和 '[null]'(列表键上只有一个裸 -)。 Obsidian 的 isEmpty() 对这些情况都会返回真;单独使用 IS NULL 只能找到第一种。 IN 列表包含了完整的判断条件。json_array_length() 则不行:它会将 '[null]' 读作长度 1,并且在普通字符串上报错。列表内部也有同样的陷阱:json_each 会将字符串成员作为纯文本交给 value,因此对它调用 json_type(value) 会报 malformed JSON;扫描结果自身的 type 列才是判别依据。

contains(link("Movies")) 会与链接值比较。在 frontmatter 中,链接列表存储的是写入的文本,因此 json_each 比较的值是字面量 [[Movies]]。 hasLink 比较的是解析后的路径:links.dst 是解析后的目标(失效链接为 NULL),因此 比较值应为目标的路径,而不是其显示名称。

sense 自带的 has(field, x) 比两种 contains 都宽松:对 JSON 数组执行精确成员匹配,对字符串执行子串匹配,因此 has(f.status, 'active') 也会匹配 inactive。已知字段是列表时,这种写法更简短;json_each 形式则是精确的转换方式。

公式

公式是 SELECT 表达式。Bases 函数对应的 SQLite 写法如下:

Bases含义
if(c, a, b)iif(c, a, b) 或 CASE WHEN
now() / today()datetime('now') / date('now')
date(x)datetime(x)
(date1 - date2).daysjulianday(date1) - julianday(date2)
(now() - acquired).months(julianday('now') - julianday(f.acquired)) / 30.44
x.round(n) / x.toFixed(n)round(x, n) / printf('%.2f', x)
d.format("YYYY-MM-DD")strftime('%Y-%m-%d', d)
x.toString()CAST(x AS TEXT)
list.length对 JSON 列使用 json_array_length(f.list),对表使用 COUNT 子查询
list(a).filter(list(b).containsAny(value)).unique()使用 EXISTS 连接的子查询;示例见 EXAMPLES.md

Bases 的时长具有类型;SQL 日期运算使用儒略日的小数。将时长字段串联起来的公式(.days.round())会简化为对儒略日差值进行运算。

引用其他公式的公式会转换为 CTE 层:SQL 无法在同一个 SELECT 列表中读取 SELECT 别名,因此每个依赖层级会将其公式计算为列,下一层再读取这些列(WITH t AS (SELECT ..., <level-1 formulas> FROM frontmatter) SELECT ..., <level-2 formulas> FROM t)。五个公式串联起来时,CTE 的数量取决于它们的层级,而不是五个。只读取基础列的公式会共用一层。

视图

  • sort:(支持多个键,每个键都有排序方向)-> ORDER BY a DESC, b ASC。limit: -> LIMIT。

  • groupBy 不会合并行。Obsidian 会将每一行按组归入标题下,并显示每组摘要。生成相同行和数值的 SQL 使用排序和窗口函数,而不是 GROUP BY:

    SELECT f.path, f.status, f.days,
           AVG(f.days)  OVER (PARTITION BY f.status) AS group_avg,
           COUNT(*)     OVER (PARTITION BY f.status) AS group_n
    FROM frontmatter f WHERE f.days IS NOT NULL
    ORDER BY f.status, f.days DESC
    

    折叠后每组只显示一行的报表使用普通的 GROUP BY。这与 Bases 视图显示的结果不同。

  • 视图的 summaries(Sum、Average、通过排序实现的 Median、Unique、Filled、Checked)对应相应的聚合函数,并像上文一样使用窗口函数保留行,或者使用单独的聚合查询。

this

this 是评估基底时所依据的笔记:嵌入该基底的笔记,或 Obsidian 的活动窗格。sense 没有窗格,因此由调用方将路径作为绑定参数传入:sense sql "..." <path>,或运行已保存的查询:sense <name> <path>。使用 CTE 可以只绑定一次:

WITH me AS (SELECT ? AS p)

每个 this.file.* 表达式都会关联 me。EXAMPLES.md 中完整的 Related.base 转换示例就是一个实际案例:通过一个绑定路径,实现链接重叠、互相链接筛选,以及使用 GROUP_CONCAT 获取共享目标列表。

无法转换的部分及其原因

  • columnSize、卡片/表格/列表/地图界面元素:只是对相同行进行像素布局。
  • Obsidian 根据活动窗格选择 this:这里没有窗格;路径参数即可确定目标。
  • 文件变更时实时重新评估:默认的 CLI 查询会增量准备所需功能;--no-build 会读取上一次已完成的代次,并要求其具备查询所需的就绪状态,而不扫描实时文件。任何内容都不会自行重新渲染。

评论

支持 Markdown