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
137QMetaType::Type fbTypeToQt(int fbType, int scale)
138{
139 if (scale != 0) {
140 /* Scaled NUMERIC/DECIMAL. INT128-backed values cannot be faithfully
141 represented as Double, so report them as String. */
142 if (fbBaseType(fbType) == SQL_INT128)
143 return QMetaType::QString;
144 return QMetaType::Double;
145 }
146 switch (fbBaseType(fbType)) {
147 case SQL_SHORT:
148 case SQL_LONG: return QMetaType::Int;
149 case SQL_INT64: return QMetaType::LongLong;
150 case SQL_INT128: return QMetaType::QString; // 128-bit; QVariant has no such type — exact decimal string
151 case SQL_FLOAT: return QMetaType::Double;
152 case SQL_DOUBLE: return QMetaType::Double;
153 case SQL_DEC16:
154 case SQL_DEC34: return QMetaType::QString; // DECFLOAT; no native Qt type
155 case SQL_BOOLEAN: return QMetaType::Bool;
156 case SQL_TYPE_DATE: return QMetaType::QDate;
157 case SQL_TYPE_TIME: return QMetaType::QTime;
158 case SQL_TIMESTAMP:
159 case SQL_TIMESTAMP_TZ:
160 case SQL_TIME_TZ: return QMetaType::QDateTime;
161 case SQL_BLOB: return QMetaType::QByteArray;
162 case SQL_ARRAY: return QMetaType::QVariantList;
163 case SQL_TEXT:
164 case SQL_VARYING: return QMetaType::QString;
165 default:
166 qCWarning(lcFirebird, "fbTypeToQt: unknown Firebird type %d", fbType);
167 return QMetaType::QString;
168 }
169}
170
171// INT128 codec — see the header for the design
172
173#ifndef QSQL_HAS_INT128
174
175// Firebird's IInt128 converts via a caller buffer; 64 chars covers INT128's 39 digits.
176static QString int128SlotToString(const char *slot, int scale, IMaster *master, IStatus *status)
177{
178 ThrowStatusWrapper st(status);
179 char buf[64];
180 master->getUtilInterface()->getInt128(&st)->toString(
181 &st, reinterpret_cast<const FB_I128 *>(slot), scale, sizeof(buf), buf);
182 return QString::fromLatin1(buf);
183}
184
185#endif // QSQL_HAS_INT128
186
187bool encodeInt128Slot(const QVariant &val, int scale, char *slot,
188 IMaster *master, IStatus *status, QString &errorText)
189{
190#ifdef QSQL_HAS_INT128
191 Q_UNUSED(master);
192 Q_UNUSED(status);
193 qinternalint128 v = 0;
194 if (!QtPrivate::sqlScaledFromVariant(val, scale, &v, &errorText))
195 return false;
196 /* FB_I128 is two 64-bit words, fb_data[0] low and fb_data[1] high, at
197 8-byte alignment in the message buffer: copy the words, never dereference
198 a 16-byte-aligned type in place. */
199 static_assert(std::is_same_v<ISC_UINT64, quint64>,
200 "the FB_I128 words are written through quint64 pointers");
201 FB_I128 raw;
202 QtPrivate::sqlInt128ToWords(v, &raw.fb_data[0], &raw.fb_data[1]);
203 std::memcpy(slot, &raw, sizeof(raw));
204 return true;
205#else
206 /* No 128-bit integer type: let IInt128::fromString scale the decimal
207 string. It would also accept exponent notation, so pre-validate with
208 the exact codec's grammar (the same canonical text, so a Bool is "1" here
209 too), and report its out-of-range exception with the exact codec's
210 wording - "out of range" for an integral input, "cannot encode" for text.
211 Both builds then accept the same values and fail with the same message. */
212 const QByteArray strVal = QtPrivate::sqlNumericInputString(val);
213 if (!QtPrivate::sqlIsPlainDecimal(strVal)) {
214 errorText = QtPrivate::sqlCannotEncodeError(strVal, scale);
215 return false;
216 }
217 try {
218 ThrowStatusWrapper st(status);
219 master->getUtilInterface()->getInt128(&st)->fromString(
220 &st, scale, strVal.constData(), reinterpret_cast<FB_I128 *>(slot));
221 return true;
222 } catch (const FbException &) {
223 status->init();
224 errorText = QtPrivate::sqlIsIntegralVariant(val.typeId())
225 ? QtPrivate::sqlOutOfRangeError(val, scale)
226 : QtPrivate::sqlCannotEncodeError(strVal, scale);
227 return false;
228 }
229#endif
230}
231
232QVariant decodeInt128Slot(const char *slot, int scale, QSql::NumericalPrecisionPolicy policy,
233 IMaster *master, IStatus *status, QString *overflowText)
234{
235#ifdef QSQL_HAS_INT128
236 Q_UNUSED(master);
237 Q_UNUSED(status);
238 // Copy the FB_I128 words out of the slot (see encodeInt128Slot for the layout)
239 FB_I128 raw;
240 std::memcpy(&raw, slot, sizeof(raw));
241 const qinternalint128 value = QtPrivate::sqlInt128FromWords(raw.fb_data[0], raw.fb_data[1]);
242 bool overflow = false;
243 const QVariant result = QtPrivate::sqlApplyScale(value, scale, policy, &overflow);
244 // A double beats a wrapped or zero value; the caller decides when to say so.
245 if (overflow && overflowText)
246 *overflowText = QtPrivate::sqlScaledToString(value, scale);
247 return result;
248#else
249 const QString str = int128SlotToString(slot, scale, master, status);
250 switch (policy) {
251 case QSql::HighPrecision:
252 return str;
253 case QSql::LowPrecisionDouble:
254 return QVariant(str.toDouble());
255 case QSql::LowPrecisionInt32:
256 case QSql::LowPrecisionInt64: {
257 /* Take the integer part straight from the decimal string (truncating
258 toward zero) rather than via double, so 128-bit values keep full
259 integer precision up to the target type's range. */
260 const qsizetype dot = str.indexOf(u'.');
261 const QStringView intPart = dot < 0 ? QStringView(str) : QStringView(str).first(dot);
262 bool ok = false;
263 if (policy == QSql::LowPrecisionInt32) {
264 const int v = intPart.toInt(&ok);
265 if (ok)
266 return QVariant(v);
267 } else {
268 const qint64 v = intPart.toLongLong(&ok);
269 if (ok)
270 return QVariant(v);
271 }
272 // Same fallback as the native codec: a double beats a wrapped or zero value.
273 if (overflowText)
274 *overflowText = str;
275 return QVariant(str.toDouble());
276 }
277 }
278 return QVariant(str.toDouble());
279#endif
280}
281
282// ---- Date/time encode helpers ----
283
284ISC_DATE encodeQDate(IUtil *util, const QDate &d)
285{
286 /* An invalid date reaches here when a non-date value is bound to a DATE
287 parameter — most often a date string in a format QVariant can't parse
288 (e.g. "25.05.2020"; QVariant only parses ISO). encodeDate(0,0,0) would
289 produce an out-of-range value the engine rejects ("value exceeds the
290 range for valid dates"). The legacy QIBASE driver instead maps an invalid
291 QDate to ISC_DATE 0 (the 1858-11-17 epoch) and stores it without error;
292 match that so such statements succeed identically. */
293 if (!d.isValid())
294 return 0;
295 return util->encodeDate(static_cast<unsigned>(d.year()),
296 static_cast<unsigned>(d.month()),
297 static_cast<unsigned>(d.day()));
298}
299
300ISC_TIME encodeQTime(IUtil *util, const QTime &t)
301{
302 if (!t.isValid())
303 return 0; // see encodeQDate: invalid time -> midnight, matching QIBASE
304 return util->encodeTime(static_cast<unsigned>(t.hour()),
305 static_cast<unsigned>(t.minute()),
306 static_cast<unsigned>(t.second()),
307 static_cast<unsigned>(t.msec()) * 10);
308}
309
310ISC_TIMESTAMP encodeQDateTime(IUtil *util, const QDateTime &dt)
311{
312 /* A plain TIMESTAMP carries no zone; Firebird just stores whatever wall-clock
313 date/time it is given and returns the same values back verbatim. Treat the
314 QDateTime as naive and pass its date()/time() straight through with no
315 conversion or relabeling - this matches Firebird's own semantics and how
316 other drivers; see decodeFirebirdTimestamp for the matching read side. */
317 ISC_TIMESTAMP ts;
318 ts.timestamp_date = encodeQDate(util, dt.date());
319 ts.timestamp_time = encodeQTime(util, dt.time());
320 return ts;
321}
322
323/*! \internal
324 Firebird names fixed-offset zones by their bare displacement ("+05:30"),
325 while Qt uses "UTC+05:30"-style ids for them. Map between the two so
326 offset-tagged QDateTimes bind and read back cleanly.
327*/
328static QByteArray toFirebirdZoneId(const QTimeZone &zone)
329{
330 QByteArray id = zone.id();
331 if (id.size() > 3 && id.startsWith("UTC") && (id.at(3) == '+' || id.at(3) == '-'))
332 id.remove(0, 3);
333 return id;
334}
335
336static QTimeZone fromFirebirdZoneId(const QByteArray &id)
337{
338 if (id.isEmpty())
339 return QTimeZone::utc();
340 if (id.at(0) == '+' || id.at(0) == '-')
341 return QTimeZone("UTC" + id);
342 return QTimeZone(id);
343}
344
345ISC_TIMESTAMP_TZ encodeQDateTimeTz(IStatus *iStatus, IUtil *util, const QDateTime &dt)
346{
347 ThrowStatusWrapper st(iStatus);
348 ISC_TIMESTAMP_TZ tstz;
349 // Invalid input -> epoch (see encodeQDate), so the engine doesn't reject it.
350 const bool valid = dt.isValid();
351 const QDate d = valid ? dt.date() : QDate(1858, 11, 17);
352 const QTime t = valid ? dt.time() : QTime(0, 0, 0);
353 const QByteArray ianaId =
354 (valid && dt.timeZone().isValid()) ? toFirebirdZoneId(dt.timeZone()) : "UTC"_ba;
355 util->encodeTimeStampTz(&st, &tstz,
356 static_cast<unsigned>(d.year()),
357 static_cast<unsigned>(d.month()),
358 static_cast<unsigned>(d.day()),
359 static_cast<unsigned>(t.hour()),
360 static_cast<unsigned>(t.minute()),
361 static_cast<unsigned>(t.second()),
362 static_cast<unsigned>(t.msec()) * 10,
363 ianaId.constData());
364 return tstz;
365}
366
367ISC_TIME_TZ encodeQTimeTz(IStatus *iStatus, IUtil *util, const QDateTime &dt)
368{
369 ThrowStatusWrapper st(iStatus);
370 ISC_TIME_TZ ttz;
371 const bool valid = dt.isValid();
372 const QTime t = valid ? dt.time() : QTime(0, 0, 0);
373 const QByteArray ianaId =
374 (valid && dt.timeZone().isValid()) ? toFirebirdZoneId(dt.timeZone()) : "UTC"_ba;
375 util->encodeTimeTz(&st, &ttz,
376 static_cast<unsigned>(t.hour()),
377 static_cast<unsigned>(t.minute()),
378 static_cast<unsigned>(t.second()),
379 static_cast<unsigned>(t.msec()) * 10,
380 ianaId.constData());
381 return ttz;
382}
383
384// ---- Date/time decode helpers ----
385
386QDate decodeFirebirdDate(IUtil *util, ISC_DATE date)
387{
388 unsigned year = 0;
389 unsigned month = 0;
390 unsigned day = 0;
391 util->decodeDate(date, &year, &month, &day);
392 return QDate(static_cast<int>(year), static_cast<int>(month), static_cast<int>(day));
393}
394
395QTime decodeFirebirdTime(IUtil *util, ISC_TIME time)
396{
397 unsigned hour = 0;
398 unsigned minute = 0;
399 unsigned sec = 0;
400 unsigned frac = 0;
401 util->decodeTime(time, &hour, &minute, &sec, &frac);
402 return QTime(static_cast<int>(hour), static_cast<int>(minute),
403 static_cast<int>(sec), static_cast<int>(frac / 10));
404}
405
406QDateTime decodeFirebirdTimestamp(IUtil *util, const ISC_TIMESTAMP &ts)
407{
408 /* A plain TIMESTAMP is naive: return its wall-clock date/time components
409 as-is, with no zone conversion or relabeling. This mirrors encodeQDateTime
410 (which stores the input's own date()/time() unmodified) and matches
411 Firebird's own semantics plus common driver behavior */
412 return QDateTime(decodeFirebirdDate(util, ts.timestamp_date),
413 decodeFirebirdTime(util, ts.timestamp_time));
414}
415
416QDateTime decodeFirebirdTimestampTz(IStatus *iStatus, IUtil *util,
417 const ISC_TIMESTAMP_TZ &tstz)
418{
419 CheckStatusWrapper st(iStatus);
420 unsigned year = 0;
421 unsigned month = 0;
422 unsigned day = 0;
423 unsigned hour = 0;
424 unsigned minute = 0;
425 unsigned sec = 0;
426 unsigned frac = 0;
427 char tzName[64] = {};
428 util->decodeTimeStampTz(&st, &tstz, &year, &month, &day, &hour, &minute, &sec, &frac,
429 sizeof(tzName), tzName);
430 const QTimeZone tz = fromFirebirdZoneId(QByteArray(tzName));
431 return QDateTime(QDate(static_cast<int>(year), static_cast<int>(month), static_cast<int>(day)),
432 QTime(static_cast<int>(hour), static_cast<int>(minute),
433 static_cast<int>(sec), static_cast<int>(frac / 10)),
434 tz);
435}
436
437QDateTime decodeFirebirdTimeTz(IStatus *iStatus, IUtil *util,
438 const ISC_TIME_TZ &ttz)
439{
440 CheckStatusWrapper st(iStatus);
441 unsigned hour = 0;
442 unsigned minute = 0;
443 unsigned sec = 0;
444 unsigned frac = 0;
445 char tzName[64] = {};
446 util->decodeTimeTz(&st, &ttz, &hour, &minute, &sec, &frac, sizeof(tzName), tzName);
447 const QTimeZone tz = fromFirebirdZoneId(QByteArray(tzName));
448 return QDateTime(QDate(1970, 1, 1),
449 QTime(static_cast<int>(hour), static_cast<int>(minute),
450 static_cast<int>(sec), static_cast<int>(frac / 10)),
451 tz);
452}
453
454/*! \internal
455 Encode a UTF-8 string into a CHAR/VARCHAR message slot: VARYING gets a
456 2-byte length prefix, TEXT is space-padded; data beyond the declared length
457 is truncated. Shared by fillInputBuffer and lookupArrayDesc's bind loop so
458 the Firebird text wire format lives in one place.
459*/
460void encodeTextValue(char *data, int fbType, qsizetype length, const QByteArray &bytes)
461{
462 if (fbBaseType(fbType) == SQL_VARYING) {
463 // VARY header: 2-byte length then data
464 const qsizetype len = std::min(length, bytes.size());
465 *reinterpret_cast<unsigned short *>(data) = static_cast<unsigned short>(len);
466 std::memcpy(data + 2, bytes.constData(), len);
467 } else {
468 const qsizetype len = std::min(bytes.size(), length);
469 std::memcpy(data, bytes.constData(), len);
470 if (len < length)
471 std::memset(data + len, ' ', length - len);
472 }
473}
474
475QT_END_NAMESPACE
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")
static QString int128SlotToString(const char *slot, int scale, IMaster *master, IStatus *status)
QSqlError fbError(IMaster *master, IStatus *status, QSqlError::ErrorType type)
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)
bool encodeInt128Slot(const QVariant &val, int scale, char *slot, Firebird::IMaster *master, Firebird::IStatus *status, QString &errorText)
QString fbErrorLog(const QSqlError &err)
Firebird::ITransaction * startDefaultTransaction(Firebird::IAttachment *att, Firebird::ThrowStatusWrapper &st, TxnWait wait)
QString fbErrorString(Firebird::IMaster *master, Firebird::IStatus *status)
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)
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)
ISC_DATE encodeQDate(Firebird::IUtil *util, const QDate &d)
QMetaType::Type fbTypeToQt(int fbType, int scale)
QVariant decodeInt128Slot(const char *slot, int scale, QSql::NumericalPrecisionPolicy policy, Firebird::IMaster *master, Firebird::IStatus *status, QString *overflowText=nullptr)