Lightweight 0.20261105.1
Loading...
Searching...
No Matches
SqlErrorDetection.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "SqlError.hpp"
6#include "SqlServerType.hpp"
7
8#include <algorithm>
9#include <array>
10#include <string_view>
11
12namespace Lightweight
13{
14
15/// Detects if an error indicates "table already exists".
16///
17/// @param error The SQL error info
18/// @param serverType The database server type
19/// @return true if this is a "table already exists" error
20[[nodiscard]] inline bool IsTableAlreadyExistsError(SqlErrorInfo const& error, SqlServerType serverType) noexcept
21{
22 switch (serverType)
23 {
24 case SqlServerType::MICROSOFT_SQL:
25 // SQLSTATE 42S01 = Base table or view already exists
26 // Native error 2714 = "There is already an object named '...' in the database"
27 return error.sqlState == "42S01" || error.nativeErrorCode == 2714;
28 case SqlServerType::POSTGRESQL:
29 // SQLSTATE 42P07 = duplicate_table
30 return error.sqlState == "42P07";
31 case SqlServerType::SQLITE:
32 // SQLite uses generic SQLSTATE, check message
33 return error.message.contains("already exists");
34 case SqlServerType::MYSQL:
35 // SQLSTATE 42S01 = Base table or view already exists
36 // Native error 1050 = "Table '...' already exists"
37 return error.sqlState == "42S01" || error.nativeErrorCode == 1050;
38 default:
39 // Fallback: check message for common pattern
40 return error.message.contains("already exists");
41 }
42}
43
44/// Detects if an error indicates "table not found".
45///
46/// @param error The SQL error info
47/// @param serverType The database server type
48/// @return true if this is a "table not found" error
49[[nodiscard]] inline bool IsTableNotFoundError(SqlErrorInfo const& error, SqlServerType serverType) noexcept
50{
51 switch (serverType)
52 {
53 case SqlServerType::MICROSOFT_SQL:
54 // SQLSTATE 42S02 = Base table or view not found
55 // Native error 208 = "Invalid object name '...'"
56 return error.sqlState == "42S02" || error.nativeErrorCode == 208;
57 case SqlServerType::POSTGRESQL:
58 // SQLSTATE 42P01 = undefined_table
59 return error.sqlState == "42P01";
60 case SqlServerType::SQLITE:
61 // SQLite uses generic SQLSTATE, check message
62 return error.message.contains("no such table");
63 case SqlServerType::MYSQL:
64 // SQLSTATE 42S02 = Base table or view not found
65 // Native error 1146 = "Table '...' doesn't exist"
66 return error.sqlState == "42S02" || error.nativeErrorCode == 1146;
67 default:
68 // Fallback: check message for common patterns
69 return error.message.contains("not found") || error.message.contains("no such")
70 || error.message.contains("does not exist");
71 }
72}
73
74/// Detects if an error is bind-class: the driver cannot bind or convert a parameter of the statement,
75/// which fails every row of a batch alike, so retrying or isolating rows would only repeat it.
76///
77/// @param error The SQL error info
78/// @return true for SQLSTATE `HYC00` (optional feature not implemented), `HY003` (invalid application
79/// buffer type), `HY004` (invalid SQL data type) and `07006` (restricted data type attribute
80/// violation)
81[[nodiscard]] inline bool IsBindClassError(SqlErrorInfo const& error) noexcept
82{
83 constexpr auto BindClassStates = std::array<std::string_view, 4> { "HYC00", "HY003", "HY004", "07006" };
84 return std::ranges::contains(BindClassStates, std::string_view { error.sqlState });
85}
86
87/// Detects if a statement failed because its query timeout expired (see @c SqlStatement::SetQueryTimeout).
88///
89/// SQL Server reports SQLSTATE `HYT00`. psqlODBC implements the timeout through PostgreSQL's `statement_timeout`,
90/// so PostgreSQL reports `57014` (query_canceled), which it also reports for an explicit cancel: the only text
91/// telling those two apart is the server's message, and that is translated by `lc_messages`. A statement that
92/// is never cancelled by hand therefore sees `57014` only from its timeout.
93///
94/// @param error The SQL error info
95/// @return true for SQLSTATE `HYT00` and `57014`
96[[nodiscard]] inline bool IsQueryTimeoutError(SqlErrorInfo const& error) noexcept
97{
98 constexpr auto QueryTimeoutStates = std::array<std::string_view, 2> { "HYT00", "57014" };
99 return std::ranges::contains(QueryTimeoutStates, std::string_view { error.sqlState });
100}
101
102/// Detects if an error is transient (retryable).
103///
104/// Transient errors are temporary conditions that may succeed if retried,
105/// such as connection issues, timeouts, or database locks.
106///
107/// @param error The SQL error info
108/// @return true if this is a transient error that may be retried
109[[nodiscard]] inline bool IsTransientError(SqlErrorInfo const& error) noexcept
110{
111 std::string_view state = error.sqlState;
112
113 // Connection errors (SQLSTATE Class 08)
114 // 08001 = Unable to connect
115 // 08003 = Connection does not exist
116 // 08004 = Connection rejected
117 // 08006 = Connection failure
118 // 08007 = Transaction resolution unknown
119 if (state.starts_with("08"))
120 return true;
121
122 // Timeout errors
123 // HYT00 = Timeout expired
124 // HYT01 = Connection timeout expired
125 if (state == "HYT00" || state == "HYT01")
126 return true;
127
128 // Transaction rollback (SQLSTATE Class 40)
129 // 40001 = Serialization failure (deadlock)
130 // 40002 = Transaction integrity constraint violation
131 // 40003 = Statement completion unknown
132 if (state.starts_with("40"))
133 return true;
134
135 // SQLite-specific busy/locked conditions
136 if (error.message.contains("database is locked"))
137 return true;
138 if (error.message.contains("SQLITE_BUSY"))
139 return true;
140 if (error.message.contains("SQLITE_LOCKED"))
141 return true;
142
143 // SQL Server specific transient errors
144 // Native error codes for transient conditions
145 switch (error.nativeErrorCode)
146 {
147 case 1205: // Deadlock victim (SQL Server)
148 case 1222: // Lock request timeout (SQL Server)
149 case -2: // Timeout (SQL Server)
150 return true;
151 default:
152 break;
153 }
154
155 return false;
156}
157
158/// Detects if an error indicates a unique constraint violation.
159///
160/// @param error The SQL error info
161/// @param serverType The database server type
162/// @return true if this is a unique constraint violation error
163[[nodiscard]] inline bool IsUniqueConstraintViolation(SqlErrorInfo const& error, SqlServerType serverType) noexcept
164{
165 switch (serverType)
166 {
167 case SqlServerType::MICROSOFT_SQL:
168 // SQLSTATE 23000 = Integrity constraint violation
169 // Native error 2627 = Violation of UNIQUE KEY constraint
170 // Native error 2601 = Cannot insert duplicate key row
171 return error.nativeErrorCode == 2627 || error.nativeErrorCode == 2601;
172 case SqlServerType::POSTGRESQL:
173 // SQLSTATE 23505 = unique_violation
174 return error.sqlState == "23505";
175 case SqlServerType::SQLITE:
176 // SQLite uses generic SQLSTATE, check message
177 return error.message.contains("UNIQUE constraint failed");
178 case SqlServerType::MYSQL:
179 // Native error 1062 = Duplicate entry for key
180 return error.nativeErrorCode == 1062;
181 default:
182 return error.sqlState == "23000" || error.sqlState == "23505";
183 }
184}
185
186/// Detects if an error indicates a foreign key constraint violation.
187///
188/// @param error The SQL error info
189/// @param serverType The database server type
190/// @return true if this is a foreign key constraint violation error
191[[nodiscard]] inline bool IsForeignKeyViolation(SqlErrorInfo const& error, SqlServerType serverType) noexcept
192{
193 switch (serverType)
194 {
195 case SqlServerType::MICROSOFT_SQL:
196 // Native error 547 = The INSERT/UPDATE/DELETE statement conflicted with FK constraint
197 return error.nativeErrorCode == 547;
198 case SqlServerType::POSTGRESQL:
199 // SQLSTATE 23503 = foreign_key_violation
200 return error.sqlState == "23503";
201 case SqlServerType::SQLITE:
202 // SQLite uses generic SQLSTATE, check message
203 return error.message.contains("FOREIGN KEY constraint failed");
204 case SqlServerType::MYSQL:
205 // Native error 1451 = Cannot delete or update a parent row
206 // Native error 1452 = Cannot add or update a child row
207 return error.nativeErrorCode == 1451 || error.nativeErrorCode == 1452;
208 default:
209 return error.sqlState == "23503";
210 }
211}
212
213} // namespace Lightweight