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_result.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
9
10#include <QtCore/qvarlengtharray.h>
11#include <QtSql/qsqlfield.h>
12
13#include <algorithm>
14#include <cstring>
15#include <limits>
16
17QT_BEGIN_NAMESPACE
18
19// Firebird OO API types are used throughout; avoid full Firebird:: qualification.
20using namespace Firebird;
21
22using namespace Qt::StringLiterals;
23
24// Helpers used only by the result implementation
25
26// Build columns metadata from IMessageMetadata
27static QList<ColumnInfo> buildColumns(IMessageMetadata *meta, ThrowStatusWrapper &st)
28{
29 QList<ColumnInfo> cols;
30 unsigned count = meta->getCount(&st);
31 cols.reserve(count);
32 for (unsigned i = 0; i < count; ++i) {
33 ColumnInfo ci;
34 ci.name = QString::fromUtf8(meta->getAlias(&st, i));
35 if (ci.name.isEmpty())
36 ci.name = QString::fromUtf8(meta->getField(&st, i));
37 ci.relation = QString::fromUtf8(meta->getRelation(&st, i));
38 ci.field = QString::fromUtf8(meta->getField(&st, i));
39 ci.fbType = static_cast<int>(meta->getType(&st, i));
40 ci.fbSubType = meta->getSubType(&st, i);
41 ci.scale = meta->getScale(&st, i);
42 ci.length = meta->getLength(&st, i);
43 ci.offset = meta->getOffset(&st, i);
44 ci.nullOffset = meta->getNullOffset(&st, i);
45 ci.nullable = (meta->isNullable(&st, i) != FB_FALSE);
46 ci.qtType = fbTypeToQt(ci.fbType, ci.scale);
47 cols.append(ci);
48 }
49 return cols;
50}
51
52/*! \internal
53 Read a BLOB into QByteArray. Uses ThrowStatusWrapper so a read error throws
54 FbException (caught by QFirebirdResult::data) instead of being swallowed and
55 returning a silently truncated buffer.
56*/
57static QByteArray readBlob(IAttachment *att, ITransaction *tra, IStatus *st,
58 const ISC_QUAD &blobId)
59{
60 ThrowStatusWrapper tsw(st);
61 FbGuard<IBlob> blob(att->openBlob(&tsw, tra, const_cast<ISC_QUAD *>(&blobId), 0, nullptr),
62 fbRelease<IBlob>);
63 if (!blob)
64 return {};
65
66 QByteArray result;
67 unsigned char seg[16384];
68 result.reserve(sizeof(seg)); // avoid reallocations for the common small-BLOB case
69 unsigned segLen = 0;
70 int code;
71 /* A getSegment or close failure throws; FbGuard releases the handle on the
72 way out (and closeWith rethrows after releasing on a failed close). */
73 do {
74 code = blob->getSegment(&tsw, sizeof(seg), seg, &segLen);
75 result.append(reinterpret_cast<const char *>(seg), segLen);
76 } while (code == IStatus::RESULT_OK || code == IStatus::RESULT_SEGMENT);
77 blob.closeWith([&](IBlob *b) { b->close(&tsw); });
78 return result;
79}
80
81// QFirebirdResultPrivate implementation
82
84{
85 /* Only touch the Firebird handles while the driver is alive AND its
86 attachment is still open. If the connection was closed while this result
87 was still around (QSqlDatabase::close() with a QSqlQuery in scope),
88 detach() has already invalidated the statement/cursor/transaction; if the
89 driver object itself was destroyed (driverAlive == false), the driver
90 private is freed and must not be dereferenced at all. In either case just
91 drop the now-dangling pointers. driverAlive is checked first so att()
92 (which reads the driver private) is never called on a freed driver. */
93 const bool attached = driverAlive && att() != nullptr;
94
95 /* Close a handle gracefully while the attachment is alive (release it if the
96 close throws); when detached, the engine has already invalidated it, so
97 just drop the now-dangling pointer. closeFn performs the type's graceful
98 shutdown (IResultSet::close, IStatement::free, ITransaction::commit). */
99 auto closeHandle = [&](auto *&handle, auto closeFn) {
100 if (!handle)
101 return;
102 if (attached) {
103 try {
104 ThrowStatusWrapper tsw(status());
105 closeFn(handle, tsw);
106 } catch (...) {
107 handle->release();
108 }
109 }
110 handle = nullptr;
111 };
112
113 closeHandle(cursor, [](IResultSet *c, ThrowStatusWrapper &st) { c->close(&st); });
114 closeHandle(stmt, [](IStatement *s, ThrowStatusWrapper &st) { s->free(&st); });
115 closeHandle(autoTrans, [](ITransaction *t, ThrowStatusWrapper &st) { t->commit(&st); });
116
117 if (outMeta) {
118 if (attached)
119 outMeta->release();
120 outMeta = nullptr;
121 }
122 if (inMeta) {
123 if (attached)
124 inMeta->release();
125 inMeta = nullptr;
126 }
127 outBuffer.clear();
128 inBuffer.clear();
129 cols.clear();
130 inCols.clear();
131 arrayDescCache.clear();
132 cachedRecord.clear();
133 recordCached = false;
134 affectedRows = -1;
135 isSelect = false;
136 isProcExec = false;
137 procRowFetched = false;
138 txnOp = TxnOp::None;
139 useRowCache = false;
140 cacheComplete = false;
141 rowCache.clear();
142}
143
145{
146 if (!cursor)
147 return;
148 try {
149 ThrowStatusWrapper tsw(status());
150 cursor->close(&tsw);
151 } catch (...) {
152 cursor->release();
153 }
154 cursor = nullptr;
155}
156
158{
159 if (index < 0)
160 return false;
161 while (!cacheComplete && rowCache.size() <= index) {
162 ThrowStatusWrapper st(status());
163 /* fetchNext writes into outBuffer; detaching it (COW) on the next
164 iteration keeps the snapshot just appended to rowCache intact. */
165 const int code = cursor->fetchNext(&st, outBuffer.data());
166 if (code == IStatus::RESULT_OK)
167 rowCache.append(outBuffer);
168 else
169 cacheComplete = true; // RESULT_NO_DATA (or non-OK): end of set
170 }
171 return index < rowCache.size();
172}
173
175{
176 QString msg = ctx + u": " + fbErrorString(master(), status());
177 status()->init();
178 return msg;
179}
180
182{
183 // User-managed transaction takes priority.
184 if (drv_d_func()->iTrans)
185 return drv_d_func()->iTrans;
186 if (!autoTrans) {
187 ThrowStatusWrapper st(status());
188 autoTrans = startDefaultTransaction(att(), st, TxnWait::Wait);
189 }
190 return autoTrans;
191}
192
193// QFirebirdResult
194
197{
198 /* Register with the driver so it can neutralise this result if it is
199 destroyed while the result is still alive (see ~QFirebirdDriver). */
200 Q_D(QFirebirdResult);
201 d->drv_d_func()->activeResults.append(d);
202}
203
205{
206 Q_D(QFirebirdResult);
207 d->cleanup();
208}
209
210void QFirebirdResult::cleanup()
211{
212 Q_D(QFirebirdResult);
213 d->cleanup();
214 setAt(QSql::BeforeFirstRow);
215 setActive(false);
216}
217
219{
220 Q_D(const QFirebirdResult);
221 return QVariant::fromValue(d->stmt);
222}
223
224/*! \internal
225 FinishQuery support: QSqlQuery::finish() routes here. Release the server
226 cursor and the client-side row cache, but keep the prepared statement and
227 its metadata caches so exec() can re-run the query without a new prepare().
228 The auto transaction (if any) stays open on purpose: BLOB/array ids already
229 handed to the application remain resolvable until the next exec()/cleanup().
230*/
232{
233 Q_D(QFirebirdResult);
234 d->closeCursor();
235 d->rowCache.clear();
236 d->cacheComplete = false;
237}
238
239/*! \internal
240 Classify a bare transaction-control statement (COMMIT/ROLLBACK [WORK]). These
241 appear in SQL scripts but cannot be executed as DSQL against the driver's
242 managed transaction (Firebird rejects it with "invalid transaction handle");
243 they are applied via the ITransaction API instead. COMMIT/ROLLBACK RETAIN
244 are not treated specially.
245*/
246static TxnOp classifyTxnControl(const QString &query)
247{
248 QString s = query.trimmed();
249 while (s.endsWith(u';'))
250 s.chop(1);
251 s = s.trimmed().toLower().simplified();
252 if (s == "commit"_L1 || s == "commit work"_L1)
253 return TxnOp::Commit;
254 if (s == "rollback"_L1 || s == "rollback work"_L1)
255 return TxnOp::Rollback;
256 return TxnOp::None;
257}
258
259bool QFirebirdResult::prepare(const QString &query)
260{
261 Q_D(QFirebirdResult);
262 cleanup();
263
264 /* Bare COMMIT/ROLLBACK is handled in exec() via the transaction API; don't
265 prepare it as a DSQL statement (the engine rejects executing it against a
266 driver-managed transaction). */
267 if (const TxnOp op = classifyTxnControl(query); op != TxnOp::None) {
268 d->txnOp = op;
269 setSelect(false);
270 return true;
271 }
272
273 const QByteArray utf8 = query.toUtf8();
274 try {
275 ThrowStatusWrapper st(d->status());
276 ITransaction *tr = d->ensureTransaction();
277 d->stmt = d->att()->prepare(&st, tr, 0, utf8.constData(),
278 SQL_DIALECT_V6,
279 IStatement::PREPARE_PREFETCH_METADATA);
280 unsigned flags = d->stmt->getFlags(&st);
281 d->isSelect = (flags & IStatement::FLAG_HAS_CURSOR) != 0;
282 setSelect(d->isSelect); // required: QSqlQuery::next() checks base-class isSelect()
283
284 d->inMeta = d->stmt->getInputMetadata(&st);
285 if (d->inMeta && d->inMeta->getCount(&st) == 0) {
286 d->inMeta->release();
287 d->inMeta = nullptr;
288 }
289 d->outMeta = d->stmt->getOutputMetadata(&st);
290 if (d->outMeta) {
291 if (d->outMeta->getCount(&st) == 0) {
292 d->outMeta->release();
293 d->outMeta = nullptr;
294 } else {
295 d->cols = buildColumns(d->outMeta, st);
296 d->outBuffer.resize(static_cast<qsizetype>(d->outMeta->getAlignedLength(&st)));
297 }
298 }
299 if (!d->isSelect && d->outMeta) {
300 // EXECUTE PROCEDURE with RETURNS clause: expose as single-row result set
301 d->isProcExec = true;
302 setSelect(true);
303 }
304 if (d->inMeta) {
305 d->inBuffer.resize(static_cast<qsizetype>(d->inMeta->getAlignedLength(&st)));
306 d->inCols = buildColumns(d->inMeta, st);
307 }
308 } catch (const FbException &e) {
309 const QSqlError err = fbError(d->master(), e.getStatus(), QSqlError::StatementError);
310 qCInfo(lcFirebird) << "prepare:" << fbErrorLog(err);
311 /* Release any partially-acquired statement/metadata so a failed prepare
312 leaves the result non-executable (exec() guards on a null stmt) instead
313 of running against a half-initialised statement. */
314 d->cleanup();
315 setSelect(false);
316 setLastError(err);
317 return false;
318 }
319 return true;
320}
321
323{
324 if (!inMeta)
325 return true;
326 inBuffer.fill(0);
327 return fillInputBuffer(values, errorText);
328}
329
330bool QFirebirdResultPrivate::encodeScaledNumeric(char *data, int fbType, int scale,
331 const QVariant &val, QString &errorText)
332{
333 /* Numeric/decimal — store as INT64 scaled (or INT128 for very wide types).
334 actual = stored * 10^scale, so stored = value / 10^scale. The scaled
335 integer is derived exactly from the value's decimal-string form (see
336 decimalToScaledInt64); out-of-range values are reported instead of
337 silently wrapped. */
338 if (fbBaseType(fbType) == SQL_INT128) {
339 // Scale the string representation via IInt128::fromString with scale
340 const QByteArray strVal = numericInputString(val);
341 ThrowStatusWrapper innerSt(status());
342 master()->getUtilInterface()->getInt128(&innerSt)->fromString(
343 &innerSt, scale, strVal.constData(),
344 reinterpret_cast<FB_I128 *>(data));
345 return true;
346 }
347
348 const auto outOfRange = [&]() {
349 errorText = u"Value '%1' out of range for NUMERIC/DECIMAL with scale %2"_s
350 .arg(val.toString(), QString::number(scale));
351 return false;
352 };
353
354 qint64 iv = 0;
355 const int t = val.typeId();
356 if (scale < 0 && -scale < 19
357 && (t == QMetaType::Int || t == QMetaType::UInt || t == QMetaType::LongLong)) {
358 /* Fast path: an integral input scales with one checked multiply — no
359 string round trip on the per-row bind path. */
360 const qint64 v = val.toLongLong();
361 const qint64 factor = pow10i(-scale);
362 if (v == (std::numeric_limits<qint64>::min)()
363 || qAbs(v) > (std::numeric_limits<qint64>::max)() / factor) {
364 return outOfRange();
365 }
366 iv = v * factor;
367 } else {
368 const QByteArray strVal = numericInputString(val);
369 if (!decimalToScaledInt64(strVal, scale, &iv)) {
370 errorText = u"Cannot encode '%1' as NUMERIC/DECIMAL with scale %2"_s
371 .arg(QString::fromLatin1(strVal), QString::number(scale));
372 return false;
373 }
374 }
375
376 // One range check for the narrower targets, then a width-dispatched store.
377 qint64 lo = (std::numeric_limits<qint64>::min)();
378 qint64 hi = (std::numeric_limits<qint64>::max)();
379 switch (fbBaseType(fbType)) {
380 case SQL_SHORT:
381 lo = (std::numeric_limits<qint16>::min)();
382 hi = (std::numeric_limits<qint16>::max)();
383 break;
384 case SQL_LONG:
385 lo = (std::numeric_limits<qint32>::min)();
386 hi = (std::numeric_limits<qint32>::max)();
387 break;
388 default:
389 break;
390 }
391 if (iv < lo || iv > hi)
392 return outOfRange();
393
394 switch (fbBaseType(fbType)) {
395 case SQL_SHORT:
396 *reinterpret_cast<qint16 *>(data) = static_cast<qint16>(iv);
397 break;
398 case SQL_LONG:
399 *reinterpret_cast<qint32 *>(data) = static_cast<qint32>(iv);
400 break;
401 default:
402 *reinterpret_cast<qint64 *>(data) = iv;
403 break;
404 }
405 return true;
406}
407
408void QFirebirdResultPrivate::writeInlineBlob(ISC_QUAD &blobId, const QByteArray &blobData)
409{
410 /* Create the blob via IAttachment. On failure the handle is released and the
411 exception propagates to exec()/execBatch, which report it — we never bind
412 SQL NULL in place of the caller's data. */
413 ThrowStatusWrapper bst(status());
414 ITransaction *tr = ensureTransaction();
415 FbGuard<IBlob> blob(att()->createBlob(&bst, tr, &blobId, 0, nullptr), fbRelease<IBlob>);
416 const char *src = blobData.constData();
417 qsizetype remaining = blobData.size();
418 while (remaining > 0) {
419 const unsigned segSize = static_cast<unsigned>(
420 std::min(remaining, static_cast<qsizetype>(65535)));
421 blob->putSegment(&bst, segSize, reinterpret_cast<const void *>(src));
422 src += segSize;
423 remaining -= segSize;
424 }
425 /* A putSegment or close failure throws; FbGuard frees the handle on the way
426 out, so the caller's data is never silently replaced with SQL NULL. */
427 blob.closeWith([&](IBlob *b) { b->close(&bst); });
428}
429
431 ITransaction *newTr)
432{
433 if (!newTr || newTr == executedTr)
434 return;
435 /* Update whichever member actually held the executed transaction (it came
436 from ensureTransaction(), so it is exactly one of these), releasing the
437 old handle once and never touching the unrelated transaction — otherwise
438 the stale, engine-invalidated handle would be left for a later
439 double-release. */
440 if (executedTr == drv_d_func()->iTrans) {
441 drv_d_func()->iTrans->release();
442 drv_d_func()->iTrans = newTr;
443 } else if (executedTr == autoTrans) {
444 autoTrans->release();
445 autoTrans = newTr;
446 }
447}
448
449bool QFirebirdResultPrivate::fillInputBuffer(const QList<QVariant> &vals,
450 QString &errorText,
451 BlobWriter blobWriter)
452{
453 if (!inMeta)
454 return true;
455
456 const qsizetype count = inCols.size();
457
458 /* Reject under-binding instead of silently executing the missing
459 parameters as zero-filled NOT NULL values (inBuffer is pre-zeroed, so an
460 unwritten parameter would bind 0/''/epoch rather than fail). Matches the
461 parameter-mismatch error other Qt SQL drivers report. */
462 if (vals.size() < count) {
463 errorText = u"Parameter count mismatch: statement expects %1, %2 bound"_s
464 .arg(count).arg(vals.size());
465 return false;
466 }
467
468 for (qsizetype i = 0; i < count; ++i) {
469 const ColumnInfo &ci = inCols.at(i);
470 short *nullFlag = reinterpret_cast<short *>(inBuffer.data() + ci.nullOffset);
471 const QVariant &val = vals[i];
472
473 /* Decide whether the value is "null". In Qt 6, a QVariant holding a
474 default-constructed value (e.g. QVariant(QString()), QVariant(QDate()))
475 reports isNull()==false even though the contained value is null/invalid,
476 so check the common types explicitly. */
477 bool valueIsNull = val.isNull();
478 if (!valueIsNull) {
479 switch (val.metaType().id()) {
480 case QMetaType::QString: valueIsNull = val.toString().isNull(); break;
481 case QMetaType::QByteArray: valueIsNull = val.toByteArray().isNull(); break;
482 case QMetaType::QDateTime: valueIsNull = !val.toDateTime().isValid(); break;
483 case QMetaType::QDate: valueIsNull = !val.toDate().isValid(); break;
484 case QMetaType::QTime: valueIsNull = !val.toTime().isValid(); break;
485 default: break;
486 }
487 }
488
489 /* Bind SQL NULL whenever the value is null, regardless of whether the
490 target column is nullable. For a NOT NULL column this lets the engine
491 raise its own constraint violation (surfaced as a QSqlError) instead
492 of silently coercing the null to a default (0/''/epoch) and inserting
493 a bogus row. The legacy QIBASE driver substituted the default here,
494 which masked the constraint — see QTBUG-114683. */
495 if (valueIsNull) {
496 *nullFlag = -1; // SQL NULL
497 continue;
498 }
499 *nullFlag = 0;
500
501 char *data = inBuffer.data() + ci.offset;
502
503 if (ci.scale != 0) {
504 QString numError;
505 if (!encodeScaledNumeric(data, ci.fbType, ci.scale, val, numError)) {
506 errorText = u"Parameter %1: %2"_s.arg(QString::number(i + 1), numError);
507 return false;
508 }
509 continue;
510 }
511
512 switch (fbBaseType(ci.fbType)) {
513 case SQL_SHORT:
514 *reinterpret_cast<qint16 *>(data) = static_cast<qint16>(val.toInt());
515 break;
516 case SQL_LONG:
517 *reinterpret_cast<qint32 *>(data) = static_cast<qint32>(val.toInt());
518 break;
519 case SQL_INT64:
520 *reinterpret_cast<qint64 *>(data) = val.toLongLong();
521 break;
522 case SQL_INT128: {
523 // Accept string or numeric; use IInt128::fromString
524 const QByteArray strVal = numericInputString(val);
525 ThrowStatusWrapper innerSt(status());
526 master()->getUtilInterface()->getInt128(&innerSt)->fromString(
527 &innerSt, 0, strVal.constData(),
528 reinterpret_cast<FB_I128 *>(data));
529 break;
530 }
531 case SQL_FLOAT:
532 *reinterpret_cast<float *>(data) = static_cast<float>(val.toDouble());
533 break;
534 case SQL_DOUBLE:
535 *reinterpret_cast<double *>(data) = val.toDouble();
536 break;
537 case SQL_DEC16: {
538 ThrowStatusWrapper innerSt(status());
539 decFloatFromString(master()->getUtilInterface()->getDecFloat16(&innerSt), innerSt,
540 numericInputString(val), reinterpret_cast<FB_DEC16 *>(data));
541 break;
542 }
543 case SQL_DEC34: {
544 ThrowStatusWrapper innerSt(status());
545 decFloatFromString(master()->getUtilInterface()->getDecFloat34(&innerSt), innerSt,
546 numericInputString(val), reinterpret_cast<FB_DEC34 *>(data));
547 break;
548 }
549 case SQL_BOOLEAN:
550 *reinterpret_cast<FB_BOOLEAN *>(data) =
551 val.toBool() ? FB_TRUE : FB_FALSE;
552 break;
553 case SQL_TYPE_DATE:
554 *reinterpret_cast<ISC_DATE *>(data) =
555 encodeQDate(master()->getUtilInterface(), val.toDate());
556 break;
557 case SQL_TYPE_TIME:
558 *reinterpret_cast<ISC_TIME *>(data) =
559 encodeQTime(master()->getUtilInterface(), val.toTime());
560 break;
561 case SQL_TIMESTAMP:
562 *reinterpret_cast<ISC_TIMESTAMP *>(data) =
563 encodeQDateTime(master()->getUtilInterface(), val.toDateTime());
564 break;
565 case SQL_TIMESTAMP_TZ:
566 *reinterpret_cast<ISC_TIMESTAMP_TZ *>(data) =
567 encodeQDateTimeTz(status(), master()->getUtilInterface(), val.toDateTime());
568 break;
569 case SQL_TIME_TZ:
570 *reinterpret_cast<ISC_TIME_TZ *>(data) =
571 encodeQTimeTz(status(), master()->getUtilInterface(), val.toDateTime());
572 break;
573 case SQL_TEXT:
574 case SQL_VARYING:
575 encodeTextValue(data, ci.fbType, ci.length, val.toString().toUtf8());
576 break;
577 case SQL_BLOB: {
578 const QByteArray blobData = val.toByteArray();
579 ISC_QUAD &blobId = *reinterpret_cast<ISC_QUAD *>(data);
580 if (blobWriter) {
581 /* Batch mode: delegate blob creation to the caller (IBatch::addBlob).
582 A failure must abort the statement, not silently bind SQL NULL. */
583 if (!blobWriter(blobId, blobData)) {
584 errorText = u"Failed to write BLOB for parameter %1"_s.arg(i + 1);
585 return false;
586 }
587 } else {
588 writeInlineBlob(blobId, blobData);
589 }
590 break;
591 }
592 case SQL_ARRAY: {
593 if (val.typeId() != QMetaType::QVariantList) {
594 errorText = u"Cannot bind non-list value to ARRAY parameter %1"_s.arg(i + 1);
595 return false;
596 }
597 ISC_QUAD &arrId = *reinterpret_cast<ISC_QUAD *>(data);
598 ITransaction *tr = ensureTransaction();
599 if (!writeArray(att(), tr, status(), master(), arrayDescCache,
600 &arrId, ci.relation, ci.field, val.toList())) {
601 errorText = u"Failed to write ARRAY parameter %1"_s.arg(i + 1);
602 return false;
603 }
604 break;
605 }
606 default:
607 break;
608 }
609 }
610 return true;
611}
612
613/*! \internal
614 After an EXECUTE PROCEDURE, copy the procedure's output columns back into the
615 bound QSql::Out / QSql::InOut parameters, mapped positionally in declaration
616 order (output column 0 -> first OUT param, and so on). The single-row result
617 set remains available via value(); this just additionally lets callers read
618 the outputs through QSqlQuery::boundValue(). Bind the OUT parameters after
619 the procedure's IN arguments.
620*/
621void QFirebirdResult::writeOutValues()
622{
623 Q_D(QFirebirdResult);
624 if (!d->isProcExec)
625 return;
626 const int columnCount = d->cols.size();
627 const int boundCount = boundValueCount();
628 int outCol = 0;
629 for (int p = 0; p < boundCount && outCol < columnCount; ++p) {
630 const QSql::ParamType type = bindValueType(p);
631 if (type.testFlag(QSql::Out)) {
632 bindValue(p, data(outCol), type);
633 ++outCol;
634 }
635 }
636}
637
639{
640 Q_D(QFirebirdResult);
641
642 /* Bare COMMIT/ROLLBACK: finish the active transaction via the API. A user
643 transaction (iTrans) takes priority; otherwise the auto-transaction. If
644 none is open (e.g. the previous statement already auto-committed) this is
645 a no-op success. The handle is reset so the next statement starts fresh. */
646 if (d->txnOp != TxnOp::None) {
647 ITransaction *&iTrans = d->drv_d_func()->iTrans;
648 ITransaction *tr = iTrans ? iTrans : d->autoTrans;
649 if (tr) {
650 try {
651 ThrowStatusWrapper st(d->status());
652 if (d->txnOp == TxnOp::Commit)
653 tr->commit(&st);
654 else
655 tr->rollback(&st);
656 } catch (const FbException &e) {
657 const QSqlError err = fbError(d->master(), e.getStatus(), QSqlError::TransactionError);
658 qCInfo(lcFirebird) << "exec(txn):" << fbErrorLog(err);
659 setLastError(err);
660 return false;
661 }
662 if (tr == iTrans)
663 iTrans = nullptr;
664 else
665 d->autoTrans = nullptr;
666 }
667 setActive(true);
668 setAt(QSql::AfterLastRow);
669 return true;
670 }
671
672 if (!d->stmt) {
673 setLastError(QSqlError(u"Statement not prepared"_s, QString(), QSqlError::StatementError));
674 return false;
675 }
676
677 /* Reject over-binding: more input values bound than the statement has
678 parameters. OUT-only parameters of a stored procedure are returned in the
679 output message and do not consume an input slot, so exclude them from the
680 count. (Under-binding is rejected in fillInputBuffer.) */
681 {
682 const qsizetype paramCount = d->inMeta ? d->inCols.size() : 0;
683 int inputCount = 0;
684 for (int p = 0, bc = boundValueCount(); p < bc; ++p) {
685 if (bindValueType(p).testFlag(QSql::In))
686 ++inputCount;
687 }
688 if (inputCount > paramCount) {
689 setLastError(QSqlError(
690 u"Parameter count mismatch: statement expects %1 input parameter(s), %2 bound"_s
691 .arg(paramCount).arg(inputCount),
692 QString(), QSqlError::StatementError));
693 return false;
694 }
695 }
696
697 // Close any open cursor from a previous execution
698 d->closeCursor();
699 d->affectedRows = -1;
700
701 try {
702 ThrowStatusWrapper st(d->status());
703 ITransaction *tr = d->ensureTransaction();
704 QString bindError;
705 if (!d->buildInputMessage(bindError)) {
706 setLastError(QSqlError(bindError, QString(), QSqlError::StatementError));
707 return false;
708 }
709
710 if (d->isSelect) {
711 d->cursor = d->stmt->openCursor(&st, tr,
712 d->inMeta,
713 d->inBuffer.isEmpty() ? nullptr : d->inBuffer.data(),
714 d->outMeta,
715 isForwardOnly() ? 0 : IStatement::CURSOR_TYPE_SCROLLABLE);
716 // Opt-in client-side caching applies only to scrollable SELECTs.
717 d->rowCache.clear();
718 d->cacheComplete = false;
719 d->useRowCache = d->drv_d_func()->cacheScrollableResults && !isForwardOnly();
720 setAt(QSql::BeforeFirstRow);
721 setActive(true);
722 } else {
723 d->isProcExec = false;
724 d->procRowFetched = false;
725 /* Zero the output buffer so stale null indicators from a previous
726 execution (e.g. a prior call that returned NULL for a field) don't
727 bleed through when Firebird only writes the value and not the flag. */
728 if (!d->outBuffer.isEmpty())
729 d->outBuffer.fill(0);
730 ITransaction *newTr = d->stmt->execute(&st, tr,
731 d->inMeta,
732 d->inBuffer.isEmpty() ? nullptr : d->inBuffer.data(),
733 d->outMeta,
734 d->outBuffer.isEmpty() ? nullptr : d->outBuffer.data());
735 /* execute() may return a transaction that replaces the one we ran
736 under; adopt it, releasing the replaced handle exactly once. */
737 d->adoptReplacementTransaction(tr, newTr);
738 d->affectedRows = QFirebirdResultPrivate::AffectedPending;
739 if (d->outMeta) {
740 /* EXECUTE PROCEDURE output: expose the filled buffer as a single
741 row (read via value()) and also copy it back to any bound
742 QSql::Out/InOut parameters (read via boundValue()). */
743 d->isProcExec = true;
744 setAt(QSql::BeforeFirstRow);
745 writeOutValues();
746 } else {
747 // Auto-commit non-SELECT statements that used an auto transaction
748 if (d->autoTrans && !d->drv_d_func()->iTrans)
749 finishAndClear(st, d->autoTrans, TxnEnd::Commit);
750 setAt(QSql::AfterLastRow);
751 }
752 setActive(true);
753 }
754 } catch (const FbException &e) {
755 const QSqlError err = fbError(d->master(), e.getStatus(), QSqlError::StatementError);
756 qCInfo(lcFirebird) << "exec:" << fbErrorLog(err);
757 setLastError(err);
758 return false;
759 }
760 return true;
761}
762
763bool QFirebirdResult::execBatch(bool arrayBind)
764{
765 Q_D(QFirebirdResult);
766
767 if (!d->stmt || !d->inMeta) {
768 // No prepared statement or no input parameters — fall back to default loop
769 return QSqlResult::execBatch(arrayBind);
770 }
771
772 const QList<QVariant> &batchValues = d->values;
773 if (batchValues.isEmpty()) {
774 setLastError(QSqlError(u"No values bound for batch execution"_s,
775 QString(), QSqlError::StatementError));
776 return false;
777 }
778
779 const qsizetype paramCount = batchValues.size();
780 const QVariantList firstList = batchValues.at(0).toList();
781 const qsizetype batchCount = firstList.size();
782 if (batchCount == 0)
783 return true;
784
785 // Close any open cursor from a previous execution
786 d->closeCursor();
787 d->affectedRows = -1;
788
789 IUtil *utl = d->master()->getUtilInterface();
790
791 try {
792 ThrowStatusWrapper st(d->status());
793 ITransaction *tr = d->ensureTransaction();
794
795 /* Build batch parameters block. The builder, batch and completion-state
796 handles are freed by FbGuard on every path (including the FbException
797 catch below), so no manual unwinding is needed. */
798 FbGuard<IXpbBuilder> pb(utl->getXpbBuilder(&st, IXpbBuilder::BATCH, nullptr, 0),
799 fbDispose<IXpbBuilder>);
800 pb->insertInt(&st, IBatch::TAG_RECORD_COUNTS, 1);
801
802 // Check if any parameter is a BLOB — enable inline blob IDs
803 bool hasBlobs = false;
804 for (const ColumnInfo &ci : std::as_const(d->inCols)) {
805 if (fbBaseType(ci.fbType) == SQL_BLOB) {
806 hasBlobs = true;
807 break;
808 }
809 }
810 if (hasBlobs)
811 pb->insertInt(&st, IBatch::TAG_BLOB_POLICY, IBatch::BLOB_ID_ENGINE);
812
813 /* Create batch from the prepared statement (pb is disposed by its guard
814 on scope exit; its buffer was already consumed by createBatch). */
815 FbGuard<IBatch> batch(d->stmt->createBatch(&st, d->inMeta,
816 pb->getBufferLength(&st), pb->getBuffer(&st)),
817 fbRelease<IBatch>);
818
819 /* Extract each parameter's value list once before the row loop —
820 toList() per (row, param) would convert the same QVariant for every
821 row of the batch. */
822 QVarLengthArray<QVariantList, 16> paramLists;
823 paramLists.reserve(paramCount);
824 for (qsizetype p = 0; p < paramCount; ++p)
825 paramLists.append(batchValues.at(p).toList());
826
827 for (qsizetype row = 0; row < batchCount; ++row) {
828 QList<QVariant> rowValues;
829 rowValues.reserve(paramCount);
830 for (qsizetype p = 0; p < paramCount; ++p) {
831 const QVariantList &col = paramLists[p];
832 rowValues.append(row < col.size() ? col.at(row) : QVariant());
833 }
834
835 d->inBuffer.fill(0);
836
837 // BlobWriter lambda for batch mode: uses IBatch::addBlob
838 auto blobWriter = [&](ISC_QUAD &blobId, const QByteArray &data) -> bool {
839 try {
840 batch->addBlob(&st, static_cast<unsigned>(data.size()),
841 data.constData(), &blobId, 0, nullptr);
842 return true;
843 } catch (const FbException &e) {
844 const auto berr = fbError(d->master(), e.getStatus(), QSqlError::StatementError);
845 qCWarning(lcFirebird) << "batch addBlob:" << fbErrorLog(berr);
846 return false;
847 }
848 };
849
850 QString bindError;
851 if (!d->fillInputBuffer(rowValues, bindError,
852 hasBlobs ? blobWriter : QFirebirdResultPrivate::BlobWriter{})) {
853 setLastError(QSqlError(bindError, QString(), QSqlError::StatementError));
854 return false; // batch guard releases the handle
855 }
856
857 batch->add(&st, 1, d->inBuffer.data());
858 }
859
860 FbGuard<IBatchCompletionState> cs(batch->execute(&st, tr),
861 fbDispose<IBatchCompletionState>);
862
863 unsigned total = cs->getSize(&st);
864 int totalAffected = 0;
865 bool hadError = false;
866 for (unsigned p = 0; p < total; ++p) {
867 int state = cs->getState(&st, p);
868 if (state == IBatchCompletionState::EXECUTE_FAILED) {
869 hadError = true;
870 } else if (state != IBatchCompletionState::SUCCESS_NO_INFO) {
871 totalAffected += state;
872 } else {
873 // SUCCESS_NO_INFO: count as 1 affected row
874 totalAffected += 1;
875 }
876 }
877 d->affectedRows = totalAffected;
878
879 if (hadError) {
880 unsigned errPos = cs->findError(&st, 0);
881 if (errPos != IBatchCompletionState::NO_MORE_ERRORS) {
882 IStatus *errStatus = d->master()->getStatus();
883 try {
884 cs->getStatus(&st, errStatus, errPos);
885 setLastError(fbError(d->master(), errStatus, QSqlError::StatementError));
886 } catch (...) {
887 setLastError(QSqlError(u"Batch execution failed at message %1"_s
888 .arg(errPos),
889 QString(), QSqlError::StatementError));
890 }
891 errStatus->dispose();
892 } else {
893 /* EXECUTE_FAILED was reported but findError returned no
894 position — should not happen, but never fail silently. */
895 setLastError(QSqlError(u"Batch execution failed"_s,
896 QString(), QSqlError::StatementError));
897 }
898 }
899
900 /* Close the batch gracefully (its guard is dismissed on success, releases
901 on a failed close); the completion state is disposed by its guard on
902 scope exit. */
903 batch.closeWith([&](IBatch *b) { b->close(&st); });
904
905 // Auto-commit if using an auto-transaction
906 if (d->autoTrans && !d->drv_d_func()->iTrans)
907 finishAndClear(st, d->autoTrans, TxnEnd::Commit);
908
909 setAt(QSql::AfterLastRow);
910 setActive(true);
911 return !hadError;
912
913 } catch (const FbException &e) {
914 const QSqlError err = fbError(d->master(), e.getStatus(), QSqlError::StatementError);
915 qCInfo(lcFirebird) << "execBatch:" << fbErrorLog(err);
916 setLastError(err);
917 return false;
918 }
919}
920
921bool QFirebirdResult::reset(const QString &query)
922{
923 if (!prepare(query))
924 return false;
925 return exec();
926}
927
928// ---------- Fetch helpers ----------
929
930/*! \internal
931 Largest non-negative row-position sentinel. Two uses, same value:
932 - passed to ensureCachedRow() to force the cache to pull every remaining row;
933 - the at() position set after fetchLast() on a server-side scrollable cursor,
934 which exposes no row count so the true last index is unknown (QSqlQuery
935 only needs a valid non-negative position; the row data comes from outBuffer).
936*/
937static constexpr int kMaxRowSentinel = 0x7FFFFFFE;
938
939bool QFirebirdResult::fetchCached(int target)
940{
941 Q_D(QFirebirdResult);
942 if (target < 0)
943 return false;
944 try {
945 if (d->ensureCachedRow(target)) {
946 /* Restore the snapshot so data()/isNull() read it unchanged. QByteArray
947 assignment is a cheap COW share, not a copy. */
948 d->outBuffer = d->rowCache.at(target);
949 setAt(target);
950 return true;
951 }
952 } catch (const FbException &e) {
953 setLastError(fbError(d->master(), e.getStatus(), QSqlError::StatementError));
954 }
955 return false;
956}
957
959{
960 Q_D(QFirebirdResult);
961 if (d->isProcExec) {
962 if (d->procRowFetched)
963 return false;
964 d->procRowFetched = true;
965 setAt(0);
966 return true;
967 }
968 if (!d->cursor)
969 return false;
970 if (d->useRowCache)
971 return fetchCached(at() == QSql::BeforeFirstRow ? 0 : at() + 1);
972 try {
973 ThrowStatusWrapper st(d->status());
974 int code = d->cursor->fetchNext(&st, d->outBuffer.data());
975 if (code == IStatus::RESULT_OK) {
976 setAt(at() == QSql::BeforeFirstRow ? 0 : at() + 1);
977 return true;
978 }
979 } catch (const FbException &e) {
980 setLastError(fbError(d->master(), e.getStatus(), QSqlError::StatementError));
981 }
982 return false;
983}
984
986{
987 Q_D(QFirebirdResult);
988 if (d->isProcExec) {
989 if (d->procRowFetched)
990 return false;
991 d->procRowFetched = true;
992 setAt(0);
993 return true;
994 }
995 if (!d->cursor)
996 return false;
997 if (d->useRowCache)
998 return fetchCached(0);
999 try {
1000 ThrowStatusWrapper st(d->status());
1001 int code;
1002 if (isForwardOnly())
1003 code = d->cursor->fetchNext(&st, d->outBuffer.data());
1004 else
1005 code = d->cursor->fetchFirst(&st, d->outBuffer.data());
1006 if (code == IStatus::RESULT_OK) {
1007 setAt(0);
1008 return true;
1009 }
1010 } catch (const FbException &e) {
1011 setLastError(fbError(d->master(), e.getStatus(), QSqlError::StatementError));
1012 }
1013 return false;
1014}
1015
1017{
1018 Q_D(QFirebirdResult);
1019 if (d->isProcExec) {
1020 // EXECUTE PROCEDURE exposes a single output row at index 0.
1021 d->procRowFetched = true;
1022 setAt(0);
1023 return true;
1024 }
1025 if (!d->cursor)
1026 return false;
1027 if (d->useRowCache) {
1028 /* Pull the whole set, then position on the real last index. Unlike the
1029 server-cursor path below, cache mode therefore gives last() a genuine
1030 absolute at() (and lets size() report a row count). */
1031 try {
1032 d->ensureCachedRow(kMaxRowSentinel);
1033 } catch (const FbException &e) {
1034 setLastError(fbError(d->master(), e.getStatus(), QSqlError::StatementError));
1035 return false;
1036 }
1037 if (d->rowCache.isEmpty())
1038 return false;
1039 return fetchCached(int(d->rowCache.size()) - 1);
1040 }
1041 if (isForwardOnly()) {
1042 /* Forward-only cursors cannot jump to the end, so walk there with
1043 fetchNext(): the last successful fetch leaves the final row in the
1044 buffer with at() at its index. Mirrors QSqlCachedResult. */
1045 if (at() == QSql::AfterLastRow)
1046 return false;
1047 if (!fetchNext())
1048 return false; // empty result set (or already exhausted)
1049 while (fetchNext())
1050 ;
1051 return true;
1052 }
1053 try {
1054 ThrowStatusWrapper st(d->status());
1055 int code = d->cursor->fetchLast(&st, d->outBuffer.data());
1056 if (code == IStatus::RESULT_OK) {
1057 /* The last row's absolute index is unknown (see kMaxRowSentinel), so
1058 use the sentinel as the position: QSqlQuery::value() treats it as
1059 valid and the row data (read from outBuffer) is correct. at() is
1060 therefore NOT a meaningful absolute index after last(); relative
1061 navigation from here (previous()) still returns correct row data
1062 but at() stays sentinel-relative. Use forward iteration from
1063 first()/next() if a true row index is required. */
1064 setAt(kMaxRowSentinel);
1065 return true;
1066 }
1067 } catch (const FbException &e) {
1068 setLastError(fbError(d->master(), e.getStatus(), QSqlError::StatementError));
1069 }
1070 return false;
1071}
1072
1074{
1075 Q_D(QFirebirdResult);
1076 if (d->isProcExec)
1077 return false; // single-row proc result: nothing precedes the one row
1078 if (!d->cursor)
1079 return false;
1080 if (d->useRowCache) {
1081 if (at() <= 0) {
1082 setAt(QSql::BeforeFirstRow);
1083 return false;
1084 }
1085 return fetchCached(at() - 1);
1086 }
1087 if (isForwardOnly())
1088 return false; // cannot move backwards on a forward-only cursor
1089 try {
1090 ThrowStatusWrapper st(d->status());
1091 int code = d->cursor->fetchPrior(&st, d->outBuffer.data());
1092 if (code == IStatus::RESULT_OK) {
1093 /* The cursor moves correctly and the row data is valid; at() is
1094 decremented from the previous position. After last() the previous
1095 position is the sentinel (see fetchLast), so at() stays
1096 sentinel-relative rather than a true absolute index. */
1097 setAt(at() > 0 ? at() - 1 : QSql::BeforeFirstRow);
1098 return true;
1099 }
1100 } catch (const FbException &e) {
1101 setLastError(fbError(d->master(), e.getStatus(), QSqlError::StatementError));
1102 }
1103 return false;
1104}
1105
1107{
1108 Q_D(QFirebirdResult);
1109 if (i < 0)
1110 return false;
1111 if (d->isProcExec) {
1112 /* EXECUTE PROCEDURE exposes a single output row at index 0, so only
1113 seek(0) is valid — this makes QSqlQuery::seek()/first() work on
1114 procedure results, not just next(). */
1115 if (i != 0)
1116 return false;
1117 d->procRowFetched = true;
1118 setAt(0);
1119 return true;
1120 }
1121 if (!d->cursor)
1122 return false;
1123 if (d->useRowCache)
1124 return fetchCached(i);
1125 if (isForwardOnly()) {
1126 /* Forward-only cursors have no random access, but absolute *forward*
1127 seeks can be emulated by iterating with fetchNext(). Backward seeks
1128 are rejected by QSqlQuery before reaching here. Mirrors the
1129 forward-only behaviour of QSqlCachedResult. */
1130 if (i < at())
1131 return false;
1132 while (at() < i) {
1133 if (!fetchNext())
1134 return false;
1135 }
1136 return true;
1137 }
1138 // IResultSet supports absolute positioning
1139 try {
1140 ThrowStatusWrapper st(d->status());
1141 int code = d->cursor->fetchAbsolute(&st, i + 1, d->outBuffer.data());
1142 if (code == IStatus::RESULT_OK) {
1143 setAt(i);
1144 return true;
1145 }
1146 } catch (const FbException &e) {
1147 setLastError(fbError(d->master(), e.getStatus(), QSqlError::StatementError));
1148 }
1149 return false;
1150}
1151
1152// ---------- Data access ----------
1153
1154bool QFirebirdResult::isNull(int field)
1155{
1156 Q_D(const QFirebirdResult);
1157 if (field < 0 || field >= d->cols.size())
1158 return true;
1159 const ColumnInfo &ci = d->cols.at(field);
1160 const short *nullFlag =
1161 reinterpret_cast<const short *>(d->outBuffer.constData() + ci.nullOffset);
1162 qCDebug(lcFirebird, "isNull(%d): nullOffset=%u nullFlag=%d", field, ci.nullOffset, int(*nullFlag));
1163 return *nullFlag != 0;
1164}
1165
1167{
1168 Q_D(const QFirebirdResult);
1169
1170 if (field < 0 || field >= d->cols.size()) {
1171 qCDebug(lcFirebird, "data(%d): out of range, cols.size()=%d", field, int(d->cols.size()));
1172 return {};
1173 }
1174
1175 const ColumnInfo &ci = d->cols.at(field);
1176
1177 /* SQL NULL: return a valid but null QVariant of the correct type.
1178 Returning {} (invalid QVariant) would violate Qt SQL conventions and
1179 break code that checks QVariant::isValid() to detect present-but-null values. */
1180 if (isNull(field)) {
1181 qCDebug(lcFirebird, "data(%d): SQL NULL, fbType=%d qtType=%d", field, ci.fbType, int(ci.qtType));
1182 return QVariant(QMetaType(ci.qtType));
1183 }
1184
1185 qCDebug(lcFirebird, "data(%d): fbType=%d qtType=%d", field, ci.fbType, int(ci.qtType));
1186 const char *ptr = d->outBuffer.constData() + ci.offset;
1187
1188 /* The Firebird OO-API conversions below (IInt128/IDecFloat, BLOB and array
1189 reads) use ThrowStatusWrapper, so any conversion or fetch failure raises
1190 FbException. Catch it here and surface a QSqlError instead of returning a
1191 silently corrupt or truncated value. */
1192 try {
1193 // Scaled numerics — honour numericalPrecisionPolicy (any non-zero scale)
1194 if (ci.scale != 0) {
1195 const auto policy = numericalPrecisionPolicy();
1196 switch (fbBaseType(ci.fbType)) {
1197 case SQL_SHORT: return applyScale(*reinterpret_cast<const qint16 *>(ptr), ci.scale, policy);
1198 case SQL_LONG: return applyScale(*reinterpret_cast<const qint32 *>(ptr), ci.scale, policy);
1199 case SQL_INT128: {
1200 // Use Firebird's IInt128 interface to format, then apply policy
1201 ThrowStatusWrapper st(d->status());
1202 const QString str = int128ToString(d->master()->getUtilInterface(), st,
1203 reinterpret_cast<const FB_I128 *>(ptr),
1204 ci.scale);
1205 if (policy == QSql::HighPrecision)
1206 return str;
1207 if (policy == QSql::LowPrecisionDouble)
1208 return QVariant(str.toDouble());
1209 /* LowPrecisionInt32/Int64: take the integer part straight from
1210 the decimal string (truncating toward zero) rather than via
1211 double, so 128-bit values keep full integer precision up to
1212 the target type's range — a double round-trip would drop bits
1213 above 2^53. */
1214 const qsizetype dot = str.indexOf(u'.');
1215 const QStringView intPart =
1216 dot < 0 ? QStringView(str) : QStringView(str).first(dot);
1217 if (policy == QSql::LowPrecisionInt32)
1218 return QVariant(intPart.toInt());
1219 return QVariant(intPart.toLongLong());
1220 }
1221 default: return applyScale(*reinterpret_cast<const qint64 *>(ptr), ci.scale, policy);
1222 }
1223 }
1224
1225 switch (fbBaseType(ci.fbType)) {
1226 case SQL_SHORT:
1227 return static_cast<int>(*reinterpret_cast<const qint16 *>(ptr));
1228 case SQL_LONG:
1229 return static_cast<int>(*reinterpret_cast<const qint32 *>(ptr));
1230 case SQL_INT64:
1231 return *reinterpret_cast<const qint64 *>(ptr);
1232 case SQL_INT128: {
1233 // 128-bit integer: use Firebird's IInt128 to convert to decimal string
1234 ThrowStatusWrapper st(d->status());
1235 return int128ToString(d->master()->getUtilInterface(), st,
1236 reinterpret_cast<const FB_I128 *>(ptr), 0);
1237 }
1238 case SQL_FLOAT:
1239 return static_cast<double>(*reinterpret_cast<const float *>(ptr));
1240 case SQL_DOUBLE:
1241 return *reinterpret_cast<const double *>(ptr);
1242 case SQL_DEC16: {
1243 ThrowStatusWrapper st(d->status());
1244 return decFloatToString(d->master()->getUtilInterface()->getDecFloat16(&st), st,
1245 reinterpret_cast<const FB_DEC16 *>(ptr));
1246 }
1247 case SQL_DEC34: {
1248 ThrowStatusWrapper st(d->status());
1249 return decFloatToString(d->master()->getUtilInterface()->getDecFloat34(&st), st,
1250 reinterpret_cast<const FB_DEC34 *>(ptr));
1251 }
1252 case SQL_BOOLEAN:
1253 return *reinterpret_cast<const FB_BOOLEAN *>(ptr) != FB_FALSE;
1254 case SQL_TYPE_DATE:
1255 return decodeFirebirdDate(d->master()->getUtilInterface(),
1256 *reinterpret_cast<const ISC_DATE *>(ptr));
1257 case SQL_TYPE_TIME:
1258 return decodeFirebirdTime(d->master()->getUtilInterface(),
1259 *reinterpret_cast<const ISC_TIME *>(ptr));
1260 case SQL_TIMESTAMP:
1261 return decodeFirebirdTimestamp(d->master()->getUtilInterface(),
1262 *reinterpret_cast<const ISC_TIMESTAMP *>(ptr));
1263 case SQL_TIMESTAMP_TZ:
1264 return decodeFirebirdTimestampTz(d->status(), d->master()->getUtilInterface(),
1265 *reinterpret_cast<const ISC_TIMESTAMP_TZ *>(ptr));
1266 case SQL_TIME_TZ:
1267 return decodeFirebirdTimeTz(d->status(), d->master()->getUtilInterface(),
1268 *reinterpret_cast<const ISC_TIME_TZ *>(ptr));
1269 case SQL_BLOB: {
1270 const ISC_QUAD &blobId = *reinterpret_cast<const ISC_QUAD *>(ptr);
1271 QByteArray ba = readBlob(d->att(), d->activeTransaction(),
1272 d->status(), blobId);
1273 // TEXT blob (sub_type 1) → return as QString
1274 if (ci.fbSubType == 1)
1275 return QString::fromUtf8(ba);
1276 return ba;
1277 }
1278 case SQL_ARRAY: {
1279 const ISC_QUAD &arrayId = *reinterpret_cast<const ISC_QUAD *>(ptr);
1280 ITransaction *tra = d->activeTransaction();
1281 return fetchArray(d->att(), tra, d->status(), d->master(),
1282 d->arrayDescCache, arrayId, ci.relation, ci.field);
1283 }
1284 case SQL_VARYING: {
1285 /* First 2 bytes are the length. Clamp it to the field's data
1286 capacity (byte length minus the 2-byte prefix) so a corrupt or
1287 oversized length indicator cannot read past the row buffer. */
1288 const unsigned short len = *reinterpret_cast<const unsigned short *>(ptr);
1289 const qsizetype maxLen =
1290 std::max(qsizetype(0), static_cast<qsizetype>(ci.length) - 2);
1291 return QString::fromUtf8(ptr + 2, std::min(static_cast<qsizetype>(len), maxLen));
1292 }
1293 case SQL_TEXT: {
1294 /* ci.length is the byte length cached from the output metadata in
1295 buildColumns(); reuse it instead of a per-row getLength() call. */
1296 QString s = QString::fromUtf8(ptr, static_cast<qsizetype>(ci.length));
1297 while (s.endsWith(u' '))
1298 s.chop(1);
1299 return s;
1300 }
1301 default:
1302 qCWarning(lcFirebird, "data: unhandled Firebird type %d for field %d", ci.fbType, field);
1303 return QString::fromUtf8(ptr);
1304 }
1305 } catch (const FbException &e) {
1306 setLastError(fbError(d->master(), e.getStatus(), QSqlError::StatementError));
1307 return {};
1308 }
1309}
1310
1312{
1313 Q_D(const QFirebirdResult);
1314 if (d->recordCached)
1315 return d->cachedRecord;
1316 QSqlRecord rec;
1317 for (const ColumnInfo &ci : d->cols) {
1318 /* Pass the source relation as the field's table name so table-qualified
1319 lookups (QSqlRecord::indexOf("TABLE.COLUMN")) can disambiguate columns
1320 that share a name across joined tables — matching the QIBASE driver. */
1321 QSqlField f(ci.name, QMetaType(ci.qtType), ci.relation);
1322 const int base = fbBaseType(ci.fbType);
1323 if (base == SQL_TEXT || base == SQL_VARYING) {
1324 /* ci.length is the UTF-8 byte length (the connection charset is
1325 UTF-8); report the declared character length instead. Integer
1326 division also absorbs the 2-byte VARYING prefix. */
1327 f.setLength(static_cast<int>(ci.length) / 4);
1328 } else {
1329 f.setLength(static_cast<int>(ci.length));
1330 }
1331 /* Precision (fractional digits) only applies to scaled numeric types;
1332 leave it unset (-1) for everything else. */
1333 if (ci.scale != 0)
1334 f.setPrecision(qAbs(ci.scale));
1335 f.setRequiredStatus(ci.nullable ? QSqlField::Optional : QSqlField::Required);
1336 rec.append(f);
1337 }
1338 d->cachedRecord = rec;
1339 d->recordCached = true;
1340 return rec;
1341}
1342
1344{
1345 Q_D(QFirebirdResult);
1346 /* The row count is only available in cache mode (FB_SCROLLABLE_CACHE=1),
1347 where the result buffers its rows client-side anyway: pull the remaining
1348 rows into the cache and report its size. Firebird cursors cannot report
1349 cardinality without fetching, so every other path returns -1. */
1350 if (!d->useRowCache)
1351 return -1;
1352 if (!d->cacheComplete) {
1353 if (!d->cursor) // e.g. after finish() released the cursor
1354 return -1;
1355 try {
1356 d->ensureCachedRow((std::numeric_limits<int>::max)());
1357 } catch (const FbException &e) {
1358 const QSqlError err = fbError(d->master(), e.getStatus(), QSqlError::StatementError);
1359 qCInfo(lcFirebird) << "size:" << fbErrorLog(err);
1360 setLastError(err);
1361 return -1;
1362 }
1363 }
1364 return int(d->rowCache.size());
1365}
1366
1368{
1369 Q_D(QFirebirdResult);
1370 if (d->affectedRows == QFirebirdResultPrivate::AffectedPending) {
1371 d->affectedRows = -1;
1372 if (d->stmt) {
1373 try {
1374 ThrowStatusWrapper st(d->status());
1375 d->affectedRows = static_cast<int>(d->stmt->getAffectedRecords(&st));
1376 } catch (const FbException &e) {
1377 qCInfo(lcFirebird) << "numRowsAffected:"
1378 << fbErrorLog(fbError(d->master(), e.getStatus(),
1379 QSqlError::StatementError));
1380 d->status()->init();
1381 }
1382 }
1383 }
1384 return d->affectedRows;
1385}
1386
1387QT_END_NAMESPACE
QString formatFbError(const QString &ctx)
void adoptReplacementTransaction(Firebird::ITransaction *executedTr, Firebird::ITransaction *newTr)
bool encodeScaledNumeric(char *data, int fbType, int scale, const QVariant &val, QString &errorText)
bool fillInputBuffer(const QList< QVariant > &vals, QString &errorText, BlobWriter blobWriter={})
Firebird::ITransaction * ensureTransaction()
bool buildInputMessage(QString &errorText)
void writeInlineBlob(ISC_QUAD &blobId, const QByteArray &blobData)
bool execBatch(bool arrayBind=false) override
bool fetchNext() override
Positions the result to the next available record (row) in the result.
bool fetchLast() override
Positions the result to the last record (last row) in the result.
QFirebirdResult(const QFirebirdDriver *driver)
bool isNull(int field) override
Returns true if the field at position index in the current row is null; otherwise returns false.
int numRowsAffected() override
Returns the number of rows affected by the last query executed, or -1 if it cannot be determined or i...
void detachFromResultSet() override
bool fetchFirst() override
Positions the result to the first record (row 0) in the result.
QSqlRecord record() const override
Returns the current record if the query is active; otherwise returns an empty QSqlRecord.
bool fetchPrevious() override
Positions the result to the previous record (row) in the result.
bool fetch(int i) override
Positions the result to an arbitrary (zero-based) row index.
int size() override
Returns the size of the SELECT result, or -1 if it cannot be determined or if the query is not a SELE...
bool exec() override
Executes the query, returning true if successful; otherwise returns false.
bool reset(const QString &query) override
Sets the result to use the SQL statement query for subsequent data retrieval.
QVariant data(int field) override
Returns the data for field index in the current row as a QVariant.
QVariant handle() const override
Returns the low-level database handle for this result set wrapped in a QVariant or an invalid QVarian...
bool prepare(const QString &query) override
Prepares the given query for execution; the query will normally use placeholders so that it can be ex...
int fbBaseType(int t)
static TxnOp classifyTxnControl(const QString &query)
static QByteArray readBlob(IAttachment *att, ITransaction *tra, IStatus *st, const ISC_QUAD &blobId)
static constexpr int kMaxRowSentinel
static QList< ColumnInfo > buildColumns(IMessageMetadata *meta, ThrowStatusWrapper &st)