sense-bases

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

sense-bases

sense-bases

A
.base
file is YAML: filters selecting notes, formulas computing values, and views ordering and grouping them. Obsidian evaluates it against its own metadata cache; sense holds the same data in SQL tables. Every Bases construct that selects or computes rows has a SQL equivalent. What has none is presentation (
columnSize
, card layout), those change pixels, not rows, so a translation loses nothing by ignoring them.
Translate one view to one query: base-level
filters
AND the view's
filters
, the view's
order
/
sort
/
limit
as the SELECT list, ORDER BY, and LIMIT. A base with four views becomes four queries; saving them under the base's name in
sense.config.json
's
queries
block keeps them runnable as
sense <name>
. The general query surface is the
sense
skill; this one only maps Bases constructs onto it.
.base
文件是 YAML:过滤器选择笔记,公式计算值,视图对它们进行排序和分组。Obsidian 根据其自身的元数据缓存对其进行评估;sense 将相同的数据保存在 SQL 表中。每个选择或计算行的 Bases 结构都有对应的 SQL 等价物。没有对应的是展示层(
columnSize
、卡片布局),它们改变的是像素而非行,因此忽略它们对翻译没有任何损失。
将一个视图翻译为一个查询:base 级别的
filters
与视图的
filters
进行 AND 连接,视图的
order
/
sort
/
limit
分别对应 SELECT 列表、ORDER BY 和 LIMIT。一个有四个视图的 base 变成四个查询;将它们以 base 的名称保存在
sense.config.json
的
queries
块中,即可通过
sense <name>
运行。通用的查询界面是
sense
skill;本 skill 仅将 Bases 结构映射到它上面。

Properties

属性

Basessensenotes
note.field
/ bare
field
f.<field>
frontmatter column, same name
file.name
basename(f.path, '.md')
the Name column: no directory, no extension
a name with extension
basename(f.path)
the suffix arg is Unix basename's: present, stripped
file.folder
f.path LIKE 'Folder/%'
for
inFolder("Folder")
file.ext
'md'
sense indexes only markdown
file.size
f._size
bytes
file.mtime
datetime(f._mtime / 1000, 'unixepoch')
stored as ms
file.ctime
datetime(f._ctime / 1000, 'unixepoch')
see the caveat below
file.tags
the
tags
table
frontmatter + inline
#tags
, deduplicated, Obsidian's grain
file.links
links WHERE embed = 0
one row per distinct written target
file.embeds
links WHERE embed = 1
file.backlinks
links WHERE dst = f.path
this
a bound
?
see below
file.ctime
is the filesystem's creation time. A
git clone
or a copy resets it, so on a tree that arrived as a checkout, every ctime is the checkout date and age formulas are wrong about authorship without being wrong about the filesystem.
map
warns when modification times have this shape; creation times share it.
Basessense说明
note.field
/ 裸
field
f.<field>
frontmatter 列,同名
file.name
basename(f.path, '.md')
Name 列:无目录、无扩展名
带扩展名的名称
basename(f.path)
suffix 参数同 Unix basename:存在则去除
file.folder
f.path LIKE 'Folder/%'
用于
inFolder("Folder")
file.ext
'md'
sense 仅索引 markdown
file.size
f._size
字节
file.mtime
datetime(f._mtime / 1000, 'unixepoch')
存储为毫秒
file.ctime
datetime(f._ctime / 1000, 'unixepoch')
见下方注意事项
file.tags
tags
表
frontmatter + 行内
#tags
,去重,Obsidian 粒度
file.links
links WHERE embed = 0
每个不同的写入目标一行
file.embeds
links WHERE embed = 1
file.backlinks
links WHERE dst = f.path
this
绑定
?
见下文
file.ctime
是文件系统的创建时间。
git clone
或复制会重置它,因此对于通过 checkout 获取的树,每个 ctime 都是 checkout 日期,年龄公式在作者归属上是错误的,但对文件系统而言并没有错。
map
在修改时间呈现这种形态时会发出警告;创建时间也是如此。

Predicates

谓词

Basessense
==
!=
>
<
>=
<=
same
&&
||
!
AND
OR
NOT
and:
/
or:
/
not:
filter blocks
parenthesized
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(...)
the
json_each
EXISTS with
value IN (...)
/regex/.matches(x)
no SQLite regex;
LIKE
/
GLOB
cover anchored and wildcard shapes
value.isType("object")
json_each
's own
type
column:
... FROM json_each(f.field) j WHERE j.type = 'object'
A field no note in the tree declares has no column at all, so a filter naming it errors with
no such column
instead of treating every row as empty. Obsidian's evaluator returns empty for unknown properties; SQL does not. Dropping the clause states the same thing the error did, and
SELECT name FROM pragma_table_info('frontmatter')
lists what exists.
isEmpty()
has four true cases because empty is stored three ways: NULL (key absent),
''
(empty string value),
'[]'
(a list written
[]
), and
'[null]'
(a list key above a bare
-
). Obsidian's
isEmpty()
is true for all of them;
IS NULL
alone finds only the first. The IN list is the whole test.
json_array_length()
is not: it reads
'[null]'
as length 1 and throws on plain strings. The same trap inside a list:
json_each
hands a string member to
value
as plain text, so
json_type(value)
throws
malformed JSON
on it; the scan's own
type
column is the discriminator.
contains(link("Movies"))
compares against a link value. In frontmatter, a list of links holds the written text, so the
json_each
comparison value is the literal
[[Movies]]
.
hasLink
compares resolved paths:
links.dst
is the resolved target (
NULL
for dead links), so the comparison value is the target's path, not its display name.
sense's own
has(field, x)
is looser than both
contains
variants: exact membership on JSON arrays but substring on strings, so
has(f.status, 'active')
also matches
inactive
. It reads shorter when the field is known to be a list; the
json_each
form is the exact translation.
Basessense
==
!=
>
<
>=
<=
相同
&&
||
!
AND
OR
NOT
and:
/
or:
/
not:
过滤器块
带括号的
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(...)
json_each
的 EXISTS 与
value IN (...)
/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()
对所有这些都返回 true;仅
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
形式才是精确的翻译。

Formulas

公式

Formulas are SELECT expressions. The Bases functions map onto SQLite's:
Basessense
if(c, a, b)
iif(c, a, b)
or
CASE WHEN
now()
/
today()
datetime('now')
/
date('now')
date(x)
datetime(x)
(date1 - date2).days
julianday(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_array_length(f.list)
on a JSON column, a COUNT subquery on a table
list(a).filter(list(b).containsAny(value)).unique()
an EXISTS-joined subquery; worked example in EXAMPLES.md
Bases durations are typed; SQL date arithmetic is julianday day-fractions. A formula chaining duration fields (
.days.round()
) flattens to arithmetic on the julianday difference.
A formula referencing another formula becomes a CTE layer: SQL cannot read a SELECT alias in the same SELECT list, so each dependency level computes its formulas as columns and the next level reads them (
WITH t AS (SELECT ..., <level-1 formulas> FROM frontmatter) SELECT ..., <level-2 formulas> FROM t
). A five-formula chain is however many levels it has, not five CTEs. Formulas that only read base columns share one layer.
公式是 SELECT 表达式。Bases 函数映射到 SQLite 的:
Basessense
if(c, a, b)
iif(c, a, b)
或
CASE WHEN
now()
/
today()
datetime('now')
/
date('now')
date(x)
datetime(x)
(date1 - date2).days
julianday(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 日期运算是 julianday 的日分数。一个链接时长字段的公式(
.days.round()
)会展开为对 julianday 差值的算术运算。
引用另一个公式的公式变成 CTE 层:SQL 无法在同一个 SELECT 列表中读取 SELECT 别名,因此每个依赖级别将其公式计算为列,下一级别读取它们(
WITH t AS (SELECT ..., <level-1 formulas> FROM frontmatter) SELECT ..., <level-2 formulas> FROM t
)。一个五公式链的 CTE 数量是其所拥有的层级数,而不是五个 CTE。仅读取基础列的公式共享一个层级。

Views

视图

  • sort:
    (multi-key, each with direction) ->
    ORDER BY a DESC, b ASC
    .
    limit:
    ->
    LIMIT
    .
  • groupBy
    does not collapse rows. Obsidian shows every row bucketed under headers with per-group summaries. The SQL producing the same rows and numbers is ordering plus window functions, not GROUP BY:
    sql
    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
    A collapsed one-row-per-group report is plain
    GROUP BY
    . That is a different result than the Bases view shows.
  • View
    summaries
    (Sum, Average, Median via ordering, Unique, Filled, Checked) are the matching aggregates, windowed as above to keep the rows, or a separate aggregate query.
  • sort:
    (多键,每个带方向)->
    ORDER BY a DESC, b ASC
    。
    limit:
    ->
    LIMIT
    。
  • groupBy
    不会折叠行。Obsidian 将每一行显示在带有每组摘要的标题下。生成相同行和数字的 SQL 是排序加窗口函数,而不是 GROUP BY:
    sql
    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

this
is the note the base is evaluated against: the embedding note, or Obsidian's active pane. sense has no pane, so the caller supplies the path as a bound parameter:
sense sql "..." <path>
, or a saved query run as
sense <name> <path>
. A CTE keeps it single-bind:
sql
WITH me AS (SELECT ? AS p)
and every
this.file.*
expression joins
me
. The full Related.base translation in EXAMPLES.md is the worked case: link overlap, mutual-link filters, and the shared-target list via
GROUP_CONCAT
, all from one bound path.
this
是 base 被评估所依据的笔记:嵌入笔记,或 Obsidian 的活动面板。sense 没有面板,因此调用者以绑定参数的形式提供路径:
sense sql "..." <path>
,或保存的查询以
sense <name> <path>
方式运行。CTE 使其保持单次绑定:
sql
WITH me AS (SELECT ? AS p)
并且每个
this.file.*
表达式都连接
me
。EXAMPLES.md 中完整的 Related.base 翻译是完整案例:链接重叠、互链过滤器,以及通过
GROUP_CONCAT
实现的共享目标列表,全部来自一个绑定路径。

Not translatable, and why it costs nothing

不可翻译,以及为什么毫无损失

  • columnSize
    , card/table/list/map chrome: pixel layout over the same rows.
  • Obsidian choosing
    this
    from the active pane: there is no pane; the path parameter is the resolution.
  • Live re-evaluation as files change: every sense query reconciles against the filesystem first, so the freshness is already there; nothing re-renders on its own.
  • columnSize
    、卡片/表格/列表/地图外观:在相同行上的像素布局。
  • Obsidian 从活动面板选择
    this
    :没有面板;路径参数就是解决方案。
  • 文件变化时的实时重新评估:每个 sense 查询都会先与文件系统同步,因此数据的新鲜度已经具备;不会有任何内容自行重新渲染。