@capgo/capacitor-fast-sql 8.2.11 → 8.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +292 -291
- package/android/build.gradle +4 -1
- package/android/src/main/java/app/capgo/capacitor/fastsql/CapgoCapacitorFastSqlPlugin.java +1 -1
- package/android/src/main/java/app/capgo/capacitor/fastsql/HttpRequestJsonParams.java +413 -0
- package/android/src/main/java/app/capgo/capacitor/fastsql/SQLHTTPServer.java +17 -65
- package/ios/Sources/CapgoCapacitorFastSqlPlugin/CapgoCapacitorFastSqlPlugin.swift +1 -1
- package/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLDatabase.swift +5 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
# @capgo/capacitor-fast-sql
|
|
1
|
+
# @capgo/capacitor-fast-sql
|
|
2
2
|
|
|
3
3
|
Run SQLite fast in your Capacitor app: queries go through a local HTTP channel instead of the bridge, so large result sets and sync jobs stay quick. A practical replacement for IndexedDB, with optional encryption.
|
|
4
4
|
|
|
5
|
-
<a href="https://capgo.app/?ref=plugin_fast_sql"><img src="https://capgo.app/readme-banner.svg?repo=Cap-go/capacitor-fast-sql" alt="Capgo -
|
|
5
|
+
<a href="https://capgo.app/?ref=plugin_fast_sql"><img src="https://capgo.app/readme-banner.svg?repo=Cap-go/capacitor-fast-sql" alt="Capgo - Build Capacitor apps with AI, ship updates instantly" /></a>
|
|
6
6
|
|
|
7
7
|
<div align="center">
|
|
8
8
|
<p><b>Capgo</b>: push fixes to your Capacitor users in minutes, build signed iOS and Android apps without a Mac, and roll back in one click.</p>
|
|
9
9
|
<h2><a href="https://capgo.app/register/?ref=plugin_fast_sql">➡️ Get started for free</a></h2>
|
|
10
10
|
<p>14-day unlimited free trial. No credit card required</p>
|
|
11
|
-
<p><a href="https://capgo.app/consulting/?ref=plugin_fast_sql">Missing a feature? We'll build the plugin for you 💪</a></p>
|
|
12
11
|
</div>
|
|
13
12
|
|
|
13
|
+
> 🤖 **Building with an AI coding agent?** Install [Capgo skills](https://capgo.app/skills/?ref=plugin_fast_sql) so Claude Code, Codex, Cursor, Copilot, or Gemini CLI know how to install and use `@capgo/capacitor-fast-sql` in your Capacitor app: `npx skills add Cap-go/capgo-skills`. Then ask your agent: *"Add @capgo/capacitor-fast-sql to my Capacitor app and wire it up."* [Agent setup guides](https://capgo.app/agents/?ref=plugin_fast_sql) · [Hosted MCP](https://capgo.app/docs/ai/mcp/?ref=plugin_fast_sql)
|
|
14
|
+
|
|
14
15
|
<p align="center">
|
|
15
16
|
<img src="https://raw.githubusercontent.com/Cap-go/capacitor-fast-sql/main/assets/github-social-preview.png" alt="@capgo/capacitor-fast-sql for Capacitor apps" width="300" />
|
|
16
17
|
</p>
|
|
@@ -24,151 +25,151 @@ Run SQLite fast in your Capacitor app: queries go through a local HTTP channel i
|
|
|
24
25
|
- **Web**: `configureWeb()` sets up the official SQLite Wasm module, with OPFS persistence when your server allows it.
|
|
25
26
|
- **Platforms**: iOS, Android and Web. Native SQLite on iOS and Android, SQLite Wasm on web.
|
|
26
27
|
|
|
27
|
-
## Why Fast SQL?
|
|
28
|
-
|
|
29
|
-
Traditional Capacitor plugins serialize data through the JavaScript bridge, which becomes inefficient with large datasets. Fast SQL solves this by establishing a local HTTP server for direct native communication, making it ideal for:
|
|
30
|
-
|
|
31
|
-
- **Local-first sync systems** (CRDTs, operational transforms)
|
|
32
|
-
- **IndexedDB replacement** on platforms with broken/limited implementations
|
|
33
|
-
- **Large dataset operations** requiring high throughput
|
|
34
|
-
- **Batch operations** with thousands of rows
|
|
35
|
-
- **Binary data storage** (BLOBs, files)
|
|
36
|
-
|
|
37
|
-
## Documentation
|
|
38
|
-
|
|
39
|
-
The most complete doc is available here: https://capgo.app/docs/plugins/fast-sql/
|
|
40
|
-
|
|
41
|
-
## Compatibility
|
|
42
|
-
|
|
43
|
-
| Plugin version | Capacitor compatibility | Maintained |
|
|
44
|
-
| -------------- | ----------------------- | ---------- |
|
|
45
|
-
| v8.\*.\* | v8.\*.\* | ✅ |
|
|
46
|
-
| v7.\*.\* | v7.\*.\* | On demand |
|
|
47
|
-
| v6.\*.\* | v6.\*.\* | ❌ |
|
|
48
|
-
| v5.\*.\* | v5.\*.\* | ❌ |
|
|
49
|
-
|
|
50
|
-
> **Note:** The major version of this plugin follows the major version of Capacitor. Use the version that matches your Capacitor installation (e.g., plugin v8 for Capacitor 8). Only the latest major version is actively maintained.
|
|
51
|
-
|
|
52
|
-
## Install
|
|
53
|
-
|
|
54
|
-
You can use our AI-Assisted Setup to install the plugin. Add the Capgo skills to your AI tool using the following command:
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
npx skills add https://github.com/cap-go/capacitor-skills --skill capacitor-plugins
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Then use the following prompt:
|
|
61
|
-
|
|
62
|
-
```text
|
|
63
|
-
Use the `capacitor-plugins` skill from `cap-go/capacitor-skills` to install the `@capgo/capacitor-fast-sql` plugin in my project.
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
If you prefer Manual Setup, install the plugin by running the following commands and follow the platform-specific instructions below:
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
npm install @capgo/capacitor-fast-sql
|
|
70
|
-
npx cap sync
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## Overview
|
|
74
|
-
|
|
75
|
-
This plugin provides direct native SQLite database access with a custom communication protocol inspired by [capacitor-blob-writer](https://github.com/diachedelic/capacitor-blob-writer). Instead of using Capacitor's standard bridge (which serializes data inefficiently), it establishes a local HTTP server for optimal performance with large datasets and sync operations.
|
|
76
|
-
|
|
77
|
-
### Key Features
|
|
78
|
-
|
|
79
|
-
- **Custom HTTP Protocol**: Bypasses Capacitor's bridge for up to 25x faster performance with large data
|
|
80
|
-
- **Direct Native SQLite**: Full SQL support with transactions, batch operations, and binary data
|
|
81
|
-
- **Sync-Friendly**: Designed for local sync systems (CRDTs, operational transforms, etc.)
|
|
82
|
-
- **IndexedDB Replacement**: Provides reliable alternative to broken/limited IndexedDB implementations
|
|
28
|
+
## Why Fast SQL?
|
|
29
|
+
|
|
30
|
+
Traditional Capacitor plugins serialize data through the JavaScript bridge, which becomes inefficient with large datasets. Fast SQL solves this by establishing a local HTTP server for direct native communication, making it ideal for:
|
|
31
|
+
|
|
32
|
+
- **Local-first sync systems** (CRDTs, operational transforms)
|
|
33
|
+
- **IndexedDB replacement** on platforms with broken/limited implementations
|
|
34
|
+
- **Large dataset operations** requiring high throughput
|
|
35
|
+
- **Batch operations** with thousands of rows
|
|
36
|
+
- **Binary data storage** (BLOBs, files)
|
|
37
|
+
|
|
38
|
+
## Documentation
|
|
39
|
+
|
|
40
|
+
The most complete doc is available here: https://capgo.app/docs/plugins/fast-sql/
|
|
41
|
+
|
|
42
|
+
## Compatibility
|
|
43
|
+
|
|
44
|
+
| Plugin version | Capacitor compatibility | Maintained |
|
|
45
|
+
| -------------- | ----------------------- | ---------- |
|
|
46
|
+
| v8.\*.\* | v8.\*.\* | ✅ |
|
|
47
|
+
| v7.\*.\* | v7.\*.\* | On demand |
|
|
48
|
+
| v6.\*.\* | v6.\*.\* | ❌ |
|
|
49
|
+
| v5.\*.\* | v5.\*.\* | ❌ |
|
|
50
|
+
|
|
51
|
+
> **Note:** The major version of this plugin follows the major version of Capacitor. Use the version that matches your Capacitor installation (e.g., plugin v8 for Capacitor 8). Only the latest major version is actively maintained.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
You can use our AI-Assisted Setup to install the plugin. Add the Capgo skills to your AI tool using the following command:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx skills add https://github.com/cap-go/capacitor-skills --skill capacitor-plugins
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then use the following prompt:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
Use the `capacitor-plugins` skill from `cap-go/capacitor-skills` to install the `@capgo/capacitor-fast-sql` plugin in my project.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If you prefer Manual Setup, install the plugin by running the following commands and follow the platform-specific instructions below:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install @capgo/capacitor-fast-sql
|
|
71
|
+
npx cap sync
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Overview
|
|
75
|
+
|
|
76
|
+
This plugin provides direct native SQLite database access with a custom communication protocol inspired by [capacitor-blob-writer](https://github.com/diachedelic/capacitor-blob-writer). Instead of using Capacitor's standard bridge (which serializes data inefficiently), it establishes a local HTTP server for optimal performance with large datasets and sync operations.
|
|
77
|
+
|
|
78
|
+
### Key Features
|
|
79
|
+
|
|
80
|
+
- **Custom HTTP Protocol**: Bypasses Capacitor's bridge for up to 25x faster performance with large data
|
|
81
|
+
- **Direct Native SQLite**: Full SQL support with transactions, batch operations, and binary data
|
|
82
|
+
- **Sync-Friendly**: Designed for local sync systems (CRDTs, operational transforms, etc.)
|
|
83
|
+
- **IndexedDB Replacement**: Provides reliable alternative to broken/limited IndexedDB implementations
|
|
83
84
|
- **Cross-Platform**: iOS, Android, and Web (using [@sqlite.org/sqlite-wasm](https://github.com/sqlite/sqlite-wasm) with OPFS persistence)
|
|
84
|
-
- **Optional RAG VectorStore**: Persist many named vector stores for retrieval-augmented generation without adding a native dependency
|
|
85
|
-
|
|
86
|
-
## iOS Configuration
|
|
87
|
-
|
|
88
|
-
This plugin runs a local HTTP server on `localhost`. iOS App Transport Security (ATS) blocks cleartext HTTP by default, so you **must** allow local networking in your `Info.plist`:
|
|
89
|
-
|
|
90
|
-
```xml
|
|
91
|
-
<key>NSAppTransportSecurity</key>
|
|
92
|
-
<dict>
|
|
93
|
-
<key>NSAllowsLocalNetworking</key>
|
|
94
|
-
<true/>
|
|
95
|
-
</dict>
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
This only permits cleartext to loopback addresses (`localhost` / `127.0.0.1`), it does not weaken ATS for external connections.
|
|
99
|
-
|
|
100
|
-
## Android Configuration
|
|
101
|
-
|
|
102
|
-
This plugin runs a local HTTP server on `localhost` to bypass Capacitor's bridge for performance. Android 9+ blocks cleartext (non-HTTPS) traffic by default, so you **must** allow it for `localhost`.
|
|
103
|
-
|
|
104
|
-
**Option A, Scoped to localhost only (recommended):**
|
|
105
|
-
|
|
106
|
-
Create `android/app/src/main/res/xml/network_security_config.xml`:
|
|
107
|
-
|
|
108
|
-
```xml
|
|
109
|
-
<?xml version="1.0" encoding="utf-8"?>
|
|
110
|
-
<network-security-config>
|
|
111
|
-
<domain-config cleartextTrafficPermitted="true">
|
|
112
|
-
<domain includeSubdomains="false">localhost</domain>
|
|
113
|
-
<domain includeSubdomains="false">127.0.0.1</domain>
|
|
114
|
-
</domain-config>
|
|
115
|
-
</network-security-config>
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Then reference it in your `AndroidManifest.xml`:
|
|
119
|
-
|
|
120
|
-
```xml
|
|
121
|
-
<application
|
|
122
|
-
android:networkSecurityConfig="@xml/network_security_config">
|
|
123
|
-
...
|
|
124
|
-
</application>
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
**Option B, Allow all cleartext (simpler but less secure):**
|
|
128
|
-
|
|
129
|
-
```xml
|
|
130
|
-
<application
|
|
131
|
-
android:usesCleartextTraffic="true">
|
|
132
|
-
...
|
|
133
|
-
</application>
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
## Encryption
|
|
137
|
-
|
|
138
|
-
### Android
|
|
139
|
-
|
|
140
|
-
Encryption uses [SQLCipher](https://www.zetetic.net/sqlcipher/) and is opt-in. Add the SQLCipher dependency to your **app-level** `build.gradle`:
|
|
141
|
-
|
|
142
|
-
```gradle
|
|
143
|
-
dependencies {
|
|
144
|
-
implementation 'net.zetetic:sqlcipher-android:4.13.0'
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Then connect with encryption enabled:
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
const db = await FastSQL.connect({
|
|
152
|
-
database: 'secure',
|
|
153
|
-
encrypted: true,
|
|
154
|
-
encryptionKey: 'my-secret-key',
|
|
155
|
-
});
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
If SQLCipher is not installed and `encrypted: true` is passed, the plugin returns a clear error message instead of crashing.
|
|
159
|
-
|
|
160
|
-
### iOS
|
|
161
|
-
|
|
162
|
-
SQLCipher is optional on iOS. The plugin builds without it; encryption calls will return `Encryption is not available in this build` unless SQLCipher is installed.
|
|
163
|
-
|
|
164
|
-
To enable encryption via CocoaPods, update your `ios/App/Podfile` after `capacitor install`/`cap sync` to use the SQLCipher subspec:
|
|
165
|
-
|
|
166
|
-
```ruby
|
|
167
|
-
pod 'CapgoCapacitorFastSql/SQLCipher', :path => '../../node_modules/@capgo/capacitor-fast-sql'
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
Then run `pod install` in the `ios/App` directory. If you skip this subspec, keep `encrypted: false`.
|
|
171
|
-
|
|
85
|
+
- **Optional RAG VectorStore**: Persist many named vector stores for retrieval-augmented generation without adding a native dependency
|
|
86
|
+
|
|
87
|
+
## iOS Configuration
|
|
88
|
+
|
|
89
|
+
This plugin runs a local HTTP server on `localhost`. iOS App Transport Security (ATS) blocks cleartext HTTP by default, so you **must** allow local networking in your `Info.plist`:
|
|
90
|
+
|
|
91
|
+
```xml
|
|
92
|
+
<key>NSAppTransportSecurity</key>
|
|
93
|
+
<dict>
|
|
94
|
+
<key>NSAllowsLocalNetworking</key>
|
|
95
|
+
<true/>
|
|
96
|
+
</dict>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This only permits cleartext to loopback addresses (`localhost` / `127.0.0.1`), it does not weaken ATS for external connections.
|
|
100
|
+
|
|
101
|
+
## Android Configuration
|
|
102
|
+
|
|
103
|
+
This plugin runs a local HTTP server on `localhost` to bypass Capacitor's bridge for performance. Android 9+ blocks cleartext (non-HTTPS) traffic by default, so you **must** allow it for `localhost`.
|
|
104
|
+
|
|
105
|
+
**Option A, Scoped to localhost only (recommended):**
|
|
106
|
+
|
|
107
|
+
Create `android/app/src/main/res/xml/network_security_config.xml`:
|
|
108
|
+
|
|
109
|
+
```xml
|
|
110
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
111
|
+
<network-security-config>
|
|
112
|
+
<domain-config cleartextTrafficPermitted="true">
|
|
113
|
+
<domain includeSubdomains="false">localhost</domain>
|
|
114
|
+
<domain includeSubdomains="false">127.0.0.1</domain>
|
|
115
|
+
</domain-config>
|
|
116
|
+
</network-security-config>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Then reference it in your `AndroidManifest.xml`:
|
|
120
|
+
|
|
121
|
+
```xml
|
|
122
|
+
<application
|
|
123
|
+
android:networkSecurityConfig="@xml/network_security_config">
|
|
124
|
+
...
|
|
125
|
+
</application>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Option B, Allow all cleartext (simpler but less secure):**
|
|
129
|
+
|
|
130
|
+
```xml
|
|
131
|
+
<application
|
|
132
|
+
android:usesCleartextTraffic="true">
|
|
133
|
+
...
|
|
134
|
+
</application>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Encryption
|
|
138
|
+
|
|
139
|
+
### Android
|
|
140
|
+
|
|
141
|
+
Encryption uses [SQLCipher](https://www.zetetic.net/sqlcipher/) and is opt-in. Add the SQLCipher dependency to your **app-level** `build.gradle`:
|
|
142
|
+
|
|
143
|
+
```gradle
|
|
144
|
+
dependencies {
|
|
145
|
+
implementation 'net.zetetic:sqlcipher-android:4.13.0'
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Then connect with encryption enabled:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
const db = await FastSQL.connect({
|
|
153
|
+
database: 'secure',
|
|
154
|
+
encrypted: true,
|
|
155
|
+
encryptionKey: 'my-secret-key',
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
If SQLCipher is not installed and `encrypted: true` is passed, the plugin returns a clear error message instead of crashing.
|
|
160
|
+
|
|
161
|
+
### iOS
|
|
162
|
+
|
|
163
|
+
SQLCipher is optional on iOS. The plugin builds without it; encryption calls will return `Encryption is not available in this build` unless SQLCipher is installed.
|
|
164
|
+
|
|
165
|
+
To enable encryption via CocoaPods, update your `ios/App/Podfile` after `capacitor install`/`cap sync` to use the SQLCipher subspec:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
pod 'CapgoCapacitorFastSql/SQLCipher', :path => '../../node_modules/@capgo/capacitor-fast-sql'
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Then run `pod install` in the `ios/App` directory. If you skip this subspec, keep `encrypted: false`.
|
|
172
|
+
|
|
172
173
|
## Web Platform
|
|
173
174
|
|
|
174
175
|
On the web, this plugin uses the official [@sqlite.org/sqlite-wasm](https://github.com/sqlite/sqlite-wasm) build with [OPFS](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system) for persistence. SQLite reads and writes the database file directly, it is not loaded entirely into RAM or mirrored through IndexedDB.
|
|
@@ -213,147 +214,147 @@ const db = await FastSQL.connect({ database: 'myapp' });
|
|
|
213
214
|
`configureWeb()` is a no-op on iOS and Android, it is safe to call unconditionally.
|
|
214
215
|
|
|
215
216
|
> **Note:** Existing databases previously stored in IndexedDB by sql.js are not migrated automatically. Re-seed or import data after upgrading if needed.
|
|
216
|
-
|
|
217
|
-
## Usage
|
|
218
|
-
|
|
219
|
-
### Basic Example
|
|
220
|
-
|
|
221
|
-
```typescript
|
|
222
|
-
import { FastSQL } from '@capgo/capacitor-fast-sql';
|
|
223
|
-
|
|
224
|
-
// Connect to database
|
|
225
|
-
const db = await FastSQL.connect({ database: 'myapp' });
|
|
226
|
-
|
|
227
|
-
// Create table
|
|
228
|
-
await db.execute(`
|
|
229
|
-
CREATE TABLE IF NOT EXISTS users (
|
|
230
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
231
|
-
name TEXT NOT NULL,
|
|
232
|
-
email TEXT UNIQUE
|
|
233
|
-
)
|
|
234
|
-
`);
|
|
235
|
-
|
|
236
|
-
// Insert data
|
|
237
|
-
const result = await db.run(
|
|
238
|
-
'INSERT INTO users (name, email) VALUES (?, ?)',
|
|
239
|
-
['John Doe', 'john@example.com']
|
|
240
|
-
);
|
|
241
|
-
console.log('Inserted row ID:', result.insertId);
|
|
242
|
-
|
|
243
|
-
// Query data
|
|
244
|
-
const users = await db.query('SELECT * FROM users WHERE name LIKE ?', ['John%']);
|
|
245
|
-
console.log('Users:', users);
|
|
246
|
-
|
|
247
|
-
// Close connection
|
|
248
|
-
await FastSQL.disconnect('myapp');
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
### Transaction Example
|
|
252
|
-
|
|
253
|
-
```typescript
|
|
254
|
-
const db = await FastSQL.connect({ database: 'myapp' });
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
await db.transaction(async (tx) => {
|
|
258
|
-
await tx.run('INSERT INTO accounts (name, balance) VALUES (?, ?)', ['Alice', 1000]);
|
|
259
|
-
await tx.run('INSERT INTO accounts (name, balance) VALUES (?, ?)', ['Bob', 500]);
|
|
260
|
-
await tx.run('UPDATE accounts SET balance = balance - 100 WHERE name = ?', ['Alice']);
|
|
261
|
-
await tx.run('UPDATE accounts SET balance = balance + 100 WHERE name = ?', ['Bob']);
|
|
262
|
-
});
|
|
263
|
-
console.log('Transaction successful!');
|
|
264
|
-
} catch (error) {
|
|
265
|
-
console.error('Transaction failed:', error);
|
|
266
|
-
}
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
### Batch Operations
|
|
270
|
-
|
|
271
|
-
```typescript
|
|
272
|
-
const db = await FastSQL.connect({ database: 'myapp' });
|
|
273
|
-
|
|
274
|
-
const results = await db.executeBatch([
|
|
275
|
-
{ statement: 'INSERT INTO logs (message) VALUES (?)', params: ['Log 1'] },
|
|
276
|
-
{ statement: 'INSERT INTO logs (message) VALUES (?)', params: ['Log 2'] },
|
|
277
|
-
{ statement: 'INSERT INTO logs (message) VALUES (?)', params: ['Log 3'] },
|
|
278
|
-
]);
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
### Key-Value Storage (Mobile-focused)
|
|
282
|
-
|
|
283
|
-
```typescript
|
|
284
|
-
import { KeyValueStore } from '@capgo/capacitor-fast-sql';
|
|
285
|
-
|
|
286
|
-
const kv = await KeyValueStore.open({
|
|
287
|
-
database: 'myapp',
|
|
288
|
-
encrypted: true,
|
|
289
|
-
encryptionKey: 'super-secret-key',
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
await kv.set('session', { token: 'abc123', expiresAt: 1710000000 });
|
|
293
|
-
const session = await kv.get('session');
|
|
294
|
-
await kv.remove('session');
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
### RAG Vector Stores (Optional)
|
|
298
|
-
|
|
299
|
-
`FastSQLVectorStore` provides a dependency-free VectorStore-compatible helper inspired by `@react-native-rag/op-sqlite`. It stores embeddings in SQLite as binary Float32 vectors and computes cosine similarity in TypeScript, so it works anywhere this plugin works.
|
|
300
|
-
|
|
301
|
-
Each store is namespaced by the `store` option, so one database can hold as many independent vector stores as you need:
|
|
302
|
-
|
|
303
|
-
```typescript
|
|
304
|
-
import { FastSQLVectorStore } from '@capgo/capacitor-fast-sql';
|
|
305
|
-
|
|
306
|
-
const docs = await FastSQLVectorStore.open({
|
|
307
|
-
database: 'rag',
|
|
308
|
-
store: 'docs',
|
|
309
|
-
embeddings,
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
const tickets = await FastSQLVectorStore.open({
|
|
313
|
-
database: 'rag',
|
|
314
|
-
store: 'tickets',
|
|
315
|
-
embeddings,
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
await docs.add({
|
|
319
|
-
document: 'Capgo ships instant updates for Capacitor apps.',
|
|
320
|
-
metadata: { source: 'docs' },
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
const results = await docs.query({
|
|
324
|
-
queryText: 'How do instant updates work?',
|
|
325
|
-
nResults: 3,
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
await docs.close();
|
|
329
|
-
await tickets.close();
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
You can also use precomputed embeddings without an embeddings provider:
|
|
333
|
-
|
|
334
|
-
```typescript
|
|
335
|
-
const store = await FastSQLVectorStore.open({
|
|
336
|
-
database: 'rag',
|
|
337
|
-
store: 'offline-embeddings',
|
|
338
|
-
embeddingDimension: 384,
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
await store.add({
|
|
342
|
-
id: 'intro',
|
|
343
|
-
document: 'Local RAG entry',
|
|
344
|
-
embedding: [0.12, 0.08, 0.31 /* ...384 values */],
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
const matches = await store.query({
|
|
348
|
-
queryEmbedding: [0.11, 0.09, 0.29 /* ...384 values */],
|
|
349
|
-
nResults: 5,
|
|
350
|
-
});
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
> Note: Web support is intended for minimal testing only. The primary focus is iOS/Android.
|
|
354
|
-
|
|
355
|
-
## API
|
|
356
|
-
|
|
217
|
+
|
|
218
|
+
## Usage
|
|
219
|
+
|
|
220
|
+
### Basic Example
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { FastSQL } from '@capgo/capacitor-fast-sql';
|
|
224
|
+
|
|
225
|
+
// Connect to database
|
|
226
|
+
const db = await FastSQL.connect({ database: 'myapp' });
|
|
227
|
+
|
|
228
|
+
// Create table
|
|
229
|
+
await db.execute(`
|
|
230
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
231
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
232
|
+
name TEXT NOT NULL,
|
|
233
|
+
email TEXT UNIQUE
|
|
234
|
+
)
|
|
235
|
+
`);
|
|
236
|
+
|
|
237
|
+
// Insert data
|
|
238
|
+
const result = await db.run(
|
|
239
|
+
'INSERT INTO users (name, email) VALUES (?, ?)',
|
|
240
|
+
['John Doe', 'john@example.com']
|
|
241
|
+
);
|
|
242
|
+
console.log('Inserted row ID:', result.insertId);
|
|
243
|
+
|
|
244
|
+
// Query data
|
|
245
|
+
const users = await db.query('SELECT * FROM users WHERE name LIKE ?', ['John%']);
|
|
246
|
+
console.log('Users:', users);
|
|
247
|
+
|
|
248
|
+
// Close connection
|
|
249
|
+
await FastSQL.disconnect('myapp');
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Transaction Example
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
const db = await FastSQL.connect({ database: 'myapp' });
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
await db.transaction(async (tx) => {
|
|
259
|
+
await tx.run('INSERT INTO accounts (name, balance) VALUES (?, ?)', ['Alice', 1000]);
|
|
260
|
+
await tx.run('INSERT INTO accounts (name, balance) VALUES (?, ?)', ['Bob', 500]);
|
|
261
|
+
await tx.run('UPDATE accounts SET balance = balance - 100 WHERE name = ?', ['Alice']);
|
|
262
|
+
await tx.run('UPDATE accounts SET balance = balance + 100 WHERE name = ?', ['Bob']);
|
|
263
|
+
});
|
|
264
|
+
console.log('Transaction successful!');
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error('Transaction failed:', error);
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Batch Operations
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
const db = await FastSQL.connect({ database: 'myapp' });
|
|
274
|
+
|
|
275
|
+
const results = await db.executeBatch([
|
|
276
|
+
{ statement: 'INSERT INTO logs (message) VALUES (?)', params: ['Log 1'] },
|
|
277
|
+
{ statement: 'INSERT INTO logs (message) VALUES (?)', params: ['Log 2'] },
|
|
278
|
+
{ statement: 'INSERT INTO logs (message) VALUES (?)', params: ['Log 3'] },
|
|
279
|
+
]);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Key-Value Storage (Mobile-focused)
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import { KeyValueStore } from '@capgo/capacitor-fast-sql';
|
|
286
|
+
|
|
287
|
+
const kv = await KeyValueStore.open({
|
|
288
|
+
database: 'myapp',
|
|
289
|
+
encrypted: true,
|
|
290
|
+
encryptionKey: 'super-secret-key',
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await kv.set('session', { token: 'abc123', expiresAt: 1710000000 });
|
|
294
|
+
const session = await kv.get('session');
|
|
295
|
+
await kv.remove('session');
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### RAG Vector Stores (Optional)
|
|
299
|
+
|
|
300
|
+
`FastSQLVectorStore` provides a dependency-free VectorStore-compatible helper inspired by `@react-native-rag/op-sqlite`. It stores embeddings in SQLite as binary Float32 vectors and computes cosine similarity in TypeScript, so it works anywhere this plugin works.
|
|
301
|
+
|
|
302
|
+
Each store is namespaced by the `store` option, so one database can hold as many independent vector stores as you need:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
import { FastSQLVectorStore } from '@capgo/capacitor-fast-sql';
|
|
306
|
+
|
|
307
|
+
const docs = await FastSQLVectorStore.open({
|
|
308
|
+
database: 'rag',
|
|
309
|
+
store: 'docs',
|
|
310
|
+
embeddings,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const tickets = await FastSQLVectorStore.open({
|
|
314
|
+
database: 'rag',
|
|
315
|
+
store: 'tickets',
|
|
316
|
+
embeddings,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
await docs.add({
|
|
320
|
+
document: 'Capgo ships instant updates for Capacitor apps.',
|
|
321
|
+
metadata: { source: 'docs' },
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const results = await docs.query({
|
|
325
|
+
queryText: 'How do instant updates work?',
|
|
326
|
+
nResults: 3,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
await docs.close();
|
|
330
|
+
await tickets.close();
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
You can also use precomputed embeddings without an embeddings provider:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
const store = await FastSQLVectorStore.open({
|
|
337
|
+
database: 'rag',
|
|
338
|
+
store: 'offline-embeddings',
|
|
339
|
+
embeddingDimension: 384,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await store.add({
|
|
343
|
+
id: 'intro',
|
|
344
|
+
document: 'Local RAG entry',
|
|
345
|
+
embedding: [0.12, 0.08, 0.31 /* ...384 values */],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const matches = await store.query({
|
|
349
|
+
queryEmbedding: [0.11, 0.09, 0.29 /* ...384 values */],
|
|
350
|
+
nResults: 5,
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
> Note: Web support is intended for minimal testing only. The primary focus is iOS/Android.
|
|
355
|
+
|
|
356
|
+
## API
|
|
357
|
+
|
|
357
358
|
<docgen-index>
|
|
358
359
|
|
|
359
360
|
* [`connect(...)`](#connect)
|
|
@@ -369,8 +370,8 @@ const matches = await store.query({
|
|
|
369
370
|
* [Type Aliases](#type-aliases)
|
|
370
371
|
* [Enums](#enums)
|
|
371
372
|
|
|
372
|
-
</docgen-index>
|
|
373
|
-
|
|
373
|
+
</docgen-index>
|
|
374
|
+
|
|
374
375
|
<docgen-api>
|
|
375
376
|
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
376
377
|
|
|
@@ -698,4 +699,4 @@ Used to override the default SQLite Wasm worker.
|
|
|
698
699
|
| **`RepeatableRead`** | <code>'REPEATABLE READ'</code> |
|
|
699
700
|
| **`Serializable`** | <code>'SERIALIZABLE'</code> |
|
|
700
701
|
|
|
701
|
-
</docgen-api>
|
|
702
|
+
</docgen-api>
|
package/android/build.gradle
CHANGED
|
@@ -40,6 +40,9 @@ android {
|
|
|
40
40
|
sourceCompatibility JavaVersion.VERSION_21
|
|
41
41
|
targetCompatibility JavaVersion.VERSION_21
|
|
42
42
|
}
|
|
43
|
+
testOptions {
|
|
44
|
+
unitTests.includeAndroidResources = true
|
|
45
|
+
}
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
repositories {
|
|
@@ -52,10 +55,10 @@ dependencies {
|
|
|
52
55
|
implementation project(':capacitor-android')
|
|
53
56
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
54
57
|
implementation 'androidx.sqlite:sqlite:2.7.1'
|
|
55
|
-
implementation 'com.google.code.gson:gson:2.14.0'
|
|
56
58
|
implementation 'org.nanohttpd:nanohttpd:2.3.1'
|
|
57
59
|
compileOnly 'net.zetetic:sqlcipher-android:4.19.1'
|
|
58
60
|
testImplementation "junit:junit:$junitVersion"
|
|
61
|
+
testImplementation 'org.robolectric:robolectric:4.16'
|
|
59
62
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
60
63
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
61
64
|
}
|
|
@@ -19,7 +19,7 @@ import java.util.Map;
|
|
|
19
19
|
@CapacitorPlugin(name = "CapgoCapacitorFastSql")
|
|
20
20
|
public class CapgoCapacitorFastSqlPlugin extends Plugin {
|
|
21
21
|
|
|
22
|
-
private final String pluginVersion = "8.2.
|
|
22
|
+
private final String pluginVersion = "8.2.13";
|
|
23
23
|
|
|
24
24
|
private Map<String, DatabaseConnection> databases = new HashMap<>();
|
|
25
25
|
private SQLHTTPServer server;
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
package app.capgo.capacitor.fastsql;
|
|
2
|
+
|
|
3
|
+
import com.getcapacitor.JSArray;
|
|
4
|
+
import java.math.BigDecimal;
|
|
5
|
+
import java.util.ArrayList;
|
|
6
|
+
import java.util.Iterator;
|
|
7
|
+
import java.util.List;
|
|
8
|
+
import org.json.JSONArray;
|
|
9
|
+
import org.json.JSONException;
|
|
10
|
+
import org.json.JSONObject;
|
|
11
|
+
import org.json.JSONTokener;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Converts HTTP request JSON parameter arrays to {@link JSArray} values with the same
|
|
15
|
+
* semantics as the previous Gson-based parsing in {@link SQLHTTPServer}.
|
|
16
|
+
*/
|
|
17
|
+
final class HttpRequestJsonParams {
|
|
18
|
+
|
|
19
|
+
private static final int MAX_NUMBER_STRING_LENGTH = 10_000;
|
|
20
|
+
|
|
21
|
+
private HttpRequestJsonParams() {}
|
|
22
|
+
|
|
23
|
+
static void populateJsArrayFromParamsJson(JSONArray paramsJson, JSArray params) throws JSONException {
|
|
24
|
+
populateJsArrayFromParamsJson(paramsJson, params, null);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static void populateJsArrayFromParamsJson(JSONArray paramsJson, JSArray params, String rawParamsArrayJson) throws JSONException {
|
|
28
|
+
List<String> rawElements = rawParamsArrayJson != null ? splitTopLevelJsonArrayElements(rawParamsArrayJson) : null;
|
|
29
|
+
for (int i = 0; i < paramsJson.length(); i++) {
|
|
30
|
+
String rawElement = rawElements != null && i < rawElements.size() ? rawElements.get(i) : null;
|
|
31
|
+
Object converted =
|
|
32
|
+
rawElement != null ? convertRequestParamFromRawJson(rawElement, paramsJson.get(i)) : convertRequestParam(paramsJson.get(i));
|
|
33
|
+
if (converted != null) {
|
|
34
|
+
params.put(converted);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static String extractJsonArrayAfterKey(String json, String key) throws JSONException {
|
|
40
|
+
int i = skipWhitespace(json, 0);
|
|
41
|
+
if (i >= json.length() || json.charAt(i) != '{') {
|
|
42
|
+
throw new JSONException("Expected JSON object");
|
|
43
|
+
}
|
|
44
|
+
i++;
|
|
45
|
+
String found = null;
|
|
46
|
+
boolean sawMatchingKey = false;
|
|
47
|
+
while (i < json.length()) {
|
|
48
|
+
i = skipWhitespace(json, i);
|
|
49
|
+
if (json.charAt(i) == '}') {
|
|
50
|
+
if (found != null) {
|
|
51
|
+
return found;
|
|
52
|
+
}
|
|
53
|
+
if (sawMatchingKey) {
|
|
54
|
+
throw new JSONException("No array for key: " + key);
|
|
55
|
+
}
|
|
56
|
+
throw new JSONException("No key: " + key);
|
|
57
|
+
}
|
|
58
|
+
int keyEnd = endOfJsonString(json, i);
|
|
59
|
+
String memberKey = parseQuotedStringToken(json, i, keyEnd);
|
|
60
|
+
i = skipWhitespace(json, keyEnd);
|
|
61
|
+
if (json.charAt(i) != ':') {
|
|
62
|
+
throw new JSONException("Expected ':' after object key");
|
|
63
|
+
}
|
|
64
|
+
i = skipWhitespace(json, i + 1);
|
|
65
|
+
if (memberKey.equals(key)) {
|
|
66
|
+
sawMatchingKey = true;
|
|
67
|
+
i = skipWhitespace(json, i);
|
|
68
|
+
if (i < json.length() && json.charAt(i) == '[') {
|
|
69
|
+
int end = indexOfMatchingBracket(json, i, '[', ']');
|
|
70
|
+
found = json.substring(i, end + 1);
|
|
71
|
+
i = end + 1;
|
|
72
|
+
} else {
|
|
73
|
+
found = null;
|
|
74
|
+
i = endOfJsonValue(json, i);
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
i = endOfJsonValue(json, i);
|
|
78
|
+
}
|
|
79
|
+
i = skipWhitespace(json, i);
|
|
80
|
+
if (i < json.length() && json.charAt(i) == ',') {
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (found != null) {
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
87
|
+
if (sawMatchingKey) {
|
|
88
|
+
throw new JSONException("No array for key: " + key);
|
|
89
|
+
}
|
|
90
|
+
throw new JSONException("No key: " + key);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Converts one JSON param value for {@link JSArray#put(Object)}.
|
|
95
|
+
*
|
|
96
|
+
* @return converted value, or {@code null} to skip unsupported values (e.g. nested arrays)
|
|
97
|
+
*/
|
|
98
|
+
static Object convertRequestParam(Object param) {
|
|
99
|
+
if (param == JSONObject.NULL) {
|
|
100
|
+
return JSONObject.NULL;
|
|
101
|
+
}
|
|
102
|
+
if (param instanceof Boolean) {
|
|
103
|
+
return param;
|
|
104
|
+
}
|
|
105
|
+
if (param instanceof Number) {
|
|
106
|
+
return convertNumberLikeGson((Number) param);
|
|
107
|
+
}
|
|
108
|
+
if (param instanceof String) {
|
|
109
|
+
return param;
|
|
110
|
+
}
|
|
111
|
+
if (param instanceof JSONObject) {
|
|
112
|
+
JSONObject obj = (JSONObject) param;
|
|
113
|
+
JSONObject jsonObj = new JSONObject();
|
|
114
|
+
Iterator<String> keys = obj.keys();
|
|
115
|
+
while (keys.hasNext()) {
|
|
116
|
+
String key = keys.next();
|
|
117
|
+
try {
|
|
118
|
+
jsonObj.put(key, elementGetAsString(obj.get(key)));
|
|
119
|
+
} catch (JSONException e) {
|
|
120
|
+
throw new IllegalStateException(e);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return jsonObj;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
static Object convertRequestParamFromRawJson(String rawElement, Object parsedFallback) {
|
|
129
|
+
String raw = rawElement.trim();
|
|
130
|
+
if (raw.startsWith("{")) {
|
|
131
|
+
return convertFlatObjectParamFromRawJson(raw);
|
|
132
|
+
}
|
|
133
|
+
if (raw.startsWith("[")) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (raw.equals("null")) {
|
|
137
|
+
return JSONObject.NULL;
|
|
138
|
+
}
|
|
139
|
+
if (raw.equals("true") || raw.equals("false")) {
|
|
140
|
+
return Boolean.parseBoolean(raw);
|
|
141
|
+
}
|
|
142
|
+
if (raw.startsWith("\"")) {
|
|
143
|
+
try {
|
|
144
|
+
return new JSONTokener(raw).nextValue();
|
|
145
|
+
} catch (JSONException e) {
|
|
146
|
+
throw new IllegalStateException(e);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return convertNumberLikeGsonFromLiteral(raw);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Mirrors Gson {@code JsonElement#getAsString()} for object field values. */
|
|
153
|
+
static String elementGetAsString(Object value) {
|
|
154
|
+
if (value == null || value == JSONObject.NULL) {
|
|
155
|
+
throw new IllegalStateException("Cannot get string from null");
|
|
156
|
+
}
|
|
157
|
+
if (value instanceof String) {
|
|
158
|
+
return (String) value;
|
|
159
|
+
}
|
|
160
|
+
if (value instanceof Number) {
|
|
161
|
+
try {
|
|
162
|
+
return JSONObject.numberToString((Number) value);
|
|
163
|
+
} catch (JSONException e) {
|
|
164
|
+
throw new IllegalStateException(e);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (value instanceof Boolean) {
|
|
168
|
+
return value.toString();
|
|
169
|
+
}
|
|
170
|
+
if (value instanceof JSONObject || value instanceof JSONArray) {
|
|
171
|
+
throw new IllegalStateException("Cannot get string from non-primitive");
|
|
172
|
+
}
|
|
173
|
+
return String.valueOf(value);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static String elementGetAsStringFromRawToken(String token) {
|
|
177
|
+
String raw = token.trim();
|
|
178
|
+
if (raw.equals("null")) {
|
|
179
|
+
throw new IllegalStateException("Cannot get string from null");
|
|
180
|
+
}
|
|
181
|
+
if (raw.startsWith("{") || raw.startsWith("[")) {
|
|
182
|
+
throw new IllegalStateException("Cannot get string from non-primitive");
|
|
183
|
+
}
|
|
184
|
+
if (raw.startsWith("\"")) {
|
|
185
|
+
try {
|
|
186
|
+
Object value = new JSONTokener(raw).nextValue();
|
|
187
|
+
if (value instanceof String) {
|
|
188
|
+
return (String) value;
|
|
189
|
+
}
|
|
190
|
+
} catch (JSONException e) {
|
|
191
|
+
throw new IllegalStateException(e);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (raw.equals("true") || raw.equals("false")) {
|
|
195
|
+
return raw;
|
|
196
|
+
}
|
|
197
|
+
return raw;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Mirrors Gson {@code JsonPrimitive#getAsLong()} with {@code getAsDouble()} fallback on
|
|
202
|
+
* {@link NumberFormatException}, using LazilyParsedNumber-style parsing on the number's text.
|
|
203
|
+
*/
|
|
204
|
+
static Number convertNumberLikeGson(Number number) {
|
|
205
|
+
return convertNumberLikeGsonFromLiteral(numberToJsonNumberString(number));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
static Number convertNumberLikeGsonFromLiteral(String literal) {
|
|
209
|
+
try {
|
|
210
|
+
return gsonStyleLongFromNumberString(literal);
|
|
211
|
+
} catch (NumberFormatException e) {
|
|
212
|
+
return gsonStyleDoubleFromNumberString(literal, Double.parseDouble(literal));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
static long gsonStyleLongFromNumberString(String s) {
|
|
217
|
+
checkNumberStringLength(s);
|
|
218
|
+
try {
|
|
219
|
+
return Long.parseLong(s);
|
|
220
|
+
} catch (NumberFormatException e) {
|
|
221
|
+
return parseBigDecimalLikeGson(s).longValue();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
static double gsonStyleDoubleFromNumberString(String s, Number number) {
|
|
226
|
+
try {
|
|
227
|
+
return Double.parseDouble(s);
|
|
228
|
+
} catch (NumberFormatException e) {
|
|
229
|
+
return number.doubleValue();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
static String numberToJsonNumberString(Number number) {
|
|
234
|
+
if (number instanceof BigDecimal) {
|
|
235
|
+
return ((BigDecimal) number).toPlainString();
|
|
236
|
+
}
|
|
237
|
+
return number.toString();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
static JSONObject convertFlatObjectParamFromRawJson(String raw) {
|
|
241
|
+
JSONObject out = new JSONObject();
|
|
242
|
+
int i = skipWhitespace(raw, 1);
|
|
243
|
+
while (true) {
|
|
244
|
+
i = skipWhitespace(raw, i);
|
|
245
|
+
if (raw.charAt(i) == '}') {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
int keyEnd = endOfJsonString(raw, i);
|
|
249
|
+
String key;
|
|
250
|
+
try {
|
|
251
|
+
key = (String) new JSONTokener(raw.substring(i, keyEnd)).nextValue();
|
|
252
|
+
} catch (JSONException e) {
|
|
253
|
+
throw new IllegalStateException(e);
|
|
254
|
+
}
|
|
255
|
+
i = skipWhitespace(raw, keyEnd);
|
|
256
|
+
if (raw.charAt(i) != ':') {
|
|
257
|
+
throw new IllegalStateException("Expected ':' after object key");
|
|
258
|
+
}
|
|
259
|
+
i = skipWhitespace(raw, i + 1);
|
|
260
|
+
int valueEnd = endOfJsonValue(raw, i);
|
|
261
|
+
String valueToken = raw.substring(i, valueEnd);
|
|
262
|
+
try {
|
|
263
|
+
out.put(key, elementGetAsStringFromRawToken(valueToken));
|
|
264
|
+
} catch (JSONException e) {
|
|
265
|
+
throw new IllegalStateException(e);
|
|
266
|
+
}
|
|
267
|
+
i = skipWhitespace(raw, valueEnd);
|
|
268
|
+
if (i < raw.length() && raw.charAt(i) == ',') {
|
|
269
|
+
i++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
static List<String> splitTopLevelJsonArrayElements(String arrayJson) {
|
|
276
|
+
String trimmed = arrayJson.trim();
|
|
277
|
+
if (trimmed.length() < 2 || trimmed.charAt(0) != '[') {
|
|
278
|
+
throw new IllegalStateException("Not a JSON array");
|
|
279
|
+
}
|
|
280
|
+
List<String> elements = new ArrayList<>();
|
|
281
|
+
int i = skipWhitespace(trimmed, 1);
|
|
282
|
+
while (true) {
|
|
283
|
+
i = skipWhitespace(trimmed, i);
|
|
284
|
+
if (trimmed.charAt(i) == ']') {
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
int end = endOfJsonValue(trimmed, i);
|
|
288
|
+
elements.add(trimmed.substring(i, end));
|
|
289
|
+
i = skipWhitespace(trimmed, end);
|
|
290
|
+
if (trimmed.charAt(i) == ',') {
|
|
291
|
+
i++;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return elements;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
static int endOfJsonValue(String s, int start) {
|
|
298
|
+
start = skipWhitespace(s, start);
|
|
299
|
+
char c = s.charAt(start);
|
|
300
|
+
if (c == '"') {
|
|
301
|
+
return endOfJsonString(s, start);
|
|
302
|
+
}
|
|
303
|
+
if (c == '{') {
|
|
304
|
+
return indexOfMatchingBracket(s, start, '{', '}') + 1;
|
|
305
|
+
}
|
|
306
|
+
if (c == '[') {
|
|
307
|
+
return indexOfMatchingBracket(s, start, '[', ']') + 1;
|
|
308
|
+
}
|
|
309
|
+
if (s.startsWith("null", start)) {
|
|
310
|
+
return start + 4;
|
|
311
|
+
}
|
|
312
|
+
if (s.startsWith("true", start)) {
|
|
313
|
+
return start + 4;
|
|
314
|
+
}
|
|
315
|
+
if (s.startsWith("false", start)) {
|
|
316
|
+
return start + 5;
|
|
317
|
+
}
|
|
318
|
+
int i = start;
|
|
319
|
+
while (i < s.length()) {
|
|
320
|
+
char ch = s.charAt(i);
|
|
321
|
+
if (ch == ',' || ch == ']' || ch == '}' || Character.isWhitespace(ch)) {
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
i++;
|
|
325
|
+
}
|
|
326
|
+
return i;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
static int endOfJsonString(String s, int start) {
|
|
330
|
+
if (s.charAt(start) != '"') {
|
|
331
|
+
throw new IllegalStateException("Expected string");
|
|
332
|
+
}
|
|
333
|
+
boolean escape = false;
|
|
334
|
+
for (int i = start + 1; i < s.length(); i++) {
|
|
335
|
+
char c = s.charAt(i);
|
|
336
|
+
if (escape) {
|
|
337
|
+
escape = false;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (c == '\\') {
|
|
341
|
+
escape = true;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (c == '"') {
|
|
345
|
+
return i + 1;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
throw new IllegalStateException("Unterminated string");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
static int indexOfMatchingBracket(String s, int openIdx, char open, char close) {
|
|
352
|
+
int depth = 0;
|
|
353
|
+
boolean inString = false;
|
|
354
|
+
boolean escape = false;
|
|
355
|
+
for (int i = openIdx; i < s.length(); i++) {
|
|
356
|
+
char c = s.charAt(i);
|
|
357
|
+
if (escape) {
|
|
358
|
+
escape = false;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (inString && c == '\\') {
|
|
362
|
+
escape = true;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (c == '"') {
|
|
366
|
+
inString = !inString;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (inString) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (c == open) {
|
|
373
|
+
depth++;
|
|
374
|
+
} else if (c == close) {
|
|
375
|
+
depth--;
|
|
376
|
+
if (depth == 0) {
|
|
377
|
+
return i;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
throw new IllegalStateException("Unbalanced brackets");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
static int skipWhitespace(String s, int index) {
|
|
385
|
+
while (index < s.length() && Character.isWhitespace(s.charAt(index))) {
|
|
386
|
+
index++;
|
|
387
|
+
}
|
|
388
|
+
return index;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
static String parseQuotedStringToken(String json, int start, int end) throws JSONException {
|
|
392
|
+
try {
|
|
393
|
+
return (String) new JSONTokener(json.substring(start, end)).nextValue();
|
|
394
|
+
} catch (JSONException e) {
|
|
395
|
+
throw new JSONException("Invalid object key", e);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private static void checkNumberStringLength(String s) {
|
|
400
|
+
if (s.length() > MAX_NUMBER_STRING_LENGTH) {
|
|
401
|
+
throw new NumberFormatException("Number string too large");
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private static BigDecimal parseBigDecimalLikeGson(String s) {
|
|
406
|
+
checkNumberStringLength(s);
|
|
407
|
+
BigDecimal decimal = new BigDecimal(s);
|
|
408
|
+
if (Math.abs((long) decimal.scale()) >= 10_000) {
|
|
409
|
+
throw new NumberFormatException("Number has unsupported scale");
|
|
410
|
+
}
|
|
411
|
+
return decimal;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
@@ -2,16 +2,12 @@ package app.capgo.capacitor.fastsql;
|
|
|
2
2
|
|
|
3
3
|
import com.getcapacitor.JSArray;
|
|
4
4
|
import com.getcapacitor.JSObject;
|
|
5
|
-
import com.google.gson.Gson;
|
|
6
|
-
import com.google.gson.JsonArray;
|
|
7
|
-
import com.google.gson.JsonElement;
|
|
8
|
-
import com.google.gson.JsonObject;
|
|
9
|
-
import com.google.gson.JsonParser;
|
|
10
5
|
import fi.iki.elonen.NanoHTTPD; // Note: org.nanohttpd:nanohttpd:2.3.1 still uses fi.iki.elonen package
|
|
11
6
|
import java.io.DataInputStream;
|
|
12
7
|
import java.io.IOException;
|
|
13
8
|
import java.nio.charset.StandardCharsets;
|
|
14
9
|
import java.security.SecureRandom;
|
|
10
|
+
import java.util.List;
|
|
15
11
|
import java.util.Map;
|
|
16
12
|
import java.util.concurrent.ExecutionException;
|
|
17
13
|
import java.util.concurrent.ExecutorService;
|
|
@@ -29,7 +25,6 @@ public class SQLHTTPServer extends NanoHTTPD {
|
|
|
29
25
|
|
|
30
26
|
private final String token;
|
|
31
27
|
private final Map<String, DatabaseConnection> databases;
|
|
32
|
-
private final Gson gson = new Gson();
|
|
33
28
|
private final ExecutorService requestExecutor = Executors.newSingleThreadExecutor((runnable) -> {
|
|
34
29
|
Thread thread = new Thread(runnable);
|
|
35
30
|
thread.setName("FastSQL-DB");
|
|
@@ -120,37 +115,14 @@ public class SQLHTTPServer extends NanoHTTPD {
|
|
|
120
115
|
private Response handleExecute(IHTTPSession session, DatabaseConnection db) throws Exception {
|
|
121
116
|
// Read request body
|
|
122
117
|
String body = readRequestBody(session);
|
|
123
|
-
|
|
118
|
+
JSONObject request = new JSONObject(body);
|
|
124
119
|
|
|
125
|
-
String statement = request.
|
|
126
|
-
|
|
120
|
+
String statement = request.getString("statement");
|
|
121
|
+
JSONArray paramsJson = request.has("params") ? request.getJSONArray("params") : new JSONArray();
|
|
127
122
|
|
|
128
|
-
// Convert to JSArray
|
|
129
123
|
JSArray params = new JSArray();
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
params.put(JSONObject.NULL);
|
|
133
|
-
} else if (param.isJsonPrimitive()) {
|
|
134
|
-
if (param.getAsJsonPrimitive().isNumber()) {
|
|
135
|
-
try {
|
|
136
|
-
params.put(param.getAsLong());
|
|
137
|
-
} catch (NumberFormatException e) {
|
|
138
|
-
params.put(param.getAsDouble());
|
|
139
|
-
}
|
|
140
|
-
} else if (param.getAsJsonPrimitive().isBoolean()) {
|
|
141
|
-
params.put(param.getAsBoolean());
|
|
142
|
-
} else {
|
|
143
|
-
params.put(param.getAsString());
|
|
144
|
-
}
|
|
145
|
-
} else if (param.isJsonObject()) {
|
|
146
|
-
JsonObject obj = param.getAsJsonObject();
|
|
147
|
-
JSONObject jsonObj = new JSONObject();
|
|
148
|
-
for (String key : obj.keySet()) {
|
|
149
|
-
jsonObj.put(key, obj.get(key).getAsString());
|
|
150
|
-
}
|
|
151
|
-
params.put(jsonObj);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
124
|
+
String rawParamsArray = request.has("params") ? HttpRequestJsonParams.extractJsonArrayAfterKey(body, "params") : null;
|
|
125
|
+
HttpRequestJsonParams.populateJsArrayFromParamsJson(paramsJson, params, rawParamsArray);
|
|
154
126
|
|
|
155
127
|
// Execute query
|
|
156
128
|
JSObject result = db.execute(statement, params);
|
|
@@ -163,41 +135,21 @@ public class SQLHTTPServer extends NanoHTTPD {
|
|
|
163
135
|
private Response handleBatch(IHTTPSession session, DatabaseConnection db) throws Exception {
|
|
164
136
|
// Read request body
|
|
165
137
|
String body = readRequestBody(session);
|
|
166
|
-
|
|
167
|
-
|
|
138
|
+
JSONObject request = new JSONObject(body);
|
|
139
|
+
JSONArray operations = request.getJSONArray("operations");
|
|
168
140
|
|
|
169
141
|
JSONArray results = new JSONArray();
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
142
|
+
String rawOperationsArray = HttpRequestJsonParams.extractJsonArrayAfterKey(body, "operations");
|
|
143
|
+
List<String> rawOperations = HttpRequestJsonParams.splitTopLevelJsonArrayElements(rawOperationsArray);
|
|
144
|
+
for (int i = 0; i < operations.length(); i++) {
|
|
145
|
+
JSONObject operation = operations.getJSONObject(i);
|
|
146
|
+
String statement = operation.getString("statement");
|
|
147
|
+
JSONArray paramsJson = operation.has("params") ? operation.getJSONArray("params") : new JSONArray();
|
|
174
148
|
|
|
175
|
-
// Convert to JSArray
|
|
176
149
|
JSArray params = new JSArray();
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
} else if (param.isJsonPrimitive()) {
|
|
181
|
-
if (param.getAsJsonPrimitive().isNumber()) {
|
|
182
|
-
try {
|
|
183
|
-
params.put(param.getAsLong());
|
|
184
|
-
} catch (NumberFormatException e) {
|
|
185
|
-
params.put(param.getAsDouble());
|
|
186
|
-
}
|
|
187
|
-
} else if (param.getAsJsonPrimitive().isBoolean()) {
|
|
188
|
-
params.put(param.getAsBoolean());
|
|
189
|
-
} else {
|
|
190
|
-
params.put(param.getAsString());
|
|
191
|
-
}
|
|
192
|
-
} else if (param.isJsonObject()) {
|
|
193
|
-
JsonObject obj = param.getAsJsonObject();
|
|
194
|
-
JSONObject jsonObj = new JSONObject();
|
|
195
|
-
for (String key : obj.keySet()) {
|
|
196
|
-
jsonObj.put(key, obj.get(key).getAsString());
|
|
197
|
-
}
|
|
198
|
-
params.put(jsonObj);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
150
|
+
String rawOperation = rawOperations.get(i);
|
|
151
|
+
String rawParamsArray = operation.has("params") ? HttpRequestJsonParams.extractJsonArrayAfterKey(rawOperation, "params") : null;
|
|
152
|
+
HttpRequestJsonParams.populateJsArrayFromParamsJson(paramsJson, params, rawParamsArray);
|
|
201
153
|
|
|
202
154
|
JSObject result = db.execute(statement, params);
|
|
203
155
|
results.put(new JSONObject(result.toString()));
|
|
@@ -10,7 +10,7 @@ import SQLite3
|
|
|
10
10
|
*/
|
|
11
11
|
@objc(CapgoCapacitorFastSqlPlugin)
|
|
12
12
|
public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
13
|
-
private let pluginVersion: String = "8.2.
|
|
13
|
+
private let pluginVersion: String = "8.2.13"
|
|
14
14
|
public let identifier = "CapgoCapacitorFastSqlPlugin"
|
|
15
15
|
public let jsName = "CapgoCapacitorFastSql"
|
|
16
16
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -50,12 +50,11 @@ class SQLDatabase {
|
|
|
50
50
|
throw SQLError.encryptionKeyMissing
|
|
51
51
|
}
|
|
52
52
|
#if canImport(SQLCipher)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
let
|
|
57
|
-
|
|
58
|
-
}
|
|
53
|
+
// `PRAGMA key` is SQLCipher's SQL interface to sqlite3_key(). It avoids
|
|
54
|
+
// calling sqlite3_key() directly, which SQLCipher's headers only declare
|
|
55
|
+
// when SQLITE_HAS_CODEC is defined for the importing target.
|
|
56
|
+
let escapedKey = key.replacingOccurrences(of: "'", with: "''")
|
|
57
|
+
let keyResult = sqlite3_exec(db, "PRAGMA key = '\(escapedKey)'", nil, nil, nil)
|
|
59
58
|
if keyResult != SQLITE_OK {
|
|
60
59
|
let error = String(cString: sqlite3_errmsg(db))
|
|
61
60
|
throw SQLError.encryptionFailed(message: error)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-fast-sql",
|
|
3
|
-
"version": "8.2.
|
|
3
|
+
"version": "8.2.13",
|
|
4
4
|
"description": "High-performance native SQLite plugin with custom protocol for efficient sync operations and IndexedDB replacement",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"rimraf": "^6.1.0",
|
|
82
82
|
"rollup": "^4.53.2",
|
|
83
83
|
"swiftlint": "^2.0.0",
|
|
84
|
-
"typescript": "^
|
|
84
|
+
"typescript": "^6.0.3"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"@capacitor/core": ">=8.0.0"
|