Lightweight 0.20261105.1
Loading...
Searching...
No Matches
Common.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "../SqlConnection.hpp"
6#include "../SqlError.hpp"
7#include "../SqlRetryPolicy.hpp"
8#include "SqlBackup.hpp"
9
10#include <chrono>
11#include <cstdint>
12#include <expected>
13#include <format>
14#include <optional>
15#include <span>
16#include <string>
17#include <string_view>
18#include <thread>
19#include <utility>
20#include <vector>
21
22#if defined(__clang__)
23 #pragma clang diagnostic push
24 #pragma clang diagnostic ignored "-Wnullability-extension"
25#endif
26#include <zip.h>
27#if defined(__clang__)
28 #pragma clang diagnostic pop
29#endif
30
31namespace Lightweight::SqlBackup::detail
32{
33
34/// Maximum declared buffer size for binary LOB columns during backup.
35/// The actual data can grow beyond this via automatic ODBC buffer resizing.
36/// Set to 16MB to handle typical BLOB/VARBINARY(MAX) columns.
37constexpr size_t MaxBinaryLobBufferSize = 16 * 1024 * 1024;
38
39/// Metadata for a ZIP entry used during restore operations.
40struct ZipEntryInfo
41{
42 zip_int64_t index {};
43 std::string name;
44 zip_uint64_t size {};
45 bool valid = false;
46};
47
48/// Determines if the given SQL error is a transient error that can be retried.
49///
50/// Thin adapter over @ref Lightweight::GenericRetryOps(), which is where the classification
51/// actually lives — the backup engine no longer carries its own copy of the heuristics. The
52/// dialect-agnostic classifier is used because this helper is reached from contexts that have an
53/// error but not the connection it came from; a caller that does have a connection should build a
54/// @ref Lightweight::SqlRetryPolicy with @c SqlRetryPolicy::For() and get the sharper per-DBMS
55/// classification instead.
56///
57/// Transient errors include:
58/// - Connection errors (ODBC class 08)
59/// - Timeout errors (HYT00, HYT01)
60/// - Transaction rollback due to deadlock/serialization (class 40)
61/// - Database locked (common in SQLite)
62///
63/// @param error The SQL error information to check.
64/// @return true if the error is transient and the operation can be retried.
65LIGHTWEIGHT_API bool IsTransientError(SqlErrorInfo const& error);
66
67/// Calculates the delay for the given retry attempt using exponential backoff.
68///
69/// Delegates to @ref Lightweight::SqlRetryPolicy::DelayFor().
70///
71/// @param attempt The current retry attempt number (0-based).
72/// @param settings The retry configuration.
73/// @return The delay to wait before the next retry.
74LIGHTWEIGHT_API std::chrono::milliseconds CalculateRetryDelay(unsigned attempt, RetrySettings const& settings) noexcept;
75
76/// What a retry loop should do after an attempt failed.
77///
78/// An alias for @ref Lightweight::SqlRetryAction; the backup engine shares the library-wide
79/// vocabulary rather than defining its own.
80///
81/// @see ClassifyRetryOutcome
82using RetryAction = SqlRetryAction;
83
84/// @brief Decides whether a failed attempt should be retried.
85///
86/// Extracted from the retry loops so the policy can be exercised without provoking a real
87/// transient driver failure. This performs no I/O and touches no handle, so a test drives it by
88/// constructing a @ref SqlErrorInfo — the same approach the `IsTransientError` cases above use.
89///
90/// Delegates to @ref Lightweight::SqlRetryPolicy::Decide(), keeping the decision in one place.
91///
92/// @param error The error reported by the failed attempt.
93/// @param attemptsSoFar How many retries have already been consumed.
94/// @param settings The retry configuration supplying the budget.
95/// @return @ref RetryAction::Retry when the caller should back off and try again.
96[[nodiscard]] LIGHTWEIGHT_API RetryAction ClassifyRetryOutcome(SqlErrorInfo const& error,
97 unsigned attemptsSoFar,
98 RetrySettings const& settings);
99
100/// @brief Decides whether and why a failed attempt on @p conn is retried, with the classifier of
101/// the connection's DBMS (@ref Lightweight::SqlRetryPolicy::For()).
102///
103/// Unlike @ref ClassifyRetryOutcome, the decision says why the policy gives up, so a caller can tell
104/// an error that is not transient (@ref SqlRetryGiveUpReason::NotTransient) from a retry budget
105/// that ran out.
106///
107/// @param conn The connection the attempt ran on; selects the DBMS-specific classifier.
108/// @param error The error reported by the failed attempt.
109/// @param attemptsSoFar How many retries have already been consumed.
110/// @param settings The retry configuration supplying the budget.
111/// @return The decision, with its give-up reason.
112[[nodiscard]] LIGHTWEIGHT_API SqlRetryDecision ClassifyRetryDecision(SqlConnection const& conn,
113 SqlErrorInfo const& error,
114 unsigned attemptsSoFar,
115 RetrySettings const& settings);
116
117/// @brief Tests whether the retry policy of @p conn's DBMS gives up on @p error as not transient: the
118/// error is about the data or the statement, not about the connection.
119///
120/// @param conn The connection the attempt ran on; selects the DBMS-specific classifier.
121/// @param error The error reported by the failed attempt.
122/// @param attemptsSoFar How many retries have already been consumed.
123/// @param settings The retry configuration supplying the budget.
124/// @return true if ClassifyRetryDecision() gives up with @ref SqlRetryGiveUpReason::NotTransient.
125[[nodiscard]] LIGHTWEIGHT_API bool IsPermanentError(SqlConnection const& conn,
126 SqlErrorInfo const& error,
127 unsigned attemptsSoFar,
128 RetrySettings const& settings);
129
130/// Connects to the database with retry logic for transient errors.
131///
132/// @param conn The connection object to use.
133/// @param connectionString The connection string.
134/// @param settings The retry configuration.
135/// @param progress Progress manager for reporting retry attempts.
136/// @param operation Name of the operation for progress messages.
137/// @return true if connection succeeded, false if failed after all retries.
138LIGHTWEIGHT_API bool ConnectWithRetry(SqlConnection& conn,
139 SqlConnectionString const& connectionString,
140 RetrySettings const& settings,
141 ProgressManager& progress,
142 std::string const& operation);
143
144/// Retries a function on transient errors with exponential backoff.
145///
146/// A thin wrapper over @ref Lightweight::SqlRetryPolicy::Execute() that routes each retry notice
147/// into @p progress. @p func is taken by value because it is invoked repeatedly — forwarding an
148/// rvalue more than once would use a moved-from callable.
149///
150/// @tparam Func The callable type.
151/// @param func The function to execute.
152/// @param settings Retry configuration.
153/// @param progress Progress manager for reporting retry attempts.
154/// @param operation Name of the operation for progress messages.
155/// @return The result of the function.
156/// @throws SqlException if max retries exceeded or non-transient error occurs.
157template <typename Func>
158auto RetryOnTransientError(Func func, RetrySettings const& settings, ProgressManager& progress, std::string const& operation)
159 -> decltype(func())
160{
161 auto policy = SqlRetryPolicy { settings };
162
163 policy.SetRetryObserver([&progress, &operation, &settings](SqlRetryAttempt const& attempt) {
164 progress.Update({ .state = Progress::State::Warning,
165 .tableName = operation,
166 .currentRows = 0,
167 .totalRows = std::nullopt,
168 // Formatting the error info reproduces SqlException::what() exactly, so
169 // the wording of these progress lines is unchanged by the migration.
170 .message = std::format("Transient error, retry {}/{}: {}",
171 attempt.retryNumber,
172 settings.maxRetries,
173 std::format("{}", attempt.error)) });
174 });
175
176 return policy.Execute(std::move(func));
177}
178
179/// Returns the current date and time in ISO 8601 format.
180///
181/// @return ISO 8601 formatted timestamp string.
182LIGHTWEIGHT_API std::string CurrentDateTime();
183
184/// Reads a ZIP entry into a container.
185///
186/// @tparam Container The container type (e.g., std::string, std::vector<uint8_t>).
187/// @param zip The ZIP archive handle.
188/// @param index The index of the entry to read.
189/// @param size The size of the entry in bytes.
190/// @return The container with the entry contents, or empty on failure.
191template <typename Container>
192Container ReadZipEntry(zip_t* zip, zip_int64_t index, zip_uint64_t size)
193{
194 zip_file_t* file = zip_fopen_index(zip, static_cast<zip_uint64_t>(index), 0);
195 if (!file)
196 return {}; // LCOV_EXCL_LINE - zip file open failure
197
198 Container data;
199 data.resize(size);
200
201 zip_int64_t bytesRead = zip_fread(file, data.data(), size);
202 zip_fclose(file);
203
204 if (bytesRead < 0 || std::cmp_not_equal(bytesRead, size))
205 return {}; // LCOV_EXCL_LINE - zip file read failure
206
207 return data;
208}
209
210/// Formats a table name with optional schema prefix.
211///
212/// @param schema The schema name (may be empty).
213/// @param table The table name.
214/// @return Quoted table name, optionally prefixed with quoted schema.
215LIGHTWEIGHT_API std::string FormatTableName(std::string_view schema, std::string_view table);
216
217/// Returns the archive folder below `data/` that holds a table's chunks.
218///
219/// A single-schema archive uses `<table>`, a multi-schema archive `<schema>/<table>`. Dots in
220/// either name become underscores, so the folder never adds a path segment of its own.
221///
222/// @param schema The table's schema in a multi-schema archive; empty in a single-schema archive.
223/// @param table The table name.
224/// @return The folder, relative to `data/`.
225LIGHTWEIGHT_API std::string ChunkDirectory(std::string_view schema, std::string_view table);
226
227/// Extracts the folder below `data/` from a chunk entry name `data/<folder>/<chunk>.msgpack`.
228///
229/// @param entryName The ZIP entry name.
230/// @return The folder (see @ref ChunkDirectory), or `std::nullopt` if @p entryName is no chunk entry.
231LIGHTWEIGHT_API std::optional<std::string_view> ChunkDirectoryOf(std::string_view entryName);
232
233/// The archive folder that holds the caller-supplied attachments (see @ref BackupAttachment).
234inline constexpr std::string_view AttachmentsPrefix = "attachments/";
235
236/// Checks that attachment names can be extracted below `attachments/` (see @ref BackupAttachment::name).
237///
238/// Each name must be a relative, `/`-separated path of well-formed UTF-8 without control characters. Names
239/// are compared ignoring ASCII letter case, since archives get extracted on case-insensitive file systems: no
240/// two may be equal, and none may also be the folder of another.
241///
242/// @param names The attachment names, without the `attachments/` prefix.
243/// @return Nothing when all names are valid, otherwise a message naming the first offending name.
244[[nodiscard]] LIGHTWEIGHT_API std::expected<void, std::string> ValidateAttachmentNames(std::span<std::string const> names);
245
246/// Joins @p names with ", ", for messages.
247///
248/// A plain loop: libc++ lacks views::join_with, and Clang cannot compile its `| ranges::to` pipe
249/// against libstdc++ 14.
250///
251/// @param names The names to join.
252/// @return The joined names.
253LIGHTWEIGHT_API std::string JoinNames(std::vector<std::string> const& names);
254
255/// Lower-cases ASCII letters, for comparing identifiers of a case-insensitive dialect.
256///
257/// @param text The identifier.
258/// @return @p text with A-Z lower-cased.
259LIGHTWEIGHT_API std::string ToLowerAscii(std::string_view text);
260
261/// Drops duplicates from @p schemas, keeping the first spelling, and sorts them; an empty list
262/// becomes the one unnamed schema.
263///
264/// @param schemas The schemas to back up.
265/// @param caseInsensitive Whether names that differ only in case are one schema.
266/// @return The schemas a backup covers.
267LIGHTWEIGHT_API std::vector<std::string> NormalizeSchemaNames(std::vector<std::string> schemas, bool caseInsensitive);
268
269/// Describes why @p tables cannot go into one archive, because two of them would share an archive
270/// key or chunk folder.
271///
272/// Two causes exist: same-named tables of different schemas in an archive that records no schema
273/// (the message points to a multi-schema archive), and names that differ only in a dot and an
274/// underscore, which the chunk folder writes alike.
275///
276/// @param tables The tables to back up.
277/// @param multiSchema Whether the archive records each table's schema.
278/// @return The description, or an empty string if no two tables collide.
279LIGHTWEIGHT_API std::string DescribeTableNameCollisions(SqlSchema::TableList const& tables, bool multiSchema);
280
281/// Drops a table if it exists, handling FK constraints via cascade.
282///
283/// @param conn The database connection.
284/// @param schema The schema name.
285/// @param tableName The table name.
286/// @param progress Progress manager for reporting errors.
287/// @return true if table was dropped or didn't exist, false on error.
288LIGHTWEIGHT_API bool DropTableIfExists(SqlConnection& conn,
289 std::string const& schema,
290 std::string const& tableName,
291 ProgressManager& progress);
292
293} // namespace Lightweight::SqlBackup::detail