Go 语言 CGO 用户深度定制 SQLite 代码

Go 语言 CGO 用户深度定制 SQLite 代码

本文是 BRUNO CALZA 记录的关于如何改变SQLite源代码,使记录行更新时可用于 Go 的更新钩子函数的过程。原文通过深度定制 C 语言的 API 函数达成目的,这几乎是所有 CGO 深度用户必然经历的过程(关于 CGO 的基本用法可以参考译者的《Go高级编程》第2章),是一个非常有借鉴意义的技术文章。

  • 作者:BRUNO CALZA
  • 译者:柴树杉,凹语言 作者、Go语言贡献者、多本Go语言图书作者,目前在蚂蚁从事 KusionStack 和 KCL 开发。
  • 原文:https://brunocalza.me/making-a-change-to-sqlite-source-code/

1. 背景

有一天,我正在考虑如何在 SQLite 中获取最近插入或更新的行记录的数据。这样做的动机是我想创建该行的 hash,本质上是为了在插入或更新行时能够构建相应表的 Merkle 树。

SQLite 提供的最符合的 API 可能是 sqlite3_update_hook:

sqlite3_update_hook() 函数为数据库连接注册一个回调函数,该数据库连接由第一个参数标识,在 rowwid 表中更新、插入或删除行时调用。

这个 API 的问题是它只返回行的 rowid。这意味着还需要为列内的行获取所有列。即使使用这种方法,我仍然无法获得行记录的原始数据。只能得到那一行的驱动信息。

关于如何构建这样的树可能有很多方法,但就我而言 SQLite API 并没有提供真正想要的东西。因此,我决定趁此机会更深入地挖掘下源代码,同时看看内部实现的细节。不仅如此,我希望可以对它进行一些修改和测试,看看能否满足需求。

因为对 C 语言的畏惧,开始我只是想假装看下几个源文件就跑路。没想到这次真的有惊喜。

2. 看看 SQLite 的代码结构

首先使用 fossil 工具克隆了 SQLite源代码,下面是文件。

Go 语言 CGO 用户深度定制 SQLite 代码

如果你对数据库比较熟悉,或许可以猜测出一些文件对应的操作。因此,我决定直接跳到 insert.c 文件,看看能不能找到一些有趣的东西。

遍历函数名列表,路过 sqlite3Insert 函数,看到以下注释:

123456
** This routine is called to handle SQL of the following forms:****    insert into TABLE (IDLIST) values(EXPRLIST),(EXPRLIST),...**    insert into TABLE (IDLIST) select**    insert into TABLE (IDLIST) default values**

也许在这个函数中有一些可鼓捣的地方。我能够对其中发生的情况进行一些猜测,但引起我注意的是对名称类似于 sqlite3vdbeXXX 的函数的函数调用的数量。

这让我想起 SQLite 底层使用了一个名为 vdbe 的虚拟机。这意味着所有SQL语句都首先被翻译成该虚拟机的语言。然后,执行引擎执行虚拟机代码。让我们看一个简单的 INSERT 语句如何被翻译成字节码:

1234567891011121314
sqlite> create table a (a int, b text);sqlite> explain INSERT INTO a VALUES (1, 'Hello');addr  opcode         p1    p2    p3    p4             p5  comment      ----  -------------  ----  ----  ----  -------------  --  -------------0     Init           0     8     0                    0   Start at 81     OpenWrite      0     2     0     2              0   root=2 iDb=0; a2     Integer        1     2     0                    0   r[2]=13     String8        0     3     0     Hello          0   r[3]='Hello'4     NewRowid       0     1     0                    0   r[1]=rowid5     MakeRecord     2     2     4     DB             0   r[4]=mkrec(r[2..3])6     Insert         0     4     1     a              57  intkey=r[1] data=r[4]7     Halt           0     0     0                    0   8     Transaction    0     1     1     0              1   usesStmtJournal=09     Goto           0     1     0                    0

我得出的结论是 sqlite3Insert 实际上是根据SQLite插入规则,将解析后的 INSERT 语句转换为一系列虚拟机字节码指令。

因此这并不是我要找的地方。我真正需要的是在插入之前创建记录的位置。我猜测那只能是执行虚拟机代码的地方,可能是执行 Insert (OP_INSERT) 操作码的地方。

根据上图我直接找到了 vdbe.c 文件的位置,直奔主题。

我发现有一个有 8000行代码的 switch( pOp->opcode ) 语句,通过 OP_INSERT 关键字找到插入操作对应的代码位置。

在对应分支的第一行中,总算找到了相关的线索:

1
Mem *pData;       /* MEM cell holding data for the record to be inserted */

所以 pData 指向要插入的记录数据。您可以在 L5402 中看到 pData = &aMem[pOp->p2];,它是如何将 pData 值设置为虚拟机内存 aMem 地址的,该地址位于虚拟机寄存器 p2 所指向的位置。

快速回顾一下: 首先在 insert.c 文件我们了解到 INSERT 语句被翻译成一堆虚拟机指令。然后通过 INSERT 的数据通过这些 sqlite3vdbeXXX 调用到达虚拟机。我假设将 OP_INSERT 操作码和数据注册到虚拟机是在第2593行:

1
sqlite3VdbeAddOp3(v, OP_Insert, iDataCur, aRegIdx[i], regNewData);

下面 regNewData 的一个更详细的说明:

123456789101112
** The regNewData parameter is the first register in a range that contains** the data to be inserted or the data after the update.  There will be** pTab->nCol+1 registers in this range.  The first register (the one** that regNewData points to) will contain the new rowid, or NULL in the** case of a WITHOUT ROWID table.  The second register in the range will** contain the content of the first table column.  The third register will** contain the content of the second table column.  And so forth.**** The regOldData parameter is similar to regNewData except that it contains** the data prior to an UPDATE rather than afterwards.  regOldData is zero** for an INSERT.  This routine can distinguish between UPDATE and INSERT by** checking regOldData for zero.

所以,在这一点上,我们正在用数据执行机器代码。代码向下滚动一点,让我们看看如何使用 pData。在 L5448-L5449 处可以看到:

12
x.pData = pData->z;x.nData = pData->n;

x 的定义如下:

1
BtreePayload x;   /* Payload to be inserted */

完美。再向下滚动一点,我们看到:

1234
rc = sqlite3BtreeInsert(pC->uc.pCursor, &x,(pOp->p5 & (OPFLAG_APPEND|OPFLAG_SAVEPOSITION|OPFLAG_PREFORMAT)),seekResult);

我们终于找到了插入原始数据的位置。但是,我们怎么知道它的格式和这里记录的一样呢? 如果仔细查看示例 INSERT 中的虚拟机代码,在 INSERT 操作码之前有一个 MakeRecord 操作码,它负责构建记录。

你可以在 vdb.c 文件中查看 OP_MakeRecord 实现,并看到以下注释:

You can check the OP_MakeRecord implementation at vdbe.c file and see the following comment:

P1 开头的 P2 寄存器转换为记录格式,用作数据库表中的数据记录或索引中的键。

case 语句的最后几行看到了关键部分:

12345678910
/* Invoke the update-hook if required. */if( rc ) goto abort_due_to_error;if( pTab ){assert( db->xUpdateCallback!=0 );assert( pTab->aCol!=0 );db->xUpdateCallback(db->pUpdateArg,(pOp->p5 & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_INSERT,zDb, pTab->zName, x.nKey);}break;

看来我需要的东西都在这里了。更新钩子钩子和原始数据。只需要更新时传递给回调函数即可。

3. 开始定制 SQLite

这就是我期望的 API:

123
db->xUpdateCallback(db->pUpdateArg, (pOp->p5 & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_INSERT,    zDb, pTab->zName, x.nKey, pData->z, pData->n);

传递的是数据( pData->z)和其大小(pData->n)。

为了解释函数签名的变化,还需要在多个地方进行相应的修改。

以下是 fossil 工具提示的变化的源文件:

123456
EDITED     src/main.cEDITED     src/sqlite.h.inEDITED     src/sqlite3ext.hEDITED     src/sqliteInt.hEDITED     src/tclsqlite.cEDITED     src/vdbe.c

还有一些针对编译提示的修改。

4. 克隆一份 Go SQLite 驱动

现在是时候在一个 Go 程序中创建一个简单的测试了。我比较熟悉与 SQLite 交互的 mattn/go-sqlite3 驱动程序。该项目通过导入SQLite合并文件并通过CGO绑定工作。

因此还需要再克隆下 Go SQLite 驱动,更新被我修改的文件。并在Go API中进行了必要的更新以访问新值。

主要是对 updateHookTrampoline 的更改,现在接收记录为 *C.Charint 类型的数据大小,转型为字节 Slice 并将其传递给回调函数:

1234
func updateHookTrampoline(handle unsafe.Pointer, op int, db *C.char, table *C.char, rowid int64, data *C.char, size int) {  callback := lookupHandle(handle).(func(int, string, string, int64, []byte)) callback(op, C.GoString(db), C.GoString(table), rowid, C.GoBytes(unsafe.Pointer(data), C.int(size)))}

RegisterUpdateHook 函数也需要做同样的调整。

5. 改动后的效果

现在已经准备好了测试的所有东西。让我们运行一个简单的例子,灵感来自 SQLite Internals: Pages & B-trees 博客文章。

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
package mainimport (    "database/sql"  "fmt"   "log"   "os"    "github.com/mattn/go-sqlite3")func main() { sqlite3conn := []*sqlite3.SQLiteConn{}  sql.Register("sqlite3_with_hook_example",       &sqlite3.SQLiteDriver{          ConnectHook: func(conn *sqlite3.SQLiteConn) error {             sqlite3conn = append(sqlite3conn, conn)             conn.RegisterUpdateHook(func(op int, db string, table string, rowid int64, data []byte) {                   switch op {                 case sqlite3.SQLITE_INSERT:                     fmt.Printf("%x\n", data)                    }               })              return nil          },      })  os.Remove("./foo.db")   srcDb, err := sql.Open("sqlite3_with_hook_example", "./foo.db") if err != nil {     log.Fatal(err)  }   defer srcDb.Close() srcDb.Ping()    _, err = srcDb.Exec(CREATE TABLE sandwiches (      id INTEGER PRIMARY KEY,     name TEXT,      length REAL,        count INTEGER   );)    if err != nil {     log.Fatal(err)  }   _, err = srcDb.Exec("INSERT INTO sandwiches (name, length, count) VALUES ('Italian', 7.5, 2);") if err != nil {     log.Fatal(err)  }}

不要忘记添加更新 go.mod 文件 replace github.com/mattn/go-sqlite3 => github.com/brunocalza/go-sqlite3 v0.0.0-20220926005737-36475033d841,重新定向驱动。

运行后应该得到以下的结果:

1
05001b07014974616c69616e401e00000000000002

这正是 ('Italian', 7.5, 2) 数据的 Efficient Sandwich 编码的结果,不包含主键和记录的长度(前两个字节)。

看到输出结果我才发现能够理解SQLite源代码的部分内容真的很有趣,尽管我不理解它的大部分。但是我做了一些更改并看到这些更改,并通过 Go 的驱动程序看到结果的变化。

老实说这种更改数据库源代码的方法风险太大。与新版本保持同步也是一个太大的问题,但这是一个值得记录的有趣经历。

Go 语言 CGO 用户深度定制 SQLite 代码

Original: https://blog.csdn.net/chai2010/article/details/127544350
Author: chai2010
Title: Go 语言 CGO 用户深度定制 SQLite 代码

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/816328/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Centos7搭建kafka集群

    1. 环境 环境 ip 软件 Centos7 192.168.2.5(node01) jdk,zookeeper,kafka Centos7 192.168.2.6(node02)…

    大数据 2023年5月28日
    078
  • 零基础简单易用的EmberJS框架

    为什么要使用ember.js ​ember.js是一个JavaScript框架,它大大减少了构建任何web应用程序所需的时间、精力和资源。它专注于让你,开发人员,通过做所有常见的,…

    大数据 2023年5月27日
    070
  • Hadoop之HDFS03【NameNode工作原理】

    NameNode的职责 序号 职责 1 负责客户端请求的响应 2 元数据的管理(查询,修改) 数据存储的形式 NameNode中的元数据信息以三种形式存储,如下 序号 方式 说明 …

    大数据 2023年5月26日
    072
  • 移动开发技术作业3

    作业要求 1、contentprovider是安卓四大组件之一,请使用其方法类进行数据获取; 2、请自建一个provider,然后在另一个app中使用resolver调用这个pro…

    大数据 2023年11月11日
    057
  • SQLite学习手册(一)

    1). 动态创建表。2). 根据sqlite3提供的API,获取表字段的信息,如字段数量以及每个字段的类型。3). 删除该表。见以下代码及关键性注释: #include <s…

    大数据 2023年11月11日
    044
  • C# winform使用SQLite

    本文仅是一个笔记,仅供参考。 SQLite SQLite是遵守ACID的关系数据库管理系统,它包含在一个相对小的C程序库中。与许多其它数据库管理系统不同,SQLite不是一个客户端…

    大数据 2023年11月10日
    038
  • C#+SQLite操作之一 连接数据库

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言 一、需要做哪些配置 二、创建数据库 三、C#连接数据库 总结 前言 某非联网设备采用了C#+SQL…

    大数据 2023年11月10日
    052
  • docker 容器方式部署 openvpn

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    大数据 2023年5月29日
    069
  • 【NLP】基于隐马尔可夫模型(HMM)的命名实体识别(NER)实现

    ; 背景 上篇文章【NLP】再看隐马尔可夫模型(HMM)原理已经介绍了隐马尔可夫模型的基本理论和相关概念。命名实体识别(named entity recognition, NER)…

    大数据 2023年5月28日
    062
  • ES: WeakSet

    [对象、Map、Set、WeakMap、WeakSet 对象、Map、Set、WeakMap、WeakSet本文写于 2020 年 11 月 24 日总的来说,Set 和 Map …

    大数据 2023年5月24日
    077
  • 定时任务调度中心(xxl-job)

    前言 在分布式架构中项目部署在多台不同的服务器上,每台服务器都有自己的crontab任务很容易造成任务执行冲突且不易于定时任务的统一管理; 此时微服务中就需要1个定时任务任务调度中…

    大数据 2023年6月3日
    0117
  • docker创建自己的镜像

    代码改变世界 Cnblogs Dashboard Login 2022-04-23 11:54 youxin 阅读(21 ) 评论() 编辑 todo Original: http…

    大数据 2023年5月28日
    070
  • Spring Boot 企业级开发教程—课后答案及部分解析

    Spring Boot 企业级开发教程 给有需要的朋友做参考,有错误欢迎大家提出改正 黑马程序员 第一章 填空题 Pivotal 团队在原有 spring 框架的基础上开发了全新的…

    大数据 2023年6月3日
    0100
  • 数据库建模 — ER建模

    前言 针对大数据数仓项目基础知识小记—数据库ER建模 一、数据库建模基本概念 数据建模实质为为数据构建组织和存储方法。存储方式根据不同数据库有所不同,mysql关系型数据库采取二维…

    大数据 2023年11月13日
    051
  • Docker compose 部署 nginx+php

    Docker compose 部署 nginx+php 拉取Docker镜像 docker pull nginx:1.21.6 docker pull php:7.4.28-fpm…

    大数据 2023年5月27日
    087
  • VMware网络配置以及找不到VMnet8网络问题的解决方案

    镜像下载、域名解析、时间同步请点击阿里云开源镜像站 前言 没安装vm的小伙伴们,可来这~VM安装和配置 可选择优质模板,享受阅读 VM设置 下载centso镜像 阿里云:下载 安装…

    大数据 2023年5月27日
    0101
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球