内核一周一审:一种客户端使用体验的改进思路

内核一周一审:一种客户端使用体验的改进思路

文一

2024-09-13 发布79 浏览 · 0 点赞 · 0 收藏

内核一周一审:以 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,参考下图:

vscode-include_path

(请结合自身的需要,导入对应的路径)

postgres-libpq

(libpq 的目录所包含的文件如图所示,请以 libpq-fe.h 所在的目录为准,注意:一台机器可能存在多个不同版本的 libpq 库,这是因为该机器的客户端可能需要对接不同型号的 PostgreSQL 服务端

bashrc

(使用 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;
}

在正常的执行结束时,程序的执行结果,如下所示:

connnect_to_postgresql

而 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 所提供的元命令,如图所示:

psql-built-in-command

(? 所展现的一部分信息)

而我们通过在源代码中搜索 '?',即可以查找出对应的代码位置

search-code

依据同样的逻辑,我们可以尝试搜索 \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;
}

总结整体流程,可以归纳如下图:

process

对 Patch 'psql: Rethinking of \du command' 的分析

Patch 是 PostgreSQL 改进的基础单位,与目前流行的 “Issue-Pr” 改进模型对比,可以参考下图:

patch-github

(两者殊途而同归,本质上都是架构于 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 将目光聚焦于用户体验的改进中,如图所示:

patch-improve

(--- 代表修订前的内容,+++ 代表修订后的内容)

而具体的做法,就是对我们先前所提及的代码,展开一定的改进,如图所示:

compare

(本质就是对构造的 SQL 指令做出了调整)

关于 CASE 语句,可以参考 https://www.postgresql.org/docs/current/functions-conditional.html 部分文档,它的核心作用,在于逻辑判断,类似于C语言中的 if 语句,而在此处的 Patch 中,作者将原本在后续填入的 “Cannot Login” 等,合入了此处的 SQL 中,由此就起到了简化逻辑,并且因为用语上面,以及输出格式上面的改进,解决了作者提出来的问题。

而通过对于这一个 Patch 的分析,我们可以对 PostgreSQL 的客户端工具 psql 建立一个基本的了解,并且对于如何参与到 PostgreSQL 的内核改进之中,建立一个相对应的了解。