Lightweight 0.20260303.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 [[nodiscard]] std::string BuildColumnDefinition(SqlColumnDeclaration const& column) const override
62 {
63 std::stringstream sqlQueryString;
64
65 sqlQueryString << '"' << column.name << "\" ";
66
67 // Detect PostgreSQL auto-increment columns by checking for nextval() in default value.
68 // This handles restore of backed-up tables where SERIAL columns have their default
69 // value captured as nextval('"TableName_id_seq"'::regclass).
70 bool const isAutoIncrementViaDefault = column.defaultValue.contains("nextval(");
71 bool const isAutoIncrement = column.primaryKey == SqlPrimaryKeyType::AUTO_INCREMENT || isAutoIncrementViaDefault;
72
73 if (isAutoIncrement)
74 sqlQueryString << "SERIAL";
75 else
76 sqlQueryString << ColumnType(column.type);
77
78 if (column.required)
79 sqlQueryString << " NOT NULL";
80
81 // Only add inline PRIMARY KEY for explicitly marked AUTO_INCREMENT columns.
82 // For columns detected via nextval() default, the table-level PRIMARY KEY constraint
83 // will handle it to avoid "multiple primary keys" error.
84 if (column.primaryKey == SqlPrimaryKeyType::AUTO_INCREMENT)
85 sqlQueryString << " PRIMARY KEY";
86 else if (column.primaryKey == SqlPrimaryKeyType::NONE && !column.index && column.unique)
87 sqlQueryString << " UNIQUE";
88
89 // Don't output default value for auto-increment columns as SERIAL handles it
90 if (!column.defaultValue.empty() && !isAutoIncrement)
91 sqlQueryString << " DEFAULT " << column.defaultValue;
92
93 return sqlQueryString.str();
94 }
95
96 [[nodiscard]] std::string ColumnType(SqlColumnTypeDefinition const& type) const override
97 {
98 using namespace SqlColumnTypeDefinitions;
99
100 // PostgreSQL stores all strings as UTF-8
101 return std::visit(detail::overloaded {
102 [](Bigint const&) -> std::string { return "BIGINT"; },
103 [](Binary const& type) -> std::string { return std::format("BYTEA", type.size); },
104 [](Bool const&) -> std::string { return "BOOLEAN"; },
105 [](Char const& type) -> std::string { return std::format("CHAR({})", type.size); },
106 [](Date const&) -> std::string { return "DATE"; },
107 [](DateTime const&) -> std::string { return "TIMESTAMP"; },
108 [](Decimal const& type) -> std::string {
109 return std::format("DECIMAL({}, {})", type.precision, type.scale);
110 },
111 [](Guid const&) -> std::string { return "UUID"; },
112 [](Integer const&) -> std::string { return "INTEGER"; },
113 [](NChar const& type) -> std::string { return std::format("CHAR({})", type.size); },
114 [](NVarchar const& type) -> std::string {
115 if (type.size == 0)
116 return "TEXT";
117 return std::format("VARCHAR({})", type.size);
118 },
119 [](Real const&) -> std::string { return "REAL"; },
120 [](Smallint const&) -> std::string { return "SMALLINT"; },
121 [](Text const&) -> std::string { return "TEXT"; },
122 [](Time const&) -> std::string { return "TIME"; },
123 [](Timestamp const&) -> std::string { return "TIMESTAMP"; },
124 // NB: PostgreSQL doesn't have a TINYINT type, but it does have a SMALLINT type.
125 [](Tinyint const&) -> std::string { return "SMALLINT"; },
126 [](VarBinary const& /*type*/) -> std::string { return std::format("BYTEA"); },
127 [](Varchar const& type) -> std::string {
128 if (type.size == 0)
129 return "TEXT";
130 return std::format("VARCHAR({})", type.size);
131 },
132 },
133 type);
134 }
135
136 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
137 [[nodiscard]] StringList AlterTable(std::string_view schemaName,
138 std::string_view tableName,
139 std::vector<SqlAlterTableCommand> const& commands) const override
140 {
141 std::stringstream sqlQueryString;
142
143 int currentCommand = 0;
144 for (SqlAlterTableCommand const& command: commands)
145 {
146 if (currentCommand > 0)
147 sqlQueryString << '\n';
148 ++currentCommand;
149
150 using namespace SqlAlterTableCommands;
151 sqlQueryString << std::visit(
152 detail::overloaded {
153 [schemaName, tableName](RenameTable const& actualCommand) -> std::string {
154 return std::format(R"(ALTER TABLE {} RENAME TO "{}";)",
155 FormatTableName(schemaName, tableName),
156 actualCommand.newTableName);
157 },
158 [schemaName, tableName, this](AddColumn const& actualCommand) -> std::string {
159 return std::format(R"(ALTER TABLE {} ADD COLUMN "{}" {} {};)",
160 FormatTableName(schemaName, tableName),
161 actualCommand.columnName,
162 ColumnType(actualCommand.columnType),
163 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
164 },
165 [schemaName, tableName, this](AlterColumn const& actualCommand) -> std::string {
166 return std::format(
167 R"(ALTER TABLE {0} ALTER COLUMN "{1}" TYPE {2}, ALTER COLUMN "{1}" {3} NOT NULL;)",
168 FormatTableName(schemaName, tableName),
169 actualCommand.columnName,
170 ColumnType(actualCommand.columnType),
171 actualCommand.nullable == SqlNullable::NotNull ? "SET" : "DROP");
172 },
173 [schemaName, tableName](RenameColumn const& actualCommand) -> std::string {
174 return std::format(R"(ALTER TABLE {} RENAME COLUMN "{}" TO "{}";)",
175 FormatTableName(schemaName, tableName),
176 actualCommand.oldColumnName,
177 actualCommand.newColumnName);
178 },
179 [schemaName, tableName](DropColumn const& actualCommand) -> std::string {
180 return std::format(R"(ALTER TABLE {} DROP COLUMN "{}";)",
181 FormatTableName(schemaName, tableName),
182 actualCommand.columnName);
183 },
184 [schemaName, tableName](AddIndex const& actualCommand) -> std::string {
185 using namespace std::string_view_literals;
186 auto const uniqueStr = actualCommand.unique ? "UNIQUE "sv : ""sv;
187 if (schemaName.empty())
188 return std::format(R"(CREATE {2}INDEX "{0}_{1}_index" ON "{0}" ("{1}");)",
189 tableName,
190 actualCommand.columnName,
191 uniqueStr);
192 else
193 return std::format(R"(CREATE {3}INDEX "{0}_{1}_{2}_index" ON "{0}"."{1}" ("{2}");)",
194 schemaName,
195 tableName,
196 actualCommand.columnName,
197 uniqueStr);
198 },
199 [schemaName, tableName](DropIndex const& actualCommand) -> std::string {
200 if (schemaName.empty())
201 return std::format(R"(DROP INDEX "{0}_{1}_index";)", tableName, actualCommand.columnName);
202 else
203 return std::format(
204 R"(DROP INDEX "{0}_{1}_{2}_index";)", schemaName, tableName, actualCommand.columnName);
205 },
206 [schemaName, tableName](AddForeignKey const& actualCommand) -> std::string {
207 // Idempotent ADD CONSTRAINT — re-applying a migration must be a no-op.
208 // PostgreSQL has no native `IF NOT EXISTS` for `ADD CONSTRAINT`, so the
209 // guard is expressed via `DO $$ … EXCEPTION WHEN duplicate_object …`.
210 return std::format(
211 "DO $$ BEGIN ALTER TABLE {} ADD {}; EXCEPTION WHEN duplicate_object THEN NULL; END $$;",
212 FormatTableName(schemaName, tableName),
213 BuildForeignKeyConstraint(tableName, actualCommand.columnName, actualCommand.referencedColumn));
214 },
215 [schemaName, tableName](DropForeignKey const& actualCommand) -> std::string {
216 return std::format(R"(ALTER TABLE {} DROP CONSTRAINT "{}";)",
217 FormatTableName(schemaName, tableName),
219 tableName, std::array { std::string_view { actualCommand.columnName } }));
220 },
221 [schemaName, tableName](AddCompositeForeignKey const& actualCommand) -> std::string {
222 std::stringstream ss;
223 ss << "ALTER TABLE " << FormatTableName(schemaName, tableName) << " ADD CONSTRAINT \""
224 << BuildForeignKeyConstraintName(tableName, actualCommand.columns) << "\" FOREIGN KEY (";
225
226 size_t i = 0;
227 for (auto const& col: actualCommand.columns)
228 {
229 if (i++ > 0)
230 ss << ", ";
231 ss << '"' << col << '"';
232 }
233 ss << ") REFERENCES " << FormatTableName(schemaName, actualCommand.referencedTableName) << " (";
234
235 i = 0;
236 for (auto const& col: actualCommand.referencedColumns)
237 {
238 if (i++ > 0)
239 ss << ", ";
240 ss << '"' << col << '"';
241 }
242 ss << ");";
243 return ss.str();
244 },
245 [schemaName, tableName, this](AddColumnIfNotExists const& actualCommand) -> std::string {
246 // PostgreSQL has native IF NOT EXISTS support for ADD COLUMN
247 return std::format(R"(ALTER TABLE {} ADD COLUMN IF NOT EXISTS "{}" {} {};)",
248 FormatTableName(schemaName, tableName),
249 actualCommand.columnName,
250 ColumnType(actualCommand.columnType),
251 actualCommand.nullable == SqlNullable::NotNull ? "NOT NULL" : "NULL");
252 },
253 [schemaName, tableName](DropColumnIfExists const& actualCommand) -> std::string {
254 // PostgreSQL has native IF EXISTS support for DROP COLUMN
255 return std::format(R"(ALTER TABLE {} DROP COLUMN IF EXISTS "{}";)",
256 FormatTableName(schemaName, tableName),
257 actualCommand.columnName);
258 },
259 [schemaName, tableName](DropIndexIfExists const& actualCommand) -> std::string {
260 // PostgreSQL has native IF EXISTS support for DROP INDEX
261 if (schemaName.empty())
262 return std::format(
263 R"(DROP INDEX IF EXISTS "{0}_{1}_index";)", tableName, actualCommand.columnName);
264 else
265 return std::format(R"(DROP INDEX IF EXISTS "{0}_{1}_{2}_index";)",
266 schemaName,
267 tableName,
268 actualCommand.columnName);
269 },
270 },
271 command);
272 }
273
274 return { sqlQueryString.str() };
275 }
276
277 [[nodiscard]] std::string QueryServerVersion() const override
278 {
279 return "SELECT version()";
280 }
281};
282
283} // 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.