Commit 10af902f by songyanzhi

feat:sqflite

parent d56adb53
.DS_Store
.packages
.pub/
ios/.generated/
packages
pubspec.lock
# Directory created by dartdoc
doc/api/
# Local folder
.local/
# Conventional directory for build outputs
build/
coverage/
# flutter
.metadata
\ No newline at end of file
<component name="libraryTable">
<library name="Flutter Plugins" type="FlutterPluginsLibraryType">
<CLASSES />
<JAVADOC />
<SOURCES />
</library>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Android API 33, extension level 3 Platform" project-jdk-type="Android SDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
\ No newline at end of file
## 2.3.3+1
* Remove android v1 embedding support
* Use `compileSdk 34` on Android
* `sdk: >= 3.3.0`
## 2.3.2
* Shared iOS/MacOS darwin implementation
* Remove FMDB podspec dependency
## 2.3.1
* Add iOS/MacOS privacy manifest
## 2.3.0
* Dart 3 only
## 2.2.8+4
* Android: Adds a namespace for compatibility with AGP 8.0.
* Android: Use compile SDK 33
* Export global sqflite API
* iOS set minimum deployment target to 11.0
## 2.2.7
* Dart 3 support
## 2.2.6
* uri support for supported implementations.
## 2.2.5
* Fix concurrency issue in database worker pool (chriscui@google.com)
* add android `setLocale` API call support.
## 2.2.4+1
* Experimental logger support.
## 2.2.3-1
* strict-casts and sdk 2.18 support
## 2.2.2
* Fix iOS/MacOS FMDB include for non-swift project
## 2.2.1
* Allow multiple threads on Android, thanks to zhenpingcui
## 2.2.1-1
* Fix iOS/MacOS FMDB include
## 2.2.0+3
* Implements `Database.queryCursor()` and `Database.rawQueryCursor()`
* Dependency update
* Initial support of cross isolate safe
* Transaction v2 update
## 2.1.0+1
* Android: fix parameter binding for non string parameters
* Android: fix unit test
## 2.0.4-dev.1
* Android: Allow turning on WAL in the manifest.
## 2.0.3+1
* MacOS: Fix crash when an invalid number of parameters is specified in the query
## 2.0.3
* iOS/Android: Flutter 3.0 support, makes all the channel calls happen on thread pool instead of the UI thread
* iOS/MacOS: make close happen in a background thread
## 2.0.2+1
* Android build: remove jcenter, compile sdk set to 31
## 2.0.1
* Bump default android thread priority to `THREAD_PRIORITY_DEFAULT`
## 2.0.0+4
* `nnbd` support
## 1.3.2+3
* iOS/macOS: Update FMDB to 2.7.5+
* android: Update gradle to 6.5
* fix logs on iOS
## 1.3.1+2
* add `databaseFactory` setter to change the default sqflite factory.
* Fix empty Blob returned as null on MacOS/iOS
* Test using `integration_test`
## 1.3.0+2
* Add sqflite_common dependency
## 1.2.2+1
* Fix iOS warning on FMDB import
* Support pedantic 1.9
* Check arguments in debug mode (print errors only)
## 1.2.1
* Support Android embedding v2
* Add private mixin
* Support iOS/MacOS incremental build
## 1.2.0
* Add MacOS support
## 1.1.8
* support `deleteDatabase` after hot-restart. Existing, if any, single instance database ìs closed
before deletion
## 1.1.7+3
* Bump flutter/dart dependency version (1.9.1/2.5.0)
* Fix hot and warm restart for opened databases on Android
* Add code documentation, code coverage and build badges
* Fix ios example build
## 1.1.6+5
* Open database in a background thread on Android.
* Prevent database deletion on Android when opening a corrupted database in read-only.
* Fix hot restart ROLLBACK warning
* Fix indexed parameter binding on iOS
## 1.1.5
* Add `databaseExists` as a top level function
* handle relative path in `databaseExists` and `deleteDatabase`
* Supports hot-restart while in a transaction on iOS and Android by recovering the database from the
native world and executing `ROLLBACK` to prevent `SQLITE_BUSY` error
* If in a transaction, execute `ROLLBACK` before closing to prevent `SQLITE_BUSY` error
## 1.1.4
* Make all db operation happen in a separate thread on iOS
## 1.1.3
* Fix deadlock issue on iOS when using isolates
## 1.1.2
* Sqflite now uses a thread handler with a background thread priority by default on Android
## 1.1.1
* Use mixin and extract non flutter code into `sqlite_api.dart`
* Deprecate `SqfliteOptions` which is only used internally
## 1.1.0
* **Breaking change**. Migrate from the deprecated original Android Support
Library to AndroidX. This shouldn't result in any functional changes, but it
requires any Android apps using this plugin to [also
migrate](https://developer.android.com/jetpack/androidx/migrate) if they're
using the original support library.
You might say thay version should be bumped to 2.0.0, however it is just a tooling issue, code is not changed.
This is a copy of the changes made in the flutter plugins
## 1.0.0
* Upgrade 0.13.0 version as 1.0.0
* Remove deprecated API (applyBatch, apply)
## 0.13.0
* Add support for `continueOrError` for batches
## 0.12.0
* iOS objective C prefix added to prevent conflict
* on iOS create the directory of the database if it does not exist
## 0.11.2
* add `Database.isOpen` which becomes false once the database is closed
## 0.11.1
* add `Sqflite.hex` to allow querying on blob fields
## 0.11.0
* add `getDatabasesPath` to use as the base location to create a database
* Warning: database are now single instance by default (based on `path`), to use the
old behavior use `singleInstance = false` when opening a database
* dart2 stable support
## 0.10.0
* Preparing for 1.0
* Remove deprecated methods (re-entrant transactions)
* Add `Transaction.batch`
* Show developer warning to prevent deadlock
## 0.9.0
* Support for in-memory database (`:memory:` path)
* Support for single instance
* new database factory for handling the new options
## 0.8.9
* Upgrade to sdk 27
## 0.8.8
* Allow testing for constraint exception
## 0.8.6
* better sql error report
* catch android native errors
* no longer print an error when deleting a database fails
## 0.8.4
* Add read-only support using `openReadOnlyDatabase`
## 0.8.3
* Allow running a batch during a transaction using `Transaction.applyBatch`
* Restore `Batch.commit` to use outside a transaction
## 0.8.2
* Although already in a transaction, allow creating nested transactions during open
## 0.8.1
* New `Transaction` mechanism not using Zone (old one still supported for now)
* Start using `Batch.apply` instead of `Batch.commit`
* Deprecate `Database.inTransaction` and `Database.synchronized` so that Zones are not used anymore
## 0.7.1
* add `Batch.query`, `Batch.rawQuery` and `Batch.execute`
* pack query result as colums/rows instead of List<Map>
## 0.7.0
* Add support for `--preview-dart-2`
## 0.6.2+1
* Add longer description to pubspec.yaml
## 0.6.2
* Fix travis warning
## 0.6.1
* Add Flutter SDK constraint to pubspec.yaml
## 0.6.0
* add support for `onConfigure` to allow for database configuration
## 0.5.0
* Escape table and column name when needed in insert/update/query/delete
* Export ConflictAlgorithm, escapeName, unescapeName in new sql.dart
## 0.4.0
* Add support for Batch (insert/update/delete)
## 0.3.1
* Remove temp concurrency experiment
## 0.3.0
2018/01/04
* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin
3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in
order to use this version of the plugin. Instructions can be found
[here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1).
## 0.2.4
* Dependency on synchronized updated to >=1.1.0
## 0.2.3
* Make Android sends the reponse in the same thread then the caller to prevent unexpected behavior when an error occured
## 0.2.2
* Fix unchecked warning on Android
## 0.2.0
* Use NSOperationQueue for all db operation on iOS
* Use ThreadHandler for all db operation on Android
## 0.0.3
* Add exception handling
## 0.0.2
* Add sqlite helpers based on Razvan Lung suggestions
## 0.0.1
* Initial experimentation
BSD 2-Clause License
Copyright (c) 2019, Alexandre Roux Tekartik
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# sqflite
[![pub package](https://img.shields.io/pub/v/sqflite.svg)](https://pub.dev/packages/sqflite)
SQLite plugin for [Flutter](https://flutter.io).
Supports iOS, Android and MacOS.
* Support transactions and batches
* Automatic version managment during open
* Helpers for insert/query/update/delete queries
* DB operation executed in a background thread on iOS and Android
Other platforms support:
* Linux/Windows/DartVM support using [sqflite_common_ffi](https://pub.dev/packages/sqflite_common_ffi)
* Experimental Web support using [sqflite_common_ffi_web](https://pub.dev/packages/sqflite_common_ffi_web).
Usage example:
* [notepad_sqflite](https://github.com/alextekartik/flutter_app_example/tree/master/notepad_sqflite): Simple flutter notepad working on iOS/Android/Windows/linux/Mac
## Getting Started
To get started, you need to add `sqflite` to your project. Follow the steps below:
1. Open the terminal in your project root. You can do this by pressing `Alt+F12` in Android Studio or `` Ctrl+` `` in VS Code.
2. Run the following command:
```bash
flutter pub add sqflite
```
This command will add a line to your package's `pubspec.yaml` file and run an implicit `flutter pub get`. The added line will look like this:
```yaml
dependencies:
sqflite:
```
## Usage example
Import `sqflite.dart`
```dart
import 'package:sqflite/sqflite.dart';
```
### Opening a database
A SQLite database is a file in the file system identified by a path. If relative, this path is relative to the path
obtained by `getDatabasesPath()`, which is the default database directory on Android and the documents directory on iOS/MacOS.
```dart
var db = await openDatabase('my_db.db');
```
There is a basic migration mechanism to handle schema changes during opening.
Many applications use one database and would never need to close it (it will be closed when the application is
terminated). If you want to release resources, you can close the database.
```dart
await db.close();
```
* See [more information on opening a database](https://github.com/tekartik/sqflite/blob/master/sqflite/doc/opening_db.md).
* Full [migration example](https://github.com/tekartik/sqflite/blob/master/sqflite/doc/migration_example.md)
### Raw SQL queries
Demo code to perform Raw SQL queries
```dart
// Get a location using getDatabasesPath
var databasesPath = await getDatabasesPath();
String path = join(databasesPath, 'demo.db');
// Delete the database
await deleteDatabase(path);
// open the database
Database database = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
// When creating the db, create the table
await db.execute(
'CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)');
});
// Insert some records in a transaction
await database.transaction((txn) async {
int id1 = await txn.rawInsert(
'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');
print('inserted1: $id1');
int id2 = await txn.rawInsert(
'INSERT INTO Test(name, value, num) VALUES(?, ?, ?)',
['another name', 12345678, 3.1416]);
print('inserted2: $id2');
});
// Update some record
int count = await database.rawUpdate(
'UPDATE Test SET name = ?, value = ? WHERE name = ?',
['updated name', '9876', 'some name']);
print('updated: $count');
// Get the records
List<Map> list = await database.rawQuery('SELECT * FROM Test');
List<Map> expectedList = [
{'name': 'updated name', 'id': 1, 'value': 9876, 'num': 456.789},
{'name': 'another name', 'id': 2, 'value': 12345678, 'num': 3.1416}
];
print(list);
print(expectedList);
assert(const DeepCollectionEquality().equals(list, expectedList));
// Count the records
count = Sqflite
.firstIntValue(await database.rawQuery('SELECT COUNT(*) FROM Test'));
assert(count == 2);
// Delete a record
count = await database
.rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);
assert(count == 1);
// Close the database
await database.close();
```
Basic information on SQL [here](https://github.com/tekartik/sqflite/blob/master/sqflite/doc/sql.md).
### SQL helpers
Example using the helpers
```dart
final String tableTodo = 'todo';
final String columnId = '_id';
final String columnTitle = 'title';
final String columnDone = 'done';
class Todo {
int id;
String title;
bool done;
Map<String, Object?> toMap() {
var map = <String, Object?>{
columnTitle: title,
columnDone: done == true ? 1 : 0
};
if (id != null) {
map[columnId] = id;
}
return map;
}
Todo();
Todo.fromMap(Map<String, Object?> map) {
id = map[columnId];
title = map[columnTitle];
done = map[columnDone] == 1;
}
}
class TodoProvider {
Database db;
Future open(String path) async {
db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db.execute('''
create table $tableTodo (
$columnId integer primary key autoincrement,
$columnTitle text not null,
$columnDone integer not null)
''');
});
}
Future<Todo> insert(Todo todo) async {
todo.id = await db.insert(tableTodo, todo.toMap());
return todo;
}
Future<Todo> getTodo(int id) async {
List<Map> maps = await db.query(tableTodo,
columns: [columnId, columnDone, columnTitle],
where: '$columnId = ?',
whereArgs: [id]);
if (maps.length > 0) {
return Todo.fromMap(maps.first);
}
return null;
}
Future<int> delete(int id) async {
return await db.delete(tableTodo, where: '$columnId = ?', whereArgs: [id]);
}
Future<int> update(Todo todo) async {
return await db.update(tableTodo, todo.toMap(),
where: '$columnId = ?', whereArgs: [todo.id]);
}
Future close() async => db.close();
}
```
### Read results
Assuming the following read results:
```dart
List<Map<String, Object?>> records = await db.query('my_table');
```
Resulting map items are read-only
```dart
// get the first record
Map<String, Object?> mapRead = records.first;
// Update it in memory...this will throw an exception
mapRead['my_column'] = 1;
// Crash... `mapRead` is read-only
```
You need to create a new map if you want to modify it in memory:
```dart
// get the first record
Map<String, Object?> map = Map<String, Object?>.from(mapRead);
// Update it in memory now
map['my_column'] = 1;
```
### Transaction
Don't use the database but only use the Transaction object in a transaction
to access the database. Keep in mind that the callbacks ```onCreate``` ```onUpgrade``` ```onDowngrade``` are already internally wrapped in a transaction, so there is no need to wrap your statements in a transaction within those callbacks.
```dart
await database.transaction((txn) async {
// Ok
await txn.execute('CREATE TABLE Test1 (id INTEGER PRIMARY KEY)');
// DON'T use the database object in a transaction
// this will deadlock!
await database.execute('CREATE TABLE Test2 (id INTEGER PRIMARY KEY)');
});
```
A transaction is committed if the callback does not throw an error. If an error is thrown,
the transaction is cancelled. So to rollback a transaction one way is to throw an exception.
### Batch support
To avoid ping-pong between dart and native code, you can use `Batch`:
```dart
batch = db.batch();
batch.insert('Test', {'name': 'item'});
batch.update('Test', {'name': 'new_item'}, where: 'name = ?', whereArgs: ['item']);
batch.delete('Test', where: 'name = ?', whereArgs: ['item']);
results = await batch.commit();
```
Getting the result for each operation has a cost (id for insertion and number of changes for
update and delete), especially on Android where an extra SQL request is executed.
If you don't care about the result and worry about performance in big batches, you can use
```dart
await batch.commit(noResult: true);
```
Warning, during a transaction, the batch won't be committed until the transaction is committed
```dart
await database.transaction((txn) async {
var batch = txn.batch();
// ...
// commit but the actual commit will happen when the transaction is committed
// however the data is available in this transaction
await batch.commit();
// ...
});
```
By default a batch stops as soon as it encounters an error (which typically reverts the uncommitted changes). You
can ignore errors so that every successfull operation is ran and committed even if one operation fails:
```dart
await batch.commit(continueOnError: true);
```
## Table and column names
In general it is better to avoid using SQLite keywords for entity names. If any of the following
name is used:
"add","all","alter","and","as","autoincrement","between","case","check","collate","commit","constraint","create","default","deferrable","delete","distinct","drop","else","escape","except","exists","foreign","from","group","having","if","in","index","insert","intersect","into","is","isnull","join","limit","not","notnull","null","on","or","order","primary","references","select","set","table","then","to","transaction","union","unique","update","using","values","when","where"
the helper will *escape* the name i.e.
```dart
db.query('table')
```
will be equivalent to manually adding double-quote around the table name (confusingly here named `table`)
```dart
db.rawQuery('SELECT * FROM "table"');
```
However in any other raw statement (including `orderBy`, `where`, `groupBy`), make sure to escape the name
properly using double quote. For example see below where the column name `group` is not escaped in the columns
argument, but is escaped in the `where` argument.
```dart
db.query('table', columns: ['group'], where: '"group" = ?', whereArgs: ['my_group']);
```
## Supported SQLite types
No validity check is done on values yet so please avoid non supported types [https://www.sqlite.org/datatype3.html](https://www.sqlite.org/datatype3.html)
`DateTime` is not a supported SQLite type. Personally I store them as
int (millisSinceEpoch) or string (iso8601)
`bool` is not a supported SQLite type. Use `INTEGER` and 0 and 1 values.
More information on supported types [here](https://github.com/tekartik/sqflite/blob/master/sqflite/doc/supported_types.md).
### INTEGER
* Dart type: `int`
* Supported values: from -2^63 to 2^63 - 1
### REAL
* Dart type: `num`
### TEXT
* Dart type: `String`
### BLOB
* Dart type: `Uint8List`
## Current issues
* Due to the way transaction works in SQLite (threads), concurrent read and write transaction are not supported.
All calls are currently synchronized and transactions block are exclusive. I thought that a basic way to support
concurrent access is to open a database multiple times but it only works on iOS as Android reuses the same database object.
I also thought a native thread could be a potential future solution however on android accessing the database in another
thread is blocked while in a transaction...
## More
* [How to](https://github.com/tekartik/sqflite/blob/master/sqflite/doc/how_to.md) guide
* [Notes about Desktop support](https://github.com/tekartik/sqflite/blob/master/sqflite/doc/desktop_support.md)
* [Notes about Encryption support](https://github.com/tekartik/sqflite/blob/master/sqflite/doc/encryption_support.md)
* [Notes about Web support](https://github.com/tekartik/sqflite/blob/master/sqflite/doc/web_support.md)
include: package:flutter_lints/flutter.yaml
# Until there are meta linter rules, each desired lint must be explicitly enabled.
# See: https://github.com/dart-lang/linter/issues/288
#
# For a list of lints, see: http://dart-lang.github.io/linter/lints/
# See the configuration guide for more
# https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer
#
# NOTE: Please keep this file in sync with
# https://github.com/flutter/flutter/blob/master/analysis_options.yaml
analyzer:
language:
strict-casts: true
strict-inference: true
errors:
# treat missing required parameters as a warning (not a hint)
missing_required_param: warning
# allow having TODOs in the code
todo: ignore
# Ignore errors like
# 'super_goes_last' is a deprecated lint rule and should not be used • included_file_warning
included_file_warning: ignore
linter:
rules:
- always_declare_return_types
- avoid_dynamic_calls
- avoid_empty_else
- avoid_relative_lib_imports
- avoid_shadowing_type_parameters
- avoid_slow_async_io
- avoid_types_as_parameter_names
- await_only_futures
- camel_case_extensions
- camel_case_types
- cancel_subscriptions
- curly_braces_in_flow_control_structures
- directives_ordering
- empty_catches
- hash_and_equals
- collection_methods_unrelated_type
- no_adjacent_strings_in_list
- no_duplicate_case_values
- non_constant_identifier_names
- omit_local_variable_types
- package_api_docs
- package_prefixed_library_names
- prefer_generic_function_type_aliases
- prefer_is_empty
- prefer_is_not_empty
- prefer_iterable_whereType
- prefer_single_quotes
- prefer_typing_uninitialized_variables
- sort_child_properties_last
- test_types_in_equals
- throw_in_finally
- unawaited_futures
- unnecessary_null_aware_assignments
- unnecessary_statements
- unrelated_type_equality_checks
- unsafe_html
- valid_regexps
- constant_identifier_names
- control_flow_in_finally
- empty_statements
- implementation_imports
- overridden_fields
- package_names
- prefer_const_constructors
- prefer_initializing_formals
- prefer_void_to_null
#
- annotate_overrides
- avoid_init_to_null
- avoid_null_checks_in_equality_operators
- avoid_return_types_on_setters
- empty_constructor_bodies
- library_names
- library_prefixes
- prefer_adjacent_string_concatenation
- prefer_collection_literals
- prefer_contains
- slash_for_doc_comments
- type_init_formals
- unnecessary_const
- unnecessary_new
- unnecessary_null_in_if_null_operators
- use_rethrow_when_possible
- dangling_library_doc_comments
- deprecated_member_use_from_same_package
- implicit_reopen
- invalid_case_patterns
- no_literal_bool_comparisons
- no_self_assignments
- no_wildcard_variable_uses
- type_literal_in_constant_pattern
# === doc rules ===
- public_member_api_docs
#
- prefer_final_locals
- sort_constructors_first
- sort_unnamed_constructors_first
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
gradle-wrapper.jar
/gradlew
/gradlew.bat
group 'com.tekartik.sqflite'
version '1.0-SNAPSHOT'
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
allprojects {
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
}
}
}
apply plugin: 'com.android.library'
android {
// Conditional for compatibility with AGP <4.2.
if (project.android.hasProperty("namespace")) {
namespace 'com.tekartik.sqflite'
}
compileSdk 34
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdk 16
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
disable 'InvalidPackage'
}
}
dependencies {
testImplementation 'junit:junit:4.13.2'
}
org.gradle.jvmargs=-Xmx1536M
#Fri Apr 05 14:03:42 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
rootProject.name = 'sqflite'
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tekartik.sqflite">
</manifest>
package com.tekartik.sqflite;
/**
* Constants between dart & Java world
*/
public class Constant {
// Can be used as the name MethodChannel or to register with
static final public String PLUGIN_KEY = "com.tekartik.sqflite";
static final public String METHOD_GET_PLATFORM_VERSION = "getPlatformVersion";
static final public String METHOD_GET_DATABASES_PATH = "getDatabasesPath";
static final public String METHOD_DEBUG = "debug";
static final public String METHOD_OPTIONS = "options";
static final public String METHOD_OPEN_DATABASE = "openDatabase";
static final public String METHOD_CLOSE_DATABASE = "closeDatabase";
static final public String METHOD_INSERT = "insert";
static final public String METHOD_EXECUTE = "execute";
static final public String METHOD_QUERY = "query";
static final public String METHOD_QUERY_CURSOR_NEXT = "queryCursorNext";
static final public String METHOD_UPDATE = "update";
static final public String METHOD_BATCH = "batch";
static final public String METHOD_DELETE_DATABASE = "deleteDatabase";
static final public String METHOD_DATABASE_EXISTS = "databaseExists";
// true when entering, false when leaving, null otherwise, should be named inTransactionChange instead
public static final String PARAM_IN_TRANSACTION_CHANGE = "inTransaction";
// Set for calls within a transaction
public static final String PARAM_TRANSACTION_ID = "transactionId";
// Special transaction id used for recovering a locked database.
public static final int TRANSACTION_ID_FORCE = -1;
// Result when opening a database
public static final String PARAM_RECOVERED = "recovered";
// Result when opening a database
public static final String PARAM_RECOVERED_IN_TRANSACTION = "recoveredInTransaction";
public static final String PARAM_SQL = "sql";
public static final String PARAM_SQL_ARGUMENTS = "arguments";
public static final String PARAM_NO_RESULT = "noResult";
public static final String PARAM_CONTINUE_OR_ERROR = "continueOnError";
public static final String PARAM_COLUMNS = "columns";
public static final String PARAM_ROWS = "rows";
// For query to use a cursor. Integer.
public static final String PARAM_CURSOR_PAGE_SIZE = "cursorPageSize";
// For queryCursorNext. Integer
public static final String PARAM_CURSOR_ID = "cursorId";
// For queryCursorNext. Boolean
public static final String PARAM_CANCEL = "cancel";
// in each operation
public static final String PARAM_METHOD = "method";
// Batch operation results
public static final String PARAM_RESULT = "result";
public static final String PARAM_ERROR = "error"; // map with code/message/data
public static final String PARAM_ERROR_CODE = "code";
public static final String PARAM_ERROR_MESSAGE = "message";
public static final String PARAM_ERROR_DATA = "data";
// android log tag
static final public String TAG = "Sqflite";
// Obsolete since 1.17
static final public String METHOD_DEBUG_MODE = "debugMode";
static final public String METHOD_ANDROID_SET_LOCALE = "androidSetLocale";
// Locale tag
static final String PARAM_LOCALE = "locale";
public static final String[] EMPTY_STRING_ARRAY = new String[0];
static final String PARAM_ID = "id";
static final String PARAM_PATH = "path";
// when opening a database
static final String PARAM_READ_ONLY = "readOnly"; // boolean
static final String PARAM_SINGLE_INSTANCE = "singleInstance"; // boolean
static final String PARAM_LOG_LEVEL = "logLevel"; // int
static final String PARAM_THREAD_PRIORITY = "androidThreadPriority"; // int
static final String PARAM_THREAD_COUNT = "androidThreadCount"; // int
// debugMode
static final String PARAM_CMD = "cmd"; // debugMode cmd: get/set
static final String CMD_GET = "get";
// in batch
static final String PARAM_OPERATIONS = "operations";
static final String SQLITE_ERROR = "sqlite_error"; // code
static final String ERROR_BAD_PARAM = "bad_param"; // internal only
static final String ERROR_OPEN_FAILED = "open_failed"; // msg
static final String ERROR_DATABASE_CLOSED = "database_closed"; // msg
// memory database path
static final String MEMORY_DATABASE_PATH = ":memory:";
}
package com.tekartik.sqflite;
import static com.tekartik.sqflite.Constant.EMPTY_STRING_ARRAY;
import static com.tekartik.sqflite.Constant.ERROR_BAD_PARAM;
import static com.tekartik.sqflite.Constant.METHOD_EXECUTE;
import static com.tekartik.sqflite.Constant.METHOD_INSERT;
import static com.tekartik.sqflite.Constant.METHOD_QUERY;
import static com.tekartik.sqflite.Constant.METHOD_UPDATE;
import static com.tekartik.sqflite.Constant.PARAM_CANCEL;
import static com.tekartik.sqflite.Constant.PARAM_COLUMNS;
import static com.tekartik.sqflite.Constant.PARAM_CURSOR_ID;
import static com.tekartik.sqflite.Constant.PARAM_CURSOR_PAGE_SIZE;
import static com.tekartik.sqflite.Constant.PARAM_OPERATIONS;
import static com.tekartik.sqflite.Constant.PARAM_ROWS;
import static com.tekartik.sqflite.Constant.PARAM_TRANSACTION_ID;
import static com.tekartik.sqflite.Constant.TAG;
import static com.tekartik.sqflite.Constant.TRANSACTION_ID_FORCE;
import static com.tekartik.sqflite.Utils.cursorRowToList;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.DatabaseErrorHandler;
import android.database.SQLException;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
import android.database.sqlite.SQLiteCursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.tekartik.sqflite.operation.BatchOperation;
import com.tekartik.sqflite.operation.MethodCallOperation;
import com.tekartik.sqflite.operation.Operation;
import com.tekartik.sqflite.operation.QueuedOperation;
import com.tekartik.sqflite.operation.SqlErrorInfo;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
class Database {
// To turn on when supported fully
// 2022-09-14 experiments show several corruption issue.
final static boolean WAL_ENABLED_BY_DEFAULT = false;
private static final String WAL_ENABLED_META_NAME = "com.tekartik.sqflite.wal_enabled";
static private Boolean walGloballyEnabled;
final boolean singleInstance;
@NonNull
final String path;
final int id;
final int logLevel;
@NonNull
final Context context;
/// Delayed operations not in the current transaction.
final List<QueuedOperation> noTransactionOperationQueue = new ArrayList<>();
final Map<Integer, SqfliteCursor> cursors = new HashMap<>();
// Set by plugin
public DatabaseWorkerPool databaseWorkerPool;
@Nullable
SQLiteDatabase sqliteDatabase;
private int transactionDepth = 0;
// Transaction
private int lastTransactionId = 0; // incremental transaction id
@Nullable
private Integer currentTransactionId;
// Cursors
private int lastCursorId = 0; // incremental cursor id
Database(Context context, String path, int id, boolean singleInstance, int logLevel) {
this.context = context;
this.path = path;
this.singleInstance = singleInstance;
this.id = id;
this.logLevel = logLevel;
}
@VisibleForTesting
@NotNull
static protected boolean checkWalEnabled(Context context) {
return checkMetaBoolean(context, WAL_ENABLED_META_NAME, WAL_ENABLED_BY_DEFAULT);
}
@SuppressWarnings("deprecation")
static ApplicationInfo getApplicationInfoWithMeta32(Context context, String packageName, int flags) throws PackageManager.NameNotFoundException {
return context.getPackageManager().getApplicationInfo(packageName, flags);
}
@VisibleForTesting
@NotNull
static protected boolean checkMetaBoolean(Context context, String metaKey, boolean defaultValue) {
try {
final String packageName = context.getPackageName();
ApplicationInfo applicationInfo;
applicationInfo = getApplicationInfoWithMeta32(context, packageName, PackageManager.GET_META_DATA);
final boolean walEnabled = applicationInfo.metaData.getBoolean(metaKey, defaultValue);
if (walEnabled) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
static void deleteDatabase(String path) {
SQLiteDatabase.deleteDatabase(new File(path));
}
/**
* Never fails
*/
public static boolean existsDatabase(String path) {
boolean exists = false;
try {
exists = new File(path).exists();
} catch (Exception ignore) {
}
return exists;
}
public void open() {
int flags = SQLiteDatabase.CREATE_IF_NECESSARY;
// Check meta data only once
if (walGloballyEnabled == null) {
walGloballyEnabled = checkWalEnabled(context);
if (walGloballyEnabled) {
if (LogLevel.hasVerboseLevel(logLevel)) {
Log.d(TAG, getThreadLogPrefix() + "[sqflite] WAL enabled");
}
}
}
if (walGloballyEnabled) {
// Turned on since 2.1.0-dev.1
flags |= SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING;
}
sqliteDatabase = SQLiteDatabase.openDatabase(path, null, flags);
}
// Change default error handler to avoid erasing the existing file.
public void openReadOnly() {
sqliteDatabase = SQLiteDatabase.openDatabase(path, null,
SQLiteDatabase.OPEN_READONLY, new DatabaseErrorHandler() {
@Override
public void onCorruption(SQLiteDatabase dbObj) {
// ignored
// default implementation delete the file
//
// This happens asynchronously so cannot be tracked. However a simple
// access should fail
}
});
}
public void close() {
if (!cursors.isEmpty()) {
if (LogLevel.hasSqlLevel(logLevel)) {
Log.d(TAG, getThreadLogPrefix() + cursors.size() + " cursor(s) are left opened");
}
}
sqliteDatabase.close();
}
public SQLiteDatabase getWritableDatabase() {
return sqliteDatabase;
}
public SQLiteDatabase getReadableDatabase() {
return sqliteDatabase;
}
public boolean enableWriteAheadLogging() {
try {
return sqliteDatabase.enableWriteAheadLogging();
} catch (Exception e) {
Log.e(TAG, getThreadLogPrefix() + "enable WAL error: " + e);
return false;
}
}
String getThreadLogTag() {
Thread thread = Thread.currentThread();
return id + "," + thread.getName() + "(" + thread.getId() + ")";
}
String getThreadLogPrefix() {
return "[" + getThreadLogTag() + "] ";
}
private Map<String, Object> cursorToResults(Cursor cursor, @Nullable Integer cursorPageSize) {
Map<String, Object> results = null;
List<List<Object>> rows = null;
int columnCount = 0;
while (cursor.moveToNext()) {
if (results == null) {
rows = new ArrayList<>();
results = new HashMap<>();
columnCount = cursor.getColumnCount();
results.put(PARAM_COLUMNS, Arrays.asList(cursor.getColumnNames()));
results.put(PARAM_ROWS, rows);
}
rows.add(cursorRowToList(cursor, columnCount));
// Paging support
if (cursorPageSize != null) {
if (rows.size() >= cursorPageSize) {
break;
}
}
}
// Handle empty
if (results == null) {
results = new HashMap<>();
}
return results;
}
private void runQueuedOperations() {
while (!noTransactionOperationQueue.isEmpty()) {
if (currentTransactionId != null) {
break;
}
QueuedOperation queuedOperation = noTransactionOperationQueue.get(0);
queuedOperation.run();
noTransactionOperationQueue.remove(0);
}
}
private void wrapSqlOperationHandler(final @NonNull Operation operation, Runnable r) {
Integer transactionId = operation.getTransactionId();
if (currentTransactionId == null) {
// ignore transactionId, could be null or -1 or something else if closed...
r.run();
} else if (transactionId != null && (transactionId.equals(currentTransactionId) || transactionId == TRANSACTION_ID_FORCE)) {
r.run();
// run queued action asynchronously
if (currentTransactionId == null && !noTransactionOperationQueue.isEmpty()) {
databaseWorkerPool.post(this, this::runQueuedOperations);
}
} else {
// Queue for later
QueuedOperation queuedOperation = new QueuedOperation(operation, r);
noTransactionOperationQueue.add(queuedOperation);
}
}
public void query(final @NonNull Operation operation) {
wrapSqlOperationHandler(operation, () -> doQuery(operation));
}
private boolean doQuery(final @NonNull Operation operation) {
// Non null means dealing with saved cursor.
Integer cursorPageSize = operation.getArgument(PARAM_CURSOR_PAGE_SIZE);
boolean cursorHasMoreData = false;
final SqlCommand command = operation.getSqlCommand();
// Might be created if reading by page and result don't fit
SqfliteCursor sqfliteCursor = null;
if (LogLevel.hasSqlLevel(logLevel)) {
Log.d(TAG, getThreadLogPrefix() + command);
}
Cursor cursor = null;
try {
cursor = getReadableDatabase().rawQueryWithFactory(
(sqLiteDatabase, sqLiteCursorDriver, editTable, sqLiteQuery) -> {
command.bindTo(sqLiteQuery);
return new SQLiteCursor(sqLiteCursorDriver, editTable, sqLiteQuery);
}, command.getSql(), EMPTY_STRING_ARRAY, null);
Map<String, Object> results = cursorToResults(cursor, cursorPageSize);
if (cursorPageSize != null) {
// We'll have potentially more data to fetch
cursorHasMoreData = !(cursor.isLast() || cursor.isAfterLast());
}
if (cursorHasMoreData) {
int cursorId = ++lastCursorId;
results.put(PARAM_CURSOR_ID, cursorId);
sqfliteCursor = new SqfliteCursor(cursorId, cursorPageSize, cursor);
cursors.put(cursorId, sqfliteCursor);
}
operation.success(results);
return true;
} catch (Exception exception) {
handleException(exception, operation);
// Cleanup
if (sqfliteCursor != null) {
closeCursor(sqfliteCursor);
}
return false;
} finally {
// Close the cursor for non-paged query
if (sqfliteCursor == null) {
if (cursor != null) {
cursor.close();
}
}
}
}
public void queryCursorNext(final @NonNull Operation operation) {
wrapSqlOperationHandler(operation, () -> doQueryCursorNext(operation));
}
private boolean doQueryCursorNext(final @NonNull Operation operation) {
// Non null means dealing with saved cursor.
int cursorId = operation.getArgument(PARAM_CURSOR_ID);
boolean cancel = Boolean.TRUE.equals(operation.getArgument(PARAM_CANCEL));
if (LogLevel.hasVerboseLevel(logLevel)) {
Log.d(TAG, getThreadLogPrefix() + "cursor " + cursorId + (cancel ? " cancel" : " next"));
}
if (cancel) {
closeCursor(cursorId);
operation.success(null);
return true;
}
SqfliteCursor sqfliteCursor = cursors.get(cursorId);
boolean cursorHasMoreData = false;
try {
if (sqfliteCursor == null) {
throw new IllegalStateException("Cursor " + cursorId + " not found");
}
Cursor cursor = sqfliteCursor.cursor;
Map<String, Object> results = cursorToResults(cursor, sqfliteCursor.pageSize);
// We'll have potentially more data to fetch
cursorHasMoreData = !(cursor.isLast() || cursor.isAfterLast());
if (cursorHasMoreData) {
// Keep the cursor Id in the response to specify that we have more data
results.put(PARAM_CURSOR_ID, cursorId);
}
operation.success(results);
return true;
} catch (Exception exception) {
handleException(exception, operation);
// Cleanup
if (sqfliteCursor != null) {
closeCursor(sqfliteCursor);
sqfliteCursor = null;
}
return false;
} finally {
// Close the cursor if we don't have any more data
if (!cursorHasMoreData) {
if (sqfliteCursor != null) {
closeCursor(sqfliteCursor);
}
}
}
}
private void closeCursor(@NonNull SqfliteCursor sqfliteCursor) {
try {
int cursorId = sqfliteCursor.cursorId;
if (LogLevel.hasVerboseLevel(logLevel)) {
Log.d(TAG, getThreadLogPrefix() + "closing cursor " + cursorId);
}
cursors.remove(cursorId);
sqfliteCursor.cursor.close();
} catch (Exception ignore) {
}
}
// No exception thrown here
private void closeCursor(int cursorId) {
SqfliteCursor sqfliteCursor = cursors.get(cursorId);
if (sqfliteCursor != null) {
closeCursor(sqfliteCursor);
}
}
void handleException(Exception exception, Operation operation) {
if (exception instanceof SQLiteCantOpenDatabaseException) {
operation.error(Constant.SQLITE_ERROR, Constant.ERROR_OPEN_FAILED + " " + path, null);
return;
} else if (exception instanceof SQLException) {
operation.error(Constant.SQLITE_ERROR, exception.getMessage(), SqlErrorInfo.getMap(operation));
return;
}
operation.error(Constant.SQLITE_ERROR, exception.getMessage(), SqlErrorInfo.getMap(operation));
}
// Called during batch, warning duplicated code!
private boolean executeOrError(Operation operation) {
SqlCommand command = operation.getSqlCommand();
if (LogLevel.hasSqlLevel(logLevel)) {
Log.d(TAG, getThreadLogPrefix() + command);
}
Boolean operationInTransaction = operation.getInTransactionChange();
try {
getWritableDatabase().execSQL(command.getSql(), command.getSqlArguments());
enterOrLeaveInTransaction(operationInTransaction);
return true;
} catch (Exception exception) {
handleException(exception, operation);
return false;
}
}
/**
* Handle inTransactionChange
*
* @param operation
* @return
*/
public void execute(final @NonNull Operation operation) {
wrapSqlOperationHandler(operation, () -> {
Boolean inTransactionChange = operation.getInTransactionChange();
// Transaction v2 support
boolean enteringTransaction = Boolean.TRUE.equals(inTransactionChange) && operation.hasNullTransactionId();
if (enteringTransaction) {
currentTransactionId = ++lastTransactionId;
}
if (!executeOrError(operation)) {
// Revert if needed
if (enteringTransaction) {
currentTransactionId = null;
}
} else if (enteringTransaction) {
/// Return the transaction id
Map<String, Object> result = new HashMap<>();
result.put(PARAM_TRANSACTION_ID, currentTransactionId);
operation.success(result);
} else {
if (Boolean.FALSE.equals(inTransactionChange)) {
// We are leaving our current transaction
currentTransactionId = null;
}
operation.success(null);
}
});
}
// Return true on success
private boolean doExecute(final Operation operation) {
if (!executeOrError(operation)) {
return false;
}
operation.success(null);
return true;
}
public void insert(final Operation operation) {
wrapSqlOperationHandler(operation, () -> doInsert(operation));
}
// Return true on success
private boolean doInsert(final Operation operation) {
if (!executeOrError(operation)) {
return false;
}
// don't get last id if not expected
if (operation.getNoResult()) {
operation.success(null);
return true;
}
Cursor cursor = null;
// Read both the changes and last insert row id in on sql call
String sql = "SELECT changes(), last_insert_rowid()";
// Handle ON CONFLICT but ignore error, issue #164
// Read the number of changes before getting the inserted id
try {
SQLiteDatabase db = getWritableDatabase();
cursor = db.rawQuery(sql, null);
if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
final int changed = cursor.getInt(0);
// If the change count is 0, assume the insert failed
// and return null
if (changed == 0) {
if (LogLevel.hasSqlLevel(logLevel)) {
Log.d(TAG, getThreadLogPrefix() + "no changes (id was " + cursor.getLong(1) + ")");
}
operation.success(null);
return true;
} else {
final long id = cursor.getLong(1);
if (LogLevel.hasSqlLevel(logLevel)) {
Log.d(TAG, getThreadLogPrefix() + "inserted " + id);
}
operation.success(id);
return true;
}
} else {
Log.e(TAG, getThreadLogPrefix() + "fail to read changes for Insert");
}
operation.success(null);
return true;
} catch (Exception exception) {
handleException(exception, operation);
return false;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
public void update(final @NonNull Operation operation) {
wrapSqlOperationHandler(operation, () -> doUpdate(operation));
}
// Return true on success
private boolean doUpdate(final Operation operation) {
if (!executeOrError(operation)) {
return false;
}
// don't get last id if not expected
if (operation.getNoResult()) {
operation.success(null);
return true;
}
Cursor cursor = null;
try {
SQLiteDatabase db = getWritableDatabase();
cursor = db.rawQuery("SELECT changes()", null);
if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
final int changed = cursor.getInt(0);
if (LogLevel.hasSqlLevel(logLevel)) {
Log.d(TAG, getThreadLogPrefix() + "changed " + changed);
}
operation.success(changed);
return true;
} else {
Log.e(TAG, getThreadLogPrefix() + "fail to read changes for Update/Delete");
}
operation.success(null);
return true;
} catch (Exception e) {
handleException(e, operation);
return false;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
void batch(final MethodCall call, final MethodChannel.Result result) {
MethodCallOperation mainOperation = new MethodCallOperation(call, result);
boolean noResult = mainOperation.getNoResult();
boolean continueOnError = mainOperation.getContinueOnError();
List<Map<String, Object>> operations = mainOperation.getArgument(PARAM_OPERATIONS);
List<Map<String, Object>> results = new ArrayList<>();
//devLog(TAG, "operations " + operations);
for (Map<String, Object> map : operations) {
//devLog(TAG, "map " + map);
BatchOperation operation = new BatchOperation(map, noResult);
String method = operation.getMethod();
switch (method) {
case METHOD_EXECUTE:
if (doExecute(operation)) {
//devLog(TAG, "results: " + operation.getBatchResults());
operation.handleSuccess(results);
} else if (continueOnError) {
operation.handleErrorContinue(results);
} else {
// we stop at the first error
operation.handleError(result);
return;
}
break;
case METHOD_INSERT:
if (doInsert(operation)) {
//devLog(TAG, "results: " + operation.getBatchResults());
operation.handleSuccess(results);
} else if (continueOnError) {
operation.handleErrorContinue(results);
} else {
// we stop at the first error
operation.handleError(result);
return;
}
break;
case METHOD_QUERY:
if (doQuery(operation)) {
//devLog(TAG, "results: " + operation.getBatchResults());
operation.handleSuccess(results);
} else if (continueOnError) {
operation.handleErrorContinue(results);
} else {
// we stop at the first error
operation.handleError(result);
return;
}
break;
case METHOD_UPDATE:
if (doUpdate(operation)) {
//devLog(TAG, "results: " + operation.getBatchResults());
operation.handleSuccess(results);
} else if (continueOnError) {
operation.handleErrorContinue(results);
} else {
// we stop at the first error
operation.handleError(result);
return;
}
break;
default:
result.error(ERROR_BAD_PARAM, "Batch method '" + method + "' not supported", null);
return;
}
}
// Set the results of all operations
// devLog(TAG, "results " + results);
if (noResult) {
result.success(null);
} else {
result.success(results);
}
}
synchronized boolean isInTransaction() {
return transactionDepth > 0;
}
synchronized void enterOrLeaveInTransaction(Boolean value) {
if (Boolean.TRUE.equals(value)) {
transactionDepth++;
} else if (Boolean.FALSE.equals(value)) {
transactionDepth--;
}
}
}
package com.tekartik.sqflite;
import androidx.annotation.Nullable;
interface DatabaseDelegate {
int getDatabaseId();
boolean isInTransaction();
}
final class DatabaseTask {
// Database this task will be run on.
//
// It can be null if the task is not running on any database. e.g. closing a NULL database.
@Nullable
private final DatabaseDelegate database;
final Runnable runnable;
DatabaseTask(DatabaseDelegate database, Runnable runnable) {
this.database = database;
this.runnable = runnable;
}
public boolean isInTransaction() {
return database != null && database.isInTransaction();
}
public Integer getDatabaseId() {
return database != null ? database.getDatabaseId() : null;
}
}
package com.tekartik.sqflite;
import android.os.Handler;
import android.os.HandlerThread;
/**
* Worker that accepts {@link DatabaseTask}.
*
* <p>Each worker instance run on one thread.
*/
class DatabaseWorker {
private final String name;
private final int priority;
private HandlerThread handlerThread;
private Handler handler;
protected Runnable onIdle;
private DatabaseTask lastTask;
DatabaseWorker(String name, int priority) {
this.name = name;
this.priority = priority;
}
synchronized void start(Runnable onIdle) {
handlerThread = new HandlerThread(name, priority);
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
this.onIdle = onIdle;
}
synchronized void quit() {
if (handlerThread != null) {
handlerThread.quit();
handlerThread = null;
handler = null;
}
}
boolean isLastTaskInTransaction() {
return lastTask != null && lastTask.isInTransaction();
}
Integer lastTaskDatabaseId() {
return lastTask != null ? lastTask.getDatabaseId() : null;
}
void postTask(final DatabaseTask task) {
handler.post(() -> this.work(task));
}
void work(DatabaseTask task) {
task.runnable.run();
lastTask = task;
onIdle.run();
}
}
package com.tekartik.sqflite;
import android.os.Handler;
import android.os.HandlerThread;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
/**
* Pool that assigns {@link DatabaseTask} to {@link DatabaseWorker}.
*/
public interface DatabaseWorkerPool {
void start();
void quit();
// Posts a new task.
//
// Some rules for assigning a task to a worker.
// - All tasks in a transaction go to the same worker. Otherwise errors will happen.
// - All tasks belonging to the same database run in FIFO manner. No overlapping between any
// two tasks.
// - Tasks belonging to different databases could be run simultaneously but not necessarily
// in FIFO manner.
void post(DatabaseTask task);
default void post(Database database, Runnable runnable) {
DatabaseDelegate delegate = database == null ? null : new DatabaseDelegate() {
@Override
public int getDatabaseId() {
return database.id;
}
@Override
public boolean isInTransaction() {
return database.isInTransaction();
}
};
this.post(new DatabaseTask(delegate, runnable));
}
static DatabaseWorkerPool create(String name, int numberOfWorkers, int priority) {
if (numberOfWorkers == 1) {
return new SingleDatabaseWorkerPoolImpl(name, priority);
}
return new DatabaseWorkerPoolImpl(name, numberOfWorkers, priority);
}
}
class SingleDatabaseWorkerPoolImpl implements DatabaseWorkerPool {
final String name;
final int priority;
private HandlerThread handlerThread;
private Handler handler;
SingleDatabaseWorkerPoolImpl(String name, int priority) {
this.name = name;
this.priority = priority;
}
@Override
public void start() {
handlerThread = new HandlerThread(name, priority);
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
}
@Override
public void quit() {
if (handlerThread != null) {
handlerThread.quit();
handlerThread = null;
handler = null;
}
}
@Override
public void post(DatabaseTask task) {
handler.post(task.runnable);
}
}
class DatabaseWorkerPoolImpl implements DatabaseWorkerPool {
final String name;
final int numberOfWorkers;
final int priority;
private final LinkedList<DatabaseTask> waitingList = new LinkedList<>();
private final Set<DatabaseWorker> idleWorkers = new HashSet<>();
private final Set<DatabaseWorker> busyWorkers = new HashSet<>();
// A map from database id to the only eligible worker.
//
// When a database id is found in the map, tasks of the database should only be run by the
// corresponding worker. Otherwise, any worker is eligible.
private final Map<Integer, DatabaseWorker> onlyEligibleWorkers = new HashMap<>();
DatabaseWorkerPoolImpl(String name, int numberOfWorkers, int priority) {
this.name = name;
this.numberOfWorkers = numberOfWorkers;
this.priority = priority;
}
@Override
public synchronized void start() {
for (int i = 0; i < numberOfWorkers; i++) {
DatabaseWorker worker = createWorker(name + i, priority);
worker.start(
() -> {
onWorkerIdle(worker);
});
idleWorkers.add(worker);
}
}
protected DatabaseWorker createWorker(String name, int priority) {
return new DatabaseWorker(name, priority);
}
@Override
public synchronized void quit() {
for (DatabaseWorker worker : idleWorkers) {
worker.quit();
}
for (DatabaseWorker worker : busyWorkers) {
worker.quit();
}
}
@Override
public synchronized void post(DatabaseTask task) {
waitingList.add(task);
Set<DatabaseWorker> workers = new HashSet<>(idleWorkers);
for (DatabaseWorker worker : workers) {
tryPostingTaskToWorker(worker);
}
}
private synchronized void tryPostingTaskToWorker(DatabaseWorker worker) {
DatabaseTask task = findTaskForWorker(worker);
if (task != null) {
// Mark the worker busy.
busyWorkers.add(worker);
idleWorkers.remove(worker);
// Since now, the worker is the only eligible one to work on the corresponding database.
// Allowing others to work on the same database could break the "FIFO manner".
if (task.getDatabaseId() != null) {
onlyEligibleWorkers.put(task.getDatabaseId(), worker);
}
worker.postTask(task);
}
}
private synchronized DatabaseTask findTaskForWorker(DatabaseWorker worker) {
ListIterator<DatabaseTask> iter = waitingList.listIterator();
while (iter.hasNext()) {
DatabaseTask task = iter.next();
DatabaseWorker onlyEligibleWorker = null;
if (task.getDatabaseId() != null) {
onlyEligibleWorker = onlyEligibleWorkers.get(task.getDatabaseId());
}
// Skip current task when the worker is not eligible for it.
if (onlyEligibleWorker != null && onlyEligibleWorker != worker) {
continue;
} else {
iter.remove();
return task;
}
}
return null;
}
private synchronized void onWorkerIdle(DatabaseWorker worker) {
// Clone idleWorkers before it get modified.
Set<DatabaseWorker> others = new HashSet<>(idleWorkers);
// Mark the worker idle.
busyWorkers.remove(worker);
idleWorkers.add(worker);
// The last task was done and any other worker is eligible to work on the corresponding
// database since then. However, there is one exception that the last task is in
// transaction and current worker is still the only eligible one.
if (!worker.isLastTaskInTransaction() && worker.lastTaskDatabaseId() != null) {
onlyEligibleWorkers.remove(worker.lastTaskDatabaseId());
}
tryPostingTaskToWorker(worker);
// The eligible relationship was changed above. Try posting tasks again.
for (DatabaseWorker other : others) {
tryPostingTaskToWorker(other);
}
}
}
package com.tekartik.sqflite;
import android.os.Build;
import androidx.annotation.RequiresApi;
import java.util.Locale;
public class LocaleUtils {
static Locale localeForLanguateTag(String localeString) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return localeForLanguageTag21(localeString);
} else {
return localeForLanguageTagPre21(localeString);
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
static Locale localeForLanguageTag21(String localeString) {
return Locale.forLanguageTag(localeString);
}
/**
* Really basic implementation, hopefully not so many dev/apps with such requirements
* should be impacted.
*
* @param localeString
* @return
*/
static Locale localeForLanguageTagPre21(String localeString) {
//Locale.Builder builder = new Locale().Builder();
String[] parts = localeString.split("-");
String language = "";
String country = "";
String variant = "";
if (parts.length > 0) {
language = parts[0];
if (parts.length > 1) {
country = parts[1];
if (parts.length > 2) {
variant = parts[parts.length - 1];
}
}
}
return new Locale(language, country, variant);
}
}
package com.tekartik.sqflite;
import static com.tekartik.sqflite.Constant.PARAM_LOG_LEVEL;
import io.flutter.plugin.common.MethodCall;
public class LogLevel {
static final int none = 0;
static final int sql = 1;
static final int verbose = 2;
static Integer getLogLevel(MethodCall methodCall) {
return methodCall.argument(PARAM_LOG_LEVEL);
}
static boolean hasSqlLevel(int level) {
return level >= sql;
}
static boolean hasVerboseLevel(int level) {
return level >= verbose;
}
}
package com.tekartik.sqflite;
import android.database.Cursor;
/**
* Sqflite cursor
*/
public class SqfliteCursor {
final int cursorId;
final int pageSize;
final Cursor cursor;
public SqfliteCursor(int cursorId, int pageSize, Cursor cursor) {
this.cursorId = cursorId;
this.pageSize = pageSize;
this.cursor = cursor;
}
}
package com.tekartik.sqflite;
import static com.tekartik.sqflite.Constant.CMD_GET;
import static com.tekartik.sqflite.Constant.MEMORY_DATABASE_PATH;
import static com.tekartik.sqflite.Constant.METHOD_ANDROID_SET_LOCALE;
import static com.tekartik.sqflite.Constant.METHOD_BATCH;
import static com.tekartik.sqflite.Constant.METHOD_CLOSE_DATABASE;
import static com.tekartik.sqflite.Constant.METHOD_DATABASE_EXISTS;
import static com.tekartik.sqflite.Constant.METHOD_DEBUG;
import static com.tekartik.sqflite.Constant.METHOD_DEBUG_MODE;
import static com.tekartik.sqflite.Constant.METHOD_DELETE_DATABASE;
import static com.tekartik.sqflite.Constant.METHOD_EXECUTE;
import static com.tekartik.sqflite.Constant.METHOD_GET_DATABASES_PATH;
import static com.tekartik.sqflite.Constant.METHOD_GET_PLATFORM_VERSION;
import static com.tekartik.sqflite.Constant.METHOD_INSERT;
import static com.tekartik.sqflite.Constant.METHOD_OPEN_DATABASE;
import static com.tekartik.sqflite.Constant.METHOD_OPTIONS;
import static com.tekartik.sqflite.Constant.METHOD_QUERY;
import static com.tekartik.sqflite.Constant.METHOD_QUERY_CURSOR_NEXT;
import static com.tekartik.sqflite.Constant.METHOD_UPDATE;
import static com.tekartik.sqflite.Constant.PARAM_CMD;
import static com.tekartik.sqflite.Constant.PARAM_ID;
import static com.tekartik.sqflite.Constant.PARAM_LOCALE;
import static com.tekartik.sqflite.Constant.PARAM_LOG_LEVEL;
import static com.tekartik.sqflite.Constant.PARAM_PATH;
import static com.tekartik.sqflite.Constant.PARAM_READ_ONLY;
import static com.tekartik.sqflite.Constant.PARAM_RECOVERED;
import static com.tekartik.sqflite.Constant.PARAM_RECOVERED_IN_TRANSACTION;
import static com.tekartik.sqflite.Constant.PARAM_SINGLE_INSTANCE;
import static com.tekartik.sqflite.Constant.TAG;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Process;
import android.util.Log;
import com.tekartik.sqflite.dev.Debug;
import com.tekartik.sqflite.operation.MethodCallOperation;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.StandardMethodCodec;
/**
* SqflitePlugin Android implementation
*/
public class SqflitePlugin implements FlutterPlugin, MethodCallHandler {
static final Map<String, Integer> _singleInstancesByPath = new HashMap<>();
@SuppressLint("UseSparseArrays")
static final Map<Integer, Database> databaseMap = new HashMap<>();
static private final Object databaseMapLocker = new Object();
static private final Object openCloseLocker = new Object();
static int logLevel = LogLevel.none;
// local cache
static String databasesPath;
static private int THREAD_PRIORITY = Process.THREAD_PRIORITY_DEFAULT;
static private int THREAD_COUNT = 1;
static private int databaseId = 0; // incremental database id
// Database worker pool execution
static private DatabaseWorkerPool databaseWorkerPool;
private Context context;
private MethodChannel methodChannel;
// Needed public constructor
public SqflitePlugin() {
}
// Testing only
public SqflitePlugin(Context context) {
this.context = context.getApplicationContext();
}
static private Map<String, Object> fixMap(Map<Object, Object> map) {
Map<String, Object> newMap = new HashMap<>();
for (Map.Entry<Object, Object> entry : map.entrySet()) {
Object value = entry.getValue();
if (value instanceof Map) {
@SuppressWarnings("unchecked")
Map<Object, Object> mapValue = (Map<Object, Object>) value;
value = fixMap(mapValue);
} else {
value = toString(value);
}
newMap.put(toString(entry.getKey()), value);
}
return newMap;
}
// Convert a value to a string
// especially byte[]
static private String toString(Object value) {
if (value == null) {
return null;
} else if (value instanceof byte[]) {
List<Integer> list = new ArrayList<>();
for (byte _byte : (byte[]) value) {
list.add((int) _byte);
}
return list.toString();
} else if (value instanceof Map) {
@SuppressWarnings("unchecked")
Map<Object, Object> mapValue = (Map<Object, Object>) value;
return fixMap(mapValue).toString();
} else {
return value.toString();
}
}
static boolean isInMemoryPath(String path) {
return (path == null || path.equals(MEMORY_DATABASE_PATH));
}
// {
// 'id': xxx
// 'recovered': true // if recovered only for single instance
// }
static Map makeOpenResult(int databaseId, boolean recovered, boolean recoveredInTransaction) {
Map<String, Object> result = new HashMap<>();
result.put(PARAM_ID, databaseId);
if (recovered) {
result.put(PARAM_RECOVERED, true);
}
if (recoveredInTransaction) {
result.put(PARAM_RECOVERED_IN_TRANSACTION, true);
}
return result;
}
@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger());
}
private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger) {
this.context = applicationContext;
methodChannel = new MethodChannel(messenger, Constant.PLUGIN_KEY,
StandardMethodCodec.INSTANCE,
messenger.makeBackgroundTaskQueue());
methodChannel.setMethodCallHandler(this);
}
@Override
public void onDetachedFromEngine(FlutterPluginBinding binding) {
context = null;
methodChannel.setMethodCallHandler(null);
methodChannel = null;
}
private Context getContext() {
return context;
}
private Database getDatabase(int databaseId) {
return databaseMap.get(databaseId);
}
private Database getDatabaseOrError(MethodCall call, Result result) {
int databaseId = call.argument(PARAM_ID);
Database database = getDatabase(databaseId);
if (database != null) {
return database;
} else {
result.error(Constant.SQLITE_ERROR, Constant.ERROR_DATABASE_CLOSED + " " + databaseId, null);
return null;
}
}
//
// query
//
private void onQueryCall(final MethodCall call, final Result result) {
final Database database = getDatabaseOrError(call, result);
if (database == null) {
return;
}
databaseWorkerPool.post(database, () -> {
MethodCallOperation operation = new MethodCallOperation(call, result);
database.query(operation);
});
}
//
// cursor query next
//
private void onQueryCursorNextCall(final MethodCall call, final Result result) {
final Database database = getDatabaseOrError(call, result);
if (database == null) {
return;
}
databaseWorkerPool.post(database, () -> {
MethodCallOperation operation = new MethodCallOperation(call, result);
database.queryCursorNext(operation);
});
}
//
// Sqflite.batch
//
private void onBatchCall(final MethodCall call, final Result result) {
final Database database = getDatabaseOrError(call, result);
if (database == null) {
return;
}
databaseWorkerPool.post(database, () -> database.batch(call, result));
}
//
// Insert
//
private void onInsertCall(final MethodCall call, final Result result) {
final Database database = getDatabaseOrError(call, result);
if (database == null) {
return;
}
databaseWorkerPool.post(database, () -> {
MethodCallOperation operation = new MethodCallOperation(call, result);
database.insert(operation);
});
}
//
// Sqflite.execute
//
private void onExecuteCall(final MethodCall call, final Result result) {
final Database database = getDatabaseOrError(call, result);
if (database == null) {
return;
}
databaseWorkerPool.post(database, () -> {
MethodCallOperation operation = new MethodCallOperation(call, result);
database.execute(operation);
});
}
private void onSetLocaleCall(final MethodCall call, final Result result) {
final Database database = getDatabaseOrError(call, result);
if (database == null) {
return;
}
databaseWorkerPool.post(database, () -> {
String localeString = call.argument(PARAM_LOCALE);
try {
database.sqliteDatabase.setLocale(Utils.localeForLanguateTag(localeString));
result.success(null);
} catch (Exception exception) {
result.error(Constant.SQLITE_ERROR, "Error calling setLocale: " + exception.getMessage(), null);
}
});
}
//
// Sqflite.update
//
private void onUpdateCall(final MethodCall call, final Result result) {
final Database database = getDatabaseOrError(call, result);
if (database == null) {
return;
}
databaseWorkerPool.post(database, () -> {
MethodCallOperation operation = new MethodCallOperation(call, result);
database.update(operation);
});
}
private void onDebugCall(final MethodCall call, final Result result) {
String cmd = call.argument(PARAM_CMD);
Map<String, Object> map = new HashMap<>();
// Get database info
if (CMD_GET.equals(cmd)) {
if (logLevel > LogLevel.none) {
map.put(PARAM_LOG_LEVEL, logLevel);
}
if (!databaseMap.isEmpty()) {
Map<String, Object> databasesInfo = new HashMap<>();
for (Map.Entry<Integer, Database> entry : databaseMap.entrySet()) {
Database database = entry.getValue();
Map<String, Object> info = new HashMap<>();
info.put(PARAM_PATH, database.path);
info.put(PARAM_SINGLE_INSTANCE, database.singleInstance);
if (database.logLevel > LogLevel.none) {
info.put(PARAM_LOG_LEVEL, database.logLevel);
}
databasesInfo.put(entry.getKey().toString(), info);
}
map.put("databases", databasesInfo);
}
}
result.success(map);
}
// Deprecated since 1.1.6
private void onDebugModeCall(final MethodCall call, final Result result) {
// Old / argument was just a boolean
Object on = call.arguments();
Debug.LOGV = Boolean.TRUE.equals(on);
Debug.EXTRA_LOGV = Debug._EXTRA_LOGV && Debug.LOGV;
// set default logs to match existing
if (Debug.LOGV) {
if (Debug.EXTRA_LOGV) {
logLevel = LogLevel.verbose;
} else if (Debug.LOGV) {
logLevel = LogLevel.sql;
}
} else {
logLevel = LogLevel.none;
}
result.success(null);
}
//
// Sqflite.open
//
private void onOpenDatabaseCall(final MethodCall call, final Result result) {
final String path = call.argument(PARAM_PATH);
final Boolean readOnly = call.argument(PARAM_READ_ONLY);
final boolean inMemory = isInMemoryPath(path);
final boolean singleInstance = !Boolean.FALSE.equals(call.argument(PARAM_SINGLE_INSTANCE)) && !inMemory;
// For single instance we create or reuse a thread right away
// DO NOT TRY TO LOAD existing instance, the database has been closed
if (singleInstance) {
// Look for in memory instance
synchronized (databaseMapLocker) {
if (LogLevel.hasVerboseLevel(logLevel)) {
Log.d(Constant.TAG, "Look for " + path + " in " + _singleInstancesByPath.keySet());
}
Integer databaseId = _singleInstancesByPath.get(path);
if (databaseId != null) {
Database database = databaseMap.get(databaseId);
if (database != null) {
if (!database.sqliteDatabase.isOpen()) {
if (LogLevel.hasVerboseLevel(logLevel)) {
Log.d(Constant.TAG, database.getThreadLogPrefix() + "single instance database of " + path + " not opened");
}
} else {
if (LogLevel.hasVerboseLevel(logLevel)) {
Log.d(Constant.TAG, database.getThreadLogPrefix() + "re-opened single instance " + (database.isInTransaction() ? "(in transaction) " : "") + databaseId + " " + path);
}
result.success(makeOpenResult(databaseId, true, database.isInTransaction()));
return;
}
}
}
}
}
// Generate new id
int newDatabaseId;
synchronized (databaseMapLocker) {
newDatabaseId = ++databaseId;
}
final int databaseId = newDatabaseId;
final Database database = new Database(context, path, databaseId, singleInstance, logLevel);
synchronized (databaseMapLocker) {
// Create worker pool if necessary
if (databaseWorkerPool == null) {
databaseWorkerPool = DatabaseWorkerPool.create(
"Sqflite", THREAD_COUNT, SqflitePlugin.THREAD_PRIORITY);
databaseWorkerPool.start();
if (LogLevel.hasSqlLevel(database.logLevel)) {
Log.d(TAG, database.getThreadLogPrefix() + "starting worker pool with priority " + SqflitePlugin.THREAD_PRIORITY);
}
}
database.databaseWorkerPool = databaseWorkerPool;
if (LogLevel.hasSqlLevel(database.logLevel)) {
Log.d(TAG, database.getThreadLogPrefix() + "opened " + databaseId + " " + path);
}
// Open in background thread
databaseWorkerPool.post(
database,
() -> {
synchronized (openCloseLocker) {
if (!inMemory) {
File file = new File(path);
File directory = new File(file.getParent());
if (!directory.exists()) {
if (!directory.mkdirs()) {
if (!directory.exists()) {
result.error(Constant.SQLITE_ERROR, Constant.ERROR_OPEN_FAILED + " " + path, null);
return;
}
}
}
}
// force opening
try {
if (Boolean.TRUE.equals(readOnly)) {
database.openReadOnly();
} else {
database.open();
}
} catch (Exception e) {
MethodCallOperation operation = new MethodCallOperation(call, result);
database.handleException(e, operation);
return;
}
synchronized (databaseMapLocker) {
if (singleInstance) {
_singleInstancesByPath.put(path, databaseId);
}
databaseMap.put(databaseId, database);
}
if (LogLevel.hasSqlLevel(database.logLevel)) {
Log.d(TAG, database.getThreadLogPrefix() + "opened " + databaseId + " " + path);
}
}
result.success(makeOpenResult(databaseId, false, false));
});
}
}
//
// Sqflite.close
//
private void onCloseDatabaseCall(MethodCall call, final Result result) {
final int databaseId = call.argument(PARAM_ID);
final Database database = getDatabaseOrError(call, result);
if (database == null) {
return;
}
if (LogLevel.hasSqlLevel(database.logLevel)) {
Log.d(TAG, database.getThreadLogPrefix() + "closing " + databaseId + " " + database.path);
}
final String path = database.path;
// Remove from map right away
synchronized (databaseMapLocker) {
databaseMap.remove(databaseId);
if (database.singleInstance) {
_singleInstancesByPath.remove(path);
}
}
databaseWorkerPool.post(database, new Runnable() {
@Override
public void run() {
synchronized (openCloseLocker) {
closeDatabase(database);
}
result.success(null);
}
});
}
//
// Sqflite.open
//
private void onDeleteDatabaseCall(final MethodCall call, final Result result) {
final String path = call.argument(PARAM_PATH);
Database foundOpenedDatabase = null;
// Look for in memory instance
synchronized (databaseMapLocker) {
if (LogLevel.hasVerboseLevel(logLevel)) {
Log.d(Constant.TAG, "Look for " + path + " in " + _singleInstancesByPath.keySet());
}
Integer databaseId = _singleInstancesByPath.get(path);
if (databaseId != null) {
Database database = databaseMap.get(databaseId);
if (database != null) {
if (database.sqliteDatabase.isOpen()) {
if (LogLevel.hasVerboseLevel(logLevel)) {
Log.d(Constant.TAG, database.getThreadLogPrefix() + "found single instance " + (database.isInTransaction() ? "(in transaction) " : "") + databaseId + " " + path);
}
foundOpenedDatabase = database;
// Remove from map right away
databaseMap.remove(databaseId);
_singleInstancesByPath.remove(path);
}
}
}
}
final Database openedDatabase = foundOpenedDatabase;
final Runnable deleteRunnable = new Runnable() {
@Override
public void run() {
synchronized (openCloseLocker) {
if (openedDatabase != null) {
closeDatabase(openedDatabase);
}
try {
if (LogLevel.hasVerboseLevel(logLevel)) {
Log.d(Constant.TAG, "delete database " + path);
}
Database.deleteDatabase(path);
} catch (Exception e) {
Log.e(TAG, "error " + e + " while closing database " + databaseId);
}
}
result.success(null);
}
};
// worker pool might not exist yet
if (databaseWorkerPool != null) {
databaseWorkerPool.post(openedDatabase, deleteRunnable);
} else {
// Otherwise run in the UI thread
deleteRunnable.run();
}
}
private void onDatabaseExistsCall(final MethodCall call, final Result result) {
final String path = call.argument(PARAM_PATH);
boolean exists = Database.existsDatabase(path);
result.success(exists);
}
private void closeDatabase(Database database) {
try {
if (LogLevel.hasSqlLevel(database.logLevel)) {
Log.d(TAG, database.getThreadLogPrefix() + "closing database ");
}
database.close();
} catch (Exception e) {
Log.e(TAG, "error " + e + " while closing database " + databaseId);
}
synchronized (databaseMapLocker) {
if (databaseMap.isEmpty() && databaseWorkerPool != null) {
if (LogLevel.hasSqlLevel(database.logLevel)) {
Log.d(TAG, database.getThreadLogPrefix() + "stopping thread");
}
databaseWorkerPool.quit();
databaseWorkerPool = null;
}
}
}
@Override
public void onMethodCall(MethodCall call, Result result) {
switch (call.method) {
// quick testing
case METHOD_GET_PLATFORM_VERSION:
result.success("Android " + android.os.Build.VERSION.RELEASE);
break;
case METHOD_CLOSE_DATABASE: {
onCloseDatabaseCall(call, result);
break;
}
case METHOD_QUERY: {
onQueryCall(call, result);
break;
}
case METHOD_INSERT: {
onInsertCall(call, result);
break;
}
case METHOD_UPDATE: {
onUpdateCall(call, result);
break;
}
case METHOD_EXECUTE: {
onExecuteCall(call, result);
break;
}
case METHOD_OPEN_DATABASE: {
onOpenDatabaseCall(call, result);
break;
}
case METHOD_BATCH: {
onBatchCall(call, result);
break;
}
case METHOD_OPTIONS: {
onOptionsCall(call, result);
break;
}
case METHOD_GET_DATABASES_PATH: {
onGetDatabasesPathCall(call, result);
break;
}
case METHOD_DELETE_DATABASE: {
onDeleteDatabaseCall(call, result);
break;
}
case METHOD_DEBUG: {
onDebugCall(call, result);
break;
}
case METHOD_QUERY_CURSOR_NEXT: {
onQueryCursorNextCall(call, result);
break;
}
case METHOD_DATABASE_EXISTS: {
onDatabaseExistsCall(call, result);
break;
}
// Obsolete
case METHOD_DEBUG_MODE: {
onDebugModeCall(call, result);
break;
}
case METHOD_ANDROID_SET_LOCALE: {
onSetLocaleCall(call, result);
break;
}
default:
result.notImplemented();
break;
}
}
void onOptionsCall(final MethodCall call, final Result result) {
Object threadPriority = call.argument(Constant.PARAM_THREAD_PRIORITY);
if (threadPriority != null) {
THREAD_PRIORITY = (Integer) threadPriority;
}
Object threadCount = call.argument(Constant.PARAM_THREAD_COUNT);
if (threadCount != null && !threadCount.equals(THREAD_COUNT)) {
THREAD_COUNT = (Integer) threadCount;
// Reset databaseWorkerPool when THREAD_COUNT change.
if (databaseWorkerPool != null) {
databaseWorkerPool.quit();
databaseWorkerPool = null;
}
}
Integer logLevel = LogLevel.getLogLevel(call);
if (logLevel != null) {
SqflitePlugin.logLevel = logLevel;
}
result.success(null);
}
//private static class Database
void onGetDatabasesPathCall(final MethodCall call, final Result result) {
if (databasesPath == null) {
String dummyDatabaseName = "tekartik_sqflite.db";
File file = context.getDatabasePath(dummyDatabaseName);
databasesPath = file.getParent();
}
result.success(databasesPath);
}
}
package com.tekartik.sqflite;
import android.database.sqlite.SQLiteProgram;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class SqlCommand {
final private String sql;
final private List<Object> rawArguments;
public SqlCommand(String sql, List<Object> rawArguments) {
this.sql = sql;
if (rawArguments == null) {
rawArguments = new ArrayList<>();
}
this.rawArguments = rawArguments;
}
// Handle list of int as byte[]
static private Object toValue(Object value) {
if (value == null) {
return null;
} else {
// Assume a list is a blob
if (value instanceof List) {
@SuppressWarnings("unchecked")
List<Integer> list = (List<Integer>) value;
byte[] blob = new byte[list.size()];
for (int i = 0; i < list.size(); i++) {
blob[i] = (byte) (int) list.get(i);
}
value = blob;
}
return value;
}
}
public String getSql() {
return sql;
}
private Object[] getSqlArguments(List<Object> rawArguments) {
List<Object> fixedArguments = new ArrayList<>();
if (rawArguments != null) {
for (Object rawArgument : rawArguments) {
fixedArguments.add(toValue(rawArgument));
}
}
return fixedArguments.toArray(new Object[0]);
}
public void bindTo(SQLiteProgram statement) {
if (rawArguments != null) {
int count = rawArguments.size();
for (int i = 0; i < count; i++) {
Object arg = toValue(rawArguments.get(i));
// sqlite3 variables are 1-indexed
int sqlIndex = i + 1;
if (arg == null) {
statement.bindNull(sqlIndex);
} else if (arg instanceof byte[]) {
statement.bindBlob(sqlIndex, (byte[]) arg);
} else if (arg instanceof Double) {
statement.bindDouble(sqlIndex, (Double) arg);
} else if (arg instanceof Integer) {
statement.bindLong(sqlIndex, (Integer) arg);
} else if (arg instanceof Long) {
statement.bindLong(sqlIndex, (Long) arg);
} else if (arg instanceof String) {
statement.bindString(sqlIndex, (String) arg);
} else if (arg instanceof Boolean) {
statement.bindLong(sqlIndex, ((Boolean) arg) ? 1 : 0);
} else {
throw new IllegalArgumentException("Could not bind " + arg + " from index "
+ i + ": Supported types are null, byte[], double, long, boolean and String");
}
}
}
}
@Override
public String toString() {
return sql + ((rawArguments == null || rawArguments.isEmpty()) ? "" : (" " + rawArguments));
}
// As expected by execSQL
public Object[] getSqlArguments() {
return getSqlArguments(rawArguments);
}
public List<Object> getRawSqlArguments() {
return rawArguments;
}
@Override
public int hashCode() {
return sql != null ? sql.hashCode() : 0;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof SqlCommand) {
SqlCommand o = (SqlCommand) obj;
if (sql != null) {
if (!sql.equals(o.sql)) {
return false;
}
} else {
if (o.sql != null) {
return false;
}
}
if (rawArguments.size() != o.rawArguments.size()) {
return false;
}
for (int i = 0; i < rawArguments.size(); i++) {
// special blob handling
if (rawArguments.get(i) instanceof byte[] && o.rawArguments.get(i) instanceof byte[]) {
if (!Arrays.equals((byte[]) rawArguments.get(i), (byte[]) o.rawArguments.get(i))) {
return false;
}
} else {
if (!rawArguments.get(i).equals(o.rawArguments.get(i))) {
return false;
}
}
}
return true;
}
return false;
}
}
package com.tekartik.sqflite;
import static com.tekartik.sqflite.Constant.TAG;
import android.database.Cursor;
import android.os.Build;
import android.util.Log;
import androidx.annotation.RequiresApi;
import com.tekartik.sqflite.dev.Debug;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class Utils {
static public List<Object> cursorRowToList(Cursor cursor, int length) {
List<Object> list = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
Object value = cursorValue(cursor, i);
if (Debug.EXTRA_LOGV) {
String type = null;
if (value != null) {
if (value.getClass().isArray()) {
type = "array(" + value.getClass().getComponentType().getName() + ")";
} else {
type = value.getClass().getName();
}
}
Log.d(TAG, "column " + i + " " + cursor.getType(i) + ": " + value + (type == null ? "" : " (" + type + ")"));
}
list.add(value);
}
return list;
}
static public Object cursorValue(Cursor cursor, int index) {
switch (cursor.getType(index)) {
case Cursor.FIELD_TYPE_NULL:
return null;
case Cursor.FIELD_TYPE_INTEGER:
return cursor.getLong(index);
case Cursor.FIELD_TYPE_FLOAT:
return cursor.getDouble(index);
case Cursor.FIELD_TYPE_STRING:
return cursor.getString(index);
case Cursor.FIELD_TYPE_BLOB:
return cursor.getBlob(index);
}
return null;
}
static Locale localeForLanguateTag(String localeString) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return localeForLanguageTag21(localeString);
} else {
return localeForLanguageTagPre21(localeString);
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
static Locale localeForLanguageTag21(String localeString) {
return Locale.forLanguageTag(localeString);
}
/**
* Really basic implementation, hopefully not so many dev/apps with such requirements
* should be impacted.
*
* @param localeString
* @return
*/
static Locale localeForLanguageTagPre21(String localeString) {
//Locale.Builder builder = new Locale().Builder();
String[] parts = localeString.split("-");
String language = "";
String country = "";
String variant = "";
if (parts.length > 0) {
language = parts[0];
if (parts.length > 1) {
country = parts[1];
if (parts.length > 2) {
variant = parts[parts.length - 1];
}
}
}
return new Locale(language, country, variant);
}
}
package com.tekartik.sqflite.dev;
import static android.content.ContentValues.TAG;
import android.util.Log;
/**
* Created by alex on 09/01/18.
*/
public class Debug {
// Log flags
public static boolean LOGV = false;
public static boolean _EXTRA_LOGV = false; // to set to true for type debugging
// public static boolean _EXTRA_LOGV = true; // to set to true for type debugging
static public boolean EXTRA_LOGV = false; // to set to true for type debugging
// Deprecated to prevent usage
@Deprecated
public static void devLog(String tag, String message) {
Log.d(TAG, message);
}
}
package com.tekartik.sqflite.operation;
/**
* Created by alex on 09/01/18.
*/
public abstract class BaseOperation extends BaseReadOperation {
// We actually have an inner object that does the implementation
protected abstract OperationResult getOperationResult();
@Override
public void success(Object result) {
getOperationResult().success(result);
}
@Override
public void error(String errorCode, String errorMessage, Object data) {
getOperationResult().error(errorCode, errorMessage, data);
}
}
package com.tekartik.sqflite.operation;
import static com.tekartik.sqflite.Constant.PARAM_CONTINUE_OR_ERROR;
import static com.tekartik.sqflite.Constant.PARAM_IN_TRANSACTION_CHANGE;
import static com.tekartik.sqflite.Constant.PARAM_NO_RESULT;
import static com.tekartik.sqflite.Constant.PARAM_SQL;
import static com.tekartik.sqflite.Constant.PARAM_SQL_ARGUMENTS;
import static com.tekartik.sqflite.Constant.PARAM_TRANSACTION_ID;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.tekartik.sqflite.SqlCommand;
import java.util.List;
/**
* Created by alex on 09/01/18.
*/
public abstract class BaseReadOperation implements Operation {
private String getSql() {
return getArgument(PARAM_SQL);
}
private List<Object> getSqlArguments() {
return getArgument(PARAM_SQL_ARGUMENTS);
}
@Nullable
public Integer getTransactionId() {
return getArgument(PARAM_TRANSACTION_ID);
}
public boolean hasNullTransactionId() {
return hasArgument(PARAM_TRANSACTION_ID) && getTransactionId() == null;
}
public SqlCommand getSqlCommand() {
return new SqlCommand(getSql(), getSqlArguments());
}
public Boolean getInTransactionChange() {
return getBoolean(PARAM_IN_TRANSACTION_CHANGE);
}
@Override
public boolean getNoResult() {
return Boolean.TRUE.equals(getArgument(PARAM_NO_RESULT));
}
@Override
public boolean getContinueOnError() {
return Boolean.TRUE.equals(getArgument(PARAM_CONTINUE_OR_ERROR));
}
private Boolean getBoolean(String key) {
Object value = getArgument(key);
if (value instanceof Boolean) {
return (Boolean) value;
}
return null;
}
// We actually have an inner object that does the implementation
protected abstract OperationResult getOperationResult();
@NonNull
@Override
public String toString() {
return getMethod() + " " + getSql() + " " + getSqlArguments();
}
}
package com.tekartik.sqflite.operation;
import static com.tekartik.sqflite.Constant.PARAM_ERROR;
import static com.tekartik.sqflite.Constant.PARAM_ERROR_CODE;
import static com.tekartik.sqflite.Constant.PARAM_ERROR_DATA;
import static com.tekartik.sqflite.Constant.PARAM_ERROR_MESSAGE;
import static com.tekartik.sqflite.Constant.PARAM_METHOD;
import static com.tekartik.sqflite.Constant.PARAM_RESULT;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.flutter.plugin.common.MethodChannel;
/**
* Created by alex on 09/01/18.
*/
public class BatchOperation extends BaseOperation {
final Map<String, Object> map;
final BatchOperationResult operationResult = new BatchOperationResult();
final boolean noResult;
public BatchOperation(Map<String, Object> map, boolean noResult) {
this.map = map;
this.noResult = noResult;
}
@Override
public String getMethod() {
return (String) map.get(PARAM_METHOD);
}
@SuppressWarnings("unchecked")
@Override
public <T> T getArgument(String key) {
return (T) map.get(key);
}
@Override
public boolean hasArgument(String key) {
return map.containsKey(key);
}
@Override
public OperationResult getOperationResult() {
return operationResult;
}
public Map<String, Object> getOperationSuccessResult() {
Map<String, Object> results = new HashMap<>();
results.put(PARAM_RESULT, operationResult.result);
return results;
}
public Map<String, Object> getOperationError() {
Map<String, Object> error = new HashMap<>();
Map<String, Object> errorDetail = new HashMap<>();
errorDetail.put(PARAM_ERROR_CODE, operationResult.errorCode);
errorDetail.put(PARAM_ERROR_MESSAGE, operationResult.errorMessage);
errorDetail.put(PARAM_ERROR_DATA, operationResult.errorData);
error.put(PARAM_ERROR, errorDetail);
return error;
}
public void handleError(MethodChannel.Result result) {
result.error(this.operationResult.errorCode, this.operationResult.errorMessage, this.operationResult.errorData);
}
@Override
public boolean getNoResult() {
return noResult;
}
public void handleSuccess(List<Map<String, Object>> results) {
if (!getNoResult()) {
results.add(getOperationSuccessResult());
}
}
public void handleErrorContinue(List<Map<String, Object>> results) {
if (!getNoResult()) {
results.add(getOperationError());
}
}
public class BatchOperationResult implements OperationResult {
// success
Object result;
// error
String errorCode;
String errorMessage;
Object errorData;
@Override
public void success(Object result) {
this.result = result;
}
@Override
public void error(String errorCode, String errorMessage, Object data) {
this.errorCode = errorCode;
this.errorMessage = errorMessage;
this.errorData = data;
}
}
}
package com.tekartik.sqflite.operation;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
/**
* Operation for Method call
*/
public class MethodCallOperation extends BaseOperation {
public final Result result;
final MethodCall methodCall;
public MethodCallOperation(MethodCall methodCall, MethodChannel.Result result) {
this.methodCall = methodCall;
this.result = new Result(result);
}
@Override
public String getMethod() {
return methodCall.method;
}
@Override
public <T> T getArgument(String key) {
return methodCall.argument(key);
}
@Override
public boolean hasArgument(String key) {
return methodCall.hasArgument(key);
}
@Override
public OperationResult getOperationResult() {
return result;
}
class Result implements OperationResult {
final MethodChannel.Result result;
Result(MethodChannel.Result result) {
this.result = result;
}
@Override
public void success(Object result) {
this.result.success(result);
}
@Override
public void error(String errorCode, String errorMessage, Object data) {
result.error(errorCode, errorMessage, data);
}
}
}
package com.tekartik.sqflite.operation;
import androidx.annotation.Nullable;
import com.tekartik.sqflite.SqlCommand;
/**
* Created by alex on 09/01/18.
*/
public interface Operation extends OperationResult {
String getMethod();
<T> T getArgument(String key);
boolean hasArgument(String key);
SqlCommand getSqlCommand();
boolean getNoResult();
// In batch, means ignoring the error
boolean getContinueOnError();
// Only for execute command, true when entering a transaction, false when exiting
Boolean getInTransactionChange();
/**
* transaction id if any, only for within a transaction
*/
@Nullable
Integer getTransactionId();
/**
* Transaction v2 support
*/
boolean hasNullTransactionId();
}
package com.tekartik.sqflite.operation;
import androidx.annotation.Nullable;
/**
* Created by alex on 09/01/18.
*/
public interface OperationResult {
void error(final String errorCode, final String errorMessage, final Object data);
void success(@Nullable final Object result);
}
package com.tekartik.sqflite.operation;
/**
* Operation runnable interface
*/
public interface OperationRunnable {
boolean run();
}
package com.tekartik.sqflite.operation;
public class QueuedOperation {
final Operation operation;
final Runnable runnable;
public QueuedOperation(Operation operation, Runnable runnable) {
this.operation = operation;
this.runnable = runnable;
}
public void run() {
runnable.run();
}
}
package com.tekartik.sqflite.operation;
import static com.tekartik.sqflite.Constant.PARAM_SQL;
import static com.tekartik.sqflite.Constant.PARAM_SQL_ARGUMENTS;
import com.tekartik.sqflite.SqlCommand;
import java.util.HashMap;
import java.util.Map;
public class SqlErrorInfo {
static public Map<String, Object> getMap(Operation operation) {
Map<String, Object> map = null;
SqlCommand command = operation.getSqlCommand();
if (command != null) {
map = new HashMap<>();
map.put(PARAM_SQL, command.getSql());
map.put(PARAM_SQL_ARGUMENTS, command.getRawSqlArguments());
}
return map;
}
}
package com.tekartik.sqflite;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
/**
* Constants between dart & Java world
*/
public class ConstantTest {
@Test
public void key() {
assertEquals("com.tekartik.sqflite", Constant.PLUGIN_KEY);
}
}
package com.tekartik.sqflite;
import static org.junit.Assert.assertEquals;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Queue;
import java.util.Set;
public class DatabaseWorkerPoolTest {
private FakeDatabaseWorkerPool pool;
private FakeDatabaseWorker worker1;
private FakeDatabaseWorker worker2 ;
private FakeDatabase database1;
private FakeDatabase database2;
@Before
public void setUp() {
pool = new FakeDatabaseWorkerPool("pool", 2, 0);
pool.start();
worker1 = pool.getWorker(0);
worker2 = pool.getWorker(1);
database1 = new FakeDatabase(1);
database2 = new FakeDatabase(2);
}
@After
public void tearDown() {
pool.quit();
}
@Test
public void tasksOfOneDBRunFIFO() {
// Arrange.
DatabaseTask task1 = new DatabaseTask(database1, () -> {});
DatabaseTask task2 = new DatabaseTask(database1, () -> {});
DatabaseTask task3 = new DatabaseTask(database2, () -> {});
// Act. Posting three tasks. The first two are belonging to the same database.
pool.post(task1);
pool.post(task2); // It should not be started until task1 is done.
pool.post(task3);
// Assert. Worker1 run task1. Worker2 skipped task2 and run task3.
assertEquals(Arrays.asList(task1), new ArrayList<>(worker1.tasks));
assertEquals(Arrays.asList(task3), new ArrayList<>(worker2.tasks));
// Act. Worker1 and worker2 finished one task.
worker1.work();
worker2.work();
// Assert. Worker1 run task2.
assertEquals(Arrays.asList(task2), new ArrayList<>(worker1.tasks));
assertEquals(Collections.emptyList(), new ArrayList<>(worker2.tasks));
}
@Test
public void tasksOfOneTransactionRunByOneWorker() {
// Arrange.
DatabaseTask task1 = new DatabaseTask(database1, () -> {});
database2.inTransaction = true;
DatabaseTask task2 = new DatabaseTask(database2, () -> {});
// Act. Posting two tasks.
pool.post(task1);
pool.post(task2);
// Assert. Worker1 run task1. Worker2 run task2 in transaction.
assertEquals(Arrays.asList(task1), new ArrayList<>(worker1.tasks));
assertEquals(Arrays.asList(task2), new ArrayList<>(worker2.tasks));
// Act. Worker1 and worker2 finished one task. Then post a new task in the same transaction.
worker1.work();
worker2.work();
DatabaseTask task3 = new DatabaseTask(database2, () -> {});
pool.post(task3);
// Assert. Worker1 was skipped. Worker2 run task2.
assertEquals(Collections.emptyList(), new ArrayList<>(worker1.tasks));
assertEquals(Arrays.asList(task3), new ArrayList<>(worker2.tasks));
}
@Test
public void tasksOfDiffTransactionsRunByTwoWorker() {
// Arrange.
DatabaseTask task1 = new DatabaseTask(database1, () -> {});
DatabaseTask task2 = new DatabaseTask(database2, () -> {});
// Act. Posting two tasks.
pool.post(task1);
pool.post(task2);
// Assert. Worker1 run task1. Worker2 run task2.
assertEquals(Arrays.asList(task1), new ArrayList<>(worker1.tasks));
assertEquals(Arrays.asList(task2), new ArrayList<>(worker2.tasks));
// Act. Worker1 and worker2 finished one task. Then post a new task.
worker1.work();
worker2.work();
DatabaseTask task3 = new DatabaseTask(database2, () -> {});
pool.post(task3);
// Assert. Task3 is not in transaction. Just find the first available worker (worker1)
// to run task2.
assertEquals(Arrays.asList(task3), new ArrayList<>(worker1.tasks));
assertEquals(Collections.emptyList(), new ArrayList<>(worker2.tasks));
}
}
class FakeDatabase implements DatabaseDelegate {
final int databaseId;
boolean inTransaction;
FakeDatabase(int databaseId) {
this.databaseId = databaseId;
}
@Override
public int getDatabaseId() {
return databaseId;
}
@Override
public boolean isInTransaction() {
return inTransaction;
}
}
class FakeDatabaseWorker extends DatabaseWorker {
Queue<DatabaseTask> tasks = new ArrayDeque<>();
FakeDatabaseWorker(String name, int priority) {
super(name, priority);
}
@Override
void start(Runnable onIdle) {
this.onIdle = onIdle;
}
@Override
void quit() {}
@Override
void postTask(final DatabaseTask task) {
tasks.add(task);
}
void work() {
DatabaseTask task = tasks.remove();
super.work(task);
}
}
class FakeDatabaseWorkerPool extends DatabaseWorkerPoolImpl {
final Set<FakeDatabaseWorker> workers = new HashSet<>();
FakeDatabaseWorkerPool(String name, int numberOfWorkers, int priority) {
super(name, numberOfWorkers, priority);
}
@Override
protected DatabaseWorker createWorker(String name, int priority) {
FakeDatabaseWorker worker = new FakeDatabaseWorker(name, priority);
workers.add(worker);
return worker;
}
FakeDatabaseWorker getWorker(int idx) {
return new ArrayList<>(workers).get(idx);
}
}
package com.tekartik.sqflite;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
/**
* Constants between dart & Java world
*/
public class LogLevelTest {
@Test
public void hasSqlLogLevel() {
assertTrue(LogLevel.hasSqlLevel(1));
assertFalse(LogLevel.hasSqlLevel(0));
}
}
package com.tekartik.sqflite;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Constants between dart & Java world
*/
public class SqlCommandTest {
@Test
public void noParam() {
SqlCommand command = new SqlCommand(null, null);
assertEquals(command.getSql(), null);
assertEquals(command.getRawSqlArguments(), new ArrayList<>());
}
@Test
public void sqlArguments() {
List<Object> arguments = Arrays.asList((Object) 1L, 2, "text",
1.234f,
4.5678, // double
new byte[]{1, 2, 3});
SqlCommand command = new SqlCommand(null, arguments);
/*
assertEquals(Arrays.asList(1L, 2, "text",
1.234f,
4.5678, // double
new byte[] {1,2, 3}), command.getRawSqlArguments());
*/
assertArrayEquals(new Object[]{1L, 2, "text",
1.234f,
4.5678, // double
new byte[]{1, 2, 3}}, command.getSqlArguments());
}
@Test
public void equals() {
SqlCommand command1 = new SqlCommand(null, null);
SqlCommand command2 = new SqlCommand(null, new ArrayList<Object>());
assertEquals(command1, command2);
command1 = new SqlCommand("", null);
assertNotEquals(command1, command2);
assertNotEquals(command2, command1);
command1 = new SqlCommand(null, Arrays.asList((Object) "test"));
assertNotEquals(command1, command2);
assertNotEquals(command2, command1);
command2 = new SqlCommand(null, Arrays.asList((Object) "test"));
assertEquals(command1, command2);
command1 = new SqlCommand(null, Arrays.asList((Object) "test_"));
assertNotEquals(command1, command2);
assertNotEquals(command2, command1);
command1 = new SqlCommand(null, Arrays.asList((Object) new byte[]{1, 2, 3}));
command2 = new SqlCommand(null, Arrays.asList((Object) new byte[]{1, 2, 3}));
assertEquals(command1, command2);
command1 = new SqlCommand(null, Arrays.asList((Object) new byte[]{1, 2}));
assertNotEquals(command1, command2);
assertNotEquals(command2, command1);
}
}
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
Pods/
Podfile
\ No newline at end of file
//
// SqfliteCursor.h
// sqflite
//
// Created by Alexandre Roux on 24/10/2022.
//
#ifndef SqfliteCursor_h
#define SqfliteCursor_h
// Cursor information
@class SqfliteDarwinResultSet;
@interface SqfliteCursor : NSObject
@property (atomic, retain) NSNumber* cursorId;
@property (atomic, retain) NSNumber* pageSize;
@property (atomic, retain) SqfliteDarwinResultSet *resultSet;
@end
#endif // SqfliteCursor_h
#import "SqfliteCursor.h"
#import "SqfliteDarwinImport.h"
@implementation SqfliteCursor
@synthesize cursorId;
@synthesize pageSize;
@synthesize resultSet;
@end
#import <Foundation/Foundation.h>
FOUNDATION_EXPORT double SqfliteDarwinDBVersionNumber;
FOUNDATION_EXPORT const unsigned char SqfliteDarwinDBVersionString[];
#import "SqfliteDarwinDatabase.h"
#import "SqfliteDarwinResultSet.h"
#import "SqfliteDarwinDatabaseAdditions.h"
#import "SqfliteDarwinDatabaseQueue.h"
#import <Foundation/Foundation.h>
#import "SqfliteDarwinResultSet.h"
NS_ASSUME_NONNULL_BEGIN
#if ! __has_feature(objc_arc)
#define SqfliteDarwinDBAutorelease(__v) ([__v autorelease]);
#define SqfliteDarwinDBReturnAutoreleased SqfliteDarwinDBAutorelease
#define SqfliteDarwinDBRetain(__v) ([__v retain]);
#define SqfliteDarwinDBReturnRetained SqfliteDarwinDBRetain
#define SqfliteDarwinDBRelease(__v) ([__v release]);
#define SqfliteDarwinDBDispatchQueueRelease(__v) (dispatch_release(__v));
#else
// -fobjc-arc
#define SqfliteDarwinDBAutorelease(__v)
#define SqfliteDarwinDBReturnAutoreleased(__v) (__v)
#define SqfliteDarwinDBRetain(__v)
#define SqfliteDarwinDBReturnRetained(__v) (__v)
#define SqfliteDarwinDBRelease(__v)
// If OS_OBJECT_USE_OBJC=1, then the dispatch objects will be treated like ObjC objects
// and will participate in ARC.
// See the section on "Dispatch Queues and Automatic Reference Counting" in "Grand Central Dispatch (GCD) Reference" for details.
#if OS_OBJECT_USE_OBJC
#define SqfliteDarwinDBDispatchQueueRelease(__v)
#else
#define SqfliteDarwinDBDispatchQueueRelease(__v) (dispatch_release(__v));
#endif
#endif
#if !__has_feature(objc_instancetype)
#define instancetype id
#endif
/**
Callback block used by @c executeStatements:withResultBlock:
*/
typedef int(^SqfliteDarwinDBExecuteStatementsCallbackBlock)(NSDictionary *resultsDictionary);
/**
Enumeration used in checkpoint methods.
*/
typedef NS_ENUM(int, SqfliteDarwinDBCheckpointMode) {
SqfliteDarwinDBCheckpointModePassive = 0, // SQLITE_CHECKPOINT_PASSIVE,
SqfliteDarwinDBCheckpointModeFull = 1, // SQLITE_CHECKPOINT_FULL,
SqfliteDarwinDBCheckpointModeRestart = 2, // SQLITE_CHECKPOINT_RESTART,
SqfliteDarwinDBCheckpointModeTruncate = 3 // SQLITE_CHECKPOINT_TRUNCATE
};
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
/** A SQLite ([https://sqlite.org/](https://sqlite.org/)) Objective-C wrapper.
Usage
The three main classes in SqfliteDarwinDB are:
- @c SqfliteDarwinDatabase - Represents a single SQLite database. Used for executing SQL statements.
- @c SqfliteDarwinResultSet - Represents the results of executing a query on an @c SqfliteDarwinDatabase .
- @c SqfliteDarwinDatabaseQueue - If you want to perform queries and updates on multiple threads, you'll want to use this class.
See also
- @c SqfliteDarwinDatabasePool - A pool of @c SqfliteDarwinDatabase objects
- @c SqfliteDarwinStatement - A wrapper for @c sqlite_stmt
External links
- [SqfliteDarwinDB on GitHub](https://github.com/ccgus/fmdb) including introductory documentation
- [SQLite web site](https://sqlite.org/)
- [SqfliteDarwinDB mailing list](http://groups.google.com/group/fmdb)
- [SQLite FAQ](https://sqlite.org/faq.html)
@warning Do not instantiate a single @c SqfliteDarwinDatabase object and use it across multiple threads. Instead, use @c SqfliteDarwinDatabaseQueue .
*/
@interface SqfliteDarwinDatabase : NSObject
///-----------------
/// @name Properties
///-----------------
/** Whether should trace execution */
@property (atomic, assign) BOOL traceExecution;
/** Whether checked out or not */
@property (atomic, assign) BOOL checkedOut;
/** Crash on errors */
@property (atomic, assign) BOOL crashOnErrors;
/** Logs errors */
@property (atomic, assign) BOOL logsErrors;
/** Dictionary of cached statements */
@property (atomic, retain, nullable) NSMutableDictionary *cachedStatements;
///---------------------
/// @name Initialization
///---------------------
/** Create a @c SqfliteDarwinDatabase object.
An @c SqfliteDarwinDatabase is created with a path to a SQLite database file. This path can be one of these three:
1. A file system path. The file does not have to exist on disk. If it does not exist, it is created for you.
2. An zero-length string. An empty database is created at a temporary location. This database is deleted with the @c SqfliteDarwinDatabase connection is closed.
3. @c nil . An in-memory database is created. This database will be destroyed with the @c SqfliteDarwinDatabase connection is closed.
For example, to open a database in the app's “Application Support” directory:
@code
NSURL *folder = [[NSFileManager defaultManager] URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:true error:&error];
NSURL *fileURL = [folder URLByAppendingPathComponent:@"test.db"];
SqfliteDarwinDatabase *db = [SqfliteDarwinDatabase databaseWithPath:fileURL.path];
@endcode
(For more information on temporary and in-memory databases, read the sqlite documentation on the subject: [https://sqlite.org/inmemorydb.html](https://sqlite.org/inmemorydb.html))
@param inPath Path of database file
@return @c SqfliteDarwinDatabase object if successful; @c nil if failure.
*/
+ (instancetype)databaseWithPath:(NSString * _Nullable)inPath;
/** Create a @c SqfliteDarwinDatabase object.
An @c SqfliteDarwinDatabase is created with a path to a SQLite database file. This path can be one of these three:
1. A file system URL. The file does not have to exist on disk. If it does not exist, it is created for you.
2. @c nil . An in-memory database is created. This database will be destroyed with the @c SqfliteDarwinDatabase connection is closed.
For example, to open a database in the app's “Application Support” directory:
@code
NSURL *folder = [[NSFileManager defaultManager] URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:true error:&error];
NSURL *fileURL = [folder URLByAppendingPathComponent:@"test.db"];
SqfliteDarwinDatabase *db = [SqfliteDarwinDatabase databaseWithURL:fileURL];
@endcode
(For more information on temporary and in-memory databases, read the sqlite documentation on the subject: [https://sqlite.org/inmemorydb.html](https://sqlite.org/inmemorydb.html))
@param url The local file URL (not remote URL) of database file
@return @c SqfliteDarwinDatabase object if successful; @c nil if failure.
*/
+ (instancetype)databaseWithURL:(NSURL * _Nullable)url;
/** Initialize a @c SqfliteDarwinDatabase object.
An @c SqfliteDarwinDatabase is created with a path to a SQLite database file. This path can be one of these three:
1. A file system path. The file does not have to exist on disk. If it does not exist, it is created for you.
2. A zero-length string. An empty database is created at a temporary location. This database is deleted with the @c SqfliteDarwinDatabase connection is closed.
3. @c nil . An in-memory database is created. This database will be destroyed with the @c SqfliteDarwinDatabase connection is closed.
For example, to open a database in the app's “Application Support” directory:
@code
NSURL *folder = [[NSFileManager defaultManager] URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:true error:&error];
NSURL *fileURL = [folder URLByAppendingPathComponent:@"test.db"];
SqfliteDarwinDatabase *db = [[SqfliteDarwinDatabase alloc] initWithPath:fileURL.path];
@endcode
(For more information on temporary and in-memory databases, read the sqlite documentation on the subject: [https://sqlite.org/inmemorydb.html](https://sqlite.org/inmemorydb.html))
@param path Path of database file.
@return @c SqfliteDarwinDatabase object if successful; @c nil if failure.
*/
- (instancetype)initWithPath:(NSString * _Nullable)path;
/** Initialize a @c SqfliteDarwinDatabase object.
An @c SqfliteDarwinDatabase is created with a local file URL to a SQLite database file. This path can be one of these three:
1. A file system URL. The file does not have to exist on disk. If it does not exist, it is created for you.
2. @c nil . An in-memory database is created. This database will be destroyed with the @c SqfliteDarwinDatabase connection is closed.
For example, to open a database in the app's “Application Support” directory:
@code
NSURL *folder = [[NSFileManager defaultManager] URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:true error:&error];
NSURL *fileURL = [folder URLByAppendingPathComponent:@"test.db"];
SqfliteDarwinDatabase *db = [[SqfliteDarwinDatabase alloc] initWithURL:fileURL];
@endcode
(For more information on temporary and in-memory databases, read the sqlite documentation on the subject: [https://sqlite.org/inmemorydb.html](https://sqlite.org/inmemorydb.html))
@param url The file @c NSURL of database file.
@return @c SqfliteDarwinDatabase object if successful; @c nil if failure.
*/
- (instancetype)initWithURL:(NSURL * _Nullable)url;
///-----------------------------------
/// @name Opening and closing database
///-----------------------------------
/// Is the database open or not?
@property (nonatomic) BOOL isOpen;
/** Opening a new database connection
The database is opened for reading and writing, and is created if it does not already exist.
@return @c YES if successful, @c NO on error.
@see [sqlite3_open()](https://sqlite.org/c3ref/open.html)
@see openWithFlags:
@see close
*/
- (BOOL)open;
/** Opening a new database connection with flags and an optional virtual file system (VFS)
@param flags One of the following three values, optionally combined with the @c SQLITE_OPEN_NOMUTEX , @c SQLITE_OPEN_FULLMUTEX , @c SQLITE_OPEN_SHAREDCACHE , @c SQLITE_OPEN_PRIVATECACHE , and/or @c SQLITE_OPEN_URI flags:
@code
SQLITE_OPEN_READONLY
@endcode
The database is opened in read-only mode. If the database does not already exist, an error is returned.
@code
SQLITE_OPEN_READWRITE
@endcode
The database is opened for reading and writing if possible, or reading only if the file is write protected by the operating system. In either case the database must already exist, otherwise an error is returned.
@code
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE
@endcode
The database is opened for reading and writing, and is created if it does not already exist. This is the behavior that is always used for @c open method.
@return @c YES if successful, @c NO on error.
@see [sqlite3_open_v2()](https://sqlite.org/c3ref/open.html)
@see open
@see close
*/
- (BOOL)openWithFlags:(int)flags;
/** Opening a new database connection with flags and an optional virtual file system (VFS)
@param flags One of the following three values, optionally combined with the @c SQLITE_OPEN_NOMUTEX , `SQLITE_OPEN_FULLMUTEX`, `SQLITE_OPEN_SHAREDCACHE`, @c SQLITE_OPEN_PRIVATECACHE , and/or @c SQLITE_OPEN_URI flags:
@code
SQLITE_OPEN_READONLY
@endcode
The database is opened in read-only mode. If the database does not already exist, an error is returned.
@code
SQLITE_OPEN_READWRITE
@endcode
The database is opened for reading and writing if possible, or reading only if the file is write protected by the operating system. In either case the database must already exist, otherwise an error is returned.
@code
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE
@endcode
The database is opened for reading and writing, and is created if it does not already exist. This is the behavior that is always used for @c open method.
@param vfsName If vfs is given the value is passed to the vfs parameter of sqlite3_open_v2.
@return @c YES if successful, @c NO on error.
@see [sqlite3_open_v2()](https://sqlite.org/c3ref/open.html)
@see open
@see close
*/
- (BOOL)openWithFlags:(int)flags vfs:(NSString * _Nullable)vfsName;
/** Closing a database connection
@return @c YES if success, @c NO on error.
@see [sqlite3_close()](https://sqlite.org/c3ref/close.html)
@see open
@see openWithFlags:
*/
- (BOOL)close;
/** Test to see if we have a good connection to the database.
This will confirm whether:
- is database open
- if open, it will try a simple @c SELECT statement and confirm that it succeeds.
@return @c YES if everything succeeds, @c NO on failure.
*/
@property (nonatomic, readonly) BOOL goodConnection;
///----------------------
/// @name Perform updates
///----------------------
/** Execute single update statement
This method executes a single SQL update statement (i.e. any SQL that does not return results, such as @c UPDATE , @c INSERT , or @c DELETE . This method employs [`sqlite3_prepare_v2`](https://sqlite.org/c3ref/prepare.html), [`sqlite3_bind`](https://sqlite.org/c3ref/bind_blob.html) to bind values to `?` placeholders in the SQL with the optional list of parameters, and [`sqlite_step`](https://sqlite.org/c3ref/step.html) to perform the update.
The optional values provided to this method should be objects (e.g. @c NSString , @c NSNumber , @c NSNull , @c NSDate , and @c NSData objects), not fundamental data types (e.g. @c int , @c long , @c NSInteger , etc.). This method automatically handles the aforementioned object types, and all other object types will be interpreted as text values using the object's @c description method.
@param sql The SQL to be performed, with optional `?` placeholders. This can be followed by iptional parameters to bind to `?` placeholders in the SQL statement. These should be Objective-C objects (e.g. @c NSString , @c NSNumber , etc.), not fundamental C data types (e.g. @c int , etc.).
@param outErr A reference to the @c NSError pointer to be updated with an auto released @c NSError object if an error if an error occurs. If @c nil , no @c NSError object will be returned.
@return @c YES upon success; @c NO upon failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see lastError
@see lastErrorCode
@see lastErrorMessage
@see [`sqlite3_bind`](https://sqlite.org/c3ref/bind_blob.html)
*/
- (BOOL)executeUpdate:(NSString*)sql withErrorAndBindings:(NSError * _Nullable __autoreleasing *)outErr, ...;
/** Execute single update statement
@see executeUpdate:withErrorAndBindings:
@warning **Deprecated**: Please use `<executeUpdate:withErrorAndBindings>` instead.
*/
- (BOOL)update:(NSString*)sql withErrorAndBindings:(NSError * _Nullable __autoreleasing *)outErr, ... __deprecated_msg("Use executeUpdate:withErrorAndBindings: instead");;
/** Execute single update statement
This method executes a single SQL update statement (i.e. any SQL that does not return results, such as @c UPDATE , @c INSERT , or @c DELETE . This method employs [`sqlite3_prepare_v2`](https://sqlite.org/c3ref/prepare.html), [`sqlite3_bind`](https://sqlite.org/c3ref/bind_blob.html) to bind values to `?` placeholders in the SQL with the optional list of parameters, and [`sqlite_step`](https://sqlite.org/c3ref/step.html) to perform the update.
The optional values provided to this method should be objects (e.g. @c NSString , @c NSNumber , @c NSNull , @c NSDate , and @c NSData objects), not fundamental data types (e.g. @c int , @c long , @c NSInteger , etc.). This method automatically handles the aforementioned object types, and all other object types will be interpreted as text values using the object's @c description method.
@param sql The SQL to be performed, with optional `?` placeholders, followed by optional parameters to bind to `?` placeholders in the SQL statement. These should be Objective-C objects (e.g. @c NSString , @c NSNumber , etc.), not fundamental C data types (e.g. @c int , etc.).
@return @c YES upon success; @c NO upon failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see lastError
@see lastErrorCode
@see lastErrorMessage
@see [`sqlite3_bind`](https://sqlite.org/c3ref/bind_blob.html)
@note This technique supports the use of `?` placeholders in the SQL, automatically binding any supplied value parameters to those placeholders. This approach is more robust than techniques that entail using @c stringWithFormat to manually build SQL statements, which can be problematic if the values happened to include any characters that needed to be quoted.
@note You cannot use this method from Swift due to incompatibilities between Swift and Objective-C variadic implementations. Consider using `<executeUpdate:values:>` instead.
*/
- (BOOL)executeUpdate:(NSString*)sql, ...;
/** Execute single update statement
This method executes a single SQL update statement (i.e. any SQL that does not return results, such as @c UPDATE , @c INSERT , or @c DELETE . This method employs [`sqlite3_prepare_v2`](https://sqlite.org/c3ref/prepare.html) and [`sqlite_step`](https://sqlite.org/c3ref/step.html) to perform the update. Unlike the other @c executeUpdate methods, this uses printf-style formatters (e.g. `%s`, `%d`, etc.) to build the SQL. Do not use `?` placeholders in the SQL if you use this method.
@param format The SQL to be performed, with `printf`-style escape sequences, followed by optional parameters to bind to use in conjunction with the `printf`-style escape sequences in the SQL statement.
@return @c YES upon success; @c NO upon failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see executeUpdate:
@see lastError
@see lastErrorCode
@see lastErrorMessage
@note This method does not technically perform a traditional printf-style replacement. What this method actually does is replace the printf-style percent sequences with a SQLite `?` placeholder, and then bind values to that placeholder. Thus the following command
@code
[db executeUpdateWithFormat:@"INSERT INTO test (name) VALUES (%@)", @"Gus"];
@endcode
is actually replacing the `%@` with `?` placeholder, and then performing something equivalent to `<executeUpdate:>`
@code
[db executeUpdate:@"INSERT INTO test (name) VALUES (?)", @"Gus"];
@endcode
There are two reasons why this distinction is important. First, the printf-style escape sequences can only be used where it is permissible to use a SQLite `?` placeholder. You can use it only for values in SQL statements, but not for table names or column names or any other non-value context. This method also cannot be used in conjunction with `pragma` statements and the like. Second, note the lack of quotation marks in the SQL. The `VALUES` clause was _not_ `VALUES ('%@')` (like you might have to do if you built a SQL statement using @c NSString method @c stringWithFormat ), but rather simply `VALUES (%@)`.
*/
- (BOOL)executeUpdateWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
/** Execute single update statement
This method executes a single SQL update statement (i.e. any SQL that does not return results, such as @c UPDATE , @c INSERT , or @c DELETE . This method employs [`sqlite3_prepare_v2`](https://sqlite.org/c3ref/prepare.html) and [`sqlite3_bind`](https://sqlite.org/c3ref/bind_blob.html) binding any `?` placeholders in the SQL with the optional list of parameters.
The optional values provided to this method should be objects (e.g. @c NSString , @c NSNumber , @c NSNull , @c NSDate , and @c NSData objects), not fundamental data types (e.g. @c int , @c long , @c NSInteger , etc.). This method automatically handles the aforementioned object types, and all other object types will be interpreted as text values using the object's @c description method.
@param sql The SQL to be performed, with optional `?` placeholders.
@param arguments A @c NSArray of objects to be used when binding values to the `?` placeholders in the SQL statement.
@return @c YES upon success; @c NO upon failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see executeUpdate:values:error:
@see lastError
@see lastErrorCode
@see lastErrorMessage
*/
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments;
/** Execute single update statement
This method executes a single SQL update statement (i.e. any SQL that does not return results, such as @c UPDATE , @c INSERT , or @c DELETE . This method employs [`sqlite3_prepare_v2`](https://sqlite.org/c3ref/prepare.html) and [`sqlite3_bind`](https://sqlite.org/c3ref/bind_blob.html) binding any `?` placeholders in the SQL with the optional list of parameters.
The optional values provided to this method should be objects (e.g. @c NSString , @c NSNumber , @c NSNull , @c NSDate , and @c NSData objects), not fundamental data types (e.g. @c int , @c long , @c NSInteger , etc.). This method automatically handles the aforementioned object types, and all other object types will be interpreted as text values using the object's @c description method.
This is similar to @c executeUpdate:withArgumentsInArray: , except that this also accepts a pointer to a @c NSError pointer, so that errors can be returned.
In Swift, this throws errors, as if it were defined as follows:
@code
func executeUpdate(sql: String, values: [Any]?) throws -> Bool { }
@endcode
@param sql The SQL to be performed, with optional `?` placeholders.
@param values A @c NSArray of objects to be used when binding values to the `?` placeholders in the SQL statement.
@param error A @c NSError object to receive any error object (if any).
@return @c YES upon success; @c NO upon failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see lastError
@see lastErrorCode
@see lastErrorMessage
*/
- (BOOL)executeUpdate:(NSString*)sql values:(NSArray * _Nullable)values error:(NSError * _Nullable __autoreleasing *)error;
/** Execute single update statement
This method executes a single SQL update statement (i.e. any SQL that does not return results, such as @c UPDATE , @c INSERT , or @c DELETE . This method employs [`sqlite3_prepare_v2`](https://sqlite.org/c3ref/prepare.html) and [`sqlite_step`](https://sqlite.org/c3ref/step.html) to perform the update. Unlike the other @c executeUpdate methods, this uses printf-style formatters (e.g. `%s`, `%d`, etc.) to build the SQL.
The optional values provided to this method should be objects (e.g. @c NSString , @c NSNumber , @c NSNull , @c NSDate , and @c NSData objects), not fundamental data types (e.g. @c int , @c long , @c NSInteger , etc.). This method automatically handles the aforementioned object types, and all other object types will be interpreted as text values using the object's @c description method.
@param sql The SQL to be performed, with optional `?` placeholders.
@param arguments A @c NSDictionary of objects keyed by column names that will be used when binding values to the `?` placeholders in the SQL statement.
@return @c YES upon success; @c NO upon failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see lastError
@see lastErrorCode
@see lastErrorMessage
*/
- (BOOL)executeUpdate:(NSString*)sql withParameterDictionary:(NSDictionary *)arguments;
/** Execute single update statement
This method executes a single SQL update statement (i.e. any SQL that does not return results, such as @c UPDATE , @c INSERT , or @c DELETE . This method employs [`sqlite3_prepare_v2`](https://sqlite.org/c3ref/prepare.html) and [`sqlite_step`](https://sqlite.org/c3ref/step.html) to perform the update. Unlike the other @c executeUpdate methods, this uses printf-style formatters (e.g. `%s`, `%d`, etc.) to build the SQL.
The optional values provided to this method should be objects (e.g. @c NSString , @c NSNumber , @c NSNull , @c NSDate , and @c NSData objects), not fundamental data types (e.g. @c int , @c long , @c NSInteger , etc.). This method automatically handles the aforementioned object types, and all other object types will be interpreted as text values using the object's @c description method.
@param sql The SQL to be performed, with optional `?` placeholders.
@param args A `va_list` of arguments.
@return @c YES upon success; @c NO upon failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see lastError
@see lastErrorCode
@see lastErrorMessage
*/
- (BOOL)executeUpdate:(NSString*)sql withVAList: (va_list)args;
/** Execute multiple SQL statements
This executes a series of SQL statements that are combined in a single string (e.g. the SQL generated by the `sqlite3` command line `.dump` command). This accepts no value parameters, but rather simply expects a single string with multiple SQL statements, each terminated with a semicolon. This uses @c sqlite3_exec .
@param sql The SQL to be performed
@return @c YES upon success; @c NO upon failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see executeStatements:withResultBlock:
@see [sqlite3_exec()](https://sqlite.org/c3ref/exec.html)
*/
- (BOOL)executeStatements:(NSString *)sql;
/** Execute multiple SQL statements with callback handler
This executes a series of SQL statements that are combined in a single string (e.g. the SQL generated by the `sqlite3` command line `.dump` command). This accepts no value parameters, but rather simply expects a single string with multiple SQL statements, each terminated with a semicolon. This uses `sqlite3_exec`.
@param sql The SQL to be performed.
@param block A block that will be called for any result sets returned by any SQL statements.
Note, if you supply this block, it must return integer value, zero upon success (this would be a good opportunity to use @c SQLITE_OK ),
non-zero value upon failure (which will stop the bulk execution of the SQL). If a statement returns values, the block will be called with the results from the query in NSDictionary *resultsDictionary.
This may be @c nil if you don't care to receive any results.
@return @c YES upon success; @c NO upon failure. If failed, you can call @c lastError ,
@c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see executeStatements:
@see [sqlite3_exec()](https://sqlite.org/c3ref/exec.html)
*/
- (BOOL)executeStatements:(NSString *)sql withResultBlock:(__attribute__((noescape)) SqfliteDarwinDBExecuteStatementsCallbackBlock _Nullable)block;
/** Last insert rowid
Each entry in an SQLite table has a unique 64-bit signed integer key called the "rowid". The rowid is always available as an undeclared column named `ROWID`, `OID`, or `_ROWID_` as long as those names are not also used by explicitly declared columns. If the table has a column of type `INTEGER PRIMARY KEY` then that column is another alias for the rowid.
This routine returns the rowid of the most recent successful @c INSERT into the database from the database connection in the first argument. As of SQLite version 3.7.7, this routines records the last insert rowid of both ordinary tables and virtual tables. If no successful @c INSERT statements have ever occurred on that database connection, zero is returned.
@return The rowid of the last inserted row.
@see [sqlite3_last_insert_rowid()](https://sqlite.org/c3ref/last_insert_rowid.html)
*/
@property (nonatomic, readonly) int64_t lastInsertRowId;
/** The number of rows changed by prior SQL statement.
This function returns the number of database rows that were changed or inserted or deleted by the most recently completed SQL statement on the database connection specified by the first parameter. Only changes that are directly specified by the @c INSERT , @c UPDATE , or @c DELETE statement are counted.
@return The number of rows changed by prior SQL statement.
@see [sqlite3_changes()](https://sqlite.org/c3ref/changes.html)
*/
@property (nonatomic, readonly) int changes;
///-------------------------
/// @name Retrieving results
///-------------------------
/** Execute select statement
Executing queries returns an @c SqfliteDarwinResultSet object if successful, and @c nil upon failure. Like executing updates, there is a variant that accepts an `NSError **` parameter. Otherwise you should use the @c lastErrorMessage and @c lastErrorMessage methods to determine why a query failed.
In order to iterate through the results of your query, you use a `while()` loop. You also need to "step" (via `<[SqfliteDarwinResultSet next]>`) from one record to the other.
This method employs [`sqlite3_bind`](https://sqlite.org/c3ref/bind_blob.html) for any optional value parameters. This properly escapes any characters that need escape sequences (e.g. quotation marks), which eliminates simple SQL errors as well as protects against SQL injection attacks. This method natively handles @c NSString , @c NSNumber , @c NSNull , @c NSDate , and @c NSData objects. All other object types will be interpreted as text values using the object's @c description method.
@param sql The SELECT statement to be performed, with optional `?` placeholders, followed by optional parameters to bind to `?` placeholders in the SQL statement. These should be Objective-C objects (e.g. @c NSString , @c NSNumber , etc.), not fundamental C data types (e.g. @c int , etc.).
@return A @c SqfliteDarwinResultSet for the result set upon success; @c nil upon failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see SqfliteDarwinResultSet
@see [`SqfliteDarwinResultSet next`](<[SqfliteDarwinResultSet next]>)
@see [`sqlite3_bind`](https://sqlite.org/c3ref/bind_blob.html)
@note You cannot use this method from Swift due to incompatibilities between Swift and Objective-C variadic implementations. Consider using `<executeQuery:values:>` instead.
*/
- (SqfliteDarwinResultSet * _Nullable)executeQuery:(NSString*)sql, ...;
/** Execute select statement
Executing queries returns an @c SqfliteDarwinResultSet object if successful, and @c nil upon failure. Like executing updates, there is a variant that accepts an `NSError **` parameter. Otherwise you should use the @c lastErrorMessage and @c lastErrorMessage methods to determine why a query failed.
In order to iterate through the results of your query, you use a `while()` loop. You also need to "step" (via `<[SqfliteDarwinResultSet next]>`) from one record to the other.
@param format The SQL to be performed, with `printf`-style escape sequences, followed by ptional parameters to bind to use in conjunction with the `printf`-style escape sequences in the SQL statement.
@return A @c SqfliteDarwinResultSet for the result set upon success; @c nil upon failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see executeQuery:
@see SqfliteDarwinResultSet
@see [`SqfliteDarwinResultSet next`](<[SqfliteDarwinResultSet next]>)
@note This method does not technically perform a traditional printf-style replacement. What this method actually does is replace the printf-style percent sequences with a SQLite `?` placeholder, and then bind values to that placeholder. Thus the following command
@code
[db executeQueryWithFormat:@"SELECT * FROM test WHERE name=%@", @"Gus"];
@endcode
is actually replacing the `%@` with `?` placeholder, and then performing something equivalent to `<executeQuery:>`
@code
[db executeQuery:@"SELECT * FROM test WHERE name=?", @"Gus"];
@endcode
There are two reasons why this distinction is important. First, the printf-style escape sequences can only be used where it is permissible to use a SQLite `?` placeholder. You can use it only for values in SQL statements, but not for table names or column names or any other non-value context. This method also cannot be used in conjunction with `pragma` statements and the like. Second, note the lack of quotation marks in the SQL. The @c WHERE clause was _not_ `WHERE name='%@'` (like you might have to do if you built a SQL statement using @c NSString method @c stringWithFormat ), but rather simply `WHERE name=%@`.
*/
- (SqfliteDarwinResultSet * _Nullable)executeQueryWithFormat:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
/** Execute select statement
Executing queries returns an @c SqfliteDarwinResultSet object if successful, and @c nil upon failure. Like executing updates, there is a variant that accepts an `NSError **` parameter. Otherwise you should use the @c lastErrorMessage and @c lastErrorMessage methods to determine why a query failed.
In order to iterate through the results of your query, you use a `while()` loop. You also need to "step" (via `<[SqfliteDarwinResultSet next]>`) from one record to the other.
@param sql The SELECT statement to be performed, with optional `?` placeholders.
@param arguments A @c NSArray of objects to be used when binding values to the `?` placeholders in the SQL statement.
@return A @c SqfliteDarwinResultSet for the result set upon success; @c nil upon failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see -executeQuery:values:error:
@see SqfliteDarwinResultSet
@see [`SqfliteDarwinResultSet next`](<[SqfliteDarwinResultSet next]>)
*/
- (SqfliteDarwinResultSet * _Nullable)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments;
/** Execute select statement
Executing queries returns an @c SqfliteDarwinResultSet object if successful, and @c nil upon failure. Like executing updates, there is a variant that accepts an `NSError **` parameter. Otherwise you should use the @c lastErrorMessage and @c lastErrorMessage methods to determine why a query failed.
In order to iterate through the results of your query, you use a `while()` loop. You also need to "step" (via `<[SqfliteDarwinResultSet next]>`) from one record to the other.
This is similar to `<executeQuery:withArgumentsInArray:>`, except that this also accepts a pointer to a @c NSError pointer, so that errors can be returned.
In Swift, this throws errors, as if it were defined as follows:
`func executeQuery(sql: String, values: [Any]?) throws -> SqfliteDarwinResultSet!`
@param sql The SELECT statement to be performed, with optional `?` placeholders.
@param values A @c NSArray of objects to be used when binding values to the `?` placeholders in the SQL statement.
@param error A @c NSError object to receive any error object (if any).
@return A @c SqfliteDarwinResultSet for the result set upon success; @c nil upon failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see SqfliteDarwinResultSet
@see [`SqfliteDarwinResultSet next`](<[SqfliteDarwinResultSet next]>)
@note When called from Swift, only use the first two parameters, `sql` and `values`. This but throws the error.
*/
- (SqfliteDarwinResultSet * _Nullable)executeQuery:(NSString *)sql values:(NSArray * _Nullable)values error:(NSError * _Nullable __autoreleasing *)error;
/** Execute select statement
Executing queries returns an @c SqfliteDarwinResultSet object if successful, and @c nil upon failure. Like executing updates, there is a variant that accepts an `NSError **` parameter. Otherwise you should use the @c lastErrorMessage and @c lastErrorMessage methods to determine why a query failed.
In order to iterate through the results of your query, you use a `while()` loop. You also need to "step" (via `<[SqfliteDarwinResultSet next]>`) from one record to the other.
@param sql The SELECT statement to be performed, with optional `?` placeholders.
@param arguments A @c NSDictionary of objects keyed by column names that will be used when binding values to the `?` placeholders in the SQL statement.
@return A @c SqfliteDarwinResultSet for the result set upon success; @c nil upon failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see SqfliteDarwinResultSet
@see [`SqfliteDarwinResultSet next`](<[SqfliteDarwinResultSet next]>)
*/
- (SqfliteDarwinResultSet * _Nullable)executeQuery:(NSString *)sql withParameterDictionary:(NSDictionary * _Nullable)arguments;
// Documentation forthcoming.
- (SqfliteDarwinResultSet * _Nullable)executeQuery:(NSString *)sql withVAList:(va_list)args;
/// Prepare SQL statement.
///
/// @param sql SQL statement to prepare, generally with `?` placeholders.
- (SqfliteDarwinResultSet *)prepare:(NSString *)sql;
///-------------------
/// @name Transactions
///-------------------
/** Begin a transaction
@return @c YES on success; @c NO on failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see commit
@see rollback
@see beginDeferredTransaction
@see isInTransaction
@warning Unlike SQLite's `BEGIN TRANSACTION`, this method currently performs
an exclusive transaction, not a deferred transaction. This behavior
is likely to change in future versions of SqfliteDarwinDB, whereby this method
will likely eventually adopt standard SQLite behavior and perform
deferred transactions. If you really need exclusive tranaction, it is
recommended that you use @c beginExclusiveTransaction, instead, not
only to make your intent explicit, but also to future-proof your code.
*/
- (BOOL)beginTransaction;
/** Begin a deferred transaction
@return @c YES on success; @c NO on failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see commit
@see rollback
@see beginTransaction
@see isInTransaction
*/
- (BOOL)beginDeferredTransaction;
/** Begin an immediate transaction
@return @c YES on success; @c NO on failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see commit
@see rollback
@see beginTransaction
@see isInTransaction
*/
- (BOOL)beginImmediateTransaction;
/** Begin an exclusive transaction
@return @c YES on success; @c NO on failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see commit
@see rollback
@see beginTransaction
@see isInTransaction
*/
- (BOOL)beginExclusiveTransaction;
/** Commit a transaction
Commit a transaction that was initiated with either `<beginTransaction>` or with `<beginDeferredTransaction>`.
@return @c YES on success; @c NO on failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see beginTransaction
@see beginDeferredTransaction
@see rollback
@see isInTransaction
*/
- (BOOL)commit;
/** Rollback a transaction
Rollback a transaction that was initiated with either `<beginTransaction>` or with `<beginDeferredTransaction>`.
@return @c YES on success; @c NO on failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see beginTransaction
@see beginDeferredTransaction
@see commit
@see isInTransaction
*/
- (BOOL)rollback;
/** Identify whether currently in a transaction or not
@see beginTransaction
@see beginDeferredTransaction
@see commit
@see rollback
*/
@property (nonatomic, readonly) BOOL isInTransaction;
- (BOOL)inTransaction __deprecated_msg("Use isInTransaction property instead");
///----------------------------------------
/// @name Cached statements and result sets
///----------------------------------------
/** Clear cached statements */
- (void)clearCachedStatements;
/** Close all open result sets */
- (void)closeOpenResultSets;
/** Whether database has any open result sets
@return @c YES if there are open result sets; @c NO if not.
*/
@property (nonatomic, readonly) BOOL hasOpenResultSets;
/** Whether should cache statements or not
*/
@property (nonatomic) BOOL shouldCacheStatements;
/** Interupt pending database operation
This method causes any pending database operation to abort and return at its earliest opportunity
@return @c YES on success; @c NO on failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
*/
- (BOOL)interrupt;
///-------------------------
/// @name Encryption methods
///-------------------------
/** Set encryption key.
@param key The key to be used.
@return @c YES if success, @c NO on error.
@see https://www.zetetic.net/sqlcipher/
@warning You need to have purchased the sqlite encryption extensions for this method to work.
*/
- (BOOL)setKey:(NSString*)key;
/** Reset encryption key
@param key The key to be used.
@return @c YES if success, @c NO on error.
@see https://www.zetetic.net/sqlcipher/
@warning You need to have purchased the sqlite encryption extensions for this method to work.
*/
- (BOOL)rekey:(NSString*)key;
/** Set encryption key using `keyData`.
@param keyData The @c NSData to be used.
@return @c YES if success, @c NO on error.
@see https://www.zetetic.net/sqlcipher/
@warning You need to have purchased the sqlite encryption extensions for this method to work.
*/
- (BOOL)setKeyWithData:(NSData *)keyData;
/** Reset encryption key using `keyData`.
@param keyData The @c NSData to be used.
@return @c YES if success, @c NO on error.
@see https://www.zetetic.net/sqlcipher/
@warning You need to have purchased the sqlite encryption extensions for this method to work.
*/
- (BOOL)rekeyWithData:(NSData *)keyData;
///------------------------------
/// @name General inquiry methods
///------------------------------
/** The path of the database file.
*/
@property (nonatomic, readonly, nullable) NSString *databasePath;
/** The file URL of the database file.
*/
@property (nonatomic, readonly, nullable) NSURL *databaseURL;
/** The underlying SQLite handle .
@return The `sqlite3` pointer.
*/
@property (nonatomic, readonly) void *sqliteHandle;
///-----------------------------
/// @name Retrieving error codes
///-----------------------------
/** Last error message
Returns the English-language text that describes the most recent failed SQLite API call associated with a database connection. If a prior API call failed but the most recent API call succeeded, this return value is undefined.
@return @c NSString of the last error message.
@see [sqlite3_errmsg()](https://sqlite.org/c3ref/errcode.html)
@see lastErrorCode
@see lastError
*/
- (NSString*)lastErrorMessage;
/** Last error code
Returns the numeric result code or extended result code for the most recent failed SQLite API call associated with a database connection. If a prior API call failed but the most recent API call succeeded, this return value is undefined.
@return Integer value of the last error code.
@see [sqlite3_errcode()](https://sqlite.org/c3ref/errcode.html)
@see lastErrorMessage
@see lastError
*/
- (int)lastErrorCode;
/** Last extended error code
Returns the numeric extended result code for the most recent failed SQLite API call associated with a database connection. If a prior API call failed but the most recent API call succeeded, this return value is undefined.
@return Integer value of the last extended error code.
@see [sqlite3_errcode()](https://sqlite.org/c3ref/errcode.html)
@see [2. Primary Result Codes versus Extended Result Codes](https://sqlite.org/rescode.html#primary_result_codes_versus_extended_result_codes)
@see [5. Extended Result Code List](https://sqlite.org/rescode.html#extrc)
@see lastErrorMessage
@see lastError
*/
- (int)lastExtendedErrorCode;
/** Had error
@return @c YES if there was an error, @c NO if no error.
@see lastError
@see lastErrorCode
@see lastErrorMessage
*/
- (BOOL)hadError;
/** Last error
@return @c NSError representing the last error.
@see lastErrorCode
@see lastErrorMessage
*/
- (NSError *)lastError;
// description forthcoming
@property (nonatomic) NSTimeInterval maxBusyRetryTimeInterval;
///------------------
/// @name Save points
///------------------
/** Start save point
@param name Name of save point.
@param outErr A @c NSError object to receive any error object (if any).
@return @c YES on success; @c NO on failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see releaseSavePointWithName:error:
@see rollbackToSavePointWithName:error:
*/
- (BOOL)startSavePointWithName:(NSString*)name error:(NSError * _Nullable __autoreleasing *)outErr;
/** Release save point
@param name Name of save point.
@param outErr A @c NSError object to receive any error object (if any).
@return @c YES on success; @c NO on failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see startSavePointWithName:error:
@see rollbackToSavePointWithName:error:
*/
- (BOOL)releaseSavePointWithName:(NSString*)name error:(NSError * _Nullable __autoreleasing *)outErr;
/** Roll back to save point
@param name Name of save point.
@param outErr A @c NSError object to receive any error object (if any).
@return @c YES on success; @c NO on failure. If failed, you can call @c lastError , @c lastErrorCode , or @c lastErrorMessage for diagnostic information regarding the failure.
@see startSavePointWithName:error:
@see releaseSavePointWithName:error:
*/
- (BOOL)rollbackToSavePointWithName:(NSString*)name error:(NSError * _Nullable __autoreleasing *)outErr;
/** Start save point
@param block Block of code to perform from within save point.
@return The NSError corresponding to the error, if any. If no error, returns @c nil .
@see startSavePointWithName:error:
@see releaseSavePointWithName:error:
@see rollbackToSavePointWithName:error:
*/
- (NSError * _Nullable)inSavePoint:(__attribute__((noescape)) void (^)(BOOL *rollback))block;
///-----------------
/// @name Checkpoint
///-----------------
/** Performs a WAL checkpoint
@param checkpointMode The checkpoint mode for @c sqlite3_wal_checkpoint_v2
@param error The @c NSError corresponding to the error, if any.
@return @c YES on success, otherwise @c NO .
*/
- (BOOL)checkpoint:(SqfliteDarwinDBCheckpointMode)checkpointMode error:(NSError * _Nullable *)error;
/** Performs a WAL checkpoint
@param checkpointMode The checkpoint mode for @c sqlite3_wal_checkpoint_v2
@param name The db name for @c sqlite3_wal_checkpoint_v2
@param error The @c NSError corresponding to the error, if any.
@return @c YES on success, otherwise @c NO .
*/
- (BOOL)checkpoint:(SqfliteDarwinDBCheckpointMode)checkpointMode name:(NSString * _Nullable)name error:(NSError * _Nullable *)error;
/** Performs a WAL checkpoint
@param checkpointMode The checkpoint mode for sqlite3_wal_checkpoint_v2
@param name The db name for sqlite3_wal_checkpoint_v2
@param error The NSError corresponding to the error, if any.
@param logFrameCount If not @c NULL , then this is set to the total number of frames in the log file or to -1 if the checkpoint could not run because of an error or because the database is not in WAL mode.
@param checkpointCount If not @c NULL , then this is set to the total number of checkpointed frames in the log file (including any that were already checkpointed before the function was called) or to -1 if the checkpoint could not run due to an error or because the database is not in WAL mode.
@return @c YES on success, otherwise @c NO .
*/
- (BOOL)checkpoint:(SqfliteDarwinDBCheckpointMode)checkpointMode name:(NSString * _Nullable)name logFrameCount:(int * _Nullable)logFrameCount checkpointCount:(int * _Nullable)checkpointCount error:(NSError * _Nullable *)error;
///----------------------------
/// @name SQLite library status
///----------------------------
/** Test to see if the library is threadsafe
@return @c NO if and only if SQLite was compiled with mutexing code omitted due to the @c SQLITE_THREADSAFE compile-time option being set to 0.
@see [sqlite3_threadsafe()](https://sqlite.org/c3ref/threadsafe.html)
*/
+ (BOOL)isSQLiteThreadSafe;
/** Examine/set limits
@param type The type of limit. See https://sqlite.org/c3ref/c_limit_attached.html
@param newLimit The new limit value. Use -1 if you don't want to change the limit, but rather only want to check it.
@return Regardless, returns previous value.
@see [sqlite3_limit()](https://sqlite.org/c3ref/limit.html)
*/
- (int)limitFor:(int)type value:(int)newLimit;
/** Run-time library version numbers
@return The sqlite library version string.
@see [sqlite3_libversion()](https://sqlite.org/c3ref/libversion.html)
*/
+ (NSString*)sqliteLibVersion;
/// The SqfliteDarwinDB version number as a string in the form of @c "2.7.8" .
///
/// If you want to compare version number strings, you can use NSNumericSearch option:
///
/// @code
/// NSComparisonResult result = [[SqfliteDarwinDatabase SqfliteDarwinDBUserVersion] compare:@"2.11.0" options:NSNumericSearch];
/// @endcode
///
/// @returns The version number string.
+ (NSString*)SqfliteDarwinDBUserVersion;
/** The SqfliteDarwinDB version
This returns the SqfliteDarwinDB as hexadecimal value, e.g., @c 0x0243 for version 2.4.3.
@warning This routine will not work if any component of the version number exceeds 15.
For example, if it is version @c 2.17.3 , this will max out at @c 0x2f3.
For this reason, we would recommend using @c SqfliteDarwinDBUserVersion and with @c NSNumericSearch option, e.g.
@code
NSComparisonResult result = [[SqfliteDarwinDatabase SqfliteDarwinDBUserVersion] compare:@"2.11.0" options:NSNumericSearch];
@endcode
@returns The version number in hexadecimal, e.g., @c 0x0243 for version 2.4.3. If any component exceeds what can be
can be represented in four bits, we'll max it out at @c 0xf .
*/
+ (SInt32)SqfliteDarwinDBVersion __deprecated_msg("Use SqfliteDarwinDBUserVersion instead");
///------------------------
/// @name Make SQL function
///------------------------
/** Adds SQL functions or aggregates or to redefine the behavior of existing SQL functions or aggregates.
For example:
@code
[db makeFunctionNamed:@"RemoveDiacritics" arguments:1 block:^(void *context, int argc, void **argv) {
SqliteValueType type = [self.db valueType:argv[0]];
if (type == SqliteValueTypeNull) {
[self.db resultNullInContext:context];
return;
}
if (type != SqliteValueTypeText) {
[self.db resultError:@"Expected text" context:context];
return;
}
NSString *string = [self.db valueString:argv[0]];
NSString *result = [string stringByFoldingWithOptions:NSDiacriticInsensitiveSearch locale:nil];
[self.db resultString:result context:context];
}];
SqfliteDarwinResultSet *rs = [db executeQuery:@"SELECT * FROM employees WHERE RemoveDiacritics(first_name) LIKE 'jose'"];
NSAssert(rs, @"Error %@", [db lastErrorMessage]);
@endcode
@param name Name of function.
@param arguments Maximum number of parameters.
@param block The block of code for the function.
@see [sqlite3_create_function()](https://sqlite.org/c3ref/create_function.html)
*/
- (void)makeFunctionNamed:(NSString *)name arguments:(int)arguments block:(void (^)(void *context, int argc, void * _Nonnull * _Nonnull argv))block;
- (void)makeFunctionNamed:(NSString *)name maximumArguments:(int)count withBlock:(void (^)(void *context, int argc, void * _Nonnull * _Nonnull argv))block __deprecated_msg("Use makeFunctionNamed:arguments:block:");
- (SqliteValueType)valueType:(void *)argv;
/**
Get integer value of parameter in custom function.
@param value The argument whose value to return.
@return The integer value.
@see makeFunctionNamed:arguments:block:
*/
- (int)valueInt:(void *)value;
/**
Get long value of parameter in custom function.
@param value The argument whose value to return.
@return The long value.
@see makeFunctionNamed:arguments:block:
*/
- (long long)valueLong:(void *)value;
/**
Get double value of parameter in custom function.
@param value The argument whose value to return.
@return The double value.
@see makeFunctionNamed:arguments:block:
*/
- (double)valueDouble:(void *)value;
/**
Get @c NSData value of parameter in custom function.
@param value The argument whose value to return.
@return The data object.
@see makeFunctionNamed:arguments:block:
*/
- (NSData * _Nullable)valueData:(void *)value;
/**
Get string value of parameter in custom function.
@param value The argument whose value to return.
@return The string value.
@see makeFunctionNamed:arguments:block:
*/
- (NSString * _Nullable)valueString:(void *)value;
/**
Return null value from custom function.
@param context The context to which the null value will be returned.
@see makeFunctionNamed:arguments:block:
*/
- (void)resultNullInContext:(void *)context NS_SWIFT_NAME(resultNull(context:));
/**
Return integer value from custom function.
@param value The integer value to be returned.
@param context The context to which the value will be returned.
@see makeFunctionNamed:arguments:block:
*/
- (void)resultInt:(int) value context:(void *)context;
/**
Return long value from custom function.
@param value The long value to be returned.
@param context The context to which the value will be returned.
@see makeFunctionNamed:arguments:block:
*/
- (void)resultLong:(long long)value context:(void *)context;
/**
Return double value from custom function.
@param value The double value to be returned.
@param context The context to which the value will be returned.
@see makeFunctionNamed:arguments:block:
*/
- (void)resultDouble:(double)value context:(void *)context;
/**
Return @c NSData object from custom function.
@param data The @c NSData object to be returned.
@param context The context to which the value will be returned.
@see makeFunctionNamed:arguments:block:
*/
- (void)resultData:(NSData *)data context:(void *)context;
/**
Return string value from custom function.
@param value The string value to be returned.
@param context The context to which the value will be returned.
@see makeFunctionNamed:arguments:block:
*/
- (void)resultString:(NSString *)value context:(void *)context;
/**
Return error string from custom function.
@param error The error string to be returned.
@param context The context to which the error will be returned.
@see makeFunctionNamed:arguments:block:
*/
- (void)resultError:(NSString *)error context:(void *)context;
/**
Return error code from custom function.
@param errorCode The integer error code to be returned.
@param context The context to which the error will be returned.
@see makeFunctionNamed:arguments:block:
*/
- (void)resultErrorCode:(int)errorCode context:(void *)context;
/**
Report memory error in custom function.
@param context The context to which the error will be returned.
@see makeFunctionNamed:arguments:block:
*/
- (void)resultErrorNoMemoryInContext:(void *)context NS_SWIFT_NAME(resultErrorNoMemory(context:));
/**
Report that string or BLOB is too long to represent in custom function.
@param context The context to which the error will be returned.
@see makeFunctionNamed:arguments:block:
*/
- (void)resultErrorTooBigInContext:(void *)context NS_SWIFT_NAME(resultErrorTooBig(context:));
///---------------------
/// @name Date formatter
///---------------------
/** Generate an @c NSDateFormatter that won't be broken by permutations of timezones or locales.
Use this method to generate values to set the dateFormat property.
Example:
@code
myDB.dateFormat = [SqfliteDarwinDatabase storeableDateFormat:@"yyyy-MM-dd HH:mm:ss"];
@endcode
@param format A valid NSDateFormatter format string.
@return A @c NSDateFormatter that can be used for converting dates to strings and vice versa.
@see hasDateFormatter
@see setDateFormat:
@see dateFromString:
@see stringFromDate:
@see storeableDateFormat:
@warning Note that @c NSDateFormatter is not thread-safe, so the formatter generated by this method should be assigned to only one SqfliteDarwinDB instance and should not be used for other purposes.
*/
+ (NSDateFormatter *)storeableDateFormat:(NSString *)format;
/** Test whether the database has a date formatter assigned.
@return @c YES if there is a date formatter; @c NO if not.
@see hasDateFormatter
@see setDateFormat:
@see dateFromString:
@see stringFromDate:
@see storeableDateFormat:
*/
- (BOOL)hasDateFormatter;
/** Set to a date formatter to use string dates with sqlite instead of the default UNIX timestamps.
@param format Set to nil to use UNIX timestamps. Defaults to nil. Should be set using a formatter generated using @c SqfliteDarwinDatabase:storeableDateFormat .
@see hasDateFormatter
@see setDateFormat:
@see dateFromString:
@see stringFromDate:
@see storeableDateFormat:
@warning Note there is no direct getter for the @c NSDateFormatter , and you should not use the formatter you pass to SqfliteDarwinDB for other purposes, as @c NSDateFormatter is not thread-safe.
*/
- (void)setDateFormat:(NSDateFormatter * _Nullable)format;
/** Convert the supplied NSString to NSDate, using the current database formatter.
@param s @c NSString to convert to @c NSDate .
@return The @c NSDate object; or @c nil if no formatter is set.
@see hasDateFormatter
@see setDateFormat:
@see dateFromString:
@see stringFromDate:
@see storeableDateFormat:
*/
- (NSDate * _Nullable)dateFromString:(NSString *)s;
/** Convert the supplied NSDate to NSString, using the current database formatter.
@param date @c NSDate of date to convert to @c NSString .
@return The @c NSString representation of the date; @c nil if no formatter is set.
@see hasDateFormatter
@see setDateFormat:
@see dateFromString:
@see stringFromDate:
@see storeableDateFormat:
*/
- (NSString * _Nullable)stringFromDate:(NSDate *)date;
@end
/** Objective-C wrapper for @c sqlite3_stmt
This is a wrapper for a SQLite @c sqlite3_stmt . Generally when using SqfliteDarwinDB you will not need to interact directly with `SqfliteDarwinStatement`, but rather with @c SqfliteDarwinDatabase and @c SqfliteDarwinResultSet only.
See also
- @c SqfliteDarwinDatabase
- @c SqfliteDarwinResultSet
- [@c sqlite3_stmt ](https://sqlite.org/c3ref/stmt.html)
*/
@interface SqfliteDarwinStatement : NSObject {
void *_statement;
NSString *_query;
long _useCount;
BOOL _inUse;
}
///-----------------
/// @name Properties
///-----------------
/** Usage count */
@property (atomic, assign) long useCount;
/** SQL statement */
@property (atomic, retain) NSString *query;
/** SQLite sqlite3_stmt
@see [@c sqlite3_stmt ](https://sqlite.org/c3ref/stmt.html)
*/
@property (atomic, assign) void *statement;
/** Indication of whether the statement is in use */
@property (atomic, assign) BOOL inUse;
///----------------------------
/// @name Closing and Resetting
///----------------------------
/** Close statement */
- (void)close;
/** Reset statement */
- (void)reset;
@end
#pragma clang diagnostic pop
NS_ASSUME_NONNULL_END
#import "SqfliteDarwinDatabase.h"
#import <unistd.h>
#import <objc/runtime.h>
#if SqfliteDarwinDB_SQLITE_STANDALONE
#import <sqlite3/sqlite3.h>
#else
#import <sqlite3.h>
#endif
// MARK: - SqfliteDarwinDatabase Private Extension
NS_ASSUME_NONNULL_BEGIN
@interface SqfliteDarwinDatabase () {
void* _db;
BOOL _isExecutingStatement;
NSTimeInterval _startBusyRetryTime;
NSMutableSet *_openResultSets;
NSMutableSet *_openFunctions;
NSDateFormatter *_dateFormat;
}
- (SqfliteDarwinResultSet * _Nullable)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray * _Nullable)arrayArgs orDictionary:(NSDictionary * _Nullable)dictionaryArgs orVAList:(va_list)args shouldBind:(BOOL)shouldBind;
- (BOOL)executeUpdate:(NSString *)sql error:(NSError * _Nullable __autoreleasing *)outErr withArgumentsInArray:(NSArray * _Nullable)arrayArgs orDictionary:(NSDictionary * _Nullable)dictionaryArgs orVAList:(va_list)args;
@end
// MARK: - SqfliteDarwinResultSet Private Extension
@interface SqfliteDarwinResultSet ()
- (int)internalStepWithError:(NSError * _Nullable __autoreleasing *)outErr;
+ (instancetype)resultSetWithStatement:(SqfliteDarwinStatement *)statement usingParentDatabase:(SqfliteDarwinDatabase*)aDB shouldAutoClose:(BOOL)shouldAutoClose;
@end
NS_ASSUME_NONNULL_END
// MARK: - SqfliteDarwinDatabase
@implementation SqfliteDarwinDatabase
// Because these two properties have all of their accessor methods implemented,
// we have to synthesize them to get the corresponding ivars. The rest of the
// properties have their ivars synthesized automatically for us.
@synthesize shouldCacheStatements = _shouldCacheStatements;
@synthesize maxBusyRetryTimeInterval = _maxBusyRetryTimeInterval;
#pragma mark SqfliteDarwinDatabase instantiation and deallocation
+ (instancetype)databaseWithPath:(NSString *)aPath {
return SqfliteDarwinDBReturnAutoreleased([[self alloc] initWithPath:aPath]);
}
+ (instancetype)databaseWithURL:(NSURL *)url {
return SqfliteDarwinDBReturnAutoreleased([[self alloc] initWithURL:url]);
}
- (instancetype)init {
return [self initWithPath:nil];
}
- (instancetype)initWithURL:(NSURL *)url {
return [self initWithPath:url.path];
}
- (instancetype)initWithPath:(NSString *)path {
assert(sqlite3_threadsafe()); // whoa there big boy- gotta make sure sqlite it happy with what we're going to do.
self = [super init];
if (self) {
_databasePath = [path copy];
_openResultSets = [[NSMutableSet alloc] init];
_db = nil;
_logsErrors = YES;
_crashOnErrors = NO;
_maxBusyRetryTimeInterval = 2;
_isOpen = NO;
}
return self;
}
#if ! __has_feature(objc_arc)
- (void)finalize {
[self close];
[super finalize];
}
#endif
- (void)dealloc {
[self close];
SqfliteDarwinDBRelease(_openResultSets);
SqfliteDarwinDBRelease(_cachedStatements);
SqfliteDarwinDBRelease(_dateFormat);
SqfliteDarwinDBRelease(_databasePath);
SqfliteDarwinDBRelease(_openFunctions);
#if ! __has_feature(objc_arc)
[super dealloc];
#endif
}
- (NSURL *)databaseURL {
return _databasePath ? [NSURL fileURLWithPath:_databasePath] : nil;
}
+ (NSString*)SqfliteDarwinDBUserVersion {
return @"2.7.8";
}
+ (SInt32)SqfliteDarwinDBVersion {
// we go through these hoops so that we only have to change the version number in a single spot.
static dispatch_once_t once;
static SInt32 SqfliteDarwinDBVersionVal = 0;
dispatch_once(&once, ^{
NSString *prodVersion = [self SqfliteDarwinDBUserVersion];
while ([[prodVersion componentsSeparatedByString:@"."] count] < 3) {
prodVersion = [prodVersion stringByAppendingString:@".0"];
}
NSArray *components = [prodVersion componentsSeparatedByString:@"."];
for (NSUInteger i = 0; i < 3; i++) {
SInt32 component = [components[i] intValue];
if (component > 15) {
NSLog(@"SqfliteDarwinDBVersion is invalid: Please use SqfliteDarwinDBUserVersion instead.");
component = 15;
}
SqfliteDarwinDBVersionVal = SqfliteDarwinDBVersionVal << 4 | component;
}
});
return SqfliteDarwinDBVersionVal;
}
#pragma mark SQLite information
+ (NSString*)sqliteLibVersion {
return [NSString stringWithFormat:@"%s", sqlite3_libversion()];
}
+ (BOOL)isSQLiteThreadSafe {
// make sure to read the sqlite headers on this guy!
return sqlite3_threadsafe() != 0;
}
- (void*)sqliteHandle {
return _db;
}
- (const char*)sqlitePath {
if (!_databasePath) {
return ":memory:";
}
if ([_databasePath length] == 0) {
return ""; // this creates a temporary database (it's an sqlite thing).
}
return [_databasePath fileSystemRepresentation];
}
- (int)limitFor:(int)type value:(int)newLimit {
return sqlite3_limit(_db, type, newLimit);
}
#pragma mark Open and close database
- (BOOL)open {
if (_isOpen) {
return YES;
}
// if we previously tried to open and it failed, make sure to close it before we try again
if (_db) {
[self close];
}
// now open database
int err = sqlite3_open([self sqlitePath], (sqlite3**)&_db );
if(err != SQLITE_OK) {
NSLog(@"error opening!: %d", err);
return NO;
}
if (_maxBusyRetryTimeInterval > 0.0) {
// set the handler
[self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval];
}
_isOpen = YES;
return YES;
}
- (BOOL)openWithFlags:(int)flags {
return [self openWithFlags:flags vfs:nil];
}
- (BOOL)openWithFlags:(int)flags vfs:(NSString *)vfsName {
#if SQLITE_VERSION_NUMBER >= 3005000
if (_isOpen) {
return YES;
}
// if we previously tried to open and it failed, make sure to close it before we try again
if (_db) {
[self close];
}
// now open database
int err = sqlite3_open_v2([self sqlitePath], (sqlite3**)&_db, flags, [vfsName UTF8String]);
if(err != SQLITE_OK) {
NSLog(@"error opening!: %d", err);
return NO;
}
if (_maxBusyRetryTimeInterval > 0.0) {
// set the handler
[self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval];
}
_isOpen = YES;
return YES;
#else
NSLog(@"openWithFlags requires SQLite 3.5");
return NO;
#endif
}
- (BOOL)close {
[self clearCachedStatements];
[self closeOpenResultSets];
if (!_db) {
return YES;
}
int rc;
BOOL retry;
BOOL triedFinalizingOpenStatements = NO;
do {
retry = NO;
rc = sqlite3_close(_db);
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
if (!triedFinalizingOpenStatements) {
triedFinalizingOpenStatements = YES;
sqlite3_stmt *pStmt;
while ((pStmt = sqlite3_next_stmt(_db, nil)) !=0) {
NSLog(@"Closing leaked statement");
sqlite3_finalize(pStmt);
pStmt = 0x00;
retry = YES;
}
}
}
else if (SQLITE_OK != rc) {
NSLog(@"error closing!: %d", rc);
}
}
while (retry);
_db = nil;
_isOpen = false;
return YES;
}
#pragma mark Busy handler routines
// NOTE: appledoc seems to choke on this function for some reason;
// so when generating documentation, you might want to ignore the
// .m files so that it only documents the public interfaces outlined
// in the .h files.
//
// This is a known appledoc bug that it has problems with C functions
// within a class implementation, but for some reason, only this
// C function causes problems; the rest don't. Anyway, ignoring the .m
// files with appledoc will prevent this problem from occurring.
static int SqfliteDarwinDBDatabaseBusyHandler(void *f, int count) {
SqfliteDarwinDatabase *self = (__bridge SqfliteDarwinDatabase*)f;
if (count == 0) {
self->_startBusyRetryTime = [NSDate timeIntervalSinceReferenceDate];
return 1;
}
NSTimeInterval delta = [NSDate timeIntervalSinceReferenceDate] - (self->_startBusyRetryTime);
if (delta < [self maxBusyRetryTimeInterval]) {
int requestedSleepInMillseconds = (int) arc4random_uniform(50) + 50;
int actualSleepInMilliseconds = sqlite3_sleep(requestedSleepInMillseconds);
if (actualSleepInMilliseconds != requestedSleepInMillseconds) {
NSLog(@"WARNING: Requested sleep of %i milliseconds, but SQLite returned %i. Maybe SQLite wasn't built with HAVE_USLEEP=1?", requestedSleepInMillseconds, actualSleepInMilliseconds);
}
return 1;
}
return 0;
}
- (void)setMaxBusyRetryTimeInterval:(NSTimeInterval)timeout {
_maxBusyRetryTimeInterval = timeout;
if (!_db) {
return;
}
if (timeout > 0) {
sqlite3_busy_handler(_db, &SqfliteDarwinDBDatabaseBusyHandler, (__bridge void *)(self));
}
else {
// turn it off otherwise
sqlite3_busy_handler(_db, nil, nil);
}
}
- (NSTimeInterval)maxBusyRetryTimeInterval {
return _maxBusyRetryTimeInterval;
}
// we no longer make busyRetryTimeout public
// but for folks who don't bother noticing that the interface to SqfliteDarwinDatabase changed,
// we'll still implement the method so they don't get suprise crashes
- (int)busyRetryTimeout {
NSLog(@"%s:%d", __FUNCTION__, __LINE__);
NSLog(@"SqfliteDarwinDB: busyRetryTimeout no longer works, please use maxBusyRetryTimeInterval");
return -1;
}
- (void)setBusyRetryTimeout:(int)i {
#pragma unused(i)
NSLog(@"%s:%d", __FUNCTION__, __LINE__);
NSLog(@"SqfliteDarwinDB: setBusyRetryTimeout does nothing, please use setMaxBusyRetryTimeInterval:");
}
#pragma mark Result set functions
- (BOOL)hasOpenResultSets {
return [_openResultSets count] > 0;
}
- (void)closeOpenResultSets {
//Copy the set so we don't get mutation errors
NSSet *openSetCopy = SqfliteDarwinDBReturnAutoreleased([_openResultSets copy]);
for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) {
SqfliteDarwinResultSet *rs = (SqfliteDarwinResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
[rs setParentDB:nil];
[rs close];
[_openResultSets removeObject:rsInWrappedInATastyValueMeal];
}
}
- (void)resultSetDidClose:(SqfliteDarwinResultSet *)resultSet {
NSValue *setValue = [NSValue valueWithNonretainedObject:resultSet];
[_openResultSets removeObject:setValue];
}
#pragma mark Cached statements
- (void)clearCachedStatements {
for (NSMutableSet *statements in [_cachedStatements objectEnumerator]) {
for (SqfliteDarwinStatement *statement in [statements allObjects]) {
[statement close];
}
}
[_cachedStatements removeAllObjects];
}
- (SqfliteDarwinStatement*)cachedStatementForQuery:(NSString*)query {
NSMutableSet* statements = [_cachedStatements objectForKey:query];
return [[statements objectsPassingTest:^BOOL(SqfliteDarwinStatement* statement, BOOL *stop) {
*stop = ![statement inUse];
return *stop;
}] anyObject];
}
- (void)setCachedStatement:(SqfliteDarwinStatement*)statement forQuery:(NSString*)query {
NSParameterAssert(query);
if (!query) {
NSLog(@"API misuse, -[SqfliteDarwinDatabase setCachedStatement:forQuery:] query must not be nil");
return;
}
query = [query copy]; // in case we got handed in a mutable string...
[statement setQuery:query];
NSMutableSet* statements = [_cachedStatements objectForKey:query];
if (!statements) {
statements = [NSMutableSet set];
}
[statements addObject:statement];
[_cachedStatements setObject:statements forKey:query];
SqfliteDarwinDBRelease(query);
}
#pragma mark Key routines
- (BOOL)rekey:(NSString*)key {
NSData *keyData = [NSData dataWithBytes:(void *)[key UTF8String] length:(NSUInteger)strlen([key UTF8String])];
return [self rekeyWithData:keyData];
}
- (BOOL)rekeyWithData:(NSData *)keyData {
#ifdef SQLITE_HAS_CODEC
if (!keyData) {
return NO;
}
int rc = sqlite3_rekey(_db, [keyData bytes], (int)[keyData length]);
if (rc != SQLITE_OK) {
NSLog(@"error on rekey: %d", rc);
NSLog(@"%@", [self lastErrorMessage]);
}
return (rc == SQLITE_OK);
#else
#pragma unused(keyData)
return NO;
#endif
}
- (BOOL)setKey:(NSString*)key {
NSData *keyData = [NSData dataWithBytes:[key UTF8String] length:(NSUInteger)strlen([key UTF8String])];
return [self setKeyWithData:keyData];
}
- (BOOL)setKeyWithData:(NSData *)keyData {
#ifdef SQLITE_HAS_CODEC
if (!keyData) {
return NO;
}
int rc = sqlite3_key(_db, [keyData bytes], (int)[keyData length]);
return (rc == SQLITE_OK);
#else
#pragma unused(keyData)
return NO;
#endif
}
#pragma mark Date routines
+ (NSDateFormatter *)storeableDateFormat:(NSString *)format {
NSDateFormatter *result = SqfliteDarwinDBReturnAutoreleased([[NSDateFormatter alloc] init]);
result.dateFormat = format;
result.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
result.locale = SqfliteDarwinDBReturnAutoreleased([[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]);
return result;
}
- (BOOL)hasDateFormatter {
return _dateFormat != nil;
}
- (void)setDateFormat:(NSDateFormatter *)format {
SqfliteDarwinDBAutorelease(_dateFormat);
_dateFormat = SqfliteDarwinDBReturnRetained(format);
}
- (NSDate *)dateFromString:(NSString *)s {
return [_dateFormat dateFromString:s];
}
- (NSString *)stringFromDate:(NSDate *)date {
return [_dateFormat stringFromDate:date];
}
#pragma mark State of database
- (BOOL)goodConnection {
if (!_isOpen) {
return NO;
}
#ifdef SQLCIPHER_CRYPTO
// Starting with Xcode8 / iOS 10 we check to make sure we really are linked with
// SQLCipher because there is no longer a linker error if we accidently link
// with unencrypted sqlite library.
//
// https://discuss.zetetic.net/t/important-advisory-sqlcipher-with-xcode-8-and-new-sdks/1688
SqfliteDarwinResultSet *rs = [self executeQuery:@"PRAGMA cipher_version"];
if ([rs next]) {
NSLog(@"SQLCipher version: %@", rs.resultDictionary[@"cipher_version"]);
[rs close];
return YES;
}
#else
SqfliteDarwinResultSet *rs = [self executeQuery:@"select name from sqlite_master where type='table'"];
if (rs) {
[rs close];
return YES;
}
#endif
return NO;
}
- (void)warnInUse {
NSLog(@"The SqfliteDarwinDatabase %@ is currently in use.", self);
#ifndef NS_BLOCK_ASSERTIONS
if (_crashOnErrors) {
NSAssert(false, @"The SqfliteDarwinDatabase %@ is currently in use.", self);
abort();
}
#endif
}
- (BOOL)databaseExists {
if (!_isOpen) {
NSLog(@"The SqfliteDarwinDatabase %@ is not open.", self);
#ifndef NS_BLOCK_ASSERTIONS
if (_crashOnErrors) {
NSAssert(false, @"The SqfliteDarwinDatabase %@ is not open.", self);
abort();
}
#endif
return NO;
}
return YES;
}
#pragma mark Error routines
- (NSString *)lastErrorMessage {
return [NSString stringWithUTF8String:sqlite3_errmsg(_db)];
}
- (BOOL)hadError {
int lastErrCode = [self lastErrorCode];
return (lastErrCode > SQLITE_OK && lastErrCode < SQLITE_ROW);
}
- (int)lastErrorCode {
return sqlite3_errcode(_db);
}
- (int)lastExtendedErrorCode {
return sqlite3_extended_errcode(_db);
}
- (NSError*)errorWithMessage:(NSString *)message {
NSDictionary* errorMessage = [NSDictionary dictionaryWithObject:message forKey:NSLocalizedDescriptionKey];
return [NSError errorWithDomain:@"SqfliteDarwinDatabase" code:sqlite3_errcode(_db) userInfo:errorMessage];
}
- (NSError*)lastError {
return [self errorWithMessage:[self lastErrorMessage]];
}
#pragma mark Update information routines
- (sqlite_int64)lastInsertRowId {
if (_isExecutingStatement) {
[self warnInUse];
return NO;
}
_isExecutingStatement = YES;
sqlite_int64 ret = sqlite3_last_insert_rowid(_db);
_isExecutingStatement = NO;
return ret;
}
- (int)changes {
if (_isExecutingStatement) {
[self warnInUse];
return 0;
}
_isExecutingStatement = YES;
int ret = sqlite3_changes(_db);
_isExecutingStatement = NO;
return ret;
}
#pragma mark SQL manipulation
- (int)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt {
if ((!obj) || ((NSNull *)obj == [NSNull null])) {
return sqlite3_bind_null(pStmt, idx);
}
// FIXME - someday check the return codes on these binds.
else if ([obj isKindOfClass:[NSData class]]) {
const void *bytes = [obj bytes];
if (!bytes) {
// it's an empty NSData object, aka [NSData data].
// Don't pass a NULL pointer, or sqlite will bind a SQL null instead of a blob.
bytes = "";
}
return sqlite3_bind_blob(pStmt, idx, bytes, (int)[obj length], SQLITE_TRANSIENT);
}
else if ([obj isKindOfClass:[NSDate class]]) {
if (self.hasDateFormatter)
return sqlite3_bind_text(pStmt, idx, [[self stringFromDate:obj] UTF8String], -1, SQLITE_TRANSIENT);
else
return sqlite3_bind_double(pStmt, idx, [obj timeIntervalSince1970]);
}
else if ([obj isKindOfClass:[NSNumber class]]) {
if (strcmp([obj objCType], @encode(char)) == 0) {
return sqlite3_bind_int(pStmt, idx, [obj charValue]);
}
else if (strcmp([obj objCType], @encode(unsigned char)) == 0) {
return sqlite3_bind_int(pStmt, idx, [obj unsignedCharValue]);
}
else if (strcmp([obj objCType], @encode(short)) == 0) {
return sqlite3_bind_int(pStmt, idx, [obj shortValue]);
}
else if (strcmp([obj objCType], @encode(unsigned short)) == 0) {
return sqlite3_bind_int(pStmt, idx, [obj unsignedShortValue]);
}
else if (strcmp([obj objCType], @encode(int)) == 0) {
return sqlite3_bind_int(pStmt, idx, [obj intValue]);
}
else if (strcmp([obj objCType], @encode(unsigned int)) == 0) {
return sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedIntValue]);
}
else if (strcmp([obj objCType], @encode(long)) == 0) {
return sqlite3_bind_int64(pStmt, idx, [obj longValue]);
}
else if (strcmp([obj objCType], @encode(unsigned long)) == 0) {
return sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongValue]);
}
else if (strcmp([obj objCType], @encode(long long)) == 0) {
return sqlite3_bind_int64(pStmt, idx, [obj longLongValue]);
}
else if (strcmp([obj objCType], @encode(unsigned long long)) == 0) {
return sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongLongValue]);
}
else if (strcmp([obj objCType], @encode(float)) == 0) {
return sqlite3_bind_double(pStmt, idx, [obj floatValue]);
}
else if (strcmp([obj objCType], @encode(double)) == 0) {
return sqlite3_bind_double(pStmt, idx, [obj doubleValue]);
}
else if (strcmp([obj objCType], @encode(BOOL)) == 0) {
return sqlite3_bind_int(pStmt, idx, ([obj boolValue] ? 1 : 0));
}
else {
return sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_TRANSIENT);
}
}
return sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_TRANSIENT);
}
- (void)extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments {
NSUInteger length = [sql length];
unichar last = '\0';
for (NSUInteger i = 0; i < length; ++i) {
id arg = nil;
unichar current = [sql characterAtIndex:i];
unichar add = current;
if (last == '%') {
switch (current) {
case '@':
arg = va_arg(args, id);
break;
case 'c':
// warning: second argument to 'va_arg' is of promotable type 'char'; this va_arg has undefined behavior because arguments will be promoted to 'int'
arg = [NSString stringWithFormat:@"%c", va_arg(args, int)];
break;
case 's':
arg = [NSString stringWithUTF8String:va_arg(args, char*)];
break;
case 'd':
case 'D':
case 'i':
arg = [NSNumber numberWithInt:va_arg(args, int)];
break;
case 'u':
case 'U':
arg = [NSNumber numberWithUnsignedInt:va_arg(args, unsigned int)];
break;
case 'h':
i++;
if (i < length && [sql characterAtIndex:i] == 'i') {
// warning: second argument to 'va_arg' is of promotable type 'short'; this va_arg has undefined behavior because arguments will be promoted to 'int'
arg = [NSNumber numberWithShort:(short)(va_arg(args, int))];
}
else if (i < length && [sql characterAtIndex:i] == 'u') {
// warning: second argument to 'va_arg' is of promotable type 'unsigned short'; this va_arg has undefined behavior because arguments will be promoted to 'int'
arg = [NSNumber numberWithUnsignedShort:(unsigned short)(va_arg(args, uint))];
}
else {
i--;
}
break;
case 'q':
i++;
if (i < length && [sql characterAtIndex:i] == 'i') {
arg = [NSNumber numberWithLongLong:va_arg(args, long long)];
}
else if (i < length && [sql characterAtIndex:i] == 'u') {
arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)];
}
else {
i--;
}
break;
case 'f':
arg = [NSNumber numberWithDouble:va_arg(args, double)];
break;
case 'g':
// warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double'
arg = [NSNumber numberWithFloat:(float)(va_arg(args, double))];
break;
case 'l':
i++;
if (i < length) {
unichar next = [sql characterAtIndex:i];
if (next == 'l') {
i++;
if (i < length && [sql characterAtIndex:i] == 'd') {
//%lld
arg = [NSNumber numberWithLongLong:va_arg(args, long long)];
}
else if (i < length && [sql characterAtIndex:i] == 'u') {
//%llu
arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)];
}
else {
i--;
}
}
else if (next == 'd') {
//%ld
arg = [NSNumber numberWithLong:va_arg(args, long)];
}
else if (next == 'u') {
//%lu
arg = [NSNumber numberWithUnsignedLong:va_arg(args, unsigned long)];
}
else {
i--;
}
}
else {
i--;
}
break;
default:
// something else that we can't interpret. just pass it on through like normal
break;
}
}
else if (current == '%') {
// percent sign; skip this character
add = '\0';
}
if (arg != nil) {
[cleanedSQL appendString:@"?"];
[arguments addObject:arg];
}
else if (add == (unichar)'@' && last == (unichar) '%') {
[cleanedSQL appendFormat:@"NULL"];
}
else if (add != '\0') {
[cleanedSQL appendFormat:@"%C", add];
}
last = current;
}
}
#pragma mark Execute queries
- (SqfliteDarwinResultSet *)executeQuery:(NSString *)sql withParameterDictionary:(NSDictionary *)arguments {
return [self executeQuery:sql withArgumentsInArray:nil orDictionary:arguments orVAList:nil shouldBind:true];
}
- (SqfliteDarwinResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args shouldBind:(BOOL)shouldBind {
if (![self databaseExists]) {
return 0x00;
}
if (_isExecutingStatement) {
[self warnInUse];
return 0x00;
}
_isExecutingStatement = YES;
int rc = 0x00;
sqlite3_stmt *pStmt = 0x00;
SqfliteDarwinStatement *statement = 0x00;
SqfliteDarwinResultSet *rs = 0x00;
if (_traceExecution && sql) {
NSLog(@"%@ executeQuery: %@", self, sql);
}
if (_shouldCacheStatements) {
statement = [self cachedStatementForQuery:sql];
pStmt = statement ? [statement statement] : 0x00;
[statement reset];
}
if (!pStmt) {
rc = sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0);
if (SQLITE_OK != rc) {
if (_logsErrors) {
NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
NSLog(@"DB Query: %@", sql);
NSLog(@"DB Path: %@", _databasePath);
}
if (_crashOnErrors) {
NSAssert(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
abort();
}
sqlite3_finalize(pStmt);
pStmt = 0x00;
_isExecutingStatement = NO;
return nil;
}
}
if (shouldBind) {
BOOL success = [self bindStatement:pStmt WithArgumentsInArray:arrayArgs orDictionary:dictionaryArgs orVAList:args];
if (!success) {
return nil;
}
}
SqfliteDarwinDBRetain(statement); // to balance the release below
if (!statement) {
statement = [[SqfliteDarwinStatement alloc] init];
[statement setStatement:pStmt];
if (_shouldCacheStatements && sql) {
[self setCachedStatement:statement forQuery:sql];
}
}
// the statement gets closed in rs's dealloc or [rs close];
// we should only autoclose if we're binding automatically when the statement is prepared
rs = [SqfliteDarwinResultSet resultSetWithStatement:statement usingParentDatabase:self shouldAutoClose:shouldBind];
[rs setQuery:sql];
NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
[_openResultSets addObject:openResultSet];
[statement setUseCount:[statement useCount] + 1];
SqfliteDarwinDBRelease(statement);
_isExecutingStatement = NO;
return rs;
}
- (BOOL)bindStatement:(sqlite3_stmt *)pStmt WithArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args {
id obj;
int idx = 0;
int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!)
// If dictionaryArgs is passed in, that means we are using sqlite's named parameter support
if (dictionaryArgs) {
for (NSString *dictionaryKey in [dictionaryArgs allKeys]) {
// Prefix the key with a colon.
NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey];
if (_traceExecution) {
NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]);
}
// Get the index for the parameter name.
int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]);
SqfliteDarwinDBRelease(parameterName);
if (namedIdx > 0) {
// Standard binding from here.
int rc = [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt];
if (rc != SQLITE_OK) {
NSLog(@"Error: unable to bind (%d, %s", rc, sqlite3_errmsg(_db));
sqlite3_finalize(pStmt);
pStmt = 0x00;
_isExecutingStatement = NO;
return false;
}
// increment the binding count, so our check below works out
idx++;
}
else {
NSLog(@"Could not find index for %@", dictionaryKey);
}
}
}
else {
while (idx < queryCount) {
if (arrayArgs && idx < (int)[arrayArgs count]) {
obj = [arrayArgs objectAtIndex:(NSUInteger)idx];
}
else if (args) {
obj = va_arg(args, id);
}
else {
//We ran out of arguments
break;
}
if (_traceExecution) {
if ([obj isKindOfClass:[NSData class]]) {
NSLog(@"data: %ld bytes", (unsigned long)[(NSData*)obj length]);
}
else {
NSLog(@"obj: %@", obj);
}
}
idx++;
int rc = [self bindObject:obj toColumn:idx inStatement:pStmt];
if (rc != SQLITE_OK) {
NSLog(@"Error: unable to bind (%d, %s", rc, sqlite3_errmsg(_db));
sqlite3_finalize(pStmt);
pStmt = 0x00;
_isExecutingStatement = NO;
return false;
}
}
}
if (idx != queryCount) {
NSLog(@"Error: the bind count is not correct for the # of variables (executeQuery)");
sqlite3_finalize(pStmt);
pStmt = 0x00;
_isExecutingStatement = NO;
return false;
}
return true;
}
- (SqfliteDarwinResultSet *)executeQuery:(NSString*)sql, ... {
va_list args;
va_start(args, sql);
id result = [self executeQuery:sql withArgumentsInArray:nil orDictionary:nil orVAList:args shouldBind:true];
va_end(args);
return result;
}
- (SqfliteDarwinResultSet *)executeQueryWithFormat:(NSString*)format, ... {
va_list args;
va_start(args, format);
NSMutableString *sql = [NSMutableString stringWithCapacity:[format length]];
NSMutableArray *arguments = [NSMutableArray array];
[self extractSQL:format argumentsList:args intoString:sql arguments:arguments];
va_end(args);
return [self executeQuery:sql withArgumentsInArray:arguments];
}
- (SqfliteDarwinResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments {
return [self executeQuery:sql withArgumentsInArray:arguments orDictionary:nil orVAList:nil shouldBind:true];
}
- (SqfliteDarwinResultSet *)executeQuery:(NSString *)sql values:(NSArray *)values error:(NSError * __autoreleasing *)error {
SqfliteDarwinResultSet *rs = [self executeQuery:sql withArgumentsInArray:values orDictionary:nil orVAList:nil shouldBind:true];
if (!rs && error) {
*error = [self lastError];
}
return rs;
}
- (SqfliteDarwinResultSet *)executeQuery:(NSString*)sql withVAList:(va_list)args {
return [self executeQuery:sql withArgumentsInArray:nil orDictionary:nil orVAList:args shouldBind:true];
}
#pragma mark Execute updates
- (BOOL)executeUpdate:(NSString*)sql error:(NSError * _Nullable __autoreleasing *)outErr withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args {
SqfliteDarwinResultSet *rs = [self executeQuery:sql withArgumentsInArray:arrayArgs orDictionary:dictionaryArgs orVAList:args shouldBind:true];
if (!rs) {
if (outErr) {
*outErr = [self lastError];
}
return false;
}
return [rs internalStepWithError:outErr] == SQLITE_DONE;
}
- (BOOL)executeUpdate:(NSString*)sql, ... {
va_list args;
va_start(args, sql);
BOOL result = [self executeUpdate:sql error:nil withArgumentsInArray:nil orDictionary:nil orVAList:args];
va_end(args);
return result;
}
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments {
return [self executeUpdate:sql error:nil withArgumentsInArray:arguments orDictionary:nil orVAList:nil];
}
- (BOOL)executeUpdate:(NSString*)sql values:(NSArray *)values error:(NSError * __autoreleasing *)error {
return [self executeUpdate:sql error:error withArgumentsInArray:values orDictionary:nil orVAList:nil];
}
- (BOOL)executeUpdate:(NSString*)sql withParameterDictionary:(NSDictionary *)arguments {
return [self executeUpdate:sql error:nil withArgumentsInArray:nil orDictionary:arguments orVAList:nil];
}
- (BOOL)executeUpdate:(NSString*)sql withVAList:(va_list)args {
return [self executeUpdate:sql error:nil withArgumentsInArray:nil orDictionary:nil orVAList:args];
}
- (BOOL)executeUpdateWithFormat:(NSString*)format, ... {
va_list args;
va_start(args, format);
NSMutableString *sql = [NSMutableString stringWithCapacity:[format length]];
NSMutableArray *arguments = [NSMutableArray array];
[self extractSQL:format argumentsList:args intoString:sql arguments:arguments];
va_end(args);
return [self executeUpdate:sql withArgumentsInArray:arguments];
}
int SqfliteDarwinDBExecuteBulkSQLCallback(void *theBlockAsVoid, int columns, char **values, char **names); // shhh clang.
int SqfliteDarwinDBExecuteBulkSQLCallback(void *theBlockAsVoid, int columns, char **values, char **names) {
if (!theBlockAsVoid) {
return SQLITE_OK;
}
int (^execCallbackBlock)(NSDictionary *resultsDictionary) = (__bridge int (^)(NSDictionary *__strong))(theBlockAsVoid);
NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:(NSUInteger)columns];
for (NSInteger i = 0; i < columns; i++) {
NSString *key = [NSString stringWithUTF8String:names[i]];
id value = values[i] ? [NSString stringWithUTF8String:values[i]] : [NSNull null];
value = value ? value : [NSNull null];
[dictionary setObject:value forKey:key];
}
return execCallbackBlock(dictionary);
}
- (BOOL)executeStatements:(NSString *)sql {
return [self executeStatements:sql withResultBlock:nil];
}
- (BOOL)executeStatements:(NSString *)sql withResultBlock:(__attribute__((noescape)) SqfliteDarwinDBExecuteStatementsCallbackBlock)block {
int rc;
char *errmsg = nil;
rc = sqlite3_exec([self sqliteHandle], [sql UTF8String], block ? SqfliteDarwinDBExecuteBulkSQLCallback : nil, (__bridge void *)(block), &errmsg);
if (errmsg && [self logsErrors]) {
NSLog(@"Error inserting batch: %s", errmsg);
}
if (errmsg) {
sqlite3_free(errmsg);
}
return (rc == SQLITE_OK);
}
- (BOOL)executeUpdate:(NSString*)sql withErrorAndBindings:(NSError * _Nullable __autoreleasing *)outErr, ... {
va_list args;
va_start(args, outErr);
BOOL result = [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:args];
va_end(args);
return result;
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (BOOL)update:(NSString*)sql withErrorAndBindings:(NSError * _Nullable __autoreleasing *)outErr, ... {
va_list args;
va_start(args, outErr);
BOOL result = [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:args];
va_end(args);
return result;
}
#pragma clang diagnostic pop
#pragma mark Prepare
- (SqfliteDarwinResultSet *)prepare:(NSString *)sql {
return [self executeQuery:sql withArgumentsInArray:nil orDictionary:nil orVAList:nil shouldBind:false];
}
#pragma mark Transactions
- (BOOL)rollback {
BOOL b = [self executeUpdate:@"rollback transaction"];
if (b) {
_isInTransaction = NO;
}
return b;
}
- (BOOL)commit {
BOOL b = [self executeUpdate:@"commit transaction"];
if (b) {
_isInTransaction = NO;
}
return b;
}
- (BOOL)beginTransaction {
BOOL b = [self executeUpdate:@"begin exclusive transaction"];
if (b) {
_isInTransaction = YES;
}
return b;
}
- (BOOL)beginDeferredTransaction {
BOOL b = [self executeUpdate:@"begin deferred transaction"];
if (b) {
_isInTransaction = YES;
}
return b;
}
- (BOOL)beginImmediateTransaction {
BOOL b = [self executeUpdate:@"begin immediate transaction"];
if (b) {
_isInTransaction = YES;
}
return b;
}
- (BOOL)beginExclusiveTransaction {
BOOL b = [self executeUpdate:@"begin exclusive transaction"];
if (b) {
_isInTransaction = YES;
}
return b;
}
- (BOOL)inTransaction {
return _isInTransaction;
}
- (BOOL)interrupt
{
if (_db) {
sqlite3_interrupt([self sqliteHandle]);
return YES;
}
return NO;
}
static NSString *SqfliteDarwinDBEscapeSavePointName(NSString *savepointName) {
return [savepointName stringByReplacingOccurrencesOfString:@"'" withString:@"''"];
}
- (BOOL)startSavePointWithName:(NSString*)name error:(NSError * _Nullable __autoreleasing *)outErr {
#if SQLITE_VERSION_NUMBER >= 3007000
NSParameterAssert(name);
NSString *sql = [NSString stringWithFormat:@"savepoint '%@';", SqfliteDarwinDBEscapeSavePointName(name)];
return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
#else
NSString *errorMessage = NSLocalizedStringFromTable(@"Save point functions require SQLite 3.7", @"SqfliteDarwinDB", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return NO;
#endif
}
- (BOOL)releaseSavePointWithName:(NSString*)name error:(NSError * _Nullable __autoreleasing *)outErr {
#if SQLITE_VERSION_NUMBER >= 3007000
NSParameterAssert(name);
NSString *sql = [NSString stringWithFormat:@"release savepoint '%@';", SqfliteDarwinDBEscapeSavePointName(name)];
return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
#else
NSString *errorMessage = NSLocalizedStringFromTable(@"Save point functions require SQLite 3.7", @"SqfliteDarwinDB", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return NO;
#endif
}
- (BOOL)rollbackToSavePointWithName:(NSString*)name error:(NSError * _Nullable __autoreleasing *)outErr {
#if SQLITE_VERSION_NUMBER >= 3007000
NSParameterAssert(name);
NSString *sql = [NSString stringWithFormat:@"rollback transaction to savepoint '%@';", SqfliteDarwinDBEscapeSavePointName(name)];
return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
#else
NSString *errorMessage = NSLocalizedStringFromTable(@"Save point functions require SQLite 3.7", @"SqfliteDarwinDB", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return NO;
#endif
}
- (NSError*)inSavePoint:(__attribute__((noescape)) void (^)(BOOL *rollback))block {
#if SQLITE_VERSION_NUMBER >= 3007000
static unsigned long savePointIdx = 0;
NSString *name = [NSString stringWithFormat:@"dbSavePoint%ld", savePointIdx++];
BOOL shouldRollback = NO;
NSError *err = 0x00;
if (![self startSavePointWithName:name error:&err]) {
return err;
}
if (block) {
block(&shouldRollback);
}
if (shouldRollback) {
// We need to rollback and release this savepoint to remove it
[self rollbackToSavePointWithName:name error:&err];
}
[self releaseSavePointWithName:name error:&err];
return err;
#else
NSString *errorMessage = NSLocalizedStringFromTable(@"Save point functions require SQLite 3.7", @"SqfliteDarwinDB", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return [NSError errorWithDomain:@"SqfliteDarwinDatabase" code:0 userInfo:@{NSLocalizedDescriptionKey : errorMessage}];
#endif
}
- (BOOL)checkpoint:(SqfliteDarwinDBCheckpointMode)checkpointMode error:(NSError * __autoreleasing *)error {
return [self checkpoint:checkpointMode name:nil logFrameCount:NULL checkpointCount:NULL error:error];
}
- (BOOL)checkpoint:(SqfliteDarwinDBCheckpointMode)checkpointMode name:(NSString *)name error:(NSError * __autoreleasing *)error {
return [self checkpoint:checkpointMode name:name logFrameCount:NULL checkpointCount:NULL error:error];
}
- (BOOL)checkpoint:(SqfliteDarwinDBCheckpointMode)checkpointMode name:(NSString *)name logFrameCount:(int *)logFrameCount checkpointCount:(int *)checkpointCount error:(NSError * __autoreleasing *)error
{
const char* dbName = [name UTF8String];
#if SQLITE_VERSION_NUMBER >= 3007006
int err = sqlite3_wal_checkpoint_v2(_db, dbName, checkpointMode, logFrameCount, checkpointCount);
#else
NSLog(@"sqlite3_wal_checkpoint_v2 unavailable before sqlite 3.7.6. Ignoring checkpoint mode: %d", mode);
int err = sqlite3_wal_checkpoint(_db, dbName);
#endif
if(err != SQLITE_OK) {
if (error) {
*error = [self lastError];
}
if (self.logsErrors) NSLog(@"%@", [self lastErrorMessage]);
if (self.crashOnErrors) {
NSAssert(false, @"%@", [self lastErrorMessage]);
abort();
}
return NO;
} else {
return YES;
}
}
#pragma mark Cache statements
- (BOOL)shouldCacheStatements {
return _shouldCacheStatements;
}
- (void)setShouldCacheStatements:(BOOL)value {
_shouldCacheStatements = value;
if (_shouldCacheStatements && !_cachedStatements) {
[self setCachedStatements:[NSMutableDictionary dictionary]];
}
if (!_shouldCacheStatements) {
[self setCachedStatements:nil];
}
}
#pragma mark Callback function
void SqfliteDarwinDBBlockSQLiteCallBackFunction(sqlite3_context *context, int argc, sqlite3_value **argv); // -Wmissing-prototypes
void SqfliteDarwinDBBlockSQLiteCallBackFunction(sqlite3_context *context, int argc, sqlite3_value **argv) {
#if ! __has_feature(objc_arc)
void (^block)(sqlite3_context *context, int argc, sqlite3_value **argv) = (id)sqlite3_user_data(context);
#else
void (^block)(sqlite3_context *context, int argc, sqlite3_value **argv) = (__bridge id)sqlite3_user_data(context);
#endif
if (block) {
@autoreleasepool {
block(context, argc, argv);
}
}
}
// deprecated because "arguments" parameter is not maximum argument count, but actual argument count.
- (void)makeFunctionNamed:(NSString *)name maximumArguments:(int)arguments withBlock:(void (^)(void *context, int argc, void **argv))block {
[self makeFunctionNamed:name arguments:arguments block:block];
}
- (void)makeFunctionNamed:(NSString *)name arguments:(int)arguments block:(void (^)(void *context, int argc, void **argv))block {
if (!_openFunctions) {
_openFunctions = [NSMutableSet new];
}
id b = SqfliteDarwinDBReturnAutoreleased([block copy]);
[_openFunctions addObject:b];
/* I tried adding custom functions to release the block when the connection is destroyed- but they seemed to never be called, so we use _openFunctions to store the values instead. */
#if ! __has_feature(objc_arc)
sqlite3_create_function([self sqliteHandle], [name UTF8String], arguments, SQLITE_UTF8, (void*)b, &SqfliteDarwinDBBlockSQLiteCallBackFunction, 0x00, 0x00);
#else
sqlite3_create_function([self sqliteHandle], [name UTF8String], arguments, SQLITE_UTF8, (__bridge void*)b, &SqfliteDarwinDBBlockSQLiteCallBackFunction, 0x00, 0x00);
#endif
}
- (SqliteValueType)valueType:(void *)value {
return sqlite3_value_type(value);
}
- (int)valueInt:(void *)value {
return sqlite3_value_int(value);
}
- (long long)valueLong:(void *)value {
return sqlite3_value_int64(value);
}
- (double)valueDouble:(void *)value {
return sqlite3_value_double(value);
}
- (NSData *)valueData:(void *)value {
const void *bytes = sqlite3_value_blob(value);
int length = sqlite3_value_bytes(value);
return bytes ? [NSData dataWithBytes:bytes length:(NSUInteger)length] : nil;
}
- (NSString *)valueString:(void *)value {
const char *cString = (const char *)sqlite3_value_text(value);
return cString ? [NSString stringWithUTF8String:cString] : nil;
}
- (void)resultNullInContext:(void *)context {
sqlite3_result_null(context);
}
- (void)resultInt:(int) value context:(void *)context {
sqlite3_result_int(context, value);
}
- (void)resultLong:(long long)value context:(void *)context {
sqlite3_result_int64(context, value);
}
- (void)resultDouble:(double)value context:(void *)context {
sqlite3_result_double(context, value);
}
- (void)resultData:(NSData *)data context:(void *)context {
sqlite3_result_blob(context, data.bytes, (int)data.length, SQLITE_TRANSIENT);
}
- (void)resultString:(NSString *)value context:(void *)context {
sqlite3_result_text(context, [value UTF8String], -1, SQLITE_TRANSIENT);
}
- (void)resultError:(NSString *)error context:(void *)context {
sqlite3_result_error(context, [error UTF8String], -1);
}
- (void)resultErrorCode:(int)errorCode context:(void *)context {
sqlite3_result_error_code(context, errorCode);
}
- (void)resultErrorNoMemoryInContext:(void *)context {
sqlite3_result_error_nomem(context);
}
- (void)resultErrorTooBigInContext:(void *)context {
sqlite3_result_error_toobig(context);
}
@end
// MARK: - SqfliteDarwinStatement
@implementation SqfliteDarwinStatement
#if ! __has_feature(objc_arc)
- (void)finalize {
[self close];
[super finalize];
}
#endif
- (void)dealloc {
[self close];
SqfliteDarwinDBRelease(_query);
#if ! __has_feature(objc_arc)
[super dealloc];
#endif
}
- (void)close {
if (_statement) {
sqlite3_finalize(_statement);
_statement = 0x00;
}
_inUse = NO;
}
- (void)reset {
if (_statement) {
sqlite3_reset(_statement);
}
_inUse = NO;
}
- (NSString*)description {
return [NSString stringWithFormat:@"%@ %ld hit(s) for query %@", [super description], _useCount, _query];
}
@end
//
// SqfliteDarwinDatabaseAdditions.h
// fmdb
//
// Created by August Mueller on 10/30/05.
// Copyright 2005 Flying Meat Inc.. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "SqfliteDarwinDatabase.h"
NS_ASSUME_NONNULL_BEGIN
/** Category of additions for @c SqfliteDarwinDatabase class.
See also
- @c SqfliteDarwinDatabase
*/
@interface SqfliteDarwinDatabase (SqfliteDarwinDatabaseAdditions)
///----------------------------------------
/// @name Return results of SQL to variable
///----------------------------------------
/** Return @c int value for query
@param query The SQL query to be performed, followed by a list of parameters that will be bound to the `?` placeholders in the SQL query.
@return @c int value.
@note This is not available from Swift.
*/
- (int)intForQuery:(NSString*)query, ...;
/** Return @c long value for query
@param query The SQL query to be performed, followed by a list of parameters that will be bound to the `?` placeholders in the SQL query.
@return @c long value.
@note This is not available from Swift.
*/
- (long)longForQuery:(NSString*)query, ...;
/** Return `BOOL` value for query
@param query The SQL query to be performed, followed by a list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `BOOL` value.
@note This is not available from Swift.
*/
- (BOOL)boolForQuery:(NSString*)query, ...;
/** Return `double` value for query
@param query The SQL query to be performed, followed by a list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `double` value.
@note This is not available from Swift.
*/
- (double)doubleForQuery:(NSString*)query, ...;
/** Return @c NSString value for query
@param query The SQL query to be performed, followed by a list of parameters that will be bound to the `?` placeholders in the SQL query.
@return @c NSString value.
@note This is not available from Swift.
*/
- (NSString * _Nullable)stringForQuery:(NSString*)query, ...;
/** Return @c NSData value for query
@param query The SQL query to be performed, followed by a list of parameters that will be bound to the `?` placeholders in the SQL query.
@return @c NSData value.
@note This is not available from Swift.
*/
- (NSData * _Nullable)dataForQuery:(NSString*)query, ...;
/** Return @c NSDate value for query
@param query The SQL query to be performed, followed by a list of parameters that will be bound to the `?` placeholders in the SQL query.
@return @c NSDate value.
@note This is not available from Swift.
*/
- (NSDate * _Nullable)dateForQuery:(NSString*)query, ...;
// Notice that there's no dataNoCopyForQuery:.
// That would be a bad idea, because we close out the result set, and then what
// happens to the data that we just didn't copy? Who knows, not I.
///--------------------------------
/// @name Schema related operations
///--------------------------------
/** Does table exist in database?
@param tableName The name of the table being looked for.
@return @c YES if table found; @c NO if not found.
*/
- (BOOL)tableExists:(NSString*)tableName;
/** The schema of the database.
This will be the schema for the entire database. For each entity, each row of the result set will include the following fields:
- `type` - The type of entity (e.g. table, index, view, or trigger)
- `name` - The name of the object
- `tbl_name` - The name of the table to which the object references
- `rootpage` - The page number of the root b-tree page for tables and indices
- `sql` - The SQL that created the entity
@return `SqfliteDarwinResultSet` of schema; @c nil on error.
@see [SQLite File Format](https://sqlite.org/fileformat.html)
*/
- (SqfliteDarwinResultSet * _Nullable)getSchema;
/** The schema of the database.
This will be the schema for a particular table as report by SQLite `PRAGMA`, for example:
PRAGMA table_info('employees')
This will report:
- `cid` - The column ID number
- `name` - The name of the column
- `type` - The data type specified for the column
- `notnull` - whether the field is defined as NOT NULL (i.e. values required)
- `dflt_value` - The default value for the column
- `pk` - Whether the field is part of the primary key of the table
@param tableName The name of the table for whom the schema will be returned.
@return `SqfliteDarwinResultSet` of schema; @c nil on error.
@see [table_info](https://sqlite.org/pragma.html#pragma_table_info)
*/
- (SqfliteDarwinResultSet * _Nullable)getTableSchema:(NSString*)tableName;
/** Test to see if particular column exists for particular table in database
@param columnName The name of the column.
@param tableName The name of the table.
@return @c YES if column exists in table in question; @c NO otherwise.
*/
- (BOOL)columnExists:(NSString*)columnName inTableWithName:(NSString*)tableName;
/** Test to see if particular column exists for particular table in database
@param columnName The name of the column.
@param tableName The name of the table.
@return @c YES if column exists in table in question; @c NO otherwise.
@see columnExists:inTableWithName:
@warning Deprecated - use `<columnExists:inTableWithName:>` instead.
*/
- (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName __deprecated_msg("Use columnExists:inTableWithName: instead");
/** Validate SQL statement
This validates SQL statement by performing `sqlite3_prepare_v2`, but not returning the results, but instead immediately calling `sqlite3_finalize`.
@param sql The SQL statement being validated.
@param error This is a pointer to a @c NSError object that will receive the autoreleased @c NSError object if there was any error. If this is @c nil , no @c NSError result will be returned.
@return @c YES if validation succeeded without incident; @c NO otherwise.
*/
- (BOOL)validateSQL:(NSString*)sql error:(NSError * _Nullable __autoreleasing *)error;
///-----------------------------------
/// @name Application identifier tasks
///-----------------------------------
/** Retrieve application ID
@return The `uint32_t` numeric value of the application ID.
@see setApplicationID:
*/
@property (nonatomic) uint32_t applicationID;
#if TARGET_OS_MAC && !TARGET_OS_IPHONE
/** Retrieve application ID string
@see setApplicationIDString:
*/
@property (nonatomic, retain) NSString *applicationIDString;
#endif
///-----------------------------------
/// @name user version identifier tasks
///-----------------------------------
/** Retrieve user version
@see setUserVersion:
*/
@property (nonatomic) uint32_t userVersion;
@end
NS_ASSUME_NONNULL_END
//
// SqfliteDarwinDatabaseAdditions.m
// fmdb
//
// Created by August Mueller on 10/30/05.
// Copyright 2005 Flying Meat Inc.. All rights reserved.
//
#import "SqfliteDarwinDatabase.h"
#import "SqfliteDarwinDatabaseAdditions.h"
#import "TargetConditionals.h"
#if SqfliteDarwinDB_SQLITE_STANDALONE
#import <sqlite3/sqlite3.h>
#else
#import <sqlite3.h>
#endif
@interface SqfliteDarwinDatabase (PrivateStuff)
- (SqfliteDarwinResultSet * _Nullable)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray * _Nullable)arrayArgs orDictionary:(NSDictionary * _Nullable)dictionaryArgs orVAList:(va_list)args shouldBind:(BOOL)shouldBind;
@end
@implementation SqfliteDarwinDatabase (SqfliteDarwinDatabaseAdditions)
#define RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(type, sel) \
va_list args; \
va_start(args, query); \
SqfliteDarwinResultSet *resultSet = [self executeQuery:query withArgumentsInArray:0x00 orDictionary:0x00 orVAList:args shouldBind:true]; \
va_end(args); \
if (![resultSet next]) { return (type)0; } \
type ret = [resultSet sel:0]; \
[resultSet close]; \
[resultSet setParentDB:nil]; \
return ret;
- (NSString *)stringForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSString *, stringForColumnIndex);
}
- (int)intForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(int, intForColumnIndex);
}
- (long)longForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(long, longForColumnIndex);
}
- (BOOL)boolForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(BOOL, boolForColumnIndex);
}
- (double)doubleForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(double, doubleForColumnIndex);
}
- (NSData*)dataForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSData *, dataForColumnIndex);
}
- (NSDate*)dateForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSDate *, dateForColumnIndex);
}
- (BOOL)tableExists:(NSString*)tableName {
tableName = [tableName lowercaseString];
SqfliteDarwinResultSet *rs = [self executeQuery:@"select [sql] from sqlite_master where [type] = 'table' and lower(name) = ?", tableName];
//if at least one next exists, table exists
BOOL returnBool = [rs next];
//close and free object
[rs close];
return returnBool;
}
/*
get table with list of tables: result colums: type[STRING], name[STRING],tbl_name[STRING],rootpage[INTEGER],sql[STRING]
check if table exist in database (patch from OZLB)
*/
- (SqfliteDarwinResultSet * _Nullable)getSchema {
//result colums: type[STRING], name[STRING],tbl_name[STRING],rootpage[INTEGER],sql[STRING]
SqfliteDarwinResultSet *rs = [self executeQuery:@"SELECT type, name, tbl_name, rootpage, sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type != 'meta' AND name NOT LIKE 'sqlite_%' ORDER BY tbl_name, type DESC, name"];
return rs;
}
/*
get table schema: result colums: cid[INTEGER], name,type [STRING], notnull[INTEGER], dflt_value[],pk[INTEGER]
*/
- (SqfliteDarwinResultSet * _Nullable)getTableSchema:(NSString*)tableName {
//result colums: cid[INTEGER], name,type [STRING], notnull[INTEGER], dflt_value[],pk[INTEGER]
SqfliteDarwinResultSet *rs = [self executeQuery:[NSString stringWithFormat: @"pragma table_info('%@')", tableName]];
return rs;
}
- (BOOL)columnExists:(NSString*)columnName inTableWithName:(NSString*)tableName {
BOOL returnBool = NO;
tableName = [tableName lowercaseString];
columnName = [columnName lowercaseString];
SqfliteDarwinResultSet *rs = [self getTableSchema:tableName];
//check if column is present in table schema
while ([rs next]) {
if ([[[rs stringForColumn:@"name"] lowercaseString] isEqualToString:columnName]) {
returnBool = YES;
break;
}
}
//If this is not done SqfliteDarwinDatabase instance stays out of pool
[rs close];
return returnBool;
}
- (uint32_t)applicationID {
#if SQLITE_VERSION_NUMBER >= 3007017
uint32_t r = 0;
SqfliteDarwinResultSet *rs = [self executeQuery:@"pragma application_id"];
if ([rs next]) {
r = (uint32_t)[rs longLongIntForColumnIndex:0];
}
[rs close];
return r;
#else
NSString *errorMessage = NSLocalizedStringFromTable(@"Application ID functions require SQLite 3.7.17", @"SqfliteDarwinDB", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return 0;
#endif
}
- (void)setApplicationID:(uint32_t)appID {
#if SQLITE_VERSION_NUMBER >= 3007017
NSString *query = [NSString stringWithFormat:@"pragma application_id=%d", appID];
SqfliteDarwinResultSet *rs = [self executeQuery:query];
[rs next];
[rs close];
#else
NSString *errorMessage = NSLocalizedStringFromTable(@"Application ID functions require SQLite 3.7.17", @"SqfliteDarwinDB", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
#endif
}
#if TARGET_OS_MAC && !TARGET_OS_IPHONE
- (NSString*)applicationIDString {
#if SQLITE_VERSION_NUMBER >= 3007017
NSString *s = NSFileTypeForHFSTypeCode([self applicationID]);
assert([s length] == 6);
s = [s substringWithRange:NSMakeRange(1, 4)];
return s;
#else
NSString *errorMessage = NSLocalizedStringFromTable(@"Application ID functions require SQLite 3.7.17", @"SqfliteDarwinDB", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return nil;
#endif
}
- (void)setApplicationIDString:(NSString*)s {
#if SQLITE_VERSION_NUMBER >= 3007017
if ([s length] != 4) {
NSLog(@"setApplicationIDString: string passed is not exactly 4 chars long. (was %ld)", [s length]);
}
[self setApplicationID:NSHFSTypeCodeFromFileType([NSString stringWithFormat:@"'%@'", s])];
#else
NSString *errorMessage = NSLocalizedStringFromTable(@"Application ID functions require SQLite 3.7.17", @"SqfliteDarwinDB", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
#endif
}
#endif
- (uint32_t)userVersion {
uint32_t r = 0;
SqfliteDarwinResultSet *rs = [self executeQuery:@"pragma user_version"];
if ([rs next]) {
r = (uint32_t)[rs longLongIntForColumnIndex:0];
}
[rs close];
return r;
}
- (void)setUserVersion:(uint32_t)version {
NSString *query = [NSString stringWithFormat:@"pragma user_version = %d", version];
SqfliteDarwinResultSet *rs = [self executeQuery:query];
[rs next];
[rs close];
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName __attribute__ ((deprecated)) {
return [self columnExists:columnName inTableWithName:tableName];
}
#pragma clang diagnostic pop
- (BOOL)validateSQL:(NSString*)sql error:(NSError * _Nullable __autoreleasing *)error {
sqlite3_stmt *pStmt = NULL;
BOOL validationSucceeded = YES;
int rc = sqlite3_prepare_v2([self sqliteHandle], [sql UTF8String], -1, &pStmt, 0);
if (rc != SQLITE_OK) {
validationSucceeded = NO;
if (error) {
*error = [NSError errorWithDomain:NSCocoaErrorDomain
code:[self lastErrorCode]
userInfo:[NSDictionary dictionaryWithObject:[self lastErrorMessage]
forKey:NSLocalizedDescriptionKey]];
}
}
sqlite3_finalize(pStmt);
return validationSucceeded;
}
@end
//
// SqfliteDarwinDatabaseQueue.h
// fmdb
//
// Created by August Mueller on 6/22/11.
// Copyright 2011 Flying Meat Inc. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "SqfliteDarwinDatabase.h"
NS_ASSUME_NONNULL_BEGIN
/** To perform queries and updates on multiple threads, you'll want to use @c SqfliteDarwinDatabaseQueue .
Using a single instance of @c SqfliteDarwinDatabase from multiple threads at once is a bad idea. It has always been OK to make a @c SqfliteDarwinDatabase object *per thread*. Just don't share a single instance across threads, and definitely not across multiple threads at the same time.
Instead, use @c SqfliteDarwinDatabaseQueue . Here's how to use it:
First, make your queue.
@code
SqfliteDarwinDatabaseQueue *queue = [SqfliteDarwinDatabaseQueue databaseQueueWithPath:aPath];
@endcode
Then use it like so:
@code
[queue inDatabase:^(SqfliteDarwinDatabase *db) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
SqfliteDarwinResultSet *rs = [db executeQuery:@"select * from foo"];
while ([rs next]) {
//…
}
}];
@endcode
An easy way to wrap things up in a transaction can be done like this:
@code
[queue inTransaction:^(SqfliteDarwinDatabase *db, BOOL *rollback) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
// if (whoopsSomethingWrongHappened) {
// *rollback = YES;
// return;
// }
// etc…
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:4]];
}];
@endcode
@c SqfliteDarwinDatabaseQueue will run the blocks on a serialized queue (hence the name of the class). So if you call @c SqfliteDarwinDatabaseQueue 's methods from multiple threads at the same time, they will be executed in the order they are received. This way queries and updates won't step on each other's toes, and every one is happy.
@warning Do not instantiate a single @c SqfliteDarwinDatabase object and use it across multiple threads. Use @c SqfliteDarwinDatabaseQueue instead.
@warning The calls to @c SqfliteDarwinDatabaseQueue 's methods are blocking. So even though you are passing along blocks, they will **not** be run on another thread.
@sa SqfliteDarwinDatabase
*/
@interface SqfliteDarwinDatabaseQueue : NSObject
/** Path of database */
@property (atomic, retain, nullable) NSString *path;
/** Open flags */
@property (atomic, readonly) int openFlags;
/** Custom virtual file system name */
@property (atomic, copy, nullable) NSString *vfsName;
///----------------------------------------------------
/// @name Initialization, opening, and closing of queue
///----------------------------------------------------
/** Create queue using path.
@param aPath The file path of the database.
@return The @c SqfliteDarwinDatabaseQueue object. @c nil on error.
*/
+ (nullable instancetype)databaseQueueWithPath:(NSString * _Nullable)aPath;
/** Create queue using file URL.
@param url The file @c NSURL of the database.
@return The @c SqfliteDarwinDatabaseQueue object. @c nil on error.
*/
+ (nullable instancetype)databaseQueueWithURL:(NSURL * _Nullable)url;
/** Create queue using path and specified flags.
@param aPath The file path of the database.
@param openFlags Flags passed to the openWithFlags method of the database.
@return The @c SqfliteDarwinDatabaseQueue object. @c nil on error.
*/
+ (nullable instancetype)databaseQueueWithPath:(NSString * _Nullable)aPath flags:(int)openFlags;
/** Create queue using file URL and specified flags.
@param url The file @c NSURL of the database.
@param openFlags Flags passed to the openWithFlags method of the database.
@return The @c SqfliteDarwinDatabaseQueue object. @c nil on error.
*/
+ (nullable instancetype)databaseQueueWithURL:(NSURL * _Nullable)url flags:(int)openFlags;
/** Create queue using path.
@param aPath The file path of the database.
@return The @c SqfliteDarwinDatabaseQueue object. @c nil on error.
*/
- (nullable instancetype)initWithPath:(NSString * _Nullable)aPath;
/** Create queue using file URL.
@param url The file `NSURL of the database.
@return The @c SqfliteDarwinDatabaseQueue object. @c nil on error.
*/
- (nullable instancetype)initWithURL:(NSURL * _Nullable)url;
/** Create queue using path and specified flags.
@param aPath The file path of the database.
@param openFlags Flags passed to the openWithFlags method of the database.
@return The @c SqfliteDarwinDatabaseQueue object. @c nil on error.
*/
- (nullable instancetype)initWithPath:(NSString * _Nullable)aPath flags:(int)openFlags;
/** Create queue using file URL and specified flags.
@param url The file path of the database.
@param openFlags Flags passed to the openWithFlags method of the database.
@return The @c SqfliteDarwinDatabaseQueue object. @c nil on error.
*/
- (nullable instancetype)initWithURL:(NSURL * _Nullable)url flags:(int)openFlags;
/** Create queue using path and specified flags.
@param aPath The file path of the database.
@param openFlags Flags passed to the openWithFlags method of the database
@param vfsName The name of a custom virtual file system
@return The @c SqfliteDarwinDatabaseQueue object. @c nil on error.
*/
- (nullable instancetype)initWithPath:(NSString * _Nullable)aPath flags:(int)openFlags vfs:(NSString * _Nullable)vfsName;
/** Create queue using file URL and specified flags.
@param url The file `NSURL of the database.
@param openFlags Flags passed to the openWithFlags method of the database
@param vfsName The name of a custom virtual file system
@return The @c SqfliteDarwinDatabaseQueue object. @c nil on error.
*/
- (nullable instancetype)initWithURL:(NSURL * _Nullable)url flags:(int)openFlags vfs:(NSString * _Nullable)vfsName;
/** Returns the Class of 'SqfliteDarwinDatabase' subclass, that will be used to instantiate database object.
Subclasses can override this method to return specified Class of 'SqfliteDarwinDatabase' subclass.
@return The Class of 'SqfliteDarwinDatabase' subclass, that will be used to instantiate database object.
*/
+ (Class)databaseClass;
/** Close database used by queue. */
- (void)close;
/** Interupt pending database operation. */
- (void)interrupt;
///-----------------------------------------------
/// @name Dispatching database operations to queue
///-----------------------------------------------
/** Synchronously perform database operations on queue.
@param block The code to be run on the queue of @c SqfliteDarwinDatabaseQueue
*/
- (void)inDatabase:(__attribute__((noescape)) void (^)(SqfliteDarwinDatabase *db))block;
/** Synchronously perform database operations on queue, using transactions.
@param block The code to be run on the queue of @c SqfliteDarwinDatabaseQueue
@warning Unlike SQLite's `BEGIN TRANSACTION`, this method currently performs
an exclusive transaction, not a deferred transaction. This behavior
is likely to change in future versions of SqfliteDarwinDB, whereby this method
will likely eventually adopt standard SQLite behavior and perform
deferred transactions. If you really need exclusive tranaction, it is
recommended that you use `inExclusiveTransaction`, instead, not only
to make your intent explicit, but also to future-proof your code.
*/
- (void)inTransaction:(__attribute__((noescape)) void (^)(SqfliteDarwinDatabase *db, BOOL *rollback))block;
/** Synchronously perform database operations on queue, using deferred transactions.
@param block The code to be run on the queue of @c SqfliteDarwinDatabaseQueue
*/
- (void)inDeferredTransaction:(__attribute__((noescape)) void (^)(SqfliteDarwinDatabase *db, BOOL *rollback))block;
/** Synchronously perform database operations on queue, using exclusive transactions.
@param block The code to be run on the queue of @c SqfliteDarwinDatabaseQueue
*/
- (void)inExclusiveTransaction:(__attribute__((noescape)) void (^)(SqfliteDarwinDatabase *db, BOOL *rollback))block;
/** Synchronously perform database operations on queue, using immediate transactions.
@param block The code to be run on the queue of @c SqfliteDarwinDatabaseQueue
*/
- (void)inImmediateTransaction:(__attribute__((noescape)) void (^)(SqfliteDarwinDatabase *db, BOOL *rollback))block;
///-----------------------------------------------
/// @name Dispatching database operations to queue
///-----------------------------------------------
/** Synchronously perform database operations using save point.
@param block The code to be run on the queue of @c SqfliteDarwinDatabaseQueue
*/
// NOTE: you can not nest these, since calling it will pull another database out of the pool and you'll get a deadlock.
// If you need to nest, use SqfliteDarwinDatabase's startSavePointWithName:error: instead.
- (NSError * _Nullable)inSavePoint:(__attribute__((noescape)) void (^)(SqfliteDarwinDatabase *db, BOOL *rollback))block;
///-----------------
/// @name Checkpoint
///-----------------
/** Performs a WAL checkpoint
@param checkpointMode The checkpoint mode for sqlite3_wal_checkpoint_v2
@param error The NSError corresponding to the error, if any.
@return YES on success, otherwise NO.
*/
- (BOOL)checkpoint:(SqfliteDarwinDBCheckpointMode)checkpointMode error:(NSError * _Nullable *)error;
/** Performs a WAL checkpoint
@param checkpointMode The checkpoint mode for sqlite3_wal_checkpoint_v2
@param name The db name for sqlite3_wal_checkpoint_v2
@param error The NSError corresponding to the error, if any.
@return YES on success, otherwise NO.
*/
- (BOOL)checkpoint:(SqfliteDarwinDBCheckpointMode)checkpointMode name:(NSString * _Nullable)name error:(NSError * _Nullable *)error;
/** Performs a WAL checkpoint
@param checkpointMode The checkpoint mode for sqlite3_wal_checkpoint_v2
@param name The db name for sqlite3_wal_checkpoint_v2
@param error The NSError corresponding to the error, if any.
@param logFrameCount If not NULL, then this is set to the total number of frames in the log file or to -1 if the checkpoint could not run because of an error or because the database is not in WAL mode.
@param checkpointCount If not NULL, then this is set to the total number of checkpointed frames in the log file (including any that were already checkpointed before the function was called) or to -1 if the checkpoint could not run due to an error or because the database is not in WAL mode.
@return YES on success, otherwise NO.
*/
- (BOOL)checkpoint:(SqfliteDarwinDBCheckpointMode)checkpointMode name:(NSString * _Nullable)name logFrameCount:(int * _Nullable)logFrameCount checkpointCount:(int * _Nullable)checkpointCount error:(NSError * _Nullable *)error;
@end
NS_ASSUME_NONNULL_END
//
// SqfliteDarwinDatabaseQueue.m
// fmdb
//
// Created by August Mueller on 6/22/11.
// Copyright 2011 Flying Meat Inc. All rights reserved.
//
#import "SqfliteDarwinDatabaseQueue.h"
#import "SqfliteDarwinDatabase.h"
#if SqfliteDarwinDB_SQLITE_STANDALONE
#import <sqlite3/sqlite3.h>
#else
#import <sqlite3.h>
#endif
typedef NS_ENUM(NSInteger, SqfliteDarwinDBTransaction) {
SqfliteDarwinDBTransactionExclusive,
SqfliteDarwinDBTransactionDeferred,
SqfliteDarwinDBTransactionImmediate,
};
/*
Note: we call [self retain]; before using dispatch_sync, just incase
SqfliteDarwinDatabaseQueue is released on another thread and we're in the middle of doing
something in dispatch_sync
*/
/*
* A key used to associate the SqfliteDarwinDatabaseQueue object with the dispatch_queue_t it uses.
* This in turn is used for deadlock detection by seeing if inDatabase: is called on
* the queue's dispatch queue, which should not happen and causes a deadlock.
*/
static const void * const kDispatchQueueSpecificKey = &kDispatchQueueSpecificKey;
@interface SqfliteDarwinDatabaseQueue () {
dispatch_queue_t _queue;
SqfliteDarwinDatabase *_db;
}
@end
@implementation SqfliteDarwinDatabaseQueue
+ (instancetype)databaseQueueWithPath:(NSString *)aPath {
SqfliteDarwinDatabaseQueue *q = [[self alloc] initWithPath:aPath];
SqfliteDarwinDBAutorelease(q);
return q;
}
+ (instancetype)databaseQueueWithURL:(NSURL *)url {
return [self databaseQueueWithPath:url.path];
}
+ (instancetype)databaseQueueWithPath:(NSString *)aPath flags:(int)openFlags {
SqfliteDarwinDatabaseQueue *q = [[self alloc] initWithPath:aPath flags:openFlags];
SqfliteDarwinDBAutorelease(q);
return q;
}
+ (instancetype)databaseQueueWithURL:(NSURL *)url flags:(int)openFlags {
return [self databaseQueueWithPath:url.path flags:openFlags];
}
+ (Class)databaseClass {
return [SqfliteDarwinDatabase class];
}
- (instancetype)initWithURL:(NSURL *)url flags:(int)openFlags vfs:(NSString *)vfsName {
return [self initWithPath:url.path flags:openFlags vfs:vfsName];
}
- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName {
self = [super init];
if (self != nil) {
_db = [[[self class] databaseClass] databaseWithPath:aPath];
SqfliteDarwinDBRetain(_db);
#if SQLITE_VERSION_NUMBER >= 3005000
BOOL success = [_db openWithFlags:openFlags vfs:vfsName];
#else
BOOL success = [_db open];
#endif
if (!success) {
NSLog(@"Could not create database queue for path %@", aPath);
SqfliteDarwinDBRelease(self);
return 0x00;
}
_path = SqfliteDarwinDBReturnRetained(aPath);
_queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
_openFlags = openFlags;
_vfsName = [vfsName copy];
}
return self;
}
- (instancetype)initWithPath:(NSString *)aPath flags:(int)openFlags {
return [self initWithPath:aPath flags:openFlags vfs:nil];
}
- (instancetype)initWithURL:(NSURL *)url flags:(int)openFlags {
return [self initWithPath:url.path flags:openFlags vfs:nil];
}
- (instancetype)initWithURL:(NSURL *)url {
return [self initWithPath:url.path];
}
- (instancetype)initWithPath:(NSString *)aPath {
// default flags for sqlite3_open
return [self initWithPath:aPath flags:SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE vfs:nil];
}
- (instancetype)init {
return [self initWithPath:nil];
}
- (void)dealloc {
SqfliteDarwinDBRelease(_db);
SqfliteDarwinDBRelease(_path);
SqfliteDarwinDBRelease(_vfsName);
if (_queue) {
SqfliteDarwinDBDispatchQueueRelease(_queue);
_queue = 0x00;
}
#if ! __has_feature(objc_arc)
[super dealloc];
#endif
}
- (void)close {
SqfliteDarwinDBRetain(self);
dispatch_sync(_queue, ^() {
[self->_db close];
SqfliteDarwinDBRelease(_db);
self->_db = 0x00;
});
SqfliteDarwinDBRelease(self);
}
- (void)interrupt {
[[self database] interrupt];
}
- (SqfliteDarwinDatabase*)database {
if (![_db isOpen]) {
if (!_db) {
_db = SqfliteDarwinDBReturnRetained([[[self class] databaseClass] databaseWithPath:_path]);
}
#if SQLITE_VERSION_NUMBER >= 3005000
BOOL success = [_db openWithFlags:_openFlags vfs:_vfsName];
#else
BOOL success = [_db open];
#endif
if (!success) {
NSLog(@"SqfliteDarwinDatabaseQueue could not reopen database for path %@", _path);
SqfliteDarwinDBRelease(_db);
_db = 0x00;
return 0x00;
}
}
return _db;
}
- (void)inDatabase:(__attribute__((noescape)) void (^)(SqfliteDarwinDatabase *db))block {
#ifndef NDEBUG
/* Get the currently executing queue (which should probably be nil, but in theory could be another DB queue
* and then check it against self to make sure we're not about to deadlock. */
SqfliteDarwinDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock");
#endif
SqfliteDarwinDBRetain(self);
dispatch_sync(_queue, ^() {
SqfliteDarwinDatabase *db = [self database];
block(db);
if ([db hasOpenResultSets]) {
NSLog(@"Warning: there is at least one open result set around after performing [SqfliteDarwinDatabaseQueue inDatabase:]");
#if defined(DEBUG) && DEBUG
NSSet *openSetCopy = SqfliteDarwinDBReturnAutoreleased([[db valueForKey:@"_openResultSets"] copy]);
for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) {
SqfliteDarwinResultSet *rs = (SqfliteDarwinResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
NSLog(@"query: '%@'", [rs query]);
}
#endif
}
});
SqfliteDarwinDBRelease(self);
}
- (void)beginTransaction:(SqfliteDarwinDBTransaction)transaction withBlock:(void (^)(SqfliteDarwinDatabase *db, BOOL *rollback))block {
SqfliteDarwinDBRetain(self);
dispatch_sync(_queue, ^() {
BOOL shouldRollback = NO;
switch (transaction) {
case SqfliteDarwinDBTransactionExclusive:
[[self database] beginTransaction];
break;
case SqfliteDarwinDBTransactionDeferred:
[[self database] beginDeferredTransaction];
break;
case SqfliteDarwinDBTransactionImmediate:
[[self database] beginImmediateTransaction];
break;
}
block([self database], &shouldRollback);
if (shouldRollback) {
[[self database] rollback];
}
else {
[[self database] commit];
}
});
SqfliteDarwinDBRelease(self);
}
- (void)inTransaction:(__attribute__((noescape)) void (^)(SqfliteDarwinDatabase *db, BOOL *rollback))block {
[self beginTransaction:SqfliteDarwinDBTransactionExclusive withBlock:block];
}
- (void)inDeferredTransaction:(__attribute__((noescape)) void (^)(SqfliteDarwinDatabase *db, BOOL *rollback))block {
[self beginTransaction:SqfliteDarwinDBTransactionDeferred withBlock:block];
}
- (void)inExclusiveTransaction:(__attribute__((noescape)) void (^)(SqfliteDarwinDatabase *db, BOOL *rollback))block {
[self beginTransaction:SqfliteDarwinDBTransactionExclusive withBlock:block];
}
- (void)inImmediateTransaction:(__attribute__((noescape)) void (^)(SqfliteDarwinDatabase * _Nonnull, BOOL * _Nonnull))block {
[self beginTransaction:SqfliteDarwinDBTransactionImmediate withBlock:block];
}
- (NSError*)inSavePoint:(__attribute__((noescape)) void (^)(SqfliteDarwinDatabase *db, BOOL *rollback))block {
#if SQLITE_VERSION_NUMBER >= 3007000
static unsigned long savePointIdx = 0;
__block NSError *err = 0x00;
SqfliteDarwinDBRetain(self);
dispatch_sync(_queue, ^() {
NSString *name = [NSString stringWithFormat:@"savePoint%ld", savePointIdx++];
BOOL shouldRollback = NO;
if ([[self database] startSavePointWithName:name error:&err]) {
block([self database], &shouldRollback);
if (shouldRollback) {
// We need to rollback and release this savepoint to remove it
[[self database] rollbackToSavePointWithName:name error:&err];
}
[[self database] releaseSavePointWithName:name error:&err];
}
});
SqfliteDarwinDBRelease(self);
return err;
#else
NSString *errorMessage = NSLocalizedStringFromTable(@"Save point functions require SQLite 3.7", @"SqfliteDarwinDB", nil);
if (_db.logsErrors) NSLog(@"%@", errorMessage);
return [NSError errorWithDomain:@"SqfliteDarwinDatabase" code:0 userInfo:@{NSLocalizedDescriptionKey : errorMessage}];
#endif
}
- (BOOL)checkpoint:(SqfliteDarwinDBCheckpointMode)mode error:(NSError * __autoreleasing *)error
{
return [self checkpoint:mode name:nil logFrameCount:NULL checkpointCount:NULL error:error];
}
- (BOOL)checkpoint:(SqfliteDarwinDBCheckpointMode)mode name:(NSString *)name error:(NSError * __autoreleasing *)error
{
return [self checkpoint:mode name:name logFrameCount:NULL checkpointCount:NULL error:error];
}
- (BOOL)checkpoint:(SqfliteDarwinDBCheckpointMode)mode name:(NSString *)name logFrameCount:(int * _Nullable)logFrameCount checkpointCount:(int * _Nullable)checkpointCount error:(NSError * __autoreleasing _Nullable * _Nullable)error
{
__block BOOL result;
SqfliteDarwinDBRetain(self);
dispatch_sync(_queue, ^() {
result = [self.database checkpoint:mode name:name logFrameCount:logFrameCount checkpointCount:checkpointCount error:error];
});
SqfliteDarwinDBRelease(self);
return result;
}
@end
//
// SqfliteDarwinImport.h
// Shared import for SqfliteDarwinDB
//
// Not a header file as XCode might complain.
//
// Created by Alexandre Roux on 03/12/2022.
//
#ifndef SqfliteDarwinImport_h
#define SqfliteDarwinImport_h
#import "SqfliteDarwinDB.h"
#endif /* SqfliteDarwinImport_h */
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#ifndef __has_feature // Optional.
#define __has_feature(x) 0 // Compatibility with non-clang compilers.
#endif
#ifndef NS_RETURNS_NOT_RETAINED
#if __has_feature(attribute_ns_returns_not_retained)
#define NS_RETURNS_NOT_RETAINED __attribute__((ns_returns_not_retained))
#else
#define NS_RETURNS_NOT_RETAINED
#endif
#endif
@class SqfliteDarwinDatabase;
@class SqfliteDarwinStatement;
/** Types for columns in a result set.
*/
typedef NS_ENUM(int, SqliteValueType) {
SqliteValueTypeInteger = 1,
SqliteValueTypeFloat = 2,
SqliteValueTypeText = 3,
SqliteValueTypeBlob = 4,
SqliteValueTypeNull = 5
};
/** Represents the results of executing a query on an @c SqfliteDarwinDatabase .
See also
- @c SqfliteDarwinDatabase
*/
@interface SqfliteDarwinResultSet : NSObject
@property (nonatomic, retain, nullable) SqfliteDarwinDatabase *parentDB;
///-----------------
/// @name Properties
///-----------------
/** Executed query */
@property (atomic, retain, nullable) NSString *query;
/** `NSMutableDictionary` mapping column names to numeric index */
@property (readonly) NSMutableDictionary *columnNameToIndexMap;
/** `SqfliteDarwinStatement` used by result set. */
@property (atomic, retain, nullable) SqfliteDarwinStatement *statement;
///------------------------------------
/// @name Creating and closing a result set
///------------------------------------
/** Close result set */
- (void)close;
///---------------------------------------
/// @name Iterating through the result set
///---------------------------------------
/** Retrieve next row for result set.
You must always invoke `next` or `nextWithError` before attempting to access the values returned in a query, even if you're only expecting one.
@return @c YES if row successfully retrieved; @c NO if end of result set reached
@see hasAnotherRow
*/
- (BOOL)next;
/** Retrieve next row for result set.
You must always invoke `next` or `nextWithError` before attempting to access the values returned in a query, even if you're only expecting one.
@param outErr A 'NSError' object to receive any error object (if any).
@return 'YES' if row successfully retrieved; 'NO' if end of result set reached
@see hasAnotherRow
*/
- (BOOL)nextWithError:(NSError * _Nullable __autoreleasing *)outErr;
/** Perform SQL statement.
@return 'YES' if successful; 'NO' if not.
@see hasAnotherRow
*/
- (BOOL)step;
/** Perform SQL statement.
@param outErr A 'NSError' object to receive any error object (if any).
@return 'YES' if successful; 'NO' if not.
@see hasAnotherRow
*/
- (BOOL)stepWithError:(NSError * _Nullable __autoreleasing *)outErr;
/** Did the last call to `<next>` succeed in retrieving another row?
@return 'YES' if there is another row; 'NO' if not.
@see next
@warning The `hasAnotherRow` method must follow a call to `<next>`. If the previous database interaction was something other than a call to `next`, then this method may return @c NO, whether there is another row of data or not.
*/
- (BOOL)hasAnotherRow;
///---------------------------------------------
/// @name Retrieving information from result set
///---------------------------------------------
/** How many columns in result set
@return Integer value of the number of columns.
*/
@property (nonatomic, readonly) int columnCount;
/** Column index for column name
@param columnName @c NSString value of the name of the column.
@return Zero-based index for column.
*/
- (int)columnIndexForName:(NSString*)columnName;
/** Column name for column index
@param columnIdx Zero-based index for column.
@return columnName @c NSString value of the name of the column.
*/
- (NSString * _Nullable)columnNameForIndex:(int)columnIdx;
/** Result set integer value for column.
@param columnName @c NSString value of the name of the column.
@return @c int value of the result set's column.
*/
- (int)intForColumn:(NSString*)columnName;
/** Result set integer value for column.
@param columnIdx Zero-based index for column.
@return @c int value of the result set's column.
*/
- (int)intForColumnIndex:(int)columnIdx;
/** Result set @c long value for column.
@param columnName @c NSString value of the name of the column.
@return @c long value of the result set's column.
*/
- (long)longForColumn:(NSString*)columnName;
/** Result set long value for column.
@param columnIdx Zero-based index for column.
@return @c long value of the result set's column.
*/
- (long)longForColumnIndex:(int)columnIdx;
/** Result set `long long int` value for column.
@param columnName @c NSString value of the name of the column.
@return `long long int` value of the result set's column.
*/
- (long long int)longLongIntForColumn:(NSString*)columnName;
/** Result set `long long int` value for column.
@param columnIdx Zero-based index for column.
@return `long long int` value of the result set's column.
*/
- (long long int)longLongIntForColumnIndex:(int)columnIdx;
/** Result set `unsigned long long int` value for column.
@param columnName @c NSString value of the name of the column.
@return `unsigned long long int` value of the result set's column.
*/
- (unsigned long long int)unsignedLongLongIntForColumn:(NSString*)columnName;
/** Result set `unsigned long long int` value for column.
@param columnIdx Zero-based index for column.
@return `unsigned long long int` value of the result set's column.
*/
- (unsigned long long int)unsignedLongLongIntForColumnIndex:(int)columnIdx;
/** Result set `BOOL` value for column.
@param columnName @c NSString value of the name of the column.
@return `BOOL` value of the result set's column.
*/
- (BOOL)boolForColumn:(NSString*)columnName;
/** Result set `BOOL` value for column.
@param columnIdx Zero-based index for column.
@return `BOOL` value of the result set's column.
*/
- (BOOL)boolForColumnIndex:(int)columnIdx;
/** Result set `double` value for column.
@param columnName @c NSString value of the name of the column.
@return `double` value of the result set's column.
*/
- (double)doubleForColumn:(NSString*)columnName;
/** Result set `double` value for column.
@param columnIdx Zero-based index for column.
@return `double` value of the result set's column.
*/
- (double)doubleForColumnIndex:(int)columnIdx;
/** Result set @c NSString value for column.
@param columnName @c NSString value of the name of the column.
@return String value of the result set's column.
*/
- (NSString * _Nullable)stringForColumn:(NSString*)columnName;
/** Result set @c NSString value for column.
@param columnIdx Zero-based index for column.
@return String value of the result set's column.
*/
- (NSString * _Nullable)stringForColumnIndex:(int)columnIdx;
/** Result set @c NSDate value for column.
@param columnName @c NSString value of the name of the column.
@return Date value of the result set's column.
*/
- (NSDate * _Nullable)dateForColumn:(NSString*)columnName;
/** Result set @c NSDate value for column.
@param columnIdx Zero-based index for column.
@return Date value of the result set's column.
*/
- (NSDate * _Nullable)dateForColumnIndex:(int)columnIdx;
/** Result set @c NSData value for column.
This is useful when storing binary data in table (such as image or the like).
@param columnName @c NSString value of the name of the column.
@return Data value of the result set's column.
*/
- (NSData * _Nullable)dataForColumn:(NSString*)columnName;
/** Result set @c NSData value for column.
@param columnIdx Zero-based index for column.
@warning For zero length BLOBs, this will return `nil`. Use `typeForColumn` to determine whether this was really a zero
length BLOB or `NULL`.
@return Data value of the result set's column.
*/
- (NSData * _Nullable)dataForColumnIndex:(int)columnIdx;
/** Result set `(const unsigned char *)` value for column.
@param columnName @c NSString value of the name of the column.
@warning For zero length BLOBs, this will return `nil`. Use `typeForColumnIndex` to determine whether this was really a zero
length BLOB or `NULL`.
@return `(const unsigned char *)` value of the result set's column.
*/
- (const unsigned char * _Nullable)UTF8StringForColumn:(NSString*)columnName;
- (const unsigned char * _Nullable)UTF8StringForColumnName:(NSString*)columnName __deprecated_msg("Use UTF8StringForColumn instead");
/** Result set `(const unsigned char *)` value for column.
@param columnIdx Zero-based index for column.
@return `(const unsigned char *)` value of the result set's column.
*/
- (const unsigned char * _Nullable)UTF8StringForColumnIndex:(int)columnIdx;
/** Result set object for column.
@param columnName Name of the column.
@return Either @c NSNumber , @c NSString , @c NSData , or @c NSNull . If the column was @c NULL , this returns `[NSNull null]` object.
@see objectForKeyedSubscript:
*/
- (id _Nullable)objectForColumn:(NSString*)columnName;
- (id _Nullable)objectForColumnName:(NSString*)columnName __deprecated_msg("Use objectForColumn instead");
/** Column type by column name.
@param columnName Name of the column.
@return The `SqliteValueType` of the value in this column.
*/
- (SqliteValueType)typeForColumn:(NSString*)columnName;
/** Column type by column index.
@param columnIdx Index of the column.
@return The `SqliteValueType` of the value in this column.
*/
- (SqliteValueType)typeForColumnIndex:(int)columnIdx;
/** Result set object for column.
@param columnIdx Zero-based index for column.
@return Either @c NSNumber , @c NSString , @c NSData , or @c NSNull . If the column was @c NULL , this returns `[NSNull null]` object.
@see objectAtIndexedSubscript:
*/
- (id _Nullable)objectForColumnIndex:(int)columnIdx;
/** Result set object for column.
This method allows the use of the "boxed" syntax supported in Modern Objective-C. For example, by defining this method, the following syntax is now supported:
@code
id result = rs[@"employee_name"];
@endcode
This simplified syntax is equivalent to calling:
@code
id result = [rs objectForKeyedSubscript:@"employee_name"];
@endcode
which is, it turns out, equivalent to calling:
@code
id result = [rs objectForColumnName:@"employee_name"];
@endcode
@param columnName @c NSString value of the name of the column.
@return Either @c NSNumber , @c NSString , @c NSData , or @c NSNull . If the column was @c NULL , this returns `[NSNull null]` object.
*/
- (id _Nullable)objectForKeyedSubscript:(NSString *)columnName;
/** Result set object for column.
This method allows the use of the "boxed" syntax supported in Modern Objective-C. For example, by defining this method, the following syntax is now supported:
@code
id result = rs[0];
@endcode
This simplified syntax is equivalent to calling:
@code
id result = [rs objectForKeyedSubscript:0];
@endcode
which is, it turns out, equivalent to calling:
@code
id result = [rs objectForColumnName:0];
@endcode
@param columnIdx Zero-based index for column.
@return Either @c NSNumber , @c NSString , @c NSData , or @c NSNull . If the column was @c NULL , this returns `[NSNull null]` object.
*/
- (id _Nullable)objectAtIndexedSubscript:(int)columnIdx;
/** Result set @c NSData value for column.
@param columnName @c NSString value of the name of the column.
@return Data value of the result set's column.
@warning If you are going to use this data after you iterate over the next row, or after you close the
result set, make sure to make a copy of the data first (or just use `<dataForColumn:>`/`<dataForColumnIndex:>`)
If you don't, you're going to be in a world of hurt when you try and use the data.
*/
- (NSData * _Nullable)dataNoCopyForColumn:(NSString *)columnName NS_RETURNS_NOT_RETAINED;
/** Result set @c NSData value for column.
@param columnIdx Zero-based index for column.
@return Data value of the result set's column.
@warning If you are going to use this data after you iterate over the next row, or after you close the
result set, make sure to make a copy of the data first (or just use `<dataForColumn:>`/`<dataForColumnIndex:>`)
If you don't, you're going to be in a world of hurt when you try and use the data.
*/
- (NSData * _Nullable)dataNoCopyForColumnIndex:(int)columnIdx NS_RETURNS_NOT_RETAINED;
/** Is the column @c NULL ?
@param columnIdx Zero-based index for column.
@return @c YES if column is @c NULL ; @c NO if not @c NULL .
*/
- (BOOL)columnIndexIsNull:(int)columnIdx;
/** Is the column @c NULL ?
@param columnName @c NSString value of the name of the column.
@return @c YES if column is @c NULL ; @c NO if not @c NULL .
*/
- (BOOL)columnIsNull:(NSString*)columnName;
/** Returns a dictionary of the row results mapped to case sensitive keys of the column names.
@warning The keys to the dictionary are case sensitive of the column names.
*/
@property (nonatomic, readonly, nullable) NSDictionary *resultDictionary;
/** Returns a dictionary of the row results
@see resultDictionary
@warning **Deprecated**: Please use `<resultDictionary>` instead. Also, beware that `<resultDictionary>` is case sensitive!
*/
- (NSDictionary * _Nullable)resultDict __deprecated_msg("Use resultDictionary instead");
///-----------------------------
/// @name Key value coding magic
///-----------------------------
/** Performs `setValue` to yield support for key value observing.
@param object The object for which the values will be set. This is the key-value-coding compliant object that you might, for example, observe.
*/
- (void)kvcMagic:(id)object;
///-----------------------------
/// @name Binding values
///-----------------------------
/// Bind array of values to prepared statement.
///
/// @param array Array of values to bind to SQL statement.
- (BOOL)bindWithArray:(NSArray*)array;
/// Bind dictionary of values to prepared statement.
///
/// @param dictionary Dictionary of values to bind to SQL statement.
- (BOOL)bindWithDictionary:(NSDictionary *)dictionary;
@end
NS_ASSUME_NONNULL_END
#import "SqfliteDarwinResultSet.h"
#import "SqfliteDarwinDatabase.h"
#import <unistd.h>
#if SqfliteDarwinDB_SQLITE_STANDALONE
#import <sqlite3/sqlite3.h>
#else
#import <sqlite3.h>
#endif
// MARK: - SqfliteDarwinDatabase Private Extension
@interface SqfliteDarwinDatabase ()
- (void)resultSetDidClose:(SqfliteDarwinResultSet *)resultSet;
- (BOOL)bindStatement:(sqlite3_stmt *)pStmt WithArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args;
@end
// MARK: - SqfliteDarwinResultSet Private Extension
@interface SqfliteDarwinResultSet () {
NSMutableDictionary *_columnNameToIndexMap;
}
@property (nonatomic) BOOL shouldAutoClose;
@end
// MARK: - SqfliteDarwinResultSet
@implementation SqfliteDarwinResultSet
+ (instancetype)resultSetWithStatement:(SqfliteDarwinStatement *)statement usingParentDatabase:(SqfliteDarwinDatabase*)aDB shouldAutoClose:(BOOL)shouldAutoClose {
SqfliteDarwinResultSet *rs = [[SqfliteDarwinResultSet alloc] init];
[rs setStatement:statement];
[rs setParentDB:aDB];
[rs setShouldAutoClose:shouldAutoClose];
NSParameterAssert(![statement inUse]);
[statement setInUse:YES]; // weak reference
return SqfliteDarwinDBReturnAutoreleased(rs);
}
#if ! __has_feature(objc_arc)
- (void)finalize {
[self close];
[super finalize];
}
#endif
- (void)dealloc {
[self close];
SqfliteDarwinDBRelease(_query);
_query = nil;
SqfliteDarwinDBRelease(_columnNameToIndexMap);
_columnNameToIndexMap = nil;
#if ! __has_feature(objc_arc)
[super dealloc];
#endif
}
- (void)close {
[_statement reset];
SqfliteDarwinDBRelease(_statement);
_statement = nil;
// we don't need this anymore... (i think)
//[_parentDB setInUse:NO];
[_parentDB resultSetDidClose:self];
[self setParentDB:nil];
}
- (int)columnCount {
return sqlite3_column_count([_statement statement]);
}
- (NSMutableDictionary *)columnNameToIndexMap {
if (!_columnNameToIndexMap) {
int columnCount = sqlite3_column_count([_statement statement]);
_columnNameToIndexMap = [[NSMutableDictionary alloc] initWithCapacity:(NSUInteger)columnCount];
int columnIdx = 0;
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
[_columnNameToIndexMap setObject:[NSNumber numberWithInt:columnIdx]
forKey:[[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)] lowercaseString]];
}
}
return _columnNameToIndexMap;
}
- (void)kvcMagic:(id)object {
int columnCount = sqlite3_column_count([_statement statement]);
int columnIdx = 0;
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
const char *c = (const char *)sqlite3_column_text([_statement statement], columnIdx);
// check for a null row
if (c) {
NSString *s = [NSString stringWithUTF8String:c];
[object setValue:s forKey:[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)]];
}
}
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (NSDictionary *)resultDict {
NSUInteger num_cols = (NSUInteger)sqlite3_data_count([_statement statement]);
if (num_cols > 0) {
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols];
NSEnumerator *columnNames = [[self columnNameToIndexMap] keyEnumerator];
NSString *columnName = nil;
while ((columnName = [columnNames nextObject])) {
id objectValue = [self objectForColumnName:columnName];
[dict setObject:objectValue forKey:columnName];
}
return SqfliteDarwinDBReturnAutoreleased([dict copy]);
}
else {
NSLog(@"Warning: There seem to be no columns in this set.");
}
return nil;
}
#pragma clang diagnostic pop
- (NSDictionary*)resultDictionary {
NSUInteger num_cols = (NSUInteger)sqlite3_data_count([_statement statement]);
if (num_cols > 0) {
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols];
int columnCount = sqlite3_column_count([_statement statement]);
int columnIdx = 0;
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
NSString *columnName = [NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)];
id objectValue = [self objectForColumnIndex:columnIdx];
[dict setObject:objectValue forKey:columnName];
}
return dict;
}
else {
NSLog(@"Warning: There seem to be no columns in this set.");
}
return nil;
}
- (BOOL)next {
return [self nextWithError:nil];
}
- (BOOL)nextWithError:(NSError * _Nullable __autoreleasing *)outErr {
int rc = [self internalStepWithError:outErr];
return rc == SQLITE_ROW;
}
- (BOOL)step {
return [self stepWithError:nil];
}
- (BOOL)stepWithError:(NSError * _Nullable __autoreleasing *)outErr {
int rc = [self internalStepWithError:outErr];
return rc == SQLITE_DONE;
}
- (int)internalStepWithError:(NSError * _Nullable __autoreleasing *)outErr {
int rc = sqlite3_step([_statement statement]);
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [_parentDB databasePath]);
NSLog(@"Database busy");
if (outErr) {
*outErr = [_parentDB lastError];
}
}
else if (SQLITE_DONE == rc || SQLITE_ROW == rc) {
// all is well, let's return.
}
else if (SQLITE_ERROR == rc) {
NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
if (outErr) {
*outErr = [_parentDB lastError];
}
}
else if (SQLITE_MISUSE == rc) {
// uh oh.
NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
if (outErr) {
if (_parentDB) {
*outErr = [_parentDB lastError];
}
else {
// If 'next' or 'nextWithError' is called after the result set is closed,
// we need to return the appropriate error.
NSDictionary* errorMessage = [NSDictionary dictionaryWithObject:@"parentDB does not exist" forKey:NSLocalizedDescriptionKey];
*outErr = [NSError errorWithDomain:@"SqfliteDarwinDatabase" code:SQLITE_MISUSE userInfo:errorMessage];
}
}
}
else {
// wtf?
NSLog(@"Unknown error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
if (outErr) {
*outErr = [_parentDB lastError];
}
}
if (rc != SQLITE_ROW && _shouldAutoClose) {
[self close];
}
return rc;
}
- (BOOL)hasAnotherRow {
return sqlite3_errcode([_parentDB sqliteHandle]) == SQLITE_ROW;
}
- (int)columnIndexForName:(NSString*)columnName {
columnName = [columnName lowercaseString];
NSNumber *n = [[self columnNameToIndexMap] objectForKey:columnName];
if (n != nil) {
return [n intValue];
}
NSLog(@"Warning: I could not find the column named '%@'.", columnName);
return -1;
}
- (int)intForColumn:(NSString*)columnName {
return [self intForColumnIndex:[self columnIndexForName:columnName]];
}
- (int)intForColumnIndex:(int)columnIdx {
return sqlite3_column_int([_statement statement], columnIdx);
}
- (long)longForColumn:(NSString*)columnName {
return [self longForColumnIndex:[self columnIndexForName:columnName]];
}
- (long)longForColumnIndex:(int)columnIdx {
return (long)sqlite3_column_int64([_statement statement], columnIdx);
}
- (long long int)longLongIntForColumn:(NSString*)columnName {
return [self longLongIntForColumnIndex:[self columnIndexForName:columnName]];
}
- (long long int)longLongIntForColumnIndex:(int)columnIdx {
return sqlite3_column_int64([_statement statement], columnIdx);
}
- (unsigned long long int)unsignedLongLongIntForColumn:(NSString*)columnName {
return [self unsignedLongLongIntForColumnIndex:[self columnIndexForName:columnName]];
}
- (unsigned long long int)unsignedLongLongIntForColumnIndex:(int)columnIdx {
return (unsigned long long int)[self longLongIntForColumnIndex:columnIdx];
}
- (BOOL)boolForColumn:(NSString*)columnName {
return [self boolForColumnIndex:[self columnIndexForName:columnName]];
}
- (BOOL)boolForColumnIndex:(int)columnIdx {
return ([self intForColumnIndex:columnIdx] != 0);
}
- (double)doubleForColumn:(NSString*)columnName {
return [self doubleForColumnIndex:[self columnIndexForName:columnName]];
}
- (double)doubleForColumnIndex:(int)columnIdx {
return sqlite3_column_double([_statement statement], columnIdx);
}
- (NSString *)stringForColumnIndex:(int)columnIdx {
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0) || columnIdx >= sqlite3_column_count([_statement statement])) {
return nil;
}
const char *c = (const char *)sqlite3_column_text([_statement statement], columnIdx);
if (!c) {
// null row.
return nil;
}
return [NSString stringWithUTF8String:c];
}
- (NSString*)stringForColumn:(NSString*)columnName {
return [self stringForColumnIndex:[self columnIndexForName:columnName]];
}
- (NSDate*)dateForColumn:(NSString*)columnName {
return [self dateForColumnIndex:[self columnIndexForName:columnName]];
}
- (NSDate*)dateForColumnIndex:(int)columnIdx {
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0) || columnIdx >= sqlite3_column_count([_statement statement])) {
return nil;
}
return [_parentDB hasDateFormatter] ? [_parentDB dateFromString:[self stringForColumnIndex:columnIdx]] : [NSDate dateWithTimeIntervalSince1970:[self doubleForColumnIndex:columnIdx]];
}
- (NSData*)dataForColumn:(NSString*)columnName {
return [self dataForColumnIndex:[self columnIndexForName:columnName]];
}
- (NSData*)dataForColumnIndex:(int)columnIdx {
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0) || columnIdx >= sqlite3_column_count([_statement statement])) {
return nil;
}
const char *dataBuffer = sqlite3_column_blob([_statement statement], columnIdx);
int dataSize = sqlite3_column_bytes([_statement statement], columnIdx);
if (dataBuffer == NULL) {
return nil;
}
return [NSData dataWithBytes:(const void *)dataBuffer length:(NSUInteger)dataSize];
}
- (NSData*)dataNoCopyForColumn:(NSString*)columnName {
return [self dataNoCopyForColumnIndex:[self columnIndexForName:columnName]];
}
- (NSData*)dataNoCopyForColumnIndex:(int)columnIdx {
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0) || columnIdx >= sqlite3_column_count([_statement statement])) {
return nil;
}
const char *dataBuffer = sqlite3_column_blob([_statement statement], columnIdx);
int dataSize = sqlite3_column_bytes([_statement statement], columnIdx);
NSData *data = [NSData dataWithBytesNoCopy:(void *)dataBuffer length:(NSUInteger)dataSize freeWhenDone:NO];
return data;
}
- (BOOL)columnIndexIsNull:(int)columnIdx {
return sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL;
}
- (BOOL)columnIsNull:(NSString*)columnName {
return [self columnIndexIsNull:[self columnIndexForName:columnName]];
}
- (const unsigned char *)UTF8StringForColumnIndex:(int)columnIdx {
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0) || columnIdx >= sqlite3_column_count([_statement statement])) {
return nil;
}
return sqlite3_column_text([_statement statement], columnIdx);
}
- (const unsigned char *)UTF8StringForColumn:(NSString*)columnName {
return [self UTF8StringForColumnIndex:[self columnIndexForName:columnName]];
}
- (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName {
return [self UTF8StringForColumn:columnName];
}
- (id)objectForColumnIndex:(int)columnIdx {
if (columnIdx < 0 || columnIdx >= sqlite3_column_count([_statement statement])) {
return nil;
}
int columnType = sqlite3_column_type([_statement statement], columnIdx);
id returnValue = nil;
if (columnType == SQLITE_INTEGER) {
returnValue = [NSNumber numberWithLongLong:[self longLongIntForColumnIndex:columnIdx]];
}
else if (columnType == SQLITE_FLOAT) {
returnValue = [NSNumber numberWithDouble:[self doubleForColumnIndex:columnIdx]];
}
else if (columnType == SQLITE_BLOB) {
returnValue = [self dataForColumnIndex:columnIdx];
}
else {
//default to a string for everything else
returnValue = [self stringForColumnIndex:columnIdx];
}
if (returnValue == nil) {
returnValue = [NSNull null];
}
return returnValue;
}
- (id)objectForColumnName:(NSString*)columnName {
return [self objectForColumn:columnName];
}
- (id)objectForColumn:(NSString*)columnName {
return [self objectForColumnIndex:[self columnIndexForName:columnName]];
}
- (SqliteValueType)typeForColumn:(NSString*)columnName {
return sqlite3_column_type([_statement statement], [self columnIndexForName:columnName]);
}
- (SqliteValueType)typeForColumnIndex:(int)columnIdx {
return sqlite3_column_type([_statement statement], columnIdx);
}
// returns autoreleased NSString containing the name of the column in the result set
- (NSString*)columnNameForIndex:(int)columnIdx {
return [NSString stringWithUTF8String: sqlite3_column_name([_statement statement], columnIdx)];
}
- (id)objectAtIndexedSubscript:(int)columnIdx {
return [self objectForColumnIndex:columnIdx];
}
- (id)objectForKeyedSubscript:(NSString *)columnName {
return [self objectForColumn:columnName];
}
// MARK: Bind
- (BOOL)bindWithArray:(NSArray*)array orDictionary:(NSDictionary *)dictionary orVAList:(va_list)args {
[_statement reset];
return [_parentDB bindStatement:_statement.statement WithArgumentsInArray:array orDictionary:dictionary orVAList:args];
}
- (BOOL)bindWithArray:(NSArray*)array {
return [self bindWithArray:array orDictionary:nil orVAList:nil];
}
- (BOOL)bindWithDictionary:(NSDictionary *)dictionary {
return [self bindWithArray:nil orDictionary:dictionary orVAList:nil];
}
@end
//
// SqfliteDatabase.h
// sqflite
//
// Created by Alexandre Roux on 24/10/2022.
//
#ifndef SqfliteDatabase_h
#define SqfliteDatabase_h
#import "SqfliteCursor.h"
#import "SqfliteOperation.h"
@class SqfliteDarwinDatabaseQueue,SqfliteDarwinDatabase;
@interface SqfliteDatabase : NSObject
@property (atomic, retain) SqfliteDarwinDatabaseQueue *fmDatabaseQueue;
@property (atomic, retain) NSNumber *databaseId;
@property (atomic, retain) NSString* path;
@property (nonatomic) bool singleInstance;
@property (nonatomic) bool inTransaction;
@property (nonatomic) int logLevel;
// Curosr support
@property (nonatomic) int lastCursorId;
@property (atomic, retain) NSMutableDictionary<NSNumber*, SqfliteCursor*>* cursorMap;
// Transaction v2
@property (nonatomic) int lastTransactionId;
@property (atomic, retain) NSNumber *currentTransactionId;
@property (atomic, retain) NSMutableArray<SqfliteQueuedOperation*>* noTransactionOperationQueue;
- (void)closeCursorById:(NSNumber*)cursorId;
- (void)closeCursor:(SqfliteCursor*)cursor;
- (void)inDatabase:(void (^)(SqfliteDarwinDatabase *db))block;
- (void)dbBatch:(SqfliteDarwinDatabase*)db operation:(SqfliteMethodCallOperation*)mainOperation;
- (void)dbExecute:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation;
- (void)dbInsert:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation;
- (void)dbUpdate:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation;
- (void)dbQuery:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation;
- (void)dbQueryCursorNext:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation;
@end
#endif // SqfliteDatabase_h
#import "SqfliteDatabase.h"
#import "SqflitePlugin.h"
#import "SqfliteDarwinImport.h"
#import <sqlite3.h>
// iOS workaround bug #214
static NSString *const SqfliteSqlPragmaSqliteDefensiveOff = @"PRAGMA sqflite -- db_config_defensive_off";
static NSString *const _paramCursorPageSize = @"cursorPageSize";
static NSString *const _paramCursorId = @"cursorId";
static NSString *const _paramCancel = @"cancel";
// For batch
static NSString *const _paramOperations = @"operations";
static int transactionIdForce = -1;
// Import hidden method
@interface SqfliteDarwinDatabase ()
- (void)resultSetDidClose:(SqfliteDarwinResultSet *)resultSet;
@end
@implementation SqfliteDatabase
@synthesize databaseId, fmDatabaseQueue, cursorMap, logLevel, currentTransactionId, noTransactionOperationQueue, lastCursorId,lastTransactionId;
- (instancetype)init {
self = [super init];
if (self) {
cursorMap = [NSMutableDictionary new];
lastCursorId = 0;
lastTransactionId = 0;
noTransactionOperationQueue = [NSMutableArray new];
}
return self;
}
- (void)inDatabase:(void (^)(SqfliteDarwinDatabase *db))block {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.fmDatabaseQueue inDatabase:block];
});
}
- (void)dbHandleError:(SqfliteDarwinDatabase*)db result:(FlutterResult)result {
// handle error
result([FlutterError errorWithCode:SqliteErrorCode
message:[NSString stringWithFormat:@"%@", [db lastError]]
details:nil]);
}
- (void)dbHandleError:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation {
NSMutableDictionary* details = nil;
NSString* sql = [operation getSql];
if (sql != nil) {
details = [NSMutableDictionary new];
[details setObject:sql forKey:SqfliteParamSql];
NSArray* sqlArguments = [operation getSqlArguments];
if (sqlArguments != nil) {
[details setObject:sqlArguments forKey:SqfliteParamSqlArguments];
}
}
[operation error:([FlutterError errorWithCode:SqliteErrorCode
message:[NSString stringWithFormat:@"%@", [db lastError]]
details:details])];
}
- (void)dbRunQueuedOperations:(SqfliteDarwinDatabase*)db {
while (![SqflitePlugin arrayIsEmpty:noTransactionOperationQueue]) {
if (currentTransactionId != nil) {
break;
}
SqfliteQueuedOperation* queuedOperation = [noTransactionOperationQueue objectAtIndex:0];
[noTransactionOperationQueue removeObjectAtIndex:0];
queuedOperation.handler(db, queuedOperation.operation);
}
}
- (void)wrapSqlOperationHandler:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation handler:(SqfliteOperationHandler)handler {
NSNumber* transactionId = [operation getTransactionId];
if (currentTransactionId == nil) {
// ignore
handler(db, operation);
} else if (transactionId != nil && (transactionId.intValue == currentTransactionId.intValue || transactionId.intValue == transactionIdForce)) {
handler(db, operation);
if (currentTransactionId == nil && ![SqflitePlugin arrayIsEmpty:noTransactionOperationQueue]) {
[self dbRunQueuedOperations:db];
}
} else {
// Queue for later
SqfliteQueuedOperation* queuedOperation = [SqfliteQueuedOperation new];
queuedOperation.operation = operation;
queuedOperation.handler = handler;
[noTransactionOperationQueue addObject:queuedOperation];
}
}
- (bool)dbDoExecute:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation {
if (![self dbExecuteOrError:db operation:operation]) {
return false;
}
[operation success:[NSNull null]];
return true;
}
- (void)dbExecute:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation {
[self wrapSqlOperationHandler:db operation:operation handler:^(SqfliteDarwinDatabase* db, SqfliteOperation* operation) {
NSNumber* inTransactionChange = [operation getInTransactionChange];
bool hasNullTransactionId = [operation hasNullTransactionId];
bool enteringTransaction = [inTransactionChange boolValue] == true && hasNullTransactionId;
if (enteringTransaction) {
self.currentTransactionId = [NSNumber numberWithInt:++self.lastTransactionId];
}
if ([self dbExecuteOrError:db operation:operation]) {
if (enteringTransaction) {
NSMutableDictionary* result = [NSMutableDictionary new];
[result setObject:self.currentTransactionId forKey:SqfliteParamTransactionId];
[operation success:result];
} else {
bool leavingTransaction = inTransactionChange != nil && [inTransactionChange boolValue] == false;
if (leavingTransaction) {
self.currentTransactionId = nil;
}
[operation success:[NSNull null]];
}
} else {
if (enteringTransaction) {
// On error revert change
self.currentTransactionId = nil;
}
}
}];
}
- (bool)dbExecuteOrError:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation {
NSString* sql = [operation getSql];
NSArray* sqlArguments = [operation getSqlArguments];
NSNumber* inTransaction = [operation getInTransactionChange];
// Handle Hardcoded workarounds
// Handle issue #525
if ([SqfliteSqlPragmaSqliteDefensiveOff isEqualToString:sql]) {
sqlite3_db_config(db.sqliteHandle, SQLITE_DBCONFIG_DEFENSIVE, 0, 0);
}
BOOL argumentsEmpty = [SqflitePlugin arrayIsEmpty:sqlArguments];
if (sqfliteHasSqlLogLevel(logLevel)) {
NSLog(@"%@ %@", sql, argumentsEmpty ? @"" : sqlArguments);
}
BOOL success;
if (!argumentsEmpty) {
success = [db executeUpdate: sql withArgumentsInArray: sqlArguments];
} else {
success = [db executeUpdate: sql];
}
// If wanted, we leave the transaction even if it fails
if (inTransaction != nil) {
if (![inTransaction boolValue]) {
self.inTransaction = false;
}
}
// handle error
if (!success) {
[self dbHandleError:db operation:operation];
return false;
}
// We enter the transaction on success
if (inTransaction != nil) {
if ([inTransaction boolValue]) {
self.inTransaction = true;
}
}
return true;
}
//
// insert
//
- (void)dbInsert:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation {
[self wrapSqlOperationHandler:db operation:operation handler:^(SqfliteDarwinDatabase* db, SqfliteOperation* operation) {
[self dbDoInsert:db operation:operation];
}];
}
- (bool)dbDoInsert:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation {
if (![self dbExecuteOrError:db operation:operation]) {
return false;
}
if ([operation getNoResult]) {
[operation success:[NSNull null]];
return true;
}
// handle ON CONFLICT IGNORE (issue #164) by checking the number of changes
// before
int changes = [db changes];
if (changes == 0) {
if (sqfliteHasSqlLogLevel(self.logLevel)) {
NSLog(@"no changes");
}
[operation success:[NSNull null]];
return true;
}
sqlite_int64 insertedId = [db lastInsertRowId];
if (sqfliteHasSqlLogLevel(self.logLevel)) {
NSLog(@"inserted %@", @(insertedId));
}
[operation success:(@(insertedId))];
return true;
}
- (void)dbUpdate:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation {
[self wrapSqlOperationHandler:db operation:operation handler:^(SqfliteDarwinDatabase* db, SqfliteOperation* operation) {
[self dbDoUpdate:db operation:operation];
}];
}
- (bool)dbDoUpdate:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation {
if (![self dbExecuteOrError:db operation:operation]) {
return false;
}
if ([operation getNoResult]) {
[operation success:[NSNull null]];
return true;
}
int changes = [db changes];
if (sqfliteHasSqlLogLevel(self.logLevel)) {
NSLog(@"changed %@", @(changes));
}
[operation success:(@(changes))];
return true;
}
//
// query
//
- (void)dbQuery:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation {
[self wrapSqlOperationHandler:db operation:operation handler:^(SqfliteDarwinDatabase* db, SqfliteOperation* operation) {
[self dbDoQuery:db operation:operation];
}];
}
- (bool)dbDoQuery:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation {
NSString* sql = [operation getSql];
NSArray* sqlArguments = [operation getSqlArguments];
bool argumentsEmpty = [SqflitePlugin arrayIsEmpty:sqlArguments];
// Non null means use a cursor
NSNumber* cursorPageSize = [operation getArgument:_paramCursorPageSize];
if (sqfliteHasSqlLogLevel(self.logLevel)) {
NSLog(@"%@ %@", sql, argumentsEmpty ? @"" : sqlArguments);
}
SqfliteDarwinResultSet *resultSet;
if (!argumentsEmpty) {
resultSet = [db executeQuery:sql withArgumentsInArray:sqlArguments];
} else {
// rs = [db executeQuery:sql];
// This crashes on MacOS if there is any ? in the query
// Workaround using an empty array
resultSet = [db executeQuery:sql withArgumentsInArray:@[]];
}
// handle error
if ([db hadError]) {
[self dbHandleError:db operation:operation];
return false;
}
NSMutableDictionary* results = [SqflitePlugin resultSetToResults:resultSet cursorPageSize:cursorPageSize];
if (cursorPageSize != nil) {
bool cursorHasMoreData = [resultSet hasAnotherRow];
if (cursorHasMoreData) {
NSNumber* cursorId = [NSNumber numberWithInt:++self.lastCursorId];
SqfliteCursor* cursor = [SqfliteCursor new];
cursor.cursorId = cursorId;
cursor.pageSize = cursorPageSize;
cursor.resultSet = resultSet;
self.cursorMap[cursorId] = cursor;
// Notify cursor support in the result
results[_paramCursorId] = cursorId;
// Prevent SqfliteDarwinDB warning, we keep a result set open on purpose
[db resultSetDidClose:resultSet];
}
}
[operation success:results];
return true;
}
//
// query
//
- (void)dbQueryCursorNext:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation {
NSNumber* cursorId = [operation getArgument:_paramCursorId];
NSNumber* cancelValue = [operation getArgument:_paramCancel];
bool cancel = [cancelValue boolValue] == true;
if (sqfliteHasVerboseLogLevel(self.logLevel))
{ NSLog(@"queryCursorNext %@%s", cursorId, cancel ? " (cancel)" : "");
}
if (cancel) {
[self closeCursorById:cursorId];
[operation success:[NSNull null]];
return;
} else {
SqfliteCursor* cursor = self.cursorMap[cursorId];
if (cursor == nil) {
NSLog(@"cursor %@ not found.", cursorId);
[operation success:[FlutterError errorWithCode:SqliteErrorCode
message: @"Cursor not found"
details:nil]];
return;
}
SqfliteDarwinResultSet* resultSet = cursor.resultSet;
NSMutableDictionary* results = [SqflitePlugin resultSetToResults:resultSet cursorPageSize:cursor.pageSize];
bool cursorHasMoreData = [resultSet hasAnotherRow];
if (cursorHasMoreData) {
// Keep the cursorId to specify that we have more data.
results[_paramCursorId] = cursorId;
// Prevent SqfliteDarwinDB warning, we keep a result set open on purpose
[db resultSetDidClose:resultSet];
} else {
[self closeCursor:cursor];
}
[operation success:results];
}
}
- (void)dbBatch:(SqfliteDarwinDatabase*)db operation:(SqfliteMethodCallOperation*)mainOperation {
bool noResult = [mainOperation getNoResult];
bool continueOnError = [mainOperation getContinueOnError];
NSArray* operations = [mainOperation getArgument:_paramOperations];
NSMutableArray* operationResults = [NSMutableArray new];
for (NSDictionary* dictionary in operations) {
// do something with object
SqfliteBatchOperation* operation = [SqfliteBatchOperation new];
operation.dictionary = dictionary;
operation.noResult = noResult;
NSString* method = [operation getMethod];
if ([SqfliteMethodInsert isEqualToString:method]) {
if ([self dbDoInsert:db operation:operation]) {
[operation handleSuccess:operationResults];
} else if (continueOnError) {
[operation handleErrorContinue:operationResults];
} else {
[operation handleError:mainOperation.flutterResult];
return;
}
} else if ([SqfliteMethodUpdate isEqualToString:method]) {
if ([self dbDoUpdate:db operation:operation]) {
[operation handleSuccess:operationResults];
} else if (continueOnError) {
[operation handleErrorContinue:operationResults];
} else {
[operation handleError:mainOperation.flutterResult];
return;
}
} else if ([SqfliteMethodExecute isEqualToString:method]) {
if ([self dbDoExecute:db operation:operation]) {
[operation handleSuccess:operationResults];
} else if (continueOnError) {
[operation handleErrorContinue:operationResults];
} else {
[operation handleError:mainOperation.flutterResult];
return;
}
} else if ([SqfliteMethodQuery isEqualToString:method]) {
if ([self dbDoQuery:db operation:operation]) {
[operation handleSuccess:operationResults];
} else if (continueOnError) {
[operation handleErrorContinue:operationResults];
} else {
[operation handleError:mainOperation.flutterResult];
return;
}
} else {
[mainOperation success:[FlutterError errorWithCode:SqfliteErrorBadParam
message:[NSString stringWithFormat:@"Batch method '%@' not supported", method]
details:nil]];
return;
}
}
if (noResult) {
[mainOperation success:[NSNull null]];
} else {
[mainOperation success:operationResults];
}
}
- (void)closeCursorById:(NSNumber*)cursorId {
SqfliteCursor* cursor = cursorMap[cursorId];
if (cursor != nil) {
[self closeCursor:cursor];
}
}
- (void)closeCursor:(SqfliteCursor*)cursor {
NSNumber* cursorId = cursor.cursorId;
if (sqfliteHasVerboseLogLevel(logLevel)) {
NSLog(@"closing cursor %@", cursorId);
}
[cursorMap removeObjectForKey:cursorId];
[cursor.resultSet close];
}
@end
//
// SqfliteImport.h
// sqflite
//
// Created by Alexandre Roux on 24/10/2022.
//
#ifndef SqfliteImport_h
#define SqfliteImport_h
#if TARGET_OS_IPHONE
#import <Flutter/Flutter.h>
#else
#import <FlutterMacOS/FlutterMacOS.h>
#endif
#endif // SqfliteImport_h
//
// Operation.h
// sqflite
//
// Created by Alexandre Roux on 09/01/2018.
//
#ifndef SqfliteOperation_h
#define SqfliteOperation_h
#import "SqfliteImport.h"
@class SqfliteDarwinDatabase;
@interface SqfliteOperation : NSObject
- (NSString*)getMethod;
- (NSString*)getSql;
- (NSArray*)getSqlArguments;
- (NSNumber*)getInTransactionChange;
- (void)success:(NSObject*)results;
- (void)error:(FlutterError*)error;
- (bool)getNoResult;
- (bool)getContinueOnError;
- (bool)hasNullTransactionId;
- (NSNumber*)getTransactionId;
// Generic way to get any argument
- (id)getArgument:(NSString*)key;
- (bool)hasArgument:(NSString*)key;
@end
@interface SqfliteBatchOperation : SqfliteOperation
@property (atomic, retain) NSDictionary* dictionary;
@property (atomic, retain) NSObject* results;
@property (atomic, retain) FlutterError* error;
@property (atomic, assign) bool noResult;
@property (atomic, assign) bool continueOnError;
- (void)handleSuccess:(NSMutableArray*)results;
- (void)handleErrorContinue:(NSMutableArray*)results;
- (void)handleError:(FlutterResult)result;
@end
@interface SqfliteMethodCallOperation : SqfliteOperation
@property (atomic, retain) FlutterMethodCall* flutterMethodCall;
@property (atomic, copy) FlutterResult flutterResult;
+ (SqfliteMethodCallOperation*)newWithCall:(FlutterMethodCall*)flutterMethodCall result:(FlutterResult)flutterResult;
@end
typedef void(^SqfliteOperationHandler)(SqfliteDarwinDatabase* db, SqfliteOperation* operation);
@interface SqfliteQueuedOperation : NSObject
@property (atomic, retain) SqfliteOperation* operation;
@property (atomic, copy) SqfliteOperationHandler handler;
@end
#endif // SqfliteOperation_h
//
// Operation.m
// sqflite
//
// Created by Alexandre Roux on 09/01/2018.
//
#import <Foundation/Foundation.h>
#import "SqfliteOperation.h"
#import "SqflitePlugin.h"
#import "SqfliteDarwinImport.h"
// Abstract
@implementation SqfliteOperation
- (NSString*)getMethod {
return nil;
}
- (NSString*)getSql {
return nil;
}
- (NSArray*)getSqlArguments {
return nil;
}
- (bool)getNoResult {
return false;
}
- (bool)getContinueOnError {
return false;
}
- (void)success:(NSObject*)results {}
- (void)error:(NSObject*)error {}
// To override
- (id)getArgument:(NSString*)key {
return nil;
}
// To override
- (bool)hasArgument:(NSString*)key {
return false;
}
// Either nil or NSNumber
- (NSNumber*)getTransactionId {
// It might be NSNull (for begin transaction)
id rawId = [self getArgument:SqfliteParamTransactionId];
if ([rawId isKindOfClass:[NSNumber class]]) {
return rawId;
}
return nil;
}
- (NSNumber*)getInTransactionChange {
return [self getArgument:SqfliteParamInTransactionChange];
}
- (bool)hasNullTransactionId {
return [self getArgument:SqfliteParamTransactionId] == [NSNull null];
}
@end
@implementation SqfliteBatchOperation
@synthesize dictionary, results, error, noResult, continueOnError;
- (NSString*)getMethod {
return [dictionary objectForKey:SqfliteParamMethod];
}
- (NSString*)getSql {
return [dictionary objectForKey:SqfliteParamSql];
}
- (NSArray*)getSqlArguments {
NSArray* arguments = [dictionary objectForKey:SqfliteParamSqlArguments];
return [SqflitePlugin toSqlArguments:arguments];
}
- (bool)getNoResult {
return noResult;
}
- (bool)getContinueOnError {
return continueOnError;
}
- (void)success:(NSObject*)results {
self.results = results;
}
- (void)error:(FlutterError*)error {
self.error = error;
}
- (void)handleSuccess:(NSMutableArray*)results {
if (![self getNoResult]) {
// We wrap the result in 'result' map
[results addObject:[NSDictionary dictionaryWithObject:((self.results == nil) ? [NSNull null] : self.results)
forKey:SqfliteParamResult]];
}
}
// Encore the flutter error in a map
- (void)handleErrorContinue:(NSMutableArray*)results {
if (![self getNoResult]) {
// We wrap the error in an 'error' map
NSMutableDictionary* error = [NSMutableDictionary new];
error[SqfliteParamErrorCode] = self.error.code;
if (self.error.message != nil) {
error[SqfliteParamErrorMessage] = self.error.message;
}
if (self.error.details != nil) {
error[SqfliteParamErrorData] = self.error.details;
}
[results addObject:[NSDictionary dictionaryWithObject:error
forKey:SqfliteParamError]];
}
}
- (void)handleError:(FlutterResult)result {
result(error);
}
- (id)getArgument:(NSString*)key {
return [dictionary objectForKey:key];
}
- (bool)hasArgument:(NSString*)key {
return [self getArgument:key] != nil;
}
@end
@implementation SqfliteMethodCallOperation
@synthesize flutterMethodCall;
@synthesize flutterResult;
+ (SqfliteMethodCallOperation*)newWithCall:(FlutterMethodCall*)flutterMethodCall result:(FlutterResult)flutterResult {
SqfliteMethodCallOperation* operation = [SqfliteMethodCallOperation new];
operation.flutterMethodCall = flutterMethodCall;
operation.flutterResult = flutterResult;
return operation;
}
- (NSString*)getMethod {
return flutterMethodCall.method;
}
- (NSString*)getSql {
return flutterMethodCall.arguments[SqfliteParamSql];
}
- (bool)getNoResult {
NSNumber* noResult = flutterMethodCall.arguments[SqfliteParamNoResult];
return [noResult boolValue];
}
- (bool)getContinueOnError {
NSNumber* noResult = flutterMethodCall.arguments[SqfliteParamContinueOnError];
return [noResult boolValue];
}
- (NSArray*)getSqlArguments {
NSArray* arguments = flutterMethodCall.arguments[SqfliteParamSqlArguments];
return [SqflitePlugin toSqlArguments:arguments];
}
- (void)success:(NSObject*)results {
flutterResult(results);
}
- (void)error:(NSObject*)error {
flutterResult(error);
}
- (id)getArgument:(NSString*)key {
return flutterMethodCall.arguments[key];
}
@end
@implementation SqfliteQueuedOperation
@synthesize operation, handler;
@end
//
// SqflitePlugin.h
// sqflite
//
// Created by Alexandre Roux on 24/10/2022.
//
#ifndef SqflitePlugin_h
#define SqflitePlugin_h
#import "SqfliteImport.h"
@class SqfliteDarwinResultSet;
@interface SqflitePlugin : NSObject<FlutterPlugin>
+ (NSArray*)toSqlArguments:(NSArray*)rawArguments;
+ (bool)arrayIsEmpty:(NSArray*)array;
+ (NSMutableDictionary*)resultSetToResults:(SqfliteDarwinResultSet*)resultSet cursorPageSize:(NSNumber*)cursorPageSize;
@end
extern NSString *const SqfliteMethodExecute;;
extern NSString *const SqfliteMethodInsert;
extern NSString *const SqfliteMethodUpdate;
extern NSString *const SqfliteMethodQuery;
extern NSString *const SqfliteErrorBadParam;
extern NSString *const SqliteErrorCode;
extern NSString *const SqfliteParamMethod;
extern NSString *const SqfliteParamSql;
extern NSString *const SqfliteParamSqlArguments;
extern NSString *const SqfliteParamInTransactionChange;
extern NSString *const SqfliteParamNoResult;
extern NSString *const SqfliteParamContinueOnError;
extern NSString *const SqfliteParamResult;
extern NSString *const SqfliteParamError;
extern NSString *const SqfliteParamErrorCode;
extern NSString *const SqfliteParamErrorMessage;
extern NSString *const SqfliteParamErrorData;
extern NSString *const SqfliteParamTransactionId;
// Static helpers
static const int sqfliteLogLevelNone = 0;
static const int sqfliteLogLevelSql = 1;
static const int sqfliteLogLevelVerbose = 2;
extern bool sqfliteHasSqlLogLevel(int logLevel);
// True for verbose debugging
extern bool sqfliteHasVerboseLogLevel(int logLevel);
#endif // SqflitePlugin_h
#import "SqflitePlugin.h"
#import "SqfliteDatabase.h"
#import "SqfliteOperation.h"
#import "SqfliteDarwinImport.h"
#import <sqlite3.h>
static NSString *const _channelName = @"com.tekartik.sqflite";
static NSString *const _inMemoryPath = @":memory:";
static NSString *const _methodGetPlatformVersion = @"getPlatformVersion";
static NSString *const _methodGetDatabasesPath = @"getDatabasesPath";
static NSString *const _methodDebugMode = @"debugMode";
static NSString *const _methodDebug = @"debug";
static NSString *const _methodOptions = @"options";
static NSString *const _methodOpenDatabase = @"openDatabase";
static NSString *const _methodCloseDatabase = @"closeDatabase";
static NSString *const _methodDeleteDatabase = @"deleteDatabase";
static NSString *const _methodDatabaseExists = @"databaseExists";
static NSString *const _methodQueryCursorNext = @"queryCursorNext";
static NSString *const _methodBatch = @"batch";
// For open
static NSString *const _paramReadOnly = @"readOnly";
static NSString *const _paramSingleInstance = @"singleInstance";
// Open result
static NSString *const _paramRecovered = @"recovered";
static NSString *const _paramRecoveredInTransaction = @"recoveredInTransaction";
// For batch
static NSString *const _paramOperations = @"operations";
// For each batch operation
static NSString *const _paramPath = @"path";
static NSString *const _paramId = @"id";
static NSString *const _paramTable = @"table";
static NSString *const _paramValues = @"values";
static NSString *const _errorOpenFailed = @"open_failed";
static NSString *const _errorDatabaseClosed = @"database_closed";
// debug
static NSString *const _paramDatabases = @"databases";
static NSString *const _paramLogLevel = @"logLevel";
static NSString *const _paramCmd = @"cmd";
static NSString *const _paramCmdGet = @"get";
// query
static NSString *const _paramCancel = @"cancel";
static NSString *const _paramCursorId = @"cursorId";
static NSString *const _paramCursorPageSize = @"cursorPageSize";
// Shared
NSString *const SqfliteMethodExecute = @"execute";
NSString *const SqfliteMethodInsert = @"insert";
NSString *const SqfliteMethodUpdate = @"update";
NSString *const SqfliteMethodQuery = @"query";
NSString *const SqliteErrorCode = @"sqlite_error";
NSString *const SqfliteErrorBadParam = @"bad_param"; // internal only
NSString *const SqfliteParamSql = @"sql";
NSString *const SqfliteParamSqlArguments = @"arguments";
NSString *const SqfliteParamInTransactionChange = @"inTransaction";
NSString *const SqfliteParamTransactionId = @"transactionId"; // int or null
NSString *const SqfliteParamNoResult = @"noResult";
NSString *const SqfliteParamContinueOnError = @"continueOnError";
NSString *const SqfliteParamMethod = @"method";
// For each operation in a batch, we have either a result or an error
NSString *const SqfliteParamResult = @"result";
NSString *const SqfliteParamError = @"error";
NSString *const SqfliteParamErrorCode = @"code";
NSString *const SqfliteParamErrorMessage = @"message";
NSString *const SqfliteParamErrorData = @"data";
// iOS workaround bug #214
NSString *const SqfliteSqlPragmaSqliteDefensiveOff = @"PRAGMA sqflite -- db_config_defensive_off";
// Import hidden method
@interface SqfliteDarwinDatabase ()
- (void)resultSetDidClose:(SqfliteDarwinResultSet *)resultSet;
@end
@interface SqflitePlugin ()
@property (atomic, retain) NSMutableDictionary<NSNumber*, SqfliteDatabase*>* databaseMap;
@property (atomic, retain) NSMutableDictionary<NSString*, SqfliteDatabase*>* singleInstanceDatabaseMap;
@property (atomic, retain) NSObject* mapLock;
@end
// True for basic debugging (open/close and sql)
bool sqfliteHasSqlLogLevel(int logLevel) {
return logLevel >= sqfliteLogLevelSql;
}
// True for verbose debugging
bool sqfliteHasVerboseLogLevel(int logLevel) {
return logLevel >= sqfliteLogLevelVerbose;
}
//
// Implementation
//
@implementation SqflitePlugin
@synthesize databaseMap;
@synthesize mapLock;
static int logLevel = sqfliteLogLevelNone;
// static BOOL _log = false;
static BOOL _extra_log = false;
static BOOL __extra_log = false; // to set to true for type debugging
static NSInteger _lastDatabaseId = 0;
static NSInteger _databaseOpenCount = 0;
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
#if TARGET_OS_IPHONE
FlutterMethodChannel* channel =
[[FlutterMethodChannel alloc] initWithName:_channelName
binaryMessenger:[registrar messenger]
codec:[FlutterStandardMethodCodec sharedInstance]
taskQueue:[registrar.messenger makeBackgroundTaskQueue]];
#else
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:_channelName
binaryMessenger:[registrar messenger]];
#endif
SqflitePlugin* instance = [[SqflitePlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (instancetype)init {
self = [super init];
if (self) {
self.databaseMap = [NSMutableDictionary new];
self.singleInstanceDatabaseMap = [NSMutableDictionary new];
self.mapLock = [NSObject new];
}
return self;
}
- (SqfliteDatabase *)getDatabaseOrError:(FlutterMethodCall*)call result:(FlutterResult)result {
NSNumber* databaseId = call.arguments[_paramId];
SqfliteDatabase* database = self.databaseMap[databaseId];
if (database == nil) {
NSLog(@"db not found.");
result([FlutterError errorWithCode:SqliteErrorCode
message: _errorDatabaseClosed
details:nil]);
}
return database;
}
- (void)handleError:(SqfliteDarwinDatabase*)db result:(FlutterResult)result {
// handle error
result([FlutterError errorWithCode:SqliteErrorCode
message:[NSString stringWithFormat:@"%@", [db lastError]]
details:nil]);
}
- (void)handleError:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation {
NSMutableDictionary* details = nil;
NSString* sql = [operation getSql];
if (sql != nil) {
details = [NSMutableDictionary new];
[details setObject:sql forKey:SqfliteParamSql];
NSArray* sqlArguments = [operation getSqlArguments];
if (sqlArguments != nil) {
[details setObject:sqlArguments forKey:SqfliteParamSqlArguments];
}
}
[operation error:([FlutterError errorWithCode:SqliteErrorCode
message:[NSString stringWithFormat:@"%@", [db lastError]]
details:details])];
}
+ (NSObject*)toSqlValue:(NSObject*)value {
if (_extra_log) {
NSLog(@"value type %@ %@", [value class], value);
}
if (value == nil) {
return nil;
} else if ([value isKindOfClass:[FlutterStandardTypedData class]]) {
FlutterStandardTypedData* typedData = (FlutterStandardTypedData*)value;
return typedData.data;
} else if ([value isKindOfClass:[NSArray class]]) {
// Assume array of number
// slow...to optimize
NSArray* array = (NSArray*)value;
NSMutableData* data = [NSMutableData new];
for (int i = 0; i < [array count]; i++) {
uint8_t byte = [((NSNumber *)[array objectAtIndex:i]) intValue];
[data appendBytes:&byte length:1];
}
return data;
} else {
return value;
}
}
+ (NSObject*)fromSqlValue:(NSObject*)sqlValue {
if (_extra_log) {
NSLog(@"sql value type %@ %@", [sqlValue class], sqlValue);
}
if (sqlValue == nil) {
return [NSNull null];
} else if ([sqlValue isKindOfClass:[NSData class]]) {
return [FlutterStandardTypedData typedDataWithBytes:(NSData*)sqlValue];
} else {
return sqlValue;
}
}
+ (bool)arrayIsEmpty:(NSArray*)array {
return (array == nil || array == (id)[NSNull null] || [array count] == 0);
}
+ (NSArray*)toSqlArguments:(NSArray*)rawArguments {
NSMutableArray* array = [NSMutableArray new];
if (![SqflitePlugin arrayIsEmpty:rawArguments]) {
for (int i = 0; i < [rawArguments count]; i++) {
[array addObject:[SqflitePlugin toSqlValue:[rawArguments objectAtIndex:i]]];
}
}
return array;
}
+ (NSDictionary*)fromSqlDictionary:(NSDictionary*)sqlDictionary {
NSMutableDictionary* dictionary = [NSMutableDictionary new];
[sqlDictionary enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) {
[dictionary setObject:[SqflitePlugin fromSqlValue:value] forKey:key];
}];
return dictionary;
}
// Rewrite to handle empty bloc reported as null
// refer to original SqfliteDarwinResultSet.objectForColumnIndex, removed
// when fixed in SqfliteDarwinDB
// See https://github.com/ccgus/fmdb/issues/350 for information
+ (id)rsObjectForColumn:(SqfliteDarwinResultSet*)rs index:(int)columnIdx {
SqfliteDarwinStatement* _statement = [rs statement];
if (columnIdx < 0 || columnIdx >= sqlite3_column_count([_statement statement])) {
return nil;
}
int columnType = sqlite3_column_type([_statement statement], columnIdx);
id returnValue = nil;
if (columnType == SQLITE_INTEGER) {
returnValue = [NSNumber numberWithLongLong:[rs longLongIntForColumnIndex:columnIdx]];
}
else if (columnType == SQLITE_FLOAT) {
returnValue = [NSNumber numberWithDouble:[rs doubleForColumnIndex:columnIdx]];
}
else if (columnType == SQLITE_BLOB) {
returnValue = [rs dataForColumnIndex:columnIdx];
// Workaround, empty blob are reported as nil
if (returnValue == nil) {
return [NSData new];
}
}
else {
//default to a string for everything else
returnValue = [rs stringForColumnIndex:columnIdx];
}
if (returnValue == nil) {
returnValue = [NSNull null];
}
return returnValue;
}
// if cursorPageSize is not null, we limit the result count
+ (NSMutableDictionary*)resultSetToResults:(SqfliteDarwinResultSet*)resultSet cursorPageSize:(NSNumber*)cursorPageSize {
NSMutableDictionary* results = [NSMutableDictionary new];
NSMutableArray* columns = nil;
NSMutableArray* rows;
int columnCount = 0;
while ([resultSet next]) {
if (columns == nil) {
columnCount = [resultSet columnCount];
columns = [NSMutableArray new];
rows = [NSMutableArray new];
for (int i = 0; i < columnCount; i++) {
[columns addObject:[resultSet columnNameForIndex:i]];
}
[results setValue:columns forKey:@"columns"];
[results setValue:rows forKey:@"rows"];
}
NSMutableArray* row = [NSMutableArray new];
for (int i = 0; i < columnCount; i++) {
[row addObject:[SqflitePlugin fromSqlValue:[self rsObjectForColumn:resultSet index:i]]];
}
[rows addObject:row];
if (cursorPageSize != nil) {
if ([rows count] >= [cursorPageSize intValue]) {
break;
}
}
}
return results;
}
//
// query
//
- (bool)query:(SqfliteDatabase*)database fmdb:(SqfliteDarwinDatabase*)db operation:(SqfliteOperation*)operation {
NSString* sql = [operation getSql];
NSArray* sqlArguments = [operation getSqlArguments];
bool argumentsEmpty = [SqflitePlugin arrayIsEmpty:sqlArguments];
// Non null means use a cursor
NSNumber* cursorPageSize = [operation getArgument:_paramCursorPageSize];
if (sqfliteHasSqlLogLevel(database.logLevel)) {
NSLog(@"%@ %@", sql, argumentsEmpty ? @"" : sqlArguments);
}
SqfliteDarwinResultSet *resultSet;
if (!argumentsEmpty) {
resultSet = [db executeQuery:sql withArgumentsInArray:sqlArguments];
} else {
// rs = [db executeQuery:sql];
// This crashes on MacOS if there is any ? in the query
// Workaround using an empty array
resultSet = [db executeQuery:sql withArgumentsInArray:@[]];
}
// handle error
if ([db hadError]) {
[self handleError:db operation:operation];
return false;
}
NSMutableDictionary* results = [SqflitePlugin resultSetToResults:resultSet cursorPageSize:cursorPageSize];
if (cursorPageSize != nil) {
bool cursorHasMoreData = [resultSet hasAnotherRow];
if (cursorHasMoreData) {
NSNumber* cursorId = [NSNumber numberWithInt:++database.lastCursorId];
SqfliteCursor* cursor = [SqfliteCursor new];
cursor.cursorId = cursorId;
cursor.pageSize = cursorPageSize;
cursor.resultSet = resultSet;
database.cursorMap[cursorId] = cursor;
// Notify cursor support in the result
results[_paramCursorId] = cursorId;
// Prevent SqfliteDarwinDB warning, we keep a result set open on purpose
[db resultSetDidClose:resultSet];
}
}
[operation success:results];
return true;
}
- (void)handleQueryCall:(FlutterMethodCall*)call result:(FlutterResult)result {
SqfliteDatabase* database = [self getDatabaseOrError:call result:result];
if (database == nil) {
return;
}
[database inDatabase:^(SqfliteDarwinDatabase *db) {
SqfliteMethodCallOperation* operation = [SqfliteMethodCallOperation newWithCall:call result:result];
[database dbQuery:db operation:operation];
}];
}
- (void)handleQueryCursorNextCall:(FlutterMethodCall*)call result:(FlutterResult)result {
SqfliteDatabase* database = [self getDatabaseOrError:call result:result];
if (database == nil) {
return;
}
[database inDatabase:^(SqfliteDarwinDatabase *db) {
SqfliteMethodCallOperation* operation = [SqfliteMethodCallOperation newWithCall:call result:result];
[database dbQueryCursorNext:db operation:operation];
}];
}
- (void)handleInsertCall:(FlutterMethodCall*)call result:(FlutterResult)result {
SqfliteDatabase* database = [self getDatabaseOrError:call result:result];
if (database == nil) {
return;
}
[database inDatabase:^(SqfliteDarwinDatabase *db) {
SqfliteMethodCallOperation* operation = [SqfliteMethodCallOperation newWithCall:call result:result];
[database dbInsert:db operation:operation];
}];
}
- (void)handleUpdateCall:(FlutterMethodCall*)call result:(FlutterResult)result {
SqfliteDatabase* database = [self getDatabaseOrError:call result:result];
if (database == nil) {
return;
}
[database inDatabase:^(SqfliteDarwinDatabase *db) {
SqfliteMethodCallOperation* operation = [SqfliteMethodCallOperation newWithCall:call result:result];
[database dbUpdate:db operation:operation];
}];
}
- (void)handleExecuteCall:(FlutterMethodCall*)call result:(FlutterResult)result {
SqfliteDatabase* database = [self getDatabaseOrError:call result:result];
if (database == nil) {
return;
}
[database inDatabase:^(SqfliteDarwinDatabase *db) {
SqfliteMethodCallOperation* operation = [SqfliteMethodCallOperation newWithCall:call result:result];
[database dbExecute:db operation:operation];
}];
}
//
// batch
//
- (void)handleBatchCall:(FlutterMethodCall*)call result:(FlutterResult)result {
SqfliteDatabase* database = [self getDatabaseOrError:call result:result];
if (database == nil) {
return;
}
[database inDatabase:^(SqfliteDarwinDatabase *db) {
SqfliteMethodCallOperation* mainOperation = [SqfliteMethodCallOperation newWithCall:call result:result];
[database dbBatch:db operation:mainOperation];
}];
}
+ (bool)isInMemoryPath:(NSString*)path {
if ([path isEqualToString:_inMemoryPath]) {
return true;
}
return false;
}
+ (NSDictionary*)makeOpenResult:(NSNumber*)databaseId recovered:(bool)recovered recoveredInTransaction:(bool)recoveredInTransaction {
NSMutableDictionary* result = [NSMutableDictionary new];
[result setObject:databaseId forKey:_paramId];
if (recovered) {
[result setObject:[NSNumber numberWithBool:recovered] forKey:_paramRecovered];
}
if (recoveredInTransaction) {
[result setObject:[NSNumber numberWithBool:recoveredInTransaction] forKey:_paramRecoveredInTransaction];
}
return result;
}
//
// open
//
- (void)handleOpenDatabaseCall:(FlutterMethodCall*)call result:(FlutterResult)result {
NSString* path = call.arguments[_paramPath];
NSNumber* readOnlyValue = call.arguments[_paramReadOnly];
bool readOnly = [readOnlyValue boolValue] == true;
NSNumber* singleInstanceValue = call.arguments[_paramSingleInstance];
bool inMemoryPath = [SqflitePlugin isInMemoryPath:path];
// A single instance must be a regular database
bool singleInstance = [singleInstanceValue boolValue] != false && !inMemoryPath;
bool _log = sqfliteHasSqlLogLevel(logLevel);
if (_log) {
NSLog(@"opening %@ %@ %@", path, readOnly ? @" read-only" : @"", singleInstance ? @"" : @" new instance");
}
// Handle hot-restart for single instance
// The dart code is killed but the native code remains
if (singleInstance) {
@synchronized (self.mapLock) {
SqfliteDatabase* database = self.singleInstanceDatabaseMap[path];
if (database != nil) {
// Check if openedŸ
if (_log) {
NSLog(@"re-opened %@singleInstance %@ id %@", database.inTransaction ? @"(in transaction) ": @"", path, database.databaseId);
}
result([SqflitePlugin makeOpenResult:database.databaseId recovered:true recoveredInTransaction:database.inTransaction]);
return;
}
}
}
// Make sure the directory exists
if (!inMemoryPath && !readOnly) {
NSError* error;
NSString* parentDir = [path stringByDeletingLastPathComponent];
if (![[NSFileManager defaultManager] fileExistsAtPath:parentDir]) {
if (_log) {
NSLog(@"Creating parent dir %@", parentDir);
}
[[NSFileManager defaultManager] createDirectoryAtPath:parentDir withIntermediateDirectories:YES attributes:nil error:&error];
// Ingore the error, it will break later during open
}
}
SqfliteDarwinDatabaseQueue *queue = [SqfliteDarwinDatabaseQueue databaseQueueWithPath:path flags:(readOnly ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE))];
bool success = queue != nil;
if (!success) {
NSLog(@"Could not open db.");
result([FlutterError errorWithCode:SqliteErrorCode
message:[NSString stringWithFormat:@"%@ %@", _errorOpenFailed, path]
details:nil]);
return;
}
// First call will be to prepare the database.
// We turn on extended result code, allowing failure
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[queue inDatabase:^(SqfliteDarwinDatabase *db) {
sqlite3_extended_result_codes(db.sqliteHandle, 1);
}];
});
NSNumber* databaseId;
@synchronized (self.mapLock) {
SqfliteDatabase* database = [SqfliteDatabase new];
databaseId = [NSNumber numberWithInteger:++_lastDatabaseId];
database.inTransaction = false;
database.fmDatabaseQueue = queue;
database.singleInstance = singleInstance;
database.databaseId = databaseId;
database.path = path;
database.logLevel = logLevel;
self.databaseMap[databaseId] = database;
// To handle hot-restart recovery
if (singleInstance) {
self.singleInstanceDatabaseMap[path] = database;
}
if (_databaseOpenCount++ == 0) {
if (sqfliteHasVerboseLogLevel(logLevel)) {
NSLog(@"Creating operation queue");
}
}
}
result([SqflitePlugin makeOpenResult: databaseId recovered:false recoveredInTransaction:false]);
}
//
// close
//
- (void)handleCloseDatabaseCall:(FlutterMethodCall*)call result:(FlutterResult)result {
SqfliteDatabase* database = [self getDatabaseOrError:call result:result];
if (database == nil) {
return;
}
if (sqfliteHasSqlLogLevel(database.logLevel)) {
NSLog(@"closing %@", database.path);
}
[self closeDatabase:database callback:^(){
// We are in a background thread here.
// resut itself is a wrapper posting on the main thread
result(nil);
}];
}
//
// close action
//
// The callback will be called from a background thread
//
- (void)closeDatabase:(SqfliteDatabase*)database callback:(void(^)(void))callback {
if (sqfliteHasSqlLogLevel(database.logLevel)) {
NSLog(@"closing %@", database.path);
}
@synchronized (self.mapLock) {
[self.databaseMap removeObjectForKey:database.databaseId];
if (database.singleInstance) {
[self.singleInstanceDatabaseMap removeObjectForKey:database.path];
}
if (--_databaseOpenCount == 0) {
if (sqfliteHasVerboseLogLevel(logLevel)) {
NSLog(@"No more databases open");
}
}
}
SqfliteDarwinDatabaseQueue* queue = database.fmDatabaseQueue;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// It is safe to call this from a background queue because the function
// dispatches immediately to its queue synchronously.
[queue close];
// TODO(gaaclarke): Remove this dispatch once the minimum Flutter value is set to 3.0.
// See also: https://github.com/flutter/flutter/issues/91635
callback();
});
}
- (void)deleteDatabaseFile:(NSString*)path {
bool _log = sqfliteHasSqlLogLevel(logLevel);
if (_log) {
NSLog(@"Deleting %@", path);
}
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
}
}
//
// delete
//
- (void)handleDeleteDatabaseCall:(FlutterMethodCall*)call result:(FlutterResult)result {
NSString* path = call.arguments[_paramPath];
bool _log = sqfliteHasSqlLogLevel(logLevel);
// Handle hot-restart for single instance
// The dart code is killed but the native code remains
SqfliteDatabase* database = nil;
@synchronized (self.mapLock) {
database = self.singleInstanceDatabaseMap[path];
if (database != nil) {
// Check if openedŸ
if (_log) {
NSLog(@"Deleting opened %@ id %@", path, database.databaseId);
}
}
}
if (database != nil) {
[self closeDatabase:database callback:^() {
// We are in a background thread here.
// resut itself is a wrapper posting on the main thread
[self deleteDatabaseFile:path];
result(nil);
}];
} else {
[self deleteDatabaseFile:path];
result(nil);
}
}
- (bool)databaseExists:(NSString*)path {
bool _log = sqfliteHasSqlLogLevel(logLevel);
if (_log) {
NSLog(@"databaseExists %@", path);
}
return ([[NSFileManager defaultManager] fileExistsAtPath:path]);
}
//
// exists
//
- (void)handleDatabaseExistsCall:(FlutterMethodCall*)call result:(FlutterResult)result {
NSString* path = call.arguments[_paramPath];
NSNumber* existsResult =[NSNumber numberWithBool:[self databaseExists: path]];
result(existsResult);
}
//
// debug
//
- (void)handleDebugCall:(FlutterMethodCall*)call result:(FlutterResult)result {
NSMutableDictionary* info = [NSMutableDictionary new];
NSString* cmd = call.arguments[_paramCmd];
// NSLog(@"cmd %@", cmd);
if ([_paramCmdGet isEqualToString:cmd]) {
@synchronized (self.mapLock) {
if ([self.databaseMap count] > 0) {
NSMutableDictionary* dbsInfo = [NSMutableDictionary new];
[self.databaseMap enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, SqfliteDatabase * _Nonnull db, BOOL * _Nonnull stop) {
NSMutableDictionary* dbInfo = [NSMutableDictionary new];
[dbInfo setObject:db.path forKey:_paramPath];
[dbInfo setObject:[NSNumber numberWithBool:db.singleInstance] forKey:_paramSingleInstance];
if (db.logLevel > sqfliteLogLevelNone) {
[dbInfo setObject:[NSNumber numberWithInteger:db.logLevel ] forKey:_paramLogLevel];
}
[dbsInfo setObject:dbInfo forKey:[key stringValue]];
[info setObject:dbsInfo forKey:_paramDatabases];
}];
}
}
if (logLevel > sqfliteLogLevelNone) {
[info setObject:[NSNumber numberWithInteger:logLevel] forKey:_paramLogLevel];
}
}
result(info);
}
//
// debug mode - trying deprecation since 1.1.6
//
- (void)handleDebugModeCall:(FlutterMethodCall*)call result:(FlutterResult)result {
NSNumber* on = (NSNumber*)call.arguments;
bool _log = [on boolValue];
NSLog(@"Debug mode %d", _log);
_extra_log = __extra_log && _log;
if (_log) {
if (_extra_log) {
logLevel = sqfliteLogLevelVerbose;
} else {
logLevel = sqfliteLogLevelSql;
}
} else {
logLevel = sqfliteLogLevelNone;
}
result(nil);
}
//
// Options
//
- (void)handleOptionsCall:(FlutterMethodCall*)call result:(FlutterResult)result {
NSNumber* logLevelNumber = call.arguments[_paramLogLevel];
if (logLevelNumber) {
logLevel = [logLevelNumber intValue];
NSLog(@"Sqflite: logLevel %d", logLevel);
}
result(nil);
}
//
// getDatabasesPath
// returns the Documents directory on iOS
//
- (void)handleGetDatabasesPath:(FlutterMethodCall*)call result:(FlutterResult)result {
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
result(paths.firstObject);
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
#if !TARGET_OS_IPHONE
// result wrapper to post the result on the main thread
// until background threads are supported for plugin services
result = ^(id res) {
dispatch_async(dispatch_get_main_queue(), ^{
result(res);
});
};
#endif
if ([_methodGetPlatformVersion isEqualToString:call.method]) {
#if TARGET_OS_IPHONE
result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
#else
result([@"macOS " stringByAppendingString:[[NSProcessInfo processInfo] operatingSystemVersionString]]);
#endif
} else if ([_methodOpenDatabase isEqualToString:call.method]) {
[self handleOpenDatabaseCall:call result:result];
} else if ([SqfliteMethodInsert isEqualToString:call.method]) {
[self handleInsertCall:call result:result];
} else if ([SqfliteMethodQuery isEqualToString:call.method]) {
[self handleQueryCall:call result:result];
} else if ([SqfliteMethodUpdate isEqualToString:call.method]) {
[self handleUpdateCall:call result:result];
} else if ([SqfliteMethodExecute isEqualToString:call.method]) {
[self handleExecuteCall:call result:result];
} else if ([_methodBatch isEqualToString:call.method]) {
[self handleBatchCall:call result:result];
} else if ([_methodQueryCursorNext isEqualToString:call.method]) {
[self handleQueryCursorNextCall:call result:result];
} else if ([_methodGetDatabasesPath isEqualToString:call.method]) {
[self handleGetDatabasesPath:call result:result];
} else if ([_methodCloseDatabase isEqualToString:call.method]) {
[self handleCloseDatabaseCall:call result:result];
} else if ([_methodDeleteDatabase isEqualToString:call.method]) {
[self handleDeleteDatabaseCall:call result:result];
} else if ([_methodDatabaseExists isEqualToString:call.method]) {
[self handleDatabaseExistsCall:call result:result];
} else if ([_methodOptions isEqualToString:call.method]) {
[self handleOptionsCall:call result:result];
} else if ([_methodDebug isEqualToString:call.method]) {
[self handleDebugCall:call
result:result];
} else if ([_methodDebugMode isEqualToString:call.method]) {
[self handleDebugModeCall:call
result:result];
} else {
result(FlutterMethodNotImplemented);
}
}
@end
If you are using FMDB in your project, I'd love to hear about it. Let Gus know
by sending an email to gus@flyingmeat.com.
And if you happen to come across either Gus Mueller or Rob Ryan in a bar, you
might consider purchasing a drink of their choosing if FMDB has been useful to
you.
Finally, and shortly, this is the MIT License.
Copyright (c) 2008-2014 Flying Meat Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
\ No newline at end of file
# Darwin implementation of sqflite plugin.
Initial implementation was using a subset of FMDB available through Cocoapods. FMDB
cocoapod FMDB version has been stuck at 2.7.5 for a long time and my iOS/MacOS is limited
to propose a proper PR to FMDB and take ownership of the FMDB pod.
As of sqflite 2.3.2-1 (2024/01/27), I made the decision to fork/copy FMDB and use it directly in sqflite assuming [FMDB_LICENSE](FMDB_LICENSE.txt)
MIT license allows it.
Only what is necessary has been copied and renamed to avoid name collision (`FMDB` -> `SqfliteDarwin`).
`flutter clean` and deleting the `Podfile.lock` is required to force the update of the FMDB dependency.
I hope everyone is OK with this decision. I'm open to any suggestion or bug report if I missed something.
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>
\ No newline at end of file
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'sqflite'
s.version = '0.0.3'
s.summary = 'SQLite plugin.'
s.description = <<-DESC
Access SQLite database.
DESC
s.homepage = 'https://github.com/tekartik/sqflite'
s.license = { :file => '../LICENSE' }
s.author = { 'Tekartik' => 'alex@tekartik.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.public_header_files = 'Classes/**/*.h'
s.ios.dependency 'Flutter'
s.osx.dependency 'FlutterMacOS'
s.ios.deployment_target = '12.0'
s.osx.deployment_target = '10.14'
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.resource_bundles = {'sqflite_darwin_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
end
## Conflict Algorithm
The APIs insert and update have a optional params conflictAlgorithm to explicit what to do in these cases.
The options of ConflictAlgorithm are:
* rollback => When a constraint violation occurs, an immediate ROLLBACK occurs, thus ending the current transaction, and the command aborts with a return code of SQLITE_CONSTRAINT. If no transaction is active (other than the implied transaction that is created on every command) then this algorithm works the same as ABORT.
* abort => When a constraint violation occurs,no ROLLBACK is executed so changes from prior commands within the same transaction are preserved. This is the default behavior.
* fail => When a constraint violation occurs, the command aborts with a return code SQLITE_CONSTRAINT. But any changes to the database that the command made prior to encountering the constraint violation are preserved and are not backed out.
* ignore => When a constraint violation occurs, the one row that contains the constraint violation is not inserted or changed. But the command continues executing normally. Other rows before and after the row that contained the constraint violation continue to be inserted or updated normally. No error is returned.
* replace => When a UNIQUE constraint violation occurs, the pre-existing rows that are causing the constraint violation are removed prior to inserting or updating the current row. Thus the insert or update always occurs. The command continues executing normally. No error is returned. If a NOT NULL constraint violation occurs, the NULL value is replaced by the default value for that column. If the column has no default value, then the ABORT algorithm is used. If a CHECK constraint violation occurs then the IGNORE algorithm is used. When this conflict resolution strategy deletes rows in order to satisfy a constraint, it does not invoke delete triggers on those rows. This behavior might change in a future release.
Examples:
```sql
CREATE TABLE Product (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT)
```
```sql
CREATE TABLE ProductImage (
productId INTEGER PRIMARY KEY,
imageUrl TEXT)
```
**Case Simple without Error:**
```dart
int recordId = await db.insert('Product', {'title': 'Example 1'});
await db.insert('ProductImage', {'productId': recordId, 'imageUrl': 'someUrlHere'});
```
**Case Simple with Error:**
The second insert to productImage will throw error.
```dart
int recordId = await db.insert('Product', {'title': 'Example 1'});
await db.insert('ProductImage', {'productId': recordId, 'imageUrl': 'someUrlHere'});
await db.insert('ProductImage', {'productId': recordId, 'imageUrl': 'someUrlHere'});
```
**Case Simple without Error but with ConflictAlgorithm:**
The second insert to productImage will replace the first insert.
```dart
int recordId = await db.insert('Product', {'title': 'Example 1'});
await db.insert('ProductImage', {'productId': recordId, 'imageUrl': 'someUrlHere'});
await db.insert('ProductImage', {'productId': recordId, 'imageUrl': 'someUpdatedUrlHere'}, conflictAlgorithm: ConflictAlgorithm.replace);
```
\ No newline at end of file
# Deleting a database
While you might be enclined to simply delete the file, you should however use
`deleteDatabase` to properly delete a database.
```dart
# Do not call File.delete, it will not work in a hot restart scenario
await File(path).delete();
# Instead do
await deleteDatabase(path);
```
* it will properly close any existing database connection
* it will properly handle the hot-restart scenario which put `SQLite` in a
weird state (basically the 'dart' side think the database is closed while
the database is in fact open on the native side)
If you call `File.delete`, while you might think it work (i.e. the file does not
exist anymore), since the database might still be opened in a hot restart scenario
the next open will re-use the open connection and at some point will get written
with the old data and `onCreate` will not be called the next time you open
the database.
\ No newline at end of file
# Desktop support
`sqflite` only includes native Android, iOS and MacOS support.
Desktop support is provided by [`sqflite_common_ffi`](https://pub.dev/packages/sqflite_common_ffi).
Since support is provided both on flutter and on DartVM on MacOS/Linux/Windows, it is not a flutter plugin.
See also some notes about how to keep you sqflite code as is and [supports Windows and Linux](https://github.com/tekartik/sqflite/blob/master/sqflite_common_ffi/doc/using_ffi_instead_of_sqflite.md)
\ No newline at end of file
# Dev tips
## Debugging
Unfortunately at this point, we cannot use sqflite in unit test.
Here are some debugging tips when you encounter issues:
### Try the experimental Logger
**Experimental feature**
The easiest is to wrap the factory you are using with `SqfliteDatabaseFactoryLogger`
```dart
import 'package:sqflite_common/sqflite_logger.dart';
Future<void> main() async {
var factoryWithLogs = SqfliteDatabaseFactoryLogger(databaseFactory,
options: SqfliteLoggerOptions(
type: SqfliteDatabaseFactoryLoggerType.all));
var db = await factoryWithLogs.openDatabase(inMemoryDatabasePath,
options: OpenDatabaseOptions(
version: 1,
onCreate: (db, _) {
db.execute('''
CREATE TABLE Product (
id TEXT PRIMARY KEY,
title TEXT
)''');
}));
await db.close();
}
```
The code above should print something like:
```
openDatabase:({path: :memory:, options: {readOnly: false, singleInstance: true, version: 1}, sw: 0:00:00.009744})
query(query:({db: 1, sql: PRAGMA user_version, result: [{user_version: 0}], sw: 0:00:00.006656}))
execute(execute:({db: 1, sql: BEGIN EXCLUSIVE, result: {transactionId: 1}, sw: 0:00:00.001008}))
query(query:({db: 1, txn: 1, sql: PRAGMA user_version, result: [{user_version: 0}], sw: 0:00:00.000166}))
execute(execute:({db: 1, txn: 1, sql: CREATE TABLE Product (
id TEXT PRIMARY KEY,
title TEXT
), sw: 0:00:00.000228}))
execute(execute:({db: 1, txn: 1, sql: PRAGMA user_version = 1, sw: 0:00:00.000057}))
execute(execute:({db: 1, txn: 1, sql: COMMIT, sw: 0:00:00.000138}))
closeDatabase:({db: 1, sw: 0:00:00.001952})
```
The logger allows for a callback to choose how to keep/display the logs.
A quick way to enable logging with a warning can be done using:
```dart
var factoryWithLogs = factory.debugQuickLoggerWrapper();
```
Or if using sqflite default factory, you can enable global logging using:
```dart
databaseFactory = databaseFactory.debugQuickLoggerWrapper();
```
### Turn on SQL console logging (old)
Temporarily turn on SQL logging on the console by adding the following call in your code before opening the first database
````dart
import 'package:sqflite_common/sqflite_dev.dart';
import 'package:sqflite/sqflite.dart';
Future<void> main() async {
// Turn logging on
await databaseFactory.setLogLevel(sqfliteLogLevelVerbose);
}
````
This call is `deprecated` on purpose to prevent keeping it in your app
### List existing tables
This will print all existing tables, views, index, trigger and their schema (`CREATE` statement).
You might see some system table (`sqlite_sequence` as well as `android_metadata` on Android)
````dart
print(await db.query("sqlite_master"));
````
### Dump a table content
you can simply dump an existing table content:
````dart
print(await db.query("my_table"));
````
## Unit tests
Errors in SQL statement are sometimes hard to debug, especially during migration where the status/schema
of the database can change.
As much as you can, try to extract your database logic using an abstract databaseFactory and database path
to allow unit tests using FFI during development:
Setup in `pubspec.yaml`:
```yaml
dev_dependencies:
sqflite_common_ffi:
```
```dart
import 'package:sqflite_common/sqlite_api.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:test/test.dart';
void main() {
// Init ffi loader if needed.
sqfliteFfiInit();
test('MyUnitTest', () async {
var factory = databaseFactoryFfi;
var db = await factory.openDatabase(inMemoryDatabasePath);
// Should fail table does not exists
try {
await db.query('Test');
} on DatabaseException catch (e) {
// no such table: Test
expect(e.isNoSuchTableError('Test'), isTrue);
print(e.toString());
}
// Ok
await db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY)');
await db.execute('ALTER TABLE Test ADD COLUMN name TEXT');
// should succeed, but empty
expect(await db.query('Test'), []);
await db.close();
});
}
```
## Extract SQLite database on Android
In Android Studio (> 3.0.1)
* Open `Device File Explorer via View > Tool Windows > Device File Explorer`
* Go to `data/data/<package_name>/databases`, where `<package_name>` is the name of your package.
Location might depends how the path was specified (assuming here that are using `getDatabasesPath` to get its base location)
* Right click on the database and select Save As.... Save it anywhere you want on your PC.
## Enable WAL on Android
WAL is disabled by default on Android. Since sqflite v2.0.4-dev.1 You can turn it on by declaring the
following in you app manifest (in the application object):
```xml
<application>
...
<!-- Enable WAL -->
<meta-data
android:name="com.tekartik.sqflite.wal_enabled"
android:value="true" />
...
</application>
```
Alternatively a more conservative (multiplatform) way is to call during onConfigure:
```db
await db.rawQuery('PRAGMA journal_mode=WAL')
```
As reported [here](https://github.com/tekartik/sqflite/issues/929) on sqflite Android the following (which should be the correct statement fails requiring to use rawQuery instead)
```db
await db.execute('PRAGMA journal_mode=WAL')
```
## setLocale on Android
Android has a specific setLocale API that allows sorting localized field according to a locale using query like:
```sql
SELECT * FROM Test ORDER BY name COLLATE LOCALIZED ASC
```
There is an extra Android only API to specify the locale to use:
```dart
await database.setLocale('fr-FR');
```
This API must be called during onConfigure (each time you open the database). The specified IETF BCP 47 language tag
string (en-US, zh-CN, fr-FR, zh-Hant-TW, ...) must be as defined in
`Locale.forLanguageTag` in Android/Java documentation.
```dart
var db = await openDatabase(path,
onConfigure: (db) async {
await db.androidSetLocale('zh-CN');
},
version: 1,
onCreate: (db, v) async {
await db.execute('CREATE TABLE Test(name TEXT)');
});
// Localized sorting.
var result = await db.query('Test', orderBy: 'name COLLATE LOCALIZED ASC'));
```
\ No newline at end of file
# Encryption support
Encryption is supported on Android, iOS and MacOS support using [`sqflite_sqlcipher`](https://pub.dev/packages/sqflite_sqlcipher)
by David Martos which has some shared code through `sqflite_common` package.
On desktop, encryption is provided by [`sqflite_common_ffi`](https://pub.dev/packages/sqflite_common_ffi).
Since support is provided both on flutter and on DartVM on MacOS/Linux/Windows, it is not a flutter plugin.
See [here](https://github.com/tekartik/sqflite/blob/master/sqflite_common_ffi/doc/encryption_support.md) for more information
of how encryption is supported on Desktop.
# External documentation and tutorials
List of external documentation and tutorials from other sources (blogs, videos). If you have
some relevant links to add, please submit a PR or propose the link [here](https://github.com/tekartik/sqflite/issues/122)
## Cookbook
* [Persist data with SQLite](https://flutter.dev/docs/cookbook/persistence/sqlite)
## Articles
* [Step by step explanation of SQLite using SQFlite](https://medium.com/flutter-community/using-sqlite-in-flutter-187c1a82e8b)
* [SQFlite Database in flutter](https://medium.com/@mohamedraja_77/sqflite-database-in-flutter-c0b7be83bcd2)
* [Todo App using BLoC Design Pattern with SQLite](https://medium.com/@vaygeth/reactive-flutter-todo-app-using-bloc-design-pattern-b71e2434f692)
## Videos
* [sqflite MVP app](https://www.youtube.com/watch?v=Yzfxqd9-6QY)
* [Adding SQLite to a Flutter App for Offline Support](https://www.youtube.com/watch?v=zEqH2qYs7Pg)
# Handling errors & exceptions
Like any database, there is always a risk of native exceptions, I/O corruption, race conditions, flutter platform exceptions, handled and unhandled inner exceptions.
SQLite should answer the best it can to I/O corruption and race conditions.
In theory, any inner exception should come out wrapped in a `DatabaseException` being thrown.
*Personal advice*: avoid exceptions as much as possible
## Handling error when opening the database
Unless you open as read-only, you should not expect any exception. Some people have expected I/O issues on iOS that got
resolved by themselves by restarting the app. Maybe it might be worth trying to catch this exception and try again 1 s later if you
experience this scenario (if so, please share in a github issues).
If you open without doing anything during open callback, open might succeed but the first action would fail as reported
by SQLite. One easy workaround (maybe it should be part of sqflite) would be to trigger a dummy command such as `getVersion()` in `onOpen` even in read-only mode.
During development, it is sometimes very likely that the error thrown by `openDatabase` was thrown from a call made during open callbacks (onCreate, onOpen....).
## Handling SQL error
There could be 2 types of error:
- *Syntax error*: If a SQL command could not be parsed (by `sqflite` or `SQLite`). Here there is not much you can do but looking at the logs and fixing your implementation.
- *SQLite error*: SQLite specific error (Syntax error can be a SQLite error)
- Unexpected error (I/O error, flutter platform state, sometimes impossible to recover...)
It is hard to catch error consistently on multiple platforms since most of the time we get the error as a text on the native side.
Your best bet is to try on both platform and parse the text accordingly. There are some helpers in `DatabaseException` that do something similar (very basic)
to find out what the error can be:
* `isNoSuchTableError`
* `isSyntaxError`
* `isOpenFailedError`
* `isDatabaseClosedError`
* `isReadOnlyError`
* `isUniqueConstraintError`
Improvements, tested additions are welcome.
For example if we perform the following SQL query if no table exists yet:
```dart
await db.query('Test');
// iOS: Error Domain=FMDatabase Code=19 "UNIQUE constraint failed: Test.name" UserInfo={NSLocalizedDescription=UNIQUE constraint failed: Test.name})
// Android: UNIQUE constraint failed: Test.name (code 2067))
```
This could be caught this way
```dart
try {
await db.query('Test');
} on DatabaseException catch (e) {
if (e.isNoSuchTableError()) {
// ok I knew it
}
}
```
## Strategy
One personal strategy is to avoid exceptions as much as possible. If any error occurs, it will likely be a developer
error (SQL commands malformed, invalid input), an error will be logged and it
will cancel the current transaction; good for testing and reproduce.
Most errors could be avoided however you might have indecies and a `isUniqueContraintError` could be thrown. Here as well you could decide to avoid the potential error by
reading data first and writing updates, everything in a transaction.
```dart
Future<bool> _exists(Transaction txn, Product product) async {
return firstIntValue(await txn.query('Product',
columns: ['COUNT(*)'],
where: 'id = ?',
whereArgs: [product.id])) ==
1;
}
Future _update(Transaction txn, Product product) async {
await txn.update('Product', product.toMap(),
where: 'id = ?', whereArgs: [product.id]);
}
Future _insert(Transaction txn, Product product) async {
await txn.insert('Product', product.toMap()..['id'] = product.id);
}
/// Product will saved (updated or inserted) by its id.
Future upsertRecord(Product product) async {
await db.transaction((txn) async {
if (await _exists(txn, product)) {
await _update(txn, product);
} else {
await _insert(txn, product);
}
});
}
var product = Product()
..id = 'table'
..title = 'Table';
await upsertRecord(product);
await upsertRecord(product);
// only one record should be present
```
or you could try to insert and handle a constraint error:
```dart
Future _update(Product product) async {
await db.update('Product', product.toMap(),
where: 'id = ?', whereArgs: [product.id]);
}
Future< _insert(Product product) async {
await db.insert('Product', product.toMap()..['id'] = product.id);
}
Future upsertRecord(Product product) async {
try {
await _insert(product);
} on DatabaseException catch (e) {
if (e.isUniqueConstraintError()) {
await _update(product);
}
}
}
var product = Product()
..id = 'table'
..title = 'Table';
await upsertRecord(product);
await upsertRecord(product);
// only one record should be present
```
In the last example above, there is a short race condition between _insert and _update that depending on your use, should be avoided.
## Concurrency
- Every read/write/transaction operation is protected by a global mutex on the database. Each action runs one after the other, first
action called, first ran. While it is a conservative solution, due to some
native implementation, you can design an app which remains fast.
- `transaction` handle the 'all or nothing' scenario. If one command fails (and throws an error), all other commands called during the
transaction are reverted. You can also throw an error to cancel a transaction.
## Limitations
### SQLiteBlobTooBigException (Row too big to fit into CursorWindow)
There seems to be a limit of (around) 1MB when reading on Android and iOS. I could not find a portable way to allow the developer to change
this limit.
I find the 1MB blob limit a good limitation (firestore has a similar limit) since otherwise performance would be pretty bad.
Solution: reduce the size of your blob or store your data in a external file and only save a reference to it in SQLite.
\ No newline at end of file
# Sqflite guide
* How to [Open a database](opening_db.md)
* How to [Open an asset database](opening_asset_db.md)
* Basic [SQL information](sql.md)
* How to [Delete a database](deleting_db.md)
* Solve you [build and runtime issues](troubleshooting.md)
* Some personal [usage recommendations](usage_recommendations.md)
* Some [dev tips](dev_tips.md)
* Get information about the [SQLite version](version.md)
* [Sqflite development](sqflite_dev_guide.md) guide
* [Unit testing](testing.md)
* [External](external.md) documentation and tutorials
* [Supports Windows and Linux](https://github.com/tekartik/sqflite/blob/master/sqflite_common_ffi/doc/using_ffi_instead_of_sqflite.md)
## Migration example
Here is a simple example of a database schema migration where:
* a column is added to an existing table
* a table is added
```dart
// Our database path
String path;
// Our database once opened
Database db;
```
In the examples below, `factory` can be replaced by `sqfliteDatabaseFactory` when using `sqflite`.
## 1st version
The first version creates a `Company` table with a `name` column.
```dart
/// Create tables
void _createTableCompanyV1(Batch batch) {
batch.execute('DROP TABLE IF EXISTS Company');
batch.execute('''CREATE TABLE Company (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT
)''');
}
// First version of the database
db = await factory.openDatabase(path,
options: OpenDatabaseOptions(
version: 1,
onCreate: (db, version) async {
var batch = db.batch();
_createTableCompanyV1(batch);
await batch.commit();
},
onDowngrade: onDatabaseDowngradeDelete));
```
## 2nd version
Let say we want to add a new table `Employee` with a reference to a `Company` entity.
We also want to add a new column `description` in the `Company` entity.
We handle the creation of a fresh database in `onCreate` and handle the schema migration in `onUpgrade`. Also since we
want to use foreign key constraints, we configure our access in `onConfigure`.
```dart
/// Let's use FOREIGN KEY constraints
Future onConfigure(Database db) async {
await db.execute('PRAGMA foreign_keys = ON');
}
/// Create Company table V2
void _createTableCompanyV2(Batch batch) {
batch.execute('DROP TABLE IF EXISTS Company');
batch.execute('''CREATE TABLE Company (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
description TEXT
)''');
}
/// Update Company table V1 to V2
void _updateTableCompanyV1toV2(Batch batch) {
batch.execute('ALTER TABLE Company ADD description TEXT');
}
/// Create Employee table V2
void _createTableEmployeeV2(Batch batch) {
batch.execute('DROP TABLE IF EXISTS Employee');
batch.execute('''CREATE TABLE Employee (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
companyId INTEGER,
FOREIGN KEY (companyId) REFERENCES Company(id) ON DELETE CASCADE
)''');
}
// 2nd version of the database
db = await factory.openDatabase(path,
options: OpenDatabaseOptions(
version: 2,
onConfigure: onConfigure,
onCreate: (db, version) async {
var batch = db.batch();
// We create all the tables
_createTableCompanyV2(batch);
_createTableEmployeeV2(batch);
await batch.commit();
},
onUpgrade: (db, oldVersion, newVersion) async {
var batch = db.batch();
if (oldVersion == 1) {
// We update existing table and create the new tables
_updateTableCompanyV1toV2(batch);
_createTableEmployeeV2(batch);
}
await batch.commit();
},
onDowngrade: onDatabaseDowngradeDelete));
```
You will have to restart your app when you change your application schema. Flutter Hot-reload won't work unless you properly close currently opened databases.
\ No newline at end of file
# Open an asset database
## Add the asset
* Add the asset in your file system at the root of your project. Typically
I would create an `assets` folder and put my file in it:
````
assets/examples.db
````
* Specify the asset(s) in your `pubspec.yaml` in the flutter section
````
flutter:
assets:
- assets/example.db
````
## Copy the database to your file system
Whether you want a fresh copy from the asset or always copy the asset is up to
you and depends on your usage
* are you modifying the asset database
* do you always want a fresh copy from the asset
* do you want to optimize for performance and size
### Optimizing for performance
For better performance you should copy the asset only once (the first time) then
always try to open the copy
```dart
import 'dart:typed_data';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
var databasesPath = await getDatabasesPath();
var path = join(databasesPath, "demo_asset_example.db");
// Check if the database exists
var exists = await databaseExists(path);
if (!exists) {
// Should happen only the first time you launch your application
print("Creating new copy from asset");
// Make sure the parent directory exists
try {
await Directory(dirname(path)).create(recursive: true);
} catch (_) {}
// Copy from asset
ByteData data = await rootBundle.load(url.join("assets", "example.db"));
List<int> bytes =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
// Write and flush the bytes written
await File(path).writeAsBytes(bytes, flush: true);
} else {
print("Opening existing database");
}
// open the database
var db = await openDatabase(path, readOnly: true);
```
### Optimizing for size
Even better on iOS you could write a native plugin that get the asset file path
and directly open it in read-only mode. Android does not have such ability
### Always getting a fresh copy from the asset
```dart
var databasesPath = await getDatabasesPath();
var path = join(databasesPath, "demo_always_copy_asset_example.db");
// delete existing if any
await deleteDatabase(path);
// Make sure the parent directory exists
try {
await Directory(dirname(path)).create(recursive: true);
} catch (_) {}
// Copy from asset
ByteData data = await rootBundle.load(url.join("assets", "example.db"));
List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await new File(path).writeAsBytes(bytes, flush: true);
// open the database
var db = await openDatabase(path, readOnly: true);
```
### Custom strategy
You might want to have a versioning strategy (not yet part of this project) to only copy the asset db when
it changes in the build system or might also allow the user to modify the database (in this case you must copy it
first).
#### One simple solution
Since issues like 'I updated my asset database but the app still see the old one', I propose a simple solution.
One simple solution is to uses a versioning system using an incremental number. An asset version file stores this number. When the app starts,
it checks the version file currently existing on the file system, compare it to the one in the assets and decide
to copy the asset (the db and the version file) or not.
Let's assume that you have a version file named `db_version_num.txt` in your assets folder.
The content of the file is a single line with the version number.
`db_version_num.txt`
```txt
1
```
along with the database file `my_asset_database.db`
You could have the following code to copy the asset database only if the version number is different:
```dart
/// Copy the asset database if needed and open it.
///
/// It uses an external version file to keep track of the asset version.
Future<Database> copyIfNeededAndOpenAssetDatabase(
{required String databasesPath,
required String versionNumFilename,
required String dbFilename}) async {
var dbPath = join(databasesPath, dbFilename);
// First check the currently installed version
var versionNumFile = File(join(databasesPath, versionNumFilename));
var existingVersionNum = 0;
if (versionNumFile.existsSync()) {
existingVersionNum = int.parse(await versionNumFile.readAsString());
}
// Read the asset version
var assetVersionNum = int.parse(
(await rootBundle.loadString(url.join('assets', versionNumFilename))).trim());
// Compare them.
print('existing/asset: $existingVersionNum/$assetVersionNum');
// If needed, copy the asset database
if (existingVersionNum < assetVersionNum) {
print('copying new version $assetVersionNum');
// Make sure the parent directory exists
try {
await Directory(databasesPath).create(recursive: true);
} catch (_) {}
// Copy from asset
var data = await rootBundle.load(url.join('assets', dbFilename));
var bytes = Uint8List.sublistView(data);
// Write and flush the database bytes written
await File(dbPath).writeAsBytes(bytes, flush: true);
// Write and flush the version file
await versionNumFile.writeAsString('$assetVersionNum', flush: true);
}
var db = await openDatabase(dbPath);
return db;
}
```
You can then call this function to open the database:
```dart
var db = await copyIfNeededAndOpenAssetDatabase(
databasesPath: await getDatabasesPath(),
// The asset database filename.
dbFilename: 'my_asset_database.db',
// The version num.
versionNumFilename: 'db_version_num.txt');
```
When you want to force updating the asset database, you can simply increment the number in the version file (2, 3...).
### Web support
Check [opening_asset_db_web.md](../../packages_web/sqflite_common_ffi_web/doc/opening_asset_db_web.md) for web support.
## Open it!
````
// open the database
Database db = await openDatabase(path);
````
# Opening a database
## finding a location path for the database
Sqflite provides a basic location strategy using the databases path on Android and the Documents folder on iOS, as
recommended on both platform. The location can be retrieved using `getDatabasesPath`.
```dart
var databasesPath = await getDatabasesPath();
var path = join(databasesPath, dbName);
// Make sure the directory exists
try {
await Directory(databasesPath).create(recursive: true);
} catch (_) {}
```
## Opening
A SQLite database is a file in the file system identified by a path. If relative, this path is relative to the path
obtained by `getDatabasesPath()`, which is the default database directory on Android and the documents directory on iOS.
```dart
var db = await openDatabase('my_db.db');
```
## Read-write
Opening a database in read-write mode is the default. One can specify a version to perform
migration strategy, can configure the database and its version.
### Configuration
`onConfigure` is the first optional callback called. It allows to perform database initialization
such as supporting cascade delete
```dart
_onConfigure(Database db) async {
// Add support for cascade delete
await db.execute("PRAGMA foreign_keys = ON");
}
var db = await openDatabase(path, onConfigure: _onConfigure);
```
### Preloading data
You might want to preload you database when opened the first time. You can either
* [Import an existing SQLite file](opening_asset_db.md) checking first whether the database file exists or not
* Populate data during `onCreate`:
```dart
_onCreate(Database db, int version) async {
// Database is created, create the table
await db.execute(
"CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)");
// populate data
await db.insert(...);
}
// Open the database, specifying a version and an onCreate callback
var db = await openDatabase(path,
version: 1,
onCreate: _onCreate);
```
### Migration
To handle database upgrades (schema changes), there is a basic version mechanism
similar to the Android API. While `getVersion` and `setVersion` are exposed,
there should not be used and instead, migrations should be performed when opening
the database.
`onCreate`, `onUpgrade`, and `onDowngrade` are called when a `version` is
specified. If the database does not exist, `onCreate` is called. If `onCreate`
is not defined, `onUpgrade` is called instead with `oldVersion` having value 0.
If the database exists and the new version is higher than the current version,
`onUpgrade` is called. Inversely, if the new version is lower than the current
version, `onDowngrade` is called. Try to avoid this by always incrementing the
database version. For the downgrade case, a special `onDatabaseDowngradeDelete`
callback exist that will simply delete the database and call `onCreate` to
create it.
These 3 callbacks are called within a transaction just before the version is set on the database.
```dart
_onCreate(Database db, int version) async {
// Database is created, create the table
await db.execute(
"CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)");
}
_onUpgrade(Database db, int oldVersion, int newVersion) async {
// Database version is updated, alter the table
await db.execute("ALTER TABLE Test ADD name TEXT");
}
// Special callback used for onDowngrade here to recreate the database
var db = await openDatabase(path,
version: 1,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
onDowngrade: onDatabaseDowngradeDelete);
```
See a [complete migration example](migration_example.md)
### Post open callback
For convenience, `onOpen` is called after the database version is set and before `openDatabase` returns.
```dart
_onOpen(Database db) async {
// Database is open, print its version
print('db version ${await db.getVersion()}');
}
var db = await openDatabase(
path,
onOpen: _onOpen,
);
```
## Read-only
```dart
// open the database in read-only mode
var db = await openReadOnlyDatabase(path);
```
## Handle corruption
Android and iOS handles corruption in a different way:
* on iOS, it fails on first access to the database
* on Android, the existing file is removed.
I don't know yet how to make it consistent without breaking the existing behavior.
It seems that one way to check if a file is a valid database file is to open it in read-only
and check its version (i.e. sqlite/iOS fails un-consistently on first access of a non-sqlite database).
Before making this a top-level function, more tests would be needed to validate the behavior.
```dart
/// Check if a file is a valid database file
///
/// An empty file is a valid empty sqlite file
Future<bool> isDatabase(String path) async {
Database db;
bool isDatabase = false;
try {
db = await openReadOnlyDatabase(path);
int version = await db.getVersion();
if (version != null) {
isDatabase = true;
}
} catch (_) {} finally {
await db?.close();
}
return isDatabase;
}
```
## Prevent database locked issue
It is strongly suggested to open a database only once. By default a database is open as
a single instance (`singleInstance: true`). i.e. re-opening the same file is safe and it
will give you the same database.
If you open the same database multiple times using `singleInstance: false`, you might encounter (at least on Android):
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
Let's consider the following helper class
```dart
class Helper {
final String path;
Helper(this.path);
Database _db;
Future<Database> getDb() async {
if (_db == null) {
_db = await openDatabase(path);
}
return _db;
}
}
```
Since `openDatabase` is async, there is a race condition risk where openDatabase
might be called twice. You could fix this with the following:
```dart
class Helper {
final String path;
Helper(this.path);
Future<Database> _db;
Future<Database> getDb() {
if (_db == null) {
_db = openDatabase(path);
}
return _db;
}
}
```
If you have some lenghty operations after `openDatabase` before considering it ready for the user
you should protect your code (put here in a private `_initDb()` method from concurrent access:
```dart
class Helper {
final String path;
Helper(this.path);
Future<Database> _db;
Future<Database> getDb() {
_db ??= _initDb();
return _db;
}
// Guaranteed to be called only once.
Future<Database> _initDb() async {
final db = await openDatabase(this.path);
// do "tons of stuff in async mode"
return db;
}
}
```
## Solving exceptions
If you get exception when opening a database:
- check the [troubleshooting](troubleshooting.md) section
- Make sure the directory where you create the database exists
- Make sure the database path points to an existing database (or nothing) and
not to a file which is not a sqlite database
- Handle any expected exception in the open callbacks (onCreate/onUpgrade/onConfigure/onOpen)
# Some perf experiment
Nexus 5, Android 6. 2019/02/26
## Background thread priority (new default)
```
TEST Running Perf 1000 insert
1000 insert 0:00:01.461457
TEST Done Perf 1000 insert
```
```
TEST Running Perf 10000 item
sw 0:00:03.638205 insert 10000 items batch
sw 0:00:00.483061 SELECT * From Test : 10000 items
sw 0:00:00.521089 SELECT * FROM Test WHERE name LIKE %item% 10000 items
sw 0:00:00.011873 SELECT * FROM Test WHERE name LIKE %dummy% 0 items
```
## Normal thread priority
```
TEST Done Perf 10000 item
TEST Running Perf android NORMAL_PRIORITY
1000 insert 0:00:01.171613
```
```
sw 0:00:03.583681 insert 10000 items batch
sw 0:00:00.408970 SELECT * From Test : 10000 items
sw 0:00:00.426629 SELECT * FROM Test WHERE name LIKE %item% 10000 items
sw 0:00:00.012783 SELECT * FROM Test WHERE name LIKE %dummy% 0 items
TEST Done Perf android NORMAL_PRIORITY
```
\ No newline at end of file
## Cross-platform support
Cross platform support is a bit weird, sorry about that. The `sqflite` package,
implemented as a "classic" flutter plugin supports only Android, iOS and MacOS.
Desktop support (Linux, Windows) is provided by the `sqflite_common_ffi` package.
It is a dart package so it also works on the dart VM (command line) so it is not
a flutter plugin. Its implementation uses ffi. This implementation can also
support Android, iOS and MacOS but it is not a flutter plugin.
Experimental web support is provided by the `sqflite_common_ffi_web` package.
> Is there a plan to publish a single federated package?
Not at this time. Some might prefer `sqflite_common_ffi` over `sqflite`. The first
giving you access to the latest sqlite3 version and the second giving a smaller binary size.
Some wants encryption support. It is hard to make the proper choice for everyone.
As a side note, I find it weird when creating a linux only flutter application, that
my app has to download the `win32` package. Currently there is no way to limit dependencies
to only the platform you are supporting. More dependencies means more chance to have breaking bugs
that you cannot fix.
I would even be more inclined to publish a separate packages for each platform that people have to include
one by one for the platforms they want.
Also I'm a freelance developer, maintaining it in my free-time so I don't always have time and
the energy to support it (since 2017!) in the best way. Documentation is poor, I know. I'm not a native english speaker.
Sometimes issues like this one bring me the opportunity to improve the documentation so I will
likely copy this response to the documentation.
# Development guide
## Check list
* run test
* no warning
* string mode / implicit-casts: false
* run the example
## Publishing
flutter packages pub publish
## Testing
### Using `test_driver`
Check [sqflite_test_app](../../sqflite_test_app/README.md).
Also, from the `example` folder, you should be able to run some native tests using:
flutter driver test_driver/main.dart
### Github Branches
#### develop
Development is done on the develop branch.
[![pub package](https://img.shields.io/pub/vpre/sqflite.svg)](https://pub.dev/packages/sqflite)
#### master
Published version are merged on master.
[![pub package](https://img.shields.io/pub/v/sqflite.svg)](https://pub.dev/packages/sqflite)
# SQL
As sqflite does not do any parsing of SQL commands, its usage is similar to
the usage on the native iOS and Android platform so you can refer to their
respective documentation as well as the generic sqlite documentation:
- Android: https://developer.android.com/training/data-storage/sqlite
- iOS (FMDB): https://github.com/ccgus/fmdb
- sqlite: https://www.sqlite.org/index.html
The API is relatively close to the Android one. For performance and compatibility reason,
cursors are not supported at this time.
It is impossible here to make a full documentation of SQL. Only basic information is given
and common pitfalls.
## Basic usage
### execute
`execute` is for commands without return values.
```dart
// Create a table
await db.execute('CREATE TABLE my_table (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, type TEXT)');
```
### insert
`insert` is for inserting data into a table. It returns the internal id of the record (an integer).
```dart
int recordId = await db.insert('my_table', {'name': 'my_name', 'type': 'my_type'});
```
See [Conflict algorithm](conflict_algorithm.md) for conflict handling.
### query
`query` is for reading a table content. It returns a list of map.
```dart
var list = await db.query('my_table', columns: ['name', 'type']);
```
The list is read-only. If you want to modify the results by adding/deleting items in memory,
you need to clone the list:
```dart
// This throws an error
list.add(<String, Object?>{'name': 'some data'});
```
```dart
// This works
list = List.from(list)
list.add(<String, Object?>{'name': 'some data'});
```
Each item (map) of the list is read-only too so you need to clone it if you want to modify the result.
```dart
map = list.first;
// This crashes
map['name'] = 'other';
```
```dart
// This works
map = Map.from(map);
map['name'] = 'other';
```
#### Query by page
If you perform a query on a huge table, you might want to avoid allocating all the rows at once.
There is a basic cursor support where you can specify the buffer size (number of rows cached using a look-ahead buffer)
```dart
// Query cursor
var cursor = await db.queryCursor(
'Product',
bufferSize: 10,
);
try {
while (await cursor.moveNext()) {
var row = cursor.current;
// ...
}
} finally {
// Important don't forget to close the cursor in case any exception is thrown before
await cursor.close();
}
```
### delete
`delete` is for deleting content in a table. It returns the number of rows deleted.
```dart
var count = await db.delete('my_table', where: 'name = ?', whereArgs: ['cat']);
```
### update
`update` is for updating content in a table. It returns the number of rows updated.
```dart
var count = await db.update('my_table', {'name': 'new cat name'}, where: 'name = ?', whereArgs: ['cat']);
```
See [Conflict algorithm](conflict_algorithm.md) for conflict handling.
### transaction
`transaction` handle the 'all or nothing' scenario. If one command fails (and throws an error), all other commands are reverted.
```dart
await db.transaction((txn) async {
await txn.insert('my_table', {'name': 'my_name'});
await txn.delete('my_table', where: 'name = ?', whereArgs: ['cat']);
});
```
* Make sure to use the inner transaction object - `txn` in the code above - is used in a transaction (using the `db` object itself will cause a deadlock),
* You can throw an error during a transaction to cancel a transaction,
* When an error is thrown during a transaction, the action is cancelled right away and previous commands in the transaction are reverted,
* No other concurrent modification on the database (even from an outside process) can happen during a transaction,
* The inner part of the transaction is called only once, it is up to the developer to handle a try-again loop - assuming it can succeed at some point.
## Parameters
When providing a raw SQL statement, you should not attempt to "sanitize" any values. Instead, you
should use the standard SQLite binding syntax:
```dart
// good
int recordId = await db.rawInsert('INSERT INTO my_table(name, year) VALUES (?, ?)', ['my_name', 2019]);
// bad
int recordId = await db.rawInsert("INSERT INTO my_table(name, year) VALUES ('my_name', 2019)");
```
The `?` character is recognized by SQLite as a placeholder for a value to be inserted.
The number of `?` characters must match the number of arguments. Arguments types must be in the list of
[supported types](supported_types.md).
Particulary, lists (expect for blob content) are not supported. A common mistake is to expect to use `IN (?)` and give a list
of values. This does not work. Instead you should list each argument one by one:
```dart
var list = await db.rawQuery('SELECT * FROM my_table WHERE name IN (?, ?, ?)', ['cat', 'dog', 'fish']);
```
Since the list size can change, having the proper number or `?` can be solved using the following solution:
```dart
List.filled(inArgsCount, '?').join(',')
```
```dart
var inArgs = ['cat', 'dog', 'fish'];
var list = await db.query('my_table',
where: 'name IN (${List.filled(inArgs.length, '?').join(',')})',
whereArgs: inArgs);
```
### Parameter position
You can use `?NNN` to specify a parameter position:
```dart
expect(
await db.rawQuery(
'SELECT ?1 as item1, ?2 as item2, ?1 + ?2 as sum', [3, 4]),
[{'item1': 3, 'item2': 4, 'sum': 7}]);
```
Be aware that Android binds argument as String. While it works in most cases (in where args), in the example above, the result
will be `[{'item1': '3', 'item2': '4', 'sum': 7}]);`. We might consider inlining num in the future.
## NULL value
`NULL` is a special value. When testing for null in a query you should not do `'WHERE my_col = ?', [null]` but use
instead `WHERE my_col IS NULL` or `WHERE my_col IS NOT NULL`.
```dart
var list = await db.query('my_table', columns: ['name'], where: 'type IS NULL');
```
# Examples
## Using `LIKE`
Look for items with `name` starting with 'Ta':
```dart
var list = await db.query('my_table', columns: ['name'], where: 'name LIKE ?', whereArgs: ['Ta%']);
```
Look for items with `name` containing with 'free':
```dart
var list = await db.query('my_table', columns: ['name'], where: 'name LIKE ?', whereArgs: ['%free%']);
```
## SQLite schema information
SQLite has a [`sqlite_master`](https://www.sqlite.org/faq.html#q7) table that store schema information:
### Check if a table exists
```dart
Future<bool> tableExists(DatabaseExecutor db, String table) async {
var count = firstIntValue(await db.query('sqlite_master',
columns: ['COUNT(*)'],
where: 'type = ? AND name = ?',
whereArgs: ['table', table]));
return count > 0;
}
```
### List table names
```dart
Future<List<String>> getTableNames(DatabaseExecutor db) async {
var tableNames = (await db
.query('sqlite_master', where: 'type = ?', whereArgs: ['table']))
.map((row) => row['name'] as String)
.toList(growable: false)
..sort();
return tableNames;
}
```
# Supported types
The API offers a way to save a record as map of type `Map<String, Object?>`. This map cannot be an
arbitrary map:
- Keys are column in a table (declared when creating the table)
- Values are field values in the record of type `num`, `String` or `Uint8List`
Nested content is not supported. For example, the following simple map is not supported:
```dart
{
"title": "Table",
"size": {"width": 80, "height": 80}
}
```
It should be flattened. One solution is to modify the map structure:
```sql
CREATE TABLE Product (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
width INTEGER,
height INTEGER)
```
```dart
{"title": "Table", "width": 80, "height": 80}
```
Another solution is to encoded nested maps and lists as json (or other format), declaring the column
as a String.
```sql
CREATE TABLE Product (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
size TEXT
)
```
```dart
{
'title': 'Table',
'size': '{"width":80,"height":80}'
};
```
## Supported SQLite types
No validity check is done on values yet so please avoid non supported types [https://www.sqlite.org/datatype3.html](https://www.sqlite.org/datatype3.html)
`DateTime` is not a supported SQLite type. Personally I store them as
int (millisSinceEpoch) or string (iso8601). SQLite `TIMESTAMP` type sometimes requires using [date functions](https://www.sqlite.org/lang_datefunc.html).
`TIMESTAMP` values are read as `String` that the application needs to parse.
`bool` is not a supported SQLite type. Use `INTEGER` and 0 and 1 values.
### INTEGER
* SQLite type: `INTEGER`
* Dart type: `int`
* Supported values: from -2^63 to 2^63 - 1
### REAL
* SQLite type: `REAL`
* Dart type: `num`
### TEXT
* SQLite type: `TEXT`
* Dart type: `String`
### BLOB
* SQLite typ: `BLOB`
* Dart type: `Uint8List`
# Unit test
Currently testing using the package `test` or `flutter_test` is not supported. Testing using sqflite requires running
on a real supported platforms. That's unfortunately an issue for all plugins where mocking cannot easily be done.
Possible alternative (not as good though) are:
## Using flutter_driver
A solution is to use flutter driver. Look at the example app:
```bash
flutter driver --target=test_driver/main.dart
```
## Using sqflite_common_ffi
This allow running unit tests using the desktop sqlite version installed. Be aware that the sqlite version used could be
different (and likely more recent).
Simple flutter test example:
```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
/// Initialize sqflite for test.
void sqfliteTestInit() {
// Initialize ffi implementation
sqfliteFfiInit();
// Set global factory
databaseFactory = databaseFactoryFfi;
}
Future main() async {
sqfliteTestInit();
test('simple', () async {
var db = await openDatabase(inMemoryDatabasePath);
await db.execute('''
CREATE TABLE Product (
id INTEGER PRIMARY KEY,
title TEXT
)
''');
await db.insert('Product', <String, Object?>{'title': 'Product 1'});
await db.insert('Product', <String, Object?>{'title': 'Product 2'});
var result = await db.query('Product');
expect(result, [
{'id': 1, 'title': 'Product 1'},
{'id': 2, 'title': 'Product 2'}
]);
await db.close();
});
}
```
More info on [sqflite_common_ffi](https://github.com/tekartik/sqflite/tree/master/sqflite_common_ffi).
### Writing widget test
There seems to be several restrictions in widget test. One solution here is to use the ffi implementation
without isolate:
```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
void main() {
// Initialize ffi implementation
sqfliteFfiInit();
// Set global factory, do not use isolate here
databaseFactory = databaseFactoryFfiNoIsolate;
testWidgets('Test sqflite database', (WidgetTester tester) async {
var db = await openDatabase(inMemoryDatabasePath, version: 1,
onCreate: (db, version) async {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)');
});
// Insert some data
await db.insert('Test', {'value': 'my_value'});
// Check content
expect(await db.query('Test'), [
{'id': 1, 'value': 'my_value'}
]);
await db.close();
});
}
```
# Troubleshooting
If you ran into build/runtime issues, please try the following:
* Update flutter to the latest version (`flutter upgrade`)
* Update sqflite dependencies to the latest version in your `pubspec.yaml`
(`sqflite >= X.Y.Z`)
* Update dependencies (`flutter packages upgrade`)
* try `flutter clean`
* Try deleting the flutter cache folder (which will
downloaded again automatically) from your flutter installation folder `$FLUTTER_TOP/bin/cache`
# Recommendations
* Enable strong-mode
* Disable implicit casts
Sample `analysis_options.yaml` file:
```
analyzer:
language:
strict-casts: true
strict-inference: true
```
# Common issues
## Cast error
```
Unhandled exception: type '_InternalLinkedHashMap' is not a subtype of type 'Map<String, Object?>'
where
_InternalLinkedHashMap is from dart:collection
Map is from dart:core
String is from dart:core
```
Make sure you create object of type `Map<String, Object?>` and not simply `Map` for records you
insert and update. The option `language: strict-casts: false` explained above helps to find such issues
## MissingPluginException
This error is typically a build/setup error after adding the dependency.
- Try all the steps defined at the top of the documents
- Make sure you stop the current running application if any (hot restart/reload won't work)
- Force a `flutter packages get`
- Try to clean your build folder `flutter clean`
- On iOS, you can try to force a `pod install` / `pod update`
- Follow the [using package flutter guide](https://flutter.dev/docs/development/packages-and-plugins/using-packages)
- Search for [other bugs in flutter](https://github.com/flutter/flutter/search?q=MissingPluginException&type=Issues)
like this, other people face the same issue with other plugins so it is likely not sqflite related
Advanced checks:
- If you are using sqflite in a FCM Messaging context, you might need
to [register the plugin earlier](https://github.com/tekartik/sqflite/issues/446).
- if the project was generated a long time ago (2019), you might have to follow
the [plugin migration guide](https://flutter.dev/docs/development/packages-and-plugins/plugin-api-migration)
- Check the GeneratedPluginRegistrant file that flutter run should have generated in your project contains
a line registering the plugin.
Android:
```java
SqflitePlugin.registerWith(registry.registrarFor("com.tekartik.sqflite.SqflitePlugin"));
```
iOS:
```objective-c
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
```
- Check MainActivity.java (Android) contains a call to
GeneratedPluginRegistrant asking it to register itself. This call should be made from the app
launch method (onCreate).
```java
public class MainActivity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
}
}
```
- Check AppDelegate.m (iOS) contains a call to
GeneratedPluginRegistrant asking it to register itself. This call should be made from the app
launch method (application:didFinishLaunchingWithOptions:).
```objective-c
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
```
- If it happens to Android release mode, make sure to [remove shrinkResources
true and minifyEnabled true lines in build.gradle](https://github.com/tekartik/sqflite/issues/452#issuecomment-655602329)
to solve the problem.
Before raising this issue, try adding another well established plugin (the simplest being
`path_provider` or `shared_preferences`) to see if you get the error here as well.
## Warning database has been locked for...
If you get this output in debug mode:
> Warning database has been locked for 0:00:10.000000. Make sure you always use the transaction object for database
> operations during a transaction
One common mistake is to use the db object in a transaction:
```dart
await
db.transaction
(
(txn) async {
// DEADLOCK HERE
await db.insert('my_table', {'name': 'my_name'});
});
```
...instead of using the correct transaction object (below named `txn`):
```dart
await
db.transaction
(
(txn) async {
// Ok!
await txn.insert('my_table', {'name': 'my_name'});
});
```
## Debugging SQL commands
A quick way to view SQL commands printed out is to call before opening any database
```dart
await
Sqflite.devSetDebugModeOn
(true);
```
This call is on purpose deprecated to force removing it once the SQL issues has been resolved.
## iOS build issue
### General
I'm not an expert on iOS so it is hard for me reply to issues you encounter especially when you integrate
into your app. Good if you can validate that you have no issue with other plugin such as `path_provider` and that
the example app work with your setup.
I test mainly with the beta channel. Good if you can try if the example work with your setup
```bash
# Switch to beta
flutter channel beta
flutter upgrade
# Get the project and build/run the example
git clone https://github.com/tekartik/sqflite.git
cd sqflite/example
flutter packages get
# build for iOS
flutter build ios
# run!
flutter run
```
If you want to use master, please try the following to see if it works with your setup
```bash
# Switch to master
flutter channel master
flutter upgrade
# Get the project and build/run the example
git clone https://github.com/tekartik/sqflite.git
cd sqflite/example
flutter packages get
# build for iOS
flutter build ios
# run!
flutter run
```
In the worst case, you can also re-create your ios project by deleting the ios/folder and running `flutter create .`
### Undefined symbols for architecture armv7
You might encounter this error on old projects or sometimes when sqflite is included by another dependency.
I have not found a solution to fix this in sqflite itself so the fix has to be done in your application Podfile
(you can just append this at the end of the `Podfile` file)
```
post_install do |installer|
installer.pods_project.build_configurations.each do |config|
config.build_settings["EXCLUDED_ARCHS"] = "armv7"
end
end
```
You might also want to enforce a SDK version, sometimes just declaring `platform :ios, '9.0'` is not sufficient. Below
is an example
```
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end
end
end
```
Since Flutter templates change over time for new sdk, you might sometimes try to delete the ios folder and re-create
your project.
### XCode 14 support
You might likely get the following error:
```
Error (Xcode): File not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a
```
See: https://developer.apple.com/forums/thread/728021
Xcode 14 only supports building for a deployment target of iOS 11.
Here as well you need to enforce the deployment target until I find a better way
as the FMDB dependency is no longer actively maintained.
In your application Podfile inside the post_install section where you have this in the
app template:
```
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
```
you need to have
(11 is used here, but you might want to specify a higher platform):
```
post_install do |installer|
installer.generated_projects.each do |project|
project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end
end
end
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
```
### Module 'FMDB' not found
Since v2.2.1-1, you might encounter `Module 'FMDB' not found` on old projects.
You need to add `use_frameworks!` in your Podfile:
```
target 'Runner' do
# Needed since v2.2.1-1
# In newly create project, this is set but it is not
# always the case on older projects
use_frameworks!
...
end
```
## Runtime exception
### Json1 extension
```
DatabaseException: DatabaseException(no such function: JSON_OBJECT)
```
I could not find the details of what each built-in version includes (for
example the version os SQLite for each Android OS version
here https://developer.android.com/reference/android/database/sqlite/package-summary)
but I doubt any of them include the json1 extension.
json1 extension requires at least of SQLite 3.38.0 (2021-02-09) (https://www.sqlite.org/json1.html)
`sqflite` uses the SQLite available on the platform. It does not ship/bundle any additional SQLite library. You can get the
version using `SELECT sqlite_version()`:
```dart
print((await db.rawQuery('SELECT sqlite_version()')).first.values.first);
```
which should give a version formatted like this:
```
3.22.0
```
Unfortunately the version of SQLite depends on the OS version.
You could get a more recent version using [`sqflite_common_ffi`](https://pub.dev/packages/sqflite_common_ffi).
You could then add [`sqlite3_flutter_libs`](https://pub.dev/packages/sqlite3_flutter_libs) for ios/android or include your own
sqlite shared library for desktop or mobile (one for each platform).
### Open error
Such error is often reported with something similar to:
```text
SqfliteFfiException(sqlite_error: 14, , open_failed: SqliteException(14): while opening the database, bad parameter or other API misuse, bad parameter or other API misuse (code 21)})
```
Please check and ensure that:
- The database path is correct (please provide it in the bug report, best is to print it out to report it exactly as it is used)
- The parent folder exists (you should create it if it does not exist)
- The parent folder is writable (try to create a file in it if you still get the error).
Solutions:
- Use the `path_provider` package to find the best location for you database (`getDatabasesPath()` is only relevant for Android)
- Build the database path properly (using `join` from the `path` package)
- Create the parent folder if it does not exist
## Error in Flutter web
Look at package [sqflite_common_ffi_web](https://pub.dev/packages/sqflite_common_ffi_web) for experimental Web support.
IndexedDB or any solution on top of it should be considered for storage on the Web.
Below are (or will be) personal recommendations on usage
## Single database connection
The API is largely inspired from Android ContentProvider where a typical SQLite implementation means
opening the database once on the first request and keeping it open.
Personally I have one global reference Database in my Flutter application to avoid lock issues. Opening the
database should be safe if called multiple times.
If you don't use `singleInstance`, keeping a reference (at the app level or in a widget) can cause issues with hot reload if the reference is lost (and the database not
closed yet).
## Isolates
Access *should* be done in the main isolate:
* sqflite native access already happens in a background native thread
* Transaction mechanism is not cross-isolate safe
* [sqflite_common_ffi](https://pub.dev/packages/sqflite_common_ffi) access is made in a separate isolate.
Many people have asked about access from another isolate and unfortunately I don't have a good answer for that.
It **should** work although I have not tested it much and I have avoided it (for example by saving the temporary data read
in a file that I handle later in the main isolate). One common usage
on Android and iOS is to have access from a push notification isolate or scheduled task.
* Good if you can avoid it and only limit to this case (background notification / scheduled task)
* In both isolates
* Use `singleInstance: false` in `openDatabase`
* Don't close the database
* Since hot reload might fail when using `singleInstance: false` (if done while a transaction is in progress), you
can try `singleInstance: true` in the main isolate only.
Some related discussions here:
* [Cannot access database instance from another Isolate](https://github.com/tekartik/sqflite/issues/186)
* [Problem tunning Sqflite in Isolate](https://github.com/tekartik/sqflite/issues/258)
* [Multi-Isolate access to Sqflite (iOS)](https://github.com/tekartik/sqflite/issues/168)
* [MissingPluginException when using sqflite via flutter_isolate](https://github.com/tekartik/sqflite/issues/169)
* [Accessing database from a background Isolate & main Isolate](https://github.com/tekartik/sqflite/issues/1118)
## Batch vs Transaction
There is always a confusion between the 2 notions, although they have a different purpose:
Transaction:
- A transaction is a SQLite concept (`BEGIN TRANSACTION`, `COMMIT`). In a transaction, you run SQL statements
as you would do normally (i.e. you await each statement) but the changes are only effective on COMMIT
- A transaction is committed if the callback does not throw an error. If an error is thrown,
the transaction is cancelled. So to rollback a transaction one way is to throw an exception.
Batch
- A batch is just a list of statement to execute all at once
- A batch on a database is run in a transaction (a transaction is created for you)
- A batch in a transaction is committed when the transaction terminates
- You can create/commit mutiple batches in a transaction however the changes are committed *for real* only when the transaction terminates (and a failure would rollback all batches)
Prefer:
- A batch if you have a list statements to execute in sequence and no query on which your behavior depends on
- Use transaction for upsert like scenario or if you want to check a query result before an insert/update or delete.
- You can use them together (i.e. a batch in a transaction.)
## Handling errors
Like any database, something wrong can happen when storing/reading data. Here are some information about [Handling errors and exceptions](handling_errors.md).
\ No newline at end of file
# SQLite version
`sqflite` uses the SQLite available on the platform. It does not ship/bundle any additional SQLite library. You can get the
version using `SELECT sqlite_version()`:
```dart
print((await db.rawQuery('SELECT sqlite_version()')).first.values.first);
```
which should give a version formatted like this:
```
3.22.0
```
Unfortunately the version of SQLite depends on the OS version.
You could get a more recent version using [`sqflite_common_ffi`](https://pub.dev/packages/sqflite_common_ffi).
You could then add [`sqlite3_flutter_libs`](https://pub.dev/packages/sqlite3_flutter_libs) for ios/android or include your own
sqlite shared library for desktop or mobile (one for each platform).
## Android
See https://developer.android.com/reference/android/database/sqlite/package-summary
| Android API | SQLite Version |
|-------------|----------------|
| API 27 | 3.19 |
| API 26 | 3.18 |
| API 24 | 3.9 |
| API 21 | 3.8 |
| API 11 | 3.7 |
## iOS
See https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS)
| iOS Version | SQLite Version |
|-------------|----------------|
| 13.1.3 | 3.28.0 |
| 12.1 | 3.24.0 |
| 11.0 | 3.19.3 |
| 10.3.1 | 3.16.0 |
| 9.3.1 | 3.8.10.2 |
| 8.2 | 3.8.5 |
## Mac OS
| MacOS Version | SQLite Version |
|---------------|----------------|
| 10.14.2 | 3.24.0 |
# Web support
There is an experimental Web support using [sqflite_common_ffi_web](https://pub.dev/packages/sqflite_common_ffi_web).
.DS_Store
.atom/
.idea
.packages
.pub/
build/
ios/.generated/
packages
pubspec.lock
.flutter-plugins
sqflite_example.iml
# local files
.local/
tmp/
# no project files support
/web/
/macos/
/ios/
\ No newline at end of file
# sqflite_example
Demonstrates how to use the [sqflite plugin](https://github.com/tekartik/sqflite).
## Quick test
flutter run
Specific app entry point
flutter run -t lib/main.dart
## Android
Some project files is no longer in source control but can be re-created using
flutter create --platforms android .
### Tests
cd android
# Java unit test
./gradlew test
# With a emulator running
./gradlew connectedAndroidTest
## iOS
Project files are no longer in source control but can be re-created using
flutter create --platforms ios .
## MacOS
Project files are no longer in source control but can be re-created using
flutter create --platforms macos .
flutter run -d macos
include: package:flutter_lints/flutter.yaml
# Until there are meta linter rules, each desired lint must be explicitly enabled.
# See: https://github.com/dart-lang/linter/issues/288
#
# For a list of lints, see: http://dart-lang.github.io/linter/lints/
# See the configuration guide for more
# https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer
#
# NOTE: Please keep this file in sync with
# https://github.com/flutter/flutter/blob/master/analysis_options.yaml
analyzer:
language:
strict-casts: true
strict-inference: true
errors:
# treat missing required parameters as a warning (not a hint)
missing_required_param: warning
# allow having TODOs in the code
todo: ignore
# Ignore errors like
# 'super_goes_last' is a deprecated lint rule and should not be used • included_file_warning
included_file_warning: ignore
linter:
rules:
- always_declare_return_types
- avoid_dynamic_calls
- avoid_empty_else
- avoid_relative_lib_imports
- avoid_shadowing_type_parameters
- avoid_slow_async_io
- avoid_types_as_parameter_names
- await_only_futures
- camel_case_extensions
- camel_case_types
- cancel_subscriptions
- curly_braces_in_flow_control_structures
- directives_ordering
- empty_catches
- hash_and_equals
- collection_methods_unrelated_type
- no_adjacent_strings_in_list
- no_duplicate_case_values
- non_constant_identifier_names
- omit_local_variable_types
- package_api_docs
- package_prefixed_library_names
- prefer_generic_function_type_aliases
- prefer_is_empty
- prefer_is_not_empty
- prefer_iterable_whereType
- prefer_single_quotes
- prefer_typing_uninitialized_variables
- sort_child_properties_last
- test_types_in_equals
- throw_in_finally
- unawaited_futures
- unnecessary_null_aware_assignments
- unnecessary_statements
- unrelated_type_equality_checks
- unsafe_html
- valid_regexps
- constant_identifier_names
- control_flow_in_finally
- empty_statements
- implementation_imports
- overridden_fields
- package_names
- prefer_const_constructors
- prefer_initializing_formals
- prefer_void_to_null
#
- annotate_overrides
- avoid_init_to_null
- avoid_null_checks_in_equality_operators
- avoid_return_types_on_setters
- empty_constructor_bodies
- library_names
- library_prefixes
- prefer_adjacent_string_concatenation
- prefer_collection_literals
- prefer_contains
- slash_for_doc_comments
- type_init_formals
- unnecessary_const
- unnecessary_new
- unnecessary_null_in_if_null_operators
- use_rethrow_when_possible
- dangling_library_doc_comments
- deprecated_member_use_from_same_package
- implicit_reopen
- invalid_case_patterns
- no_literal_bool_comparisons
- no_self_assignments
- no_wildcard_variable_uses
- type_literal_in_constant_pattern
# === doc rules ===
- public_member_api_docs
#
# - prefer_final_locals
- sort_constructors_first
- sort_unnamed_constructors_first
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
android {
namespace 'com.tekartik.sqfliteexample'
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.tekartik.sqfliteexample"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'androidx.test:rules:1.3.0'
}
package com.tekartik.sqflite;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class FileUtils {
public static String convertStreamToString(InputStream is) throws IOException {
// http://www.java2s.com/Code/Java/File-Input-Output/ConvertInputStreamtoString.htm
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line = null;
Boolean firstLine = true;
while ((line = reader.readLine()) != null) {
if (firstLine) {
sb.append(line);
firstLine = false;
} else {
sb.append("\n").append(line);
}
}
reader.close();
return sb.toString();
}
public static String getStringFromFile(File file) throws IOException {
FileInputStream fin = new FileInputStream(file);
String ret = convertStreamToString(fin);
//Make sure you close all streams.
fin.close();
return ret;
}
}
package com.tekartik.sqflite;
import static org.junit.Assert.assertEquals;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Locale;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class LocaleUtilsTest {
interface CheckLocaleRunnable {
void run(Locale locale);
}
private void check(String languageTag, CheckLocaleRunnable runnable) {
Locale locale = Utils.localeForLanguageTag21(languageTag);
runnable.run(locale);
locale = Utils.localeForLanguageTagPre21(languageTag);
runnable.run(locale);
locale = Utils.localeForLanguateTag(languageTag);
runnable.run(locale);
}
@Test
public void localeForLanguateTag() {
String enUsPosixTag = "en-US-x-lvariant-POSIX";
check(enUsPosixTag, locale -> {
assertEquals("en", locale.getLanguage());
assertEquals("US", locale.getCountry());
assertEquals("POSIX", locale.getVariant());
assertEquals(null, locale.getExtension('x'));
assertEquals("en-US-POSIX", locale.toLanguageTag());
assertEquals("en_US_POSIX", locale.toString());
});
String chineseTag = Locale.CHINESE.toLanguageTag();
assertEquals("zh", chineseTag);
check(chineseTag, locale -> {
assertEquals("zh", locale.getLanguage());
assertEquals("", locale.getCountry());
assertEquals("", locale.getVariant());
assertEquals("zh", locale.toLanguageTag());
assertEquals("zh", locale.toString());
});
chineseTag = Locale.SIMPLIFIED_CHINESE.toLanguageTag();
assertEquals("zh-CN", chineseTag);
check(chineseTag, locale -> {
assertEquals("zh", locale.getLanguage());
assertEquals("CN", locale.getCountry());
assertEquals("", locale.getVariant());
assertEquals("zh-CN", locale.toLanguageTag());
assertEquals("zh_CN", locale.toString());
});
String franceTag = Locale.FRANCE.toLanguageTag();
assertEquals("fr-FR", franceTag);
check(franceTag, locale -> {
assertEquals("fr", locale.getLanguage());
assertEquals("FR", locale.getCountry());
assertEquals("", locale.getVariant());
assertEquals("fr-FR", locale.toLanguageTag());
assertEquals("fr_FR", locale.toString());
});
}
}
package com.tekartik.sqflite;
import static org.junit.Assert.assertEquals;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class RawSqliteTest {
static String TAG = "SQFLTest";
@Test
public void getSetVersion() {
SQLiteDatabase db = SQLiteDatabase.createInMemory(new SQLiteDatabase.OpenParams.Builder().build());
assertEquals(0, db.getVersion());
db.setVersion(1);
assertEquals(1, db.getVersion());
db.close();
}
@Test
public void substr() {
// Issue #771
SQLiteDatabase db = SQLiteDatabase.createInMemory(new SQLiteDatabase.OpenParams.Builder().build());
Cursor cursor = db.rawQuery("SELECT substr('2022-04-01', 1, 4)", new String[0]);
cursor.moveToFirst();
assertEquals("2022", cursor.getString(0));
cursor.close();
db.close();
}
}
package com.tekartik.sqflite;
import android.content.Context;
/**
* Allow creating the object directly
*/
public class TestSqflitePlugin extends SqflitePlugin {
public TestSqflitePlugin(Context context) {
super(context);
}
}
package com.tekartik.sqflite;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class TestSqflitePluginTest {
static String TAG = "SQFLTest";
Context appContext = ApplicationProvider.getApplicationContext();
class Data {
CountDownLatch signal;
Integer id;
}
@Test
public void missingFile() {
File file = new File(appContext.getFilesDir(), "missing.db");
Database database = new Database(appContext, file.getPath(), 0, true, 0);
Exception exception = null;
try {
database.openReadOnly();
database.close();
} catch (Exception e) {
exception = e;
}
assertTrue(exception != null);
}
@Test
public void emptyFile() throws IOException {
File file = new File(appContext.getFilesDir(), "empty.db");
FileWriter fileWriter = new FileWriter(file);
fileWriter.write("");
fileWriter.close();
Database database = new Database(appContext, file.getPath(), 0, true, 0);
database.openReadOnly();
database.close();
}
@Test
public void nonSqfliteFile() throws IOException {
File file = new File(appContext.getFilesDir(), "non_sqflite_file.db");
FileWriter fileWriter = new FileWriter(file);
fileWriter.write("test");
fileWriter.close();
Database database = new Database(appContext, file.getPath(), 0, true, 0);
database.openReadOnly();
database.close();
assertEquals(FileUtils.getStringFromFile(file), "test");
}
@Test
public void databaseExists() throws IOException {
File file = new File(appContext.getFilesDir(), "exists_file.db");
try {
file.delete();
} catch (Exception e) {
}
assertEquals(false, Database.existsDatabase(file.getPath()));
Database database = new Database(appContext, file.getPath(), 0, true, 0);
try {
database.open();
database.sqliteDatabase.execSQL("PRAGMA version = 1");
} finally {
database.close();
}
}
@Test
public void walEnabled() {
// False, uncomment in manifest to check for true
assertFalse(Database.checkWalEnabled(appContext));
}
@Test
public void checkMetaBoolean() {
// False, uncomment in manifest to check for true
assertTrue(Database.checkMetaBoolean(appContext, "com.tekartik.sqfliteexample.flagtrue", false));
assertFalse(Database.checkMetaBoolean(appContext, "com.tekartik.sqfliteexample.flagfalse", false));
assertTrue(Database.checkMetaBoolean(appContext, "com.tekartik.sqfliteexample.flagmissing", true));
assertFalse(Database.checkMetaBoolean(appContext, "com.tekartik.sqfliteexample.flagmissing", false));
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET" />
<application>
<!-- For android unit test -->
<activity
android:name="androidx.test.core.app.InstrumentationActivityInvoker$BootstrapActivity"
android:exported="true" />
</application>
</manifest>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name="${applicationName}"
android:label="example">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!-- Uncomment to enable WAL (or unit test walEnabled that should fail then) -->
<!--
<meta-data
android:name="com.tekartik.sqflite.wal_enabled"
android:value="true" />
-->
<meta-data
android:name="com.tekartik.sqfliteexample.flagtrue"
android:value="true" />
<meta-data
android:name="com.tekartik.sqfliteexample.flagfalse"
android:value="false" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility?hl=en and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>
package com.tekartik.sqfliteexample;
import io.flutter.embedding.android.FlutterActivity;
public class MainActivity extends FlutterActivity {
}
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
allprojects {
repositories {
google()
mavenCentral()
}
// Extra java lint
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
}
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
}
include ":app"
// Copyright 2019, the Chromium 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:io';
import 'dart:io' as io;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart' hide test;
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_example/src/common_import.dart';
import 'package:test/test.dart' show test;
// ignore_for_file: avoid_print
void main() {
WidgetsFlutterBinding.ensureInitialized();
group('sqflite', () {
group('open', () {
test('missing directory', () async {
//await devVerbose();
final path = join('test_missing_sub_dir', 'simple.db');
try {
await Directory(join(await getDatabasesPath(), dirname(path)))
.delete(recursive: true);
} catch (_) {}
final db =
await openDatabase(path, version: 1, onCreate: (db, version) async {
expect(await db.getVersion(), 0);
});
expect(await db.getVersion(), 1);
await db.close();
});
test('failure', () {
// This one seems ignored
// fail('regular test failure');
});
test('in_memory', () async {
final db = await openDatabase(inMemoryDatabasePath, version: 1,
onCreate: (db, version) async {
expect(await db.getVersion(), 0);
});
expect(await db.getVersion(), 1);
await db.close();
});
});
test('exists', () async {
expect(await databaseExists(inMemoryDatabasePath), isFalse);
const path = 'test_exists.db';
await deleteDatabase(path);
expect(await databaseExists(path), isFalse);
final db = await openDatabase(path);
try {
expect(await databaseExists(path), isTrue);
} finally {
await db.close();
}
});
test('close in transaction', () async {
// await Sqflite.devSetDebugModeOn(true);
const path = 'test_close_in_transaction.db';
final factory = databaseFactory;
await deleteDatabase(path);
var db = await factory.openDatabase(path,
options: OpenDatabaseOptions(version: 1));
try {
await db.execute('BEGIN TRANSACTION');
await db.close();
db = await factory.openDatabase(path,
options: OpenDatabaseOptions(version: 1));
} finally {
await db.close();
}
});
/// Check if a file is a valid database file
///
/// An empty file is a valid empty sqlite file
Future<bool> isDatabase(String path) async {
var isDatabase = false;
Database? db;
try {
db = await openReadOnlyDatabase(path);
//await db.getVersion();
await db.query('sqlite_master');
isDatabase = true;
} catch (_) {
} finally {
await db?.close();
}
return isDatabase;
}
test('read_only missing database', () async {
const path = 'test_missing_database.db';
await deleteDatabase(path);
try {
final db = await openReadOnlyDatabase(path);
fail('should fail ${db.path}');
} on DatabaseException catch (_) {}
expect(await isDatabase(path), isFalse);
});
test('read_only empty file', () async {
const path = 'empty_file_database.db';
await deleteDatabase(path);
final fullPath = join((await getDatabasesPath()), path);
await Directory(dirname(fullPath)).create(recursive: true);
await File(fullPath).writeAsString('');
// Open is fine, that is the native behavior
final db = await openReadOnlyDatabase(fullPath);
expect(await File(fullPath).readAsString(), '');
await db.getVersion();
await db.close();
expect(await File(fullPath).readAsString(), '');
expect(await isDatabase(fullPath), isTrue);
});
test('read_only missing bad format', () async {
const path = 'test_bad_format_database.db';
await deleteDatabase(path);
final fullPath = join((await getDatabasesPath()), path);
await Directory(dirname(fullPath)).create(recursive: true);
await File(fullPath).writeAsString('test');
// Open is fine, that is the native behavior
final db = await openReadOnlyDatabase(fullPath);
print('DEBUG');
expect(await File(fullPath).readAsString(), 'test');
try {
final version = await db.getVersion();
print(await db.query('sqlite_master'));
fail('getVersion should fail ${db.path} $version');
} on DatabaseException catch (_) {
// Android: DatabaseException(file is not a database (code 26 SQLITE_NOTADB)) sql 'PRAGMA user_version' args []}
}
await db.close();
expect(await File(fullPath).readAsString(), 'test');
expect(await isDatabase(fullPath), isFalse);
expect(await isDatabase(fullPath), isFalse);
expect(await File(fullPath).readAsString(), 'test');
});
test('multiple database', () async {
//await Sqflite.devSetDebugModeOn(true);
const count = 10;
final dbs = List<Database?>.filled(count, null, growable: false);
for (var i = 0; i < count; i++) {
final path = 'test_multiple_$i.db';
await deleteDatabase(path);
dbs[i] =
await openDatabase(path, version: 1, onCreate: (db, version) async {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
expect(
await db
.rawInsert('INSERT INTO Test (name) VALUES (?)', ['test_$i']),
1);
});
}
for (var i = 0; i < count; i++) {
final db = dbs[i]!;
try {
final name = (await db.query('Test', columns: ['name']))
.first
.values
.first as String?;
expect(name, 'test_$i');
} finally {
await db.close();
}
}
for (var i = 0; i < count; i++) {
final db = dbs[i]!;
await db.close();
}
});
test('version', () async {
// await Sqflite.devSetDebugModeOn(true);
const path = 'test_version.db';
await deleteDatabase(path);
var db = await openDatabase(path, version: 1);
try {
expect(await db.getVersion(), 1);
unawaited(db.close());
db = await openDatabase(path, version: 2);
expect(await db.getVersion(), 2);
unawaited(db.close());
db = await openDatabase(path, version: 1);
expect(await db.getVersion(), 1);
unawaited(db.close());
db = await openDatabase(path, version: 1);
expect(await db.getVersion(), 1);
expect(await isDatabase(path), isTrue);
} finally {
await db.close();
}
expect(await isDatabase(path), isTrue);
});
test('duplicated_column', () async {
// await Sqflite.devSetDebugModeOn(true);
const path = 'test_duplicated_column.db';
await deleteDatabase(path);
final db = await openDatabase(path);
try {
await db.execute('CREATE TABLE Test (col1 INTEGER, col2 INTEGER)');
await db.insert('Test', {'col1': 1, 'col2': 2});
final result = await db.rawQuery(
'SELECT t.col1, col1, t.col2, col2 AS col1 FROM Test AS t');
expect(result, [
{'col1': 2, 'col2': 2}
]);
} finally {
await db.close();
}
});
test('indexed_param', () async {
final db = await openDatabase(':memory:');
expect(await db.rawQuery('SELECT ?1 + ?2', [3, 4]), [
{'?1 + ?2': 7}
]);
await db.close();
});
test('deleteDatabase', () async {
// await devVerbose();
const path = 'test_delete_database.db';
await deleteDatabase(path);
expect(await databaseExists(path), isFalse);
late Database db;
try {
db = await openDatabase(path);
expect(await db.getVersion(), 0);
await db.setVersion(1);
await db.close();
expect(await databaseExists(path), isTrue);
await deleteDatabase(path);
expect(await databaseExists(path), isFalse);
db = await openDatabase(path);
expect(await db.getVersion(), 0);
await db.setVersion(1);
// delete without closing every time
await deleteDatabase(path);
db = await openDatabase(path);
expect(await db.getVersion(), 0);
await db.execute('BEGIN TRANSACTION');
await db.setVersion(2);
await deleteDatabase(path);
db = await openDatabase(path);
expect(await db.getVersion(), 0);
await db.setVersion(3);
await deleteDatabase(path);
db = await openDatabase(path);
expect(await db.getVersion(), 0);
} finally {
await db.close();
}
});
test('androidSetLocale', () async {
if (!kIsWeb && io.Platform.isAndroid) {
const path = 'test_android_set_locale.db';
await deleteDatabase(path);
late Database db;
db = await openDatabase(path,
onConfigure: (db) async {
await db.androidSetLocale('zh-CN');
},
version: 1,
onCreate: (db, v) async {
await db.execute('CREATE TABLE Test(name TEXT)');
for (var row in <Map<String, Object?>>[
{'name': '桌'},
{'name': '椅'},
{'name': '盘'},
]) {
await db.insert('Test', row);
}
});
Future<List<String>> getNames() async {
return (await db.query('Test', orderBy: 'name COLLATE LOCALIZED ASC'))
.map((e) => e.values.first!.toString())
.toList();
}
try {
// Order ok
expect(await getNames(), ['盘', '椅', '桌']);
// Reopen same locale
await db.close();
db = await openDatabase(
path,
onConfigure: (db) async {
await db.androidSetLocale('zh-CN');
},
);
// order ok too
expect(await getNames(), ['盘', '椅', '桌']);
await db.close();
// No locale
db = await openDatabase(path);
// order not ok
expect(await getNames(), ['桌', '椅', '盘']);
await db.close();
db = await openDatabase(
path,
onConfigure: (db) async {
await db.androidSetLocale('en-US');
},
);
// order not ok
expect(await getNames(), ['桌', '椅', '盘']);
await db.close();
db = await openDatabase(
path,
onConfigure: (db) async {
await db.androidSetLocale('zh-CN');
},
);
// order ok again
expect(await getNames(), ['盘', '椅', '桌']);
} finally {
await db.close();
}
}
});
});
}
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart';
import 'test_page.dart';
/// Batch test page.
class BatchTestPage extends TestPage {
/// Batch test page.
BatchTestPage({Key? key}) : super('Batch tests', key: key) {
test('BatchQuery', () async {
// await Sqflite.devSetDebugModeOn();
final path = await initDeleteDb('batch_query.db');
final db = await openDatabase(path);
// empty batch
var batch = db.batch();
batch.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
batch.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item1']);
var results = await batch.commit();
expect(results, [null, 1]);
final dbResult = await db.rawQuery('SELECT id, name FROM Test');
// devPrint('dbResult $dbResult');
expect(dbResult, [
{'id': 1, 'name': 'item1'}
]);
// one query
batch = db.batch();
batch.rawQuery('SELECT id, name FROM Test');
batch.query('Test', columns: ['id', 'name']);
results = await batch.commit();
// devPrint('select $results ${results?.first}');
expect(results, [
[
{'id': 1, 'name': 'item1'}
],
[
{'id': 1, 'name': 'item1'}
]
]);
await db.close();
});
test('Batch', () async {
// await databaseFactory.devSetDebugModeOn();
final path = await initDeleteDb('batch.db');
final db = await openDatabase(path);
// empty batch
var batch = db.batch();
var results = await batch.commit();
expect(results.length, 0);
expect(results, isEmpty);
// one create table
batch = db.batch();
batch.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
results = await batch.commit();
// devPrint('1 $results ${results?.first}');
expect(results, [null]);
expect(results[0], null);
// one insert
batch = db.batch();
batch.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item1']);
results = await batch.commit();
expect(results, [1]);
// one query
batch = db.batch();
batch.rawQuery('SELECT id, name FROM Test');
batch.query('Test', columns: ['id', 'name']);
results = await batch.commit();
// devPrint('select $results ${results?.first}');
expect(results, [
[
{'id': 1, 'name': 'item1'}
],
[
{'id': 1, 'name': 'item1'}
]
]);
// two insert
batch = db.batch();
batch.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item2']);
batch.insert('Test', {'name': 'item3'});
results = await batch.commit();
expect(results, [2, 3]);
// update
batch = db.batch();
batch.rawUpdate(
'UPDATE Test SET name = ? WHERE name = ?', ['new_item', 'item1']);
batch.update('Test', {'name': 'new_other_item'},
where: 'name != ?', whereArgs: <String>['new_item']);
results = await batch.commit();
expect(results, [1, 2]);
// delete
batch = db.batch();
batch.rawDelete('DELETE FROM Test WHERE name = ?', ['new_item']);
batch.delete('Test',
where: 'name = ?', whereArgs: <String>['new_other_item']);
results = await batch.commit();
expect(results, [1, 2]);
// No result
batch = db.batch();
batch.insert('Test', {'name': 'item'});
batch.update('Test', {'name': 'new_item'},
where: 'name = ?', whereArgs: <String>['item']);
batch.delete('Test', where: 'name = ?', whereArgs: ['item']);
results = await batch.commit(noResult: true);
expect(results, isEmpty);
await db.close();
});
test('Batch in transaction', () async {
// await Sqflite.devSetDebugModeOn();
final path = await initDeleteDb('batch_in_transaction.db');
final db = await openDatabase(path);
late List<Object?> results;
await db.transaction((txn) async {
final batch1 = txn.batch();
batch1.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
final batch2 = txn.batch();
batch2.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item1']);
results = await batch1.commit();
expect(results, [null]);
results = await batch2.commit();
expect(results, [1]);
});
await db.close();
});
test('Apply in database', () async {
// await Sqflite.devSetDebugModeOn();
final path = await initDeleteDb('apply_in_database.db');
final db = await openDatabase(path);
late List<Object?> results;
final batch1 = db.batch();
batch1.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
final batch2 = db.batch();
batch2.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item1']);
results = await batch1.apply();
expect(results, [null]);
results = await batch2.apply();
expect(results, [1]);
await db.close();
});
test('Apply in transaction', () async {
// await Sqflite.devSetDebugModeOn();
final path = await initDeleteDb('apply_in_transaction.db');
final db = await openDatabase(path);
late List<Object?> results;
await db.transaction((txn) async {
final batch1 = txn.batch();
batch1.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
final batch2 = txn.batch();
batch2.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item1']);
results = await batch1.apply();
expect(results, [null]);
results = await batch2.apply();
expect(results, [1]);
});
await db.close();
});
test('Batch continue on error', () async {
// await Sqflite.devSetDebugModeOn();
final path = await initDeleteDb('batch_continue_on_error.db');
final db = await openDatabase(path);
try {
final batch = db.batch();
batch.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item1']);
batch.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
batch.execute('DUMMY');
batch.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item1']);
batch.rawQuery('SELECT * FROM Test');
final results = await batch.commit(continueOnError: true);
// devPrint(results);
// First result is an exception
var exception = results[0] as DatabaseException;
expect(exception.isNoSuchTableError(), true);
// Second result is null (create table)
expect(results[1], null);
// Third result is an exception
exception = results[2] as DatabaseException;
expect(exception.isSyntaxError(), true);
// Fourth result is an insert
expect(results[3], 1);
// Fifth is a select
expect(results[4], [
{'id': 1, 'name': 'item1'}
]);
} finally {
await db.close();
}
});
}
}
export 'database_impl.dart';
export 'database_io.dart' if (dart.library.js_interop) 'database_web.dart';
import 'dart:typed_data';
import 'package:sqflite/sqflite.dart';
import 'database.dart';
/// Custom platform Handler, need to handle Web or IO differently or from a
/// custom app
abstract class PlatformHandler {
/// delete the db, create the folder and returns its path
Future<String> initDeleteDb(String dbName) async {
if (await databaseExists(dbName)) {
await deleteDatabase(dbName);
}
return dbName;
}
/// Write the db file directly to the file system
Future<void> writeFileAsBytes(String path, List<int> bytes,
{bool flush = false});
/// Read a file as bytes
Future<Uint8List> readFileAsBytes(String path);
/// Write a file as a string
Future<void> writeFileAsString(String path, String text,
{bool flush = false});
/// Read a file as a string
Future<String> readFileAsString(String path);
/// Check if a path exists.
Future<bool> pathExists(String path);
/// Recursively create a directory
Future<void> createDirectory(String path);
/// Recursively delete a directory
Future<void> deleteDirectory(String path);
/// Check if a directory exists
Future<bool> existsDirectory(String path);
}
// ---
// Compat, to keep the example page as is
// ---
/// delete the db, create the folder and returnes its path
Future<String> initDeleteDb(String dbName) =>
platformHandler.initDeleteDb(dbName);
/// Write the db file directly to the file system
Future<void> writeFileAsBytes(String path, List<int> bytes,
{bool flush = false}) =>
platformHandler.writeFileAsBytes(path, bytes, flush: flush);
/// Read a file as bytes
Future<Uint8List> readFileAsBytes(String path) =>
platformHandler.readFileAsBytes(path);
/// Write a file as a string
Future<void> writeFileAsString(String path, String text,
{bool flush = false}) =>
platformHandler.writeFileAsString(path, text, flush: flush);
/// Read a file as a string
Future<String> readFileAsString(String path) =>
platformHandler.readFileAsString(path);
/// Check if a path exists.
Future<bool> pathExists(String path) => platformHandler.pathExists(path);
/// Recursively create a directory
Future<void> createDirectory(String path) =>
platformHandler.createDirectory(path);
/// Recursively delete a directory
Future<void> deleteDirectory(String path) =>
platformHandler.deleteDirectory(path);
/// Check if a directory exists
Future<bool> existsDirectory(String path) =>
platformHandler.existsDirectory(path);
PlatformHandler? _platformHandler;
/// Platform handler (can be overriden, needed for the web test app)
PlatformHandler get platformHandler => _platformHandler ??= platformHandlerIo;
set platformHandler(PlatformHandler handler) => _platformHandler = handler;
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_example/database/database.dart';
class _PlatformHandlerIo extends PlatformHandler {
/// delete the db, create the folder and returns its path
@override
Future<String> initDeleteDb(String dbName) async {
final databasePath = await getDatabasesPath();
// print(databasePath);
final path = join(databasePath, dbName);
// make sure the folder exists
// ignore: avoid_slow_async_io
if (await Directory(dirname(path)).exists()) {
await deleteDatabase(path);
} else {
try {
await Directory(dirname(path)).create(recursive: true);
} catch (e) {
// ignore: avoid_print
print(e);
}
}
return path;
}
/// Write the db file directly to the file system
@override
Future<void> writeFileAsBytes(String path, List<int> bytes,
{bool flush = false}) async {
await File(path).writeAsBytes(bytes, flush: flush);
}
/// Read a file as bytes
@override
Future<Uint8List> readFileAsBytes(String path) async {
return File(path).readAsBytes();
}
/// Write a file as a string
@override
Future<void> writeFileAsString(String path, String text,
{bool flush = false}) async {
await File(path).writeAsString(text, flush: true);
}
/// Read a file as a string
@override
Future<String> readFileAsString(String path) async {
return File(path).readAsString();
}
/// Check if a path exists.
@override
Future<bool> pathExists(String path) async {
// ignore: avoid_slow_async_io
return File(path).exists();
}
/// Recursively create a directory
@override
Future<void> createDirectory(String path) async {
await Directory(dirname(path)).create(recursive: true);
}
/// Recursively delete a directory
@override
Future<void> deleteDirectory(String path) async {
await Directory(path).delete(recursive: true);
}
/// Check if a directory exists
@override
Future<bool> existsDirectory(String path) async {
// ignore: avoid_slow_async_io
return Directory(path).exists();
}
}
/// Io platform handler
PlatformHandler platformHandlerIo = _PlatformHandlerIo();
import 'package:sqflite_example/database/database.dart';
/// platform handler io not supported on the web.
PlatformHandler get platformHandlerIo =>
throw UnsupportedError('platform handler io not supported on the web');
import 'package:flutter/foundation.dart';
import 'test_page.dart';
/// Deprecated test page.
class DeprecatedTestPage extends TestPage {
/// Deprecated test page.
DeprecatedTestPage({Key? key}) : super('Deprecated tests', key: key) {
test('None', () async {});
}
}
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite/sql.dart';
import 'src/common_import.dart';
import 'test_page.dart';
// ignore_for_file: avoid_print
/// Exception test page.
class ExceptionTestPage extends TestPage {
/// Exception test page.
ExceptionTestPage({Key? key}) : super('Exception tests', key: key) {
test('Transaction failed', () async {
//await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('transaction_failed.db');
final db = await openDatabase(path);
await db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
// insert then fails to make sure the transaction is cancelled
var hasFailed = false;
try {
await db.transaction((txn) async {
await txn.rawInsert(
'INSERT INTO Test (name) VALUES (?)', <Object>['item']);
final afterCount = Sqflite.firstIntValue(
await txn.rawQuery('SELECT COUNT(*) FROM Test'));
expect(afterCount, 1);
hasFailed = true;
// this failure should cancel the insertion before
await txn.execute('DUMMY CALL');
hasFailed = false;
});
} on DatabaseException catch (e) {
// iOS: native_error: PlatformException(sqlite_error, Error Domain=FMDatabase Code=1 'near 'DUMMY': syntax error' UserInfo={NSLocalizedDescription=near 'DUMMY': syntax error}, null)
print('native_error: $e');
}
verify(hasFailed);
final afterCount =
Sqflite.firstIntValue(await db.rawQuery('SELECT COUNT(*) FROM Test'));
expect(afterCount, 0);
await db.close();
});
test('Batch failed', () async {
//await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('batch_failed.db');
final db = await openDatabase(path);
await db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
final batch = db.batch();
batch.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item']);
batch.execute('DUMMY CALL');
var hasFailed = true;
try {
await batch.commit();
hasFailed = false;
} on DatabaseException catch (e) {
print('native_error: $e');
}
verify(hasFailed);
final afterCount =
Sqflite.firstIntValue(await db.rawQuery('SELECT COUNT(*) FROM Test'));
expect(afterCount, 0);
await db.close();
});
test('Sqlite Exception', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('exception.db');
final db = await openDatabase(path);
// Query
try {
await db.rawQuery('SELECT COUNT(*) FROM Test');
fail(); // should fail before
} on DatabaseException catch (e) {
verify(e.isNoSuchTableError('Test'));
// Error Domain=FMDatabase Code=1 'no such table: Test' UserInfo={NSLocalizedDescription=no such table: Test})
}
// Catch without using on DatabaseException
try {
await db.rawQuery('malformed query');
fail(); // should fail before
} on DatabaseException catch (e) {
verify(e.isSyntaxError());
//verify(e.toString().contains('sql 'malformed query' args'));
// devPrint(e);
}
try {
await db.rawQuery('malformed query with args ?', [1]);
fail(); // should fail before
} on DatabaseException catch (e) {
verify(e.isSyntaxError());
print(e);
verify(e
.toString()
.contains("sql 'malformed query with args ?' args [1]"));
}
try {
await db.execute('DUMMY');
fail(); // should fail before
} on Exception catch (e) {
//verify(e.isSyntaxError());
print(e);
verify(e.toString().contains('DUMMY'));
}
try {
await db.rawInsert('DUMMY');
fail(); // should fail before
} on DatabaseException catch (e) {
verify(e.isSyntaxError());
verify(e.toString().contains('DUMMY'));
}
try {
await db.rawUpdate('DUMMY');
fail(); // should fail before
} on DatabaseException catch (e) {
verify(e.isSyntaxError());
verify(e.toString().contains('DUMMY'));
}
await db.close();
});
test('Sqlite constraint Exception', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('constraint_exception.db');
final db = await openDatabase(path, version: 1, onCreate: (db, version) {
db.execute('CREATE TABLE Test (name TEXT UNIQUE)');
});
await db.insert('Test', {'name': 'test1'});
try {
await db.insert('Test', {'name': 'test1'});
} on DatabaseException catch (e) {
// iOS: Error Domain=FMDatabase Code=19 'UNIQUE constraint failed: Test.name' UserInfo={NSLocalizedDescription=UNIQUE constraint failed: Test.name}) s
// Android: UNIQUE constraint failed: Test.name (code 2067))
print(e);
verify(e.isUniqueConstraintError());
expect(e.getResultCode(), 2067);
verify(e.isUniqueConstraintError('Test.name'));
}
await db.close();
});
test('Sqlite constraint primary key', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('constraint_primary_key_exception.db');
final db = await openDatabase(path, version: 1, onCreate: (db, version) {
db.execute('CREATE TABLE Test (name TEXT PRIMARY KEY)');
});
await db.insert('Test', {'name': 'test1'});
try {
await db.insert('Test', {'name': 'test1'});
} on DatabaseException catch (e) {
// iOS: Error Domain=FMDatabase Code=19 'UNIQUE constraint failed: Test.name' UserInfo={NSLocalizedDescription=UNIQUE constraint failed: Test.name}) s
// Android: UNIQUE constraint failed: Test.name (code 1555))
print(e);
verify(e.isUniqueConstraintError());
verify(e.isUniqueConstraintError('Test.name'));
expect(e.getResultCode(), 1555);
}
await db.close();
});
test('Sqlite batch Exception', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('batch_exception.db');
final db = await openDatabase(path);
// Query
try {
final batch = db.batch();
batch.rawQuery('SELECT COUNT(*) FROM Test');
await batch.commit();
fail(); // should fail before
} on DatabaseException catch (e) {
print(e);
verify(e.isNoSuchTableError('Test'));
}
// Catch without using on DatabaseException
try {
final batch = db.batch();
batch.rawQuery('malformed query');
await batch.commit();
fail(); // should fail before
} on DatabaseException catch (e) {
verify(e.isSyntaxError());
print(e);
verify(e.toString().contains("sql 'malformed query'"));
}
try {
final batch = db.batch();
batch.rawQuery('malformed query with args ?', [1]);
await batch.commit();
fail(); // should fail before
} on DatabaseException catch (e) {
verify(e.isSyntaxError());
print(e);
verify(e
.toString()
.contains("sql 'malformed query with args ?' args [1]"));
}
try {
final batch = db.batch();
batch.execute('DUMMY');
await batch.commit();
fail(); // should fail before
} on DatabaseException catch (e) {
verify(e.isSyntaxError());
// devPrint(e);
// iOS Error Domain=FMDatabase Code=1 "near "DUMMY": syntax error" UserInfo={NSLocalizedDescription=near "DUMMY": syntax error})
verify(e.toString().contains("sql 'DUMMY'"));
}
try {
final batch = db.batch();
batch.rawInsert('DUMMY');
await batch.commit();
fail(); // should fail before
} on DatabaseException catch (e) {
verify(e.isSyntaxError());
verify(e.toString().contains("sql 'DUMMY'"));
}
try {
final batch = db.batch();
batch.rawUpdate('DUMMY');
await batch.commit();
fail(); // should fail before
} on DatabaseException catch (e) {
verify(e.isSyntaxError());
verify(e.toString().contains("sql 'DUMMY'"));
}
await db.close();
});
test('Open onDowngrade fail', () async {
final path = await initDeleteDb('open_on_downgrade_fail.db');
var database = await openDatabase(path, version: 2,
onCreate: (Database db, int version) async {
await db.execute('CREATE TABLE Test(id INTEGER PRIMARY KEY)');
});
await database.close();
// currently this is crashing...
// should fail going back in versions
try {
database = await openDatabase(path,
version: 1, onDowngrade: onDatabaseVersionChangeError);
verify(false);
} catch (e) {
print(e);
}
// should work
database = await openDatabase(path,
version: 2, onDowngrade: onDatabaseVersionChangeError);
print(database);
await database.close();
});
test('Access after close', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('access_after_close.db');
final database = await openDatabase(path, version: 3,
onCreate: (Database db, int version) async {
await db.execute('CREATE TABLE Test(id INTEGER PRIMARY KEY)');
});
await database.close();
try {
await database.getVersion();
verify(false);
} on DatabaseException catch (e) {
print(e);
verify(e.isDatabaseClosedError());
}
try {
await database.setVersion(1);
fail();
} on DatabaseException catch (e) {
print(e);
verify(e.isDatabaseClosedError());
}
});
test('Non escaping fields', () async {
//await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('non_escaping_fields.db');
final db = await openDatabase(path);
const table = 'table';
try {
await db.execute('CREATE TABLE $table (group INTEGER)');
fail('should fail');
} on DatabaseException catch (e) {
print(e);
verify(e.isSyntaxError());
}
try {
await db.execute('INSERT INTO $table (group) VALUES (1)');
fail('should fail');
} on DatabaseException catch (e) {
print(e);
verify(e.isSyntaxError());
}
try {
await db.rawQuery('SELECT * FROM $table ORDER BY group DESC');
} on DatabaseException catch (e) {
print(e);
verify(e.isSyntaxError());
}
try {
await db.rawQuery('DELETE FROM $table');
} on DatabaseException catch (e) {
print(e);
verify(e.isSyntaxError());
}
// Build our escape list from all the sqlite keywords
final toExclude = <String>[];
for (var name in allEscapeNames) {
try {
await db.execute('CREATE TABLE $name (value INTEGER)');
} on DatabaseException catch (e) {
await db.execute('CREATE TABLE ${escapeName(name)} (value INTEGER)');
verify(e.isSyntaxError());
toExclude.add(name);
}
}
print(json.encode(toExclude));
await db.close();
});
test('Bind no argument (no iOS/MacOS)', () async {
if (!platform.isIOS && !platform.isMacOS) {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('bind_no_arg_failed.db');
final db = await openDatabase(path);
await db.execute('CREATE TABLE Test (name TEXT)');
await db.rawInsert('INSERT INTO Test (name) VALUES ("?")', []);
await db.rawQuery('SELECT * FROM Test WHERE name = ?', []);
await db.rawDelete('DELETE FROM Test WHERE name = ?', []);
await db.close();
}
});
test('crash ios (no iOS)', () async {
// This crashes natively on iOS...can't catch it yet
if (!platform.isIOS) {
//if (true) {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('bind_no_arg_failed.db');
final db = await openDatabase(path);
await db.execute('CREATE TABLE Test (name TEXT)');
await db.rawInsert('INSERT INTO Test (name) VALUES ("?")', []);
await db.rawQuery('SELECT * FROM Test WHERE name = ?', []);
await db.close();
}
});
test('Bind null argument', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('bind_null_failed.db');
final db = await openDatabase(path);
await db.execute('CREATE TABLE Test (name TEXT)');
//await db.rawInsert("INSERT INTO Test (name) VALUES (\"?\")", [null]);
/*
nnbd this can no longer be tested!
try {
await db.rawInsert('INSERT INTO Test (name) VALUES (?)', [null]);
} on DatabaseException catch (e) {
print('ERR: $e');
expect(e.toString().contains("sql 'INSERT"), true);
}
try {
await db.rawQuery('SELECT * FROM Test WHERE name = ?', [null]);
} on DatabaseException catch (e) {
print('ERR: $e');
expect(e.toString().contains("sql 'SELECT * FROM Test"), true);
}
try {
await db.rawDelete('DELETE FROM Test WHERE name = ?', [null]);
} on DatabaseException catch (e) {
print('ERR: $e');
expect(e.toString().contains("sql 'DELETE FROM Test"), true);
}
*/
await db.close();
});
test('Bind no parameter', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('bind_no_parameter_failed.db');
final db = await openDatabase(path);
await db.execute('CREATE TABLE Test (name TEXT)');
try {
await db
.rawInsert('INSERT INTO Test (name) VALUES ("value")', ['value2']);
} on DatabaseException catch (e) {
print('ERR: $e');
expect(e.toString().contains("sql 'INSERT INTO Test"), true);
}
try {
await db
.rawQuery('SELECT * FROM Test WHERE name = "value"', ['value2']);
} on DatabaseException catch (e) {
print('ERR: $e');
expect(e.toString().contains("sql 'SELECT * FROM Test"), true);
}
try {
await db.rawDelete('DELETE FROM Test WHERE name = "value"', ['value2']);
} on DatabaseException catch (e) {
print('ERR: $e');
expect(e.toString().contains("sql 'DELETE FROM Test"), true);
}
await db.close();
});
// Using the db object in a transaction lead to a deadlock...
test('Dead lock', () async {
final path = await initDeleteDb('dead_lock.db');
final db = await openDatabase(path);
try {
var hasTimedOut = false;
var callbackCount = 0;
Sqflite.setLockWarningInfo(
duration: const Duration(milliseconds: 200),
callback: () {
callbackCount++;
});
await db.transaction((txn) async {
try {
await db.getVersion().timeout(const Duration(milliseconds: 1500));
fail('should fail');
} on TimeoutException catch (_) {
hasTimedOut = true;
}
});
expect(hasTimedOut, true);
expect(callbackCount, 1);
} finally {
await db.close();
}
});
test('Thread dead lock', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('thread_dead_lock.db');
final db1 = await openDatabase(path, singleInstance: false);
final db2 = await openDatabase(path, singleInstance: false);
try {
await db1.execute('BEGIN IMMEDIATE TRANSACTION');
try {
// this should block the main thread
await db2
.execute('BEGIN IMMEDIATE TRANSACTION')
.timeout(const Duration(milliseconds: 500));
fail('should timeout');
} on TimeoutException catch (e) {
print('caught $e');
}
// Try to open another db to check that the main thread is free
final db = await openDatabase(inMemoryDatabasePath);
await db.close();
try {
// clean up
await db1.execute('ROLLBACK');
} catch (_) {}
try {
await db2.execute('ROLLBACK');
} catch (_) {}
} finally {
await db1.close();
await db2.close();
}
});
}
}
/// Name that should be escaped.
var escapeNames = [
'add',
'all',
'alter',
'and',
'as',
'autoincrement',
'between',
'case',
'check',
'collate',
'commit',
'constraint',
'create',
'default',
'deferrable',
'delete',
'distinct',
'drop',
'else',
'escape',
'except',
'exists',
'foreign',
'from',
'group',
'having',
'if',
'in',
'index',
'insert',
'intersect',
'into',
'is',
'isnull',
'join',
'limit',
'not',
'notnull',
'null',
'on',
'or',
'order',
'primary',
'references',
'select',
'set',
'table',
'then',
'to',
'transaction',
'union',
'unique',
'update',
'using',
'values',
'when',
'where'
];
/// all SQLite keywords to escape.
var allEscapeNames = [
'abort',
'action',
'add',
'after',
'all',
'alter',
'analyze',
'and',
'as',
'asc',
'attach',
'autoincrement',
'before',
'begin',
'between',
'by',
'cascade',
'case',
'cast',
'check',
'collate',
'column',
'commit',
'conflict',
'constraint',
'create',
'cross',
'current_date',
'current_time',
'current_timestamp',
'database',
'default',
'deferrable',
'deferred',
'delete',
'desc',
'detach',
'distinct',
'drop',
'each',
'else',
'end',
'escape',
'except',
'exclusive',
'exists',
'explain',
'fail',
'for',
'foreign',
'from',
'full',
'glob',
'group',
'having',
'if',
'ignore',
'immediate',
'in',
'index',
'indexed',
'initially',
'inner',
'insert',
'instead',
'intersect',
'into',
'is',
'isnull',
'join',
'key',
'left',
'like',
'limit',
'match',
'natural',
'no',
'not',
'notnull',
'null',
'of',
'offset',
'on',
'or',
'order',
'outer',
'plan',
'pragma',
'primary',
'query',
'raise',
'recursive',
'references',
'regexp',
'reindex',
'release',
'rename',
'replace',
'restrict',
'right',
'rollback',
'row',
'savepoint',
'select',
'set',
'table',
'temp',
'temporary',
'then',
'to',
'transaction',
'trigger',
'union',
'unique',
'update',
'using',
'vacuum',
'values',
'view',
'virtual',
'when',
'where',
'with',
'without'
];
import 'dart:isolate';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_common/sqflite_dev.dart';
import 'package:sqflite_example/src/common_import.dart';
import 'package:sqflite_example/utils.dart';
import 'test_page.dart';
// ignore_for_file: avoid_print
/// `todo` table name
const String tableTodo = 'todo';
/// id column name
const String columnId = '_id';
/// title column name
const String columnTitle = 'title';
/// done column name
const String columnDone = 'done';
/// Experiment test page.
class ExpTestPage extends TestPage {
/// Experiment test page.
ExpTestPage({Key? key}) : super('Exp Tests', key: key) {
test('order_by', () async {
//await Sqflite.setDebugModeOn(true);
final path = await initDeleteDb('order_by_exp.db');
final db = await openDatabase(path);
const table = 'test';
await db
.execute('CREATE TABLE $table (column_1 INTEGER, column_2 INTEGER)');
// inserted in a wrong order to check ASC/DESC
await db
.execute('INSERT INTO $table (column_1, column_2) VALUES (11, 180)');
await db
.execute('INSERT INTO $table (column_1, column_2) VALUES (10, 180)');
await db
.execute('INSERT INTO $table (column_1, column_2) VALUES (10, 2000)');
final expectedResult = [
{'column_1': 10, 'column_2': 2000},
{'column_1': 10, 'column_2': 180},
{'column_1': 11, 'column_2': 180}
];
var result = await db.rawQuery(
'SELECT * FROM $table ORDER BY column_1 ASC, column_2 DESC');
//print(JSON.encode(result));
expect(result, expectedResult);
result = await db.query(table, orderBy: 'column_1 ASC, column_2 DESC');
expect(result, expectedResult);
await db.close();
});
test('in', () async {
//await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('simple_exp.db');
final db = await openDatabase(path);
const table = 'test';
await db
.execute('CREATE TABLE $table (column_1 INTEGER, column_2 INTEGER)');
await db
.execute('INSERT INTO $table (column_1, column_2) VALUES (1, 1001)');
await db
.execute('INSERT INTO $table (column_1, column_2) VALUES (2, 1002)');
await db
.execute('INSERT INTO $table (column_1, column_2) VALUES (2, 1012)');
await db
.execute('INSERT INTO $table (column_1, column_2) VALUES (3, 1003)');
final expectedResult = [
{'column_1': 1, 'column_2': 1001},
{'column_1': 2, 'column_2': 1002},
{'column_1': 2, 'column_2': 1012}
];
// testing with value in the In clause
var result = await db.query(table,
where: 'column_1 IN (1, 2)', orderBy: 'column_1 ASC, column_2 ASC');
//print(JSON.encode(result));
expect(result, expectedResult);
// testing with value as arguments
result = await db.query(table,
where: 'column_1 IN (?, ?)',
whereArgs: ['1', '2'],
orderBy: 'column_1 ASC, column_2 ASC');
expect(result, expectedResult);
await db.close();
});
test('Raw escaping', () async {
//await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('raw_escaping_fields.db');
final db = await openDatabase(path);
const table = 'table';
await db.execute('CREATE TABLE "$table" ("group" INTEGER)');
// inserted in a wrong order to check ASC/DESC
await db.execute('INSERT INTO "$table" ("group") VALUES (1)');
final expectedResult = [
{'group': 1}
];
var result = await db
.rawQuery('SELECT "group" FROM "$table" ORDER BY "group" DESC');
print(result);
expect(result, expectedResult);
result =
await db.rawQuery("SELECT * FROM '$table' ORDER BY `group` DESC");
//print(JSON.encode(result));
expect(result, expectedResult);
await db.rawDelete("DELETE FROM '$table'");
await db.close();
});
test('Escaping fields', () async {
//await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('escaping_fields.db');
final db = await openDatabase(path);
const table = 'group';
await db.execute('CREATE TABLE "$table" ("group" TEXT)');
// inserted in a wrong order to check ASC/DESC
await db.insert(table, {'group': 'group_value'});
await db.update(table, {'group': 'group_new_value'},
where: "\"group\" = 'group_value'");
final expectedResult = [
{'group': 'group_new_value'}
];
final result =
await db.query(table, columns: ['group'], orderBy: '"group" DESC');
//print(JSON.encode(result));
expect(result, expectedResult);
await db.delete(table);
await db.close();
});
test('Functions', () async {
//await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('exp_functions.db');
final db = await openDatabase(path);
const table = 'functions';
await db.execute('CREATE TABLE "$table" (one TEXT, another TEXT)');
await db.insert(table, {'one': '1', 'another': '2'});
await db.insert(table, {'one': '1', 'another': '3'});
await db.insert(table, {'one': '2', 'another': '2'});
var result = await db.rawQuery('''
select one, GROUP_CONCAT(another) as my_col
from $table
GROUP BY one''');
//print('result :$result');
expect(result, [
{'one': '1', 'my_col': '2,3'},
{'one': '2', 'my_col': '2'}
]);
result = await db.rawQuery('''
select one, GROUP_CONCAT(another)
from $table
GROUP BY one''');
// print('result :$result');
expect(result, [
{'one': '1', 'GROUP_CONCAT(another)': '2,3'},
{'one': '2', 'GROUP_CONCAT(another)': '2'}
]);
// user alias
result = await db.rawQuery('''
select t.one, GROUP_CONCAT(t.another)
from $table as t
GROUP BY t.one''');
//print('result :$result');
expect(result, [
{'one': '1', 'GROUP_CONCAT(t.another)': '2,3'},
{'one': '2', 'GROUP_CONCAT(t.another)': '2'}
]);
await db.close();
});
test('Alias', () async {
//await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('exp_alias.db');
final db = await openDatabase(path);
try {
const table = 'alias';
await db.execute(
'CREATE TABLE $table (column_1 INTEGER, column_2 INTEGER)');
await db.insert(table, {'column_1': 1, 'column_2': 2});
final result = await db.rawQuery('''
select t.column_1, t.column_1 as "t.column1", column_1 as column_alias_1, column_2
from $table as t''');
print('result :$result');
expect(result, [
{'t.column1': 1, 'column_1': 1, 'column_alias_1': 1, 'column_2': 2}
]);
} finally {
await db.close();
}
});
test('Dart2 query', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('exp_dart2_query.db');
final db = await openDatabase(path);
try {
const table = 'test';
await db.execute(
'CREATE TABLE $table (column_1 INTEGER, column_2 INTEGER)');
await db.insert(table, {'column_1': 1, 'column_2': 2});
final result = await db.rawQuery('''
select column_1, column_2
from $table as t
''');
print('result: $result');
// test output types
print('result.first: ${result.first}');
final first = result.first;
print('result.first.keys: ${first.keys}');
var keys = result.first.keys;
var values = result.first.values;
verify(keys.first == 'column_1' || keys.first == 'column_2');
verify(values.first == 1 || values.first == 2);
print('result.last.keys: ${result.last.keys}');
keys = result.last.keys;
values = result.last.values;
verify(keys.last == 'column_1' || keys.last == 'column_2');
verify(values.last == 1 || values.last == 2);
} finally {
await db.close();
}
});
/*
Save code that modify a map from a result - unused
var rawResult = await rawQuery(builder.sql, builder.arguments);
// Super slow if we escape a name, please avoid it
// This won't be called if no keywords were used
if (builder.hasEscape) {
for (Map map in rawResult) {
var keys = new Set<String>();
for (String key in map.keys) {
if (isEscapedName(key)) {
keys.add(key);
}
}
if (keys.isNotEmpty) {
for (var key in keys) {
var value = map[key];
map.remove(key);
map[unescapeName(key)] = value;
}
}
}
}
return rawResult;
*/
test('Issue#48', () async {
// Sqflite.devSetDebugModeOn(true);
// devPrint('issue #48');
// Try to query on a non-indexed field
final path = await initDeleteDb('exp_issue_48.db');
final db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db
.execute('CREATE TABLE npa (id INT, title TEXT, identifier TEXT)');
await db.insert(
'npa', {'id': 128, 'title': 'title 1', 'identifier': '0001'});
await db.insert('npa',
{'id': 215, 'title': 'title 1', 'identifier': '0008120150514'});
});
var resultSet = await db.query('npa',
columns: ['id', 'title', 'identifier'],
where: '"identifier" = ?',
whereArgs: ['0008120150514']);
// print(resultSet);
expect(resultSet.length, 1);
// but the results is always - empty QueryResultSet[].
// If i'm trying to do the same with the id field and integer value like
resultSet = await db.query('npa',
columns: ['id', 'title', 'identifier'],
where: '"id" = ?',
whereArgs: [215]);
// print(resultSet);
expect(resultSet.length, 1);
await db.close();
});
test('Issue#52', () async {
// Sqflite.devSetDebugModeOn(true);
// Try to insert string with quote
final path = await initDeleteDb('exp_issue_52.db');
final db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db.execute('CREATE TABLE test (id INT, value TEXT)');
await db.insert('test', {'id': 1, 'value': 'without quote'});
await db.insert('test', {'id': 2, 'value': 'with " quote'});
});
var resultSet = await db
.query('test', where: 'value = ?', whereArgs: ['with " quote']);
expect(resultSet.length, 1);
expect(resultSet.first['id'], 2);
resultSet = await db
.rawQuery('SELECT * FROM test WHERE value = ?', ['with " quote']);
expect(resultSet.length, 1);
expect(resultSet.first['id'], 2);
await db.close();
});
test('Issue#64', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('issue_64.db');
// delete existing if any
await deleteDatabase(path);
// Copy from asset
final data = await rootBundle.load(url.join('assets', 'issue_64.db'));
final bytes =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await databaseFactory.writeDatabaseBytes(path, bytes);
// open the database
final db = await openDatabase(path);
var result = await db.query('recordings',
columns: ['id', 'content', 'file', 'speaker', 'reference']);
print('result1: $result');
expect(result.length, 2);
// This one does not work
// to investigate
result = await db.query('recordings',
columns: ['id', 'content', 'file', 'speaker', 'reference'],
where: 'speaker = ?',
whereArgs: [1]);
print('result2: $result');
expect(result.length, 2);
result = await db.query(
'recordings',
columns: ['id', 'content', 'file', 'speaker', 'reference'],
where: 'speaker = 1',
);
print('result3: $result');
expect(result.length, 2);
await db.close();
});
test('sql dump file', () async {
// await Sqflite.devSetDebugModeOn(true);
// try to import an sql dump file (not working)
final path = await initDeleteDb('sql_file.db');
final db = await openDatabase(path);
try {
const table = 'test';
const sql = '''
CREATE TABLE test (value INTEGER);
INSERT INTO test (value) VALUES (1);
INSERT INTO test (value) VALUES (10);
''';
await db.execute(sql);
// that should be the expected result
// var expectedResult = [
// {'value': 1},
// {'value': 10}
// ];
final result = await db.rawQuery('SELECT * FROM $table');
print(json.encode(result));
// However (at least on Android)
// result is empty, only the first statement is executed
// Ok when using ffi...
if (platform.isLinux) {
// Ok when using ffi linux implementation
// TODO check windows and mac.
// that should be the expected result
var expectedResult = [
{'value': 1},
{'value': 10}
];
expect(result, expectedResult);
} else {
expect(result, isEmpty);
}
} finally {
await db.close();
}
});
test('Issue#164', () async {
//await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('issue_164.db');
final db = await openDatabase(path);
try {
await db.execute('''
CREATE TABLE test (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT NOT NULL,
UNIQUE (label) ON CONFLICT IGNORE
);
''');
// inserted in a wrong order to check ASC/DESC
var id = await db.rawInsert('''
INSERT INTO test (label) VALUES(?)
''', ['label-1']);
expect(id, 1);
id = await db.rawInsert('''
INSERT INTO test (label) VALUES(?)
''', ['label-2']);
expect(id, 2);
id = await db.rawInsert('''
INSERT INTO test (label) VALUES(?)
''', ['label-1']);
expect(id, 0);
} finally {
await db.close();
}
});
test('Defensive mode', () async {
// This shold succeed even on on iOS 14
final db = await openDatabase(inMemoryDatabasePath);
try {
await db.execute('CREATE TABLE Test(value TEXT)');
// Workaround for iOS 14
await db.execute('PRAGMA sqflite -- db_config_defensive_off');
await db.execute('PRAGMA writable_schema = ON');
expect(
await db.update(
'sqlite_master', {'sql': 'CREATE TABLE Test(value BLOB)'},
where: 'name = \'Test\' and type = \'table\''),
1);
} finally {
await db.close();
}
});
test('Defensive mode (should fail on iOS 14)', () async {
// This shold fail on iOS 14
final db = await openDatabase(inMemoryDatabasePath);
try {
await db.execute('CREATE TABLE Test(value TEXT)');
await db.execute('PRAGMA writable_schema = ON');
expect(
await db.update(
'sqlite_master', {'sql': 'CREATE TABLE Test(value BLOB)'},
where: 'name = \'Test\' and type = \'table\''),
1);
} finally {
await db.close();
}
});
test('PRAGMA quick check', () async {
//await databaseFactory.debugSetLogLevel(sqfliteLogLevelVerbose);
final db = await openDatabase(inMemoryDatabasePath);
try {
await db.rawQuery('PRAGMA quick_check');
try {
await db.execute('PRAGMA quick_check');
} catch (e) {
print('execute PRAGMA quick_check failed: $e');
}
} finally {
await db.close();
}
});
test('ATTACH database', () async {
final db1Path = await initDeleteDb('attach1.db');
final db2Path = await initDeleteDb('attach2.db');
// Create some data on db1 and close it
var db1 = await databaseFactory.openDatabase(db1Path);
try {
var batch = db1.batch();
batch.execute('CREATE TABLE table1 (col1 INTEGER)');
batch.insert('table1', {'col1': 1234});
await batch.commit();
} finally {
await db1.close();
}
// Open a new db2 database, attach db1 and query it
var db2 = await databaseFactory.openDatabase(db2Path);
try {
await db2.execute('ATTACH DATABASE \'$db1Path\' AS db1');
var rows = await db2.query('db1.table1');
expect(rows, [
{'col1': 1234}
]);
} finally {
await db2.close();
}
});
/// fts4
var fts4Supports = supportsCompatMode;
if (fts4Supports) {
test('Issue#206', () async {
//await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('issue_206.db');
final db = await openDatabase(path);
try {
final sqls = LineSplitter.split(
'''CREATE VIRTUAL TABLE Food using fts4(description TEXT)
INSERT Into Food (description) VALUES ('banana')
INSERT Into Food (description) VALUES ('apple')''');
final batch = db.batch();
for (var sql in sqls) {
batch.execute(sql);
}
await batch.commit();
final results = await db.rawQuery(
'SELECT description, matchinfo(Food) as matchinfo FROM Food WHERE Food MATCH ?',
['ban*']);
print(results);
// matchinfo is currently returned as binary bloc
expect(results.length, 1);
final map = results.first;
final matchInfo = map['matchinfo'] as Uint8List;
// Convert to Uint32List
final uint32ListLength = matchInfo.length ~/ 4;
final uint32List = Uint32List(uint32ListLength);
final data = ByteData.view(
matchInfo.buffer, matchInfo.offsetInBytes, matchInfo.length);
for (var i = 0; i < uint32ListLength; i++) {
uint32List[i] = data.getUint32(i * 4, Endian.host);
}
// print(uint32List);
expect(uint32List, [1, 1, 1, 1, 1]);
expect(map['matchinfo'], const TypeMatcher<Uint8List>());
} finally {
await db.close();
}
});
}
test('Log level', () async {
// test setting log level
Database? db;
try {
// ignore: deprecated_member_use
await databaseFactory.setLogLevel(sqfliteLogLevelVerbose);
//await databaseFactory.setLogLevel(sqfliteLogLevelSql);
db = await openDatabase(inMemoryDatabasePath);
await db.execute('CREATE TABLE test (value TEXT UNIQUE)');
const table = 'test';
final map = <String, dynamic>{'value': 'test'};
await db.insert(table, map,
conflictAlgorithm: ConflictAlgorithm.replace);
expect(
Sqflite.firstIntValue(await db.query(table, columns: ['COUNT(*)'])),
1);
} finally {
// ignore: deprecated_member_use
await databaseFactory.setLogLevel(sqfliteLogLevelNone);
await db?.close();
}
});
Future<void> testBigBlog(int size) async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('big_blob.db');
var db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value BLOB)');
});
try {
var blob =
Uint8List.fromList(List.generate(size, (index) => index % 256));
var id = await db.insert('Test', {'value': blob});
/// Get the value field from a given id
Future<Uint8List> getValue(int id) async {
return ((await db.query('Test', where: 'id = $id')).first)['value']
as Uint8List;
}
expect((await getValue(id)).length, blob.length);
} finally {
await db.close();
}
}
// We don't test automatically above as it crashes seriously on Android
test('big blob 800 Ko', () async {
await testBigBlog(800000);
});
Future<void> testBigText(int size) async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('big_text.db');
var db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)');
});
try {
var text = List.generate(size, (index) => 'A').join();
var id = await db.insert('Test', {'value': text});
/// Get the value field from a given id
Future<String> getValue(int id) async {
return ((await db.query('Test', where: 'id = $id')).first)['value']
as String;
}
expect((await getValue(id)).length, text.length);
} finally {
await db.close();
}
}
// We don't test automatically above as it crashes seriously on Android
test('big text 800 Ko', () async {
await testBigText(800000);
});
/*
test('big blob 1500 Ko (fails on Android sqlite)', () async {
await testBigBlog(1500000);
});
test('big blob 2 Mo (fails on Android sqlite)', () async {
await testBigBlog(2000000);
});
test('big blob 15 Mo (fails on Android sqlite)', () async {
await testBigBlog(15000000);
});
*/
/*
test('Isolate', () async {
// This test does not work yet
// Need background registration. I Kept the code for future reference
await Future.sync(() async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('isolate.db');
// Open the db in the main isolate
Database db =
await openDatabase(path, version: 1, onCreate: (db, version) {
db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
});
try {
await insert(db, 1);
expect(await db.rawQuery('SELECT id, name FROM Test'), [
{'id': 1, 'name': 'item 1'}
]);
// Keep it open and run the isolate
final receivePort = ReceivePort();
await Isolate.spawn(simpleInsertQueryIsolate, receivePort.sendPort);
int index = 0;
SendPort sendPort;
List<Map<String, Object?>> results;
var completer = Completer<void>();
var subscription = receivePort.listen((data) {
switch (index++) {
case 0:
// first is the port to send
sendPort = data as SendPort;
// Send path
sendPort.send(path);
break;
case 1:
// second is result
results = data as List<Map<String, Object?>>;
completer.complete();
break;
}
});
await completer.future;
await subscription?.cancel();
print(results);
expect(results, {});
// Query again in main isolate
expect(await db.rawQuery('SELECT id, name FROM Test'), {});
} finally {
await db.close();
}
}).timeout(Duration(seconds: 3));
});
*/
test('missing parameter', () async {
var db = await openDatabase(inMemoryDatabasePath);
await db.execute(
'CREATE TABLE IF NOT EXISTS foo (id int primary key, name text)');
var missingParameterShouldFail = !supportsCompatMode;
try {
await db.rawQuery('SELECT * FROM foo WHERE id=?');
} catch (e) {
expect(missingParameterShouldFail, isTrue);
}
await db.close();
});
// Issue https://github.com/tekartik/sqflite/issues/929
// Pragma has to use rawQuery...why, on sqflite Android
test('wal', () async {
// await Sqflite.devSetDebugModeOn(true);
var db = await openDatabase(inMemoryDatabasePath);
try {
await db.execute('PRAGMA journal_mode=WAL');
} catch (e) {
print(e);
await db.rawQuery('PRAGMA journal_mode=WAL');
}
await db.execute('CREATE TABLE test (id INTEGER)');
await db.insert('test', <String, Object?>{'id': 1});
try {
var resultSet = await db.rawQuery('SELECT id FROM test');
expect(resultSet, [
{'id': 1},
]);
} finally {
await db.close();
}
});
}
}
/// Insert a record with a given id.
Future insert(Database db, int id) async {
await db.insert('Test', {'id': id, 'name': 'item $id'});
}
/// Open, insert and query for isolate testing.
Future simpleInsertQueryIsolate(SendPort sendPort) async {
final receivePort = ReceivePort();
// First share our receive port
sendPort.send(receivePort.sendPort);
// Get the path
final path = await receivePort.first as String;
final db = await openDatabase(path, version: 1, onCreate: (db, version) {
db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
});
List<Map<String, Object?>> results;
try {
await insert(db, 2);
results = await db.rawQuery('SELECT id, name FROM Test');
print(results);
} finally {
await db.close();
}
// Done send the result
sendPort.send(results);
}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:sqflite_example/batch_test_page.dart';
import 'package:sqflite_example/deprecated_test_page.dart';
import 'package:sqflite_example/exception_test_page.dart';
import 'package:sqflite_example/exp_test_page.dart';
import 'package:sqflite_example/manual_test_page.dart';
import 'package:sqflite_example/src/dev_utils.dart';
import 'model/main_item.dart';
import 'open_test_page.dart';
import 'raw_test_page.dart';
import 'slow_test_page.dart';
import 'src/main_item_widget.dart';
import 'todo_test_page.dart';
import 'type_test_page.dart';
void main() {
mainExampleApp();
}
/// Example app main entry point, exported for external application
///
/// might move to a different shared package.
void mainExampleApp() {
WidgetsFlutterBinding.ensureInitialized();
// debugAutoStartRouteName = testOpenRoute;
runApp(const SqfliteExampleApp());
}
/// Sqflite test app
class SqfliteExampleApp extends StatefulWidget {
/// test app.
const SqfliteExampleApp({super.key});
// This widget is the root of your application.
@override
// ignore: library_private_types_in_public_api
_SqfliteExampleAppState createState() => _SqfliteExampleAppState();
}
/// Simple test page.
const String testRawRoute = '/test/simple';
/// Open test page.
const String testOpenRoute = '/test/open';
/// Slow test page.
const String testSlowRoute = '/test/slow';
/// Type test page.
const String testTypeRoute = '/test/type';
/// Batch test page.
const String testBatchRoute = '/test/batch';
/// `todo` example test page.
const String testTodoRoute = '/test/todo';
/// Exception test page.
const String testExceptionRoute = '/test/exception';
/// Manual test page.
const String testManualRoute = '/test/manual';
/// Experiment test page.
const String testExpRoute = '/test/exp';
/// Deprecated test page.
const String testDeprecatedRoute = '/test/deprecated';
class _SqfliteExampleAppState extends State<SqfliteExampleApp> {
var routes = <String, WidgetBuilder>{
'/test': (BuildContext context) => MyHomePage(),
testRawRoute: (BuildContext context) => RawTestPage(),
testOpenRoute: (BuildContext context) => OpenTestPage(),
testSlowRoute: (BuildContext context) => SlowTestPage(),
testTodoRoute: (BuildContext context) => TodoTestPage(),
testTypeRoute: (BuildContext context) => TypeTestPage(),
testManualRoute: (BuildContext context) => const ManualTestPage(),
testBatchRoute: (BuildContext context) => BatchTestPage(),
testExceptionRoute: (BuildContext context) => ExceptionTestPage(),
testExpRoute: (BuildContext context) => ExpTestPage(),
testDeprecatedRoute: (BuildContext context) => DeprecatedTestPage(),
};
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sqflite Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with 'flutter run'. You'll see
// the application has a blue toolbar. Then, without quitting
// the app, try changing the primarySwatch below to Colors.green
// and then invoke 'hot reload' (press 'r' in the console where
// you ran 'flutter run', or press Run > Hot Reload App in IntelliJ).
// Notice that the counter didn't reset back to zero -- the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Sqflite Demo Home Page'),
routes: routes);
}
}
/// App home menu page.
class MyHomePage extends StatefulWidget {
/// App home menu page.
MyHomePage({super.key, this.title}) {
_items.add(
MainItem('Raw tests', 'Raw SQLite operations', route: testRawRoute));
_items.add(MainItem('Open tests', 'Open onCreate/onUpgrade/onDowngrade',
route: testOpenRoute));
_items
.add(MainItem('Type tests', 'Test value types', route: testTypeRoute));
_items.add(MainItem('Batch tests', 'Test batch operations',
route: testBatchRoute));
_items.add(
MainItem('Slow tests', 'Lengthy operations', route: testSlowRoute));
_items.add(MainItem(
'Todo database example', 'Simple Todo-like database usage example',
route: testTodoRoute));
_items.add(MainItem('Exp tests', 'Experimental and various tests',
route: testExpRoute));
_items.add(MainItem('Exception tests', 'Tests that trigger exceptions',
route: testExceptionRoute));
_items.add(MainItem('Manual tests', 'Tests that requires manual execution',
route: testManualRoute));
_items.add(MainItem('Deprecated test',
'Keeping some old tests for deprecated functionalities',
route: testDeprecatedRoute));
// Uncomment to view all logs
//Sqflite.devSetDebugModeOn(true);
}
final List<MainItem> _items = [];
/// Page title.
final String? title;
@override
// ignore: library_private_types_in_public_api
_MyHomePageState createState() => _MyHomePageState();
}
String? _debugAutoStartRouteName;
/// (debug) set the route to start with.
String? get debugAutoStartRouteName => _debugAutoStartRouteName;
/// Deprecated to avoid calls
@Deprecated('Deb only')
set debugAutoStartRouteName(String? routeName) =>
_debugAutoStartRouteName = routeName;
class _MyHomePageState extends State<MyHomePage> {
int get _itemCount => widget._items.length;
@override
void initState() {
super.initState();
Future<void>.delayed(Duration.zero).then((_) async {
if (mounted) {
// Use it to auto start a test page
if (debugAutoStartRouteName != null) {
// only once
// await Navigator.of(context).pushNamed(testExpRoute);
// await Navigator.of(context).pushNamed(testRawRoute);
final future =
Navigator.of(context).pushNamed(debugAutoStartRouteName!);
// ignore: deprecated_member_use_from_same_package
debugAutoStartRouteName = null;
await future;
// await Navigator.of(context).pushNamed(testExceptionRoute);
}
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Center(
child: Text('Sqflite demo', textAlign: TextAlign.center)),
),
body:
ListView.builder(itemBuilder: _itemBuilder, itemCount: _itemCount));
}
//new Center(child: new Text('Running on: $_platformVersion\n')),
Widget _itemBuilder(BuildContext context, int index) {
return MainItemWidget(widget._items[index], (MainItem item) {
Navigator.of(context).pushNamed(item.route!);
});
}
}
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
// ignore: implementation_imports
import 'package:sqflite/src/factory_mixin.dart' as impl;
import 'package:sqflite/utils/utils.dart';
import 'package:sqflite_example/src/item_widget.dart';
import 'package:sqflite_example/utils.dart';
// ignore_for_file: avoid_print
import 'model/item.dart';
import 'src/common_import.dart';
/// Manual test page.
class ManualTestPage extends StatefulWidget {
/// Test page.
const ManualTestPage({super.key});
@override
// ignore: library_private_types_in_public_api
_ManualTestPageState createState() => _ManualTestPageState();
}
class _ManualTestPageState extends State<ManualTestPage> {
Database? database;
static const String dbName = 'manual_test.db';
Future<void> showToast(String message) async {
ScaffoldMessenger.of(context)
..clearSnackBars()
..showSnackBar(SnackBar(
content: Text(message), duration: const Duration(milliseconds: 300)));
}
Future<Database> _openDatabase() async {
return database ??= await databaseFactory.openDatabase(dbName);
}
Future _closeDatabase() async {
await database?.close();
database = null;
}
Future _deleteDatabase() async {
await databaseFactory.deleteDatabase(dbName);
}
Future _incrementVersion() async {
final version = await database!.getVersion();
print('version $version');
await database!.setVersion(version + 1);
}
late List<SqfMenuItem> items;
late List<ItemWidget> itemWidgets;
Future<void> _addAndQuery({int? msDelay, bool? noSynchronized}) async {
// await databaseFactory.debugSetLogLevel(sqfliteLogLevelVerbose);
var db = await _openDatabase();
// ignore: invalid_use_of_visible_for_testing_member
db.internalsDoNotUseSynchronized = noSynchronized ?? false;
await db.transaction((txn) async {
await txn.execute(
'CREATE TABLE IF NOT EXISTS Task(id INTEGER PRIMARY KEY, name TEXT)');
await txn.execute('INSERT INTO Task(name) VALUES (?)',
['task ${DateTime.now().toIso8601String()}']);
var count =
firstIntValue(await txn.query('Task', columns: [sqlCountColumn]));
unawaited(showToast('$count task(s)'));
if (msDelay != null) {
await Future<void>.delayed(Duration(milliseconds: msDelay));
}
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
items = <SqfMenuItem>[
SqfMenuItem('SQLite version', () async {
final db = await openDatabase(inMemoryDatabasePath);
final results = await db.rawQuery('select sqlite_version()');
print('select sqlite_version(): $results');
var version = results.first.values.first;
print('sqlite version: $version');
await db.close();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('select sqlite_version(): $version'),
));
}
}, summary: 'select sqlite_version()'),
SqfMenuItem('Factory information', () async {
var info = databaseFactory.toString();
print('sqlite database factory: $info');
unawaited(showToast(info));
}, summary: 'toString()'),
SqfMenuItem('openDatabase', () async {
await _openDatabase();
}, summary: 'Open the database'),
SqfMenuItem('transaction add and query and pause', () async {
await _addAndQuery(msDelay: 5000);
}, summary: 'open/create table/add/query/pause'),
SqfMenuItem('transaction add and query and pause no synchronized',
() async {
await _addAndQuery(msDelay: 5000, noSynchronized: true);
}, summary: 'open/create table/add/query/pause'),
SqfMenuItem('BEGIN EXCLUSIVE', () async {
final db = await _openDatabase();
await db.execute('BEGIN EXCLUSIVE');
},
summary:
'Execute than exit or hot-restart the application. Open the database if needed'),
SqfMenuItem('close', () async {
await _closeDatabase();
},
summary:
'Execute after starting then exit the app using the back button on Android and restart from the launcher.'),
SqfMenuItem('delete', () async {
await _deleteDatabase();
},
summary:
'Try open (then optionally) delete, exit or hot-restart then delete then open'),
SqfMenuItem('log level: none', () async {
// ignore: deprecated_member_use
await Sqflite.devSetOptions(
// ignore: deprecated_member_use
SqfliteOptions(logLevel: sqfliteLogLevelNone));
}, summary: 'No logs'),
SqfMenuItem('log level: sql', () async {
// ignore: deprecated_member_use
await Sqflite.devSetOptions(
// ignore: deprecated_member_use
SqfliteOptions(logLevel: sqfliteLogLevelSql));
}, summary: 'Log sql command and basic database operation'),
SqfMenuItem('log level: verbose', () async {
// ignore: deprecated_member_use
await Sqflite.devSetOptions(
// ignore: deprecated_member_use
SqfliteOptions(logLevel: sqfliteLogLevelVerbose));
}, summary: 'Verbose logs, for debugging'),
SqfMenuItem('Get info', () async {
final factory = databaseFactory as impl.SqfliteDatabaseFactoryMixin;
final info = await factory.getDebugInfo();
print(info.toString());
}, summary: 'Implementation info (dev only)'),
SqfMenuItem('Increment version', () async {
print(await _incrementVersion());
}, summary: 'Implementation info (dev only)'),
SqfMenuItem('Multiple db', () async {
await Navigator.of(context).push<void>(MaterialPageRoute(builder: (_) {
return const MultipleDbTestPage();
}));
}, summary: 'Open multiple databases'),
...[800000, 1500000, 15000000, 150000000]
.map((size) => SqfMenuItem('Big blob $size', () async {
await testBigBlog(size);
}))
];
}
Future<void> testBigBlog(int size) async {
// await Sqflite.devSetDebugModeOn(true);
var db = await openDatabase(inMemoryDatabasePath, version: 1,
onCreate: (Database db, int version) async {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value BLOB)');
});
try {
var blob =
Uint8List.fromList(List.generate(size, (index) => index % 256));
var id = await db.insert('Test', {'value': blob});
/// Get the value field from a given id
Future<Uint8List> getValue(int id) async {
return ((await db.query('Test', where: 'id = $id')).first)['value']
as Uint8List;
}
var ok = (await getValue(id)).length == blob.length;
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('$size: $ok')));
}
} finally {
await db.close();
}
}
@override
Widget build(BuildContext context) {
itemWidgets = items
.map((item) => ItemWidget(
item,
(item) async {
final stopwatch = Stopwatch()..start();
final future = (item as SqfMenuItem).run();
setState(() {});
await future;
// always add a small delay
final elapsed = stopwatch.elapsedMilliseconds;
if (elapsed < 300) {
await sleep(300 - elapsed);
}
setState(() {});
},
summary: item.summary,
))
.toList(growable: false);
return Scaffold(
appBar: AppBar(
title: const Text('Manual tests'),
),
body: ListView(
children: itemWidgets,
),
);
}
}
/// Multiple db test page.
class MultipleDbTestPage extends StatelessWidget {
/// Test page.
const MultipleDbTestPage({super.key});
@override
Widget build(BuildContext context) {
Widget dbTile(String name) {
return ListTile(
title: Text(name),
onTap: () {
Navigator.of(context).push<void>(MaterialPageRoute(builder: (_) {
return SimpleDbTestPage(
dbName: name,
);
}));
},
);
}
return Scaffold(
appBar: AppBar(
title: const Text('Multiple databases'),
),
body: ListView(
children: <Widget>[
dbTile('data1.db'),
dbTile('data2.db'),
dbTile('data3.db')
],
),
);
}
}
/// Simple db test page.
class SimpleDbTestPage extends StatefulWidget {
/// Simple db test page.
const SimpleDbTestPage({super.key, required this.dbName});
/// db name.
final String dbName;
@override
// ignore: library_private_types_in_public_api
_SimpleDbTestPageState createState() => _SimpleDbTestPageState();
}
class _SimpleDbTestPageState extends State<SimpleDbTestPage> {
Database? database;
Future<Database> _openDatabase() async {
// await Sqflite.devSetOptions(SqfliteOptions(logLevel: sqfliteLogLevelVerbose));
return database ??= await databaseFactory.openDatabase(widget.dbName,
options: OpenDatabaseOptions(
version: 1,
onCreate: (db, version) async {
await db.execute('CREATE TABLE Test (value TEXT)');
}));
}
Future _closeDatabase() async {
await database?.close();
database = null;
}
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Simple db ${widget.dbName}'),
),
body: Builder(
builder: (context) {
Widget menuItem(String title, void Function() onTap,
{String? summary}) {
return ListTile(
title: Text(title),
subtitle: summary == null ? null : Text(summary),
onTap: onTap,
);
}
Future countRecord() async {
final db = await _openDatabase();
final result =
firstIntValue(await db.query('test', columns: ['COUNT(*)']));
// Temp for nnbd successful lint
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('$result records'),
duration: const Duration(milliseconds: 700),
));
}
}
final items = <Widget>[
menuItem('open Database', () async {
await _openDatabase();
}, summary: 'Open the database'),
menuItem('Add record', () async {
final db = await _openDatabase();
await db.insert('test', {'value': 'some_value'});
await countRecord();
}, summary: 'Add one record. Open the database if needed'),
menuItem('Count record', () async {
await countRecord();
}, summary: 'Count records. Open the database if needed'),
menuItem(
'Close Database',
() async {
await _closeDatabase();
},
),
menuItem(
'Delete database',
() async {
await databaseFactory.deleteDatabase(widget.dbName);
},
),
];
return ListView(
children: items,
);
},
));
}
}
import 'package:sqflite_example/src/common_import.dart';
/// Item states.
enum ItemState {
/// test not run yet.
none,
/// test is running.
running,
/// test succeeded.
success,
/// test fails.
failure
}
/// Menu item.
class Item {
/// Menu item.
Item(this.name);
/// Menu item state.
ItemState state = ItemState.running;
/// Menu item name/
String name;
}
/// Menu item implementation.
class SqfMenuItem extends Item {
/// Menu item implementation.
SqfMenuItem(super.name, this.body, {this.summary}) {
state = ItemState.none;
}
/// Summary.
String? summary;
/// Run the item.
Future run() {
state = ItemState.running;
return Future<void>.delayed(const Duration()).then((_) async {
try {
await body();
state = ItemState.success;
} catch (e) {
state = ItemState.failure;
rethrow;
}
});
}
/// Menu item body.
final FutureOr Function() body;
}
/// Main item.
class MainItem {
/// Main item.
MainItem(this.title, this.description, {this.route});
/// Title.
String title;
/// Description.
String description;
/// Page route.
String? route;
}
/// Main route item.
class MainRouteItem {
/// Main route item.
MainRouteItem(this.title, this.description, {this.route});
/// Title.
String title;
/// Description.
String description;
/// Page route.
MainRouteItem? route;
}
import 'dart:async';
/// Test definition.
class Test {
/// Test definition.
Test(this.name, this.fn, {bool? solo, bool? skip})
: solo = solo == true,
skip = skip == true;
/// Only run this test.
final bool solo;
/// Skip this test.
final bool skip;
/// Test name.
String name;
/// Test body.
FutureOr Function() fn;
}
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite/src/database_mixin.dart' // ignore: implementation_imports
show
SqfliteDatabaseMixin;
import 'package:sqflite/src/factory_mixin.dart' // ignore: implementation_imports
show
SqfliteDatabaseFactoryMixin;
import 'package:sqflite_example/utils.dart';
import 'package:synchronized/synchronized.dart';
import 'src/common_import.dart';
import 'test_page.dart';
// ignore_for_file: avoid_slow_async_io
// ignore_for_file: avoid_print
/// Open callbacks.
class OpenCallbacks {
/// Open callbacks.
OpenCallbacks() {
onConfigure = (Database db) {
// devPrint('onConfigure');
//verify(!onConfigureCalled, 'onConfigure must be called once');
expect(onConfigureCalled, false,
reason:
'onConfigure already called'); // onConfigure must be called once
onConfigureCalled = true;
};
onCreate = (Database db, int version) {
//print('onCreate');
expect(onConfigureCalled, true, reason: 'onConfigure not called');
expect(onCreateCalled, false, reason: 'onCreate already called');
onCreateCalled = true;
};
onOpen = (Database db) {
//print('onOpen');
verify(onConfigureCalled, 'onConfigure must be called before onOpen');
verify(!onOpenCalled!, 'onOpen already called');
onOpenCalled = true;
};
onUpgrade = (Database db, int oldVersion, int newVersion) {
verify(onConfigureCalled, 'onConfigure not called in onUpgrade');
verify(!onUpgradeCalled!, 'onUpgradeCalled already called');
onUpgradeCalled = true;
};
onDowngrade = (Database db, int oldVersion, int newVersion) {
verify(onConfigureCalled, 'onConfigure not called');
verify(!onDowngradeCalled!, 'onDowngrade already called');
onDowngradeCalled = true;
};
reset();
}
/// true when onConfigure is called.
bool? onConfigureCalled;
/// true when onOpen is called.
bool? onOpenCalled;
/// true when onCreate is called.
bool? onCreateCalled;
/// true when onDowngrade is called.
bool? onDowngradeCalled;
/// true when onUpgrade is called.
bool? onUpgradeCalled;
/// onCreate callback.
late OnDatabaseCreateFn onCreate;
/// onConfigure callback.
OnDatabaseConfigureFn? onConfigure;
/// onDowngrade callback.
late OnDatabaseVersionChangeFn onDowngrade;
/// onUpgrade callback.
late OnDatabaseVersionChangeFn onUpgrade;
/// onOpen callback.
late OnDatabaseOpenFn onOpen;
/// reset callbacks called information.
void reset() {
onConfigureCalled = false;
onOpenCalled = false;
onCreateCalled = false;
onDowngradeCalled = false;
onUpgradeCalled = false;
}
/// open the database.
Future<Database> open(String path, {required int version}) async {
reset();
return await databaseFactory.openDatabase(path,
options: OpenDatabaseOptions(
version: version,
onCreate: onCreate,
onConfigure: onConfigure!,
onDowngrade: onDowngrade,
onUpgrade: onUpgrade,
onOpen: onOpen));
}
}
/// Check if a file is a valid database file
///
/// An empty file is a valid empty sqlite file
Future<bool> isDatabase(String path) async {
Database? db;
var isDatabase = false;
try {
db = await openReadOnlyDatabase(path);
await db.getVersion();
isDatabase = true;
} catch (_) {
} finally {
await db?.close();
}
return isDatabase;
}
/// Open test page.
class OpenTestPage extends TestPage {
/// Open test page.
OpenTestPage({Key? key}) : super('Open tests', key: key) {
final factory = databaseFactory;
test('Databases path', () async {
// await Sqflite.devSetDebugModeOn(false);
final databasesPath = await factory.getDatabasesPath();
// On Android we know it is current a 'databases' folder in the package folder
print('databasesPath: $databasesPath');
if (platform.isAndroid) {
expect(basename(databasesPath), 'databases');
} else if (platform.isIOS) {
expect(basename(databasesPath), 'Documents');
}
final path = join(databasesPath, 'in_default_directory.db');
await factory.deleteDatabase(path);
final db = await factory.openDatabase(path);
await db.close();
});
test('Delete database', () async {
// await Sqflite.devSetDebugModeOn(false);
final path = await initDeleteDb('delete_database.db');
expect(await databaseExists(path), false);
final db = await openDatabase(path);
await db.close();
expect((await pathExists(path)), true);
expect(await databaseExists(path), true);
print('Deleting database $path');
await deleteDatabase(path);
expect((await pathExists(path)), false);
expect(await databaseExists(path), false);
});
test('Open no version', () async {
//await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('open_no_version.db');
expect((await pathExists(path)), false);
final db = await openDatabase(path);
verify(await pathExists(path));
expect(await db.getVersion(), 0);
await db.close();
});
test('isOpen', () async {
//await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('is_open.db');
expect((await pathExists(path)), false);
final db = await openDatabase(path);
expect(db.isOpen, true);
verify(await pathExists(path));
await db.close();
expect(db.isOpen, false);
});
test('Open no version onCreate', () async {
// should fail
final path = await initDeleteDb('open_no_version_on_create.db');
verify(!(await pathExists(path)));
Database? db;
try {
db = await openDatabase(path, onCreate: (Database db, int version) {
// never called
verify(false);
});
verify(false);
} on ArgumentError catch (_) {}
verify(!await pathExists(path));
expect(db, null);
});
test('Open onCreate', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('open_test2.db');
var onCreate = false;
var onCreateTransaction = false;
final db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
expect(version, 1);
onCreate = true;
await db.transaction((txn) async {
await txn.execute('CREATE TABLE Test2 (id INTEGER PRIMARY KEY)');
onCreateTransaction = true;
});
});
verify(onCreate);
expect(onCreateTransaction, true);
expect(await db.getVersion(), 1);
await db.close();
});
test('Simple onCreate', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('open_simple_on_create.db');
expect(await isDatabase(path), isFalse);
final db =
await openDatabase(path, version: 1, onCreate: (db, version) async {
final batch = db.batch();
batch.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, text NAME)');
await batch.commit();
});
try {
expect(
await db.rawInsert('INSERT INTO Test (text) VALUES (?)', ['test']),
1);
final result = await db.query('Test');
final expected = [
{'id': 1, 'text': 'test'}
];
expect(result, expected);
expect(await isDatabase(path), isTrue);
} finally {
await db.close();
}
expect(await isDatabase(path), isTrue);
});
test('Open 2 databases', () async {
//await Sqflite.devSetDebugModeOn(true);
final path1 = await initDeleteDb('open_db_1.db');
final path2 = await initDeleteDb('open_db_2.db');
final db1 = await openDatabase(path1, version: 1);
final db2 = await openDatabase(path2, version: 1);
await db1.close();
await db2.close();
});
test('Open onUpgrade', () async {
// await Sqflite.devSetDebugModeOn(true);
var onUpgrade = false;
final path = await initDeleteDb('open_on_upgrade.db');
var database = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db.execute('CREATE TABLE Test(id INTEGER PRIMARY KEY)');
});
try {
await database
.insert('Test', <String, Object?>{'id': 1, 'name': 'test'});
fail('should fail');
} on DatabaseException catch (e) {
print(e);
}
expect(await database.getVersion(), 1);
await database.close();
database = await openDatabase(path, version: 2,
onUpgrade: (Database db, int oldVersion, int newVersion) async {
expect(oldVersion, 1);
expect(newVersion, 2);
await db.execute('ALTER TABLE Test ADD name TEXT');
onUpgrade = true;
});
verify(onUpgrade);
expect(await database.getVersion(), 2);
try {
expect(
await database
.insert('Test', <String, Object?>{'id': 1, 'name': 'test'}),
1);
} finally {
await database.close();
}
});
test('Open onDowngrade', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('open_on_downgrade.db');
var database = await openDatabase(path, version: 2,
onCreate: (Database db, int version) async {
await db.execute('CREATE TABLE Test(id INTEGER PRIMARY KEY)');
}, onDowngrade: (Database db, int oldVersion, int newVersion) async {
verify(false, 'should not be called');
});
expect(await database.getVersion(), 2);
await database.close();
var onDowngrade = false;
database = await openDatabase(path, version: 1,
onDowngrade: (Database db, int oldVersion, int newVersion) async {
expect(oldVersion, 2);
expect(newVersion, 1);
await db.execute('ALTER TABLE Test ADD name TEXT');
onDowngrade = true;
});
verify(onDowngrade);
expect(await database.getVersion(), 1);
await database.close();
});
test('Open bad path', () async {
try {
await openDatabase('/invalid_path');
fail();
} on DatabaseException catch (e) {
verify(e.isOpenFailedError());
}
});
test('Open asset database', () async {
// await Sqflite.devSetDebugModeOn(false);
final databasesPath = await getDatabasesPath();
final path = join(databasesPath, 'asset_example.db');
// delete existing if any
await deleteDatabase(path);
// Make sure the parent directory exists
try {
await createDirectory(path);
} catch (_) {}
// Copy from asset to a database file.
final data = await rootBundle.load(url.join('assets', 'example.db'));
final bytes =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await databaseFactory.writeDatabaseBytes(path, bytes);
// open the database
final db = await openDatabase(path);
// Our database as a single table with a single element
final list = await db.rawQuery('SELECT * FROM Test');
print('list $list');
expect(list.first['name'], 'simple value');
await db.close();
});
test('Open on configure', () async {
final path = await initDeleteDb('open_on_configure.db');
var onConfigured = false;
var onConfiguredTransaction = false;
Future onConfigure(Database db) async {
onConfigured = true;
await db.execute('CREATE TABLE Test1 (id INTEGER PRIMARY KEY)');
await db.transaction((txn) async {
await txn.execute('CREATE TABLE Test2 (id INTEGER PRIMARY KEY)');
onConfiguredTransaction = true;
});
}
final db = await openDatabase(path, onConfigure: onConfigure);
expect(onConfigured, true);
expect(onConfiguredTransaction, true);
await db.close();
});
test('Open onDowngrade delete', () async {
// await Sqflite.devSetDebugModeOn(false);
final path = await initDeleteDb('open_on_downgrade_delete.db');
var database = await openDatabase(path, version: 3,
onCreate: (Database db, int version) async {
await db.execute('CREATE TABLE Test(id INTEGER PRIMARY KEY)');
});
await database.close();
// should fail going back in versions
var onCreated = false;
var onOpened = false;
var onConfiguredOnce = false; // onConfigure will be called twice here
// since the database is re-opened
var onConfigured = false;
database =
await openDatabase(path, version: 2, onConfigure: (Database db) {
// Must not be configured nor created yet
verify(!onConfigured);
verify(!onCreated);
if (!onConfiguredOnce) {
// first time
onConfiguredOnce = true;
} else {
onConfigured = true;
}
}, onCreate: (Database db, int version) {
verify(onConfigured);
verify(!onCreated);
verify(!onOpened);
onCreated = true;
expect(version, 2);
}, onOpen: (Database db) {
verify(onCreated);
onOpened = true;
}, onDowngrade: onDatabaseDowngradeDelete);
await database.close();
expect(onCreated, true);
expect(onOpened, true);
expect(onConfigured, true);
onCreated = false;
onOpened = false;
database = await openDatabase(path, version: 2,
onCreate: (Database db, int version) {
expect(false, 'should not be called');
}, onOpen: (Database db) {
onOpened = true;
}, onDowngrade: onDatabaseDowngradeDelete);
expect(onOpened, true);
await database.close();
});
test('All open callback', () async {
// await Sqflite.devSetDebugModeOn(false);
final path = await initDeleteDb('open_all_callbacks.db');
var step = 1;
final openCallbacks = OpenCallbacks();
var db = await openCallbacks.open(path, version: 1);
try {
verify(openCallbacks.onConfigureCalled, 'onConfiguredCalled $step');
verify(openCallbacks.onCreateCalled, 'onCreateCalled $step');
verify(openCallbacks.onOpenCalled, 'onOpenCalled $step');
verify(!openCallbacks.onUpgradeCalled!, 'onUpgradeCalled $step');
verify(!openCallbacks.onDowngradeCalled!, 'onDowngradeCalled $step');
await db.close();
++step;
db = await openCallbacks.open(path, version: 3);
verify(openCallbacks.onConfigureCalled, 'onConfiguredCalled $step');
verify(!openCallbacks.onCreateCalled!, 'onCreateCalled $step');
verify(openCallbacks.onOpenCalled, 'onOpenCalled $step');
verify(openCallbacks.onUpgradeCalled, 'onUpgradeCalled $step');
verify(!openCallbacks.onDowngradeCalled!, 'onDowngradeCalled $step');
await db.close();
++step;
// devPrint('downgrading');
db = await openCallbacks.open(path, version: 2);
verify(openCallbacks.onConfigureCalled, 'onConfiguredCalled $step');
verify(!openCallbacks.onCreateCalled!, 'onCreateCalled $step');
verify(openCallbacks.onOpenCalled, 'onOpenCalled $step');
verify(!openCallbacks.onUpgradeCalled!, 'onDowngradeCalled $step');
verify(openCallbacks.onDowngradeCalled, 'onDowngradCalled $step');
await db.close();
// devPrint('downgrading delete');
openCallbacks.onDowngrade = onDatabaseDowngradeDelete;
var configureCount = 0;
final callback = openCallbacks.onConfigure;
// allow being called twice
openCallbacks.onConfigure = (Database db) {
if (configureCount == 1) {
openCallbacks.onConfigureCalled = false;
}
configureCount++;
callback!(db);
};
++step;
db = await openCallbacks.open(path, version: 1);
/*
verify(openCallbacks.onConfigureCalled,'onConfiguredCalled $step');
verify(configureCount == 2, 'onConfigure count');
verify(openCallbacks.onCreateCalled, 'onCreateCalled $step');
verify(openCallbacks.onOpenCalled, 'onOpenCalled $step');
verify(!openCallbacks.onUpgradeCalled, 'onUpgradeCalled $step');
verify(!openCallbacks.onDowngradeCalled, 'onDowngradCalled $step');
*/
} finally {
await db.close();
}
});
test('Open batch', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('open_batch.db');
Future onConfigure(Database db) async {
final batch = db.batch();
batch.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)');
await batch.commit();
}
Future onCreate(Database db, int version) async {
final batch = db.batch();
// await db.execute('INSERT INTO Test(value) VALUES("value1")'); This does not work using ffi!
batch.execute('INSERT INTO Test(value) VALUES(?)', ['value1']);
await batch.commit();
}
Future onOpen(Database db) async {
final batch = db.batch();
//batch.rawInsert('INSERT INTO Test(value) VALUES("value2")');
batch.rawInsert('INSERT INTO Test(value) VALUES(?)', ['value2']);
await batch.commit();
}
final db = await openDatabase(path,
version: 1,
onConfigure: onConfigure,
onCreate: onCreate,
onOpen: onOpen);
expect(
Sqflite.firstIntValue(await db.rawQuery('SELECT COUNT(*) FROM Test')),
2);
await db.close();
});
test('Open read-only', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('open_read_only.db');
Future onCreate(Database db, int version) async {
final batch = db.batch();
batch.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)');
//batch.rawInsert('INSERT INTO Test(value) VALUES("value1")'); This does not work using ffi
batch.rawInsert('INSERT INTO Test(value) VALUES(?)', ['value1']);
await batch.commit();
}
var db = await openDatabase(path, version: 1, onCreate: onCreate);
expect(
Sqflite.firstIntValue(await db.rawQuery('SELECT COUNT(*) FROM Test')),
1);
await db.close();
db = await openReadOnlyDatabase(path);
expect(
Sqflite.firstIntValue(await db.rawQuery('SELECT COUNT(*) FROM Test')),
1);
try {
await db.rawInsert('INSERT INTO Test(value) VALUES(?)', ['value1']);
fail('should fail');
} on DatabaseException catch (e) {
// Error DatabaseException(attempt to write a readonly database (code 8)) running Open read-only
expect(e.isReadOnlyError(), true);
}
final batch = db.batch();
batch.rawQuery('SELECT COUNT(*) FROM Test');
await batch.commit();
await db.close();
});
test('Open demo (doc)', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('open_read_only.db');
{
Future onConfigure(Database db) async {
// Add support for cascade delete
await db.execute('PRAGMA foreign_keys = ON');
}
final db = await openDatabase(path, onConfigure: onConfigure);
await db.close();
}
{
Future onCreate(Database db, int version) async {
// Database is created, delete the table
await db.execute(
'CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)');
}
Future onUpgrade(Database db, int oldVersion, int newVersion) async {
// Database version is updated, alter the table
await db.execute('ALTER TABLE Test ADD name TEXT');
}
// Special callback used for onDowngrade here to recreate the database
final db = await openDatabase(path,
version: 1,
onCreate: onCreate,
onUpgrade: onUpgrade,
onDowngrade: onDatabaseDowngradeDelete);
await db.close();
}
{
Future onOpen(Database db) async {
// Database is open, print its version
print('db version ${await db.getVersion()}');
}
final db = await openDatabase(
path,
onOpen: onOpen,
);
await db.close();
}
// asset (use existing copy if any
{
// Check if we have an existing copy first
final databasesPath = await getDatabasesPath();
final path = join(databasesPath, 'demo_asset_example.db');
// try opening (will work if it exists)
Database? db;
try {
db = await openDatabase(path, readOnly: true);
} catch (e) {
print('Error $e');
}
if (db == null) {
// Should happen only the first time you launch your application
print('Creating new copy from asset');
// Copy from asset
final data = await rootBundle.load(url.join('assets', 'example.db'));
final bytes =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await writeFileAsBytes(path, bytes);
// open the database
db = await openDatabase(path, readOnly: true);
} else {
print('Opening existing database');
}
await db.close();
}
});
test('Database locked (doc)', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('open_locked.db');
final helper = Helper(path);
// without the synchronized fix, this could faild
for (var i = 0; i < 100; i++) {
// ignore: unawaited_futures
helper.getDb();
}
final db = await helper.getDb();
await db.close();
});
test('single/multi instance (using factory)', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('instances_test.db');
Database? db1, db2, db3;
try {
db1 = await databaseFactory.openDatabase(path,
options: OpenDatabaseOptions(singleInstance: false));
db2 = await databaseFactory.openDatabase(path,
options: OpenDatabaseOptions(singleInstance: true));
db3 = await databaseFactory.openDatabase(path,
options: OpenDatabaseOptions(singleInstance: true));
expect(db1, isNot(db2));
expect(db2, db3);
} finally {
await db1!.close();
await db2!.close();
await db3!.close(); // safe to close the same instance
}
});
test('single/multi instance', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('instances_test.db');
final db1 = await openDatabase(path, singleInstance: false);
final db2 = await openDatabase(path, singleInstance: true);
final db3 = await openDatabase(path, singleInstance: true);
verify(db1 != db2);
verify(db2 == db3);
await db1.close();
await db2.close();
await db3.close(); // safe to close the same instance
});
test('In memory database', () async {
const inMemoryPath =
inMemoryDatabasePath; // tried null without success, as it crashes on Android
const path = inMemoryPath;
var db = await openDatabase(path);
await db
.execute('CREATE TABLE IF NOT EXISTS Test(id INTEGER PRIMARY KEY)');
await db.insert('Test', {'id': 1});
expect(await db.query('Test'), [
{'id': 1}
]);
await db.close();
// reopen, content should be gone
db = await openDatabase(path);
try {
await db.query('Test');
fail('fail');
} on DatabaseException catch (e) {
print(e);
}
await db.close();
});
test('Not in memory database', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('not_in_memory.db');
var db = await openDatabase(path);
await db
.execute('CREATE TABLE IF NOT EXISTS Test(id INTEGER PRIMARY KEY)');
await db.insert('Test', {'id': 1});
expect(await db.query('Test'), [
{'id': 1}
]);
await db.close();
// reopen, content should be done
db = await openDatabase(path);
expect(await db.query('Test'), [
{'id': 1}
]);
await db.close();
});
test('open in sub directory', () async {
final databasesPath = await factory.getDatabasesPath();
final path = join(databasesPath, 'sub_that_should_not_exists');
try {
await deleteDirectory(path);
} catch (_) {}
final dbPath = join(path, 'open.db');
final db = await factory.openDatabase(dbPath);
try {} finally {
await db.close();
}
});
test('open in sub sub directory', () async {
// await Sqflite.devSetDebugModeOn(true);
final databasesPath = await factory.getDatabasesPath();
final path =
join(databasesPath, 'sub2_that_should_not_exists', 'sub_sub');
try {
await deleteDirectory(path);
} catch (_) {}
expect(await existsDirectory(path), false);
final dbPath = join(path, 'open.db');
final db = await factory.openDatabase(dbPath);
try {} finally {
await db.close();
}
});
test('open_close_open_no_wait', () async {
// await Sqflite.devSetDebugModeOn(true);
const path = 'open_close_open_no_wait.db';
final factory = databaseFactory;
await factory.deleteDatabase(path);
final db = await factory.openDatabase(path,
options: OpenDatabaseOptions(version: 1));
try {
expect(await db.getVersion(), 1);
// close no wait
unawaited(db.close());
final db2 = await factory.openDatabase(path,
options: OpenDatabaseOptions(version: 1));
print('$db, $db2');
verify(db != db2);
verify((db as SqfliteDatabaseMixin).id !=
(db2 as SqfliteDatabaseMixin).id);
expect(await db2.getVersion(), 1);
} finally {
await db.close();
}
});
test('close in transaction', () async {
// await Sqflite.devSetDebugModeOn(true);
const path = 'test_close_in_transaction.db';
final factory = databaseFactory;
await factory.deleteDatabase(path);
var db = await factory.openDatabase(path,
options: OpenDatabaseOptions(version: 1));
try {
//await db.getVersion();
await db.execute('BEGIN IMMEDIATE');
await db.close();
db = await factory.openDatabase(path,
options: OpenDatabaseOptions(version: 1));
} finally {
await db.close();
}
});
test('Open in transaction', () async {
// await Sqflite.devSetDebugModeOn(true);
const path = 'test_open_in_transaction.db';
final factory = databaseFactory;
await factory.deleteDatabase(path);
var db = await factory.openDatabase(path,
options: OpenDatabaseOptions(version: 1));
try {
//await db.getVersion();
await db.execute('BEGIN IMMEDIATE');
// Trick to make sure we don't reuse the same instance during open
(factory as SqfliteDatabaseFactoryMixin)
.databaseOpenHelpers
.remove(db.path);
final db2 = await factory.openDatabase(path,
options: OpenDatabaseOptions(version: 1));
print('after open');
verify(db != db2);
expect(
(db as SqfliteDatabaseMixin).id, (db2 as SqfliteDatabaseMixin).id);
//await db.getVersion();
//await db.execute('ROLLBACK');
db = await factory.openDatabase(path,
options: OpenDatabaseOptions(version: 1));
expect(db, db2);
} finally {
await db.close();
}
});
test('Open non sqlite file', () async {
// Kind of corruption simulation
// await Sqflite.devSetDebugModeOn(true);
final factory = databaseFactory;
final path =
join(await factory.getDatabasesPath(), 'test_non_sqlite_file.db');
await factory.deleteDatabase(path);
// Write dummy content
await writeFileAsString(path, 'dummy', flush: true);
// check content
expect(await readFileAsString(path), 'dummy');
// try read-only
{
late Database db;
try {
db = await factory.openDatabase(path,
options: OpenDatabaseOptions(readOnly: true));
} catch (e) {
print('open error');
}
try {
await db.getVersion();
} catch (e) {
print('getVersion error');
}
await db.close();
// check content
expect(await readFileAsString(path), 'dummy');
}
expect(await isDatabase(path), isFalse);
// try read-write
const minExpectedSize = 1000;
expect((await readFileAsBytes(path)).length, lessThan(minExpectedSize));
var db = await factory.openDatabase(path);
var versionShouldFail =
!supportsCompatMode || platform.isIOS || platform.isMacOS;
if (versionShouldFail) {
// On iOS it fails
try {
await db.getVersion();
} catch (e) {
print('getVersion error');
}
} else {
// On Android the database is re-created
await db.getVersion();
}
await db.close();
if (versionShouldFail) {
// On iOS it fails
try {
db = await factory.openDatabase(path,
options: OpenDatabaseOptions(version: 1));
} catch (e) {
print('getVersion error');
}
} else {
db = await factory.openDatabase(path,
options: OpenDatabaseOptions(version: 1));
// On Android the database is re-created
await db.getVersion();
}
await db.close();
if (platform.isAndroid) {
// Content has changed, it is a big file now!
expect(
(await readFileAsBytes(path)).length, greaterThan(minExpectedSize));
}
});
test('Read/write bytes', () async {
var path = await initDeleteDb('database_read_bytes.db');
var writtenPath = await initDeleteDb('database_written_bytes.db');
var db = await factory.openDatabase(path,
options: OpenDatabaseOptions(
version: 1,
onCreate: (db, version) async {
await db.execute(
'CREATE TABLE Test(id INTEGER PRIMARY KEY, value TEXT)');
}));
var textValue = 'value_to_read';
await db.insert('Test', {'id': 1, 'value': textValue});
expect(await db.query('Test'), [
{'id': 1, 'value': textValue}
]);
await db.close();
var bytes = await factory.readDatabaseBytes(path);
//expect(bytes.length, 8192);
expect(bytes.sublist(0, 4), [
83,
81,
76,
105,
]);
await factory.writeDatabaseBytes(writtenPath, bytes);
db = await factory.openDatabase(writtenPath);
expect(await db.query('Test'), [
{'id': 1, 'value': textValue}
]);
await db.close();
});
}
}
/// Open helper.
class Helper {
/// Open helper.
Helper(this.path);
/// Datebase path.
final String path;
Database? _db;
final _lock = Lock();
/// Open the database if not done.
Future<Database> getDb() async {
if (_db == null) {
await _lock.synchronized(() async {
// Check again once entering the synchronized block
_db ??= await openDatabase(path);
});
}
return _db!;
}
}
import 'dart:io' as io;
import 'package:flutter/foundation.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite/utils/utils.dart';
import 'package:sqflite_example/utils.dart';
import 'src/common_import.dart';
import 'test_page.dart';
// ignore_for_file: avoid_print
/// Raw test page.
class RawTestPage extends TestPage {
/// Raw test page.
RawTestPage({Key? key}) : super('Raw tests', key: key) {
test('Simple', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('raw_simple.db');
final db = await openDatabase(path);
try {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
expect(
await db.rawInsert('INSERT INTO Test (name) VALUES (?)', ['test']),
1);
final result = await db.query('Test');
final expected = [
{'id': 1, 'name': 'test'}
];
expect(result, expected);
} finally {
await db.close();
}
});
test('Sqlite version', () async {
final db = await openDatabase(inMemoryDatabasePath);
final results = await db.rawQuery('select sqlite_version()');
print('sqlite version: ${results.first.values.first}');
await db.close();
});
test('Options', () async {
// Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('raw_query_format.db');
final db = await openDatabase(path);
try {
final batch = db.batch();
batch.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
batch.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item 1']);
batch.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item 2']);
await batch.commit();
var sql = 'SELECT id, name FROM Test';
// ignore: deprecated_member_use
var resultSet = await db.devInvokeSqlMethod<Object?>('query', sql);
var expectedResultSetMap = {
'columns': ['id', 'name'],
'rows': [
[1, 'item 1'],
[2, 'item 2']
]
};
print('result as r/c $resultSet');
expect(resultSet, expectedResultSetMap);
// empty
sql = 'SELECT id, name FROM Test WHERE id=1234';
// ignore: deprecated_member_use
resultSet = await db.devInvokeSqlMethod('query', sql);
expectedResultSetMap = {};
print('result as r/c $resultSet');
try {
// This might be just for compatibility
expect(resultSet, expectedResultSetMap);
} catch (e) {
// Allow empty result
expectedResultSetMap = {
'columns': ['id', 'name'],
'rows': []
};
expect(resultSet, expectedResultSetMap);
}
} finally {
await db.close();
}
});
test('Transaction', () async {
//Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('simple_transaction.db');
final db = await openDatabase(path);
try {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
Future testItem(int i) async {
await db.transaction((txn) async {
final count = Sqflite.firstIntValue(
await txn.rawQuery('SELECT COUNT(*) FROM Test'))!;
await Future<void>.delayed(const Duration(milliseconds: 40));
await txn
.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item $i']);
//print(await db.query('SELECT COUNT(*) FROM Test'));
final afterCount = Sqflite.firstIntValue(
await txn.rawQuery('SELECT COUNT(*) FROM Test'));
expect(count + 1, afterCount);
});
}
final futures = <Future>[];
for (var i = 0; i < 4; i++) {
futures.add(testItem(i));
}
await Future.wait(futures);
} finally {
await db.close();
}
});
test('Concurrency 1', () async {
// Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('simple_concurrency_1.db');
final db = await openDatabase(path);
try {
final step1 = Completer<void>();
final step2 = Completer<void>();
final step3 = Completer<void>();
Future action1() async {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
step1.complete();
await step2.future;
try {
await db
.rawQuery('SELECT COUNT(*) FROM Test')
.timeout(const Duration(seconds: 1));
throw 'should fail';
} catch (e) {
expect(e is TimeoutException, true);
}
step3.complete();
}
Future action2() async {
await db.transaction((txn) async {
// Wait for table being created;
await step1.future;
await txn
.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item 1']);
step2.complete();
await step3.future;
final count = Sqflite.firstIntValue(
await txn.rawQuery('SELECT COUNT(*) FROM Test'));
expect(count, 1);
});
}
final future1 = action1();
final future2 = action2();
await Future.wait([future1, future2]);
final count = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM Test'));
expect(count, 1);
} finally {
await db.close();
}
});
test('Concurrency 2', () async {
// Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('simple_concurrency_2.db');
final db = await openDatabase(path);
try {
final step1 = Completer<void>();
final step2 = Completer<void>();
final step3 = Completer<void>();
Future action1() async {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
step1.complete();
await step2.future;
try {
await db
.rawQuery('SELECT COUNT(*) FROM Test')
.timeout(const Duration(seconds: 1));
throw 'should fail';
} catch (e) {
expect(e is TimeoutException, true);
}
step3.complete();
}
Future action2() async {
// This is the change from concurrency 1
// Wait for table being created;
await step1.future;
await db.transaction((txn) async {
// Wait for table being created;
await txn
.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item 1']);
step2.complete();
await step3.future;
final count = Sqflite.firstIntValue(
await txn.rawQuery('SELECT COUNT(*) FROM Test'));
expect(count, 1);
});
}
final future1 = action1();
final future2 = action2();
await Future.wait([future1, future2]);
final count = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM Test'));
expect(count, 1);
} finally {
await db.close();
}
});
test('Transaction recursive', () async {
final path = await initDeleteDb('transaction_recursive.db');
final db = await openDatabase(path);
try {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
// insert then fails to make sure the transaction is cancelled
await db.transaction((txn) async {
await txn.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item 1']);
await txn.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item 2']);
});
final afterCount = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM Test'));
expect(afterCount, 2);
} finally {
await db.close();
}
});
test('Transaction open twice', () async {
//Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('transaction_open_twice.db');
final db = await openDatabase(path);
Database? db2;
try {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
db2 = await openDatabase(path);
await db.transaction((txn) async {
await txn.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item']);
final afterCount = Sqflite.firstIntValue(
await txn.rawQuery('SELECT COUNT(*) FROM Test'));
expect(afterCount, 1);
/*
// this is not working on Android
int db2AfterCount =
Sqflite.firstIntValue(await db2.rawQuery('SELECT COUNT(*) FROM Test'));
assert(db2AfterCount == 0);
*/
});
final db2AfterCount = Sqflite.firstIntValue(
await db2.rawQuery('SELECT COUNT(*) FROM Test'));
expect(db2AfterCount, 1);
final afterCount = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM Test'));
expect(afterCount, 1);
} finally {
await db.close();
await db2?.close();
}
});
if (supportsCompatMode) {
test('Debug mode (log)', () async {
//await Sqflite.devSetDebugModeOn(false);
final path = await initDeleteDb('debug_mode.db');
final db = await openDatabase(path);
try {
// ignore: deprecated_member_use
final debugModeOn = await Sqflite.getDebugModeOn();
// ignore: deprecated_member_use
await Sqflite.setDebugModeOn(true);
await db.setVersion(1);
// ignore: deprecated_member_use
await Sqflite.setDebugModeOn(false);
// this message should not appear
await db.setVersion(2);
// ignore: deprecated_member_use
await Sqflite.setDebugModeOn(true);
await db.setVersion(3);
// restore
// ignore: deprecated_member_use
await Sqflite.setDebugModeOn(debugModeOn);
} finally {
await db.close();
}
});
}
test('Demo', () async {
// await Sqflite.devSetDebugModeOn();
final path = await initDeleteDb('simple_demo.db');
final database = await openDatabase(path);
try {
//int version = await database.update('PRAGMA user_version');
//print('version: ${await database.update('PRAGMA user_version')}');
print('version: ${await database.rawQuery('PRAGMA user_version')}');
//print('drop: ${await database.update('DROP TABLE IF EXISTS Test')}');
await database.execute('DROP TABLE IF EXISTS Test');
print('dropped');
await database.execute(
'CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)');
print('table created');
var id = await database.rawInsert(
// This does not work using ffi
// 'INSERT INTO Test(name, value, num) VALUES("some name",1234,?)',
// [456.789]);
'INSERT INTO Test(name, value, num) VALUES(?,1234,?)',
['some name', 456.789]);
print('inserted1: $id');
id = await database.rawInsert(
'INSERT INTO Test(name, value) VALUES(?, ?)',
['another name', 12345678]);
print('inserted2: $id');
var count = await database.rawUpdate(
'UPDATE Test SET name = ?, value = ? WHERE name = ?',
['updated name', '9876', 'some name']);
print('updated: $count');
expect(count, 1);
var list = await database.rawQuery('SELECT * FROM Test');
var expectedList = <Map>[
{'name': 'updated name', 'id': 1, 'value': 9876, 'num': 456.789},
{'name': 'another name', 'id': 2, 'value': 12345678, 'num': null}
];
print('list: ${json.encode(list)}');
print('expected $expectedList');
expect(list, expectedList);
count = await database
.rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);
print('deleted: $count');
expect(count, 1);
list = await database.rawQuery('SELECT * FROM Test');
expectedList = [
{'name': 'updated name', 'id': 1, 'value': 9876, 'num': 456.789},
];
print('list: ${json.encode(list)}');
print('expected $expectedList');
expect(list, expectedList);
} finally {
await database.close();
}
});
test('Demo clean', () async {
// Get a location
final databasesPath = await getDatabasesPath();
// Make sure the directory exists
try {
if (!kIsWeb) {
// ignore: avoid_slow_async_io
if (!await io.Directory(databasesPath).exists()) {
await io.Directory(databasesPath).create(recursive: true);
}
}
} catch (_) {}
final path = join(databasesPath, 'demo.db');
// Delete the database
await deleteDatabase(path);
// open the database
final database = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
// When creating the db, create the table
await db.execute(
'CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)');
});
// Insert some records in a transaction
await database.transaction((txn) async {
final id1 = await txn.rawInsert(
// 'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)'); This does not work using ffi
'INSERT INTO Test(name, value, num) VALUES(?, 1234, 456.789)',
['some name']);
print('inserted1: $id1');
final id2 = await txn.rawInsert(
'INSERT INTO Test(name, value, num) VALUES(?, ?, ?)',
['another name', 12345678, 3.1416]);
print('inserted2: $id2');
});
// Update some record
var count = await database.rawUpdate(
'UPDATE Test SET name = ?, value = ? WHERE name = ?',
['updated name', '9876', 'some name']);
print('updated: $count');
// Get the records
final list = await database.rawQuery('SELECT * FROM Test');
final expectedList = [
{'name': 'updated name', 'id': 1, 'value': 9876, 'num': 456.789},
{'name': 'another name', 'id': 2, 'value': 12345678, 'num': 3.1416}
];
print(list);
print(expectedList);
//assert(const DeepCollectionEquality().equals(list, expectedList));
expect(list, expectedList);
// Count the records
count = (Sqflite.firstIntValue(
await database.rawQuery('SELECT COUNT(*) FROM Test')))!;
expect(count, 2);
// Delete a record
count = await database
.rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);
expect(count, 1);
// Close the database
await database.close();
});
test('Open twice', () async {
// Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('open_twice.db');
final db = await openDatabase(path);
Database? db2;
try {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
db2 = await openReadOnlyDatabase(path);
final count = Sqflite.firstIntValue(
await db2.rawQuery('SELECT COUNT(*) FROM Test'));
expect(count, 0);
} finally {
await db.close();
await db2?.close();
}
});
test('text primary key', () async {
// Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('text_primary_key.db');
final db = await openDatabase(path);
try {
// This table has no primary key however sqlite generates an hidden row id
await db.execute('CREATE TABLE Test (name TEXT PRIMARY KEY)');
var id = await db.insert('Test', {'name': 'test'});
expect(id, 1);
id = await db.insert('Test', {'name': 'other'});
expect(id, 2);
// row id is not retrieve by default
var list = await db.query('Test');
expect(list, [
{'name': 'test'},
{'name': 'other'}
]);
list = await db.query('Test', columns: ['name', 'rowid']);
expect(list, [
{'name': 'test', 'rowid': 1},
{'name': 'other', 'rowid': 2}
]);
} finally {
await db.close();
}
});
test('Without rowid', () async {
// Sqflite.devSetDebugModeOn(true);
// this fails on iOS
late Database db;
try {
final path = await initDeleteDb('without_rowid.db');
db = await openDatabase(path);
// This table has no primary key and we ask sqlite not to generate
// a rowid
await db
.execute('CREATE TABLE Test (name TEXT PRIMARY KEY) WITHOUT ROWID');
var id = await db.insert('Test', {'name': 'test'});
// it seems to always return 1 on Android, 0 on iOS..., 0 using ffi
var rowIdAlways0 =
(!supportsCompatMode || (platform.isIOS || platform.isMacOS));
if (rowIdAlways0) {
expect(id, 0);
} else {
expect(id, 1);
}
id = await db.insert('Test', {'name': 'other'});
// it seems to always return 1
if (rowIdAlways0) {
expect(id, 0);
} else {
expect(id, 1);
}
// Insert conflict
// Only tested on Android for now...
try {
id = await db.insert('Test', {'name': 'other'});
} on DatabaseException catch (e) {
// Test.name (code 1555 SQLITE_CONSTRAINT_PRIMARYKEY)) sql 'INSERT INTO Test (name) VALUES (?)' args [other] running without rowid
expect(e.getResultCode(), 1555);
}
// notice the order is based on the primary key
final list = await db.query('Test');
expect(list, [
{'name': 'other'},
{'name': 'test'}
]);
} finally {
await db.close();
}
});
test('Reference query', () async {
final path = await initDeleteDb('reference_query.db');
final db = await openDatabase(path);
try {
final batch = db.batch();
batch.execute('CREATE TABLE Other (id INTEGER PRIMARY KEY, name TEXT)');
batch.execute(
'CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, other REFERENCES Other(id))');
batch.rawInsert('INSERT INTO Other (name) VALUES (?)', ['other 1']);
batch.rawInsert(
'INSERT INTO Test (other, name) VALUES (?, ?)', [1, 'item 2']);
await batch.commit();
var result = await db.query('Test',
columns: ['other', 'name'], where: 'other = 1');
print(result);
expect(result, [
{'other': 1, 'name': 'item 2'}
]);
result = await db.query('Test',
columns: ['other', 'name'], where: 'other = ?', whereArgs: [1]);
print(result);
expect(result, [
{'other': 1, 'name': 'item 2'}
]);
} finally {
await db.close();
}
});
test('Binding null (fails on Android)', () async {
final db = await openDatabase(inMemoryDatabasePath);
try {
for (var value in [null, 2]) {
expect(
firstIntValue(await db.rawQuery(
'SELECT CASE WHEN 0 = 1 THEN 1 ELSE ? END', [value])),
value);
}
} finally {
await db.close();
}
});
test('Query by page', () async {
// await databaseFactory.debugSetLogLevel(sqfliteLogLevelVerbose);
//final path = await initDeleteDb('query_by_page.db');
//final db = await openDatabase(path);
final db = await openDatabase(inMemoryDatabasePath);
try {
await db.execute('''
CREATE TABLE test (
id INTEGER PRIMARY KEY
)''');
await db.insert('test', {'id': 1});
await db.insert('test', {'id': 2});
await db.insert('test', {'id': 3});
var resultsList = <List>[];
// Use a cursor
var cursor =
await db.rawQueryCursor('SELECT * FROM test', null, bufferSize: 2);
resultsList.clear();
var results = <Map<String, Object?>>[];
while (await cursor.moveNext()) {
results.add(cursor.current);
}
expect(results, [
{'id': 1},
{'id': 2},
{'id': 3}
]);
// Multiple cursors a cursor
var cursor1 =
await db.rawQueryCursor('SELECT * FROM test', null, bufferSize: 2);
var cursor2 =
await db.rawQueryCursor('SELECT * FROM test', null, bufferSize: 1);
await cursor1.moveNext();
expect(cursor1.current.values, [1]);
await cursor2.moveNext();
await cursor2.moveNext();
expect(cursor2.current.values, [2]);
await cursor1.moveNext();
expect(cursor1.current.values, [2]);
await cursor1.close();
await cursor1.close(); // ok to call twice
try {
cursor1.current.values;
fail('should fail get current');
} on StateError catch (_) {}
await cursor2.moveNext();
expect(cursor2.current.values, [3]);
expect(await cursor2.moveNext(), isFalse);
expect(await cursor1.moveNext(), isFalse);
try {
cursor2.current.values;
fail('should fail get current');
} on StateError catch (_) {}
// No data
cursor = await db.rawQueryCursor('SELECT * FROM test WHERE id > ?', [3],
bufferSize: 2);
expect(await cursor.moveNext(), isFalse);
// Matching page size
cursor = await db.rawQueryCursor('SELECT * FROM test WHERE id > ?', [1],
bufferSize: 2);
expect(await cursor.moveNext(), isTrue);
expect(await cursor.moveNext(), isTrue);
expect(await cursor.moveNext(), isFalse);
} finally {
await db.close();
}
});
}
}
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart';
import 'src/common_import.dart';
import 'test_page.dart';
// ignore_for_file: avoid_print
/// Slow test page.
class SlowTestPage extends TestPage {
/// Slow test page.
SlowTestPage({Key? key}) : super('Slow tests', key: key) {
test('Perf 100 insert', () async {
final path = await initDeleteDb('slow_txn_100_insert.db');
final db = await openDatabase(path);
await db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
await db.transaction((txn) async {
for (var i = 0; i < 100; i++) {
await txn
.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item $i']);
}
});
await db.close();
});
test('Perf 100 insert no txn', () async {
final path = await initDeleteDb('slow_100_insert.db');
final db = await openDatabase(path);
await db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
for (var i = 0; i < 1000; i++) {
await db.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item $i']);
}
await db.close();
});
test('Perf 1000 insert', () async {
await perfInsert();
});
test('Perf 1000 insert batch', () async {
final path = await initDeleteDb('slow_txn_1000_insert_batch.db');
final db = await openDatabase(path);
await db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
final sw = Stopwatch()..start();
final batch = db.batch();
for (var i = 0; i < 1000; i++) {
batch.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item $i']);
}
await batch.commit();
print('1000 insert batch ${sw.elapsed}');
await db.close();
});
test('Perf 1000 insert batch no result', () async {
final path =
await initDeleteDb('slow_txn_1000_insert_batch_no_result.db');
final db = await openDatabase(path);
await db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
final sw = Stopwatch()..start();
final batch = db.batch();
for (var i = 0; i < 1000; i++) {
batch.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item $i']);
}
await batch.commit(noResult: true);
print('1000 insert batch no result ${sw.elapsed}');
await db.close();
});
const count = 10000;
test('Perf $count item', () async {
//Sqflite.devSetDebugModeOn(true);
await perfDo(count);
});
if (platform.isAndroid) {
test('Perf android NORMAL_PRIORITY', () async {
// ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package
await Sqflite.devSetOptions(
SqfliteOptions()..androidThreadPriority = 0);
try {
await perfInsert();
await perfDo(count);
} finally {
// Background priority
await Sqflite.devSetOptions(
SqfliteOptions()..androidThreadPriority = 10);
}
});
}
}
/// basic performance testing.
Future perfDo(int count) async {
final path = await initDeleteDb('pref_${count}_items.db');
final db = await openDatabase(path);
try {
await db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
var sw = Stopwatch()..start();
final batch = db.batch();
for (var i = 0; i < count; i++) {
batch.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item $i']);
}
await batch.commit();
print('sw ${sw.elapsed} insert $count items batch ');
sw = Stopwatch()..start();
var result = await db.query('Test');
print('sw ${sw.elapsed} SELECT * From Test : ${result.length} items');
sw = Stopwatch()..start();
result =
await db.query('Test', where: 'name LIKE ?', whereArgs: ['%item%']);
print(
'sw ${sw.elapsed} SELECT * FROM Test WHERE name LIKE %item% ${result.length} items');
sw = Stopwatch()..start();
result =
await db.query('Test', where: 'name LIKE ?', whereArgs: ['%dummy%']);
print(
'sw ${sw.elapsed} SELECT * FROM Test WHERE name LIKE %dummy% ${result.length} items');
} finally {
await db.close();
}
}
/// Insert perf testing.
Future perfInsert() async {
final path = await initDeleteDb('slow_txn_1000_insert.db');
final db = await openDatabase(path);
await db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)');
final sw = Stopwatch()..start();
await db.transaction((txn) async {
for (var i = 0; i < 1000; i++) {
await txn.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item $i']);
}
});
print('1000 insert ${sw.elapsed}');
await db.close();
}
// 2019-02-26
// BACKGROUND
}
export 'dart:async';
export 'dart:convert';
export 'package:collection/collection.dart';
export 'package:sqflite_common/src/internals.dart';
export 'package:sqflite_common/src/platform/platform.dart';
export 'dev_utils.dart';
export 'dart:async';
// ignore_for_file: avoid_print
/// Deprecated to prevent keeping the code used.
@Deprecated('Dev only')
void devPrint(Object object) {
print(object);
}
/// Deprecated to prevent keeping the code used.
///
/// Can be use as a todo for weird code. int value = devWarning(myFunction());
/// The function is always called
@Deprecated('Dev only')
T devWarning<T>(T value) => value;
// Copyright (c) 2015, 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:async';
import 'package:matcher/matcher.dart';
/// An exception thrown when a test assertion fails.
class TestFailure {
/// An exception thrown when a test assertion fails.
TestFailure(this.message);
/// Exception message
final String message;
@override
String toString() => message;
}
/// The type used for functions that can be used to build up error reports
/// upon failures in [expect].
@Deprecated('Will be removed in 0.13.0.')
typedef ErrorFormatter = String Function(dynamic actual, Matcher matcher,
String? reason, Map matchState, bool verbose);
/// Assert that [actual] matches [matcher].
///
/// This is the main assertion function. [reason] is optional and is typically
/// not supplied, as a reason is generated from [matcher]; if [reason]
/// is included it is appended to the reason generated by the matcher.
///
/// [matcher] can be a value in which case it will be wrapped in an
/// [equals] matcher.
///
/// If the assertion fails a [TestFailure] is thrown.
///
/// If [skip] is a String or `true`, the assertion is skipped. The arguments are
/// still evaluated, but [actual] is not verified to match [matcher]. If
/// [actual] is a [Future], the test won't complete until the future emits a
/// value.
///
/// If [skip] is a string, it should explain why the assertion is skipped; this
/// reason will be printed when running the test.
///
/// Certain matchers, like [completion] and [throwsA], either match or fail
/// asynchronously. When you use [expect] with these matchers, it ensures that
/// the test doesn't complete until the matcher has either matched or failed. If
/// you want to wait for the matcher to complete before continuing the test, you
/// can call [expectLater] instead and `await` the result.
void expect(
Object? actual,
Object? matcher, {
String? reason,
Object? skip,
}) {
_expect(actual, matcher, reason: reason, skip: skip);
}
/// Just like [expect], but returns a [Future] that completes when the matcher
/// has finished matching.
///
/// For the [completes] and [completion] matchers, as well as [throwsA] and
/// related matchers when they're matched against a [Future], the returned
/// future completes when the matched future completes. For the [prints]
/// matcher, it completes when the future returned by the callback completes.
/// Otherwise, it completes immediately.
///
/// If the matcher fails asynchronously, that failure is piped to the returned
/// future where it can be handled by user code.
Future expectLater(Object? actual, Object? matcher,
{String? reason, Object? skip}) =>
_expect(actual, matcher, reason: reason, skip: skip) as Future;
String _formatFailure(Matcher expected, Object? actual, String which,
{String? reason}) {
final buffer = StringBuffer();
buffer.writeln(indent(prettyPrint(expected), first: 'Expected: '));
buffer.writeln(indent(prettyPrint(actual), first: ' Actual: '));
if (which.isNotEmpty) buffer.writeln(indent(which, first: ' Which: '));
if (reason != null) buffer.writeln(reason);
return buffer.toString();
}
/// The implementation of [expect] and [expectLater].
FutureOr _expect(Object? actual, Object? matcher,
{String? reason,
Object? skip,
bool verbose = false,
// ignore: deprecated_member_use, deprecated_member_use_from_same_package
ErrorFormatter? formatter}) {
formatter ??= (actual, matcher, reason, matchState, verbose) {
final mismatchDescription = StringDescription();
matcher.describeMismatch(actual, mismatchDescription, matchState, verbose);
// ignore: deprecated_member_use
return _formatFailure(matcher, actual, mismatchDescription.toString(),
reason: reason);
};
if (skip != null && skip is! bool && skip is! String) {
throw ArgumentError.value(skip, 'skip', 'must be a bool or a String');
}
matcher = wrapMatcher(matcher);
final matchState = <Object?, Object?>{};
try {
if ((matcher as Matcher).matches(actual, matchState)) {
return Future.sync(() {});
}
} catch (e, trace) {
reason ??= '$e at $trace';
}
fail(formatter(actual, matcher as Matcher, reason, matchState, verbose));
}
/// Convenience method for throwing a new [TestFailure] with the provided
/// [message].
Never fail([String? message]) => throw TestFailure(message ?? 'should fail');
/// index text helper.
String indent(String text, {String? first}) {
if (first != null) {
return '$first $text';
}
return text;
}
/// index text helper.
String prettyPrint(dynamic text, {String? first}) {
if (first != null) {
return '$first $text';
}
return '$text';
}
/// The default error formatter.
@Deprecated('Will be removed in 0.13.0.')
String formatFailure(Matcher expected, Object? actual, String which,
{String? reason}) {
final buffer = StringBuffer();
buffer.writeln(indent(prettyPrint(expected), first: 'Expected: '));
buffer.writeln(indent(prettyPrint(actual), first: ' Actual: '));
if (which.isNotEmpty) buffer.writeln(indent(which, first: ' Which: '));
if (reason != null) buffer.writeln(reason);
return buffer.toString();
}
import 'package:flutter/material.dart';
import 'package:sqflite_example/model/item.dart';
/// Item widget.
class ItemWidget extends StatefulWidget {
/// Item widget.
const ItemWidget(this.item, this.onTap, {this.summary, super.key});
/// item summary.
final String? summary;
/// item data.
final Item item;
/// Action when pressed (typically run).
final void Function(Item item) onTap; // = Function(MainItem item);
@override
// ignore: library_private_types_in_public_api
_ItemWidgetState createState() => _ItemWidgetState();
}
class _ItemWidgetState extends State<ItemWidget> {
@override
Widget build(BuildContext context) {
IconData? icon;
Color? color;
switch (widget.item.state) {
case ItemState.none:
icon = Icons.arrow_forward_ios;
break;
case ItemState.running:
icon = Icons.more_horiz;
break;
case ItemState.success:
icon = Icons.check;
color = Colors.green;
break;
case ItemState.failure:
icon = Icons.close;
color = Colors.red;
break;
}
return ListTile(
// isThreeLine: widget.summary != null,
leading: SizedBox(
child: IconButton(
icon: Icon(icon, color: color),
onPressed: null, // null disables the button
)),
title: Text(widget.item.name),
subtitle: widget.summary != null ? Text(widget.summary!) : null,
onTap: _onTap);
}
void _onTap() {
widget.onTap(widget.item);
//print(widget.item.route);
//Navigator.pushNamed(context, widget.item.route);
}
}
import 'package:flutter/material.dart';
import 'package:sqflite_example/model/main_item.dart';
/// Main item widget.
class MainItemWidget extends StatefulWidget {
/// Main item widget.
const MainItemWidget(this.item, this.onTap, {super.key});
/// item data.
final MainItem item;
/// onTap action (typically run or open).
final void Function(MainItem item) onTap; // = Function(MainItem item);
@override
// ignore: library_private_types_in_public_api
_MainItemWidgetState createState() => _MainItemWidgetState();
}
class _MainItemWidgetState extends State<MainItemWidget> {
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(widget.item.title),
subtitle: Text(widget.item.description),
onTap: _onTap);
}
void _onTap() {
widget.onTap(widget.item);
//print(widget.item.route);
//Navigator.pushNamed(context, widget.item.route);
}
}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:sqflite_example/src/common_import.dart';
import 'package:sqflite_example/src/expect.dart';
import 'model/item.dart';
import 'model/test.dart';
import 'src/item_widget.dart';
export 'package:matcher/matcher.dart';
export 'package:sqflite_example/database/database.dart';
export 'src/expect.dart' show expect, fail;
// ignore_for_file: avoid_print
/// Base test page.
class TestPage extends StatefulWidget {
/// Base test page.
TestPage(this.title, {super.key});
/// The title.
final String title;
/// Test list.
final List<Test> tests = [];
/// define a test.
void test(String name, FutureOr Function() fn) {
tests.add(Test(name, fn));
}
/// define a solo test.
@Deprecated('SOLO_TEST - On purpose to remove before checkin')
// ignore: non_constant_identifier_names
void solo_test(String name, FutureOr Function() fn) {
tests.add(Test(name, fn, solo: true));
}
/// skip a test.
@Deprecated('SKIP_TEST - On purpose to remove before checkin')
// ignore: non_constant_identifier_names
void skip_test(String name, FutureOr Function() fn) {
tests.add(Test(name, fn, skip: true));
}
/// Thrown an exception
void fail([String? message]) {
throw Exception(message ?? 'should fail');
}
@override
// ignore: library_private_types_in_public_api
_TestPageState createState() => _TestPageState();
}
/// Verify a condition.
bool? verify(bool? condition, [String? message]) {
message ??= 'verify failed';
expect(condition, true, reason: message);
/*
if (condition == null) {
throw new Exception(''$message' null condition');
}
if (!condition) {
throw new Exception(''$message'');
}
*/
return condition;
}
/// Group.
abstract mixin class Group {
/// List of tests.
List<Test> get tests;
bool? _hasSolo;
final _tests = <Test>[];
/// Add a test.
void add(Test test) {
if (!test.skip) {
if (test.solo) {
if (_hasSolo != true) {
_hasSolo = true;
_tests.clear();
}
_tests.add(test);
} else if (_hasSolo != true) {
_tests.add(test);
}
}
}
/// true if it has solo or contains item with solo feature
bool? get hasSolo => _hasSolo;
}
class _TestPageState extends State<TestPage> with Group {
int get _itemCount => items.length;
List<Item> items = [];
Future _run() async {
if (!mounted) {
return null;
}
setState(() {
items.clear();
});
_tests.clear();
for (var test in widget.tests) {
add(test);
}
for (var test in _tests) {
var item = Item(test.name);
late int position;
setState(() {
position = items.length;
items.add(item);
});
try {
await test.fn();
item = Item(test.name)..state = ItemState.success;
} catch (e, st) {
print(e);
print(st);
item = Item(test.name)..state = ItemState.failure;
}
if (!mounted) {
return null;
}
setState(() {
items[position] = item;
});
}
}
Future _runTest(int index) async {
if (!mounted) {
return null;
}
final test = _tests[index];
var item = items[index];
setState(() {
item.state = ItemState.running;
});
try {
print('TEST Running ${test.name}');
await test.fn();
print('TEST Done ${test.name}');
item = Item(test.name)..state = ItemState.success;
} catch (e, st) {
print('TEST Error $e running ${test.name}');
try {
print(st);
} catch (_) {}
item = Item(test.name)..state = ItemState.failure;
}
if (!mounted) {
return null;
}
setState(() {
items[index] = item;
});
}
@override
void initState() {
super.initState();
/*
setState(() {
_itemCount = 3;
});
*/
_run();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title), actions: <Widget>[
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Run again',
onPressed: _run,
),
]),
body:
ListView.builder(itemBuilder: _itemBuilder, itemCount: _itemCount));
}
Widget _itemBuilder(BuildContext context, int index) {
final item = getItem(index);
return ItemWidget(item, (Item item) {
//Navigator.of(context).pushNamed(item.route);
_runTest(index);
});
}
Item getItem(int index) {
return items[index];
}
@override
List<Test> get tests => widget.tests;
}
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart';
import 'test_page.dart';
/// `todo` table name
const String tableTodo = 'todo';
/// id column name
const String columnId = '_id';
/// title column name
const String columnTitle = 'title';
/// done column name
const String columnDone = 'done';
/// Todo model.
class Todo {
/// Todo model.
Todo();
/// Read from a record.
Todo.fromMap(Map map) {
id = map[columnId] as int?;
title = map[columnTitle] as String?;
done = map[columnDone] == 1;
}
/// id.
int? id;
/// title.
String? title;
/// done.
bool? done;
/// Convert to a record.
Map<String, Object?> toMap() {
final map = <String, Object?>{
columnTitle: title,
columnDone: done == true ? 1 : 0
};
if (id != null) {
map[columnId] = id;
}
return map;
}
}
/// Todo provider.
class TodoProvider {
/// The database when opened.
late Database db;
/// Open the database.
Future open(String path) async {
db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db.execute('''
create table $tableTodo (
$columnId integer primary key autoincrement,
$columnTitle text not null,
$columnDone integer not null)
''');
});
}
/// Insert a todo.
Future<Todo> insert(Todo todo) async {
todo.id = await db.insert(tableTodo, todo.toMap());
return todo;
}
/// Get a todo.
Future<Todo?> getTodo(int id) async {
final List<Map> maps = await db.query(tableTodo,
columns: [columnId, columnDone, columnTitle],
where: '$columnId = ?',
whereArgs: [id]);
if (maps.isNotEmpty) {
return Todo.fromMap(maps.first);
}
return null;
}
/// Delete a todo.
Future<int> delete(int id) async {
return await db.delete(tableTodo, where: '$columnId = ?', whereArgs: [id]);
}
/// Update a todo.
Future<int> update(Todo todo) async {
return await db.update(tableTodo, todo.toMap(),
where: '$columnId = ?', whereArgs: [todo.id!]);
}
/// Close database.
Future close() async => db.close();
}
/// Todo test page.
class TodoTestPage extends TestPage {
/// Todo test page.
TodoTestPage({Key? key}) : super('Todo example', key: key) {
test('open', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('simple_todo_open.db');
final todoProvider = TodoProvider();
await todoProvider.open(path);
await todoProvider.close();
//await Sqflite.setDebugModeOn(false);
});
test('insert/query/update/delete', () async {
// await Sqflite.devSetDebugModeOn();
final path = await initDeleteDb('simple_todo.db');
final todoProvider = TodoProvider();
await todoProvider.open(path);
var todo = Todo()..title = 'test';
await todoProvider.insert(todo);
expect(todo.id, 1);
expect(await todoProvider.getTodo(0), null);
todo = (await todoProvider.getTodo(1))!;
expect(todo.id, 1);
expect(todo.title, 'test');
expect(todo.done, false);
todo.done = true;
expect(await todoProvider.update(todo), 1);
todo = (await todoProvider.getTodo(1))!;
expect(todo.id, 1);
expect(todo.title, 'test');
expect(todo.done, true);
expect(await todoProvider.delete(0), 0);
expect(await todoProvider.delete(1), 1);
expect(await todoProvider.getTodo(1), null);
await todoProvider.close();
//await Sqflite.setDebugModeOn(false);
});
}
}
import 'dart:async';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_example/utils.dart';
import 'test_page.dart';
// ignore_for_file: avoid_print
class _Data {
late Database db;
}
/// Type test page.
class TypeTestPage extends TestPage {
/// Type test page.
TypeTestPage({Key? key}) : super('Type tests', key: key) {
test('int', () async {
//await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('type_int.db');
data.db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db.execute(
'CREATE TABLE Test (id INTEGER PRIMARY KEY AUTOINCREMENT, value INTEGER)');
});
var id = await insertValue(-1);
expect(await getValue(id), -1);
// less than 32 bits
id = await insertValue(pow(2, 31));
expect(await getValue(id), pow(2, 31));
// more than 32 bits
id = await insertValue(pow(2, 33));
//devPrint('2^33: ${await getValue(id)}');
expect(await getValue(id), pow(2, 33));
// about web limits
id = await insertValue(pow(2, 53));
expect(await getValue(id), pow(2, 53));
if (!kIsWeb) {
// Not for web
id = await insertValue(pow(2, 62));
//devPrint('2^62: ${pow(2, 62)} ${await getValue(id)}');
expect(await getValue(id), pow(2, 62),
reason: '2^62: ${pow(2, 62)} ${await getValue(id)}');
var value = pow(2, 63).round() - 1;
id = await insertValue(value);
//devPrint('${value} ${await getValue(id)}');
expect(await getValue(id), value,
reason: '$value ${await getValue(id)}');
value = -(pow(2, 63)).round();
id = await insertValue(value);
//devPrint('${value} ${await getValue(id)}');
expect(await getValue(id), value,
reason: '$value ${await getValue(id)}');
}
var bigValue = 1234567890123456;
id = await insertValue(bigValue);
expect(await getValue(id), bigValue);
/*
id = await insertValue(pow(2, 63));
devPrint('2^63: ${pow(2, 63)} ${await getValue(id)}');
assert(await getValue(id) == pow(2, 63), '2^63: ${pow(2, 63)} ${await getValue(id)}');
// more then 64 bits
id = await insertValue(pow(2, 65));
assert(await getValue(id) == pow(2, 65));
// more then 128 bits
id = await insertValue(pow(2, 129));
assert(await getValue(id) == pow(2, 129));
*/
await data.db.close();
});
test('real', () async {
//await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('type_real.db');
data.db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value REAL)');
});
var id = await insertValue(-1.1);
expect(await getValue(id), -1.1);
// big float
id = await insertValue(1 / 3);
expect(await getValue(id), 1 / 3);
if (!kIsWeb) {
id = await insertValue(pow(2, 63) + .1);
expect(await getValue(id), pow(2, 63) + 0.1);
// integer?
id = await insertValue(pow(2, 62));
expect(await getValue(id), pow(2, 62));
}
var bigValue = 1234567890123456789.0;
id = await insertValue(bigValue);
expect(await getValue(id), bigValue);
await data.db.close();
});
test('text', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('type_text.db');
data.db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db.execute(
'CREATE TABLE Test (id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT)');
});
try {
var id = await insertValue('simple text');
expect(await getValue(id), 'simple text');
// null
id = await insertValue(null);
expect(await getValue(id), null);
// utf-8
id = await insertValue('àöé');
expect(await getValue(id), 'àöé');
} finally {
await data.db.close();
}
});
test('blob', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('type_blob.db');
data.db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value BLOB)');
});
int id;
try {
// insert text in blob
id = await insertValue('simple text');
expect(await getValue(id), 'simple text');
// UInt8List - default
final byteData = ByteData(1);
byteData.setInt8(0, 1);
final blob = byteData.buffer.asUint8List();
id = await insertValue(blob);
//print(await getValue(id));
final result = (await getValue(id)) as List;
print(result.runtimeType);
expect(result is Uint8List, true);
expect(result.length, 1);
expect(result, [1]);
// empty array not supported
//id = await insertValue([]);
//print(await getValue(id));
//assert(eq.equals(await getValue(id), []));
var blob1234 = [1, 2, 3, 4];
if (!supportsCompatMode) {
blob1234 = Uint8List.fromList(blob1234);
}
id = await insertValue(blob1234);
dynamic value = (await getValue(id)) as List;
print(value);
print('${(value as List).length}');
expect(value, blob1234, reason: '${await getValue(id)}');
// test hex feature on sqlite
final hexResult = await data.db
.rawQuery('SELECT hex(value) FROM Test WHERE id = ?', [id]);
expect(hexResult[0].values.first, '01020304');
// try blob lookup (works on Android since 2022-09-19)
var rows = await data.db
.rawQuery('SELECT * FROM Test WHERE value = ?', [blob1234]);
expect(rows.length, 1);
// try blob lookup using hex
rows = await data.db.rawQuery(
'SELECT * FROM Test WHERE hex(value) = ?', [Sqflite.hex(blob1234)]);
expect(rows.length, 1);
expect(rows[0]['id'], 3);
// Insert empty blob
final blobEmpty = Uint8List(0);
id = await insertValue(blobEmpty);
value = await getValue(id);
expect(value, const TypeMatcher<Uint8List>());
expect(value, isEmpty);
} finally {
await data.db.close();
}
});
test('null', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('type_null.db');
data.db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)');
});
try {
final id = await insertValue(null);
expect(await getValue(id), null);
// Make a string
expect(await updateValue(id, 'dummy'), 1);
expect(await getValue(id), 'dummy');
expect(await updateValue(id, null), 1);
expect(await getValue(id), null);
} finally {
await data.db.close();
}
});
test('date_time', () async {
// await Sqflite.devSetDebugModeOn(true);
final path = await initDeleteDb('type_date_time.db');
data.db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db
.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)');
});
try {
var failed = false;
try {
await insertValue(DateTime.fromMillisecondsSinceEpoch(1234567890));
} on ArgumentError catch (_) {
failed = true;
} on UnsupportedError catch (_) {
// this happens on the web
failed = true;
}
expect(failed, true);
} finally {
await data.db.close();
}
});
test('bool', () async {
//await Sqflite.devSetDebugModeOn(true);
data.db = await openDatabase(inMemoryDatabasePath, version: 1,
onCreate: (Database db, int version) async {
await db.execute(
'CREATE TABLE Test (id INTEGER PRIMARY KEY, value BOOLEAN)');
});
try {
var failed = false;
try {
await insertValue(true);
} on ArgumentError catch (_) {
failed = true;
} on DatabaseException catch (_) {
// this happens on the web
failed = true;
}
if (supportsCompatMode) {
print('for now bool are accepted but inconsistent on iOS/Android');
expect(failed, isFalse);
}
} finally {
await data.db.close();
}
});
}
/// Out internal data.
// ignore: library_private_types_in_public_api
final _Data data = _Data();
/// Get the value field from a given id
Future<dynamic> getValue(int id) async {
return ((await data.db.query('Test', where: 'id = $id')).first)['value'];
}
/// insert the value field and return the id
Future<int> insertValue(dynamic value) async {
return await data.db.insert('Test', {'value': value});
}
/// insert the value field and return the id
Future<int> updateValue(int id, dynamic value) async {
return await data.db.update('Test', {'value': value}, where: 'id = $id');
}
}
import 'package:sqflite/sqflite.dart';
import 'package:sqflite/sqflite_dev.dart';
/// Usage: await sleep(500);
Future sleep([int milliseconds = 0]) =>
Future<void>.delayed(Duration(milliseconds: milliseconds));
/// Supports compat mode (devSetDebugModeOn, queryAsMap, fts4, some error handled - missing parameter, bad file)
bool get supportsCompatMode {
// ignore: invalid_use_of_visible_for_testing_member
return databaseFactory == sqfliteDatabaseFactoryDefault;
}
name: sqflite_example
description: Demonstrates how to use the sqflite plugin and perform some tests.
publish_to: none
version: '0.4.0'
environment:
sdk: '>=3.3.0 <4.0.0'
dependencies:
path: any
collection: any
flutter:
sdk: flutter
sqflite:
sqflite_common:
synchronized:
matcher:
meta:
dev_dependencies:
flutter_lints: '>=2.0.0'
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
test:
process_run: '>=0.11.0+2'
pub_semver:
# For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the Icons class.
uses-material-design: true
# To add assets to your application, add an assets section here, in
# this "flutter" section, as in:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
assets:
- assets/example.db
- assets/issue_64.db
dependency_overrides:
sqflite:
path: ../
sqflite_common:
path: ../../sqflite_common
import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite_example/src/common_import.dart';
import 'package:sqflite_example/utils.dart';
void main() {
group('example', () {
test('sleep', () async {
await sleep(1).timeout(const Duration(milliseconds: 100));
try {
await sleep(100).timeout(const Duration(milliseconds: 1));
fail('should fail');
} on TimeoutException catch (_) {}
});
});
}
import 'package:sqflite_example/main.dart';
Future main() async {
// ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package
// debugAutoStartRouteName = testExceptionRoute;
// debugAutoStartRouteName = testOpenRoute;
// debugAutoStartRouteName = testManualRoute;
debugAutoStartRouteName = testRawRoute;
// debugAutoStartRouteName = testTypeRoute;
// debugAutoStartRouteName = testExpRoute;
mainExampleApp();
}
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter_test/flutter_test.dart';
// Keep a dummy file to avoid it be created again each time
void main() {
testWidgets('dummy', (WidgetTester tester) async {});
}
// ignore: import_of_legacy_library_into_null_safe
import 'package:integration_test/integration_test_driver.dart';
Future<void> main() => integrationDriver();
import 'package:process_run/shell.dart';
import 'package:sqflite_example/src/common_import.dart';
// ignore_for_file: avoid_print
Future<void> main() async {
final controller = StreamController<List<int>>();
final shell = Shell(stdout: controller.sink, verbose: false);
controller.stream
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
if (line.contains('Sqflite')) {
print(line);
}
});
await shell.run('adb logcat');
// We'll never get there actually...
await controller.close();
}
import 'package:process_run/shell.dart';
Future<void> main() async {
final shell = Shell();
await shell.run('''
adb uninstall com.tekartik.sqfliteexample
''');
}
import 'package:process_run/shell.dart';
Future<void> main() async {
await buildIos();
}
Future<void> buildIos() async {
final shell = Shell();
await shell.run('flutter build ios --no-codesign');
}
import 'package:process_run/shell.dart';
Future<void> main() async {
await buildMacos();
}
Future<void> buildMacos() async {
final shell = Shell();
await shell.run('flutter build macos');
}
import 'package:process_run/shell.dart';
Future<void> main() async {
await createProjectIos();
}
Future<void> createProjectIos() async {
final shell = Shell();
await shell.run('flutter create --platforms ios .');
}
import 'package:process_run/shell.dart';
Future<void> main() async {
await createProjectIosObjc();
}
Future<void> createProjectIosObjc() async {
final shell = Shell();
await shell.run('flutter create --platforms ios -i objc .');
}
import 'package:process_run/shell.dart';
Future<void> main() async {
await createProjectMacos();
}
Future<void> createProjectMacos() async {
final shell = Shell();
await shell.run('flutter create --platforms macos .');
}
import 'dart:io';
import 'package:process_run/shell.dart';
Future<void> main(List<String> arguments) async {
var deviceId = ShellEnvironment().vars['SQFLITE_TEST_DEVICE_ID'];
if (deviceId == null) {
// ignore: avoid_print
stdout.writeln(
'To run on a specific device set SQFLITE_TEST_DEVICE_ID=<deviceId>,'
' for example \'emulator-5554\' typically for android emulator');
}
await runIntegrationTest(deviceId: deviceId);
}
Future<void> runIntegrationTest({String? deviceId}) async {
final shell = Shell();
await shell.run('flutter drive${deviceId != null ? ' -d $deviceId ' : ''}'
' --driver=test_driver/integration_test.dart'
' --target=integration_test/sqflite_test.dart');
}
import 'run_integration_test.dart';
Future<void> main(List<String> arguments) async {
await runIntegrationTest(deviceId: 'macos');
}
import 'dart:async';
import 'package:sqflite/src/compat.dart';
import 'package:sqflite/src/constant.dart';
import 'package:sqflite/src/sqflite_android.dart';
import 'package:sqflite/src/sqflite_impl.dart';
import 'package:sqflite/src/utils.dart' as impl;
import 'package:sqflite/utils/utils.dart' as utils;
import 'sqlite_api.dart';
export 'package:sqflite/sql.dart' show ConflictAlgorithm;
export 'package:sqflite/src/compat.dart';
export 'package:sqflite_common/sqflite.dart';
export 'sqlite_api.dart';
export 'src/factory_impl.dart' show databaseFactorySqflitePlugin;
export 'src/sqflite_plugin.dart' show SqflitePlugin;
///
/// sqflite plugin
///
class Sqflite {
/// Turns on debug mode if you want to see the SQL query
/// executed natively.
@Deprecated('Removed in next major release')
static Future<void> setDebugModeOn([bool on = true]) async {
await invokeMethod<dynamic>(methodSetDebugModeOn, on);
}
/// Planned Deprecated for 1.1.7
@Deprecated('Removed in next major release')
static Future<bool> getDebugModeOn() async {
return impl.debugModeOn;
}
/// deprecated on purpose to remove from code.
///
/// To use during developpment/debugging
/// Set extra dart and nativate debug logs
@Deprecated('Dev only')
static Future<void> devSetDebugModeOn([bool on = true]) {
impl.debugModeOn = on;
return setDebugModeOn(on);
}
/// Testing only.
///
/// deprecated on purpose to remove from code.
@Deprecated('Dev only')
static Future<void> devSetOptions(SqfliteOptions options) async {
await invokeMethod<dynamic>(methodOptions, options.toMap());
}
/// Testing only
@Deprecated('Dev only')
static Future<void> devInvokeMethod(String method,
[Object? arguments]) async {
await invokeMethod<dynamic>(method, arguments);
}
/// helper to get the first int value in a query
/// Useful for COUNT(*) queries
static int? firstIntValue(List<Map<String, Object?>> list) =>
utils.firstIntValue(list);
/// Utility to encode a blob to allow blob query using
/// 'hex(blob_field) = ?', Sqlite.hex([1,2,3])
static String hex(List<int> bytes) => utils.hex(bytes);
/// Sqlite has a dead lock warning feature that will print some text
/// after 10s, you can override the default behavior
static void setLockWarningInfo(
{Duration? duration, void Function()? callback}) {
utils.setLockWarningInfo(duration: duration!, callback: callback!);
}
}
/// Android only API
extension SqfliteDatabaseAndroidExt on Database {
/// Sets the locale for this database. The specified IETF BCP 47 language tag
/// string (en-US, zh-CN, fr-FR, zh-Hant-TW, ...) must be as defined in
/// `Locale.forLanguageTag` in Android/Java documentation.
///
/// Only on Android.
Future<void> androidSetLocale(String languageTag) =>
SqfliteDatabaseAndroidExtImpl(this).androidSetLocale(languageTag);
}
import 'package:flutter/cupertino.dart';
import 'package:sqflite/src/factory.dart';
import 'package:sqflite/src/factory_impl.dart';
import 'package:sqflite_common/sqlite_api.dart';
export 'package:sqflite/src/factory_impl.dart'
show sqfliteDatabaseFactoryDefault;
/// Change the default factory used.
///
/// Test only.
///
@visibleForTesting
void setMockDatabaseFactory(DatabaseFactory? factory) {
// ignore: invalid_use_of_visible_for_testing_member
sqfliteDatabaseFactory = factory as SqfliteDatabaseFactory?;
}
// Only export:
// * [ConflictAlgorithm]
// * [escapeName]
// * [unescapeName]
export 'package:sqflite/src/sql_builder.dart'
show ConflictAlgorithm, escapeName, unescapeName;
export 'package:sqflite_common/sqlite_api.dart';
export 'package:sqflite_common/src/batch.dart';
export 'package:sqflite_common/src/collection_utils.dart';
export 'package:sqflite_common/src/compat.dart';
export 'package:sqflite_common/src/constant.dart';
export 'package:sqflite_common/src/database.dart';
export 'package:sqflite_common/src/database_mixin.dart';
export 'package:sqflite_common/src/dev_utils.dart';
export 'package:sqflite_common/src/env_utils.dart';
export 'package:sqflite_common/src/exception.dart';
import 'package:sqflite/src/services_impl.dart';
import 'package:sqflite/src/sqflite_import.dart';
/// Wrap any exception to a [DatabaseException]
Future<T> wrapDatabaseException<T>(Future<T> Function() action) async {
try {
final result = await action();
return result;
} on PlatformException catch (e) {
if (e.code == sqliteErrorCode) {
throw SqfliteDatabaseException(e.message!, e.details);
//rethrow;
} else {
rethrow;
}
}
}
export 'package:sqflite_common/src/factory.dart';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite/src/exception_impl.dart' as impl;
import 'package:sqflite/src/sqflite_impl.dart' as impl;
import 'package:sqflite/src/sqflite_import.dart';
import 'package:sqflite_common/sqflite.dart' as sqflite_common;
// ignore: implementation_imports
import 'package:sqflite_common/src/mixin/platform.dart';
import 'dev_utils.dart'; // ignore: unused_import
/// sqflite Default factory
@visibleForTesting
SqfliteDatabaseFactory get sqfliteDatabaseFactory =>
// ignore: invalid_use_of_visible_for_testing_member
(databaseFactoryOrNull ?? databaseFactorySqflitePlugin)
as SqfliteDatabaseFactory;
final SqfliteDatabaseFactory _databaseFactorySqflitePlugin =
SqfliteDatabaseFactoryImpl();
/// Default factory that uses the plugin.
DatabaseFactory get databaseFactorySqflitePlugin =>
_databaseFactorySqflitePlugin;
/// Default factory that uses the plugin.
final sqfliteDatabaseFactoryDefault = _databaseFactorySqflitePlugin;
/// Change the default factory. test only.
@visibleForTesting
set sqfliteDatabaseFactory(SqfliteDatabaseFactory? databaseFactory) =>
sqflite_common.databaseFactory = databaseFactory;
/// Factory implementation
class SqfliteDatabaseFactoryImpl with SqfliteDatabaseFactoryMixin {
/// Only to set for extra debugging
//static var _debugInternals = devWarning(true);
static const _debugInternals = false;
@override
Future<T> wrapDatabaseException<T>(Future<T> Function() action) =>
impl.wrapDatabaseException(action);
@override
Future<T> invokeMethod<T>(String method, [Object? arguments]) =>
!_debugInternals
? impl.invokeMethod(method, arguments)
: _invokeMethodWithLog(method, arguments);
Future<T> _invokeMethodWithLog<T>(String method, [Object? arguments]) async {
// ignore: avoid_print
print('-> $method $arguments');
final result = await impl.invokeMethod<T>(method, arguments);
// ignore: avoid_print
print('<- $result');
return result;
}
@override
Future<Uint8List> readDatabaseBytes(String path) async {
return await platform.databaseFileSystem.readDatabaseBytes(path);
}
@override
Future<void> writeDatabaseBytes(String path, Uint8List bytes) async {
await platform.databaseFileSystem.writeDatabaseBytes(path, bytes);
}
}
export 'package:sqflite_common/src/factory_mixin.dart';
export 'package:sqflite_common/src/mixin.dart';
export 'package:sqflite_common/src/mixin/constant.dart';
export 'package:sqflite_common/src/mixin/factory.dart';
export 'package:sqflite_common/src/open_options.dart';
export 'package:flutter/services.dart';
import 'package:sqflite/src/sqflite_import.dart';
import 'package:sqflite_common/sqlite_api.dart';
import 'constant.dart';
/// Native Android setLocale call.
const String methodAndroidSetLocale = 'androidSetLocale';
/// Locale param.
const String paramLocale = 'locale';
/// Private implementation in an extension for Android only
extension SqfliteDatabaseAndroidExtImpl on Database {
SqfliteDatabaseMixin get _mixin => this as SqfliteDatabaseMixin;
/// Set the locale.
Future<void> androidSetLocale(String languageTag) async {
await _mixin.safeInvokeMethod<void>(methodAndroidSetLocale,
<String, Object?>{paramId: _mixin.id, paramLocale: languageTag});
}
}
import 'dart:async';
import 'package:sqflite/src/compat.dart';
import 'package:sqflite/src/constant.dart';
import 'package:sqflite/src/sqflite_impl.dart';
import 'package:sqflite/src/utils.dart' as impl;
import 'package:sqflite/utils/utils.dart' as utils;
///
/// sqflite plugin. Might be deprecated/removed in the future.
///
class Sqflite {
/// Turns on debug mode if you want to see the SQL query
/// executed natively.
static Future<void> setDebugModeOn([bool on = true]) async {
await invokeMethod<dynamic>(methodSetDebugModeOn, on);
}
/// Planned Deprecated for 1.1.7
static Future<bool> getDebugModeOn() async {
return impl.debugModeOn;
}
/// deprecated on purpose to remove from code.
///
/// To use during developpment/debugging
/// Set extra dart and nativate debug logs
@Deprecated('Dev only')
static Future<void> devSetDebugModeOn([bool on = true]) {
impl.debugModeOn = on;
return setDebugModeOn(on);
}
/// Testing only.
///
/// deprecated on purpose to remove from code.
@Deprecated('Dev only')
static Future<void> devSetOptions(SqfliteOptions options) async {
await invokeMethod<dynamic>(methodOptions, options.toMap());
}
/// Testing only
@Deprecated('Dev only')
static Future<void> devInvokeMethod(String method,
[Object? arguments]) async {
await invokeMethod<dynamic>(method, arguments);
}
/// helper to get the first int value in a query
/// Useful for COUNT(*) queries
static int? firstIntValue(List<Map<String, Object?>> list) =>
utils.firstIntValue(list);
/// Utility to encode a blob to allow blob query using
/// 'hex(blob_field) = ?', Sqlite.hex([1,2,3])
static String hex(List<int> bytes) => utils.hex(bytes);
/// Sqlite has a dead lock warning feature that will print some text
/// after 10s, you can override the default behavior
static void setLockWarningInfo(
{Duration? duration, void Function()? callback}) {
utils.setLockWarningInfo(duration: duration!, callback: callback!);
}
}
import 'dart:core';
import 'services_impl.dart';
/// Sqflite channel name
const String channelName = 'com.tekartik.sqflite';
/// Sqflite channel
const MethodChannel channel = MethodChannel(channelName);
/// Temp flag to test concurrent reads
const supportsConcurrency = false;
/// Invoke a native method
Future<T> invokeMethod<T>(String method, [Object? arguments]) async =>
await channel.invokeMethod<T>(method, arguments) as T;
///
/// Export for implementation: sqflite, sqflite_common_ffi
///
library;
export 'package:sqflite_common/src/compat.dart'
show SqfliteOptions; // ignore: deprecated_member_use
/// Explicit list of needed private import
export 'package:sqflite_common/src/database.dart' // ignore: implementation_imports
show
SqfliteDatabaseOpenHelper,
SqfliteDatabase;
export 'package:sqflite_common/src/database_mixin.dart' // ignore: implementation_imports
show
SqfliteDatabaseMixin,
SqfliteDatabaseBase;
export 'package:sqflite_common/src/exception.dart'
show SqfliteDatabaseException;
export 'package:sqflite_common/src/factory.dart' show SqfliteDatabaseFactory;
export 'package:sqflite_common/src/factory_mixin.dart'
show SqfliteDatabaseFactoryBase, SqfliteDatabaseFactoryMixin;
export 'package:sqflite_common/src/mixin/constant.dart'
show
methodOpenDatabase,
methodCloseDatabase,
methodOptions,
sqliteErrorCode,
methodInsert,
methodQuery,
methodUpdate,
methodExecute,
methodBatch;
export 'package:sqflite_common/src/mixin/factory.dart'
show buildDatabaseFactory, SqfliteInvokeHandler;
import 'package:sqflite/sqflite.dart';
import 'factory_impl.dart';
/// sqflite Plugin registration.
class SqflitePlugin {
/// Registers this plugin as the default database factory (if not already set).
static void registerWith() {
databaseFactoryOrNull ??= sqfliteDatabaseFactoryDefault;
}
}
export 'package:sqflite_common/src/sql_builder.dart';
export 'package:sqflite_common/src/transaction.dart';
export 'package:sqflite_common/src/utils.dart';
export 'package:sqflite_common/src/value_utils.dart';
export 'package:sqflite_common/utils/utils.dart';
name: sqflite
repository: https://github.com/tekartik/sqflite/tree/master/sqflite
issue_tracker: https://github.com/tekartik/sqflite/issues
description: Flutter plugin for SQLite, a self-contained, high-reliability,
embedded, SQL database engine.
version: 2.3.3+1
# This package supports all platforms listed below.
platforms:
android:
ios:
macos:
topics:
- sql
- database
funding:
- https://github.com/sponsors/alextekartik
environment:
sdk: '>=3.3.0 <4.0.0'
# Flutter versions prior to 3.7 did not support the
# sharedDarwinSource option.
flutter: ">=3.7.0"
flutter:
plugin:
platforms:
android:
package: com.tekartik.sqflite
pluginClass: SqflitePlugin
dartPluginClass: SqflitePlugin
ios:
pluginClass: SqflitePlugin
dartPluginClass: SqflitePlugin
sharedDarwinSource: true
macos:
pluginClass: SqflitePlugin
dartPluginClass: SqflitePlugin
sharedDarwinSource: true
dependencies:
flutter:
sdk: flutter
sqflite_common: '>=2.5.0-1 <4.0.0'
path: '>=1.8.0 <3.0.0'
dev_dependencies:
flutter_lints: '>=1.0.0'
process_run: '>=0.12.0'
http: '>=0.13.0'
flutter_test:
sdk: flutter
test_api: '>=0.2.19'
pub_semver: '>=2.0.0'
import 'dart:async';
import 'package:sqflite/sqlite_api.dart';
import 'package:sqflite/src/mixin/factory.dart';
Future<void> main() async {
final factory = buildDatabaseFactory(
invokeMethod: (String method, [Object? arguments]) async {
Object? result;
// ignore: avoid_print
print('$method: $arguments');
if (method == 'openDatabase') {
result = 1;
} else if (method == 'query' &&
(arguments as Map)['sql'] == 'PRAGMA user_version') {
result = {'user_version': 0};
}
return result;
});
final db = await factory.openDatabase(inMemoryDatabasePath);
await db.getVersion();
}
import 'package:flutter_test/flutter_test.dart';
import 'package:process_run/shell_run.dart';
void main() {
group('no_flutter', () {
test('simple', () async {
await run('dart test/no_flutter_main.dart');
});
});
}
import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite/sqflite_dev.dart';
import 'src_mixin_test.dart' show MockDatabaseFactoryEmpty;
void main() {
group('sqflite_dev', () {
test('setMockDatabaseFactory', () async {
final factory = MockDatabaseFactoryEmpty();
expect(factory, isNot(databaseFactoryOrNull));
// ignore: deprecated_member_use_from_same_package
setMockDatabaseFactory(factory);
expect(factory, databaseFactory);
try {
await openDatabase(inMemoryDatabasePath);
expect(factory.methods, ['openDatabase']);
} finally {
// ignore: deprecated_member_use_from_same_package
setMockDatabaseFactory(null);
}
});
// Check that public api are exported
test('exported', () {
for (var value in <dynamic>[
MockDatabaseFactoryEmpty,
]) {
expect(value, isNotNull);
}
});
});
}
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite/src/factory_impl.dart';
import 'package:sqflite/src/mixin/factory.dart';
import 'package:sqflite/src/sqflite_impl.dart';
T? _ambiguate<T>(T? value) => value;
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('sqflite', () {
const channel = MethodChannel('com.tekartik.sqflite');
final log = <MethodCall>[];
String? response;
_ambiguate(TestDefaultBinaryMessengerBinding.instance)!
.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
log.add(methodCall);
return response;
});
tearDown(() {
log.clear();
});
test('databaseFactory', () async {
expect(databaseFactorySqflitePlugin is SqfliteInvokeHandler, isTrue);
});
test('supportsConcurrency', () async {
expect(supportsConcurrency, isFalse);
});
test('deprecated', () {
sqfliteDatabaseFactory = null;
for (var element in [
// ignore: unnecessary_statements
sqfliteDatabaseFactory
]) {
expect(element, isNotNull);
}
});
});
}
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite/sqflite.dart';
T? _ambiguate<T>(T? value) => value;
const channel = MethodChannel('com.tekartik.sqflite');
class MockMethodCall {
String? expectedMethod;
dynamic expectedArguments;
dynamic response;
@override
String toString() => '$expectedMethod $expectedArguments $response';
}
class MockScenario {
MockScenario(List<List> data) {
methodsCalls = data
.map((list) => MockMethodCall()
..expectedMethod = list[0]?.toString()
..expectedArguments = list[1]
..response = list[2])
.toList(growable: false);
}
late List<MockMethodCall> methodsCalls;
var index = 0;
dynamic exception;
void end() {
expect(exception, isNull, reason: '$exception');
expect(index, methodsCalls.length);
}
}
MockScenario startScenario(List<List> data) {
final scenario = MockScenario(data);
_ambiguate(TestDefaultBinaryMessengerBinding.instance)!
.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
final index = scenario.index++;
// devPrint('$index ${scenario.methodsCalls[index]}');
final item = scenario.methodsCalls[index];
try {
expect(methodCall.method, item.expectedMethod);
expect(methodCall.arguments, item.expectedArguments);
} catch (e) {
scenario.exception ??= '$e $index';
}
return item.response;
});
return scenario;
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
databaseFactory = databaseFactorySqflitePlugin;
group('sqflite', () {
test('open single instance false', () async {
final scenario = startScenario([
[
'openDatabase',
{'path': ':memory:', 'singleInstance': false},
1
],
[
'closeDatabase',
{'id': 1},
null
],
]);
final db = await openDatabase(inMemoryDatabasePath);
await db.close();
scenario.end();
});
test('open with version', () async {
final scenario = startScenario([
[
'openDatabase',
{'path': ':memory:', 'singleInstance': false},
1
],
[
'query',
{'sql': 'PRAGMA user_version', 'id': 1},
// ignore: inference_failure_on_collection_literal
{}
],
[
'execute',
{
'sql': 'BEGIN EXCLUSIVE',
'id': 1,
'transactionId': null,
'inTransaction': true
},
null
],
[
'query',
{'sql': 'PRAGMA user_version', 'id': 1},
// ignore: inference_failure_on_collection_literal
{}
],
[
'execute',
{'sql': 'PRAGMA user_version = 1', 'id': 1},
null
],
[
'execute',
{'sql': 'COMMIT', 'id': 1, 'inTransaction': false},
null
],
[
'closeDatabase',
{'id': 1},
null
],
]);
final db = await openDatabase(inMemoryDatabasePath,
version: 1, onCreate: (db, version) {});
await db.close();
scenario.end();
});
});
}
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite/sqflite.dart';
import 'src_mixin_test.dart' show MockDatabaseFactoryEmpty, MockInvalidFactory;
T? _ambiguate<T>(T? value) => value;
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
databaseFactory = databaseFactorySqflitePlugin;
group('sqflite', () {
const channel = MethodChannel('com.tekartik.sqflite');
final log = <MethodCall>[];
String? response;
_ambiguate(TestDefaultBinaryMessengerBinding.instance)!
.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
log.add(methodCall);
return response;
});
tearDown(() {
log.clear();
});
test('setDebugModeOn', () async {
// ignore: deprecated_member_use_from_same_package
await Sqflite.setDebugModeOn();
expect(log.first.method, 'debugMode');
expect(log.first.arguments, true);
});
// Check that public api are exported
test('exported', () {
for (var value in <dynamic>[
// Not part of sqflite_api
openDatabase,
openReadOnlyDatabase,
getDatabasesPath,
deleteDatabase,
databaseExists,
Sqflite,
// ignore: deprecated_member_use, deprecated_member_use_from_same_package
SqfliteOptions,
// sqflite_api
OpenDatabaseOptions,
DatabaseFactory,
Database,
Transaction,
Batch,
ConflictAlgorithm,
// ignore: deprecated_member_use, deprecated_member_use_from_same_package
Sqflite.devSetOptions,
sqfliteLogLevelNone,
sqfliteLogLevelSql,
sqfliteLogLevelVerbose,
databaseFactory,
]) {
expect(value, isNotNull);
}
});
test('firstIntValue', () {
expect(
Sqflite.firstIntValue(<Map<String, Object?>>[
<String, Object?>{'test': 1}
]),
1);
expect(
Sqflite.firstIntValue(<Map<String, Object?>>[
<String, Object?>{'test': 1},
<String, Object?>{'test': 1}
]),
1);
expect(
Sqflite.firstIntValue(<Map<String, Object?>>[
<String, Object?>{'test': null}
]),
null);
expect(Sqflite.firstIntValue(<Map<String, Object?>>[<String, Object?>{}]),
isNull);
expect(Sqflite.firstIntValue(<Map<String, Object?>>[]), isNull);
expect(Sqflite.firstIntValue(<Map<String, Object?>>[<String, Object?>{}]),
isNull);
});
test('hex', () {
expect(
Sqflite.hex(<int>[
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
255
]),
'000102030405060708090A0B0C0D0E0F1011FF');
expect(Sqflite.hex(<int>[]), '');
expect(Sqflite.hex(<int>[32]), '20');
try {
Sqflite.hex(<int>[-1]);
fail('should fail');
} on FormatException catch (_) {}
try {
Sqflite.hex(<int>[256]);
fail('should fail');
} on FormatException catch (_) {}
});
/*
test('open null', () async {
AssertionError? exception;
try {
await openDatabase(null!);
} on AssertionError catch (e) {
exception = e;
}
expect(exception, isNotNull);
});
*/
test('databaseFactory', () {
final originalDefaultFactory = databaseFactory;
try {
databaseFactory = null;
final factory = MockDatabaseFactoryEmpty();
databaseFactory = factory;
expect(databaseFactory, factory);
expect(databaseFactory, isNot(originalDefaultFactory));
databaseFactory = originalDefaultFactory; // stderr trigger here
try {
databaseFactory = MockInvalidFactory();
fail('should fail');
} on ArgumentError catch (_) {}
} finally {
databaseFactory = null;
databaseFactory = originalDefaultFactory;
}
});
});
}
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite/sql.dart';
T? _ambiguate<T>(T? value) => value;
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('sqflite', () {
const channel = MethodChannel('com.tekartik.sqflite');
final log = <MethodCall>[];
String? response;
_ambiguate(TestDefaultBinaryMessengerBinding.instance)!
.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
log.add(methodCall);
return response;
});
tearDown(() {
log.clear();
});
test('exported', () {
expect(ConflictAlgorithm.abort, isNotNull);
});
test('escapeName_export', () {
expect(escapeName('group'), '"group"');
});
test('unescapeName_export', () {
expect(unescapeName('"group"'), 'group');
});
});
}
import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite/sqlite_api.dart';
void main() {
group('sqlite_api', () {
// Check that public api are exported
test('exported', () {
for (var value in <dynamic>[
OpenDatabaseOptions,
DatabaseFactory,
Database,
Transaction,
Batch,
ConflictAlgorithm,
inMemoryDatabasePath,
OnDatabaseConfigureFn,
OnDatabaseCreateFn,
OnDatabaseOpenFn,
OnDatabaseVersionChangeFn,
onDatabaseDowngradeDelete,
sqfliteLogLevelNone,
sqfliteLogLevelSql,
sqfliteLogLevelVerbose,
]) {
expect(value, isNotNull);
}
try {
// ignore: unnecessary_cast
SqfliteDatabaseExecutorExt((null as Database?)!).getVersion();
} catch (_) {}
});
});
}
import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite/sqlite_api.dart';
import 'package:sqflite/src/constant.dart';
import 'package:sqflite/src/mixin/factory.dart';
import 'src_mixin_test.dart';
void main() {
group('mixin_factory', () {
test('public', () {
// ignore: unnecessary_statements
buildDatabaseFactory;
// ignore: unnecessary_statements
SqfliteInvokeHandler;
});
test('buildDatabaseFactory', () async {
final methods = <String>[];
final factory = buildDatabaseFactory(
invokeMethod: (String method, [Object? arguments]) async {
methods.add(method);
return mockResult(method);
});
expect(factory is SqfliteInvokeHandler, isTrue);
await factory.openDatabase(inMemoryDatabasePath);
expect(methods, <String>['openDatabase']);
});
});
}
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqlite_api.dart';
import 'package:sqflite/src/constant.dart';
import 'package:sqflite/src/database.dart';
import 'package:sqflite/src/mixin.dart';
import 'package:sqflite/src/open_options.dart';
import 'package:sqflite/utils/utils.dart';
void main() {
group('mixin_flutter', () {
run();
});
}
/// Mock the result based on the method used
dynamic mockResult(String method) {
// devPrint('$method');
switch (method) {
case methodOpenDatabase:
return 1;
case methodInsert:
return 0;
case methodUpdate:
return 0;
case methodQuery:
return <String, Object?>{};
case methodDatabaseExists:
return false;
}
return null;
}
class MockDatabase extends SqfliteDatabaseBase {
MockDatabase(super.openHelper, [super.name = 'mock']);
int? version;
List<String> methods = <String>[];
List<String?> sqls = <String?>[];
List<Map<String, Object?>?> argumentsLists = <Map<String, Object?>?>[];
@override
Future<T> invokeMethod<T>(String method, [Object? arguments]) async {
// return super.invokeMethod(method, arguments);
methods.add(method);
if (arguments is Map) {
argumentsLists.add(arguments.cast<String, Object?>());
if (arguments[paramOperations] != null) {
final operations =
arguments[paramOperations] as List<Map<String, Object?>>;
for (var operation in operations) {
final sql = operation[paramSql] as String?;
sqls.add(sql);
}
} else {
final sql = arguments[paramSql] as String?;
sqls.add(sql);
// Basic version handling
if (sql?.startsWith('PRAGMA user_version = ') == true) {
version = int.tryParse(sql!.split(' ').last);
} else if (sql == 'PRAGMA user_version') {
return <Map<String, Object?>>[
<String, Object?>{'user_version': version}
] as T;
}
}
} else {
argumentsLists.add(null);
sqls.add(null);
}
//devPrint('$method $arguments');
return mockResult(method) as T;
}
}
class MockDatabaseFactory extends SqfliteDatabaseFactoryBase {
final List<String> methods = <String>[];
final List<Object?> argumentsList = <Object?>[];
final Map<String, MockDatabase> databases = <String, MockDatabase>{};
@override
Future<T> invokeMethod<T>(String method, [Object? arguments]) async {
methods.add(method);
argumentsList.add(arguments);
return mockResult(method) as T;
}
SqfliteDatabase newEmptyDatabase() {
const path = 'empty';
final helper = SqfliteDatabaseOpenHelper(this, path, OpenDatabaseOptions());
final db = helper.newDatabase(path)..id = 1;
return db;
}
@override
SqfliteDatabaseMixin newDatabase(
SqfliteDatabaseOpenHelper openHelper, String path) {
final existing = databases[path];
final db = MockDatabase(openHelper, path);
// Copy version
db.version = existing?.version;
// Last replaces
databases[path] = db;
return db;
}
@override
Future<String> getDatabasesPath() async {
return join('.dart_tool', 'sqlite', 'test', 'mock');
}
}
class MockDatabaseFactoryEmpty extends SqfliteDatabaseFactoryBase {
final List<String> methods = <String>[];
@override
Future<T> invokeMethod<T>(String method, [Object? arguments]) async {
methods.add(method);
return mockResult(method) as T;
}
}
/// This unfortunately won't work, you must extends SqfliteDatabaseFactory
class MockInvalidFactory extends DatabaseFactory {
@override
Future<bool> databaseExists(String path) async {
return false;
}
@override
Future<void> deleteDatabase(String path) async {}
@override
Future<String> getDatabasesPath() async {
throw UnimplementedError();
}
@override
Future<Database> openDatabase(String path,
{OpenDatabaseOptions? options}) async {
throw UnimplementedError('openDatabase');
}
@override
Future<void> setDatabasesPath(String path) {
throw UnimplementedError();
}
@override
Future<Uint8List> readDatabaseBytes(String path) {
throw UnimplementedError();
}
@override
Future<void> writeDatabaseBytes(String path, Uint8List bytes) {
throw UnimplementedError();
}
}
final MockDatabaseFactory mockDatabaseFactory = MockDatabaseFactory();
void run() {
group('database_factory', () {
test('getDatabasesPath', () async {
final factory = MockDatabaseFactoryEmpty();
try {
await factory.getDatabasesPath();
fail('should fail');
} on DatabaseException catch (_) {}
expect(factory.methods, <String>['getDatabasesPath']);
//expect(directory, )
});
});
group('database', () {
test('transaction', () async {
final Database db = mockDatabaseFactory.newEmptyDatabase();
await db.execute('test');
await db.insert('test', <String, Object?>{'test': 1});
await db.update('test', <String, Object?>{'test': 1});
await db.delete('test');
await db.query('test');
await db.transaction((Transaction txn) async {
await txn.execute('test');
await txn.insert('test', <String, Object?>{'test': 1});
await txn.update('test', <String, Object?>{'test': 1});
await txn.delete('test');
await txn.query('test');
});
final batch = db.batch();
batch.execute('test');
batch.insert('test', <String, Object?>{'test': 1});
batch.update('test', <String, Object?>{'test': 1});
batch.delete('test');
batch.query('test');
await batch.commit();
});
group('open', () {
test('read-only', () async {
// var db = mockDatabaseFactory.newEmptyDatabase();
final db = await mockDatabaseFactory.openDatabase('test',
options: SqfliteOpenDatabaseOptions(readOnly: true))
as MockDatabase;
await db.close();
expect(db.methods, <String>['openDatabase', 'closeDatabase']);
expect(db.argumentsLists.first, <String, Object?>{
'path': absolute(
join(await mockDatabaseFactory.getDatabasesPath(), 'test')),
'readOnly': true,
'singleInstance': true
});
});
test('not single_instance', () async {
// var db = mockDatabaseFactory.newEmptyDatabase();
final db = await mockDatabaseFactory.openDatabase('single_instance.db',
options: SqfliteOpenDatabaseOptions(singleInstance: false))
as MockDatabase;
await db.close();
expect(db.methods, <String>['openDatabase', 'closeDatabase']);
expect(db.argumentsLists.first, <String, Object?>{
'path': absolute(join(await mockDatabaseFactory.getDatabasesPath(),
'single_instance.db')),
'singleInstance': false
});
});
test('rollback transaction', () async {
// var db = mockDatabaseFactory.newEmptyDatabase();
final db = await mockDatabaseFactory.openDatabase(
'rollback_transaction.db',
options: SqfliteOpenDatabaseOptions(singleInstance: false))
as MockDatabase;
await db.execute('BEGIN TRANSACTION');
await db.close();
expect(db.methods,
<String>['openDatabase', 'execute', 'execute', 'closeDatabase']);
expect(db.argumentsLists.first, <String, Object?>{
'path': absolute(join(await mockDatabaseFactory.getDatabasesPath(),
'rollback_transaction.db')),
'singleInstance': false
});
expect(db.argumentsLists[2], <String, Object?>{
'sql': 'ROLLBACK',
'id': 1,
'transactionId': -1,
'inTransaction': false
});
});
test('isOpen', () async {
// var db = mockDatabaseFactory.newEmptyDatabase();
final db = await mockDatabaseFactory.openDatabase('is_open.db',
options: SqfliteOpenDatabaseOptions(readOnly: true))
as MockDatabase;
expect(db.isOpen, true);
final closeFuture = db.close();
// it is not closed right away
expect(db.isOpen, true);
await closeFuture;
expect(db.isOpen, false);
});
test('reOpenSameVersion', () async {
var db = await mockDatabaseFactory.openDatabase('on_reopen.db',
options: OpenDatabaseOptions(
version: 1,
)) as MockDatabase;
await db.close();
expect(db.sqls, <String?>[
null,
'PRAGMA user_version',
'BEGIN EXCLUSIVE',
'PRAGMA user_version',
'PRAGMA user_version = 1',
'COMMIT',
null
]);
db = await mockDatabaseFactory.openDatabase('on_reopen.db',
options: OpenDatabaseOptions(
version: 1,
)) as MockDatabase;
await db.close();
expect(db.sqls, <String?>[null, 'PRAGMA user_version', null]);
});
});
group('openTransaction', () {
test('onCreate', () async {
final db = await mockDatabaseFactory.openDatabase('on_create.db',
options: SqfliteOpenDatabaseOptions(
version: 1,
onCreate: (Database db, int version) async {
await db.execute('test1');
await db.transaction((Transaction txn) async {
await txn.execute('test2');
});
})) as MockDatabase;
await db.close();
expect(db.methods, <String>[
'openDatabase',
'query',
'execute',
'query',
'execute',
'execute',
'execute',
'execute',
'closeDatabase'
]);
expect(db.sqls, <String?>[
null,
'PRAGMA user_version',
'BEGIN EXCLUSIVE',
'PRAGMA user_version',
'test1',
'test2',
'PRAGMA user_version = 1',
'COMMIT',
null
]);
});
test('onConfigure', () async {
final db = await mockDatabaseFactory.openDatabase('on_configure.db',
options: OpenDatabaseOptions(
version: 1,
onConfigure: (Database db) async {
await db.execute('test1');
await db.transaction((Transaction txn) async {
await txn.execute('test2');
});
})) as MockDatabase;
await db.close();
expect(db.sqls, <String?>[
null,
'test1',
'BEGIN IMMEDIATE',
'test2',
'COMMIT',
'PRAGMA user_version',
'BEGIN EXCLUSIVE',
'PRAGMA user_version',
'PRAGMA user_version = 1',
'COMMIT',
null
]);
});
test('onOpen', () async {
final db = await mockDatabaseFactory.openDatabase('on_open',
options: OpenDatabaseOptions(
version: 1,
onOpen: (Database db) async {
await db.execute('test1');
await db.transaction((Transaction txn) async {
await txn.execute('test2');
});
})) as MockDatabase;
await db.close();
expect(db.sqls, <String?>[
null,
'PRAGMA user_version',
'BEGIN EXCLUSIVE',
'PRAGMA user_version',
'PRAGMA user_version = 1',
'COMMIT',
'test1',
'BEGIN IMMEDIATE',
'test2',
'COMMIT',
null
]);
});
test('batch', () async {
final db = await mockDatabaseFactory.openDatabase('test',
options: OpenDatabaseOptions(
version: 1,
onConfigure: (Database db) async {
final batch = db.batch();
batch.execute('test1');
await batch.commit();
},
onCreate: (Database db, _) async {
final batch = db.batch();
batch.execute('test2');
await batch.commit(noResult: true);
},
onOpen: (Database db) async {
final batch = db.batch();
batch.execute('test3');
await batch.commit(continueOnError: true);
})) as MockDatabase;
await db.close();
expect(db.sqls, <String?>[
null,
'BEGIN IMMEDIATE',
'test1',
'COMMIT',
'PRAGMA user_version',
'BEGIN EXCLUSIVE',
'PRAGMA user_version',
'test2',
'PRAGMA user_version = 1',
'COMMIT',
'BEGIN IMMEDIATE',
'test3',
'COMMIT',
null
]);
expect(db.argumentsLists, <dynamic>[
<String, Object?>{
'path': absolute(
join(await mockDatabaseFactory.getDatabasesPath(), 'test')),
'singleInstance': true
},
<String, Object?>{
'sql': 'BEGIN IMMEDIATE',
'id': 1,
'transactionId': null,
'inTransaction': true
},
<String, Object?>{
'operations': <dynamic>[
<String, Object?>{
'method': 'execute',
'sql': 'test1',
}
],
'id': 1
},
<String, Object?>{'sql': 'COMMIT', 'id': 1, 'inTransaction': false},
<String, Object?>{'sql': 'PRAGMA user_version', 'id': 1},
<String, Object?>{
'sql': 'BEGIN EXCLUSIVE',
'inTransaction': true,
'transactionId': null,
'id': 1
},
<String, Object?>{'sql': 'PRAGMA user_version', 'id': 1},
<String, Object?>{
'operations': <Map<String, Object?>>[
<String, Object?>{
'method': 'execute',
'sql': 'test2',
}
],
'id': 1,
'noResult': true
},
<String, Object?>{'sql': 'PRAGMA user_version = 1', 'id': 1},
<String, Object?>{'sql': 'COMMIT', 'id': 1, 'inTransaction': false},
<String, Object?>{
'sql': 'BEGIN IMMEDIATE',
'id': 1,
'inTransaction': true,
'transactionId': null,
},
<String, Object?>{
'operations': <Map<String, Object?>>[
<String, Object?>{
'method': 'execute',
'sql': 'test3',
}
],
'id': 1,
'continueOnError': true
},
<String, Object?>{'sql': 'COMMIT', 'id': 1, 'inTransaction': false},
<String, Object?>{'id': 1}
]);
});
});
group('concurrency', () {
test('concurrent 1', () async {
final db = mockDatabaseFactory.newEmptyDatabase() as MockDatabase;
final step1 = Completer<dynamic>();
final step2 = Completer<dynamic>();
final step3 = Completer<dynamic>();
Future<void> action1() async {
await db.execute('test');
step1.complete();
await step2.future;
try {
await db.execute('test').timeout(const Duration(milliseconds: 100));
throw 'should fail';
} catch (e) {
expect(e is TimeoutException, true);
}
step3.complete();
}
Future<void> action2() async {
// This is the change with concurrency 2
await step1.future;
await db.transaction((Transaction txn) async {
// Wait for table being created;
await txn.execute('test');
step2.complete();
await step3.future;
await txn.execute('test');
});
}
final Future<dynamic> future1 = action1();
final Future<dynamic> future2 = action2();
await Future.wait<dynamic>(<Future<dynamic>>[future1, future2]);
// check ready
await db.transaction<dynamic>((_) async {});
});
test('concurrent 2', () async {
final db = mockDatabaseFactory.newEmptyDatabase() as MockDatabase;
final step1 = Completer<dynamic>();
final step2 = Completer<dynamic>();
final step3 = Completer<dynamic>();
Future<void> action1() async {
await db.execute('test');
step1.complete();
await step2.future;
try {
await db.execute('test').timeout(const Duration(milliseconds: 100));
throw 'should fail';
} catch (e) {
expect(e is TimeoutException, true);
}
step3.complete();
}
Future<void> action2() async {
await db.transaction((Transaction txn) async {
await step1.future;
// Wait for table being created;
await txn.execute('test');
step2.complete();
await step3.future;
await txn.execute('test');
});
}
final Future<dynamic> future1 = action1();
final Future<dynamic> future2 = action2();
await Future.wait<dynamic>(<Future<dynamic>>[future1, future2]);
});
});
group('compatibility 1', () {
test('concurrent 1', () async {
final db = mockDatabaseFactory.newEmptyDatabase() as MockDatabase;
final step1 = Completer<dynamic>();
final step2 = Completer<dynamic>();
final step3 = Completer<dynamic>();
Future<void> action1() async {
await db.execute('test');
step1.complete();
await step2.future;
try {
await db.execute('test').timeout(const Duration(milliseconds: 100));
throw 'should fail';
} catch (e) {
expect(e is TimeoutException, true);
}
step3.complete();
}
Future<void> action2() async {
// This is the change with concurrency 2
await step1.future;
await db.transaction((Transaction txn) async {
// Wait for table being created;
await txn.execute('test');
step2.complete();
await step3.future;
await txn.execute('test');
});
}
final Future<dynamic> future1 = action1();
final Future<dynamic> future2 = action2();
await Future.wait<dynamic>(<Future<dynamic>>[future1, future2]);
// check ready
await db.transaction<dynamic>((_) async {});
});
test('concurrent 2', () async {
final db = mockDatabaseFactory.newEmptyDatabase() as MockDatabase;
final step1 = Completer<dynamic>();
final step2 = Completer<dynamic>();
final step3 = Completer<dynamic>();
Future<void> action1() async {
await step1.future;
try {
await db.execute('test').timeout(const Duration(milliseconds: 100));
throw 'should fail';
} catch (e) {
expect(e is TimeoutException, true);
}
await step2.future;
try {
await db.execute('test').timeout(const Duration(milliseconds: 100));
throw 'should fail';
} catch (e) {
expect(e is TimeoutException, true);
}
step3.complete();
}
Future<void> action2() async {
await db.transaction((Transaction txn) async {
step1.complete();
// Wait for table being created;
await txn.execute('test');
step2.complete();
await step3.future;
await txn.execute('test');
});
}
final Future<dynamic> future2 = action2();
final Future<dynamic> future1 = action1();
await Future.wait<dynamic>(<Future<dynamic>>[future1, future2]);
// check ready
await db.transaction<dynamic>((_) async {});
});
});
group('batch', () {
test('simple', () async {
final db = await mockDatabaseFactory.openDatabase('batch_simple.db')
as MockDatabase;
final batch = db.batch();
batch.execute('test');
await batch.commit();
await batch.commit();
await db.close();
expect(db.methods, <String>[
'openDatabase',
'execute',
'batch',
'execute',
'execute',
'batch',
'execute',
'closeDatabase'
]);
expect(db.sqls, <String?>[
null,
'BEGIN IMMEDIATE',
'test',
'COMMIT',
'BEGIN IMMEDIATE',
'test',
'COMMIT',
null
]);
});
test('in_transaction', () async {
final db = await mockDatabaseFactory
.openDatabase('batch_in_transaction.db') as MockDatabase;
await db.transaction((Transaction txn) async {
final batch = txn.batch();
batch.execute('test');
await batch.commit();
await batch.commit();
});
await db.close();
expect(db.methods, <String>[
'openDatabase',
'execute',
'batch',
'batch',
'execute',
'closeDatabase'
]);
expect(db.sqls,
<String?>[null, 'BEGIN IMMEDIATE', 'test', 'test', 'COMMIT', null]);
});
});
group('instances', () {
test('singleInstance same', () async {
final futureDb1 = mockDatabaseFactory.openDatabase('test',
options: OpenDatabaseOptions(singleInstance: true));
final db2 = await mockDatabaseFactory.openDatabase('test',
options: OpenDatabaseOptions(singleInstance: true)) as MockDatabase;
final db1 = await futureDb1 as MockDatabase;
expect(db1, db2);
});
test('singleInstance', () async {
final futureDb1 = mockDatabaseFactory.openDatabase('test',
options: OpenDatabaseOptions(singleInstance: true));
final db2 = await mockDatabaseFactory.openDatabase('test',
options: OpenDatabaseOptions(singleInstance: true)) as MockDatabase;
final db1 = await futureDb1 as MockDatabase;
final db3 = await mockDatabaseFactory.openDatabase('other',
options: OpenDatabaseOptions(singleInstance: true)) as MockDatabase;
final db4 = await mockDatabaseFactory.openDatabase(join('.', 'other'),
options: OpenDatabaseOptions(singleInstance: true)) as MockDatabase;
//expect(db1, db2);
expect(db1, isNot(db3));
expect(db3, db4);
await db1.close();
await db2.close();
await db3.close();
});
test('multiInstances', () async {
final futureDb1 = mockDatabaseFactory.openDatabase('multi_instances.db',
options: OpenDatabaseOptions(singleInstance: false));
final db2 = await mockDatabaseFactory.openDatabase('multi_instances.db',
options: OpenDatabaseOptions(singleInstance: false))
as MockDatabase;
final db1 = await futureDb1 as MockDatabase;
expect(db1, isNot(db2));
await db1.close();
await db2.close();
});
});
test('dead lock', () async {
final db = mockDatabaseFactory.newEmptyDatabase() as MockDatabase;
var hasTimedOut = false;
var callbackCount = 0;
setLockWarningInfo(
duration: const Duration(milliseconds: 200),
callback: () {
callbackCount++;
});
try {
await db.transaction((Transaction txn) async {
await db.execute('test');
fail('should fail');
}).timeout(const Duration(milliseconds: 500));
} on TimeoutException catch (_) {
hasTimedOut = true;
}
expect(hasTimedOut, isTrue);
expect(callbackCount, 1);
await db.close();
});
test('deleted/exists', () async {
const path = 'test_exists.db';
await mockDatabaseFactory.deleteDatabase(path);
final exists = await mockDatabaseFactory.databaseExists(path);
expect(exists, isFalse);
final expectedPath =
absolute(join(await mockDatabaseFactory.getDatabasesPath(), path));
expect(mockDatabaseFactory.methods,
<String>['deleteDatabase', 'databaseExists']);
expect(mockDatabaseFactory.argumentsList, <Map<String, Object?>>[
<String, Object?>{'path': expectedPath},
<String, Object?>{'path': expectedPath}
]);
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment