Lightweight 0.20261023.0
Loading...
Searching...
No Matches
SqlQueryFormatter.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "Api.hpp"
6#include "SqlConnection.hpp"
7#include "SqlQuery/MigrationPlan.hpp"
8#include "SqlServerType.hpp"
9
10#include <string>
11#include <string_view>
12
13namespace Lightweight
14{
15
16class SqlAdvisoryLockHandler;
17class SqlRetryClassifier;
18
19/// API to format SQL queries for different SQL dialects.
20class [[nodiscard]] LIGHTWEIGHT_API SqlQueryFormatter
21{
22 public:
23 /// Default constructor.
24 SqlQueryFormatter() = default;
25 /// Default move constructor.
27 /// Default copy constructor.
29 /// Default move assignment operator.
31 /// Default copy assignment operator.
33 virtual ~SqlQueryFormatter() = default;
34
35 /// Converts a boolean value to a string literal.
36 [[nodiscard]] virtual std::string_view BooleanLiteral(bool value) const noexcept = 0;
37
38 /// Returns the SQL function name used to retrieve the current date.
39 [[nodiscard]] virtual std::string_view DateFunction() const noexcept = 0;
40
41 /// Converts a string value to a string literal.
42 [[nodiscard]] virtual std::string StringLiteral(std::string_view value) const noexcept = 0;
43
44 /// Converts a character value to a string literal.
45 [[nodiscard]] virtual std::string StringLiteral(char value) const noexcept = 0;
46
47 /// Converts a binary value to a hex-encoded string literal.
48 [[nodiscard]] virtual std::string BinaryLiteral(std::span<uint8_t const> data) const = 0;
49
50 /// Formats a qualified table name with proper quoting for this database.
51 /// @param schema The schema name (can be empty for default schema)
52 /// @param table The table name
53 /// @return The properly quoted qualified table name (e.g., "schema"."table" or [schema].[table])
54 [[nodiscard]] virtual std::string QualifiedTableName(std::string_view schema, std::string_view table) const = 0;
55
56 /// Constructs an SQL INSERT query.
57 ///
58 /// @param intoTable The table to insert into.
59 /// @param fields The fields to insert into.
60 /// @param values The values to insert.
61 ///
62 /// The fields and values must be in the same order.
63 [[nodiscard]] virtual std::string Insert(std::string_view intoTable,
64 std::string_view fields,
65 std::string_view values) const = 0;
66
67 /// Constructs an SQL INSERT query with a schema prefix.
68 [[nodiscard]] virtual std::string Insert(std::string_view schema,
69 std::string_view intoTable,
70 std::string_view fields,
71 std::string_view values) const = 0;
72
73 /// Retrieves the last insert ID of the given table.
74 [[nodiscard]] virtual std::string QueryLastInsertId(std::string_view tableName) const = 0;
75
76 /// Constructs an SQL SELECT query for all rows.
77 [[nodiscard]] virtual std::string SelectAll(bool distinct,
78 std::string_view fields,
79 std::string_view fromTable,
80 std::string_view fromTableAlias,
81 std::string_view tableJoins,
82 std::string_view whereCondition,
83 std::string_view orderBy,
84 std::string_view groupBy) const = 0;
85
86 /// Constructs an SQL SELECT query for the first row.
87 ///
88 /// When @p groupBy is non-empty it is emitted between the WHERE and the ORDER BY clause, so
89 /// the row limit given by @p count applies to the grouped result set.
90 [[nodiscard]] virtual std::string SelectFirst(bool distinct,
91 std::string_view fields,
92 std::string_view fromTable,
93 std::string_view fromTableAlias,
94 std::string_view tableJoins,
95 std::string_view whereCondition,
96 std::string_view orderBy,
97 std::string_view groupBy,
98 size_t count) const = 0;
99
100 /// Constructs an SQL SELECT query for a range of rows.
101 [[nodiscard]] virtual std::string SelectRange(bool distinct,
102 std::string_view fields,
103 std::string_view fromTable,
104 std::string_view fromTableAlias,
105 std::string_view tableJoins,
106 std::string_view whereCondition,
107 std::string_view orderBy,
108 std::string_view groupBy,
109 std::size_t offset,
110 std::size_t limit) const = 0;
111
112 /// Constructs an SQL SELECT query retrieve the count of rows matching the given condition.
113 ///
114 /// When @p groupBy is non-empty the query counts the rows of each group, i.e. it yields one
115 /// row per group rather than a single total.
116 [[nodiscard]] virtual std::string SelectCount(bool distinct,
117 std::string_view fromTable,
118 std::string_view fromTableAlias,
119 std::string_view tableJoins,
120 std::string_view whereCondition,
121 std::string_view groupBy) const = 0;
122
123 /// Constructs an SQL UPDATE query.
124 [[nodiscard]] virtual std::string Update(std::string_view table,
125 std::string_view tableAlias,
126 std::string_view setFields,
127 std::string_view whereCondition) const = 0;
128
129 /// Constructs an SQL DELETE query.
130 [[nodiscard]] virtual std::string Delete(std::string_view fromTable,
131 std::string_view fromTableAlias,
132 std::string_view tableJoins,
133 std::string_view whereCondition) const = 0;
134
135 /// Alias for a list of SQL statement strings.
136 using StringList = std::vector<std::string>;
137
138 /// Convert the given column type definition to the SQL type.
139 [[nodiscard]] virtual std::string ColumnType(SqlColumnTypeDefinition const& type) const = 0;
140
141 /// Constructs an SQL CREATE TABLE query.
142 ///
143 /// @param schema The schema name of the table to create.
144 /// @param tableName The name of the table to create.
145 /// @param columns The columns of the table.
146 /// @param foreignKeys The foreign key constraints of the table.
147 /// @param ifNotExists If true, generates CREATE TABLE IF NOT EXISTS instead of CREATE TABLE.
148 [[nodiscard]] virtual StringList CreateTable(std::string_view schema,
149 std::string_view tableName,
150 std::vector<SqlColumnDeclaration> const& columns,
151 std::vector<SqlCompositeForeignKeyConstraint> const& foreignKeys,
152 bool ifNotExists = false) const = 0;
153
154 /// Constructs an SQL ALTER TABLE query.
155 [[nodiscard]] virtual StringList AlterTable(std::string_view schema,
156 std::string_view tableName,
157 std::vector<SqlAlterTableCommand> const& commands) const = 0;
158
159 /// Constructs an SQL DROP TABLE query.
160 ///
161 /// @param schema The schema name of the table to drop.
162 /// @param tableName The name of the table to drop.
163 /// @param ifExists If true, generates DROP TABLE IF EXISTS instead of DROP TABLE.
164 /// @param cascade If true, drops all foreign key constraints referencing this table first.
165 [[nodiscard]] virtual StringList DropTable(std::string_view schema,
166 std::string_view const& tableName,
167 bool ifExists = false,
168 bool cascade = false) const = 0;
169
170 /// Returns the SQL query to retrieve the full server version string.
171 ///
172 /// This query returns detailed version information specific to each database:
173 /// - SQL Server: Returns result of SELECT @@VERSION (includes build, edition, OS info)
174 /// - PostgreSQL: Returns result of SELECT version() (includes build info)
175 /// - SQLite: Returns result of SELECT sqlite_version()
176 [[nodiscard]] virtual std::string QueryServerVersion() const = 0;
177
178 /// Retrieves the SQL query formatter for SQLite.
179 static SqlQueryFormatter const& Sqlite();
180
181 /// Retrieves the SQL query formatter for Microsoft SQL server.
182 static SqlQueryFormatter const& SqlServer();
183
184 /// Retrieves the SQL query formatter for PostgreSQL.
185 static SqlQueryFormatter const& PostgrSQL();
186
187 /// Retrieves the SQL query formatter for the given SqlServerType.
188 static SqlQueryFormatter const* Get(SqlServerType serverType) noexcept;
189
190 /// @brief Whether the dialect must rebuild a table to apply an `ALTER TABLE` schema change that it
191 /// cannot express in place — adding/dropping a foreign-key constraint, or altering a column's
192 /// type/nullability (`ALTER TABLE … ADD/DROP CONSTRAINT` / `… ALTER COLUMN`).
193 ///
194 /// SQLite returns `true`; every other backend defaults to `false`. The migration executor consults
195 /// this (via @ref SqlConnection::RequiresTableRebuildForSchemaChange) to decide whether to take the
196 /// table-rebuild path for the `-- LIGHTWEIGHT_SQLITE_GUARD:` sentinels these dialects emit.
197 [[nodiscard]] virtual bool RequiresTableRebuildForSchemaChange() const noexcept
198 {
199 return false;
200 }
201
202 /// @brief The per-table hint that asks for a dirty read (READ UNCOMMITTED) of one table in a SELECT.
203 ///
204 /// Rendered right after a table reference (and its alias) in the FROM clause and in each JOIN of a
205 /// query built with @ref SqlSelectQueryBuilder::ReadUncommitted. Only that statement is affected;
206 /// the connection's transaction isolation level is left alone.
207 ///
208 /// Defaults to an empty string, meaning the dialect has no per-table hint and renders the
209 /// statement unchanged. SQL Server returns `" WITH (READUNCOMMITTED)"`.
210 ///
211 /// @return The hint text including its leading space, or an empty string. The text must stay
212 /// valid for as long as the formatter does.
213 [[nodiscard]] virtual std::string_view ReadUncommittedTableHint() const noexcept
214 {
215 return {};
216 }
217
218 /// @brief Whether the dialect provides a batched, whole-database schema-introspection
219 /// fast path that `SqlSchema::ReadAllTables` can use instead of the per-table ODBC
220 /// catalog loop.
221 ///
222 /// Defaults to `false`, meaning the generic per-table catalog reader is used.
223 /// SQL Server returns `true`: it can answer the entire schema with a handful of
224 /// `sys.*` queries, collapsing thousands of per-table round-trips into a few.
225 /// The batched path must produce byte-identical schema metadata to the legacy path.
226 ///
227 /// @return `true` if the dialect supports the batched fast path, `false` otherwise.
228 [[nodiscard]] virtual bool SupportsBatchedSchemaIntrospection() const noexcept
229 {
230 return false;
231 }
232
233 /// @brief Whether the dialect treats narrow (`CHAR`/`VARCHAR`) and wide
234 /// (`NCHAR`/`NVARCHAR`) text as distinct types — distinct enough that a foreign key
235 /// between them is rejected and changing a column between them needs its dependent
236 /// constraints and indexes dropped first.
237 ///
238 /// Defaults to `false` (PostgreSQL and SQLite store both the same way). SQL Server
239 /// returns `true`.
240 ///
241 /// @return `true` if narrow and wide text are distinct types in this dialect.
242 [[nodiscard]] virtual bool DistinguishesNarrowAndWideText() const noexcept
243 {
244 return false;
245 }
246
247 /// @brief Largest number of values this dialect should be handed in a single `WHERE ... IN (...)`
248 /// predicate.
249 ///
250 /// The batched relation loading behind `Query<Record>().With<&Record::relation>()` resolves a whole
251 /// result set through `IN` predicates over the collected keys. Those keys are rendered as literals,
252 /// so a large batch would otherwise produce one enormous expression — SQL Server raises
253 /// "an expression services limit has been reached", and SQLite has its own parser limits. Splitting
254 /// the batch into chunks of this size keeps every generated statement inside what the dialect
255 /// accepts; the cost is one extra round-trip per chunk, still a constant number of queries per
256 /// relation instead of one per record.
257 ///
258 /// @return The maximum number of values per `IN` predicate. Defaults to 1000 for every dialect.
259 [[nodiscard]] virtual size_t MaxInPredicateValues() const noexcept
260 {
261 return 1000;
262 }
263
264 /// @brief Builds the canonical foreign-key constraint name for a set of columns.
265 ///
266 /// Produces `FK_<table>_<col1>[_<col2>…]`. A single-column FK collapses to
267 /// `FK_<table>_<col>`; a composite FK includes every column so that a composite
268 /// FK whose first column matches an existing single-column FK doesn't collide
269 /// on the constraint name (which MSSQL enforces as globally unique per DB).
270 ///
271 /// Consumers include the SQL Server, PostgreSQL and SQLite formatters —
272 /// centralising the convention here keeps CREATE/ALTER emission in sync with
273 /// DROP CONSTRAINT lookup (e.g. `SQLiteRebuildDropForeignKey`).
274 template <typename Range>
275 [[nodiscard]] static std::string BuildForeignKeyConstraintName(std::string_view tableName, Range const& columns)
276 {
277 std::string name { "FK_" };
278 name.append(tableName);
279 for (auto const& col: columns)
280 {
281 name.push_back('_');
282 name.append(col);
283 }
284 return name;
285 }
286
287 /// @brief Returns the SQL statement to execute after connect to make
288 /// `schema` the connection-level default for unqualified DDL/DML.
289 ///
290 /// Returns an empty string when the DBMS has no session-level concept of a
291 /// default schema (SQL Server, SQLite). Callers must skip emission for
292 /// empty results. PostgreSQL implements this via `SET search_path TO ...`.
293 ///
294 /// The `schema` value is interpolated into the statement; callers must
295 /// validate it (e.g. whitelist `[A-Za-z0-9_]`) before invoking.
296 [[nodiscard]] virtual std::string SetDefaultSchemaStatement(std::string_view schema) const
297 {
298 (void) schema;
299 return {};
300 }
301
302 /// @brief Returns the statements a connection executes after connecting to realise @p settings.
303 ///
304 /// Only the SQLite dialect has these settings, and emits one @c PRAGMA per setting; every other
305 /// dialect returns an empty list, which leaves the settings inert there.
306 ///
307 /// @param settings The connection's SQLite settings.
308 /// @return The statements to execute, in order; empty when there is nothing to apply.
309 [[nodiscard]] virtual StringList SqliteSettingsStatements(SqliteConnectionSettings const& settings) const
310 {
311 (void) settings;
312 return {};
313 }
314
315 /// @brief Returns the dialect-specific handler used by `SqlScopedLock` to
316 /// acquire and release named cross-process advisory locks.
317 ///
318 /// The returned reference is to a process-singleton, valid for the lifetime
319 /// of the program. Each backend implements its own primitive — SQL Server
320 /// uses `sp_getapplock`, PostgreSQL uses `pg_advisory_lock`, SQLite uses
321 /// a lock table — and the implementation lives in the backend's formatter
322 /// translation unit, so adding a new dialect only touches that unit and
323 /// `SqlScopedLock` itself stays dialect-agnostic.
324 [[nodiscard]] virtual SqlAdvisoryLockHandler const& AdvisoryLockOps() const = 0;
325
326 /// Returns the dialect's transient-error classifier, used by @c SqlRetryPolicy to decide
327 /// whether a failed statement is worth another attempt.
328 ///
329 /// Routing the classification through the formatter is what keeps per-DBMS error-code
330 /// knowledge out of business logic — the same reason @ref AdvisoryLockOps() exists.
331 [[nodiscard]] virtual SqlRetryClassifier const& RetryOps() const noexcept = 0;
332
333 /// @brief Whether the dialect resolves table and column names without regard to case, even
334 /// when they are quoted — so a declared `ITEM_NAME` names the live column `Item_Name`.
335 ///
336 /// Defaults to `false`. SQLite returns `true`. SQL Server returns `true` as well, which assumes
337 /// a case-insensitive database collation (the default); it is not probed per database.
338 /// PostgreSQL keeps quoted identifiers case-sensitive and returns `false`.
339 ///
340 /// New hooks are appended at the end of the class: that keeps the slots of the earlier ones for
341 /// callers, but a subclass outside this library must still be rebuilt.
342 ///
343 /// @return `true` if identifiers differing only in ASCII letter case name the same object.
344 [[nodiscard]] virtual bool IdentifiersAreCaseInsensitive() const noexcept
345 {
346 return false;
347 }
348
349 /// @brief Whether the dialect has schemas that tables can be created in.
350 ///
351 /// Defaults to `true`. SQLite returns `false`: it has no schemas, only attached databases.
352 ///
353 /// @return `true` if a table can be created in a named schema.
354 [[nodiscard]] virtual bool SupportsSchemas() const noexcept
355 {
356 return true;
357 }
358
359 /// @brief Returns the statement that creates @p schema unless it already exists.
360 ///
361 /// Returns an empty string when the dialect has no schemas (see @ref SupportsSchemas()).
362 ///
363 /// @param schema The schema to create.
364 /// @return The statement to execute, or an empty string when there is nothing to execute.
365 [[nodiscard]] virtual std::string CreateSchemaIfNotExists(std::string_view schema) const
366 {
367 (void) schema;
368 return {};
369 }
370
371 /// @brief Returns the statement that makes the session report affected-row counts again.
372 ///
373 /// Defaults to an empty string, meaning the dialect always reports them. SQL Server returns
374 /// `SET NOCOUNT OFF`: under `NOCOUNT ON` the driver may report 0 rows for a statement that
375 /// changed rows. The setting stays in effect for the session after the statement ran.
376 ///
377 /// @return The statement to execute, or an empty string when there is nothing to execute.
378 [[nodiscard]] virtual std::string_view ReportRowCountsStatement() const noexcept
379 {
380 return {};
381 }
382
383 /// @brief A query estimating a table's row count from the catalog's statistics, without reading the
384 /// table. It takes two parameters, the schema name and then the table name, and yields one integer.
385 ///
386 /// @return The query, or empty when the dialect keeps no such statistics; callers then count with
387 /// `SELECT COUNT(*)`, which reads the whole table.
388 [[nodiscard]] virtual std::string_view TableRowCountEstimateQuery() const noexcept
389 {
390 return {};
391 }
392
393 protected:
394 /// Formats a table name with optional schema prefix.
395 static std::string FormatTableName(std::string_view schema, std::string_view table);
396
397 /// Doubles every @p quote in @p text, so @p text can be embedded between two such quotes.
398 /// @param text The text to embed.
399 /// @param quote The quote character that delimits the embedded text.
400 /// @return @p text with each @p quote doubled.
401 static std::string DoubleQuoteCharacter(std::string_view text, char quote);
402};
403
404} // namespace Lightweight
API to format SQL queries for different SQL dialects.
virtual size_t MaxInPredicateValues() const noexcept
Largest number of values this dialect should be handed in a single WHERE ... IN (....
virtual bool SupportsBatchedSchemaIntrospection() const noexcept
Whether the dialect provides a batched, whole-database schema-introspection fast path that SqlSchema:...
virtual bool DistinguishesNarrowAndWideText() const noexcept
Whether the dialect treats narrow (CHAR/VARCHAR) and wide (NCHAR/NVARCHAR) text as distinct types — d...
virtual std::string SetDefaultSchemaStatement(std::string_view schema) const
Returns the SQL statement to execute after connect to make schema the connection-level default for un...
virtual std::string CreateSchemaIfNotExists(std::string_view schema) const
Returns the statement that creates schema unless it already exists.
SqlQueryFormatter(SqlQueryFormatter const &)=default
Default copy constructor.
virtual std::string_view ReportRowCountsStatement() const noexcept
Returns the statement that makes the session report affected-row counts again.
SqlQueryFormatter()=default
Default constructor.
SqlQueryFormatter & operator=(SqlQueryFormatter &&)=default
Default move assignment operator.
virtual SqlAdvisoryLockHandler const & AdvisoryLockOps() const =0
Returns the dialect-specific handler used by SqlScopedLock to acquire and release named cross-process...
static std::string BuildForeignKeyConstraintName(std::string_view tableName, Range const &columns)
Builds the canonical foreign-key constraint name for a set of columns.
virtual bool SupportsSchemas() const noexcept
Whether the dialect has schemas that tables can be created in.
virtual std::string_view ReadUncommittedTableHint() const noexcept
The per-table hint that asks for a dirty read (READ UNCOMMITTED) of one table in a SELECT.
virtual std::string_view DateFunction() const noexcept=0
Returns the SQL function name used to retrieve the current date.
static std::string DoubleQuoteCharacter(std::string_view text, char quote)
virtual StringList SqliteSettingsStatements(SqliteConnectionSettings const &settings) const
Returns the statements a connection executes after connecting to realise settings.
std::vector< std::string > StringList
Alias for a list of SQL statement strings.
SqlQueryFormatter(SqlQueryFormatter &&)=default
Default move constructor.
virtual SqlRetryClassifier const & RetryOps() const noexcept=0
SqlQueryFormatter & operator=(SqlQueryFormatter const &)=default
Default copy assignment operator.
virtual std::string_view BooleanLiteral(bool value) const noexcept=0
Converts a boolean value to a string literal.
static std::string FormatTableName(std::string_view schema, std::string_view table)
Formats a table name with optional schema prefix.
virtual std::string_view TableRowCountEstimateQuery() const noexcept
A query estimating a table's row count from the catalog's statistics, without reading the table....
Dialect-specific classification of SQL errors into transient and permanent.
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.
Represents a SQL column declaration.
Represents a composite foreign key constraint.
Connection-level settings applied when a connection to a SQLite database is established.