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