diff --git a/.github/ci-paths.json b/.github/ci-paths.json index c0faedd9..dbc8a432 100644 --- a/.github/ci-paths.json +++ b/.github/ci-paths.json @@ -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" ], @@ -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" @@ -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/**", @@ -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/**", @@ -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/**" ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44ad266d..4117fb3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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++ diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml index 9f498d99..283d4bca 100644 --- a/.github/workflows/test-macos.yml +++ b/.github/workflows/test-macos.yml @@ -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 diff --git a/.prettierignore b/.prettierignore index 79e92d52..a037f45e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,6 +7,7 @@ *.markdown packages/react-native-nitro-sqlite/lib +packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle .well-known android diff --git a/bun.lock b/bun.lock index 359c96f5..65759395 100644 --- a/bun.lock +++ b/bun.lock @@ -77,12 +77,14 @@ "@react-navigation/native-stack": "^7.21.0", "chai": "^4.3.7", "chance": "^1.1.9", + "drizzle-orm": "0.45.4", "events": "^3.3.0", "expo-status-bar": "^1.12.1", "react": "19.3.1", "react-native": "0.88.0", "react-native-nitro-modules": "*", "react-native-nitro-sqlite": "10.2.0", + "react-native-nitro-sqlite-drizzle": "10.2.0", "react-native-nitro-sqlite-vec": "10.2.0", "react-native-quick-base64": "^3.0.1", "react-native-quick-crypto": "^1.1.8", @@ -116,6 +118,7 @@ "@types/node": "^22.7.4", "@types/react": "^19.3.0", "@types/react-test-renderer": "^19.3.0", + "babel-plugin-inline-import": "3.0.0", "mocha": "^12.0.2", "postinstall-postinstall": "^2.1.0", "react-native-harness": "1.6.0", @@ -130,6 +133,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", @@ -139,6 +143,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:*", "readable-stream": "^4.7.0", "reflect-metadata": "^0.2.2", @@ -154,6 +159,7 @@ "@react-native/babel-preset": "0.81.6", "@react-native/community-cli-plugin": "0.81.6", "@react-native/metro-config": "0.81.6", + "babel-plugin-inline-import": "3.0.0", }, }, "packages/react-native-nitro-sqlite": { @@ -183,6 +189,23 @@ "better-sqlite3", ], }, + "packages/react-native-nitro-sqlite-drizzle": { + "name": "react-native-nitro-sqlite-drizzle", + "version": "10.2.0", + "devDependencies": { + "babel-plugin-inline-import": "3.0.0", + "drizzle-kit": "0.31.11", + "drizzle-orm": "0.45.4", + "react": "19.3.1", + "react-native-nitro-sqlite": "10.2.0", + "typescript": "^5.8.3", + }, + "peerDependencies": { + "drizzle-orm": ">=0.45.4 <1.0.0", + "react": ">=17.0.0", + "react-native-nitro-sqlite": ">=10.3.0", + }, + }, "packages/react-native-nitro-sqlite-vec": { "name": "react-native-nitro-sqlite-vec", "version": "10.2.0", @@ -460,12 +483,18 @@ "@craftzdog/react-native-buffer": ["@craftzdog/react-native-buffer@6.1.2", "", { "dependencies": { "ieee754": "^1.2.1", "react-native-quick-base64": "^3.0.0" } }, "sha512-KV1HitN05FHLLDG7Zb/yftDsa+mKBYBzFMQ0PMldvUicq6vWOtAvz9mDavt7Fzozh+WNqORE+yFDkkdWysZ/SA=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@emnapi/core": ["@emnapi/core@1.7.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw=="], "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], @@ -1274,6 +1303,8 @@ "babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="], + "babel-plugin-inline-import": ["babel-plugin-inline-import@3.0.0", "", { "dependencies": { "require-resolve": "0.0.2" } }, "sha512-thnykl4FMb8QjMjVCuZoUmAM7r2mnTn5qJwrryCvDv6rugbJlTHZMctdjDtEgD0WBAXJOLJSGXN3loooEwx7UQ=="], + "babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="], "babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@29.6.3", "", { "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" } }, "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg=="], @@ -1542,6 +1573,10 @@ "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], + "drizzle-kit": ["drizzle-kit@0.31.11", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-YCYqxTLIB2OCqf6w9/Vef13baBVbwQIPcbT4Y56AfO6B9ajZhDhD8Jkveg/hJvT/iuMloKD/IYYkyMMNhr7Kqg=="], + + "drizzle-orm": ["drizzle-orm@0.45.4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@netlify/db": ">=0.4.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@netlify/db", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-NiM1YZpJ2UxyeE67DpDDZ+Tsw4YLX3trSlkNT7Tm9eKoUNb1LvaAaQzXgYQsWs+AQ9TUmmwMmBOCXuqqxUWKKQ=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], @@ -1776,6 +1811,8 @@ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + "get-tsconfig": ["get-tsconfig@4.14.3", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA=="], + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], @@ -2484,6 +2521,8 @@ "path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="], + "path-extra": ["path-extra@1.0.3", "", {}, "sha512-vYm3+GCkjUlT1rDvZnDVhNLXIRvwFPaN8ebHAFcuMJM/H0RBOPD7JrcldiNLd9AS3dhAyUHLa4Hny5wp1A+Ffw=="], + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -2586,6 +2625,8 @@ "react-native-nitro-sqlite": ["react-native-nitro-sqlite@workspace:packages/react-native-nitro-sqlite"], + "react-native-nitro-sqlite-drizzle": ["react-native-nitro-sqlite-drizzle@workspace:packages/react-native-nitro-sqlite-drizzle"], + "react-native-nitro-sqlite-example": ["react-native-nitro-sqlite-example@workspace:example"], "react-native-nitro-sqlite-macos-example": ["react-native-nitro-sqlite-macos-example@workspace:example/macos"], @@ -2674,6 +2715,8 @@ "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + "require-resolve": ["require-resolve@0.0.2", "", { "dependencies": { "x-path": "^0.0.2" } }, "sha512-eafQVaxdQsWUB8HybwognkdcIdKdQdQBwTxH48FuE6WI0owZGKp63QYr1MRp73PoX0AcyB7MDapZThYUY8FD0A=="], + "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], @@ -2682,6 +2725,8 @@ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], @@ -2900,6 +2945,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsx": ["tsx@4.23.15", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -3044,6 +3091,8 @@ "ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "x-path": ["x-path@0.0.2", "", { "dependencies": { "path-extra": "^1.0.2" } }, "sha512-zQ4WFI0XfJN1uEkkrB19Y4TuXOlHqKSxUJo0Yt+axPjRm8tCG6SJ6+Wo3/+Kjg4c2c8IvBXuJ0uYoshxNn4qMw=="], + "xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="], "xpath": ["xpath@0.0.34", "", {}, "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA=="], @@ -3074,6 +3123,10 @@ "@babel/eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@2.1.0", "", {}, "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + + "@esbuild-kit/core-utils/source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "@google-automations/git-file-utils/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], @@ -3348,6 +3401,8 @@ "del/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "errorhandler/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], "eslint/@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], @@ -3728,6 +3783,8 @@ "react-native-macos-local/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "react-native-nitro-sqlite-drizzle/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "react-native-nitro-sqlite-example/@types/node": ["@types/node@22.19.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA=="], "react-native-nitro-sqlite-macos-example/@react-native-community/cli": ["@react-native-community/cli@19.1.2", "", { "dependencies": { "@react-native-community/cli-clean": "19.1.2", "@react-native-community/cli-config": "19.1.2", "@react-native-community/cli-doctor": "19.1.2", "@react-native-community/cli-server-api": "19.1.2", "@react-native-community/cli-tools": "19.1.2", "@react-native-community/cli-types": "19.1.2", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-b28TLqODMgQRx6f4gbHoHYpnKyFbWzJkIk3+Ggpad/at493KfGQ+WvKg1sts/st8mxzmbk0T6lCc/9A3QoFKkQ=="], @@ -3870,6 +3927,52 @@ "yargs-unparser/is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + + "@esbuild-kit/core-utils/source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "@google-automations/git-file-utils/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -4072,6 +4175,58 @@ "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "drizzle-kit/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "errorhandler/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "errorhandler/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], diff --git a/docs/content/docs/integrations/drizzle.mdx b/docs/content/docs/integrations/drizzle.mdx new file mode 100644 index 00000000..73d3041e --- /dev/null +++ b/docs/content/docs/integrations/drizzle.mdx @@ -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 Migration failed: {error.message} + if (!success) return Migrating... + return Ready +} +``` + +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. diff --git a/docs/content/docs/integrations/meta.json b/docs/content/docs/integrations/meta.json index db829fc7..489578a1 100644 --- a/docs/content/docs/integrations/meta.json +++ b/docs/content/docs/integrations/meta.json @@ -1,4 +1,4 @@ { "title": "Integrations", - "pages": ["typeorm", "vector-search"] + "pages": ["drizzle", "typeorm", "vector-search"] } diff --git a/docs/scripts/check-api.mjs b/docs/scripts/check-api.mjs index d094e1d7..ea0a0163 100644 --- a/docs/scripts/check-api.mjs +++ b/docs/scripts/check-api.mjs @@ -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)) { diff --git a/docs/tsconfig.typedoc.json b/docs/tsconfig.typedoc.json index 85341fd1..a7ada0b1 100644 --- a/docs/tsconfig.typedoc.json +++ b/docs/tsconfig.typedoc.json @@ -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" ] } diff --git a/docs/typedoc.json b/docs/typedoc.json index 58f712bc..211c1002 100644 --- a/docs/typedoc.json +++ b/docs/typedoc.json @@ -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", diff --git a/example/babel.config.shared.js b/example/babel.config.shared.js index bc29acbe..0563b5b7 100644 --- a/example/babel.config.shared.js +++ b/example/babel.config.shared.js @@ -41,6 +41,7 @@ function createExampleBabelConfig(projectRoot, { buffer, crypto } = {}) { ], 'babel-plugin-transform-typescript-metadata', ['@babel/plugin-proposal-decorators', { legacy: true }], + ['inline-import', { extensions: ['.sql'] }], ], } } diff --git a/example/macos/metro.config.js b/example/macos/metro.config.js index 2a125816..f9e645a4 100644 --- a/example/macos/metro.config.js +++ b/example/macos/metro.config.js @@ -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. @@ -78,4 +82,4 @@ const config = { }, } -module.exports = mergeConfig(getDefaultConfig(__dirname), config) +module.exports = mergeConfig(defaultConfig, config) diff --git a/example/macos/package.json b/example/macos/package.json index 62d39c4c..2a7954cd 100644 --- a/example/macos/package.json +++ b/example/macos/package.json @@ -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", @@ -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", @@ -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" } diff --git a/example/macos/scripts/test-macos.mjs b/example/macos/scripts/test-macos.mjs index 43374187..119cd532 100644 --- a/example/macos/scripts/test-macos.mjs +++ b/example/macos/scripts/test-macos.mjs @@ -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 { @@ -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() diff --git a/example/macos/scripts/test-macos.test.mjs b/example/macos/scripts/test-macos.test.mjs index 159d1c61..c23ba880 100644 --- a/example/macos/scripts/test-macos.test.mjs +++ b/example/macos/scripts/test-macos.test.mjs @@ -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 () => { @@ -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) diff --git a/example/metro.config.js b/example/metro.config.js index 46f2eb69..56e800b4 100644 --- a/example/metro.config.js +++ b/example/metro.config.js @@ -4,6 +4,7 @@ const pak = require('../package.json') const root = path.resolve(__dirname, '..') const modules = Object.keys({ ...pak.peerDependencies }) +const defaultConfig = getDefaultConfig(__dirname) const config = { projectRoot: __dirname, @@ -16,6 +17,7 @@ const config = { acc[name] = path.join(__dirname, 'node_modules', name) return acc }, {}), + sourceExts: [...defaultConfig.resolver.sourceExts, 'sql'], }, transformer: { @@ -28,4 +30,4 @@ const config = { }, } -module.exports = mergeConfig(getDefaultConfig(__dirname), config) +module.exports = mergeConfig(defaultConfig, config) diff --git a/example/package.json b/example/package.json index 2f54b362..62d2821a 100644 --- a/example/package.json +++ b/example/package.json @@ -28,12 +28,14 @@ "@react-navigation/native-stack": "^7.21.0", "chai": "^4.3.7", "chance": "^1.1.9", + "drizzle-orm": "0.45.4", "events": "^3.3.0", "expo-status-bar": "^1.12.1", "react": "19.3.1", "react-native": "0.88.0", "react-native-nitro-modules": "*", "react-native-nitro-sqlite": "10.2.0", + "react-native-nitro-sqlite-drizzle": "10.2.0", "react-native-nitro-sqlite-vec": "10.2.0", "react-native-quick-base64": "^3.0.1", "react-native-quick-crypto": "^1.1.8", @@ -67,6 +69,7 @@ "@types/node": "^22.7.4", "@types/react": "^19.3.0", "@types/react-test-renderer": "^19.3.0", + "babel-plugin-inline-import": "3.0.0", "mocha": "^12.0.2", "postinstall-postinstall": "^2.1.0", "react-native-harness": "1.6.0", diff --git a/example/tests/harness/drizzle.harness.tsx b/example/tests/harness/drizzle.harness.tsx new file mode 100644 index 00000000..1925ada0 --- /dev/null +++ b/example/tests/harness/drizzle.harness.tsx @@ -0,0 +1,33 @@ +import '@/initGlobals' +import React from 'react' +import { describe, expect, it, render, waitUntil } from 'react-native-harness' +import { open } from 'react-native-nitro-sqlite' +import { drizzle, useMigrations } from 'react-native-nitro-sqlite-drizzle' +import migrations from 'react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle/migrations' +import { registerDrizzleUnitTests } from '@tests/unit' +import init from './init' + +init() +registerDrizzleUnitTests() + +describe('useMigrations', () => { + it('reports success once the migrations ran', async () => { + const connection = open({ name: 'test_drizzle_hook' }) + const db = drizzle(connection) + let state: ReturnType | undefined + function Migrations() { + state = useMigrations(db, migrations) + return null + } + + await render() + await waitUntil(() => state?.success) + + expect( + connection.execute("SELECT name FROM sqlite_master WHERE name = 'users'") + .results, + ).toEqual([{ name: 'users' }]) + connection.close() + connection.delete() + }) +}) diff --git a/example/tests/runAll.ts b/example/tests/runAll.ts index 428a4acc..b75b1aad 100644 --- a/example/tests/runAll.ts +++ b/example/tests/runAll.ts @@ -1,6 +1,7 @@ import type { MochaTestResult } from './MochaSetup' import { runTests } from './MochaSetup' import { + registerDrizzleUnitTests, registerSqliteVecUnitTests, registerTypeORMUnitTests, registerUnitTests, @@ -14,6 +15,7 @@ export async function runAllTests(): Promise { registerUnitTests, registerTypeORMUnitTests, registerSqliteVecUnitTests, + registerDrizzleUnitTests, ) return results diff --git a/example/tests/unit/index.ts b/example/tests/unit/index.ts index 977ca3cf..c5b00969 100644 --- a/example/tests/unit/index.ts +++ b/example/tests/unit/index.ts @@ -9,6 +9,7 @@ import registerPreparedStatementUnitTests from './specs/operations/preparedState import registerTypeORMUnitTestsSpecs from './specs/typeorm.spec' import registerDatabaseQueueUnitTests from './specs/DatabaseQueue.spec' import registerSqliteVecUnitTestsSpecs from './specs/sqlite-vec.spec' +import registerDrizzleUnitTestsSpecs from './specs/drizzle.spec' import registerConnectionUnitTests from './specs/Connections.spec' import registerBenchmarkDatabaseUnitTests from './specs/benchmarkDatabase.spec' @@ -36,3 +37,7 @@ export function registerTypeORMUnitTests() { export function registerSqliteVecUnitTests() { describe('sqlite-vec.spec.ts', registerSqliteVecUnitTestsSpecs) } + +export function registerDrizzleUnitTests() { + describe('drizzle.spec.ts', registerDrizzleUnitTestsSpecs) +} diff --git a/example/tests/unit/specs/DatabaseQueue.spec.ts b/example/tests/unit/specs/DatabaseQueue.spec.ts index 930e11f2..135588b3 100644 --- a/example/tests/unit/specs/DatabaseQueue.spec.ts +++ b/example/tests/unit/specs/DatabaseQueue.spec.ts @@ -106,50 +106,49 @@ export default function registerDatabaseQueueUnitTests() { expect(testDbQueue.inProgress).toBe(false) }) - it('mixed transactions and executeBatchAsync operations are queued', async () => { + it('runs mixed transactions and async batches in submission order', async () => { + testDb.execute('CREATE TABLE MixedQueueOrder (value INTEGER)') const transaction1Promise = testDb.transaction(async (tx) => { - tx.execute('SELECT * FROM [User];') + tx.execute('INSERT INTO MixedQueueOrder VALUES (1)') }) expect(testDbQueue.queue.length).toBe(0) expect(testDbQueue.inProgress).toBe(true) - const executeBatch1Promise = testDb.executeBatchAsync(TEST_BATCH_COMMANDS) + const executeBatch1Promise = testDb.executeBatchAsync([ + { query: 'INSERT INTO MixedQueueOrder VALUES (2)' }, + ]) expect(testDbQueue.queue.length).toBe(1) expect(testDbQueue.inProgress).toBe(true) const transaction2Promise = testDb.transaction(async (tx) => { - tx.execute(TEST_QUERY) + tx.execute('INSERT INTO MixedQueueOrder VALUES (3)') }) expect(testDbQueue.queue.length).toBe(2) expect(testDbQueue.inProgress).toBe(true) - const executeBatch2Promise = testDb.executeBatchAsync(TEST_BATCH_COMMANDS) + const executeBatch2Promise = testDb.executeBatchAsync([ + { query: 'INSERT INTO MixedQueueOrder VALUES (4)' }, + ]) expect(testDbQueue.queue.length).toBe(3) expect(testDbQueue.inProgress).toBe(true) - await transaction1Promise - - expect(testDbQueue.queue.length).toBe(2) - expect(testDbQueue.inProgress).toBe(true) - - await executeBatch1Promise - - expect(testDbQueue.queue.length).toBe(1) - expect(testDbQueue.inProgress).toBe(true) - - await transaction2Promise - - expect(testDbQueue.queue.length).toBe(0) - expect(testDbQueue.inProgress).toBe(true) - - await executeBatch2Promise + await Promise.all([ + transaction1Promise, + executeBatch1Promise, + transaction2Promise, + executeBatch2Promise, + ]) expect(testDbQueue.queue.length).toBe(0) expect(testDbQueue.inProgress).toBe(false) + expect( + testDb.execute('SELECT value FROM MixedQueueOrder ORDER BY rowid') + .results, + ).toEqual([{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }]) }) it('errors are thrown through DatabaseQueue', async () => { diff --git a/example/tests/unit/specs/drizzle.spec.ts b/example/tests/unit/specs/drizzle.spec.ts new file mode 100644 index 00000000..a1d487e3 --- /dev/null +++ b/example/tests/unit/specs/drizzle.spec.ts @@ -0,0 +1,126 @@ +import { afterEach, beforeEach, it } from '@tests/TestApi' +import { expect } from '@tests/unit/common' +import { eq, sql } from 'drizzle-orm' +import { blob, sqliteTable } from 'drizzle-orm/sqlite-core' +import { open } from 'react-native-nitro-sqlite' +import type { NitroSQLiteConnection } from 'react-native-nitro-sqlite' +import { drizzle, migrate } from 'react-native-nitro-sqlite-drizzle' +import type { NitroSQLiteDatabase } from 'react-native-nitro-sqlite-drizzle' +import migrations from 'react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle/migrations' +import { + pets, + users, +} from 'react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/schema' + +const DB_NAME = 'test_drizzle' +let connection: NitroSQLiteConnection +let db: NitroSQLiteDatabase + +export default function registerDrizzleUnitTests() { + beforeEach(async () => { + connection = open({ name: DB_NAME }) + db = drizzle(connection) + await migrate(db, migrations) + }) + + afterEach(() => { + connection.close() + connection.delete() + }) + + it('applies drizzle-kit migrations once', async () => { + await migrate(db, migrations) + + expect( + connection.execute('SELECT created_at FROM __drizzle_migrations').results, + ).toEqual( + migrations.journal.entries.map((entry) => ({ created_at: entry.when })), + ) + }) + + it('round trips blob views and JSON through native bindings', async () => { + connection.execute('CREATE TABLE files (content BLOB, metadata BLOB)') + const files = sqliteTable('files', { + content: blob('content', { mode: 'buffer' }).$type(), + metadata: blob('metadata', { mode: 'json' }), + }) + const bytes = Buffer.from([99, 1, 2, 88]).subarray(1, 3) + const metadata = { name: 'receipt.jpg' } + + await db.insert(files).values({ content: bytes, metadata }) + + expect(await db.select().from(files)).toEqual([ + { content: Buffer.from([1, 2]), metadata }, + ]) + }) + + it('keeps both sides of a join whose columns share names', async () => { + await db.insert(users).values([ + { id: 1, name: 'Dave' }, + { id: 2, name: 'Ritesh' }, + ]) + await db.insert(pets).values({ id: 7, name: 'Rex', ownerId: 1 }) + + expect( + await db + .select() + .from(users) + .leftJoin(pets, eq(pets.ownerId, users.id)) + .orderBy(users.id), + ).toEqual([ + { + users: { id: 1, name: 'Dave' }, + pets: { id: 7, name: 'Rex', ownerId: 1 }, + }, + { users: { id: 2, name: 'Ritesh' }, pets: null }, + ]) + }) + + it('runs raw SQL and prepared queries', async () => { + await db.run(sql`INSERT INTO users (id, name) VALUES (1, 'Dave')`) + + expect(await db.all(sql`SELECT name FROM users`)).toEqual([ + { name: 'Dave' }, + ]) + expect(await db.values(sql`SELECT id, name FROM users`)).toEqual([ + [1, 'Dave'], + ]) + + const byId = db + .select() + .from(users) + .where(eq(users.id, sql.placeholder('id'))) + .prepare() + expect(await byId.get({ id: 1 })).toEqual({ id: 1, name: 'Dave' }) + expect(await byId.get({ id: 2 })).toBe(undefined) + }) + + it('commits transactions and rolls back savepoints', async () => { + await db.transaction(async (tx) => { + await tx.insert(users).values({ id: 1, name: 'Dave' }) + await tx + .transaction(async (nested) => { + await nested.insert(users).values({ id: 2, name: 'Ritesh' }) + nested.rollback() + }) + .catch(() => undefined) + }) + + expect(await db.select().from(users)).toEqual([{ id: 1, name: 'Dave' }]) + }) + + it('rolls back when tx.rollback() is called', async () => { + let error: unknown + try { + await db.transaction(async (tx) => { + await tx.insert(users).values({ id: 1, name: 'Dave' }) + tx.rollback() + }) + } catch (e) { + error = e + } + + expect((error as Error).message).toBe('Rollback') + expect(await db.select().from(users)).toEqual([]) + }) +} diff --git a/example/tsconfig.json b/example/tsconfig.json index 91542814..79e27c6c 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -1,12 +1,21 @@ { "extends": "../config/tsconfig.json", "include": ["src", "tests", "index.js", "rn-harness.config.mjs"], - "references": [{ "path": "../packages/react-native-nitro-sqlite" }], + "references": [ + { "path": "../packages/react-native-nitro-sqlite" }, + { "path": "../packages/react-native-nitro-sqlite-drizzle" } + ], "compilerOptions": { "paths": { "react-native-nitro-sqlite": [ "../packages/react-native-nitro-sqlite/src/index.ts" ], + "react-native-nitro-sqlite-drizzle": [ + "../packages/react-native-nitro-sqlite-drizzle/src/index.ts" + ], + "react-native-nitro-sqlite-drizzle/*": [ + "../packages/react-native-nitro-sqlite-drizzle/*" + ], "@nitro-sqlite/*": ["../packages/react-native-nitro-sqlite/src/*"], "@/*": ["./src/*"], "@tests/*": ["./tests/*"] diff --git a/packages/react-native-nitro-sqlite-drizzle/.eslintrc.js b/packages/react-native-nitro-sqlite-drizzle/.eslintrc.js new file mode 100644 index 00000000..07a980a5 --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + root: true, + extends: '../../config/.eslintrc.js', + ignorePatterns: ['src/__tests__/fixtures/drizzle'], + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + }, +} diff --git a/packages/react-native-nitro-sqlite-drizzle/README.md b/packages/react-native-nitro-sqlite-drizzle/README.md new file mode 100644 index 00000000..c0dd4a07 --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/README.md @@ -0,0 +1,31 @@ +# react-native-nitro-sqlite-drizzle + +[Drizzle ORM](https://orm.drizzle.team) driver for [`react-native-nitro-sqlite`](https://github.com/margelo/react-native-nitro-sqlite). + +## Installation + +```bash +npm install react-native-nitro-sqlite react-native-nitro-sqlite-drizzle drizzle-orm +``` + +## Usage + +```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' + +const users = sqliteTable('users', { + id: integer('id').primaryKey(), + name: text('name').notNull(), +}) + +const db = drizzle(open({ name: 'app.sqlite' })) + +await db.insert(users).values({ id: 1, name: 'Ada' }) +const rows = await db.select().from(users) +``` + +Every query is asynchronous. Inside `db.transaction()`, run queries through `tx`: a query awaited through `db` waits for the transaction and never completes. + +See the [Drizzle ORM guide](https://sqlite.margelo.com/docs/integrations/drizzle) for migrations with Drizzle Kit, `useMigrations`, and transactions. diff --git a/packages/react-native-nitro-sqlite-drizzle/babel.config.js b/packages/react-native-nitro-sqlite-drizzle/babel.config.js new file mode 100644 index 00000000..6eb8c4d0 --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ['module:@react-native/babel-preset'], + plugins: [['inline-import', { extensions: ['.sql'] }]], +} diff --git a/packages/react-native-nitro-sqlite-drizzle/package.json b/packages/react-native-nitro-sqlite-drizzle/package.json new file mode 100644 index 00000000..ea7fb1c0 --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/package.json @@ -0,0 +1,63 @@ +{ + "name": "react-native-nitro-sqlite-drizzle", + "version": "10.2.0", + "description": "Drizzle ORM driver for react-native-nitro-sqlite.", + "source": "./src/index", + "react-native": "./src/index", + "main": "./src/index", + "types": "./src/index", + "files": [ + "src", + "!**/__tests__", + "README.md" + ], + "scripts": { + "test": "jest", + "typecheck": "tsc --build", + "lint": "eslint \"**/*.{js,ts,tsx}\" --fix", + "prepublishOnly": "bun typecheck" + }, + "keywords": [ + "react-native", + "sqlite", + "drizzle", + "drizzle-orm", + "orm", + "nitro-modules", + "ios", + "android" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/margelo/react-native-nitro-sqlite.git" + }, + "author": "Christoph Pader", + "license": "MIT", + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + }, + "peerDependencies": { + "drizzle-orm": ">=0.45.4 <1.0.0", + "react": ">=17.0.0", + "react-native-nitro-sqlite": ">=10.3.0" + }, + "devDependencies": { + "babel-plugin-inline-import": "3.0.0", + "drizzle-kit": "0.31.11", + "drizzle-orm": "0.45.4", + "react": "19.3.1", + "react-native-nitro-sqlite": "10.2.0", + "typescript": "^5.8.3" + }, + "jest": { + "preset": "@react-native/jest-preset", + "watchman": false, + "moduleNameMapper": { + "^react-native-nitro-sqlite/mock$": "/../react-native-nitro-sqlite/src/mock.ts" + }, + "testMatch": [ + "/src/__tests__/**/*.test.ts" + ] + } +} diff --git a/packages/react-native-nitro-sqlite-drizzle/src/__tests__/driver.test.ts b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/driver.test.ts new file mode 100644 index 00000000..bb6c5bbe --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/driver.test.ts @@ -0,0 +1,137 @@ +import { eq, sql } from 'drizzle-orm' +import { blob, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' +import { open, resetAllDatabases } from 'react-native-nitro-sqlite/mock' +import type { NitroSQLiteConnection } from 'react-native-nitro-sqlite' +import { drizzle } from '../index' + +const users = sqliteTable('users', { + id: integer('id').primaryKey(), + name: text('name').notNull(), +}) + +const pets = sqliteTable('pets', { + id: integer('id').primaryKey(), + name: text('name').notNull(), + ownerId: integer('owner_id'), +}) + +function createDatabase() { + const connection = open({ name: 'drizzle' }) as NitroSQLiteConnection + connection.execute( + 'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)', + ) + connection.execute( + 'CREATE TABLE pets (id INTEGER PRIMARY KEY, name TEXT NOT NULL, owner_id INTEGER)', + ) + return drizzle(connection) +} + +afterEach(resetAllDatabases) + +describe('drizzle', () => { + it('binds blob views as ArrayBuffers containing only their bytes', async () => { + const db = createDatabase() + db.$client.execute('CREATE TABLE files (content BLOB, metadata BLOB)') + const files = sqliteTable('files', { + content: blob('content', { mode: 'buffer' }), + metadata: blob('metadata', { mode: 'json' }), + }) + const execute = jest.spyOn(db.$client, 'executeRawAsync') + const bytes = Buffer.from([99, 1, 2, 88]).subarray(1, 3) + const metadata = { name: 'receipt.jpg' } + + await db.insert(files).values({ content: bytes, metadata }) + + const params = execute.mock.calls[0]?.[1] + expect(params?.[0]).toBeInstanceOf(ArrayBuffer) + expect(params?.[1]).toBeInstanceOf(ArrayBuffer) + expect(params?.[0]).toEqual(Uint8Array.from([1, 2]).buffer) + expect(await db.select().from(files)).toEqual([ + { content: Buffer.from([1, 2]), metadata }, + ]) + execute.mockRestore() + }) + + it('inserts and selects rows', async () => { + const db = createDatabase() + await db.insert(users).values([ + { id: 1, name: 'Dave' }, + { id: 2, name: 'Ritesh' }, + ]) + + expect(await db.select().from(users)).toEqual([ + { id: 1, name: 'Dave' }, + { id: 2, name: 'Ritesh' }, + ]) + expect(await db.select().from(users).where(eq(users.id, 2)).get()).toEqual({ + id: 2, + name: 'Ritesh', + }) + }) + + it('keeps both sides of a join whose columns share names', async () => { + const db = createDatabase() + await db.insert(users).values([ + { id: 1, name: 'Dave' }, + { id: 2, name: 'Ritesh' }, + ]) + await db.insert(pets).values({ id: 7, name: 'Rex', ownerId: 1 }) + + expect( + await db + .select() + .from(users) + .leftJoin(pets, eq(pets.ownerId, users.id)) + .orderBy(users.id), + ).toEqual([ + { + users: { id: 1, name: 'Dave' }, + pets: { id: 7, name: 'Rex', ownerId: 1 }, + }, + { users: { id: 2, name: 'Ritesh' }, pets: null }, + ]) + }) + + it('runs raw SQL and prepared queries', async () => { + const db = createDatabase() + await db.run(sql`INSERT INTO users (id, name) VALUES (1, 'Dave')`) + + expect(await db.all(sql`SELECT name FROM users`)).toEqual([ + { name: 'Dave' }, + ]) + expect(await db.get(sql`SELECT name FROM users`)).toEqual({ name: 'Dave' }) + expect(await db.values(sql`SELECT id, name FROM users`)).toEqual([ + [1, 'Dave'], + ]) + + const byId = db + .select() + .from(users) + .where(eq(users.id, sql.placeholder('id'))) + .prepare() + expect(await byId.get({ id: 1 })).toEqual({ id: 1, name: 'Dave' }) + expect(await byId.get({ id: 2 })).toBeUndefined() + }) + + it('commits transactions and rolls back savepoints and failures', async () => { + const db = createDatabase() + await db.transaction(async (tx) => { + await tx.insert(users).values({ id: 1, name: 'Dave' }) + await tx + .transaction(async (nested) => { + await nested.insert(users).values({ id: 2, name: 'Ritesh' }) + nested.rollback() + }) + .catch(() => undefined) + }) + expect(await db.select().from(users)).toEqual([{ id: 1, name: 'Dave' }]) + + await expect( + db.transaction(async (tx) => { + await tx.insert(users).values({ id: 3, name: 'Ada' }) + tx.rollback() + }), + ).rejects.toThrow('Rollback') + expect(await db.select().from(users)).toEqual([{ id: 1, name: 'Dave' }]) + }) +}) diff --git a/packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle.config.ts b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle.config.ts new file mode 100644 index 00000000..42f0aab3 --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'drizzle-kit' + +export default defineConfig({ + schema: './src/__tests__/fixtures/schema.ts', + out: './src/__tests__/fixtures/drizzle', + dialect: 'sqlite', + driver: 'expo', +}) diff --git a/packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle/0000_init.sql b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle/0000_init.sql new file mode 100644 index 00000000..c07176fa --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle/0000_init.sql @@ -0,0 +1,12 @@ +CREATE TABLE `pets` ( + `id` integer PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `owner_id` integer, + FOREIGN KEY (`owner_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE INDEX `pets_owner_id` ON `pets` (`owner_id`);--> statement-breakpoint +CREATE TABLE `users` ( + `id` integer PRIMARY KEY NOT NULL, + `name` text NOT NULL +); diff --git a/packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle/meta/_journal.json b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle/meta/_journal.json new file mode 100644 index 00000000..6802c430 --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1792274745571, + "tag": "0000_init", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle/migrations.js b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle/migrations.js new file mode 100644 index 00000000..87020f29 --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/drizzle/migrations.js @@ -0,0 +1,12 @@ +// This file is required for Expo/React Native SQLite migrations - https://orm.drizzle.team/quick-sqlite/expo + +import journal from './meta/_journal.json'; +import m0000 from './0000_init.sql'; + + export default { + journal, + migrations: { + m0000 + } + } + \ No newline at end of file diff --git a/packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/schema.ts b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/schema.ts new file mode 100644 index 00000000..2fcc915a --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/fixtures/schema.ts @@ -0,0 +1,16 @@ +import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' + +export const users = sqliteTable('users', { + id: integer('id').primaryKey(), + name: text('name').notNull(), +}) + +export const pets = sqliteTable( + 'pets', + { + id: integer('id').primaryKey(), + name: text('name').notNull(), + ownerId: integer('owner_id').references(() => users.id), + }, + (table) => [index('pets_owner_id').on(table.ownerId)], +) diff --git a/packages/react-native-nitro-sqlite-drizzle/src/__tests__/migrator.test.ts b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/migrator.test.ts new file mode 100644 index 00000000..21dcf2f5 --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/migrator.test.ts @@ -0,0 +1,95 @@ +import { eq } from 'drizzle-orm' +import { open, resetAllDatabases } from 'react-native-nitro-sqlite/mock' +import type { NitroSQLiteConnection } from 'react-native-nitro-sqlite' +import { drizzle, migrate } from '../index' +import migrations from './fixtures/drizzle/migrations' +import { pets, users } from './fixtures/schema' + +afterEach(resetAllDatabases) + +describe('migrate', () => { + it('serializes concurrent migrations across wrappers sharing a connection', async () => { + const connection = open({ + name: 'concurrent-migrate', + }) as NitroSQLiteConnection + const db = drizzle(connection) + + await Promise.all([ + migrate(db, migrations), + migrate(drizzle(connection), migrations), + migrate(db, migrations), + ]) + + expect( + connection.execute('SELECT created_at FROM __drizzle_migrations').results, + ).toEqual( + migrations.journal.entries.map((entry) => ({ created_at: entry.when })), + ) + }) + + it('rolls back a failed migration and lets the next queued call retry', async () => { + const connection = open({ name: 'retry-migrate' }) as NitroSQLiteConnection + const db = drizzle(connection) + const failed = { + ...migrations, + migrations: { + m0000: + 'CREATE TABLE rollback_probe (id integer);--> statement-breakpoint SELECT FROM invalid', + }, + } + + const results = await Promise.allSettled([ + migrate(db, failed), + migrate(db, migrations), + ]) + + expect(results.map((result) => result.status)).toEqual([ + 'rejected', + 'fulfilled', + ]) + expect( + connection.execute( + "SELECT name FROM sqlite_master WHERE name = 'rollback_probe'", + ).results, + ).toEqual([]) + expect( + connection.execute('SELECT created_at FROM __drizzle_migrations').results, + ).toHaveLength(1) + }) + + it('applies each drizzle-kit migration once', async () => { + const connection = open({ name: 'migrate' }) as NitroSQLiteConnection + const db = drizzle(connection) + + await migrate(db, migrations) + await migrate(db, migrations) + + expect( + connection.execute('SELECT created_at FROM __drizzle_migrations').results, + ).toEqual( + migrations.journal.entries.map((entry) => ({ created_at: entry.when })), + ) + expect( + connection.execute( + "SELECT name FROM sqlite_master WHERE name = 'pets_owner_id'", + ).results, + ).toEqual([{ name: 'pets_owner_id' }]) + + await db.insert(users).values({ id: 1, name: 'Dave' }) + await db.insert(pets).values({ id: 7, name: 'Rex', ownerId: 1 }) + expect( + await db + .select({ owner: users.name, pet: pets.name }) + .from(pets) + .innerJoin(users, eq(pets.ownerId, users.id)), + ).toEqual([{ owner: 'Dave', pet: 'Rex' }]) + }) + + it('rejects when a journal entry has no migration', async () => { + const db = drizzle(open({ name: 'migrate' }) as NitroSQLiteConnection) + + await expect( + migrate(db, { ...migrations, migrations: {} }), + ).rejects.toThrow('Missing migration: 0000_init') + }) +}) diff --git a/packages/react-native-nitro-sqlite-drizzle/src/__tests__/useMigrations.test.ts b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/useMigrations.test.ts new file mode 100644 index 00000000..cf40fac3 --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/src/__tests__/useMigrations.test.ts @@ -0,0 +1,86 @@ +import { createElement, StrictMode } from 'react' +import { act, create } from 'react-test-renderer' +import type { ReactTestRenderer } from 'react-test-renderer' +import { open, resetAllDatabases } from 'react-native-nitro-sqlite/mock' +import type { NitroSQLiteConnection } from 'react-native-nitro-sqlite' +import { drizzle, useMigrations } from '../index' +import type { MigrationState, NitroSQLiteDatabase } from '../index' +import migrations from './fixtures/drizzle/migrations' + +let renderer: ReactTestRenderer | undefined + +afterEach(async () => { + await act(async () => renderer?.unmount()) + renderer = undefined + jest.restoreAllMocks() + resetAllDatabases() +}) + +it('finishes migrations after a Strict Mode effect replay', async () => { + const connection = open({ name: 'strict-migrate' }) as NitroSQLiteConnection + const db = drizzle(connection) + let state: MigrationState | undefined + function Migrations() { + state = useMigrations(db, migrations) + return null + } + + await act(async () => { + renderer = create( + createElement(StrictMode, null, createElement(Migrations)), + ) + }) + + expect(state).toEqual({ success: true }) + expect( + connection.execute('SELECT created_at FROM __drizzle_migrations').results, + ).toHaveLength(1) +}) + +it('ignores an earlier database result while the current migration is pending', async () => { + const firstDb = drizzle( + open({ name: 'first-migrate' }) as NitroSQLiteConnection, + ) + const nextDb = drizzle( + open({ name: 'next-migrate' }) as NitroSQLiteConnection, + ) + const first = Promise.withResolvers() + const next = Promise.withResolvers() + jest.spyOn(firstDb.dialect, 'migrate').mockReturnValue(first.promise) + jest.spyOn(nextDb.dialect, 'migrate').mockReturnValue(next.promise) + let state: MigrationState | undefined + function Migrations({ db }: { db: NitroSQLiteDatabase }) { + state = useMigrations(db, migrations) + return null + } + + await act(async () => { + renderer = create(createElement(Migrations, { db: firstDb })) + }) + await act(async () => { + renderer?.update(createElement(Migrations, { db: nextDb })) + }) + await act(async () => first.resolve()) + + expect(state).toEqual({ success: false }) + + await act(async () => next.resolve()) + expect(state).toEqual({ success: true }) +}) + +it('reports a migration failure', async () => { + const db = drizzle(open({ name: 'failed-migrate' }) as NitroSQLiteConnection) + const missing = { ...migrations, migrations: {} } + let state: MigrationState | undefined + function Migrations() { + state = useMigrations(db, missing) + return null + } + + await act(async () => { + renderer = create(createElement(Migrations)) + }) + + expect(state?.success).toBe(false) + expect(state?.error?.message).toBe('Missing migration: 0000_init') +}) diff --git a/packages/react-native-nitro-sqlite-drizzle/src/driver.ts b/packages/react-native-nitro-sqlite-drizzle/src/driver.ts new file mode 100644 index 00000000..136c4201 --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/src/driver.ts @@ -0,0 +1,87 @@ +import { + createTableRelationsHelpers, + DefaultLogger, + entityKind, + extractTablesRelationalConfig, +} from 'drizzle-orm' +import type { + DrizzleConfig, + RelationalSchemaConfig, + TablesRelationalConfig, +} from 'drizzle-orm' +import { BaseSQLiteDatabase, SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core' +import type { + NitroSQLiteConnection, + NitroSQLiteRawQueryResult, +} from 'react-native-nitro-sqlite' +import { NitroSQLiteSession } from './session' + +/** + * Asynchronous Drizzle database backed by an open Nitro SQLite connection. + * Create it with {@link drizzle}; queries and prepared queries remain usable + * until you close the connection. The database does not own or close it. + * Use the callback's transaction handle for every query inside a transaction. + * @typeParam TSchema Tables and relations supplied in the Drizzle configuration. + */ +export class NitroSQLiteDatabase< + TSchema extends Record = Record, +> extends BaseSQLiteDatabase<'async', NitroSQLiteRawQueryResult, TSchema> { + /** @internal Drizzle entity discriminator. */ + static override readonly [entityKind]: string = 'NitroSQLiteDatabase' + + /** Connection attached by {@link drizzle}. Close it after all queries finish. */ + $client?: NitroSQLiteConnection +} + +/** + * Create a Drizzle database whose queries run asynchronously on a Nitro SQLite connection. + * Creating the wrapper does not open, migrate, or close the connection. + * @typeParam TSchema Tables and relations supplied in `config.schema`. + * @param connection Connection returned by `open()`. + * @param config Drizzle options such as `schema`, `logger`, and `casing`. + * @returns A database whose `$client` is the supplied connection. + * @throws If Drizzle cannot read the supplied schema configuration. + * Query promises reject on binding, SQLite, or closed-connection errors. + */ +export function drizzle< + TSchema extends Record = Record, +>( + connection: NitroSQLiteConnection, + config: DrizzleConfig = {}, +): NitroSQLiteDatabase & { $client: NitroSQLiteConnection } { + const dialect = new SQLiteAsyncDialect({ casing: config.casing }) + let logger + if (config.logger === true) { + logger = new DefaultLogger() + } else if (config.logger !== false) { + logger = config.logger + } + + let schema: RelationalSchemaConfig | undefined + if (config.schema) { + const tablesConfig = extractTablesRelationalConfig( + config.schema, + createTableRelationsHelpers, + ) + schema = { + fullSchema: config.schema, + schema: tablesConfig.tables, + tableNamesMap: tablesConfig.tableNamesMap, + } + } + + const session = new NitroSQLiteSession( + connection, + connection, + dialect, + schema, + { logger }, + ) + const db = new NitroSQLiteDatabase( + 'async', + dialect, + session, + schema, + ) as NitroSQLiteDatabase + return Object.assign(db, { $client: connection }) +} diff --git a/packages/react-native-nitro-sqlite-drizzle/src/drizzle-orm.ts b/packages/react-native-nitro-sqlite-drizzle/src/drizzle-orm.ts new file mode 100644 index 00000000..3bc1e376 --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/src/drizzle-orm.ts @@ -0,0 +1,60 @@ +// drizzle-orm 0.45 ships these at runtime but marks them internal, so its type declarations omit them. +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-shadow -- merged declarations must repeat drizzle-orm's type parameters */ +import type { + AnyColumn, + ExtractTablesWithRelations, + SelectedFieldsOrdered, + TablesRelationalConfig, +} from 'drizzle-orm' +import type { + SQLiteAsyncDialect, + SQLiteSession, + SQLiteSyncDialect, +} from 'drizzle-orm/sqlite-core' + +declare module 'drizzle-orm/utils' { + export function mapResultRow( + columns: SelectedFieldsOrdered, + row: unknown[], + joinsNotNullableMap: Record | undefined, + ): TResult +} + +declare module 'drizzle-orm/sqlite-core/session' { + interface SQLitePreparedQuery { + joinsNotNullableMap?: Record + } + + interface SQLiteSession< + TResultKind extends 'sync' | 'async', + TRunResult, + TFullSchema extends Record, + TSchema extends TablesRelationalConfig, + > { + readonly dialect: { + sync: SQLiteSyncDialect + async: SQLiteAsyncDialect + }[TResultKind] + } +} + +declare module 'drizzle-orm/sqlite-core/db' { + interface BaseSQLiteDatabase< + TResultKind extends 'sync' | 'async', + TRunResult, + TFullSchema extends Record = Record, + TSchema extends + TablesRelationalConfig = ExtractTablesWithRelations, + > { + readonly dialect: { + sync: SQLiteSyncDialect + async: SQLiteAsyncDialect + }[TResultKind] + readonly session: SQLiteSession< + TResultKind, + TRunResult, + TFullSchema, + TSchema + > + } +} diff --git a/packages/react-native-nitro-sqlite-drizzle/src/index.ts b/packages/react-native-nitro-sqlite-drizzle/src/index.ts new file mode 100644 index 00000000..c971bad2 --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/src/index.ts @@ -0,0 +1,9 @@ +export { drizzle, NitroSQLiteDatabase } from './driver' +export { migrate, useMigrations } from './migrator' +export type { MigrationConfig, MigrationState } from './migrator' +export { + NitroSQLitePreparedQuery, + NitroSQLiteSession, + NitroSQLiteTransaction, +} from './session' +export type { NitroSQLiteSessionOptions } from './session' diff --git a/packages/react-native-nitro-sqlite-drizzle/src/migrator.ts b/packages/react-native-nitro-sqlite-drizzle/src/migrator.ts new file mode 100644 index 00000000..a54af385 --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/src/migrator.ts @@ -0,0 +1,112 @@ +import { useEffect, useState } from 'react' +import type { MigrationMeta } from 'drizzle-orm/migrator' +import type { NitroSQLiteDatabase } from './driver' +import './drizzle-orm' + +const pendingMigrations = new WeakMap>() + +/** Bundled SQL and journal metadata exported by drizzle-kit's `migrations.js`. */ +export interface MigrationConfig { + /** Generated journal listing migrations in application order. */ + journal: { + /** Migration indexes, creation timestamps, file tags, and statement-breakpoint settings. */ + entries: { idx: number; when: number; tag: string; breakpoints: boolean }[] + } + /** SQL strings keyed by the journal index, such as `m0000`. */ + migrations: Record +} + +/** + * Apply the migrations that drizzle-kit generated and that have not run on this database yet. + * Pending migrations run in one transaction and roll back together on failure. + * Calls sharing a connection wait for earlier calls before reading migration history. + * A failed call does not prevent later calls from retrying. + * @typeParam TSchema Tables and relations supplied to Drizzle. + * @param db Database returned by `drizzle()`. + * @param config The `migrations.js` module that drizzle-kit generates. + * @returns Resolves when all pending migrations finish. + * @throws Rejects if a journal entry has no SQL, or a query or transaction fails. + */ +export async function migrate>( + db: NitroSQLiteDatabase, + config: MigrationConfig, +): Promise { + const connection = db.$client ?? db.session + const previous = pendingMigrations.get(connection) ?? Promise.resolve() + const pending = previous + .catch(() => undefined) + .then(() => db.dialect.migrate(readMigrationFiles(config), db.session)) + pendingMigrations.set(connection, pending) + + try { + await pending + } finally { + if (pendingMigrations.get(connection) === pending) { + pendingMigrations.delete(connection) + } + } +} + +/** Result of the migration attempt for the hook's current inputs. */ +export interface MigrationState { + /** True after all pending migrations finish successfully. */ + success: boolean + /** Failure from the migration attempt; absent while pending or after success. */ + error?: Error +} + +/** + * Run {@linkcode migrate} on mount and when the database or migration bundle changes. + * Concurrent calls on the same connection are serialized, including Strict Mode + * effect replays. Results from earlier inputs or after unmount do not update state. + * @typeParam TSchema Tables and relations supplied to Drizzle. + * @param db Database returned by `drizzle()`. + * @param migrations The `migrations.js` module that drizzle-kit generates. + * @returns `success` once the migrations finished, or the `error` they failed with. + */ +export function useMigrations>( + db: NitroSQLiteDatabase, + migrations: MigrationConfig, +): MigrationState { + const [state, setState] = useState({ success: false }) + + useEffect(() => { + let active = true + setState({ success: false }) + migrate(db, migrations) + .then(() => { + if (active) setState({ success: true }) + }) + .catch((error: unknown) => { + if (active) { + setState({ + success: false, + error: error instanceof Error ? error : new Error(String(error)), + }) + } + }) + return () => { + active = false + } + }, [db, migrations]) + + return state +} + +function readMigrationFiles({ + journal, + migrations, +}: MigrationConfig): MigrationMeta[] { + return journal.entries.map((entry) => { + const query = migrations[`m${entry.idx.toString().padStart(4, '0')}`] + if (!query) { + throw new Error(`Missing migration: ${entry.tag}`) + } + return { + sql: query.split('--> statement-breakpoint'), + bps: entry.breakpoints, + folderMillis: entry.when, + hash: '', + } + }) +} diff --git a/packages/react-native-nitro-sqlite-drizzle/src/session.ts b/packages/react-native-nitro-sqlite-drizzle/src/session.ts new file mode 100644 index 00000000..c078c70b --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/src/session.ts @@ -0,0 +1,344 @@ +import { entityKind, fillPlaceholders, NoopLogger, sql } from 'drizzle-orm' +import type { + Logger, + Query, + RelationalSchemaConfig, + TablesRelationalConfig, +} from 'drizzle-orm' +import { + SQLitePreparedQuery, + SQLiteSession, + SQLiteTransaction, +} from 'drizzle-orm/sqlite-core' +import type { + PreparedQueryConfig, + SelectedFieldsOrdered, + SQLiteAsyncDialect, + SQLiteExecuteMethod, + SQLiteTransactionConfig, +} from 'drizzle-orm/sqlite-core' +import { mapResultRow } from 'drizzle-orm/utils' +import type { + NitroSQLiteConnection, + NitroSQLiteRawQueryResult, + SQLiteQueryParams, + Transaction, +} from 'react-native-nitro-sqlite' +import './drizzle-orm' + +/** Logging configuration shared by a session and its transactions. */ +export interface NitroSQLiteSessionOptions { + /** Receives SQL and bound parameters before execution. Omit to disable logging. */ + logger?: Logger +} + +/** + * Executes Drizzle queries through a Nitro SQLite connection or transaction. + * {@link drizzle} creates sessions for normal use. A session does not close its + * connection; a transaction-bound session is usable only within its callback. + * @typeParam TFullSchema Full schema object supplied to Drizzle. + * @typeParam TSchema Relational table configuration extracted from that schema. + */ +export class NitroSQLiteSession< + TFullSchema extends Record, + TSchema extends TablesRelationalConfig, +> extends SQLiteSession< + 'async', + NitroSQLiteRawQueryResult, + TFullSchema, + TSchema +> { + /** @internal Drizzle entity discriminator. */ + static override readonly [entityKind]: string = 'NitroSQLiteSession' + + private logger: Logger + + /** + * Bind a session to an existing connection or active transaction. + * @param connection Open connection used to start managed transactions. + * @param client The connection, or the active transaction used for queries. + * @param dialect Asynchronous SQLite dialect used to compile queries. + * @param schema Relational schema configuration, or `undefined` when none was supplied. + * @param options Query logging options, inherited by transactions. + */ + constructor( + private connection: NitroSQLiteConnection, + // The connection, or the transaction handle while a transaction runs. + private client: NitroSQLiteConnection | Transaction, + dialect: SQLiteAsyncDialect, + private schema: RelationalSchemaConfig | undefined, + private options: NitroSQLiteSessionOptions = {}, + ) { + super(dialect) + this.logger = options.logger ?? new NoopLogger() + } + + /** + * Create a reusable query wrapper without preparing a native SQLite statement. + * @typeParam T Result types for the query's execution methods. + * @param query Compiled SQL and positional parameters, including placeholders. + * @param fields Ordered selected fields used to decode result rows. + * @param executeMethod Method invoked by the wrapper's `execute()` call. + * @param isResponseInArrayMode Whether Drizzle expects positional rows. + * @param customResultMapper Optional decoder for relational query results. + * @returns A wrapper bound to this session's client. It needs no disposal. + */ + prepareQuery( + query: Query, + fields: SelectedFieldsOrdered | undefined, + executeMethod: SQLiteExecuteMethod, + isResponseInArrayMode: boolean, + customResultMapper?: (rows: unknown[][]) => unknown, + ): NitroSQLitePreparedQuery { + return new NitroSQLitePreparedQuery( + this.client, + query, + this.logger, + fields, + executeMethod, + isResponseInArrayMode, + customResultMapper, + ) + } + + /** + * Hold the connection until an asynchronous transaction callback finishes. + * Await every query through `tx`; awaiting queries through the outer database + * waits for this transaction and deadlocks the callback. + * @typeParam T Callback result type. + * @param transaction Callback that commits on success and rolls back on failure. + * @param _config Accepted for Drizzle compatibility. `behavior` is ignored; + * the connection starts the transaction with `BEGIN`. + * @returns The callback result after the transaction commits. + * @throws Rejects with `NitroSQLiteError` on query, callback, or finalization failure. + * An uncaught `tx.rollback()` rejects with the message `Rollback`. + */ + override async transaction( + transaction: ( + tx: NitroSQLiteTransaction, + ) => Promise, + _config?: SQLiteTransactionConfig, + ): Promise { + return this.connection.transaction(async (nitroTransaction) => { + const session = new NitroSQLiteSession( + this.connection, + nitroTransaction, + this.dialect, + this.schema, + this.options, + ) + const tx = new NitroSQLiteTransaction( + 'async', + this.dialect, + session, + this.schema, + ) + return transaction(tx) + }) + } +} + +/** + * Drizzle transaction handle supplied to a transaction callback. + * Use it for queries and nested transactions only while the callback is active. + * Inherited `rollback()` throws Drizzle's `TransactionRollbackError`; an error + * escaping the outer callback becomes a `NitroSQLiteError` with message `Rollback`. + * @typeParam TFullSchema Full schema object supplied to Drizzle. + * @typeParam TSchema Relational table configuration extracted from that schema. + */ +export class NitroSQLiteTransaction< + TFullSchema extends Record, + TSchema extends TablesRelationalConfig, +> extends SQLiteTransaction< + 'async', + NitroSQLiteRawQueryResult, + TFullSchema, + TSchema +> { + /** @internal Drizzle entity discriminator. */ + static override readonly [entityKind]: string = 'NitroSQLiteTransaction' + + /** + * Run a nested transaction using a SQLite savepoint. + * @typeParam T Nested callback result type. + * @param transaction Callback whose changes roll back to the savepoint if it throws. + * @returns The callback result after releasing the savepoint. + * @throws Rejects with the callback error, or a query error if savepoint operations fail. + * Catch the rejection in the outer callback to continue that transaction. + */ + override async transaction( + transaction: ( + tx: NitroSQLiteTransaction, + ) => Promise, + ): Promise { + const savepointName = `sp${this.nestedIndex}` + const tx = new NitroSQLiteTransaction( + 'async', + this.dialect, + this.session, + this.schema, + this.nestedIndex + 1, + ) + await this.session.run(sql.raw(`savepoint ${savepointName}`)) + try { + const result = await transaction(tx) + await this.session.run(sql.raw(`release savepoint ${savepointName}`)) + return result + } catch (error) { + await this.session.run(sql.raw(`rollback to savepoint ${savepointName}`)) + throw error + } + } +} + +/** + * Reusable compiled Drizzle query with asynchronous execution and row decoding. + * Each execution binds fresh placeholder values. No native prepared statement is + * retained, so no finalization is needed. A transaction-bound query must finish + * before its callback returns; other queries require an open connection. + * @typeParam T Result types supplied by Drizzle for each execution method. + */ +export class NitroSQLitePreparedQuery< + T extends PreparedQueryConfig = PreparedQueryConfig, +> extends SQLitePreparedQuery<{ + type: 'async' + run: NitroSQLiteRawQueryResult + all: T['all'] + get: T['get'] + values: T['values'] + execute: T['execute'] +}> { + /** @internal Drizzle entity discriminator. */ + static override readonly [entityKind]: string = 'NitroSQLitePreparedQuery' + + /** + * Create a query wrapper without executing SQL. + * @param client Open connection or active transaction used for execution. + * @param query Compiled SQL and positional parameters, including placeholders. + * @param logger Receives each query before execution. + * @param fields Ordered selected fields used to decode result rows. + * @param executeMethod Method invoked by `execute()`. + * @param _isResponseInArrayMode Whether Drizzle expects positional rows. + * @param customResultMapper Optional decoder for relational query results. + */ + constructor( + private client: NitroSQLiteConnection | Transaction, + query: Query, + private logger: Logger, + private fields: SelectedFieldsOrdered | undefined, + executeMethod: SQLiteExecuteMethod, + private _isResponseInArrayMode: boolean, + private customResultMapper?: (rows: unknown[][]) => unknown, + ) { + super('async', executeMethod, query) + } + + /** + * Execute SQL and return Nitro SQLite's raw result, including affected rows. + * @param placeholderValues Values for every named placeholder in the query. + * @returns Raw positional rows, column names, and execution metadata. + * @throws Rejects on missing placeholders, invalid bindings, SQLite errors, + * or a closed connection or finalized transaction. + */ + async run( + placeholderValues?: Record, + ): Promise { + return this.client.executeRawAsync( + this.query.sql, + this.bindParams(placeholderValues), + ) + } + + /** + * Execute SQL and decode every row with the selected fields or custom mapper. + * Without either, rows are objects keyed by SQLite column names. + * @param placeholderValues Values for every named placeholder in the query. + * @returns All decoded rows, or the custom mapper's result. + * @throws Rejects on binding, query, or decoding errors, including an inactive client. + */ + async all(placeholderValues?: Record): Promise { + const { fields, customResultMapper } = this + if (!fields && !customResultMapper) { + const result = await this.client.executeAsync( + this.query.sql, + this.bindParams(placeholderValues), + ) + return result.results + } + + const rows = (await this.values(placeholderValues)) as unknown[][] + if (customResultMapper) { + return customResultMapper(rows) as T['all'] + } + return rows.map((row) => + mapResultRow(fields!, row, this.joinsNotNullableMap), + ) + } + + /** + * Execute SQL and decode its first row. + * @param placeholderValues Values for every named placeholder in the query. + * @returns The first decoded row, `undefined` if empty, or the custom mapper's result. + * @throws Rejects on binding, query, or decoding errors, including an inactive client. + */ + async get(placeholderValues?: Record): Promise { + const { fields, customResultMapper } = this + if (!fields && !customResultMapper) { + const result = await this.client.executeAsync( + this.query.sql, + this.bindParams(placeholderValues), + ) + return result.results[0] + } + + const rows = (await this.values(placeholderValues)) as unknown[][] + const row = rows[0] + if (!row) { + return undefined + } + if (customResultMapper) { + return customResultMapper(rows) as T['get'] + } + return mapResultRow(fields!, row, this.joinsNotNullableMap) + } + + /** + * Execute SQL and return positional rows without Drizzle field decoding. + * @param placeholderValues Values for every named placeholder in the query. + * @returns Row arrays in SQLite's selected-column order. + * @throws Rejects on binding or query errors, including an inactive client. + */ + async values( + placeholderValues?: Record, + ): Promise { + const result = await this.client.executeRawAsync( + this.query.sql, + this.bindParams(placeholderValues), + ) + return result.rows + } + + /** @internal */ + isResponseInArrayMode(): boolean { + return this._isResponseInArrayMode + } + + private bindParams( + placeholderValues: Record | undefined, + ): SQLiteQueryParams { + const params = fillPlaceholders( + this.query.params, + placeholderValues ?? {}, + ).map((value) => + ArrayBuffer.isView(value) + ? new Uint8Array( + value.buffer, + value.byteOffset, + value.byteLength, + ).slice().buffer + : value, + ) + this.logger.logQuery(this.query.sql, params) + return params as SQLiteQueryParams + } +} diff --git a/packages/react-native-nitro-sqlite-drizzle/tsconfig.json b/packages/react-native-nitro-sqlite-drizzle/tsconfig.json new file mode 100644 index 00000000..288dd859 --- /dev/null +++ b/packages/react-native-nitro-sqlite-drizzle/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../config/tsconfig.json", + "include": ["src", "src/**/*.json"], + "references": [{ "path": "../react-native-nitro-sqlite" }], + "compilerOptions": { + "types": ["node", "jest"], + "rootDir": "src", + "outDir": ".tsbuild", + "emitDeclarationOnly": true, + "noEmit": false, + "tsBuildInfoFile": ".tsbuild/tsconfig.tsbuildinfo", + "paths": { + "react-native-nitro-sqlite": [ + "../react-native-nitro-sqlite/src/index.ts" + ], + "react-native-nitro-sqlite/mock": [ + "../react-native-nitro-sqlite/src/mock.ts" + ] + } + } +} diff --git a/release-please-config.json b/release-please-config.json index 347f45f5..ba5f2ad5 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -7,7 +7,7 @@ "draft": true, "force-tag-creation": true, "pull-request-title-pattern": "chore: release ${version}", - "pull-request-header": "Publishing requires a matching version across both packages and the example. This release updates their versions together and records the changes below.", + "pull-request-header": "Publishing requires a matching version across all packages and the example. This release updates their versions together and records the changes below.", "changelog-sections": [ { "type": "chore", @@ -55,6 +55,16 @@ "path": "packages/react-native-nitro-sqlite-vec/package.json", "jsonpath": "$.devDependencies['react-native-nitro-sqlite']" }, + { + "type": "json", + "path": "packages/react-native-nitro-sqlite-drizzle/package.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "packages/react-native-nitro-sqlite-drizzle/package.json", + "jsonpath": "$.devDependencies['react-native-nitro-sqlite']" + }, { "type": "json", "path": "example/package.json", @@ -69,6 +79,11 @@ "type": "json", "path": "example/package.json", "jsonpath": "$.dependencies['react-native-nitro-sqlite-vec']" + }, + { + "type": "json", + "path": "example/package.json", + "jsonpath": "$.dependencies['react-native-nitro-sqlite-drizzle']" } ] } diff --git a/scripts/publish-release.test.ts b/scripts/publish-release.test.ts index 8f275324..363dde93 100644 --- a/scripts/publish-release.test.ts +++ b/scripts/publish-release.test.ts @@ -14,7 +14,7 @@ import { fileURLToPath } from 'node:url' const script = fileURLToPath(new URL('./publish-release.ts', import.meta.url)) -test('publishing validates both packages and publishers before publishing in dependency order', () => { +test('publishing validates all packages and publishers before publishing in dependency order', () => { const result = runPublication() assert.equal(result.status, 0, result.stderr) const publish = result.calls.findIndex((line) => line.startsWith('npm:')) @@ -39,11 +39,12 @@ test('publishing validates both packages and publishers before publishing in dep [ 'npm:react-native-nitro-sqlite:publish --access public --provenance --tag latest --workspaces=false', 'npm:react-native-nitro-sqlite-vec:publish --access public --provenance --tag latest --workspaces=false', + 'npm:react-native-nitro-sqlite-drizzle:publish --access public --provenance --tag latest --workspaces=false', ], ) }) -test('validation failure prevents publishing either package', () => { +test('validation failure prevents publishing any package', () => { const result = runPublication({ PUBLISH_TEST_FAIL_VALIDATION: '1' }) assert.notEqual(result.status, 0) assert.equal( @@ -52,7 +53,7 @@ test('validation failure prevents publishing either package', () => { ) }) -test('second trusted publisher failure prevents publishing either package', () => { +test('second trusted publisher failure prevents publishing any package', () => { const result = runPublication({ PUBLISH_TEST_FAIL_OIDC: '1' }) assert.notEqual(result.status, 0) assert.match(result.stderr, /OIDC exchange failed/) @@ -66,8 +67,9 @@ test('retry skips a package already published from the selected commit', () => { const result = runPublication({ PUBLISH_TEST_CORE_EXISTS: '1' }) assert.equal(result.status, 0, result.stderr) const published = result.calls.filter((line) => line.startsWith('npm:')) - assert.equal(published.length, 1) + assert.equal(published.length, 2) assert.match(published[0]!, /^npm:react-native-nitro-sqlite-vec:/) + assert.match(published[1]!, /^npm:react-native-nitro-sqlite-drizzle:/) }) function runPublication(extraEnv: Record = {}) { @@ -84,6 +86,7 @@ function runPublication(extraEnv: Record = {}) { 'example', 'packages/react-native-nitro-sqlite', 'packages/react-native-nitro-sqlite-vec', + 'packages/react-native-nitro-sqlite-drizzle', ]) { mkdirSync(join(project, path), { recursive: true }) writeFileSync( @@ -94,6 +97,7 @@ function runPublication(extraEnv: Record = {}) { dependencies: { 'react-native-nitro-sqlite': '1.0.0', 'react-native-nitro-sqlite-vec': '1.0.0', + 'react-native-nitro-sqlite-drizzle': '1.0.0', }, }), ) @@ -129,7 +133,7 @@ function runPublication(extraEnv: Record = {}) { 'globalThis.fetch = async (input) => {', ' const url = String(input)', " if (url.startsWith('https://oidc.example.invalid')) return Response.json({ value: 'fixture-identity' })", - " const name = url.includes('react-native-nitro-sqlite-vec') ? 'react-native-nitro-sqlite-vec' : 'react-native-nitro-sqlite'", + " const name = ['react-native-nitro-sqlite-vec', 'react-native-nitro-sqlite-drizzle'].find((addon) => url.includes(addon)) ?? 'react-native-nitro-sqlite'", " if (url.includes('/token/exchange/')) {", " appendFileSync(process.env.PUBLISH_TEST_LOG, 'trusted:' + name + '\\n')", " if (name.endsWith('-vec') && process.env.PUBLISH_TEST_FAIL_OIDC) return new Response('', { status: 403 })", diff --git a/scripts/release-plan.ts b/scripts/release-plan.ts index f800781c..9fefdb16 100644 --- a/scripts/release-plan.ts +++ b/scripts/release-plan.ts @@ -76,12 +76,17 @@ export function validateReleaseVersions(version: string): void { 'packages/react-native-nitro-sqlite-vec/package.json', ['version', 'devDependencies.react-native-nitro-sqlite'], ], + [ + 'packages/react-native-nitro-sqlite-drizzle/package.json', + ['version', 'devDependencies.react-native-nitro-sqlite'], + ], [ 'example/package.json', [ 'version', 'dependencies.react-native-nitro-sqlite', 'dependencies.react-native-nitro-sqlite-vec', + 'dependencies.react-native-nitro-sqlite-drizzle', ], ], ] satisfies [string, string[]][]) { diff --git a/scripts/release-registry.ts b/scripts/release-registry.ts index 25e2faf3..0cbbe390 100644 --- a/scripts/release-registry.ts +++ b/scripts/release-registry.ts @@ -4,6 +4,7 @@ import { compareVersions, parseVersion } from './release-plan.ts' export const releasePackages = [ 'react-native-nitro-sqlite', 'react-native-nitro-sqlite-vec', + 'react-native-nitro-sqlite-drizzle', ] /** Select the channel without moving latest backwards on maintenance releases. diff --git a/scripts/release.test.ts b/scripts/release.test.ts index 39b0a749..ea8bd58c 100644 --- a/scripts/release.test.ts +++ b/scripts/release.test.ts @@ -334,6 +334,7 @@ function fixture(run: (directory: string) => void): void { 'package.json', 'packages/react-native-nitro-sqlite/package.json', 'packages/react-native-nitro-sqlite-vec/package.json', + 'packages/react-native-nitro-sqlite-drizzle/package.json', 'example/package.json', ]) { mkdirSync(join(directory, path, '..'), { recursive: true }) @@ -344,6 +345,7 @@ function fixture(run: (directory: string) => void): void { dependencies: { 'react-native-nitro-sqlite': '1.0.0', 'react-native-nitro-sqlite-vec': '1.0.0', + 'react-native-nitro-sqlite-drizzle': '1.0.0', }, devDependencies: { 'react-native-nitro-sqlite': '1.0.0' }, }), diff --git a/tsconfig.json b/tsconfig.json index ae8a25a4..4127043d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "references": [ { "path": "./packages/react-native-nitro-sqlite" }, { "path": "./packages/react-native-nitro-sqlite-vec" }, + { "path": "./packages/react-native-nitro-sqlite-drizzle" }, { "path": "./example" } ] }