Lightweight 0.20261105.1
Loading...
Searching...
No Matches
PostgreSqlFormatter.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 <algorithm>
10#include <format>
11#include <string>
12#include <string_view>
13
14namespace Lightweight
15{
16
17class PostgreSqlFormatter final: public SQLiteQueryFormatter
18{
19 public:
20 using SQLiteQueryFormatter::CreateTable;
21
22 [[nodiscard]] bool RequiresTableRebuildForSchemaChange() const noexcept override
23 {
24 return false;
25 }
26
27 [[nodiscard]] bool IdentifiersAreCaseInsensitive() const noexcept override
28 {
29 return false;
30 }
31
32 [[nodiscard]] bool HasNativeGuidType() const noexcept override
33 {
34 return true;
35 }
36
37 [[nodiscard]] bool ComparesFixedCharWithPadSemantics() const noexcept override
38 {
39 return true;
40 }
41
42 [[nodiscard]] bool BoundsBulkLoadTransactionSize() const noexcept override
43 {
44 return false;
45 }
46
47 [[nodiscard]] std::string FoldUnquotedIdentifier(std::string_view identifier) const override
48 {
49 auto folded = std::string(identifier);
50 std::ranges::transform(
51 folded, folded.begin(), [](char c) { return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c; });
52 return folded;
53 }
54
55 [[nodiscard]] bool SupportsSchemas() const noexcept override
56 {
57 return true;
58 }
59
60 [[nodiscard]] std::string CreateSchemaIfNotExists(std::string_view schema) const override
61 {
62 // CREATE SCHEMA IF NOT EXISTS checks the CREATE privilege on the database even when the schema exists.
63 // The dollar-quote tag must not occur in the name, or the name would end the DO body.
64 auto tag = std::string { "$lw$" };
65 auto tagNumber = 0;
66 while (schema.contains(tag))
67 tag = std::format("$lw{}$", ++tagNumber);
68 return std::format(R"(DO {0} BEGIN IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = '{1}') THEN )"
69 R"(CREATE SCHEMA "{2}"; END IF; END {0})",
70 tag,
71 DoubleQuoteCharacter(schema, '\''),
72 DoubleQuoteCharacter(schema, '"'));
73 }
74
75 [[nodiscard]] StringList DropTable(std::string_view schemaName,
76 std::string_view const& tableName,
77 bool ifExists = false,
78 bool cascade = false) const override
79 {
80 std::string sql = ifExists ? std::format("DROP TABLE IF EXISTS {}", FormatTableName(schemaName, tableName))
81 : std::format("DROP TABLE {}", FormatTableName(schemaName, tableName));
82 if (cascade)
83 sql += " CASCADE";
84 sql += ";";
85 return { sql };
86 }
87
88 [[nodiscard]] std::string BinaryLiteral(std::span<uint8_t const> data) const override
89 {
90 std::string result;
91 result.reserve((data.size() * 2) + 4);
92 result += "'\\x";
93 for (uint8_t byte: data)
94 result += std::format("{:02X}", byte);
95 result += "'";
96 return result;
97 }
98
99 [[nodiscard]] std::string QueryLastInsertId(std::string_view /*tableName*/) const override
100 {
101 // NB: Find a better way to do this on the given table.
102 // In our case it works, because we're expected to call this right after an insert.
103 // But a race condition may still happen if another client inserts a row at the same time too.
104 return std::format("SELECT lastval();");
105 }
106
107 [[nodiscard]] std::string_view DateFunction() const noexcept override
108 {
109 return "CURRENT_DATE";
110 }
111
112 /// PostgreSQL session default schema is selected via `search_path`. We
113 /// pin the migration tool's chosen schema first and fall back to `public`
114 /// so unqualified references to built-ins keep resolving. Schema names
115 /// must be pre-validated by the caller (see
116 /// `MigrationManager::SetDefaultSchema`).
117 [[nodiscard]] std::string SetDefaultSchemaStatement(std::string_view schema) const override
118 {
119 if (schema.empty())
120 return {};
121 return std::format(R"(SET search_path TO "{}", public)", schema);
122 }
123
124 [[nodiscard]] std::string BuildColumnDefinition(SqlColumnDeclaration const& column) const override
125 {
126 std::stringstream sqlQueryString;
127
128 sqlQueryString << '"' << column.name << "\" ";
129
130 // Detect PostgreSQL auto-increment columns by checking for nextval() in default value.
131 // This handles restore of backed-up tables where SERIAL columns have their default
132 // value captured as nextval('"TableName_id_seq"'::regclass).
133 bool const isAutoIncrementViaDefault = column.defaultValue.contains("nextval(");
134 bool const isAutoIncrement = column.primaryKey == SqlPrimaryKeyType::AUTO_INCREMENT || isAutoIncrementViaDefault;
135
136 if (isAutoIncrement)
137 sqlQueryString << SerialColumnType(column.type);
138 else
139 sqlQueryString << ColumnType(column.type);
140
141 if (column.required)
142 sqlQueryString << " NOT NULL";
143
144 // Only add inline PRIMARY KEY for explicitly marked AUTO_INCREMENT columns.
145 // For columns detected via nextval() default, the table-level PRIMARY KEY constraint
146 // will handle it to avoid "multiple primary keys" error.
147 if (column.primaryKey == SqlPrimaryKeyType::AUTO_INCREMENT)
148 sqlQueryString << " PRIMARY KEY";
149 else if (column.primaryKey == SqlPrimaryKeyType::NONE && !column.index && column.unique)
150 sqlQueryString << " UNIQUE";
151
152 // Don't output default value for auto-increment columns as SERIAL handles it
153 if (!column.defaultValue.empty() && !isAutoIncrement)
154 sqlQueryString << " DEFAULT " << column.defaultValue;
155
156 return sqlQueryString.str();
157 }
158
159 /// Maps a declared integer column type onto the matching PostgreSQL auto-increment pseudo-type.
160 ///
161 /// PostgreSQL picks the underlying integer width from the serial spelling, so the width must
162 /// follow the declared type. Emitting a plain `SERIAL` for a `Bigint` key would silently create
163 /// a 32-bit `integer` column that caps at 2^31-1 and no longer matches `BIGINT` foreign keys
164 /// referencing it.
165 ///
166 /// @note `Smallint` and `Tinyint` map onto `SMALLSERIAL`, whose underlying `smallint` caps at
167 /// 32767 — declaring a narrow auto-increment key buys a correspondingly narrow key space.
168 /// @note A declared type with no integer equivalent at all (`Guid`, `Varchar`, `Decimal`, ...)
169 /// falls through to `SERIAL`, i.e. it is silently coerced to a 32-bit integer column.
170 /// PostgreSQL accepts that DDL, so the mismatch only surfaces later when reading the
171 /// column back into the declared C++ type. Auto-increment is only meaningful on an
172 /// integer key; prefer rejecting such records at the call site.
173 ///
174 /// @param type The declared column type.
175 /// @return The serial pseudo-type to emit; `SERIAL` for any type without a narrower or wider
176 /// serial equivalent.
177 [[nodiscard]] static std::string_view SerialColumnType(SqlColumnTypeDefinition const& type)
178 {
179 using namespace SqlColumnTypeDefinitions;
180
181 return std::visit(detail::overloaded {
182 [](Bigint const&) -> std::string_view { return "BIGSERIAL"; },
183 [](Smallint const&) -> std::string_view { return "SMALLSERIAL"; },
184 // NB: PostgreSQL has no 1-byte integer type, so the narrowest
185 // available serial is used here, matching ColumnType(Tinyint).
186 [](Tinyint const&) -> std::string_view { return "SMALLSERIAL"; },
187 [](auto const&) -> std::string_view { return "SERIAL"; },
188 },
189 type);
190 }
191
192 /// The widest `VARCHAR(n)` PostgreSQL accepts. A wider column, such as a SQL Server `VARCHAR(MAX)` or `TEXT`
193 /// column read from an archive, can only be `TEXT` here.
194 static constexpr std::size_t MaxVarcharLength = 10'485'760;
195
196 [[nodiscard]] std::string ColumnType(SqlColumnTypeDefinition const& type) const override
197 {
198 using namespace SqlColumnTypeDefinitions;
199
200 // PostgreSQL stores all strings as UTF-8
201 return std::visit(detail::overloaded {
202 [](Bigint const&) -> std::string { return "BIGINT"; },
203 [](Binary const& type) -> std::string { return std::format("BYTEA", type.size); },
204 [](Bool const&) -> std::string { return "BOOLEAN"; },
205 [](Char const& type) -> std::string { return std::format("CHAR({})", type.size); },
206 [](Date const&) -> std::string { return "DATE"; },
207 [](DateTime const&) -> std::string { return "TIMESTAMP"; },
208 [](Decimal const& type) -> std::string {
209 return std::format("DECIMAL({}, {})", type.precision, type.scale);
210 },
211 [](Guid const&) -> std::string { return "UUID"; },
212 [](Integer const&) -> std::string { return "INTEGER"; },
213 [](NChar const& type) -> std::string { return std::format("CHAR({})", type.size); },
214 [](NVarchar const& type) -> std::string {
215 if (type.size == 0 || type.size > MaxVarcharLength)
216 return "TEXT";
217 return std::format("VARCHAR({})", type.size);
218 },
219 [](Real const& type) -> std::string {
220 // PostgreSQL REAL is float4; a Real with precision > 24 (e.g. an
221 // introspected double-precision/float(53) column) must round-trip
222 // as DOUBLE PRECISION or restore silently narrows it to float32.
223 return type.precision > 24 ? "DOUBLE PRECISION" : "REAL";
224 },
225 [](Smallint const&) -> std::string { return "SMALLINT"; },
226 [](Text const&) -> std::string { return "TEXT"; },
227 [](Time const&) -> std::string { return "TIME"; },
228 [](Timestamp const&) -> std::string { return "TIMESTAMP"; },
229 // NB: PostgreSQL doesn't have a TINYINT type, but it does have a SMALLINT type.
230 [](Tinyint const&) -> std::string { return "SMALLINT"; },
231 [](VarBinary const& /*type*/) -> std::string { return std::format("BYTEA"); },
232 [](Varchar const& type) -> std::string {
233 if (type.size == 0 || type.size > MaxVarcharLength)
234 return "TEXT";
235 return std::format("VARCHAR({})", type.size);
236 },
237 },
238 type);
239 }
240
241 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
242 [[nodiscard]] StringList AlterTable(std::string_view schemaName,
243 std::string_view tableName,
244 std::vector<SqlAlterTableCommand> const& commands) const override
245 {
246 std::stringstream sqlQueryString;
247
248 int currentCommand = 0;
249 for (SqlAlterTableCommand const& command: commands)
250 {
251 if (currentCommand > 0)
252 sqlQueryString << '\n';
253 ++currentCommand;
254
255 using namespace SqlAlterTableCommands;
256 sqlQueryString << std::visit(
257 detail::overloaded {
258 [schemaName, tableName](RenameTable const& actualCommand) -> std::string {
259 return std::format(R"(ALTER TABLE {} RENAME TO "{}";)",
260 FormatTableName(schemaName, tableName),
261 actualCommand.newTableName);
262 },
263 [schemaName, tableName, this](AddColumn const& actualCommand) -> std::string {
264 return std::format(R"(ALTER TABLE {} ADD COLUMN "{}" {} {};)",
265 FormatTableName(schemaName, tableName),
266 actualCommand.columnName,
267 ColumnType(actualCommand.columnType),
268 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
269 },
270 [schemaName, tableName, this](AlterColumn const& actualCommand) -> std::string {
271 return std::format(
272 R"(ALTER TABLE {0} ALTER COLUMN "{1}" TYPE {2}, ALTER COLUMN "{1}" {3} NOT NULL;)",
273 FormatTableName(schemaName, tableName),
274 actualCommand.columnName,
275 ColumnType(actualCommand.columnType),
276 actualCommand.nullable == SqlNullable::NotNull ? "SET" : "DROP");
277 },
278 [schemaName, tableName](RenameColumn const& actualCommand) -> std::string {
279 return std::format(R"(ALTER TABLE {} RENAME COLUMN "{}" TO "{}";)",
280 FormatTableName(schemaName, tableName),
281 actualCommand.oldColumnName,
282 actualCommand.newColumnName);
283 },
284 [schemaName, tableName](DropColumn const& actualCommand) -> std::string {
285 return std::format(R"(ALTER TABLE {} DROP COLUMN "{}";)",
286 FormatTableName(schemaName, tableName),
287 actualCommand.columnName);
288 },
289 [schemaName, tableName](AddIndex const& actualCommand) -> std::string {
290 using namespace std::string_view_literals;
291 auto const uniqueStr = actualCommand.unique ? "UNIQUE "sv : ""sv;
292 if (schemaName.empty())
293 return std::format(R"(CREATE {2}INDEX "{0}_{1}_index" ON "{0}" ("{1}");)",
294 tableName,
295 actualCommand.columnName,
296 uniqueStr);
297 else
298 return std::format(R"(CREATE {3}INDEX "{0}_{1}_{2}_index" ON "{0}"."{1}" ("{2}");)",
299 schemaName,
300 tableName,
301 actualCommand.columnName,
302 uniqueStr);
303 },
304 [schemaName, tableName](DropIndex const& actualCommand) -> std::string {
305 if (schemaName.empty())
306 return std::format(R"(DROP INDEX "{0}_{1}_index";)", tableName, actualCommand.columnName);
307 else
308 return std::format(
309 R"(DROP INDEX "{0}_{1}_{2}_index";)", schemaName, tableName, actualCommand.columnName);
310 },
311 [schemaName, tableName](AddForeignKey const& actualCommand) -> std::string {
312 // Idempotent ADD CONSTRAINT — re-applying a migration must be a no-op.
313 // PostgreSQL has no native `IF NOT EXISTS` for `ADD CONSTRAINT`, so the
314 // guard is expressed via `DO $$ … EXCEPTION WHEN duplicate_object …`.
315 return std::format(
316 "DO $$ BEGIN ALTER TABLE {} ADD {}; EXCEPTION WHEN duplicate_object THEN NULL; END $$;",
317 FormatTableName(schemaName, tableName),
318 BuildForeignKeyConstraint(tableName, actualCommand.columnName, actualCommand.referencedColumn));
319 },
320 [schemaName, tableName](DropForeignKey const& actualCommand) -> std::string {
321 return std::format(R"(ALTER TABLE {} DROP CONSTRAINT "{}";)",
322 FormatTableName(schemaName, tableName),
324 tableName, std::array { std::string_view { actualCommand.columnName } }));
325 },
326 [schemaName, tableName](AddCompositeForeignKey const& actualCommand) -> std::string {
327 std::stringstream ss;
328 ss << "ALTER TABLE " << FormatTableName(schemaName, tableName) << " ADD CONSTRAINT \""
329 << BuildForeignKeyConstraintName(tableName, actualCommand.columns) << "\" FOREIGN KEY (";
330
331 size_t i = 0;
332 for (auto const& col: actualCommand.columns)
333 {
334 if (i++ > 0)
335 ss << ", ";
336 ss << '"' << col << '"';
337 }
338 auto const referencedSchemaName = actualCommand.referencedSchemaName.empty()
339 ? schemaName
340 : std::string_view { actualCommand.referencedSchemaName };
341 ss << ") REFERENCES " << FormatTableName(referencedSchemaName, actualCommand.referencedTableName)
342 << " (";
343
344 i = 0;
345 for (auto const& col: actualCommand.referencedColumns)
346 {
347 if (i++ > 0)
348 ss << ", ";
349 ss << '"' << col << '"';
350 }
351 ss << ");";
352 return ss.str();
353 },
354 [schemaName, tableName, this](AddColumnIfNotExists const& actualCommand) -> std::string {
355 // PostgreSQL has native IF NOT EXISTS support for ADD COLUMN
356 return std::format(R"(ALTER TABLE {} ADD COLUMN IF NOT EXISTS "{}" {} {};)",
357 FormatTableName(schemaName, tableName),
358 actualCommand.columnName,
359 ColumnType(actualCommand.columnType),
360 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
361 },
362 [schemaName, tableName](DropColumnIfExists const& actualCommand) -> std::string {
363 // PostgreSQL has native IF EXISTS support for DROP COLUMN
364 return std::format(R"(ALTER TABLE {} DROP COLUMN IF EXISTS "{}";)",
365 FormatTableName(schemaName, tableName),
366 actualCommand.columnName);
367 },
368 [schemaName, tableName](DropIndexIfExists const& actualCommand) -> std::string {
369 // PostgreSQL has native IF EXISTS support for DROP INDEX
370 if (schemaName.empty())
371 return std::format(
372 R"(DROP INDEX IF EXISTS "{0}_{1}_index";)", tableName, actualCommand.columnName);
373 else
374 return std::format(R"(DROP INDEX IF EXISTS "{0}_{1}_{2}_index";)",
375 schemaName,
376 tableName,
377 actualCommand.columnName);
378 },
379 },
380 command);
381 }
382
383 return { sqlQueryString.str() };
384 }
385
386 [[nodiscard]] std::string QueryServerVersion() const override
387 {
388 return "SELECT version()";
389 }
390
391 /// PostgreSQL has no SQLite settings: undoes the PRAGMAs inherited from the SQLite formatter.
392 [[nodiscard]] StringList SqliteSettingsStatements(SqliteConnectionSettings const& /*settings*/) const override
393 {
394 return {};
395 }
396
397 /// PostgreSQL uses `pg_advisory_lock` / `pg_advisory_unlock`. Inline delegation
398 /// keeps the vtable weak — see `SQLiteQueryFormatter::AdvisoryLockOps()` for
399 /// the rationale.
400 [[nodiscard]] SqlAdvisoryLockHandler const& AdvisoryLockOps() const override
401 {
402 return PostgreSqlAdvisoryLockOps();
403 }
404
405 /// PostgreSQL has a dedicated SQLSTATE for every recoverable condition.
406 /// See `SQLiteQueryFormatter::AdvisoryLockOps()` for why this delegates.
407 [[nodiscard]] SqlRetryClassifier const& RetryOps() const noexcept override
408 {
409 return PostgreSqlRetryOps();
410 }
411
412 /// The planner's tuple estimate of `pg_class`, which `ANALYZE` and autovacuum keep.
413 [[nodiscard]] std::string_view TableRowCountEstimateQuery() const noexcept override
414 {
415 return "SELECT CAST(GREATEST(c.reltuples, 0) AS BIGINT) FROM pg_catalog.pg_class AS c "
416 "JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace "
417 "WHERE n.nspname = ? AND c.relname = ?";
418 }
419};
420
421} // 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.
LIGHTWEIGHT_API SqlRetryClassifier const & PostgreSqlRetryOps() noexcept
Returns the PostgreSQL-specific singleton classifier. See SqliteRetryOps().