blob: 5bfc770cf128aca2556b77d128f5842c4a486801 [file] [log] [blame]
// 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.
// @dart = 2.9
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 {
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.");
}
if (_currentRow != null) {
_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;
}