Lightweight 0.20260617.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 RequiresTableRebuildForForeignKeyChange() 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 << "SERIAL";
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 [[nodiscard]] std::string ColumnType(SqlColumnTypeDefinition const& type) const override
109 {
110 using namespace SqlColumnTypeDefinitions;
111
112 // PostgreSQL stores all strings as UTF-8
113 return std::visit(detail::overloaded {
114 [](Bigint const&) -> std::string { return "BIGINT"; },
115 [](Binary const& type) -> std::string { return std::format("BYTEA", type.size); },
116 [](Bool const&) -> std::string { return "BOOLEAN"; },
117 [](Char const& type) -> std::string { return std::format("CHAR({})", type.size); },
118 [](Date const&) -> std::string { return "DATE"; },
119 [](DateTime const&) -> std::string { return "TIMESTAMP"; },
120 [](Decimal const& type) -> std::string {
121 return std::format("DECIMAL({}, {})", type.precision, type.scale);
122 },
123 [](Guid const&) -> std::string { return "UUID"; },
124 [](Integer const&) -> std::string { return "INTEGER"; },
125 [](NChar const& type) -> std::string { return std::format("CHAR({})", type.size); },
126 [](NVarchar const& type) -> std::string {
127 if (type.size == 0)
128 return "TEXT";
129 return std::format("VARCHAR({})", type.size);
130 },
131 [](Real const& type) -> std::string {
132 // PostgreSQL REAL is float4; a Real with precision > 24 (e.g. an
133 // introspected double-precision/float(53) column) must round-trip
134 // as DOUBLE PRECISION or restore silently narrows it to float32.
135 return type.precision > 24 ? "DOUBLE PRECISION" : "REAL";
136 },
137 [](Smallint const&) -> std::string { return "SMALLINT"; },
138 [](Text const&) -> std::string { return "TEXT"; },
139 [](Time const&) -> std::string { return "TIME"; },
140 [](Timestamp const&) -> std::string { return "TIMESTAMP"; },
141 // NB: PostgreSQL doesn't have a TINYINT type, but it does have a SMALLINT type.
142 [](Tinyint const&) -> std::string { return "SMALLINT"; },
143 [](VarBinary const& /*type*/) -> std::string { return std::format("BYTEA"); },
144 [](Varchar const& type) -> std::string {
145 if (type.size == 0)
146 return "TEXT";
147 return std::format("VARCHAR({})", type.size);
148 },
149 },
150 type);
151 }
152
153 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
154 [[nodiscard]] StringList AlterTable(std::string_view schemaName,
155 std::string_view tableName,
156 std::vector<SqlAlterTableCommand> const& commands) const override
157 {
158 std::stringstream sqlQueryString;
159
160 int currentCommand = 0;
161 for (SqlAlterTableCommand const& command: commands)
162 {
163 if (currentCommand > 0)
164 sqlQueryString << '\n';
165 ++currentCommand;
166
167 using namespace SqlAlterTableCommands;
168 sqlQueryString << std::visit(
169 detail::overloaded {
170 [schemaName, tableName](RenameTable const& actualCommand) -> std::string {
171 return std::format(R"(ALTER TABLE {} RENAME TO "{}";)",
172 FormatTableName(schemaName, tableName),
173 actualCommand.newTableName);
174 },
175 [schemaName, tableName, this](AddColumn const& actualCommand) -> std::string {
176 return std::format(R"(ALTER TABLE {} ADD COLUMN "{}" {} {};)",
177 FormatTableName(schemaName, tableName),
178 actualCommand.columnName,
179 ColumnType(actualCommand.columnType),
180 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
181 },
182 [schemaName, tableName, this](AlterColumn const& actualCommand) -> std::string {
183 return std::format(
184 R"(ALTER TABLE {0} ALTER COLUMN "{1}" TYPE {2}, ALTER COLUMN "{1}" {3} NOT NULL;)",
185 FormatTableName(schemaName, tableName),
186 actualCommand.columnName,
187 ColumnType(actualCommand.columnType),
188 actualCommand.nullable == SqlNullable::NotNull ? "SET" : "DROP");
189 },
190 [schemaName, tableName](RenameColumn const& actualCommand) -> std::string {
191 return std::format(R"(ALTER TABLE {} RENAME COLUMN "{}" TO "{}";)",
192 FormatTableName(schemaName, tableName),
193 actualCommand.oldColumnName,
194 actualCommand.newColumnName);
195 },
196 [schemaName, tableName](DropColumn const& actualCommand) -> std::string {
197 return std::format(R"(ALTER TABLE {} DROP COLUMN "{}";)",
198 FormatTableName(schemaName, tableName),
199 actualCommand.columnName);
200 },
201 [schemaName, tableName](AddIndex const& actualCommand) -> std::string {
202 using namespace std::string_view_literals;
203 auto const uniqueStr = actualCommand.unique ? "UNIQUE "sv : ""sv;
204 if (schemaName.empty())
205 return std::format(R"(CREATE {2}INDEX "{0}_{1}_index" ON "{0}" ("{1}");)",
206 tableName,
207 actualCommand.columnName,
208 uniqueStr);
209 else
210 return std::format(R"(CREATE {3}INDEX "{0}_{1}_{2}_index" ON "{0}"."{1}" ("{2}");)",
211 schemaName,
212 tableName,
213 actualCommand.columnName,
214 uniqueStr);
215 },
216 [schemaName, tableName](DropIndex const& actualCommand) -> std::string {
217 if (schemaName.empty())
218 return std::format(R"(DROP INDEX "{0}_{1}_index";)", tableName, actualCommand.columnName);
219 else
220 return std::format(
221 R"(DROP INDEX "{0}_{1}_{2}_index";)", schemaName, tableName, actualCommand.columnName);
222 },
223 [schemaName, tableName](AddForeignKey const& actualCommand) -> std::string {
224 // Idempotent ADD CONSTRAINT — re-applying a migration must be a no-op.
225 // PostgreSQL has no native `IF NOT EXISTS` for `ADD CONSTRAINT`, so the
226 // guard is expressed via `DO $$ … EXCEPTION WHEN duplicate_object …`.
227 return std::format(
228 "DO $$ BEGIN ALTER TABLE {} ADD {}; EXCEPTION WHEN duplicate_object THEN NULL; END $$;",
229 FormatTableName(schemaName, tableName),
230 BuildForeignKeyConstraint(tableName, actualCommand.columnName, actualCommand.referencedColumn));
231 },
232 [schemaName, tableName](DropForeignKey const& actualCommand) -> std::string {
233 return std::format(R"(ALTER TABLE {} DROP CONSTRAINT "{}";)",
234 FormatTableName(schemaName, tableName),
236 tableName, std::array { std::string_view { actualCommand.columnName } }));
237 },
238 [schemaName, tableName](AddCompositeForeignKey const& actualCommand) -> std::string {
239 std::stringstream ss;
240 ss << "ALTER TABLE " << FormatTableName(schemaName, tableName) << " ADD CONSTRAINT \""
241 << BuildForeignKeyConstraintName(tableName, actualCommand.columns) << "\" FOREIGN KEY (";
242
243 size_t i = 0;
244 for (auto const& col: actualCommand.columns)
245 {
246 if (i++ > 0)
247 ss << ", ";
248 ss << '"' << col << '"';
249 }
250 ss << ") REFERENCES " << FormatTableName(schemaName, actualCommand.referencedTableName) << " (";
251
252 i = 0;
253 for (auto const& col: actualCommand.referencedColumns)
254 {
255 if (i++ > 0)
256 ss << ", ";
257 ss << '"' << col << '"';
258 }
259 ss << ");";
260 return ss.str();
261 },
262 [schemaName, tableName, this](AddColumnIfNotExists const& actualCommand) -> std::string {
263 // PostgreSQL has native IF NOT EXISTS support for ADD COLUMN
264 return std::format(R"(ALTER TABLE {} ADD COLUMN IF NOT EXISTS "{}" {} {};)",
265 FormatTableName(schemaName, tableName),
266 actualCommand.columnName,
267 ColumnType(actualCommand.columnType),
268 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
269 },
270 [schemaName, tableName](DropColumnIfExists const& actualCommand) -> std::string {
271 // PostgreSQL has native IF EXISTS support for DROP COLUMN
272 return std::format(R"(ALTER TABLE {} DROP COLUMN IF EXISTS "{}";)",
273 FormatTableName(schemaName, tableName),
274 actualCommand.columnName);
275 },
276 [schemaName, tableName](DropIndexIfExists const& actualCommand) -> std::string {
277 // PostgreSQL has native IF EXISTS support for DROP INDEX
278 if (schemaName.empty())
279 return std::format(
280 R"(DROP INDEX IF EXISTS "{0}_{1}_index";)", tableName, actualCommand.columnName);
281 else
282 return std::format(R"(DROP INDEX IF EXISTS "{0}_{1}_{2}_index";)",
283 schemaName,
284 tableName,
285 actualCommand.columnName);
286 },
287 },
288 command);
289 }
290
291 return { sqlQueryString.str() };
292 }
293
294 [[nodiscard]] std::string QueryServerVersion() const override
295 {
296 return "SELECT version()";
297 }
298
299 /// PostgreSQL uses `pg_advisory_lock` / `pg_advisory_unlock`. Inline delegation
300 /// keeps the vtable weak — see `SQLiteQueryFormatter::AdvisoryLockOps()` for
301 /// the rationale.
302 [[nodiscard]] SqlAdvisoryLockHandler const& AdvisoryLockOps() const override
303 {
304 return PostgreSqlAdvisoryLockOps();
305 }
306};
307
308} // 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.