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 return true;
410}
411
413{
414 Q_D(QFirebirdDriver);
415 if (!isOpen())
416 return;
417
418 CheckStatusWrapper st(d->iStatus);
419 // Cancel all event subscriptions before detaching
420 for (auto *sub : std::as_const(d->eventSubscriptions)) {
421 sub->stop();
422 sub->release();
423 }
424 d->eventSubscriptions.clear();
425 if (d->iTrans) {
426 try {
427 ThrowStatusWrapper tsw(d->iStatus);
428 d->iTrans->rollback(&tsw);
429 } catch (...) {
430 d->iTrans->release();
431 }
432 d->iTrans = nullptr;
433 }
434 if (d->iAtt) {
435 /* Withdraw the attachment from cancelQuery()'s reach before detaching:
436 once the lock is dropped, a concurrent cancelQuery() sees nullptr,
437 and one already inside cancelOperation() has finished (it holds the
438 mutex for the duration of the call). */
439 IAttachment *att;
440 {
441 const std::lock_guard<std::mutex> lock(d->attMutex);
442 att = d->iAtt;
443 d->iAtt = nullptr;
444 }
445 try {
446 ThrowStatusWrapper tsw(d->iStatus);
447 att->detach(&tsw);
448 } catch (...) {
449 att->release();
450 }
451 }
452 d->iStatus->init();
453 setOpen(false);
454 setOpenError(false);
455}
456
458{
459 return new QFirebirdResult(this);
460}
461
462/*! \internal
463 Cross-thread cancellation (QSqlDriver::CancelQuery). This runs on a
464 DIFFERENT thread than the one executing the query, so it must not touch any
465 driver state beyond the mutex-guarded attachment: no setLastError, and not
466 iStatus (the executing thread owns it) — a private status object is used
467 instead. fb_cancel_raise asks the engine to abort the current operation at
468 its next cancellation point; the executing call then fails with
469 isc_cancelled, which the normal FbException -> QSqlError path reports on the
470 executing thread. The attachment itself stays open and usable.
471*/
473{
474 Q_D(QFirebirdDriver);
475 const std::lock_guard<std::mutex> lock(d->attMutex);
476 if (!d->iAtt)
477 return false;
478 FbGuard<IStatus> status(d->master->getStatus(), fbDispose<IStatus>);
479 try {
480 ThrowStatusWrapper st(status.get());
481 d->iAtt->cancelOperation(&st, fb_cancel_raise);
482 } catch (const FbException &e) {
483 const auto err = fbError(d->master, e.getStatus(), QSqlError::UnknownError);
484 qCWarning(lcFirebird) << "cancelQuery:" << fbErrorLog(err);
485 return false;
486 }
487 return true;
488}
489
491{
492 Q_D(QFirebirdDriver);
493 /* QSqlDatabase::transaction() forwards here after only a feature check, so
494 guard against a closed (or failed-to-open) database: without it the
495 start call below would dereference a null attachment. */
496 if (!isOpen() || isOpenError()) {
497 setLastError(QSqlError(u"Database not open"_s, {},
498 QSqlError::TransactionError));
499 return false;
500 }
501 if (d->iTrans) {
502 setLastError(QSqlError(u"Transaction already active"_s, {},
503 QSqlError::TransactionError));
504 return false;
505 }
506 try {
507 ThrowStatusWrapper st(d->iStatus);
508 d->iTrans = startDefaultTransaction(d->iAtt, st, TxnWait::NoWait);
509 } catch (const FbException &e) {
510 setLastError(fbError(d->master, e.getStatus(), QSqlError::TransactionError));
511 d->iStatus->init();
512 return false;
513 }
514 return true;
515}
516
518{
519 Q_D(QFirebirdDriver);
520 return d->finishTransaction(TxnEnd::Commit);
521}
522
524{
525 Q_D(QFirebirdDriver);
526 return d->finishTransaction(TxnEnd::Rollback);
527}
528
530{
531 Q_D(const QFirebirdDriver);
532 return QVariant::fromValue(d->iAtt);
533}
534
535QString QFirebirdDriver::escapeIdentifier(const QString &identifier,
536 IdentifierType /*type*/) const
537{
538 QString res = identifier;
539 if (!identifier.isEmpty() && !identifier.startsWith(u'"') && !identifier.endsWith(u'"')) {
540 res.replace(u'"', u"\"\""_s);
541 res.replace(u'.', u"\".\""_s);
542 res = u'"' + res + u'"';
543 }
544 return res;
545}
546
547QString QFirebirdDriver::formatValue(const QSqlField &field, bool trimStrings) const
548{
549 switch (field.metaType().id()) {
550 case QMetaType::QDateTime: {
551 /* A plain TIMESTAMP is naive: emit the QDateTime's own wall-clock
552 date/time components verbatim (no conversion), matching the
553 bound-parameter path (encodeQDateTime). */
554 const QDateTime dt = field.value().toDateTime();
555 if (dt.isValid())
556 return dt.toString(u"''yyyy-MM-dd HH:mm:ss.zzz''");
557 return u"NULL"_s;
558 }
559 case QMetaType::QTime: {
560 const QTime t = field.value().toTime();
561 if (t.isValid())
562 return t.toString(u"''HH:mm:ss.zzz''");
563 return u"NULL"_s;
564 }
565 case QMetaType::QDate: {
566 const QDate d = field.value().toDate();
567 if (d.isValid())
568 return d.toString(u"''yyyy-MM-dd''");
569 return u"NULL"_s;
570 }
571 default:
572 return QSqlDriver::formatValue(field, trimStrings);
573 }
574}
575
577{
578 // Firebird 4.0+ raised the maximum identifier length to 63 characters
579 return 63;
580}
581
582// Schema introspection helpers
583
584/*! \internal
585 setLastError only mutates the driver's error state, which is logically
586 mutable from the const introspection methods — hence the const_cast.
587*/
588QSqlQuery QFirebirdDriver::execMetadataQuery(const QString &sql,
589 const QVariantList &binds) const
590{
591 QSqlQuery q(createResult());
592 q.prepare(sql);
593 for (const QVariant &v : binds)
594 q.addBindValue(v);
595 if (!q.exec()) {
596 qCWarning(lcFirebird) << "metadata query failed:"
597 << fbErrorLog(q.lastError()) << "SQL:" << sql;
598 const_cast<QFirebirdDriver *>(this)->setLastError(q.lastError());
599 }
600 return q;
601}
602
603QStringList QFirebirdDriver::tables(QSql::TableType type) const
604{
605 QStringList result;
606 if (!isOpen())
607 return result;
608
609 QString filter;
610 if (type == QSql::SystemTables) {
611 filter = u"WHERE RDB$SYSTEM_FLAG != 0"_s;
612 } else if (type == (QSql::SystemTables | QSql::Views)) {
613 filter = u"WHERE RDB$SYSTEM_FLAG != 0 OR RDB$VIEW_SOURCE IS NOT NULL"_s;
614 } else {
615 QStringList conditions;
616 if (!(type & QSql::SystemTables))
617 conditions << u"RDB$SYSTEM_FLAG = 0"_s;
618 if (!(type & QSql::Views))
619 conditions << u"RDB$VIEW_SOURCE IS NULL"_s;
620 if (!(type & QSql::Tables))
621 conditions << u"RDB$VIEW_SOURCE IS NOT NULL"_s;
622 if (!conditions.isEmpty())
623 filter = u"WHERE "_s + conditions.join(u" AND "_s);
624 }
625
626 QSqlQuery q = execMetadataQuery(
627 u"SELECT TRIM(RDB$RELATION_NAME) FROM RDB$RELATIONS "_s
628 + filter + u" ORDER BY RDB$RELATION_NAME"_s);
629 while (q.next())
630 result.append(q.value(0).toString());
631 return result;
632}
633
634QSqlRecord QFirebirdDriver::record(const QString &tableName) const
635{
636 QSqlRecord rec;
637 if (!isOpen())
638 return rec;
639
640 /* A quoted (delimited) identifier is case-sensitive: strip its delimiters and
641 match RDB$RELATION_NAME verbatim. An unquoted name is passed through as-is
642 (stripDelimiters() is a no-op for it); the caller is responsible for the
643 upper-case folding Firebird applies to unquoted identifiers. This mirrors
644 the QIBASE driver's behaviour. */
645 const QString table = stripDelimiters(tableName, QSqlDriver::TableName);
646
647 /* RDB$FIELDS.RDB$FIELD_TYPE stores the BLR type codes (blr_short, blr_long, …).
648 Map them to Qt types. */
649 const QString sql =
650 u"SELECT TRIM(f.RDB$FIELD_NAME), fld.RDB$FIELD_TYPE, fld.RDB$FIELD_SUB_TYPE,"
651 u" fld.RDB$FIELD_SCALE, fld.RDB$CHARACTER_LENGTH, f.RDB$NULL_FLAG,"
652 u" fld.RDB$FIELD_PRECISION, fld.RDB$FIELD_LENGTH"
653 u" FROM RDB$RELATION_FIELDS f"
654 u" JOIN RDB$FIELDS fld ON fld.RDB$FIELD_NAME = f.RDB$FIELD_SOURCE"
655 u" WHERE TRIM(f.RDB$RELATION_NAME) = ?"
656 u" ORDER BY f.RDB$FIELD_POSITION"_s;
657
658 QSqlQuery q = execMetadataQuery(sql, {table});
659
660 while (q.next()) {
661 QString name = q.value(0).toString().trimmed();
662 int rdbType = q.value(1).toInt();
663 int subType = q.value(2).toInt();
664 int scale = q.value(3).toInt(); // negative for NUMERIC/DECIMAL
665 int charLen = q.value(4).toInt();
666 bool notNull = (q.value(5).toInt() == 1);
667 int precision = q.value(6).toInt(); // total digits for NUMERIC
668 int byteLen = q.value(7).toInt();
669
670 QMetaType::Type qtType = blrTypeToQt(static_cast<unsigned char>(rdbType), scale < 0);
671 QSqlField fld(name, QMetaType(qtType));
672 fld.setRequired(notNull);
673
674 // Set length and precision for NUMERIC/DECIMAL; character length for text types
675 if (scale < 0 && precision > 0) {
676 fld.setLength(precision);
677 fld.setPrecision(-scale);
678 } else if (rdbType == blr_text || rdbType == blr_varying) { // CHAR / VARCHAR
679 fld.setLength(charLen > 0 ? charLen : byteLen);
680 }
681
682 Q_UNUSED(subType)
683 rec.append(fld);
684 }
685 return rec;
686}
687
688QSqlIndex QFirebirdDriver::primaryIndex(const QString &tableName) const
689{
690 QSqlIndex index;
691 if (!isOpen())
692 return index;
693
694 // See record(): strip a quoted identifier and match RDB$RELATION_NAME verbatim.
695 const QString table = stripDelimiters(tableName, QSqlDriver::TableName);
696
697 const QString sql =
698 u"SELECT TRIM(s.RDB$FIELD_NAME)"
699 u" FROM RDB$RELATION_CONSTRAINTS c"
700 u" JOIN RDB$INDEX_SEGMENTS s ON s.RDB$INDEX_NAME = c.RDB$INDEX_NAME"
701 u" WHERE c.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY'"
702 u" AND TRIM(c.RDB$RELATION_NAME) = ?"
703 u" ORDER BY s.RDB$FIELD_POSITION"_s;
704
705 QSqlQuery q = execMetadataQuery(sql, {table});
706
707 QSqlRecord rec = record(tableName);
708 while (q.next()) {
709 QString col = q.value(0).toString().trimmed();
710 index.append(rec.field(col));
711 }
712 return index;
713}
714
715// Event notification API
716
717bool QFirebirdDriver::subscribeToNotification(const QString &name)
718{
719 Q_D(QFirebirdDriver);
720 if (!isOpen()) {
721 qCWarning(lcFirebird, "QFirebirdDriver::subscribeToNotification: database not open.");
722 return false;
723 }
724 if (d->eventSubscriptions.contains(name)) {
725 qCWarning(lcFirebird,
726 "QFirebirdDriver::subscribeToNotification: already subscribed to '%ls'.",
727 qUtf16Printable(name));
728 return false;
729 }
730
731 auto *sub = new QFirebirdEventSubscription(this, d->iAtt, name);
732 sub->addRef(); // our reference
733
734 try {
735 ThrowStatusWrapper csw(d->iStatus);
736 sub->events = d->iAtt->queEvents(&csw, sub,
737 sub->bufferLength, sub->eventBuffer);
738 } catch (const FbException &e) {
739 sub->release();
740 auto err = fbError(d->master, e.getStatus(), QSqlError::ConnectionError);
741 setLastError(QSqlError(
742 tr("Could not subscribe to event notifications for %1.").arg(name),
743 err.databaseText(), err.type(), err.nativeErrorCode()));
744 return false;
745 }
746
747 d->eventSubscriptions.insert(name, sub);
748 return true;
749}
750
752{
753 Q_D(QFirebirdDriver);
754 if (!isOpen()) {
755 qCWarning(lcFirebird, "QFirebirdDriver::unsubscribeFromNotification: database not open.");
756 return false;
757 }
758 if (!d->eventSubscriptions.contains(name)) {
759 qCWarning(lcFirebird,
760 "QFirebirdDriver::unsubscribeFromNotification: not subscribed to '%ls'.",
761 qUtf16Printable(name));
762 return false;
763 }
764
765 QFirebirdEventSubscription *sub = d->eventSubscriptions.take(name);
766 sub->stop();
767 sub->release();
768 return true;
769}
770
772{
773 Q_D(const QFirebirdDriver);
774 return QStringList(d->eventSubscriptions.keys());
775}
776
777void QFirebirdDriver::qHandleEventNotification(const QString &name)
778{
779 Q_D(QFirebirdDriver);
780
781 QFirebirdEventSubscription *sub = d->eventSubscriptions.value(name, nullptr);
782 if (!sub || sub->counter.load(std::memory_order_relaxed) == 0)
783 return; // already unsubscribed or spurious call
784
785 /* isc_event_counts writes one counter per event registered with isc_event_block.
786 Each QFirebirdEventSubscription registers exactly one event name. */
787 static constexpr int maxEventNamesPerSubscription = 1;
788 ISC_ULONG counts[maxEventNamesPerSubscription] = {};
789 {
790 /* Serialise the resultBuffer read against the Firebird event thread's
791 memcpy in eventCallbackFunction — the only cross-thread buffer access. */
792 std::lock_guard<std::mutex> lock(sub->mutex);
793 isc_event_counts(counts, static_cast<ISC_USHORT>(sub->bufferLength),
794 sub->eventBuffer, sub->resultBuffer);
795 sub->counter.store(0, std::memory_order_relaxed);
796 }
797
798 /* Re-arm: queEvents is one-shot. Release the old IEvents handle and
799 re-register using the updated eventBuffer as the new baseline. This runs
800 OUTSIDE sub->mutex on purpose: queEvents takes Firebird's internal event
801 lock, which the event thread also holds while calling back into us, so
802 holding our mutex across queEvents could deadlock. No callback can write
803 resultBuffer between the read above and this re-arm (queEvents is
804 one-shot), so the buffer stays stable in the gap. */
805 IEvents *old = sub->events;
806 sub->events = nullptr;
807
808 IStatus *s = d->master->getStatus();
809 ThrowStatusWrapper csw(s);
810 try {
811 sub->events = sub->attachment->queEvents(&csw, sub,
812 sub->bufferLength, sub->eventBuffer);
813 } catch (const FbException &e) {
814 const auto err = fbError(d->master, e.getStatus(), QSqlError::ConnectionError);
815 qCWarning(lcFirebird) << "qHandleEventNotification: re-arm failed for"
816 << name << ':' << fbErrorLog(err);
817 } catch (...) {
818 qCWarning(lcFirebird, "qHandleEventNotification: unexpected exception re-arming '%ls'",
819 qUtf16Printable(name));
820 }
821 if (old)
822 old->release();
823 s->dispose();
824
825 /* Suppress the first callback (initial registration baseline). Update the
826 subscription state BEFORE emitting: a directly-connected slot may
827 unsubscribe, releasing the subscription, so sub must not be touched
828 after the emit. */
829 const bool suppress = sub->first;
830 sub->first = false;
831 if (counts[0] > 0 && !suppress)
832 emit notification(name, QSqlDriver::UnknownSource, QVariant());
833}
834
835QT_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)