Lightweight 0.20260921.0
Loading...
Searching...
No Matches
CxxModelPrinter.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include <Lightweight/DataMapper/Field.hpp>
6#include <Lightweight/SqlSchema.hpp>
7#include <Lightweight/Utils.hpp>
8
9#include <expected>
10#include <filesystem>
11#include <map>
12#include <set>
13#include <string>
14#include <unordered_set>
15#include <utility>
16#include <vector>
17
18namespace Lightweight::Tools
19{
20
21using ColumnNameOverrides = std::map<SqlSchema::ColumnIdentifier, std::string>;
22
23class CxxModelPrinter
24{
25 public:
26 using UnicodeTextColumnOverrides = std::unordered_map<std::string /*table*/, std::unordered_set<std::string /*column*/>>;
27
28 struct Config
29 {
30 std::vector<std::string> stripSuffixes = { "_id", "_nr" };
31 bool makeAliases = false;
32 FormatType formatType = FormatType::camelCase;
33 PrimaryKey primaryKeyAssignment = PrimaryKey::ServerSideAutoIncrement;
34 ColumnNameOverrides columnNameOverrides;
35 bool forceUnicodeTextColumns = false;
36 UnicodeTextColumnOverrides unicodeTextColumnOverrides;
37 bool suppressWarnings = false;
38 size_t sqlFixedStringMaxSize = SqlOptimalMaxColumnSize;
39 /// When set, emit `extern template` declarations in the headers plus one explicit-
40 /// instantiation .cpp per record and a CMakeLists.txt that builds them into a library.
41 /// Consuming translation units then link that library instead of re-instantiating the
42 /// (expensive) DataMapper relation machinery for every record.
43 bool generateInstantiations = false;
44 /// Name of the CMake library target emitted alongside the instantiation sources.
45 std::string instantiationTargetName = "LightweightEntities";
46 };
47
48 explicit CxxModelPrinter(Config config) noexcept;
49
50 std::string ToString(std::string_view modelNamespace);
51
52 [[nodiscard]] std::string TableIncludes() const;
53
54 [[nodiscard]] std::string AliasTableName(std::string_view name) const;
55
56 [[nodiscard]] std::expected<void, std::string> PrintCumulativeHeaderFile(
57 std::filesystem::path const& outputDirectory, std::filesystem::path const& cumulativeHeaderFile);
58
59 void PrintToFiles(std::string_view modelNamespace, std::string_view outputDirectory);
60
61 std::string HeaderFileForTheTable(std::string_view modelNamespace, std::string const& tableName);
62
63 [[nodiscard]] std::string Example(SqlSchema::Table const& table) const;
64
65 auto StripSuffix(std::string name) -> std::string;
66
67 static auto SanitizeName(std::string name) -> std::string;
68
69 static auto FormatTableName(std::string_view name) -> std::string;
70
71 static SqlSchema::ForeignKeyConstraint const& GetForeignKey(
72 SqlSchema::Column const& column, std::vector<SqlSchema::ForeignKeyConstraint> const& foreignKeys);
73
74 static std::string MakeType(SqlSchema::Column const& column,
75 std::string const& tableName,
76 bool forceUnicodeTextColumn,
77 UnicodeTextColumnOverrides const& unicodeTextColumnOverrides,
78 size_t sqlFixedStringMaxSize);
79
80 /// Renders the `// NOTE:` comment block to emit above a generated member for a `DECIMAL`
81 /// column whose declared precision is wider than what `SqlDataBinder<SqlNumeric<P, S>>` can
82 /// actually deliver.
83 ///
84 /// The emitted `Light::SqlNumeric<P, S>` carries the column's declared precision, but the
85 /// transfer does not always carry the matching number of digits, and nothing at the call site
86 /// says so. Rather than silently generating a lossy record, state the limit where a ddl2cpp
87 /// consumer reads it — in the generated header itself.
88 ///
89 /// @param column Column to describe; non-`DECIMAL` columns and narrow ones yield no note.
90 /// @return The note, each line already indented and newline-terminated, or an empty string.
91 [[nodiscard]] static std::string MakeDecimalPrecisionNote(SqlSchema::Column const& column);
92
93 [[nodiscard]] std::optional<std::string> MapColumnNameOverride(SqlSchema::FullyQualifiedTableName const& tableName,
94 std::string const& columnName) const;
95
96 void ResolveOrderAndPrintTable(std::vector<SqlSchema::Table> const& tables);
97
98 /// @brief One inverse or through relation to emit on a record.
99 ///
100 /// A `BelongsTo` is derivable from the child table alone, but every relation pointing the other
101 /// way needs to know about foreign keys declared on *other* tables. `RelationPlan` is that
102 /// schema-wide answer, computed once by `PlanRelations` and consumed per table.
104 {
105 /// Which relation template to emit.
106 enum class Kind : uint8_t
107 {
108 HasMany, //!< One-to-many: the inverse of a child's BelongsTo.
109 HasOne, //!< One-to-one: as HasMany, but the child's foreign key is uniquely indexed.
110 HasManyThrough, //!< Many-to-many across a join table.
111 HasOneThrough, //!< As HasManyThrough, but the far side is uniquely indexed.
112 };
113
114 /// Which relation template to emit for this relation.
116
117 /// Table this relation is emitted on.
118 std::string ownerTable;
119
120 /// The record the relation yields: the child table for HasMany/HasOne, the far table for the
121 /// through relations.
122 std::string referencedTable;
123
124 /// Join table, for the through relations only; empty otherwise.
125 std::string throughTable;
126
127 /// Foreign key column linking the child (or join record) back to the owner. Emitted as a
128 /// `SqlRealName` selector so that several foreign keys into one table stay distinguishable.
130
131 /// Foreign key column linking the join record to the far table; empty for HasMany/HasOne.
133
134 /// Whether a selector must be emitted for @ref ownerForeignKeyColumn. It is required when the
135 /// owner is reachable from the same child table through more than one foreign key, and is
136 /// harmless otherwise - but emitting it unconditionally would churn every generated header,
137 /// so it is tracked.
139
140 /// As @ref ownerSelectorRequired, for @ref referencedForeignKeyColumn.
142
143 /// Member name to emit, already sanitized and formatted.
144 std::string memberName;
145 };
146
147 /// @brief Relations to emit, keyed by the table they belong on.
148 using RelationPlan = std::map<std::string, std::vector<PlannedRelation>>;
149
150 /// @brief Works out every inverse and through relation implied by a schema.
151 ///
152 /// Applies the rules documented in `docs/ddl2cpp-relation-generation.md`: a join table (exactly
153 /// two single-column foreign keys to two distinct tables, and no payload columns) becomes a
154 /// `HasManyThrough` on both sides; every other single-column foreign key becomes a `HasMany` on
155 /// the referenced side; and either degrades to its scalar form when the relevant foreign key is
156 /// covered by a single-column unique index. Composite foreign keys are ignored, matching the
157 /// existing `BelongsTo` behaviour.
158 ///
159 /// Pure: it reads the schema and returns a plan, so it is testable without a database.
160 ///
161 /// @param tables The whole schema. Relations are only planned between tables present here.
162 /// @return The relations to emit, keyed by owning table name.
163 [[nodiscard]] static RelationPlan PlanRelations(std::vector<SqlSchema::Table> const& tables);
164
165 /// @param table Table to emit.
166 /// @param relationPlan Inverse and through relations to emit on it, from @ref PlanRelations.
167 /// Defaults to none, which yields the BelongsTo-only output.
168 void PrintTable(SqlSchema::Table const& table, std::vector<PlannedRelation> const& relationPlan = {});
169
170 void PrintReport();
171
172 private:
173 // Writes the CMakeLists.txt that builds the per-record instantiation sources into a library.
174 void WriteInstantiationCMakeLists(std::string_view outputDirectory, std::vector<std::string> instantiationSources) const;
175
176 struct TableInfo
177 {
178 std::stringstream text;
179 /// Headers this record depends on, one entry per *distinct* referenced table. A table with
180 /// several foreign-key columns pointing at the same target must still be included once, so
181 /// this is a set (which also gives the emitted `#include` block a stable, sorted order).
182 std::set<std::string> requiredTables;
183
184 /// Records this one names but must *not* include, emitted as forward declarations instead.
185 ///
186 /// An inverse relation points from parent to child while the child's `BelongsTo` points back,
187 /// so including both ways would be a cycle. `HasMany` stores
188 /// `std::vector<std::shared_ptr<OtherRecord>>` and the through relations are equally
189 /// indirect, so a declaration is sufficient at the point of use - the definition is only
190 /// needed where the relation is actually loaded, which is a `.cpp`.
191 std::set<std::string> forwardDeclaredTables;
192 std::string structName; //< C++ struct name (possibly aliased).
193 std::vector<std::pair<std::string, std::string>> members; //< (emitted member id, SQL column name), in order.
194 };
195
196 // Renders the Description<> specialization for one table (emitted at global scope so it
197 // can specialize the Lightweight customization point). Returns empty if there is nothing to emit.
198 [[nodiscard]] static std::string RecordDescriptorFor(std::string_view modelNamespace, TableInfo const& info);
199
200 // Renders the `extern template` declaration appended to a record's header so consuming TUs do
201 // not implicitly instantiate the heavy relation machinery. Returns empty if nothing to emit.
202 [[nodiscard]] static std::string ExternTemplateDeclarationFor(std::string_view modelNamespace, TableInfo const& info);
203
204 // Renders the .cpp that explicitly instantiates a record's relation machinery exactly once.
205 [[nodiscard]] static std::string InstantiationSourceFor(std::string_view modelNamespace,
206 std::string const& headerFileName,
207 TableInfo const& info);
208
209 std::map<std::string, TableInfo> _definitions;
210 Config _config;
211 std::map<SqlSchema::FullyQualifiedTableName, SqlSchema::ForeignKeyConstraint> _warningOnUnsupportedMultiKeyForeignKey;
212 size_t _numberOfColumnsListed = 0;
213 size_t _numberOfForeignKeysListed = 0;
214 size_t _numberOfRelationsListed = 0;
215};
216
217} // namespace Lightweight::Tools
One inverse or through relation to emit on a record.
Kind kind
Which relation template to emit for this relation.
@ HasOneThrough
As HasManyThrough, but the far side is uniquely indexed.
@ HasOne
One-to-one: as HasMany, but the child's foreign key is uniquely indexed.
@ HasMany
One-to-many: the inverse of a child's BelongsTo.
std::string memberName
Member name to emit, already sanitized and formatted.
bool referencedSelectorRequired
As ownerSelectorRequired, for referencedForeignKeyColumn.
std::string ownerTable
Table this relation is emitted on.
std::string throughTable
Join table, for the through relations only; empty otherwise.
std::string referencedForeignKeyColumn
Foreign key column linking the join record to the far table; empty for HasMany/HasOne.