Однонаправленная асинхронная дельта репликация средствами СУБД Firebird

Материал из GedeminWiki
Версия от 08:39, 22 февраля 2012; SYSDBA (обсуждение | вклад)

(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к: навигация, поиск

Увеличение размера файла базы данных практически всегда приводит к уменьшению скорости выполнения запросов. Однажды размер базы переходит ту критическую черту, за которой пользователи начинают ощущать дискомфорт от работы с программой, а потери времени на построение отчетов становятся существенными. Одним из решений указанной проблемы является создание отдельной оперативной базы с данными за небольшой период (например, за один год). Пользователи работают с оперативной базой, а исходная база (назовем ее архивной) используется для накопления всей информации и построения аналитических отчетов за прошлые периоды. Если оперативная и архивная базы расположены на разных компьютерах, то последняя может выполнять роль моментальной замены в случае выхода из строя основного сервера.

Периодически, все изменения из оперативной базы должны передаваться на архивную. Т.е. должна происходить однонаправленная репликация. В сети можно найти достаточное количество платных и бесплатных репликаторов. Мы же ставим перед собой задачу реализовать механизм однонаправленной асинхронной дельта репликации исключительно средствами СУБД Firebird без привлечения сторонних программ или библиотек UDF.

Создание оперативной базы данных

Исходная ситуация выглядит следующим образом:

db_repl_fb_1.png

Существующая база данных будет архивной. Копированием создадим из нее оперативную, из которой удалим информацию прошлых периодов, заменив ее сальдовыми записями для корректного построения отчетов. Разумеется, можно ничего не удалять. Тогда мы получим две идентичные базы данных.

db_repl_fb_2.png

С помощью приведенного ниже скрипта создаем в оперативной базе данных необходимые объекты для хранения истории изменений. Проиллюстрируем их на примере таблицы TEST, следующей структуры:

CREATE TABLE test (
  id     INTEGER NOT NULL PRIMARY KEY,
  str    VARCHAR(60))

История изменений каждой записи из TEST записывается с помощью трех триггеров в созданную скриптом таблицу GR2$TEST, которая отличается от исходной только префиксом в названии и добавленным полем GR2$SEQ.

CREATE TABLE GR2$TEST 
(
  ID	        INTEGER,
  STR	        VARCHAR(60) CHARACTER SET WIN1251,
  GR2$SEQ	INTEGER NOT NULL,
 
  PRIMARY KEY (GR2$SEQ)
);
 
CREATE TRIGGER GR2$_AI_TEST FOR TEST
  AFTER INSERT
  POSITION 32000
AS   
  DECLARE VARIABLE I INTEGER; 
BEGIN   
  I = GEN_ID(GR2$GSEQ, 1);   
  INSERT INTO GR2$TEST (ID,STR,GR2$SEQ) VALUES (NEW.ID,NEW.STR,:I);   
  INSERT INTO gr2$log (gr2$seq, gr2$relation_name, gr2$log_relation_name, gr2$act) 
    VALUES (:I, 'TEST', 'GR2$TEST', 'I'); 
END;
 
CREATE TRIGGER GR2$_AU_TEST FOR TEST
  AFTER UPDATE
  POSITION 32000
AS   
  DECLARE VARIABLE I INTEGER; 
BEGIN   
  I = GEN_ID(GR2$GSEQ, 2);   
  INSERT INTO GR2$TEST (ID,STR,GR2$SEQ) VALUES (OLD.ID,OLD.STR,:I - 1);   
  INSERT INTO GR2$TEST (ID,STR,GR2$SEQ) VALUES (NEW.ID,NEW.STR,:I);   
  INSERT INTO gr2$log (gr2$seq, gr2$relation_name, gr2$log_relation_name, gr2$act) 
    VALUES (:I, 'TEST', 'GR2$TEST', 'U'); 
END;
 
CREATE TRIGGER GR2$_AD_TEST FOR TEST
  AFTER DELETE
  POSITION 32000
AS   
  DECLARE VARIABLE I INTEGER; 
BEGIN   
  I = GEN_ID(GR2$GSEQ, 1);   
  INSERT INTO GR2$TEST (ID,STR,GR2$SEQ) VALUES (OLD.ID,OLD.STR,:I);   
  INSERT INTO gr2$log (gr2$seq, gr2$relation_name, gr2$log_relation_name, gr2$act) 
    VALUES (:I, 'TEST', 'GR2$TEST', 'D'); 
END;

Очередность операций, выполненных сервером, фиксируется в таблице GR2$LOG с помощью генератора GR2$GSEQ:

CREATE TABLE GR2$LOG 
(
  GR2$SEQ	        INTEGER NOT NULL,
  GR2$RELATION_NAME	CHAR(31) CHARACTER SET UNICODE_FSS NOT NULL,
  GR2$LOG_RELATION_NAME	CHAR(31) CHARACTER SET UNICODE_FSS NOT NULL,
  GR2$ACT	CHAR(1) CHARACTER SET WIN1251,
 
  CHECK (GR2$ACT IN ('I', 'U', 'D'))
);

Кроме указанных метаданных, скрипт формирует код переноса изменений на архивную базу данных и сохраняет его в таблице GR2$BLOCK_CODE:

CREATE TABLE GR2$BLOCK_CODE 
(
  GR2$RELATION_NAME	CHAR(31) CHARACTER SET UNICODE_FSS NOT NULL,
  GR2$BLOCK_TEXT	BLOB SUB_TYPE 1 SEGMENT SIZE 80 CHARACTER SET WIN1251 NOT NULL,
 
  PRIMARY KEY (GR2$RELATION_NAME)
);

Для таблицы TEST сформированный код будет выглядеть следующим образом:

EXECUTE BLOCK (seq INTEGER = :seq, act CHAR(1) = :act) 
AS 
  DECLARE VARIABLE ID INTEGER;
  DECLARE VARIABLE OLD_ID INTEGER;
  DECLARE VARIABLE STR VARCHAR(60) CHARACTER SET WIN1251;
BEGIN   
  IF (:act = 'I') THEN   
  BEGIN     
    SELECT ID,STR    
    FROM GR2$TEST    
    WHERE gr2$seq = :seq     
    INTO :ID,:STR;     
 
    EXECUTE STATEMENT ('INSERT INTO TEST (ID,STR) VALUES (:ID,:STR) ') (ID:=:ID,STR:=:STR)       
    ON EXTERNAL 'xxx:yyy'       
    WITH COMMON TRANSACTION       
    AS USER 'sysdba'       
    PASSWORD 'masterkey';  
  END 
  ELSE IF (:act = 'U') THEN   
  BEGIN     
    SELECT ID,STR    
    FROM GR2$TEST    
    WHERE gr2$seq = :seq     
    INTO :ID,:STR;     
 
    SELECT ID    
    FROM GR2$TEST    
    WHERE gr2$seq = :seq - 1     
    INTO :OLD_ID;     
 
    EXECUTE STATEMENT ('UPDATE TEST SET ID=:ID,STR=:STR WHERE ID=:OLD_ID') (ID:=:ID,STR:=:STR,OLD_ID:=:OLD_ID)       
    ON EXTERNAL 'xxx:yyy'       
    WITH COMMON TRANSACTION       
    AS USER 'sysdba'       
    PASSWORD 'masterkey';  
  END   
  ELSE BEGIN     
    SELECT ID    
    FROM GR2$TEST    
    WHERE gr2$seq = :seq     
    INTO :OLD_ID;     
 
    EXECUTE STATEMENT ('DELETE FROM TEST WHERE ID=:OLD_ID') (OLD_ID:=:OLD_ID)       
    ON EXTERNAL 'xxx:yyy'       
    WITH COMMON TRANSACTION       
    AS USER 'sysdba'       
    PASSWORD 'masterkey';  
  END 
END

Перенос изменений на архивную базу данных

В процессе работы с оперативной базой изменения будут накапливаться в таблицах "дублерах", а их очередность фиксироваться в логе GR2$LOG. Перенос изменений выполняется с помощью скрипта. Его можно выполнять вручную или сохранить в файл средствами операционной системы организовать автоматический периодический вызов.

Сам по себе скрипт чрезвычайно прост и делает ровно следующее:

  1. Проверим, что нет иных подключений к архивной базе кроме нашего.
  2. Проверим, что нет иных активных транзакций.
  3. Запомним список активных триггеров в таблице GR2$TRIGGERS. Если такой таблицы еще нет, то предварительно создадим ее.
  4. Последовательно будем читать историю изменений из GR2$LOG и для каждого выполнять заранее заготовленный в GR2$BLOCK_CODE код переноса данных на архивную базу.
  5. Подключим триггеры.
  6. Очистим лог и все таблицы "дублеры".

Обратите внимание: очистка лога и перенос данных выполняются на одной транзакции, что страхует нас от потерь в случае выключения сервера или падения канала связи.

Недостатки предложенной схемы

  1. Всегда хранится полная копия вставляемой или удаляемой записи. Для изменяемой записи всегда сохраняются ее старое и новое состояние.
  2. Перенос изменений требует прямого коннекта к архивной базе данных.

Исходный код

Переменная external_db хранит полное имя архивной базы данных (сервер, протокол, путь к файлу или алиас). Не забудьте скорректировать ее перед выполнением скрипта.

Подготовка оперативной базы данных

Блок вернет список таблиц без первичных ключей, для которых невозможно создать метаданные репликации.

EXECUTE BLOCK
  RETURNS(
    out_rn VARCHAR(200) CHARACTER SET UNICODE_FSS
  )
AS
  DECLARE VARIABLE rn VARCHAR(31) CHARACTER SET UNICODE_FSS;
  DECLARE VARIABLE fn VARCHAR(31) CHARACTER SET UNICODE_FSS;
  DECLARE VARIABLE ft VARCHAR(128) CHARACTER SET UNICODE_FSS;
  DECLARE VARIABLE is_pk INTEGER;
  DECLARE VARIABLE create_statement VARCHAR(32000);
  DECLARE VARIABLE trigger_statement VARCHAR(32000);
 
  DECLARE VARIABLE block_statement BLOB SUB_TYPE 1;
  DECLARE VARIABLE block_declare VARCHAR(32000);
  DECLARE VARIABLE block_fields_list VARCHAR(32000);
  DECLARE VARIABLE block_fields_params VARCHAR(32000);
  DECLARE VARIABLE block_fields_set_params VARCHAR(32000);
  DECLARE VARIABLE block_update_list VARCHAR(32000);
  DECLARE VARIABLE block_keys_list VARCHAR(32000);
  DECLARE VARIABLE block_keys_params VARCHAR(32000);
  DECLARE VARIABLE block_keys_set_params VARCHAR(32000);
  DECLARE VARIABLE block_condition VARCHAR(32000);
 
  DECLARE VARIABLE relname VARCHAR(31) CHARACTER SET UNICODE_FSS;
  DECLARE VARIABLE ai_trigger VARCHAR(31) CHARACTER SET UNICODE_FSS;
  DECLARE VARIABLE au_trigger VARCHAR(31) CHARACTER SET UNICODE_FSS;
  DECLARE VARIABLE ad_trigger VARCHAR(31) CHARACTER SET UNICODE_FSS;
 
  DECLARE VARIABLE external_db VARCHAR(255) = 'xxx:yyy';
  DECLARE VARIABLE external_user VARCHAR(31) = 'sysdba';
  DECLARE VARIABLE external_password VARCHAR(10) = 'masterkey';
BEGIN
  IF (EXISTS(SELECT * FROM rdb$relations WHERE rdb$relation_name LIKE 'GR2$%')) THEN
    EXIT;
 
  IF (EXISTS(SELECT * FROM rdb$generators WHERE rdb$generator_name = 'GD_G_UNIQUE')) THEN
    EXECUTE STATEMENT 'SELECT GEN_ID(gd_g_unique, 10000) FROM rdb$database'
    WITH AUTONOMOUS TRANSACTION;
 
  EXECUTE STATEMENT
    'create table gr2$log (' ||
      'gr2$seq INTEGER NOT NULL, ' ||
      'gr2$relation_name CHAR(31) CHARACTER SET UNICODE_FSS NOT NULL, ' ||
      'gr2$log_relation_name CHAR(31) CHARACTER SET UNICODE_FSS NOT NULL, ' ||
      'gr2$act CHAR(1), CHECK (GR2$ACT IN (''I'', ''U'', ''D'')) ' ||
    ')'
    WITH AUTONOMOUS TRANSACTION;
 
  EXECUTE STATEMENT
    'GRANT ALL ON gr2$log TO Administrator'
    WITH AUTONOMOUS TRANSACTION;
 
  EXECUTE STATEMENT
    'CREATE TABLE gr2$block_code (' ||
      'gr2$relation_name CHAR(31) CHARACTER SET UNICODE_FSS NOT NULL PRIMARY KEY, ' ||
      /*'gr2$block_text VARCHAR(32000) NOT NULL ' ||*/
      'gr2$block_text BLOB SUB_TYPE 1 NOT NULL ' ||
    ')'
    WITH AUTONOMOUS TRANSACTION;
 
  EXECUTE STATEMENT
    'GRANT ALL ON gr2$block_code TO Administrator'
    WITH AUTONOMOUS TRANSACTION;
 
  EXECUTE STATEMENT
    'create generator gr2$gseq'
    WITH AUTONOMOUS TRANSACTION;
 
  FOR
    SELECT TRIM(rdb$relation_name)
    FROM rdb$relations
    WHERE COALESCE(rdb$system_flag, 0) = 0
      AND NOT rdb$relation_name LIKE 'GR2$%'
      AND NOT (rdb$relation_name IN ('LOG', 'BLOCK_CODE'))
      AND rdb$external_file IS NULL
      AND rdb$view_source IS NULL
    INTO :rn
  DO BEGIN
    IF (EXISTS(SELECT * FROM rdb$relation_constraints WHERE rdb$constraint_type = 'PRIMARY KEY'
      AND rdb$relation_name = :rn)) THEN
    BEGIN
      create_statement = '';
      trigger_statement = '';
      block_declare = '';
      block_fields_list = '';
      block_fields_params = '';
      block_fields_set_params = '';
      block_update_list = '';
      block_keys_list = '';
      block_keys_params = '';
      block_keys_set_params = '';
      block_condition = '';
      FOR
        SELECT TRIM(rf.rdb$field_name),
          CASE f.rdb$field_type
            WHEN 7 THEN 'SMALLINT'
            WHEN 8 THEN
              CASE f.rdb$field_sub_type
                WHEN 0 THEN 'INTEGER'
                WHEN 1 THEN 'NUMERIC(' || f.rdb$field_precision || ',' || ABS(f.rdb$field_scale) || ')'
                WHEN 2 THEN 'DECIMAL(' || f.rdb$field_precision || ',' || ABS(f.rdb$field_scale) || ')'
                ELSE 'unknown'
              END
            WHEN 10 THEN 'FLOAT'
            WHEN 12 THEN 'DATE'
            WHEN 13 THEN 'TIME'
            WHEN 14 THEN 'CHAR(' || f.rdb$character_length || ') CHARACTER SET ' ||
              (SELECT TRIM(rdb$character_set_name) FROM rdb$character_sets WHERE rdb$character_set_id = f.rdb$character_set_id)
            WHEN 16 THEN 'BIGINT'
            WHEN 27 THEN 'DOUBLE PRECISION'
            WHEN 35 THEN 'TIMESTAMP'
            WHEN 37 THEN 'VARCHAR(' || f.rdb$character_length || ') CHARACTER SET ' ||
              (SELECT TRIM(rdb$character_set_name) FROM rdb$character_sets WHERE rdb$character_set_id = f.rdb$character_set_id)
            WHEN 261 THEN
              CASE f.rdb$field_sub_type
                WHEN 1 THEN 'BLOB SUB_TYPE 1 CHARACTER SET ' ||
                  (SELECT TRIM(rdb$character_set_name) FROM rdb$character_sets WHERE rdb$character_set_id = f.rdb$character_set_id)
                ELSE 'BLOB SUB_TYPE 0'
              END
            ELSE 'unknown'
          END,
          (SELECT 1
           FROM rdb$relation_constraints rc2
             JOIN rdb$indices i2 ON i2.rdb$index_name = rc2.rdb$index_name
             JOIN rdb$index_segments iseg2 ON iseg2.rdb$index_name = i2.rdb$index_name
           WHERE
             rc2.rdb$constraint_type = 'PRIMARY KEY'
             AND rc2.rdb$relation_name = :rn AND iseg2.rdb$field_name = rf.rdb$field_name)
        FROM rdb$relation_fields rf JOIN rdb$fields f
          ON f.rdb$field_name = rf.rdb$field_source
        WHERE rf.rdb$relation_name = :rn AND f.rdb$computed_source IS NULL
        ORDER BY rf.rdb$field_position
        INTO :fn, :ft, :is_pk
      DO
      BEGIN
        create_statement = :create_statement || :fn || ' ' || :ft || ',';
        trigger_statement = :trigger_statement || 'meta.' || :fn || ',';
        block_declare = :block_declare || 'DECLARE VARIABLE ' || :fn || ' ' || :ft || ';';
        block_fields_list = :block_fields_list || :fn || ',';
        block_fields_params = :block_fields_params || ':' || :fn || ',';
        block_fields_set_params = :block_fields_set_params || :fn || ':=:' || :fn || ',';
        block_update_list = :block_update_list || :fn || '=:' || :fn || ',';
        IF (:is_pk = 1) THEN
        BEGIN
          block_declare = :block_declare || 'DECLARE VARIABLE OLD_' || :fn || ' ' || :ft || ';';
          block_keys_list = :block_keys_list || :fn || ',';
          block_keys_params = :block_keys_params || ':OLD_' || :fn || ',';
          block_keys_set_params = :block_keys_set_params || 'OLD_' || :fn || ':=:OLD_' || :fn || ',';
          block_condition = :block_condition || :fn || '=:OLD_' || :fn || ' AND ';
        END
      END
 
      create_statement = LEFT(:create_statement, CHAR_LENGTH(:create_statement) - 1);
      trigger_statement = LEFT(:trigger_statement, CHAR_LENGTH(:trigger_statement) - 1);
 
      block_fields_list = LEFT(:block_fields_list, CHAR_LENGTH(:block_fields_list) - 1);
      block_fields_params = LEFT(:block_fields_params, CHAR_LENGTH(:block_fields_params) - 1);
      block_fields_set_params = LEFT(:block_fields_set_params, CHAR_LENGTH(:block_fields_set_params) - 1);
      block_update_list = LEFT(:block_update_list, CHAR_LENGTH(:block_update_list) - 1);
      block_keys_list = LEFT(:block_keys_list, CHAR_LENGTH(:block_keys_list) - 1);
      block_keys_params = LEFT(:block_keys_params, CHAR_LENGTH(:block_keys_params) - 1);
      block_keys_set_params = LEFT(:block_keys_set_params, CHAR_LENGTH(:block_keys_set_params) - 1);
      block_condition = LEFT(:block_condition, CHAR_LENGTH(:block_condition) - 5);
 
      relname = LEFT('GR2$' || :rn, 31);
      ai_trigger = LEFT('GR2$_AI_' || :rn, 31);
      au_trigger = LEFT('GR2$_AU_' || :rn, 31);
      ad_trigger = LEFT('GR2$_AD_' || :rn, 31);
 
      EXECUTE STATEMENT 'CREATE TABLE ' || :relname || ' (' || :create_statement ||
        ',GR2$SEQ INTEGER NOT NULL, PRIMARY KEY(GR2$SEQ))';
 
      --out_rn = 'Ok! ' || :rn;
      --SUSPEND;
 
      EXECUTE STATEMENT
        'CREATE TRIGGER ' || :ai_trigger || ' FOR ' || :rn || ' AFTER INSERT POSITION 32000 ' ||
        'AS ' ||
        '  DECLARE VARIABLE I INTEGER; ' ||
        'BEGIN ' ||
        '  I = GEN_ID(GR2$GSEQ, 1); ' ||
        '  INSERT INTO ' || :relname || ' (' || REPLACE(:trigger_statement, 'meta.', '') || ',GR2$SEQ) VALUES (' ||
             REPLACE(:trigger_statement, 'meta.', 'NEW.') || ',:I); ' ||
        '  INSERT INTO gr2$log (gr2$seq, gr2$relation_name, gr2$log_relation_name, gr2$act) VALUES (' ||
             ':I, ''' || :rn || ''', ''' || :relname || ''', ''I''); ' ||
        'END';
 
      EXECUTE STATEMENT
        'CREATE TRIGGER ' || :au_trigger || ' FOR ' || :rn || ' AFTER UPDATE POSITION 32000 ' ||
        'AS ' ||
        '  DECLARE VARIABLE I INTEGER; ' ||
        'BEGIN ' ||
        '  I = GEN_ID(GR2$GSEQ, 2); ' ||
        '  INSERT INTO ' || :relname || ' (' || REPLACE(:trigger_statement, 'meta.', '') || ',GR2$SEQ) VALUES (' ||
             REPLACE(:trigger_statement, 'meta.', 'OLD.') || ',:I - 1); ' ||
        '  INSERT INTO ' || :relname || ' (' || REPLACE(:trigger_statement, 'meta.', '') || ',GR2$SEQ) VALUES (' ||
             REPLACE(:trigger_statement, 'meta.', 'NEW.') || ',:I); ' ||
        '  INSERT INTO gr2$log (gr2$seq, gr2$relation_name, gr2$log_relation_name, gr2$act) VALUES (' ||
             ':I, ''' || :rn || ''', ''' || :relname || ''', ''U''); ' ||
        'END';
 
      EXECUTE STATEMENT
        'CREATE TRIGGER ' || :ad_trigger || ' FOR ' || :rn || ' AFTER DELETE POSITION 32000 ' ||
        'AS ' ||
        '  DECLARE VARIABLE I INTEGER; ' ||
        'BEGIN ' ||
        '  I = GEN_ID(GR2$GSEQ, 1); ' ||
        '  INSERT INTO ' || :relname || ' (' || REPLACE(:trigger_statement, 'meta.', '') || ',GR2$SEQ) VALUES (' ||
             REPLACE(:trigger_statement, 'meta.', 'OLD.') || ',:I); ' ||
        '  INSERT INTO gr2$log (gr2$seq, gr2$relation_name, gr2$log_relation_name, gr2$act) VALUES (' ||
             ':I, ''' || :rn || ''', ''' || :relname || ''', ''D''); ' ||
        'END';
 
      block_statement =
        'EXECUTE BLOCK (seq INTEGER = :seq, act CHAR(1) = :act) AS ' ||
          :block_declare ||
        'BEGIN ' ||
        '  IF (:act = ''I'') THEN ' ||
        '  BEGIN ' ||
        '    SELECT ' || :block_fields_list ||
        '    FROM ' || :relname ||
        '    WHERE gr2$seq = :seq ' ||
        '    INTO ' || :block_fields_params || '; ' ||
        '    EXECUTE STATEMENT (''INSERT INTO ' || :rn || ' (' || :block_fields_list || ') VALUES (' || :block_fields_params || ') '') ' ||
        '      (' || :block_fields_set_params || ') ' ||
        '      ON EXTERNAL ''' || :external_db || ''' ' ||
        '      WITH COMMON TRANSACTION ' ||
        '      AS USER ''' || :external_user || ''' ' ||
        '      PASSWORD ''' || :external_password || ''';' ||
        '  END ELSE IF (:act = ''U'') THEN ' ||
        '  BEGIN ' ||
        '    SELECT ' || :block_fields_list ||
        '    FROM ' || :relname ||
        '    WHERE gr2$seq = :seq ' ||
        '    INTO ' || :block_fields_params || '; ' ||
        '    SELECT ' || :block_keys_list ||
        '    FROM ' || :relname ||
        '    WHERE gr2$seq = :seq - 1 ' ||
        '    INTO ' || :block_keys_params || '; ' ||
        '    EXECUTE STATEMENT (''UPDATE ' || :rn || ' SET ' || :block_update_list || ' WHERE ' || :block_condition || ''') (' ||
               :block_fields_set_params || ',' || :block_keys_set_params || ') ' ||
        '      ON EXTERNAL ''' || :external_db || ''' ' ||
        '      WITH COMMON TRANSACTION ' ||
        '      AS USER ''' || :external_user || ''' ' ||
        '      PASSWORD ''' || :external_password || ''';' ||
        '  END ' ||
        '  ELSE BEGIN '||
        '    SELECT ' || :block_keys_list ||
        '    FROM ' || :relname ||
        '    WHERE gr2$seq = :seq ' ||
        '    INTO ' || :block_keys_params || '; ' ||
        '    EXECUTE STATEMENT (''DELETE FROM ' || :rn || ' WHERE ' || :block_condition || ''') (' || :block_keys_set_params || ') ' ||
        '      ON EXTERNAL ''' || :external_db || ''' ' ||
        '      WITH COMMON TRANSACTION ' ||
        '      AS USER ''' || :external_user || ''' ' ||
        '      PASSWORD ''' || :external_password || ''';' ||
        '  END ' ||
        'END';
 
      EXECUTE STATEMENT ('INSERT INTO gr2$block_code (gr2$relation_name, gr2$block_text) VALUES (?, ?)')
        (:rn, :block_statement);
    END ELSE
    BEGIN
      out_rn = 'No PK! ' || :rn;
      SUSPEND;
    END
  END
END

Репликация изменений на архивную базу данных

Если по структуре архивной базы данных некоторые триггеры не должны быть деактивированы перед переносом данных, соответствующие условия должны быть добавлены в код ниже.

EXECUTE BLOCK
AS
  DECLARE VARIABLE external_db VARCHAR(255) = 'xxx:yyy';
  DECLARE VARIABLE external_user VARCHAR(31) = 'sysdba';
  DECLARE VARIABLE external_password VARCHAR(10) = 'masterkey';
  DECLARE VARIABLE c INTEGER;
  DECLARE VARIABLE seq INTEGER;
  DECLARE VARIABLE act CHAR(1);
  DECLARE VARIABLE block_text BLOB SUB_TYPE 1;
  DECLARE VARIABLE activate_triggers VARCHAR(2048) = '';
  DECLARE VARIABLE rn VARCHAR(31) CHARACTER SET UNICODE_FSS;
BEGIN
  IF (EXISTS(SELECT * FROM rdb$relations WHERE rdb$relation_name = 'GR2$LOG')) THEN
  BEGIN
    EXECUTE STATEMENT
      'SELECT COUNT(*) FROM mon$attachments'
      ON EXTERNAL :external_db
      WITH AUTONOMOUS TRANSACTION
      AS USER :external_user
      PASSWORD :external_password
      INTO :C;
 
    IF (:c <> 1) THEN
      EXIT;
 
    EXECUTE STATEMENT
      'SELECT COUNT(*) FROM mon$transactions WHERE mon$state = 1 AND mon$read_only = 0 '
      ON EXTERNAL :external_db
      WITH AUTONOMOUS TRANSACTION
      AS USER :external_user
      PASSWORD :external_password
      INTO :C;
 
    IF (:c <> 1) THEN
      EXIT;
 
    EXECUTE STATEMENT
      'SELECT COUNT(*) FROM rdb$relations WHERE rdb$relation_name = ''GR2$TRIGGERS'' '
      ON EXTERNAL :external_db
      WITH AUTONOMOUS TRANSACTION
      AS USER :external_user
      PASSWORD :external_password
      INTO :C;
 
    IF (:c = 0) THEN
    BEGIN
      EXECUTE STATEMENT
        'CREATE TABLE gr2$triggers ( ' ||
          'gr2$trigger_name VARCHAR(31) CHARACTER SET UNICODE_FSS NOT NULL) '
        ON EXTERNAL :external_db
        WITH AUTONOMOUS TRANSACTION
        AS USER :external_user
        PASSWORD :external_password;
    END ELSE
    BEGIN
      EXECUTE STATEMENT
        'DELETE FROM gr2$triggers '
        ON EXTERNAL :external_db
        WITH AUTONOMOUS TRANSACTION
        AS USER :external_user
        PASSWORD :external_password;
    END
 
    EXECUTE STATEMENT
      'EXECUTE BLOCK AS ' ||
      '  DECLARE VARIABLE TN VARCHAR(31) CHARACTER SET UNICODE_FSS; ' ||
      'BEGIN ' ||
      '  INSERT INTO gr2$triggers (gr2$trigger_name) SELECT rdb$trigger_name ' ||
      '    FROM rdb$triggers WHERE rdb$system_flag = 0 AND rdb$trigger_inactive = 0;' ||
      '  FOR SELECT gr2$trigger_name FROM gr2$triggers INTO :tn ' ||
      '  DO BEGIN ' ||
      '    EXECUTE STATEMENT ''ALTER TRIGGER '' || :tn || '' INACTIVE ''; ' ||
      '  END ' ||
      'END'
      ON EXTERNAL :external_db
      WITH AUTONOMOUS TRANSACTION
      AS USER :external_user
      PASSWORD :external_password;
 
    activate_triggers =
      'EXECUTE BLOCK AS ' ||
      '  DECLARE VARIABLE TN VARCHAR(31) CHARACTER SET UNICODE_FSS; ' ||
      'BEGIN ' ||
      '  FOR SELECT gr2$trigger_name FROM gr2$triggers INTO :tn ' ||
      '  DO BEGIN ' ||
      '    EXECUTE STATEMENT ''ALTER TRIGGER '' || :tn || '' ACTIVE ''; ' ||
      '  END ' ||
      '  DELETE FROM gr2$triggers; ' ||
      'END';
 
    FOR
      SELECT l.gr2$seq, l.gr2$act, b.gr2$block_text
      FROM gr2$log l JOIN gr2$block_code b ON
        l.gr2$relation_name = b.gr2$relation_name
      ORDER BY l.gr2$seq
      INTO :seq, :act, :block_text
    DO BEGIN
      EXECUTE STATEMENT (:block_text) (seq := :seq, act := :act);
    END
 
    FOR
      SELECT DISTINCT(rdb$relation_name)
      FROM rdb$relation_fields
      WHERE rdb$field_name = 'GR2$SEQ'
      INTO :rn
    DO
      EXECUTE STATEMENT 'DELETE FROM ' || :rn || ' WHERE gr2$seq <= ' || :seq;
 
    EXECUTE STATEMENT :activate_triggers
      ON EXTERNAL :external_db
      WITH AUTONOMOUS TRANSACTION
      AS USER :external_user
      PASSWORD :external_password;
  END
 
  WHEN ANY DO
  BEGIN
    IF (:activate_triggers > '') THEN
      EXECUTE STATEMENT :activate_triggers
        ON EXTERNAL :external_db
        WITH AUTONOMOUS TRANSACTION
        AS USER :external_user
        PASSWORD :external_password;
    EXCEPTION;
  END
END

Удаление метаданных репликатора из архивной и оперативной баз данных

Скрипт выполняется на каждой из баз.

EXECUTE block
AS
  DECLARE variable tn VARCHAR(31) CHARACTER SET unicode_fss;
  DECLARE variable rn VARCHAR(31) CHARACTER SET unicode_fss;
  DECLARE variable gn VARCHAR(31) CHARACTER SET unicode_fss;
BEGIN
  FOR SELECT rdb$trigger_name FROM rdb$triggers
    WHERE rdb$trigger_name LIKE 'GR2$%'
    INTO :tn
  do BEGIN
    EXECUTE statement 'DROP TRIGGER ' || :tn;
  END
 
  FOR SELECT rdb$relation_name FROM rdb$relations
    WHERE rdb$relation_name LIKE 'GR2$%'
    INTO :rn
  do BEGIN
    EXECUTE statement 'DROP TABLE ' || :rn;
  END
 
  FOR SELECT rdb$generator_name FROM rdb$generators
    WHERE rdb$generator_name LIKE 'GR2$%'
    INTO :gn
  do BEGIN
    EXECUTE statement 'DROP GENERATOR ' || :gn;
  END
END
Персональные инструменты
Пространства имён

Варианты
Действия
Навигация
Инструменты