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