跳转至

0.3 前端约定

前端含义

首先本节标题中的“前端”不是指 GUI (Graphical User Interface,图形界面),而是指我们设计的系统(更像是一个引擎)与人交互时所提供的输入输出形式。

在本文档诞生之前,实验验收采用手动测试的方法(如查看输出总行数),输入方面同样也没有给出足够具体的要求,例如是否需要支持多行输入,分号隔开的多语句输入等。最终同学们作业大多呈现为 CLI (Command-Line Interface,俗称“命令行”) 下的单行输入、| 等符号作为列分隔符的多行输出。

现在我们在这方面进行给出了一定的规范,以便完成手动检查和自动化测试。我们将手动检查时的模式称作“交互模式”,将面向自动化测试的模式称作“批处理模式”。强烈建议你后端运行结果统一返回为包含meta信息和数据的“表”,前端输出时再根据当前的运行模式来分别输出为不同的格式(但 DESC 需要特别处理,其要求见后文)。

自动化测试

实验从 2023 年秋季学期开始引入了基于 GitLab CI 的自动化测试,并作为评分标准的一部分,因此批处理模式与交互模式都是必须支持的。

运行模式

1. 交互模式

交互模式可以参考 MySQL 的模式,如下是一个示例

mysql> SELECT * FROM student;
+------------+--------+-----------+------+
| id         | name   | status    | sex  |
+------------+--------+-----------+------+
| 2077310001 | 王五   | 博士生    | 男   |
| 2077010001 | 张三   | 本科生    | 男   |
| 2077210001 | 李四   | 硕士生    | 男   |
+------------+--------+-----------+------+
3 rows in set (0.000 sec)
mysql> SELECT COUNT(*)
    -> FROM
    -> student
    ->
    -> ;
+----------+
| COUNT(*) |
+----------+
|        3 |
+----------+
1 row in set (0.000 sec)
mysql>

交互模式不给出严格的要求,主要为了便于自己调试以及助教检查,能看出数据即可。仿照 MySQL 的 shell,以下给一些可选地参考实现细节:

  • 输入时在左边给出前缀并给出当前使用的数据库名
  • 跨行输入时给出规整前缀(无需考虑“修改上一行”的操作)
  • 输入在最后一个字符是 ; 后停止
  • 尝试画表格边框以提高美观度
  • 尝试计算出一列的最大宽度后输出时用空格补齐以保持规整
  • 在输出表格后面给出行数和后端查询时间
  • 输出某一字段时,对应列的表头采用 <table_name>.<column_name> 的格式输出(尤指 JOIN 意义下),当字段名没有歧义时可以只输出 <column_name> 以精简显示
  • 输出聚合查询(若实现了该功能)时直接以 SELECT 后的 selector 作为对应列的表头即可。

没有歧义的字段名

在多表查询下可能多张表有相同的字段,例如当我们多表查询 course, course_detail, teacher 时,如果表头显示 name 则会有 course.nameteacher.name 的歧义,而显示 creditcourse.credit 均可确定地指明同一字段。

2. 批处理模式

批处理模式下我们不在乎输入输出的友好性,通常是从 stdin 或者一个 SQL 文件读入多行数据,再输出到 stdout 或一个指定文件。在使用 stdin 和 stdout 时常常搭配重定向。

批处理模式下的输出就如同做 OJ 一般不能夹带任何冗余信息,必须按照约定的格式给出数据,常见格式是用 tab 分隔符,如 MySQL 等成熟的数据库系统会支持包括 CSV、JSON 在内的多种输出方式自选。

为了适配自动化测试,我们为批处理模式做出如下约定:

  • 完成一个 SQL 语句执行结果的输出后,额外输出 @ 开始的一行作为分隔符,你可以在 @ 后面加入注释以便调试等(如标记 SQL 执行时长),这不会影响评测。
  • 执行结果输出到 stdout,你可以将调试信息输出到 stderr,评测器不会分析这些输出
  • 仅下述语句需要给出对应表头并输出内容(即便没有内容也应该输出一个表头),其他直接输出分隔符即可:
    • SELECT 语句,注意表头精简与否都可以,简便起见在 SELECT 后的 selector 不是 * 时,表头与 selector 一致即可,尤其是聚合查询,我们承诺不会添加额外的空格等导致隐藏的坑;在 selector* 时一定是单表查询,表名输出与否不影响答案正确性。
    • SHOW 开头的语句,对于 SHOW XX 的语句,输出的表头即为 XX
    • INSERTDELETEUPDATELOAD 开头的语句均输出一个表头为 rows、内容为一个整数的表来展示插入/删除/更新/导入的行数。注意,对于 UPDATE 操作应该显示数据被修改的行数而非匹配到的行数,也即修改前后数据不变的行不应被计入内。
    • 任何语句如果破坏了完整性约束,则应该输出一个表头为 !ERROR,内容为一个字符串说明错误信息(你可以统一将批处理模式下所有的错误都这样输出以减少工作量,因为我们的测例中保证了不会有其他错误情况),CI 检查会采用关键词检测的办法判断你的报错信息,错误类型和关键词如下(注:无视大小写,且不能包含其他约束被破坏时用到的关键词):
      • 增改记录导致破坏唯一性约束(包含主键和选做的 UNIQUE):duplicate
      • 任何原因导致破坏外键约束:foreign
      • Schema 的修改导致主键约束的数量错误:primary
      • 特别的,在选做内容的“日期”类型中,如果输入的字符串不是一个合法的日期:date
  • 输出统一使用简化的 CSV 格式:同一行的不同字段间用英文逗号 , 分隔,无需考虑数据包含逗号的二义性问题。
  • 需要输出多列数据时,如果多列来自 SELECT *,则列序是任意的,否则列应该按照 SELECT 后面给出的顺序给出,包括 DESC 指令的输出也应该按照后文要求给出列序。
  • 需要输出多行数据时,如果不涉及 ORDER BY 则行序可以是任意的,否则应该按照 ORDER BY 的要求给出,排序关键字相同的若干行顺序是任意的。此外我们约定排序关键字一定是整型或浮点数,不会是字符串
  • FLOAT 在存储上应采用64位的双精度浮点数(即C++中的 double),输入时保证小数位数不超过两位,输出时统一精确到小数点后两位;
  • 无需考虑非 ASCII 字符的编码及长度问题,最终测试保证全是 ASCII 可打印字符(你自行测试时它很可能是由你终端的设置决定);
  • 不考虑语法错误以及一些边界情况导致需要报错的情形(对应内容的检查会在交互式模式下进行),但是注意出于验收效率考虑,完整性约束仍然需要在自动化测试中被考虑,且在自动化测试中对于完整性约束我们给出下述条件

    • 会破坏完整性约束的插入、删除、更新语句一定只操作至多一行记录
    • 破坏完整性约束的语句一次只会破坏一个完整性约束,不会同时破坏主外键约束
    • 具体输出格式见上文
  • 无需输出执行时间和行数统计;

  • 使用命令行可选参数 -b 表示以批处理模式启动;
  • 使用命令行可选参数 -d <database> 指定数据库名,即相当于已经执行了 use <database>; (交互模式也适用);

命令行参数

命令行参数可以用 argv 来手动处理,也可以使用一些用于处理命令行参数的库,例如 C++ 可以使用单头文件的第三方库 argparse ,Python 可以使用内置库 argparse

数据导入

考虑到部分同学实现大量 INSERT 时效率并不高,为了完成压力测试,我们需要新增一种操作将数据直接导入数据库。

往常没有统一约定读入指令或参数,现在统一约定如下:

  • 数据导入需要在批量模式下进行,且必须指定数据库;
  • 使用命令行可选参数 -f <path> 指定导入文件的路径;
  • 使用命令行可选参数 -t <table> 指定目标表名;

在上述约定下,假设我们编译得到的可执行程序是 mydb,则可以通过类似如下语句进行数据导入

./mydb -b -f path/to/data/student.txt -t student -d curriculum

此外我们也约定一种 SQL 语法用于在数据库进程运行期间导入数据以减少进程启动次数,详见 SQL.g4 中的 load_table。在实际自动化测试时可能会用到上述命令行参数,也可能用到下述 SQL 语句,你需要两者都实现,但这并不必做两次工作,它们在已经取得数据库、文件路径、表名三个参数的情况下完全可以用同一个函数处理。注意,使用 LOAD DATA 前应该已经指定过当前在使用的数据库。

下面是用 load_table 语法时一个与上述命令行参数有相同效果的 SQL 指令:

USE curriculum;
LOAD DATA INFILE "path/to/data/student.txt" INTO TABLE student FIELDS TERMINATED BY ',';

注意,后面的 FIELDS TERMINATED BY ',' 仅仅是为了体现我们文法与 MySQL 的兼容性,实验并不要求实现不同字符甚至不同字符串的分隔符,你可以在处理时直接忽略这部分内容。我们约定数据格式同批量模式输出一样采用 CSV,且字符串中不会出现 ,,因此可以很方便地手动解析。

另外,我们默认使用数据导入时数据是“安全”的,即格式正确、满足完整性约定等,无需在这部分做对应检查,你应当采用尽可能高效的方法将数据批量导入,例如填满一页记录后一次写入理应比逐条写入记录更快。

高效导入

数据导入理论上能够比 INSERT INTO 效率更高,一方面导入时不需要检查完整性约束,另一方面可以将批量导入实现为预先写满一整页后再将该页写入文件,减少缓存拷贝次数和文件IO次数。

注意,虽然不用检查完整性约束,但是约束仍然要建立,例如外键仍然要建立对应的索引,你可以自行考虑应该边导入数据边建立索引,还是等数据全部导入后一次建立索引,这可能会有不小的性能差异。

desc

这里我们给出 DESC <table_name> 指令的输出规范:

  1. 首先输出一张表(格式同 SELECT),表中包含从左到右四列: Field、Type、Null、Default
  2. 接下来输出主键信息
  3. 然后输出外键信息
  4. (如果实现了)输出 UNIQUE 信息
  5. 最后输出索引信息

注意上面五部分中只有第一部分是一定有的(一张表至少有一列,我们的语法文件已经保证了这一点),剩下四部分如果没有则直接跳过,如果有则一行一条信息,每条信息后加分号。每一部分各条信息的顺序不作要求。在批处理模式中,第一部分的“表”按前文提及的简化的 CSV 格式输出,后面部分按照上述要求输出。

这里用举例的方法给出一个参考,注意这里相比后端约定章节中的 student_course 表多了几个字段用于演示 UNIQUE 与 INDEX,这里的 index_field_Aindex_field_a 形成组合索引,而 index_field_B 是单列索引。另外下面 course_id_number 是一个命名外键,其他的约束都是匿名的,如果有名字则都应该加在第一个小括号左边,和小括号之间没有空格。

mydb> desc student_course;
+---------------+-------------+------+---------+
| Field         | Type        | Null | Default |
+---------------+-------------+------+---------+
| student_id    | INT         | NO   | NULL    |
| course_id     | INT         | NO   | NULL    |
| course_number | INT         | NO   | NULL    |
| grade         | VARCHAR(3)  | YES  | NULL    |
| term          | VARCHAR(16) | NO   | NULL    |
| unique_field  | INT         | NO   | NULL    |
| index_field_A | INT         | NO   | NULL    |
| index_field_a | INT         | NO   | NULL    |
| index_field_B | INT         | NO   | NULL    |
+---------------+-------------+------+---------+
PRIMARY KEY (student_id, course_id, term);
FOREIGN KEY (student_id) REFERENCES student(id);
FOREIGN KEY course_id_number(course_id, course_number) REFERENCES course_detail(course_id, course_number);
UNIQUE (unique_field);
INDEX (index_field_A, index_field_a);
INDEX (index_field_B);

注意,在自动化测试时输出完表格后,在输出约束信息前必须输出一个空行作为分隔(如果没有任何约束信息,则空行输出与否不影响结果正确性),并且下面的格式需要严格按照上述标准。由于我们的文法允许生成匿名主外键、索引,同学们可能会为它们设定不同的默认名字,因此在命名上我们给出如下约束:

  • 输出约束名与否不影响正确性,建议你输出自己所存的约束名以便于调试
  • 自动化测试中 DROP 主键时一定不给出主键名
  • 自动化测试中如果要 DROP 其他约束,则一定会在创建时为它命名
  • 自动化测试时约束名长度一定不超过32

Default NULL

注意默认值显示为 NULL 有两种含义:当该字段有 NOT NULL 约束时,表示没有设置默认值,此时 INSERT 必须对该字段赋值;否则表示默认值为 NULL

在必做内容中不要求支持部分字段插入,也不要求实现新增列的操作,因此 Default 实际没有用上。

对于建立外键是否显示索引、建立主键是否自动创建 UNIQUE 约束、删除主外键时是否删除手动创建过的索引等问题,文档没有做细节要求,取决于自己的实现标准。为了规范自动化测试的要求,这里将因为创建外键、主键等约束带来的索引称作“隐式索引”,通过包含 INDEX 关键字的语句手动创建的索引称作“显式索引”,我们统一要求 DESC 只显示显式索引,不显示隐式索引。同时我们保证在自动化测试的测例中不会出现隐式索引和显式索引混淆的情况,例如保证如果已经建立联合索引 $(C_1,C_2)$,则在删除它之前一定不会再建立主键 $(C_1,C_2)$,反之亦然。

Authors: Congyuan Rao