From c97f2de32109bf3d94b14411af5eb67ba6426b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuzhan=20Arslanta=C5=9F?= Date: Wed, 21 Oct 2026 01:24:03 +0200 Subject: [PATCH 1/5] sqflite_watchos 0.0.1: SQLite on the watch, on the system libsqlite3 watchOS ships libsqlite3; watchos/Package.swift links it, and Dart calls its C API over dart:ffi, resolved from the process. sqflite's method protocol (open, close, execute, insert, update, query with paged cursors, batch with noResult and continueOnError, transactions with transaction ids, database files and bytes) is answered in Dart with sqflite_common_ffi's behaviour, since method channels do not run on watchOS. registerWith makes the factory sqflite's default, as sqflite_darwin does with its method-channel one. Two FFI exports: the Documents directory, and sqlite3_libversion(), which also keeps libsqlite3 linked. buildDatabaseFactory and SqfliteDatabaseException come from sqflite_common's src/ libraries, as sqflite_common_ffi imports them. sqflite's own example app depends on the unpublished sqflite_example_common, so the example's lib/ is a small watch demo; the official integration test is upstream's, unchanged. It uses package:test's test, which drive does not collect, so it runs with flutter-watchos test. The root README lists the package and drops sqflite from "Feasible but not yet ported"; tool/upstream_versions.yaml gains sqflite 2.4.4+2. Checked: 34 host tests running real SQL against macOS's libsqlite3; the official integration test (14 tests) on the watchOS 27.0 Simulator; the example there saving and listing rows. Not yet checked on a physical watch. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 +- packages/sqflite_watchos/.gitignore | 23 + packages/sqflite_watchos/CHANGELOG.md | 17 + packages/sqflite_watchos/LICENSE | 25 + packages/sqflite_watchos/PORTING_REPORT.md | 80 +++ packages/sqflite_watchos/README.md | 69 ++ .../sqflite_watchos/analysis_options.yaml | 19 + packages/sqflite_watchos/example/.gitignore | 14 + packages/sqflite_watchos/example/README.md | 16 + .../example/analysis_options.yaml | 4 + .../integration_test/sqflite_test.dart | 377 +++++++++++ .../sqflite_watchos/example/lib/main.dart | 112 ++++ packages/sqflite_watchos/example/pubspec.yaml | 28 + .../example/test_driver/integration_test.dart | 4 + .../example/watchos/Flutter/.gitignore | 3 + .../AppIcon.appiconset/Contents.json | 13 + .../HostApp/Assets.xcassets/Contents.json | 6 + .../example/watchos/HostApp/Info.plist | 48 ++ .../watchos/Runner.xcodeproj/project.pbxproj | 438 +++++++++++++ .../xcshareddata/xcschemes/Runner.xcscheme | 81 +++ .../xcschemes/sqflite_example.xcscheme | 77 +++ .../example/watchos/Runner/App.swift | 57 ++ .../AppIcon.appiconset/Contents.json | 140 ++++ .../AppIcon.appiconset/icon_1024.png | Bin 0 -> 13949 bytes .../icon_108x108_2x_44mm.png | Bin 0 -> 3890 bytes .../icon_117x117_2x_45mm.png | Bin 0 -> 4182 bytes .../icon_129x129_2x_49mm.png | Bin 0 -> 4452 bytes .../AppIcon.appiconset/icon_24x24_2x_38mm.png | Bin 0 -> 1066 bytes .../icon_27.5x27.5_2x_42mm.png | Bin 0 -> 1261 bytes .../icon_29x29_2x_companion.png | Bin 0 -> 1214 bytes .../icon_29x29_3x_companion.png | Bin 0 -> 1603 bytes .../AppIcon.appiconset/icon_33x33_2x_45mm.png | Bin 0 -> 1366 bytes .../AppIcon.appiconset/icon_40x40_2x_38mm.png | Bin 0 -> 1559 bytes .../AppIcon.appiconset/icon_44x44_2x_40mm.png | Bin 0 -> 1756 bytes .../AppIcon.appiconset/icon_46x46_2x_41mm.png | Bin 0 -> 1800 bytes .../AppIcon.appiconset/icon_50x50_2x_44mm.png | Bin 0 -> 2068 bytes .../AppIcon.appiconset/icon_51x51_2x_45mm.png | Bin 0 -> 1846 bytes .../AppIcon.appiconset/icon_54x54_2x_49mm.png | Bin 0 -> 1987 bytes .../AppIcon.appiconset/icon_86x86_2x.png | Bin 0 -> 2891 bytes .../AppIcon.appiconset/icon_98x98_2x.png | Bin 0 -> 3680 bytes .../example/watchos/Runner/Info.plist | 28 + .../sqflite_watchos/lib/sqflite_watchos.dart | 75 +++ packages/sqflite_watchos/lib/src/handler.dart | 553 ++++++++++++++++ packages/sqflite_watchos/lib/src/sqlite3.dart | 502 +++++++++++++++ packages/sqflite_watchos/pubspec.yaml | 55 ++ .../test/sqflite_watchos_test.dart | 601 ++++++++++++++++++ .../watchos/Classes/sqflite_watchos_ffi.h | 32 + .../watchos/Classes/sqflite_watchos_ffi.m | 30 + .../sqflite_watchos/watchos/Package.swift | 34 + tool/upstream_versions.yaml | 1 + 50 files changed, 3564 insertions(+), 1 deletion(-) create mode 100644 packages/sqflite_watchos/.gitignore create mode 100644 packages/sqflite_watchos/CHANGELOG.md create mode 100644 packages/sqflite_watchos/LICENSE create mode 100644 packages/sqflite_watchos/PORTING_REPORT.md create mode 100644 packages/sqflite_watchos/README.md create mode 100644 packages/sqflite_watchos/analysis_options.yaml create mode 100644 packages/sqflite_watchos/example/.gitignore create mode 100644 packages/sqflite_watchos/example/README.md create mode 100644 packages/sqflite_watchos/example/analysis_options.yaml create mode 100644 packages/sqflite_watchos/example/integration_test/sqflite_test.dart create mode 100644 packages/sqflite_watchos/example/lib/main.dart create mode 100644 packages/sqflite_watchos/example/pubspec.yaml create mode 100644 packages/sqflite_watchos/example/test_driver/integration_test.dart create mode 100644 packages/sqflite_watchos/example/watchos/Flutter/.gitignore create mode 100644 packages/sqflite_watchos/example/watchos/HostApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/sqflite_watchos/example/watchos/HostApp/Assets.xcassets/Contents.json create mode 100644 packages/sqflite_watchos/example/watchos/HostApp/Info.plist create mode 100644 packages/sqflite_watchos/example/watchos/Runner.xcodeproj/project.pbxproj create mode 100644 packages/sqflite_watchos/example/watchos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packages/sqflite_watchos/example/watchos/Runner.xcodeproj/xcshareddata/xcschemes/sqflite_example.xcscheme create mode 100644 packages/sqflite_watchos/example/watchos/Runner/App.swift create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_1024.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_108x108_2x_44mm.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_117x117_2x_45mm.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_129x129_2x_49mm.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_24x24_2x_38mm.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_27.5x27.5_2x_42mm.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_29x29_2x_companion.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_29x29_3x_companion.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_33x33_2x_45mm.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_40x40_2x_38mm.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_44x44_2x_40mm.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_46x46_2x_41mm.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_50x50_2x_44mm.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_51x51_2x_45mm.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_54x54_2x_49mm.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_86x86_2x.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_98x98_2x.png create mode 100644 packages/sqflite_watchos/example/watchos/Runner/Info.plist create mode 100644 packages/sqflite_watchos/lib/sqflite_watchos.dart create mode 100644 packages/sqflite_watchos/lib/src/handler.dart create mode 100644 packages/sqflite_watchos/lib/src/sqlite3.dart create mode 100644 packages/sqflite_watchos/pubspec.yaml create mode 100644 packages/sqflite_watchos/test/sqflite_watchos_test.dart create mode 100644 packages/sqflite_watchos/watchos/Classes/sqflite_watchos_ffi.h create mode 100644 packages/sqflite_watchos/watchos/Classes/sqflite_watchos_ffi.m create mode 100644 packages/sqflite_watchos/watchos/Package.swift diff --git a/README.md b/README.md index bf62315..a01216c 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ at the time; the last full run on the Simulator is under | [`sensors_plus_watchos`](packages/sensors_plus_watchos) [![pub](https://img.shields.io/pub/v/sensors_plus_watchos.svg)](https://pub.dev/packages/sensors_plus_watchos) | [`sensors_plus`](https://pub.dev/packages/sensors_plus) | CoreMotion (`CMMotionManager`) | | [`local_auth_watchos`](packages/local_auth_watchos) [![pub](https://img.shields.io/pub/v/local_auth_watchos.svg)](https://pub.dev/packages/local_auth_watchos) | [`local_auth`](https://pub.dev/packages/local_auth) | LocalAuthentication (passcode) | | [`geolocator_watchos`](packages/geolocator_watchos) [![pub](https://img.shields.io/pub/v/geolocator_watchos.svg)](https://pub.dev/packages/geolocator_watchos) | [`geolocator`](https://pub.dev/packages/geolocator) | CoreLocation (`CLLocationManager`) | +| [`sqflite_watchos`](packages/sqflite_watchos) | [`sqflite`](https://pub.dev/packages/sqflite) | The system SQLite (`libsqlite3`), its C API called from Dart | | [`video_player_watchos`](packages/video_player_watchos) [![pub](https://img.shields.io/pub/v/video_player_watchos.svg)](https://pub.dev/packages/video_player_watchos) | [`video_player`](https://pub.dev/packages/video_player) | AVFoundation + AVKit platform view | | [`audioplayers_watchos`](packages/audioplayers_watchos) [![pub](https://img.shields.io/pub/v/audioplayers_watchos.svg)](https://pub.dev/packages/audioplayers_watchos) | [`audioplayers`](https://pub.dev/packages/audioplayers) | AVFoundation (`AVPlayer`) | | [`in_app_purchase_watchos`](packages/in_app_purchase_watchos) [![pub](https://img.shields.io/pub/v/in_app_purchase_watchos.svg)](https://pub.dev/packages/in_app_purchase_watchos) | [`in_app_purchase`](https://pub.dev/packages/in_app_purchase) | StoreKit (`SKProductsRequest`, `SKPaymentQueue`) | @@ -115,7 +116,7 @@ watch** — plugins built on those are portable, not excluded. These have a watchOS-viable native backend and are good future additions; they are simply out of scope for now (large surface or partial support): -`sqflite` (SQLite), `flutter_tts` (AVFoundation), +`flutter_tts` (AVFoundation), `wakelock_plus` (only a session-typed `WKExtendedRuntimeSession`, not a general idle-timer disable), and `cloud_firestore` (the Firebase Apple SDK ships Firestore's core as a prebuilt binary with no watchOS slice, so a diff --git a/packages/sqflite_watchos/.gitignore b/packages/sqflite_watchos/.gitignore new file mode 100644 index 0000000..7b8ac9e --- /dev/null +++ b/packages/sqflite_watchos/.gitignore @@ -0,0 +1,23 @@ +# Dart / Flutter +.dart_tool/ +build/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub/ + +# Xcode / SwiftPM (per-user, generated when watchos/Package.swift is opened) +**/.swiftpm/ +**/.build/ +**/xcuserdata/ + +# flutter-watchos app wiring (generated in an app's watchos/, never a plugin's) +watchos/Flutter/ + +# IDE +.idea/ +.vscode/ +*.iml + +# macOS +.DS_Store diff --git a/packages/sqflite_watchos/CHANGELOG.md b/packages/sqflite_watchos/CHANGELOG.md new file mode 100644 index 0000000..d0f7848 --- /dev/null +++ b/packages/sqflite_watchos/CHANGELOG.md @@ -0,0 +1,17 @@ +## 0.0.1 + +First version, staged: checked on the watchOS 27.0 Simulator, not yet on a +physical watch. + +* sqflite on the watch, on the system SQLite: `watchos/Package.swift` links + `libsqlite3`, and Dart calls its C API over dart:ffi. +* sqflite's method protocol answered in Dart: open, close, execute, insert, + update, query with paged cursors, batch with `noResult` and + `continueOnError`, transactions with transaction ids, database files and + bytes, as sqflite_common_ffi answers it. +* `registerWith` makes this package's factory sqflite's default on watchOS. +* Two FFI exports: `sqflite_watchos_databases_path` and + `sqflite_watchos_sqlite_version`. +* The example's `lib/` is a small watch demo, because sqflite's own example + depends on the unpublished `sqflite_example_common`; its official + integration test is upstream's, unchanged. diff --git a/packages/sqflite_watchos/LICENSE b/packages/sqflite_watchos/LICENSE new file mode 100644 index 0000000..5385068 --- /dev/null +++ b/packages/sqflite_watchos/LICENSE @@ -0,0 +1,25 @@ +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. diff --git a/packages/sqflite_watchos/PORTING_REPORT.md b/packages/sqflite_watchos/PORTING_REPORT.md new file mode 100644 index 0000000..dcd956b --- /dev/null +++ b/packages/sqflite_watchos/PORTING_REPORT.md @@ -0,0 +1,80 @@ +# sqflite_watchos — porting report + +## Verification status + +| Aspect | Result | +|---|---| +| Implementation | ✅ Working (system SQLite over FFI) | +| watchOS capability | Full — every method of sqflite's protocol. Not yet checked on a physical watch | +| Host unit tests (`flutter test`) | ✅ pass — real SQL against macOS's `libsqlite3`, through sqflite_common's `Database` API and the protocol below it | +| Upstream integration test | ✅ passes verbatim on the watchOS 27.0 Simulator (14 tests), against the watch's own `libsqlite3`. Run it with `flutter-watchos test`: it uses `package:test`'s `test`, which `flutter-watchos drive` does not collect, so `drive` reports nothing and waits | +| Internal unified demo | ○ not included yet | + +Marking: ✅ full / passes · ◐ partial — reason given · ○ not applicable (no upstream test) · ✗ unsupported on watchOS. + +> This scaffold has been **finished by hand** into a working FFI package; see +> the status above. The generated notes below are kept for provenance. + +Generated by `flutter-watchos plugin port` on 2026-10-19. + +Source: `sqflite_darwin` 2.4.4+1 +Base platform: ios (Objective-C) +Output: `./sqflite_watchos` + +## How this differs from sqflite_darwin + +sqflite_darwin is a method-channel plugin around FMDB. Method channels do +not run on watchOS, and FMDB is a wrapper the watch does not need, so this +package keeps none of that native code: + +- `lib/src/sqlite3.dart` binds the part of SQLite's C API sqflite needs + (`sqlite3_open_v2`, `_prepare_v2`, `_step`, the `_bind_*` and `_column_*` + functions, `_changes`, `_last_insert_rowid`, `_get_autocommit`, the error + calls). On the watch they resolve from the process, because + `watchos/Package.swift` links the system `libsqlite3`; host tests open the + macOS system library instead, so the same code runs real SQL there. +- `lib/src/handler.dart` answers sqflite's method protocol: the same + methods, arguments and results the method channel carries on iOS, with the + behaviour of sqflite_common_ffi, the protocol's reference implementation + outside a method channel. That covers transaction ids (a call from outside + the open transaction waits for it), `transactionClosed` when SQLite rolls a + transaction back, paged cursors, batches with `continueOnError`, and + single-instance databases. +- `watchos/Classes/sqflite_watchos_ffi.m` has two helpers: the Documents + directory, and `sqlite3_libversion()`, which also keeps `libsqlite3` linked. +- sqflite federates through sqflite_common's default factory: `registerWith` + sets `databaseFactoryOrNull` to a factory from sqflite_common's + `buildDatabaseFactory`, as sqflite_darwin sets its method-channel factory. + `buildDatabaseFactory` and `SqfliteDatabaseException` come from + sqflite_common's `src/` libraries, which sqflite_common_ffi imports the + same way; `sqflite_common: ^2.5.11` pins them. + +The one API the port tool flagged, `UIDevice` (the platform version), is +replaced by `Platform.operatingSystemVersion`. + +## Example + +sqflite's own example app is built on `sqflite_example_common`, which is +not published, so `example/lib/main.dart` is a small watch demo instead. +`example/integration_test/sqflite_test.dart` is upstream's test, unchanged. + +## Protocol coverage + +| Method | watchOS | +|---|---| +| `openDatabase` | ✅ read-write (creating parent directories) or read-only; single instance by path | +| `closeDatabase` | ✅ cancels operations queued behind a transaction; closing twice is fine | +| `execute` | ✅ several statements without arguments, one with; begins and ends transactions | +| `insert` | ✅ the new row id, or null when nothing was inserted (sqflite_common returns 0) | +| `update` | ✅ the changed row count | +| `query` / `queryCursorNext` | ✅ all rows, or pages for a cursor | +| `batch` | ✅ `noResult`, `continueOnError` | +| `getDatabasesPath` | ✅ the app's Documents directory | +| `databaseExists` / `deleteDatabase` | ✅ | +| `readDatabaseBytes` / `writeDatabaseBytes` | ✅ | +| `options` / `debugMode` | ✅ kept; nothing is logged | +| `getPlatformVersion` | ✅ `watchOS ` | + +--- + +See the finished plugins in the `flutterwatch/plugins` repo (e.g. `path_provider_watchos`, `battery_plus_watchos`) for worked examples of this exact shape. diff --git a/packages/sqflite_watchos/README.md b/packages/sqflite_watchos/README.md new file mode 100644 index 0000000..7b83302 --- /dev/null +++ b/packages/sqflite_watchos/README.md @@ -0,0 +1,69 @@ +# sqflite_watchos + +The watchOS implementation of [`sqflite`](https://pub.dev/packages/sqflite). + +Databases are the **system SQLite**: watchOS ships `libsqlite3`, the +flutter-watchos CLI links it, and Dart calls its C API over dart:ffi. +sqflite's method protocol (open, execute, insert, update, query with cursors, +batch, transactions) is answered in Dart, the way `sqflite_common_ffi` +answers it on desktop, so the `Database` API behaves as it does on iOS and +Android. + +> Scaffolded by [`flutter-watchos plugin port`](https://github.com/flutterwatch/flutter-watchos) +> from `sqflite_darwin`, then implemented and verified by hand. + +## Usage + +This is a federated plugin implementation. Apps that already depend on +`sqflite` and target watchOS only need to add this package alongside it: + +```yaml +dependencies: + sqflite: ^2.4.4 + sqflite_watchos: ^0.0.1 +``` + +The plugin registers automatically via Flutter's federated registry — no +explicit imports required from app code. Use sqflite as usual: + +```dart +final Database db = await openDatabase( + 'laps.db', + version: 1, + onCreate: (Database db, int version) => + db.execute('CREATE TABLE lap (id INTEGER PRIMARY KEY, at INTEGER)'), +); +await db.insert('lap', {'at': DateTime.now().millisecondsSinceEpoch}); +``` + +## Behaviour on watchOS + +| sqflite | watchOS | +|---|---| +| `openDatabase` / `openReadOnlyDatabase` | supported; relative paths live in the app's Documents directory, as on iOS, and missing parent directories are created | +| `execute` / `insert` / `update` / `delete` / `rawQuery` and friends | supported; integers are 64-bit, text is UTF-8, blobs come back as `Uint8List` | +| `queryCursor` | supported, page by page | +| `batch` | supported, with `noResult` and `continueOnError` | +| `transaction` | supported; calls from outside an open transaction wait until it ends | +| `databaseExists` / `deleteDatabase` / `getDatabasesPath` | supported; deleting an open single-instance database closes it first | +| `readDatabaseBytes` / `writeDatabaseBytes` | supported | +| `DatabaseException` helpers | `isUniqueConstraintError`, `isNoSuchTableError`, `isSyntaxError`, `isOpenFailedError`, `isReadOnlyError`, `isDatabaseClosedError`; `getResultCode()` is SQLite's extended result code | + +SQL runs on the isolate that calls it, synchronously inside each `Future`. +On iOS sqflite runs it on a native background thread; on the watch a long +query holds up the frame it runs in, so keep heavy work small or move it to +an isolate of its own. + +`SqfliteWatchos.sqliteVersion` reports the system SQLite's version. + +## Not supported on watchOS + +Nothing: every method of sqflite's protocol is answered. +`androidSetLocale` and the other Android-only calls are Android-only on +every other platform too. The sqflite-specific `PRAGMA sqflite_…` +statements are accepted and ignored. +`PORTING_REPORT.md` lists each method under "Protocol coverage". + +## License + +The FlutterWatch Authors under a BSD-3-Clause license. See `LICENSE` for the full text. diff --git a/packages/sqflite_watchos/analysis_options.yaml b/packages/sqflite_watchos/analysis_options.yaml new file mode 100644 index 0000000..1d83d30 --- /dev/null +++ b/packages/sqflite_watchos/analysis_options.yaml @@ -0,0 +1,19 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + +linter: + rules: + public_member_api_docs: true diff --git a/packages/sqflite_watchos/example/.gitignore b/packages/sqflite_watchos/example/.gitignore new file mode 100644 index 0000000..3b47c00 --- /dev/null +++ b/packages/sqflite_watchos/example/.gitignore @@ -0,0 +1,14 @@ +.dart_tool/ +.packages +build/ +.flutter-plugins +.flutter-plugins-dependencies +*.iml +.idea/ +.DS_Store + +# watchOS / CocoaPods +watchos/Pods/ +watchos/Podfile.lock +watchos/.symlinks/ +watchos/Flutter/Flutter.framework diff --git a/packages/sqflite_watchos/example/README.md b/packages/sqflite_watchos/example/README.md new file mode 100644 index 0000000..69ed5b7 --- /dev/null +++ b/packages/sqflite_watchos/example/README.md @@ -0,0 +1,16 @@ +# sqflite_example + +A watchOS-only example: laps saved to a SQLite database on the watch. Run it +on an Apple Watch simulator with: + +```sh +flutter-watchos run +``` + +`integration_test/sqflite_test.dart` is sqflite's official integration test, +unchanged. It uses `package:test`'s `test`, which `flutter-watchos drive` +does not collect, so run it with `test`: + +```sh +flutter-watchos test integration_test/sqflite_test.dart -d +``` diff --git a/packages/sqflite_watchos/example/analysis_options.yaml b/packages/sqflite_watchos/example/analysis_options.yaml new file mode 100644 index 0000000..801d885 --- /dev/null +++ b/packages/sqflite_watchos/example/analysis_options.yaml @@ -0,0 +1,4 @@ +analyzer: + exclude: + - build/** +include: package:flutter_lints/flutter.yaml diff --git a/packages/sqflite_watchos/example/integration_test/sqflite_test.dart b/packages/sqflite_watchos/example/integration_test/sqflite_test.dart new file mode 100644 index 0000000..1eb7855 --- /dev/null +++ b/packages/sqflite_watchos/example/integration_test/sqflite_test.dart @@ -0,0 +1,377 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:io' as io; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart' hide test; +import 'package:material_ui/material_ui.dart'; +import 'package:path/path.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:test/test.dart' show test; + +// ignore_for_file: avoid_print +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + group('sqflite', () { + group('open', () { + test('missing directory', () async { + //await devVerbose(); + final path = join('test_missing_sub_dir', 'simple.db'); + try { + await Directory(join(await getDatabasesPath(), dirname(path))) + .delete(recursive: true); + } catch (_) {} + final db = await openDatabase( + path, + version: 1, + onCreate: (db, version) async { + expect(await db.getVersion(), 0); + }, + ); + expect(await db.getVersion(), 1); + await db.close(); + }); + test('failure', () { + // This one seems ignored + // fail('regular test failure'); + }); + test('in_memory', () async { + final db = await openDatabase( + inMemoryDatabasePath, + version: 1, + onCreate: (db, version) async { + expect(await db.getVersion(), 0); + }, + ); + expect(await db.getVersion(), 1); + await db.close(); + }); + }); + + test('exists', () async { + expect(await databaseExists(inMemoryDatabasePath), isFalse); + const path = 'test_exists.db'; + await deleteDatabase(path); + expect(await databaseExists(path), isFalse); + final db = await openDatabase(path); + try { + expect(await databaseExists(path), isTrue); + } finally { + await db.close(); + } + }); + test('close in transaction', () async { + // await Sqflite.devSetDebugModeOn(true); + const path = 'test_close_in_transaction.db'; + final factory = databaseFactory; + await deleteDatabase(path); + var db = await factory.openDatabase( + path, + options: OpenDatabaseOptions(version: 1), + ); + try { + await db.execute('BEGIN TRANSACTION'); + await db.close(); + + db = await factory.openDatabase( + path, + options: OpenDatabaseOptions(version: 1), + ); + } finally { + await db.close(); + } + }); + + /// Check if a file is a valid database file + /// + /// An empty file is a valid empty sqlite file + Future isDatabase(String path) async { + var isDatabase = false; + Database? db; + try { + db = await openReadOnlyDatabase(path); + //await db.getVersion(); + await db.query('sqlite_master'); + isDatabase = true; + } catch (_) { + } finally { + await db?.close(); + } + return isDatabase; + } + + test('read_only missing database', () async { + const path = 'test_missing_database.db'; + await deleteDatabase(path); + try { + final db = await openReadOnlyDatabase(path); + fail('should fail ${db.path}'); + } on DatabaseException catch (_) {} + + expect(await isDatabase(path), isFalse); + }); + + test('read_only empty file', () async { + const path = 'empty_file_database.db'; + await deleteDatabase(path); + final fullPath = join((await getDatabasesPath()), path); + await Directory(dirname(fullPath)).create(recursive: true); + await File(fullPath).writeAsString(''); + + // Open is fine, that is the native behavior + final db = await openReadOnlyDatabase(fullPath); + expect(await File(fullPath).readAsString(), ''); + + await db.getVersion(); + + await db.close(); + expect(await File(fullPath).readAsString(), ''); + expect(await isDatabase(fullPath), isTrue); + }); + + test('read_only missing bad format', () async { + const path = 'test_bad_format_database.db'; + await deleteDatabase(path); + final fullPath = join((await getDatabasesPath()), path); + await Directory(dirname(fullPath)).create(recursive: true); + await File(fullPath).writeAsString('test'); + + // Open is fine, that is the native behavior + final db = await openReadOnlyDatabase(fullPath); + print('DEBUG'); + expect(await File(fullPath).readAsString(), 'test'); + try { + final version = await db.getVersion(); + + print(await db.query('sqlite_master')); + fail('getVersion should fail ${db.path} $version'); + } on DatabaseException catch (_) { + // Android: DatabaseException(file is not a database (code 26 SQLITE_NOTADB)) sql 'PRAGMA user_version' args []} + } + await db.close(); + expect(await File(fullPath).readAsString(), 'test'); + + expect(await isDatabase(fullPath), isFalse); + expect(await isDatabase(fullPath), isFalse); + + expect(await File(fullPath).readAsString(), 'test'); + }); + + test('multiple database', () async { + //await Sqflite.devSetDebugModeOn(true); + const count = 10; + final dbs = List.filled(count, null, growable: false); + for (var i = 0; i < count; i++) { + final path = 'test_multiple_$i.db'; + await deleteDatabase(path); + dbs[i] = await openDatabase( + path, + version: 1, + onCreate: (db, version) async { + await db.execute( + 'CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)', + ); + expect( + await db.rawInsert('INSERT INTO Test (name) VALUES (?)', [ + 'test_$i', + ]), + 1, + ); + }, + ); + } + + for (var i = 0; i < count; i++) { + final db = dbs[i]!; + try { + final name = + (await db.query('Test', columns: ['name'])).first.values.first + as String?; + expect(name, 'test_$i'); + } finally { + await db.close(); + } + } + + for (var i = 0; i < count; i++) { + final db = dbs[i]!; + await db.close(); + } + }); + + test('version', () async { + // await Sqflite.devSetDebugModeOn(true); + const path = 'test_version.db'; + await deleteDatabase(path); + var db = await openDatabase(path, version: 1); + try { + expect(await db.getVersion(), 1); + unawaited(db.close()); + + db = await openDatabase(path, version: 2); + expect(await db.getVersion(), 2); + unawaited(db.close()); + + db = await openDatabase(path, version: 1); + expect(await db.getVersion(), 1); + unawaited(db.close()); + + db = await openDatabase(path, version: 1); + expect(await db.getVersion(), 1); + expect(await isDatabase(path), isTrue); + } finally { + await db.close(); + } + expect(await isDatabase(path), isTrue); + }); + + test('duplicated_column', () async { + // await Sqflite.devSetDebugModeOn(true); + const path = 'test_duplicated_column.db'; + await deleteDatabase(path); + final db = await openDatabase(path); + try { + await db.execute('CREATE TABLE Test (col1 INTEGER, col2 INTEGER)'); + await db.insert('Test', {'col1': 1, 'col2': 2}); + + final result = await db.rawQuery( + 'SELECT t.col1, col1, t.col2, col2 AS col1 FROM Test AS t', + ); + expect(result, [ + {'col1': 2, 'col2': 2}, + ]); + } finally { + await db.close(); + } + }); + + test('indexed_param', () async { + final db = await openDatabase(':memory:'); + expect(await db.rawQuery('SELECT ?1 + ?2', [3, 4]), [ + {'?1 + ?2': 7}, + ]); + await db.close(); + }); + + test('deleteDatabase', () async { + // await devVerbose(); + const path = 'test_delete_database.db'; + await deleteDatabase(path); + expect(await databaseExists(path), isFalse); + + late Database db; + try { + db = await openDatabase(path); + expect(await db.getVersion(), 0); + await db.setVersion(1); + await db.close(); + expect(await databaseExists(path), isTrue); + + await deleteDatabase(path); + expect(await databaseExists(path), isFalse); + + db = await openDatabase(path); + expect(await db.getVersion(), 0); + await db.setVersion(1); + + // delete without closing every time + await deleteDatabase(path); + db = await openDatabase(path); + expect(await db.getVersion(), 0); + await db.execute('BEGIN TRANSACTION'); + await db.setVersion(2); + + await deleteDatabase(path); + db = await openDatabase(path); + expect(await db.getVersion(), 0); + await db.setVersion(3); + + await deleteDatabase(path); + db = await openDatabase(path); + expect(await db.getVersion(), 0); + } finally { + await db.close(); + } + }); + test('androidSetLocale', () async { + if (!kIsWeb && io.Platform.isAndroid) { + const path = 'test_android_set_locale.db'; + await deleteDatabase(path); + late Database db; + + db = await openDatabase( + path, + onConfigure: (db) async { + await db.androidSetLocale('zh-CN'); + }, + version: 1, + onCreate: (db, v) async { + await db.execute('CREATE TABLE Test(name TEXT)'); + for (var row in >[ + {'name': '桌'}, + {'name': '椅'}, + {'name': '盘'}, + ]) { + await db.insert('Test', row); + } + }, + ); + Future> getNames() async { + return (await db.query( + 'Test', + orderBy: 'name COLLATE LOCALIZED ASC', + )).map((e) => e.values.first!.toString()).toList(); + } + + try { + // Order ok + expect(await getNames(), ['盘', '椅', '桌']); + + // Reopen same locale + await db.close(); + db = await openDatabase( + path, + onConfigure: (db) async { + await db.androidSetLocale('zh-CN'); + }, + ); + // order ok too + expect(await getNames(), ['盘', '椅', '桌']); + + await db.close(); + // No locale + db = await openDatabase(path); + // order not ok + expect(await getNames(), ['桌', '椅', '盘']); + + await db.close(); + db = await openDatabase( + path, + onConfigure: (db) async { + await db.androidSetLocale('en-US'); + }, + ); + // order not ok + expect(await getNames(), ['桌', '椅', '盘']); + + await db.close(); + db = await openDatabase( + path, + onConfigure: (db) async { + await db.androidSetLocale('zh-CN'); + }, + ); + // order ok again + expect(await getNames(), ['盘', '椅', '桌']); + } finally { + await db.close(); + } + } + }); + }); +} diff --git a/packages/sqflite_watchos/example/lib/main.dart b/packages/sqflite_watchos/example/lib/main.dart new file mode 100644 index 0000000..292ec95 --- /dev/null +++ b/packages/sqflite_watchos/example/lib/main.dart @@ -0,0 +1,112 @@ +// Copyright 2026 The FlutterWatch Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// sqflite's own example app is built on sqflite_example_common, which is not +// published, so this watch-sized demo stands in for it. The official +// integration test in integration_test/ is upstream's, unchanged. + +import 'package:flutter/material.dart'; +import 'package:sqflite/sqflite.dart'; + +void main() => runApp(const SqfliteExampleApp()); + +/// Laps saved to a SQLite database on the watch. +class SqfliteExampleApp extends StatelessWidget { + /// The example app. + const SqfliteExampleApp({super.key}); + + @override + Widget build(BuildContext context) => MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData.dark(useMaterial3: true), + home: const LapsPage(), + ); +} + +/// Add, list and clear laps. +class LapsPage extends StatefulWidget { + /// The one page. + const LapsPage({super.key}); + + @override + State createState() => _LapsPageState(); +} + +class _LapsPageState extends State { + Database? _db; + List> _laps = const >[]; + String? _error; + + @override + void initState() { + super.initState(); + _open(); + } + + @override + void dispose() { + _db?.close(); + super.dispose(); + } + + Future _open() async { + try { + _db = await openDatabase( + 'laps.db', + version: 1, + onCreate: (Database db, int version) => db.execute( + 'CREATE TABLE lap (id INTEGER PRIMARY KEY, at INTEGER NOT NULL)'), + ); + await _reload(); + } on DatabaseException catch (e) { + setState(() => _error = '$e'); + } + } + + Future _reload() async { + final List> laps = + await _db!.query('lap', orderBy: 'id DESC'); + setState(() => _laps = laps); + } + + Future _add() async { + await _db!.insert('lap', { + 'at': DateTime.now().millisecondsSinceEpoch, + }); + await _reload(); + } + + Future _clear() async { + await _db!.delete('lap'); + await _reload(); + } + + String _time(Object? millis) { + final DateTime t = DateTime.fromMillisecondsSinceEpoch(millis! as int); + String two(int v) => v.toString().padLeft(2, '0'); + return '${two(t.hour)}:${two(t.minute)}:${two(t.second)}'; + } + + @override + Widget build(BuildContext context) => Scaffold( + body: ListView( + padding: const EdgeInsets.fromLTRB(12, 28, 12, 24), + children: [ + Text('${_laps.length} laps', + style: + const TextStyle(fontSize: 24, fontWeight: FontWeight.w600)), + if (_error != null) + Text(_error!, style: const TextStyle(color: Colors.orangeAccent)), + FilledButton( + onPressed: _db == null ? null : _add, + child: const Text('Add lap'), + ), + for (final Map lap in _laps.take(5)) + Text('#${lap['id']} ${_time(lap['at'])}'), + if (_laps.isNotEmpty) + TextButton(onPressed: _clear, child: const Text('Clear')), + ], + ), + ); +} diff --git a/packages/sqflite_watchos/example/pubspec.yaml b/packages/sqflite_watchos/example/pubspec.yaml new file mode 100644 index 0000000..8aa9ab6 --- /dev/null +++ b/packages/sqflite_watchos/example/pubspec.yaml @@ -0,0 +1,28 @@ +name: sqflite_example +description: "watchOS example for sqflite_watchos." +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: ">=3.1.0 <4.0.0" + +dependencies: + flutter: + sdk: flutter + sqflite: ^2.4.2 + sqflite_watchos: + path: ../ + path: ^1.9.0 + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + flutter_lints: ^4.0.0 + # The official integration test imports these two. + material_ui: ^1.5.0 + test: ^1.25.0 + +flutter: + uses-material-design: true diff --git a/packages/sqflite_watchos/example/test_driver/integration_test.dart b/packages/sqflite_watchos/example/test_driver/integration_test.dart new file mode 100644 index 0000000..2acb97e --- /dev/null +++ b/packages/sqflite_watchos/example/test_driver/integration_test.dart @@ -0,0 +1,4 @@ +// Generated by `flutter-watchos plugin port --include-example`. +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/sqflite_watchos/example/watchos/Flutter/.gitignore b/packages/sqflite_watchos/example/watchos/Flutter/.gitignore new file mode 100644 index 0000000..196f947 --- /dev/null +++ b/packages/sqflite_watchos/example/watchos/Flutter/.gitignore @@ -0,0 +1,3 @@ +# Build artifacts placed here by `flutter-watchos build` — never commit. +* +!.gitignore diff --git a/packages/sqflite_watchos/example/watchos/HostApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/sqflite_watchos/example/watchos/HostApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/packages/sqflite_watchos/example/watchos/HostApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/sqflite_watchos/example/watchos/HostApp/Assets.xcassets/Contents.json b/packages/sqflite_watchos/example/watchos/HostApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/packages/sqflite_watchos/example/watchos/HostApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/sqflite_watchos/example/watchos/HostApp/Info.plist b/packages/sqflite_watchos/example/watchos/HostApp/Info.plist new file mode 100644 index 0000000..3dd42a5 --- /dev/null +++ b/packages/sqflite_watchos/example/watchos/HostApp/Info.plist @@ -0,0 +1,48 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Sqflite Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + ITSWatchOnlyContainer + + LSApplicationLaunchProhibited + + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/packages/sqflite_watchos/example/watchos/Runner.xcodeproj/project.pbxproj b/packages/sqflite_watchos/example/watchos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3d9e382 --- /dev/null +++ b/packages/sqflite_watchos/example/watchos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,438 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + AB0000000000000000000001 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF0000000000000000000001 /* App.swift */; }; + AB0000000000000000000003 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF0000000000000000000003 /* Flutter.framework */; }; + AB0000000000000000000004 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = AF0000000000000000000003 /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + AB0000000000000000000005 /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = AF0000000000000000000004 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + AB0000000000000000000006 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = AF0000000000000000000005 /* flutter_assets */; }; + AB0000000000000000000007 /* icudtl.dat in Resources */ = {isa = PBXBuildFile; fileRef = AF0000000000000000000006 /* icudtl.dat */; }; + AB0000000000000000000008 /* vm_isolate_snapshot.bin in Resources */ = {isa = PBXBuildFile; fileRef = AF000000000000000000000A /* vm_isolate_snapshot.bin */; }; + AB0000000000000000000009 /* isolate_snapshot.bin in Resources */ = {isa = PBXBuildFile; fileRef = AF000000000000000000000B /* isolate_snapshot.bin */; }; + AB0000000000000000000010 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AF000000000000000000000C /* Assets.xcassets */; }; + CB0000000000000000000002 /* Runner.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = AF0000000000000000000007 /* Runner.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + CB0000000000000000000003 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CF0000000000000000000004 /* Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + CA0000000000000000000004 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AA0000000000000000000002 /* Project object */; + proxyType = 1; + remoteGlobalIDString = AA0000000000000000000001; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + AC0000000000000000000001 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + AB0000000000000000000004 /* Flutter.framework in Embed Frameworks */, + AB0000000000000000000005 /* App.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + CD0000000000000000000011 /* Embed Watch Content */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; + dstSubfolderSpec = 16; + files = ( + CB0000000000000000000002 /* Runner.app in Embed Watch Content */, + ); + name = "Embed Watch Content"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0B66C03D2FE326E4002C6E54 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AF0000000000000000000001 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; + AF0000000000000000000003 /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Flutter.framework; sourceTree = ""; }; + AF0000000000000000000004 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = App.framework; sourceTree = ""; }; + AF0000000000000000000005 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = flutter_assets; sourceTree = ""; }; + AF0000000000000000000006 /* icudtl.dat */ = {isa = PBXFileReference; lastKnownFileType = file; path = icudtl.dat; sourceTree = ""; }; + AF0000000000000000000007 /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AF000000000000000000000A /* vm_isolate_snapshot.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = vm_isolate_snapshot.bin; sourceTree = ""; }; + AF000000000000000000000B /* isolate_snapshot.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = isolate_snapshot.bin; sourceTree = ""; }; + AF000000000000000000000C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + AF000000000000000000000D /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + AF000000000000000000000E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + CF0000000000000000000001 /* sqflite_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = sqflite_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + CF0000000000000000000003 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CF0000000000000000000004 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AD0000000000000000000001 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AB0000000000000000000003 /* Flutter.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + AE0000000000000000000001 = { + isa = PBXGroup; + children = ( + AE0000000000000000000002 /* Runner */, + CE0000000000000000000001 /* HostApp */, + AE0000000000000000000004 /* Flutter */, + AE0000000000000000000003 /* Products */, + ); + sourceTree = ""; + }; + AE0000000000000000000002 /* Runner */ = { + isa = PBXGroup; + children = ( + AF0000000000000000000001 /* App.swift */, + AF000000000000000000000C /* Assets.xcassets */, + 0B66C03D2FE326E4002C6E54 /* Info.plist */, + ); + path = Runner; + sourceTree = ""; + }; + AE0000000000000000000003 /* Products */ = { + isa = PBXGroup; + children = ( + AF0000000000000000000007 /* Runner.app */, + CF0000000000000000000001 /* sqflite_example.app */, + ); + name = Products; + sourceTree = ""; + }; + AE0000000000000000000004 /* Flutter */ = { + isa = PBXGroup; + children = ( + AF0000000000000000000003 /* Flutter.framework */, + AF0000000000000000000004 /* App.framework */, + AF0000000000000000000005 /* flutter_assets */, + AF0000000000000000000006 /* icudtl.dat */, + AF000000000000000000000A /* vm_isolate_snapshot.bin */, + AF000000000000000000000B /* isolate_snapshot.bin */, + AF000000000000000000000D /* Debug.xcconfig */, + AF000000000000000000000E /* Release.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + CE0000000000000000000001 /* HostApp */ = { + isa = PBXGroup; + children = ( + CF0000000000000000000004 /* Assets.xcassets */, + CF0000000000000000000003 /* Info.plist */, + ); + path = HostApp; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AA0000000000000000000001 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = BA0000000000000000000002 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + AD0000000000000000000002 /* Sources */, + AD0000000000000000000001 /* Frameworks */, + AD0000000000000000000003 /* Resources */, + AC0000000000000000000001 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = AF0000000000000000000007 /* Runner.app */; + productType = "com.apple.product-type.application"; + }; + CA0000000000000000000001 /* HostApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = CA0000000000000000000003 /* Build configuration list for PBXNativeTarget "HostApp" */; + buildPhases = ( + CD0000000000000000000012 /* Resources */, + CD0000000000000000000011 /* Embed Watch Content */, + ); + buildRules = ( + ); + dependencies = ( + CA0000000000000000000002 /* PBXTargetDependency */, + ); + name = HostApp; + productName = sqflite_example; + productReference = CF0000000000000000000001 /* sqflite_example.app */; + productType = "com.apple.product-type.application.watchapp2-container"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AA0000000000000000000002 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastUpgradeCheck = 2700; + TargetAttributes = { + AA0000000000000000000001 = { + CreatedOnToolsVersion = 15.0; + }; + CA0000000000000000000001 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = BA0000000000000000000001 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AE0000000000000000000001; + productRefGroup = AE0000000000000000000003 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AA0000000000000000000001 /* Runner */, + CA0000000000000000000001 /* HostApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AD0000000000000000000003 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AB0000000000000000000006 /* flutter_assets in Resources */, + AB0000000000000000000007 /* icudtl.dat in Resources */, + AB0000000000000000000008 /* vm_isolate_snapshot.bin in Resources */, + AB0000000000000000000009 /* isolate_snapshot.bin in Resources */, + AB0000000000000000000010 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CD0000000000000000000012 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CB0000000000000000000003 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AD0000000000000000000002 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AB0000000000000000000001 /* App.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + CA0000000000000000000002 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AA0000000000000000000001 /* Runner */; + targetProxy = CA0000000000000000000004 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + BB0000000000000000000001 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + ENABLE_BITCODE = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + BB0000000000000000000002 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AF000000000000000000000D /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/Flutter"; + GENERATE_INFOPLIST_FILE = NO; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Flutter"; + INFOPLIST_FILE = Runner/Info.plist; + INSTALL_PATH = "$(LOCAL_APPS_DIR)"; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/Flutter"; + MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.sqfliteExample.watchkitapp"; + PRODUCT_NAME = Runner; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 26.0; + }; + name = Debug; + }; + BB0000000000000000000003 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + ENABLE_BITCODE = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = s; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + BB0000000000000000000004 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AF000000000000000000000E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/Flutter"; + GENERATE_INFOPLIST_FILE = NO; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Flutter"; + INFOPLIST_FILE = Runner/Info.plist; + INSTALL_PATH = "$(LOCAL_APPS_DIR)"; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/Flutter"; + MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.sqfliteExample.watchkitapp"; + PRODUCT_NAME = Runner; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 26.0; + }; + name = Release; + }; + CC0000000000000000000001 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AF000000000000000000000D /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = HostApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.sqfliteExample"; + PRODUCT_NAME = sqflite_example; + SDKROOT = iphoneos; + SKIP_INSTALL = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CC0000000000000000000002 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AF000000000000000000000E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = HostApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.sqfliteExample"; + PRODUCT_NAME = sqflite_example; + SDKROOT = iphoneos; + SKIP_INSTALL = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + BA0000000000000000000001 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BB0000000000000000000001 /* Debug */, + BB0000000000000000000003 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BA0000000000000000000002 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BB0000000000000000000002 /* Debug */, + BB0000000000000000000004 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CA0000000000000000000003 /* Build configuration list for PBXNativeTarget "HostApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CC0000000000000000000001 /* Debug */, + CC0000000000000000000002 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AA0000000000000000000002 /* Project object */; +} diff --git a/packages/sqflite_watchos/example/watchos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/sqflite_watchos/example/watchos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..fd1accd --- /dev/null +++ b/packages/sqflite_watchos/example/watchos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/sqflite_watchos/example/watchos/Runner.xcodeproj/xcshareddata/xcschemes/sqflite_example.xcscheme b/packages/sqflite_watchos/example/watchos/Runner.xcodeproj/xcshareddata/xcschemes/sqflite_example.xcscheme new file mode 100644 index 0000000..f301338 --- /dev/null +++ b/packages/sqflite_watchos/example/watchos/Runner.xcodeproj/xcshareddata/xcschemes/sqflite_example.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/sqflite_watchos/example/watchos/Runner/App.swift b/packages/sqflite_watchos/example/watchos/Runner/App.swift new file mode 100644 index 0000000..1fb6589 --- /dev/null +++ b/packages/sqflite_watchos/example/watchos/Runner/App.swift @@ -0,0 +1,57 @@ +import SwiftUI + +#if !arch(arm64_32) +// The Flutter host (frame display, input, native overlays) — compiled by +// flutter-watchos into watchos/Flutter/, like Flutter.framework itself. +import FlutterWatchOS +#endif + +@main +struct SqfliteExampleApp: App { + #if !arch(arm64_32) + // Remote-notification plumbing (APNs device token, notification payloads) + // for plugins that need it, e.g. firebase_messaging. Safe to keep even if + // no plugin uses notifications. + @WKApplicationDelegateAdaptor(FlutterWatchOSAppDelegate.self) + private var flutterAppDelegate + + init() { + // Native platform views: register a SwiftUI factory for every + // `viewType` your Dart code embeds with WatchPlatformView + // (package:flutter_watchos). Example: + // + // WatchPlatformViewRegistry.register("my-gauge") { params in + // AnyView(MyGaugeView(params: params)) + // } + } + #endif + + var body: some Scene { + WindowGroup { + #if arch(arm64_32) + // 32-bit watches (Series 4–8 / SE) are not supported. + UnsupportedDeviceView() + #else + // Holds a black launch placeholder over the screen until Flutter's + // first frame is ready, then fades it out. Supply your own to + // replace it: `FlutterHostView { MyLaunchScreen() }`. + FlutterHostView() + #endif + } + } +} + +// Shown on 32-bit watches (Series 4–8 / SE), which this app does not support. +struct UnsupportedDeviceView: View { + var body: some View { + VStack(spacing: 8) { + Text("Sqflite Example") + .font(.headline) + Text("Requires Apple Watch Series 9 or later.") + .font(.footnote) + .multilineTextAlignment(.center) + .foregroundStyle(.secondary) + } + .padding() + } +} diff --git a/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..287d73c --- /dev/null +++ b/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,140 @@ +{ + "images" : [ + { + "filename" : "icon_24x24_2x_38mm.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "24x24", + "subtype" : "38mm" + }, + { + "filename" : "icon_27.5x27.5_2x_42mm.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "27.5x27.5", + "subtype" : "42mm" + }, + { + "filename" : "icon_29x29_2x_companion.png", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "icon_29x29_3x_companion.png", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "icon_33x33_2x_45mm.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "33x33", + "subtype" : "45mm" + }, + { + "filename" : "icon_40x40_2x_38mm.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "40x40", + "subtype" : "38mm" + }, + { + "filename" : "icon_44x44_2x_40mm.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "44x44", + "subtype" : "40mm" + }, + { + "filename" : "icon_46x46_2x_41mm.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "46x46", + "subtype" : "41mm" + }, + { + "filename" : "icon_50x50_2x_44mm.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "50x50", + "subtype" : "44mm" + }, + { + "filename" : "icon_51x51_2x_45mm.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "51x51", + "subtype" : "45mm" + }, + { + "filename" : "icon_54x54_2x_49mm.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "54x54", + "subtype" : "49mm" + }, + { + "filename" : "icon_86x86_2x.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "86x86", + "subtype" : "38mm" + }, + { + "filename" : "icon_98x98_2x.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "98x98", + "subtype" : "42mm" + }, + { + "filename" : "icon_108x108_2x_44mm.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "108x108", + "subtype" : "44mm" + }, + { + "filename" : "icon_117x117_2x_45mm.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "117x117", + "subtype" : "45mm" + }, + { + "filename" : "icon_129x129_2x_49mm.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "129x129", + "subtype" : "49mm" + }, + { + "filename" : "icon_1024.png", + "idiom" : "watch-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_1024.png b/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..819d623869aef43d1258129b6a7881eaf044d358 GIT binary patch literal 13949 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 R!dgH-&h(veeOkzl{{x@0>-_)# literal 0 HcmV?d00001 diff --git a/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_108x108_2x_44mm.png b/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_108x108_2x_44mm.png new file mode 100644 index 0000000000000000000000000000000000000000..7b3140c5bd31fb73521cce7311c79f4fe6c0b3a3 GIT binary patch literal 3890 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 XnmZWr70ryY00000NkvXXu0mjfMqFv` literal 0 HcmV?d00001 diff --git a/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_29x29_2x_companion.png b/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_29x29_2x_companion.png new file mode 100644 index 0000000000000000000000000000000000000000..796aa3f3f41a0b817d86991185799927570009cc GIT binary patch literal 1214 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 JPDHLkV1lby=yd=9 literal 0 HcmV?d00001 diff --git a/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_44x44_2x_40mm.png b/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_44x44_2x_40mm.png new file mode 100644 index 0000000000000000000000000000000000000000..ec54e61006487ba7a596c7653cd3df452b02e6ca GIT binary patch literal 1756 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 literal 0 HcmV?d00001 diff --git a/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_54x54_2x_49mm.png b/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_54x54_2x_49mm.png new file mode 100644 index 0000000000000000000000000000000000000000..457d76ac599f48e8736a28139476fe7f4540f844 GIT binary patch literal 1987 zcmV;!2R!(RP) zPiz!b9LIm}y_wy%yZuKSdN78z7>gn#ni!hsLD7IFDi<#t{DW6dMo>>u)EK;I;=$-e zK_bS40|X3z4w69BQbO@i6iB2E7Vtpn(z5QhGxK}q&2*RTvTV1rZ|1#e=X)Sz={7r` z{r=wX_j~hu1L2%QtolUje1EJMq8!Cel%N=*1jP^~D26COF+>T9AxcmTQG#NK5)?y} zpctYA84xLP04WPal)|D(z8{lxiQEbC%mtb2$7+IndjR<)#T?fHC9>H~Hn<<%~ z)^#GvBPgOc?Rqc(g8DiU<$5qkf|@!Jr4kgGWNm;1b#x+1Cm75+sIL<-f&}$~h|&oL zxen?G5rZHoyB^dUB1V~@77#Jw1l4C}luIz}Rx{xeRP03b2wL)b5)}khIuWJ!#bM=Q zT_9o*QLKodnsCM-x;hn6tjviR&7MRfCt_rK5?vr-^y@*z5OL`e3<)AGX@X%S7F>#% zBvdEj63ryxIT4qE2ZMDYE)Nd|2odGp?bI)m1P&2{*jdn+pnQl}GWD?>JSac0U__=q z^mQUeXX-;!Ct|o$AN9|%5#1nSh*KYP6UU&Jj0jcKyfE-;VO<<4fxH7vOxpOj5fy?C z7e97!8o#`3*Avc$`+=YDVy|@@l*{4hA0l_Z(u@^GPf{c}Fk)qGF?#K9zE=>m_;X!s z>naUqw!QcKo8zDSX7tCZjjLX!HO*`_c#3md!Wjy|)vIH(WjoCCLJxk3D& zhNzX`6b@Vv1QXQ%w{>e4Hm;=aK5TlwfGDpMy-(1aCcD=s(uP>483}`+KMyjo>Hv@2-e%NP$Dk1Cq+JXIo9aM zTBbgV9^5%nuLs3C=y}k6g=pCJF_&Yv%dxozKi0B0BP)vMJlGXf6wBYsQh}!5|4r+kjv-aI9tO!*&Uto5XEHvv~)Sek-TD9t><93}hFFRpmIZ zC%HdCgTSyLICDM6Z{)e->olqGU@;fFuaF3B%Cbrvd2KyOk>HzWW^y((8=^TI%ZgB@ zn&slacedF*9Fuh6J?eP`9cW;4uS{7mmgU8L2Ms+K;3-qbUD`&od|<@7p2J65>8`ak z5F8R7Y$#a=iTkfDCUmF7`}o3raY&gYq@%iy%kl3!{JT+W`FLt-!xM`Z(dgwb3G31;O#Sa_`JDfQPb`4D6^LOQl zm`peQbMEZPeFyS35~BWrDxpH8MBHF{qMh#VPqrrDzs2u|b3`MRnixKN=k&?LLpQeU zK0aePln_+PF>EJNw=Z1NLicS*wwsIN=n>4EKXdKaQ9IR{=x9ALcxB6H$MdQ=D#*^v z<@iVo-PgYejvO69aCq?A(VwBYlTc>aw%OKrVsKPBM>VHRSqaA#HN>?bm`-QT{WgB& zAf!8iGTNzSqs>_NTHzeDua-1VLbZY$IPec%;fP$b`a9a+VsyV7W@61Zp zN8v}i0!QvRMsqsz`!C}M_L6jmAn1CqRJNnokBT2(zzTl+enYZ7S?)(cuq8ct`q=pX zuSjZz>p{nzASq|0){p9+0l~`Ry5Pqm!E1XzCrzyaoINZ4qztG5gq95>DfFXyh)~AS z^CL&2)g}1tZb&o;UPdD{t$@Zh@fTA0W2^F``f|T)ANQ{*d~Gm4Wj4+Hdid&|FQB8F zv^@anm5@k@AF33~Wikyqo5^kJ?>Y4T<_1&rg*4!{P$P&i$B|#k!YkjLy8OdlW7RsR zp-uQsEQRIyq>2nit=r*o)FPaVVgzi2z7+ZK#h%Xf&)BSm0g9z?F$BH!G0Zw#FDH7k zksFkpADh@TaQNobjL~R1&f*}3>_@$v=*ghOcJT4;juQhfceXUxIg2qrpEEQ+>Ie~B z6BRhVRN&~xM7AA9zSa~XRN!c9>qmVdLUjD7V`3?j?V!P^n`e&PoXVSxCi>VG=c0bh zXL2v}_Z<3QbAnMW>HI}BAVPuT@YuxWfg?93vS2d5XD}#aa!Q;{~H8ZHV~nN zB_@f?|jg6hoAt7@`El5G5#vC_ynq35p>~Pz(|3*Z)M6 Voim7*OhNzv002ovPDHLkV1k0`qHh2I literal 0 HcmV?d00001 diff --git a/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_86x86_2x.png b/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_86x86_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..28feaed7bc280a3673c14d0830dd2544e38736a8 GIT binary patch literal 2891 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 T`1_FiAsKM)j5)FBG>P(Ge{Mg_ literal 0 HcmV?d00001 diff --git a/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_98x98_2x.png b/packages/sqflite_watchos/example/watchos/Runner/Assets.xcassets/AppIcon.appiconset/icon_98x98_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3b901e585b3ba2f2eeb0d174f395a6c543d33248 GIT binary patch literal 3680 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 literal 0 HcmV?d00001 diff --git a/packages/sqflite_watchos/example/watchos/Runner/Info.plist b/packages/sqflite_watchos/example/watchos/Runner/Info.plist new file mode 100644 index 0000000..d45c2e6 --- /dev/null +++ b/packages/sqflite_watchos/example/watchos/Runner/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Sqflite Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + sqflite_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + WKApplication + + WKWatchOnly + + + diff --git a/packages/sqflite_watchos/lib/sqflite_watchos.dart b/packages/sqflite_watchos/lib/sqflite_watchos.dart new file mode 100644 index 0000000..5e3f70a --- /dev/null +++ b/packages/sqflite_watchos/lib/sqflite_watchos.dart @@ -0,0 +1,75 @@ +// Copyright 2026 The FlutterWatch Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// watchOS implementation of `sqflite`, over dart:ffi. +// +// Method-channel plugins are not supported on watchOS, so sqflite's method +// protocol is answered in Dart (`src/handler.dart`) over the system SQLite's +// C API (`src/sqlite3.dart`). sqflite federates through sqflite_common's +// default database factory: `registerWith` sets it, as sqflite_darwin does +// with its method-channel factory. + +import 'dart:ffi'; +import 'dart:io'; + +import 'package:ffi/ffi.dart'; +// ignore: implementation_imports +import 'package:sqflite_common/src/mixin/factory.dart' + show buildDatabaseFactory; +import 'package:sqflite_common/sqflite.dart'; + +import 'src/handler.dart'; +import 'src/sqlite3.dart'; + +/// watchOS implementation of `sqflite`. +abstract final class SqfliteWatchos { + /// Test hook: the library SQLite's C API resolves from, instead of the + /// process. Host tests pass the macOS system SQLite. + static DynamicLibrary? libraryOverride; + + /// Test hook: where relative database paths live, instead of the app's + /// Documents directory. + static String? databasesPathOverride; + + static DatabaseFactory? _factory; + + /// The database factory that answers sqflite's calls on the watch. + static DatabaseFactory get databaseFactory => _factory ??= () { + final DynamicLibrary library = + libraryOverride ?? DynamicLibrary.process(); + final SqfliteWatchosHandler handler = SqfliteWatchosHandler( + Sqlite3Library(library), + databasesPath: databasesPathOverride ?? _databasesPath(library), + platformVersion: 'watchOS ${Platform.operatingSystemVersion}', + ); + return buildDatabaseFactory( + tag: 'watchos', + invokeMethod: handler.handle, + ); + }(); + + /// Registers this implementation as sqflite's default database factory on + /// watchOS, unless the app already set one. + static void registerWith() { + databaseFactoryOrNull ??= databaseFactory; + } + + /// Forgets the factory so the next use builds a new one, for tests that + /// change [libraryOverride] or [databasesPathOverride]. + static void resetForTesting() => _factory = null; + + static String _databasesPath(DynamicLibrary library) => library + .lookupFunction Function(), Pointer Function()>( + 'sqflite_watchos_databases_path')() + .toDartString(); + + /// The version of the SQLite the watch runs, for example `3.46.1`. + static String get sqliteVersion => + (libraryOverride ?? DynamicLibrary.process()) + .lookupFunction Function(), Pointer Function()>( + libraryOverride == null + ? 'sqflite_watchos_sqlite_version' + : 'sqlite3_libversion')() + .toDartString(); +} diff --git a/packages/sqflite_watchos/lib/src/handler.dart b/packages/sqflite_watchos/lib/src/handler.dart new file mode 100644 index 0000000..3c2676d --- /dev/null +++ b/packages/sqflite_watchos/lib/src/handler.dart @@ -0,0 +1,553 @@ +// Copyright 2026 The FlutterWatch Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// sqflite's method protocol, answered in Dart over SQLite's C API. +// +// sqflite_common drives every platform through one call, +// `invokeMethod(method, arguments)`: on iOS and Android it crosses a method +// channel, here it lands in [SqfliteWatchosHandler.handle]. The methods and +// the shape of their arguments and results are sqflite's own; the behaviour +// follows sqflite_common_ffi, the reference implementation of the protocol +// outside a method channel: transaction ids, operations queued behind +// another transaction, paged cursors, and batches that continue on error. + +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:path/path.dart' as p; +// ignore: implementation_imports +import 'package:sqflite_common/src/exception.dart' + show SqfliteDatabaseException; + +import 'sqlite3.dart'; + +/// `inMemoryDatabasePath`. +const String _inMemory = ':memory:'; + +/// `paramTransactionIdValueForce`: run even though another transaction is +/// open, as sqflite_common does to roll back a recovered one. +const int _forceTransactionId = -1; + +/// Statements sqflite reserves for itself, which go to no database. +const String _sqflitePragmaPrefix = 'PRAGMA sqflite_'; + +/// One open database and what is in flight on it. +class _Database { + _Database({ + required this.id, + required this.connection, + required this.path, + required this.readOnly, + required this.singleInstance, + }); + + final int id; + final SqliteConnection connection; + final String path; + final bool readOnly; + final bool singleInstance; + + int _lastTransactionId = 0; + int? currentTransactionId; + + /// Operations from outside the current transaction, run once it ends. + final List queued = []; + final List cancels = []; + + final Map cursors = {}; + int _lastCursorId = 0; + + Map toDebugMap() => { + 'path': path, + 'id': id, + 'readOnly': readOnly, + 'singleInstance': singleInstance, + }; + + void closeCursor(int cursorId) => cursors.remove(cursorId)?.statement.close(); + + /// Runs queued operations until one opens a transaction again. + void runQueued() { + while (queued.isNotEmpty && currentTransactionId == null) { + cancels.removeAt(0); + queued.removeAt(0)(); + } + } + + void close() { + for (final void Function() cancel in cancels) { + cancel(); + } + queued.clear(); + cancels.clear(); + for (final int id in cursors.keys.toList()) { + closeCursor(id); + } + connection.close(); + } +} + +class _Cursor { + _Cursor(this.id, this.statement, this.pageSize); + + final int id; + final SqliteStatement statement; + final int pageSize; +} + +/// Answers sqflite's method calls for every database this isolate opened. +class SqfliteWatchosHandler { + /// A handler over [library], keeping relative paths under + /// [databasesPath] and reporting [platformVersion]. + SqfliteWatchosHandler( + this.library, { + required this.databasesPath, + required this.platformVersion, + }); + + /// The SQLite C API. + final Sqlite3Library library; + + /// Where relative database paths live. + final String databasesPath; + + /// What `getPlatformVersion` answers. + final String platformVersion; + + final Map _databases = {}; + final Map _singleInstances = {}; + int _lastId = 0; + + /// sqflite's log level, as set through `options`. Kept for parity; this + /// handler logs nothing itself. + int logLevel = 0; + + /// Runs one sqflite method call. + Future handle(String method, [Object? arguments]) async { + final Map args = arguments is Map + ? arguments + : const {}; + switch (method) { + case 'openDatabase': + return _guard(() => _open(args), sql: null, arguments: null); + case 'closeDatabase': + return _guard(() => _close(args), sql: null, arguments: null); + case 'execute': + return _onDatabase(args, (_Database db) => _execute(db, args)); + case 'insert': + return _onDatabase(args, (_Database db) => _insert(db, args)); + case 'update': + return _onDatabase(args, (_Database db) => _update(db, args)); + case 'query': + return _onDatabase(args, (_Database db) => _query(db, args)); + case 'queryCursorNext': + return _onDatabase(args, (_Database db) => _cursorNext(db, args)); + case 'batch': + return _onDatabase(args, (_Database db) => _batch(db, args)); + case 'getDatabasesPath': + return databasesPath; + case 'getPlatformVersion': + return platformVersion; + case 'databaseExists': + final String path = _path(args); + return path != _inMemory && File(path).existsSync(); + case 'deleteDatabase': + _delete(_path(args)); + return null; + case 'readDatabaseBytes': + return {'bytes': File(_path(args)).readAsBytesSync()}; + case 'writeDatabaseBytes': + final File file = File(_path(args)); + file.parent.createSync(recursive: true); + file.writeAsBytesSync(args['bytes']! as Uint8List); + return null; + case 'options': + final Object? level = args['logLevel']; + if (level is int) { + logLevel = level; + } + return null; + case 'debugMode': + if (arguments == true) { + logLevel = 2; // sqfliteLogLevelVerbose + } + return null; + default: + throw ArgumentError('Invalid method $method'); + } + } + + // ---- Paths and databases ------------------------------------------------ + + String _resolve(String path) => path == _inMemory || p.isAbsolute(path) + ? path + : p.join(databasesPath, path); + + String _path(Map args) => _resolve(args['path']! as String); + + bool _isClosedId(Object? id) => + id is int && id > 0 && id <= _lastId && !_databases.containsKey(id); + + _Database _databaseOrThrow(Map args) { + final Object? id = args['id']; + final _Database? db = _databases[id]; + if (db != null) { + return db; + } + if (_isClosedId(id)) { + // Matches sqflite_common's isDatabaseClosedError. + throw StateError('This database has already been closed'); + } + throw StateError('Database $id not found'); + } + + Map _open(Map args) { + final String path = _path(args); + final bool readOnly = args['readOnly'] == true; + final bool singleInstance = args['singleInstance'] == true; + if (singleInstance) { + final _Database? existing = _singleInstances[path]; + if (existing != null) { + return {'id': existing.id}; + } + } + if (!readOnly && path != _inMemory) { + // sqflite creates the missing parent directories on every platform. + Directory(p.dirname(path)).createSync(recursive: true); + } + final SqliteConnection connection; + try { + connection = library.open( + path, + readOnly + ? SqliteCode.openReadOnly + : SqliteCode.openReadWrite | SqliteCode.openCreate, + ); + } on SqliteFailure catch (failure) { + throw SqfliteDatabaseException( + 'open_failed: $failure', + {'path': path}, + resultCode: failure.extendedCode, + ); + } + final int id = ++_lastId; + final _Database db = _Database( + id: id, + connection: connection, + path: path, + readOnly: readOnly, + singleInstance: singleInstance, + ); + _databases[id] = db; + if (singleInstance) { + _singleInstances[path] = db; + } + return {'id': id}; + } + + Object? _close(Map args) { + if (_isClosedId(args['id'])) { + // Already closed, as a single instance deleted while open is. + return null; + } + final _Database db = _databaseOrThrow(args); + _forget(db); + return null; + } + + void _forget(_Database db) { + _databases.remove(db.id); + if (identical(_singleInstances[db.path], db)) { + _singleInstances.remove(db.path); + } + db.close(); + } + + void _delete(String path) { + final _Database? open = _singleInstances[path]; + if (open != null) { + _forget(open); + } + try { + final File file = File(path); + if (file.existsSync()) { + file.deleteSync(); + } + } catch (_) { + // As on every platform: deleting what cannot be deleted is not an error. + } + } + + // ---- Running SQL -------------------------------------------------------- + + /// Runs [action] on the database the call names, or queues it while + /// another transaction is open on that database. + Future _onDatabase( + Map args, + Object? Function(_Database db) action, + ) { + final _Database db; + try { + db = _databaseOrThrow(args); + } catch (e) { + return Future.error(_wrap(e, null, args)); + } + final Object? transactionId = args['transactionId']; + if (db.currentTransactionId == null) { + return Future.sync(() => _run(db, args, action)); + } + if (transactionId == db.currentTransactionId || + transactionId == _forceTransactionId) { + try { + return Future.value(_run(db, args, action)); + } catch (e, st) { + return Future.error(e, st); + } finally { + if (db.currentTransactionId == null) { + scheduleMicrotask(db.runQueued); + } + } + } + final Completer completer = Completer(); + db.queued.add(() { + try { + completer.complete(_run(db, args, action)); + } catch (e, st) { + completer.completeError(e, st); + } + }); + db.cancels.add(() => completer.completeError( + _wrap(StateError('Database has been closed'), db, args))); + return completer.future; + } + + /// Runs [action], turning a failure into sqflite's exception, and marks + /// the transaction closed when SQLite rolled it back. + Object? _run( + _Database db, + Map args, + Object? Function(_Database db) action, + ) { + try { + return action(db); + } catch (e) { + final SqfliteDatabaseException wrapped = _wrap(e, db, args); + if (db.currentTransactionId != null && db.connection.autocommit) { + db.currentTransactionId = null; + wrapped.transactionClosed = true; + scheduleMicrotask(db.runQueued); + } + throw wrapped; + } + } + + Object? _guard(Object? Function() action, + {required String? sql, required List? arguments}) { + try { + return action(); + } catch (e) { + throw _wrap(e, null, { + if (sql != null) 'sql': sql, + if (arguments != null) 'arguments': arguments, + }); + } + } + + SqfliteDatabaseException _wrap( + Object e, _Database? db, Map args) { + if (e is SqfliteDatabaseException) { + return e; + } + final Object? sql = args['sql']; + final Object? arguments = args['arguments']; + final Map details = { + if (db != null) 'database': db.toDebugMap(), + if (sql != null) 'sql': sql, + if (arguments != null) 'arguments': arguments, + }; + return SqfliteDatabaseException( + '$e', + details.isEmpty ? null : details, + resultCode: e is SqliteFailure ? e.extendedCode : null, + ); + } + + static List _arguments(Map args) => + (args['arguments'] as List?) ?? const []; + + void _executeSql(_Database db, String sql, List arguments) { + if (sql.startsWith(_sqflitePragmaPrefix)) { + return; + } + if (arguments.isEmpty) { + db.connection.executeAll(sql); + } else { + db.connection.execute(sql, arguments); + } + } + + void _checkWritable(_Database db) { + if (db.readOnly) { + throw SqfliteDatabaseException('Database readonly', null); + } + } + + Object? _execute(_Database db, Map args) { + final bool? inTransactionChange = args['inTransaction'] as bool?; + final bool entering = inTransactionChange == true && + args.containsKey('transactionId') && + args['transactionId'] == null; + if (entering) { + db.currentTransactionId = ++db._lastTransactionId; + } + try { + _executeSql(db, args['sql']! as String, _arguments(args)); + } catch (_) { + if (entering) { + db.currentTransactionId = null; + } + rethrow; + } + if (entering) { + return {'transactionId': db.currentTransactionId}; + } + if (inTransactionChange == false) { + db.currentTransactionId = null; + } + return null; + } + + /// The id of the inserted row, or null when nothing was inserted. + int? _lastInsertId(_Database db) => + db.connection.changes == 0 ? null : db.connection.lastInsertRowId; + + Object? _insert(_Database db, Map args) { + _checkWritable(db); + _executeSql(db, args['sql']! as String, _arguments(args)); + return _lastInsertId(db); + } + + Object? _update(_Database db, Map args) { + _checkWritable(db); + _executeSql(db, args['sql']! as String, _arguments(args)); + return db.connection.changes; + } + + static Map _pack( + List columns, List> rows) => + {'columns': columns, 'rows': rows}; + + Object? _query(_Database db, Map args) { + final String sql = args['sql']! as String; + final List arguments = _arguments(args); + final Object? pageSize = args['cursorPageSize']; + if (pageSize is! int) { + final ({List columns, List> rows}) result = + db.connection.select(sql, arguments); + return _pack(result.columns, result.rows); + } + final SqliteStatement statement = db.connection.prepare(sql, arguments); + final _Cursor cursor = _Cursor(++db._lastCursorId, statement, pageSize); + db.cursors[cursor.id] = cursor; + return _page(db, cursor); + } + + Map _page(_Database db, _Cursor cursor) { + try { + final List> rows = >[]; + bool atEnd = false; + while (rows.length < cursor.pageSize) { + if (!cursor.statement.step()) { + atEnd = true; + break; + } + rows.add(cursor.statement.row()); + } + final Map page = _pack(cursor.statement.columns, rows); + if (atEnd) { + db.closeCursor(cursor.id); + } else { + page['cursorId'] = cursor.id; + } + return page; + } catch (_) { + db.closeCursor(cursor.id); + rethrow; + } + } + + Object? _cursorNext(_Database db, Map args) { + final int cursorId = args['cursorId']! as int; + if (args['cancel'] == true) { + db.closeCursor(cursorId); + return null; + } + final _Cursor? cursor = db.cursors[cursorId]; + if (cursor == null) { + throw StateError('Cursor $cursorId not found'); + } + return _page(db, cursor); + } + + Object? _batch(_Database db, Map args) { + final bool noResult = args['noResult'] == true; + final bool continueOnError = args['continueOnError'] == true; + final List>? results = + noResult ? null : >[]; + for (final Object? item in args['operations']! as List) { + final Map operation = item! as Map; + final String method = operation['method']! as String; + final String? sql = operation['sql'] as String?; + final List arguments = _arguments(operation); + try { + final Object? result = switch (method) { + 'insert' => () { + _checkWritable(db); + _executeSql(db, sql!, arguments); + return _lastInsertId(db); + }(), + 'update' => () { + _checkWritable(db); + _executeSql(db, sql!, arguments); + return db.connection.changes; + }(), + 'execute' => () { + _executeSql(db, sql!, arguments); + return null; + }(), + 'query' => () { + final ({List columns, List> rows}) r = + db.connection.select(sql!, arguments); + return _pack(r.columns, r.rows); + }(), + _ => throw UnsupportedError('batch operation $method not supported'), + }; + results?.add({'result': result}); + } catch (e) { + final SqfliteDatabaseException wrapped = _wrap(e, db, operation); + if (!continueOnError) { + throw wrapped; + } + results?.add({ + 'error': { + 'code': 'sqlite_error', + 'message': wrapped.message, + if (wrapped.getResultCode() != null) + 'resultCode': wrapped.getResultCode(), + 'data': { + 'sql': sql, + if (arguments.isNotEmpty) 'arguments': arguments, + }, + }, + }); + if (db.currentTransactionId != null && db.connection.autocommit) { + // SQLite rolled the transaction back: nothing after this counts. + throw wrapped..transactionClosed = true; + } + } + } + return results; + } +} diff --git a/packages/sqflite_watchos/lib/src/sqlite3.dart b/packages/sqflite_watchos/lib/src/sqlite3.dart new file mode 100644 index 0000000..aea79ce --- /dev/null +++ b/packages/sqflite_watchos/lib/src/sqlite3.dart @@ -0,0 +1,502 @@ +// Copyright 2026 The FlutterWatch Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// The part of SQLite's C API this package uses, bound over dart:ffi. +// +// On the watch these resolve from the process: watchOS ships libsqlite3 and +// the CLI links it. Host tests pass the macOS system library instead, so the +// same code runs real SQL there. + +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; + +/// An open `sqlite3*` connection. +final class Sqlite3 extends Opaque {} + +/// A prepared `sqlite3_stmt*`. +final class Sqlite3Statement extends Opaque {} + +/// Result codes and flags from `sqlite3.h`. +abstract final class SqliteCode { + /// `SQLITE_OK`. + static const int ok = 0; + + /// `SQLITE_ROW`. + static const int row = 100; + + /// `SQLITE_DONE`. + static const int done = 101; + + /// `SQLITE_OPEN_READONLY`. + static const int openReadOnly = 0x00000001; + + /// `SQLITE_OPEN_READWRITE`. + static const int openReadWrite = 0x00000002; + + /// `SQLITE_OPEN_CREATE`. + static const int openCreate = 0x00000004; + + /// `SQLITE_OPEN_FULLMUTEX`. + static const int openFullMutex = 0x00010000; + + /// `SQLITE_INTEGER`. + static const int typeInteger = 1; + + /// `SQLITE_FLOAT`. + static const int typeFloat = 2; + + /// `SQLITE_TEXT`. + static const int typeText = 3; + + /// `SQLITE_BLOB`. + static const int typeBlob = 4; +} + +typedef _Destructor = Pointer)>>; + +/// `SQLITE_TRANSIENT`: SQLite copies a bound value before the call returns, +/// so the Dart-side buffer can go right after. +final _Destructor _transient = _Destructor.fromAddress(-1); + +/// A failed SQLite call: its extended result code and `sqlite3_errmsg`. +class SqliteFailure implements Exception { + /// A failure with these values. + const SqliteFailure(this.extendedCode, this.message); + + /// The extended result code, for example 2067 for a UNIQUE constraint. + final int extendedCode; + + /// The message SQLite gave. + final String message; + + @override + String toString() => '$message (code $extendedCode)'; +} + +/// The SQLite C functions, resolved from one library. +class Sqlite3Library { + /// Binds the functions in [library]. + Sqlite3Library(DynamicLibrary library) + : _openV2 = library.lookupFunction< + Int32 Function( + Pointer, Pointer>, Int32, Pointer), + int Function(Pointer, Pointer>, int, + Pointer)>('sqlite3_open_v2'), + _closeV2 = library.lookupFunction), + int Function(Pointer)>('sqlite3_close_v2'), + _extendedResultCodes = library.lookupFunction< + Int32 Function(Pointer, Int32), + int Function( + Pointer, int)>('sqlite3_extended_result_codes'), + _prepareV2 = library.lookupFunction< + Int32 Function(Pointer, Pointer, Int32, + Pointer>, Pointer>), + int Function( + Pointer, + Pointer, + int, + Pointer>, + Pointer>)>('sqlite3_prepare_v2'), + _step = library.lookupFunction< + Int32 Function(Pointer), + int Function(Pointer)>('sqlite3_step'), + _finalize = library.lookupFunction< + Int32 Function(Pointer), + int Function(Pointer)>('sqlite3_finalize'), + _bindParameterCount = library.lookupFunction< + Int32 Function(Pointer), + int Function( + Pointer)>('sqlite3_bind_parameter_count'), + _bindNull = library.lookupFunction< + Int32 Function(Pointer, Int32), + int Function(Pointer, int)>('sqlite3_bind_null'), + _bindInt64 = library.lookupFunction< + Int32 Function(Pointer, Int32, Int64), + int Function( + Pointer, int, int)>('sqlite3_bind_int64'), + _bindDouble = library.lookupFunction< + Int32 Function(Pointer, Int32, Double), + int Function( + Pointer, int, double)>('sqlite3_bind_double'), + _bindText = library.lookupFunction< + Int32 Function(Pointer, Int32, Pointer, + Int32, _Destructor), + int Function(Pointer, int, Pointer, int, + _Destructor)>('sqlite3_bind_text'), + _bindBlob = library.lookupFunction< + Int32 Function(Pointer, Int32, Pointer, + Int32, _Destructor), + int Function(Pointer, int, Pointer, int, + _Destructor)>('sqlite3_bind_blob'), + _bindZeroBlob = library.lookupFunction< + Int32 Function(Pointer, Int32, Int32), + int Function( + Pointer, int, int)>('sqlite3_bind_zeroblob'), + _columnCount = library.lookupFunction< + Int32 Function(Pointer), + int Function(Pointer)>('sqlite3_column_count'), + _columnName = library.lookupFunction< + Pointer Function(Pointer, Int32), + Pointer Function( + Pointer, int)>('sqlite3_column_name'), + _columnType = library.lookupFunction< + Int32 Function(Pointer, Int32), + int Function( + Pointer, int)>('sqlite3_column_type'), + _columnInt64 = library.lookupFunction< + Int64 Function(Pointer, Int32), + int Function( + Pointer, int)>('sqlite3_column_int64'), + _columnDouble = library.lookupFunction< + Double Function(Pointer, Int32), + double Function( + Pointer, int)>('sqlite3_column_double'), + _columnText = library.lookupFunction< + Pointer Function(Pointer, Int32), + Pointer Function( + Pointer, int)>('sqlite3_column_text'), + _columnBlob = library.lookupFunction< + Pointer Function(Pointer, Int32), + Pointer Function( + Pointer, int)>('sqlite3_column_blob'), + _columnBytes = library.lookupFunction< + Int32 Function(Pointer, Int32), + int Function( + Pointer, int)>('sqlite3_column_bytes'), + _errmsg = library.lookupFunction< + Pointer Function(Pointer), + Pointer Function(Pointer)>('sqlite3_errmsg'), + _errstr = library.lookupFunction Function(Int32), + Pointer Function(int)>('sqlite3_errstr'), + _extendedErrcode = library.lookupFunction< + Int32 Function(Pointer), + int Function(Pointer)>('sqlite3_extended_errcode'), + _changes = library.lookupFunction), + int Function(Pointer)>('sqlite3_changes'), + _lastInsertRowId = library.lookupFunction< + Int64 Function(Pointer), + int Function(Pointer)>('sqlite3_last_insert_rowid'), + _getAutocommit = library.lookupFunction< + Int32 Function(Pointer), + int Function(Pointer)>('sqlite3_get_autocommit'), + _libVersion = library.lookupFunction Function(), + Pointer Function()>('sqlite3_libversion'); + + final int Function( + Pointer, Pointer>, int, Pointer) _openV2; + final int Function(Pointer) _closeV2; + final int Function(Pointer, int) _extendedResultCodes; + final int Function(Pointer, Pointer, int, + Pointer>, Pointer>) _prepareV2; + final int Function(Pointer) _step; + final int Function(Pointer) _finalize; + final int Function(Pointer) _bindParameterCount; + final int Function(Pointer, int) _bindNull; + final int Function(Pointer, int, int) _bindInt64; + final int Function(Pointer, int, double) _bindDouble; + final int Function( + Pointer, int, Pointer, int, _Destructor) + _bindText; + final int Function( + Pointer, int, Pointer, int, _Destructor) + _bindBlob; + final int Function(Pointer, int, int) _bindZeroBlob; + final int Function(Pointer) _columnCount; + final Pointer Function(Pointer, int) _columnName; + final int Function(Pointer, int) _columnType; + final int Function(Pointer, int) _columnInt64; + final double Function(Pointer, int) _columnDouble; + final Pointer Function(Pointer, int) _columnText; + final Pointer Function(Pointer, int) _columnBlob; + final int Function(Pointer, int) _columnBytes; + final Pointer Function(Pointer) _errmsg; + final Pointer Function(int) _errstr; + final int Function(Pointer) _extendedErrcode; + final int Function(Pointer) _changes; + final int Function(Pointer) _lastInsertRowId; + final int Function(Pointer) _getAutocommit; + final Pointer Function() _libVersion; + + /// `sqlite3_libversion()`. + String get version => _libVersion().toDartString(); + + /// Opens [path] with [flags]. Throws a [SqliteFailure] when SQLite cannot. + SqliteConnection open(String path, int flags) => using((Arena arena) { + final Pointer> out = arena>(); + final int rc = + _openV2(path.toNativeUtf8(allocator: arena), out, flags, nullptr); + final Pointer db = out.value; + if (rc != SqliteCode.ok) { + final SqliteFailure failure = db == nullptr + ? SqliteFailure(rc, _errstr(rc).toDartString()) + : SqliteFailure(_extendedErrcode(db), _errmsg(db).toDartString()); + if (db != nullptr) { + _closeV2(db); + } + throw failure; + } + _extendedResultCodes(db, 1); + return SqliteConnection._(this, db); + }); +} + +/// One open connection, and the statements run on it. +class SqliteConnection { + SqliteConnection._(this._lib, this._db); + + final Sqlite3Library _lib; + final Pointer _db; + bool _closed = false; + + /// Whether [close] has run. + bool get isClosed => _closed; + + /// `sqlite3_changes`: rows changed by the last statement. + int get changes => _lib._changes(_db); + + /// `sqlite3_last_insert_rowid`. + int get lastInsertRowId => _lib._lastInsertRowId(_db); + + /// Whether no transaction is open (`sqlite3_get_autocommit`). + bool get autocommit => _lib._getAutocommit(_db) != 0; + + SqliteFailure _failure() => SqliteFailure( + _lib._extendedErrcode(_db), _lib._errmsg(_db).toDartString()); + + /// Closes the connection. Statements still open are finalized first. + void close() { + if (_closed) { + return; + } + _closed = true; + _lib._closeV2(_db); + } + + /// Runs every statement in [sql], as `sqlite3_exec` would. + void executeAll(String sql) { + using((Arena arena) { + final Uint8List bytes = utf8.encode(sql); + final Pointer buffer = arena(bytes.length + 1); + buffer.asTypedList(bytes.length).setAll(0, bytes); + buffer[bytes.length] = 0; + final Pointer> out = + arena>(); + final Pointer> tail = arena>(); + Pointer cursor = buffer; + final int end = buffer.address + bytes.length; + while (cursor.address < end) { + final int remaining = end - cursor.address; + if (_lib._prepareV2(_db, cursor, remaining, out, tail) != + SqliteCode.ok) { + throw _failure(); + } + final Pointer statement = out.value; + final Pointer next = tail.value; + if (statement != nullptr) { + try { + int rc; + do { + rc = _lib._step(statement); + } while (rc == SqliteCode.row); + if (rc != SqliteCode.done) { + throw _failure(); + } + } finally { + _lib._finalize(statement); + } + } + if (next == nullptr || next.address <= cursor.address) { + break; + } + cursor = next; + } + }); + } + + /// Prepares the first statement of [sql] and binds [arguments]. + SqliteStatement prepare(String sql, List arguments) => + using((Arena arena) { + final Uint8List bytes = utf8.encode(sql); + final Pointer buffer = arena(bytes.length + 1); + buffer.asTypedList(bytes.length).setAll(0, bytes); + buffer[bytes.length] = 0; + final Pointer> out = + arena>(); + if (_lib._prepareV2(_db, buffer, bytes.length, out, nullptr) != + SqliteCode.ok) { + throw _failure(); + } + final SqliteStatement statement = SqliteStatement._(this, out.value); + try { + statement._bind(arguments); + } catch (_) { + statement.close(); + rethrow; + } + return statement; + }); + + /// Runs [sql] with [arguments], discarding any rows. + void execute(String sql, List arguments) { + final SqliteStatement statement = prepare(sql, arguments); + try { + while (statement.step()) {} + } finally { + statement.close(); + } + } + + /// Runs [sql] with [arguments] and returns its columns and every row. + ({List columns, List> rows}) select( + String sql, List arguments) { + final SqliteStatement statement = prepare(sql, arguments); + try { + final List> rows = >[]; + while (statement.step()) { + rows.add(statement.row()); + } + return (columns: statement.columns, rows: rows); + } finally { + statement.close(); + } + } +} + +/// A prepared statement, stepped row by row. +class SqliteStatement { + SqliteStatement._(this._connection, this._statement); + + final SqliteConnection _connection; + final Pointer _statement; + bool _closed = false; + + Sqlite3Library get _lib => _connection._lib; + + void _check(int rc) { + if (rc != SqliteCode.ok) { + throw _connection._failure(); + } + } + + void _bind(List arguments) { + if (_statement == nullptr) { + // Only whitespace or a comment: nothing to bind, nothing to run. + return; + } + final int count = _lib._bindParameterCount(_statement); + if (arguments.length != count) { + throw SqliteFailure( + 25, // SQLITE_RANGE + 'Expected $count arguments, got ${arguments.length}'); + } + for (int i = 0; i < arguments.length; i++) { + final int index = i + 1; + final Object? value = arguments[i]; + switch (value) { + case null: + _check(_lib._bindNull(_statement, index)); + case int(): + _check(_lib._bindInt64(_statement, index, value)); + case double(): + _check(_lib._bindDouble(_statement, index, value)); + case BigInt(): + if (!value.isValidInt) { + throw ArgumentError.value( + value, 'arguments', 'does not fit in a 64-bit integer'); + } + _check(_lib._bindInt64(_statement, index, value.toInt())); + case String(): + using((Arena arena) { + final Uint8List bytes = utf8.encode(value); + final Pointer p = + arena(bytes.isEmpty ? 1 : bytes.length); + p.asTypedList(bytes.length).setAll(0, bytes); + _check( + _lib._bindText(_statement, index, p, bytes.length, _transient)); + }); + case Uint8List(): + if (value.isEmpty) { + _check(_lib._bindZeroBlob(_statement, index, 0)); + } else { + using((Arena arena) { + final Pointer p = arena(value.length); + p.asTypedList(value.length).setAll(0, value); + _check(_lib._bindBlob( + _statement, index, p, value.length, _transient)); + }); + } + default: + throw ArgumentError( + "Invalid sql argument type '${value.runtimeType}': $value"); + } + } + } + + /// The result column names. + List get columns => [ + if (_statement != nullptr) + for (int i = 0; i < _lib._columnCount(_statement); i++) + _lib._columnName(_statement, i).toDartString(), + ]; + + /// Steps once: true when a row is ready, false when the statement is done. + bool step() { + if (_statement == nullptr) { + return false; + } + final int rc = _lib._step(_statement); + if (rc == SqliteCode.row) { + return true; + } + if (rc == SqliteCode.done) { + return false; + } + throw _connection._failure(); + } + + /// The current row's values: int, double, String, Uint8List or null. + List row() { + final int count = _lib._columnCount(_statement); + return [ + for (int i = 0; i < count; i++) _value(i), + ]; + } + + Object? _value(int i) { + switch (_lib._columnType(_statement, i)) { + case SqliteCode.typeInteger: + return _lib._columnInt64(_statement, i); + case SqliteCode.typeFloat: + return _lib._columnDouble(_statement, i); + case SqliteCode.typeText: + final Pointer text = _lib._columnText(_statement, i); + final int length = _lib._columnBytes(_statement, i); + return text == nullptr + ? '' + : utf8.decode(text.asTypedList(length), allowMalformed: true); + case SqliteCode.typeBlob: + final Pointer blob = _lib._columnBlob(_statement, i); + final int length = _lib._columnBytes(_statement, i); + return blob == nullptr + ? Uint8List(0) + : Uint8List.fromList(blob.asTypedList(length)); + default: + return null; + } + } + + /// Finalizes the statement. + void close() { + if (_closed) { + return; + } + _closed = true; + if (_statement != nullptr) { + _lib._finalize(_statement); + } + } +} diff --git a/packages/sqflite_watchos/pubspec.yaml b/packages/sqflite_watchos/pubspec.yaml new file mode 100644 index 0000000..8cdaa7f --- /dev/null +++ b/packages/sqflite_watchos/pubspec.yaml @@ -0,0 +1,55 @@ +name: sqflite_watchos +description: "watchOS implementation of the sqflite plugin: SQLite databases on Apple Watch, on the system SQLite via dart:ffi." +version: 0.0.1 +homepage: https://flutterwatch.dev +repository: https://github.com/flutterwatch/plugins/tree/main/packages/sqflite_watchos +issue_tracker: https://github.com/flutterwatch/plugins/issues +topics: + - watchos + - ffi + - sqlite + - database +# Generated by `flutter-watchos plugin port`. See PORTING_REPORT.md. +# License holder: The FlutterWatch Authors + +environment: + # Records and switch expressions, which src/ uses, need 3.0; 3.1 matches + # the other packages here. + sdk: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" + +dependencies: + ffi: ^2.1.0 + flutter: + sdk: flutter + path: ^1.8.0 + # buildDatabaseFactory and SqfliteDatabaseException, which + # sqflite_common_ffi imports the same way. + sqflite_common: ^2.5.11 + +dev_dependencies: + flutter_lints: ^4.0.0 + flutter_test: + sdk: flutter + +flutter: + plugin: + implements: sqflite + platforms: + watchos: + # FFI plugin: the CLI compiles watchos/Classes/*.m into a + # static archive force-loaded into the watch binary, and links + # the system libsqlite3 that Package.swift names; Dart resolves + # every symbol via DynamicLibrary.process(). + # Method-channel plugins are not supported on watchOS, + # so this is the supported plugin model. + ffiPlugin: true + dartPluginClass: SqfliteWatchos + # This package's own C exports. They survive the link because the + # CLI force-loads the plugin archive, each export is marked used + # with default visibility, and the CLI keeps global symbols + # through the App Store strip. SQLite's own functions come from + # the system libsqlite3. + ffiSymbols: + - sqflite_watchos_databases_path + - sqflite_watchos_sqlite_version diff --git a/packages/sqflite_watchos/test/sqflite_watchos_test.dart b/packages/sqflite_watchos/test/sqflite_watchos_test.dart new file mode 100644 index 0000000..1b9f2ac --- /dev/null +++ b/packages/sqflite_watchos/test/sqflite_watchos_test.dart @@ -0,0 +1,601 @@ +// Copyright 2026 The FlutterWatch Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Host-side tests that run real SQL. The handler binds SQLite's C API, which +// macOS ships too, so these point it at the system libsqlite3 and drive it +// through sqflite_common's own Database API, the way an app does. The watch +// resolves the same functions from its process instead. + +import 'dart:async'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as p; +import 'package:sqflite_common/sqflite.dart'; +// ignore: implementation_imports +import 'package:sqflite_common/src/exception.dart' + show SqfliteDatabaseException; +import 'package:sqflite_watchos/sqflite_watchos.dart'; +import 'package:sqflite_watchos/src/handler.dart'; +import 'package:sqflite_watchos/src/sqlite3.dart'; + +void main() { + late Directory dir; + late DatabaseFactory factory; + + setUpAll(() { + dir = Directory.systemTemp.createTempSync('sqflite_watchos_test'); + SqfliteWatchos.libraryOverride = + DynamicLibrary.open('/usr/lib/libsqlite3.dylib'); + SqfliteWatchos.databasesPathOverride = dir.path; + SqfliteWatchos.resetForTesting(); + factory = SqfliteWatchos.databaseFactory; + }); + + tearDownAll(() => dir.deleteSync(recursive: true)); + + Future openFresh(String name, + {OpenDatabaseOptions? options}) async { + await factory.deleteDatabase(name); + return factory.openDatabase(name, options: options); + } + + test('registerWith makes the watchOS factory the default', () { + databaseFactoryOrNull = null; + SqfliteWatchos.registerWith(); + expect(identical(databaseFactoryOrNull, factory), isTrue); + }); + + test('registerWith keeps a factory the app already set', () { + final DatabaseFactory other = factory; + databaseFactoryOrNull = other; + SqfliteWatchos.registerWith(); + expect(identical(databaseFactoryOrNull, other), isTrue); + }); + + test('getDatabasesPath is where relative paths go', () async { + expect(await factory.getDatabasesPath(), dir.path); + final Database db = await openFresh('relative.db'); + expect(db.path, p.join(dir.path, 'relative.db')); + await db.close(); + expect(File(p.join(dir.path, 'relative.db')).existsSync(), isTrue); + }); + + test('a missing parent directory is created', () async { + final Database db = await openFresh(p.join('sub', 'dir', 'nested.db')); + await db.close(); + expect( + File(p.join(dir.path, 'sub', 'dir', 'nested.db')).existsSync(), isTrue); + }); + + test('every SQLite type comes back as it went in', () async { + final Database db = await openFresh('types.db'); + await db.execute('CREATE TABLE t (i INTEGER, r REAL, s TEXT, b BLOB, n)'); + final Uint8List blob = Uint8List.fromList([0, 1, 2, 254, 255]); + await db.insert('t', { + 'i': 4611686018427387904, // 2^62: beyond a double's exact integers + 'r': 3.25, + 's': 'İstanbul ☕ 日本', + 'b': blob, + 'n': null, + }); + await db.insert('t', { + 'i': -1, + 'r': -0.5, + 's': '', + 'b': Uint8List(0), + 'n': null, + }); + final List> rows = await db.query('t'); + expect(rows[0]['i'], 4611686018427387904); + expect(rows[0]['r'], 3.25); + expect(rows[0]['s'], 'İstanbul ☕ 日本'); + expect(rows[0]['b'], blob); + expect(rows[0]['n'], isNull); + expect(rows[1]['s'], ''); + expect(rows[1]['b'], Uint8List(0)); + await db.close(); + }); + + test('insert returns the row id, and 0 when nothing was inserted', () async { + final Database db = await openFresh('insert.db'); + await db + .execute('CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT UNIQUE)'); + expect(await db.insert('t', {'name': 'a'}), 1); + expect(await db.insert('t', {'name': 'b'}), 2); + expect( + await db.insert('t', {'name': 'a'}, + conflictAlgorithm: ConflictAlgorithm.ignore), + 0); + await db.close(); + }); + + test('update and delete return the changed row count', () async { + final Database db = await openFresh('update.db'); + await db.execute('CREATE TABLE t (x INTEGER)'); + for (int i = 0; i < 5; i++) { + await db.insert('t', {'x': i}); + } + expect(await db.update('t', {'x': 10}, where: 'x > 2'), 2); + expect(await db.delete('t', where: 'x = 10'), 2); + expect(await db.rawUpdate('UPDATE t SET x = x + 1'), 3); + await db.close(); + }); + + test('several statements run in one execute', () async { + final Database db = await openFresh('multi.db'); + await db.execute('CREATE TABLE a (x); CREATE TABLE b (y); ' + 'INSERT INTO a VALUES (1); INSERT INTO b VALUES (2);'); + expect(await db.rawQuery('SELECT x FROM a'), >[ + {'x': 1}, + ]); + expect(await db.rawQuery('SELECT y FROM b'), >[ + {'y': 2}, + ]); + await db.close(); + }); + + test( + 'numbered parameters and duplicated columns read as on other ' + 'platforms', () async { + final Database db = await factory.openDatabase(inMemoryDatabasePath); + expect( + await db.rawQuery('SELECT ?1 + ?2', [3, 4]), + >[ + {'?1 + ?2': 7}, + ]); + await db.execute('CREATE TABLE t (col1 INTEGER, col2 INTEGER)'); + await db.insert('t', {'col1': 1, 'col2': 2}); + expect( + await db + .rawQuery('SELECT t.col1, col1, t.col2, col2 AS col1 FROM t AS t'), + >[ + {'col1': 2, 'col2': 2}, + ]); + await db.close(); + }); + + test('versions run onCreate and onUpgrade', () async { + final List calls = []; + Database db = await openFresh( + 'version.db', + options: OpenDatabaseOptions( + version: 1, + onCreate: (Database db, int version) async { + calls.add('create $version'); + await db.execute('CREATE TABLE t (x)'); + }, + ), + ); + expect(await db.getVersion(), 1); + await db.close(); + db = await factory.openDatabase( + 'version.db', + options: OpenDatabaseOptions( + version: 2, + onUpgrade: (Database db, int from, int to) async { + calls.add('upgrade $from $to'); + }, + ), + ); + expect(await db.getVersion(), 2); + expect(calls, ['create 1', 'upgrade 1 2']); + await db.close(); + }); + + group('errors', () { + test('a unique constraint names itself and carries its code', () async { + final Database db = await openFresh('unique.db'); + await db.execute('CREATE TABLE t (name TEXT UNIQUE)'); + await db.insert('t', {'name': 'a'}); + try { + await db.insert('t', {'name': 'a'}); + fail('the second insert should fail'); + } on DatabaseException catch (e) { + expect(e.isUniqueConstraintError('t.name'), isTrue); + expect(e.getResultCode(), 2067); // SQLITE_CONSTRAINT_UNIQUE + } + await db.close(); + }); + + test('a syntax error and a missing table are recognised', () async { + final Database db = await factory.openDatabase(inMemoryDatabasePath); + await expectLater( + db.execute('CREAT TABLE t (x)'), + throwsA(isA().having( + (DatabaseException e) => e.isSyntaxError(), 'syntax', true)), + ); + await expectLater( + db.query('missing'), + throwsA(isA().having( + (DatabaseException e) => e.isNoSuchTableError('missing'), + 'no such table', + true)), + ); + await db.close(); + }); + + test('opening a missing database read-only fails to open', () async { + await factory.deleteDatabase('absent.db'); + await expectLater( + factory.openDatabase('absent.db', + options: OpenDatabaseOptions(readOnly: true)), + throwsA(isA().having( + (DatabaseException e) => e.isOpenFailedError(), + 'open failed', + true)), + ); + }); + + test('writing to a read-only database fails as read-only', () async { + final Database rw = await openFresh('readonly.db'); + await rw.execute('CREATE TABLE t (x)'); + await rw.close(); + final Database ro = await factory.openDatabase('readonly.db', + options: OpenDatabaseOptions(readOnly: true)); + await expectLater( + ro.insert('t', {'x': 1}), + throwsA(isA().having( + (DatabaseException e) => e.isReadOnlyError(), 'readonly', true)), + ); + expect(await ro.query('t'), isEmpty); + await ro.close(); + }); + + test('a file that is not a database opens, then fails to read', () async { + final File file = File(p.join(dir.path, 'not_a_database.db')) + ..writeAsStringSync('test'); + final Database db = await factory.openDatabase(file.path, + options: OpenDatabaseOptions(readOnly: true)); + await expectLater(db.getVersion(), throwsA(isA())); + await db.close(); + expect(file.readAsStringSync(), 'test'); + }); + + test('a closed database says so', () async { + final Database db = await openFresh('closed.db'); + await db.close(); + // sqflite_common refuses before the call reaches the handler, and + // synchronously. + await expectLater( + () => db.query('sqlite_master'), + throwsA(isA().having( + (DatabaseException e) => e.isDatabaseClosedError(), + 'closed', + true)), + ); + }); + }); + + group('transactions', () { + test('a transaction commits', () async { + final Database db = await openFresh('commit.db'); + await db.execute('CREATE TABLE t (x)'); + await db.transaction((Transaction txn) async { + await txn.insert('t', {'x': 1}); + await txn.insert('t', {'x': 2}); + }); + expect(await db.query('t'), hasLength(2)); + await db.close(); + }); + + test('a failing transaction rolls back', () async { + final Database db = await openFresh('rollback.db'); + await db.execute('CREATE TABLE t (x UNIQUE)'); + await expectLater( + db.transaction((Transaction txn) async { + await txn.insert('t', {'x': 1}); + await txn.insert('t', {'x': 1}); + }), + throwsA(isA()), + ); + expect(await db.query('t'), isEmpty); + await db.close(); + }); + + test('a call outside a running transaction waits for it', () async { + final Database db = await openFresh('queue.db'); + await db.execute('CREATE TABLE t (x)'); + final Completer inside = Completer(); + final Completer release = Completer(); + final Future txn = db.transaction((Transaction txn) async { + await txn.insert('t', {'x': 1}); + inside.complete(); + await release.future; + await txn.insert('t', {'x': 2}); + }); + await inside.future; + final Future>> outside = db.query('t'); + bool outsideDone = false; + unawaited(outside.then((_) => outsideDone = true)); + await pumpEventQueue(); + expect(outsideDone, isFalse, reason: 'the transaction is still open'); + release.complete(); + await txn; + expect(await outside, hasLength(2), + reason: 'it ran after the transaction committed'); + await db.close(); + }); + + test('closing inside a raw transaction, then reopening, works', () async { + Database db = await openFresh('close_in_txn.db', + options: OpenDatabaseOptions(version: 1)); + await db.execute('BEGIN TRANSACTION'); + await db.close(); + db = await factory.openDatabase('close_in_txn.db', + options: OpenDatabaseOptions(version: 1)); + expect(await db.getVersion(), 1); + await db.close(); + }); + }); + + group('batch', () { + test('results come back in order', () async { + final Database db = await openFresh('batch.db'); + final Batch batch = db.batch() + ..execute('CREATE TABLE t (id INTEGER PRIMARY KEY, x)') + ..insert('t', {'x': 'a'}) + ..insert('t', {'x': 'b'}) + ..update('t', {'x': 'c'}, where: 'id = 1') + ..query('t', columns: ['x']); + final List results = await batch.commit(); + expect(results[1], 1); + expect(results[2], 2); + expect(results[3], 1); + expect(results[4], >[ + {'x': 'c'}, + {'x': 'b'}, + ]); + await db.close(); + }); + + test('continueOnError keeps going and reports the failure', () async { + final Database db = await openFresh('batch_errors.db'); + await db.execute('CREATE TABLE t (x UNIQUE)'); + final Batch batch = db.batch() + ..insert('t', {'x': 1}) + ..insert('t', {'x': 1}) + ..insert('t', {'x': 2}); + final List results = await batch.commit(continueOnError: true); + expect(results[0], 1); + expect(results[1], isA()); + expect( + (results[1]! as DatabaseException).isUniqueConstraintError(), isTrue); + // The failed insert used no row id. + expect(results[2], 2); + expect(await db.query('t'), hasLength(2)); + await db.close(); + }); + + test('noResult returns nothing and still runs', () async { + final Database db = await openFresh('batch_noresult.db'); + final Batch batch = db.batch() + ..execute('CREATE TABLE t (x)') + ..insert('t', {'x': 1}); + expect(await batch.commit(noResult: true), isEmpty); + expect(await db.query('t'), hasLength(1)); + await db.close(); + }); + }); + + group('cursors', () { + late Database db; + + setUp(() async { + db = await factory.openDatabase(inMemoryDatabasePath); + await db.execute('CREATE TABLE t (x INTEGER)'); + final Batch batch = db.batch(); + for (int i = 0; i < 7; i++) { + batch.insert('t', {'x': i}); + } + await batch.commit(noResult: true); + }); + + tearDown(() => db.close()); + + test('a cursor pages through every row', () async { + final QueryCursor cursor = + await db.queryCursor('t', orderBy: 'x', bufferSize: 3); + final List seen = []; + while (await cursor.moveNext()) { + seen.add(cursor.current['x']); + } + expect(seen, [0, 1, 2, 3, 4, 5, 6]); + }); + + test('a cursor closed early releases its statement', () async { + final QueryCursor cursor = + await db.queryCursor('t', orderBy: 'x', bufferSize: 2); + expect(await cursor.moveNext(), isTrue); + await cursor.close(); + // A statement left open would keep the table busy for this drop. + await db.execute('DROP TABLE t'); + }); + }); + + group('files', () { + test('databaseExists and deleteDatabase', () async { + expect(await factory.databaseExists(inMemoryDatabasePath), isFalse); + await factory.deleteDatabase('exists.db'); + expect(await factory.databaseExists('exists.db'), isFalse); + final Database db = await factory.openDatabase('exists.db'); + expect(await factory.databaseExists('exists.db'), isTrue); + await db.close(); + await factory.deleteDatabase('exists.db'); + expect(await factory.databaseExists('exists.db'), isFalse); + }); + + test('deleting an open single-instance database closes it first', () async { + Database db = await openFresh('delete_open.db'); + await db.setVersion(1); + await db.execute('BEGIN TRANSACTION'); + await db.setVersion(2); + await factory.deleteDatabase('delete_open.db'); + db = await factory.openDatabase('delete_open.db'); + expect(await db.getVersion(), 0); + await db.close(); + }); + + test('database bytes can be read and written', () async { + final Database db = await openFresh('bytes.db'); + await db.execute('CREATE TABLE t (x)'); + await db.insert('t', {'x': 42}); + await db.close(); + final Uint8List bytes = await factory.readDatabaseBytes('bytes.db'); + expect(bytes.length, greaterThan(0)); + await factory.writeDatabaseBytes('copy.db', bytes); + final Database copy = await factory.openDatabase('copy.db'); + expect(await copy.query('t'), >[ + {'x': 42}, + ]); + await copy.close(); + }); + }); + + test('sqliteVersion reads the library version', () { + expect(SqfliteWatchos.sqliteVersion, matches(RegExp(r'^3\.\d+\.\d+'))); + }); + + group('the method protocol, below sqflite_common', () { + late SqfliteWatchosHandler handler; + + setUp(() { + handler = SqfliteWatchosHandler( + Sqlite3Library(DynamicLibrary.open('/usr/lib/libsqlite3.dylib')), + databasesPath: dir.path, + platformVersion: 'watchOS test', + ); + }); + + Future open() async => + ((await handler.handle('openDatabase', { + 'path': inMemoryDatabasePath, + }))! as Map)['id']! as int; + + test('a call from outside an open transaction is queued until it ends', + () async { + final int id = await open(); + await handler.handle('execute', { + 'id': id, + 'sql': 'CREATE TABLE t (x)', + }); + final Object? begun = await handler.handle('execute', { + 'id': id, + 'sql': 'BEGIN IMMEDIATE', + 'inTransaction': true, + 'transactionId': null, + }); + final int txn = + (begun! as Map)['transactionId']! as int; + + bool outsideDone = false; + final Future outside = + handler.handle('insert', { + 'id': id, + 'sql': 'INSERT INTO t VALUES (?)', + 'arguments': ['outside'], + }).then((Object? v) { + outsideDone = true; + return v; + }); + await handler.handle('insert', { + 'id': id, + 'sql': 'INSERT INTO t VALUES (?)', + 'arguments': ['inside'], + 'transactionId': txn, + }); + await pumpEventQueue(); + expect(outsideDone, isFalse); + + await handler.handle('execute', { + 'id': id, + 'sql': 'COMMIT', + 'inTransaction': false, + 'transactionId': txn, + }); + expect(await outside, 2); + final Object? rows = await handler.handle('query', { + 'id': id, + 'sql': 'SELECT x FROM t', + }); + expect((rows! as Map)['rows'], [ + ['inside'], + ['outside'], + ]); + }); + + test('closing a database cancels what is queued on it', () async { + final int id = await open(); + final Object? begun = await handler.handle('execute', { + 'id': id, + 'sql': 'BEGIN', + 'inTransaction': true, + 'transactionId': null, + }); + expect(begun, isA>()); + final Future queued = handler + .handle('query', {'id': id, 'sql': 'SELECT 1'}); + // Listened to before the close fails it. + final Future failed = + expectLater(queued, throwsA(isA())); + await handler.handle('closeDatabase', {'id': id}); + await failed; + }); + + test('a closed database id answers as closed', () async { + final int id = await open(); + await handler.handle('closeDatabase', {'id': id}); + // Closing twice is fine. + await handler.handle('closeDatabase', {'id': id}); + await expectLater( + handler.handle('query', {'id': id, 'sql': 'SELECT 1'}), + throwsA(isA().having( + (DatabaseException e) => e.isDatabaseClosedError(), + 'closed', + true)), + ); + }); + + test('a failure that rolls the transaction back marks it closed', () async { + final int id = await open(); + await handler.handle('execute', { + 'id': id, + 'sql': 'CREATE TABLE t (x UNIQUE ON CONFLICT ROLLBACK)', + }); + final Object? begun = await handler.handle('execute', { + 'id': id, + 'sql': 'BEGIN', + 'inTransaction': true, + 'transactionId': null, + }); + final int txn = + (begun! as Map)['transactionId']! as int; + Future insert() => handler.handle('insert', { + 'id': id, + 'sql': 'INSERT INTO t VALUES (1)', + 'transactionId': txn, + }); + await insert(); + await expectLater( + insert(), + throwsA(isA().having( + (SqfliteDatabaseException e) => e.transactionClosed, + 'transactionClosed', + true)), + ); + // The database takes calls from outside again at once. + expect( + await handler + .handle('query', {'id': id, 'sql': 'SELECT 1'}), + isNotNull); + }); + + test('getPlatformVersion and an unknown method', () async { + expect(await handler.handle('getPlatformVersion'), 'watchOS test'); + await expectLater(handler.handle('nope'), throwsA(isA())); + }); + }); +} diff --git a/packages/sqflite_watchos/watchos/Classes/sqflite_watchos_ffi.h b/packages/sqflite_watchos/watchos/Classes/sqflite_watchos_ffi.h new file mode 100644 index 0000000..b61c578 --- /dev/null +++ b/packages/sqflite_watchos/watchos/Classes/sqflite_watchos_ffi.h @@ -0,0 +1,32 @@ +// Copyright 2026 The FlutterWatch Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// watchOS implementation of `sqflite`, over dart:ffi. +// +// SQLite itself is the system library: watchOS ships libsqlite3, the CLI links +// it (`.linkedLibrary("sqlite3")` in Package.swift), and Dart calls its C API +// directly through `DynamicLibrary.process()`. This file adds only what +// SQLite does not have: where databases live, and a call into libsqlite3 +// that keeps the library linked into the watch binary. + +#ifndef SQFLITE_WATCHOS_FFI_H +#define SQFLITE_WATCHOS_FFI_H + +// Each exported symbol is marked `used` + default-visibility so it survives +// the linker's `-dead_strip` and lands in the executable's dynamic symbol +// table, where `DynamicLibrary.process()` / dlsym can resolve it. +#define SQFLITE_WATCHOS_EXPORT \ + __attribute__((visibility("default"))) __attribute__((used)) + +// The app's Documents directory, where sqflite keeps databases on iOS too. +// The string is owned by this file and lives as long as the app. +SQFLITE_WATCHOS_EXPORT +const char* sqflite_watchos_databases_path(void); + +// sqlite3_libversion(), for example "3.46.1". Also what keeps libsqlite3 +// linked: the binary references it from here. +SQFLITE_WATCHOS_EXPORT +const char* sqflite_watchos_sqlite_version(void); + +#endif // SQFLITE_WATCHOS_FFI_H diff --git a/packages/sqflite_watchos/watchos/Classes/sqflite_watchos_ffi.m b/packages/sqflite_watchos/watchos/Classes/sqflite_watchos_ffi.m new file mode 100644 index 0000000..23ba53e --- /dev/null +++ b/packages/sqflite_watchos/watchos/Classes/sqflite_watchos_ffi.m @@ -0,0 +1,30 @@ +// Copyright 2026 The FlutterWatch Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// The two native helpers sqflite_watchos needs; see the header. Everything +// else is libsqlite3's own C API, called from Dart. + +#import "sqflite_watchos_ffi.h" + +#import +#import + +const char* sqflite_watchos_databases_path(void) { + static NSString* path; + static dispatch_once_t once; + dispatch_once(&once, ^{ + path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, + NSUserDomainMask, YES) + .firstObject; + if (path == nil) { + path = NSTemporaryDirectory(); + } + }); + // `path` is never released, so its UTF-8 buffer stays valid. + return path.fileSystemRepresentation; +} + +const char* sqflite_watchos_sqlite_version(void) { + return sqlite3_libversion(); +} diff --git a/packages/sqflite_watchos/watchos/Package.swift b/packages/sqflite_watchos/watchos/Package.swift new file mode 100644 index 0000000..57724d1 --- /dev/null +++ b/packages/sqflite_watchos/watchos/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version:5.9 +// Copyright 2026 The FlutterWatch Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// FFI plugin manifest. The flutter-watchos CLI discovers the plugin through +// this file, compiles `Classes/*.m` into a static archive force-loaded into +// the watch binary, and links the frameworks and libraries declared below. +import PackageDescription + +let package = Package( + name: "sqflite_watchos", + platforms: [ + .watchOS("7.0"), + ], + products: [ + .library(name: "sqflite-watchos", targets: ["sqflite_watchos"]), + ], + targets: [ + .target( + name: "sqflite_watchos", + path: "Classes", + publicHeadersPath: ".", + cSettings: [ + .headerSearchPath("."), + ], + linkerSettings: [ + .linkedFramework("Foundation"), + // The system SQLite, whose C API Dart calls directly. + .linkedLibrary("sqlite3"), + ] + ), + ] +) diff --git a/tool/upstream_versions.yaml b/tool/upstream_versions.yaml index 08ec57e..8124cde 100644 --- a/tool/upstream_versions.yaml +++ b/tool/upstream_versions.yaml @@ -25,6 +25,7 @@ upstreams: path_provider: 2.1.6 sensors_plus: 7.1.0 shared_preferences: 2.5.5 + sqflite: 2.4.4+2 url_launcher: 6.3.2 video_player: 2.14.0 From ee4752e19c571e9613566e6e3171273068042a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuzhan=20Arslanta=C5=9F?= Date: Wed, 21 Oct 2026 11:46:50 +0200 Subject: [PATCH 2/5] sqflite_watchos: delete the files SQLite keeps beside a database deleteDatabase removed the database file alone. sqflite_darwin also removes its -wal, -shm and -journal files, and a write-ahead log left behind would be replayed into a new database created at the same path. The databases path is now a copy native owns: fileSystemRepresentation's buffer is only autoreleased, whatever keeps the string alive. Co-Authored-By: Claude Opus 5.5 --- packages/sqflite_watchos/README.md | 2 +- packages/sqflite_watchos/lib/src/handler.dart | 26 ++++++++++++++----- .../test/sqflite_watchos_test.dart | 12 +++++++++ .../watchos/Classes/sqflite_watchos_ffi.m | 20 ++++++++------ 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/packages/sqflite_watchos/README.md b/packages/sqflite_watchos/README.md index 7b83302..3e662b9 100644 --- a/packages/sqflite_watchos/README.md +++ b/packages/sqflite_watchos/README.md @@ -45,7 +45,7 @@ await db.insert('lap', {'at': DateTime.now().millisecondsSinceE | `queryCursor` | supported, page by page | | `batch` | supported, with `noResult` and `continueOnError` | | `transaction` | supported; calls from outside an open transaction wait until it ends | -| `databaseExists` / `deleteDatabase` / `getDatabasesPath` | supported; deleting an open single-instance database closes it first | +| `databaseExists` / `deleteDatabase` / `getDatabasesPath` | supported; deleting an open single-instance database closes it first, and the `-wal`, `-shm` and `-journal` files beside it go too, as on iOS | | `readDatabaseBytes` / `writeDatabaseBytes` | supported | | `DatabaseException` helpers | `isUniqueConstraintError`, `isNoSuchTableError`, `isSyntaxError`, `isOpenFailedError`, `isReadOnlyError`, `isDatabaseClosedError`; `getResultCode()` is SQLite's extended result code | diff --git a/packages/sqflite_watchos/lib/src/handler.dart b/packages/sqflite_watchos/lib/src/handler.dart index 3c2676d..4fcb446 100644 --- a/packages/sqflite_watchos/lib/src/handler.dart +++ b/packages/sqflite_watchos/lib/src/handler.dart @@ -270,13 +270,27 @@ class SqfliteWatchosHandler { if (open != null) { _forget(open); } - try { - final File file = File(path); - if (file.existsSync()) { - file.deleteSync(); + if (path == _inMemory) { + return; + } + // With the files SQLite keeps beside it, as sqflite_darwin deletes them: + // a write-ahead log left behind would be replayed into a new database + // created at the same path. + for (final String suffix in const [ + '', + '-wal', + '-shm', + '-journal' + ]) { + try { + final File file = File('$path$suffix'); + if (file.existsSync()) { + file.deleteSync(); + } + } catch (_) { + // As on every platform: deleting what cannot be deleted is not an + // error. } - } catch (_) { - // As on every platform: deleting what cannot be deleted is not an error. } } diff --git a/packages/sqflite_watchos/test/sqflite_watchos_test.dart b/packages/sqflite_watchos/test/sqflite_watchos_test.dart index 1b9f2ac..470a38f 100644 --- a/packages/sqflite_watchos/test/sqflite_watchos_test.dart +++ b/packages/sqflite_watchos/test/sqflite_watchos_test.dart @@ -428,6 +428,18 @@ void main() { expect(await factory.databaseExists('exists.db'), isFalse); }); + test('deleteDatabase deletes the files SQLite keeps beside it', () async { + final String path = p.join(dir.path, 'sidecars.db'); + final List files = [ + for (final String suffix in ['', '-wal', '-shm', '-journal']) + File('$path$suffix')..writeAsStringSync('x'), + ]; + await factory.deleteDatabase('sidecars.db'); + for (final File file in files) { + expect(file.existsSync(), isFalse, reason: file.path); + } + }); + test('deleting an open single-instance database closes it first', () async { Database db = await openFresh('delete_open.db'); await db.setVersion(1); diff --git a/packages/sqflite_watchos/watchos/Classes/sqflite_watchos_ffi.m b/packages/sqflite_watchos/watchos/Classes/sqflite_watchos_ffi.m index 23ba53e..0d84007 100644 --- a/packages/sqflite_watchos/watchos/Classes/sqflite_watchos_ffi.m +++ b/packages/sqflite_watchos/watchos/Classes/sqflite_watchos_ffi.m @@ -9,20 +9,24 @@ #import #import +#import const char* sqflite_watchos_databases_path(void) { - static NSString* path; + static char* path; static dispatch_once_t once; dispatch_once(&once, ^{ - path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, - NSUserDomainMask, YES) - .firstObject; - if (path == nil) { - path = NSTemporaryDirectory(); + NSString* directory = + NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, + NSUserDomainMask, YES) + .firstObject; + if (directory == nil) { + directory = NSTemporaryDirectory(); } + // A copy of its own: fileSystemRepresentation's buffer is only + // autoreleased, whatever keeps the string alive. + path = strdup(directory.fileSystemRepresentation); }); - // `path` is never released, so its UTF-8 buffer stays valid. - return path.fileSystemRepresentation; + return path; } const char* sqflite_watchos_sqlite_version(void) { From a8a9c85a97b8daf7502e7cfc09066eefe22049e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuzhan=20Arslanta=C5=9F?= Date: Wed, 21 Oct 2026 12:03:09 +0200 Subject: [PATCH 3/5] sqflite_watchos: format upstream's integration test as CI checks it Its content is upstream's; only the layout changes, so that CI's dart format step passes. Co-Authored-By: Claude Opus 5.5 --- packages/sqflite_watchos/PORTING_REPORT.md | 3 ++- .../example/integration_test/sqflite_test.dart | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/sqflite_watchos/PORTING_REPORT.md b/packages/sqflite_watchos/PORTING_REPORT.md index dcd956b..6edd881 100644 --- a/packages/sqflite_watchos/PORTING_REPORT.md +++ b/packages/sqflite_watchos/PORTING_REPORT.md @@ -56,7 +56,8 @@ replaced by `Platform.operatingSystemVersion`. sqflite's own example app is built on `sqflite_example_common`, which is not published, so `example/lib/main.dart` is a small watch demo instead. -`example/integration_test/sqflite_test.dart` is upstream's test, unchanged. +`example/integration_test/sqflite_test.dart` is upstream's test, unchanged but for +the formatting this repo's CI checks. ## Protocol coverage diff --git a/packages/sqflite_watchos/example/integration_test/sqflite_test.dart b/packages/sqflite_watchos/example/integration_test/sqflite_test.dart index 1eb7855..1f4e919 100644 --- a/packages/sqflite_watchos/example/integration_test/sqflite_test.dart +++ b/packages/sqflite_watchos/example/integration_test/sqflite_test.dart @@ -189,9 +189,10 @@ void main() { for (var i = 0; i < count; i++) { final db = dbs[i]!; try { - final name = - (await db.query('Test', columns: ['name'])).first.values.first - as String?; + final name = (await db.query('Test', columns: ['name'])) + .first + .values + .first as String?; expect(name, 'test_$i'); } finally { await db.close(); @@ -325,7 +326,9 @@ void main() { return (await db.query( 'Test', orderBy: 'name COLLATE LOCALIZED ASC', - )).map((e) => e.values.first!.toString()).toList(); + )) + .map((e) => e.values.first!.toString()) + .toList(); } try { From bddc6d11e4c5ef7af8250942607641aa73160f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuzhan=20Arslanta=C5=9F?= Date: Sat, 24 Oct 2026 00:04:58 +0200 Subject: [PATCH 4/5] sqflite_watchos: the review's fixes From the review of PR #5: * The host tests open the host's system SQLite: /usr/lib/libsqlite3.dylib on macOS, libsqlite3.so.0 on CI's Linux runner, where every test failed in setUpAll. * lib/src/handler.dart is adapted from sqflite_common_ffi's sqflite_ffi_impl.dart and now says so, with Alexandre Roux Tekartik's BSD-2-Clause notice. The README names BSD-2-Clause, as LICENSE does, and PORTING_REPORT names sqflite_common_ffi as a source. * A bool argument binds as 1 or 0, and a plain List as a blob, as sqflite_darwin and sqflite_common_ffi bind them; both threw. * tool/upstream_versions.yaml: sqflite 2.4.5, the latest release. * Each connection waits up to 2 seconds for another connection's lock, FMDB's default on iOS; a second connection failed at once. * An in-memory database is never a single instance, as in sqflite_darwin. * sqflite_common ">=2.5.14 <2.6.0": 2.5.14 fixes a hang when a cursor is closed inside a transaction, and the src/ imports stop below the next minor. * SqfliteWatchos extends SqflitePlatform, as AUTHORING asks; its factory is databaseFactoryWatchos, since SqflitePlatform has an instance databaseFactory. * The registerWith test compared a factory with itself; it uses a second factory now. The example names sqflite ^2.4.4. The README lists the deprecated Sqflite.* debug helpers, which call the method channel directly, and PORTING_REPORT no longer marks the unified demo with the "no upstream test" sign. Checked: 39 host tests on macOS (5 new; four fail on the old code), and upstream's integration test, 14 tests, on the watchOS 27.0 Simulator. The host tests have not run on Linux here. Co-Authored-By: Claude Opus 5.5 --- packages/sqflite_watchos/CHANGELOG.md | 14 ++- packages/sqflite_watchos/PORTING_REPORT.md | 45 +++++--- packages/sqflite_watchos/README.md | 24 ++-- packages/sqflite_watchos/example/pubspec.yaml | 2 +- .../sqflite_watchos/lib/sqflite_watchos.dart | 16 +-- packages/sqflite_watchos/lib/src/handler.dart | 22 ++-- packages/sqflite_watchos/lib/src/sqlite3.dart | 16 ++- packages/sqflite_watchos/pubspec.yaml | 9 +- .../test/sqflite_watchos_test.dart | 108 ++++++++++++++++-- tool/upstream_versions.yaml | 2 +- 10 files changed, 202 insertions(+), 56 deletions(-) diff --git a/packages/sqflite_watchos/CHANGELOG.md b/packages/sqflite_watchos/CHANGELOG.md index d0f7848..bb63e56 100644 --- a/packages/sqflite_watchos/CHANGELOG.md +++ b/packages/sqflite_watchos/CHANGELOG.md @@ -8,8 +8,18 @@ physical watch. * sqflite's method protocol answered in Dart: open, close, execute, insert, update, query with paged cursors, batch with `noResult` and `continueOnError`, transactions with transaction ids, database files and - bytes, as sqflite_common_ffi answers it. -* `registerWith` makes this package's factory sqflite's default on watchOS. + bytes. `lib/src/handler.dart` is adapted from sqflite_common_ffi's + `sqflite_ffi_impl.dart` and carries its BSD-2-Clause notice. +* Arguments bind as on iOS: a `bool` as 1 or 0, a `Uint8List` or plain + `List` as a blob. +* A connection waits up to 2 seconds for another connection's lock, FMDB's + default on iOS. +* An in-memory database is never a single instance. +* `SqfliteWatchos` extends `SqflitePlatform`; `registerWith` makes its + `databaseFactoryWatchos` sqflite's default on watchOS. +* `sqflite_common: ">=2.5.14 <2.6.0"`: 2.5.14 fixes a hang when a cursor is + closed inside a transaction, and the `src/` imports stop below the next + minor. * Two FFI exports: `sqflite_watchos_databases_path` and `sqflite_watchos_sqlite_version`. * The example's `lib/` is a small watch demo, because sqflite's own example diff --git a/packages/sqflite_watchos/PORTING_REPORT.md b/packages/sqflite_watchos/PORTING_REPORT.md index 6edd881..cb9b1f1 100644 --- a/packages/sqflite_watchos/PORTING_REPORT.md +++ b/packages/sqflite_watchos/PORTING_REPORT.md @@ -5,10 +5,10 @@ | Aspect | Result | |---|---| | Implementation | ✅ Working (system SQLite over FFI) | -| watchOS capability | Full — every method of sqflite's protocol. Not yet checked on a physical watch | -| Host unit tests (`flutter test`) | ✅ pass — real SQL against macOS's `libsqlite3`, through sqflite_common's `Database` API and the protocol below it | +| watchOS capability | Full — every method of sqflite's protocol. The deprecated `Sqflite.*` debug helpers call the method channel directly and throw (see the README). Not yet checked on a physical watch | +| Host unit tests (`flutter test`) | ✅ pass on macOS — real SQL against the host's system SQLite (`/usr/lib/libsqlite3.dylib`; `libsqlite3.so.0` on CI's Linux runner), through sqflite_common's `Database` API and the protocol below it | | Upstream integration test | ✅ passes verbatim on the watchOS 27.0 Simulator (14 tests), against the watch's own `libsqlite3`. Run it with `flutter-watchos test`: it uses `package:test`'s `test`, which `flutter-watchos drive` does not collect, so `drive` reports nothing and waits | -| Internal unified demo | ○ not included yet | +| Internal unified demo | not in the internal test app yet | Marking: ✅ full / passes · ◐ partial — reason given · ○ not applicable (no upstream test) · ✗ unsupported on watchOS. @@ -18,6 +18,7 @@ Marking: ✅ full / passes · ◐ partial — reason given · ○ not applicable Generated by `flutter-watchos plugin port` on 2026-10-19. Source: `sqflite_darwin` 2.4.4+1 +Protocol handler adapted from: `sqflite_common_ffi` 2.4.4, `lib/src/sqflite_ffi_impl.dart` (BSD-2-Clause, Alexandre Roux Tekartik) Base platform: ios (Objective-C) Output: `./sqflite_watchos` @@ -29,25 +30,32 @@ package keeps none of that native code: - `lib/src/sqlite3.dart` binds the part of SQLite's C API sqflite needs (`sqlite3_open_v2`, `_prepare_v2`, `_step`, the `_bind_*` and `_column_*` - functions, `_changes`, `_last_insert_rowid`, `_get_autocommit`, the error - calls). On the watch they resolve from the process, because - `watchos/Package.swift` links the system `libsqlite3`; host tests open the - macOS system library instead, so the same code runs real SQL there. + functions, `_changes`, `_last_insert_rowid`, `_get_autocommit`, + `_busy_timeout`, the error calls). Arguments bind as sqflite_darwin binds + them: a `bool` as 1 or 0, a `Uint8List` or plain `List` as a blob. + Each connection waits up to 2 seconds for another connection's lock, + FMDB's default on iOS. On the watch the functions resolve from the + process, because `watchos/Package.swift` links the system `libsqlite3`; + host tests open the host's system library instead, so the same code runs + real SQL there. - `lib/src/handler.dart` answers sqflite's method protocol: the same - methods, arguments and results the method channel carries on iOS, with the - behaviour of sqflite_common_ffi, the protocol's reference implementation - outside a method channel. That covers transaction ids (a call from outside + methods, arguments and results the method channel carries on iOS. It is + adapted from sqflite_common_ffi's `lib/src/sqflite_ffi_impl.dart`, the + protocol's reference implementation outside a method channel, and carries + its copyright notice. That covers transaction ids (a call from outside the open transaction waits for it), `transactionClosed` when SQLite rolls a transaction back, paged cursors, batches with `continueOnError`, and - single-instance databases. + single-instance databases, never for `:memory:`, as in sqflite_darwin. - `watchos/Classes/sqflite_watchos_ffi.m` has two helpers: the Documents directory, and `sqlite3_libversion()`, which also keeps `libsqlite3` linked. -- sqflite federates through sqflite_common's default factory: `registerWith` - sets `databaseFactoryOrNull` to a factory from sqflite_common's - `buildDatabaseFactory`, as sqflite_darwin sets its method-channel factory. - `buildDatabaseFactory` and `SqfliteDatabaseException` come from - sqflite_common's `src/` libraries, which sqflite_common_ffi imports the - same way; `sqflite_common: ^2.5.11` pins them. +- sqflite federates through sqflite_common's default factory. `SqfliteWatchos` + extends `SqflitePlatform`, as `SqfliteDarwin` does, and its `registerWith` + sets `databaseFactoryOrNull` to `databaseFactoryWatchos`, a factory from + sqflite_common's `buildDatabaseFactory`, as sqflite_darwin sets its + method-channel factory. `buildDatabaseFactory` and + `SqfliteDatabaseException` come from sqflite_common's `src/` libraries, + which sqflite_common_ffi imports the same way; `src/` is not public API, so + `sqflite_common: ">=2.5.14 <2.6.0"` stops below the next minor. The one API the port tool flagged, `UIDevice` (the platform version), is replaced by `Platform.operatingSystemVersion`. @@ -63,11 +71,12 @@ the formatting this repo's CI checks. | Method | watchOS | |---|---| -| `openDatabase` | ✅ read-write (creating parent directories) or read-only; single instance by path | +| `openDatabase` | ✅ read-write (creating parent directories) or read-only; single instance by path, never for `:memory:` | | `closeDatabase` | ✅ cancels operations queued behind a transaction; closing twice is fine | | `execute` | ✅ several statements without arguments, one with; begins and ends transactions | | `insert` | ✅ the new row id, or null when nothing was inserted (sqflite_common returns 0) | | `update` | ✅ the changed row count | +| arguments, every method | ✅ null, int, `BigInt` within 64 bits, double, bool (1 or 0), String, `Uint8List` and `List` (a blob) | | `query` / `queryCursorNext` | ✅ all rows, or pages for a cursor | | `batch` | ✅ `noResult`, `continueOnError` | | `getDatabasesPath` | ✅ the app's Documents directory | diff --git a/packages/sqflite_watchos/README.md b/packages/sqflite_watchos/README.md index 3e662b9..62cec34 100644 --- a/packages/sqflite_watchos/README.md +++ b/packages/sqflite_watchos/README.md @@ -40,8 +40,8 @@ await db.insert('lap', {'at': DateTime.now().millisecondsSinceE | sqflite | watchOS | |---|---| -| `openDatabase` / `openReadOnlyDatabase` | supported; relative paths live in the app's Documents directory, as on iOS, and missing parent directories are created | -| `execute` / `insert` / `update` / `delete` / `rawQuery` and friends | supported; integers are 64-bit, text is UTF-8, blobs come back as `Uint8List` | +| `openDatabase` / `openReadOnlyDatabase` | supported; relative paths live in the app's Documents directory, as on iOS, and missing parent directories are created. A connection waits up to 2 seconds for another connection's lock, as on iOS | +| `execute` / `insert` / `update` / `delete` / `rawQuery` and friends | supported; integers are 64-bit, a `bool` argument is stored as 1 or 0, text is UTF-8, and a blob (a `Uint8List` or a `List` argument) comes back as `Uint8List` | | `queryCursor` | supported, page by page | | `batch` | supported, with `noResult` and `continueOnError` | | `transaction` | supported; calls from outside an open transaction wait until it ends | @@ -58,12 +58,20 @@ an isolate of its own. ## Not supported on watchOS -Nothing: every method of sqflite's protocol is answered. -`androidSetLocale` and the other Android-only calls are Android-only on -every other platform too. The sqflite-specific `PRAGMA sqflite_…` -statements are accepted and ignored. -`PORTING_REPORT.md` lists each method under "Protocol coverage". +Every method of sqflite's protocol is answered; `PORTING_REPORT.md` lists +each under "Protocol coverage". What is left: + +| sqflite | On watchOS | Why | +|---|---|---| +| `Sqflite.setDebugModeOn`, `Sqflite.devSetDebugModeOn`, `Sqflite.devSetOptions`, `Sqflite.devInvokeMethod` (all deprecated) | throw `MissingPluginException` | they call sqflite's method channel directly, not the database factory; `databaseFactory.setLogLevel` reaches this package | +| `androidSetLocale` | fails | Android only, as on iOS | + +`Sqflite.firstIntValue`, `Sqflite.hex` and `Sqflite.setLockWarningInfo` +run in Dart and work. The sqflite-specific `PRAGMA sqflite_…` statements +are accepted and ignored. ## License -The FlutterWatch Authors under a BSD-3-Clause license. See `LICENSE` for the full text. +BSD-2-Clause, as upstream sqflite's. `lib/src/handler.dart` is adapted from +sqflite_common_ffi and carries its copyright notice. See `LICENSE` for the +full text. diff --git a/packages/sqflite_watchos/example/pubspec.yaml b/packages/sqflite_watchos/example/pubspec.yaml index 8aa9ab6..ef6fa26 100644 --- a/packages/sqflite_watchos/example/pubspec.yaml +++ b/packages/sqflite_watchos/example/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: flutter: sdk: flutter - sqflite: ^2.4.2 + sqflite: ^2.4.4 sqflite_watchos: path: ../ path: ^1.9.0 diff --git a/packages/sqflite_watchos/lib/sqflite_watchos.dart b/packages/sqflite_watchos/lib/sqflite_watchos.dart index 5e3f70a..f70922e 100644 --- a/packages/sqflite_watchos/lib/sqflite_watchos.dart +++ b/packages/sqflite_watchos/lib/sqflite_watchos.dart @@ -7,8 +7,8 @@ // Method-channel plugins are not supported on watchOS, so sqflite's method // protocol is answered in Dart (`src/handler.dart`) over the system SQLite's // C API (`src/sqlite3.dart`). sqflite federates through sqflite_common's -// default database factory: `registerWith` sets it, as sqflite_darwin does -// with its method-channel factory. +// default database factory: `registerWith` sets it, as sqflite_darwin, which +// also extends SqflitePlatform, does with its method-channel factory. import 'dart:ffi'; import 'dart:io'; @@ -18,14 +18,15 @@ import 'package:ffi/ffi.dart'; import 'package:sqflite_common/src/mixin/factory.dart' show buildDatabaseFactory; import 'package:sqflite_common/sqflite.dart'; +import 'package:sqflite_platform_interface/sqflite_platform_interface.dart'; import 'src/handler.dart'; import 'src/sqlite3.dart'; /// watchOS implementation of `sqflite`. -abstract final class SqfliteWatchos { +class SqfliteWatchos extends SqflitePlatform { /// Test hook: the library SQLite's C API resolves from, instead of the - /// process. Host tests pass the macOS system SQLite. + /// process. Host tests pass the host's system SQLite. static DynamicLibrary? libraryOverride; /// Test hook: where relative database paths live, instead of the app's @@ -34,8 +35,9 @@ abstract final class SqfliteWatchos { static DatabaseFactory? _factory; - /// The database factory that answers sqflite's calls on the watch. - static DatabaseFactory get databaseFactory => _factory ??= () { + /// The database factory that answers sqflite's calls on the watch, as + /// [SqflitePlatform.databaseFactoryMethodChannel] answers them on iOS. + static DatabaseFactory get databaseFactoryWatchos => _factory ??= () { final DynamicLibrary library = libraryOverride ?? DynamicLibrary.process(); final SqfliteWatchosHandler handler = SqfliteWatchosHandler( @@ -52,7 +54,7 @@ abstract final class SqfliteWatchos { /// Registers this implementation as sqflite's default database factory on /// watchOS, unless the app already set one. static void registerWith() { - databaseFactoryOrNull ??= databaseFactory; + databaseFactoryOrNull ??= databaseFactoryWatchos; } /// Forgets the factory so the next use builds a new one, for tests that diff --git a/packages/sqflite_watchos/lib/src/handler.dart b/packages/sqflite_watchos/lib/src/handler.dart index 4fcb446..46cc4e8 100644 --- a/packages/sqflite_watchos/lib/src/handler.dart +++ b/packages/sqflite_watchos/lib/src/handler.dart @@ -1,16 +1,21 @@ +// Copyright (c) 2019, Alexandre Roux Tekartik. All rights reserved. // Copyright 2026 The FlutterWatch Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// Use of this source code is governed by the BSD 2-Clause license that can +// be found in the LICENSE file. // // sqflite's method protocol, answered in Dart over SQLite's C API. // +// Adapted from sqflite_common_ffi's lib/src/sqflite_ffi_impl.dart, the +// reference implementation of the protocol outside a method channel: its +// structure, its error messages, and its handling of transaction ids, +// operations queued behind another transaction, closed database ids, paged +// cursors, and batches that continue on error. Here SQLite is the system's, +// bound in sqlite3.dart, instead of package:sqlite3. +// // sqflite_common drives every platform through one call, // `invokeMethod(method, arguments)`: on iOS and Android it crosses a method // channel, here it lands in [SqfliteWatchosHandler.handle]. The methods and -// the shape of their arguments and results are sqflite's own; the behaviour -// follows sqflite_common_ffi, the reference implementation of the protocol -// outside a method channel: transaction ids, operations queued behind -// another transaction, paged cursors, and batches that continue on error. +// the shape of their arguments and results are sqflite's own. import 'dart:async'; import 'dart:io'; @@ -206,7 +211,10 @@ class SqfliteWatchosHandler { Map _open(Map args) { final String path = _path(args); final bool readOnly = args['readOnly'] == true; - final bool singleInstance = args['singleInstance'] == true; + // An in-memory database is never shared, as in sqflite_darwin: each + // open is a database of its own. + final bool singleInstance = + args['singleInstance'] == true && path != _inMemory; if (singleInstance) { final _Database? existing = _singleInstances[path]; if (existing != null) { diff --git a/packages/sqflite_watchos/lib/src/sqlite3.dart b/packages/sqflite_watchos/lib/src/sqlite3.dart index aea79ce..beea6ff 100644 --- a/packages/sqflite_watchos/lib/src/sqlite3.dart +++ b/packages/sqflite_watchos/lib/src/sqlite3.dart @@ -92,6 +92,9 @@ class Sqlite3Library { Int32 Function(Pointer, Int32), int Function( Pointer, int)>('sqlite3_extended_result_codes'), + _busyTimeout = library.lookupFunction< + Int32 Function(Pointer, Int32), + int Function(Pointer, int)>('sqlite3_busy_timeout'), _prepareV2 = library.lookupFunction< Int32 Function(Pointer, Pointer, Int32, Pointer>, Pointer>), @@ -190,6 +193,7 @@ class Sqlite3Library { Pointer, Pointer>, int, Pointer) _openV2; final int Function(Pointer) _closeV2; final int Function(Pointer, int) _extendedResultCodes; + final int Function(Pointer, int) _busyTimeout; final int Function(Pointer, Pointer, int, Pointer>, Pointer>) _prepareV2; final int Function(Pointer) _step; @@ -221,6 +225,10 @@ class Sqlite3Library { final int Function(Pointer) _getAutocommit; final Pointer Function() _libVersion; + /// How long a statement waits for another connection's lock before it + /// fails with `SQLITE_BUSY`: FMDB's default, which sqflite keeps on iOS. + static const int busyTimeoutMilliseconds = 2000; + /// `sqlite3_libversion()`. String get version => _libVersion().toDartString(); @@ -240,6 +248,7 @@ class Sqlite3Library { throw failure; } _extendedResultCodes(db, 1); + _busyTimeout(db, busyTimeoutMilliseconds); return SqliteConnection._(this, db); }); } @@ -401,6 +410,9 @@ class SqliteStatement { _check(_lib._bindNull(_statement, index)); case int(): _check(_lib._bindInt64(_statement, index, value)); + case bool(): + // As sqflite_darwin and sqflite_common_ffi bind it. + _check(_lib._bindInt64(_statement, index, value ? 1 : 0)); case double(): _check(_lib._bindDouble(_statement, index, value)); case BigInt(): @@ -418,7 +430,9 @@ class SqliteStatement { _check( _lib._bindText(_statement, index, p, bytes.length, _transient)); }); - case Uint8List(): + case List(): + // A Uint8List, or a plain List as sqflite_darwin and + // sqflite_common_ffi take one: a blob of one byte per value. if (value.isEmpty) { _check(_lib._bindZeroBlob(_statement, index, 0)); } else { diff --git a/packages/sqflite_watchos/pubspec.yaml b/packages/sqflite_watchos/pubspec.yaml index 8cdaa7f..dcde999 100644 --- a/packages/sqflite_watchos/pubspec.yaml +++ b/packages/sqflite_watchos/pubspec.yaml @@ -23,9 +23,12 @@ dependencies: flutter: sdk: flutter path: ^1.8.0 - # buildDatabaseFactory and SqfliteDatabaseException, which - # sqflite_common_ffi imports the same way. - sqflite_common: ^2.5.11 + # buildDatabaseFactory and SqfliteDatabaseException come from src/, which + # sqflite_common_ffi imports the same way. src/ is not public API, so the + # constraint stops below the next minor. 2.5.14 fixes a hang when a cursor + # is closed inside a transaction. + sqflite_common: ">=2.5.14 <2.6.0" + sqflite_platform_interface: ^2.4.1 dev_dependencies: flutter_lints: ^4.0.0 diff --git a/packages/sqflite_watchos/test/sqflite_watchos_test.dart b/packages/sqflite_watchos/test/sqflite_watchos_test.dart index 470a38f..110f941 100644 --- a/packages/sqflite_watchos/test/sqflite_watchos_test.dart +++ b/packages/sqflite_watchos/test/sqflite_watchos_test.dart @@ -3,9 +3,10 @@ // found in the LICENSE file. // // Host-side tests that run real SQL. The handler binds SQLite's C API, which -// macOS ships too, so these point it at the system libsqlite3 and drive it -// through sqflite_common's own Database API, the way an app does. The watch -// resolves the same functions from its process instead. +// macOS and CI's Linux runner ship too, so these point it at the host's +// system libsqlite3 and drive it through sqflite_common's own Database API, +// the way an app does. The watch resolves the same functions from its +// process instead. import 'dart:async'; import 'dart:ffi'; @@ -18,21 +19,30 @@ import 'package:sqflite_common/sqflite.dart'; // ignore: implementation_imports import 'package:sqflite_common/src/exception.dart' show SqfliteDatabaseException; +// ignore: implementation_imports +import 'package:sqflite_common/src/mixin/factory.dart' + show buildDatabaseFactory; +import 'package:sqflite_platform_interface/sqflite_platform_interface.dart'; import 'package:sqflite_watchos/sqflite_watchos.dart'; import 'package:sqflite_watchos/src/handler.dart'; import 'package:sqflite_watchos/src/sqlite3.dart'; +/// The host's system SQLite: `/usr/lib` on macOS, the shared library's +/// soname on Linux. +DynamicLibrary hostSqlite() => Platform.isMacOS + ? DynamicLibrary.open('/usr/lib/libsqlite3.dylib') + : DynamicLibrary.open('libsqlite3.so.0'); + void main() { late Directory dir; late DatabaseFactory factory; setUpAll(() { dir = Directory.systemTemp.createTempSync('sqflite_watchos_test'); - SqfliteWatchos.libraryOverride = - DynamicLibrary.open('/usr/lib/libsqlite3.dylib'); + SqfliteWatchos.libraryOverride = hostSqlite(); SqfliteWatchos.databasesPathOverride = dir.path; SqfliteWatchos.resetForTesting(); - factory = SqfliteWatchos.databaseFactory; + factory = SqfliteWatchos.databaseFactoryWatchos; }); tearDownAll(() => dir.deleteSync(recursive: true)); @@ -50,10 +60,18 @@ void main() { }); test('registerWith keeps a factory the app already set', () { - final DatabaseFactory other = factory; + final DatabaseFactory other = buildDatabaseFactory( + invokeMethod: (String method, [Object? arguments]) async => null); databaseFactoryOrNull = other; SqfliteWatchos.registerWith(); expect(identical(databaseFactoryOrNull, other), isTrue); + expect(identical(databaseFactoryOrNull, factory), isFalse); + databaseFactoryOrNull = null; + SqfliteWatchos.registerWith(); + }); + + test('the implementation is a SqflitePlatform', () { + expect(SqfliteWatchos(), isA()); }); test('getDatabasesPath is where relative paths go', () async { @@ -100,6 +118,57 @@ void main() { await db.close(); }); + test('bools and plain int lists bind as on iOS: 1 or 0, and a blob', + () async { + final Database db = await factory.openDatabase(inMemoryDatabasePath); + await db.execute('CREATE TABLE t (flag INTEGER, data BLOB)'); + await db.insert('t', { + 'flag': true, + 'data': [1, 2, 255], + }); + await db.insert('t', {'flag': false, 'data': []}); + final List> rows = + await db.rawQuery('SELECT flag, data, typeof(data) AS type FROM t'); + expect(rows[0]['flag'], 1); + expect(rows[0]['data'], Uint8List.fromList([1, 2, 255])); + expect(rows[0]['type'], 'blob'); + expect(rows[1]['flag'], 0); + expect(rows[1]['data'], Uint8List(0)); + expect(rows[1]['type'], 'blob'); + expect( + await db + .rawQuery('SELECT count(*) AS n FROM t WHERE data = ?', [ + [1, 2, 255] + ]), + >[ + {'n': 1}, + ]); + await db.close(); + }); + + test('a locked database is waited for, as FMDB waits on iOS', () async { + final OpenDatabaseOptions separate = + OpenDatabaseOptions(singleInstance: false); + final Database first = await openFresh('busy.db', options: separate); + await first.execute('CREATE TABLE t (x)'); + final Database second = + await factory.openDatabase('busy.db', options: separate); + expect(await second.rawQuery('PRAGMA busy_timeout'), >[ + {'timeout': 2000}, + ]); + await first.execute('BEGIN IMMEDIATE'); + final Stopwatch waited = Stopwatch()..start(); + await expectLater( + second.insert('t', {'x': 1}), + throwsA(isA().having( + (DatabaseException e) => e.getResultCode(), 'SQLITE_BUSY', 5)), + ); + expect(waited.elapsedMilliseconds, greaterThanOrEqualTo(1500)); + await first.execute('ROLLBACK'); + await second.close(); + await first.close(); + }); + test('insert returns the row id, and 0 when nothing was inserted', () async { final Database db = await openFresh('insert.db'); await db @@ -476,7 +545,7 @@ void main() { setUp(() { handler = SqfliteWatchosHandler( - Sqlite3Library(DynamicLibrary.open('/usr/lib/libsqlite3.dylib')), + Sqlite3Library(hostSqlite()), databasesPath: dir.path, platformVersion: 'watchOS test', ); @@ -605,6 +674,29 @@ void main() { isNotNull); }); + test('an in-memory database is never a single instance', () async { + Future openShared() async => + ((await handler.handle('openDatabase', { + 'path': inMemoryDatabasePath, + 'singleInstance': true, + }))! as Map)['id']! as int; + final int first = await openShared(); + final int second = await openShared(); + expect(second, isNot(first)); + await handler.handle('execute', { + 'id': first, + 'sql': 'CREATE TABLE t (x)', + }); + await expectLater( + handler.handle( + 'query', {'id': second, 'sql': 'SELECT x FROM t'}), + throwsA(isA().having( + (DatabaseException e) => e.isNoSuchTableError('t'), + 'no such table', + true)), + ); + }); + test('getPlatformVersion and an unknown method', () async { expect(await handler.handle('getPlatformVersion'), 'watchOS test'); await expectLater(handler.handle('nope'), throwsA(isA())); diff --git a/tool/upstream_versions.yaml b/tool/upstream_versions.yaml index 8124cde..4325f75 100644 --- a/tool/upstream_versions.yaml +++ b/tool/upstream_versions.yaml @@ -25,7 +25,7 @@ upstreams: path_provider: 2.1.6 sensors_plus: 7.1.0 shared_preferences: 2.5.5 - sqflite: 2.4.4+2 + sqflite: 2.4.5 url_launcher: 6.3.2 video_player: 2.14.0 From 2e8fa394e35358d8e4bbec490812f8994d679f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuzhan=20Arslanta=C5=9F?= Date: Sat, 24 Oct 2026 00:38:55 +0200 Subject: [PATCH 5/5] sqflite_watchos: check the lock wait only against Apple's SQLite CI's first run on Linux passed 38 of 39 host tests. The one that failed waited for a locked database: Ubuntu's libsqlite3 failed the second connection's insert with SQLITE_BUSY after 39 ms instead of waiting out the timeout, though PRAGMA busy_timeout read 2000 there too. The timeout is still checked on both; the wait itself only on macOS, whose SQLite is the watch's. The README also says that sqflite_common warns once per type, in debug, about bool and List arguments, as the CI log shows it does. Co-Authored-By: Claude Opus 5.5 --- packages/sqflite_watchos/README.md | 2 +- packages/sqflite_watchos/test/sqflite_watchos_test.dart | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/sqflite_watchos/README.md b/packages/sqflite_watchos/README.md index 62cec34..bde9f0e 100644 --- a/packages/sqflite_watchos/README.md +++ b/packages/sqflite_watchos/README.md @@ -41,7 +41,7 @@ await db.insert('lap', {'at': DateTime.now().millisecondsSinceE | sqflite | watchOS | |---|---| | `openDatabase` / `openReadOnlyDatabase` | supported; relative paths live in the app's Documents directory, as on iOS, and missing parent directories are created. A connection waits up to 2 seconds for another connection's lock, as on iOS | -| `execute` / `insert` / `update` / `delete` / `rawQuery` and friends | supported; integers are 64-bit, a `bool` argument is stored as 1 or 0, text is UTF-8, and a blob (a `Uint8List` or a `List` argument) comes back as `Uint8List` | +| `execute` / `insert` / `update` / `delete` / `rawQuery` and friends | supported; integers are 64-bit, a `bool` argument is stored as 1 or 0, text is UTF-8, and a blob (a `Uint8List` or a `List` argument) comes back as `Uint8List`. sqflite_common still warns once per type, in debug, about `bool` and `List` arguments, as on every platform | | `queryCursor` | supported, page by page | | `batch` | supported, with `noResult` and `continueOnError` | | `transaction` | supported; calls from outside an open transaction wait until it ends | diff --git a/packages/sqflite_watchos/test/sqflite_watchos_test.dart b/packages/sqflite_watchos/test/sqflite_watchos_test.dart index 110f941..a3b200e 100644 --- a/packages/sqflite_watchos/test/sqflite_watchos_test.dart +++ b/packages/sqflite_watchos/test/sqflite_watchos_test.dart @@ -163,7 +163,13 @@ void main() { throwsA(isA().having( (DatabaseException e) => e.getResultCode(), 'SQLITE_BUSY', 5)), ); - expect(waited.elapsedMilliseconds, greaterThanOrEqualTo(1500)); + // Apple's SQLite, which the watch runs, waits out the timeout first. The + // libsqlite3 of CI's Ubuntu runner failed the same insert after 39 ms, + // without waiting, so the wait is only checked on macOS; the timeout + // itself is checked above on both. + if (Platform.isMacOS) { + expect(waited.elapsedMilliseconds, greaterThanOrEqualTo(1500)); + } await first.execute('ROLLBACK'); await second.close(); await first.close();