会话扩展

、简介

会话扩展提供了一种机制,用于记录对 SQLite 数据库中部分或全部rowid 表的更改,并将这些更改打包到“变更集”或“补丁集”文件中,稍后可用于将同一组更改应用于另一个具有相同架构和兼容起始数据的数据库。“变更集”也可以反转并用于“撤消”会话。

本文档是对会话扩展的介绍。该接口的详细信息在单独的 会话扩展 C 语言接口文档中。

1.1. 典型用例

假设 SQLite 用作特定设计应用程序的应用程序文件格式。两个用户 Alice 和 Bob,每个用户都从一个大约 1 GB 大小的基准设计开始。他们整天并行工作,每个人都对设计进行自己的定制和调整。在一天结束时,他们希望将他们的更改合并到一个统一的设计中。

会话扩展通过记录对 Alice 和 Bob 的数据库的所有更改并将这些更改写入变更集或补丁集文件来促进这一点。在一天结束时,Alice 可以将她的变更集发送给 Bob,Bob 可以将其“应用”到他的数据库中。结果(假设没有冲突)是 Bob 的数据库然后包含他的更改和 Alice 的更改。同样,Bob 可以将他的工作变更集发送给 Alice,她可以将他的变更应用到她的数据库中。

换句话说,会话扩展为 SQLite 数据库文件提供了一种类似于 unix 补丁实用程序的工具,或者类似于FossilGitMercurial等版本控制系统的“合并”功能

1.2. 获取会话扩展

版本 3.13.0(2016-05-18)起,会话扩展已包含在 SQLite 合并源分发中。默认情况下,会话扩展被禁用。要启用它,请使用以下编译器开关进行构建:

-DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_PREUPDATE_HOOK

或者,如果使用 autoconf 构建系统,请将 --enable-session 选项传递给配置脚本。

1.3. 限制

  • 在 SQLite 版本 3.17.0 之前,会话扩展仅适用于 rowid 表,不适用于WITHOUT ROWID表。从 3.17.0 开始,支持 rowid 和 WITHOUT ROWID 表。

  • 不支持虚拟表不会捕获对虚拟表的更改。

  • 会话扩展仅适用于具有声明的 PRIMARY KEY 的表。表的 P​​RIMARY KEY 可以是 INTEGER PRIMARY KEY(rowid 别名)或外部 PRIMARY KEY。

  • SQLite 允许将NULL 值存储在 PRIMARY KEY 列中。但是,会话扩展会忽略所有此类行。会话模块不会记录影响 PRIMARY KEY 列中具有一个或多个 NULL 值的行的更改。

2.概念

2.1. 变更集和补丁集

会话模块围绕创建和操作变更集展开。变更集是对数据库的一系列变更进行编码的数据块。变更集中的每个变更都是以下之一:

  • 一个插入INSERT 更改包含要添加到数据库表的单行。INSERT 更改的有效负载由新行的每个字段的值组成。

  • 一个删除DELETE 更改表示要从数据库表中删除的一行(由其主键值标识)。DELETE 更改的有效负载包含已删除行的所有字段的值。

  • 更新_ UPDATE 更改表示对数据库表中单个行的一个或多个非 PRIMARY KEY 字段的修改,由其 PRIMARY KEY 字段标识。更新更改的有效负载包括:

    • 标识修改行的 PRIMARY KEY 值,
    • 该行的每个修改字段的新值,以及
    • 行的每个修改字段的原始值。

    UPDATE 更改不包含有关未被更改修改的非 PRIMARY KEY 字段的任何信息。UPDATE 更改不可能指定对 PRIMARY KEY 字段的修改。

单个变更集可能包含应用于多个数据库表的变更。对于变更集至少包含一个变更的每个表,它还对以下数据进行编码:

  • 数据库表的名称,
  • 表格的列数,以及
  • 其中哪些列是 PRIMARY KEY 列。

变更集只能应用于包含与存储在变更集中的上述三个条件匹配的表的数据库。

补丁集类似于变更集。它比变更集稍微紧凑一些,但提供了更有限的冲突检测和解决选项(有关详细信息,请参阅下一节)。补丁集和变更集之间的区别在于:

  • 对于DELETE更改,有效负载仅包含 PRIMARY KEY 字段。其他字段的原始值不存储为补丁集的一部分。

  • 对于UPDATE更改,有效负载仅包含 PRIMARY KEY 字段和修改字段的新值。修改字段的原始值不存储为补丁集的一部分。

2.2. 冲突

将变更集或补丁集应用于数据库时,会尝试为每个 INSERT 更改插入一个新行,为每个 DELETE 更改删除一行并为每个 UPDATE 更改修改一行。如果目标数据库与记录变更集的原始数据库处于相同状态,这是一件简单的事情。但是,如果目标数据库的内容不完全处于此状态,则在应用变更集或补丁集时可能会发生冲突。

处理INSERT更改时,可能会发生以下冲突:

  • 目标数据库可能已经包含具有与 INSERT 更改指定的相同 PRIMARY KEY 值的行。
  • 插入新行时可能会违反某些其他数据库约束,例如 UNIQUE 或 CHECK 约束。

处理DELETE更改时,可能会检测到以下冲突:

  • 目标数据库可能不包含具有要删除的指定 PRIMARY KEY 值的行。
  • 目标数据库可能包含具有指定 PRIMARY KEY 值的行,但其他字段可能包含与存储为变更集一部分的值不匹配的值。使用补丁集时不会检测到此类冲突。

处理更新更改时,可能会检测到以下冲突:

  • 目标数据库可能不包含具有要修改的指定 PRIMARY KEY 值的行。
  • 目标数据库可能包含具有指定 PRIMARY KEY 值的行,但将被更改修改的字段的当前值可能与存储在变更集中的原始值不匹配。使用补丁集时不会检测到此类冲突。
  • 更新行时可能会违反某些其他数据库约束,例如 UNIQUE 或 CHECK 约束。

根据冲突的类型,会话应用程序有多种可配置的选项来处理冲突,包括忽略冲突的更改、中止整个更改集应用程序或尽管发生冲突仍应用更改。有关详细信息,请参阅sqlite3changeset_apply() API 的文档。

2.3. 变更集构建

配置会话对象后,它开始监视对其配置表的更改。但是,每次修改数据库中的行时,它不会记录整个更改。相反,它只记录每个插入行的 PRIMARY KEY 字段,以及任何更新或删除行的 PRIMARY KEY 和所有原始行值。如果单个会话多次修改一行,则不会记录任何新信息。

当调用 sqlite3session_changeset()sqlite3session_patchset()时,从数据库文件中读取创建变更集或补丁集所需的其他信息具体来说,

  • 对于作为 INSERT 操作结果记录的每个主键,会话模块检查表中是否仍然存在具有匹配主键的行。如果是,则向变更集中添加一个 INSERT 变更。

  • 对于作为 UPDATE 或 DELETE 操作结果记录的每个主键,会话模块还会检查表中具有匹配主键的行。如果可以找到一个,但一个或多个非 PRIMARY KEY 字段与原始记录值不匹配,则将 UPDATE 添加到变更集中。或者,如果根本没有具有指定主键的行,则会将 DELETE 添加到变更集中。如果该行确实存在但没有修改任何非 PRIMARY KEY 字段,则不会向变更集添加任何更改。

上面的一个含义是,如果在单个会话中进行了更改然后又取消了更改(例如,如果插入一行然后再次删除),会话模块根本不会报告任何更改。或者,如果一行在同一会话中更新了多次,则所有更新都将合并为任何变更集或补丁集 blob 中的单个更新。

3.使用会话扩展

本节提供了演示如何使用会话扩展的示例。

3.1. 捕获变更集

下面的示例代码演示了在执行 SQL 命令时捕获变更集所涉及的步骤。总之:

  1. 通过调用sqlite3session_create() API 函数创建会话对象(类型 sqlite3_session*)。

    单个会话对象通过单个 sqlite3* 数据库句柄监视对单个数据库(即“主”、“临时”或附加数据库)所做的更改。

  2. 会话对象配置有一组表来监视更改。

    默认情况下,会话对象不监视任何数据库表的更改。在这样做之前,必须对其进行配置。可以通过三种方式配置表集以监视更改:

    下面的示例代码使用上面列举的第二种方法 - 它监视所有数据库表的更改。

  3. 通过执行 SQL 语句对数据库进行更改。会话对象记录这些变化。

  4. 使用对sqlite3session_changeset()的调用(或者,如果使用补丁集,则调用sqlite3session_patchset()函数) 从会话对象中提取变更集 blob 。

  5. 使用对sqlite3session_delete() API 函数 的调用删除会话对象 。

    从中提取变更集或补丁集后,不必删除会话对象。它可以保留在数据库句柄上,并将像以前一样继续监视配置表上的更改。但是,如果 在会话对象上第二次调用sqlite3session_changeset()sqlite3session_patchset(),则变更集或补丁集将包含自会话创建以来连接发生的所有更改。换句话说,调用 sqlite3session_changeset() 或 sqlite3session_patchset() 不会重置或清零会话对象。

/*
** Argument zSql points to a buffer containing an SQL script to execute 
** against the database handle passed as the first argument. As well as
** executing the SQL script, this function collects a changeset recording
** all changes made to the "main" database file. Assuming no error occurs,
** output variables (*ppChangeset) and (*pnChangeset) are set to point
** to a buffer containing the changeset and the size of the changeset in
** bytes before returning SQLITE_OK. In this case it is the responsibility
** of the caller to eventually free the changeset blob by passing it to
** the sqlite3_free function.
**
** Or, if an error does occur, return an SQLite error code. The final
** value of (*pChangeset) and (*pnChangeset) are undefined in this case.
*/
int sql_exec_changeset(
  sqlite3 *db,                  /* Database handle */
  const char *zSql,             /* SQL script to execute */
  int *pnChangeset,             /* OUT: Size of changeset blob in bytes */
  void **ppChangeset            /* OUT: Pointer to changeset blob */
){
  sqlite3_session *pSession = 0;
  int rc;

  /* Create a new session object */
  rc = sqlite3session_create(db, "main", &pSession);

  /* Configure the session object to record changes to all tables */
  if( rc==SQLITE_OK ) rc = sqlite3session_attach(pSession, NULL);

  /* Execute the SQL script */
  if( rc==SQLITE_OK ) rc = sqlite3_exec(db, zSql, 0, 0, 0);

  /* Collect the changeset */
  if( rc==SQLITE_OK ){
    rc = sqlite3session_changeset(pSession, pnChangeset, ppChangeset);
  }

  /* Delete the session object */
  sqlite3session_delete(pSession);

  return rc;
}

3.2. 将变更集应用到数据库

将变更集应用到数据库比捕获变更集更简单。通常,如以下示例代码所示, 单次调用sqlite3changeset_apply()就足够了。

在复杂的情况下,应用变更集的复杂性在于解决冲突。有关详细信息,请参阅上面链接的 API 文档。

/*
** Conflict handler callback used by apply_changeset(). See below.
*/
static int xConflict(void *pCtx, int eConflict, sqlite3_changset_iter *pIter){
  int ret = (int)pCtx;
  return ret;
}

/*
** Apply the changeset contained in blob pChangeset, size nChangeset bytes,
** to the main database of the database handle passed as the first argument.
** Return SQLITE_OK if successful, or an SQLite error code if an error
** occurs.
**
** If parameter bIgnoreConflicts is true, then any conflicting changes 
** within the changeset are simply ignored. Or, if bIgnoreConflicts is
** false, then this call fails with an SQLTIE_ABORT error if a changeset
** conflict is encountered.
*/
int apply_changeset(
  sqlite3 *db,                  /* Database handle */
  int bIgnoreConflicts,         /* True to ignore conflicting changes */
  int nChangeset,               /* Size of changeset in bytes */
  void *pChangeset              /* Pointer to changeset blob */
){
  return sqlite3changeset_apply(
      db, 
      nChangeset, pChangeset, 
      0, xConflict, 
      (void*)bIgnoreConflicts
  );
}

3.3. 检查变更集的内容

下面的示例代码演示了用于迭代和提取与变更集中所有变更相关的数据的技术。总结一下:

  1. 调用sqlite3changeset_start () API 来创建和初始化迭代器以遍历变更集的内容。最初,迭代器根本不指向任何元素。

  2. 对迭代器上的sqlite3changeset_next()的第一次调用将它移动到指向变更集中的第一个更改(或者指向 EOF,如果变更集完全为空)。如果 sqlite3changeset_next() 将迭代器移动到指向有效条目,则返回 SQLITE_ROW;如果将迭代器移动到 EOF,则返回 SQLITE_DONE;如果发生错误,则返回 SQLite 错误代码。

  3. 如果迭代器指向有效条目,则sqlite3changeset_op() API 可用于确定迭代器指向的更改类型(INSERT、UPDATE 或 DELETE)。此外,相同的 API 可用于获取应用更改的表的名称及其预期的列数和主键列数。

  4. 如果迭代器指向有效的 INSERT 或 UPDATE 条目,则 sqlite3changeset_new() API 可用于获取更改有效负载中的 new.* 值。

  5. 如果迭代器指向有效的 DELETE 或 UPDATE 条目,则 sqlite3changeset_old() API 可用于获取更改有效负载中的 old.* 值。

  6. 使用对sqlite3changeset_finalize() API的调用删除迭代器 。如果在迭代时发生错误,则返回 SQLite 错误代码(即使相同的错误代码已经由 sqlite3changeset_next() 返回)。或者,如果没有发生错误,则返回 SQLITE_OK。

/*
** Print the contents of the changeset to stdout.
*/
static int print_changeset(void *pChangeset, int nChangeset){
  int rc;
  sqlite3_changeset_iter *pIter = 0;

  /* Create an iterator to iterate through the changeset */
  rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset);
  if( rc!=SQLITE_OK ) return rc;

  /* This loop runs once for each change in the changeset */
  while( SQLITE_ROW==sqlite3changeset_next(pIter) ){
    const char *zTab;           /* Table change applies to */
    int nCol;                   /* Number of columns in table zTab */
    int op;                     /* SQLITE_INSERT, UPDATE or DELETE */
    sqlite3_value *pVal;

    /* Print the type of operation and the table it is on */
    rc = sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0);
    if( rc!=SQLITE_OK ) goto exit_print_changeset;
    printf("%s on table %s\n",
      op==SQLITE_INSERT?"INSERT" : op==SQLITE_UPDATE?"UPDATE" : "DELETE",
      zTab
    );

    /* If this is an UPDATE or DELETE, print the old.* values */
    if( op==SQLITE_UPDATE || op==SQLITE_DELETE ){
      printf("Old values:");
      for(i=0; i<nCol; i++){
        rc = sqlite3changeset_old(pIter, i, &pVal);
        if( rc!=SQLITE_OK ) goto exit_print_changeset;
        printf(" %s", pVal ? sqlite3_value_text(pVal) : "-");
      }
      printf("\n");
    }

    /* If this is an UPDATE or INSERT, print the new.* values */
    if( op==SQLITE_UPDATE || op==SQLITE_INSERT ){
      printf("New values:");
      for(i=0; i<nCol; i++){
        rc = sqlite3changeset_new(pIter, i, &pVal);
        if( rc!=SQLITE_OK ) goto exit_print_changeset;
        printf(" %s", pVal ? sqlite3_value_text(pVal) : "-");
      }
      printf("\n");
    }
  }

  /* Clean up the changeset and return an error code (or SQLITE_OK) */
 exit_print_changeset:
  rc2 = sqlite3changeset_finalize(pIter);
  if( rc==SQLITE_OK ) rc = rc2;
  return rc;
}

4.扩展功能

大多数应用程序只会使用上一节中描述的会话模块功能。但是,以下附加功能可用于变更集和补丁集 blob 的使用和操作:

  • 可以使用 sqlite3changeset_concat()sqlite3_changegroup接口组合两个或多个变更集/补丁集。

  • 可以使用sqlite3changeset_invert() API 函数“反转”变更集。反向变更集会撤消原始变更集所做的更改。如果变更集 C +是变更集 C 的逆,那么将 C 再应用 C +到数据库应该保持数据库不变。