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