无需规范的细化
摘要
一篇博客文章,解释如何使用细化映射在数据库模式更改期间保留外部属性,并通过将布尔列迁移到可空时间戳再到事件溯源的例子进行说明。
<p>假设我们有一个SQL数据库,其中包含一个<code>user</code>表,用户有一个不可为空的<code>is_activated</code>布尔列。在阅读了<a href="https://ntietz.com/blog/that-boolean-should-probably-be-something-else/" target="_blank">那个布尔值很可能应该是别的东西</a>之后,你决定将其迁移到一个可空的<code>activated_at</code>列。你可以更改任何读取/更新<code>user</code>表的SQL查询,但不能更改使用这些查询结果的代码。我们能否以一种保留所有外部属性的方式进行这种更改? </p>
<p>是的。如果一个更新会将<code>is_activated</code>设置为true,则改为将其设置为当前日期。现在定义<strong>细化映射</strong>,它接受一个<code>new_user</code>并返回一个<code>old_user</code>。所有列将保持不变<em>除了</em><code>is_activated</code>,它将变为</p>
<div class="codehilite"><pre><span></span><code>f(new_user).is_activated =
if new_user.activated_at == NULL
then FALSE
else TRUE
</code></pre></div>
<p>现在新代码可以直接使用<code>new_user</code>,而旧代码可以使用<code>f(new_user)</code>,其行为与<code>old_user</code>无法区分。 </p>
<p>过了一段时间,你决定切换到一个类似<a href="https://martinfowler.com/eaaDev/EventSourcing.html" target="_blank">事件溯源</a>的模型。因此,不再使用<code>activated_at</code>列,而是使用一个<code>user_events</code>表,其中每条记录是<code>(user_id, timestamp, event)</code>。所以添加一个<code>activate</code>事件将激活用户,添加一个<code>deactivate</code>事件将停用用户。同样,我们可以更新查询,但不能更改使用这些查询结果的任何代码。我们能否进行一种保留所有外部属性的更改?</p>
<p>是的。如果一个更新会改变<code>is_activated</code>,则改为在事件表中添加一条适当的记录。现在,定义细化映射,它接受<code>newer_user</code>并返回<code>new_user</code>。<code>activated_at</code>字段将按如下方式计算:</p>
<div class="codehilite"><pre><span></span><code>g(newer_user).activated_at =
# last_activated_event
let lae =
newer_user.events
.filter(event = "activate" | "deactivate")
.last,
in
if lae.event == "activate"
then lae.timestamp
else NULL
</code></pre></div>
<p class="empty-line" style="height:16px; margin:0px !important;"></p>
<p>现在新代码可以直接使用<code>newer_user</code>,旧代码可以使用<code>g(newer_user)</code>,而非常旧的代码可以使用<code>f(g(newer_user))</code>。</p>
<h3>可变性约束</h3>
<div class="subscribe-form"></div>
<p>我之前说“这些保留了所有外部属性”,但那是说谎。这取决于我们明确拥有的属性,而我没有列出任何属性。对我来说,真正有趣的属性是关于系统如何演化的可变性约束。所以让我们回到过去,为<code>user</code>添加一个约束:</p>
<div class="codehilite"><pre><span></span><code>C1(u) = u.is_activated => u.is_activated'
</code></pre></div>
<p>这个约束意味着如果一个用户被激活,任何更改都将保持其激活状态。这意味着一个用户可以从停用变为激活,但不能反过来。这不是一个特别好的约束,但足以用于教学目的。这种SQL约束可以通过<a href="https://www.postgresql.org/docs/current/sql-createeventtrigger.html" target="_blank">触发器</a>来强制执行。 </p>
<p>现在我们可以对<code>new_user</code>施加一个约束:</p>
<div class="codehilite"><pre><span></span><code>C2(nu) = nu.activated_at != NULL => nu.activated_at' != NULL
</code></pre></div>
<p>如果<code>nu</code>满足<code>C2</code>,那么<code>f(nu)</code>满足<code>C1</code>。因此细化仍然成立。</p>
<p>对于<code>newer_u</code>,我们<em>不能</em>保证<code>g(newer_u)</code>满足<code>C2</code>,因为我们可以通过追加一个新事件从“激活”变为“停用”。所以这不是一个细化。这可以通过移除停用事件来修复,那样也行。</p>
<p>所以一个更有趣的情况是<code>bad_user</code>,它是<code>user</code>的一个细化,同时具有<code>activated_at</code>和<code>activated_until</code>。我们提出细化映射<code>b</code>:</p>
<div class="codehilite"><pre><span></span><code>b(bad_user).activated =
if bad_user.activated_at == NULL && activated_until == NULL
then FALSE
else bad_user.activated_at <= now() < bad_user.activated_until
</code></pre></div>
<p>但现在如果经过足够的时间,<code>b(bad_user).activated' = false</code>,所以这也不是一个细化。</p>
<h3>点睛之笔</h3>
<p>细化是形式化规范中最强大的技术之一,但也是最难理解的技术之一。我开始认为它之所以如此困难,是因为人们在<em>同时</em>学习形式化方法时学习细化,因此在一个陌生的环境中面对一个陌生的话题。如果是这样,那么也许在像数据库这样更常见的上下文中引入细化会更容易。</p>
<p>我曾在<a href="https://hillelwayne.com/post/refinement/" target="_blank">这里</a>写过一些关于正常上下文中细化的内容(展示一个规范是另一个规范的实现)。我有点想把这种解释写进书里,但可能已经太晚了,不适合做这样大的内容补充。</p>
<p>(思考:细化映射与数据库视图有何关系?)</p>
查看缓存全文
缓存时间: 2026/05/16 03:40
# 无需规范的精化
来源:https://buttondown.com/hillelwayne/archive/refinement-without-specification
假设我们有一个 SQL 数据库,其中包含一个 `user` 表,并且用户有一个非空 `is_activated` 布尔列。在阅读了那篇《那个布尔值可能应该是别的什么》(https://ntietz.com/blog/that-boolean-should-probably-be-something-else/) 后,你决定将其迁移为可为空的 `activated_at` 列。你可以修改任何读取/更新 `user` 表的 SQL 查询,但不能修改使用这些查询结果的代码。能否以保留所有外部属性的方式进行这项改动?
可以。如果某个更新会将 `is_activated` 设为 true,则改为将其设为当前日期。现在定义**精化映射**,它接受一个 `new_user` 并返回一个 `old_user`。所有列都将保持不变,*除了* `is_activated`,它将被定义为:
```
f(new_user).is_activated =
if new_user.activated_at == NULL
then FALSE
else TRUE
```
现在新代码可以直接使用 `new_user`,而旧代码可以使用 `f(new_user)` 代替,其行为与 `old_user` 无法区分。
又过了一段时间,你决定切换到类似事件溯源(https://martinfowler.com/eaaDev/EventSourcing.html) 的模型。因此,不再使用 `activated_at` 列,而是有一个 `user_events` 表,其中每条记录是 `(user_id, timestamp, event)`。因此,添加一个 `activate` 事件将激活用户,添加一个 `deactivate` 事件将停用用户。同样,我们可以更新查询,但不能修改使用查询结果的代码。能否以保留所有外部属性的方式进行这项改动?
可以。如果某个更新会改变 `is_activated`,则改为在事件表中添加一条适当的记录。现在定义精化映射,它接受 `newer_user` 并返回 `new_user`。`activated_at` 字段将按如下方式计算:
```
g(newer_user).activated_at =
# last_activated_event
let lae =
newer_user.events
.filter(event = "activate" | "deactivate")
.last,
in
if lae.event == "activate"
then lae.timestamp
else NULL
```
现在新代码可以直接使用 `newer_user`,旧代码可以使用 `g(newer_user)`,而更早的代码可以使用 `f(g(newer_user))`。
### 可变性约束
我之前说“这些保留所有外部属性”,这是个谎言。这取决于我们明确列出的属性,而我并没有列出任何具体属性。对我来说真正有趣的属性是关于系统如何演化的可变性约束。所以我们回到过去,对 `user` 添加一个约束:
```
C1(u) = u.is_activated => u.is_activated'
```
这个约束意味着,如果用户被激活,任何更改都将保留其激活状态。也就是说,用户可以从未激活变为激活,但不能反过来。这不是一个特别好的约束,但足以用于教学目的。这样的 SQL 约束可以通过触发器(https://www.postgresql.org/docs/current/sql-createeventtrigger.html) 来强制执行。
现在我们可以在 `new_user` 上添加一个约束:
```
C2(nu) = nu.activated_at != NULL => nu.activated_at' != NULL
```
如果 `nu` 满足 `C2`,那么 `f(nu)` 满足 `C1`。因此这个精化仍然成立。
对于 `newer_u`,我们*无法*保证 `g(newer_u)` 满足 `C2`,因为仅仅通过追加一个新事件,我们就可以从“已激活”变为“已停用”。因此这不是一个精化。这可以通过移除停用事件来修复,那样也能行。
所以一个更有趣的案例是 `bad_user`,它是 `user` 的一个精化,同时具有 `activated_at` 和 `activated_until`。我们提出精化映射 `b`:
```
b(bad_user).activated =
if bad_user.activated_at == NULL && activated_until == NULL
then FALSE
else bad_user.activated_at <= now() < bad_user.activated_until
```
但这样一来,如果经过足够长的时间,`b(bad_user).activated' = false`,因此这也不是一个精化。
### 点睛之笔
精化是形式化规范中最强大的技术之一,但也是最难理解的技术之一。我开始认为它之所以如此困难,是因为人们在*同时*学习形式化方法时学习精化,因此在一个不熟悉的语境中面对一个不熟悉的主题。如果是这样,那么在更常见的语境(比如数据库)中引入精化可能会更容易。
我曾在普通语境下写过一些关于精化的文章,见这里(https://hillelwayne.com/post/refinement/)(展示一个规范是另一个规范的实现)。我有点想把这种解释写进书里,但现在可能已经来不及做这样大的内容增补了。
(思考题:精化映射与数据库视图有何关系?)
相似文章
TRACE:面向可审计代理承诺的操作推理模式
本文介绍了TRACE(Typed Reasoning And Commitment Evidence,类型化推理与承诺证据),这是一种带类型和版本号的模式,用于记录代理系统中的推理轨迹,以实现可审计性并提升推理质量。文中定义了参考写入器、测量机制和消费者契约,并通过两个实例说明了该方法。
SOMA-SQL:通过合成日志与执行探测解决NL-to-SQL中的多源歧义
Soma-SQL提出了一种自主方法,利用合成查询日志和歧义驱动的执行探测,解决自然语言到SQL翻译中的多源歧义问题,在执行准确率上比最先进的基线平均提升13%。
论键、本质与性能
一篇博客文章,为关系模型中键和规范化的必要性辩护,认为它们反映了关于现实进行连贯话语所需的本体论条件,反驳了关于定义键和域的实际困难之类的批评。
可执行模式合约:从自动摄取到多源检索
本文提出一个系统,能够从原始多源数据中自动发现可执行模式,并将其用于知识图谱构建和查询时检索,在多个QA基准测试上优于基线方法。
Schema-Aware Localisation (SAL):针对 Oracle NL2SQL 的实时模式锚定与幻觉验证
本文介绍了一种轻量级中间件 Schema-Aware Localisation (SAL),它通过将 LLM 生成的 SQL 查询锚定到实时 Oracle 模式目录中,从而消除幻觉错误,在无需手动模式整理的情况下,在 TPC-H 问题上实现了 62.6% 的执行锚定正确率。