| // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import "dart:collection"; | |
| import "dart:ffi"; | |
| import "package:ffi/ffi.dart"; | |
| import "bindings/bindings.dart"; | |
| import "bindings/types.dart" as types; | |
| import "bindings/types.dart" hide Database; | |
| import "bindings/constants.dart"; | |
| import "collections/closable_iterator.dart"; | |
| /// [Database] represents an open connection to a SQLite database. | |
| /// | |
| /// All functions against a database may throw [SQLiteError]. | |
| /// | |
| /// This database interacts with SQLite synchronously. | |
| class Database { | |
| late DatabaseResource _database; | |
| bool _open = false; | |
| /// Open a database located at the file [path]. | |
| Database( | |
| String path, [ | |
| int flags = Flags.SQLITE_OPEN_READWRITE | Flags.SQLITE_OPEN_CREATE, | |
| ]) { | |
| Pointer<Pointer<types.Database>> dbOut = calloc(); | |
| final pathC = Utf8Resource(path.toNativeUtf8()); | |
| final int resultCode = bindings.sqlite3_open_v2( | |
| pathC.unsafe(), | |
| dbOut, | |
| flags, | |
| nullptr, | |
| ); | |
| _database = DatabaseResource(dbOut.value); | |
| calloc.free(dbOut); | |
| pathC.free(); | |
| if (resultCode == Errors.SQLITE_OK) { | |
| _open = true; | |
| } else { | |
| // Even if "open" fails, sqlite3 will still create a database object. We | |
| // can just destroy it. | |
| SQLiteException exception = _loadError(resultCode); | |
| close(); | |
| throw exception; | |
| } | |
| } | |
| /// Close the database. | |
| /// | |
| /// This should only be called once on a database unless an exception is | |
| /// thrown. It should be called at least once to finalize the database and | |
| /// avoid resource leaks. | |
| void close() { | |
| assert(_open); | |
| final int resultCode = _database.close(); | |
| if (resultCode == Errors.SQLITE_OK) { | |
| _open = false; | |
| } else { | |
| throw _loadError(resultCode); | |
| } | |
| } | |
| /// Execute a query, discarding any returned rows. | |
| void execute(String query) { | |
| Pointer<Pointer<Statement>> statementOut = malloc(); | |
| final queryC = Utf8Resource(query.toNativeUtf8()); | |
| int resultCode = _database.prepare(queryC, -1, statementOut, nullptr); | |
| final statement = StatementResource(statementOut.value); | |
| calloc.free(statementOut); | |
| queryC.free(); | |
| while (resultCode == Errors.SQLITE_ROW || resultCode == Errors.SQLITE_OK) { | |
| resultCode = statement.step(); | |
| } | |
| statement.finalize(); | |
| if (resultCode != Errors.SQLITE_DONE) { | |
| throw _loadError(resultCode); | |
| } | |
| } | |
| /// Evaluate a query and return the resulting rows as an iterable. | |
| Result query(String query) { | |
| Pointer<Pointer<Statement>> statementOut = malloc(); | |
| final queryC = Utf8Resource(query.toNativeUtf8()); | |
| int resultCode = _database.prepare(queryC, -1, statementOut, nullptr); | |
| final statement = StatementResource(statementOut.value); | |
| calloc.free(statementOut); | |
| queryC.free(); | |
| if (resultCode != Errors.SQLITE_OK) { | |
| statement.finalize(); | |
| throw _loadError(resultCode); | |
| } | |
| Map<String, int> columnIndices = {}; | |
| int columnCount = statement.columnCount; | |
| for (int i = 0; i < columnCount; i++) { | |
| String columnName = statement.columnName(i); | |
| columnIndices[columnName] = i; | |
| } | |
| return Result._(this, statement, columnIndices); | |
| } | |
| SQLiteException _loadError([int? errorCode]) { | |
| String errorMessage = _database.errmsg().toDartString(); | |
| if (errorCode == null) { | |
| return SQLiteException(errorMessage); | |
| } | |
| String errorCodeExplanation = bindings | |
| .sqlite3_errstr(errorCode) | |
| .toDartString(); | |
| return SQLiteException( | |
| "$errorMessage (Code $errorCode: $errorCodeExplanation)", | |
| ); | |
| } | |
| } | |
| /// [Result] represents a [Database.query]'s result and provides an [Iterable] | |
| /// interface for the results to be consumed. | |
| /// | |
| /// Please note that this iterator should be [close]d manually if not all [Row]s | |
| /// are consumed. | |
| class Result extends IterableBase<Row> implements ClosableIterable<Row> { | |
| final ClosableIterator<Row> _iterator; | |
| Result._( | |
| Database database, | |
| StatementResource statement, | |
| Map<String, int> columnIndices, | |
| ) : _iterator = _ResultIterator(statement, columnIndices) {} | |
| void close() => _iterator.close(); | |
| ClosableIterator<Row> get iterator => _iterator; | |
| } | |
| class _ResultIterator implements ClosableIterator<Row> { | |
| final StatementResource _statement; | |
| final Map<String, int> _columnIndices; | |
| Row? _currentRow; | |
| bool _closed = false; | |
| _ResultIterator(this._statement, this._columnIndices) {} | |
| bool moveNext() { | |
| if (_closed) { | |
| throw SQLiteException("The result has already been closed."); | |
| } | |
| _currentRow?._setNotCurrent(); | |
| int stepResult = _statement.step(); | |
| if (stepResult == Errors.SQLITE_ROW) { | |
| _currentRow = Row._(_statement, _columnIndices); | |
| return true; | |
| } else { | |
| close(); | |
| return false; | |
| } | |
| } | |
| Row get current { | |
| if (_closed) { | |
| throw SQLiteException("The result has already been closed."); | |
| } | |
| return _currentRow!; | |
| } | |
| void close() { | |
| _currentRow?._setNotCurrent(); | |
| _closed = true; | |
| _statement.finalize(); | |
| } | |
| } | |
| class Row { | |
| final StatementResource _statement; | |
| final Map<String, int> _columnIndices; | |
| bool _isCurrentRow = true; | |
| Row._(this._statement, this._columnIndices) {} | |
| /// Reads column [columnName]. | |
| /// | |
| /// By default it returns a dynamically typed value. If [convert] is set to | |
| /// [Convert.StaticType] the value is converted to the static type computed | |
| /// for the column by the query compiler. | |
| dynamic readColumn( | |
| String columnName, { | |
| Convert convert = Convert.DynamicType, | |
| }) { | |
| return readColumnByIndex(_columnIndices[columnName]!, convert: convert); | |
| } | |
| /// Reads column [columnName]. | |
| /// | |
| /// By default it returns a dynamically typed value. If [convert] is set to | |
| /// [Convert.StaticType] the value is converted to the static type computed | |
| /// for the column by the query compiler. | |
| dynamic readColumnByIndex( | |
| int columnIndex, { | |
| Convert convert = Convert.DynamicType, | |
| }) { | |
| _checkIsCurrentRow(); | |
| Type dynamicType; | |
| if (convert == Convert.DynamicType) { | |
| dynamicType = _typeFromCode(_statement.columnType(columnIndex)); | |
| } else { | |
| dynamicType = _typeFromText( | |
| _statement.columnDecltype(columnIndex).toDartString(), | |
| ); | |
| } | |
| switch (dynamicType) { | |
| case Type.Integer: | |
| return readColumnByIndexAsInt(columnIndex); | |
| case Type.Text: | |
| return readColumnByIndexAsText(columnIndex); | |
| case Type.Null: | |
| return null; | |
| default: | |
| } | |
| } | |
| /// Reads column [columnName] and converts to [Type.Integer] if not an | |
| /// integer. | |
| int readColumnAsInt(String columnName) { | |
| return readColumnByIndexAsInt(_columnIndices[columnName]!); | |
| } | |
| /// Reads column [columnIndex] and converts to [Type.Integer] if not an | |
| /// integer. | |
| int readColumnByIndexAsInt(int columnIndex) { | |
| _checkIsCurrentRow(); | |
| return _statement.columnInt(columnIndex); | |
| } | |
| /// Reads column [columnName] and converts to [Type.Text] if not text. | |
| String readColumnAsText(String columnName) { | |
| return readColumnByIndexAsText(_columnIndices[columnName]!); | |
| } | |
| /// Reads column [columnIndex] and converts to [Type.Text] if not text. | |
| String readColumnByIndexAsText(int columnIndex) { | |
| _checkIsCurrentRow(); | |
| return _statement.columnText(columnIndex).toDartString(); | |
| } | |
| void _checkIsCurrentRow() { | |
| if (!_isCurrentRow) { | |
| throw Exception( | |
| "This row is not the current row, reading data from the non-current" | |
| " row is not supported by sqlite.", | |
| ); | |
| } | |
| } | |
| void _setNotCurrent() { | |
| _isCurrentRow = false; | |
| } | |
| } | |
| class DatabaseResource implements Finalizable { | |
| static final NativeFinalizer _finalizer = NativeFinalizer( | |
| bindings.sqlite3_close_v2_native_return_void.cast(), | |
| ); | |
| /// [_statement] must never escape [StatementResource], otherwise the | |
| /// [_finalizer] will run prematurely. | |
| Pointer<types.Database> _database; | |
| DatabaseResource(this._database) { | |
| _finalizer.attach(this, _database.cast(), detach: this); | |
| } | |
| int close() { | |
| _finalizer.detach(this); | |
| return bindings.sqlite3_close_v2(_database); | |
| } | |
| int prepare( | |
| Utf8Resource query, | |
| int nbytes, | |
| Pointer<Pointer<Statement>> statementOut, | |
| Pointer<Pointer<Utf8>> tail, | |
| ) { | |
| int result = bindings.sqlite3_prepare_v2( | |
| _database, | |
| query.unsafe(), | |
| nbytes, | |
| statementOut, | |
| tail, | |
| ); | |
| return result; | |
| } | |
| Pointer<Utf8> errmsg() => bindings.sqlite3_errmsg(_database); | |
| } | |
| class StatementResource implements Finalizable { | |
| static final NativeFinalizer _finalizer = NativeFinalizer( | |
| bindings.sqlite3_finalize_native_return_void.cast(), | |
| ); | |
| /// [_statement] must never escape [StatementResource], otherwise the | |
| /// [_finalizer] will run prematurely. | |
| final Pointer<Statement> _statement; | |
| StatementResource(this._statement) { | |
| _finalizer.attach(this, _statement.cast(), detach: this); | |
| } | |
| int finalize() { | |
| _finalizer.detach(this); | |
| return bindings.sqlite3_finalize(_statement); | |
| } | |
| int get columnCount => bindings.sqlite3_column_count(_statement); | |
| String columnName(int index) => | |
| bindings.sqlite3_column_name(_statement, index).toDartString(); | |
| int step() => bindings.sqlite3_step(_statement); | |
| int columnType(int columnIndex) => | |
| bindings.sqlite3_column_type(_statement, columnIndex); | |
| Pointer<Utf8> columnDecltype(int columnIndex) => | |
| bindings.sqlite3_column_decltype(_statement, columnIndex); | |
| int columnInt(int columnIndex) => | |
| bindings.sqlite3_column_int(_statement, columnIndex); | |
| Pointer<Utf8> columnText(int columnIndex) => | |
| bindings.sqlite3_column_text(_statement, columnIndex); | |
| } | |
| class Utf8Resource implements Finalizable { | |
| static final NativeFinalizer _finalizer = NativeFinalizer(posixFree); | |
| /// [_cString] must never escape [Utf8Resource], otherwise the | |
| /// [_finalizer] will run prematurely. | |
| final Pointer<Utf8> _cString; | |
| Utf8Resource(this._cString) { | |
| _finalizer.attach(this, _cString.cast(), detach: this); | |
| } | |
| void free() { | |
| _finalizer.detach(this); | |
| calloc.free(_cString); | |
| } | |
| /// Ensure this [Utf8Resource] stays in scope longer than the inner resource. | |
| Pointer<Utf8> unsafe() => _cString; | |
| } | |
| final DynamicLibrary stdlib = DynamicLibrary.process(); | |
| final posixFree = stdlib.lookup<NativeFunction<Void Function(Pointer)>>("free"); | |
| Type _typeFromCode(int code) { | |
| switch (code) { | |
| case Types.SQLITE_INTEGER: | |
| return Type.Integer; | |
| case Types.SQLITE_FLOAT: | |
| return Type.Float; | |
| case Types.SQLITE_TEXT: | |
| return Type.Text; | |
| case Types.SQLITE_BLOB: | |
| return Type.Blob; | |
| case Types.SQLITE_NULL: | |
| return Type.Null; | |
| } | |
| throw Exception("Unknown type [$code]"); | |
| } | |
| Type _typeFromText(String textRepresentation) { | |
| switch (textRepresentation) { | |
| case "integer": | |
| return Type.Integer; | |
| case "float": | |
| return Type.Float; | |
| case "text": | |
| return Type.Text; | |
| case "blob": | |
| return Type.Blob; | |
| case "null": | |
| return Type.Null; | |
| } | |
| throw Exception("Unknown type [$textRepresentation]"); | |
| } | |
| enum Type { Integer, Float, Text, Blob, Null } | |
| enum Convert { DynamicType, StaticType } | |
| class SQLiteException { | |
| final String message; | |
| SQLiteException(this.message); | |
| String toString() => message; | |
| } |