AP AutoExtract to Documents AI Database Migration from Postgres to MSSQL
- Inventory (Postgres Features Used)
- Databases: talaria (app data), temporal*, temporal_visibility* (*Temporal OSS does NOT natively support SQL Server today; keep Postgres/MySQL for Temporal or isolate those DBs).
- Data types: SERIAL, VARCHAR, TEXT, JSONB, TIMESTAMPTZ, DECIMAL(p,s), UUID (as text), BOOL.
- Constraints: UNIQUE, FOREIGN KEY (some ON DELETE CASCADE), composite uniques.
- Views: vw_logs_engine, vw_logs_exchange, vw_UserClientRoles, vw_UserRoles, vw_BlockedAddresses, vw_ExchangeSubscriptions, vw_Users, vw_Clients, vw_UserClients.
- “ON CONFLICT DO NOTHING” used in seed inserts.
- Type Mapping (Postgres → SQL Server)
- SERIAL -> INT IDENTITY(1,1)
- VARCHAR(n) -> VARCHAR(n)
- TEXT -> NVARCHAR(MAX) (or VARCHAR(MAX) if ASCII only)
- JSONB -> NVARCHAR(MAX) (with optional computed columns + JSON_VALUE indexes or redesign to relational tables)
- TIMESTAMPTZ -> DATETIME2(3) (store in UTC; enforce at app layer)
- DECIMAL(p,s) -> DECIMAL(p,s)
- BOOL -> BIT
- UUID stored as text -> UNIQUEIDENTIFIER (if you convert) or keep VARCHAR(48); prefer UNIQUEIDENTIFIER for indexing.
- NOW() default -> SYSUTCDATETIME()
- ON CONFLICT DO NOTHING -> MERGE pattern or IF NOT EXISTS (…) INSERT.
- Table-by-Table Notes (talaria) clients
- JSONB config: store as NVARCHAR(MAX). If frequent key lookups, add persisted computed columns: e.g. config_displayName AS JSON_VALUE([config_json], ‘$.displayName’) PERSISTED and index. users
- hashed_password, salt, iterations unchanged.
- last_logged_on VARCHAR(24) -> consider DATETIME2 if actual timestamp. user_client (bridge)
- Keep composite unique (user_id, client_id). roles, permissions, role_permissions
- Straightforward translation; TEXT -> NVARCHAR(MAX) only if long descriptions; else VARCHAR(400). user_roles, user_client_roles
- Maintain composite uniques; ensure indexed foreign keys. groups, user_groups
- Same pattern. documents
- document_hash UNIQUE; consider ADD UNIQUE INDEX. invoices
- Many monetary DECIMAL(10,2): OK.
- Stage/status frequently filtered: add nonclustered indexes. invoice_snapshots
- Potentially large; partition by snapshot_date (monthly) in Azure SQL Hyperscale or Managed Instance for growth. invoice_line_items
- DECIMAL(12,4) preserved. workflows, workflow_logs
- workflow_id (string) vs internal id: add nonclustered index on workflow_id. engine_logs, exchange_logs
- payload JSONB -> NVARCHAR(MAX); if querying fields, consider extraction pipeline or separate detail table; heavy logging may justify moving to Azure Log Analytics instead of RDBMS. exchange_blocked_senders
- Index (client_id, value). exchange_inbox_subscriptions/tokens
- Unique constraints map directly; DATETIME2 for timestamps. webhooks
- UNIQUE (client_id, url) fine. feedback
- fields JSONB -> NVARCHAR(MAX).
-
Views Translation Replace LIMIT 1000 with TOP (1000). Example: CREATE OR ALTER VIEW vw_logs_engine AS SELECT TOP (1000) id, [timestamp], log_level AS level, message, client_id, invoice_id FROM engine_logs ORDER BY [timestamp] DESC; Ordering in a view without TOP is ignored in SQL Server; keep TOP to emulate snapshot list.
-
Sample T-SQL DDL Patterns (Representative) clients: CREATE TABLE dbo.clients ( id INT IDENTITY(1,1) PRIMARY KEY, client_id VARCHAR(48) UNIQUE, client_name VARCHAR(100) UNIQUE, display_name VARCHAR(100), contact_name VARCHAR(100), contact_email VARCHAR(100), config NVARCHAR(MAX), created_on DATETIME2(3) NOT NULL CONSTRAINT DF_clients_created_on DEFAULT SYSUTCDATETIME(), modified_on DATETIME2(3) NOT NULL CONSTRAINT DF_clients_modified_on DEFAULT SYSUTCDATETIME(), enabled BIT ); users: CREATE TABLE dbo.users ( id INT IDENTITY(1,1) PRIMARY KEY, user_id VARCHAR(100) UNIQUE, username VARCHAR(100), first_name VARCHAR(100), last_name VARCHAR(100), hashed_password VARCHAR(200), salt VARCHAR(24), iterations INT, email VARCHAR(100) UNIQUE, api_key VARCHAR(48), created_on DATETIME2(3) NOT NULL DEFAULT SYSUTCDATETIME(), modified_on DATETIME2(3) NOT NULL DEFAULT SYSUTCDATETIME(), enabled BIT, last_logged_on VARCHAR(24), selected_client_name VARCHAR(100) ); (Repeat pattern for others.)
-
JSONB Strategy Options Option A (Minimal): Store as NVARCHAR(MAX), no indexing—fastest migration. Option B (Hybrid): Create computed persisted columns for high-value keys, index them. Option C (Refactor): Normalize key substructures (e.g., stages config) into relational tables—higher effort, better query performance.
-
Index Strategy Additions
- invoices: INDEX IX_invoices_stage_status (stage, status, client_id)
- invoice_line_items: INDEX IX_line_items_invoice (invoice_id)
- workflows: INDEX IX_workflows_workflow_id (workflow_id)
- engine_logs/exchange_logs: filtered or partitioned indexes on timestamp for retention mgmt.
-
Temporal Engine Databases Do not migrate temporal / temporal_visibility to SQL Server unless Temporal project adds support. Keep Postgres or move to MySQL if consolidation required. Isolate app schema from Temporal schema for clarity.
-
Migration Phases Phase 0: Governance
- Decide Azure SQL SKU (Business Critical vs General Purpose; consider Hyperscale for large logs).
- Enable AAD authentication; plan Managed Identity for app connectivity.
Phase 1: Assessment
- Export Postgres statistics (row counts, index usage).
- Classify JSONB access patterns.
Phase 2: Schema Translation
- Generate baseline T-SQL via tooling (pg2sql converters) then manually adjust types & constraints as above.
- Add SCD (system-versioned temporal tables) optionally for critical audit (e.g., invoices) if needed.
Phase 3: Prototype & Performance
- Load subset (5–10%) with Azure Data Factory or pg_dump + custom ETL.
- Validate query plans parity; add missing indexes.
Phase 4: Full Data Migration
- Freeze writes (or implement dual-write / CDC using Debezium).
- Bulk copy order:
- clients, users, roles, permissions
- link tables (user_client, role_permissions, user_roles, user_client_roles, groups, user_groups)
- documents
- invoices
- invoice_line_items
- invoice_snapshots
- workflows, workflow_logs
- logs (optional; consider archive strategy)
- Use BULK INSERT / bcp / Azure Data Factory Copy. Disable nonclustered indexes during massive loads, then rebuild.
Phase 5: Cutover
- Deploy updated application config (connection string to Azure SQL; use Managed Identity).
- Run checksum validation (row counts + sample hash of key columns).
- Release.
Phase 6: Post-Cutover
- Monitor DTU / vCore utilization, query store, deadlocks.
- Implement automatic index tuning (review suggestions; do not blindly accept).
Rollback Plan
- Keep Postgres in read-only snapshot for N days.
- Have a reversible data sync script (if limited delta window) or accept downtime rollback by DNS/app setting revert.
- Application Code Changes
- Replace Postgres-specific upserts (ON CONFLICT DO NOTHING) with: IF NOT EXISTS (SELECT 1 FROM dbo.clients WHERE client_id = @client_id) INSERT …
- Adjust any raw SQL quoting differences (double quotes → brackets or none).
- Replace NOW() with SYSUTCDATETIME().
- JSON operations moved to app layer (deserialize in Python) or use OPENJSON (if migrating to T-SQL usage later).
- Security & Compliance
- Use Azure AD authentication; assign least-privilege roles (db_datareader, db_datawriter, custom for DDL).
- Store secrets (if any legacy) in Key Vault; application uses Managed Identity.
- Enable Transparent Data Encryption (default).
- Enable Auditing & Threat Detection.
- Performance Considerations
- Parameterize all queries (already typical in drivers).
- Connection pooling via your Python driver (e.g., asyncpg replacement -> pyodbc / ODBC with pooling).
- Consider memory-optimized temp structures only if heavy temp table usage (not seen here).
- Offload large log tables to Azure Log Analytics or Event Hub pipeline.
- Testing Checklist
- Referential integrity (count mismatches).
- Sample invoice lifecycle in new DB.
- Role/permission resolution queries.
- Views return expected columns & row caps.
- Performance baseline (p95 latency comparable or better).
- Optional Enhancements
- Introduce system-versioned temporal tables for invoices & invoice_line_items.
- Partition large log tables by month.
- Materialized reporting tables for analytics instead of ad-hoc JSON parsing.
- Summary Risk Flags
- JSONB heavy queries may degrade unless refactored or indexed logically.
- Temporal engine DB cannot be moved to SQL Server now—plan hybrid persistence.
- Large log growth could inflate Azure SQL costs: consider alternate sink.
JSON Mapping File: