1. Introducción 1.1. Caso de uso típico 1.2. Obtención de la extensión de sesión 1.3. Limitaciones 2. Conceptos 2.1. Conjuntos de cambios y conjuntos de parches2.2. Conflictos 2.3. Construcción del conjunto de cambios 3. Uso de la extensión de sesión 3.1. Capturar un conjunto de cambios3.2. Aplicar un conjunto de cambios a una base de datos 3.3. Inspección del contenido de un conjunto de cambios 4. Funcionalidad extendida

La extensión de sesión proporciona un mecanismo para registrar cambios en algunas o todas las tablas rowid en una base de datos SQLite, y empaquetar esos cambios en un archivo de “conjunto de cambios” o “conjunto de parches” que luego se puede usar para aplicar el mismo conjunto de cambios a otro base de datos con el mismo esquema y datos de inicio compatibles. Un “conjunto de cambios” también se puede invertir y utilizar para “deshacer” una sesión.

Este documento es una introducción a la extensión de la sesión. Los detalles de la interfaz están en el Interfaz de lenguaje C de extensión de sesión documento.

1.1. Caso de uso típico

Suponga que SQLite se utiliza como formato de archivo de aplicación para una aplicación de diseño en particular. Dos usuarios, Alice y Bob, comienzan cada uno con un diseño básico de aproximadamente un gigabyte de tamaño. Trabajan todo el día, en paralelo, cada uno haciendo sus propias personalizaciones y ajustes al diseño. Al final del día, les gustaría fusionar sus cambios en un solo diseño unificado.

La extensión de sesión facilita esto al registrar todos los cambios en las bases de datos de Alice y Bob y escribir esos cambios en archivos de conjuntos de cambios o conjuntos de parches. Al final del día, Alice puede enviar su conjunto de cambios a Bob y Bob puede “aplicarlo” a su base de datos. El resultado (asumiendo que no hay conflictos) es que la base de datos de Bob contiene tanto sus cambios como los de Alice. Del mismo modo, Bob puede enviar un conjunto de cambios de su trabajo a Alice y ella puede aplicar sus cambios a su base de datos.

En otras palabras, la extensión de sesión proporciona una función para archivos de base de datos SQLite que es similar a la de unix. parche programa de utilidad, o las capacidades de “fusión” de los sistemas de control de versiones como Fósil, Git, o Mercurial.

1.2. Obtener la extensión de la sesión

Ya que versión 3.13.0 (2016-05-18), la extensión de la sesión se ha incluido en la distribución de la fuente de fusión de SQLite. De forma predeterminada, la extensión de la sesión está desactivada. Para habilitarlo, compile con los siguientes modificadores del compilador:

-DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_PREUPDATE_HOOK

O, si usa el sistema de compilación autoconf, pase la opción –enable-session al script de configuración.

1.3. Limitaciones

  • Antes de la versión 3.17.0 de SQLite, la extensión de sesión solo funcionaba con tablas rowid, no SIN tablas ROWID. A partir de 3.17.0, se admiten las tablas rowid y WITHOUT ROWID.

  • No hay soporte para tablas virtuales. Los cambios en las tablas virtuales no se capturan.

  • La extensión de sesión solo funciona con tablas que tienen una CLAVE PRIMARIA declarada. La CLAVE PRIMARIA de una tabla puede ser una CLAVE PRIMARIA INTEGER (alias rowid) o una CLAVE PRIMARIA externa.

  • SQLite permite almacenar valores NULL en columnas PRIMARY KEY. Sin embargo, la extensión de la sesión ignora todas esas filas. El módulo de sesiones no registra ningún cambio que afecte a las filas con uno o más valores NULL en las columnas PRIMARY KEY.

2.1. Conjuntos de cambios y conjuntos de parches

El módulo de sesiones gira en torno a la creación y manipulación de conjuntos de cambios. Un conjunto de cambios es una masa de datos que codifica una serie de cambios en una base de datos. Cada cambio en un conjunto de cambios es uno de los siguientes:

  • Un INSERTAR. Un cambio INSERT contiene una sola fila para agregar a una tabla de base de datos. La carga útil del cambio INSERT consta de los valores para cada campo de la nueva fila.

  • A ELIMINAR. Un cambio DELETE representa una fila, identificada por sus valores de clave principal, para eliminar de una tabla de base de datos. La carga útil de un cambio DELETE consta de los valores de todos los campos de la fila eliminada.

  • Un ACTUALIZAR. Un cambio de ACTUALIZACIÓN representa la modificación de uno o más campos de CLAVE PRIMARIA de una sola fila dentro de una tabla de base de datos, identificados por sus campos CLAVE PRIMARIA. La carga útil para un cambio de ACTUALIZACIÓN consiste en:

    • Los valores de PRIMARY KEY que identifican la fila modificada,
    • Los nuevos valores para cada campo modificado de la fila, y
    • Los valores originales de cada campo modificado de la fila.

    Un cambio de ACTUALIZACIÓN no contiene ninguna información sobre los campos de CLAVE PRINCIPAL que no son modificados por el cambio. No es posible que un cambio de ACTUALIZACIÓN especifique modificaciones en los campos CLAVE PRIMARIA.

Un solo conjunto de cambios puede contener cambios que se apliquen a más de una tabla de base de datos. Para cada tabla para la que el conjunto de cambios incluye al menos un cambio, también codifica los siguientes datos:

  • El nombre de la tabla de la base de datos,
  • El número de columnas que tiene la tabla y
  • ¿Cuáles de esas columnas son columnas de CLAVE PRINCIPAL?

Los conjuntos de cambios solo se pueden aplicar a las bases de datos que contienen tablas que coinciden con los tres criterios anteriores tal como se almacenan en el conjunto de cambios.

Un conjunto de parches es similar a un conjunto de cambios. Es un poco más compacto que un conjunto de cambios, pero proporciona opciones de resolución y detección de conflictos más limitadas (consulte la siguiente sección para obtener más detalles). Las diferencias entre un conjunto de parches y un conjunto de cambios son las siguientes:

  • Para ELIMINAR cambio, la carga útil consta únicamente de los campos CLAVE PRIMARIA. Los valores originales de otros campos no se almacenan como parte de un conjunto de parches.

  • Por un ACTUALIZAR cambio, la carga útil consta de los campos CLAVE PRIMARIA y los nuevos valores de los campos modificados únicamente. Los valores originales de los campos modificados no se almacenan como parte de un conjunto de parches.

2.2. Conflictos

Cuando se aplica un conjunto de cambios o un conjunto de parches a una base de datos, se intenta insertar una nueva fila para cada cambio INSERT, eliminar una fila para cada cambio DELETE y modificar una fila para cada cambio UPDATE. Si la base de datos de destino está en el mismo estado que la base de datos original en la que se registró el conjunto de cambios, esto es una cuestión sencilla. Sin embargo, si el contenido de la base de datos de destino no se encuentra exactamente en este estado, pueden producirse conflictos al aplicar el conjunto de cambios o el conjunto de parches.

Al procesar un INSERTAR cambio, pueden producirse los siguientes conflictos:

  • Es posible que la base de datos de destino ya contenga una fila con los mismos valores de PRIMARY KEY especificados por el cambio INSERT.
  • Algunas otras restricciones de la base de datos, por ejemplo, una restricción UNIQUE o CHECK, pueden violarse cuando se inserta la nueva fila.

Al procesar un ELIMINAR cambiar, se pueden detectar los siguientes conflictos:

  • Es posible que la base de datos de destino no contenga ninguna fila con los valores de PRIMARY KEY especificados para eliminar.
  • La base de datos de destino puede contener una fila con los valores de PRIMARY KEY especificados, pero los otros campos pueden contener valores que no coinciden con los almacenados como parte del conjunto de cambios. Este tipo de conflicto no se detecta cuando se utiliza un conjunto de parches.

Al procesar un ACTUALIZAR cambiar, se pueden detectar los siguientes conflictos:

  • La base de datos de destino no puede contener ninguna fila con los valores de CLAVE PRIMARIA especificados para modificar.
  • La base de datos de destino puede contener una fila con los valores de CLAVE PRIMARIA especificados, pero los valores actuales de los campos que serán modificados por el cambio pueden no coincidir con los valores originales almacenados dentro del conjunto de cambios. Este tipo de conflicto no se detecta cuando se utiliza un conjunto de parches.
  • Es posible que se infrinja alguna otra restricción de la base de datos, por ejemplo, una restricción UNIQUE o CHECK, cuando se actualiza la fila.

Dependiendo del tipo de conflicto, una aplicación de sesiones tiene una variedad de opciones configurables para lidiar con los conflictos, que van desde omitir el cambio conflictivo, abortar toda la aplicación del conjunto de cambios o aplicar el cambio a pesar del conflicto. Para obtener más información, consulte la documentación de la API sqlite3changeset_apply ().

2.3. Construcción del conjunto de cambios

Una vez que se ha configurado un objeto de sesión, comienza a monitorear los cambios en sus tablas configuradas. Sin embargo, no registra un cambio completo cada vez que se modifica una fila dentro de la base de datos. En su lugar, registra solo los campos CLAVE PRIMARIA para cada fila insertada, y solo la CLAVE PRIMARIA y todos los valores de fila originales para cualquier fila actualizada o eliminada. Si una fila se modifica más de una vez por una sola sesión, no se registra nueva información.

La otra información necesaria para crear un conjunto de cambios o un conjunto de parches se lee del archivo de base de datos cuando se llama a sqlite3session_changeset () o sqlite3session_patchset (). Específicamente,

  • Para cada clave primaria registrada como resultado de una operación INSERT, el módulo de sesiones verifica si hay una fila con una clave primaria coincidente todavía en la tabla. Si es así, se agrega un cambio INSERT al conjunto de cambios.

  • Para cada clave primaria registrada como resultado de una operación ACTUALIZAR o ELIMINAR, el módulo de sesiones también busca una fila con una clave primaria coincidente dentro de la tabla. Si se puede encontrar uno, pero uno o más de los campos de CLAVE PRINCIPAL no coinciden con el valor registrado original, se agrega una ACTUALIZACIÓN al conjunto de cambios. O, si no hay ninguna fila con la clave primaria especificada, se agrega un DELETE al conjunto de cambios. Si la fila existe pero ninguno de los campos que no son de CLAVE PRINCIPAL se ha modificado, no se agrega ningún cambio al conjunto de cambios.

Una implicación de lo anterior es que si se realiza un cambio y luego se deshace dentro de una sola sesión (por ejemplo, si se inserta una fila y luego se elimina nuevamente), el módulo de sesiones no informa ningún cambio en absoluto. O si una fila se actualiza varias veces dentro de la misma sesión, todas las actualizaciones se fusionan en una única actualización dentro de cualquier conjunto de cambios o blob de conjuntos de parches.

Esta sección proporciona ejemplos que demuestran cómo utilizar la extensión de sesiones.

3.1. Capturar un conjunto de cambios

El siguiente código de ejemplo demuestra los pasos necesarios para capturar un conjunto de cambios mientras se ejecutan comandos SQL. En resumen:

  1. Un objeto de sesión (tipo sqlite3_session *) se crea haciendo una llamada a la función de API sqlite3session_create ().

    Un único objeto de sesión monitorea los cambios realizados en una sola base de datos (es decir, “principal”, “temporal” o una base de datos adjunta) a través de un solo identificador de base de datos sqlite3 *.

  2. El objeto de sesión está configurado con un conjunto de tablas para monitorear los cambios.

    De forma predeterminada, un objeto de sesión no supervisa los cambios en ninguna tabla de la base de datos. Antes de hacerlo, debe configurarse. Hay tres formas de configurar el conjunto de tablas para monitorear cambios en:

    • Especificando explícitamente tablas usando una llamada a sqlite3session_attach () para cada tabla, o
    • Especificando que todas las tablas en la base de datos deben ser monitoreadas por cambios usando una llamada a sqlite3session_attach () con un argumento NULL, o
    • Al configurar una devolución de llamada para que se invoque la primera vez que se escribe cada tabla, eso indica al módulo de sesión si los cambios en la tabla deben ser monitoreados o no.

    El código de ejemplo a continuación utiliza el segundo de los métodos enumerados anteriormente: monitorea los cambios en todas las tablas de la base de datos.

  3. Los cambios se realizan en la base de datos mediante la ejecución de sentencias SQL. El objeto de sesión registra estos cambios.

  4. Un blob de conjunto de cambios se extrae del objeto de sesión mediante una llamada a sqlite3session_changeset () (o, si utiliza conjuntos de parches, una llamada a la función sqlite3session_patchset ()).

  5. El objeto de sesión se elimina mediante una llamada a la función de API sqlite3session_delete ().

    No es necesario eliminar un objeto de sesión después de extraer un conjunto de cambios o un conjunto de parches de él. Se puede dejar adjunto al identificador de la base de datos y continuará monitoreando los cambios en las tablas configuradas como antes. Sin embargo, si sqlite3session_changeset () o sqlite3session_patchset () se llama por segunda vez en un objeto de sesión, el conjunto de cambios o parche contendrá todos cambios que han tenido lugar en la conexión desde que se creó la sesión. En otras palabras, un objeto de sesión no se restablece ni se pone a cero mediante una llamada a sqlite3session_changeset () o 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. Aplicar un conjunto de cambios a una base de datos

Aplicar un conjunto de cambios a una base de datos es más sencillo que capturar un conjunto de cambios. Por lo general, una sola llamada a sqlite3changeset_apply (), como se muestra en el código de ejemplo a continuación, es suficiente.

En los casos en que es complicado, las complicaciones de aplicar un conjunto de cambios radican en la resolución de conflictos. Consulte la documentación de la API vinculada anteriormente para obtener más detalles.

/*
** 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. Inspeccionar el contenido de un conjunto de cambios

El código de ejemplo a continuación demuestra las técnicas utilizadas para iterar y extraer los datos relacionados con todos los cambios en un conjunto de cambios. Para resumir:

  1. Se llama a la API sqlite3changeset_start () para crear e inicializar un iterador para iterar a través del contenido de un conjunto de cambios. Inicialmente, el iterador no apunta a ningún elemento.

  2. La primera llamada a sqlite3changeset_next () en el iterador lo mueve para apuntar al primer cambio en el conjunto de cambios (o a EOF, si el conjunto de cambios está completamente vacío). sqlite3changeset_next () devuelve SQLITE_ROW si mueve el iterador para apuntar a una entrada válida, SQLITE_DONE si mueve el iterador a EOF, o un código de error SQLite si ocurre un error.

  3. Si el iterador apunta a una entrada válida, la API sqlite3changeset_op () puede usarse para determinar el tipo de cambio (INSERT, UPDATE o DELETE) al que apunta el iterador. Además, la misma API se puede utilizar para obtener el nombre de la tabla a la que se aplica el cambio y su número esperado de columnas y columnas de clave primaria.

  4. Si el iterador apunta a una entrada INSERT o UPDATE válida, se puede usar la API sqlite3changeset_new () para obtener los nuevos valores. * Dentro de la carga útil del cambio.

  5. Si el iterador apunta a una entrada válida DELETE o UPDATE, se puede usar la API sqlite3changeset_old () para obtener los valores antiguos. * Dentro de la carga útil del cambio.

  6. Un iterador se elimina mediante una llamada a la API sqlite3changeset_finalize (). Si se produce un error durante la iteración, se devuelve un código de error SQLite (incluso si sqlite3changeset_next () ya ha devuelto el mismo código de error). O, si no se ha producido ningún error, se devuelve 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 %sn",
      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 

  /* 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;

La mayoría de las aplicaciones solo usarán la funcionalidad del módulo de sesión descrita en la sección anterior. Sin embargo, la siguiente funcionalidad adicional está disponible para el uso y manipulación de blobs de conjuntos de cambios y conjuntos de parches:

  • Se pueden combinar dos o más conjuntos de cambios / conjuntos de parches utilizando las interfaces sqlite3changeset_concat () o sqlite3_changegroup.

  • Un conjunto de cambios se puede “invertir” usando la función de API sqlite3changeset_invert (). Un conjunto de cambios invertido deshace los cambios realizados por el original. Si el conjunto de cambios C+ es la inversa del conjunto de cambios C, luego aplica C y luego C+ a una base de datos debe dejar la base de datos sin cambios.