Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qsql_firebird_helpers.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// Copyright (C) 2026 Andreas Bacher
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4// Qt-Security score:significant reason:default
6
7#include <QtCore/qtimezone.h>
8
9#include <firebird/impl/blr.h> // BLR type constants for array element types
10
11#include <algorithm>
12#include <cstring>
13#include <limits>
14#include <iterator>
15
16QT_BEGIN_NAMESPACE
17
18// Firebird OO API types are used throughout; avoid full Firebird:: qualification.
19using namespace Firebird;
20
21Q_LOGGING_CATEGORY(lcFirebird, "qt.sql.firebird")
22
23using namespace Qt::StringLiterals;
24
25// Helper: convert Firebird status to a QSqlError string
26
27QString fbErrorString(IMaster *master, IStatus *status)
28{
29 IUtil *util = master->getUtilInterface();
30 char buf[1024];
31 util->formatStatus(buf, sizeof(buf), status);
32 return QString::fromUtf8(buf);
33}
34
35/*! \internal
36 Convert a Firebird status vector into a QSqlError. `type` is the context in
37 which the error occurred, supplied by the caller (open() -> ConnectionError,
38 prepare()/exec() -> StatementError, begin/commit/rollback ->
39 TransactionError); QSqlError::ErrorType categorises errors by that context,
40 not by the underlying Firebird cause. The SQLCODE is carried through as the
41 native error code.
42*/
43QSqlError fbError(IMaster *master, IStatus *status, QSqlError::ErrorType type)
44{
45 const QString msg = fbErrorString(master, status);
46 const ISC_LONG sqlcode = isc_sqlcode(status->getErrors());
47 const QString code = (sqlcode != -1 && sqlcode != 0)
48 ? QString::number(sqlcode) : QString();
49
50 return QSqlError({}, msg, type, code);
51}
52
53QString fbErrorLog(const QSqlError &err)
54{
55 static const char *typeNames[] = {
56 "NoError", "ConnectionError", "StatementError", "TransactionError", "UnknownError"
57 };
58 const int ti = std::clamp(static_cast<int>(err.type()), 0,
59 static_cast<int>(std::size(typeNames)) - 1);
60 const QString code = err.nativeErrorCode();
61 const QString text = err.databaseText().isEmpty() ? err.driverText() : err.databaseText();
62 return QString::fromLatin1("[%1, %2] %3").arg(QLatin1StringView(typeNames[ti]), code, text);
63}
64
65/*! \internal
66 Commit (or roll back) a transaction whose handle must not outlive a failure:
67 on a throw the interface is released and the member cleared before the
68 rethrow, on success commit/rollback already released it, so the member is
69 cleared too. Taking the handle by reference makes "released implies cleared"
70 structural — every finish site shares this discipline instead of repeating
71 it. (The bare-COMMIT path in exec() intentionally does NOT use this: a
72 failed user COMMIT keeps the still-valid handle so it can be retried.)
73*/
74void finishAndClear(Firebird::ThrowStatusWrapper &st,
75 Firebird::ITransaction *&tr, TxnEnd end)
76{
77 try {
78 if (end == TxnEnd::Commit)
79 tr->commit(&st);
80 else
81 tr->rollback(&st);
82 } catch (...) {
83 tr->release();
84 tr = nullptr;
85 throw;
86 }
87 tr = nullptr;
88}
89
90/*! \internal
91 Start a transaction with the driver's default TPB: read-committed,
92 record-version, read-write. wait selects isc_tpb_wait vs isc_tpb_nowait —
93 the only axis on which the auto-started transaction (wait) and the explicit
94 user transaction (nowait) differ.
95*/
96ITransaction *startDefaultTransaction(IAttachment *att, ThrowStatusWrapper &st,
97 TxnWait wait)
98{
99 const unsigned char tpb[] = {
100 isc_tpb_version3, isc_tpb_write, isc_tpb_read_committed, isc_tpb_rec_version,
101 static_cast<unsigned char>(wait == TxnWait::Wait ? isc_tpb_wait : isc_tpb_nowait)
102 };
103 return att->startTransaction(&st, sizeof(tpb), tpb);
104}
105
106// Map BLR element type (from ISC_ARRAY_DESC) to QMetaType
107QMetaType::Type blrTypeToQt(unsigned char blrType, bool hasScale)
108{
109 switch (blrType) {
110 case blr_text:
111 case blr_varying:
112 case blr_cstring: return QMetaType::QString;
113 case blr_short:
114 case blr_long: return hasScale ? QMetaType::Double : QMetaType::Int;
115 case blr_quad:
116 case blr_int64: return hasScale ? QMetaType::Double : QMetaType::LongLong;
117 case blr_float:
118 case blr_double:
119 case blr_d_float:
120 case blr_dec64:
121 case blr_dec128: return QMetaType::Double;
122 case blr_int128: return QMetaType::QString;
123 case blr_sql_date: return QMetaType::QDate;
124 case blr_sql_time: return QMetaType::QTime;
125 case blr_sql_time_tz: return QMetaType::QDateTime;
126 case blr_timestamp:
127 case blr_timestamp_tz: return QMetaType::QDateTime;
128 case blr_bool: return QMetaType::Bool;
129 /* Note: no blr_blob case. blr_blob (261) exceeds unsigned char range and
130 Firebird has no array-of-BLOB type, so the element dtype is never a blob. */
131 default: return QMetaType::QString;
132 }
133}
134
135// Numeric codecs
136
137/*! \internal
138 10^n as a 64-bit integer (n >= 0). Firebird numeric scale magnitudes are
139 small (NUMERIC/DECIMAL precision <= 38, INT64 <= 18), so this never overflows
140 for the integer-backed types it is used with.
141*/
143{
144 qint64 r = 1;
145 for (int i = 0; i < n; ++i)
146 r *= 10;
147 return r;
148}
149
150QMetaType::Type fbTypeToQt(int fbType, int scale)
151{
152 if (scale != 0) {
153 /* Scaled NUMERIC/DECIMAL. INT128-backed values cannot be faithfully
154 represented as Double, so report them as String. */
155 if (fbBaseType(fbType) == SQL_INT128)
156 return QMetaType::QString;
157 return QMetaType::Double;
158 }
159 switch (fbBaseType(fbType)) {
160 case SQL_SHORT:
161 case SQL_LONG: return QMetaType::Int;
162 case SQL_INT64: return QMetaType::LongLong;
163 case SQL_INT128: return QMetaType::QString; // 128-bit; no native Qt integer type
164 case SQL_FLOAT: return QMetaType::Double;
165 case SQL_DOUBLE: return QMetaType::Double;
166 case SQL_DEC16:
167 case SQL_DEC34: return QMetaType::QString; // DECFLOAT; no native Qt type
168 case SQL_BOOLEAN: return QMetaType::Bool;
169 case SQL_TYPE_DATE: return QMetaType::QDate;
170 case SQL_TYPE_TIME: return QMetaType::QTime;
171 case SQL_TIMESTAMP:
172 case SQL_TIMESTAMP_TZ:
173 case SQL_TIME_TZ: return QMetaType::QDateTime;
174 case SQL_BLOB: return QMetaType::QByteArray;
175 case SQL_ARRAY: return QMetaType::QVariantList;
176 case SQL_TEXT:
177 case SQL_VARYING: return QMetaType::QString;
178 default:
179 qCWarning(lcFirebird, "fbTypeToQt: unknown Firebird type %d", fbType);
180 return QMetaType::QString;
181 }
182}
183
184/*! \internal
185 Canonical decimal string for binding a value to an INT128/DECFLOAT parameter
186 via Firebird's fromString(). Integer and string variants are already plain
187 C-locale decimals; floating-point variants are reformatted only when
188 QVariant::toByteArray() yields exponential notation (e.g. a large double),
189 which IInt128::fromString does not accept.
190*/
191QByteArray numericInputString(const QVariant &val)
192{
193 QByteArray s = val.toByteArray();
194 const int t = val.typeId();
195 if ((t == QMetaType::Double || t == QMetaType::Float)
196 && (s.contains('e') || s.contains('E'))) {
197 s = QByteArray::number(val.toDouble(), 'f', 18);
198 if (s.contains('.')) {
199 while (s.endsWith('0'))
200 s.chop(1);
201 if (s.endsWith('.'))
202 s.chop(1);
203 }
204 }
205 return s;
206}
207
208/*! \internal
209 Convert a plain decimal string (as produced by numericInputString) into the
210 scaled integer Firebird stores for a NUMERIC/DECIMAL column: shift the
211 decimal point by -scale digits (right for the usual negative scale), round
212 half away from zero, and detect qint64 overflow. Works on the digit string,
213 never through double — a double round-trip silently drops digits above 2^53
214 and an out-of-range double-to-integer cast is undefined behaviour. The read
215 path (applyScale) avoids double for the same reason. Exponent notation is
216 rejected, matching IInt128::fromString on the INT128 path.
217*/
218bool decimalToScaledInt64(const QByteArray &decimal, int scale, qint64 *out)
219{
220 const QByteArray s = decimal.trimmed();
221 qsizetype pos = 0;
222 bool negative = false;
223 if (pos < s.size() && (s[pos] == '+' || s[pos] == '-'))
224 negative = (s[pos++] == '-');
225
226 QByteArray intPart, fracPart;
227 bool dot = false, anyDigit = false;
228 for (; pos < s.size(); ++pos) {
229 const char c = s[pos];
230 if (c == '.') {
231 if (dot)
232 return false;
233 dot = true;
234 } else if (c >= '0' && c <= '9') {
235 (dot ? fracPart : intPart).append(c);
236 anyDigit = true;
237 } else {
238 return false;
239 }
240 }
241 if (!anyDigit)
242 return false;
243
244 // Shift the decimal point right by -scale digits (left for positive scale).
245 const int shift = -scale;
246 if (shift >= 0) {
247 while (fracPart.size() < shift)
248 fracPart.append('0');
249 intPart += fracPart.left(shift);
250 fracPart.remove(0, shift);
251 } else {
252 const qsizetype k = -shift;
253 while (intPart.size() < k)
254 intPart.prepend('0');
255 fracPart.prepend(intPart.right(k));
256 intPart.chop(k);
257 }
258
259 // Round half away from zero on the first remaining fractional digit.
260 const bool roundUp = !fracPart.isEmpty() && fracPart[0] >= '5';
261
262 constexpr quint64 maxPos = quint64((std::numeric_limits<qint64>::max)());
263 const quint64 limit = negative ? maxPos + 1 : maxPos;
264 quint64 magnitude = 0;
265 for (const char c : intPart) {
266 const unsigned d = unsigned(c - '0');
267 if (magnitude > (limit - d) / 10)
268 return false; // overflow
269 magnitude = magnitude * 10 + d;
270 }
271 if (roundUp) {
272 if (magnitude == limit)
273 return false; // overflow
274 ++magnitude;
275 }
276
277 if (negative) {
278 *out = (magnitude == maxPos + 1)
279 ? (std::numeric_limits<qint64>::min)()
280 : -static_cast<qint64>(magnitude);
281 } else {
282 *out = static_cast<qint64>(magnitude);
283 }
284 return true;
285}
286
287QString int128ToString(IUtil *util, ThrowStatusWrapper &st,
288 const FB_I128 *value, int scale)
289{
290 char buf[64];
291 util->getInt128(&st)->toString(&st, value, scale, sizeof(buf), buf);
292 return QString::fromLatin1(buf);
293}
294
295// ---- Date/time encode helpers ----
296
297ISC_DATE encodeQDate(IUtil *util, const QDate &d)
298{
299 /* An invalid date reaches here when a non-date value is bound to a DATE
300 parameter — most often a date string in a format QVariant can't parse
301 (e.g. "25.05.2020"; QVariant only parses ISO). encodeDate(0,0,0) would
302 produce an out-of-range value the engine rejects ("value exceeds the
303 range for valid dates"). The legacy QIBASE driver instead maps an invalid
304 QDate to ISC_DATE 0 (the 1858-11-17 epoch) and stores it without error;
305 match that so such statements succeed identically. */
306 if (!d.isValid())
307 return 0;
308 return util->encodeDate(static_cast<unsigned>(d.year()),
309 static_cast<unsigned>(d.month()),
310 static_cast<unsigned>(d.day()));
311}
312
313ISC_TIME encodeQTime(IUtil *util, const QTime &t)
314{
315 if (!t.isValid())
316 return 0; // see encodeQDate: invalid time -> midnight, matching QIBASE
317 return util->encodeTime(static_cast<unsigned>(t.hour()),
318 static_cast<unsigned>(t.minute()),
319 static_cast<unsigned>(t.second()),
320 static_cast<unsigned>(t.msec()) * 10);
321}
322
323ISC_TIMESTAMP encodeQDateTime(IUtil *util, const QDateTime &dt)
324{
325 /* A plain TIMESTAMP carries no zone; Firebird just stores whatever wall-clock
326 date/time it is given and returns the same values back verbatim. Treat the
327 QDateTime as naive and pass its date()/time() straight through with no
328 conversion or relabeling - this matches Firebird's own semantics and how
329 other drivers; see decodeFirebirdTimestamp for the matching read side. */
330 ISC_TIMESTAMP ts;
331 ts.timestamp_date = encodeQDate(util, dt.date());
332 ts.timestamp_time = encodeQTime(util, dt.time());
333 return ts;
334}
335
336/*! \internal
337 Firebird names fixed-offset zones by their bare displacement ("+05:30"),
338 while Qt uses "UTC+05:30"-style ids for them. Map between the two so
339 offset-tagged QDateTimes bind and read back cleanly.
340*/
341static QByteArray toFirebirdZoneId(const QTimeZone &zone)
342{
343 QByteArray id = zone.id();
344 if (id.size() > 3 && id.startsWith("UTC") && (id.at(3) == '+' || id.at(3) == '-'))
345 id.remove(0, 3);
346 return id;
347}
348
349static QTimeZone fromFirebirdZoneId(const QByteArray &id)
350{
351 if (id.isEmpty())
352 return QTimeZone::utc();
353 if (id.at(0) == '+' || id.at(0) == '-')
354 return QTimeZone("UTC" + id);
355 return QTimeZone(id);
356}
357
358ISC_TIMESTAMP_TZ encodeQDateTimeTz(IStatus *iStatus, IUtil *util, const QDateTime &dt)
359{
360 ThrowStatusWrapper st(iStatus);
361 ISC_TIMESTAMP_TZ tstz;
362 // Invalid input -> epoch (see encodeQDate), so the engine doesn't reject it.
363 const bool valid = dt.isValid();
364 const QDate d = valid ? dt.date() : QDate(1858, 11, 17);
365 const QTime t = valid ? dt.time() : QTime(0, 0, 0);
366 const QByteArray ianaId =
367 (valid && dt.timeZone().isValid()) ? toFirebirdZoneId(dt.timeZone()) : "UTC"_ba;
368 util->encodeTimeStampTz(&st, &tstz,
369 static_cast<unsigned>(d.year()),
370 static_cast<unsigned>(d.month()),
371 static_cast<unsigned>(d.day()),
372 static_cast<unsigned>(t.hour()),
373 static_cast<unsigned>(t.minute()),
374 static_cast<unsigned>(t.second()),
375 static_cast<unsigned>(t.msec()) * 10,
376 ianaId.constData());
377 return tstz;
378}
379
380ISC_TIME_TZ encodeQTimeTz(IStatus *iStatus, IUtil *util, const QDateTime &dt)
381{
382 ThrowStatusWrapper st(iStatus);
383 ISC_TIME_TZ ttz;
384 const bool valid = dt.isValid();
385 const QTime t = valid ? dt.time() : QTime(0, 0, 0);
386 const QByteArray ianaId =
387 (valid && dt.timeZone().isValid()) ? toFirebirdZoneId(dt.timeZone()) : "UTC"_ba;
388 util->encodeTimeTz(&st, &ttz,
389 static_cast<unsigned>(t.hour()),
390 static_cast<unsigned>(t.minute()),
391 static_cast<unsigned>(t.second()),
392 static_cast<unsigned>(t.msec()) * 10,
393 ianaId.constData());
394 return ttz;
395}
396
397// ---- Date/time decode helpers ----
398
399QDate decodeFirebirdDate(IUtil *util, ISC_DATE date)
400{
401 unsigned year = 0;
402 unsigned month = 0;
403 unsigned day = 0;
404 util->decodeDate(date, &year, &month, &day);
405 return QDate(static_cast<int>(year), static_cast<int>(month), static_cast<int>(day));
406}
407
408QTime decodeFirebirdTime(IUtil *util, ISC_TIME time)
409{
410 unsigned hour = 0;
411 unsigned minute = 0;
412 unsigned sec = 0;
413 unsigned frac = 0;
414 util->decodeTime(time, &hour, &minute, &sec, &frac);
415 return QTime(static_cast<int>(hour), static_cast<int>(minute),
416 static_cast<int>(sec), static_cast<int>(frac / 10));
417}
418
419QDateTime decodeFirebirdTimestamp(IUtil *util, const ISC_TIMESTAMP &ts)
420{
421 /* A plain TIMESTAMP is naive: return its wall-clock date/time components
422 as-is, with no zone conversion or relabeling. This mirrors encodeQDateTime
423 (which stores the input's own date()/time() unmodified) and matches
424 Firebird's own semantics plus common driver behavior */
425 return QDateTime(decodeFirebirdDate(util, ts.timestamp_date),
426 decodeFirebirdTime(util, ts.timestamp_time));
427}
428
429QDateTime decodeFirebirdTimestampTz(IStatus *iStatus, IUtil *util,
430 const ISC_TIMESTAMP_TZ &tstz)
431{
432 CheckStatusWrapper st(iStatus);
433 unsigned year = 0;
434 unsigned month = 0;
435 unsigned day = 0;
436 unsigned hour = 0;
437 unsigned minute = 0;
438 unsigned sec = 0;
439 unsigned frac = 0;
440 char tzName[64] = {};
441 util->decodeTimeStampTz(&st, &tstz, &year, &month, &day, &hour, &minute, &sec, &frac,
442 sizeof(tzName), tzName);
443 const QTimeZone tz = fromFirebirdZoneId(QByteArray(tzName));
444 return QDateTime(QDate(static_cast<int>(year), static_cast<int>(month), static_cast<int>(day)),
445 QTime(static_cast<int>(hour), static_cast<int>(minute),
446 static_cast<int>(sec), static_cast<int>(frac / 10)),
447 tz);
448}
449
450QDateTime decodeFirebirdTimeTz(IStatus *iStatus, IUtil *util,
451 const ISC_TIME_TZ &ttz)
452{
453 CheckStatusWrapper st(iStatus);
454 unsigned hour = 0;
455 unsigned minute = 0;
456 unsigned sec = 0;
457 unsigned frac = 0;
458 char tzName[64] = {};
459 util->decodeTimeTz(&st, &ttz, &hour, &minute, &sec, &frac, sizeof(tzName), tzName);
460 const QTimeZone tz = fromFirebirdZoneId(QByteArray(tzName));
461 return QDateTime(QDate(1970, 1, 1),
462 QTime(static_cast<int>(hour), static_cast<int>(minute),
463 static_cast<int>(sec), static_cast<int>(frac / 10)),
464 tz);
465}
466
467/*! \internal
468 Encode a UTF-8 string into a CHAR/VARCHAR message slot: VARYING gets a
469 2-byte length prefix, TEXT is space-padded; data beyond the declared length
470 is truncated. Shared by fillInputBuffer and lookupArrayDesc's bind loop so
471 the Firebird text wire format lives in one place.
472*/
473void encodeTextValue(char *data, int fbType, qsizetype length, const QByteArray &bytes)
474{
475 if (fbBaseType(fbType) == SQL_VARYING) {
476 // VARY header: 2-byte length then data
477 const qsizetype len = std::min(length, bytes.size());
478 *reinterpret_cast<unsigned short *>(data) = static_cast<unsigned short>(len);
479 std::memcpy(data + 2, bytes.constData(), len);
480 } else {
481 const qsizetype len = std::min(bytes.size(), length);
482 std::memcpy(data, bytes.constData(), len);
483 if (len < length)
484 std::memset(data + len, ' ', length - len);
485 }
486}
487
488QT_END_NAMESPACE
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")
static QByteArray toFirebirdZoneId(const QTimeZone &zone)
static QTimeZone fromFirebirdZoneId(const QByteArray &id)
QDateTime decodeFirebirdTimestamp(Firebird::IUtil *util, const ISC_TIMESTAMP &ts)
int fbBaseType(int t)
QString fbErrorLog(const QSqlError &err)
Firebird::ITransaction * startDefaultTransaction(Firebird::IAttachment *att, Firebird::ThrowStatusWrapper &st, TxnWait wait)
QString fbErrorString(Firebird::IMaster *master, Firebird::IStatus *status)
bool decimalToScaledInt64(const QByteArray &decimal, int scale, qint64 *out)
ISC_TIMESTAMP encodeQDateTime(Firebird::IUtil *util, const QDateTime &dt)
ISC_TIME encodeQTime(Firebird::IUtil *util, const QTime &t)
QDate decodeFirebirdDate(Firebird::IUtil *util, ISC_DATE date)
QDateTime decodeFirebirdTimeTz(Firebird::IStatus *iStatus, Firebird::IUtil *util, const ISC_TIME_TZ &ttz)
void encodeTextValue(char *data, int fbType, qsizetype length, const QByteArray &bytes)
void finishAndClear(Firebird::ThrowStatusWrapper &st, Firebird::ITransaction *&tr, TxnEnd end)
QDateTime decodeFirebirdTimestampTz(Firebird::IStatus *iStatus, Firebird::IUtil *util, const ISC_TIMESTAMP_TZ &tstz)
QByteArray numericInputString(const QVariant &val)
QMetaType::Type blrTypeToQt(unsigned char blrType, bool hasScale)
QTime decodeFirebirdTime(Firebird::IUtil *util, ISC_TIME time)
ISC_TIMESTAMP_TZ encodeQDateTimeTz(Firebird::IStatus *iStatus, Firebird::IUtil *util, const QDateTime &dt)
ISC_TIME_TZ encodeQTimeTz(Firebird::IStatus *iStatus, Firebird::IUtil *util, const QDateTime &dt)
QString int128ToString(Firebird::IUtil *util, Firebird::ThrowStatusWrapper &st, const FB_I128 *value, int scale)
QSqlError fbError(Firebird::IMaster *master, Firebird::IStatus *status, QSqlError::ErrorType type)
ISC_DATE encodeQDate(Firebird::IUtil *util, const QDate &d)
QMetaType::Type fbTypeToQt(int fbType, int scale)
qint64 pow10i(int n)