SQLon test coverage


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 95.3% 82 / 0 / 86
Functions: 100.0% 16 / 0 / 16
Branches: 75.3% 55 / 0 / 73

include/sqlon/relation.h
Line Branch Exec Source
1 /******************************************************************************
2 **
3 ** Copyright (C) 2026 Ivan Pinezhaninov <ivan.pinezhaninov@gmail.com>
4 **
5 ** This file is part of the SQLon - which can be found at
6 ** https://github.com/IvanPinezhaninov/sqlon/.
7 **
8 ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
9 ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
10 ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
11 ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
12 ** DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
13 ** OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
14 ** THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15 **
16 ******************************************************************************/
17
18 #ifndef SQLON_RELATION_H
19 #define SQLON_RELATION_H
20
21 #include <sqlon/error.h>
22 #include <sqlon/export.h>
23 #include <sqlon/expression.h>
24
25 #include <initializer_list>
26 #include <memory>
27 #include <string>
28 #include <type_traits>
29 #include <utility>
30 #include <vector>
31
32 namespace sqlon {
33
34 class assignment;
35
36 namespace detail {
37 struct relation_access;
38 }
39
40 /** @brief Structured qualified SQL identifier. */
41 class qualified_name final {
42 public:
43 /** @brief Creates an identifier from one or more ordered name components. */
44 SQLON_API explicit qualified_name(std::initializer_list<std::string> parts);
45
46 /** @brief Returns the ordered identifier components. */
47 SQLON_API const std::vector<std::string>& parts() const noexcept;
48
49 private:
50 std::vector<std::string> m_parts;
51 };
52
53 /** @brief SQL relation usable in FROM and JOIN clauses. */
54 class relation {
55 public:
56 /** @brief Creates an INNER JOIN with an ON condition. */
57 template<typename Right>
58 relation inner_join(const Right& right, const expression<bool>& condition) const;
59
60 /** @brief Creates a LEFT JOIN with an ON condition. */
61 template<typename Right>
62 relation left_join(const Right& right, const expression<bool>& condition) const;
63
64 /** @brief Creates a RIGHT JOIN with an ON condition. */
65 template<typename Right>
66 relation right_join(const Right& right, const expression<bool>& condition) const;
67
68 /** @brief Creates a FULL JOIN with an ON condition. */
69 template<typename Right>
70 relation full_join(const Right& right, const expression<bool>& condition) const;
71
72 /** @brief Creates a CROSS JOIN. */
73 template<typename Right>
74 relation cross_join(const Right& right) const;
75
76 protected:
77 /** @brief Creates a relation from an internal node. */
78 SQLON_API explicit relation(detail::relation_ptr node);
79
80 private:
81 friend struct detail::relation_access;
82
83 detail::relation_ptr m_node;
84 };
85
86 /** @brief Named SQL table relation. */
87 class table : public relation {
88 public:
89 /** @brief Creates an unaliased table. */
90 SQLON_API explicit table(std::string name);
91
92 /** @brief Creates a table with a SQL alias. */
93 SQLON_API table(std::string name, std::string alias);
94
95 /** @brief Creates an unaliased table from a structured qualified name. */
96 SQLON_API explicit table(qualified_name name);
97
98 /** @brief Creates a qualified table with a SQL alias. */
99 SQLON_API table(qualified_name name, std::string alias);
100
101 // Descriptors can be copied or moved, but not reassigned.
102 table(const table&) = default;
103 table(table&&) noexcept = default;
104 table& operator=(const table&) = delete;
105 table& operator=(table&&) = delete;
106
107 /** @brief Returns the source table name. */
108 SQLON_API const std::string& name() const noexcept;
109
110 /** @brief Returns the structured source table identifier. */
111 SQLON_API const qualified_name& identifier() const noexcept;
112
113 /** @brief Returns the alias, or an empty string when unaliased. */
114 SQLON_API const std::string& alias_name() const noexcept;
115
116 private:
117 static detail::relation_ptr make_node(const qualified_name& name, const std::string& alias);
118
119 qualified_name m_identifier;
120 std::string m_alias;
121 };
122
123 /** @brief Typed column belonging to a table relation. */
124 template<typename T>
125 class column final : public expression<T> {
126 public:
127 /** @brief Creates a column with an owning table and SQL name. */
128 502 column(const table& owner, std::string name)
129 : expression<T>(make_node(owner, name))
130
4/4
sqlon::column<ClientUuid>::column(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >):
✓ Branch 1 taken 1 time.
sqlon::column<bool>::column(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >):
✓ Branch 1 taken 126 times.
sqlon::column<long>::column(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >):
✓ Branch 1 taken 247 times.
sqlon::column<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::column(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >):
✓ Branch 1 taken 127 times.
502 , m_name(std::move(name))
131 501 {}
132
133 /** @brief Returns the SQL column name. */
134 const std::string& name() const noexcept
135 {
136 return m_name;
137 }
138
139 /** @brief Creates an assignment of a compatible value to this column. */
140 template<typename Right>
141 assignment operator=(Right&& value) const;
142
143 /** @brief Creates an assignment from another column with the same value type. */
144 assignment operator=(const column& value) const;
145
146 private:
147 502 static detail::expression_ptr make_node(const table& owner, const std::string& name)
148 {
149
6/12
sqlon::column<ClientUuid>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 5 not taken.
sqlon::column<bool>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✗ Branch 1 not taken.
✓ Branch 2 taken 126 times.
✗ Branch 5 not taken.
sqlon::column<long>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 247 times.
✓ Branch 5 taken 1 time.
sqlon::column<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✗ Branch 1 not taken.
✓ Branch 2 taken 127 times.
✗ Branch 5 not taken.
502 if (name.empty()) throw invalid_query{"a column requires a non-empty name"};
150
4/4
sqlon::column<ClientUuid>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 1 taken 1 time.
sqlon::column<bool>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 1 taken 126 times.
sqlon::column<long>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 1 taken 247 times.
sqlon::column<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 1 taken 127 times.
501 std::shared_ptr<detail::expression_node> node = std::make_shared<detail::expression_node>();
151 501 node->kind = detail::expression_kind::column;
152
7/8
sqlon::column<ClientUuid>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
sqlon::column<bool>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 2 taken 117 times.
✓ Branch 3 taken 9 times.
sqlon::column<long>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 2 taken 224 times.
✓ Branch 3 taken 23 times.
sqlon::column<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 2 taken 118 times.
✓ Branch 3 taken 9 times.
501 if (owner.alias_name().empty())
153
4/4
sqlon::column<ClientUuid>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 4 taken 1 time.
sqlon::column<bool>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 4 taken 117 times.
sqlon::column<long>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 4 taken 224 times.
sqlon::column<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 4 taken 118 times.
460 node->qualifier = owner.identifier().parts();
154 else
155
9/20
sqlon::column<ClientUuid>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
sqlon::column<bool>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 1 taken 9 times.
✓ Branch 3 taken 9 times.
✓ Branch 4 taken 9 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
sqlon::column<long>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 1 taken 23 times.
✓ Branch 3 taken 23 times.
✓ Branch 4 taken 23 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
sqlon::column<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 1 taken 9 times.
✓ Branch 3 taken 9 times.
✓ Branch 4 taken 9 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
82 node->qualifier = {owner.alias_name()};
156
4/4
sqlon::column<ClientUuid>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 2 taken 1 time.
sqlon::column<bool>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 2 taken 126 times.
sqlon::column<long>::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 2 taken 247 times.
sqlon::column<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::make_node(sqlon::table const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 2 taken 127 times.
501 node->text = name;
157 1002 return node;
158 542 }
159
160 std::string m_name;
161 };
162
163 namespace detail {
164
165 template<typename>
166 struct is_column : std::false_type {};
167
168 template<typename T>
169 struct is_column<column<T>> : std::true_type {};
170
171 } // namespace detail
172
173 /** @brief References a typed column value proposed by the current INSERT. */
174 template<typename T>
175 15 expression<T> inserted(const column<T>& value)
176 {
177 15 const detail::expression_ptr& source = detail::expression_access::node(value);
178
1/1
✓ Branch 1 taken 15 times.
15 std::shared_ptr<detail::expression_node> node = std::make_shared<detail::expression_node>();
179 15 node->kind = detail::expression_kind::inserted_column;
180
1/1
✓ Branch 3 taken 15 times.
15 node->qualifier = source->qualifier;
181
1/1
✓ Branch 3 taken 15 times.
15 node->text = source->text;
182 30 return detail::expression_access::make<T>(std::move(node));
183 15 }
184
185 namespace detail {
186
187 struct relation_access final {
188 static const relation_ptr& node(const relation& value) noexcept;
189 static relation make(relation_ptr node);
190 };
191
192 SQLON_API relation_ptr to_relation(const relation& value);
193
194 SQLON_API relation make_join(detail::join_kind kind, detail::relation_ptr left, detail::relation_ptr right,
195 detail::expression_ptr condition = {});
196
197 } // namespace detail
198
199 template<typename Right>
200 4 relation relation::inner_join(const Right& right, const expression<bool>& condition) const
201 {
202 4 return detail::make_join(detail::join_kind::inner, m_node, detail::to_relation(right),
203
4/4
sqlon::relation sqlon::relation::inner_join<OrdersTable>(OrdersTable const&, sqlon::expression<bool> const&) const:
✓ Branch 3 taken 3 times.
✓ Branch 7 taken 3 times.
sqlon::relation sqlon::relation::inner_join<UsersTable>(UsersTable const&, sqlon::expression<bool> const&) const:
✓ Branch 3 taken 1 time.
✓ Branch 7 taken 1 time.
4 detail::expression_access::node(condition));
204 }
205
206 template<typename Right>
207 3 relation relation::left_join(const Right& right, const expression<bool>& condition) const
208 {
209 3 return detail::make_join(detail::join_kind::left, m_node, detail::to_relation(right),
210
4/4
sqlon::relation sqlon::relation::left_join<OrdersTable>(OrdersTable const&, sqlon::expression<bool> const&) const:
✓ Branch 3 taken 2 times.
✓ Branch 7 taken 2 times.
sqlon::relation sqlon::relation::left_join<sqlon::relation>(sqlon::relation const&, sqlon::expression<bool> const&) const:
✓ Branch 3 taken 1 time.
✓ Branch 7 taken 1 time.
3 detail::expression_access::node(condition));
211 }
212
213 template<typename Right>
214 3 relation relation::right_join(const Right& right, const expression<bool>& condition) const
215 {
216 3 return detail::make_join(detail::join_kind::right, m_node, detail::to_relation(right),
217
2/2
✓ Branch 3 taken 3 times.
✓ Branch 7 taken 3 times.
3 detail::expression_access::node(condition));
218 }
219
220 template<typename Right>
221 3 relation relation::full_join(const Right& right, const expression<bool>& condition) const
222 {
223 3 return detail::make_join(detail::join_kind::full, m_node, detail::to_relation(right),
224
2/2
✓ Branch 3 taken 3 times.
✓ Branch 7 taken 3 times.
3 detail::expression_access::node(condition));
225 }
226
227 template<typename Right>
228 3 relation relation::cross_join(const Right& right) const
229 {
230
2/2
✓ Branch 2 taken 3 times.
✓ Branch 6 taken 3 times.
3 return detail::make_join(detail::join_kind::cross, m_node, detail::to_relation(right));
231 }
232
233 /** @brief Returns a typed alias of a declared table. */
234 template<typename Table, std::enable_if_t<std::is_base_of_v<table, Table>, int> = 0>
235 13 Table alias(const Table& value, std::string alias_name)
236 {
237 13 if (alias_name.empty()) throw invalid_query{"a table alias cannot be empty"};
238 if constexpr (std::is_constructible_v<Table, qualified_name, std::string>) {
239 12 return Table{static_cast<const table&>(value).identifier(), std::move(alias_name)};
240 } else {
241 static_assert(std::is_constructible_v<Table, std::string, std::string>,
242 "table aliases require inherited table constructors");
243 const table& base = static_cast<const table&>(value);
244 if (base.identifier().parts().size() != 1)
245 throw invalid_query{"qualified table aliases require qualified-name table constructors"};
246 return Table{base.name(), std::move(alias_name)};
247 }
248 }
249
250 } // namespace sqlon
251
252 #endif // SQLON_RELATION_H
253