Solanica for MSSQL: bumping default version and introducing PITR

Solanica for MSSQL: bumping default version and introducing PITR

By Sergey Pronin Sergey Pronin

When we introduced the Solanica MSSQL operator and the OpenEverest provider, the story was mostly about shape: two custom resources for standalone and Always-On, Lease-based failover in the data plane, native backups straight to object storage. This release is about depth. Two things changed that matter if you actually run SQL Server in production:

  • SQL Server 2025 is now the default engine version
  • Point-in-time recovery (PITR) is a first-class, provider-managed capability - not a pile of manual RESTORE … WITH NORECOVERY you assemble by hand at 3 a.m.

Here’s what shipped, and - because this is the interesting part - what the operator actually does at the database level to make PITR work.

2025 as the default version

The provider ships a small version catalog. Each Instance selects a bundle via spec.version, and the bundle marked default: true is used when you omit it. That default is now 2025:

versions:
  - name: "2025"          # default
    components:
      engine: "2025-latest"   # mcr.microsoft.com/mssql/server:2025-latest
  - name: "2022"
    components:
      engine: "2022-latest"
  - name: "2019"
    components:
      engine: "2019-latest"

New Instances land on mcr.microsoft.com/mssql/server:2025-latest unless you pin spec.version: “2022” (or “2019”). Existing Instances are untouched - the bundle you selected is the bundle you keep. 2022 and 2019 remain fully supported; native backup to object storage and contained availability groups both rely on 2022+ engine features, so those are the floor for the interesting capabilities.

Under the hood this is just a catalog bump in the provider definition plus an operator version bump (the mssql-operator subchart moved to 0.1.6, now pulled from the private OCI registry rather than a local path), so the provider and operator versions stay in lockstep.

Backups, restated as a BackupClass

Backups now surface to OpenEverest through a dedicated mssql-native BackupClass with executionMode: ProviderManaged and supportsPITR: true. That last flag is what tells OpenEverest’s restore validation that this engine can rewind to an instant, not just to a backup.

“Provider-managed” means there is no side-car Job shuffling volumes around. Backups run inside the engine through SQL Server’s native BACKUP ... TO URL, writing directly to S3-compatible object storage. The provider maps OpenEverest’s generic backup intent onto three operator CRDs:

  • MSSQLBackup — a one-shot full, differential, or log backup. Terminal: once it’s Succeeded or Failed, it never re-runs.
  • MSSQLBackupSchedule — cron-driven backups with independent full / differential / log cadences and retention (keepLast, keepDays, optional deleteFromStore).
  • MSSQLRestore — restore by backup reference, by explicit URLs (disaster recovery when no Kubernetes record survives), or by assembling a full → differential → log chain with an optional stop time.

The class exposes just a few knobs per layer — compression, copyOnly, backup type at backup time; recovery/withReplace at restore time; and a small PITR block:

type MssqlPITRParameters struct {
    // Cron for the automatic transaction-log backups. Defaults to every 5 minutes.
    LogBackupSchedule string `json:"logBackupSchedule,omitempty"`
    // How long archived log backups (and the recovery window) are kept. Defaults to 7.
    LogRetentionDays  int32  `json:"logRetentionDays,omitempty"`
}

You enable PITR per storage with spec.backup.storages[].pitr.enabled: true*.* From there the provider maintains the log-backup schedule for you.

What PITR actually does at the database level

Point-in-time recovery isn’t magic; it’s the transaction log, replayed carefully. Here’s the chain of database-level facts the operator has to get right.

1. The database has to be in FULL recovery model. A transaction-log backup (BACKUP LOG) is only meaningful when the log isn’t being truncated at every checkpoint. Enabling PITR implies FULL recovery so the log accumulates until it’s backed up - which is exactly what gives you the continuous timeline to rewind to.

2. A full backup is the anchor; logs are the timeline. PITR only works as a layered cadence: a periodic full as the base, optional differentials to shorten replay, and frequent log backups (every 5 minutes by default) that capture the fine-grained timeline. A typical schedule is a weekly full, daily differential, and 15-minute logs:

full:         { schedule: "0 1 * * 0" }    # weekly
differential: { schedule: "0 1 * * 1-6" }  # daily
log:          { schedule: "*/15 * * * *" } # every 15 minutes
Backup cadence → one continuous timeline
full — the anchor differential — shortens replay log — the fine-grained timeline shaded = restorable window

3. Restoring to an instant means orchestrating a multi-statement chain. When you ask for a restore to time T, the operator doesn’t restore one file — it assembles the chain: pick the latest full that finished at or before T, add the latest applicable differential, then apply every log backup that extends past that anchor up to and including the log that spans T. Chain ordering prefers LSN metadata — matching a differential’s DatabaseBackupLSN to the full’s CheckpointLSN, and ordering logs by FirstLSN/LastLSN — and only falls back to backup finish timestamps when headers are unavailable. That LSN-first approach is what keeps the chain correct even when clocks and backup durations overlap.

Each link is a RESTORE, and the recovery flag matters: every link except the last runs WITH NORECOVERY (the database stays “restoring”, ready for more log), and the final log link runs with the stop time and brings the database online:

RESTORE DATABASE [salesdb] FROM URL = '...full...'  WITH NORECOVERY, REPLACE, STATS = 10
RESTORE DATABASE [salesdb] FROM URL = '...diff...'  WITH NORECOVERY, STATS = 10
RESTORE LOG      [salesdb] FROM URL = '...log-1...' WITH NORECOVERY, STATS = 10
RESTORE LOG      [salesdb] FROM URL = '...log-N...' WITH RECOVERY, STATS = 10,
                 STOPAT = '2026-08-27T14:32:00'

STOPAT is what makes it point-in-time: SQL Server replays the final log only up to that instant and discards everything after it. Restore to a different database name and the operator computes the MOVE clauses from RESTORE FILELISTONLY so you can clone into a fresh database without colliding with the original.

4. Availability Groups keep the chain intact automatically. For AG targets, agReplicaPreference: preferred lets SQL Server pick the backup-preferred replica via sys.fn_hadr_backup_is_preferred_replica, and any backup that lands on a secondary is automatically marked COPY_ONLY - so a routine backup on a readable secondary never breaks the differential/log chain. (One current limit worth calling out: PITR restore into an AG isn’t supported yet - restore into a standalone MSSQLInstance. Full and log backups on an AG are fine.)

The recovery window, surfaced where you’d look for it

status.backup.storages[s3].pitr Available
Window opens at the oldest full backup and extends to the newest log. Before a full exists, the state is Unavailable.

Knowing that you can restore is only half the answer; you need to know between which two instants. The provider now publishes the observed recovery window on the Instance’s backup status for every PITR-enabled storage. It derives the window from the instance’s successful backups: the window opens at the oldest full backup and extends to the newest backup (a log when logs exist, otherwise the newest full). Before a full exists, the storage reports Unavailable with a reason, so “there’s nothing to restore yet” is an explicit state rather than an empty field.

Try it

If you’re already running the provider, bumping to this release gets you 2025 on new Instances and the mssql-native BackupClass automatically. Turn on PITR by adding pitr.enabled: true to a backup storage, let a full and a few logs accumulate, and watch the recovery window populate on the Instance status. Then restore to a timestamp and let the operator rebuild the chain for you.

See Also