SQLite之FTS5 扩展笔记

FTS5 扩展笔记

1. FTS5扩展

Extending FTS5

本笔记是对官方文档中Extending FTS5部分的低水平翻译与理解笔记,建立在理解全文索引基本策略的基础上。渣翻掺杂个人理解与口语化处理,如有错误望指出。虽然腾讯社区有相关文档的中文翻译,但关于该部分的翻译同样略有奇怪且排版也不友好。最终决定自己翻译学习一遍,对加深理解是更有好处的。

(tokenizer,写作”分词器“)

原文地址

FTS5的特征API允许它被以下两种方式扩展:

  • 添加新的辅助函数(用C语言实现)
  • 添加新的分词器(也用C语言实现)

本文档中提出的【内置的分词器和辅助函数】全都使用下面描述的公共可用API实现。

在一个新辅助函数或者是新分词器的实现被注册到FTS5之前,应用必须先获得一个指向”fts5_api”结构体的指针。

每一个与FTS5扩展注册的数据库连接都有一个fts5_api结构体。为了获得这个指针,应用程序调用一个用户定义的只有一个参数的fts5()函数。必须用sqlite3_bind_pointer()接口来把这个参数设置为一个指向fts5_api对象的指针。下面是代码演示。

/*
** Return a pointer to the fts5_api pointer for database connection db.
** If an error occurs, return NULL and leave an error in the database 
** handle (accessible using sqlite3_errcode()/errmsg()).
*/
fts5_api *fts5_api_from_db(sqlite3 *db){
  fts5_api *pRet = 0;
  sqlite3_stmt *pStmt = 0;

  if( SQLITE_OK==sqlite3_prepare(db, "SELECT fts5(?1)", -1, &pStmt, 0) ){
    sqlite3_bind_pointer(pStmt, (void*)&pRet, "fts5_api_ptr", NULL);
   	// ph:通过这个sqlite3_bind_pointer函数来给参数pRet赋值。
    
    sqlite3_step(pStmt);
  }
  sqlite3_finalize(pStmt);
  return pRet;
}
复制代码

Fts5_api结构体被定义如下。它公开了三个方法,一种用于注册新辅助函数和新分词器,另一种用于检索已经存在的分词器。后者旨在促进类似于内置的”porter”分词器的”tokenizer wrappers”的实现。

typedef struct fts5_api fts5_api;
struct fts5_api {
  int iVersion;                   /* Currently always set to 2 */

  /* Create a new tokenizer */
  int (*xCreateTokenizer)(
    fts5_api *pApi,
    const char *zName,
    void *pContext,
    fts5_tokenizer *pTokenizer,
    void (*xDestroy)(void*)
  );

  /* Find an existing tokenizer */
  int (*xFindTokenizer)(
    fts5_api *pApi,
    const char *zName,
    void **ppContext,
    fts5_tokenizer *pTokenizer
  );

  /* Create a new auxiliary function */
  int (*xCreateFunction)(
    fts5_api *pApi,
    const char *zName,
    void *pContext,
    fts5_extension_function xFunction,
    void (*xDestroy)(void*)
  );
};
复制代码

为了调用fts5_api对象的方法,fts5_api指针本身应该被作为第一个参数传递,然后传递其他的方法特定参数。

例如:

rc = pFts5Api->xCreateTokenizer(pFts5Api, ... other args ...);
复制代码

fts5_api结构体的方法将在下面的部分被分别描述。

1.1 定制分词器

为了创建一个定制的分词器,一个应用必须实现三个方法:一个分词器构造器(xCreate)、一个析构器(xDelete)以及一个真正实现词语切分的函数(xTokenize)。每一个函数的类型与fts5_tokenizer结构体的成员变量相同:

typedef struct Fts5Tokenizer Fts5Tokenizer;
typedef struct fts5_tokenizer fts5_tokenizer;
struct fts5_tokenizer {
  int (*xCreate)(void*, const char **azArg, int nArg, Fts5Tokenizer **ppOut);
  // ph:函数指针xCreate
  void (*xDelete)(Fts5Tokenizer*);
  // ph:函数指针xDelete
  int (*xTokenize)(Fts5Tokenizer*, 
      void *pCtx,
      int flags,            /* Mask of FTS5_TOKENIZE_* flags */
      const char *pText, int nText, 
      int (*xToken)(
        void *pCtx,         /* Copy of 2nd argument to xTokenize() */
        int tflags,         /* Mask of FTS5_TOKEN_* flags */
        const char *pToken, /* Pointer to buffer containing token */
        int nToken,         /* Size of token in bytes */
        int iStart,         /* Byte offset of token within input text */
        int iEnd            /* Byte offset of end of token within input text */
      )
  );
  // ph:函数指针xTokenize
};


/* Flags that may be passed as the third argument to xTokenize() */
#define FTS5_TOKENIZE_QUERY     0x0001
#define FTS5_TOKENIZE_PREFIX    0x0002
#define FTS5_TOKENIZE_DOCUMENT  0x0004
#define FTS5_TOKENIZE_AUX       0x0008

/* Flags that may be passed by the tokenizer implementation back to FTS5
** as the third argument to the supplied xToken callback. */
#define FTS5_TOKEN_COLOCATED    0x0001      /* Same position as prev. token */
复制代码

通过调用fts5_api对象的xCreateTokenizer方法来将实现注册到FTS5模块。如果已经有一个同名的分词器的话,就替换它。如果一个non-NULL的xDestroy参数被传递给了xCreateTokenizer方法,当数据库句柄被关闭或者当表及其被替换时,它(xCreateTokenizer方法)会被调用(with一个作为唯一参数传递的pContext指针的副本)。

如果成功,xCreateTokenizer方法会返回一个SQLITE_OK。否则,它会返回一个SQLITE error code。在这种情况下xDestroy函数是不会被调用的。

当一个FTS5表使用了定制的分词器时,FTS5 core会调用xCreate()方法创建一个分词器,然后xTokenize()会被零次或多次地进行处理字符串,然后xDelete()会释放掉所有被xCreate()分配的资源。

更具体地来说:

  • xCreate

这个函数用来分配并初始化一个tokenizer实例。一个tokenizer实例是真正tokenize文本所必要的。

传给这个函数的第一个参数就是一个(void*)指针的副本,这个指针是在fts5_tokenizer对象被注册到FTS5时由应用程序提供的。

balabalabala……

  • xDelete

这个方法被调用以删除一个之前由xCreate()分配的tokenizer句柄。Fts5确保:每当xCreate()成功调用一次,这个方法都会且只会被调用一次。

  • xTokenize

(我们重点看这个)

这个方法被期待去处理一个nTextbyte大小的字符串,这个字符串由参数pText传入(见上文)。字符串pText可以是也可以不是以nul结尾。(nul-terminated的翻译应该是这样吧。)

传给该函数的第一个参数是一个指向Fts5Tokenizer对象的指针,这个对象是前面调用xCreate()方法返回的。

第二个参数指示了FTS5请求所提供文本的词语切分的原因。这总是下面四个值之一。

FTS5_TOKENIZE_DOCUMENT

一个文档正在被插入FTS表(或者正被从FTS表移除)。tokenizer正在被调用以确定要从FTS index中添加或者删除的tokens集合

FTS5_TOKENIZE_QUERY

一个MATCH查询正在针对FTS index执行。tokenizer正在被调用以tokenize一个作为查询一部分的指定的裸字或带引号的字符串。

(FTS5_TOKENIZE_QUERY | FTS5_TOKENIZE_PREFIX)

与FTS5_TOKENIZE_QUERY相同,不同之处在于裸字或者带引号的字符串后面跟着一个*字符,表明tokenizer返回的最后一个token将会被视作一个token前缀。

FTS5_TOKENIZE_AUX

tokenizer正在被调用以满足辅助函数所做的fts5_api.xTokenize()请求。或者在columnsize=0的数据库上做出的一个fts5_api.xColumnSize()请求。

对输入字符串的每一个token,所提供的回调函数xToken()都必须被调用。它第一个参数应该是一个指针的副本,作为传给xTokenize()方法的第二个参数。(这里是说pCtx)

第三和第四个参数是一个指向包含了token文本的buffer的指针,以及token的字节数大小。(是指pToken和nToken)

最后两个参数(iStart和iEnd)是字节偏移量,输入的文本里的偏移量以及它结束位置的偏移量。

传给xToken()函数的第二个参数(tflags)通常应该被设置为0。例外是当tokenizer支持近义词的情况。在这种情况下可以看下面的详细讨论。

FTS5认为xToken()回调函数为每一个token调用,按照它们在输入文本中出现的顺序。

如果一个xToken()回调函数返回了任何不是SQLITE_OK的值,那么tokenization应该被抛弃,并且xTokenize()方法应该like返回一个xToken()返回值的副本。或者,如果输入的buffer耗尽的话,xTokenize()应该返回SQLITE_OK。最后,如果一个错误在xTokenize()自身实现中发生,它可能抛弃tokenization并且返回任何【不是SQLITE_OK或SQLITE_DONE】的error code。

1.1.1 同义词支持

定制分词器也能支持同义词。考虑这样一个情况:用户希望搜索一个词组比如”first place”,使用内置的分词器,FTS5搜索”first + place”会去匹配文档集里的”first place”实例,但不会匹配它的替代形式”1st place”。在一些应用中,把这些形式都匹配了是更好的,不论用户指定了什么样的查询文本。

在FTS5中,有几种方式来实现它:

  1. 通过把所有的同义词映射到一个单独的token。在这种情况下,用上面的例子来说,意味着分词器对于”first”和”1st”这两种输入返回相同的token。就是说token事实上是”first”,以至于当用户插入一条文本”I won 1st place”时,词目会这样被添加到index中:”i”,”won”,”first”,”place”。如果用户查询了”1st + place”,分词器会用”first”去替代”1st”,查询就会按照预期进行。

  2. 通过分别查询每个term的所有同义词的索引。在这种情况下,当在对查询文本进行分词时,分词器可能为文档中的一个term提供多个同义词。FTS5然后会对每一个同义词单独查询index。例如,面对这样的查询:

    • ... MATCH 'first place'

    分词器就会提供”1st”和”first”作为同义词,给在MATCH查询中的”first” token(ph:个人感觉这里和下面的first应该是”first“)。并且FTS5实际上执行了一个类似这样的查询:

    • ... MATCH '(first OR 1st) place'

    除了那种情况以外,出于辅助函数的目的,查询仍然会包含只有两个词组-“first OR 1st”被当做一个单独的词组。

  3. 通过为一个term增加多个同义词到FTS index中。用这种方法,当在对文档文本进行分词时,分词器为每一个token提供了多个同义词。因此在一个例如”I won first place”的文档被分词时,词目被这样添加到FTS index中”i”,”won”,”first”,”1st”,”place”。(比第一种方法多了”1st”)。这种方法,即使在对查询文本分词的时候分词器不提供同义词(它不应该。因为这么做低效),如果用户查询了”first + place”或者”1st + place”也不要紧,因为在FTS index中有词目与”first” token的所有形式都有对应。

(ph简单理解:首先有两个分词过程:对文档的分词和对查询文本(用户输入)的分词,对文档分词后会加进FTS index,对查询文本分词后会执行查询。方法1就是在文档分词时就做了同义词映射,直接把所有同义词映射到一个term,查的时候也会用这个映射来替换查询文本;方法2就是在查询时拿所有的同义词去FTS index里查询;方法3是直接把同义词添加到FTS index中。)

不论它是解析文档或者是查询文本,任何【指定了一个”tflags”的参数FTS5_TOKEN_COLOCATED位的】对xToken()的调用都被认为是给前面的token提供一个同义词。

例如:当解析文档”I won first place”时,一个支持同义词的分词器会调用xToken()函数5次,像这样:

xToken(pCtx, 0, "i", 1, 0, 1);
xToken(pCtx, 0, "won", 3, 2, 5);
xToken(pCtx, 0, "first", 5, 6, 11);
xToken(pCtx, FTS5_TOKEN_COLOCATED, "1st", 3, 6, 11);
xToken(pCtx, 0, "place", 5, 12, 17);
复制代码

(从这里也有助于我们分析xToken()的参数。第四个参数是长度,后面俩是始末位置。)

xToken()被第一次调用的时候指定FTS5_TOKEN_COLOCATED这个flag是一个错误。多种同义词可能会被指定给一个token,通过多次依次调用xToken(FTS5_TOKEN_COLOCATED)。为一个token提供的同义词的数量是没有限制的。

在很多情况下,上面提到的的方法1是最好的实现方式。它不需要向FTS index中添加额外的数据或者需要FTS5区查询多个terms,因此在磁盘空间和查询速度方面它是高效的。然而,它对前缀查询支持的就不好了。如果,按照上面所建议的,分词器用”first”这个token去替换了”1st”,然后查询:

... MATCH '1s*'

这将不会匹配包含”1st”这个token的文档。(因为分词器可能不会将”1st”映射到任何”first”的前缀)

对全前缀的支持,方法3可能会被优先考虑。在这种情况下,因为index同时包含”first”和”1st”这两个词目,像fi*1s*这样的前缀查询就能被正确匹配。然而,因为往FTS index中添加了额外的词目,这个方法会用到数据库的更多空间。

方法2提供了方法1和方法3的折中。用这种方式,一个像1s*这样的查询会匹配到包含了token1st的文档,而不是first(假定分词器不能提供前缀同义词)。然而,一个像”1st”这样的非前缀查询将会匹配”1st”和”first”。这个方法不需要额外的磁盘空间,没有额外的词目会被添加到FTS index中。另一方面,它可能需要更多的cpu运转来运行MATCH查询,因为对每一个同义词来说,对FTS index的单独查询都是必要的。

当使用方法2或方法3时,很重要的一点是,分词器只在对文档文本分词(方法2的的操作)或者对查询文本分词(方法3的操作)的时候提供同义词,而不是它们全部。

这么做不会导致任何错误,但是低效。

1.2 定制辅助函数

实现一个定制的辅助函数与实现一个标量的SQL函数类似。实现应该是一个fts5_extension_function类型的C函数。定义如下:

typedef struct Fts5ExtensionApi Fts5ExtensionApi;
typedef struct Fts5Context Fts5Context;
typedef struct Fts5PhraseIter Fts5PhraseIter;

typedef void (*fts5_extension_function)(
  const Fts5ExtensionApi *pApi,   /* API offered by current FTS version */
  Fts5Context *pFts,              /* First arg to pass to pApi functions */
  sqlite3_context *pCtx,          /* Context for returning result/error */
  int nVal,                       /* Number of values in apVal[] array */
  sqlite3_value **apVal           /* Array of trailing arguments */
);
复制代码

实现是通过调用fts5_api对象的xCreateFunction()方法来注册到FTS5模块的。如果已经有一个同名的辅助函数,它会被新的函数替换。如果一个non-NULL的xDestroy参数被传给xCreateFunction(),它会被调用(with一个pContext指针的副本作为唯一的参数)当数据库句柄关闭时或者当注册辅助函数被替换时。

如果成功,xCreateFunction()函数返回SQLITE_OK。否则,它会返回一个SQLITE error code。在这种情况下xDestroy()就不会被调用了。

最后三个传给辅助函数回调的参数与实现标量SQL函数时传的三个参数类似。除了第一个被传给辅助函数以外的所有参数,对在apVal数组中的实现都是可用的。这个实现应该通过内容句柄pCtx返回一个结果或者错误。

第一个被传给辅助函数回调的参数是一个指向接结构体的指针,该结构体包含【可以获取当前查询或行的信息的】函数。第二个参数是一个不透明的句柄,应该作为第一个参数传给任何这样的方法调用。例如,下面的辅助函数定义就返回了在当前行的所有列的tokens的总数。

/*
** Implementation of an auxiliary function that returns the number
** of tokens in the current row (including all columns).
*/
static void column_size_imp(
  const Fts5ExtensionApi *pApi,
  Fts5Context *pFts,
  sqlite3_context *pCtx,
  int nVal,
  sqlite3_value **apVal
){
  int rc;
  int nToken;
  rc = pApi->xColumnSize(pFts, -1, &nToken);
  if( rc==SQLITE_OK ){
    sqlite3_result_int(pCtx, nToken);
  }else{
    sqlite3_result_error_code(pCtx, rc);
  }
}
复制代码

下面的部分描述了提供给辅助函数实现细节的API。更多的例子可以在源文件”fts5_aux.c”中被找到。

1.2.1 定制辅助函数API参照

struct Fts5ExtensionApi {
  int iVersion;                   /* Currently always set to 3 */

  void *(*xUserData)(Fts5Context*);

  int (*xColumnCount)(Fts5Context*);
  int (*xRowCount)(Fts5Context*, sqlite3_int64 *pnRow);
  int (*xColumnTotalSize)(Fts5Context*, int iCol, sqlite3_int64 *pnToken);

  int (*xTokenize)(Fts5Context*, 
    const char *pText, int nText, /* Text to tokenize */
    void *pCtx,                   /* Context passed to xToken() */
    int (*xToken)(void*, int, const char*, int, int, int)       /* Callback */
  );

  int (*xPhraseCount)(Fts5Context*);
  int (*xPhraseSize)(Fts5Context*, int iPhrase);

  int (*xInstCount)(Fts5Context*, int *pnInst);
  int (*xInst)(Fts5Context*, int iIdx, int *piPhrase, int *piCol, int *piOff);

  sqlite3_int64 (*xRowid)(Fts5Context*);
  int (*xColumnText)(Fts5Context*, int iCol, const char **pz, int *pn);
  int (*xColumnSize)(Fts5Context*, int iCol, int *pnToken);

  int (*xQueryPhrase)(Fts5Context*, int iPhrase, void *pUserData,
    int(*)(const Fts5ExtensionApi*,Fts5Context*,void*)
  );
  int (*xSetAuxdata)(Fts5Context*, void *pAux, void(*xDelete)(void*));
  void *(*xGetAuxdata)(Fts5Context*, int bClear);

  int (*xPhraseFirst)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*, int*);
  void (*xPhraseNext)(Fts5Context*, Fts5PhraseIter*, int *piCol, int *piOff);

  int (*xPhraseFirstColumn)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*);
  void (*xPhraseNextColumn)(Fts5Context*, Fts5PhraseIter*, int *piCol);
};
复制代码

这里提到了一大堆函数,我们这里不去翻译每个详细的函数,只对一些重要的做记录。

  • int (*xInst)(Fts5Context*, int iIdx, int *piPhrase, int *piCol, int *piOff);

查询当前行的短语匹配iIdx的详细信息。短语匹配从零开始编号,因此iIdx参数应该大于或等于零,并且小于xInstCount()输出的值。

通常,输出参数*piPhrase被设置为短语编号,*piCol被设置为它所在的列,*piOff被设置为短语的第一个标记的标记偏移量。如果成功则返回SQLITE_OK,如果发生错误则返回一个错误代码(即SQLITE_NOMEM)。

如果使用“detail=none”或“detail=column”选项创建的FTS5表,这个API可能会非常慢。

  • int (*xPhraseSize)(Fts5Context*, int iPhrase);

返回查询的短语iPhrase中tokens的数量。短语从零开始编号。

  • int (*xInstCount)(Fts5Context*, int *pnInst);

设置*pnInst为当前行查询中所有短语出现的总数。如果成功则返回SQLITE_OK,如果发生错误则返回一个错误代码(即SQLITE_NOMEM)。

如果使用“detail=none”或“detail=column”选项创建的FTS5表,这个API可能会非常慢。如果FTS5表是用”detail=none”或”detail=column”和”content=”选项创建的(例如,如果它是一个没有内容的表),那么这个API总是返回0。

  • int (*xColumnText)(Fts5Context*, int iCol, const char **pz, int *pn);

这个函数试图检索当前文档的列iCol的文本。如果成功,将(*pz)设置为指向一个包含utf-8编码文本的缓冲区,将(*pn)设置为缓冲区的字节(而不是字符)大小,并返回SQLITE_OK。否则,如果发生错误,则返回SQLite错误代码,并且(*pz)和(*pn)的最终值未定义。

  • int (*xTokenize)(Fts5Context*, const char *pText, int nText, /* Text to tokenize */ void *pCtx, /* Context passed to xToken() */ int (*xToken)(void*, int, const char*, int, int, int) /* Callback */ );

使用属于FTS5表的标记器标记文本。

为了编写可加载扩展,还必须查看官网文档部分的Run-Time Loadable Extensions

(里面就提到了入口函数名字的事情)

运行时可加载扩展

1. 概述

sqlite有在运行时加载扩展的能力(包括应用定义的sql函数等。)这个特征允许扩展的代码与应用分离,扩展可以被单独地开发和测试,然后在需要的时候加载。

扩展也可以被静态地连接到应用。下面展示的代码模板将会和静态链接扩展一样地工作。你应该给你的入口函数sqlite3_extension_init一个不同的名字以避免名字冲突(如果你的应用包含两个或更多的扩展的话,因为默认的扩展入口函数名字是这个)。

2. 加载一个扩展

一个sqlite扩展是一个动态库或者dll。为了加载它,你需要向Sqlite提供包含共享库或者dll的文件的名称,以及初始化扩展的入口点。在C代码中,这个信息是使用sqlite3_load_extension()这个API提供的。想了解该函数的额外信息可以看它的文档。

注意,不同的操作系统对共享库有不同的文件名后缀。如果希望代码可移植的话,我们可以省略共享库文件名中的后缀,适当的后缀会由sqlite3_load_extension()接口自动添加。

还有一个sql函数可以用来加载扩展:load_extension(X, Y)。它就像C接口的sqlite3_load_extension()一样工作。

两个加载扩展的函数都允许你来制定一个扩展入口点的名字。你能将这个参数留空,即传一个NULL指针给C接口的sqlite3_load_extension()函数,或者忽视掉sql接口的load_extension(X,Y)的第二个参数。扩展加载器会尝试来解决它自己的入口点。它会首先尝试普遍的扩展名sqlite3_extension_init方法。如果那不行,它会构建一个入口点,名字模板就是”sqlite3_x_init()”,这里的x就会被替代。

它会忽略路径中最后一个/及前面部分;第一个.及后面的部分也会被忽略。都会转小写。

例如如果文件名是”/usr/lib/libmathfunc-4.8.so”,那么入口名字将会是”sqlite3_mathfunc_init”。

(ph:-4.8为啥被忽略了?)

为了安全原因,扩展加载默认是关闭的。为了使用C语言接口或者是SQL扩展家在函数,必须首先开启扩展加载——在应用里使用sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, NULL)这个C的API。

从命令行shell中,扩展能够这样加载:

.load ./YourCode
复制代码

注意命令行shell程序已经开启扩展加载。(通过调用sqlite3_enable_load_extension()接口作为它设置的一部分),因此上面的命令可以工作而不需要任何特别的开关、设置或者其他复杂的事情。

.load这个命令有一个参数,调用sqlite3_load_extension(),其中zProc参数被设置为NULL,导致SQLite首先寻找名字为sqlite3_extension_init的入口点,然后sqlite3_X_init(其中X名字就是从文件名来的)。如果你的扩展有一个不同名字的入口,简单地提供那个名字作为第二个参数即可。例如:

.load ./YourCode nonstandard_entry_point
复制代码

3. 编译一个可加载扩展

可加载扩展是C代码。为了编译它们在类UNIX操作系统上,通常的命令是这样的:

gcc -g -fPIC -shared YourCode.c -o YourCode.so
复制代码

(编译动态共享库)

Macs也是类unix的,但是它们没有跟随通常的共享库管理。为了在Mac上编译一个共享库,用这样的命令:

gcc -g -fPIC -dynamiclib YourCode.c -o YourCode.dylib
复制代码

window的指令这里就不列了。文档里写有。

4. 可扩展编程★

一个模板可加载扩展包含下面三个部分。

  1. 在源代码文件头部使用#include <sqlite3ext.h>而不是#include <sqlite3.h>

  2. 将宏指令SQLITE_EXTENSION_INIT1单独放在一行,在上面那行头文件引入的后面。

  3. 添加一个扩展加载入口点的例程,大概就像下面这样:

    #ifdef _WIN32
    __declspec(dllexport)
    #endif
    int sqlite3_extension_init( /* <== Change this name, maybe */
      sqlite3 *db, 
      char **pzErrMsg, 
      const sqlite3_api_routines *pApi
    ){
      int rc = SQLITE_OK;
      SQLITE_EXTENSION_INIT2(pApi);
      /* insert code to initialize your extension here */
      return rc;
    }
    复制代码

    你要定制你入口点的名字,它与你将要生成的共享库的名字相对应,而不是使用”sqlite3_extension_init”这个通用的名字。给你的扩展弄一个定制的入口点名字能允许你静态地连接两个或更多的扩展到一个同名的程序中而不会导致一个连接冲突,如果你之后决定使用静态链接而不是运行时连接的话。如果你的共享库最终被命名为”YourCode.so”或者”YourCode.dll”或者”YourCode.dylib”就像上面编译器例子中所展示的那样,那么正确的入口名字应是”sqlite3_yourcode.init”。

这里是一个完整的模板扩展,你可以copy/paste来尝试。

/* Add your header comment here */
#include <sqlite3ext.h> /* Do not use <sqlite3.h>! */
SQLITE_EXTENSION_INIT1

/* Insert your extension code here */

#ifdef _WIN32
__declspec(dllexport)
#endif
/* TODO: Change the entry point name so that "extension" is replaced by
** text derived from the shared library filename as follows:  Copy every
** ASCII alphabetic character from the filename after the last "/" through
** the next following ".", converting each character to lowercase, and
** discarding the first three characters if they are "lib".
*/
int sqlite3_extension_init(
  sqlite3 *db, 
  char **pzErrMsg, 
  const sqlite3_api_routines *pApi
){
  int rc = SQLITE_OK;
  SQLITE_EXTENSION_INIT2(pApi);
  /* Insert here calls to
  **     sqlite3_create_function_v2(),
  **     sqlite3_create_collation_v2(),
  **     sqlite3_create_module_v2(), and/or
  **     sqlite3_vfs_register()
  ** to register the new features that your extension adds.
  */
  return rc;
}
复制代码

4.1 一些扩展的例子

可以在官网找到,好像还有FTS5的扩展实现。

5. 持久可加载扩展

一个可加载扩展的默认行为是,当最初调用了”sqlite3_load_extension()”的那个数据库连接断掉时,这个扩展就会被从内存中卸载。

然而,如果初始化进程返回一个SQLITE_OK_LOAD_PERMANENTLY而不是SQLITE_OK的话,那么扩展就不会被卸载,同时扩展将会无限期地保存在内存中。

需要澄清的是:一个反回了SQLITE_OK_LOAD_PERMANENTLY的初始化函数会在数据库连接close后继续存在于内存中。然而,扩展并不会自动地注册到后续的数据库连接中。这使得我们实现新的扩展进行加载成为可能。为了永久地加载并注册一个实现了新SQL函数、排序序列或是虚拟表的扩展,一边这些添加的功能对所有的后续数据库连接可用,那么初始化例程应该也在将注册这些服务的子函数上调用sqlite3_auto_extension()

详情可以看官方文档提供的一个示例文件:vfsstat.c

自定义SQL函数(简述)

使用SQLite的应用程序可以定义自定义SQL函数,这些函数回调应用程序代码来计算结果。定制SQL函数实现可以嵌入到应用程序代码本身中,也可以是可加载的扩展。

sqlite3_create_function()系列接口用于创建新的自定义SQL函数。

直接来看这个函数:

int sqlite3_create_function(
  sqlite3 *db,
  const char *zFunctionName,
  int nArg,
  int eTextRep,
  void *pApp,
  void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
  void (*xStep)(sqlite3_context*,int,sqlite3_value**),
  void (*xFinal)(sqlite3_context*)
);
复制代码

第一个参数就是sqlite3的句柄db

第二个参数是字符串,我们要注册的函数的名字,比如”phslicer”

第三个参数nArg是该函数接受参数的个数。取值是-1SQLITE_MAX_FUNCTION_ARG之间的整数(默认127)。取-1意味着该SQL函数是可变参数的函数,可以接受0SQLITE_MAX_FUNCTION_ARG任意数量的参数。

第四个参数eTextRep传达关于新函数的各种属性,涉及文本编码等问题。

第五个参数pApp是一个传入回调例程的任意指针。SQLite本身不对它做任何操作,只是让回调函数可以使用该指针,并且在函数未注册时将其传递给析构函数。

**xFunc就是我们传入的参数实现。**它包含三个参数。

第一个context,指向不透明对象的指针,该对象描述了调用SQL函数的内容。这个context点成为很多【函数实现可能会调用的】例程的第一个参数。

第二个第三个参数argc和argv是SQL函数本身的参数数量和每个参数的值(如同main函数的argc和argv)。参数值可以是任何类型,因此存储在sqlite3_value对象的实例中。可以使用sqlite3_value()系列接口从该对象中提取特定的c语言值。

static void mysqlfunc(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  assert( argc==1 );
  sqlite3_result_value(context, argv[0]);
}
复制代码

demo

int demoTokenize(Fts5Tokenizer*, 
    void *pCtx,
    int flags,
    const char *pText,
    int nText,
    int (*xToken)(void *,
    						int,
    						const char *,
    						int,
    						int,
    						int)) {
    // 对于英文字母文档的分词功能demo示例
    int rc = SQLITE_OK;
    int start = 0;
    int index;
    std::string res;
    for (index = 0; index < nText; index++) {
    	if (isspace(pText[index])) {
    			// 遇到空白字符,截断
    			res.clear();
    			// 将分出的词条赋值给res
    			std::copy(pText + start, pText + index, std::back_inserter(res));
    			// 英文字母转小写
    			std::transform(res.begin(), res.end(), res.begin(), [](unsigned char c){ return std::tolower(c); });
    			// 标记分词
    			rc = xToken(pCtx, 0, res.c_str(), res.length(), start, index);
    			// 更新start
          start = index + 1;
       }
    }
    return SQLITE_OK;
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享