Lightweight 0.20260617.0
Loading...
Searching...
No Matches
SqlServerFormatter.hpp
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include "../SqlQueryFormatter.hpp"
5#include "SQLiteFormatter.hpp"
6
7#include <reflection-cpp/reflection.hpp>
8
9#include <cassert>
10#include <format>
11
12namespace Lightweight
13{
14
15class SqlServerQueryFormatter final: public SQLiteQueryFormatter
16{
17 protected:
18 [[nodiscard]] static std::string FormatFromTable(std::string_view table)
19 {
20 // If already quoted (starts with [ or "), return as-is
21 if (!table.empty() && (table.front() == '[' || table.front() == '"'))
22 return std::string(table);
23 // For backward compatibility, use double quotes for simple table names
24 // Square brackets are used for qualified names via QualifiedTableName
25 return std::format(R"("{}")", table);
26 }
27
28 public:
29 [[nodiscard]] bool RequiresTableRebuildForForeignKeyChange() const noexcept override
30 {
31 return false;
32 }
33
34 [[nodiscard]] bool SupportsBatchedSchemaIntrospection() const noexcept override
35 {
36 return true;
37 }
38
39 [[nodiscard]] StringList DropTable(std::string_view schemaName,
40 std::string_view const& tableName,
41 bool ifExists = false,
42 bool cascade = false) const override
43 {
44 StringList result;
45
46 if (cascade)
47 {
48 // Drop all FK constraints referencing this table first using dynamic SQL. An empty
49 // schema means the connection's DEFAULT schema (SCHEMA_NAME()) — the same schema the
50 // unqualified DROP TABLE below resolves to — NOT a hard-coded 'dbo': for users whose
51 // default schema differs (e.g. one named after the login), a 'dbo' filter matches
52 // nothing, the referencing FKs survive, and the DROP TABLE fails.
53 std::string const schemaFilter = schemaName.empty() ? "SCHEMA_NAME()" : std::format("'{}'", schemaName);
54
55 result.emplace_back(std::format(
56 R"(DECLARE @sql NVARCHAR(MAX) = N'';
57SELECT @sql = @sql + 'ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(fk.parent_object_id)) + '.' + QUOTENAME(OBJECT_NAME(fk.parent_object_id)) + ' DROP CONSTRAINT ' + QUOTENAME(fk.name) + '; '
58FROM sys.foreign_keys fk
59WHERE OBJECT_NAME(fk.referenced_object_id) = '{}' AND OBJECT_SCHEMA_NAME(fk.referenced_object_id) = {};
60EXEC sp_executesql @sql;)",
61 tableName,
62 schemaFilter));
63 }
64
65 // Then drop the table
66 if (ifExists)
67 result.emplace_back(std::format("DROP TABLE IF EXISTS {};", FormatTableName(schemaName, tableName)));
68 else
69 result.emplace_back(std::format("DROP TABLE {};", FormatTableName(schemaName, tableName)));
70
71 return result;
72 }
73
74 [[nodiscard]] std::string BinaryLiteral(std::span<uint8_t const> data) const override
75 {
76 std::string result;
77 result.reserve((data.size() * 2) + 2);
78 result += "0x";
79 for (uint8_t byte: data)
80 result += std::format("{:02X}", byte);
81 return result;
82 }
83
84 /// @brief Emits a SQL Server string literal that round-trips arbitrary UTF-8
85 /// content correctly under any database/connection code page.
86 ///
87 /// MSSQL parses the bytes between `'...'` (and `N'...'`) using the client/connection
88 /// ANSI code page — *not* UTF-8. A UTF-8 multi-byte sequence like `0xC3 0xBC` (`ü`)
89 /// embedded raw in `N'...'` is decoded as two separate CP-1252 characters (`ü`),
90 /// which:
91 /// 1. garbles the stored value, and
92 /// 2. inflates the perceived character count, causing legitimate 100-codepoint
93 /// strings to overflow `NVARCHAR(100)` with `String or binary data would be
94 /// truncated` (error 2628).
95 ///
96 /// The `N'...'` prefix alone is *not* enough: it tells MSSQL to store as Unicode,
97 /// but doesn't change how the source bytes are decoded.
98 ///
99 /// To get exact round-tripping we split the literal at every non-ASCII codepoint
100 /// and concatenate `NCHAR(N)` calls for each one:
101 ///
102 /// ```
103 /// "EK-Min für x" → N'EK-Min f' + NCHAR(252) + N'r x'
104 /// ```
105 ///
106 /// `NCHAR()` takes the Unicode codepoint as an integer and returns the matching
107 /// `nchar(1)`. Concatenation with `+` glues the slices into one Unicode value
108 /// MSSQL counts at exactly the codepoint length our application sees, so
109 /// `lup-truncate`'s codepoint accounting matches the server's char accounting.
110 ///
111 /// Supplementary-plane codepoints (> U+FFFF) are emitted as a UTF-16 surrogate
112 /// pair (two `NCHAR()` calls) — required for any potential emoji or rare CJK in
113 /// the data.
114 [[nodiscard]] std::string StringLiteral(std::string_view value) const noexcept override
115 {
116 return EncodeUnicodeLiteral(value);
117 }
118
119 [[nodiscard]] std::string StringLiteral(char value) const noexcept override
120 {
121 // Single-char path goes through the same encoder so the escaping rules
122 // stay in one place. ASCII fast-paths produce `N'x'` directly.
123 char const buf[1] = { value };
124 return EncodeUnicodeLiteral(std::string_view { buf, 1 });
125 }
126
127 private:
128 /// @brief Decodes the byte length of the UTF-8 sequence starting at `s[i]`.
129 /// Returns `1` (and treats the byte as ASCII) for malformed lead bytes so the
130 /// encoder always makes forward progress.
131 static constexpr std::size_t Utf8SequenceLength(unsigned char c) noexcept
132 {
133 if (c < 0x80)
134 return 1;
135 if (c < 0xC0)
136 return 1; // stray continuation; treat as 1 to advance
137 if (c < 0xE0)
138 return 2;
139 if (c < 0xF0)
140 return 3;
141 return 4;
142 }
143
144 /// @brief Decodes one UTF-8 codepoint at `s[i]`. Returns the codepoint and the
145 /// number of bytes consumed. Malformed sequences yield the lead byte verbatim
146 /// as a single-byte codepoint so the encoder still produces *some* output.
147 static constexpr std::pair<char32_t, std::size_t> DecodeUtf8(std::string_view s, std::size_t i) noexcept
148 {
149 auto const len = Utf8SequenceLength(static_cast<unsigned char>(s[i]));
150 if (len == 1 || i + len > s.size())
151 return { static_cast<char32_t>(static_cast<unsigned char>(s[i])), 1 };
152 char32_t cp = 0;
153 auto const lead = static_cast<unsigned char>(s[i]);
154 switch (len)
155 {
156 case 2:
157 cp = lead & 0x1F;
158 break;
159 case 3:
160 cp = lead & 0x0F;
161 break;
162 case 4:
163 cp = lead & 0x07;
164 break;
165 default:
166 cp = lead;
167 break; // unreachable given the early-return above
168 }
169 for (std::size_t k = 1; k < len; ++k)
170 cp = (cp << 6) | (static_cast<unsigned char>(s[i + k]) & 0x3F);
171 return { cp, len };
172 }
173
174 /// @brief Encodes one codepoint into the running output. ASCII goes verbatim
175 /// (with `'` doubled per SQL escaping); BMP non-ASCII becomes `NCHAR(N)`;
176 /// supplementary-plane codepoints become a surrogate pair.
177 static void AppendCodepoint(std::string& out, bool& inQuotedRun, char32_t cp)
178 {
179 auto const closeRun = [&] {
180 if (inQuotedRun)
181 {
182 out += '\'';
183 inQuotedRun = false;
184 }
185 };
186 auto const openRun = [&] {
187 if (!inQuotedRun)
188 {
189 if (!out.empty())
190 out += " + ";
191 out += "N'";
192 inQuotedRun = true;
193 }
194 };
195
196 if (cp < 0x80)
197 {
198 openRun();
199 if (cp == '\'')
200 out += "''";
201 else
202 out += static_cast<char>(cp);
203 return;
204 }
205 closeRun();
206 if (!out.empty())
207 out += " + ";
208 if (cp <= 0xFFFF)
209 {
210 out += std::format("NCHAR({})", static_cast<unsigned>(cp));
211 return;
212 }
213 // Supplementary plane: emit a UTF-16 surrogate pair.
214 char32_t const adjusted = cp - 0x10000;
215 char32_t const hi = 0xD800 + (adjusted >> 10);
216 char32_t const lo = 0xDC00 + (adjusted & 0x3FF);
217 out += std::format("NCHAR({}) + NCHAR({})", static_cast<unsigned>(hi), static_cast<unsigned>(lo));
218 }
219
220 /// @brief Top-level encoder: walks the UTF-8 input and emits a T-SQL expression
221 /// that evaluates to the input string under any client code page. Empty inputs
222 /// produce `N''` (the canonical empty Unicode literal).
223 static std::string EncodeUnicodeLiteral(std::string_view value)
224 {
225 if (value.empty())
226 return "N''";
227 std::string out;
228 out.reserve(value.size() + 4);
229 bool inQuotedRun = false;
230 std::size_t i = 0;
231 while (i < value.size())
232 {
233 auto const [cp, len] = DecodeUtf8(value, i);
234 AppendCodepoint(out, inQuotedRun, cp);
235 i += len;
236 }
237 if (inQuotedRun)
238 out += '\'';
239 return out;
240 }
241
242 public:
243 [[nodiscard]] std::string QualifiedTableName(std::string_view schema, std::string_view table) const override
244 {
245 if (schema.empty())
246 return std::format("[{}]", table);
247 return std::format("[{}].[{}]", schema, table);
248 }
249
250 [[nodiscard]] std::string QueryLastInsertId(std::string_view /*tableName*/) const override
251 {
252 // TODO: Figure out how to get the last insert id in SQL Server for a given table.
253 return std::format("SELECT @@IDENTITY");
254 }
255
256 [[nodiscard]] std::string_view BooleanLiteral(bool literalValue) const noexcept override
257 {
258 return literalValue ? "1" : "0";
259 }
260
261 [[nodiscard]] std::string_view DateFunction() const noexcept override
262 {
263 return "GETDATE()";
264 }
265
266 [[nodiscard]] std::string SelectFirst(bool distinct,
267 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
268 std::string_view fields,
269 std::string_view fromTable,
270 std::string_view fromTableAlias,
271 std::string_view tableJoins,
272 std::string_view whereCondition,
273 std::string_view orderBy,
274 size_t count) const override
275 {
276 std::stringstream sqlQueryString;
277 sqlQueryString << "SELECT";
278 if (distinct)
279 sqlQueryString << " DISTINCT";
280 sqlQueryString << " TOP " << count;
281 sqlQueryString << ' ' << fields;
282 sqlQueryString << " FROM " << FormatFromTable(fromTable);
283 if (!fromTableAlias.empty())
284 sqlQueryString << " AS [" << fromTableAlias << ']';
285 sqlQueryString << tableJoins;
286 sqlQueryString << whereCondition;
287 sqlQueryString << orderBy;
288 return sqlQueryString.str();
289 }
290
291 [[nodiscard]] std::string SelectRange(bool distinct,
292 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
293 std::string_view fields,
294 std::string_view fromTable,
295 std::string_view fromTableAlias,
296 std::string_view tableJoins,
297 std::string_view whereCondition,
298 std::string_view orderBy,
299 std::string_view groupBy,
300 std::size_t offset,
301 std::size_t limit) const override
302 {
303 assert(!orderBy.empty());
304 std::stringstream sqlQueryString;
305 sqlQueryString << "SELECT " << fields;
306 if (distinct)
307 sqlQueryString << " DISTINCT";
308 sqlQueryString << " FROM " << FormatFromTable(fromTable);
309 if (!fromTableAlias.empty())
310 sqlQueryString << " AS [" << fromTableAlias << ']';
311 sqlQueryString << tableJoins;
312 sqlQueryString << whereCondition;
313 sqlQueryString << groupBy;
314 sqlQueryString << orderBy;
315 sqlQueryString << " OFFSET " << offset << " ROWS FETCH NEXT " << limit << " ROWS ONLY";
316 return sqlQueryString.str();
317 }
318
319 [[nodiscard]] std::string ColumnType(SqlColumnTypeDefinition const& type) const override
320 {
321 using namespace SqlColumnTypeDefinitions;
322 return std::visit(detail::overloaded {
323 [](Bigint const&) -> std::string { return "BIGINT"; },
324 [](Binary const& type) -> std::string {
325 if (type.size == 0 || type.size > 8000)
326 return "VARBINARY(MAX)";
327 else
328 return std::format("VARBINARY({})", type.size);
329 },
330 [](Bool const&) -> std::string { return "BIT"; },
331 [](Char const& type) -> std::string { return std::format("CHAR({})", type.size); },
332 [](Date const&) -> std::string { return "DATE"; },
333 [](DateTime const&) -> std::string { return "DATETIME"; },
334 [](Decimal const& type) -> std::string {
335 return std::format("DECIMAL({}, {})", type.precision, type.scale);
336 },
337 [](Guid const&) -> std::string { return "UNIQUEIDENTIFIER"; },
338 [](Integer const&) -> std::string { return "INTEGER"; },
339 [](NChar const& type) -> std::string { return std::format("NCHAR({})", type.size); },
340 [](NVarchar const& type) -> std::string {
341 if (type.size == 0 || type.size > SqlOptimalMaxColumnSize)
342 return "NVARCHAR(MAX)";
343 else
344 return std::format("NVARCHAR({})", type.size);
345 },
346 [](Real const& type) -> std::string {
347 // REAL is FLOAT(24) (float32); a Real with precision > 24 (e.g.
348 // an introspected float(53) column) must round-trip as FLOAT(53)
349 // or restore silently narrows every double to float32.
350 return type.precision > 24 ? "FLOAT(53)" : "REAL";
351 },
352 [](Smallint const&) -> std::string { return "SMALLINT"; },
353 [](Text const&) -> std::string { return "VARCHAR(MAX)"; },
354 [](Time const&) -> std::string { return "TIME"; },
355 [](Timestamp const&) -> std::string { return "TIMESTAMP"; },
356 [](Tinyint const&) -> std::string { return "TINYINT"; },
357 [](VarBinary const& type) -> std::string {
358 if (type.size == 0 || type.size > 8000)
359 return "VARBINARY(MAX)";
360 else
361 return std::format("VARBINARY({})", type.size);
362 },
363 [](Varchar const& type) -> std::string {
364 if (type.size == 0 || type.size > SqlOptimalMaxColumnSize)
365 return "VARCHAR(MAX)";
366 else
367 return std::format("VARCHAR({})", type.size);
368 },
369 },
370 type);
371 }
372
373 [[nodiscard]] std::string BuildColumnDefinition(SqlColumnDeclaration const& column) const override
374 {
375 std::stringstream sqlQueryString;
376 sqlQueryString << '"' << column.name << "\" " << ColumnType(column.type);
377
378 if (column.required)
379 sqlQueryString << " NOT NULL";
380
381 if (column.primaryKey == SqlPrimaryKeyType::AUTO_INCREMENT)
382 sqlQueryString << " IDENTITY(1,1) PRIMARY KEY";
383 else if (column.primaryKey == SqlPrimaryKeyType::NONE && !column.index && column.unique)
384 sqlQueryString << " UNIQUE";
385
386 if (!column.defaultValue.empty())
387 sqlQueryString << " DEFAULT " << column.defaultValue;
388
389 return sqlQueryString.str();
390 }
391
392 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
393 [[nodiscard]] StringList CreateTable(std::string_view schema,
394 std::string_view tableName,
395 std::vector<SqlColumnDeclaration> const& columns,
396 std::vector<SqlCompositeForeignKeyConstraint> const& foreignKeys,
397 bool ifNotExists = false) const override
398 {
399 std::stringstream ss;
400
401 // SQL Server doesn't have CREATE TABLE IF NOT EXISTS, use conditional block
402 if (ifNotExists)
403 {
404 std::string schemaFilter = schema.empty() ? "dbo" : std::string(schema);
405 ss << std::format("IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = '{}' AND schema_id = SCHEMA_ID('{}'))\n",
406 tableName,
407 schemaFilter);
408 }
409
410 ss << std::format("CREATE TABLE {} (", FormatTableName(schema, tableName));
411
412 bool first = true;
413 for (auto const& column: columns)
414 {
415 if (!first)
416 ss << ",";
417 first = false;
418 ss << "\n " << BuildColumnDefinition(column);
419 }
420
421 auto const primaryKeys = [&]() -> std::vector<std::string> {
422 std::vector<std::pair<uint16_t, std::string>> indexedPrimaryKeys;
423 for (auto const& col: columns)
424 if (col.primaryKey != SqlPrimaryKeyType::NONE)
425 indexedPrimaryKeys.emplace_back(col.primaryKeyIndex, col.name);
426 std::ranges::sort(indexedPrimaryKeys, [](auto const& a, auto const& b) { return a.first < b.first; });
427
428 std::vector<std::string> primaryKeys;
429 primaryKeys.reserve(indexedPrimaryKeys.size());
430 for (auto const& [index, name]: indexedPrimaryKeys)
431 primaryKeys.push_back(name);
432 return primaryKeys;
433 }();
434
435 if (!primaryKeys.empty())
436 {
437 // If primary key is AUTO_INCREMENT, it's already defined inline in BuildColumnDefinition.
438 // Only add explicit PRIMARY KEY constraint if NOT AUTO_INCREMENT?
439 // SQLiteFormatter logic:
440 // if (!primaryKeys.empty()) ss << ", PRIMARY KEY (" << Join(primaryKeys, ", ") << ")";
441 // But BuildColumnDefinition adds "PRIMARY KEY" for AUTO_INCREMENT!
442 // Double primary key definition is invalid.
443
444 // Check if any column is AUTO_INCREMENT
445 bool hasIdentity = false;
446 for (auto const& col: columns)
447 if (col.primaryKey == SqlPrimaryKeyType::AUTO_INCREMENT)
448 hasIdentity = true;
449
450 if (!hasIdentity)
451 {
452 ss << ",\n PRIMARY KEY (";
453 bool firstPk = true;
454 for (auto const& pk: primaryKeys)
455 {
456 if (!firstPk)
457 ss << ", ";
458 firstPk = false;
459 ss << '"' << pk << '"';
460 }
461 ss << ")";
462 }
463 }
464
465 if (!foreignKeys.empty())
466 {
467 for (auto const& fk: foreignKeys)
468 {
469 ss << ",\n CONSTRAINT \"" << BuildForeignKeyConstraintName(tableName, fk.columns) << '"'
470 << " FOREIGN KEY (";
471
472 size_t i = 0;
473 for (auto const& col: fk.columns)
474 {
475 if (i++ > 0)
476 ss << ", ";
477 ss << '"' << col << '"';
478 }
479
480 ss << ") REFERENCES " << FormatTableName(schema, fk.referencedTableName) << " (";
481
482 i = 0;
483 for (auto const& col: fk.referencedColumns)
484 {
485 if (i++ > 0)
486 ss << ", ";
487 ss << '"' << col << '"';
488 }
489 ss << ")";
490 }
491 }
492
493 // Add single-column foreign keys that were defined inline in SQLite but need to be table-constraints here
494 // or just appended if we didn't add them in BuildColumnDefinition (which we didn't).
495 for (auto const& column: columns)
496 {
497 if (column.foreignKey)
498 {
499 ss << ",\n " << BuildForeignKeyConstraint(tableName, column.name, *column.foreignKey);
500 }
501 }
502
503 ss << "\n);";
504
505 StringList result;
506 result.emplace_back(ss.str());
507
508 // Create Indexes
509 for (SqlColumnDeclaration const& column: columns)
510 {
511 if (column.index && column.primaryKey == SqlPrimaryKeyType::NONE)
512 {
513 // primary keys are always indexed
514 if (column.unique)
515 {
516 if (schema.empty())
517 result.emplace_back(std::format(R"(CREATE UNIQUE INDEX "{}_{}_index" ON "{}" ("{}");)",
518 tableName,
519 column.name,
520 tableName,
521 column.name));
522 else
523 result.emplace_back(std::format(R"(CREATE UNIQUE INDEX "{}_{}_index" ON "{}"."{}" ("{}");)",
524 tableName,
525 column.name,
526 schema,
527 tableName,
528 column.name));
529 }
530 else
531 {
532 if (schema.empty())
533 result.emplace_back(std::format(R"(CREATE INDEX "{}_{}_index" ON "{}" ("{}");)",
534 tableName,
535 column.name,
536 tableName,
537 column.name));
538 else
539 result.emplace_back(std::format(R"(CREATE INDEX "{}_{}_index" ON "{}"."{}" ("{}");)",
540 tableName,
541 column.name,
542 schema,
543 tableName,
544 column.name));
545 }
546 }
547 }
548
549 return result;
550 }
551
552 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
553 [[nodiscard]] StringList AlterTable(std::string_view schemaName,
554 std::string_view tableName,
555 std::vector<SqlAlterTableCommand> const& commands) const override
556 {
557 std::stringstream sqlQueryString;
558
559 int currentCommand = 0;
560 for (SqlAlterTableCommand const& command: commands)
561 {
562 if (currentCommand > 0)
563 sqlQueryString << '\n';
564 ++currentCommand;
565
566 using namespace SqlAlterTableCommands;
567 sqlQueryString << std::visit(
568 detail::overloaded {
569 [schemaName, tableName](RenameTable const& actualCommand) -> std::string {
570 return std::format(R"(ALTER TABLE {} RENAME TO "{}";)",
571 FormatTableName(schemaName, tableName),
572 actualCommand.newTableName);
573 },
574 [schemaName, tableName, this](AddColumn const& actualCommand) -> std::string {
575 return std::format(R"(ALTER TABLE {} ADD "{}" {} {};)",
576 FormatTableName(schemaName, tableName),
577 actualCommand.columnName,
578 ColumnType(actualCommand.columnType),
579 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
580 },
581 [schemaName, tableName, this](AlterColumn const& actualCommand) -> std::string {
582 return std::format(R"(ALTER TABLE {} ALTER COLUMN "{}" {} {};)",
583 FormatTableName(schemaName, tableName),
584 actualCommand.columnName,
585 ColumnType(actualCommand.columnType),
586 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
587 },
588 [schemaName, tableName](RenameColumn const& actualCommand) -> std::string {
589 return std::format(R"(ALTER TABLE {} RENAME COLUMN "{}" TO "{}";)",
590 FormatTableName(schemaName, tableName),
591 actualCommand.oldColumnName,
592 actualCommand.newColumnName);
593 },
594 [schemaName, tableName](DropColumn const& actualCommand) -> std::string {
595 return std::format(R"(ALTER TABLE {} DROP COLUMN "{}";)",
596 FormatTableName(schemaName, tableName),
597 actualCommand.columnName);
598 },
599 [schemaName, tableName](AddIndex const& actualCommand) -> std::string {
600 using namespace std::string_view_literals;
601 auto const uniqueStr = actualCommand.unique ? "UNIQUE "sv : ""sv;
602 if (schemaName.empty())
603 return std::format(R"(CREATE {2}INDEX "{0}_{1}_index" ON "{0}" ("{1}");)",
604 tableName,
605 actualCommand.columnName,
606 uniqueStr);
607 else
608 return std::format(R"(CREATE {3}INDEX "{0}_{1}_{2}_index" ON "{0}"."{1}" ("{2}");)",
609 schemaName,
610 tableName,
611 actualCommand.columnName,
612 uniqueStr);
613 },
614 [schemaName, tableName](DropIndex const& actualCommand) -> std::string {
615 if (schemaName.empty())
616 return std::format(R"(DROP INDEX "{0}_{1}_index";)", tableName, actualCommand.columnName);
617 else
618 return std::format(
619 R"(DROP INDEX "{0}_{1}_{2}_index";)", schemaName, tableName, actualCommand.columnName);
620 },
621 [schemaName, tableName](AddForeignKey const& actualCommand) -> std::string {
622 // Idempotent ADD CONSTRAINT — re-applying a migration must be a no-op.
623 // SQL Server has no `ADD CONSTRAINT IF NOT EXISTS`, so the guard is
624 // expressed as `IF NOT EXISTS (SELECT … FROM sys.foreign_keys …)`.
625 auto const fkName = BuildForeignKeyConstraintName(
626 tableName, std::array { std::string_view { actualCommand.columnName } });
627 return std::format(
628 R"(IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = '{0}') ALTER TABLE {1} ADD {2};)",
629 fkName,
630 FormatTableName(schemaName, tableName),
631 BuildForeignKeyConstraint(tableName, actualCommand.columnName, actualCommand.referencedColumn));
632 },
633 [schemaName, tableName](DropForeignKey const& actualCommand) -> std::string {
634 return std::format(R"(ALTER TABLE {} DROP CONSTRAINT "{}";)",
635 FormatTableName(schemaName, tableName),
637 tableName, std::array { std::string_view { actualCommand.columnName } }));
638 },
639 [schemaName, tableName](AddCompositeForeignKey const& actualCommand) -> std::string {
640 std::stringstream ss;
641 ss << "ALTER TABLE " << FormatTableName(schemaName, tableName) << " ADD CONSTRAINT \""
642 << BuildForeignKeyConstraintName(tableName, actualCommand.columns) << "\" FOREIGN KEY (";
643
644 size_t i = 0;
645 for (auto const& col: actualCommand.columns)
646 {
647 if (i++ > 0)
648 ss << ", ";
649 ss << '"' << col << '"';
650 }
651 ss << ") REFERENCES " << FormatTableName(schemaName, actualCommand.referencedTableName) << " (";
652
653 i = 0;
654 for (auto const& col: actualCommand.referencedColumns)
655 {
656 if (i++ > 0)
657 ss << ", ";
658 ss << '"' << col << '"';
659 }
660 ss << ");";
661 return ss.str();
662 },
663 [schemaName, tableName, this](AddColumnIfNotExists const& actualCommand) -> std::string {
664 // SQL Server uses conditional IF NOT EXISTS
665 return std::format(
666 R"(IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('{}') AND name = '{}')
667ALTER TABLE {} ADD "{}" {} {};)",
668 FormatTableName(schemaName, tableName),
669 actualCommand.columnName,
670 FormatTableName(schemaName, tableName),
671 actualCommand.columnName,
672 ColumnType(actualCommand.columnType),
673 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
674 },
675 [schemaName, tableName](DropColumnIfExists const& actualCommand) -> std::string {
676 // SQL Server uses conditional IF EXISTS
677 return std::format(
678 R"(IF EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('{}') AND name = '{}')
679ALTER TABLE {} DROP COLUMN "{}";)",
680 FormatTableName(schemaName, tableName),
681 actualCommand.columnName,
682 FormatTableName(schemaName, tableName),
683 actualCommand.columnName);
684 },
685 [schemaName, tableName](DropIndexIfExists const& actualCommand) -> std::string {
686 if (schemaName.empty())
687 return std::format(
688 R"(IF EXISTS (SELECT * FROM sys.indexes WHERE name = '{0}_{1}_index' AND object_id = OBJECT_ID('{0}'))
689DROP INDEX "{0}_{1}_index" ON "{0}";)",
690 tableName,
691 actualCommand.columnName);
692 else
693 return std::format(
694 R"(IF EXISTS (SELECT * FROM sys.indexes WHERE name = '{0}_{1}_{2}_index')
695DROP INDEX "{0}_{1}_{2}_index" ON "{0}"."{1}";)",
696 schemaName,
697 tableName,
698 actualCommand.columnName);
699 },
700 },
701 command);
702 }
703
704 return { sqlQueryString.str() };
705 }
706
707 [[nodiscard]] std::string QueryServerVersion() const override
708 {
709 return "SELECT @@VERSION";
710 }
711
712 /// Microsoft SQL Server uses `sp_getapplock` / `sp_releaseapplock`. Inline
713 /// delegation keeps the vtable weak — see `SQLiteQueryFormatter::AdvisoryLockOps()`
714 /// for the rationale.
715 [[nodiscard]] SqlAdvisoryLockHandler const& AdvisoryLockOps() const override
716 {
717 return SqlServerAdvisoryLockOps();
718 }
719};
720
721} // 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.
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.
SqlPrimaryKeyType
Represents a primary key type.