如果你对 38911 BASIC BYTES FREE 有条件反射,PL/CBMBASIC 这件事大概会让你笑出声:PostgreSQL 现在可以把函数体交给 Commodore 64 BASIC V2 执行。不是仿制品,也不是语法糖,而是基于 cbmbasic 项目把 1982 年的 Microsoft/Commodore BASIC 解释器静态重编译成 C,再编进 PostgreSQL 扩展共享库。
每次函数调用都像一次内存里的开机:清零 64KB RAM、重置 CPU 寄存器、从 ROM 入口重新开始。听起来离谱,但代价大约只有 15 到 20 微秒,足够低到可以在表的每一行上调用。
这不是玩具外壳,而是真 BASIC
PL/CBMBASIC 的核心趣味在于:它尽量让数据库函数真的活在 C64 BASIC 的限制里。
你写函数时仍然需要行号,而且用户代码从第 10 行开始。第 0 到第 9 行由扩展保留,用来把 PostgreSQL 参数注入成 BASIC 变量赋值。
CREATE EXTENSION plcbmbasic;
CREATE FUNCTION hello(who text)
RETURNS text AS $$
10 PRINT "HELLO, ";WHO$;"!"
$$ LANGUAGE plcbmbasic;
SELECT hello('WORLD');
-- HELLO, WORLD!
参数会按 BASIC 的世界观映射:
text参数who变成WHO$smallint可以变成 16 位整数变量,例如LIVES%- 其他数字大多落到 CBM 的 40 位浮点数里,约 9 位有效数字
这也意味着,字符串字面量、变量名和输出都会进入那个大写、短变量名、255 字符字符串上限的年代。
连 BASIC 的坏脾气也被带进来了
C64 BASIC V2 有一些非常“复古”的限制:关键字会在标识符内部被 tokeniser 识别,变量名只有前两个字符显著,TI 和 ST 是系统变量。
所以类似 TOTAL 这种名字会踩中 TO,SCORE 会包含 OR,BUDGET 会包含 GET。PL/CBMBASIC 的 validator 会在 CREATE FUNCTION 阶段提前报错,而不是让你运行时才陷进 1980 年代的调试噩梦。
可以这样理解:PostgreSQL 得到了一个带怀旧提示语的 BASIC 语法审查员。
OUT 参数:从 64KB 内存里捞结果
BASIC 程序结束后,变量还留在模拟的 64KB RAM 里。PL/CBMBASIC 对 OUT 和 INOUT 参数的处理方式很直接:沿着 BASIC 自己的变量表走一遍,解码变量名、类型和值,再转换回 SQL 类型。
下面这个例子可以改造成任何小型数值计算函数:
CREATE FUNCTION divmod_cbm(
num int,
den int,
OUT quot int,
OUT rmd int
) AS $$
10 QUOT=INT(NUM/DEN)
20 RMD=NUM-QUOT*DEN
$$ LANGUAGE plcbmbasic;
SELECT * FROM divmod_cbm(47, 5);
-- quot | rmd
-- -----+-----
-- 9 | 2
这不是现代扩展 API 的常规姿势,更像是“程序跑完后,从另一台机器的内存里 PEEK 一下结果”。但放在 C64 BASIC 这个语境里,它反而非常合理。
设备 8 变成了数据库
最有意思的设计是 I/O。C64 上磁盘驱动器通常是 device 8,程序通过 OPEN、INPUT#、GET#、PRINT#、CLOSE 和 ST 状态变量跟它交互。
在 PL/CBMBASIC 里,device 8 就是 PostgreSQL 数据库。你 OPEN 的“文件名”是一条 SQL,通过 SPI 在当前事务里执行。
可以这样实践一个排行榜读取函数:
CREATE TABLE hiscores (
name text NOT NULL,
score int NOT NULL
);
INSERT INTO hiscores(name, score) VALUES
('ADA', 4200),
('GRACE', 3800),
('LINUS', 1200);
CREATE FUNCTION top_scores_cbm()
RETURNS text AS $$
10 OPEN 1,8,0,"SELECT NAME, SCORE FROM HISCORES ORDER BY SCORE DESC"
20 INPUT#1,N$,S
30 IF ST<>0 AND N$="" THEN 60
40 PRINT N$;" ";S
50 IF ST=0 THEN 20
60 CLOSE 1
$$ LANGUAGE plcbmbasic;
SELECT top_scores_cbm();
查询结果会像磁盘记录一样一条条流回来,记录以 CR 结尾,ST 在 EOF 时带上状态位。那种“读到没有为止”的循环,居然能原样搬进数据库函数。
更复古的是 secondary address 15:它原本是磁盘命令通道。在这里,你可以往它 PRINT# SQL 命令:
CREATE FUNCTION purge_low_scores_cbm()
RETURNS void AS $$
10 OPEN 15,8,15
20 PRINT#15,"DELETE FROM HISCORES WHERE SCORE < 1000"
30 INPUT#15,EN,EM$,RC,ES
40 CLOSE 15
$$ LANGUAGE plcbmbasic;
SELECT purge_low_scores_cbm();
状态记录会模拟原来的风格,例如 0,OK,<rows>,0。这很酷,也很危险:这是 untrusted procedural language,只适合超级用户安装和使用。
性能不是笑话,但边界很清楚
PL/Python 这类保持解释器热启动的语言当然更快。来源测试里,PL/Python 在轻量调用上大约快 14 到 19 倍;如果函数内部也要查询 100 行数据,差距缩小到约 6 倍,因为两边都要付 SPI 的成本。
但换个角度看,一个 1982 年的 8 位 BASIC 解释器,每次调用还完整“开机”,仍然能把调用成本压到十几微秒,并执行约每秒百万条 BASIC 语句,这已经很不可思议。
真正需要注意的是语义边界:
- 没有
NULL,通常需要在 SQL 层用COALESCE处理 - 字符串最长 255 字符
- 浮点只有约 9 位有效数字
- 输入设备不存在,
INPUT会报错 POKE和SYS作用于模拟 64KB 内存,下一次调用会重新清零,但当前调用仍可能被你自己玩坏- 无限循环会被 PostgreSQL 的
statement_timeout打断,因为 RUN/STOP 检查被接到了CHECK_FOR_INTERRUPTS()
采用建议:把它当成工程标本,而不是生产默认项
PL/CBMBASIC 的价值不在于“让 PostgreSQL 多一种严肃业务语言”,而在于它展示了 PostgreSQL 扩展机制、SPI、过程语言 handler、错误处理和运行时隔离可以被玩到多远。
如果你想试,可以从三个方向入手:
- 在本地 PostgreSQL 环境里安装扩展,跑简单纯计算函数
- 用 device 8 读取小表,理解 SPI 查询如何被包装成 BASIC 文件 I/O
- 故意写一个坏变量名或除零错误,观察 validator 和 ROM 错误如何进入 PostgreSQL 报错
别把它放进核心生产路径,除非你非常清楚 superuser、untrusted language 和 1982 年 BASIC 限制带来的后果。但作为一个能运行 SQL、能读变量表、能处理中断的怀旧工程实验,它漂亮得过分。
READY.