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.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
10
11#include <QtCore/qdatetime.h>
12#include <QtCore/qloggingcategory.h>
13#include <QtCore/qvariant.h>
14#include <QtSql/qsqlfield.h>
15#include <QtSql/qsqlindex.h>
16#include <QtSql/qsqlquery.h>
17#include <QtSql/qsqlrecord.h>
18
19#include <ibase.h> // DPB/TPB constants, isc_event_block
20#include <firebird/Interface.h>
21
22#include <algorithm>
23#include <atomic>
24#include <cstring>
25#include <mutex>
26
27/* The driver uses the Firebird 4.0 object-oriented C++ API. Configure-time
28 detection (FindFirebird.cmake with Firebird_MINIMUM_API_VERSION) already
29 disables the sql-firebird feature for older clients; this static_assert is a
30 backstop for out-of-tree builds that bypass configure. */
31static_assert(FB_API_VER >= 40,
32 "The QFIREBIRD driver requires Firebird 4.0 or later client libraries.");
33
34/* Error handling is built on the API's ThrowStatusWrapper/FbException model,
35 so the driver cannot be compiled without exceptions. Configure-time detection
36 already disables the sql-firebird feature in a -no-exceptions build; this is
37 the backstop for out-of-tree builds that bypass configure. */
38#ifdef QT_NO_EXCEPTIONS
39# error The QFIREBIRD driver requires C++ exception support
40#endif
41
42QT_BEGIN_NAMESPACE
43
44// Firebird OO API types are used throughout; avoid full Firebird:: qualification.
45using namespace Firebird;
46
47using namespace Qt::StringLiterals;
48
49/*! \internal
50 Event notification support (OO API)
51
52 Follows the pattern from Firebird's examples/interfaces/08.events.cpp:
53 - The subscription IS the callback (IEventCallback), ref-counted
54 - eventCallbackFunction (Firebird thread) memcpy's result + posts; it holds
55 a self-reference for its duration and a mutex around the buffer write and
56 the driver-pointer use (stop() clears the pointer under the same mutex,
57 so a returned stop() guarantees no callback still touches the driver)
58 - isc_event_counts and re-registration happen on the main thread; the read
59 takes the same mutex (re-arm does not — see qHandleEventNotification)
60 - Only release(), never cancel(), is used on IEvents
61*/
62
65{
66public:
68 const QString &eventName)
70 {
71 bufferLength = static_cast<unsigned>(
72 isc_event_block(&eventBuffer, &resultBuffer,
73 1, eventName.toUtf8().constData()));
74 }
75
76 // IRefCounted
77 void addRef() override { ++refCount; }
79 if (--refCount == 0) {
80 delete this;
81 return 0;
82 }
83 return 1;
84 }
85
86 /*! \internal
87 IEventCallback — called on Firebird's internal event thread.
88 Holds a self-reference for the duration so the owner thread's stop()/
89 release() cannot delete the subscription (and free resultBuffer) while
90 this callback is still running. The mutex serialises the resultBuffer
91 write against qHandleEventNotification's read on the owner thread, and
92 covers the driver-pointer use: stop() clears the pointer under the same
93 mutex, so the driver cannot be destroyed between the read and the
94 invokeMethod. The queued invokeMethod only posts an event — it neither
95 blocks nor re-enters Firebird — so holding the mutex across it cannot
96 deadlock with the re-arm lock-ordering constraint.
97 */
98 void eventCallbackFunction(unsigned length, const unsigned char *data) override
99 {
100 addRef();
101 {
102 std::lock_guard<std::mutex> lock(mutex);
103 /* Clamp to the registered buffer size: fbclient should never
104 deliver a longer block, but the length arrives off the wire. */
105 length = std::min(length, bufferLength);
106 if (length > 0)
107 memcpy(resultBuffer, data, length);
108 ++counter;
109 if (driver) {
110 QMetaObject::invokeMethod(driver, "qHandleEventNotification",
111 Qt::QueuedConnection, Q_ARG(QString, name));
112 }
113 }
114 release();
115 }
116
117 /*! \internal
118 Disconnect from the driver so the callback no longer posts to it,
119 and release the IEvents handle (no cancel — avoids Firebird internal
120 lock contention that causes crashes on Windows). Clearing the pointer
121 under the mutex waits out any in-flight callback that already loaded it,
122 so after stop() returns the driver can be destroyed safely.
123 */
124 void stop()
125 {
126 {
127 std::lock_guard<std::mutex> lock(mutex);
128 driver = nullptr;
129 }
130 if (events) {
131 events->release();
132 events = nullptr;
133 }
134 }
135
137 QFirebirdDriver *driver = nullptr; // guarded by mutex (written on the owner thread,
138 // read on the Firebird event thread)
140 IEvents *events = nullptr;
143 unsigned bufferLength = 0;
144 bool first = true;
145 std::atomic<int> counter = 0;
146 std::mutex mutex; // serialises resultBuffer access with the FB event thread
147
148private:
149 ~QFirebirdEventSubscription()
150 {
151 if (events)
152 events->release();
153 if (eventBuffer)
154 isc_free(reinterpret_cast<char *>(eventBuffer));
155 if (resultBuffer)
156 isc_free(reinterpret_cast<char *>(resultBuffer));
157 }
158
159 std::atomic<int> refCount = 0;
160};
161
162// QFirebirdDriverPrivate
163
164void QFirebirdDriverPrivate::setFbError(const QString &context, QSqlError::ErrorType type)
165{
166 Q_Q(QFirebirdDriver);
167 auto err = fbError(master, iStatus, type);
168 q->setLastError(QSqlError(context + u": " + err.databaseText(),
169 {}, err.type(), err.nativeErrorCode()));
170 iStatus->init();
171}
172
174{
175 Q_Q(QFirebirdDriver);
176 if (!iTrans) {
177 q->setLastError(QSqlError(u"No active transaction"_s, {},
178 QSqlError::TransactionError));
179 return false;
180 }
181 try {
182 ThrowStatusWrapper st(iStatus);
183 finishAndClear(st, iTrans, end);
184 } catch (const FbException &e) {
185 q->setLastError(fbError(master, e.getStatus(), QSqlError::TransactionError));
186 iStatus->init();
187 return false;
188 }
189 return true;
190}
191
192// QFirebirdDriver
193
198
200{
201 close();
202 /* Neutralise any results that outlive this driver: once the driver private
203 below is freed, their drv_d_func() would dangle. After this, cleanup()
204 on those results becomes a no-op (see QFirebirdResultPrivate::cleanup()). */
205 Q_D(QFirebirdDriver);
206 for (QFirebirdResultPrivate *r : std::as_const(d->activeResults))
207 r->driverAlive = false;
208 d->activeResults.clear();
209}
210
211bool QFirebirdDriver::hasFeature(DriverFeature feature) const
212{
213 Q_D(const QFirebirdDriver);
214 switch (feature) {
215 case Transactions:
216 case Unicode:
217 case BLOB:
218 case PreparedQueries:
219 case PositionalPlaceholders:
220 case LowPrecisionNumbers:
221 case EventNotifications:
222 case BatchOperations:
223 case FinishQuery: // QSqlQuery::finish() releases the cursor, keeps the prepare
224 case CancelQuery: // cancelQuery() raises fb_cancel_raise from another thread
225 return true;
226 case QuerySize:
227 /* Firebird cursors cannot report cardinality without fetching, so the
228 count is only available when the opt-in client-side row cache
229 (connect option FB_SCROLLABLE_CACHE=1) buffers the result set anyway
230 — see QFirebirdResult::size(). */
231 return d->cacheScrollableResults;
232 case NamedPlaceholders:
233 case LastInsertId:
234 case SimpleLocking:
235 case MultipleResultSets:
236 return false;
237 }
238 return false;
239}
240
241/*! \internal
242 Connection options recognized by open(), mapped to their DPB tag and value
243 kind. lc_ctype and the SQL dialect are intentionally NOT exposed: the driver
244 hardcodes UTF-8 and dialect 3 and decodes results on that basis. Integer
245 kinds also cover the boolean DPB options (pass 0/1).
246*/
247namespace {
248enum DpbValueKind : quint8 { DpbString, DpbInt };
249struct DpbOption {
250 QLatin1StringView key;
251 unsigned char tag;
252 DpbValueKind kind;
253};
254constexpr DpbOption dpbOptionTable[] = {
255 { "ISC_DPB_SQL_ROLE_NAME"_L1, isc_dpb_sql_role_name, DpbString },
256 { "ISC_DPB_SESSION_TIME_ZONE"_L1, isc_dpb_session_time_zone, DpbString },
257 { "ISC_DPB_NUM_BUFFERS"_L1, isc_dpb_num_buffers, DpbInt },
258 { "ISC_DPB_CONNECT_TIMEOUT"_L1, isc_dpb_connect_timeout, DpbInt },
259 { "ISC_DPB_DUMMY_PACKET_INTERVAL"_L1, isc_dpb_dummy_packet_interval, DpbInt },
260 { "ISC_DPB_NO_GARBAGE_COLLECT"_L1, isc_dpb_no_garbage_collect, DpbInt },
261 { "ISC_DPB_NO_DB_TRIGGERS"_L1, isc_dpb_no_db_triggers, DpbInt },
262};
263
264/*! \internal
265 Options that map to real DPB tags but which the driver sets itself and will
266 not let the caller override: the connection charset is fixed to UTF-8 and the
267 SQL dialect to 3, because results are decoded on that basis. Recognized here
268 so they get an accurate message instead of being reported as "unknown".
269*/
270constexpr QLatin1StringView driverManagedKeys[] = {
271 "ISC_DPB_LC_CTYPE"_L1,
272 "ISC_DPB_SQL_DIALECT"_L1,
273};
274} // namespace
275
276bool QFirebirdDriver::open(const QString &db,
277 const QString &user,
278 const QString &password,
279 const QString &host,
280 int port,
281 const QString &connOpts)
282{
283 Q_D(QFirebirdDriver);
284
285 close();
286
287 // Build the connection string: host/port:database (Firebird uses / between host and port)
288 QString connStr;
289 if (!host.isEmpty()) {
290 connStr = host;
291 if (port > 0)
292 connStr += u'/' + QString::number(port);
293 connStr += u':';
294 }
295 connStr += db;
296
297 // Build DPB using IXpbBuilder
298 IUtil *util = d->master->getUtilInterface();
299 ThrowStatusWrapper st(d->iStatus);
300 try {
301 // Disposed by its guard on scope exit (normal return or the catch below).
302 FbGuard<IXpbBuilder> dpb(util->getXpbBuilder(&st, IXpbBuilder::DPB, nullptr, 0),
303 fbDispose<IXpbBuilder>);
304
305 dpb->insertString(&st, isc_dpb_user_name,
306 user.toUtf8().constData());
307 dpb->insertString(&st, isc_dpb_password,
308 password.toUtf8().constData());
309 // UTF-8 connection charset
310 dpb->insertString(&st, isc_dpb_lc_ctype, "UTF8");
311
312 /* Apply recognized connection options (semicolon-separated key=value).
313 Unknown or malformed options are warned about rather than silently
314 ignored. */
315 for (const auto &opt : QStringView(connOpts).split(u';', Qt::SkipEmptyParts)) {
316 const auto kv = opt.trimmed();
317 const auto eqIdx = kv.indexOf(u'=');
318 if (eqIdx < 0) {
319 qCWarning(lcFirebird) << "QFirebirdDriver: ignoring malformed connection option"
320 << kv.toString();
321 continue;
322 }
323 const QString key = kv.left(eqIdx).trimmed().toString().toUpper();
324 const QString value = kv.mid(eqIdx + 1).trimmed().toString();
325
326 /* Driver-level option (not a DPB tag): enable client-side row
327 caching for scrollable results. Handled before the DPB lookup so
328 it isn't reported as "unknown". */
329 if (key == "FB_SCROLLABLE_CACHE"_L1) {
330 d->cacheScrollableResults = (value != "0"_L1)
331 && (value.compare("false"_L1, Qt::CaseInsensitive) != 0);
332 continue;
333 }
334
335 /* Driver-managed options: silently accept a value that matches what
336 the driver already enforces (e.g. ISC_DPB_LC_CTYPE=UTF8); warn
337 only when the caller asks for something the driver won't honor. */
338 bool managed = false;
339 for (const auto &k : driverManagedKeys) {
340 if (key == k) {
341 managed = true;
342 break;
343 }
344 }
345 if (managed) {
346 const bool isLcCtype = (key == "ISC_DPB_LC_CTYPE"_L1);
347 const bool matchesDefault = isLcCtype
348 ? (value.compare("UTF8"_L1, Qt::CaseInsensitive) == 0
349 || value.compare("UTF-8"_L1, Qt::CaseInsensitive) == 0)
350 : (value == "3"_L1);
351 if (!matchesDefault) {
352 qCWarning(lcFirebird) << "QFirebirdDriver: connection option" << key
353 << "is managed by the driver and cannot be overridden"
354 " (charset is fixed to UTF-8, dialect to 3); ignoring"
355 << value;
356 }
357 continue;
358 }
359
360 const DpbOption *match = nullptr;
361 for (const auto &o : dpbOptionTable) {
362 if (key == o.key) {
363 match = &o;
364 break;
365 }
366 }
367 if (!match) {
368 qCWarning(lcFirebird) << "QFirebirdDriver: ignoring unknown connection option" << key;
369 continue;
370 }
371 if (match->kind == DpbString) {
372 QByteArray s = value.toUtf8();
373 if (match->tag == isc_dpb_sql_role_name)
374 s.truncate(255);
375 dpb->insertString(&st, match->tag, s.constData());
376 } else {
377 bool ok = false;
378 const int n = value.toInt(&ok);
379 if (!ok) {
380 qCWarning(lcFirebird) << "QFirebirdDriver: non-integer value for connection option"
381 << key << ':' << value;
382 continue;
383 }
384 dpb->insertInt(&st, match->tag, n);
385 }
386 }
387
388 const unsigned char *dpbData = dpb->getBuffer(&st);
389 unsigned dpbLen = dpb->getBufferLength(&st);
390
391 IAttachment *att = d->master->getDispatcher()->attachDatabase(
392 &st, connStr.toUtf8().constData(), dpbLen, dpbData);
393 /* Publish under the mutex so a concurrent cancelQuery() never sees a
394 half-initialised attachment. */
395 {
396 const std::lock_guard<std::mutex> lock(d->attMutex);
397 d->iAtt = att;
398 }
399 } catch (const FbException &e) {
400 setLastError(fbError(d->master, e.getStatus(), QSqlError::ConnectionError));
401 d->iStatus->init();
402 setOpenError(true);
403 return false;
404 }
405
406 setOpen(true);
407 setOpenError(false);
408
409#ifdef QSQL_HAS_INT128
410 qCDebug(lcFirebird, "QFirebirdDriver: INT128 codec: native 128-bit integer");
411#else
412 qCDebug(lcFirebird, "QFirebirdDriver: INT128 codec: IInt128 string interface");
413#endif
414
415 return true;
416}
417
419{
420 Q_D(QFirebirdDriver);
421 if (!isOpen())
422 return;
423
424 CheckStatusWrapper st(d->iStatus);
425 // Cancel all event subscriptions before detaching
426 for (auto *sub : std::as_const(d->eventSubscriptions)) {
427 sub->stop();
428 sub->release();
429 }
430 d->eventSubscriptions.clear();
431 if (d->iTrans) {
432 try {
433 ThrowStatusWrapper tsw(d->iStatus);
434 d->iTrans->rollback(&tsw);
435 } catch (...) {
436 d->iTrans->release();
437 }
438 d->iTrans = nullptr;
439 }
440 if (d->iAtt) {
441 /* Withdraw the attachment from cancelQuery()'s reach before detaching:
442 once the lock is dropped, a concurrent cancelQuery() sees nullptr,
443 and one already inside cancelOperation() has finished (it holds the
444 mutex for the duration of the call). */
445 IAttachment *att;
446 {
447 const std::lock_guard<std::mutex> lock(d->attMutex);
448 att = d->iAtt;
449 d->iAtt = nullptr;
450 }
451 try {
452 ThrowStatusWrapper tsw(d->iStatus);
453 att->detach(&tsw);
454 } catch (...) {
455 att->release();
456 }
457 }
458 d->iStatus->init();
459 setOpen(false);
460 setOpenError(false);
461}
462
464{
465 return new QFirebirdResult(this);
466}
467
468/*! \internal
469 Cross-thread cancellation (QSqlDriver::CancelQuery). This runs on a
470 DIFFERENT thread than the one executing the query, so it must not touch any
471 driver state beyond the mutex-guarded attachment: no setLastError, and not
472 iStatus (the executing thread owns it) — a private status object is used
473 instead. fb_cancel_raise asks the engine to abort the current operation at
474 its next cancellation point; the executing call then fails with
475 isc_cancelled, which the normal FbException -> QSqlError path reports on the
476 executing thread. The attachment itself stays open and usable.
477*/
479{
480 Q_D(QFirebirdDriver);
481 const std::lock_guard<std::mutex> lock(d->attMutex);
482 if (!d->iAtt)
483 return false;
484 FbGuard<IStatus> status(d->master->getStatus(), fbDispose<IStatus>);
485 try {
486 ThrowStatusWrapper st(status.get());
487 d->iAtt->cancelOperation(&st, fb_cancel_raise);
488 } catch (const FbException &e) {
489 const auto err = fbError(d->master, e.getStatus(), QSqlError::UnknownError);
490 qCWarning(lcFirebird) << "cancelQuery:" << fbErrorLog(err);
491 return false;
492 }
493 return true;
494}
495
497{
498 Q_D(QFirebirdDriver);
499 /* QSqlDatabase::transaction() forwards here after only a feature check, so
500 guard against a closed (or failed-to-open) database: without it the
501 start call below would dereference a null attachment. */
502 if (!isOpen() || isOpenError()) {
503 setLastError(QSqlError(u"Database not open"_s, {},
504 QSqlError::TransactionError));
505 return false;
506 }
507 if (d->iTrans) {
508 setLastError(QSqlError(u"Transaction already active"_s, {},
509 QSqlError::TransactionError));
510 return false;
511 }
512 try {
513 ThrowStatusWrapper st(d->iStatus);
514 d->iTrans = startDefaultTransaction(d->iAtt, st, TxnWait::NoWait);
515 } catch (const FbException &e) {
516 setLastError(fbError(d->master, e.getStatus(), QSqlError::TransactionError));
517 d->iStatus->init();
518 return false;
519 }
520 return true;
521}
522
524{
525 Q_D(QFirebirdDriver);
526 return d->finishTransaction(TxnEnd::Commit);
527}
528
530{
531 Q_D(QFirebirdDriver);
532 return d->finishTransaction(TxnEnd::Rollback);
533}
534
536{
537 Q_D(const QFirebirdDriver);
538 return QVariant::fromValue(d->iAtt);
539}
540
541QString QFirebirdDriver::escapeIdentifier(const QString &identifier,
542 IdentifierType /*type*/) const
543{
544 QString res = identifier;
545 if (!identifier.isEmpty() && !identifier.startsWith(u'"') && !identifier.endsWith(u'"')) {
546 res.replace(u'"', u"\"\""_s);
547 res.replace(u'.', u"\".\""_s);
548 res = u'"' + res + u'"';
549 }
550 return res;
551}
552
553QString QFirebirdDriver::formatValue(const QSqlField &field, bool trimStrings) const
554{
555 switch (field.metaType().id()) {
556 case QMetaType::QDateTime: {
557 /* A plain TIMESTAMP is naive: emit the QDateTime's own wall-clock
558 date/time components verbatim (no conversion), matching the
559 bound-parameter path (encodeQDateTime). */
560 const QDateTime dt = field.value().toDateTime();
561 if (dt.isValid())
562 return dt.toString(u"''yyyy-MM-dd HH:mm:ss.zzz''");
563 return u"NULL"_s;
564 }
565 case QMetaType::QTime: {
566 const QTime t = field.value().toTime();
567 if (t.isValid())
568 return t.toString(u"''HH:mm:ss.zzz''");
569 return u"NULL"_s;
570 }
571 case QMetaType::QDate: {
572 const QDate d = field.value().toDate();
573 if (d.isValid())
574 return d.toString(u"''yyyy-MM-dd''");
575 return u"NULL"_s;
576 }
577 default:
578 return QSqlDriver::formatValue(field, trimStrings);
579 }
580}
581
583{
584 // Firebird 4.0+ raised the maximum identifier length to 63 characters
585 return 63;
586}
587
588// Schema introspection helpers
589
590/*! \internal
591 setLastError only mutates the driver's error state, which is logically
592 mutable from the const introspection methods — hence the const_cast.
593*/
594QSqlQuery QFirebirdDriver::execMetadataQuery(const QString &sql,
595 const QVariantList &binds) const
596{
597 QSqlQuery q(createResult());
598 q.prepare(sql);
599 for (const QVariant &v : binds)
600 q.addBindValue(v);
601 if (!q.exec()) {
602 qCWarning(lcFirebird) << "metadata query failed:"
603 << fbErrorLog(q.lastError()) << "SQL:" << sql;
604 const_cast<QFirebirdDriver *>(this)->setLastError(q.lastError());
605 }
606 return q;
607}
608
609QStringList QFirebirdDriver::tables(QSql::TableType type) const
610{
611 QStringList result;
612 if (!isOpen())
613 return result;
614
615 QString filter;
616 if (type == QSql::SystemTables) {
617 filter = u"WHERE RDB$SYSTEM_FLAG != 0"_s;
618 } else if (type == (QSql::SystemTables | QSql::Views)) {
619 filter = u"WHERE RDB$SYSTEM_FLAG != 0 OR RDB$VIEW_SOURCE IS NOT NULL"_s;
620 } else {
621 QStringList conditions;
622 if (!(type & QSql::SystemTables))
623 conditions << u"RDB$SYSTEM_FLAG = 0"_s;
624 if (!(type & QSql::Views))
625 conditions << u"RDB$VIEW_SOURCE IS NULL"_s;
626 if (!(type & QSql::Tables))
627 conditions << u"RDB$VIEW_SOURCE IS NOT NULL"_s;
628 if (!conditions.isEmpty())
629 filter = u"WHERE "_s + conditions.join(u" AND "_s);
630 }
631
632 QSqlQuery q = execMetadataQuery(
633 u"SELECT TRIM(RDB$RELATION_NAME) FROM RDB$RELATIONS "_s
634 + filter + u" ORDER BY RDB$RELATION_NAME"_s);
635 while (q.next())
636 result.append(q.value(0).toString());
637 return result;
638}
639
640QSqlRecord QFirebirdDriver::record(const QString &tableName) const
641{
642 QSqlRecord rec;
643 if (!isOpen())
644 return rec;
645
646 /* A quoted (delimited) identifier is case-sensitive: strip its delimiters and
647 match RDB$RELATION_NAME verbatim. An unquoted name is passed through as-is
648 (stripDelimiters() is a no-op for it); the caller is responsible for the
649 upper-case folding Firebird applies to unquoted identifiers. This mirrors
650 the QIBASE driver's behaviour. */
651 const QString table = stripDelimiters(tableName, QSqlDriver::TableName);
652
653 /* RDB$FIELDS.RDB$FIELD_TYPE stores the BLR type codes (blr_short, blr_long, …).
654 Map them to Qt types. */
655 const QString sql =
656 u"SELECT TRIM(f.RDB$FIELD_NAME), fld.RDB$FIELD_TYPE, fld.RDB$FIELD_SUB_TYPE,"
657 u" fld.RDB$FIELD_SCALE, fld.RDB$CHARACTER_LENGTH, f.RDB$NULL_FLAG,"
658 u" fld.RDB$FIELD_PRECISION, fld.RDB$FIELD_LENGTH"
659 u" FROM RDB$RELATION_FIELDS f"
660 u" JOIN RDB$FIELDS fld ON fld.RDB$FIELD_NAME = f.RDB$FIELD_SOURCE"
661 u" WHERE TRIM(f.RDB$RELATION_NAME) = ?"
662 u" ORDER BY f.RDB$FIELD_POSITION"_s;
663
664 QSqlQuery q = execMetadataQuery(sql, {table});
665
666 while (q.next()) {
667 QString name = q.value(0).toString().trimmed();
668 int rdbType = q.value(1).toInt();
669 int subType = q.value(2).toInt();
670 int scale = q.value(3).toInt(); // negative for NUMERIC/DECIMAL
671 int charLen = q.value(4).toInt();
672 bool notNull = (q.value(5).toInt() == 1);
673 int precision = q.value(6).toInt(); // total digits for NUMERIC
674 int byteLen = q.value(7).toInt();
675
676 QMetaType::Type qtType = blrTypeToQt(static_cast<unsigned char>(rdbType), scale < 0);
677 QSqlField fld(name, QMetaType(qtType));
678 fld.setRequired(notNull);
679
680 // Set length and precision for NUMERIC/DECIMAL; character length for text types
681 if (scale < 0 && precision > 0) {
682 fld.setLength(precision);
683 fld.setPrecision(-scale);
684 } else if (rdbType == blr_text || rdbType == blr_varying) { // CHAR / VARCHAR
685 fld.setLength(charLen > 0 ? charLen : byteLen);
686 }
687
688 Q_UNUSED(subType)
689 rec.append(fld);
690 }
691 return rec;
692}
693
694QSqlIndex QFirebirdDriver::primaryIndex(const QString &tableName) const
695{
696 QSqlIndex index;
697 if (!isOpen())
698 return index;
699
700 // See record(): strip a quoted identifier and match RDB$RELATION_NAME verbatim.
701 const QString table = stripDelimiters(tableName, QSqlDriver::TableName);
702
703 const QString sql =
704 u"SELECT TRIM(s.RDB$FIELD_NAME)"
705 u" FROM RDB$RELATION_CONSTRAINTS c"
706 u" JOIN RDB$INDEX_SEGMENTS s ON s.RDB$INDEX_NAME = c.RDB$INDEX_NAME"
707 u" WHERE c.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY'"
708 u" AND TRIM(c.RDB$RELATION_NAME) = ?"
709 u" ORDER BY s.RDB$FIELD_POSITION"_s;
710
711 QSqlQuery q = execMetadataQuery(sql, {table});
712
713 QSqlRecord rec = record(tableName);
714 while (q.next()) {
715 QString col = q.value(0).toString().trimmed();
716 index.append(rec.field(col));
717 }
718 return index;
719}
720
721// Event notification API
722
723bool QFirebirdDriver::subscribeToNotification(const QString &name)
724{
725 Q_D(QFirebirdDriver);
726 if (!isOpen()) {
727 qCWarning(lcFirebird, "QFirebirdDriver::subscribeToNotification: database not open.");
728 return false;
729 }
730 if (d->eventSubscriptions.contains(name)) {
731 qCWarning(lcFirebird,
732 "QFirebirdDriver::subscribeToNotification: already subscribed to '%ls'.",
733 qUtf16Printable(name));
734 return false;
735 }
736
737 auto *sub = new QFirebirdEventSubscription(this, d->iAtt, name);
738 sub->addRef(); // our reference
739
740 try {
741 ThrowStatusWrapper csw(d->iStatus);
742 sub->events = d->iAtt->queEvents(&csw, sub,
743 sub->bufferLength, sub->eventBuffer);
744 } catch (const FbException &e) {
745 sub->release();
746 auto err = fbError(d->master, e.getStatus(), QSqlError::ConnectionError);
747 setLastError(QSqlError(
748 tr("Could not subscribe to event notifications for %1.").arg(name),
749 err.databaseText(), err.type(), err.nativeErrorCode()));
750 return false;
751 }
752
753 d->eventSubscriptions.insert(name, sub);
754 return true;
755}
756
758{
759 Q_D(QFirebirdDriver);
760 if (!isOpen()) {
761 qCWarning(lcFirebird, "QFirebirdDriver::unsubscribeFromNotification: database not open.");
762 return false;
763 }
764 if (!d->eventSubscriptions.contains(name)) {
765 qCWarning(lcFirebird,
766 "QFirebirdDriver::unsubscribeFromNotification: not subscribed to '%ls'.",
767 qUtf16Printable(name));
768 return false;
769 }
770
771 QFirebirdEventSubscription *sub = d->eventSubscriptions.take(name);
772 sub->stop();
773 sub->release();
774 return true;
775}
776
778{
779 Q_D(const QFirebirdDriver);
780 return QStringList(d->eventSubscriptions.keys());
781}
782
783void QFirebirdDriver::qHandleEventNotification(const QString &name)
784{
785 Q_D(QFirebirdDriver);
786
787 QFirebirdEventSubscription *sub = d->eventSubscriptions.value(name, nullptr);
788 if (!sub || sub->counter.load(std::memory_order_relaxed) == 0)
789 return; // already unsubscribed or spurious call
790
791 /* isc_event_counts writes one counter per event registered with isc_event_block.
792 Each QFirebirdEventSubscription registers exactly one event name. */
793 static constexpr int maxEventNamesPerSubscription = 1;
794 ISC_ULONG counts[maxEventNamesPerSubscription] = {};
795 {
796 /* Serialise the resultBuffer read against the Firebird event thread's
797 memcpy in eventCallbackFunction — the only cross-thread buffer access. */
798 std::lock_guard<std::mutex> lock(sub->mutex);
799 isc_event_counts(counts, static_cast<ISC_USHORT>(sub->bufferLength),
800 sub->eventBuffer, sub->resultBuffer);
801 sub->counter.store(0, std::memory_order_relaxed);
802 }
803
804 /* Re-arm: queEvents is one-shot. Release the old IEvents handle and
805 re-register using the updated eventBuffer as the new baseline. This runs
806 OUTSIDE sub->mutex on purpose: queEvents takes Firebird's internal event
807 lock, which the event thread also holds while calling back into us, so
808 holding our mutex across queEvents could deadlock. No callback can write
809 resultBuffer between the read above and this re-arm (queEvents is
810 one-shot), so the buffer stays stable in the gap. */
811 IEvents *old = sub->events;
812 sub->events = nullptr;
813
814 IStatus *s = d->master->getStatus();
815 ThrowStatusWrapper csw(s);
816 try {
817 sub->events = sub->attachment->queEvents(&csw, sub,
818 sub->bufferLength, sub->eventBuffer);
819 } catch (const FbException &e) {
820 const auto err = fbError(d->master, e.getStatus(), QSqlError::ConnectionError);
821 qCWarning(lcFirebird) << "qHandleEventNotification: re-arm failed for"
822 << name << ':' << fbErrorLog(err);
823 } catch (...) {
824 qCWarning(lcFirebird, "qHandleEventNotification: unexpected exception re-arming '%ls'",
825 qUtf16Printable(name));
826 }
827 if (old)
828 old->release();
829 s->dispose();
830
831 /* Suppress the first callback (initial registration baseline). Update the
832 subscription state BEFORE emitting: a directly-connected slot may
833 unsubscribe, releasing the subscription, so sub must not be touched
834 after the emit. */
835 const bool suppress = sub->first;
836 sub->first = false;
837 if (counts[0] > 0 && !suppress)
838 emit notification(name, QSqlDriver::UnknownSource, QVariant());
839}
840
841QT_END_NAMESPACE
bool finishTransaction(TxnEnd end)
void setFbError(const QString &context, QSqlError::ErrorType type=QSqlError::UnknownError)
bool commitTransaction() override
This function is called to commit a transaction.
QSqlResult * createResult() const override
Creates an empty SQL result on the database.
bool cancelQuery() override
bool hasFeature(DriverFeature feature) const override
Returns true if the driver supports feature feature; otherwise returns false.
QString escapeIdentifier(const QString &identifier, IdentifierType type) const override
Returns the identifier escaped according to the database rules.
QSqlRecord record(const QString &tableName) const override
Returns a QSqlRecord populated with the names of the fields in table tableName.
QFirebirdDriver(QObject *parent=nullptr)
bool rollbackTransaction() override
This function is called to rollback a transaction.
QSqlIndex primaryIndex(const QString &tableName) const override
Returns the primary index for table tableName.
int maximumIdentifierLength(IdentifierType type) const override
QVariant handle() const override
Returns the low-level database handle wrapped in a QVariant or an invalid variant if there is no hand...
bool open(const QString &db, const QString &user, const QString &password, const QString &host, int port, const QString &connOpts) override
Derived classes must reimplement this pure virtual function to open a database connection on database...
QStringList subscribedToNotifications() const override
Returns a list of the names of the event notifications that are currently subscribed to.
bool unsubscribeFromNotification(const QString &name) override
This function is called to unsubscribe from event notifications from the database.
QString formatValue(const QSqlField &field, bool trimStrings) const override
Returns a string representation of the field value for the database.
bool beginTransaction() override
This function is called to begin a transaction.
bool subscribeToNotification(const QString &name) override
This function is called to subscribe to event notifications from the database.
~QFirebirdDriver() override
void close() override
Derived classes must reimplement this pure virtual function in order to close the database connection...
void eventCallbackFunction(unsigned length, const unsigned char *data) override
QFirebirdEventSubscription(QFirebirdDriver *drv, IAttachment *att, const QString &eventName)
QFirebirdResult(const QFirebirdDriver *driver)