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
2 changes: 2 additions & 0 deletions .github/ci-paths.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"packages/react-native-nitro-sqlite/package.json",
"packages/react-native-nitro-sqlite-vec/package.json",
"patches/react-native-nitro-modules*",
"patches/@react-native-harness*",
"example/react-native.config.js",
"packages/react-native-nitro-sqlite/react-native.config.js",
"packages/react-native-nitro-sqlite-vec/react-native.config.js",
Expand Down Expand Up @@ -137,6 +138,7 @@
"packages/react-native-nitro-sqlite/package.json",
"packages/react-native-nitro-sqlite-vec/package.json",
"patches/react-native-nitro-modules*",
"patches/@react-native-harness*",
"example/react-native.config.js",
"packages/react-native-nitro-sqlite/react-native.config.js",
"packages/react-native-nitro-sqlite-vec/react-native.config.js",
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ jobs:
run: |
node --test scripts/ci-dependencies.test.ts
bun run test:release
- name: Test Harness Metro cache handoff
if: steps.select.outputs.dependencies == 'true'
run: node --test scripts/ci-harness-cache.test.ts
- name: Compile TypeScript
if: steps.select.outputs.typescript == 'true'
run: bun typecheck
Expand Down
14 changes: 12 additions & 2 deletions docs/content/docs/guides/parameters-and-results.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,18 @@ Be especially careful with nullable columns and expressions when specifying a ro
The connection's JavaScript helpers normalize database failures to `NitroSQLiteError`. Async methods reject; sync methods throw. An empty SQL string or one containing only comments also fails with the native category `SqlExecutionError`, because SQLite produces no statement to execute. Catch the error around the operation you can recover from:

```ts
import { NitroSQLiteError } from 'react-native-nitro-sqlite'
import {
NitroSQLiteError,
NitroSQLiteExceptionType,
} from 'react-native-nitro-sqlite/errors'

try {
await db.executeAsync('SELECT * FROM missing_table')
} catch (error) {
if (error instanceof NitroSQLiteError) {
if (
error instanceof NitroSQLiteError &&
error.type === NitroSQLiteExceptionType.SqlExecutionError
) {
console.error(error.message)
} else {
throw error
Expand All @@ -68,4 +74,8 @@ try {

When a native `NitroSQLiteException` reaches a managed helper, `error.type` holds a [`NitroSQLiteExceptionType`](/api/react-native-nitro-sqlite/type-aliases/NitroSQLiteExceptionType) such as `SqlExecutionError`. It is `undefined` for JavaScript errors, including connection queue errors, and unrecognized native categories. These categories are not SQLite's numeric error codes. The native categories are `UnknownError`, `DatabaseCannotBeOpened`, `EncryptionNotEnabled`, `DatabaseCannotBeDecrypted`, `DatabaseNotOpen`, `UnableToAttachToDatabase`, `SqlExecutionError`, `CouldNotLoadFile`, and `NoBatchCommandsProvided`. See [encrypt a database](/docs/guides/encryption#handle-open-failures) for the encryption errors. The current native `DatabaseNotOpen` helper emits `UnableToAttachToDatabase`, and a missing database file emits `SqlExecutionError`. Calls through `NitroSQLite.native` throw raw native errors.

The [`NitroSQLiteExceptionType` constants](/api/react-native-nitro-sqlite/variables/NitroSQLiteExceptionType) expose these categories as runtime strings. The object and the TypeScript union share the same name, so existing type-only imports still work. Both the constants and `NitroSQLiteError` are available from the package root and `react-native-nitro-sqlite/errors`. The `/errors` entry point does not import React Native or initialize Nitro's native module, so shared classifiers and Node tests can use it without a native runtime. `error.name` remains `NitroSQLiteError` for every managed failure; use `error.type` to distinguish categories.

`DatabaseCannotBeOpened` covers several causes, including invalid paths, an already-open default connection, and filesystem failures. It does not by itself establish that the device is low on storage.

For exact types, see [`QueryResult`](/api/react-native-nitro-sqlite/type-aliases/QueryResult), [`NitroSQLiteQueryResult`](/api/react-native-nitro-sqlite/hybrid-objects/NitroSQLiteQueryResult), and [`NitroSQLiteQueryResultRows`](/api/react-native-nitro-sqlite/type-aliases/NitroSQLiteQueryResultRows). The row generic narrows `rows._array` and `rows.item()`, but not the raw `results` array. Query results also inherit Nitro hybrid object members; see [native access](/docs/guides/sync-and-async#global-helpers-and-native-access).
11 changes: 11 additions & 0 deletions packages/react-native-nitro-sqlite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@
"default": "./lib/commonjs/index.js"
}
},
"./errors": {
"react-native": "./src/errors.ts",
"import": {
"types": "./lib/typescript/module/errors.d.ts",
"default": "./lib/module/errors.js"
},
"require": {
"types": "./lib/typescript/commonjs/errors.d.ts",
"default": "./lib/commonjs/errors.js"
}
},
"./mock": {
"import": {
"types": "./lib/typescript/module/mock.d.ts",
Expand Down
65 changes: 40 additions & 25 deletions packages/react-native-nitro-sqlite/src/NitroSQLiteError.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,49 @@
const NITRO_SQLITE_ERROR_NAME = 'NitroSQLiteError' as const
const NATIVE_EXCEPTION_PREFIX = '[NativeNitroSQLiteException]['

/** Category attached to an error thrown by NitroSQLite's native implementation. */
/**
* Native error categories exposed by {@linkcode NitroSQLiteError.type}.
* Compare against these constants when handling a specific failure. The values
* are categories, not SQLite's numeric result codes. Import them from
* `react-native-nitro-sqlite/errors` to avoid initializing the native module.
* @see {@linkcode NitroSQLiteError}
*/
export const NitroSQLiteExceptionType = {
/** Native failure without a more specific category. */
UnknownError: 'UnknownError',
/** Open failure, including invalid paths and an already-open default connection. */
DatabaseCannotBeOpened: 'DatabaseCannotBeOpened',
/** An encryption key was supplied to a build without SQLite SEE. */
EncryptionNotEnabled: 'EncryptionNotEnabled',
/** Invalid encryption key, unreadable encrypted database, or existing plaintext file. */
DatabaseCannotBeDecrypted: 'DatabaseCannotBeDecrypted',
/** Category for a database connection that is not open. */
DatabaseNotOpen: 'DatabaseNotOpen',
/** Database attachment failure or an unopened native connection. */
UnableToAttachToDatabase: 'UnableToAttachToDatabase',
/** SQL execution or parameter binding failure, including a missing database file. */
SqlExecutionError: 'SqlExecutionError',
/** A SQL file could not be read or executed. */
CouldNotLoadFile: 'CouldNotLoadFile',
/** An empty batch was submitted. */
NoBatchCommandsProvided: 'NoBatchCommandsProvided',
} as const

/**
* Category attached to {@linkcode NitroSQLiteError.type} for a recognized native
* failure. The corresponding {@linkcode NitroSQLiteExceptionType} object exposes
* the same values at runtime.
* @see {@linkcode NitroSQLiteError.type}
*/
export type NitroSQLiteExceptionType =
| 'UnknownError'
| 'DatabaseCannotBeOpened'
| 'EncryptionNotEnabled'
| 'DatabaseCannotBeDecrypted'
| 'DatabaseNotOpen'
| 'UnableToAttachToDatabase'
| 'SqlExecutionError'
| 'CouldNotLoadFile'
| 'NoBatchCommandsProvided'
(typeof NitroSQLiteExceptionType)[keyof typeof NitroSQLiteExceptionType]

/** Error thrown by managed NitroSQLite operations. Native errors are wrapped with this class. */
export default class NitroSQLiteError extends Error {
/** Native exception category, if this error originated in NitroSQLite's C++ code. */
/**
* Recognized native category from {@linkcode NitroSQLiteExceptionType}, or
* `undefined` for JavaScript failures and unrecognized native categories.
*/
readonly type: NitroSQLiteExceptionType | undefined

/** Create an error with a message and optional cause. */
Expand Down Expand Up @@ -71,18 +99,5 @@ function getNativeExceptionType(
if (typeEnd === -1) return undefined

const type = message.slice(typeStart, typeEnd)
switch (type) {
case 'UnknownError':
case 'DatabaseCannotBeOpened':
case 'EncryptionNotEnabled':
case 'DatabaseCannotBeDecrypted':
case 'DatabaseNotOpen':
case 'UnableToAttachToDatabase':
case 'SqlExecutionError':
case 'CouldNotLoadFile':
case 'NoBatchCommandsProvided':
return type
default:
return undefined
}
return Object.values(NitroSQLiteExceptionType).find((value) => value === type)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import NitroSQLiteError from '../NitroSQLiteError'
jest.mock('react-native-nitro-modules', () => {
throw new Error('The errors entry point must not load the native runtime')
})

import { NitroSQLiteError, NitroSQLiteExceptionType } from '../errors'

describe('NitroSQLiteError', () => {
it('keeps its name, cause, and prototype', () => {
Expand All @@ -12,6 +16,16 @@ describe('NitroSQLiteError', () => {
expect(error.type).toBeUndefined()
})

it('supports comparing a native failure against the exported category', () => {
const error = NitroSQLiteError.fromError(
'[NativeNitroSQLiteException][DatabaseCannotBeOpened] failed',
)
const category: NitroSQLiteExceptionType =
NitroSQLiteExceptionType.DatabaseCannotBeOpened

expect(error.type).toBe(category)
})

it('returns an existing NitroSQLiteError unchanged', () => {
const error = new NitroSQLiteError('existing')
expect(NitroSQLiteError.fromError(error)).toBe(error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,26 @@ jest.mock('react-native-nitro-modules', () => ({
}))

import { NitroModules } from 'react-native-nitro-modules'
import { NitroSQLite, open } from '../index'
import {
NitroSQLite,
NitroSQLiteError,
NitroSQLiteExceptionType,
open,
} from '../index'
import {
NitroSQLiteError as ErrorFromErrors,
NitroSQLiteExceptionType as ExceptionTypeFromErrors,
} from '../errors'
import { HybridNitroSQLite } from '../nitro'
import { init as androidInit } from '../OnLoad.android'
import { init as iosInit } from '../OnLoad'

describe('entrypoints', () => {
it('shares the error class and category constants with the errors entry point', () => {
expect(NitroSQLiteError).toBe(ErrorFromErrors)
expect(NitroSQLiteExceptionType).toBe(ExceptionTypeFromErrors)
})

it('creates the native object and exposes its native and managed methods', () => {
expect(NitroModules.createHybridObject).toHaveBeenCalledWith('NitroSQLite')
expect(NitroSQLite.native).toBe(HybridNitroSQLite)
Expand Down
4 changes: 4 additions & 0 deletions packages/react-native-nitro-sqlite/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
default as NitroSQLiteError,
NitroSQLiteExceptionType,
} from './NitroSQLiteError'
3 changes: 1 addition & 2 deletions packages/react-native-nitro-sqlite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ export const NitroSQLite = {
}

export { open } from './operations/session'
export { default as NitroSQLiteError } from './NitroSQLiteError'
export type { NitroSQLiteExceptionType } from './NitroSQLiteError'
export { NitroSQLiteError, NitroSQLiteExceptionType } from './errors'
export type { DatabaseQueueKey } from './DatabaseQueue'
export type { NitroSQLite as NitroSQLiteNative } from './specs/NitroSQLite.nitro'
export type { NitroSQLitePreparedStatement } from './specs/NitroSQLitePreparedStatement.nitro'
Expand Down
66 changes: 66 additions & 0 deletions patches/@react-native-harness+cli+1.5.0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
diff --git a/node_modules/@react-native-harness/cli/dist/ci/plan-metro-save.js b/node_modules/@react-native-harness/cli/dist/ci/plan-metro-save.js
index 55a6fed..d20c85c 100644
--- a/node_modules/@react-native-harness/cli/dist/ci/plan-metro-save.js
+++ b/node_modules/@react-native-harness/cli/dist/ci/plan-metro-save.js
@@ -9,7 +9,10 @@ const parseSnapshot = (raw) => {
return {};
}
try {
- return JSON.parse(raw);
+ const json = raw.trimStart().startsWith('{')
+ ? raw
+ : getFs().readFileSync(raw, 'utf8');
+ return JSON.parse(json);
}
catch (error) {
console.warn('Failed to parse the metroSnapshot input. Treating the pre-run cache state as empty.', error);
diff --git a/node_modules/@react-native-harness/cli/dist/ci/snapshot-metro.js b/node_modules/@react-native-harness/cli/dist/ci/snapshot-metro.js
index 24965c3..6786315 100644
--- a/node_modules/@react-native-harness/cli/dist/ci/snapshot-metro.js
+++ b/node_modules/@react-native-harness/cli/dist/ci/snapshot-metro.js
@@ -36,7 +36,11 @@ export const runSnapshotMetro = async () => {
else {
console.info('Metro cache: no existing cache entry matched — starting cold.');
}
- const output = `metroSnapshot=${JSON.stringify(snapshotAfterRestore)}\n`;
+ // Keep large snapshots out of step environment variables. This file stays
+ // beside GITHUB_OUTPUT for the later cache-save step in the same job.
+ const snapshotPath = `${githubOutput}.metro-snapshot.json`;
+ getFs().writeFileSync(snapshotPath, JSON.stringify(snapshotAfterRestore));
+ const output = `metroSnapshot=${snapshotPath}\n`;
getFs().appendFileSync(githubOutput, output);
}
catch (error) {
diff --git a/node_modules/@react-native-harness/cli/src/ci/plan-metro-save.ts b/node_modules/@react-native-harness/cli/src/ci/plan-metro-save.ts
index 959420f..aa28677 100644
--- a/node_modules/@react-native-harness/cli/src/ci/plan-metro-save.ts
+++ b/node_modules/@react-native-harness/cli/src/ci/plan-metro-save.ts
@@ -16,7 +16,10 @@ const parseSnapshot = (raw: string | undefined): CacheSnapshot => {
}

try {
- return JSON.parse(raw) as CacheSnapshot;
+ const json = raw.trimStart().startsWith('{')
+ ? raw
+ : getFs().readFileSync(raw, 'utf8');
+ return JSON.parse(json) as CacheSnapshot;
} catch (error) {
console.warn(
'Failed to parse the metroSnapshot input. Treating the pre-run cache state as empty.',
diff --git a/node_modules/@react-native-harness/cli/src/ci/snapshot-metro.ts b/node_modules/@react-native-harness/cli/src/ci/snapshot-metro.ts
index b24379f..1faae3f 100644
--- a/node_modules/@react-native-harness/cli/src/ci/snapshot-metro.ts
+++ b/node_modules/@react-native-harness/cli/src/ci/snapshot-metro.ts
@@ -50,7 +50,11 @@ export const runSnapshotMetro = async (): Promise<void> => {
);
}

- const output = `metroSnapshot=${JSON.stringify(snapshotAfterRestore)}\n`;
+ // Keep large snapshots out of step environment variables. This file stays
+ // beside GITHUB_OUTPUT for the later cache-save step in the same job.
+ const snapshotPath = `${githubOutput}.metro-snapshot.json`;
+ getFs().writeFileSync(snapshotPath, JSON.stringify(snapshotAfterRestore));
+ const output = `metroSnapshot=${snapshotPath}\n`;

getFs().appendFileSync(githubOutput, output);
} catch (error) {
Loading
Loading