Skip to content

Commit 74c7409

Browse files
authored
feat: expose error type constants (#441)
* feat: expose error category constants * fix(ci): pass Harness Metro snapshots through files
1 parent f376d67 commit 74c7409

12 files changed

Lines changed: 329 additions & 31 deletions

File tree

‎.github/ci-paths.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
"packages/react-native-nitro-sqlite/package.json",
101101
"packages/react-native-nitro-sqlite-vec/package.json",
102102
"patches/react-native-nitro-modules*",
103+
"patches/@react-native-harness*",
103104
"example/react-native.config.js",
104105
"packages/react-native-nitro-sqlite/react-native.config.js",
105106
"packages/react-native-nitro-sqlite-vec/react-native.config.js",
@@ -137,6 +138,7 @@
137138
"packages/react-native-nitro-sqlite/package.json",
138139
"packages/react-native-nitro-sqlite-vec/package.json",
139140
"patches/react-native-nitro-modules*",
141+
"patches/@react-native-harness*",
140142
"example/react-native.config.js",
141143
"packages/react-native-nitro-sqlite/react-native.config.js",
142144
"packages/react-native-nitro-sqlite-vec/react-native.config.js",

‎.github/workflows/ci.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ jobs:
109109
run: |
110110
node --test scripts/ci-dependencies.test.ts
111111
bun run test:release
112+
- name: Test Harness Metro cache handoff
113+
if: steps.select.outputs.dependencies == 'true'
114+
run: node --test scripts/ci-harness-cache.test.ts
112115
- name: Compile TypeScript
113116
if: steps.select.outputs.typescript == 'true'
114117
run: bun typecheck

‎docs/content/docs/guides/parameters-and-results.mdx‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,18 @@ Be especially careful with nullable columns and expressions when specifying a ro
5151
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:
5252

5353
```ts
54-
import { NitroSQLiteError } from 'react-native-nitro-sqlite'
54+
import {
55+
NitroSQLiteError,
56+
NitroSQLiteExceptionType,
57+
} from 'react-native-nitro-sqlite/errors'
5558

5659
try {
5760
await db.executeAsync('SELECT * FROM missing_table')
5861
} catch (error) {
59-
if (error instanceof NitroSQLiteError) {
62+
if (
63+
error instanceof NitroSQLiteError &&
64+
error.type === NitroSQLiteExceptionType.SqlExecutionError
65+
) {
6066
console.error(error.message)
6167
} else {
6268
throw error
@@ -68,4 +74,8 @@ try {
6874

6975
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.
7076

77+
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.
78+
79+
`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.
80+
7181
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).

‎packages/react-native-nitro-sqlite/package.json‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@
1919
"default": "./lib/commonjs/index.js"
2020
}
2121
},
22+
"./errors": {
23+
"react-native": "./src/errors.ts",
24+
"import": {
25+
"types": "./lib/typescript/module/errors.d.ts",
26+
"default": "./lib/module/errors.js"
27+
},
28+
"require": {
29+
"types": "./lib/typescript/commonjs/errors.d.ts",
30+
"default": "./lib/commonjs/errors.js"
31+
}
32+
},
2233
"./mock": {
2334
"import": {
2435
"types": "./lib/typescript/module/mock.d.ts",

‎packages/react-native-nitro-sqlite/src/NitroSQLiteError.ts‎

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,49 @@
11
const NITRO_SQLITE_ERROR_NAME = 'NitroSQLiteError' as const
22
const NATIVE_EXCEPTION_PREFIX = '[NativeNitroSQLiteException]['
33

4-
/** Category attached to an error thrown by NitroSQLite's native implementation. */
4+
/**
5+
* Native error categories exposed by {@linkcode NitroSQLiteError.type}.
6+
* Compare against these constants when handling a specific failure. The values
7+
* are categories, not SQLite's numeric result codes. Import them from
8+
* `react-native-nitro-sqlite/errors` to avoid initializing the native module.
9+
* @see {@linkcode NitroSQLiteError}
10+
*/
11+
export const NitroSQLiteExceptionType = {
12+
/** Native failure without a more specific category. */
13+
UnknownError: 'UnknownError',
14+
/** Open failure, including invalid paths and an already-open default connection. */
15+
DatabaseCannotBeOpened: 'DatabaseCannotBeOpened',
16+
/** An encryption key was supplied to a build without SQLite SEE. */
17+
EncryptionNotEnabled: 'EncryptionNotEnabled',
18+
/** Invalid encryption key, unreadable encrypted database, or existing plaintext file. */
19+
DatabaseCannotBeDecrypted: 'DatabaseCannotBeDecrypted',
20+
/** Category for a database connection that is not open. */
21+
DatabaseNotOpen: 'DatabaseNotOpen',
22+
/** Database attachment failure or an unopened native connection. */
23+
UnableToAttachToDatabase: 'UnableToAttachToDatabase',
24+
/** SQL execution or parameter binding failure, including a missing database file. */
25+
SqlExecutionError: 'SqlExecutionError',
26+
/** A SQL file could not be read or executed. */
27+
CouldNotLoadFile: 'CouldNotLoadFile',
28+
/** An empty batch was submitted. */
29+
NoBatchCommandsProvided: 'NoBatchCommandsProvided',
30+
} as const
31+
32+
/**
33+
* Category attached to {@linkcode NitroSQLiteError.type} for a recognized native
34+
* failure. The corresponding {@linkcode NitroSQLiteExceptionType} object exposes
35+
* the same values at runtime.
36+
* @see {@linkcode NitroSQLiteError.type}
37+
*/
538
export type NitroSQLiteExceptionType =
6-
| 'UnknownError'
7-
| 'DatabaseCannotBeOpened'
8-
| 'EncryptionNotEnabled'
9-
| 'DatabaseCannotBeDecrypted'
10-
| 'DatabaseNotOpen'
11-
| 'UnableToAttachToDatabase'
12-
| 'SqlExecutionError'
13-
| 'CouldNotLoadFile'
14-
| 'NoBatchCommandsProvided'
39+
(typeof NitroSQLiteExceptionType)[keyof typeof NitroSQLiteExceptionType]
1540

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

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

73101
const type = message.slice(typeStart, typeEnd)
74-
switch (type) {
75-
case 'UnknownError':
76-
case 'DatabaseCannotBeOpened':
77-
case 'EncryptionNotEnabled':
78-
case 'DatabaseCannotBeDecrypted':
79-
case 'DatabaseNotOpen':
80-
case 'UnableToAttachToDatabase':
81-
case 'SqlExecutionError':
82-
case 'CouldNotLoadFile':
83-
case 'NoBatchCommandsProvided':
84-
return type
85-
default:
86-
return undefined
87-
}
102+
return Object.values(NitroSQLiteExceptionType).find((value) => value === type)
88103
}

‎packages/react-native-nitro-sqlite/src/__tests__/NitroSQLiteError.test.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import NitroSQLiteError from '../NitroSQLiteError'
1+
jest.mock('react-native-nitro-modules', () => {
2+
throw new Error('The errors entry point must not load the native runtime')
3+
})
4+
5+
import { NitroSQLiteError, NitroSQLiteExceptionType } from '../errors'
26

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

19+
it('supports comparing a native failure against the exported category', () => {
20+
const error = NitroSQLiteError.fromError(
21+
'[NativeNitroSQLiteException][DatabaseCannotBeOpened] failed',
22+
)
23+
const category: NitroSQLiteExceptionType =
24+
NitroSQLiteExceptionType.DatabaseCannotBeOpened
25+
26+
expect(error.type).toBe(category)
27+
})
28+
1529
it('returns an existing NitroSQLiteError unchanged', () => {
1630
const error = new NitroSQLiteError('existing')
1731
expect(NitroSQLiteError.fromError(error)).toBe(error)

‎packages/react-native-nitro-sqlite/src/__tests__/entrypoints.test.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,26 @@ jest.mock('react-native-nitro-modules', () => ({
77
}))
88

99
import { NitroModules } from 'react-native-nitro-modules'
10-
import { NitroSQLite, open } from '../index'
10+
import {
11+
NitroSQLite,
12+
NitroSQLiteError,
13+
NitroSQLiteExceptionType,
14+
open,
15+
} from '../index'
16+
import {
17+
NitroSQLiteError as ErrorFromErrors,
18+
NitroSQLiteExceptionType as ExceptionTypeFromErrors,
19+
} from '../errors'
1120
import { HybridNitroSQLite } from '../nitro'
1221
import { init as androidInit } from '../OnLoad.android'
1322
import { init as iosInit } from '../OnLoad'
1423

1524
describe('entrypoints', () => {
25+
it('shares the error class and category constants with the errors entry point', () => {
26+
expect(NitroSQLiteError).toBe(ErrorFromErrors)
27+
expect(NitroSQLiteExceptionType).toBe(ExceptionTypeFromErrors)
28+
})
29+
1630
it('creates the native object and exposes its native and managed methods', () => {
1731
expect(NitroModules.createHybridObject).toHaveBeenCalledWith('NitroSQLite')
1832
expect(NitroSQLite.native).toBe(HybridNitroSQLite)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export {
2+
default as NitroSQLiteError,
3+
NitroSQLiteExceptionType,
4+
} from './NitroSQLiteError'

‎packages/react-native-nitro-sqlite/src/index.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ export const NitroSQLite = {
2626
}
2727

2828
export { open } from './operations/session'
29-
export { default as NitroSQLiteError } from './NitroSQLiteError'
30-
export type { NitroSQLiteExceptionType } from './NitroSQLiteError'
29+
export { NitroSQLiteError, NitroSQLiteExceptionType } from './errors'
3130
export type { DatabaseQueueKey } from './DatabaseQueue'
3231
export type { NitroSQLite as NitroSQLiteNative } from './specs/NitroSQLite.nitro'
3332
export type { NitroSQLitePreparedStatement } from './specs/NitroSQLitePreparedStatement.nitro'
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
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
2+
index 55a6fed..d20c85c 100644
3+
--- a/node_modules/@react-native-harness/cli/dist/ci/plan-metro-save.js
4+
+++ b/node_modules/@react-native-harness/cli/dist/ci/plan-metro-save.js
5+
@@ -9,7 +9,10 @@ const parseSnapshot = (raw) => {
6+
return {};
7+
}
8+
try {
9+
- return JSON.parse(raw);
10+
+ const json = raw.trimStart().startsWith('{')
11+
+ ? raw
12+
+ : getFs().readFileSync(raw, 'utf8');
13+
+ return JSON.parse(json);
14+
}
15+
catch (error) {
16+
console.warn('Failed to parse the metroSnapshot input. Treating the pre-run cache state as empty.', error);
17+
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
18+
index 24965c3..6786315 100644
19+
--- a/node_modules/@react-native-harness/cli/dist/ci/snapshot-metro.js
20+
+++ b/node_modules/@react-native-harness/cli/dist/ci/snapshot-metro.js
21+
@@ -36,7 +36,11 @@ export const runSnapshotMetro = async () => {
22+
else {
23+
console.info('Metro cache: no existing cache entry matched — starting cold.');
24+
}
25+
- const output = `metroSnapshot=${JSON.stringify(snapshotAfterRestore)}\n`;
26+
+ // Keep large snapshots out of step environment variables. This file stays
27+
+ // beside GITHUB_OUTPUT for the later cache-save step in the same job.
28+
+ const snapshotPath = `${githubOutput}.metro-snapshot.json`;
29+
+ getFs().writeFileSync(snapshotPath, JSON.stringify(snapshotAfterRestore));
30+
+ const output = `metroSnapshot=${snapshotPath}\n`;
31+
getFs().appendFileSync(githubOutput, output);
32+
}
33+
catch (error) {
34+
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
35+
index 959420f..aa28677 100644
36+
--- a/node_modules/@react-native-harness/cli/src/ci/plan-metro-save.ts
37+
+++ b/node_modules/@react-native-harness/cli/src/ci/plan-metro-save.ts
38+
@@ -16,7 +16,10 @@ const parseSnapshot = (raw: string | undefined): CacheSnapshot => {
39+
}
40+
41+
try {
42+
- return JSON.parse(raw) as CacheSnapshot;
43+
+ const json = raw.trimStart().startsWith('{')
44+
+ ? raw
45+
+ : getFs().readFileSync(raw, 'utf8');
46+
+ return JSON.parse(json) as CacheSnapshot;
47+
} catch (error) {
48+
console.warn(
49+
'Failed to parse the metroSnapshot input. Treating the pre-run cache state as empty.',
50+
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
51+
index b24379f..1faae3f 100644
52+
--- a/node_modules/@react-native-harness/cli/src/ci/snapshot-metro.ts
53+
+++ b/node_modules/@react-native-harness/cli/src/ci/snapshot-metro.ts
54+
@@ -50,7 +50,11 @@ export const runSnapshotMetro = async (): Promise<void> => {
55+
);
56+
}
57+
58+
- const output = `metroSnapshot=${JSON.stringify(snapshotAfterRestore)}\n`;
59+
+ // Keep large snapshots out of step environment variables. This file stays
60+
+ // beside GITHUB_OUTPUT for the later cache-save step in the same job.
61+
+ const snapshotPath = `${githubOutput}.metro-snapshot.json`;
62+
+ getFs().writeFileSync(snapshotPath, JSON.stringify(snapshotAfterRestore));
63+
+ const output = `metroSnapshot=${snapshotPath}\n`;
64+
65+
getFs().appendFileSync(githubOutput, output);
66+
} catch (error) {

0 commit comments

Comments
 (0)