Lightweight 0.20261105.1
Loading...
Searching...
No Matches
UnicodeUpgradeProgress.hpp
1// SPDX-License-Identifier: Apache-2.0
2//
3// How `MigrationManager::UnicodeUpgradeTables` reports its progress. Internal to the library;
4// header-only so the tests can drive it without a database.
5
6#pragma once
7
8#include "ProgressTask.hpp"
9#include "SqlConnection.hpp"
10#include "SqlError.hpp"
11#include "SqlMigration.hpp"
12#include "SqlQueryFormatter.hpp"
13#include "SqlSchema.hpp"
14#include "SqlStatement.hpp"
15
16#include <algorithm>
17#include <cstdint>
18#include <format>
19#include <memory>
20#include <optional>
21#include <string>
22#include <string_view>
23#include <utility>
24#include <vector>
25
26namespace Lightweight::detail
27{
28
29/// One table of a Unicode upgrade, as its progress shows it.
30struct UnicodeUpgradeTableProgress
31{
32 std::string name; ///< Display name of the table.
33 std::uint64_t rows = 0; ///< Its estimated rows; 0 when unknown.
34 std::size_t columns = 0; ///< Columns the upgrade alters.
35};
36
37/// @brief Reports a Unicode upgrade to a ProgressTask.
38///
39/// First an indeterminate planning task. Then, unless nothing is to be done or it is a dry run, one
40/// overall task whose total is the planned tables' rows (their number when all counts are 0); under it
41/// one task per unit, whose total is its tables; under that one task per table of the running unit,
42/// whose total is its rows. A table reaches 100 % once its ALTER statements are done, and the overall
43/// task then advances by its rows. Tasks end in reverse order of their opening.
44class UnicodeUpgradeProgress
45{
46 public:
47 /// Title of the planning task.
48 static constexpr std::string_view PlanningTitle = "Plan the Unicode upgrade";
49 /// Title of the overall task.
50 static constexpr std::string_view OverallTitle = "Upgrade text columns to Unicode";
51
52 /// Opens the planning task under @p parent.
53 explicit UnicodeUpgradeProgress(ProgressTask& parent):
54 _parent { parent },
55 _planning { parent.BeginSubTask(PlanningTitle, 0) }
56 {
57 }
58
59 UnicodeUpgradeProgress(UnicodeUpgradeProgress const&) = delete;
60 UnicodeUpgradeProgress(UnicodeUpgradeProgress&&) = delete;
61 UnicodeUpgradeProgress& operator=(UnicodeUpgradeProgress const&) = delete;
62 UnicodeUpgradeProgress& operator=(UnicodeUpgradeProgress&&) = delete;
63
64 /// Ends the tasks still open, innermost first.
65 ~UnicodeUpgradeProgress()
66 {
67 EndTables();
68 _unit.reset();
69 _overall.reset();
70 _planning.reset();
71 }
72
73 /// Ends planning and, for a real run with work, opens the overall task.
74 /// @param units The plan's tables, grouped into units in upgrade order.
75 /// @param mode Whether the call only reports its plan.
76 void Planned(std::vector<std::vector<UnicodeUpgradeTableProgress>> units, SqlMigration::ExecutionMode mode)
77 {
78 _planning.reset();
79 _units = std::move(units);
80 auto tables = std::size_t {};
81 auto columns = std::size_t {};
82 auto rows = std::uint64_t {};
83 for (auto const& unit: _units)
84 for (auto const& table: unit)
85 {
86 ++tables;
87 columns += table.columns;
88 rows += table.rows;
89 }
90 if (tables == 0)
91 {
92 _parent.Log(ProgressTask::LogLevel::Info, "No text columns to upgrade.");
93 return;
94 }
96 {
97 _parent.Log(
98 ProgressTask::LogLevel::Info,
99 std::format("Would upgrade {} column(s) across {} table(s) in {} unit(s).", columns, tables, _units.size()));
100 return;
101 }
102 _weightByRows = rows > 0;
103 _overall = _parent.BeginSubTask(OverallTitle, _weightByRows ? rows : tables);
104 }
105
106 /// Opens the task of unit @p index and of each of its tables.
107 void BeginUnit(std::size_t index)
108 {
109 _unitIndex = index;
110 auto const& unit = _units.at(index);
111 _unit = _overall->BeginSubTask(std::format("Unit {} of {}", index + 1, _units.size()), unit.size());
112 for (auto const& table: unit)
113 _tables.push_back(_unit->BeginSubTask(table.name, table.rows));
114 }
115
116 /// Names a step that runs for the whole unit, such as dropping its dependent objects, on each of its tables.
117 void UnitStep(std::string_view detail)
118 {
119 for (auto const& table: _tables)
120 table->Advance(0, detail);
121 }
122
123 /// The ALTER statements of table @p tableIndex of the running unit start.
124 void TableAltering(std::size_t tableIndex)
125 {
126 _runningTable = tableIndex;
127 _tables.at(tableIndex)->Advance(0, std::format("alter {} column(s)", _units.at(_unitIndex).at(tableIndex).columns));
128 }
129
130 /// The ALTER statements of table @p tableIndex are done.
131 void TableAltered(std::size_t tableIndex)
132 {
133 auto const& table = _units.at(_unitIndex).at(tableIndex);
134 _tables.at(tableIndex)->Advance(table.rows, "done");
135 _unit->Advance(1);
136 _overall->Advance(_weightByRows ? table.rows : 1);
137 _runningTable.reset();
138 }
139
140 /// The running unit committed; its tasks end.
141 void UnitCommitted()
142 {
143 _runningTable.reset();
144 EndTables();
145 _unit.reset();
146 }
147
148 /// The upgrade failed. During planning that task is marked failed; afterwards the table whose ALTER
149 /// statements run (if any), the running unit and the overall task are.
150 /// @param reason What went wrong, UTF-8.
151 void Failed(std::string_view reason)
152 {
153 if (_planning)
154 {
155 _planning->Fail(reason);
156 return;
157 }
158 if (_runningTable)
159 _tables.at(*_runningTable)->Fail(reason);
160 if (_unit)
161 _unit->Fail(reason);
162 if (_overall)
163 _overall->Fail(reason);
164 }
165
166 /// @return Whether the parent task asked to cancel.
167 [[nodiscard]] bool IsCancellationRequested() const
168 {
169 return _parent.IsCancellationRequested();
170 }
171
172 private:
173 void EndTables()
174 {
175 while (!_tables.empty())
176 _tables.pop_back();
177 }
178
179 ProgressTask& _parent;
180 std::unique_ptr<ProgressTask> _planning;
181 std::unique_ptr<ProgressTask> _overall;
182 std::unique_ptr<ProgressTask> _unit;
183 std::vector<std::unique_ptr<ProgressTask>> _tables;
184 std::vector<std::vector<UnicodeUpgradeTableProgress>> _units;
185 std::size_t _unitIndex = 0;
186 std::optional<std::size_t> _runningTable;
187 bool _weightByRows = false;
188};
189
190/// @brief The rows of @p table for a progress total: the dialect's catalog estimate, else `COUNT(*)`.
191/// @return The count, or 0 when it cannot be read (e.g. a login without permission on the catalog
192/// view); the table then shows as indeterminate.
193[[nodiscard]] inline std::uint64_t RowCountForProgress(SqlConnection& connection,
194 SqlSchema::FullyQualifiedTableName const& table)
195{
196 try
197 {
198 auto const& formatter = connection.QueryFormatter();
199 auto stmt = SqlStatement { connection };
200 auto const estimate = formatter.TableRowCountEstimateQuery();
201 if (estimate.empty() || table.schema.empty())
202 {
203 auto const rows = stmt.ExecuteDirectScalar<std::int64_t>(
204 std::format("SELECT COUNT(*) FROM {}", formatter.QualifiedTableName(table.schema, table.table)));
205 return static_cast<std::uint64_t>(std::max<std::int64_t>(rows.value_or(0), 0));
206 }
207 stmt.Prepare(estimate);
208 auto cursor = stmt.Execute(table.schema, table.table);
209 if (!cursor.FetchRow())
210 return 0;
211 return static_cast<std::uint64_t>(std::max<std::int64_t>(cursor.GetNullableColumn<std::int64_t>(1).value_or(0), 0));
212 }
213 catch (SqlException const&)
214 {
215 return 0;
216 }
217}
218
219} // namespace Lightweight::detail
@ DryRun
Report the plan and change nothing.