Application Note for QuNect ODBC for QuickBase
Synching all Quickbase tables in SQL Server
The following stored procedure will synch all tables the linked server has access to from Quickbase to SQL Server 2008 and beyond. For earlier versions of SQL Server please search and replace DATETIME2 with DATETIME and datetime2 with datetime. After running this stored procedure the tables in SQL Server will be up to date with the tables in Quickbase. This stored procedure takes two optional parameters. @keepAllVersionsOfModifiedRecords is set to false by default. If you set it to true then you are keeping a history of the state of all your records at each point that this stored procedure was run. The optional parameter @keepAllDeletedRecords is also set to false by default. If you set it to true then deleted records are not removed from the SQL Server version of the Quickbase table. This is useful if you want the capability to restore deleted records to Quickbase. However if your objective is to have an exact SQL Server image of your Quickbase data then you'll want to leave these parameters at their default value of false. This stored procedure will handle schema changes in Quickbase by adding new columns into the existing SQL Server table. If columns are renamed, deleted or changed to a different type in Quickbase then this stored procedure will make a copy of the corresponding existing SQL Server table with a timestamp in the name and start from scratch with a new SQL Server table. This stored procedure only modifies SQL Server tables, not the corresponding Quickbase tables and it requires version x.18.07.56 or higher of QuNect ODBC for QuickBase. If you want to go in the other direction please read Synching a SQL Server table in Quickbase.
/****** Requires version x.18.07.56 or higher of QuNect ODBC for QuickBase**************/ /****** Requires version SQL Server 2016 SP1 onward ***************************************/ -- Helper procedure for schema synchronization (create first - no dependencies) CREATE OR ALTER PROCEDURE [dbo].[SyncTableSchema] @tableName nvarchar(128) AS BEGIN TRY DECLARE @tempsql nvarchar(max) -- Get current table schema CREATE TABLE #current_schema ( COLUMN_NAME nvarchar(128), DATA_TYPE nvarchar(128), CHARACTER_MAXIMUM_LENGTH int, NUMERIC_PRECISION int, NUMERIC_SCALE int ) CREATE TABLE #temp_schema ( COLUMN_NAME nvarchar(128), DATA_TYPE nvarchar(128), CHARACTER_MAXIMUM_LENGTH int, NUMERIC_PRECISION int, NUMERIC_SCALE int ) -- Get schema info using set-based approach INSERT INTO #current_schema SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @tableName INSERT INTO #temp_schema SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME LIKE '%qunect_temp%' AND TABLE_SCHEMA = 'dbo' -- Add missing columns using set-based operations DECLARE @alterStatements TABLE (AlterSQL nvarchar(max)) INSERT INTO @alterStatements (AlterSQL) SELECT 'ALTER TABLE "' + @tableName + '" ADD "' + temp.COLUMN_NAME + '" ' + temp.DATA_TYPE + CASE WHEN temp.DATA_TYPE = 'varchar' THEN '(' + CAST(temp.CHARACTER_MAXIMUM_LENGTH AS varchar(10)) + ')' WHEN temp.DATA_TYPE = 'numeric' THEN '(' + CAST(temp.NUMERIC_PRECISION AS varchar(10)) + ',' + CAST(temp.NUMERIC_SCALE AS varchar(10)) + ')' ELSE '' END FROM #temp_schema temp LEFT JOIN #current_schema curr ON temp.COLUMN_NAME = curr.COLUMN_NAME WHERE curr.COLUMN_NAME IS NULL -- Execute all ALTER statements DECLARE @sql nvarchar(max) DECLARE alter_cursor CURSOR FOR SELECT AlterSQL FROM @alterStatements OPEN alter_cursor FETCH NEXT FROM alter_cursor INTO @sql WHILE @@FETCH_STATUS = 0 BEGIN BEGIN TRY EXEC(@sql) END TRY BEGIN CATCH PRINT 'Failed to execute: ' + @sql + ' - ' + ERROR_MESSAGE() END CATCH FETCH NEXT FROM alter_cursor INTO @sql END CLOSE alter_cursor DEALLOCATE alter_cursor END TRY BEGIN CATCH PRINT 'Error in SyncTableSchema for ' + @tableName + ': ' + ERROR_MESSAGE() END CATCH GO -- Helper procedure for merging data (depends on SyncTableSchema) CREATE OR ALTER PROCEDURE [dbo].[MergeIncrementalData] @tableName nvarchar(128), @columnList nvarchar(max), @recordIDFieldName nvarchar(128), @dateModifiedFieldName nvarchar(128), @keepAllVersionsOfModifiedRecords bit = 0 AS BEGIN TRY DECLARE @tempsql nvarchar(max) -- Handle schema differences EXEC dbo.SyncTableSchema @tableName = @tableName IF OBJECT_ID('qunect_temp') IS NOT NULL BEGIN -- Merge the data IF @keepAllVersionsOfModifiedRecords = 0 BEGIN -- Delete existing records with same IDs, then insert new ones SET @tempsql = 'DELETE target FROM "' + @tableName + '" target INNER JOIN qunect_temp temp ON target."' + @recordIDFieldName + '" = temp."' + @recordIDFieldName + '"' EXEC(@tempsql) SET @tempsql = 'INSERT INTO "' + @tableName + '" (' + @columnList + ') SELECT ' + @columnList + ' FROM qunect_temp' EXEC(@tempsql) END ELSE BEGIN -- Just insert all records (keeping versions) SET @tempsql = 'INSERT INTO "' + @tableName + '" (' + @columnList + ') SELECT ' + @columnList + ' FROM qunect_temp' EXEC(@tempsql) END END END TRY BEGIN CATCH PRINT 'Error in MergeIncrementalData for ' + @tableName + ': ' + ERROR_MESSAGE() END CATCH GO -- Helper procedure for incremental sync (depends on MergeIncrementalData) CREATE OR ALTER PROCEDURE [dbo].[PerformIncrementalSync] @tableName nvarchar(128), @columnList nvarchar(max), @dateModifiedFieldName nvarchar(128), @recordIDFieldName nvarchar(128), @keepAllVersionsOfModifiedRecords bit = 0, @keepAllDeletedRecords bit = 0 AS BEGIN TRY DECLARE @tempsql nvarchar(max) DECLARE @maxDateModified datetime2 DECLARE @maxDateModifiedText nvarchar(128) DECLARE @tempTableRecordCount int = 0 -- Get current records from QuickBase for comparison SET @tempsql = 'SELECT "' + @recordIDFieldName + '" as rid, "' + @dateModifiedFieldName + '" as dateModified INTO qunect_modified FROM OPENQUERY(QUICKBASE, ''select "' + @recordIDFieldName + '", "' + @dateModifiedFieldName + '" FROM "' + @tableName + '"'')' EXEC(@tempsql) -- Remove deleted records if configured IF @keepAllDeletedRecords = 0 BEGIN SET @tempsql = 'DELETE FROM "' + @tableName + '" WHERE "' + @recordIDFieldName + '" NOT IN(SELECT rid FROM qunect_modified)' EXEC(@tempsql) END -- Get the latest modification date from local table CREATE TABLE #qunect_last_modified (dateModified datetime2) SET @tempsql = ' INSERT INTO #qunect_last_modified (dateModified) SELECT TOP 1 qb.dateModified FROM qunect_modified qb LEFT JOIN "' + @tableName + '" local ON qb.rid = local."' + @recordIDFieldName + '" AND qb.dateModified = local."' + @dateModifiedFieldName + '" WHERE local."' + @dateModifiedFieldName + '" IS NULL OR qb.dateModified <> local."' + @dateModifiedFieldName + '" ORDER BY qb.dateModified ASC' EXEC(@tempsql) SELECT @maxDateModified = dateModified FROM #qunect_last_modified -- Check if any sync is needed DECLARE @backupCount int CREATE TABLE #backup_count (record_count int) SET @tempsql = 'INSERT INTO #backup_count SELECT COUNT(*) FROM "' + @tableName + '"' EXEC(@tempsql) SELECT @backupCount = record_count FROM #backup_count DROP TABLE #backup_count IF @maxDateModified IS NULL AND @backupCount > 0 BEGIN PRINT 'No records modified since last sync for table: ' + @tableName -- Cleanup IF OBJECT_ID('qunect_modified') IS NOT NULL DROP TABLE qunect_modified RETURN END IF OBJECT_ID('qunect_temp') IS NOT NULL DROP TABLE qunect_temp -- Get incremental records SET @maxDateModified = DATEADD(second, -1, @maxDateModified) SET @maxDateModifiedText = '{ts ''''' + CONVERT(VARCHAR(23), @maxDateModified, 121) + '''''}' PRINT 'Get incremental records' SET @tempsql = 'SELECT * INTO qunect_temp FROM OPENQUERY(QUICKBASE, ''select ' + @columnList + ' FROM "' + @tableName + '" WHERE ' + @dateModifiedFieldName + ' > ' + @maxDateModifiedText + ''')' EXEC(@tempsql) SET @tempTableRecordCount = 0 IF OBJECT_ID('qunect_temp') IS NOT NULL BEGIN PRINT 'Remove duplicates that already exist locally' -- Remove duplicates that already exist locally SET @tempsql = 'DELETE temp FROM qunect_temp temp INNER JOIN "' + @tableName + '" local ON temp."' + @recordIDFieldName + '" = local."' + @recordIDFieldName + '" AND temp."' + @dateModifiedFieldName + '" = local."' + @dateModifiedFieldName + '"' EXEC(@tempsql) SELECT @tempTableRecordCount = COUNT(*) FROM qunect_temp END PRINT 'Counting successfully processed records.' IF @tempTableRecordCount > 0 BEGIN EXEC dbo.MergeIncrementalData @tableName = @tableName, @columnList = @columnList, @recordIDFieldName = @recordIDFieldName, @dateModifiedFieldName = @dateModifiedFieldName, @keepAllVersionsOfModifiedRecords = @keepAllVersionsOfModifiedRecords PRINT 'Successfully processed ' + CAST(@tempTableRecordCount AS varchar(10)) + ' records for table: ' + @tableName END ELSE BEGIN PRINT 'No new records to process for table: ' + @tableName END -- Cleanup temp tables IF OBJECT_ID('qunect_modified') IS NOT NULL DROP TABLE qunect_modified IF OBJECT_ID('qunect_temp') IS NOT NULL DROP TABLE qunect_temp END TRY BEGIN CATCH PRINT 'Error in PerformIncrementalSync for ' + @tableName + ': ' + ERROR_MESSAGE() PRINT @tempsql -- Cleanup on error IF OBJECT_ID('qunect_modified') IS NOT NULL DROP TABLE qunect_modified IF OBJECT_ID('qunect_temp') IS NOT NULL DROP TABLE qunect_temp END CATCH GO -- Helper procedure to process individual tables (depends on PerformIncrementalSync) CREATE OR ALTER PROCEDURE [dbo].[ProcessSingleQuickBaseTable] @tableName nvarchar(128), @keepAllVersionsOfModifiedRecords bit = 0, @keepAllDeletedRecords bit = 0, @ignoreVirtualFields bit = 1 AS BEGIN TRY DECLARE @tempsql nvarchar(max) DECLARE @columnList nvarchar(max) DECLARE @dateModifiedFieldName nvarchar(128) DECLARE @recordIDFieldName nvarchar(128) DECLARE @doesExist int -- Clean up any existing temp tables IF OBJECT_ID('qunect_field_columns') IS NOT NULL DROP TABLE qunect_field_columns IF OBJECT_ID('qunect_temp') IS NOT NULL DROP TABLE qunect_temp IF OBJECT_ID('qunect_modified') IS NOT NULL DROP TABLE qunect_modified -- Get field information SET @tempsql = 'SELECT * INTO qunect_field_columns FROM OPENQUERY(QUICKBASE, ''select * FROM "' + @tableName + '~fields"' IF @ignoreVirtualFields = 1 SET @tempsql = @tempsql + ' WHERE mode = '''''''' OR mode IS NULL OR field_type = ''''recordid'''' '')' ELSE SET @tempsql = @tempsql + ' WHERE (mode = '''''''' OR mode IS NULL OR field_type = ''''recordid'''') OR (mode = ''''virtual'''' AND field_type NOT IN (''''dblink'''',''''url'''') ) '')' EXEC(@tempsql) -- Build column list using STRING_AGG (SQL Server 2017+) or XML PATH for older versions IF @@VERSION LIKE '%Microsoft SQL Server 2017%' OR @@VERSION LIKE '%Microsoft SQL Server 201[89]%' OR @@VERSION LIKE '%Microsoft SQL Server 202%' BEGIN -- Use STRING_AGG for newer versions SELECT @columnList = STRING_AGG('"' + REPLACE(COLUMN_NAME, '''', '''''') + '"', ',') FROM qunect_field_columns END ELSE BEGIN -- Use XML PATH for older versions SELECT @columnList = STUFF(( SELECT ',' + '"' + REPLACE(COLUMN_NAME, '''', '''''') + '"' FROM qunect_field_columns FOR XML PATH('') ), 1, 1, '') END -- Get key field names SELECT @dateModifiedFieldName = COLUMN_NAME FROM qunect_field_columns WHERE fid = 2 SELECT @recordIDFieldName = COLUMN_NAME FROM qunect_field_columns WHERE fid = 3 SELECT @doesExist = COUNT(*) FROM sysobjects WHERE Name = @tableName AND xType = 'U' -- If table doesn't exist, create it with all data IF @doesExist = 0 BEGIN SET @tempsql = 'SELECT * INTO "' + @tableName + '" FROM OPENQUERY(QUICKBASE, ''select ' + @columnList + ' FROM "' + @tableName + '"'')' EXEC(@tempsql) -- Cleanup IF OBJECT_ID('qunect_field_columns') IS NOT NULL DROP TABLE qunect_field_columns RETURN END -- Table exists, do incremental sync EXEC dbo.PerformIncrementalSync @tableName = @tableName, @columnList = @columnList, @dateModifiedFieldName = @dateModifiedFieldName, @recordIDFieldName = @recordIDFieldName, @keepAllVersionsOfModifiedRecords = @keepAllVersionsOfModifiedRecords, @keepAllDeletedRecords = @keepAllDeletedRecords -- Cleanup IF OBJECT_ID('qunect_field_columns') IS NOT NULL DROP TABLE qunect_field_columns END TRY BEGIN CATCH PRINT 'Error in ProcessSingleQuickBaseTable for ' + @tableName + ': ' + ERROR_MESSAGE() -- Cleanup on error IF OBJECT_ID('qunect_field_columns') IS NOT NULL DROP TABLE qunect_field_columns IF OBJECT_ID('qunect_temp') IS NOT NULL DROP TABLE qunect_temp IF OBJECT_ID('qunect_modified') IS NOT NULL DROP TABLE qunect_modified END CATCH GO -- Main procedure (depends on ProcessSingleQuickBaseTable) CREATE OR ALTER PROCEDURE [dbo].[SyncQuickBaseTablesToSQLServer] /****** Requires version x.18.07.56 or higher of QuNect ODBC for QuickBase ***************************************/ @keepAllVersionsOfModifiedRecords bit = 0, @keepAllDeletedRecords bit = 0, @ignoreVirtualFields bit = 1 AS BEGIN TRY SET NOCOUNT ON; -- Check for concurrent execution IF EXISTS (SELECT * FROM sysobjects WHERE id = object_id(N'[dbo].[qunect_tables_to_sync]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) BEGIN PRINT 'This procedure is terminating without performing any backups because' PRINT 'an instance of this stored procedure is already running.' PRINT 'This is based on detecting the presence of the table called' PRINT '"qunect_tables_to_sync". If this table exists and another instance of this stored' PRINT 'procedure is not running then manually delete the table called "qunect_tables_to_sync"' PRINT 'and rerun this stored procedure.' RETURN -1 END -- Create sync control table CREATE TABLE [dbo].[qunect_tables_to_sync] ( [TABLE_QUALIFIER] [nvarchar] (128) NULL, [TABLE_OWNER] [nvarchar] (128) NULL, [TABLE_NAME] [nvarchar] (128) NULL, [TABLE_TYPE] [nvarchar] (128) NULL, [REMARKS] [varchar] (254) NULL, [ProcessingOrder] int IDENTITY(1,1), [IsProcessed] bit DEFAULT 0, [HasError] bit DEFAULT 0, [ErrorMessage] nvarchar(max) NULL ) ON [PRIMARY] INSERT qunect_tables_to_sync (TABLE_QUALIFIER, TABLE_OWNER, TABLE_NAME, TABLE_TYPE, REMARKS) EXEC sp_tables_ex quickbase DECLARE @numQuickBaseTables int, @numQuickBaseTablesBackedUp int = 0 SELECT @numQuickBaseTables = COUNT(*) FROM qunect_tables_to_sync WHERE [TABLE_TYPE] = 'TABLE' -- Main processing loop using WHILE with set-based operations WHILE EXISTS (SELECT 1 FROM qunect_tables_to_sync WHERE TABLE_TYPE = 'TABLE' AND IsProcessed = 0 AND HasError = 0) BEGIN DECLARE @currentTable nvarchar(128) -- Get next table to process SELECT TOP 1 @currentTable = TABLE_NAME FROM qunect_tables_to_sync WHERE TABLE_TYPE = 'TABLE' AND IsProcessed = 0 AND HasError = 0 ORDER BY ProcessingOrder PRINT '' PRINT '####################' PRINT 'Processing table: ' + @currentTable BEGIN TRY EXEC dbo.ProcessSingleQuickBaseTable @tableName = @currentTable, @keepAllVersionsOfModifiedRecords = @keepAllVersionsOfModifiedRecords, @keepAllDeletedRecords = @keepAllDeletedRecords, @ignoreVirtualFields = @ignoreVirtualFields -- Mark as successfully processed UPDATE qunect_tables_to_sync SET IsProcessed = 1 WHERE TABLE_NAME = @currentTable SET @numQuickBaseTablesBackedUp = @numQuickBaseTablesBackedUp + 1 END TRY BEGIN CATCH -- Mark as error UPDATE qunect_tables_to_sync SET HasError = 1, ErrorMessage = ERROR_MESSAGE() WHERE TABLE_NAME = @currentTable PRINT 'Error processing table ' + @currentTable + ': ' + ERROR_MESSAGE() END CATCH END -- Cleanup DROP TABLE [dbo].[qunect_tables_to_sync] RETURN @numQuickBaseTables - @numQuickBaseTablesBackedUp END TRY BEGIN CATCH PRINT ERROR_MESSAGE() IF EXISTS (SELECT * FROM sysobjects WHERE id = object_id(N'[dbo].[qunect_tables_to_sync]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) DROP TABLE [dbo].[qunect_tables_to_sync] RETURN -1 END CATCH GO