This module is the portable SQL layer, so a caller opens a database, runs statements, and reads rows through the same types on every supported platform. The unit of work is a connection. Construct a Connection with a registered driver name, such as "sqlite". That allocates a backend; it does not open the database. open() takes a driver-specific connection string with no driver prefix.
Connection is not copyable. Statement, Result, Row, Value, Cursor and Blob are shared values: copying is cheap, and the backend lives until the last wrapper is destroyed.
Without an EventLoop every operation is synchronous and completes on the calling thread. setActive() attaches an EventLoop for asynchronous work. The loop does not own the connection: the code that creates it keeps it alive while an operation is still waiting on the loop. Asynchronous operations use a begin/end pair and a finished signal. The slot calls the matching end method. C++20 awaitables wrap the same pairs for co_await.
A Statement is a prepared query with named host variables. execute() runs a statement that does not return rows. select() returns a fully buffered Result. For a result that is too large to hold at once, getCursor() returns a Cursor that fetches batches.
A Transaction begins on a connection, commits or rolls back, and rolls back from the destructor if it is still active.
A failure that is specific to this module is a DbError. What each operation throws is documented on that member.
The rest of this chapter is the connection, then statements, then buffered results, then cursors, then transactions.
This chapter covers:
Connection is the database handle. Construct it with a registered driver name. open() establishes the session with a driver-specific connection string; the string does not include the driver name. close() ends the session. isOpen() reports whether a session is established, and ping() tests whether the backend still answers.
The same object runs SQL directly and prepares statements. execute() runs a statement that does not return rows and reports how many rows changed. select() runs a query and returns a fully buffered Result. prepare() compiles a Statement that can bind host variables and be executed more than once. prepareCached() compiles the same way and keeps the statement in a cache keyed by the SQL text, so a later call with the same text reuses it. clearStatementCache() drops that cache. lastInsertId() is the generated row id of the last insert on this connection.
Without an EventLoop every method above is synchronous. setActive() attaches an EventLoop so the begin/end forms can run. beginOpen() / endOpen() / openFinished() are the async open; close, ping, execute, select, prepare and prepareCached follow the same pattern. isIdle() is true when no async operation is pending. cancel() aborts a pending operation. The loop does not own the connection.
C++20 awaitables wrap those pairs: openAsync(), closeAsync(), executeAsync(), selectAsync() and pingAsync() are used with co_await and complete through the same signals.
Only one async operation may be in flight on a connection, a statement, or a cursor that uses it. Starting another while one is pending throws InvalidConnection or ConnectionError. An unknown driver name throws InvalidConnection from the constructor.
hasTransaction() is true while a Transaction is active on this connection. Begin, commit and rollback are operations of Transaction, not of Connection.
Connection is the database handle the group described: a driver name allocates a backend, and open() establishes the session. The object is not copyable. Destroying it cancels a pending async operation and releases the backend.
The driver constructor takes a registered name such as "sqlite". The connection is not open after that call. The IConnection constructor takes ownership of an existing backend and is meant for tests and custom backends.
open() and close() are the synchronous session. The connection string is driver-specific and has no driver prefix. isOpen() reports the session. operator!() is true when the session is not open. ping() tests whether the backend still answers. lastInsertId() is the generated row id of the last insert; pass a sequence name on backends that use named sequences, or an empty string otherwise.
execute() runs SQL that does not return rows. select() runs a query and returns a buffered Result. prepare() compiles a Statement. prepareCached() compiles and caches by SQL text. clearStatementCache() drops that cache.
setActive() attaches an EventLoop for asynchronous work. The loop does not own the connection. Each async operation is a begin/end pair and a finished signal; the slot calls the matching end method. isIdle() is true when none is pending. cancel() aborts a pending operation. Starting another while one is pending throws InvalidConnection or ConnectionError.
When C++20 is available, openAsync(), closeAsync(), executeAsync(), selectAsync() and pingAsync() wrap those pairs for co_await.
hasTransaction() is true while a Transaction is active. Begin, commit and rollback are operations of that type.
Statement is a compiled query with named host variables. Obtain one from Connection::prepare() or Connection::prepareCached(). It is a shared value: copying is cheap, and the backend lives until the last wrapper is destroyed. An unbound statement is empty; operator!() is true for that state.
A host variable is a colon, then a name that starts with a letter and continues with letters, digits or underscore, for example :id. Names are not scanned inside strings delimited by apostrophes, quotation marks or backticks. A backslash prevents the next character from being special. set() binds a name to a typed value. setNull() binds a name to SQL NULL. clear() sets every host variable to NULL.
execute() runs a statement that does not return rows, typically INSERT, UPDATE or DELETE, and returns how many rows changed. lastInsertId() is the generated row id after that execute. select() runs a query and returns a fully buffered Result. selectRow() returns the first row and discards the rest; it throws InvalidQuery when the query returns no row. selectValue() returns the first column of the first row, with the same empty-result error.
For a result that should not be fully buffered, getCursor() opens a batch Cursor on this statement. Cursor use is documented with that type.
Asynchronous execute and select follow the connection model: beginExecute() / endExecute() / executeFinished(), and beginSelect() / endSelect() / selectFinished(). cancel() aborts a pending operation on this statement. The connection must have been attached with setActive() first.
Statement is the compiled query the group described. Obtain it from Connection::prepare() or Connection::prepareCached(). It is a shared value. An unbound statement is empty; operator!() is true then. The constructor that takes an IStatement takes ownership of that backend.
Host variables are :name. Bind them with set() before execute or select. setNull() binds SQL NULL. clear() sets every host variable to NULL. A const char pointer that is 0 is bound as NULL.
execute() runs a statement that does not return rows and returns how many rows changed. lastInsertId() is the generated id after that execute. select() returns a buffered Result. selectRow() returns the first row and discards the rest. selectValue() returns the first column of the first row. Both throw InvalidQuery when the query returns no row.
getCursor() opens a batch cursor on this statement. beginExecute() / endExecute() / executeFinished() and beginSelect() / endSelect() / selectFinished() are the async forms. cancel() aborts a pending operation.
A Result is a query result held entirely in memory. Rows are addressed by index and visited with a random-access iterator, so the set can be walked more than once and in any order. size() is the number of rows. getFieldCount() is the number of columns. empty() is true when there are no rows. operator[] and getRow() return a Row at an index without range checking.
A Row is one of those rows. Columns are addressed by index. size() is the column count. getValue() and operator[] return a Value. Typed getters such as getInt() and getString() convert that column. isNull() reports SQL NULL at an index. The row also has a random-access iterator over its values.
A Value is one column. isNull() is true for SQL NULL and for an unbound value. Typed getters convert the stored value; a null or a conversion that cannot be performed throws TypeMismatch. getBlob() writes binary data into a Blob.
Blob is a copy-on-write binary value. data() and size() describe the bytes. assign() replaces them. Equality compares the bytes.
Result, Row, Value and Blob are shared values. Copying is cheap. They do not keep the connection busy: once a Result is returned, the connection can run another statement.
Use a Result when the whole set fits in memory and random access is useful. Use a Cursor when the set is large and rows should arrive in batches.
Result is the in-memory result the group described. Rows are addressed by index and visited with a random-access iterator. The set can be walked more than once. It is a shared value. An unbound result is empty; operator!() is true then.
size() is the number of rows. getFieldCount() is the number of columns. empty() is true when there are no rows. operator[] and getRow() return a Row without range checking. getValue() returns a column of a row the same way. begin() and end() are random-access iterators, so the result can be used with standard algorithms.
Once a Result is returned, the connection can run another statement. For a set that should not be fully buffered, use a Cursor.
Row is one row of a Result. Columns are addressed by index. It is a shared value. An unbound row is empty; operator!() is true then.
size() is the column count. empty() is true when there are no columns. getValue() and operator[] return a Value without range checking. Typed getters convert that column. isNull() reports SQL NULL at an index. A null or a conversion that cannot be performed throws TypeMismatch. begin() and end() are random-access iterators over the values.
Value is one column of a Row. It is a shared value. An unbound value and SQL NULL both make isNull() true. operator!() is true when the object is not bound.
Typed getters convert the stored value. A null or a conversion that cannot be performed throws TypeMismatch. getChar() on a string returns the first character. getBlob() writes binary data into a Blob.
Blob holds binary column data as a shared, copy-on-write value. The default constructor is empty. The data constructor copies len bytes from data. The IBlob constructor takes ownership of a custom implementation.
assign() replaces the bytes and copies on write when the value is shared. data() returns the bytes or 0 when empty. size() is the length. Equality compares the bytes.
A Cursor reads a query a batch at a time instead of loading every row into one Result. Obtain it from Statement::getCursor() with the number of rows per batch. It is a shared value: copying is cheap, and the backend cursor is closed when the last copy is destroyed or when close() is called.
Range-for walks every row and fetches the next batch when the current one is exhausted. That path is synchronous.
fetch() loads the next batch into the cursor's buffer and returns true while the batch is not empty. result() is that buffer, a Result with random access to the rows of the current batch only. When fetch() returns false, no more rows are available.
Asynchronous fetch needs a connection that was attached with setActive(). Connect to fetchFinished(), then call beginFetch(). In the slot, endFetch() returns the batch. isOpen() is true while the cursor can still produce rows; when it becomes false, the cursor has closed itself. Call beginFetch() again for the next batch while it stays open.
A cursor holds the statement's result stream. Do not run another operation on the same connection until the cursor is closed.
Cursor is the batch reader the group described. Obtain it from Statement::getCursor() with the number of rows per batch. It is a shared value. The backend cursor is closed when the last copy is destroyed or when close() is called. A default-constructed cursor is closed.
Range-for walks every row and fetches the next batch when the current one is exhausted. That path is synchronous. begin() fetches the first batch; end() is the sentinel.
fetch() loads the next batch into the cursor's buffer and returns true while the batch is not empty. result() is that buffer, a Result of the current batch only.
Asynchronous fetch needs a connection attached with setActive(). Connect to fetchFinished(), then call beginFetch(). In the slot, endFetch() returns the batch. isOpen() is true while the cursor can still produce rows. Call beginFetch() again for the next batch while it stays open. close() cancels a fetch in progress.
Do not run another operation on the same connection until the cursor is closed.
Transaction is a unit of work on one Connection. The constructor begins a deferred transaction unless the start flag is false. begin() starts one; if a transaction is already active it is rolled back first. commit() ends it and keeps the changes. rollback() ends it and discards them. If the object is destroyed while still active, the destructor rolls back and swallows any error from that rollback.
The object is not copyable. It does not own the connection.
Asynchronous begin, commit and rollback follow the connection model and need setActive() on that connection. beginStart() / endStart() / startFinished() begin the transaction. beginCommit() / endCommit() / commitFinished() commit it. beginRollback() / endRollback() / rollbackFinished() roll it back. endStart() is the call that marks the transaction active on the async path.
Override onGetBeginSql(), onGetCommitSql() and onGetRollbackSql() to supply backend-specific SQL. A null return lets the backend use its default. SqliteTransaction() uses that hook to run BEGIN IMMEDIATE TRANSACTION when immediate locking is requested.
Transaction is the unit of work the group described. The constructor begins a deferred transaction unless starttransaction is false. The object is not copyable and does not own the connection.
begin() starts a transaction; if one is already active it is rolled back first. commit() keeps the changes. rollback() discards them. If the object is destroyed while still active, the destructor rolls back and swallows any error from that rollback.
Asynchronous begin, commit and rollback need setActive() on the connection. beginStart() / endStart() / startFinished() begin the transaction; endStart() is the call that marks it active. beginCommit() / endCommit() / commitFinished() commit. beginRollback() / endRollback() / rollbackFinished() roll back.
Override onGetBeginSql(), onGetCommitSql() and onGetRollbackSql() to supply backend-specific SQL. A null return lets the backend use its default.
SqliteTransaction is a Transaction that can start with BEGIN IMMEDIATE TRANSACTION instead of the backend default. Pass immediate true for that locking. start still controls whether the constructor begins the transaction.