Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/sql-migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ plan.AlterTable("users")
| `.DropForeignKey(column)` | Remove foreign key constraint |
| `.RenameTo(newName)` | Rename the table |

> **SQLite note:** SQLite has no native `ALTER TABLE … ALTER COLUMN` or `… ADD/DROP CONSTRAINT`, so
> `.AlterColumn(...)`, `.AddForeignKey(...)`, and `.DropForeignKey(...)` are applied by rebuilding the
> table. That rebuild runs only when the migration is applied through `MigrationManager`
> (`ApplyPendingMigrations()`); the generated `ToSql()` text for these operations is a
> `-- LIGHTWEIGHT_SQLITE_GUARD:` sentinel comment that does nothing if executed directly. Applying such
> a migration via `SqlStatement::MigrateDirect` throws rather than silently skipping the change.

**Conditional operations:**

```cpp
Expand Down
2 changes: 1 addition & 1 deletion src/Lightweight/QueryFormatter/PostgreSqlFormatter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class PostgreSqlFormatter final: public SQLiteQueryFormatter
public:
using SQLiteQueryFormatter::CreateTable;

[[nodiscard]] bool RequiresTableRebuildForForeignKeyChange() const noexcept override
[[nodiscard]] bool RequiresTableRebuildForSchemaChange() const noexcept override
{
return false;
}
Expand Down
51 changes: 39 additions & 12 deletions src/Lightweight/QueryFormatter/SQLiteFormatter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ class SQLiteQueryFormatter: public SqlQueryFormatter
}

public:
/// SQLite has no native `ALTER TABLE … ADD/DROP CONSTRAINT`, so foreign-key
/// changes route through the migration executor's table-rebuild path. The
/// formatter signals that intent by emitting `-- LIGHTWEIGHT_SQLITE_GUARD:`
/// sentinels and overriding this hook so the executor takes the rebuild
/// branch instead of executing the (commented-out) sentinel script directly.
[[nodiscard]] bool RequiresTableRebuildForForeignKeyChange() const noexcept override
/// SQLite has no native `ALTER TABLE … ADD/DROP CONSTRAINT` and no `… ALTER COLUMN`, so foreign-key
/// changes and column type/nullability changes route through the migration executor's table-rebuild
/// path. The formatter signals that intent by emitting `-- LIGHTWEIGHT_SQLITE_GUARD:` sentinels and
/// overriding this hook so the executor takes the rebuild branch instead of executing the
/// (commented-out) sentinel script directly.
[[nodiscard]] bool RequiresTableRebuildForSchemaChange() const noexcept override
{
return true;
}
Expand Down Expand Up @@ -417,6 +417,24 @@ class SQLiteQueryFormatter: public SqlQueryFormatter
}

private:
/// @brief Escape an identifier for embedding inside a `"..."` field of a `LIGHTWEIGHT_SQLITE_GUARD`
/// sentinel by doubling embedded double-quotes, so a name containing `"` cannot desync the field
/// parsing on the executor side (which decodes `""` back to `"`).
/// @param identifier The raw identifier (table or column name).
/// @return The escaped text to place between the surrounding sentinel quotes.
[[nodiscard]] static std::string EscapeSentinelField(std::string_view identifier)
{
std::string out;
out.reserve(identifier.size());
for (auto const ch: identifier)
{
out += ch;
if (ch == '"')
out += '"';
}
return out;
}

[[nodiscard]] std::string FormatAlterTableCommand(std::string_view tableName, SqlAlterTableCommand const& command) const
{
auto const formatTable = [tableName]() {
Expand All @@ -436,12 +454,21 @@ class SQLiteQueryFormatter: public SqlQueryFormatter
ColumnType(actualCommand.columnType),
actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
},
[&formatTable, this](AlterColumn const& actualCommand) -> std::string {
return std::format(R"(ALTER TABLE {} ALTER COLUMN "{}" {} {};)",
formatTable(),
actualCommand.columnName,
ColumnType(actualCommand.columnType),
actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
[&formatTable, tableName, this](AlterColumn const& actualCommand) -> std::string {
// SQLite has no `ALTER TABLE … ALTER COLUMN`. Route through the migration
// executor's table-rebuild path: emit a sentinel carrying the new column
// definition (type + nullability) that the executor recognises and applies by
// recreating the table with the modified column. The commented-out MSSQL-style
// ALTER below keeps dry-run output readable. Identifier fields are `""`-escaped so a
// name containing a double-quote cannot desync the sentinel's field parsing.
return std::format(
R"(-- LIGHTWEIGHT_SQLITE_GUARD: ALTER_COLUMN "{0}" "{1}" "{2}" "{3}"
-- ALTER TABLE {4} ALTER COLUMN "{1}" {2} {3};)",
EscapeSentinelField(tableName),
EscapeSentinelField(actualCommand.columnName),
ColumnType(actualCommand.columnType),
actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL",
formatTable());
},
[&formatTable](RenameColumn const& actualCommand) -> std::string {
return std::format(R"(ALTER TABLE {} RENAME COLUMN "{}" TO "{}";)",
Expand Down
2 changes: 1 addition & 1 deletion src/Lightweight/QueryFormatter/SqlServerFormatter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class SqlServerQueryFormatter final: public SQLiteQueryFormatter
}

public:
[[nodiscard]] bool RequiresTableRebuildForForeignKeyChange() const noexcept override
[[nodiscard]] bool RequiresTableRebuildForSchemaChange() const noexcept override
{
return false;
}
Expand Down
5 changes: 5 additions & 0 deletions src/Lightweight/SqlConnection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,11 @@ std::string SqlConnection::ServerVersion() const
return GetInfoStringW(m_hDbc, SQL_DBMS_VER);
}

bool SqlConnection::RequiresTableRebuildForSchemaChange() const noexcept
{
return QueryFormatter().RequiresTableRebuildForSchemaChange();
}

bool SqlConnection::TransactionActive() const noexcept
{
SQLUINTEGER state {};
Expand Down
9 changes: 9 additions & 0 deletions src/Lightweight/SqlConnection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ class SqlConnection final
/// Retrieves a query formatter suitable for the SQL server being connected.
[[nodiscard]] SqlQueryFormatter const& QueryFormatter() const noexcept;

/// @brief Whether this backend must rebuild a table to apply an `ALTER TABLE` schema change it
/// cannot express in place (foreign-key add/drop, or column type/nullability change).
///
/// The migration executor consults this to decide whether a `-- LIGHTWEIGHT_SQLITE_GUARD:` sentinel
/// must be turned into a table rebuild (`true` for SQLite) or executed directly. Exposed on the
/// connection so capability decisions go through the backend, delegating the dialect knowledge to
/// the query formatter.
[[nodiscard]] LIGHTWEIGHT_API bool RequiresTableRebuildForSchemaChange() const noexcept;

/// @brief Whether this connection's ODBC driver supports native parameter-array binding
/// (`SQL_ATTR_PARAMSET_SIZE` > 1) for batched row-wise execution.
///
Expand Down
Loading
Loading