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.
This diff is collapsed. Click to expand it.
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 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 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"
//
// 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
//
// 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 */
//
// 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
//
// 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
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();
});
}
```
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
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