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