Lightweight 0.20260921.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 RequiresTableRebuildForSchemaChange() 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 std::string_view groupBy,
275 size_t count) const override
276 {
277 std::stringstream sqlQueryString;
278 sqlQueryString << "SELECT";
279 if (distinct)
280 sqlQueryString << " DISTINCT";
281 sqlQueryString << " TOP " << count;
282 sqlQueryString << ' ' << fields;
283 sqlQueryString << " FROM " << FormatFromTable(fromTable);
284 if (!fromTableAlias.empty())
285 sqlQueryString << " AS [" << fromTableAlias << ']';
286 sqlQueryString << tableJoins;
287 sqlQueryString << whereCondition;
288 sqlQueryString << groupBy;
289 sqlQueryString << orderBy;
290 return sqlQueryString.str();
291 }
292
293 [[nodiscard]] std::string SelectRange(bool distinct,
294 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
295 std::string_view fields,
296 std::string_view fromTable,
297 std::string_view fromTableAlias,
298 std::string_view tableJoins,
299 std::string_view whereCondition,
300 std::string_view orderBy,
301 std::string_view groupBy,
302 std::size_t offset,
303 std::size_t limit) const override
304 {
305 assert(!orderBy.empty());
306 std::stringstream sqlQueryString;
307 sqlQueryString << "SELECT ";
308 if (distinct)
309 sqlQueryString << "DISTINCT ";
310 sqlQueryString << fields;
311 sqlQueryString << " FROM " << FormatFromTable(fromTable);
312 if (!fromTableAlias.empty())
313 sqlQueryString << " AS [" << fromTableAlias << ']';
314 sqlQueryString << tableJoins;
315 sqlQueryString << whereCondition;
316 sqlQueryString << groupBy;
317 sqlQueryString << orderBy;
318 sqlQueryString << " OFFSET " << offset << " ROWS FETCH NEXT " << limit << " ROWS ONLY";
319 return sqlQueryString.str();
320 }
321
322 [[nodiscard]] std::string ColumnType(SqlColumnTypeDefinition const& type) const override
323 {
324 using namespace SqlColumnTypeDefinitions;
325 return std::visit(detail::overloaded {
326 [](Bigint const&) -> std::string { return "BIGINT"; },
327 [](Binary const& type) -> std::string {
328 if (type.size == 0 || type.size > 8000)
329 return "VARBINARY(MAX)";
330 else
331 return std::format("VARBINARY({})", type.size);
332 },
333 [](Bool const&) -> std::string { return "BIT"; },
334 [](Char const& type) -> std::string { return std::format("CHAR({})", type.size); },
335 [](Date const&) -> std::string { return "DATE"; },
336 [](DateTime const&) -> std::string { return "DATETIME"; },
337 [](Decimal const& type) -> std::string {
338 return std::format("DECIMAL({}, {})", type.precision, type.scale);
339 },
340 [](Guid const&) -> std::string { return "UNIQUEIDENTIFIER"; },
341 [](Integer const&) -> std::string { return "INTEGER"; },
342 [](NChar const& type) -> std::string { return std::format("NCHAR({})", type.size); },
343 [](NVarchar const& type) -> std::string {
344 if (type.size == 0 || type.size > SqlOptimalMaxColumnSize)
345 return "NVARCHAR(MAX)";
346 else
347 return std::format("NVARCHAR({})", type.size);
348 },
349 [](Real const& type) -> std::string {
350 // REAL is FLOAT(24) (float32); a Real with precision > 24 (e.g.
351 // an introspected float(53) column) must round-trip as FLOAT(53)
352 // or restore silently narrows every double to float32.
353 return type.precision > 24 ? "FLOAT(53)" : "REAL";
354 },
355 [](Smallint const&) -> std::string { return "SMALLINT"; },
356 [](Text const&) -> std::string { return "VARCHAR(MAX)"; },
357 [](Time const&) -> std::string { return "TIME"; },
358 // On SQL Server, `TIMESTAMP` is a deprecated synonym for `rowversion`: an
359 // 8-byte, server-generated, non-writable binary counter, not a point in
360 // time. Emit `DATETIME2` instead, so the column is an actual writable
361 // temporal type, matching `Light::SqlDateTime` on the C++ side.
362 [](Timestamp const&) -> std::string { return "DATETIME2"; },
363 [](Tinyint const&) -> std::string { return "TINYINT"; },
364 [](VarBinary const& type) -> std::string {
365 if (type.size == 0 || type.size > 8000)
366 return "VARBINARY(MAX)";
367 else
368 return std::format("VARBINARY({})", type.size);
369 },
370 [](Varchar const& type) -> std::string {
371 if (type.size == 0 || type.size > SqlOptimalMaxColumnSize)
372 return "VARCHAR(MAX)";
373 else
374 return std::format("VARCHAR({})", type.size);
375 },
376 },
377 type);
378 }
379
380 [[nodiscard]] std::string BuildColumnDefinition(SqlColumnDeclaration const& column) const override
381 {
382 std::stringstream sqlQueryString;
383 sqlQueryString << '"' << column.name << "\" " << ColumnType(column.type);
384
385 if (column.required)
386 sqlQueryString << " NOT NULL";
387
388 if (column.primaryKey == SqlPrimaryKeyType::AUTO_INCREMENT)
389 sqlQueryString << " IDENTITY(1,1) PRIMARY KEY";
390 else if (column.primaryKey == SqlPrimaryKeyType::NONE && !column.index && column.unique)
391 sqlQueryString << " UNIQUE";
392
393 if (!column.defaultValue.empty())
394 sqlQueryString << " DEFAULT " << column.defaultValue;
395
396 return sqlQueryString.str();
397 }
398
399 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
400 [[nodiscard]] StringList CreateTable(std::string_view schema,
401 std::string_view tableName,
402 std::vector<SqlColumnDeclaration> const& columns,
403 std::vector<SqlCompositeForeignKeyConstraint> const& foreignKeys,
404 bool ifNotExists = false) const override
405 {
406 std::stringstream ss;
407
408 // SQL Server doesn't have CREATE TABLE IF NOT EXISTS, use conditional block
409 if (ifNotExists)
410 {
411 std::string schemaFilter = schema.empty() ? "dbo" : std::string(schema);
412 ss << std::format("IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = '{}' AND schema_id = SCHEMA_ID('{}'))\n",
413 tableName,
414 schemaFilter);
415 }
416
417 ss << std::format("CREATE TABLE {} (", FormatTableName(schema, tableName));
418
419 bool first = true;
420 for (auto const& column: columns)
421 {
422 if (!first)
423 ss << ",";
424 first = false;
425 ss << "\n " << BuildColumnDefinition(column);
426 }
427
428 auto const primaryKeys = [&]() -> std::vector<std::string> {
429 std::vector<std::pair<uint16_t, std::string>> indexedPrimaryKeys;
430 for (auto const& col: columns)
431 if (col.primaryKey != SqlPrimaryKeyType::NONE)
432 indexedPrimaryKeys.emplace_back(col.primaryKeyIndex, col.name);
433 std::ranges::sort(indexedPrimaryKeys, [](auto const& a, auto const& b) { return a.first < b.first; });
434
435 std::vector<std::string> primaryKeys;
436 primaryKeys.reserve(indexedPrimaryKeys.size());
437 for (auto const& [index, name]: indexedPrimaryKeys)
438 primaryKeys.push_back(name);
439 return primaryKeys;
440 }();
441
442 if (!primaryKeys.empty())
443 {
444 // If primary key is AUTO_INCREMENT, it's already defined inline in BuildColumnDefinition.
445 // Only add explicit PRIMARY KEY constraint if NOT AUTO_INCREMENT?
446 // SQLiteFormatter logic:
447 // if (!primaryKeys.empty()) ss << ", PRIMARY KEY (" << Join(primaryKeys, ", ") << ")";
448 // But BuildColumnDefinition adds "PRIMARY KEY" for AUTO_INCREMENT!
449 // Double primary key definition is invalid.
450
451 // Check if any column is AUTO_INCREMENT
452 bool hasIdentity = false;
453 for (auto const& col: columns)
454 if (col.primaryKey == SqlPrimaryKeyType::AUTO_INCREMENT)
455 hasIdentity = true;
456
457 if (!hasIdentity)
458 {
459 ss << ",\n PRIMARY KEY (";
460 bool firstPk = true;
461 for (auto const& pk: primaryKeys)
462 {
463 if (!firstPk)
464 ss << ", ";
465 firstPk = false;
466 ss << '"' << pk << '"';
467 }
468 ss << ")";
469 }
470 }
471
472 if (!foreignKeys.empty())
473 {
474 for (auto const& fk: foreignKeys)
475 {
476 ss << ",\n CONSTRAINT \"" << BuildForeignKeyConstraintName(tableName, fk.columns) << '"'
477 << " FOREIGN KEY (";
478
479 size_t i = 0;
480 for (auto const& col: fk.columns)
481 {
482 if (i++ > 0)
483 ss << ", ";
484 ss << '"' << col << '"';
485 }
486
487 ss << ") REFERENCES " << FormatTableName(schema, fk.referencedTableName) << " (";
488
489 i = 0;
490 for (auto const& col: fk.referencedColumns)
491 {
492 if (i++ > 0)
493 ss << ", ";
494 ss << '"' << col << '"';
495 }
496 ss << ")";
497 }
498 }
499
500 // Add single-column foreign keys that were defined inline in SQLite but need to be table-constraints here
501 // or just appended if we didn't add them in BuildColumnDefinition (which we didn't).
502 for (auto const& column: columns)
503 {
504 if (column.foreignKey)
505 {
506 ss << ",\n " << BuildForeignKeyConstraint(tableName, column.name, *column.foreignKey);
507 }
508 }
509
510 ss << "\n);";
511
512 StringList result;
513 result.emplace_back(ss.str());
514
515 // Create Indexes
516 for (SqlColumnDeclaration const& column: columns)
517 {
518 if (column.index && column.primaryKey == SqlPrimaryKeyType::NONE)
519 {
520 // primary keys are always indexed
521 if (column.unique)
522 {
523 if (schema.empty())
524 result.emplace_back(std::format(R"(CREATE UNIQUE INDEX "{}_{}_index" ON "{}" ("{}");)",
525 tableName,
526 column.name,
527 tableName,
528 column.name));
529 else
530 result.emplace_back(std::format(R"(CREATE UNIQUE INDEX "{}_{}_index" ON "{}"."{}" ("{}");)",
531 tableName,
532 column.name,
533 schema,
534 tableName,
535 column.name));
536 }
537 else
538 {
539 if (schema.empty())
540 result.emplace_back(std::format(R"(CREATE INDEX "{}_{}_index" ON "{}" ("{}");)",
541 tableName,
542 column.name,
543 tableName,
544 column.name));
545 else
546 result.emplace_back(std::format(R"(CREATE INDEX "{}_{}_index" ON "{}"."{}" ("{}");)",
547 tableName,
548 column.name,
549 schema,
550 tableName,
551 column.name));
552 }
553 }
554 }
555
556 return result;
557 }
558
559 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
560 [[nodiscard]] StringList AlterTable(std::string_view schemaName,
561 std::string_view tableName,
562 std::vector<SqlAlterTableCommand> const& commands) const override
563 {
564 std::stringstream sqlQueryString;
565
566 int currentCommand = 0;
567 for (SqlAlterTableCommand const& command: commands)
568 {
569 if (currentCommand > 0)
570 sqlQueryString << '\n';
571 ++currentCommand;
572
573 using namespace SqlAlterTableCommands;
574 sqlQueryString << std::visit(
575 detail::overloaded {
576 [schemaName, tableName](RenameTable const& actualCommand) -> std::string {
577 return std::format(R"(ALTER TABLE {} RENAME TO "{}";)",
578 FormatTableName(schemaName, tableName),
579 actualCommand.newTableName);
580 },
581 [schemaName, tableName, this](AddColumn const& actualCommand) -> std::string {
582 return std::format(R"(ALTER TABLE {} ADD "{}" {} {};)",
583 FormatTableName(schemaName, tableName),
584 actualCommand.columnName,
585 ColumnType(actualCommand.columnType),
586 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
587 },
588 [schemaName, tableName, this](AlterColumn const& actualCommand) -> std::string {
589 return std::format(R"(ALTER TABLE {} ALTER COLUMN "{}" {} {};)",
590 FormatTableName(schemaName, tableName),
591 actualCommand.columnName,
592 ColumnType(actualCommand.columnType),
593 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
594 },
595 [schemaName, tableName](RenameColumn const& actualCommand) -> std::string {
596 return std::format(R"(ALTER TABLE {} RENAME COLUMN "{}" TO "{}";)",
597 FormatTableName(schemaName, tableName),
598 actualCommand.oldColumnName,
599 actualCommand.newColumnName);
600 },
601 [schemaName, tableName](DropColumn const& actualCommand) -> std::string {
602 return std::format(R"(ALTER TABLE {} DROP COLUMN "{}";)",
603 FormatTableName(schemaName, tableName),
604 actualCommand.columnName);
605 },
606 [schemaName, tableName](AddIndex const& actualCommand) -> std::string {
607 using namespace std::string_view_literals;
608 auto const uniqueStr = actualCommand.unique ? "UNIQUE "sv : ""sv;
609 if (schemaName.empty())
610 return std::format(R"(CREATE {2}INDEX "{0}_{1}_index" ON "{0}" ("{1}");)",
611 tableName,
612 actualCommand.columnName,
613 uniqueStr);
614 else
615 return std::format(R"(CREATE {3}INDEX "{0}_{1}_{2}_index" ON "{0}"."{1}" ("{2}");)",
616 schemaName,
617 tableName,
618 actualCommand.columnName,
619 uniqueStr);
620 },
621 [schemaName, tableName](DropIndex const& actualCommand) -> std::string {
622 if (schemaName.empty())
623 return std::format(R"(DROP INDEX "{0}_{1}_index";)", tableName, actualCommand.columnName);
624 else
625 return std::format(
626 R"(DROP INDEX "{0}_{1}_{2}_index";)", schemaName, tableName, actualCommand.columnName);
627 },
628 [schemaName, tableName](AddForeignKey const& actualCommand) -> std::string {
629 // Idempotent ADD CONSTRAINT — re-applying a migration must be a no-op.
630 // SQL Server has no `ADD CONSTRAINT IF NOT EXISTS`, so the guard is
631 // expressed as `IF NOT EXISTS (SELECT … FROM sys.foreign_keys …)`.
632 auto const fkName = BuildForeignKeyConstraintName(
633 tableName, std::array { std::string_view { actualCommand.columnName } });
634 return std::format(
635 R"(IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = '{0}') ALTER TABLE {1} ADD {2};)",
636 fkName,
637 FormatTableName(schemaName, tableName),
638 BuildForeignKeyConstraint(tableName, actualCommand.columnName, actualCommand.referencedColumn));
639 },
640 [schemaName, tableName](DropForeignKey const& actualCommand) -> std::string {
641 return std::format(R"(ALTER TABLE {} DROP CONSTRAINT "{}";)",
642 FormatTableName(schemaName, tableName),
644 tableName, std::array { std::string_view { actualCommand.columnName } }));
645 },
646 [schemaName, tableName](AddCompositeForeignKey const& actualCommand) -> std::string {
647 std::stringstream ss;
648 ss << "ALTER TABLE " << FormatTableName(schemaName, tableName) << " ADD CONSTRAINT \""
649 << BuildForeignKeyConstraintName(tableName, actualCommand.columns) << "\" FOREIGN KEY (";
650
651 size_t i = 0;
652 for (auto const& col: actualCommand.columns)
653 {
654 if (i++ > 0)
655 ss << ", ";
656 ss << '"' << col << '"';
657 }
658 ss << ") REFERENCES " << FormatTableName(schemaName, actualCommand.referencedTableName) << " (";
659
660 i = 0;
661 for (auto const& col: actualCommand.referencedColumns)
662 {
663 if (i++ > 0)
664 ss << ", ";
665 ss << '"' << col << '"';
666 }
667 ss << ");";
668 return ss.str();
669 },
670 [schemaName, tableName, this](AddColumnIfNotExists const& actualCommand) -> std::string {
671 // SQL Server uses conditional IF NOT EXISTS
672 return std::format(
673 R"(IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('{}') AND name = '{}')
674ALTER TABLE {} ADD "{}" {} {};)",
675 FormatTableName(schemaName, tableName),
676 actualCommand.columnName,
677 FormatTableName(schemaName, tableName),
678 actualCommand.columnName,
679 ColumnType(actualCommand.columnType),
680 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
681 },
682 [schemaName, tableName](DropColumnIfExists const& actualCommand) -> std::string {
683 // SQL Server uses conditional IF EXISTS
684 return std::format(
685 R"(IF EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('{}') AND name = '{}')
686ALTER TABLE {} DROP COLUMN "{}";)",
687 FormatTableName(schemaName, tableName),
688 actualCommand.columnName,
689 FormatTableName(schemaName, tableName),
690 actualCommand.columnName);
691 },
692 [schemaName, tableName](DropIndexIfExists const& actualCommand) -> std::string {
693 if (schemaName.empty())
694 return std::format(
695 R"(IF EXISTS (SELECT * FROM sys.indexes WHERE name = '{0}_{1}_index' AND object_id = OBJECT_ID('{0}'))
696DROP INDEX "{0}_{1}_index" ON "{0}";)",
697 tableName,
698 actualCommand.columnName);
699 else
700 return std::format(
701 R"(IF EXISTS (SELECT * FROM sys.indexes WHERE name = '{0}_{1}_{2}_index')
702DROP INDEX "{0}_{1}_{2}_index" ON "{0}"."{1}";)",
703 schemaName,
704 tableName,
705 actualCommand.columnName);
706 },
707 },
708 command);
709 }
710
711 return { sqlQueryString.str() };
712 }
713
714 [[nodiscard]] std::string QueryServerVersion() const override
715 {
716 return "SELECT @@VERSION";
717 }
718
719 /// Microsoft SQL Server uses `sp_getapplock` / `sp_releaseapplock`. Inline
720 /// delegation keeps the vtable weak — see `SQLiteQueryFormatter::AdvisoryLockOps()`
721 /// for the rationale.
722 [[nodiscard]] SqlAdvisoryLockHandler const& AdvisoryLockOps() const override
723 {
724 return SqlServerAdvisoryLockOps();
725 }
726
727 /// SQL Server carries most transient conditions in the native error code, not the SQLSTATE.
728 /// See `SQLiteQueryFormatter::AdvisoryLockOps()` for why this delegates.
729 [[nodiscard]] SqlRetryClassifier const& RetryOps() const noexcept override
730 {
731 return SqlServerRetryOps();
732 }
733};
734
735} // 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.
LIGHTWEIGHT_API SqlRetryClassifier const & SqlServerRetryOps() noexcept
Returns the SQL Server-specific singleton classifier. See SqliteRetryOps().