本文记录 README.md、tsql.md、plsql.md、mysql.md 里每条「支持 / 不支持 / 有差异」声明的实测依据。
1. 复核环境
| 项目 | 值 |
|---|---|
| 日期 | 2026-11-13 |
| 系统 | WSL2,Linux 6.18.58.1-microsoft-standard-WSL2 |
| 二进制 | /path/to/dbsql/target/release/dbsql,mtime 2026-11-10 18:44:48 +0800 |
| 插件 | plugins/libdbsql_oracle_plugin.so、plugins/libdbsql_mysql_plugin.so,mtime 2026-11-13 09:40:03 +0800 |
| 数据根 | /tmp/dbsql-dialect-verify(脚本每次 rm -rf 重建,不碰任何客户库) |
| 库 | tdb(T-SQL)、pdb(PL/SQL)、mdb(MySQL) |
2. 怎么跑
cd /path/to/dbsql
bash docs/dialects/verify.sh > /tmp/verify-out.txt 2>&1
echo $? # 0
wc -l /tmp/verify-out.txt # 776
- 脚本自包含:自建数据根与三个库,逐条打印
### [标签] (方言) SQL与缩进的结果。 - 实测整轮约 6 秒(
real 0m5.936s)。 - 每条都是一次独立 CLI 进程,所以事务类语句(
COMMIT)天然没有活动事务,见 3.2。 - 输出里的
[插件]行被过滤掉,其余原样保留。 - 想复跑单个断言,直接抄表里的 SQL 按第 3 节命令执行即可。
3. 方言怎么选
3.1 命令行
CLI=./target/release/dbsql
$CLI --root /tmp/r create database tdb
$CLI --root /tmp/r --dialect standard sql tdb "SELECT 1 AS x" # T-SQL(默认)
$CLI --root /tmp/r --dialect procedural sql pdb "SELECT 1 AS x" # PL/SQL
$CLI --root /tmp/r --dialect mysql sql mdb "SELECT 1 AS x" # MySQL
--root缺省时用当前目录的数据根;本文所有命令都显式给 root。--plugin必须写在子命令(sql/create)之前。- 在仓库根目录执行时,两个插件会被自动发现(
plugins/lib*.so),无需--plugin。
3.2 插件缺席 / 用错方言的原文
$ dbsql --dialect procedural sql pdb "SELECT 1"
错误:PL/SQL 方言需要插件,请用 --plugin <路径> 指定 dbsql_oracle_plugin.so
$ dbsql --dialect mysql sql mdb "SELECT 1"
错误:MySQL 方言需要插件,请用 --plugin <路径> 指定 dbsql_mysql_plugin.so
$ dbsql --dialect standard --plugin ./plugins/libdbsql_mysql_plugin.so sql x "SELECT 1"
错误:--plugin 只在 procedural/mysql 方言下生效,当前方言为 standard;请加 --dialect procedural 或 --dialect mysql(收到的插件路径:./plugins/libdbsql_mysql_plugin.so)
3.3 HTTP
/api/sql、/api/select、create-routine 支持请求体里的 "dialect" 字段,取值 standard / procedural / mysql。
注意:未知取值会静默回退到 T-SQL(standard),不报错——拼错方言名时会得到「语法莫名其妙报错」而不是「方言名无效」。
4. 关键声明与实测输出
4.1 T-SQL 方言
| 声明 | SQL 片段 | 实测输出 |
|---|---|---|
列默认 NOT NULL |
CREATE TABLE t (id INT PRIMARY KEY, v VARCHAR(5)) 后 INSERT INTO t (id) VALUES (1) |
错误:第 1 行的列 'v' 不允许为空(未提供值且无默认值) |
默认不可空列写显式 NULL 也拒 |
INSERT INTO t (id, v) VALUES (2, NULL) |
错误:第 1 行的列 'v' 不允许为空 |
声明了 NULL 才能存空 |
n INT NULL 后 INSERT INTO tt (id, v, n) VALUES (40, 'a', NULL) |
INSERT 1 行 |
VARCHAR(n) 不校验长度 |
VARCHAR(5) 存 'abcdefgh' |
INSERT 1 行 |
| 比较区分大小写 | 已存 'abcdefgh',WHERE v = 'ABCDEFGH' |
n = 0 |
CHAR(n) 是单字符 |
CHAR(3) 插 'abcd' |
错误:列 'ch' 为字符列,字面量必须是单个字符,如 'a' |
DECIMAL(p,s) 生效 |
DECIMAL(5,2) 插 1234.567 |
错误:列 'amt' 的值超出精度约束:小数位 3 超过声明小数位 2 |
| 双引号是字符串 | SELECT "id" AS x FROM tt |
结果 id |
TOP / OFFSET…FETCH |
SELECT TOP 1 …;OFFSET 0 ROWS FETCH NEXT 2 ROWS ONLY |
1 行;2 行 |
INTERSECT / EXCEPT |
SELECT id FROM tt INTERSECT … WHERE id = 1;EXCEPT … |
1 行;其余行 |
CROSS JOIN 不支持 |
FROM tt a CROSS JOIN tt b |
错误:SELECT 语句末尾存在多余内容(位于 31) |
窗口函数不支持 PARTITION BY |
ROW_NUMBER() OVER (PARTITION BY v ORDER BY id) |
错误:ROW_NUMBER 的 PARTITION BY 暂不支持(位于 30) |
TOP n PERCENT 不支持 |
SELECT TOP 50 PERCENT id FROM tt |
错误:TOP n PERCENT 不支持,请改写为 LIMIT(位于 14) |
dbo. 前缀不支持 |
FROM dbo.tt |
错误:表名 'dbo' 带库/架构前缀,dbsql 不支持跨库引用(请改写为同库表名)(位于 29) |
ISNULL 不存在 |
SELECT ISNULL(v, 'x') FROM tt |
错误:未定义的函数 'ISNULL' |
TRUNCATE 不支持 |
TRUNCATE TABLE tt |
错误:不支持的 SQL 语句类型:TRUNCATE |
| 唯一索引不支持 | CREATE UNIQUE INDEX ux ON tt (v) |
错误:CREATE 后必须是 DATABASE / TABLE / INDEX / VIEW / PROCEDURE / FUNCTION / TRIGGER / SCHEDULE |
| 外键不支持 | CREATE TABLE tfk (id INT PRIMARY KEY, tid INT REFERENCES tt(id)) |
错误:外键约束 'tt'(引用 'tt')暂不支持:外键的跨表校验还没有实现… |
| 触发器可拦截 | CREATE TRIGGER trg_b BEFORE INSERT ON tt AS RAISE '不允许插入' |
插入报 错误:触发器 'trg_b':不允许插入;DROP TRIGGER trg_b 可删 |
| 触发器体可多语句 | AFTER INSERT … INSERT INTO tt_audit …; INSERT INTO tt_audit … |
触发一次后审计表 n = 2 |
游标 / 动态 EXEC |
CREATE PROCEDURE pc () AS BEGIN DECLARE cur CURSOR …;EXEC('INSERT …') |
均创建成功,CALL pc() / CALL pdyn() 返回 CALL |
SELECT … INTO |
SELECT id, v INTO tt_copy FROM tt WHERE id = 1 |
返回 0 行列集,但 tt_copy 有 1 行 |
| 顶层多条语句不支持 | SELECT 1 AS x; SELECT 2 AS y |
错误:SELECT 语句末尾存在多余内容(位于 13) |
4.2 PL/SQL 方言
| 声明 | SQL 片段 | 实测输出 |
|---|---|---|
无 FROM 可用 |
SELECT 1 AS x |
x = 1 |
DUAL 可用 |
SELECT SYSDATE AS d FROM DUAL |
返回当前时间 |
|| 拼接 |
SELECT 'a' || 'b' AS s FROM DUAL |
ab |
q'[…]' 不做 Oracle 解开 |
SELECT q'[it''s]' AS s FROM DUAL |
原样返回 it''s |
空串不是 NULL |
一行存 ''(id=5),一行存 NULL(id=2) |
WHERE nm IS NULL → id=2;WHERE nm = '' → id=5 |
NULL 排序在前 |
ORDER BY nm ASC |
id=2(NULL)排第一 |
ROWNUM 不可用 |
SELECT ROWNUM, id FROM pt |
错误:SELECT 列 'ROWNUM' 不存在 |
INSERT 值只能是字面量 |
VALUES (6, UPPER('a')) |
错误:期望字面量,实际遇到 'UPPER' |
UPDATE 值只能是字面量 |
UPDATE pt SET nm = nm WHERE id = 1 |
错误:期望字面量,实际遇到 'nm' |
INSERT … SELECT 不支持 |
INSERT INTO pt (id, nm) SELECT 9, 'q' FROM DUAL |
错误:期望关键字 'VALUES',实际遇到 'SELECT' |
| 匿名块可用 | BEGIN NULL; END; |
执行成功 |
EXCEPTION 不支持 |
BEGIN NULL; EXCEPTION WHEN OTHERS THEN NULL; END; |
错误:无法识别的 PL/SQL 语句:EXCEPTION WHEN OTHERS THEN NULL |
MINUS / UNION / INTERSECT / CTE |
SELECT … MINUS SELECT …;WITH x AS (…) SELECT … |
均返回行集 |
EXCEPT 不识别 |
SELECT id FROM pt EXCEPT SELECT id FROM pt WHERE id = 1 |
错误:只支持单条语句;语句结束后仍有多余内容:'SELECT'(位于 25) |
| 递归 CTE 不支持 | WITH r (n) AS (SELECT 1 FROM DUAL UNION ALL SELECT n+1 FROM r …) |
错误:数据表 'r' 不存在(库 'pdb' 中没有该表) |
| 窗口函数不支持 | ROW_NUMBER() OVER (ORDER BY id) |
错误:只支持单条语句;语句结束后仍有多余内容:'LParen'(位于 29) |
FETCH FIRST 可用 |
ORDER BY id FETCH FIRST 1 ROWS ONLY |
1 行 |
OFFSET … FETCH NEXT 可用 |
ORDER BY id OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY |
1 行 |
单独 FETCH NEXT 不可用 |
ORDER BY id FETCH NEXT 1 ROWS ONLY |
错误:期望关键字 'FIRST',实际遇到 'NEXT' |
| 过程体字符串可写 | CREATE PROCEDURE pz () AS BEGIN INSERT INTO pt (id, nm) VALUES (10, 'p'); END |
创建成功,CALL pz() 后 id=10 存在 |
CREATE OR REPLACE VIEW / ALTER |
CREATE OR REPLACE VIEW pv AS …;ALTER TABLE pt ADD flag INT / DROP COLUMN flag |
均 OK |
| 索引 / 触发器 / 序列不支持 | CREATE INDEX / CREATE TRIGGER / CREATE SEQUENCE |
错误:CREATE 后不支持的对象类型(位于 7) |
TRUNCATE / MERGE 不支持 |
TRUNCATE TABLE pt;MERGE INTO … |
错误:不支持或无法识别的 Oracle 语句(位于 0):'TRUNCATE' / 'MERGE' |
NUMBER(p,s) / VARCHAR2(n) 不校验 |
NUMBER(5,2) 存 1234.567;VARCHAR2(3) 存 6 字符 |
均 INSERT 1 行 |
复合主键必须显式 NOT NULL |
a INT, b INT, PRIMARY KEY (a, b) |
错误:列 'a' 声明为 NULL,不能做复合主键的一部分;请改成 NOT NULL |
DATE '…' 字面量不支持 |
VALUES (1, DATE '2026-01-02') |
错误:缺少 VALUES 行右括号(位于 38);改写字符串 '2026-01-02' 可入库 |
单独 COMMIT 无效 |
COMMIT |
错误:没有活动事务 |
4.3 MySQL 方言
| 声明 | SQL 片段 | 实测输出 |
|---|---|---|
反引号 / LIMIT n,m |
SELECT `id` FROM `mt` LIMIT 0, 1 |
1 行 |
LIMIT n OFFSET m |
LIMIT 1 OFFSET 0 |
1 行 |
无 FROM / CTE / 递归 CTE |
SELECT 1 AS x;WITH x AS (…);WITH RECURSIVE r … |
均成功 |
INTERSECT / EXCEPT |
SELECT id FROM mt INTERSECT … id = 1;EXCEPT … |
1 行;其余行 |
| 窗口函数不支持 | ROW_NUMBER() OVER (ORDER BY id) |
错误:语句结束后存在多余内容:'LParen'(位于 29) |
INSERT 值只能是字面量 |
VALUES (2, UPPER('b'), 2) |
错误:期望字面量,实际遇到 'UPPER' |
INSERT … SELECT / SET / 多表 UPDATE |
三种写法 | 错误:期望关键字 'VALUES',实际遇到 'SELECT' / 'SET';错误:期望关键字 'SET',实际遇到 'Comma' |
AUTO_INCREMENT 必须 NOT NULL |
id INT AUTO_INCREMENT PRIMARY KEY |
错误:列 'id' 声明为 NULL,不能做主键;请改成 NOT NULL |
| 只按主键判重 | 唯一键冲突时 INSERT IGNORE / REPLACE / ON DUPLICATE KEY UPDATE |
三条都报 错误:唯一约束 'uk' 冲突:列 (k) 的值 ('9') 与已存在的行重复 |
| 主键冲突正常 | 主键冲突时 INSERT IGNORE / ON DUPLICATE KEY UPDATE / REPLACE |
跳过 / 更新 / 替换(id=1 的 k 依次变为 11、12) |
SHOW COLUMNS 形状 |
SHOW COLUMNS FROM mu |
TEXT→VARCHAR(255)、BOOL→TINYINT(1)、自增 Extra=auto_increment |
SHOW CREATE TABLE 丢约束 |
带 UNIQUE KEY uk (k) 的 mu |
回显只有 PRIMARY KEY;UNIQUE KEY 缺失 |
SHOW 缺表不报错 |
SHOW CREATE TABLE nope |
返回空骨架 CREATE TABLE \nope` (\n\n) ENGINE=InnoDB…` |
| 用户变量静默 | SELECT @v |
原样返回字符串 @v |
| 过程:数字/参数正常 | CREATE PROCEDURE pnum (IN n INT) BEGIN INSERT INTO mu (k) VALUES (n); END + CALL pnum(7) |
INSERT 1 行 |
| 过程:字符串字面量有缺陷 | BEGIN INSERT INTO mt (v) VALUES ('x'); END + CALL pstr() |
错误:列 'v' 的值写错了:表中不存在列 'x'。可用列: |
过程:SELECT 'x' AS s 有缺陷 |
CALL psel() |
错误:无法计算表达式:x AS s |
| 控制流可用,循环有上限 | WHILE i < n DO … END WHILE;CALL ploop(10001) |
3 次循环写入 3 行;10001 次报 错误:WHILE 循环超过 10000 次,可能是死循环 |
| 默认值 / 生成列限制 | DEFAULT CURRENT_TIMESTAMP;GENERATED ALWAYS AS (v + 1) |
错误:列 'ts':默认值 'CURRENT_TIMESTAMP' 要按写入时刻求值,尚未支持…;错误:暂不支持生成列… |
ALTER 一次一个子句 |
ADD COLUMN e INT, ADD COLUMN f INT |
错误:ALTER TABLE 一次只支持一个子句(逗号分隔的多子句暂不支持) |
索引 / 触发器 / OR REPLACE VIEW |
CREATE INDEX;CREATE TRIGGER;CREATE OR REPLACE VIEW |
错误:CREATE 后不支持的对象类型(位于 7);DROP INDEX 报 DROP 后不支持的对象类型(位于 5) |
TRUNCATE 语义不同 |
TRUNCATE TABLE mu(6 行) |
返回 DELETE 6 行,随后自增从 1 重新开始 |
DROP TABLE IF EXISTS 缺表异形 |
DROP TABLE IF EXISTS nope |
不报错,返回一行 COUNT(*) = 0 |
5. 跨方言隔离
存储过程/函数按方言隔离,同名互不可见:
# T-SQL 下建 only_tsql
### [建 T-SQL 过程(隔离用)] (standard) CREATE PROCEDURE only_tsql () AS BEGIN INSERT … END
CREATE ROUTINE
### [PL/SQL 调 T-SQL 过程] (procedural) CALL only_tsql()
错误:存储过程 'only_tsql' 不存在
### [MySQL 调 T-SQL 过程] (mysql) CALL only_tsql()
错误:存储过程 'only_tsql' 不存在
表、约束、数据是共享的(三个库都在同一个 root 下),方言只决定解析与例程的命名空间。
6. 已知缺陷复现
| 缺陷 | 复现 | 实测 |
|---|---|---|
| MySQL 过程体字符串字面量当列名 | CALL pstr() |
列 'v' 的值写错了:表中不存在列 'x'。可用列: |
MySQL 过程体 SELECT 'x' AS s |
CALL psel() |
无法计算表达式:x AS s |
| MySQL 只按主键判重 | INSERT IGNORE 命中唯一键 |
唯一约束 'uk' 冲突 |
SHOW CREATE TABLE 丢 UNIQUE KEY |
SHOW CREATE TABLE mu |
只回显 PRIMARY KEY |
SHOW CREATE TABLE 缺表回空骨架 |
SHOW CREATE TABLE nope |
一行空骨架,不报错 |
SELECT @v 静默返回字符串 |
SELECT @v |
值就是 @v |
PL/SQL ROWNUM 收下但执行不了 |
SELECT ROWNUM, id FROM pt |
SELECT 列 'ROWNUM' 不存在 |
T-SQL CHAR(n) 单字符 / VARCHAR(n) 不校验 |
见 4.1 | 见 4.1 |
MySQL DROP TABLE IF EXISTS 缺表返回 COUNT(*) 行 |
DROP TABLE IF EXISTS nope |
COUNT(*)=0 |
7. 清理
复核产生的文件都在临时目录,可直接删:
rm -rf /tmp/dbsql-dialect-verify /tmp/verify-out*.txt /tmp/plugchk
verify.sh 本身不写仓库文件,也不连接任何线上服务。