// 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> 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> 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> 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 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 implements ClosableIterable { final ClosableIterator _iterator; Result._( Database database, StatementResource statement, Map columnIndices, ) : _iterator = _ResultIterator(statement, columnIndices) {} void close() => _iterator.close(); ClosableIterator get iterator => _iterator; } class _ResultIterator implements ClosableIterator { final StatementResource _statement; final Map _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 _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 _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> statementOut, Pointer> tail) { int result = bindings.sqlite3_prepare_v2( _database, query.unsafe(), nbytes, statementOut, tail); return result; } Pointer 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; 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 columnDecltype(int columnIndex) => bindings.sqlite3_column_decltype(_statement, columnIndex); int columnInt(int columnIndex) => bindings.sqlite3_column_int(_statement, columnIndex); Pointer 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 _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 unsafe() => _cString; } final DynamicLibrary stdlib = DynamicLibrary.process(); final posixFree = stdlib.lookup>("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; }