Lightweight 0.20251202.0
Loading...
Searching...
No Matches
SQLiteFormatter.hpp
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include "../SqlQueryFormatter.hpp"
5
6#include <reflection-cpp/reflection.hpp>
7
8#include <format>
9
10namespace Lightweight
11{
12
13class SQLiteQueryFormatter: public SqlQueryFormatter
14{
15 protected:
16 /// Formats a table name for use in a FROM clause.
17 /// If the table name is already quoted (starts with " or [), returns it as-is.
18 /// Otherwise, wraps it in double quotes.
19 [[nodiscard]] static std::string FormatFromTable(std::string_view table)
20 {
21 if (!table.empty() && (table.front() == '"' || table.front() == '['))
22 return std::string(table); // Already quoted/qualified
23 return std::format(R"("{}")", table);
24 }
25
26 public:
27 [[nodiscard]] std::string Insert(std::string_view intoTable,
28 std::string_view fields,
29 std::string_view values) const override
30 {
31 return std::format(R"(INSERT INTO "{}" ({}) VALUES ({}))", intoTable, fields, values);
32 }
33
34 [[nodiscard]] std::string Insert(std::string_view /*schema*/,
35 std::string_view intoTable,
36 std::string_view fields,
37 std::string_view values) const override
38 {
39 // SQLite doesn't support schemas - ignore schema parameter
40 return std::format(R"(INSERT INTO "{}" ({}) VALUES ({}))", intoTable, fields, values);
41 }
42
43 [[nodiscard]] std::string QueryLastInsertId(std::string_view /*tableName*/) const override
44 {
45 // This is SQLite syntax. We might want to provide aspecialized SQLite class instead.
46 return "SELECT LAST_INSERT_ROWID()";
47 }
48
49 [[nodiscard]] std::string_view BooleanLiteral(bool literalValue) const noexcept override
50 {
51 return literalValue ? "TRUE" : "FALSE";
52 }
53
54 [[nodiscard]] std::string_view DateFunction() const noexcept override
55 {
56 return "date()";
57 }
58
59 [[nodiscard]] std::string StringLiteral(std::string_view value) const noexcept override
60 {
61 if (value.empty())
62 return "''";
63
64 std::string escaped;
65 escaped.reserve(value.size() + 2);
66 escaped += '\'';
67 for (char const c: value)
68 {
69 if (c == '\'')
70 escaped += "''";
71 else
72 escaped += c;
73 }
74 escaped += '\'';
75 return escaped;
76 }
77
78 [[nodiscard]] std::string StringLiteral(char value) const noexcept override
79 {
80 if (value == '\'')
81 return "''''";
82 return std::format("'{}'", value);
83 }
84
85 [[nodiscard]] std::string BinaryLiteral(std::span<uint8_t const> data) const override
86 {
87 std::string result;
88 result.reserve((data.size() * 2) + 3);
89 result += "X'";
90 for (uint8_t byte: data)
91 result += std::format("{:02X}", byte);
92 result += "'";
93 return result;
94 }
95
96 [[nodiscard]] std::string QualifiedTableName(std::string_view schema, std::string_view table) const override
97 {
98 // SQLite doesn't use schemas in the same way - just return the quoted table name
99 if (schema.empty())
100 return std::format(R"("{}")", table);
101 // For SQLite attached databases, use database.table syntax
102 return std::format(R"("{}"."{}")", schema, table);
103 }
104
105 [[nodiscard]] std::string SelectCount(bool distinct,
106 std::string_view fromTable,
107 std::string_view fromTableAlias,
108 std::string_view tableJoins,
109 std::string_view whereCondition) const override
110 {
111 auto const formattedTable = FormatFromTable(fromTable);
112 if (fromTableAlias.empty())
113 return std::format(
114 "SELECT{} COUNT(*) FROM {}{}{}", distinct ? " DISTINCT" : "", formattedTable, tableJoins, whereCondition);
115 else
116 return std::format(R"(SELECT{} COUNT(*) FROM {} AS "{}"{}{})",
117 distinct ? " DISTINCT" : "",
118 formattedTable,
119 fromTableAlias,
120 tableJoins,
121 whereCondition);
122 }
123
124 [[nodiscard]] std::string SelectAll(bool distinct,
125 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
126 std::string_view fields,
127 std::string_view fromTable,
128 std::string_view fromTableAlias,
129 std::string_view tableJoins,
130 std::string_view whereCondition,
131 std::string_view orderBy,
132 std::string_view groupBy) const override
133 {
134 std::stringstream sqlQueryString;
135 sqlQueryString << "SELECT ";
136 if (distinct)
137 sqlQueryString << "DISTINCT ";
138 sqlQueryString << fields;
139 sqlQueryString << " FROM " << FormatFromTable(fromTable);
140 if (!fromTableAlias.empty())
141 sqlQueryString << " AS \"" << fromTableAlias << '"';
142 sqlQueryString << tableJoins;
143 sqlQueryString << whereCondition;
144 sqlQueryString << groupBy;
145 sqlQueryString << orderBy;
146
147 return sqlQueryString.str();
148 }
149
150 [[nodiscard]] std::string SelectFirst(bool distinct,
151 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
152 std::string_view fields,
153 std::string_view fromTable,
154 std::string_view fromTableAlias,
155 std::string_view tableJoins,
156 std::string_view whereCondition,
157 std::string_view orderBy,
158 size_t count) const override
159 {
160 std::stringstream sqlQueryString;
161 sqlQueryString << "SELECT " << fields;
162 if (distinct)
163 sqlQueryString << " DISTINCT";
164 sqlQueryString << " FROM " << FormatFromTable(fromTable);
165 if (!fromTableAlias.empty())
166 sqlQueryString << " AS \"" << fromTableAlias << "\"";
167 sqlQueryString << tableJoins;
168 sqlQueryString << whereCondition;
169 sqlQueryString << orderBy;
170 sqlQueryString << " LIMIT " << count;
171 return sqlQueryString.str();
172 }
173
174 [[nodiscard]] std::string SelectRange(bool distinct,
175 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
176 std::string_view fields,
177 std::string_view fromTable,
178 std::string_view fromTableAlias,
179 std::string_view tableJoins,
180 std::string_view whereCondition,
181 std::string_view orderBy,
182 std::string_view groupBy,
183 std::size_t offset,
184 std::size_t limit) const override
185 {
186 std::stringstream sqlQueryString;
187 sqlQueryString << "SELECT " << fields;
188 if (distinct)
189 sqlQueryString << " DISTINCT";
190 sqlQueryString << " FROM " << FormatFromTable(fromTable);
191 if (!fromTableAlias.empty())
192 sqlQueryString << " AS \"" << fromTableAlias << "\"";
193 sqlQueryString << tableJoins;
194 sqlQueryString << whereCondition;
195 sqlQueryString << groupBy;
196 sqlQueryString << orderBy;
197 sqlQueryString << " LIMIT " << limit << " OFFSET " << offset;
198 return sqlQueryString.str();
199 }
200
201 [[nodiscard]] std::string Update(std::string_view table,
202 std::string_view tableAlias,
203 std::string_view setFields,
204 std::string_view whereCondition) const override
205 {
206 auto const formattedTable = FormatFromTable(table);
207 if (tableAlias.empty())
208 return std::format("UPDATE {} SET {}{}", formattedTable, setFields, whereCondition);
209 else
210 return std::format(R"(UPDATE {} AS "{}" SET {}{})", formattedTable, tableAlias, setFields, whereCondition);
211 }
212
213 [[nodiscard]] std::string Delete(std::string_view fromTable,
214 std::string_view fromTableAlias,
215 std::string_view tableJoins,
216 std::string_view whereCondition) const override
217 {
218 auto const formattedTable = FormatFromTable(fromTable);
219 if (fromTableAlias.empty())
220 return std::format("DELETE FROM {}{}{}", formattedTable, tableJoins, whereCondition);
221 else
222 return std::format(R"(DELETE FROM {} AS "{}"{}{})", formattedTable, fromTableAlias, tableJoins, whereCondition);
223 }
224
225 [[nodiscard]] virtual std::string BuildColumnDefinition(SqlColumnDeclaration const& column) const
226 {
227 std::stringstream sqlQueryString;
228
229 sqlQueryString << '"' << column.name << "\" ";
230
231 if (column.primaryKey != SqlPrimaryKeyType::AUTO_INCREMENT)
232 sqlQueryString << ColumnType(column.type);
233 else
234 sqlQueryString << ColumnType(SqlColumnTypeDefinitions::Integer {});
235
236 if (column.required)
237 sqlQueryString << " NOT NULL";
238
239 if (column.primaryKey == SqlPrimaryKeyType::AUTO_INCREMENT)
240 sqlQueryString << " PRIMARY KEY AUTOINCREMENT";
241 else if (column.primaryKey == SqlPrimaryKeyType::NONE && !column.index && column.unique)
242 sqlQueryString << " UNIQUE";
243
244 if (!column.defaultValue.empty())
245 sqlQueryString << " DEFAULT " << column.defaultValue;
246
247 return sqlQueryString.str();
248 }
249
250 [[nodiscard]] static std::string BuildForeignKeyConstraint(std::string_view tableName,
251 std::string_view columnName,
252 SqlForeignKeyReferenceDefinition const& referencedColumn)
253 {
254 return std::format(R"(CONSTRAINT {} FOREIGN KEY ("{}") REFERENCES "{}"("{}"))",
255 std::format("FK_{}_{}", tableName, columnName),
256 columnName,
257 referencedColumn.tableName,
258 referencedColumn.columnName);
259 }
260
261 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
262 [[nodiscard]] StringList CreateTable(std::string_view schema,
263 std::string_view tableName,
264 std::vector<SqlColumnDeclaration> const& columns,
265 std::vector<SqlCompositeForeignKeyConstraint> const& foreignKeys,
266 bool ifNotExists = false) const override
267 {
268 auto sqlQueries = StringList {};
269
270 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
271 sqlQueries.emplace_back([&]() {
272 std::stringstream sqlQueryString;
273 sqlQueryString << "CREATE TABLE ";
274 if (ifNotExists)
275 sqlQueryString << "IF NOT EXISTS ";
276 // SQLite doesn't support schemas - ignore schema parameter
277 (void) schema;
278 sqlQueryString << "\"" << tableName << "\" (";
279 std::vector<SqlColumnDeclaration const*> pks;
280 size_t currentColumn = 0;
281 std::string foreignKeyConstraints;
282 for (SqlColumnDeclaration const& column: columns)
283 {
284 if (currentColumn > 0)
285 sqlQueryString << ",";
286 ++currentColumn;
287 sqlQueryString << "\n ";
288 sqlQueryString << BuildColumnDefinition(column);
289
290 if (column.primaryKey != SqlPrimaryKeyType::NONE)
291 pks.push_back(&column);
292
293 if (column.foreignKey)
294 {
295 foreignKeyConstraints += ",\n ";
296 foreignKeyConstraints += BuildForeignKeyConstraint(tableName, column.name, *column.foreignKey);
297 }
298 }
299
300 for (SqlCompositeForeignKeyConstraint const& fk: foreignKeys)
301 {
302 foreignKeyConstraints += ",\n FOREIGN KEY (";
303 for (size_t i = 0; i < fk.columns.size(); ++i)
304 {
305 if (i > 0)
306 foreignKeyConstraints += ", ";
307 foreignKeyConstraints += '"' + fk.columns[i] + '"';
308 }
309 foreignKeyConstraints += ") REFERENCES \"";
310 foreignKeyConstraints += fk.referencedTableName;
311 foreignKeyConstraints += "\" (";
312 for (size_t i = 0; i < fk.referencedColumns.size(); ++i)
313 {
314 if (i > 0)
315 foreignKeyConstraints += ", ";
316 foreignKeyConstraints += '"' + fk.referencedColumns[i] + '"';
317 }
318 foreignKeyConstraints += ")";
319 }
320
321 // Filter out AUTO_INCREMENT from table-level PK constraint if it's the ONLY PK,
322 // because SQLite handles it inline. But for composite keys involving auto-inc (if that's even valid/used),
323 // or if we just want to be explicit.
324 // SQLite restriction: "INTEGER PRIMARY KEY" must be on the column definition for auto-increment.
325 // If we have AUTO_INCREMENT, it's already in BuildColumnDefinition.
326
327 std::erase_if(
328 pks, [](SqlColumnDeclaration const* col) { return col->primaryKey == SqlPrimaryKeyType::AUTO_INCREMENT; });
329
330 if (!pks.empty())
331 {
332 std::ranges::sort(pks, [](SqlColumnDeclaration const* a, SqlColumnDeclaration const* b) {
333 // If both have index, use it. If one has 0 (no index), treat it as "after" indexed ones?
334 // Or just rely on stable sort?
335 // Let's assume indices are properly set for composite keys.
336 // 1-based index vs 0. 0 means "unordered".
337 if (a->primaryKeyIndex != 0 && b->primaryKeyIndex != 0)
338 return a->primaryKeyIndex < b->primaryKeyIndex;
339 if (a->primaryKeyIndex != 0)
340 return true; // a comes first
341 if (b->primaryKeyIndex != 0)
342 return false; // b comes first
343 return false; // keep original order
344 });
345
346 sqlQueryString << ",\n PRIMARY KEY (";
347 for (size_t i = 0; i < pks.size(); ++i)
348 {
349 if (i > 0)
350 sqlQueryString << ", ";
351 sqlQueryString << '"' << pks[i]->name << '"';
352 }
353 sqlQueryString << ")";
354 }
355
356 sqlQueryString << foreignKeyConstraints;
357
358 sqlQueryString << "\n);";
359 return sqlQueryString.str();
360 }());
361
362 for (SqlColumnDeclaration const& column: columns)
363 {
364 if (column.index && column.primaryKey == SqlPrimaryKeyType::NONE)
365 {
366 // primary keys are always indexed
367 if (column.unique)
368 sqlQueries.emplace_back(std::format(R"(CREATE UNIQUE INDEX "{}_{}_index" ON "{}" ("{}");)",
369 tableName,
370 column.name,
371 tableName,
372 column.name));
373 else
374 sqlQueries.emplace_back(std::format(
375 R"(CREATE INDEX "{}_{}_index" ON "{}" ("{}");)", tableName, column.name, tableName, column.name));
376 }
377 }
378
379 return sqlQueries;
380 }
381
382 [[nodiscard]] StringList AlterTable(std::string_view /*schemaName*/,
383 std::string_view tableName,
384 std::vector<SqlAlterTableCommand> const& commands) const override
385 {
386 // SQLite doesn't support schemas - use table name only
387 auto const formatTable = [tableName]() {
388 return std::format(R"("{}")", tableName);
389 };
390
391 std::stringstream sqlQueryString;
392
393 int currentCommand = 0;
394 for (SqlAlterTableCommand const& command: commands)
395 {
396 if (currentCommand > 0)
397 sqlQueryString << '\n';
398 ++currentCommand;
399
400 using namespace SqlAlterTableCommands;
401 sqlQueryString << std::visit(
402 detail::overloaded {
403 [&formatTable](RenameTable const& actualCommand) -> std::string {
404 return std::format(R"(ALTER TABLE {} RENAME TO "{}";)", formatTable(), actualCommand.newTableName);
405 },
406 [&formatTable, this](AddColumn const& actualCommand) -> std::string {
407 return std::format(R"(ALTER TABLE {} ADD COLUMN "{}" {} {};)",
408 formatTable(),
409 actualCommand.columnName,
410 ColumnType(actualCommand.columnType),
411 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
412 },
413 [&formatTable, this](AlterColumn const& actualCommand) -> std::string {
414 return std::format(R"(ALTER TABLE {} ALTER COLUMN "{}" {} {};)",
415 formatTable(),
416 actualCommand.columnName,
417 ColumnType(actualCommand.columnType),
418 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
419 },
420 [&formatTable](RenameColumn const& actualCommand) -> std::string {
421 return std::format(R"(ALTER TABLE {} RENAME COLUMN "{}" TO "{}";)",
422 formatTable(),
423 actualCommand.oldColumnName,
424 actualCommand.newColumnName);
425 },
426 [&formatTable](DropColumn const& actualCommand) -> std::string {
427 return std::format(R"(ALTER TABLE {} DROP COLUMN "{}";)", formatTable(), actualCommand.columnName);
428 },
429 [tableName](AddIndex const& actualCommand) -> std::string {
430 using namespace std::string_view_literals;
431 auto const uniqueStr = actualCommand.unique ? "UNIQUE "sv : ""sv;
432 return std::format(R"(CREATE {2}INDEX "{0}_{1}_index" ON "{0}" ("{1}");)",
433 tableName,
434 actualCommand.columnName,
435 uniqueStr);
436 },
437 [tableName](DropIndex const& actualCommand) -> std::string {
438 return std::format(R"(DROP INDEX "{0}_{1}_index";)", tableName, actualCommand.columnName);
439 },
440 [&formatTable, tableName](AddForeignKey const& actualCommand) -> std::string {
441 return std::format(
442 R"(ALTER TABLE {} ADD {};)",
443 formatTable(),
444 BuildForeignKeyConstraint(tableName, actualCommand.columnName, actualCommand.referencedColumn));
445 },
446 [&formatTable, tableName](DropForeignKey const& actualCommand) -> std::string {
447 return std::format(R"(ALTER TABLE {} DROP CONSTRAINT "{}";)",
448 formatTable(),
449 std::format("FK_{}_{}", tableName, actualCommand.columnName));
450 },
451 [tableName](AddCompositeForeignKey const& /*actualCommand*/) -> std::string {
452 // SQLite limitation: ALTER TABLE ADD CONSTRAINT not supported.
453 // We return empty string or comment to satisfy visitor.
454 return std::format(R"(-- AddCompositeForeignKey not supported for {};)", tableName);
455 },
456 [&formatTable, this](AddColumnIfNotExists const& actualCommand) -> std::string {
457 // SQLite doesn't support IF NOT EXISTS for ADD COLUMN.
458 // Generate a comment and the statement; caller should handle errors.
459 return std::format(
460 R"(-- AddColumnIfNotExists: SQLite doesn't support IF NOT EXISTS for columns
461ALTER TABLE {} ADD COLUMN "{}" {} {};)",
462 formatTable(),
463 actualCommand.columnName,
464 ColumnType(actualCommand.columnType),
465 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
466 },
467 [&formatTable](DropColumnIfExists const& actualCommand) -> std::string {
468 // SQLite doesn't support IF EXISTS for DROP COLUMN.
469 return std::format(
470 R"(-- DropColumnIfExists: SQLite doesn't support IF EXISTS for columns
471ALTER TABLE {} DROP COLUMN "{}";)",
472 formatTable(),
473 actualCommand.columnName);
474 },
475 [tableName](DropIndexIfExists const& actualCommand) -> std::string {
476 return std::format(R"(DROP INDEX IF EXISTS "{0}_{1}_index";)", tableName, actualCommand.columnName);
477 },
478 },
479 command);
480 }
481
482 return { sqlQueryString.str() };
483 }
484
485 [[nodiscard]] std::string ColumnType(SqlColumnTypeDefinition const& type) const override
486 {
487 using namespace SqlColumnTypeDefinitions;
488 return std::visit(detail::overloaded {
489 [](Bigint const&) -> std::string { return "BIGINT"; },
490 [](Binary const&) -> std::string { return "BLOB"; },
491 [](Bool const&) -> std::string { return "BOOLEAN"; },
492 [](Char const& type) -> std::string { return std::format("CHAR({})", type.size); },
493 [](Date const&) -> std::string { return "DATE"; },
494 [](DateTime const&) -> std::string { return "DATETIME"; },
495 [](Decimal const& type) -> std::string {
496 return std::format("DECIMAL({}, {})", type.precision, type.scale);
497 },
498 [](Guid const&) -> std::string { return "GUID"; },
499 [](Integer const&) -> std::string { return "INTEGER"; },
500 [](NChar const& type) -> std::string { return std::format("NCHAR({})", type.size); },
501 [](NVarchar const& type) -> std::string { return std::format("NVARCHAR({})", type.size); },
502 [](Real const&) -> std::string { return "REAL"; },
503 [](Smallint const&) -> std::string { return "SMALLINT"; },
504 [](Text const&) -> std::string { return "TEXT"; },
505 [](Time const&) -> std::string { return "TIME"; },
506 [](Timestamp const&) -> std::string { return "TIMESTAMP"; },
507 [](Tinyint const&) -> std::string { return "TINYINT"; },
508 [](VarBinary const& type) -> std::string { return std::format("VARBINARY({})", type.size); },
509 [](Varchar const& type) -> std::string { return std::format("VARCHAR({})", type.size); },
510 },
511 type);
512 }
513
514 [[nodiscard]] StringList DropTable(std::string_view /*schemaName*/,
515 std::string_view const& tableName,
516 bool ifExists = false,
517 bool cascade = false) const override
518 {
519 // SQLite doesn't support CASCADE syntax, but if FK constraints are disabled
520 // (PRAGMA foreign_keys = OFF), dropping works. The cascade flag is ignored.
521 // SQLite doesn't support schemas - ignore schemaName parameter
522 (void) cascade;
523 if (ifExists)
524 return { std::format(R"(DROP TABLE IF EXISTS "{}";)", tableName) };
525 else
526 return { std::format(R"(DROP TABLE "{}";)", tableName) };
527 }
528
529 [[nodiscard]] std::string QueryServerVersion() const override
530 {
531 return "SELECT sqlite_version()";
532 }
533};
534
535} // namespace Lightweight