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