diff --git a/docs/migration/ravendb-to-sql-migration-instructions.md b/docs/migration/ravendb-to-sql-migration-instructions.md
index 91b421c143..d229d03d15 100644
--- a/docs/migration/ravendb-to-sql-migration-instructions.md
+++ b/docs/migration/ravendb-to-sql-migration-instructions.md
@@ -1,9 +1,9 @@
# Migrating from RavenDB to SQL Server or PostgreSQL
-This page covers what you can run today. How the migration works, and what is planned, is in the [migration overview](ravendb-to-sql-migration-overview.md) and the [system design diagram](migration-system-design-diagram.png).
+This page covers reporting on the RavenDB source before you migrate. How the migration works is in the [migration overview](ravendb-to-sql-migration-overview.md) and the [system design diagram](migration-system-design-diagram.png).
> [!NOTE]
-> Copying data is not built yet. The one migration command available is the source report. It sends RavenDB only reads, but loading a database lets RavenDB's own expiration, its automatic deletion of documents past their retention date, run against it. If you are keeping the RavenDB database as a fallback, back it up before you run the report, as [Goals](ravendb-to-sql-migration-overview.md#goals) explains.
+> The source report sends RavenDB only reads, but loading a database lets RavenDB's own expiration, its automatic deletion of documents past their retention date, run against it. If you are keeping the RavenDB database as a fallback, back it up before you run the report, as [Goals](ravendb-to-sql-migration-overview.md#goals) explains.
## Before you start
@@ -42,10 +42,11 @@ The report prints the RavenDB server version, whether the source is embedded or
## If the report fails
-The error names the setting to fix:
+The error says what to fix:
- **"has no database named ..."**: the database name setting it quotes is wrong.
- **"refused its client certificate access ..."**: grant that certificate Read access to the database, or supply a certificate that has it.
+- **"could not start a server for the embedded database ..."**: a ServiceControl instance is still running against that data directory and holds it. Stop the instance, run the report, then start it again.
## Not available yet
diff --git a/docs/migration/ravendb-to-sql-migration-overview.md b/docs/migration/ravendb-to-sql-migration-overview.md
index 9c167c3c0b..b1277bd1e8 100644
--- a/docs/migration/ravendb-to-sql-migration-overview.md
+++ b/docs/migration/ravendb-to-sql-migration-overview.md
@@ -1,8 +1,8 @@
# Moving data from RavenDB to SQL
-## Problem
+## Purpose
-A customer can already point ServiceControl at SQL Server or PostgreSQL. They cannot bring their existing data with them.
+The migration moves an error instance's data from RavenDB to SQL Server or PostgreSQL, so a customer can switch persisters and keep their data.
This covers the error instance only. The audit instance has no SQL persister, so a customer who finishes this migration still runs RavenDB for audit.
@@ -16,7 +16,7 @@ This covers the error instance only. The audit instance has no SQL persister, so
- **Minimal downtime**. Only the required data copies with ServiceControl closed. Optional data copies in the background while it serves traffic.
- **All three RavenDB sources are supported**. Embedded, a container, or RavenDB Cloud, on one code path rather than three.
-- **No writes through the client**. The copier never changes the source, but RavenDB's own expiration does: the primary database already has it configured, and the sweep keeps deleting failed messages and event log items throughout the migration and for as long afterwards as the instance is left running. The old database is a fallback that degrades from the moment you start.
+- **No writes through the client**. The copier never changes the source, but RavenDB's own expiration does: the primary database has it configured, and the sweep keeps deleting failed messages and event log items throughout the migration and for as long afterwards as the instance is left running. The old database is a fallback that degrades from the moment you start.
- **Abandonable up to a known point, and only up to that point**. While ServiceControl is closed the copy can be thrown away at no cost, because nothing but the copier has written to SQL and the migration has written nothing to RavenDB: see [the one point you can go back](#the-one-point-you-can-go-back). Once the host opens there is no way back at all.
- **No duplicates and no gaps**. Rows and the resume cursor, a marker of the last row copied, commit in one transaction, so a crash needs no reconciliation.
- **Every identifier anything depends on is carried across**. The event log, historic retry operations and pending integration events are renumbered, because nothing references their keys.
@@ -26,13 +26,13 @@ This covers the error instance only. The audit instance has no SQL persister, so
- **Known before it starts, visible while it runs**. A dry run reports what will move and how long ServiceControl is closed, and every category transition is reported as it happens.
- **Use existing functionality where possible**. Progress goes through custom checks and the activity feed, so no new client or screen is needed.
-## Deliberately not built, and not currently planned
+## Non-goals
- **Zero downtime.** The required data is copied with ServiceControl closed, so there is a real, if short, outage.
- **Reversible once ServiceControl opens.** Nothing copies SQL rows back to RavenDB, so once the host has served traffic there is no rollback of any kind.
- **Steerable while running.** No pause or resume, and no abort command. Going back during the closed window means stopping and reconfiguring, and changing anything else means editing configuration and restarting.
- **A general-purpose migration tool.** The source is always RavenDB and the target is always a ServiceControl EF Core persister, both at versions this build can read.
-- **Custom migration UI via ServicePulse.** Custom checks and the event log will be used for progress reporting, but migration configuration and migration engine control will not be available via the UI.
+- **Custom migration UI via ServicePulse.** Custom checks and the event log report progress, and migration configuration and control are not available in the UI.
## Supported migration scenarios
@@ -57,7 +57,7 @@ The copier runs inside the ServiceControl host, so every row and every message b
- The ServiceControl host needs network access to the RavenDB source, the SQL target and the body store simultaneously
- Both RavenDB databases, primary and throughput, on one server or cluster
- A SQL Server target must have Full-Text Search installed. `--setup` checks `SERVERPROPERTY('IsFullTextInstalled')` and fails if it is absent, because message search is not optional. A stock SQL Server container image does not include it. PostgreSQL needs nothing extra, since its index is a GIN over `to_tsvector`
-- A managed target's transient failures are already survivable: retry on failure is on by default and there is no setting to turn it off
+- A managed target's transient failures are survivable: retry on failure is on by default and there is no setting to turn it off
**Not supported:**
@@ -75,12 +75,12 @@ The copier runs inside the ServiceControl host, so every row and every message b
4. Run the [dry run](#dry-run). It reports what it resolved as a source, what each category holds, and an estimate of how long ServiceControl will be closed. Read [what the dry run reports](#dry-run) before booking an outage around its estimate.
5. Start ServiceControl (`MigrationMode=true`).
6. Every check runs before a single row moves. If one fails the host does not start and names which, having copied nothing, so a wrong database name or unconfigured body storage costs a restart rather than a half-finished migration.
-7. The copying of [required data](#required) starts, with ServiceControl still closed. How long it takes depends on how many unresolved failures you have and how busy the last 7 days were, and the [dry run](#dry-run) gives you an estimate. If a required category halts, the host stays closed until you fix the cause and restart, or abandon that category.
-8. ServiceControl opens, and whatever [optional data](#optional) they asked for is copied in the background while the instance runs normally. They can watch it from ServicePulse custom checks and events, but not steer it.
+7. The copying of [required data](#required) starts, with ServiceControl still closed: the copy runs inside that same start, before the API begins listening and before any background service runs. How long it takes depends on how many unresolved failures you have and how busy the last 7 days were, and the [dry run](#dry-run) gives you an estimate. If a required category halts, the host stays closed until you fix the cause and restart, or abandon that category.
+8. ServiceControl opens by itself the moment the required copy finishes, with no second restart to perform, and whatever [optional data](#optional) you asked for is copied in the background while the instance runs normally. You can watch it from ServicePulse custom checks and events, but not steer it.
9. You run the verification pass once the background job has completed, which reports row counts on both sides category by category, accounting for deliberate skips so a difference is explained rather than reported as a fault, then set `MigrationMode=false` and restart. Counts can differ in both directions without anything being wrong. SQL can hold more rows, because RavenDB keeps expiring rows the copier already took. SQL can also hold fewer, because once ServiceControl opens it sends pending integration events, removes group comments whose group has no failed messages left, and removes failed error imports once they are imported again.
10. RavenDB data can be removed.
-- If `MigrationMode=false` is set while a selected category is still incomplete, the host refuses to start and names exactly what is outstanding.
+- If `MigrationMode=false` is set while a selected category is still incomplete, the startup is gated: it refuses and names exactly what is outstanding, or, where the [free abort](#the-one-point-you-can-go-back) is still open, starts with a warning that says so. See [turning migration mode off is a gated startup too](#turning-migration-mode-off-is-a-gated-startup-too).
- A category that ended *complete with errors* counts as complete and does not block, though its skipped count is printed so the loss is stated rather than silent.
- An explicit override exists for a customer who has changed their mind and accepts leaving data behind. It marks the outstanding categories as abandoned, which is a deliberate end state rather than a failure, so the progress check settles and the guard stays armed for any later migration.
- While a selected category is still unfinished, ServiceControl pauses its own clean-up: the retention sweep, the purge API, the heartbeat settings sync and throughput collection. Your SQL database grows until the copy finishes, and these restart on their own once it does.
@@ -96,7 +96,7 @@ flowchart TB
subgraph host["One ServiceControl host process, started with MigrationMode = true"]
direction LR
- raven["RavenDB persister
own AssemblyLoadContext
new read-only lifecycle"]
+ raven["RavenDB persister
own AssemblyLoadContext
read-only lifecycle"]
engine["MigrationEngine
categories, throttle,
dry run, verification"]
target["EF Core persister
SQL Server or PostgreSQL
own AssemblyLoadContext"]
raven -->|"IMigrationSource"| engine
@@ -104,7 +104,7 @@ flowchart TB
end
old[("Old RavenDB
read only, never written to")]
- sql[("SQL Server or PostgreSQL
plus a new checkpoint table")]
+ sql[("SQL Server or PostgreSQL
plus the checkpoint table")]
bodies[("Message body store
filesystem, Azure Blob or S3")]
cfg --> host
@@ -114,7 +114,7 @@ flowchart TB
target --> bodies
```
-- **The engine and the host know no store.** They deal in categories, cursors and counts. The source maps a category to what it reads and describes itself as labelled facts, the target maps a category to where it writes and how to count it, and each contributes its own startup checks. RavenDB to SQL is the only pair built, and another pair, such as SQL to RavenDB, would add a source or a target without changing the engine.
+- **The engine and the host know no store.** They deal in categories, cursors and counts. The source maps a category to what it reads and describes itself as labelled facts, the target maps a category to where it writes and how to count it, and each contributes its own startup checks. RavenDB to SQL is the only supported pair, and the engine does not depend on it.
- **The source reads the instance's own RavenDB settings**, so an existing customer sets nothing new. Leave them in place when switching `PersistenceType`.
- **Both persisters load into the same process**, each into its own `AssemblyLoadContext`.
- **The engine references neither assembly.** It knows only `IMigrationSource` and `IMigrationTarget`, and treats the resume cursor as an opaque value it passes from one to the other, so it can be tested against fakes on either side.
@@ -123,18 +123,23 @@ flowchart TB
```mermaid
flowchart TB
- A["Restart with MigrationMode = true"] --> B["Open the SQL target, exactly as today"]
- B --> C["Open the old RavenDB, read only"]
- C --> D{"All checks pass?"}
- D -->|"No"| E["Host does not start.
Says which check failed.
Nothing has been copied."]
- D -->|"Yes"| F["Copy what cannot be recreated.
How long depends on unresolved failures
and the last 7 days of events.
ServiceControl still closed."]
- F -->|"A required category halts"| M["Host stays closed.
Fix the cause and restart,
or abandon the category."]
- F -->|"Required data copied"| G["ServiceControl opens.
New failed messages go straight to SQL."]
- G --> H["Copy the selected history in the background,
with a fixed pause between batches"]
- H --> I["Verify row counts on both sides,
category by category"]
- I --> J{"MigrationMode = false,
everything complete?"}
- J -->|"No"| K["Host does not start.
Names what is outstanding.
An override exists."]
- J -->|"Yes"| L["RavenDB is never opened again"]
+ A["ServiceControl starts on SQL"] --> M{"MigrationMode?"}
+
+ M -->|"On"| B["Open the SQL target
and the RavenDB source, read only"]
+ B --> D{"All checks pass?"}
+ D -->|"No"| E["Host does not start.
Names the failed check.
Nothing is copied."]
+ D -->|"Yes"| F["Copy the required categories.
The API is not listening."]
+ F -->|"A required category halts"| T["Host stays closed.
Fix the cause and restart,
or abandon the category."]
+ F -->|"Required categories settled"| G["ServiceControl opens.
New failed messages go to SQL."]
+ G --> H["Copy the selected optional categories
in the background"]
+
+ M -->|"Off"| N{"Any category
outstanding?"}
+ N -->|"No"| L["ServiceControl opens.
RavenDB is not opened."]
+ N -->|"Yes"| O{"Override set?"}
+ O -->|"Yes"| P["Mark each outstanding category abandoned,
log what it leaves behind,
and open."]
+ O -->|"No"| Q{"Has this instance
ever opened on SQL?"}
+ Q -->|"No"| R["Open with a warning: point PersistenceType
back at RavenDB, or carry on
and lose the way back."]
+ Q -->|"Yes"| S["Host does not start.
Names every outstanding category,
its counts, and every route out."]
```
**Checked before a single row moves:**
@@ -147,6 +152,20 @@ flowchart TB
- The selected categories are valid
- `RetryHistoryDepth` is greater than zero. At zero or less, the first completed retry after the migration deletes the entire copied retry history, and no row count would ever show it
+### Turning migration mode off is a gated startup too
+
+*The right-hand branch above is the half a customer meets last and expects least, so it is worth reading before the migration starts rather than at the end of one.*
+
+Every startup on a SQL Server or PostgreSQL instance looks at the checkpoint table before ServiceControl opens, whether `MigrationMode` is on or off. That is what stops a migration ending by accident, and it costs nothing on an instance with nothing outstanding: one that has never migrated holds no checkpoint rows, and one whose categories all settled has none left open. Both start normally. A RavenDB instance never reaches the gate.
+
+With `MigrationMode` off and at least one category still outstanding, one of three things happens, and each is said out loud at startup rather than discovered weeks later:
+
+- **The override is set.** Every outstanding category is recorded as abandoned, with its copied and skipped counts left as they are, and the host starts. Each one is logged saying what state it was in, how much it had copied, and that whatever it had not copied stays only in RavenDB. Abandoning is final: selecting that category in a later migration does not copy it again.
+- **The override is not set, and this instance has never opened on SQL.** This is the [free abort](#the-one-point-you-can-go-back), so the host starts and warns rather than refusing. The warning names the two moves: stop now and point `PersistenceType` back at RavenDB, which discards the partial copy and costs nothing else, or carry on, which opens ServiceControl on a partly copied database and ends the free abort. It deliberately does not mention the override, because at that moment nothing is lost yet.
+- **The override is not set, and this instance has already opened on SQL.** The host does not start. The error names every outstanding category, its state, its copied and skipped counts and its last error, and then the three routes out: restart with `MigrationMode=true` to let the copy finish or to resume a halted category once its cause is fixed, set the override to abandon what is outstanding and start without it, or, if RavenDB is already gone, abandon, because that is the only exit left.
+
+**Which is why the source stays until verification passes.** A customer who decommissions RavenDB while a category is outstanding has both doors shut: `MigrationMode=true` cannot start, because it opens the source before it copies anything, and `MigrationMode=false` refuses. Abandoning is then the only way to start the instance, and it is a real loss whose size is the counts in that message.
+
## Data to be migrated (Categories)
### Required
@@ -215,17 +234,17 @@ That window closes the moment ServiceControl opens. From then on new failed mess
## Reading from RavenDB
-- A third RavenDB lifecycle opens the source: connect, check the version, stop. It never calls `DatabaseSetup.Execute`.
+- A dedicated read-only RavenDB lifecycle opens the source: connect, check the version, stop. It never calls `DatabaseSetup.Execute`.
- Both source databases must be on the same server or cluster (`LicensingDataStore.cs:35`).
-- The source has to be at a ServiceControl version this build can read, and nothing in RavenDB records one today. The only version check that exists compares the RavenDB server version to the RavenDB client version, and runs only for an external source. So a marker is stamped into the database on upgrade, and a source without one, or one from a newer major version, is refused by name rather than misread.
+- The source has to be at a ServiceControl version this build can read. ServiceControl stamps a version marker into the database on upgrade, because the RavenDB server version says nothing about which ServiceControl version wrote the data. A source without a marker, or one from a newer major version, is refused by name rather than misread.
- Duration scales with distance to the source. The copier already holds the document from the stream, so each body costs **one** round trip rather than two, but it is one per message and they are not batched. Egress out of RavenDB Cloud is billed to the customer. See [batching and throttling](#batching-and-throttling).
## Writing to SQL
-- A whole `FailedMessage` is written with its stored status intact. No existing caller does that, though the dialect upsert already accepts a status, so the gap is smaller than it looks.
+- A whole `FailedMessage` is written with its stored status intact.
- `UniqueMessageId` keeps its value, but converts type: the source holds a string and the target column is a `uniqueidentifier`. It is the primary key, the ServicePulse URL, the retry correlation key and the body lookup key at once.
- `StatusChangedAt` is reconstructed from `@expires` for resolved and archived messages, which is the only place RavenDB sets it. Unresolved and retry-issued messages normally have no `@expires` (see [the exception](#what-does-not-come-across) for messages from version 6.18 or earlier), so the copier uses the newest processing attempt's timestamp. The column is `NOT NULL`, so it cannot be left empty, but the value is harmless for those two: the retention sweep only considers resolved and archived rows, so an unresolved message never ages out whatever is written here.
-- Message bodies go through `IBodyStoragePersistence`, which owns the compression threshold and the choice of filesystem, Azure Blob or S3. The separate 102,400-byte inline threshold is not there: it lives on the ingestion path, so the copier has to apply it rather than inherit it.
+- Message bodies go through `IBodyStoragePersistence`, which owns the compression threshold and the choice of filesystem, Azure Blob or S3. The copier applies the 102,400-byte inline threshold itself, because that threshold lives on the ingestion path rather than in `IBodyStoragePersistence`.
- Throughput rows are written directly rather than through the collector, and the write sets each day's count rather than adding to it. Throughput is a required category, so it copies while ServiceControl is closed, before any collector has written to SQL. Setting is what makes the category safe to resume after a crash, where adding would double-count. Copying the rows is also what stops the audit and broker collectors re-gathering the same days when the host opens, because `LastCollectedDate` is derived from the newest throughput row rather than stored (`LicensingDataStore.cs:45`). The checkpoint is what stops a second pass overwriting days the collectors have written since.
- Identifiers narrow on the way across, and the dry run counts every kind. What narrows, merges or cannot be stored at all is in [what does not come across](#what-does-not-come-across).
@@ -380,5 +399,5 @@ A containerised instance runs all three as a one-off `docker run` of the same im
## Out of scope
-- The audit instance, which has no EF Core persister at all, so a customer who finishes this migration is still running RavenDB for audit. This is stated up front under [Problem](#problem), because it changes whether the migration is worth doing at all
+- The audit instance, which has no EF Core persister at all, so a customer who finishes this migration is still running RavenDB for audit. This is stated up front under [Purpose](#purpose), because it changes whether the migration is worth doing at all
- The monitoring instance, which keeps its data in memory, so there is nothing to move
diff --git a/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/20260916125150_AddMigrationCheckpoints.Designer.cs b/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/20260916125150_AddMigrationCheckpoints.Designer.cs
new file mode 100644
index 0000000000..a8a3dd5b3b
--- /dev/null
+++ b/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/20260916125150_AddMigrationCheckpoints.Designer.cs
@@ -0,0 +1,976 @@
+//
+using System;
+using System.Collections.Generic;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using ServiceControl.Persistence.EFCore.PostgreSql;
+
+#nullable disable
+
+namespace ServiceControl.Persistence.EFCore.PostgreSql.Migrations
+{
+ [DbContext(typeof(PostgreSqlServiceControlDbContext))]
+ [Migration("20260916125150_AddMigrationCheckpoints")]
+ partial class AddMigrationCheckpoints
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.ArchiveOperationEntity", b =>
+ {
+ b.Property("RequestId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("request_id");
+
+ b.Property("ArchiveType")
+ .HasColumnType("integer")
+ .HasColumnName("archive_type");
+
+ b.Property("OperationType")
+ .HasColumnType("integer")
+ .HasColumnName("operation_type");
+
+ b.Property("CurrentBatch")
+ .HasColumnType("integer")
+ .HasColumnName("current_batch");
+
+ b.Property("GroupName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("group_name");
+
+ b.Property("InitiatedById")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("initiated_by_id");
+
+ b.Property("InitiatedByName")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("initiated_by_name");
+
+ b.Property("NumberOfBatches")
+ .HasColumnType("integer")
+ .HasColumnName("number_of_batches");
+
+ b.Property("NumberOfMessagesProcessed")
+ .HasColumnType("integer")
+ .HasColumnName("number_of_messages_processed");
+
+ b.Property("OperationId")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("operation_id");
+
+ b.Property("Started")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("started");
+
+ b.Property("TotalNumberOfMessages")
+ .HasColumnType("integer")
+ .HasColumnName("total_number_of_messages");
+
+ b.HasKey("RequestId", "ArchiveType", "OperationType")
+ .HasName("pk_archive_operations");
+
+ b.HasIndex("Started")
+ .HasDatabaseName("ix_archive_operations_started");
+
+ b.ToTable("archive_operations", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.CustomCheckEntity", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Category")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("category");
+
+ b.Property("CustomCheckId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("custom_check_id");
+
+ b.Property("FailureReason")
+ .HasColumnType("text")
+ .HasColumnName("failure_reason");
+
+ b.Property("OriginatingEndpointHost")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("originating_endpoint_host");
+
+ b.Property("OriginatingEndpointHostId")
+ .HasColumnType("uuid")
+ .HasColumnName("originating_endpoint_host_id");
+
+ b.Property("OriginatingEndpointName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("originating_endpoint_name");
+
+ b.Property("ReportedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("reported_at");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasColumnName("status");
+
+ b.HasKey("Id")
+ .HasName("pk_custom_checks");
+
+ b.HasIndex("ReportedAt")
+ .HasDatabaseName("ix_custom_checks_reported_at");
+
+ b.HasIndex("Status", "ReportedAt")
+ .HasDatabaseName("ix_custom_checks_status_reported_at");
+
+ b.ToTable("custom_checks", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.EndpointSettingsEntity", b =>
+ {
+ b.Property("Name")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("name");
+
+ b.Property("TrackInstances")
+ .HasColumnType("boolean")
+ .HasColumnName("track_instances");
+
+ b.HasKey("Name")
+ .HasName("pk_endpoint_settings");
+
+ b.ToTable("endpoint_settings", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.EventLogItemEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Category")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("category");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property("EventType")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("event_type");
+
+ b.Property("RaisedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("raised_at");
+
+ b.PrimitiveCollection>("RelatedTo")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("related_to");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.HasKey("Id")
+ .HasName("pk_event_log_items");
+
+ b.HasIndex("RaisedAt", "Id")
+ .IsDescending()
+ .HasDatabaseName("ix_event_log_items_raised_at_id");
+
+ b.ToTable("event_log_items", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.ExternalIntegrationDispatchRequestEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("DispatchContextJson")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("dispatch_context_json");
+
+ b.Property("DispatchContextTypeName")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("dispatch_context_type_name");
+
+ b.HasKey("Id")
+ .HasName("pk_external_integration_dispatch_requests");
+
+ b.ToTable("external_integration_dispatch_requests", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedErrorImportEntity", b =>
+ {
+ b.Property("UniqueMessageId")
+ .HasColumnType("uuid")
+ .HasColumnName("unique_message_id");
+
+ b.Property("Body")
+ .IsRequired()
+ .HasColumnType("bytea")
+ .HasColumnName("body");
+
+ b.Property("BodyStoredExternally")
+ .HasColumnType("boolean")
+ .HasColumnName("body_stored_externally");
+
+ b.Property("ExceptionInfo")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("exception_info");
+
+ b.Property("FailedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("failed_at");
+
+ b.Property("HeadersJson")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("headers_json");
+
+ b.Property("MessageId")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("message_id");
+
+ b.HasKey("UniqueMessageId")
+ .HasName("pk_failed_error_imports");
+
+ b.HasIndex("FailedAt")
+ .HasDatabaseName("ix_failed_error_imports_failed_at");
+
+ b.ToTable("failed_error_imports", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageEditEntity", b =>
+ {
+ b.Property("UniqueMessageId")
+ .HasColumnType("uuid")
+ .HasColumnName("unique_message_id");
+
+ b.Property("EditId")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("edit_id");
+
+ b.HasKey("UniqueMessageId")
+ .HasName("pk_failed_message_edits");
+
+ b.HasIndex("EditId")
+ .HasDatabaseName("ix_failed_message_edits_edit_id");
+
+ b.ToTable("failed_message_edits", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageEntity", b =>
+ {
+ b.Property("UniqueMessageId")
+ .HasColumnType("uuid")
+ .HasColumnName("unique_message_id");
+
+ b.Property("BodyContentType")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("body_content_type");
+
+ b.Property("BodySize")
+ .HasColumnType("integer")
+ .HasColumnName("body_size");
+
+ b.Property("BodyStoredExternally")
+ .HasColumnType("boolean")
+ .HasColumnName("body_stored_externally");
+
+ b.Property("BodyText")
+ .HasColumnType("text")
+ .HasColumnName("body_text");
+
+ b.Property("ConversationId")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("conversation_id");
+
+ b.Property("ExceptionMessage")
+ .HasColumnType("text")
+ .HasColumnName("exception_message");
+
+ b.Property("ExceptionType")
+ .HasColumnType("text")
+ .HasColumnName("exception_type");
+
+ b.Property("FailingEndpointAddress")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("failing_endpoint_address");
+
+ b.Property("FirstTimeOfFailure")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("first_time_of_failure");
+
+ b.Property("HeadersJson")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("headers_json");
+
+ b.Property("IsSystemMessage")
+ .HasColumnType("boolean")
+ .HasColumnName("is_system_message");
+
+ b.Property("LastAttemptedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_attempted_at");
+
+ b.Property("LastModified")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_modified");
+
+ b.Property("LastTimeOfFailure")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_time_of_failure");
+
+ b.Property("MessageId")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("message_id");
+
+ b.Property("MessageType")
+ .HasColumnType("text")
+ .HasColumnName("message_type");
+
+ b.Property("NumberOfProcessingAttempts")
+ .HasColumnType("integer")
+ .HasColumnName("number_of_processing_attempts");
+
+ b.Property("ReceivingEndpointHost")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("receiving_endpoint_host");
+
+ b.Property("ReceivingEndpointHostId")
+ .HasColumnType("uuid")
+ .HasColumnName("receiving_endpoint_host_id");
+
+ b.Property("ReceivingEndpointName")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("receiving_endpoint_name");
+
+ b.Property("SendingEndpointHost")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("sending_endpoint_host");
+
+ b.Property("SendingEndpointHostId")
+ .HasColumnType("uuid")
+ .HasColumnName("sending_endpoint_host_id");
+
+ b.Property("SendingEndpointName")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("sending_endpoint_name");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasColumnName("status");
+
+ b.Property("StatusChangedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("status_changed_at");
+
+ b.Property("TimeSent")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("time_sent");
+
+ b.HasKey("UniqueMessageId")
+ .HasName("pk_failed_messages");
+
+ b.HasIndex("ConversationId")
+ .HasDatabaseName("ix_failed_messages_conversation_id");
+
+ b.HasIndex("FailingEndpointAddress")
+ .HasDatabaseName("ix_failed_messages_failing_endpoint_address");
+
+ b.HasIndex("ReceivingEndpointName")
+ .HasDatabaseName("ix_failed_messages_receiving_endpoint_name");
+
+ b.HasIndex("StatusChangedAt")
+ .HasDatabaseName("ix_failed_messages_status_changed_at")
+ .HasFilter("status IN (2, 4)");
+
+ b.HasIndex("TimeSent")
+ .HasDatabaseName("ix_failed_messages_time_sent");
+
+ b.HasIndex("Status", "LastModified")
+ .HasDatabaseName("ix_failed_messages_status_last_modified");
+
+ b.ToTable("failed_messages", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageGroupEntity", b =>
+ {
+ b.Property("FailedMessageUniqueId")
+ .HasColumnType("uuid")
+ .HasColumnName("failed_message_unique_id");
+
+ b.Property("GroupId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("group_id");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)")
+ .HasColumnName("type");
+
+ b.HasKey("FailedMessageUniqueId", "GroupId")
+ .HasName("pk_failed_message_groups");
+
+ b.HasIndex("GroupId")
+ .HasDatabaseName("ix_failed_message_groups_group_id");
+
+ b.HasIndex("Type", "GroupId")
+ .HasDatabaseName("ix_failed_message_groups_type_group_id");
+
+ b.ToTable("failed_message_groups", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageRetryEntity", b =>
+ {
+ b.Property("UniqueMessageId")
+ .HasColumnType("uuid")
+ .HasColumnName("unique_message_id");
+
+ b.Property("RetryBatchId")
+ .HasColumnType("uuid")
+ .HasColumnName("retry_batch_id");
+
+ b.Property("StageAttempts")
+ .HasColumnType("integer")
+ .HasColumnName("stage_attempts");
+
+ b.HasKey("UniqueMessageId")
+ .HasName("pk_failed_message_retries");
+
+ b.HasIndex("RetryBatchId")
+ .HasDatabaseName("ix_failed_message_retries_retry_batch_id");
+
+ b.ToTable("failed_message_retries", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.GroupCommentEntity", b =>
+ {
+ b.Property("GroupId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("group_id");
+
+ b.Property("Comment")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("comment");
+
+ b.HasKey("GroupId")
+ .HasName("pk_group_comments");
+
+ b.ToTable("group_comments", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.HistoricRetryOperationEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CompletionTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("completion_time");
+
+ b.Property("Failed")
+ .HasColumnType("boolean")
+ .HasColumnName("failed");
+
+ b.Property("NumberOfMessagesProcessed")
+ .HasColumnType("integer")
+ .HasColumnName("number_of_messages_processed");
+
+ b.Property("Originator")
+ .HasColumnType("text")
+ .HasColumnName("originator");
+
+ b.Property("RequestId")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("character varying(400)")
+ .HasColumnName("request_id");
+
+ b.Property("RetryType")
+ .HasColumnType("integer")
+ .HasColumnName("retry_type");
+
+ b.Property("StartTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start_time");
+
+ b.HasKey("Id")
+ .HasName("pk_historic_retry_operations");
+
+ b.HasIndex("CompletionTime", "Id")
+ .IsDescending()
+ .HasDatabaseName("ix_historic_retry_operations_completion_time_id");
+
+ b.ToTable("historic_retry_operations", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.KnownEndpointEntity", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Host")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("host");
+
+ b.Property("HostId")
+ .HasColumnType("uuid")
+ .HasColumnName("host_id");
+
+ b.Property("Monitored")
+ .HasColumnType("boolean")
+ .HasColumnName("monitored");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("name");
+
+ b.HasKey("Id")
+ .HasName("pk_known_endpoints");
+
+ b.ToTable("known_endpoints", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.LicensingEndpointEntity", b =>
+ {
+ b.Property("NormalizedName")
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)")
+ .HasColumnName("normalized_name");
+
+ b.Property("ThroughputSource")
+ .HasColumnType("integer")
+ .HasColumnName("throughput_source");
+
+ b.PrimitiveCollection>("EndpointIndicators")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("endpoint_indicators");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)")
+ .HasColumnName("name");
+
+ b.Property("NormalizedSanitizedName")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("normalized_sanitized_name");
+
+ b.Property("SanitizedName")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("sanitized_name");
+
+ b.Property("Scope")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("scope");
+
+ b.Property("UserIndicator")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("user_indicator");
+
+ b.HasKey("NormalizedName", "ThroughputSource")
+ .HasName("pk_licensing_endpoints");
+
+ b.HasIndex("NormalizedSanitizedName")
+ .HasDatabaseName("ix_licensing_endpoints_normalized_sanitized_name");
+
+ b.ToTable("licensing_endpoints", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.LicensingEndpointThroughputEntity", b =>
+ {
+ b.Property("NormalizedName")
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)")
+ .HasColumnName("normalized_name");
+
+ b.Property("ThroughputSource")
+ .HasColumnType("integer")
+ .HasColumnName("throughput_source");
+
+ b.Property("DateUtc")
+ .HasColumnType("date")
+ .HasColumnName("date_utc");
+
+ b.Property("MessageCount")
+ .HasColumnType("bigint")
+ .HasColumnName("message_count");
+
+ b.HasKey("NormalizedName", "ThroughputSource", "DateUtc")
+ .HasName("pk_licensing_endpoint_throughput");
+
+ b.HasIndex("DateUtc")
+ .HasDatabaseName("ix_licensing_endpoint_throughput_date_utc");
+
+ b.ToTable("licensing_endpoint_throughput", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.MessageRedirectEntity", b =>
+ {
+ b.Property("FromPhysicalAddress")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("from_physical_address");
+
+ b.Property("LastModified")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_modified");
+
+ b.Property("ToPhysicalAddress")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("to_physical_address");
+
+ b.HasKey("FromPhysicalAddress")
+ .HasName("pk_message_redirects");
+
+ b.ToTable("message_redirects", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.MigrationCheckpointEntity", b =>
+ {
+ b.Property("CategoryId")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("category_id");
+
+ b.Property("AlreadyPresentCount")
+ .HasColumnType("bigint")
+ .HasColumnName("already_present_count");
+
+ b.Property("CopiedCount")
+ .HasColumnType("bigint")
+ .HasColumnName("copied_count");
+
+ b.Property("Cursor")
+ .HasColumnType("text")
+ .HasColumnName("cursor");
+
+ b.Property("LastError")
+ .HasColumnType("text")
+ .HasColumnName("last_error");
+
+ b.Property("LastProgressAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_progress_at");
+
+ b.Property("SettledAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("settled_at");
+
+ b.Property("SkipReasons")
+ .HasColumnType("text")
+ .HasColumnName("skip_reasons");
+
+ b.Property("SkippedCount")
+ .HasColumnType("bigint")
+ .HasColumnName("skipped_count");
+
+ b.Property("SourceTotal")
+ .HasColumnType("bigint")
+ .HasColumnName("source_total");
+
+ b.Property("StartedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("started_at");
+
+ b.Property("State")
+ .HasColumnType("integer")
+ .HasColumnName("state");
+
+ b.Property("Version")
+ .IsConcurrencyToken()
+ .HasColumnType("bigint")
+ .HasColumnName("version");
+
+ b.HasKey("CategoryId")
+ .HasName("pk_migration_checkpoints");
+
+ b.ToTable("migration_checkpoints", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.RetryBatchEntity", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Classifier")
+ .HasColumnType("text")
+ .HasColumnName("classifier");
+
+ b.Property("Context")
+ .HasColumnType("text")
+ .HasColumnName("context");
+
+ b.Property("InitialBatchSize")
+ .HasColumnType("integer")
+ .HasColumnName("initial_batch_size");
+
+ b.Property("InitiatedById")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("initiated_by_id");
+
+ b.Property("InitiatedByName")
+ .HasColumnType("text")
+ .HasColumnName("initiated_by_name");
+
+ b.Property("Last")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last");
+
+ b.Property("OperationId")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("operation_id");
+
+ b.Property("Originator")
+ .HasColumnType("text")
+ .HasColumnName("originator");
+
+ b.Property("RequestId")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("request_id");
+
+ b.Property("RetrySessionId")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("retry_session_id");
+
+ b.Property("RetryType")
+ .HasColumnType("integer")
+ .HasColumnName("retry_type");
+
+ b.Property("StagingId")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("staging_id");
+
+ b.Property("StartTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start_time");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasColumnName("status");
+
+ b.HasKey("Id")
+ .HasName("pk_retry_batches");
+
+ b.HasIndex("Status", "RetrySessionId")
+ .HasDatabaseName("ix_retry_batches_status_retry_session_id");
+
+ b.ToTable("retry_batches", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.RetryBatchNowForwardingEntity", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ b.Property("RetryBatchId")
+ .HasColumnType("uuid")
+ .HasColumnName("retry_batch_id");
+
+ b.HasKey("Id")
+ .HasName("pk_retry_batch_now_forwarding");
+
+ b.ToTable("retry_batch_now_forwarding", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.SettingEntity", b =>
+ {
+ b.Property("Key")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("key");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Key")
+ .HasName("pk_settings");
+
+ b.ToTable("settings", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.SubscriptionEntity", b =>
+ {
+ b.Property("MessageType")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("message_type");
+
+ b.Property("TransportAddress")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("transport_address");
+
+ b.Property("Endpoint")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("endpoint");
+
+ b.HasKey("MessageType", "TransportAddress")
+ .HasName("pk_subscriptions");
+
+ b.ToTable("subscriptions", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.UnacknowledgedRetryOperationEntity", b =>
+ {
+ b.Property("RequestId")
+ .HasMaxLength(400)
+ .HasColumnType("character varying(400)")
+ .HasColumnName("request_id");
+
+ b.Property("RetryType")
+ .HasColumnType("integer")
+ .HasColumnName("retry_type");
+
+ b.Property("Classifier")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("classifier");
+
+ b.Property("CompletionTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("completion_time");
+
+ b.Property("Failed")
+ .HasColumnType("boolean")
+ .HasColumnName("failed");
+
+ b.Property("Last")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last");
+
+ b.Property("NumberOfMessagesProcessed")
+ .HasColumnType("integer")
+ .HasColumnName("number_of_messages_processed");
+
+ b.Property("Originator")
+ .HasColumnType("text")
+ .HasColumnName("originator");
+
+ b.Property("StartTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start_time");
+
+ b.HasKey("RequestId", "RetryType")
+ .HasName("pk_unacknowledged_retry_operations");
+
+ b.ToTable("unacknowledged_retry_operations", (string)null);
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageGroupEntity", b =>
+ {
+ b.HasOne("ServiceControl.Persistence.EFCore.Entities.FailedMessageEntity", null)
+ .WithMany()
+ .HasForeignKey("FailedMessageUniqueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_failed_message_groups_failed_messages_failed_message_unique");
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.LicensingEndpointThroughputEntity", b =>
+ {
+ b.HasOne("ServiceControl.Persistence.EFCore.Entities.LicensingEndpointEntity", null)
+ .WithMany()
+ .HasForeignKey("NormalizedName", "ThroughputSource")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_licensing_endpoint_throughput_licensing_endpoints_normalize");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/20260916125150_AddMigrationCheckpoints.cs b/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/20260916125150_AddMigrationCheckpoints.cs
new file mode 100644
index 0000000000..b2b97c730a
--- /dev/null
+++ b/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/20260916125150_AddMigrationCheckpoints.cs
@@ -0,0 +1,45 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace ServiceControl.Persistence.EFCore.PostgreSql.Migrations
+{
+ ///
+ public partial class AddMigrationCheckpoints : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "migration_checkpoints",
+ columns: table => new
+ {
+ category_id = table.Column(type: "character varying(450)", maxLength: 450, nullable: false),
+ state = table.Column(type: "integer", nullable: false),
+ cursor = table.Column(type: "text", nullable: true),
+ copied_count = table.Column(type: "bigint", nullable: false),
+ skipped_count = table.Column(type: "bigint", nullable: false),
+ source_total = table.Column(type: "bigint", nullable: true),
+ skip_reasons = table.Column(type: "text", nullable: true),
+ started_at = table.Column(type: "timestamp with time zone", nullable: true),
+ last_progress_at = table.Column(type: "timestamp with time zone", nullable: true),
+ settled_at = table.Column(type: "timestamp with time zone", nullable: true),
+ last_error = table.Column(type: "text", nullable: true),
+ already_present_count = table.Column(type: "bigint", nullable: false),
+ version = table.Column(type: "bigint", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_migration_checkpoints", x => x.category_id);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "migration_checkpoints");
+ }
+ }
+}
diff --git a/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/PostgreSqlServiceControlDbContextModelSnapshot.cs b/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/PostgreSqlServiceControlDbContextModelSnapshot.cs
index 06b1a9b639..cfc74e1483 100644
--- a/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/PostgreSqlServiceControlDbContextModelSnapshot.cs
+++ b/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/PostgreSqlServiceControlDbContextModelSnapshot.cs
@@ -715,6 +715,68 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.ToTable("message_redirects", (string)null);
});
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.MigrationCheckpointEntity", b =>
+ {
+ b.Property("CategoryId")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)")
+ .HasColumnName("category_id");
+
+ b.Property("AlreadyPresentCount")
+ .HasColumnType("bigint")
+ .HasColumnName("already_present_count");
+
+ b.Property("CopiedCount")
+ .HasColumnType("bigint")
+ .HasColumnName("copied_count");
+
+ b.Property("Cursor")
+ .HasColumnType("text")
+ .HasColumnName("cursor");
+
+ b.Property("LastError")
+ .HasColumnType("text")
+ .HasColumnName("last_error");
+
+ b.Property("LastProgressAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_progress_at");
+
+ b.Property("SettledAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("settled_at");
+
+ b.Property("SkipReasons")
+ .HasColumnType("text")
+ .HasColumnName("skip_reasons");
+
+ b.Property("SkippedCount")
+ .HasColumnType("bigint")
+ .HasColumnName("skipped_count");
+
+ b.Property("SourceTotal")
+ .HasColumnType("bigint")
+ .HasColumnName("source_total");
+
+ b.Property("StartedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("started_at");
+
+ b.Property("State")
+ .HasColumnType("integer")
+ .HasColumnName("state");
+
+ b.Property("Version")
+ .IsConcurrencyToken()
+ .HasColumnType("bigint")
+ .HasColumnName("version");
+
+ b.HasKey("CategoryId")
+ .HasName("pk_migration_checkpoints");
+
+ b.ToTable("migration_checkpoints", (string)null);
+ });
+
modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.RetryBatchEntity", b =>
{
b.Property("Id")
diff --git a/src/ServiceControl.Persistence.EFCore.SqlServer/Migrations/20260916125144_AddMigrationCheckpoints.Designer.cs b/src/ServiceControl.Persistence.EFCore.SqlServer/Migrations/20260916125144_AddMigrationCheckpoints.Designer.cs
new file mode 100644
index 0000000000..5dfcc173b9
--- /dev/null
+++ b/src/ServiceControl.Persistence.EFCore.SqlServer/Migrations/20260916125144_AddMigrationCheckpoints.Designer.cs
@@ -0,0 +1,782 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using ServiceControl.Persistence.EFCore.SqlServer;
+
+#nullable disable
+
+namespace ServiceControl.Persistence.EFCore.SqlServer.Migrations
+{
+ [DbContext(typeof(SqlServerServiceControlDbContext))]
+ [Migration("20260916125144_AddMigrationCheckpoints")]
+ partial class AddMigrationCheckpoints
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.ArchiveOperationEntity", b =>
+ {
+ b.Property("RequestId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("ArchiveType")
+ .HasColumnType("int");
+
+ b.Property("OperationType")
+ .HasColumnType("int");
+
+ b.Property("CurrentBatch")
+ .HasColumnType("int");
+
+ b.Property("GroupName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("InitiatedById")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("InitiatedByName")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("NumberOfBatches")
+ .HasColumnType("int");
+
+ b.Property("NumberOfMessagesProcessed")
+ .HasColumnType("int");
+
+ b.Property("OperationId")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Started")
+ .HasColumnType("datetime2");
+
+ b.Property("TotalNumberOfMessages")
+ .HasColumnType("int");
+
+ b.HasKey("RequestId", "ArchiveType", "OperationType");
+
+ b.HasIndex("Started");
+
+ b.ToTable("ArchiveOperations");
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.CustomCheckEntity", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Category")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CustomCheckId")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("FailureReason")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("OriginatingEndpointHost")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("OriginatingEndpointHostId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("OriginatingEndpointName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ReportedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ReportedAt");
+
+ b.HasIndex("Status", "ReportedAt");
+
+ b.ToTable("CustomChecks");
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.EndpointSettingsEntity", b =>
+ {
+ b.Property("Name")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("TrackInstances")
+ .HasColumnType("bit");
+
+ b.HasKey("Name");
+
+ b.ToTable("EndpointSettings");
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.EventLogItemEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Category")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("EventType")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("RaisedAt")
+ .HasColumnType("datetime2");
+
+ b.PrimitiveCollection("RelatedTo")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Severity")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RaisedAt", "Id")
+ .IsDescending();
+
+ b.ToTable("EventLogItems");
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.ExternalIntegrationDispatchRequestEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("DispatchContextJson")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DispatchContextTypeName")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.ToTable("ExternalIntegrationDispatchRequests");
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedErrorImportEntity", b =>
+ {
+ b.Property("UniqueMessageId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Body")
+ .IsRequired()
+ .HasColumnType("varbinary(max)");
+
+ b.Property("BodyStoredExternally")
+ .HasColumnType("bit");
+
+ b.Property("ExceptionInfo")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("FailedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("HeadersJson")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("MessageId")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("UniqueMessageId");
+
+ b.HasIndex("FailedAt");
+
+ b.ToTable("FailedErrorImports");
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageEditEntity", b =>
+ {
+ b.Property("UniqueMessageId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("EditId")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("UniqueMessageId");
+
+ b.HasIndex("EditId");
+
+ b.ToTable("FailedMessageEdits");
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageEntity", b =>
+ {
+ b.Property("UniqueMessageId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("BodyContentType")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("BodySize")
+ .HasColumnType("int");
+
+ b.Property("BodyStoredExternally")
+ .HasColumnType("bit");
+
+ b.Property("BodyText")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ConversationId")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ExceptionMessage")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ExceptionType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("FailingEndpointAddress")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("FirstTimeOfFailure")
+ .HasColumnType("datetime2");
+
+ b.Property("HeadersJson")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsSystemMessage")
+ .HasColumnType("bit");
+
+ b.Property("LastAttemptedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("LastModified")
+ .HasColumnType("datetime2");
+
+ b.Property("LastTimeOfFailure")
+ .HasColumnType("datetime2");
+
+ b.Property("MessageId")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("MessageType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("NumberOfProcessingAttempts")
+ .HasColumnType("int");
+
+ b.Property("ReceivingEndpointHost")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ReceivingEndpointHostId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ReceivingEndpointName")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("SendingEndpointHost")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("SendingEndpointHostId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("SendingEndpointName")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("StatusChangedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("TimeSent")
+ .HasColumnType("datetime2");
+
+ b.HasKey("UniqueMessageId");
+
+ b.HasIndex("ConversationId");
+
+ b.HasIndex("FailingEndpointAddress");
+
+ b.HasIndex("ReceivingEndpointName");
+
+ b.HasIndex("StatusChangedAt")
+ .HasFilter("[Status] IN (2, 4)");
+
+ b.HasIndex("TimeSent");
+
+ b.HasIndex("Status", "LastModified");
+
+ b.ToTable("FailedMessages");
+ });
+
+ modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageGroupEntity", b =>
+ {
+ b.Property("FailedMessageUniqueId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("GroupId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property