Lightweight 0.20260921.0
Loading...
Searching...
No Matches
SQLiteFormatter.hpp
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include "../SqlAdvisoryLock.hpp"
5#include "../SqlQueryFormatter.hpp"
6#include "../SqlRetryClassifier.hpp"
7
8#include <reflection-cpp/reflection.hpp>
9
10#include <format>
11
12namespace Lightweight
13{
14
15class SQLiteQueryFormatter: public SqlQueryFormatter
16{
17 protected:
18 /// Formats a table name for use in a FROM clause.
19 /// If the table name is already quoted (starts with " or [), returns it as-is.
20 /// Otherwise, wraps it in double quotes.
21 [[nodiscard]] static std::string FormatFromTable(std::string_view table)
22 {
23 if (!table.empty() && (table.front() == '"' || table.front() == '['))
24 return std::string(table); // Already quoted/qualified
25 return std::format(R"("{}")", table);
26 }
27
28 public:
29 /// SQLite has no native `ALTER TABLE … ADD/DROP CONSTRAINT` and no `… ALTER COLUMN`, so foreign-key
30 /// changes and column type/nullability changes route through the migration executor's table-rebuild
31 /// path. The formatter signals that intent by emitting `-- LIGHTWEIGHT_SQLITE_GUARD:` sentinels and
32 /// overriding this hook so the executor takes the rebuild branch instead of executing the
33 /// (commented-out) sentinel script directly.
34 [[nodiscard]] bool RequiresTableRebuildForSchemaChange() const noexcept override
35 {
36 return true;
37 }
38
39 /// Builds the SQL query used to check whether a column exists on a SQLite table.
40 ///
41 /// The migration executor uses this to resolve the `-- LIGHTWEIGHT_SQLITE_GUARD:`
42 /// sentinels emitted by @ref AlterTable for `AddColumnIfNotExists` / `DropColumnIfExists`.
43 /// Keeping the pragma SQL here ensures the sentinel-emitting side and the
44 /// runtime-presence-check side share a single source of truth.
45 ///
46 /// @param tableName Name of the table to inspect.
47 /// @param columnName Name of the column whose existence to check.
48 /// @return SQL string returning a single integer column: non-zero iff the column exists.
49 [[nodiscard]] static std::string BuildColumnExistsQuery(std::string_view tableName, std::string_view columnName)
50 {
51 return std::format(R"(SELECT COUNT(*) FROM pragma_table_info('{}') WHERE name = '{}';)", tableName, columnName);
52 }
53
54 [[nodiscard]] std::string Insert(std::string_view intoTable,
55 std::string_view fields,
56 std::string_view values) const override
57 {
58 return std::format(R"(INSERT INTO "{}" ({}) VALUES ({}))", intoTable, fields, values);
59 }
60
61 [[nodiscard]] std::string Insert(std::string_view /*schema*/,
62 std::string_view intoTable,
63 std::string_view fields,
64 std::string_view values) const override
65 {
66 // SQLite doesn't support schemas - ignore schema parameter
67 return std::format(R"(INSERT INTO "{}" ({}) VALUES ({}))", intoTable, fields, values);
68 }
69
70 [[nodiscard]] std::string QueryLastInsertId(std::string_view /*tableName*/) const override
71 {
72 // This is SQLite syntax. We might want to provide aspecialized SQLite class instead.
73 return "SELECT LAST_INSERT_ROWID()";
74 }
75
76 [[nodiscard]] std::string_view BooleanLiteral(bool literalValue) const noexcept override
77 {
78 return literalValue ? "TRUE" : "FALSE";
79 }
80
81 [[nodiscard]] std::string_view DateFunction() const noexcept override
82 {
83 return "date()";
84 }
85
86 [[nodiscard]] std::string StringLiteral(std::string_view value) const noexcept override
87 {
88 if (value.empty())
89 return "''";
90
91 std::string escaped;
92 escaped.reserve(value.size() + 2);
93 escaped += '\'';
94 for (char const c: value)
95 {
96 if (c == '\'')
97 escaped += "''";
98 else
99 escaped += c;
100 }
101 escaped += '\'';
102 return escaped;
103 }
104
105 [[nodiscard]] std::string StringLiteral(char value) const noexcept override
106 {
107 if (value == '\'')
108 return "''''";
109 return std::format("'{}'", value);
110 }
111
112 [[nodiscard]] std::string BinaryLiteral(std::span<uint8_t const> data) const override
113 {
114 std::string result;
115 result.reserve((data.size() * 2) + 3);
116 result += "X'";
117 for (uint8_t byte: data)
118 result += std::format("{:02X}", byte);
119 result += "'";
120 return result;
121 }
122
123 [[nodiscard]] std::string QualifiedTableName(std::string_view schema, std::string_view table) const override
124 {
125 // SQLite doesn't use schemas in the same way - just return the quoted table name
126 if (schema.empty())
127 return std::format(R"("{}")", table);
128 // For SQLite attached databases, use database.table syntax
129 return std::format(R"("{}"."{}")", schema, table);
130 }
131
132 [[nodiscard]] std::string SelectCount(bool distinct,
133 std::string_view fromTable,
134 std::string_view fromTableAlias,
135 std::string_view tableJoins,
136 std::string_view whereCondition,
137 std::string_view groupBy) const override
138 {
139 auto const formattedTable = FormatFromTable(fromTable);
140 if (fromTableAlias.empty())
141 return std::format("SELECT{} COUNT(*) FROM {}{}{}{}",
142 distinct ? " DISTINCT" : "",
143 formattedTable,
144 tableJoins,
145 whereCondition,
146 groupBy);
147 else
148 return std::format(R"(SELECT{} COUNT(*) FROM {} AS "{}"{}{}{})",
149 distinct ? " DISTINCT" : "",
150 formattedTable,
151 fromTableAlias,
152 tableJoins,
153 whereCondition,
154 groupBy);
155 }
156
157 [[nodiscard]] std::string SelectAll(bool distinct,
158 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
159 std::string_view fields,
160 std::string_view fromTable,
161 std::string_view fromTableAlias,
162 std::string_view tableJoins,
163 std::string_view whereCondition,
164 std::string_view orderBy,
165 std::string_view groupBy) const override
166 {
167 std::stringstream sqlQueryString;
168 sqlQueryString << "SELECT ";
169 if (distinct)
170 sqlQueryString << "DISTINCT ";
171 sqlQueryString << fields;
172 sqlQueryString << " FROM " << FormatFromTable(fromTable);
173 if (!fromTableAlias.empty())
174 sqlQueryString << " AS \"" << fromTableAlias << '"';
175 sqlQueryString << tableJoins;
176 sqlQueryString << whereCondition;
177 sqlQueryString << groupBy;
178 sqlQueryString << orderBy;
179
180 return sqlQueryString.str();
181 }
182
183 [[nodiscard]] std::string SelectFirst(bool distinct,
184 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
185 std::string_view fields,
186 std::string_view fromTable,
187 std::string_view fromTableAlias,
188 std::string_view tableJoins,
189 std::string_view whereCondition,
190 std::string_view orderBy,
191 std::string_view groupBy,
192 size_t count) const override
193 {
194 std::stringstream sqlQueryString;
195 sqlQueryString << "SELECT ";
196 if (distinct)
197 sqlQueryString << "DISTINCT ";
198 sqlQueryString << fields;
199 sqlQueryString << " FROM " << FormatFromTable(fromTable);
200 if (!fromTableAlias.empty())
201 sqlQueryString << " AS \"" << fromTableAlias << "\"";
202 sqlQueryString << tableJoins;
203 sqlQueryString << whereCondition;
204 sqlQueryString << groupBy;
205 sqlQueryString << orderBy;
206 sqlQueryString << " LIMIT " << count;
207 return sqlQueryString.str();
208 }
209
210 [[nodiscard]] std::string SelectRange(bool distinct,
211 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
212 std::string_view fields,
213 std::string_view fromTable,
214 std::string_view fromTableAlias,
215 std::string_view tableJoins,
216 std::string_view whereCondition,
217 std::string_view orderBy,
218 std::string_view groupBy,
219 std::size_t offset,
220 std::size_t limit) const override
221 {
222 std::stringstream sqlQueryString;
223 sqlQueryString << "SELECT ";
224 if (distinct)
225 sqlQueryString << "DISTINCT ";
226 sqlQueryString << fields;
227 sqlQueryString << " FROM " << FormatFromTable(fromTable);
228 if (!fromTableAlias.empty())
229 sqlQueryString << " AS \"" << fromTableAlias << "\"";
230 sqlQueryString << tableJoins;
231 sqlQueryString << whereCondition;
232 sqlQueryString << groupBy;
233 sqlQueryString << orderBy;
234 sqlQueryString << " LIMIT " << limit << " OFFSET " << offset;
235 return sqlQueryString.str();
236 }
237
238 [[nodiscard]] std::string Update(std::string_view table,
239 std::string_view tableAlias,
240 std::string_view setFields,
241 std::string_view whereCondition) const override
242 {
243 auto const formattedTable = FormatFromTable(table);
244 if (tableAlias.empty())
245 return std::format("UPDATE {} SET {}{}", formattedTable, setFields, whereCondition);
246 else
247 return std::format(R"(UPDATE {} AS "{}" SET {}{})", formattedTable, tableAlias, setFields, whereCondition);
248 }
249
250 [[nodiscard]] std::string Delete(std::string_view fromTable,
251 std::string_view fromTableAlias,
252 std::string_view tableJoins,
253 std::string_view whereCondition) const override
254 {
255 auto const formattedTable = FormatFromTable(fromTable);
256 if (fromTableAlias.empty())
257 return std::format("DELETE FROM {}{}{}", formattedTable, tableJoins, whereCondition);
258 else
259 return std::format(R"(DELETE FROM {} AS "{}"{}{})", formattedTable, fromTableAlias, tableJoins, whereCondition);
260 }
261
262 [[nodiscard]] virtual std::string BuildColumnDefinition(SqlColumnDeclaration const& column) const
263 {
264 std::stringstream sqlQueryString;
265
266 sqlQueryString << '"' << column.name << "\" ";
267
268 if (column.primaryKey != SqlPrimaryKeyType::AUTO_INCREMENT)
269 sqlQueryString << ColumnType(column.type);
270 else
271 sqlQueryString << ColumnType(SqlColumnTypeDefinitions::Integer {});
272
273 if (column.required)
274 sqlQueryString << " NOT NULL";
275
276 if (column.primaryKey == SqlPrimaryKeyType::AUTO_INCREMENT)
277 sqlQueryString << " PRIMARY KEY AUTOINCREMENT";
278 else if (column.primaryKey == SqlPrimaryKeyType::NONE && !column.index && column.unique)
279 sqlQueryString << " UNIQUE";
280
281 if (!column.defaultValue.empty())
282 sqlQueryString << " DEFAULT " << column.defaultValue;
283
284 return sqlQueryString.str();
285 }
286
287 [[nodiscard]] static std::string BuildForeignKeyConstraint(std::string_view tableName,
288 std::string_view columnName,
289 SqlForeignKeyReferenceDefinition const& referencedColumn)
290 {
291 // Double-quote the constraint name so PostgreSQL preserves its case (otherwise
292 // PG would fold to lowercase, breaking the matching `DROP CONSTRAINT "FK_<…>"`
293 // emitted by `DropForeignKey`). The name is shared with the runtime rebuild
294 // path (`SqliteRebuildAddForeignKey`) via `BuildForeignKeyConstraintName` so
295 // CREATE-table and ALTER-table produce identical constraint names.
296 return std::format(R"(CONSTRAINT "{}" FOREIGN KEY ("{}") REFERENCES "{}"("{}"))",
297 BuildForeignKeyConstraintName(tableName, std::array { columnName }),
298 columnName,
299 referencedColumn.tableName,
300 referencedColumn.columnName);
301 }
302
303 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
304 [[nodiscard]] StringList CreateTable(std::string_view schema,
305 std::string_view tableName,
306 std::vector<SqlColumnDeclaration> const& columns,
307 std::vector<SqlCompositeForeignKeyConstraint> const& foreignKeys,
308 bool ifNotExists = false) const override
309 {
310 auto sqlQueries = StringList {};
311
312 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
313 sqlQueries.emplace_back([&]() {
314 std::stringstream sqlQueryString;
315 sqlQueryString << "CREATE TABLE ";
316 if (ifNotExists)
317 sqlQueryString << "IF NOT EXISTS ";
318 // SQLite doesn't support schemas - ignore schema parameter
319 (void) schema;
320 sqlQueryString << "\"" << tableName << "\" (";
321 std::vector<SqlColumnDeclaration const*> pks;
322 size_t currentColumn = 0;
323 std::string foreignKeyConstraints;
324 for (SqlColumnDeclaration const& column: columns)
325 {
326 if (currentColumn > 0)
327 sqlQueryString << ",";
328 ++currentColumn;
329 sqlQueryString << "\n ";
330 sqlQueryString << BuildColumnDefinition(column);
331
332 if (column.primaryKey != SqlPrimaryKeyType::NONE)
333 pks.push_back(&column);
334
335 if (column.foreignKey)
336 {
337 foreignKeyConstraints += ",\n ";
338 foreignKeyConstraints += BuildForeignKeyConstraint(tableName, column.name, *column.foreignKey);
339 }
340 }
341
342 for (SqlCompositeForeignKeyConstraint const& fk: foreignKeys)
343 {
344 // Emit a deterministic CONSTRAINT name (`FK_<table>_<col1>_<col2>...`) so that
345 // a later `DropForeignKey` / SQLite-rebuild can locate the constraint by name
346 // instead of relying on the backend's auto-generated identifier (PostgreSQL
347 // would otherwise pick `<table>_<col>_fkey`).
348 foreignKeyConstraints += ",\n CONSTRAINT \"";
349 foreignKeyConstraints += BuildForeignKeyConstraintName(tableName, fk.columns);
350 foreignKeyConstraints += "\" FOREIGN KEY (";
351 for (size_t i = 0; i < fk.columns.size(); ++i)
352 {
353 if (i > 0)
354 foreignKeyConstraints += ", ";
355 foreignKeyConstraints += '"' + fk.columns[i] + '"';
356 }
357 foreignKeyConstraints += ") REFERENCES \"";
358 foreignKeyConstraints += fk.referencedTableName;
359 foreignKeyConstraints += "\" (";
360 for (size_t i = 0; i < fk.referencedColumns.size(); ++i)
361 {
362 if (i > 0)
363 foreignKeyConstraints += ", ";
364 foreignKeyConstraints += '"' + fk.referencedColumns[i] + '"';
365 }
366 foreignKeyConstraints += ")";
367 }
368
369 // Filter out AUTO_INCREMENT from table-level PK constraint if it's the ONLY PK,
370 // because SQLite handles it inline. But for composite keys involving auto-inc (if that's even valid/used),
371 // or if we just want to be explicit.
372 // SQLite restriction: "INTEGER PRIMARY KEY" must be on the column definition for auto-increment.
373 // If we have AUTO_INCREMENT, it's already in BuildColumnDefinition.
374
375 std::erase_if(
376 pks, [](SqlColumnDeclaration const* col) { return col->primaryKey == SqlPrimaryKeyType::AUTO_INCREMENT; });
377
378 if (!pks.empty())
379 {
380 std::ranges::sort(pks, [](SqlColumnDeclaration const* a, SqlColumnDeclaration const* b) {
381 // If both have index, use it. If one has 0 (no index), treat it as "after" indexed ones?
382 // Or just rely on stable sort?
383 // Let's assume indices are properly set for composite keys.
384 // 1-based index vs 0. 0 means "unordered".
385 if (a->primaryKeyIndex != 0 && b->primaryKeyIndex != 0)
386 return a->primaryKeyIndex < b->primaryKeyIndex;
387 if (a->primaryKeyIndex != 0)
388 return true; // a comes first
389 if (b->primaryKeyIndex != 0)
390 return false; // b comes first
391 return false; // keep original order
392 });
393
394 sqlQueryString << ",\n PRIMARY KEY (";
395 for (size_t i = 0; i < pks.size(); ++i)
396 {
397 if (i > 0)
398 sqlQueryString << ", ";
399 sqlQueryString << '"' << pks[i]->name << '"';
400 }
401 sqlQueryString << ")";
402 }
403
404 sqlQueryString << foreignKeyConstraints;
405
406 sqlQueryString << "\n);";
407 return sqlQueryString.str();
408 }());
409
410 for (SqlColumnDeclaration const& column: columns)
411 {
412 if (column.index && column.primaryKey == SqlPrimaryKeyType::NONE)
413 {
414 // primary keys are always indexed
415 if (column.unique)
416 sqlQueries.emplace_back(std::format(R"(CREATE UNIQUE INDEX "{}_{}_index" ON "{}" ("{}");)",
417 tableName,
418 column.name,
419 tableName,
420 column.name));
421 else
422 sqlQueries.emplace_back(std::format(
423 R"(CREATE INDEX "{}_{}_index" ON "{}" ("{}");)", tableName, column.name, tableName, column.name));
424 }
425 }
426
427 return sqlQueries;
428 }
429
430 private:
431 /// @brief Escape an identifier for embedding inside a `"..."` field of a `LIGHTWEIGHT_SQLITE_GUARD`
432 /// sentinel by doubling embedded double-quotes, so a name containing `"` cannot desync the field
433 /// parsing on the executor side (which decodes `""` back to `"`).
434 /// @param identifier The raw identifier (table or column name).
435 /// @return The escaped text to place between the surrounding sentinel quotes.
436 [[nodiscard]] static std::string EscapeSentinelField(std::string_view identifier)
437 {
438 std::string out;
439 out.reserve(identifier.size());
440 for (auto const ch: identifier)
441 {
442 out += ch;
443 if (ch == '"')
444 out += '"';
445 }
446 return out;
447 }
448
449 [[nodiscard]] std::string FormatAlterTableCommand(std::string_view tableName, SqlAlterTableCommand const& command) const
450 {
451 auto const formatTable = [tableName]() {
452 return std::format(R"("{}")", tableName);
453 };
454
455 using namespace SqlAlterTableCommands;
456 return std::visit(
457 detail::overloaded {
458 [&formatTable](RenameTable const& actualCommand) -> std::string {
459 return std::format(R"(ALTER TABLE {} RENAME TO "{}";)", formatTable(), actualCommand.newTableName);
460 },
461 [&formatTable, this](AddColumn const& actualCommand) -> std::string {
462 return std::format(R"(ALTER TABLE {} ADD COLUMN "{}" {} {};)",
463 formatTable(),
464 actualCommand.columnName,
465 ColumnType(actualCommand.columnType),
466 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
467 },
468 [&formatTable, tableName, this](AlterColumn const& actualCommand) -> std::string {
469 // SQLite has no `ALTER TABLE … ALTER COLUMN`. Route through the migration
470 // executor's table-rebuild path: emit a sentinel carrying the new column
471 // definition (type + nullability) that the executor recognises and applies by
472 // recreating the table with the modified column. The commented-out MSSQL-style
473 // ALTER below keeps dry-run output readable. Identifier fields are `""`-escaped so a
474 // name containing a double-quote cannot desync the sentinel's field parsing.
475 return std::format(
476 R"(-- LIGHTWEIGHT_SQLITE_GUARD: ALTER_COLUMN "{0}" "{1}" "{2}" "{3}"
477-- ALTER TABLE {4} ALTER COLUMN "{1}" {2} {3};)",
478 EscapeSentinelField(tableName),
479 EscapeSentinelField(actualCommand.columnName),
480 ColumnType(actualCommand.columnType),
481 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL",
482 formatTable());
483 },
484 [&formatTable](RenameColumn const& actualCommand) -> std::string {
485 return std::format(R"(ALTER TABLE {} RENAME COLUMN "{}" TO "{}";)",
486 formatTable(),
487 actualCommand.oldColumnName,
488 actualCommand.newColumnName);
489 },
490 [&formatTable](DropColumn const& actualCommand) -> std::string {
491 return std::format(R"(ALTER TABLE {} DROP COLUMN "{}";)", formatTable(), actualCommand.columnName);
492 },
493 [tableName](AddIndex const& actualCommand) -> std::string {
494 using namespace std::string_view_literals;
495 auto const uniqueStr = actualCommand.unique ? "UNIQUE "sv : ""sv;
496 return std::format(R"(CREATE {2}INDEX "{0}_{1}_index" ON "{0}" ("{1}");)",
497 tableName,
498 actualCommand.columnName,
499 uniqueStr);
500 },
501 [tableName](DropIndex const& actualCommand) -> std::string {
502 return std::format(R"(DROP INDEX "{0}_{1}_index";)", tableName, actualCommand.columnName);
503 },
504 [tableName](AddForeignKey const& actualCommand) -> std::string {
505 // SQLite cannot `ALTER TABLE … ADD CONSTRAINT`. The runtime executor
506 // recognizes this sentinel and rebuilds the table from sqlite_schema.
507 // All metadata the executor needs fits in the sentinel itself. We keep
508 // a commented-out equivalent MSSQL-style ALTER below so dry-run output
509 // stays readable.
510 return std::format(
511 R"(-- LIGHTWEIGHT_SQLITE_GUARD: ADD_FOREIGN_KEY "{0}" "{1}" "{2}" "{3}"
512-- ALTER TABLE "{0}" ADD {4};)",
513 tableName,
514 actualCommand.columnName,
515 actualCommand.referencedColumn.tableName,
516 actualCommand.referencedColumn.columnName,
517 BuildForeignKeyConstraint(tableName, actualCommand.columnName, actualCommand.referencedColumn));
518 },
519 [tableName](DropForeignKey const& actualCommand) -> std::string {
520 // SQLite has no `DROP CONSTRAINT`. Executor rebuilds the table without
521 // the matching `CONSTRAINT FK_<tbl>_<col>` clause.
522 return std::format(
523 R"(-- LIGHTWEIGHT_SQLITE_GUARD: DROP_FOREIGN_KEY "{0}" "{1}"
524-- ALTER TABLE "{0}" DROP CONSTRAINT "{2}";)",
525 tableName,
526 actualCommand.columnName,
528 std::array { std::string_view { actualCommand.columnName } }));
529 },
530 [tableName](AddCompositeForeignKey const& actualCommand) -> std::string {
531 // SQLite cannot `ALTER TABLE … ADD CONSTRAINT`. Mirror the single-column
532 // AddForeignKey path: emit a sentinel the runtime executor recognises and
533 // translates into a table rebuild with the composite FK clause appended.
534 // Column tuples are encoded as comma-joined lists inside the sentinel's
535 // quoted fields; SQL identifiers in the target corpora do not contain
536 // commas so the split-on-',' parse on the runtime side is unambiguous.
537 auto const joinComma = [](std::vector<std::string> const& v) {
538 std::string out;
539 for (size_t i = 0; i < v.size(); ++i)
540 {
541 if (i != 0)
542 out += ',';
543 out += v[i];
544 }
545 return out;
546 };
547 auto const joinQuoted = [](std::vector<std::string> const& v) {
548 std::string out;
549 for (size_t i = 0; i < v.size(); ++i)
550 {
551 if (i != 0)
552 out += ", ";
553 out += '"';
554 out += v[i];
555 out += '"';
556 }
557 return out;
558 };
559 auto const fkName = BuildForeignKeyConstraintName(tableName, actualCommand.columns);
560 return std::format(
561 R"(-- LIGHTWEIGHT_SQLITE_GUARD: ADD_COMPOSITE_FOREIGN_KEY "{0}" "{1}" "{2}" "{3}"
562-- ALTER TABLE "{0}" ADD CONSTRAINT "{4}" FOREIGN KEY ({5}) REFERENCES "{2}"({6});)",
563 tableName,
564 joinComma(actualCommand.columns),
565 actualCommand.referencedTableName,
566 joinComma(actualCommand.referencedColumns),
567 fkName,
568 joinQuoted(actualCommand.columns),
569 joinQuoted(actualCommand.referencedColumns));
570 },
571 [&formatTable, tableName, this](AddColumnIfNotExists const& actualCommand) -> std::string {
572 // SQLite doesn't support IF NOT EXISTS for ADD COLUMN.
573 // Emit a sentinel comment so the migration executor can presence-check
574 // via pragma_table_info() before running the ALTER TABLE.
575 return std::format(
576 R"(-- LIGHTWEIGHT_SQLITE_GUARD: ADD_COLUMN_IF_NOT_EXISTS "{0}" "{1}"
577ALTER TABLE {2} ADD COLUMN "{1}" {3} {4};)",
578 tableName,
579 actualCommand.columnName,
580 formatTable(),
581 ColumnType(actualCommand.columnType),
582 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
583 },
584 [&formatTable, tableName](DropColumnIfExists const& actualCommand) -> std::string {
585 // SQLite doesn't support IF EXISTS for DROP COLUMN; guarded like above.
586 return std::format(
587 R"(-- LIGHTWEIGHT_SQLITE_GUARD: DROP_COLUMN_IF_EXISTS "{0}" "{1}"
588ALTER TABLE {2} DROP COLUMN "{1}";)",
589 tableName,
590 actualCommand.columnName,
591 formatTable());
592 },
593 [tableName](DropIndexIfExists const& actualCommand) -> std::string {
594 return std::format(R"(DROP INDEX IF EXISTS "{0}_{1}_index";)", tableName, actualCommand.columnName);
595 },
596 },
597 command);
598 }
599
600 public:
601 [[nodiscard]] StringList AlterTable(std::string_view /*schemaName*/,
602 std::string_view tableName,
603 std::vector<SqlAlterTableCommand> const& commands) const override
604 {
605 // SQLite has no native IF NOT EXISTS / IF EXISTS for columns. We emit each command
606 // as its own result entry; guarded commands are prefixed with a sentinel comment
607 // that the migration executor recognizes and uses to perform a pragma_table_info
608 // presence check at runtime.
609 StringList result;
610 for (SqlAlterTableCommand const& command: commands)
611 {
612 auto sql = FormatAlterTableCommand(tableName, command);
613 if (!sql.empty())
614 result.push_back(std::move(sql));
615 }
616 return result;
617 }
618
619 [[nodiscard]] std::string ColumnType(SqlColumnTypeDefinition const& type) const override
620 {
621 using namespace SqlColumnTypeDefinitions;
622 return std::visit(detail::overloaded {
623 [](Bigint const&) -> std::string { return "BIGINT"; },
624 [](Binary const&) -> std::string { return "BLOB"; },
625 [](Bool const&) -> std::string { return "BOOLEAN"; },
626 [](Char const& type) -> std::string { return std::format("CHAR({})", type.size); },
627 [](Date const&) -> std::string { return "DATE"; },
628 [](DateTime const&) -> std::string { return "DATETIME"; },
629 [](Decimal const& type) -> std::string {
630 return std::format("DECIMAL({}, {})", type.precision, type.scale);
631 },
632 [](Guid const&) -> std::string { return "GUID"; },
633 [](Integer const&) -> std::string { return "INTEGER"; },
634 [](NChar const& type) -> std::string { return std::format("NCHAR({})", type.size); },
635 [](NVarchar const& type) -> std::string { return std::format("NVARCHAR({})", type.size); },
636 [](Real const&) -> std::string { return "REAL"; },
637 [](Smallint const&) -> std::string { return "SMALLINT"; },
638 [](Text const&) -> std::string { return "TEXT"; },
639 [](Time const&) -> std::string { return "TIME"; },
640 [](Timestamp const&) -> std::string { return "TIMESTAMP"; },
641 [](Tinyint const&) -> std::string { return "TINYINT"; },
642 [](VarBinary const& type) -> std::string { return std::format("VARBINARY({})", type.size); },
643 [](Varchar const& type) -> std::string { return std::format("VARCHAR({})", type.size); },
644 },
645 type);
646 }
647
648 [[nodiscard]] StringList DropTable(std::string_view /*schemaName*/,
649 std::string_view const& tableName,
650 bool ifExists = false,
651 bool cascade = false) const override
652 {
653 // SQLite doesn't support CASCADE syntax, but if FK constraints are disabled
654 // (PRAGMA foreign_keys = OFF), dropping works. The cascade flag is ignored.
655 // SQLite doesn't support schemas - ignore schemaName parameter
656 (void) cascade;
657 if (ifExists)
658 return { std::format(R"(DROP TABLE IF EXISTS "{}";)", tableName) };
659 else
660 return { std::format(R"(DROP TABLE "{}";)", tableName) };
661 }
662
663 [[nodiscard]] std::string QueryServerVersion() const override
664 {
665 return "SELECT sqlite_version()";
666 }
667
668 /// SQLite has no native advisory-lock primitive, so the handler maintains
669 /// a `_lightweight_locks` table guarded by a unique constraint. The
670 /// override is intentionally inline-delegating: putting the body in
671 /// `SqlQueryFormatter.cpp` would have made it the class's *key function*
672 /// and forced the vtable into a single TU — fine on Windows, but on
673 /// Linux with `CXX_VISIBILITY_PRESET=hidden` + `LIGHTWEIGHT_BUILD_SHARED=ON`
674 /// the hidden vtable would fail to link from consumers of `Lightweight`.
675 /// Inline + free-function delegation keeps the vtable weak.
676 [[nodiscard]] SqlAdvisoryLockHandler const& AdvisoryLockOps() const override
677 {
678 return SqliteAdvisoryLockOps();
679 }
680
681 /// SQLite reports contention through the driver's message text rather than a SQLSTATE.
682 /// Delegates to the free function for the same reason `AdvisoryLockOps()` does.
683 [[nodiscard]] SqlRetryClassifier const& RetryOps() const noexcept override
684 {
685 return SqliteRetryOps();
686 }
687};
688
689} // namespace Lightweight
static std::string BuildForeignKeyConstraintName(std::string_view tableName, Range const &columns)
Builds the canonical foreign-key constraint name for a set of columns.
std::vector< std::string > StringList
Alias for a list of SQL statement strings.
std::variant< SqlAlterTableCommands::RenameTable, SqlAlterTableCommands::AddColumn, SqlAlterTableCommands::AddColumnIfNotExists, SqlAlterTableCommands::AlterColumn, SqlAlterTableCommands::AddIndex, SqlAlterTableCommands::RenameColumn, SqlAlterTableCommands::DropColumn, SqlAlterTableCommands::DropColumnIfExists, SqlAlterTableCommands::DropIndex, SqlAlterTableCommands::DropIndexIfExists, SqlAlterTableCommands::AddForeignKey, SqlAlterTableCommands::AddCompositeForeignKey, SqlAlterTableCommands::DropForeignKey > SqlAlterTableCommand
Represents a single SQL ALTER TABLE command.
LIGHTWEIGHT_API SqlRetryClassifier const & SqliteRetryOps() noexcept
Returns the SQLite-specific singleton classifier.