Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ https://discord.gg/W9XmqCQCKP

Some of the big supported features:

- iOS, Android, macOS and web support
- Vanilla sqlite
- Libsql is supported as a compilation target
- SQLCipher is supported as a compilation target
Expand Down
22 changes: 22 additions & 0 deletions docs/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ export const db = open({
});
```

### Open Async (Web)

On web, opening is async-only and requires OPFS.

```tsx
import { openAsync } from '@op-engineering/op-sqlite';

export const db = await openAsync({
name: 'myDb.sqlite',
});
```

On web, `open()` intentionally throws. Use `openAsync()`.

### SQLCipher Open

If you are using SQLCipher all the methods are the same with the exception of the open method which needs an extra `encryptionKey` to encrypt/decrypt the database.
Expand Down Expand Up @@ -51,6 +65,12 @@ try {
}
```

### Web note

On web, `execute()` runs the full SQL string passed to it.
On native, `execute()` currently runs only the first prepared statement.
If you need identical behavior across platforms, avoid multi-statement SQL strings.

### Execute with Host Objects

It’s possible to return HostObjects when using a query. The benefit is that HostObjects are only created in C++ and only when you try to access a value inside of them a C++ value → JS value conversion happens. This means creation is fast, property access is slow. The use case is clear if you are returning **massive** amount of objects but only displaying/accessing a few of them at the time.
Expand Down Expand Up @@ -109,6 +129,8 @@ You can do sync queries via the `executeSync` functions. Not available in transa
let res = db.executeSync('SELECT 1');
```

On web, sync APIs intentionally throw. Use async methods only.

## Transactions

Wraps the code inside in a transaction. Any error thrown inside of the transaction body function will ROLLBACK the transaction.
Expand Down
27 changes: 26 additions & 1 deletion docs/docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,32 @@ npx expo install @op-engineering/op-sqlite
npx expo prebuild --clean
```

This package only runs on `iOS`, `Android` and `macOS`.
This package runs on `iOS`, `Android`, `macOS` and `web`.

## Web requirements

Web support is async-only and uses the sqlite wasm worker API with OPFS persistence.

`@sqlite.org/sqlite-wasm` is an optional dependency. You only need to install it if you use op-sqlite on web.

```bash
yarn add @sqlite.org/sqlite-wasm
```

If your app does not target web, you do not need to install this package.

Required runtime behavior on web:

- Use `openAsync()` to open the database.
- Use async DB methods such as `execute()` and `closeAsync()`.
- Synchronous APIs (for example `open()` and `executeSync()`) intentionally throw on web.
- SQLCipher and libsql are not supported on web. As well as loading extensions.
- OPFS is required for the web backend. If OPFS is unavailable, `openAsync()` throws.

Required response headers (for worker/OPFS setup):

- `Cross-Origin-Opener-Policy: same-origin`
- `Cross-Origin-Embedder-Policy: require-corp`

# Configuration

Expand Down
1 change: 1 addition & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ yarn-error.log

# Bundle artifact
*.jsbundle
web-build/

# Ruby / CocoaPods
/ios/Pods/
Expand Down
15 changes: 15 additions & 0 deletions example/index.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { AppRegistry } from "react-native";
import { name as appName } from "./app.json";
import App from "./src/AppWeb";

AppRegistry.registerComponent(appName, () => App);

const rootTag = (globalThis as any).document?.getElementById("root");
if (!rootTag) {
throw new Error("Root element not found");
}

AppRegistry.runApplication(appName, {
rootTag,
initialProps: {},
});
14 changes: 11 additions & 3 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,22 @@
"pods:nuke": "cd ios && rm -rf Pods && rm -rf Podfile.lock && bundle exec pod install",
"run:android:release": "cd android && ./gradlew assembleRelease && adb install -r app/build/outputs/apk/release/app-release.apk && adb shell am start -n com.op.sqlite.example/.MainActivity",
"build:android": "cd android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a",
"build:ios": "cd ios && xcodebuild -workspace OPSQLiteExample.xcworkspace -scheme debug -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO"
"build:ios": "cd ios && xcodebuild -workspace OPSQLiteExample.xcworkspace -scheme debug -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO",
"web": "vite",
"web:build": "vite build",
"web:preview": "vite preview"
},
"dependencies": {
"@op-engineering/op-test": "^0.2.5",
"@sqlite.org/sqlite-wasm": "^3.51.2-build8",
"chance": "^1.1.9",
"clsx": "^2.0.0",
"events": "^3.3.0",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-native": "0.82.1",
"react-native-safe-area-context": "^5.6.2"
"react-native-safe-area-context": "^5.6.2",
"react-native-web": "^0.21.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand All @@ -36,11 +42,13 @@
"@react-native/typescript-config": "0.81.5",
"@types/chance": "^1.1.7",
"@types/react": "^19.1.1",
"@vitejs/plugin-react": "^5.1.0",
"patch-package": "^8.0.1",
"react-native-builder-bob": "^0.40.13",
"react-native-monorepo-config": "^0.1.9",
"react-native-restart": "^0.0.27",
"tailwindcss": "3.3.2"
"tailwindcss": "3.3.2",
"vite": "^7.1.9"
},
"engines": {
"node": ">=18"
Expand Down
184 changes: 92 additions & 92 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,108 +1,108 @@
import {useEffect, useState} from 'react';
import {
displayResults,
runTests,
allTestsPassed,
} from '@op-engineering/op-test';
import './tests'; // import all tests to register them
import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context';
allTestsPassed,
displayResults,
runTests,
} from "@op-engineering/op-test";
import { useEffect, useState } from "react";
import "./tests"; // import all tests to register them
// import {performanceTest} from './performance_test';
import {StyleSheet, Text, View} from 'react-native';
import { StyleSheet, Text, View } from "react-native";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
// import {open} from '@op-engineering/op-sqlite';

export default function App() {
const [results, setResults] = useState<any>(null);
const [perfResult, setPerfResult] = useState<number>(0);
const [openTime, setOpenTime] = useState(0);
const [results, setResults] = useState<any>(null);
const [perfResult, setPerfResult] = useState<number>(0);
const [openTime, setOpenTime] = useState(0);

useEffect(() => {
console.log("App has started 🟢")
const work = async () => {
// let start = performance.now();
// open({
// name: 'dummyDb.sqlite',
// });
// setOpenTime(performance.now() - start);
useEffect(() => {
console.log("App has started 🟢");
const work = async () => {
// let start = performance.now();
// open({
// name: 'dummyDb.sqlite',
// });
// setOpenTime(performance.now() - start);

try {
console.log("TESTS STARTED 🟠");
const results = await runTests();
const passed = allTestsPassed(results);
console.log("TESTS FINISHED 🟢")
console.log(`OPSQLITE_TEST_RESULT:${passed ? 'PASS' : 'FAIL'}`);
setResults(results);
} catch (e) {
console.log(`TEST FAILED 🟥 ${e}`)
console.log('OPSQLITE_TEST_RESULT:FAIL');
}
try {
console.log("TESTS STARTED 🟠");
const results = await runTests();
const passed = allTestsPassed(results);
console.log("TESTS FINISHED 🟢");
console.log(`OPSQLITE_TEST_RESULT:${passed ? "PASS" : "FAIL"}`);
setResults(results);
} catch (e) {
console.log(`TEST FAILED 🟥 ${e}`);
console.log("OPSQLITE_TEST_RESULT:FAIL");
}

// setTimeout(() => {
// try {
// global?.gc?.();
// let perfRes = performanceTest();
// setPerfResult(perfRes);
// } catch (e) {
// // intentionally left blank
// }
// }, 1000);
};
// setTimeout(() => {
// try {
// global?.gc?.();
// let perfRes = performanceTest();
// setPerfResult(perfRes);
// } catch (e) {
// // intentionally left blank
// }
// }, 1000);
};

work();
work();

return () => {};
}, []);
return () => {};
}, []);

// const shareDb = async () => {
// try {
// const db = open({
// name: 'shareableDb.sqlite',
// });
// await db.execute(
// 'CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT)',
// );
// await db.execute("INSERT INTO test (name) VALUES ('test')");
// const res = await db.execute('SELECT * FROM test');
// console.log(res);
// await db.close();
// await Share.open({
// url: 'file://' + db.getDbPath(),
// failOnCancel: false,
// type: 'application/x-sqlite3',
// });
// } catch (e) {
// console.log(e);
// }
// };
// const shareDb = async () => {
// try {
// const db = open({
// name: 'shareableDb.sqlite',
// });
// await db.execute(
// 'CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT)',
// );
// await db.execute("INSERT INTO test (name) VALUES ('test')");
// const res = await db.execute('SELECT * FROM test');
// console.log(res);
// await db.close();
// await Share.open({
// url: 'file://' + db.getDbPath(),
// failOnCancel: false,
// type: 'application/x-sqlite3',
// });
// } catch (e) {
// console.log(e);
// }
// };

return (
<SafeAreaProvider>
<SafeAreaView style={styles.container}>
<View>
<Text style={styles.performanceText}>
Open DB time: {openTime.toFixed(0)} ms
</Text>
<Text style={styles.performanceText}>
100_000 query time: {perfResult.toFixed(0)} ms
</Text>
</View>
<View style={styles.results}>{displayResults(results)}</View>
</SafeAreaView>
</SafeAreaProvider>
);
return (
<SafeAreaProvider>
<SafeAreaView style={styles.container}>
<View>
<Text style={styles.performanceText}>
Open DB time: {openTime.toFixed(0)} ms
</Text>
<Text style={styles.performanceText}>
100_000 query time: {perfResult.toFixed(0)} ms
</Text>
</View>
<View style={styles.results}>{displayResults(results)}</View>
</SafeAreaView>
</SafeAreaProvider>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#222',
gap: 4,
padding: 10,
},
results: {
flex: 1,
},
performanceText: {
color: 'white',
fontSize: 16,
},
container: {
flex: 1,
backgroundColor: "#222",
gap: 4,
padding: 10,
},
results: {
flex: 1,
},
performanceText: {
color: "white",
fontSize: 16,
},
});
Loading
Loading