Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
<ActionBar title="nativescript-sqlite" class="action-bar"> </ActionBar>
<StackLayout class="p-20">
<ScrollView class="h-full">
<StackLayout>
<Button text="Test nativescript-sqlite" (tap)="demoShared.testIt()" class="btn btn-primary"></Button>
<!-- Rotating square — stays smooth as long as the UI thread is free -->
<StackLayout class="h-full" orientation="vertical">
<StackLayout orientation="horizontal" horizontalAlignment="center" class="m-b-10 p-10">
<StackLayout #spinner width="60" height="60" backgroundColor="#3d5afe" borderRadius="8" class="m-r-10 m-10"></StackLayout>
<Label text="UI thread free ✓" verticalAlignment="center" fontSize="14" color="#3d5afe"></Label>
</StackLayout>
</ScrollView>
<ScrollView>
<StackLayout>
<Label text="─── Tests ───" class="h3 text-center m-b-10"></Label>
<Button text="Run All Tests" (tap)="demoShared.testAll()" class="btn btn-primary m-b-5"></Button>
<Button text="CRUD" (tap)="demoShared.testCRUD()" class="btn btn-outline m-b-5"></Button>
<Button text="Data Types (NULL/INT/REAL/TEXT/BLOB)" (tap)="demoShared.testDataTypes()" class="btn btn-outline m-b-5"></Button>
<Button text="Parameter Binding (positional &amp; named)" (tap)="demoShared.testParams()" class="btn btn-outline m-b-5"></Button>
<Button text="selectArray / getArray" (tap)="demoShared.testSelectArray()" class="btn btn-outline m-b-5"></Button>
<Button text="Transactions &amp; Savepoints" (tap)="demoShared.testTransactions()" class="btn btn-outline m-b-5"></Button>
<Button text="Prepared Statements" (tap)="demoShared.testPreparedStatements()" class="btn btn-outline m-b-5"></Button>
<Button text="Sync API" (tap)="demoShared.testSyncAPI()" class="btn btn-outline m-b-5"></Button>
<Button text="Error Handling" (tap)="demoShared.testErrorHandling()" class="btn btn-outline m-b-5"></Button>
<Button text="getRuntimeInfo" (tap)="demoShared.testRuntimeInfo()" class="btn btn-outline m-b-5"></Button>
<Button text="In-Memory Database" (tap)="demoShared.testInMemoryDB()" class="btn btn-outline m-b-5"></Button>
<Button text="Low-Level Transaction API" (tap)="demoShared.testLowLevelTransactions()" class="btn btn-outline m-b-5"></Button>
<Button text="SQLCipher (Android optional dep)" (tap)="demoShared.testSQLCipher()" class="btn btn-outline m-b-5"></Button>

<Label text="─── Benchmarks ───" class="h3 text-center m-t-20 m-b-10"></Label>
<Button text="Run All Benchmarks" (tap)="demoShared.benchmarkAll()" class="btn btn-primary m-b-5"></Button>
<Button text="Bulk Insert (autocommit vs tx vs prepared)" (tap)="demoShared.benchmarkBulkInsert()" class="btn btn-outline m-b-5"></Button>
<Button text="Bulk Select (object vs array vs sync)" (tap)="demoShared.benchmarkBulkSelect()" class="btn btn-outline m-b-5"></Button>
<Button text="Concurrent Reads (pool)" (tap)="demoShared.benchmarkConcurrentReads()" class="btn btn-outline m-b-5"></Button>
<Button text="Prepared vs Direct" (tap)="demoShared.benchmarkPreparedVsDirect()" class="btn btn-outline m-b-5"></Button>
</StackLayout>
</ScrollView>
</StackLayout>
</StackLayout>
Original file line number Diff line number Diff line change
@@ -1,17 +1,47 @@
import { Component, NgZone } from '@angular/core';
import { Component, ElementRef, NgZone, ViewChild, OnInit, OnDestroy } from '@angular/core';
import { DemoSharedNativescriptSqlite } from '@demo/shared';
import {} from '@edusperoni/nativescript-sqlite';
import { StackLayout } from '@nativescript/core';

@Component({
selector: 'demo-nativescript-sqlite',
standalone: false,
templateUrl: 'nativescript-sqlite.component.html',
})
export class NativescriptSqliteComponent {
export class NativescriptSqliteComponent implements OnInit, OnDestroy {
demoShared: DemoSharedNativescriptSqlite;
@ViewChild('spinner', { static: false }) spinnerRef: ElementRef;

private _spinning = true;

constructor(private _ngZone: NgZone) {}

ngOnInit() {
this.demoShared = new DemoSharedNativescriptSqlite();
}

ngAfterViewInit() {
this._spin();
}

ngOnDestroy() {
this._spinning = false;
}

private _spin() {
if (!this._spinning) return;
const view: StackLayout = this.spinnerRef?.nativeElement;
if (!view) return;
let last: number | null = null;
const step = (ts: number) => {
if (!this._spinning) return;
if (last !== null) {
const delta = ts - last;
view.rotate = (view.rotate + (delta / 800) * 360) % 360;
}
last = ts;
requestAnimationFrame(step);
};
requestAnimationFrame(step);
}
}
2 changes: 1 addition & 1 deletion apps/demo-angular/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"@edusperoni/nativescript-sms-inbox": ["packages/nativescript-sms-inbox/index.d.ts"],
"~/*": ["apps/demo-angular/src/*"],
"@edusperoni/nativescript-supabase": ["../../packages/nativescript-supabase/index.ts"],
"@edusperoni/nativescript-sqlite": ["../../packages/nativescript-sqlite/index.d.ts"]
"@edusperoni/nativescript-sqlite": ["packages/nativescript-sqlite/index.android.ts"]
}
},
"files": ["./references.d.ts", "./src/main.ts", "./src/polyfills.ts"],
Expand Down
23 changes: 21 additions & 2 deletions apps/demo/src/plugin-demos/nativescript-sqlite.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,28 @@
<StackLayout class="p-20">
<ScrollView class="h-full">
<StackLayout>
<Button text="Test nativescript-sqlite" tap="{{ testIt }}" class="btn btn-primary"/>
<Label text="─── Tests ───" class="h3 text-center m-b-10"/>
<Button text="Run All Tests" tap="{{ testAll }}" class="btn btn-primary m-b-5"/>
<Button text="CRUD" tap="{{ testCRUD }}" class="btn btn-outline m-b-5"/>
<Button text="Data Types (NULL/INT/REAL/TEXT/BLOB)" tap="{{ testDataTypes }}" class="btn btn-outline m-b-5"/>
<Button text="Parameter Binding (positional &amp; named)" tap="{{ testParams }}" class="btn btn-outline m-b-5"/>
<Button text="selectArray / getArray" tap="{{ testSelectArray }}" class="btn btn-outline m-b-5"/>
<Button text="Transactions &amp; Savepoints" tap="{{ testTransactions }}" class="btn btn-outline m-b-5"/>
<Button text="Prepared Statements" tap="{{ testPreparedStatements }}" class="btn btn-outline m-b-5"/>
<Button text="Sync API" tap="{{ testSyncAPI }}" class="btn btn-outline m-b-5"/>
<Button text="Error Handling" tap="{{ testErrorHandling }}" class="btn btn-outline m-b-5"/>
<Button text="getRuntimeInfo" tap="{{ testRuntimeInfo }}" class="btn btn-outline m-b-5"/>
<Button text="In-Memory Database" tap="{{ testInMemoryDB }}" class="btn btn-outline m-b-5"/>
<Button text="Low-Level Transaction API" tap="{{ testLowLevelTransactions }}" class="btn btn-outline m-b-5"/>
<Button text="SQLCipher (Android optional dep)" tap="{{ testSQLCipher }}" class="btn btn-outline m-b-5"/>

<Label text="─── Benchmarks ───" class="h3 text-center m-t-20 m-b-10"/>
<Button text="Run All Benchmarks" tap="{{ benchmarkAll }}" class="btn btn-primary m-b-5"/>
<Button text="Bulk Insert (autocommit vs tx vs prepared)" tap="{{ benchmarkBulkInsert }}" class="btn btn-outline m-b-5"/>
<Button text="Bulk Select (object vs array vs sync)" tap="{{ benchmarkBulkSelect }}" class="btn btn-outline m-b-5"/>
<Button text="Concurrent Reads (pool)" tap="{{ benchmarkConcurrentReads }}" class="btn btn-outline m-b-5"/>
<Button text="Prepared vs Direct" tap="{{ benchmarkPreparedVsDirect }}" class="btn btn-outline m-b-5"/>
</StackLayout>
</ScrollView>
</StackLayout>
</Page>
</Page>
94 changes: 73 additions & 21 deletions packages/nativescript-sqlite/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A high-performance SQLite plugin for NativeScript. All database operations run on background threads via GCD — the JavaScript thread is never blocked.

**Platform support:** iOS (Android planned)
**Platform support:** iOS, Android

## Features

Expand All @@ -13,7 +13,8 @@ A high-performance SQLite plugin for NativeScript. All database operations run o
- Prepared statements
- Two result formats: objects (`select`) or columnar arrays (`selectArray`)
- Synchronous API available for simple use cases (migrations, setup)
- Custom SQLite builds supported via CocoaPods
- Custom SQLite builds supported (CocoaPods on iOS, Gradle on Android)
- Optional SQLCipher encryption on both platforms
- Drizzle ORM driver included

## Installation
Expand All @@ -22,13 +23,15 @@ A high-performance SQLite plugin for NativeScript. All database operations run o
npm install @edusperoni/nativescript-sqlite
```

The plugin does not bundle SQLite — you must link one. For most apps, add to `App_Resources/iOS/build.xcconfig`:
**iOS:** The plugin does not bundle SQLite — you must link one. For most apps add to `App_Resources/iOS/build.xcconfig`:

```
OTHER_LDFLAGS = $(inherited) -lsqlite3
```

For other options (custom builds, SQLCipher encryption), see [SQLite Linking](#sqlite-linking).
**Android:** The Android SDK ships with SQLite built-in — no extra setup needed. For encryption see [Android SQLite Setup](#android-sqlite-setup).

For all linking options see [iOS SQLite Linking](#ios-sqlite-linking) and [Android SQLite Setup](#android-sqlite-setup).

## Quick Start

Expand Down Expand Up @@ -83,12 +86,12 @@ const db = openDatabase({
});
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `path` | `string` | *required* | Full path to the database file, or `":memory:"` |
| `readOnly` | `boolean` | `false` | Open in read-only mode |
| `poolSize` | `number` | `4` | Number of reader connections in the pool |
| `busyTimeout` | `number` | `5000` | Busy timeout in milliseconds |
| Option | Type | Default | Description |
| --------------- | ----------- | ------------ | ------------------------------------------------- |
| `path` | `string` | *required* | Full path to the database file, or `":memory:"` |
| `readOnly` | `boolean` | `false` | Open in read-only mode |
| `poolSize` | `number` | `4` | Number of reader connections in the pool |
| `busyTimeout` | `number` | `5000` | Busy timeout in milliseconds |

### SQLiteDatabase

Expand Down Expand Up @@ -161,13 +164,13 @@ The prefix (`:`, `$`, `@`) is added automatically if omitted — you can pass `{

**Supported value types:**

| JS Type | SQLite Type |
|---------|-------------|
| `string` | TEXT |
| `number` | INTEGER or REAL (auto-detected) |
| `boolean` | INTEGER (0 or 1) |
| `null` | NULL |
| `ArrayBuffer` | BLOB |
| JS Type | SQLite Type |
| --------------- | ------------------------------- |
| `string` | TEXT |
| `number` | INTEGER or REAL (auto-detected) |
| `boolean` | INTEGER (0 or 1) |
| `null` | NULL |
| `ArrayBuffer` | BLOB |

### Transactions

Expand Down Expand Up @@ -345,14 +348,15 @@ WAL (Write-Ahead Logging) mode is enabled automatically on the writer. WAL allow

### Performance

- All SQLite work (prepare, bind, step, column extraction) happens on background GCD threads.
- All SQLite work (prepare, bind, step, column extraction) happens on background threads.
- Results are serialized to a JSON string on the background thread. Only one value (the string) crosses the native-to-JS bridge. `JSON.parse` in V8 is highly optimized native C++ code.
- Blob columns are returned as separate `NSData` objects and converted to `ArrayBuffer` via `interop.bufferFromData` (no extra copy).
- Blob columns are returned separately (iOS: `NSData``ArrayBuffer` via `interop.bufferFromData`; Android: `byte[]`→`ArrayBuffer` by direct copy) and re-inserted into the parsed objects before the Promise resolves.
- The `selectArray` / `getArray` format avoids repeating column names per row, reducing both serialization cost and memory usage for large result sets.
- Named parameters (`:name`, `$name`, `@name`) are bound natively on both platforms via `sqlite3_bind_parameter_index`. The prefix is added automatically if omitted — pass `{ name: 'Alice' }` and the binding adds the `:` before looking up the parameter index.

## SQLite Linking
## iOS SQLite Linking

The plugin does **not** bundle or link a SQLite library — you must provide one. This gives you full control over the SQLite version and features available. Add **one** of the following to your app:
The plugin does **not** bundle or link a SQLite library on iOS — you must provide one. This gives you full control over the SQLite version and features available. Add **one** of the following to your app:

### Option A: System SQLite (simplest)

Expand Down Expand Up @@ -393,6 +397,54 @@ const db = openDatabase({

Every connection in the pool (writer, readers, sync) automatically receives the key via `PRAGMA key` after opening. If the key is wrong or missing for an encrypted database, operations will fail with `SQLITE_NOTADB`.

## Android SQLite Setup

This package uses a 100% native C++ architecture on Android and provides three ways to link SQLite. Configure via `App_Resources/Android/gradle.properties`.

### Option A: Bundled SQLite (Default)

By default, the plugin compiles and statically links its own bundled copy of the SQLite C amalgamation (`sqlite3.c`). This ensures a consistent, up-to-date version across all Android devices regardless of OS version.

You can pass extra C compile flags to the bundled build:

```properties
# App_Resources/Android/gradle.properties
nscsqlite.sqliteImpl=bundled
nscsqlite.sqliteFlags=-DSQLITE_ENABLE_FTS5;-DSQLITE_ENABLE_JSON1
```

### Option B: SQLCipher (Encryption)

The plugin ships a bundled SQLCipher amalgamation that provides transparent AES-256-CBC encryption via OpenSSL. Selecting this option statically links SQLCipher instead of plain SQLite — no separate `.so` or pod required.

```properties
# App_Resources/Android/gradle.properties
nscsqlite.sqliteImpl=sqlcipher
```

Then pass an encryption key when opening the database:

```typescript
const db = openDatabase({
path: knownFolders.documents().path + '/encrypted.sqlite',
encryptionKey: 'my-secret-passphrase',
});
```

Every connection in the pool automatically receives the key via `PRAGMA key` after opening. If the key is wrong or missing for an encrypted database, operations fail with `SQLITE_NOTADB`.

### Option C: Custom Build

Link against any pre-built SQLite-compatible shared library (`.so`) you supply — useful for a heavily customized SQLite build or a third-party distribution. Place ABI subdirectories (`arm64-v8a/`, `x86_64/`, etc.) under `sqliteLibDir`.

```properties
# App_Resources/Android/gradle.properties
nscsqlite.sqliteImpl=custom
nscsqlite.sqliteLibName=sqlite3
nscsqlite.sqliteIncludeDir=/path/to/include
nscsqlite.sqliteLibDir=/path/to/libs
```

## Type Definitions

```typescript
Expand Down
59 changes: 59 additions & 0 deletions packages/nativescript-sqlite/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,65 @@ export interface DatabaseOptions {
encryptionKey?: string;
}

export interface RuntimeInfo {
version: string;
sourceId: string;
compileOptions: string[];
}

export interface ReadTransaction {
select<T extends SQLiteRow = SQLiteRow>(sql: string, params?: SQLiteParams): Promise<T[]>;
selectArray<T extends SQLiteValue[] = SQLiteValue[]>(sql: string, params?: SQLiteParams): Promise<SQLiteArrayResult<T>>;
get<T extends SQLiteRow = SQLiteRow>(sql: string, params?: SQLiteParams): Promise<T | undefined>;
getArray<T extends SQLiteValue[] = SQLiteValue[]>(sql: string, params?: SQLiteParams): Promise<SQLiteArrayResult<T>>;
}

export interface Transaction extends ReadTransaction {
execute(sql: string, params?: SQLiteParams): Promise<void>;
savepoint<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
}

export interface PreparedStatement {
execute(params?: SQLiteParams): Promise<void>;
select<T extends SQLiteRow = SQLiteRow>(params?: SQLiteParams): Promise<T[]>;
selectArray<T extends SQLiteValue[] = SQLiteValue[]>(params?: SQLiteParams): Promise<SQLiteArrayResult<T>>;
get<T extends SQLiteRow = SQLiteRow>(params?: SQLiteParams): Promise<T | undefined>;
getArray<T extends SQLiteValue[] = SQLiteValue[]>(params?: SQLiteParams): Promise<SQLiteArrayResult<T>>;
finalize(): Promise<void>;
}

export interface SQLiteDatabase {
execute(sql: string, params?: SQLiteParams): Promise<void>;
select<T extends SQLiteRow = SQLiteRow>(sql: string, params?: SQLiteParams): Promise<T[]>;
selectArray<T extends SQLiteValue[] = SQLiteValue[]>(sql: string, params?: SQLiteParams): Promise<SQLiteArrayResult<T>>;
get<T extends SQLiteRow = SQLiteRow>(sql: string, params?: SQLiteParams): Promise<T | undefined>;
getArray<T extends SQLiteValue[] = SQLiteValue[]>(sql: string, params?: SQLiteParams): Promise<SQLiteArrayResult<T>>;

transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
readTransaction<T>(fn: (tx: ReadTransaction) => Promise<T>): Promise<T>;

prepare(sql: string): Promise<PreparedStatement>;

executeSync(sql: string, params?: SQLiteParams): void;
selectSync<T extends SQLiteRow = SQLiteRow>(sql: string, params?: SQLiteParams): T[];
selectArraySync<T extends SQLiteValue[] = SQLiteValue[]>(sql: string, params?: SQLiteParams): SQLiteArrayResult<T>;
getSync<T extends SQLiteRow = SQLiteRow>(sql: string, params?: SQLiteParams): T | undefined;
getArraySync<T extends SQLiteValue[] = SQLiteValue[]>(sql: string, params?: SQLiteParams): SQLiteArrayResult<T>;

// Low-level transaction control (for driver integrations like drizzle)
beginTransaction(behavior?: 'deferred' | 'immediate' | 'exclusive'): Promise<number>;
executeInTransaction(txId: number, sql: string, params?: SQLiteParams): Promise<void>;
selectInTransaction(txId: number, sql: string, params?: SQLiteParams): Promise<SQLiteRow[]>;
selectArrayInTransaction(txId: number, sql: string, params?: SQLiteParams): Promise<SQLiteArrayResult>;
commitTransaction(txId: number): Promise<void>;
rollbackTransaction(txId: number): Promise<void>;

getRuntimeInfo(): RuntimeInfo;

close(): Promise<void>;
readonly isOpen: boolean;
}

export class SQLiteError extends Error {
constructor(
message: string,
Expand Down
Loading