
内核一周一审:一种客户端使用体验的改进思路
内核一周一审:以 Patch 'psql: Rethinking of \du command'(一种客户端使用体验的改进思路) 为例
唯有日日有所沉淀,日日有所发展,方可成就卓尔不凡的事业,我们将继续前进,在把握需求中,不断提升水平,我们同时欢迎更多有志于发展内核的朋友们共同参与建设与讨论,在共同进步中不断赢得新胜利。
今天我们所审议的 Patch,来自于 https://commitfest.postgresql.org/49/4738/,它代表了一种对于 psql 的改进思路,在此之前,我们首先需要对一些前置的知识,展开一定的了解工作:
客户端
客户端负责接受来自于用户的指令,将其打包为数据包,依据 PostgreSQL 协议(可以参考 https://www.postgresql.org/docs/devel/protocol.html 部分的文档)将数据包提交至 PostgreSQL 服务端,并根据服务端反馈的结果,呈现给用户执行的结果。
一般而言,所有的 PostgreSQL 以及类 PostgreSQL 的发行版本,都会基于 libpq 库(可以参考 https://www.postgresql.org/docs/devel/libpq.html 部分的文档)中提供的接口展开客户端程序的编写工作,而在开发的时候,我们往往需要将其文件与路径,导入到 VSCODE 的 Include Path
,参考下图:
(请结合自身的需要,导入对应的路径)
(libpq 的目录所包含的文件如图所示,请以 libpq-fe.h 所在的目录为准,注意:一台机器可能存在多个不同版本的 libpq 库,这是因为该机器的客户端可能需要对接不同型号的 PostgreSQL 服务端)
(使用 libpq 库,需要将库文件乃至于头文件导入于环境变量之中,否则 gcc/clang 可能会搜索不到而报错)
一则良好的案例如下所示:
/*
connnect_to_postgresql.c
An example of connect to postgresql & execute sql command.
Ref: https://www.postgresql.org/docs/devel/libpq-example.html
Wen Yi
*/
#include <stdio.h>
#include <stdlib.h>
#include "libpq-fe.h"
int main(int argc, char *argv[])
{
/* Descibe the connection between server and client */
PGconn *connect;
/* The result of the server */
PGresult *res;
/* The number of the fields */
int nFields;
/* Connection Strings */
connect = PQconnectdb("dbname = postgres");
/* Connect Failed */
if (PQstatus(connect) != CONNECTION_OK)
{
fprintf(stderr, "%s", PQerrorMessage(connect));
PQfinish(connect);
return 0;
}
/* Execute "SELECT 'Hello world' AS result;" */
res = PQexec(connect, "SELECT 'Hello world' AS result");
/* Execute Failed */
if (PQresultStatus(res) != PGRES_TUPLES_OK)
{
fprintf(stderr, "Failed: %s", PQerrorMessage(connect));
PQclear(res);
PQfinish(connect);
return 0;
}
/* Print out the attribute names */
nFields = PQnfields(res);
for (int i = 0; i < nFields; i++)
printf("%10s", PQfname(res, i));
printf("\n================\n");
/* Print out the rows */
for (int i = 0; i < PQntuples(res); i++)
{
/* Print out the tuple of row */
for (int j = 0; j < nFields; j++)
{
printf("%13s\n", PQgetvalue(res, i, j));
}
}
PQclear(res);
PQfinish(connect);
return 0;
}
在正常的执行结束时,程序的执行结果,如下所示:
而 psql,可以视作为对这一客户端程序的延伸,只不过它所需要考虑的情况,复杂许多,我们在本次“内核一周一审”中,将就它的一个部分加以解读。
PostgreSQL Roles
在 PostgreSQL 中,Roles 被用于描述一组权限的集合(请参考 https://www.postgresql.org/docs/current/user-manag.html 部分的文档),每拥有一种权限,即可展开对应的工作(这里必须体积 User 的概念,User 是被给予了登录权限(LOGIN)权限的 Roles,而值得注意的事情在于,倘若没有给予登录的权限,我们是无法使用一个 Roles 连接到 PostgreSQL 的),而在 PostgreSQL 的系统表 pg_roles 中,我们可以查看各个 Roles 被给予的权限。
/* 查看所有的 Roles 及其拥有的权限 */
SELECT * FROM pg_roles;
/* 查询所有具备登录权限的 Roles */
SELECT rolname FROM pg_roles WHERE rolcanlogin = 't';
而除了使用 SELECT 自 pg_roles 查询之外,psql 的元命令 \du,同样为查询 Roles 提供了支持(本质还是对 SQL 语句的封装),我们将在随后的内容中,详细阐述。
psql 的元命令管理模式,以及对 '\du' 指令的实现分析
元命令,即为客户端工具自身的拓展指令,在 psql 工具之中,可以通过 '?' 了解到 psql 所提供的元命令,如图所示:
(? 所展现的一部分信息)
而我们通过在源代码中搜索 '?',即可以查找出对应的代码位置
依据同样的逻辑,我们可以尝试搜索 \du,即可以找到对应的代码实现,参考如下:
/* src/bin/psql/describe.c */
/*
* \du or \dg
*
* Describes roles. Any schema portion of the pattern is ignored.
*/
bool
describeRoles(const char *pattern, bool verbose, bool showSystem)
{
/*
PQExpBufferData 是 PostgreSQL 中一种构建可变程度字符串的方式,
可以将其理解为记录了长度与最大长度限制的C语言字符数组
*/
PQExpBufferData buf;
/* 用于存储结果 */
PGresult *res;
/* 负责描述输出的数据表与对于数据表形式本身的设置 */
printTableContent cont;
printTableOpt myopt = pset.popt.topt;
int ncols = 2;
int nrows = 0;
int i;
int conns;
const char align = 'l';
char **attr;
myopt.default_footer = false;
/* buf 用于存储待执行的 SQL 语句 */
initPQExpBuffer(&buf);
printfPQExpBuffer(&buf,
"SELECT r.rolname, r.rolsuper, r.rolinherit,\n"
" r.rolcreaterole, r.rolcreatedb, r.rolcanlogin,\n"
" r.rolconnlimit, r.rolvaliduntil");
/* 为 \du+ 而准备,如果指定了 +,则打印额外的一定信息 */
if (verbose)
{
appendPQExpBufferStr(&buf, "\n, pg_catalog.shobj_description(r.oid, 'pg_authid') AS description");
ncols++;
}
appendPQExpBufferStr(&buf, "\n, r.rolreplication");
/*
PostgreSQL 的版本号规则
请参考 https://www.postgresql.org/support/versioning/
以及 src/include/pg_config.h 中的有关记录
*/
if (pset.sversion >= 90500)
{
appendPQExpBufferStr(&buf, "\n, r.rolbypassrls");
}
appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_roles r\n");
/* 在 \duS 打开的情况下,psql 将不会规避 PostgreSQL 系统默认 Roles 的输出 */
if (!showSystem && !pattern)
appendPQExpBufferStr(&buf, "WHERE r.rolname !~ '^pg_'\n");
/*
验证 pattern 表达式是否正确,注意正则表达式只是 PostgreSQL 中的其中一种
pattern 表达式
请参考 https://www.postgresql.org/docs/current/functions-matching.html
*/
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "r.rolname", NULL, NULL,
NULL, 1))
{
termPQExpBuffer(&buf);
return false;
}
appendPQExpBufferStr(&buf, "ORDER BY 1;");
/* 开始执行构造好的 SQL 指令 */
res = PSQLexec(buf.data);
if (!res)
return false;
/* 判断返回的元组数量 */
nrows = PQntuples(res);
attr = pg_malloc0((nrows + 1) * sizeof(*attr));
/* 打印输出的标题 */
printTableInit(&cont, &myopt, _("List of roles"), ncols, nrows);
/* 添加输出的字段 */
printTableAddHeader(&cont, gettext_noop("Role name"), true, align);
printTableAddHeader(&cont, gettext_noop("Attributes"), true, align);
if (verbose)
printTableAddHeader(&cont, gettext_noop("Description"), true, align);
/* 诸行元组提取反馈结果,逐个字段的判断,并且给出反馈意见 */
for (i = 0; i < nrows; i++)
{
printTableAddCell(&cont, PQgetvalue(res, i, 0), false, false);
resetPQExpBuffer(&buf);
/*
构造好的 SQL 指令中,各个字段分别代表某种权限,具体的含义可以参考
https://www.postgresql.org/docs/current/view-pg-roles.html
*/
/* add_role_attribute 代表了一种输出后的结果 */
if (strcmp(PQgetvalue(res, i, 1), "t") == 0)
add_role_attribute(&buf, _("Superuser"));
if (strcmp(PQgetvalue(res, i, 2), "t") != 0)
add_role_attribute(&buf, _("No inheritance"));
if (strcmp(PQgetvalue(res, i, 3), "t") == 0)
add_role_attribute(&buf, _("Create role"));
if (strcmp(PQgetvalue(res, i, 4), "t") == 0)
add_role_attribute(&buf, _("Create DB"));
if (strcmp(PQgetvalue(res, i, 5), "t") != 0)
add_role_attribute(&buf, _("Cannot login"));
if (strcmp(PQgetvalue(res, i, (verbose ? 9 : 8)), "t") == 0)
add_role_attribute(&buf, _("Replication"));
if (pset.sversion >= 90500)
if (strcmp(PQgetvalue(res, i, (verbose ? 10 : 9)), "t") == 0)
add_role_attribute(&buf, _("Bypass RLS"));
conns = atoi(PQgetvalue(res, i, 6));
if (conns >= 0)
{
if (buf.len > 0)
appendPQExpBufferChar(&buf, '\n');
if (conns == 0)
appendPQExpBufferStr(&buf, _("No connections"));
else
appendPQExpBuffer(&buf, ngettext("%d connection",
"%d connections",
conns),
conns);
}
if (strcmp(PQgetvalue(res, i, 7), "") != 0)
{
if (buf.len > 0)
appendPQExpBufferChar(&buf, '\n');
appendPQExpBufferStr(&buf, _("Password valid until "));
appendPQExpBufferStr(&buf, PQgetvalue(res, i, 7));
}
attr[i] = pg_strdup(buf.data);
printTableAddCell(&cont, attr[i], false, false);
if (verbose)
printTableAddCell(&cont, PQgetvalue(res, i, 8), false, false);
}
/* 信息提取完成,清空缓冲区 */
termPQExpBuffer(&buf);
/* 打印数据表 */
printTable(&cont, pset.queryFout, false, pset.logfile);
printTableCleanup(&cont);
/* 清理剩余资源,结束工作 */
for (i = 0; i < nrows; i++)
free(attr[i]);
free(attr);
PQclear(res);
return true;
}
总结整体流程,可以归纳如下图:
对 Patch 'psql: Rethinking of \du command' 的分析
Patch 是 PostgreSQL 改进的基础单位,与目前流行的 “Issue-Pr” 改进模型对比,可以参考下图:
(两者殊途而同归,本质上都是架构于 git 上的上层建筑)
而我们这一次展开分析的 Patch,其核心方面便是对于用户体验的改进, 最关键的信息如下所示:
It seems weird that some attributes are described in the negative
("Cannot login", "No inheritance"). I realize that this corresponds
to the defaults, so that a user created by CREATE USER with no options
shows nothing in the Attributes column; but I wonder how much that's
worth.
在表达上来说,一些属性的描述似乎相当消极,
如 (“Cannot login”, “No inheritance”),
我意识到这些都对应的是一些默认的选项,
因此一名经由 CREATE USER 创建的 Roles 将不会被显示在属性列表中,
但是我非常希望清楚这种做法的价值究竟有多大。
因此,在这种看法的基础之上,作者提供的 Patch 将目光聚焦于用户体验的改进中,如图所示:
(--- 代表修订前的内容,+++ 代表修订后的内容)
而具体的做法,就是对我们先前所提及的代码,展开一定的改进,如图所示:
(本质就是对构造的 SQL 指令做出了调整)
关于 CASE 语句,可以参考 https://www.postgresql.org/docs/current/functions-conditional.html 部分文档,它的核心作用,在于逻辑判断,类似于C语言中的 if 语句,而在此处的 Patch 中,作者将原本在后续填入的 “Cannot Login” 等,合入了此处的 SQL 中,由此就起到了简化逻辑,并且因为用语上面,以及输出格式上面的改进,解决了作者提出来的问题。
而通过对于这一个 Patch 的分析,我们可以对 PostgreSQL 的客户端工具 psql 建立一个基本的了解,并且对于如何参与到 PostgreSQL 的内核改进之中,建立一个相对应的了解。