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
26 changes: 23 additions & 3 deletions .github/ci-paths.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
"packages/react-native-nitro-sqlite-vec/**/*.jsx",
"packages/react-native-nitro-sqlite-vec/tsconfig.json",
"packages/react-native-nitro-sqlite-vec/package.json",
"packages/react-native-nitro-sqlite-drizzle/**/*.ts",
"packages/react-native-nitro-sqlite-drizzle/**/*.js",
"packages/react-native-nitro-sqlite-drizzle/tsconfig.json",
"packages/react-native-nitro-sqlite-drizzle/package.json",
"scripts/**/*.ts",
"example/jest*.js"
],
Expand Down Expand Up @@ -75,6 +79,8 @@
"packages/react-native-nitro-sqlite/package.json",
"packages/react-native-nitro-sqlite-vec/src/**",
"packages/react-native-nitro-sqlite-vec/package.json",
"packages/react-native-nitro-sqlite-drizzle/src/**",
"packages/react-native-nitro-sqlite-drizzle/package.json",
"config/tsconfig.json",
"package.json",
"bun.lock"
Expand Down Expand Up @@ -110,9 +116,14 @@
"scripts/android-sqlite-flags/**",
"example/index.js",
"example/babel.config*.js",
"example/metro.config.js",
"example/app.json",
"packages/react-native-nitro-sqlite-vec/src/**",
"!packages/react-native-nitro-sqlite-vec/src/__tests__/**"
"!packages/react-native-nitro-sqlite-vec/src/__tests__/**",
"packages/react-native-nitro-sqlite-drizzle/src/**",
"packages/react-native-nitro-sqlite-drizzle/package.json",
"!packages/react-native-nitro-sqlite-drizzle/src/__tests__/**",
"packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/**"
],
"ios": [
".github/workflows/**",
Expand Down Expand Up @@ -148,9 +159,14 @@
"scripts/test-podspec-threadsafe.rb",
"example/index.js",
"example/babel.config*.js",
"example/metro.config.js",
"example/app.json",
"packages/react-native-nitro-sqlite-vec/src/**",
"!packages/react-native-nitro-sqlite-vec/src/__tests__/**"
"!packages/react-native-nitro-sqlite-vec/src/__tests__/**",
"packages/react-native-nitro-sqlite-drizzle/src/**",
"packages/react-native-nitro-sqlite-drizzle/package.json",
"!packages/react-native-nitro-sqlite-drizzle/src/__tests__/**",
"packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/**"
],
"macos": [
".github/workflows/**",
Expand Down Expand Up @@ -187,6 +203,10 @@
"example/react-native.config.js",
"example/macos/react-native.config.js",
"!packages/react-native-nitro-sqlite/src/__tests__/**",
"!packages/react-native-nitro-sqlite-vec/src/__tests__/**"
"!packages/react-native-nitro-sqlite-vec/src/__tests__/**",
"packages/react-native-nitro-sqlite-drizzle/src/**",
"packages/react-native-nitro-sqlite-drizzle/package.json",
"!packages/react-native-nitro-sqlite-drizzle/src/__tests__/**",
"packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/**"
]
}
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,14 @@ jobs:
bun --cwd example lint
bun --cwd packages/react-native-nitro-sqlite lint
bun --cwd packages/react-native-nitro-sqlite-vec lint
bun --cwd packages/react-native-nitro-sqlite-drizzle lint
git diff --exit-code HEAD -- . ':(exclude)bun.lock'
- name: Test JavaScript wrappers and example runners
if: steps.select.outputs.typescript == 'true'
run: |
(cd packages/react-native-nitro-sqlite && ../../node_modules/.bin/jest --coverage --runInBand)
(cd packages/react-native-nitro-sqlite-vec && ../../node_modules/.bin/jest --coverage --runInBand)
(cd packages/react-native-nitro-sqlite-drizzle && ../../node_modules/.bin/jest --coverage --runInBand)
(cd example && ../node_modules/.bin/jest --config jest.mocha.config.js --runInBand --watchman=false)
node --test example/macos/scripts/test-macos.test.mjs
- name: Format C++
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
test:
name: macOS integration tests
runs-on: macos-26
timeout-minutes: 30
timeout-minutes: 40
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*.markdown

packages/react-native-nitro-sqlite/lib
packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle

.well-known
android
Expand Down
155 changes: 155 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

121 changes: 121 additions & 0 deletions docs/content/docs/integrations/drizzle.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
title: Drizzle ORM
description: Use Drizzle ORM with Nitro SQLite through the react-native-nitro-sqlite-drizzle driver.
---

`react-native-nitro-sqlite-drizzle` runs [Drizzle ORM](https://orm.drizzle.team) queries on a Nitro SQLite connection. Pass an open connection to `drizzle()` and query it with Drizzle's API. Every query is asynchronous.

## Install

Install the driver with Drizzle and Nitro SQLite. Drizzle Kit and the Babel plugin are only needed for migrations.

```bash
npm install react-native-nitro-sqlite react-native-nitro-sqlite-drizzle drizzle-orm
npm install --save-dev drizzle-kit babel-plugin-inline-import
```

The driver supports `drizzle-orm` 0.45.4 and later 0.x releases.

## Create a database

```ts
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
import { open } from 'react-native-nitro-sqlite'
import { drizzle } from 'react-native-nitro-sqlite-drizzle'

export const users = sqliteTable('users', {
id: integer('id').primaryKey(),
name: text('name').notNull(),
})

export const db = drizzle(open({ name: 'app.sqlite' }))

await db.insert(users).values({ id: 1, name: 'Ada' })
const rows = await db.select().from(users)
```

Create `db` once, outside your components. `drizzle(connection, config)` accepts Drizzle's `schema`, `logger`, and `casing` options. `db.$client` is the connection passed to `drizzle()`.

SQLite stores binary values as BLOBs. When you pass a `Buffer` or another typed-array view through Drizzle, the driver binds its bytes as an `ArrayBuffer`, preserving the view's byte range.

## Migrations

Drizzle Kit generates SQL migration files from your schema. Set `driver: 'expo'` so it also writes a `migrations.js` file that bundles them for React Native:

```ts title="drizzle.config.ts"
import { defineConfig } from 'drizzle-kit'

export default defineConfig({
schema: './db/schema.ts',
out: './drizzle',
dialect: 'sqlite',
driver: 'expo',
})
```

Run `npx drizzle-kit generate` after each schema change.

`migrations.js` imports `.sql` files. Add the Babel plugin so they load as strings:

```js title="babel.config.js"
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [['inline-import', { extensions: ['.sql'] }]],
}
```

Add `sql` to Metro's source extensions:

```js title="metro.config.js"
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config')

const defaultConfig = getDefaultConfig(__dirname)

module.exports = mergeConfig(defaultConfig, {
resolver: { sourceExts: [...defaultConfig.resolver.sourceExts, 'sql'] },
})
```

Run the migrations when the app starts with `useMigrations`. It applies the migrations this database has not run yet, in one transaction, and records them in the `__drizzle_migrations` table:

```tsx
import { Text } from 'react-native'
import { useMigrations } from 'react-native-nitro-sqlite-drizzle'
import migrations from './drizzle/migrations'
import { db } from './db'

export function App() {
const { success, error } = useMigrations(db, migrations)

if (error) return <Text>Migration failed: {error.message}</Text>
if (!success) return <Text>Migrating...</Text>
return <Text>Ready</Text>
}
```

To run migrations outside a component, such as in startup code or a test, call `migrate` and await it:

```ts
import { migrate } from 'react-native-nitro-sqlite-drizzle'

await migrate(db, migrations)
```

Calls using the same connection wait for earlier migration calls before checking which migrations are pending. React Strict Mode can replay `useMigrations` without applying the same migration twice. If a migration fails, its transaction rolls back and a later call can retry it.

## Transactions

Run every query inside a transaction through `tx`:

```ts
import { eq } from 'drizzle-orm'

await db.transaction(async (tx) => {
await tx.insert(users).values({ id: 2, name: 'Grace' })
await tx.update(users).set({ name: 'Ada L.' }).where(eq(users.id, 1))
})
```

A transaction holds the connection until its callback finishes. A query awaited through `db` inside the callback, including a query prepared from `db`, waits for the transaction and never completes. Nested `tx.transaction()` calls use savepoints.

`tx.rollback()` rolls back the transaction and rejects it with a `NitroSQLiteError` whose message is `Rollback`, not Drizzle's `TransactionRollbackError`. The `behavior` option of `db.transaction()` is ignored: transactions start with `BEGIN`. See [transactions](/docs/guides/transactions) for how Nitro SQLite runs them.
2 changes: 1 addition & 1 deletion docs/content/docs/integrations/meta.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"title": "Integrations",
"pages": ["typeorm", "vector-search"]
"pages": ["drizzle", "typeorm", "vector-search"]
}
12 changes: 12 additions & 0 deletions docs/scripts/check-api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ const requiredExports = {
'createVectorTable',
'knnSearch',
],
'react-native-nitro-sqlite-drizzle': [
'drizzle',
'migrate',
'useMigrations',
'MigrationConfig',
'MigrationState',
'NitroSQLiteDatabase',
'NitroSQLiteSessionOptions',
'NitroSQLiteSession',
'NitroSQLiteTransaction',
'NitroSQLitePreparedQuery',
],
}

for (const [packageName, exports] of Object.entries(requiredExports)) {
Expand Down
3 changes: 2 additions & 1 deletion docs/tsconfig.typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"files": [
"../packages/react-native-nitro-sqlite/src/index.ts",
"../packages/react-native-nitro-sqlite/src/mock.ts",
"../packages/react-native-nitro-sqlite-vec/src/index.ts"
"../packages/react-native-nitro-sqlite-vec/src/index.ts",
"../packages/react-native-nitro-sqlite-drizzle/src/index.ts"
]
}
1 change: 1 addition & 0 deletions docs/typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"entryPoints": [
"../packages/react-native-nitro-sqlite",
"../packages/react-native-nitro-sqlite-vec",
"../packages/react-native-nitro-sqlite-drizzle",
"./mock-api"
],
"entryPointStrategy": "packages",
Expand Down
1 change: 1 addition & 0 deletions example/babel.config.shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function createExampleBabelConfig(projectRoot, { buffer, crypto } = {}) {
],
'babel-plugin-transform-typescript-metadata',
['@babel/plugin-proposal-decorators', { legacy: true }],
['inline-import', { extensions: ['.sql'] }],
],
}
}
Expand Down
6 changes: 5 additions & 1 deletion example/macos/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,21 @@ const extraNodeModules = Object.keys(libraryPackage.peerDependencies).reduce(
},
{},
)
const defaultConfig = getDefaultConfig(__dirname)

const config = {
projectRoot: __dirname,
watchFolders: [exampleRoot, root],
resolver: {
// The example shares migration fixtures with the Drizzle package tests.
blockList: /\/__tests__\/(?!fixtures\/).*/,
disableHierarchicalLookup: true,
nodeModulesPaths: [
path.join(__dirname, 'node_modules'),
path.join(root, 'node_modules'),
],
platforms: ['macos', 'ios', 'android'],
sourceExts: [...defaultConfig.resolver.sourceExts, 'sql'],
extraNodeModules,
resolveRequest: (context, moduleName, platform) => {
// TypeORM imports the workspace package without going through Babel.
Expand Down Expand Up @@ -78,4 +82,4 @@ const config = {
},
}

module.exports = mergeConfig(getDefaultConfig(__dirname), config)
module.exports = mergeConfig(defaultConfig, config)
3 changes: 3 additions & 0 deletions example/macos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"buffer": "^6.0.3",
"chai": "^4.3.7",
"chance": "^1.1.9",
"drizzle-orm": "0.45.4",
"events": "^3.3.0",
"expo-status-bar": "^1.12.1",
"mocha": "^10.1.0",
Expand All @@ -22,6 +23,7 @@
"react-native-macos-local": "npm:react-native-macos@0.81.9",
"react-native-nitro-modules": "0.37.1",
"react-native-nitro-sqlite": "workspace:*",
"react-native-nitro-sqlite-drizzle": "workspace:*",
"react-native-nitro-sqlite-vec": "workspace:*",
"reflect-metadata": "^0.2.2",
"typeorm": "1.1.1",
Expand All @@ -34,6 +36,7 @@
"@react-native-community/cli-platform-apple": "19.1.2",
"@react-native-community/cli-platform-ios": "19.1.2",
"@react-native/babel-preset": "0.81.6",
"babel-plugin-inline-import": "3.0.0",
"@react-native/community-cli-plugin": "0.81.6",
"@react-native/metro-config": "0.81.6"
}
Expand Down
28 changes: 15 additions & 13 deletions example/macos/scripts/test-macos.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -183,22 +183,23 @@ function watchProcessFailure(child, name, getOutput) {
}
}

async function waitForReport(report, failures) {
async function waitForReport(report, failures, getOutput) {
let timeout
try {
return await Promise.race([
report,
...failures,
new Promise((_, reject) => {
timeout = setTimeout(
() =>
reject(
new Error(
`Timed out waiting for macOS test results after ${testTimeoutMs}ms`,
),
timeout = setTimeout(() => {
const output = getOutput()
reject(
new Error(
`Timed out waiting for macOS test results after ${testTimeoutMs}ms${
output.length > 0 ? `\n${output}` : ''
}`,
),
testTimeoutMs,
)
)
}, testTimeoutMs)
}),
])
} finally {
Expand Down Expand Up @@ -325,10 +326,11 @@ async function main() {
.join('\n\n')
}
appFailure = watchProcessFailure(app, 'The macOS app', getAppAndMetroOutput)
const result = await waitForReport(resultServer.report, [
appFailure.failure,
metroFailure.failure,
])
const result = await waitForReport(
resultServer.report,
[appFailure.failure, metroFailure.failure],
getAppAndMetroOutput,
)
appFailure.stopWatching()
appFailure = undefined
metroFailure.stopWatching()
Expand Down
11 changes: 10 additions & 1 deletion example/macos/scripts/test-macos.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ test('times out when the app does not report results', async () => {
result.output,
/Timed out waiting for macOS test results after 100ms/,
)
assert.match(
result.output,
/App output:[\s\S]*fake app did not report results/,
)
assert.match(result.output, /Metro output:[\s\S]*fake Metro is ready/)
})

test('fails promptly when Metro exits during startup', async () => {
Expand Down Expand Up @@ -136,11 +141,15 @@ const server = createServer((_request, response) => {
response.end('packager-status:running')
})

server.listen(port, '127.0.0.1')
server.listen(port, '127.0.0.1', () => console.log('fake Metro is ready'))
process.on('SIGTERM', () => server.close(() => process.exit(0)))
`

const fakeAppSource = `#!/usr/bin/env node
if (process.env.FAKE_APP_SCENARIO === 'no-report') {
console.error('fake app did not report results')
}

if (process.env.FAKE_APP_SCENARIO === 'early-exit') {
console.error('fake app failed')
process.exit(23)
Expand Down
Loading
Loading