| // 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 synchonously. |
| class Database { |
| late Pointer<types.Database> _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 = path.toNativeUtf8(); |
| final int resultCode = |
| bindings.sqlite3_open_v2(pathC, dbOut, flags, nullptr); |
| _database = dbOut.value; |
| calloc.free(dbOut); |
| calloc.free(pathC); |
| |
| 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 = bindings.sqlite3_close_v2(_database); |
| 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 = calloc(); |
| Pointer<Utf8> queryC = query.toNativeUtf8(); |
| int resultCode = bindings.sqlite3_prepare_v2( |
| _database, queryC, -1, statementOut, nullptr); |
| Pointer<Statement> statement = statementOut.value; |
| calloc.free(statementOut); |
| calloc.free(queryC); |
| |
| while (resultCode == Errors.SQLITE_ROW || resultCode == Errors.SQLITE_OK) { |
| resultCode = bindings.sqlite3_step(statement); |
| } |
| bindings.sqlite3_finalize(statement); |
| 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 = calloc(); |
| Pointer<Utf8> queryC = query.toNativeUtf8(); |
| int resultCode = bindings.sqlite3_prepare_v2( |
| _database, queryC, -1, statementOut, nullptr); |
| Pointer<Statement> statement = statementOut.value; |
| calloc.free(statementOut); |
| calloc.free(queryC); |
| |
| if (resultCode != Errors.SQLITE_OK) { |
| bindings.sqlite3_finalize(statement); |
| throw _loadError(resultCode); |
| } |
| |
| Map<String, int> columnIndices = {}; |
| int columnCount = bindings.sqlite3_column_count(statement); |
| for (int i = 0; i < columnCount; i++) { |
| String columnName = |
| bindings.sqlite3_column_name(statement, i).toDartString(); |
| columnIndices[columnName] = i; |
| } |
| |
| return Result._(this, statement, columnIndices); |
| } |
| |
| SQLiteException _loadError(int errorCode) { |
| String errorMessage = bindings.sqlite3_errmsg(_database).toDartString(); |
| 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, |
| Pointer<Statement> statement, |
| Map<String, int> columnIndices, |
| ) : _iterator = _ResultIterator(statement, columnIndices) {} |
| |
| void close() => _iterator.close(); |
| |
| ClosableIterator<Row> get iterator => _iterator; |
| } |
| |
| class _ResultIterator implements ClosableIterator<Row> { |
| final Pointer<Statement> _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 = bindings.sqlite3_step(_statement); |
| 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; |
| bindings.sqlite3_finalize(_statement); |
| } |
| } |
| |
| class Row { |
| final Pointer<Statement> _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(bindings.sqlite3_column_type(_statement, columnIndex)); |
| } else { |
| dynamicType = _typeFromText(bindings |
| .sqlite3_column_decltype(_statement, 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 bindings.sqlite3_column_int(_statement, 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 bindings.sqlite3_column_text(_statement, 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; |
| } |
| } |
| |
| 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; |
| } |