-- ============================================================================= -- GB5 Job Engine — Core Schema Migration (SQL Server) -- Migration: 20260726 — JobEngine Schema -- -- Changes applied: -- 1. TSCHEDULER — ADD new schedule-type columns (SCHEDULERTYPE, INTERVALSECONDS, -- SCHEDULEDDATE, DESCRIPTION, SCHEDULERSTATUS, UPDATEDBY, UPDATEDDATE) -- 2. MJOBDEFINE — ADD new runtime/routing columns (MAXRETRIES, TIMEOUTSECONDS, -- NEXTRUNTIME, ISCONCURRENT, JOBCATEGORY, SOURCEMODULE, -- SOURCEOBJECTID, DAPRPUBSUBTOPIC, CREATEDBY/DATE, UPDATEDBY/DATE) -- 3. TJOBEXECUTION — CREATE execution history table -- 4. TJOBQUEUE — CREATE persistent async message queue table -- 5. QRTZ_* — CREATE Quartz.NET 3.x SQL Server scheduler tables -- -- All blocks are idempotent (IF NOT EXISTS guards) — safe to re-run. -- ============================================================================= -- ============================================================================= -- 1. TSCHEDULER — add new columns -- ============================================================================= IF NOT EXISTS ( SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'TSCHEDULER' AND COLUMN_NAME = 'SCHEDULERTYPE' ) BEGIN ALTER TABLE TSCHEDULER ADD SCHEDULERTYPE NVARCHAR(20) NOT NULL CONSTRAINT DF_TSCHEDULER_SCHEDULERTYPE DEFAULT 'CRON', INTERVALSECONDS INT NULL, SCHEDULEDDATE DATETIME NULL, DESCRIPTION NVARCHAR(500) NULL, SCHEDULERSTATUS NVARCHAR(20) NOT NULL CONSTRAINT DF_TSCHEDULER_SCHEDULERSTATUS DEFAULT 'ACTIVE', CREATEDBY INT NULL, CREATEDDATE DATETIME NOT NULL CONSTRAINT DF_TSCHEDULER_CREATEDDATE DEFAULT GETDATE(), UPDATEDBY INT NULL, UPDATEDDATE DATETIME NULL; END GO -- CHECK constraint for SCHEDULERTYPE values IF NOT EXISTS ( SELECT 1 FROM sys.check_constraints WHERE parent_object_id = OBJECT_ID('TSCHEDULER') AND name = 'CK_TSCHEDULER_SCHEDULERTYPE' ) ALTER TABLE TSCHEDULER ADD CONSTRAINT CK_TSCHEDULER_SCHEDULERTYPE CHECK (SCHEDULERTYPE IN ('CRON', 'INTERVAL', 'ONE_TIME')); GO IF NOT EXISTS ( SELECT 1 FROM sys.check_constraints WHERE parent_object_id = OBJECT_ID('TSCHEDULER') AND name = 'CK_TSCHEDULER_SCHEDULERSTATUS' ) ALTER TABLE TSCHEDULER ADD CONSTRAINT CK_TSCHEDULER_SCHEDULERSTATUS CHECK (SCHEDULERSTATUS IN ('ACTIVE', 'PAUSED', 'DISABLED')); GO -- ============================================================================= -- 2. MJOBDEFINE — add new columns -- ============================================================================= IF NOT EXISTS ( SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'MJOBDEFINE' AND COLUMN_NAME = 'MAXRETRIES' ) BEGIN ALTER TABLE MJOBDEFINE ADD MAXRETRIES INT NOT NULL CONSTRAINT DF_MJOBDEFINE_MAXRETRIES DEFAULT 3, TIMEOUTSECONDS INT NOT NULL CONSTRAINT DF_MJOBDEFINE_TIMEOUTSECONDS DEFAULT 300, NEXTRUNTIME DATETIME NULL, ISCONCURRENT TINYINT NOT NULL CONSTRAINT DF_MJOBDEFINE_ISCONCURRENT DEFAULT 0, JOBCATEGORY NVARCHAR(50) NULL, SOURCEMODULE NVARCHAR(50) NULL, SOURCEOBJECTID INT NULL, DAPRPUBSUBTOPIC NVARCHAR(200) NULL, CREATEDBY INT NULL, CREATEDDATE DATETIME NULL CONSTRAINT DF_MJOBDEFINE_CREATEDDATE DEFAULT GETDATE(), UPDATEDBY INT NULL, UPDATEDDATE DATETIME NULL; END GO -- Index: scheduler preview — jobs due in next 24h IF NOT EXISTS ( SELECT 1 FROM sys.indexes WHERE name = 'IX_MJOBDEFINE_STATUS_NEXTRUNTIME' AND object_id = OBJECT_ID('MJOBDEFINE') ) CREATE INDEX IX_MJOBDEFINE_STATUS_NEXTRUNTIME ON MJOBDEFINE (STATUS, NEXTRUNTIME) WHERE STATUS = 1; GO -- Index: module-driven upsert (UPSERT_BY_SOURCE query) IF NOT EXISTS ( SELECT 1 FROM sys.indexes WHERE name = 'IX_MJOBDEFINE_SOURCEMODULE_OBJECTID_TENANT' AND object_id = OBJECT_ID('MJOBDEFINE') ) CREATE INDEX IX_MJOBDEFINE_SOURCEMODULE_OBJECTID_TENANT ON MJOBDEFINE (SOURCEMODULE, SOURCEOBJECTID, TENANTID) WHERE SOURCEMODULE IS NOT NULL; GO -- ============================================================================= -- 3. TJOBEXECUTION — execution history per job attempt -- ============================================================================= IF NOT EXISTS ( SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'TJOBEXECUTION' ) BEGIN CREATE TABLE TJOBEXECUTION ( EXECUTIONID INT NOT NULL IDENTITY(1,1), JOBID INT NULL, TENANTID INT NOT NULL, JOBNAME NVARCHAR(200) NOT NULL, TRIGGERTYPE NVARCHAR(20) NOT NULL, -- SCHEDULED | MANUAL | QUEUE | API SCHEDULEDFOR DATETIME NULL, STARTEDAT DATETIME NULL, COMPLETEDAT DATETIME NULL, DURATIONMS INT NULL, STATUS NVARCHAR(20) NOT NULL, -- RUNNING | SUCCESS | FAILED | CANCELLED | TIMEOUT RETRYATTEMPT INT NOT NULL CONSTRAINT DF_TJOBEXECUTION_RETRYATTEMPT DEFAULT 0, CORRELATIONID NVARCHAR(100) NULL, HOSTINSTANCE NVARCHAR(200) NULL, ERRORDETAILS NVARCHAR(MAX) NULL, RESULTJSON NVARCHAR(MAX) NULL, CREATEDDATE DATETIME NOT NULL CONSTRAINT DF_TJOBEXECUTION_CREATEDDATE DEFAULT GETDATE(), CONSTRAINT PK_TJOBEXECUTION PRIMARY KEY (EXECUTIONID), CONSTRAINT CK_TJOBEXECUTION_TRIGGERTYPE CHECK (TRIGGERTYPE IN ('SCHEDULED','MANUAL','QUEUE','API')), CONSTRAINT CK_TJOBEXECUTION_STATUS CHECK (STATUS IN ('RUNNING','SUCCESS','FAILED','CANCELLED','TIMEOUT')) ); CREATE INDEX IX_TJOBEXECUTION_TENANT_START ON TJOBEXECUTION (TENANTID, STARTEDAT DESC); CREATE INDEX IX_TJOBEXECUTION_JOBID ON TJOBEXECUTION (JOBID) WHERE JOBID IS NOT NULL; CREATE INDEX IX_TJOBEXECUTION_STATUS_TENANT ON TJOBEXECUTION (STATUS, TENANTID); END GO -- ============================================================================= -- 4. TJOBQUEUE — persistent async message queue -- ============================================================================= IF NOT EXISTS ( SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'TJOBQUEUE' ) BEGIN CREATE TABLE TJOBQUEUE ( QUEUEID INT NOT NULL IDENTITY(1,1), TENANTID INT NOT NULL, MESSAGETYPE NVARCHAR(100) NOT NULL, PAYLOAD NVARCHAR(MAX) NOT NULL, PRIORITY TINYINT NOT NULL CONSTRAINT DF_TJOBQUEUE_PRIORITY DEFAULT 5, STATUS NVARCHAR(20) NOT NULL CONSTRAINT DF_TJOBQUEUE_STATUS DEFAULT 'PENDING', SCHEDULEDFOR DATETIME NOT NULL CONSTRAINT DF_TJOBQUEUE_SCHEDULEDFOR DEFAULT GETDATE(), MAXRETRIES INT NOT NULL CONSTRAINT DF_TJOBQUEUE_MAXRETRIES DEFAULT 3, RETRYATTEMPT INT NOT NULL CONSTRAINT DF_TJOBQUEUE_RETRYATTEMPT DEFAULT 0, NEXTATTEMPTAFTER DATETIME NULL, PROCESSINGSTARTED DATETIME NULL, ERRORDETAILS NVARCHAR(MAX) NULL, MESSAGEID NVARCHAR(200) NOT NULL, CORRELATIONID NVARCHAR(100) NULL, SOURCESERVICE NVARCHAR(100) NULL, CREATEDDATE DATETIME NOT NULL CONSTRAINT DF_TJOBQUEUE_CREATEDDATE DEFAULT GETDATE(), UPDATEDDATE DATETIME NULL, CONSTRAINT PK_TJOBQUEUE PRIMARY KEY (QUEUEID), CONSTRAINT UK_TJOBQUEUE_MESSAGEID UNIQUE (MESSAGEID, TENANTID), CONSTRAINT CK_TJOBQUEUE_STATUS CHECK (STATUS IN ('PENDING','PROCESSING','COMPLETED','FAILED','DLQ','CANCELLED')) ); -- Hot path: claim-batch poll (CLAIM_BATCH query) — READPAST skips locked rows CREATE INDEX IX_TJOBQUEUE_PROCESS ON TJOBQUEUE (STATUS, PRIORITY DESC, SCHEDULEDFOR) WHERE STATUS IN ('PENDING', 'FAILED'); CREATE INDEX IX_TJOBQUEUE_TENANT_STATUS ON TJOBQUEUE (TENANTID, STATUS, CREATEDDATE DESC); END GO -- ============================================================================= -- 5. QRTZ_* — Quartz.NET 3.x SQL Server persistent job store tables -- Source: https://github.com/quartznet/quartznet/blob/main/database/tables/tables_sqlServer.sql -- Run once on the database pointed to by ConnectionStrings:QuartzStore in appsettings.json. -- All tables are prefixed QRTZ_ and are safe to run in the application database. -- ============================================================================= IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'QRTZ_JOB_DETAILS') BEGIN CREATE TABLE [dbo].[QRTZ_JOB_DETAILS] ( [SCHED_NAME] NVARCHAR(120) NOT NULL, [JOB_NAME] NVARCHAR(150) NOT NULL, [JOB_GROUP] NVARCHAR(150) NOT NULL, [DESCRIPTION] NVARCHAR(250) NULL, [JOB_CLASS_NAME] NVARCHAR(250) NOT NULL, [IS_DURABLE] BIT NOT NULL, [IS_NONCONCURRENT] BIT NOT NULL, [IS_UPDATE_DATA] BIT NOT NULL, [REQUESTS_RECOVERY] BIT NOT NULL, [JOB_DATA] VARBINARY(MAX) NULL, CONSTRAINT PK_QRTZ_JOB_DETAILS PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP) ); END GO IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'QRTZ_TRIGGERS') BEGIN CREATE TABLE [dbo].[QRTZ_TRIGGERS] ( [SCHED_NAME] NVARCHAR(120) NOT NULL, [TRIGGER_NAME] NVARCHAR(150) NOT NULL, [TRIGGER_GROUP] NVARCHAR(150) NOT NULL, [JOB_NAME] NVARCHAR(150) NOT NULL, [JOB_GROUP] NVARCHAR(150) NOT NULL, [DESCRIPTION] NVARCHAR(250) NULL, [NEXT_FIRE_TIME] BIGINT NULL, [PREV_FIRE_TIME] BIGINT NULL, [PRIORITY] INT NULL, [TRIGGER_STATE] NVARCHAR(16) NOT NULL, [TRIGGER_TYPE] NVARCHAR(8) NOT NULL, [START_TIME] BIGINT NOT NULL, [END_TIME] BIGINT NULL, [CALENDAR_NAME] NVARCHAR(200) NULL, [MISFIRE_INSTR] SMALLINT NULL, [JOB_DATA] VARBINARY(MAX) NULL, CONSTRAINT PK_QRTZ_TRIGGERS PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), CONSTRAINT FK_QRTZ_TRIGGERS_JOB FOREIGN KEY (SCHED_NAME, JOB_NAME, JOB_GROUP) REFERENCES [dbo].[QRTZ_JOB_DETAILS] (SCHED_NAME, JOB_NAME, JOB_GROUP) ); END GO IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'QRTZ_SIMPLE_TRIGGERS') BEGIN CREATE TABLE [dbo].[QRTZ_SIMPLE_TRIGGERS] ( [SCHED_NAME] NVARCHAR(120) NOT NULL, [TRIGGER_NAME] NVARCHAR(150) NOT NULL, [TRIGGER_GROUP] NVARCHAR(150) NOT NULL, [REPEAT_COUNT] BIGINT NOT NULL, [REPEAT_INTERVAL] BIGINT NOT NULL, [TIMES_TRIGGERED] BIGINT NOT NULL, CONSTRAINT PK_QRTZ_SIMPLE_TRIGGERS PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), CONSTRAINT FK_QRTZ_SIMPLE_TRIGGERS FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) REFERENCES [dbo].[QRTZ_TRIGGERS] (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) ); END GO IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'QRTZ_SIMPROP_TRIGGERS') BEGIN CREATE TABLE [dbo].[QRTZ_SIMPROP_TRIGGERS] ( [SCHED_NAME] NVARCHAR(120) NOT NULL, [TRIGGER_NAME] NVARCHAR(150) NOT NULL, [TRIGGER_GROUP] NVARCHAR(150) NOT NULL, [STR_PROP_1] NVARCHAR(512) NULL, [STR_PROP_2] NVARCHAR(512) NULL, [STR_PROP_3] NVARCHAR(512) NULL, [INT_PROP_1] INT NULL, [INT_PROP_2] INT NULL, [LONG_PROP_1] BIGINT NULL, [LONG_PROP_2] BIGINT NULL, [DEC_PROP_1] NUMERIC(13, 4) NULL, [DEC_PROP_2] NUMERIC(13, 4) NULL, [BOOL_PROP_1] BIT NULL, [BOOL_PROP_2] BIT NULL, [TIME_ZONE_ID] NVARCHAR(80) NULL, CONSTRAINT PK_QRTZ_SIMPROP_TRIGGERS PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), CONSTRAINT FK_QRTZ_SIMPROP_TRIGGERS FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) REFERENCES [dbo].[QRTZ_TRIGGERS] (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) ); END GO IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'QRTZ_CRON_TRIGGERS') BEGIN CREATE TABLE [dbo].[QRTZ_CRON_TRIGGERS] ( [SCHED_NAME] NVARCHAR(120) NOT NULL, [TRIGGER_NAME] NVARCHAR(150) NOT NULL, [TRIGGER_GROUP] NVARCHAR(150) NOT NULL, [CRON_EXPRESSION] NVARCHAR(120) NOT NULL, [TIME_ZONE_ID] NVARCHAR(80) NULL, CONSTRAINT PK_QRTZ_CRON_TRIGGERS PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), CONSTRAINT FK_QRTZ_CRON_TRIGGERS FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) REFERENCES [dbo].[QRTZ_TRIGGERS] (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) ); END GO IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'QRTZ_BLOB_TRIGGERS') BEGIN CREATE TABLE [dbo].[QRTZ_BLOB_TRIGGERS] ( [SCHED_NAME] NVARCHAR(120) NOT NULL, [TRIGGER_NAME] NVARCHAR(150) NOT NULL, [TRIGGER_GROUP] NVARCHAR(150) NOT NULL, [BLOB_DATA] VARBINARY(MAX) NULL, CONSTRAINT PK_QRTZ_BLOB_TRIGGERS PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), CONSTRAINT FK_QRTZ_BLOB_TRIGGERS FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) REFERENCES [dbo].[QRTZ_TRIGGERS] (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) ); END GO IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'QRTZ_CALENDARS') BEGIN CREATE TABLE [dbo].[QRTZ_CALENDARS] ( [SCHED_NAME] NVARCHAR(120) NOT NULL, [CALENDAR_NAME] NVARCHAR(200) NOT NULL, [CALENDAR] VARBINARY(MAX) NOT NULL, CONSTRAINT PK_QRTZ_CALENDARS PRIMARY KEY (SCHED_NAME, CALENDAR_NAME) ); END GO IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'QRTZ_PAUSED_TRIGGER_GRPS') BEGIN CREATE TABLE [dbo].[QRTZ_PAUSED_TRIGGER_GRPS] ( [SCHED_NAME] NVARCHAR(120) NOT NULL, [TRIGGER_GROUP] NVARCHAR(150) NOT NULL, CONSTRAINT PK_QRTZ_PAUSED_TRIGGER_GRPS PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP) ); END GO IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'QRTZ_FIRED_TRIGGERS') BEGIN CREATE TABLE [dbo].[QRTZ_FIRED_TRIGGERS] ( [SCHED_NAME] NVARCHAR(120) NOT NULL, [ENTRY_ID] NVARCHAR(140) NOT NULL, [TRIGGER_NAME] NVARCHAR(150) NOT NULL, [TRIGGER_GROUP] NVARCHAR(150) NOT NULL, [INSTANCE_NAME] NVARCHAR(200) NOT NULL, [FIRED_TIME] BIGINT NOT NULL, [SCHED_TIME] BIGINT NOT NULL, [PRIORITY] INT NOT NULL, [STATE] NVARCHAR(16) NOT NULL, [JOB_NAME] NVARCHAR(150) NULL, [JOB_GROUP] NVARCHAR(150) NULL, [IS_NONCONCURRENT] BIT NULL, [REQUESTS_RECOVERY] BIT NULL, CONSTRAINT PK_QRTZ_FIRED_TRIGGERS PRIMARY KEY (SCHED_NAME, ENTRY_ID) ); CREATE INDEX IX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME); CREATE INDEX IX_QRTZ_FT_INST_JOB_REQ ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME, REQUESTS_RECOVERY); CREATE INDEX IX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP); CREATE INDEX IX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_GROUP); END GO IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'QRTZ_SCHEDULER_STATE') BEGIN CREATE TABLE [dbo].[QRTZ_SCHEDULER_STATE] ( [SCHED_NAME] NVARCHAR(120) NOT NULL, [INSTANCE_NAME] NVARCHAR(200) NOT NULL, [LAST_CHECKIN_TIME] BIGINT NOT NULL, [CHECKIN_INTERVAL] BIGINT NOT NULL, CONSTRAINT PK_QRTZ_SCHEDULER_STATE PRIMARY KEY (SCHED_NAME, INSTANCE_NAME) ); END GO IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'QRTZ_LOCKS') BEGIN CREATE TABLE [dbo].[QRTZ_LOCKS] ( [SCHED_NAME] NVARCHAR(120) NOT NULL, [LOCK_NAME] NVARCHAR(40) NOT NULL, CONSTRAINT PK_QRTZ_LOCKS PRIMARY KEY (SCHED_NAME, LOCK_NAME) ); -- Quartz requires these two seed rows for its internal locking mechanism INSERT INTO QRTZ_LOCKS (SCHED_NAME, LOCK_NAME) VALUES ('GB5Scheduler', 'TRIGGER_ACCESS'); INSERT INTO QRTZ_LOCKS (SCHED_NAME, LOCK_NAME) VALUES ('GB5Scheduler', 'JOB_ACCESS'); END GO -- ============================================================================= -- Required Quartz trigger indexes (separate from the table creation above) -- ============================================================================= IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_QRTZ_T_J' AND object_id = OBJECT_ID('QRTZ_TRIGGERS')) BEGIN CREATE INDEX IX_QRTZ_T_J ON QRTZ_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP); CREATE INDEX IX_QRTZ_T_JG ON QRTZ_TRIGGERS (SCHED_NAME, JOB_GROUP); CREATE INDEX IX_QRTZ_T_C ON QRTZ_TRIGGERS (SCHED_NAME, CALENDAR_NAME); CREATE INDEX IX_QRTZ_T_G ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP); CREATE INDEX IX_QRTZ_T_STATE ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE); CREATE INDEX IX_QRTZ_T_NF ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME); CREATE INDEX IX_QRTZ_T_NFT ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_STATE); CREATE INDEX IX_QRTZ_T_NFTG ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_GROUP, TRIGGER_STATE); END GO -- ============================================================================= -- END OF MIGRATION 20260726_JobEngine_Schema (SQL Server variant) -- =============================================================================