Lightweight 0.20261031.0
Loading...
Searching...
No Matches
SqlMigration.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "Api.hpp"
6#include "DataMapper/DataMapper.hpp"
7#include "ProgressTask.hpp"
8#include "SqlError.hpp"
9#include "SqlQuery/Migrate.hpp"
10#include "SqlQuery/MigrationPlan.hpp"
11#include "SqlSchema.hpp"
12#include "SqlTransaction.hpp"
13
14#include <cstddef>
15#include <cstdint>
16#include <exception>
17#include <expected>
18#include <format>
19#include <functional>
20#include <list>
21#include <map>
22#include <memory>
23#include <optional>
24#include <set>
25#include <span>
26#include <stdexcept>
27#include <string>
28#include <string_view>
29#include <vector>
30
31namespace Lightweight
32{
33
34class SqlConnection;
35
36/// @defgroup SqlMigration SQL Migration
37/// @brief Classes and functions for SQL schema migrations.
38
39namespace SqlMigration
40{
41 class MigrationBase;
42
43 /// Represents a unique timestamp of a migration.
44 ///
45 /// This struct is used to identify migrations and is used as a key in the migration history table.
46 ///
47 /// Note, a recommended format for the timestamp is a human readable format like YYYYMMDDHHMMSS
48 ///
49 /// @code
50 /// MigrationTimestamp { 2026'01'17'00'31'20 };
51 /// @endcode
52 ///
53 /// @ingroup SqlMigration
55 {
56 /// The numeric timestamp value identifying the migration.
57 uint64_t value {};
58
59 /// Three-way comparison operator.
60 constexpr std::weak_ordering operator<=>(MigrationTimestamp const& other) const noexcept = default;
61 };
62
63 /// Exception thrown when applying or reverting a single migration fails.
64 ///
65 /// Carries structured diagnostic context so callers (CLI, GUI) can render
66 /// the *which migration*, *which step*, *which SQL statement* and the
67 /// underlying driver error as separate fields instead of parsing one
68 /// opaque message string.
69 ///
70 /// @ingroup SqlMigration
71 class MigrationException: public SqlException
72 {
73 public:
74 /// Whether the failure happened while applying (Up) or reverting (Down).
75 enum class Operation : std::uint8_t
76 {
77 Apply,
78 Revert,
79 };
80
81 /// Constructs a migration exception that wraps a driver error with
82 /// the migration identity and the exact SQL statement that failed.
83 ///
84 /// @param operation Whether the failure happened during apply or revert.
85 /// @param timestamp The migration that failed.
86 /// @param title Human-readable migration title.
87 /// @param stepIndex Zero-based step index inside the migration plan.
88 /// @param failedSql The SQL statement that produced the driver error.
89 /// @param driverError The ODBC-level error info as received from the driver.
90 LIGHTWEIGHT_API MigrationException(Operation operation,
91 MigrationTimestamp timestamp,
92 std::string title,
93 std::size_t stepIndex,
94 std::string failedSql,
95 SqlErrorInfo driverError);
96
97 /// Whether the failure occurred while applying or reverting.
98 [[nodiscard]] Operation GetOperation() const noexcept
99 {
100 return _operation;
101 }
102 /// Timestamp of the failing migration.
103 [[nodiscard]] MigrationTimestamp GetMigrationTimestamp() const noexcept
104 {
105 return _timestamp;
106 }
107 /// Human-readable title of the failing migration.
108 [[nodiscard]] std::string const& GetMigrationTitle() const noexcept
109 {
110 return _title;
111 }
112 /// Zero-based step index inside the plan of the failing migration.
113 [[nodiscard]] std::size_t GetStepIndex() const noexcept
114 {
115 return _stepIndex;
116 }
117 /// The exact SQL statement that the driver rejected.
118 [[nodiscard]] std::string const& GetFailedSql() const noexcept
119 {
120 return _failedSql;
121 }
122 /// Raw driver error message, without the migration context prefix that
123 /// `what()` and `info().message` decorate it with.
124 [[nodiscard]] std::string const& GetDriverMessage() const noexcept
125 {
126 return _driverMessage;
127 }
128
129 private:
130 Operation _operation;
131 MigrationTimestamp _timestamp;
132 std::string _title;
133 std::size_t _stepIndex;
134 std::string _failedSql;
135 std::string _driverMessage;
136 };
137
138 /// Result of verifying a migration's checksum.
139 ///
140 /// @ingroup SqlMigration
142 {
143 MigrationTimestamp timestamp; ///< The timestamp of the verified migration.
144 std::string_view title; ///< The title of the verified migration.
145 std::string storedChecksum; ///< The checksum stored in the database.
146 std::string computedChecksum; ///< The checksum computed from the current migration definition.
147 bool matches; ///< Whether the stored and computed checksums match.
148 };
149
150 /// Result of reverting multiple migrations.
151 ///
152 /// @ingroup SqlMigration
154 {
155 std::vector<MigrationTimestamp> revertedTimestamps; ///< Successfully reverted migrations
156 std::optional<MigrationTimestamp> failedAt; ///< Migration that failed, if any
157 std::string errorMessage; ///< Short error message if failed (driver message only)
158
159 /// Title of the migration that failed (if any). Empty when no failure or
160 /// when the failure happened before the migration could be located.
161 std::string failedTitle;
162
163 /// Zero-based step index inside the failed migration's plan. Meaningful
164 /// only when `failedAt` is set and the failure came from a driver error
165 /// (not from e.g. a missing registered migration).
166 std::size_t failedStepIndex {};
167
168 /// The exact SQL statement that failed, if available. Empty when the
169 /// failure happened outside of SQL execution (e.g. missing Down()
170 /// implementation, unregistered migration).
171 std::string failedSql;
172
173 /// SQLSTATE diagnostic code from the driver, if available.
174 std::string sqlState;
175
176 /// Native driver error code, if available.
177 SQLINTEGER nativeErrorCode {};
178 };
179
180 /// Status summary of migrations.
181 ///
182 /// @ingroup SqlMigration
184 {
185 size_t appliedCount {}; ///< Number of migrations that have been applied
186 size_t pendingCount {}; ///< Number of migrations waiting to be applied
187 size_t mismatchCount {}; ///< Number of applied migrations with checksum mismatches
188 size_t unknownAppliedCount {}; ///< Number of applied migrations not found in registered list
189 size_t totalRegistered {}; ///< Total number of registered migrations
190 };
191
192 /// Associates a software release with the highest migration timestamp present at release time.
193 ///
194 /// Releases are declared in source (typically a migration plugin) via
195 /// `LIGHTWEIGHT_SQL_RELEASE(version, highestTimestamp[, options])`. They let tools answer questions like
196 /// "which migrations belong to release 6.7.0?" or "roll back everything applied after 6.7.0".
197 ///
198 /// The `highestTimestamp` is an inclusive upper bound: a migration `M` belongs to the release
199 /// iff `prev_release_ts < M.timestamp <= highestTimestamp`, where `prev_release_ts` is the
200 /// previous release's timestamp (or 0 if there is none).
201 ///
202 /// @ingroup SqlMigration
204 {
205 /// Human-readable version string, e.g. "6.7.0".
206 std::string version;
207 /// Highest migration timestamp contained in this release (inclusive).
209 /// Whether every migration of this release requires a backup of the database taken after its
210 /// last migration (see `MigrationManager::BackupRequirementFor`).
211 bool requiresBackup = false;
212 };
213
214 /// Options of a release declared with `LIGHTWEIGHT_SQL_RELEASE`.
215 ///
216 /// @ingroup SqlMigration
218 {
219 /// Shorthand for "every migration of the release requires a backup".
220 bool requiresBackup = false;
221 };
222
223 /// What a `schema_backups` record documents.
224 ///
225 /// @ingroup SqlMigration
226 enum class BackupRecordKind : std::uint8_t
227 {
228 Backup, ///< A verified backup archive was written; stored as `backup`.
229 Restore, ///< A backup archive was restored into the database; stored as `restore`.
230 /// A migrating run went ahead without a backup because the user explicitly waived it; stored as
231 /// `waived`. Never satisfies a requirement.
232 Waived,
233 };
234
235 /// Name of the table that holds the backup records.
236 ///
237 /// @ingroup SqlMigration
238 inline constexpr std::string_view BackupRecordTableName = "schema_backups";
239
240 /// The SHA-256 of an empty `schema_migrations` history (the digest of no rows).
241 ///
242 /// @ingroup SqlMigration
243 inline constexpr std::string_view EmptyMigrationHistoryDigest =
244 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
245
246 /// The migration history physically recorded in `schema_migrations`, as read from the database.
247 ///
248 /// @ingroup SqlMigration
250 {
251 /// Highest applied version; empty when none is recorded. For messages.
252 std::optional<MigrationTimestamp> lastAppliedMigration;
253 /// The latest `applied_at` over all recorded migrations; empty when none carries a time. Informational.
254 std::optional<SqlDateTime> lastAppliedAt;
255 /// SHA-256, lowercase hex, of the whole physical history: one line `<version>|<applied_at>` per row,
256 /// sorted by version, `applied_at` as milliseconds since the Unix epoch (empty when null). Any
257 /// applied, re-applied, rolled back or re-stamped migration changes it.
258 std::string digest = std::string { EmptyMigrationHistoryDigest };
259 };
260
261 /// One row of the `schema_backups` table.
262 ///
263 /// @ingroup SqlMigration
265 {
266 /// Assigned by the database; the newest record has the highest.
267 std::uint64_t id = 0;
268 /// UTC, informational: for a backup when it started reading the database, for a restore when it
269 /// finished. It plays no part in `BackupRecordCovers`.
271 /// UTF-8, without its directory.
272 std::string archiveFileName;
273 /// SHA-256 of the archive file, lowercase hex.
274 std::string archiveSha256;
275 /// The history the backup saw, from `ReadMigrationHistoryState`. Read it before the backup reads
276 /// any data (for a restore: after it finished). A record is built only from a state read from the
277 /// database, never from a clock. Stored: `lastAppliedMigration` and `digest`.
279 /// The tool that wrote the record, e.g. `dbtool 1.2.3`.
280 std::string toolVersion;
281 /// What the record documents.
283 };
284
285 /// Whether the work a command will run needs a backup, and whether a recorded one satisfies it.
286 ///
287 /// @ingroup SqlMigration
289 {
290 /// Whether anything demands a backup.
291 bool required = false;
292 /// What demands it: `release <version>`, `migration <timestamp> "<title>"` and `step "<title>"`, in that order.
293 std::vector<std::string> reasons;
294 /// Whether nothing is required or the newest backup or restore record covers the database's current state.
295 bool satisfied = true;
296 /// The newest `backup` or `restore` record; `waived` records are skipped.
297 std::optional<BackupRecord> lastBackup;
298 };
299
300 /// Reads the migration history from `schema_migrations`.
301 ///
302 /// Physical rows only; the virtual-applied overlay does not count.
303 ///
304 /// @param dataMapper The migration connection's mapper.
305 /// @return The state; empty when the table is missing. Other errors throw.
306 /// @ingroup SqlMigration
307 [[nodiscard]] LIGHTWEIGHT_API MigrationHistoryState ReadMigrationHistoryState(DataMapper& dataMapper);
308
309 /// Reads the newest `backup` or `restore` record, by id; `waived` records are skipped.
310 ///
311 /// @param dataMapper The migration connection's mapper.
312 /// @return The record; empty when there is none or the table is missing. Other errors throw.
313 /// @ingroup SqlMigration
314 [[nodiscard]] LIGHTWEIGHT_API std::optional<BackupRecord> ReadLatestBackupRecord(DataMapper& dataMapper);
315
316 /// Appends a record to `schema_backups`, creating the table when it is missing.
317 ///
318 /// @param dataMapper The migration connection's mapper.
319 /// @param record The record to store; its `id` is ignored.
320 /// @return The record with the id the database assigned, or the database error.
321 /// @ingroup SqlMigration
322 [[nodiscard]] LIGHTWEIGHT_API std::expected<BackupRecord, SqlErrorInfo> WriteBackupRecord(DataMapper& dataMapper,
323 BackupRecord record);
324
325 /// Whether @p record covers the database state @p state.
326 ///
327 /// Only a `backup` or `restore` record can cover, and it does when it saw exactly the current migration
328 /// history: its `observedState.digest` equals the state's. Any migration applied, re-applied, rolled
329 /// back or re-stamped since changes the digest. Only values read from the database are compared, so
330 /// `createdAt` and the clocks of the hosts involved play no part. Any other kind covers nothing.
331 ///
332 /// @param record The record to test.
333 /// @param state The current migration state.
334 /// @ingroup SqlMigration
335 [[nodiscard]] LIGHTWEIGHT_API bool BackupRecordCovers(BackupRecord const& record,
336 MigrationHistoryState const& state) noexcept;
337
338 /// Whether @p schema is a schema name `MigrationManager::SetDefaultSchema` accepts: `[A-Za-z0-9_]+`.
339 ///
340 /// @param schema The name to check.
341 /// @return False for an empty name and for any other character.
342 /// @ingroup SqlMigration
343 [[nodiscard]] LIGHTWEIGHT_API bool IsValidSchemaName(std::string_view schema) noexcept;
344
345 /// The failure of one unit of `MigrationManager::UnicodeUpgradeTables`, which was rolled back.
346 ///
347 /// `info()` keeps the SQLSTATE and native error code of the cause; its message names the unit,
348 /// its tables and what the units before it committed, followed by the cause's message.
349 ///
350 /// @ingroup SqlMigration
351 class UnicodeUpgradeUnitError final: public SqlException
352 {
353 public:
354 /// @param info The cause's error info, its message already prefixed with the unit's.
355 /// @param unitNumber The failed unit, counted from 1.
356 /// @param unitCount The number of units of the upgrade.
357 UnicodeUpgradeUnitError(SqlErrorInfo info, std::size_t unitNumber, std::size_t unitCount):
358 SqlException(std::move(info)),
359 _unitNumber { unitNumber },
360 _unitCount { unitCount }
361 {
362 }
363
364 /// The failed unit, counted from 1; the units before it are committed.
365 [[nodiscard]] std::size_t UnitNumber() const noexcept
366 {
367 return _unitNumber;
368 }
369
370 /// The number of units of the upgrade.
371 [[nodiscard]] std::size_t UnitCount() const noexcept
372 {
373 return _unitCount;
374 }
375
376 private:
377 std::size_t _unitNumber;
378 std::size_t _unitCount;
379 };
380
381 class MigrationManager;
382
383 /// Whether an operation changes the database or only reports what it would change.
384 ///
385 /// @ingroup SqlMigration
386 enum class ExecutionMode : std::uint8_t
387 {
388 Execute, ///< Carry the operation out.
389 DryRun, ///< Report the plan and change nothing.
390 };
391
392 /// Options of a pre-migrate step.
393 ///
394 /// @ingroup SqlMigration
396 {
397 /// Whether this run of the step demands a backup of the database taken after its last
398 /// migration (see `MigrationManager::BackupRequirementFor`), e.g. "the dry-run plan is not
399 /// empty". Evaluated on the migration connection before the run; empty means never. Hosts call it
400 /// on every requirement check and from any thread (a worker, or a GUI's own), so keep it cheap.
402 };
403
404 /// Work a plugin runs before the first pending migration of `migrate`, `apply` and `migrate-to-release`.
405 ///
406 /// The first parameter is the migration connection, on which the host holds the migration lock
407 /// unless it opted out of locking; then the host's manager, the step's own task and the execution
408 /// mode, in which a dry run makes the step report its plan and change nothing.
409 ///
410 /// @ingroup SqlMigration
412
413 /// A registered pre-migrate step, without its code.
414 ///
415 /// @ingroup SqlMigration
417 {
418 std::string title; ///< The step's title, as its task shows it.
419 bool declaresBackupRequirement = false; ///< Whether the step has a `requiresBackup` predicate.
420 };
421
422 /// How `MigrationManager::RunPreMigrateSteps` ended.
423 ///
424 /// @ingroup SqlMigration
425 enum class PreMigrateStepsOutcome : std::uint8_t
426 {
427 Completed, ///< Every step ran; the migrations may follow.
428 Cancelled, ///< The task asked to cancel; the caller applies no migration.
429 };
430
431 /// A pre-migrate step threw; the command stops and applies no migration.
432 ///
433 /// @ingroup SqlMigration
434 class PreMigrateStepError final: public std::runtime_error
435 {
436 public:
437 /// @param stepTitle The failed step.
438 /// @param cause What the step threw.
439 PreMigrateStepError(std::string stepTitle, std::string_view cause):
440 std::runtime_error(std::format("Pre-migrate step \"{}\" failed: {}", stepTitle, cause)),
441 _stepTitle { std::move(stepTitle) }
442 {
443 }
444
445 /// @return The title of the step that failed.
446 [[nodiscard]] std::string const& StepTitle() const noexcept
447 {
448 return _stepTitle;
449 }
450
451 private:
452 std::string _stepTitle;
453 };
454
455 /// Main API to use for managing SQL migrations
456 ///
457 /// This class is a singleton and can be accessed using the GetInstance() method.
458 ///
459 /// @ingroup SqlMigration
461 {
462 public:
463 /// Type alias for a list of migration pointers.
464 using MigrationList = std::list<MigrationBase const*>;
465
466 /// Get the singleton instance of the migration manager.
467 ///
468 /// @return Reference to the migration manager.
469 /// Get the singleton instance of the migration manager.
470 ///
471 /// @return Reference to the migration manager.
472 LIGHTWEIGHT_API static MigrationManager& GetInstance();
473
474 /// Add a migration to the manager.
475 ///
476 /// @param migration Pointer to the migration to add.
477 LIGHTWEIGHT_API void AddMigration(MigrationBase const* migration);
478
479 /// Get all migrations that have been added to the manager.
480 ///
481 /// @return List of migrations.
482 [[nodiscard]] LIGHTWEIGHT_API MigrationList const& GetAllMigrations() const noexcept;
483
484 /// Get a migration by timestamp.
485 ///
486 /// @param timestamp Timestamp of the migration to get.
487 /// @return Pointer to the migration if found, nullptr otherwise.
488 [[nodiscard]] LIGHTWEIGHT_API MigrationBase const* GetMigration(MigrationTimestamp timestamp) const noexcept;
489
490 /// Remove a single migration from the manager.
491 ///
492 /// Called by @c MigrationBase's destructor so a migration that goes out of scope does not
493 /// leave a dangling pointer behind. Removing a migration that was never added is a no-op.
494 ///
495 /// @param migration Pointer to the migration to remove.
496 LIGHTWEIGHT_API void RemoveMigration(MigrationBase const* migration) noexcept;
497
498 /// Remove all migrations from the manager.
499 ///
500 /// This function is useful if the migration manager should be reset.
501 LIGHTWEIGHT_API void RemoveAllMigrations();
502
503 /// Get all migrations that have not been applied yet.
504 ///
505 /// A dependency cycle does not throw: the list then falls back to timestamp order, and the error
506 /// surfaces again from ApplyPendingMigrations() / ValidateDependencies().
507 ///
508 /// @return List of pending migrations.
509 /// @throws std::runtime_error Reading the applied migrations failed in a way other than the
510 /// table being absent (e.g. an unexpected NULL in `schema_migrations`).
511 [[nodiscard]] LIGHTWEIGHT_API std::list<MigrationBase const*> GetPending() const;
512
513 /// Callback type invoked during migration execution to report progress.
515 std::function<void(MigrationBase const& /*migration*/, size_t /*current*/, size_t /*total*/)>;
516
517 /// Apply a single migration from a migration object.
518 ///
519 /// @param migration Pointer to the migration to apply.
520 LIGHTWEIGHT_API void ApplySingleMigration(MigrationBase const& migration);
521
522 /// @brief Variant of `ApplySingleMigration` that threads a caller-owned render
523 /// context through to `ToSql`. Use this from loops over multiple migrations so
524 /// the column-width cache (and other per-run compat state) accumulates across
525 /// the sequence.
526 LIGHTWEIGHT_API void ApplySingleMigration(MigrationBase const& migration, MigrationRenderContext& context);
527
528 /// Revert a single migration from a migration object.
529 ///
530 /// @param migration Pointer to the migration to revert.
531 LIGHTWEIGHT_API void RevertSingleMigration(MigrationBase const& migration);
532
533 /// Apply all migrations that have not been applied yet.
534 ///
535 /// @param feedbackCallback Callback to be called for each migration.
536 /// @return Number of applied migrations.
537 LIGHTWEIGHT_API size_t ApplyPendingMigrations(ExecuteCallback const& feedbackCallback = {});
538
539 /// Apply pending migrations whose timestamp is <= `targetInclusive`.
540 ///
541 /// Honors dependency-respecting topological order, just like
542 /// `ApplyPendingMigrations`, and threads a single render context across
543 /// the run so column-width state from earlier CREATE TABLEs is visible
544 /// to later compat-aware INSERT/UPDATE rendering.
545 ///
546 /// If a pending migration with `ts <= target` declares a dependency on
547 /// a migration whose timestamp is `> target` (i.e. excluded by the
548 /// bound) and is not already applied, this throws — partial states
549 /// that violate dependencies are refused up front.
550 ///
551 /// @param targetInclusive Highest timestamp to apply (inclusive).
552 /// @param feedbackCallback Callback to be called for each migration.
553 /// @return Number of applied migrations (may be zero if already past `targetInclusive`).
554 /// @throws std::runtime_error if a dependency would cross the boundary.
555 LIGHTWEIGHT_API size_t ApplyPendingMigrationsUpTo(MigrationTimestamp targetInclusive,
556 ExecuteCallback const& feedbackCallback = {});
557
558 /// Create the migration history table if it does not exist.
559 LIGHTWEIGHT_API void CreateMigrationHistory();
560
561 /// @brief Sets the default schema applied to migration connections.
562 ///
563 /// When non-empty, installs a chained `SqlConnection::SetPostConnectedHook`
564 /// that executes the formatter's `SetDefaultSchemaStatement(schema)`
565 /// (e.g. `SET search_path TO "<schema>", public` on PostgreSQL) for
566 /// every new connection. SQL Server and SQLite have no portable
567 /// session-level "default schema" command — for SQL Server, the
568 /// connecting login's server-side `DEFAULT_SCHEMA` is what determines
569 /// where unqualified DDL/DML lands.
570 ///
571 /// `schema` is validated against a `[A-Za-z0-9_]` whitelist; rejecting
572 /// invalid input by throwing a `std::invalid_argument`. Passing an
573 /// empty string clears any previously installed schema hook.
574 ///
575 /// Calling this **after** a connection has already been opened on the
576 /// current thread does not retroactively apply the schema — the hook
577 /// fires on subsequent `SqlConnection::Connect` calls. For dbtool and
578 /// the GUI this is invoked before the first connection.
579 ///
580 /// @param schema The default schema to use. Empty disables.
581 LIGHTWEIGHT_API void SetDefaultSchema(std::string schema);
582
583 /// @brief Returns the default schema currently installed on the manager
584 /// (empty when unset).
585 [[nodiscard]] LIGHTWEIGHT_API std::string_view DefaultSchema() const noexcept;
586
587 /// Get all applied migration IDs.
588 ///
589 /// Returns the union of timestamps physically present in `schema_migrations`
590 /// and the in-memory "virtual applied" overlay (see `AddVirtualAppliedMigrations`),
591 /// sorted ascending and deduplicated. The accessor never creates the
592 /// `schema_migrations` table — when the table does not exist the database
593 /// half of the union is the empty set, so a pure-overlay state is fully
594 /// observable here without any side effects.
595 ///
596 /// @return Vector of applied migration IDs (database rows ∪ overlay).
597 [[nodiscard]] LIGHTWEIGHT_API std::vector<MigrationTimestamp> GetAppliedMigrationIds() const;
598
599 /// Contribute "virtual" applied migration IDs that participate in every
600 /// `GetAppliedMigrationIds()` read but are not persisted to
601 /// `schema_migrations` until the manager actually performs a write
602 /// (`ApplySingleMigration`, `MarkMigrationAsApplied`, or
603 /// `RevertSingleMigration`). Designed for plugin post-init hooks that
604 /// need to bridge a foreign version-tracking table (e.g. the legacy
605 /// `LASTRADA_PROPERTIES NR=4` row) into the modern applied-set view
606 /// without paying for table creation on read-only commands like
607 /// `status` or `list-applied`.
608 ///
609 /// The input is merged into an internal sorted, deduplicated vector;
610 /// passing the same timestamp twice is a no-op.
611 ///
612 /// @param timestamps Timestamps to add to the overlay.
613 LIGHTWEIGHT_API void AddVirtualAppliedMigrations(std::span<MigrationTimestamp const> timestamps);
614
615 /// Drop every entry from the in-memory virtual-applied overlay. Intended
616 /// for test setup/teardown and for callers that want to re-derive the
617 /// overlay from scratch (e.g. a `dbtool-gui` reconnect against a
618 /// different profile). Has no effect on `schema_migrations` rows.
619 LIGHTWEIGHT_API void ClearVirtualAppliedMigrations();
620
621 /// Optional sink for informational status messages emitted by plugin
622 /// hooks (e.g. the "Transitioning from LASTRADA_PROPERTIES …" banner).
623 /// `dbtool` installs a sink that prints to `stdout` to preserve the
624 /// historical CLI output; `dbtool-gui` installs a sink that routes
625 /// through its `LogInfo` channel so the banner shows up in the
626 /// activity log instead of going to a hidden stream.
627 using LogSink = std::function<void(std::string_view)>;
628
629 /// Install a log sink. Pass `{}` to clear.
630 ///
631 /// @param sink Callable invoked once per status message.
632 LIGHTWEIGHT_API void SetLogSink(LogSink sink);
633
634 /// Emit a status message via the installed sink. No-op when no sink is
635 /// installed — silent by default keeps the library suitable for use in
636 /// embedded contexts that have no obvious output stream.
637 ///
638 /// @param message The message to deliver.
639 LIGHTWEIGHT_API void Log(std::string_view message) const;
640
641 /// @brief Number of rows `Insert(...).IfNotExists(...)` steps skipped because a row
642 /// with the same key values already existed, counted over the manager's lifetime.
643 ///
644 /// Each skip is also reported through @ref Log as one line of the form
645 /// `Skipped existing row: <timestamp> <table> (<key>=<value>, ...)`. Both happen once the
646 /// migration has committed, so a migration that fails and rolls back reports no skips.
647 /// Callers that need the count of one run take the difference before and after it.
648 [[nodiscard]] LIGHTWEIGHT_API std::size_t SkippedExistingRowCount() const noexcept;
649
650 /// Registers a step that `migrate`, `apply` and `migrate-to-release` run, in registration order,
651 /// before their first pending migration (also when none is pending). Plugins register steps
652 /// from their post-init hook; a step is code of that plugin, so hosts call
653 /// `RemoveAllPreMigrateSteps` before running post-init hooks again and before unloading it.
654 ///
655 /// @param title The step's title, unique among the steps.
656 /// @param options The step's options.
657 /// @param step The step's code.
658 /// @throws std::invalid_argument when @p step is empty or a step titled @p title exists.
659 LIGHTWEIGHT_API void AddPreMigrateStep(std::string title, PreMigrateStepOptions options, PreMigrateStep step);
660
661 /// @return The registered pre-migrate steps, in registration order.
662 [[nodiscard]] LIGHTWEIGHT_API std::vector<PreMigrateStepInfo> GetPreMigrateSteps() const;
663
664 /// Removes every pre-migrate step.
665 LIGHTWEIGHT_API void RemoveAllPreMigrateSteps() noexcept;
666
667 /// Runs the pre-migrate steps in registration order, each under a sub-task of @p parent named
668 /// like the step, on the migration connection. The caller holds the migration lock (unless it
669 /// opted out of locking) and calls this before applying the first pending migration. Status and
670 /// read-only commands never call it. Cancellation is checked before and after each step.
671 ///
672 /// @param parent The task the steps' tasks open under.
673 /// @param mode Whether the steps only report their plan.
674 /// @return `Cancelled` when @p parent asked to cancel; the caller then applies no migration.
675 /// @throws PreMigrateStepError when a step throws, also something other than a `std::exception`
676 /// ("unknown exception"); the steps after it do not run.
677 [[nodiscard]] LIGHTWEIGHT_API PreMigrateStepsOutcome RunPreMigrateSteps(ProgressTask& parent, ExecutionMode mode);
678
679 /// Get the data mapper used for migrations.
680 ///
681 /// Owned by the manager and connected with the default connection string on first use. When
682 /// the application replaces the default (@ref SqlConnection::SetDefaultConnectionString), the
683 /// next call reconnects the same instance - the reference stays valid - to the new default.
684 /// A call from a copy of the library that never had a default set (a plugin that links
685 /// Lightweight statically, calling into the host's manager from a hook) keeps the mapper as it is.
686 ///
687 /// @return The migration data mapper.
688 /// @throws SqlException Connecting failed.
689 [[nodiscard]] LIGHTWEIGHT_API DataMapper& GetDataMapper();
690
691 /// Get the data mapper used for migrations.
692 [[nodiscard]] LIGHTWEIGHT_API DataMapper& GetDataMapper() const
693 {
694 return const_cast<MigrationManager*>(this)->GetDataMapper();
695 }
696
697 /// Close the data mapper.
698 ///
699 /// This function is useful if explicitly closing the connection is desired before
700 /// the migration manager is destroyed.
701 LIGHTWEIGHT_API void CloseDataMapper();
702
703 /// @brief Per-migration compat policy.
704 ///
705 /// Given a migration about to be applied (or previewed), returns the set of compat
706 /// flags that should be active while rendering it. Plugins that ship legacy
707 /// migrations use this to scope compat behaviour to their own timestamp range
708 /// (e.g. a plugin enables `lup-truncate` for migrations predating a given
709 /// release). Unset means "strict for every migration".
710 using CompatPolicy = std::function<std::set<std::string>(MigrationBase const&)>;
711
712 /// @brief Installs a per-migration compat policy. Pass `{}` to clear.
713 ///
714 /// Typically called once from a plugin's static initializer. Replaces any policy
715 /// previously installed — composition is the caller's responsibility for now.
716 /// A policy from a plugin is code in that plugin's library: a host must clear it
717 /// before it unloads the plugin, as dbtool's `Tools::UnloadPlugins` does.
718 LIGHTWEIGHT_API void SetCompatPolicy(CompatPolicy policy);
719
720 /// @brief Returns a view of the installed policy so it can be propagated across
721 /// managers (plugin → central). Empty callable if no policy was installed.
722 [[nodiscard]] LIGHTWEIGHT_API CompatPolicy const& GetCompatPolicy() const noexcept;
723
724 /// @brief Composes an additional policy with the currently installed one. If no
725 /// policy is installed, the argument becomes the policy as-is; otherwise both
726 /// policies are consulted and their flag sets unioned per migration.
727 LIGHTWEIGHT_API void ComposeCompatPolicy(CompatPolicy policy);
728
729 /// @brief Returns the compat flags the current policy assigns to `migration`, or
730 /// an empty set if no policy is installed.
731 [[nodiscard]] LIGHTWEIGHT_API std::set<std::string> CompatFlagsFor(MigrationBase const& migration) const;
732
733 /// Get a transaction for the data mapper.
734 ///
735 /// @return Transaction.
736 LIGHTWEIGHT_API SqlTransaction Transaction();
737
738 /// Preview SQL statements for a single migration without executing.
739 ///
740 /// This is useful for dry-run mode to see what SQL would be executed.
741 ///
742 /// @param migration The migration to preview.
743 /// @return Vector of SQL statements that would be executed.
744 [[nodiscard]] LIGHTWEIGHT_API std::vector<std::string> PreviewMigration(MigrationBase const& migration) const;
745
746 /// @brief Variant of `PreviewMigration` that threads a caller-owned render
747 /// context so the column-width cache accumulates across a sequence of preview
748 /// calls. Used by `PreviewPendingMigrations` to render compat-aware dry runs.
749 [[nodiscard]] LIGHTWEIGHT_API std::vector<std::string> PreviewMigrationWithContext(
750 MigrationBase const& migration, MigrationRenderContext& context) const;
751
752 /// @brief One row in the `RewriteChecksumsResult.entries` list.
754 {
755 MigrationTimestamp timestamp; ///< The migration whose stored checksum was rewritten.
756 std::string_view title; ///< Title of the registered migration (or "(Unknown Migration)").
757 std::string oldChecksum; ///< Stored checksum before the rewrite. Empty if there was none.
758 std::string newChecksum; ///< Stored checksum after the rewrite.
759 };
760
761 /// @brief Result of a `RewriteChecksums` call.
763 {
764 std::vector<ChecksumRewriteEntry> entries; ///< One entry per rewritten or would-be-rewritten row.
765 std::vector<MigrationTimestamp>
766 unregisteredTimestamps; ///< Applied rows whose migration is no longer registered.
767 bool wasDryRun = false; ///< True if the call ran in dry-run mode.
768 };
769
770 /// @brief Snapshot of the schema the registered migrations *intend* to produce.
771 ///
772 /// Pure plan-walk — never executes SQL, never opens a connection. Folds the
773 /// effects of every registered migration (up to an optional cut-off timestamp)
774 /// into a per-table view of "the final shape" plus a chronological list of
775 /// data steps and surviving indexes/releases. Used by:
776 ///
777 /// - `HardReset` to know which tables the migrations would have created
778 /// - `UnicodeUpgradeTables` to know which char/varchar columns the migrations
779 /// now declare wide
780 ///
781 /// Both are pure operations — `FoldRegisteredMigrations` itself never
782 /// executes a single statement. See `FoldRegisteredMigrations` for the API.
784 {
785 /// Per-table state: ordered column declarations + per-table FK list.
787 {
788 /// Ordered column declarations as they would be emitted in CREATE TABLE.
789 std::vector<SqlColumnDeclaration> columns;
790 /// Composite foreign keys declared on this table (single-column FKs are
791 /// carried inline on the corresponding `SqlColumnDeclaration::foreignKey`).
792 std::vector<SqlCompositeForeignKeyConstraint> compositeForeignKeys;
793 /// Whether the original CREATE TABLE used IF NOT EXISTS (carried through
794 /// to emitters so they can replay it verbatim).
795 bool ifNotExists = false;
796 };
797
798 /// (schema, table) → folded `TableState`. Insertion order is *not* preserved by
799 /// `std::map` — for emission order use `creationOrder` below.
800 std::map<SqlSchema::FullyQualifiedTableName, TableState> tables;
801
802 /// Tables in *creation* order (first-time-seen). Reverse for safe DROP ordering
803 /// when tearing the schema down.
804 std::vector<SqlSchema::FullyQualifiedTableName> creationOrder;
805
806 /// Indexes that survive folding (created on tables still present at end).
807 std::vector<SqlCreateIndexPlan> indexes;
808
809 /// One data step (INSERT/UPDATE/DELETE/RawSql) tagged with its source migration.
810 struct DataStep
811 {
812 /// Timestamp of the migration that contributed this data step.
814 /// Title of the migration that contributed this data step.
815 std::string sourceTitle;
816 /// The plan element itself (INSERT, UPDATE, DELETE, or RawSql).
818 };
819
820 /// Data steps in chronological order. **No coalescing** — the fold replays
821 /// every data step verbatim, exactly as if migrations were applied in order.
822 std::vector<DataStep> dataSteps;
823
824 /// Releases declared via `LIGHTWEIGHT_SQL_RELEASE` that fall within the fold range.
825 std::vector<MigrationRelease> releases;
826
827 /// Migrations that contributed to the fold (timestamp + title). Used by emitters
828 /// to write a header comment explaining what was collapsed.
829 std::vector<std::pair<MigrationTimestamp, std::string>> foldedMigrations;
830 };
831
832 /// @brief Pure plan-walk that folds the effect of every registered migration.
833 ///
834 /// Visits each migration's `Up()` plan in timestamp order (or up to
835 /// `upToInclusive` if provided) and accumulates the cumulative end-state into a
836 /// `PlanFoldingResult`. **Never** executes SQL or touches a database connection
837 /// — the supplied formatter is only used to build the in-memory plan elements.
838 ///
839 /// @param formatter Formatter used by the migration query builder while walking
840 /// each migration's `Up()`. Any standard formatter works; the
841 /// walk inspects plan element shapes, not rendered SQL.
842 /// @param upToInclusive If set, fold only migrations with `timestamp <= upToInclusive`.
843 /// If unset, fold all registered migrations.
844 ///
845 /// @return The folded snapshot. Safe to call without a `MigrationManager`-attached
846 /// data mapper.
847 [[nodiscard]] LIGHTWEIGHT_API PlanFoldingResult FoldRegisteredMigrations(
848 SqlQueryFormatter const& formatter, std::optional<MigrationTimestamp> upToInclusive = std::nullopt) const;
849
850 /// @brief Result of a `HardReset` call.
852 {
853 /// True when the populating call was a dry-run; the result describes the
854 /// would-be plan and no DDL was issued.
855 bool wasDryRun = false;
856 /// Tables the registered migrations *would* have created and were also present
857 /// in the live DB — these were dropped (or would be dropped on a real run).
858 std::vector<SqlSchema::FullyQualifiedTableName> droppedTables;
859 /// Tables registered migrations declare but that aren't in the live DB —
860 /// nothing to do for these. Reported for visibility only.
861 std::vector<SqlSchema::FullyQualifiedTableName> absentTables;
862 /// Tables in the live DB the registered migrations don't know about — left
863 /// alone (user-owned). Reported prominently so operators notice.
864 std::vector<SqlSchema::FullyQualifiedTableName> preservedTables;
865 /// Whether the `schema_migrations` table itself was dropped (always true on a
866 /// real run, false on dry-run).
868 /// Whether `schema_backups` exists and was dropped (or, in a dry run, would be), with every
869 /// backup, restore and waived record in it.
871 };
872
873 /// @brief Drops every table the registered migrations would create (incl.
874 /// `schema_migrations`), preserving any user-created tables.
875 ///
876 /// Folds all registered migrations to compute the migration-owned set, intersects
877 /// with the live schema (via `SqlSchema::ReadAllTables`), then drops the matching
878 /// live tables in **reverse** creation order with `cascade=true ifExists=true`. The
879 /// `schema_migrations` table is dropped explicitly because it's created outside the
880 /// registered-migrations stream.
881 ///
882 /// Tables present in the live DB but not in the migration plan are left alone and
883 /// reported under `preservedTables` so operators can spot them.
884 ///
885 /// Also drops `schema_backups`: its records describe backups of the migration state being discarded.
886 ///
887 /// @param mode `ExecutionMode::DryRun` returns the would-be plan without writing anything.
888 [[nodiscard]] LIGHTWEIGHT_API HardResetResult HardReset(ExecutionMode mode = ExecutionMode::Execute);
889
890 /// @brief One column upgrade entry in `UnicodeUpgradeResult`.
892 {
893 /// Fully-qualified name of the table that owns the column.
894 SqlSchema::FullyQualifiedTableName table;
895 /// Name of the column being upgraded.
896 std::string column;
897 /// Live byte-counted type the column currently has.
898 SqlColumnTypeDefinition liveType;
899 /// Char-counted type the migrations now declare for this column.
900 SqlColumnTypeDefinition intendedType;
901 /// Whether the column is nullable (preserved across the type rewrite).
902 bool nullable = true;
903 /// Empty for a column the migrations declare wide. Otherwise the column is upgraded only
904 /// because a foreign key joins it to an upgraded column, named here as `table.column`,
905 /// or as `schema.table.column` when the upgrade spans several schemas.
906 std::string joinedByForeignKeyTo {};
907 };
908
909 /// @brief Result of an `UnicodeUpgradeTables` call.
911 {
912 /// True when the populating call was a dry-run; the result describes the
913 /// would-be diff and no DDL was issued.
914 bool wasDryRun = false;
915 /// Columns whose live type drifted from the intended type and were upgraded
916 /// (or would be on a real run).
917 std::vector<ColumnUpgradeEntry> columns;
918 /// Foreign keys that had to be dropped + re-added to upgrade their
919 /// participating columns. Reported so operators see the FK churn.
920 std::vector<SqlCompositeForeignKeyConstraint> rebuiltForeignKeys;
921 /// SQL Server: every object that depends on an upgraded column — keys, unique,
922 /// foreign-key, default and check constraints, indexes, statistics — and is
923 /// dropped and recreated under its original name around the type change.
924 std::vector<std::string> rebuiltObjects;
925 /// The tables in the order they are upgraded, grouped into units: the tables a
926 /// foreign key joins through an upgraded column share one unit, and each unit is
927 /// upgraded and committed in a transaction of its own.
928 std::vector<std::vector<SqlSchema::FullyQualifiedTableName>> units;
929 /// True when the call stopped after a committed unit because its progress task asked to cancel;
930 /// a later call continues with the next unit.
931 bool wasCancelled = false;
932 /// Units this call committed: all of `units` unless it was cancelled, 0 for a dry run. A failing call
933 /// throws and returns no result.
934 std::size_t committedUnitCount = 0;
935 };
936
937 /// @brief Rewrites legacy `VARCHAR/CHAR` columns to `NVARCHAR/NCHAR` where the
938 /// registered migrations now declare wide types.
939 ///
940 /// Compares the folded plan's intended column types against `SqlSchema::ReadAllTables`
941 /// output; an upgrade is triggered iff intended is `NVarchar`/`NChar` AND live is
942 /// `Varchar`/`Char` with the same `size`. Foreign keys that touch any upgrade column
943 /// are dropped before the alter and re-added afterwards; SQL Server and PostgreSQL read
944 /// them from the catalog and recreate them under their own names with their own
945 /// definitions (referenced schema, actions), whoever created them. Cross-backend; SQLite
946 /// uses the in-tree `RebuildSqliteTable` recipe under the hood.
947 ///
948 /// Declared names match live ones without regard to case where the dialect's identifiers
949 /// are case-insensitive (`SqlQueryFormatter::IdentifiersAreCaseInsensitive`).
950 ///
951 /// The live tables of every schema are read. A declared table resolves to the schema its
952 /// migration names; else to the first schema holding that name in the connection's name
953 /// resolution order (SQL Server: the user's default schema, then `dbo`; PostgreSQL: the
954 /// search path); else to the only schema holding it. Each column is altered, and its
955 /// dependents found, in the schema it was resolved to. When several schemas hold a declared
956 /// name, none of them is in that order and one has a column to widen, the call throws
957 /// `std::runtime_error` naming them.
958 ///
959 /// Where a foreign key needs both sides to share one text type (SQL Server), every column
960 /// a foreign key joins to an upgraded column is upgraded too, declared or not, and reported
961 /// with `ColumnUpgradeEntry::joinedByForeignKeyTo` set. Such a column must have the same narrow
962 /// type and size and must not be declared with another type; otherwise the call throws
963 /// `std::runtime_error` naming it, in a dry run too, before anything has changed.
964 ///
965 /// Each unit (see `UnicodeUpgradeResult::units`) runs in a transaction of its own and is
966 /// committed before the next one starts; the log sink reports each committed unit. Under
967 /// the simple recovery model, or while log backups run, the transaction log then only has
968 /// to hold one unit, so the largest unit sets its peak. A failing unit is rolled back and
969 /// stops the call with `UnicodeUpgradeUnitError`, which names it and says how many units
970 /// were committed before it. A later call continues where this one stopped, as the plan is
971 /// computed from the live schema.
972 ///
973 /// A unit that fails is reported as failed on its tasks (the failing table's, the unit's and the
974 /// overall one) before the call throws.
975 ///
976 /// @param mode `ExecutionMode::DryRun` returns the would-be diff without writing anything and
977 /// reports only the plan to @p progress.
978 /// @param progress Receives planning, an overall task weighted by the tables' row counts (SQL
979 /// Server: `sys.partitions`; PostgreSQL: `pg_class.reltuples`; otherwise
980 /// `COUNT(*)`), one task per unit and per table; null reports nothing. Cancellation is
981 /// checked after each committed unit except the last; see `UnicodeUpgradeResult::wasCancelled`.
983 ProgressTask* progress = nullptr);
984
985 /// @brief Re-stamps `schema_migrations.checksum` rows that have drifted.
986 ///
987 /// Applied migrations whose stored checksum no longer matches the current
988 /// `MigrationBase::ComputeChecksum` output are updated in-place. Migrations that
989 /// match are left alone. Rows that reference a migration which is no longer
990 /// registered (e.g. removed from the codebase) are surfaced via
991 /// `RewriteChecksumsResult.unregisteredTimestamps` and *not* touched.
992 ///
993 /// This is a recovery tool used when a regen of generated migrations changes
994 /// their byte-level shape but not their logical effect — running it without
995 /// understanding *why* the checksum drifted would defeat the integrity check.
996 ///
997 /// @param dryRun When true, returns the would-be diff without writing anything.
998 [[nodiscard]] LIGHTWEIGHT_API RewriteChecksumsResult RewriteChecksums(bool dryRun = false);
999
1000 /// Preview SQL statements for all pending migrations without executing.
1001 ///
1002 /// This is useful for dry-run mode to see what SQL would be executed.
1003 ///
1004 /// @param feedbackCallback Optional callback to be called for each migration.
1005 /// @return Vector of all SQL statements that would be executed.
1006 [[nodiscard]] LIGHTWEIGHT_API std::vector<std::string> PreviewPendingMigrations(
1007 ExecuteCallback const& feedbackCallback = {}) const;
1008
1009 /// Preview SQL for pending migrations whose timestamp is <= `targetInclusive`.
1010 ///
1011 /// Bounded counterpart of `PreviewPendingMigrations`. Same dependency
1012 /// rules as `ApplyPendingMigrationsUpTo`: if any included migration
1013 /// depends on an excluded pending migration, this throws.
1014 ///
1015 /// @param targetInclusive Highest timestamp to preview (inclusive).
1016 /// @param feedbackCallback Optional callback to be called for each migration.
1017 /// @return Vector of all SQL statements that would be executed.
1018 /// @throws std::runtime_error if a dependency would cross the boundary.
1019 [[nodiscard]] LIGHTWEIGHT_API std::vector<std::string> PreviewPendingMigrationsUpTo(
1020 MigrationTimestamp targetInclusive, ExecuteCallback const& feedbackCallback = {}) const;
1021
1022 /// Verify checksums of all applied migrations.
1023 ///
1024 /// Compares the stored checksums in the database with the computed checksums
1025 /// of the current migration definitions. This helps detect if migrations
1026 /// have been modified after they were applied.
1027 ///
1028 /// @return Vector of verification results for migrations with mismatched or missing checksums.
1029 [[nodiscard]] LIGHTWEIGHT_API std::vector<ChecksumVerificationResult> VerifyChecksums() const;
1030
1031 /// Mark a migration as applied without executing its Up() method.
1032 ///
1033 /// This is useful for:
1034 /// - Baseline migrations when setting up an existing database
1035 /// - Marking migrations that were applied manually or through other means
1036 /// - Skipping migrations that are not applicable to a specific environment
1037 ///
1038 /// @param migration The migration to mark as applied.
1039 /// @throws std::runtime_error if the migration is already applied.
1040 LIGHTWEIGHT_API void MarkMigrationAsApplied(MigrationBase const& migration);
1041
1042 /// Revert all migrations applied after the target timestamp.
1043 ///
1044 /// This method reverts migrations in reverse chronological order,
1045 /// rolling back from the most recent to just after the target.
1046 /// The target migration itself is NOT reverted.
1047 ///
1048 /// @param target Target timestamp to revert to. Migrations > target are reverted.
1049 /// @param feedbackCallback Optional callback for progress updates.
1050 /// @return Result containing list of reverted timestamps or error information.
1051 /// @note Stops on first error. Previously reverted migrations in this call are NOT rolled back.
1052 [[nodiscard]] LIGHTWEIGHT_API RevertResult RevertToMigration(MigrationTimestamp target,
1053 ExecuteCallback const& feedbackCallback = {});
1054
1055 /// Get a summary status of all migrations.
1056 ///
1057 /// This method provides a quick overview of the migration state without
1058 /// needing to iterate through individual migrations.
1059 ///
1060 /// @return Status struct with counts of applied, pending, and mismatched migrations.
1061 [[nodiscard]] LIGHTWEIGHT_API MigrationStatus GetMigrationStatus() const;
1062
1063 /// Validate that all registered migrations have satisfiable dependencies.
1064 ///
1065 /// For each pending migration, verifies that every declared dependency is either
1066 /// already applied or itself pending (so it will be applied first due to topological
1067 /// ordering). Also detects cycles among pending migrations.
1068 ///
1069 /// @throws std::runtime_error if any dependency is unresolved or a cycle is found.
1070 LIGHTWEIGHT_API void ValidateDependencies() const;
1071
1072 /// Register a release marker for a software version.
1073 ///
1074 /// Typically called from `LIGHTWEIGHT_SQL_RELEASE(version, timestamp)` at static init time.
1075 /// Releases are ordered by `highestTimestamp`; two releases may not share the same
1076 /// `highestTimestamp`, and the same version string may not be registered twice.
1077 ///
1078 /// @param version Human-readable version, e.g. "6.7.0".
1079 /// @param highestTimestamp Highest migration timestamp (inclusive) that belongs to this release.
1080 /// @param options Release options, e.g. `{ .requiresBackup = true }`.
1081 /// @throws std::runtime_error on duplicate version or duplicate timestamp.
1082 LIGHTWEIGHT_API void RegisterRelease(std::string version,
1083 MigrationTimestamp highestTimestamp,
1084 MigrationReleaseOptions options = {});
1085
1086 /// Remove all registered releases. Useful for resetting state in tests.
1087 LIGHTWEIGHT_API void RemoveAllReleases();
1088
1089 /// Get all registered releases, sorted ascending by `highestTimestamp`.
1090 [[nodiscard]] LIGHTWEIGHT_API std::vector<MigrationRelease> const& GetAllReleases() const noexcept;
1091
1092 /// Find a release by exact version string match.
1093 ///
1094 /// @return Pointer to the matching release, or nullptr if no release with that version is registered.
1095 [[nodiscard]] LIGHTWEIGHT_API MigrationRelease const* FindReleaseByVersion(std::string_view version) const noexcept;
1096
1097 /// Find the release whose timestamp range contains `timestamp`.
1098 ///
1099 /// Returns the release with the smallest `highestTimestamp` that is `>= timestamp`.
1100 /// Returns nullptr if `timestamp` is greater than every registered release's highestTimestamp
1101 /// (i.e., the migration is post-all-releases / unreleased).
1102 [[nodiscard]] LIGHTWEIGHT_API MigrationRelease const* FindReleaseForTimestamp(
1103 MigrationTimestamp timestamp) const noexcept;
1104
1105 /// Get all registered migrations belonging to a given release.
1106 ///
1107 /// A migration `M` belongs to release `R` iff
1108 /// `prev_release_ts < M.timestamp <= R.highestTimestamp`, where `prev_release_ts` is the
1109 /// previous release's timestamp (or 0 if `R` is the first release).
1110 ///
1111 /// @param version The version string to look up.
1112 /// @return Migrations in the release, ordered by timestamp. Empty if the version is unknown.
1113 [[nodiscard]] LIGHTWEIGHT_API MigrationList GetMigrationsForRelease(std::string_view version) const;
1114
1115 /// Whether the work a command will run needs a backup first, and whether one exists.
1116 ///
1117 /// Only that work counts: a migration of @p toRun demands a backup when it `RequiresBackup()` or
1118 /// its release declares `requiresBackup`, and a registered pre-migrate step when its
1119 /// `requiresBackup` predicate returns true (evaluated here, on the migration connection). Pass
1120 /// all pending migrations for `migrate`, the pending ones up to the release for
1121 /// `migrate-to-release`, the named one for `apply`. Only when something demands a backup are
1122 /// `schema_backups` and `schema_migrations` read: the newest `backup` or `restore` record must
1123 /// cover the database's current state (see `BackupRecordCovers`).
1124 ///
1125 /// @param toRun The migrations the command will run.
1126 /// @return The requirement; `required` is false when nothing demands a backup.
1127 /// @throws whatever a step's predicate throws.
1128 [[nodiscard]] LIGHTWEIGHT_API BackupRequirement BackupRequirementFor(std::span<MigrationTimestamp const> toRun);
1129
1130 private:
1131 /// Return the pending list in dependency-respecting order.
1132 ///
1133 /// Dependencies among pending migrations are resolved via topological sort.
1134 /// Migrations with no dependency between them keep timestamp order.
1135 /// Throws on missing deps or cycles.
1136 [[nodiscard]] MigrationList TopoSortPending(MigrationList pending,
1137 std::vector<MigrationTimestamp> const& applied) const;
1138
1139 /// @brief Builds a fresh, policy-agnostic render context. The per-migration
1140 /// compat knobs are layered on by `ApplySingleMigration` / `PreviewMigrationWithContext`
1141 /// before rendering a given migration's steps.
1142 [[nodiscard]] static MigrationRenderContext MakeRenderContext();
1143
1144 /// @brief Materialises any pending virtual-applied entries as real rows
1145 /// in `schema_migrations`. Creates the history table if necessary, then
1146 /// inserts one row per overlay entry that maps to a registered
1147 /// migration and is not yet present in the table. Clears the overlay on
1148 /// success. Called as the first action of every write path so the
1149 /// physical table only exists when there is at least one row to put in
1150 /// it.
1151 void PersistVirtualAppliedMigrations();
1152
1153 /// @brief Counts the rows a committed migration's `IfNotExists` inserts skipped and logs
1154 /// each of @p skippedRows via @ref Log.
1155 void ReportSkippedRows(std::vector<std::string> const& skippedRows);
1156
1157 /// A registered pre-migrate step with its code.
1158 struct RegisteredPreMigrateStep
1159 {
1160 std::string title;
1161 PreMigrateStepOptions options;
1162 PreMigrateStep run;
1163 };
1164
1165 MigrationList _migrations;
1166 std::vector<MigrationRelease> _releases;
1167 std::unique_ptr<DataMapper> _dataMapper;
1168 /// The @ref SqlConnection::DefaultConnectionStringGeneration @c _dataMapper was connected under.
1169 std::uint32_t _dataMapperGeneration {};
1170 CompatPolicy _compatPolicy;
1171 std::string _defaultSchema; ///< Default schema applied to migration connections, empty when unset.
1172
1173 /// @brief In-memory overlay of applied migration timestamps that are
1174 /// not (yet) persisted to `schema_migrations`. Populated by plugin
1175 /// post-init hooks via `AddVirtualAppliedMigrations`; drained into the
1176 /// table by `PersistVirtualAppliedMigrations` whenever the manager
1177 /// transitions from a read-only state into actually writing. Always
1178 /// kept sorted ascending and deduplicated.
1179 std::vector<MigrationTimestamp> _virtualAppliedIds;
1180
1181 LogSink _logSink; ///< Optional status-message sink. Empty by default.
1182
1183 std::size_t _skippedExistingRowCount {}; ///< Rows skipped by `IfNotExists` inserts so far.
1184
1185 /// Registered pre-migrate steps, in registration order.
1186 std::vector<RegisteredPreMigrateStep> _preMigrateSteps;
1187 };
1188
1189/// Requires the user to call LIGHTWEIGHT_MIGRATION_PLUGIN() in exactly one CPP file of the migration plugin.
1190///
1191/// @ingroup SqlMigration
1192#define LIGHTWEIGHT_MIGRATION_PLUGIN() \
1193 /* NOLINTNEXTLINE(bugprone-macro-parentheses) */ \
1194 extern "C" LIGHTWEIGHT_EXPORT Lightweight::SqlMigration::MigrationManager* AcquireMigrationManager() \
1195 { \
1196 return &Lightweight::SqlMigration::MigrationManager::GetInstance(); \
1197 }
1198
1199/// Defines the non-throwing exported C entry point @p entryPoint of a plugin hook, which runs the
1200/// body that follows the macro as @p body and hands the message of an exception it throws to the
1201/// host's error sink, returning false; no exception crosses the shared-library boundary. Use
1202/// `LIGHTWEIGHT_MIGRATION_PLUGIN_POSTINIT` or `LIGHTWEIGHT_MIGRATION_PLUGIN_POSTMIGRATE` instead.
1203#define LIGHTWEIGHT_DETAIL_MIGRATION_PLUGIN_HOOK(entryPoint, body, connArg, mgrArg) \
1204 static void body(Lightweight::SqlConnection&, Lightweight::SqlMigration::MigrationManager&); \
1205 extern "C" LIGHTWEIGHT_EXPORT bool entryPoint(Lightweight::SqlConnection& connection, \
1206 Lightweight::SqlMigration::MigrationManager& manager, \
1207 Lightweight::SqlMigration::PluginHookErrorSink reportError, \
1208 void* errorContext) noexcept \
1209 { \
1210 try \
1211 { \
1212 body(connection, manager); \
1213 return true; \
1214 } \
1215 catch (std::exception const& exception) \
1216 { \
1217 if (reportError != nullptr) \
1218 reportError(errorContext, exception.what()); \
1219 } \
1220 catch (...) \
1221 { \
1222 if (reportError != nullptr) \
1223 reportError(errorContext, "unknown exception"); \
1224 } \
1225 return false; \
1226 } \
1227 static void body(Lightweight::SqlConnection&(connArg), Lightweight::SqlMigration::MigrationManager&(mgrArg))
1228
1229/// Optional hook executed once per plugin by dbtool, after migration history has been
1230/// created and an active connection is available on the central manager. Plugins use
1231/// this to perform one-shot bridging work that needs both a live `SqlConnection` and
1232/// the merged `MigrationManager` — for example, importing legacy version tracking
1233/// from a custom table into `schema_migrations`.
1234///
1235/// A hook body that throws is reported by the host and does not stop it. The macro exports the
1236/// non-throwing C entry point `LightweightMigrationPluginPostInitReported`, which catches the
1237/// exception and hands its message to the host, so no exception crosses the shared-library
1238/// boundary, whatever exception model the plugin uses. Hosts skip, with a warning, a plugin that
1239/// exports only the earlier `LightweightMigrationPluginPostInit`; rebuild such a plugin.
1240///
1241/// Use as:
1242///
1243/// @code
1244/// LIGHTWEIGHT_MIGRATION_PLUGIN_POSTINIT(connection, manager)
1245/// {
1246/// MyPlugin::ImportLegacyVersions(manager, connection);
1247/// }
1248/// @endcode
1249///
1250/// @ingroup SqlMigration
1251#define LIGHTWEIGHT_MIGRATION_PLUGIN_POSTINIT(connArg, mgrArg) \
1252 LIGHTWEIGHT_DETAIL_MIGRATION_PLUGIN_HOOK( \
1253 LightweightMigrationPluginPostInitReported, LightweightMigrationPluginPostInitBody, connArg, mgrArg)
1254
1255/// Optional hook executed once per plugin by dbtool and dbtool-gui after a successful
1256/// `migrate`, `migrate-to-release` or `apply`, including runs where nothing was pending.
1257/// It runs on the migration connection while the migration lock is still held (unless the
1258/// caller opted out of locking, as with `dbtool --no-lock`), and never for dry runs,
1259/// read-only commands or a failed migration. Plugins use it for work that
1260/// needs the migrated schema, such as seeding reference data.
1261///
1262/// A hook body that throws makes the command fail; the migrations it ran after stay applied.
1263/// The macro exports a non-throwing C entry point that catches the exception and hands its
1264/// message to the host, so no exception crosses the shared-library boundary.
1265///
1266/// Use as:
1267///
1268/// @code
1269/// LIGHTWEIGHT_MIGRATION_PLUGIN_POSTMIGRATE(connection, manager)
1270/// {
1271/// MyPlugin::SeedReferenceData(connection);
1272/// }
1273/// @endcode
1274///
1275/// @ingroup SqlMigration
1276#define LIGHTWEIGHT_MIGRATION_PLUGIN_POSTMIGRATE(connArg, mgrArg) \
1277 LIGHTWEIGHT_DETAIL_MIGRATION_PLUGIN_HOOK( \
1278 LightweightMigrationPluginPostMigrate, LightweightMigrationPluginPostMigrateBody, connArg, mgrArg)
1279
1280 /// Host-supplied sink that a plugin's post-init or post-migrate entry point reports a failure
1281 /// message to. The entry point skips the report when the host passes a null sink.
1282 /// @param context The opaque context the host passed alongside the sink.
1283 /// @param message The failure message, only valid for the duration of the call. May be null
1284 /// (an exception whose `what()` returns null), so the sink must handle that.
1285 using PluginHookErrorSink = void (*)(void* context, char const* message) noexcept;
1286
1287 /// The sink of the post-migrate entry point; see `PluginHookErrorSink`.
1288 using PostMigrateErrorSink = PluginHookErrorSink;
1289
1290 /// Represents a single unique SQL migration.
1291 ///
1292 /// @ingroup SqlMigration
1294 {
1295 public:
1296 /// Default copy constructor.
1297 MigrationBase(MigrationBase const&) = default;
1298 MigrationBase(MigrationBase&&) = delete;
1299 /// Default copy assignment operator.
1302 /// Constructs a migration with the given timestamp and title.
1303 MigrationBase(MigrationTimestamp timestamp, std::string_view title):
1304 _timestamp { timestamp },
1305 _title { title }
1306 {
1308 }
1309
1310 /// Unregisters this migration from the singleton manager the constructor registered it with.
1311 ///
1312 /// Registration is a side effect of construction, so deregistration has to be one of
1313 /// destruction: otherwise the manager keeps a pointer to a dead object, and its
1314 /// duplicate-timestamp lookup dereferences it. A later migration reusing that storage is then
1315 /// reported as a duplicate of itself.
1317 {
1319 }
1320
1321 /// Apply the migration.
1322 ///
1323 /// @param plan Query builder to use for building the migration plan.
1324 virtual void Up(SqlMigrationQueryBuilder& plan) const = 0;
1325
1326 /// Revert the migration.
1327 ///
1328 /// @param plan Query builder to use for building the migration plan.
1329 virtual void Down(SqlMigrationQueryBuilder& plan) const
1330 {
1331 (void) plan;
1332 }
1333
1334 /// Check if this migration has a Down() implementation for rollback.
1335 ///
1336 /// This method determines whether the migration can be safely reverted.
1337 /// The default implementation returns false. Derived classes that implement
1338 /// Down() should override this to return true.
1339 ///
1340 /// @return true if Down() is implemented and can revert this migration.
1341 [[nodiscard]] virtual bool HasDownImplementation() const noexcept
1342 {
1343 return false;
1344 }
1345
1346 /// Get the timestamps of migrations that this migration depends on.
1347 ///
1348 /// Dependencies are enforced at apply time: each declared dependency must be
1349 /// registered and applied (or pending in the same run, in dependency order)
1350 /// before this migration will run. The default implementation returns no
1351 /// dependencies, preserving timestamp-ordered apply behavior.
1352 ///
1353 /// @return Vector of dependency timestamps. Empty if this migration has none.
1354 [[nodiscard]] virtual std::vector<MigrationTimestamp> GetDependencies() const
1355 {
1356 return {};
1357 }
1358
1359 /// Get the author of the migration, if any.
1360 ///
1361 /// @return Author string, empty if not set.
1362 [[nodiscard]] virtual std::string_view GetAuthor() const noexcept
1363 {
1364 return {};
1365 }
1366
1367 /// Get the long-form description of the migration, if any.
1368 ///
1369 /// This is a multi-line description in addition to the short title.
1370 ///
1371 /// @return Description string, empty if not set.
1372 [[nodiscard]] virtual std::string_view GetDescription() const noexcept
1373 {
1374 return {};
1375 }
1376
1377 /// Whether running this migration requires a backup of the database taken after its last
1378 /// migration (see `MigrationManager::BackupRequirementFor`). A release's `requiresBackup` adds
1379 /// the same demand for all of its migrations.
1380 ///
1381 /// @return False unless overridden.
1382 [[nodiscard]] virtual bool RequiresBackup() const noexcept
1383 {
1384 return false;
1385 }
1386
1387 /// Get the timestamp of the migration.
1388 ///
1389 /// @return Timestamp of the migration.
1390 [[nodiscard]] MigrationTimestamp GetTimestamp() const noexcept
1391 {
1392 return _timestamp;
1393 }
1394
1395 /// Get the title of the migration.
1396 ///
1397 /// @return Title of the migration.
1398 [[nodiscard]] std::string_view GetTitle() const noexcept
1399 {
1400 return _title;
1401 }
1402
1403 /// Compute SHA-256 checksum of migration's Up() SQL statements.
1404 ///
1405 /// The checksum is computed from the SQL statements that would be executed
1406 /// by this migration. This allows detecting if a migration has been modified
1407 /// after it was applied.
1408 ///
1409 /// @param formatter The SQL query formatter to use for generating SQL.
1410 /// @return SHA-256 hex string (64 characters).
1411 [[nodiscard]] LIGHTWEIGHT_API std::string ComputeChecksum(SqlQueryFormatter const& formatter) const;
1412
1413 private:
1414 MigrationTimestamp _timestamp;
1415 std::string_view _title;
1416 };
1417
1418 /// Represents a single unique SQL migration.
1419 ///
1420 /// This class is a convenience class that can be used to create a migration.
1421 ///
1422 /// @ingroup SqlMigration
1423 /// Optional metadata attached to a Migration at construction time.
1424 ///
1425 /// All fields are optional. Unset fields behave as if the feature
1426 /// were not used (e.g. empty dependencies preserves timestamp order).
1427 ///
1428 /// @ingroup SqlMigration
1430 {
1431 /// Migrations that must be applied before this one.
1432 std::vector<MigrationTimestamp> dependencies {};
1433 /// Author of the migration. Recorded in schema_migrations when the migration is applied.
1434 std::string_view author {};
1435 /// Long-form description. Recorded in schema_migrations when the migration is applied.
1436 std::string_view description {};
1437 /// Whether running the migration requires a backup; see `MigrationBase::RequiresBackup`.
1438 bool requiresBackup = false;
1439 };
1440
1441 template <uint64_t TsValue>
1442 class Migration: public MigrationBase
1443 {
1444 public:
1445 /// The migration's timestamp, available at compile time from the type itself.
1446 ///
1447 /// Exposed so @ref TimestampOf can extract the timestamp without
1448 /// having to repeat the literal at every use site (e.g. in dependency lists).
1449 static constexpr MigrationTimestamp TimeStamp { TsValue };
1450
1451 /// Create a new migration.
1452 ///
1453 /// @param title Title of the migration.
1454 /// @param up Function to apply the migration.
1455 /// @param down Function to revert the migration (optional).
1456 Migration(std::string_view title,
1457 std::function<void(SqlMigrationQueryBuilder&)> const& up,
1458 std::function<void(SqlMigrationQueryBuilder&)> const& down = {}):
1459 MigrationBase(TimeStamp, title),
1460 _up { up },
1461 _down { down }
1462 {
1463 }
1464
1465 /// Create a new migration with optional metadata (dependencies, author, description).
1466 ///
1467 /// @param title Title of the migration.
1468 /// @param metadata Optional metadata including dependencies, author, and description.
1469 /// @param up Function to apply the migration.
1470 /// @param down Function to revert the migration (optional).
1471 Migration(std::string_view title,
1472 MigrationMetadata metadata,
1473 std::function<void(SqlMigrationQueryBuilder&)> const& up,
1474 std::function<void(SqlMigrationQueryBuilder&)> const& down = {}):
1475 MigrationBase(TimeStamp, title),
1476 _up { up },
1477 _down { down },
1478 _metadata { std::move(metadata) }
1479 {
1480 }
1481
1482 /// Apply the migration.
1483 ///
1484 /// @param builder Query builder to use for building the migration plan.
1485 void Up(SqlMigrationQueryBuilder& builder) const override
1486 {
1487 _up(builder);
1488 }
1489
1490 /// Revert the migration.
1491 ///
1492 /// @param builder Query builder to use for building the migration plan.
1493 void Down(SqlMigrationQueryBuilder& builder) const override
1494 {
1495 if (_down)
1496 _down(builder);
1497 }
1498
1499 /// Check if this migration has a Down() implementation.
1500 ///
1501 /// @return true if a down function was provided.
1502 [[nodiscard]] bool HasDownImplementation() const noexcept override
1503 {
1504 return static_cast<bool>(_down);
1505 }
1506
1507 /// Get the declared dependencies for this migration.
1508 [[nodiscard]] std::vector<MigrationTimestamp> GetDependencies() const override
1509 {
1510 return _metadata.dependencies;
1511 }
1512
1513 /// Get the author of this migration.
1514 [[nodiscard]] std::string_view GetAuthor() const noexcept override
1515 {
1516 return _metadata.author;
1517 }
1518
1519 /// Get the long-form description of this migration.
1520 [[nodiscard]] std::string_view GetDescription() const noexcept override
1521 {
1522 return _metadata.description;
1523 }
1524
1525 /// Whether running this migration requires a backup; see `MigrationBase::RequiresBackup`.
1526 [[nodiscard]] bool RequiresBackup() const noexcept override
1527 {
1528 return _metadata.requiresBackup;
1529 }
1530
1531 private:
1532 std::function<void(SqlMigrationQueryBuilder&)> _up;
1533 std::function<void(SqlMigrationQueryBuilder&)> _down;
1534 MigrationMetadata _metadata {};
1535 };
1536
1537 /// Extracts the @ref MigrationTimestamp from a migration type that exposes a static
1538 /// constexpr `TimeStamp` member.
1539 ///
1540 /// Works with both the class template @ref Migration and the struct generated by the
1541 /// @ref LIGHTWEIGHT_SQL_MIGRATION macro. Use via `decltype`:
1542 ///
1543 /// @code
1544 /// auto migrationA = SqlMigration::Migration<202601010001>("dep base", [](auto&){ ... });
1545 /// auto tsA = TimestampOf<decltype(migrationA)>; // MigrationTimestamp { 202601010001 }
1546 /// @endcode
1547 ///
1548 /// @tparam T A migration type exposing `static constexpr MigrationTimestamp TimeStamp`.
1549 template <typename T>
1550 inline constexpr MigrationTimestamp TimestampOf = T::TimeStamp;
1551
1552 namespace detail
1553 {
1554 /// RAII registrar used by `LIGHTWEIGHT_SQL_RELEASE` to register a release with the
1555 /// migration manager at static-initialization time. Not intended for direct use.
1556 ///
1557 /// @ingroup SqlMigration
1558 struct ReleaseRegistrar
1559 {
1560 /// Registers `{version, highestTimestamp, options}` with the singleton manager.
1561 ReleaseRegistrar(std::string version, MigrationTimestamp highestTimestamp, MigrationReleaseOptions options = {})
1562 {
1563 MigrationManager::GetInstance().RegisterRelease(std::move(version), highestTimestamp, options);
1564 }
1565
1566 /// Registers `{version, highestTimestamp, options}` with the singleton manager.
1567 ReleaseRegistrar(std::string version, std::uint64_t highestTimestamp, MigrationReleaseOptions options = {})
1568 {
1570 std::move(version), MigrationTimestamp { highestTimestamp }, options);
1571 }
1572 };
1573 } // namespace detail
1574
1575} // namespace SqlMigration
1576
1577} // namespace Lightweight
1578
1579#define _LIGHTWEIGHT_CONCATENATE(s1, s2) s1##s2
1580#define _LIGHTWEIGHT_CONCATENATE_INNER(s1, s2) _LIGHTWEIGHT_CONCATENATE(s1, s2)
1581
1582/// @brief Represents the C++ migration object for a given timestamped migration.
1583/// @param timestamp Timestamp of the migration.
1584///
1585/// @ingroup SqlMigration
1586#define LIGHTWEIGHT_MIGRATION_INSTANCE(timestamp) migration_##timestamp
1587
1588/// @brief Creates a new migration.
1589/// @param timestamp Timestamp of the migration.
1590/// @param description Description of the migration.
1591///
1592/// @note The migration will be registered with the migration manager automatically.
1593///
1594/// @code
1595/// #include <Lightweight/SqlMigration.hpp>
1596///
1597/// LIGHTWEIGHT_SQL_MIGRATION(20260117234120, "Create table 'MyTable'")
1598/// {
1599/// // Use 'plan' to define the migration steps, for example creating tables.
1600/// }
1601/// @endcode
1602///
1603/// @see Lightweight::SqlMigrationQueryBuilder
1604///
1605/// @ingroup SqlMigration
1606#define LIGHTWEIGHT_SQL_MIGRATION(timestamp, description) \
1607 struct Migration_##timestamp: public Lightweight::SqlMigration::MigrationBase \
1608 { \
1609 /** The migration's timestamp, exposed so @ref TimestampOf can extract it from the type. */ \
1610 static constexpr Lightweight::SqlMigration::MigrationTimestamp TimeStamp { static_cast<uint64_t>(timestamp) }; \
1611 explicit Migration_##timestamp(): \
1612 Lightweight::SqlMigration::MigrationBase(TimeStamp, description) \
1613 { \
1614 } \
1615 \
1616 void Up(Lightweight::SqlMigrationQueryBuilder& plan) const override; \
1617 void Down(Lightweight::SqlMigrationQueryBuilder& /*plan*/) const override {} \
1618 }; \
1619 \
1620 static Migration_##timestamp _LIGHTWEIGHT_CONCATENATE(migration_, timestamp); \
1621 \
1622 void Migration_##timestamp::Up(Lightweight::SqlMigrationQueryBuilder& plan) const
1623
1624/// @brief Creates a new reversible migration whose `Down()` is defined out-of-line
1625/// via @ref LIGHTWEIGHT_SQL_MIGRATION_DOWN.
1626///
1627/// Use this variant when the migration needs to be reversible. Follow the macro with
1628/// the `Up()` body, then later use `LIGHTWEIGHT_SQL_MIGRATION_DOWN(timestamp) { ... }`
1629/// to provide the `Down()` body.
1630///
1631/// @param timestamp Timestamp of the migration.
1632/// @param description Description of the migration.
1633///
1634/// @code
1635/// LIGHTWEIGHT_SQL_MIGRATION_REVERSIBLE(20260117234120, "Create table 'MyTable'")
1636/// {
1637/// plan.CreateTable("MyTable").PrimaryKey("id", Integer());
1638/// }
1639///
1640/// LIGHTWEIGHT_SQL_MIGRATION_DOWN(20260117234120)
1641/// {
1642/// plan.DropTable("MyTable");
1643/// }
1644/// @endcode
1645///
1646/// @ingroup SqlMigration
1647#define LIGHTWEIGHT_SQL_MIGRATION_REVERSIBLE(timestamp, description) \
1648 struct Migration_##timestamp: public Lightweight::SqlMigration::MigrationBase \
1649 { \
1650 explicit Migration_##timestamp(): \
1651 Lightweight::SqlMigration::MigrationBase(Lightweight::SqlMigration::MigrationTimestamp { timestamp }, \
1652 description) \
1653 { \
1654 } \
1655 \
1656 void Up(Lightweight::SqlMigrationQueryBuilder& plan) const override; \
1657 void Down(Lightweight::SqlMigrationQueryBuilder& plan) const override; \
1658 \
1659 [[nodiscard]] bool HasDownImplementation() const noexcept override \
1660 { \
1661 return true; \
1662 } \
1663 }; \
1664 \
1665 static Migration_##timestamp _LIGHTWEIGHT_CONCATENATE(migration_, timestamp); \
1666 \
1667 void Migration_##timestamp::Up(Lightweight::SqlMigrationQueryBuilder& plan) const
1668
1669/// @brief Companion to @ref LIGHTWEIGHT_SQL_MIGRATION_REVERSIBLE — defines the out-of-line `Down()` body.
1670///
1671/// Must be used in the same translation unit and follow the matching
1672/// `LIGHTWEIGHT_SQL_MIGRATION_REVERSIBLE(timestamp, ...)` declaration.
1673///
1674/// @param timestamp Timestamp of the migration whose Down() body follows.
1675///
1676/// @ingroup SqlMigration
1677#define LIGHTWEIGHT_SQL_MIGRATION_DOWN(timestamp) \
1678 void Migration_##timestamp::Down(Lightweight::SqlMigrationQueryBuilder& plan) const
1679
1680/// @brief Associates a software release (version string) with the highest migration timestamp
1681/// present at the time of that release.
1682///
1683/// Declare one `LIGHTWEIGHT_SQL_RELEASE` per cut release, alongside the migrations that belong to it.
1684/// The macro registers with the migration manager at static-initialization time. Multiple releases
1685/// may coexist in the same translation unit.
1686///
1687/// @param version A string literal, e.g. `"2.0.0"`.
1688/// @param ... The highest migration timestamp of the release, optionally followed by its
1689/// `MigrationReleaseOptions`, e.g. `{ .requiresBackup = true }`.
1690///
1691/// @code
1692/// LIGHTWEIGHT_SQL_RELEASE("1.0.0", 20260101120000);
1693/// LIGHTWEIGHT_SQL_RELEASE("2.0.0", 20260501120000, { .requiresBackup = true });
1694/// @endcode
1695///
1696/// @ingroup SqlMigration
1697#define LIGHTWEIGHT_SQL_RELEASE(version, ...) \
1698 static ::Lightweight::SqlMigration::detail::ReleaseRegistrar _LIGHTWEIGHT_CONCATENATE_INNER(_lw_release_, __COUNTER__) \
1699 { \
1700 (version), __VA_ARGS__ \
1701 }
Main API for mapping records to and from the database using high level C++ syntax.
Progress of one task, which the library reports and a host (a CLI, a GUI) renders.
Represents a connection to a SQL database.
Query builder for building SQL migration queries.
Definition Migrate.hpp:521
virtual void Up(SqlMigrationQueryBuilder &plan) const =0
MigrationTimestamp GetTimestamp() const noexcept
std::string_view GetTitle() const noexcept
virtual std::vector< MigrationTimestamp > GetDependencies() const
virtual std::string_view GetAuthor() const noexcept
virtual std::string_view GetDescription() const noexcept
virtual bool RequiresBackup() const noexcept
MigrationBase(MigrationBase const &)=default
Default copy constructor.
LIGHTWEIGHT_API std::string ComputeChecksum(SqlQueryFormatter const &formatter) const
MigrationBase(MigrationTimestamp timestamp, std::string_view title)
Constructs a migration with the given timestamp and title.
virtual void Down(SqlMigrationQueryBuilder &plan) const
virtual bool HasDownImplementation() const noexcept
MigrationBase & operator=(MigrationBase const &)=default
Default copy assignment operator.
std::string const & GetMigrationTitle() const noexcept
Human-readable title of the failing migration.
Operation GetOperation() const noexcept
Whether the failure occurred while applying or reverting.
std::string const & GetFailedSql() const noexcept
The exact SQL statement that the driver rejected.
std::string const & GetDriverMessage() const noexcept
Operation
Whether the failure happened while applying (Up) or reverting (Down).
MigrationTimestamp GetMigrationTimestamp() const noexcept
Timestamp of the failing migration.
LIGHTWEIGHT_API MigrationException(Operation operation, MigrationTimestamp timestamp, std::string title, std::size_t stepIndex, std::string failedSql, SqlErrorInfo driverError)
std::size_t GetStepIndex() const noexcept
Zero-based step index inside the plan of the failing migration.
LIGHTWEIGHT_API void MarkMigrationAsApplied(MigrationBase const &migration)
LIGHTWEIGHT_API void ComposeCompatPolicy(CompatPolicy policy)
Composes an additional policy with the currently installed one. If no policy is installed,...
std::function< void(MigrationBase const &, size_t, size_t)> ExecuteCallback
Callback type invoked during migration execution to report progress.
LIGHTWEIGHT_API DataMapper & GetDataMapper()
LIGHTWEIGHT_API RevertResult RevertToMigration(MigrationTimestamp target, ExecuteCallback const &feedbackCallback={})
LIGHTWEIGHT_API void AddMigration(MigrationBase const *migration)
std::list< MigrationBase const * > MigrationList
Type alias for a list of migration pointers.
LIGHTWEIGHT_API MigrationList const & GetAllMigrations() const noexcept
LIGHTWEIGHT_API void SetDefaultSchema(std::string schema)
Sets the default schema applied to migration connections.
LIGHTWEIGHT_API std::vector< std::string > PreviewPendingMigrationsUpTo(MigrationTimestamp targetInclusive, ExecuteCallback const &feedbackCallback={}) const
LIGHTWEIGHT_API size_t ApplyPendingMigrationsUpTo(MigrationTimestamp targetInclusive, ExecuteCallback const &feedbackCallback={})
LIGHTWEIGHT_API std::vector< std::string > PreviewPendingMigrations(ExecuteCallback const &feedbackCallback={}) const
LIGHTWEIGHT_API void RevertSingleMigration(MigrationBase const &migration)
std::function< void(std::string_view)> LogSink
LIGHTWEIGHT_API std::string_view DefaultSchema() const noexcept
Returns the default schema currently installed on the manager (empty when unset).
LIGHTWEIGHT_API void RemoveAllReleases()
Remove all registered releases. Useful for resetting state in tests.
LIGHTWEIGHT_API PlanFoldingResult FoldRegisteredMigrations(SqlQueryFormatter const &formatter, std::optional< MigrationTimestamp > upToInclusive=std::nullopt) const
Pure plan-walk that folds the effect of every registered migration.
LIGHTWEIGHT_API SqlTransaction Transaction()
LIGHTWEIGHT_API MigrationBase const * GetMigration(MigrationTimestamp timestamp) const noexcept
LIGHTWEIGHT_API std::set< std::string > CompatFlagsFor(MigrationBase const &migration) const
Returns the compat flags the current policy assigns to migration, or an empty set if no policy is ins...
LIGHTWEIGHT_API std::vector< ChecksumVerificationResult > VerifyChecksums() const
LIGHTWEIGHT_API MigrationRelease const * FindReleaseByVersion(std::string_view version) const noexcept
LIGHTWEIGHT_API RewriteChecksumsResult RewriteChecksums(bool dryRun=false)
Re-stamps schema_migrations.checksum rows that have drifted.
LIGHTWEIGHT_API CompatPolicy const & GetCompatPolicy() const noexcept
Returns a view of the installed policy so it can be propagated across managers (plugin → central)....
LIGHTWEIGHT_API void RemoveMigration(MigrationBase const *migration) noexcept
LIGHTWEIGHT_API HardResetResult HardReset(ExecutionMode mode=ExecutionMode::Execute)
Drops every table the registered migrations would create (incl. schema_migrations),...
LIGHTWEIGHT_API void RegisterRelease(std::string version, MigrationTimestamp highestTimestamp, MigrationReleaseOptions options={})
LIGHTWEIGHT_API MigrationRelease const * FindReleaseForTimestamp(MigrationTimestamp timestamp) const noexcept
LIGHTWEIGHT_API MigrationList GetMigrationsForRelease(std::string_view version) const
LIGHTWEIGHT_API std::vector< std::string > PreviewMigration(MigrationBase const &migration) const
LIGHTWEIGHT_API size_t ApplyPendingMigrations(ExecuteCallback const &feedbackCallback={})
LIGHTWEIGHT_API UnicodeUpgradeResult UnicodeUpgradeTables(ExecutionMode mode=ExecutionMode::Execute, ProgressTask *progress=nullptr)
Rewrites legacy VARCHAR/CHAR columns to NVARCHAR/NCHAR where the registered migrations now declare wi...
LIGHTWEIGHT_API void CreateMigrationHistory()
Create the migration history table if it does not exist.
LIGHTWEIGHT_API MigrationStatus GetMigrationStatus() const
LIGHTWEIGHT_API std::list< MigrationBase const * > GetPending() const
LIGHTWEIGHT_API void AddVirtualAppliedMigrations(std::span< MigrationTimestamp const > timestamps)
LIGHTWEIGHT_API void SetCompatPolicy(CompatPolicy policy)
Installs a per-migration compat policy. Pass {} to clear.
LIGHTWEIGHT_API void SetLogSink(LogSink sink)
LIGHTWEIGHT_API void ValidateDependencies() const
LIGHTWEIGHT_API void AddPreMigrateStep(std::string title, PreMigrateStepOptions options, PreMigrateStep step)
LIGHTWEIGHT_API PreMigrateStepsOutcome RunPreMigrateSteps(ProgressTask &parent, ExecutionMode mode)
std::function< std::set< std::string >(MigrationBase const &)> CompatPolicy
Per-migration compat policy.
LIGHTWEIGHT_API void RemoveAllPreMigrateSteps() noexcept
Removes every pre-migrate step.
LIGHTWEIGHT_API void Log(std::string_view message) const
LIGHTWEIGHT_API std::vector< MigrationRelease > const & GetAllReleases() const noexcept
Get all registered releases, sorted ascending by highestTimestamp.
LIGHTWEIGHT_API void ApplySingleMigration(MigrationBase const &migration)
LIGHTWEIGHT_API std::size_t SkippedExistingRowCount() const noexcept
Number of rows Insert(...).IfNotExists(...) steps skipped because a row with the same key values alre...
LIGHTWEIGHT_API std::vector< std::string > PreviewMigrationWithContext(MigrationBase const &migration, MigrationRenderContext &context) const
Variant of PreviewMigration that threads a caller-owned render context so the column-width cache accu...
static LIGHTWEIGHT_API MigrationManager & GetInstance()
LIGHTWEIGHT_API std::vector< MigrationTimestamp > GetAppliedMigrationIds() const
LIGHTWEIGHT_API void ClearVirtualAppliedMigrations()
LIGHTWEIGHT_API std::vector< PreMigrateStepInfo > GetPreMigrateSteps() const
LIGHTWEIGHT_API BackupRequirement BackupRequirementFor(std::span< MigrationTimestamp const > toRun)
PreMigrateStepError(std::string stepTitle, std::string_view cause)
std::string const & StepTitle() const noexcept
std::size_t UnitNumber() const noexcept
The failed unit, counted from 1; the units before it are committed.
std::size_t UnitCount() const noexcept
The number of units of the upgrade.
UnicodeUpgradeUnitError(SqlErrorInfo info, std::size_t unitNumber, std::size_t unitCount)
API to format SQL queries for different SQL dialects.
std::variant< SqlCreateTablePlan, SqlAlterTablePlan, SqlDropTablePlan, SqlCreateIndexPlan, SqlRawSqlPlan, SqlInsertDataPlan, SqlUpdateDataPlan, SqlDeleteDataPlan > SqlMigrationPlanElement
Represents a single SQL migration plan element.
LIGHTWEIGHT_API bool BackupRecordCovers(BackupRecord const &record, MigrationHistoryState const &state) noexcept
LIGHTWEIGHT_API std::optional< BackupRecord > ReadLatestBackupRecord(DataMapper &dataMapper)
constexpr std::string_view BackupRecordTableName
constexpr std::string_view EmptyMigrationHistoryDigest
std::function< void(SqlConnection &, MigrationManager &, ProgressTask &, ExecutionMode)> PreMigrateStep
LIGHTWEIGHT_API std::expected< BackupRecord, SqlErrorInfo > WriteBackupRecord(DataMapper &dataMapper, BackupRecord record)
LIGHTWEIGHT_API bool IsValidSchemaName(std::string_view schema) noexcept
LIGHTWEIGHT_API MigrationHistoryState ReadMigrationHistoryState(DataMapper &dataMapper)
@ Execute
Carry the operation out.
@ DryRun
Report the plan and change nothing.
@ Completed
Every step ran; the migrations may follow.
@ Cancelled
The task asked to cancel; the caller applies no migration.
@ Backup
A verified backup archive was written; stored as backup.
Opt-in behavioural knobs that the ToSql migration-plan renderer honours.
Represents an ODBC SQL error.
Definition SqlError.hpp:32
BackupRecordKind kind
What the record documents.
std::string archiveFileName
UTF-8, without its directory.
std::string archiveSha256
SHA-256 of the archive file, lowercase hex.
std::string toolVersion
The tool that wrote the record, e.g. dbtool 1.2.3.
std::vector< std::string > reasons
What demands it: release <version>, migration <timestamp> "<title>" and step "<title>",...
bool required
Whether anything demands a backup.
bool satisfied
Whether nothing is required or the newest backup or restore record covers the database's current stat...
std::optional< BackupRecord > lastBackup
The newest backup or restore record; waived records are skipped.
MigrationTimestamp timestamp
The timestamp of the verified migration.
std::string computedChecksum
The checksum computed from the current migration definition.
std::string storedChecksum
The checksum stored in the database.
bool matches
Whether the stored and computed checksums match.
std::string_view title
The title of the verified migration.
std::optional< SqlDateTime > lastAppliedAt
The latest applied_at over all recorded migrations; empty when none carries a time....
std::optional< MigrationTimestamp > lastAppliedMigration
Highest applied version; empty when none is recorded. For messages.
One row in the RewriteChecksumsResult.entries list.
std::string_view title
Title of the registered migration (or "(Unknown Migration)").
MigrationTimestamp timestamp
The migration whose stored checksum was rewritten.
std::string oldChecksum
Stored checksum before the rewrite. Empty if there was none.
std::string newChecksum
Stored checksum after the rewrite.
One column upgrade entry in UnicodeUpgradeResult.
SqlColumnTypeDefinition liveType
Live byte-counted type the column currently has.
SqlSchema::FullyQualifiedTableName table
Fully-qualified name of the table that owns the column.
SqlColumnTypeDefinition intendedType
Char-counted type the migrations now declare for this column.
bool nullable
Whether the column is nullable (preserved across the type rewrite).
std::string column
Name of the column being upgraded.
std::vector< SqlSchema::FullyQualifiedTableName > absentTables
std::vector< SqlSchema::FullyQualifiedTableName > preservedTables
std::vector< SqlSchema::FullyQualifiedTableName > droppedTables
One data step (INSERT/UPDATE/DELETE/RawSql) tagged with its source migration.
std::string sourceTitle
Title of the migration that contributed this data step.
SqlMigrationPlanElement element
The plan element itself (INSERT, UPDATE, DELETE, or RawSql).
MigrationTimestamp sourceTimestamp
Timestamp of the migration that contributed this data step.
Per-table state: ordered column declarations + per-table FK list.
std::vector< SqlColumnDeclaration > columns
Ordered column declarations as they would be emitted in CREATE TABLE.
std::vector< SqlCompositeForeignKeyConstraint > compositeForeignKeys
Snapshot of the schema the registered migrations intend to produce.
std::vector< SqlCreateIndexPlan > indexes
Indexes that survive folding (created on tables still present at end).
std::map< SqlSchema::FullyQualifiedTableName, TableState > tables
std::vector< MigrationRelease > releases
Releases declared via LIGHTWEIGHT_SQL_RELEASE that fall within the fold range.
std::vector< SqlSchema::FullyQualifiedTableName > creationOrder
std::vector< std::pair< MigrationTimestamp, std::string > > foldedMigrations
std::vector< MigrationTimestamp > unregisteredTimestamps
Applied rows whose migration is no longer registered.
std::vector< ChecksumRewriteEntry > entries
One entry per rewritten or would-be-rewritten row.
std::vector< SqlCompositeForeignKeyConstraint > rebuiltForeignKeys
std::vector< std::vector< SqlSchema::FullyQualifiedTableName > > units
bool requiresBackup
Whether running the migration requires a backup; see MigrationBase::RequiresBackup.
std::vector< MigrationTimestamp > dependencies
Migrations that must be applied before this one.
std::string_view author
Author of the migration. Recorded in schema_migrations when the migration is applied.
std::string_view description
Long-form description. Recorded in schema_migrations when the migration is applied.
bool requiresBackup
Shorthand for "every migration of the release requires a backup".
MigrationTimestamp highestTimestamp
Highest migration timestamp contained in this release (inclusive).
std::string version
Human-readable version string, e.g. "6.7.0".
size_t mismatchCount
Number of applied migrations with checksum mismatches.
size_t appliedCount
Number of migrations that have been applied.
size_t totalRegistered
Total number of registered migrations.
size_t pendingCount
Number of migrations waiting to be applied.
size_t unknownAppliedCount
Number of applied migrations not found in registered list.
constexpr std::weak_ordering operator<=>(MigrationTimestamp const &other) const noexcept=default
Three-way comparison operator.
uint64_t value
The numeric timestamp value identifying the migration.
std::string title
The step's title, as its task shows it.
bool declaresBackupRequirement
Whether the step has a requiresBackup predicate.
std::function< bool(SqlConnection &, MigrationManager &)> requiresBackup
std::vector< MigrationTimestamp > revertedTimestamps
Successfully reverted migrations.
std::optional< MigrationTimestamp > failedAt
Migration that failed, if any.
std::string sqlState
SQLSTATE diagnostic code from the driver, if available.
std::string errorMessage
Short error message if failed (driver message only)
SQLINTEGER nativeErrorCode
Native driver error code, if available.