46ee7eb873
Closes https://github.com/dart-lang/sdk/pull/50919 TEST=ci GitOrigin-RevId: 5ac389ee109d6a06f89d405da916945c36e1d24d Change-Id: Ica078a14e87234459c0703c41c8bae5157132b14 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/278532 Reviewed-by: Michael Thomsen <mit@google.com> Reviewed-by: Slava Egorov <vegorov@google.com> Commit-Queue: Slava Egorov <vegorov@google.com>
387 lines
11 KiB
Dart
387 lines
11 KiB
Dart
// 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;
|
|
}
|