diff --git a/docs/2.connectors/1.index.md b/docs/2.connectors/1.index.md index 0f367d74..6b275a61 100644 --- a/docs/2.connectors/1.index.md +++ b/docs/2.connectors/1.index.md @@ -11,6 +11,8 @@ Currently supported connectors: - [Bun](/connectors/bun) - [Cloudflare D1](/connectors/cloudflare) - [LibSQL](/connectors/libsql) +- [Neon](/connectors/neon) +- [Neon Instant](/connectors/neon-instant) - [PlanetScale](/connectors/planetscale) - [PostgreSQL](/connectors/postgresql) - [Prisma Postgres](/connectors/postgresql#prisma-postgres) diff --git a/docs/2.connectors/neon-instant.md b/docs/2.connectors/neon-instant.md new file mode 100644 index 00000000..ef175a3d --- /dev/null +++ b/docs/2.connectors/neon-instant.md @@ -0,0 +1,100 @@ +--- +icon: cbi:neon +--- + +# NEON INSTANT + +> The [Neon connector](/connectors/neon), plus a database provisioned for you when you don't have one yet. + +:read-more{to="https://neon.com/docs/reference/neon-new"} + +## Instant Postgres Provisioning + +This connector behaves exactly like the [Neon connector](/connectors/neon), except that it does not require a connection string. On first use, it resolves one in this order: + +1. The `url` / `connectionString` option, if given. +2. The `DATABASE_URL` environment variable (see [`dotEnvKey`](#dotenvfile-dotenvkey)), if set. +3. Otherwise, it provisions a claimable Postgres database via [`neon-new`](https://www.npmjs.com/package/neon-new), optionally seeding it from a `.sql` file. + +This is intended as a development-time affordance. When `NODE_ENV` is `production`, nothing is provisioned and a missing connection string throws — use the [Neon connector](/connectors/neon) there. + +`neon-new` is imported lazily, only when a database actually has to be provisioned, so it stays out of your production bundle. + +## Usage + +Install the Neon Serverless Driver for the postgres connection, and `neon-new` to provision the database. + +:pm-install{name="@neondatabase/serverless neon-new"} + +With those dependencies installed, you can immediately start building: + +```ts +import { createDatabase } from "db0"; +import neonInstant from "db0/connectors/neon-instant"; + +const db = createDatabase( + neonInstant({ + seed: { type: "sql-script", path: "init.sql" }, + }), +); +``` + +```sql [init.sql] +CREATE TABLE IF NOT EXISTS xmen ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL +); + +INSERT INTO xmen (name) VALUES + ('Wolverine'), + ('Cyclops'), + ('Storm'), + ('Jean Grey'), + ('Beast'), + ('Professor X'), + ('Gambit'), + ('Rogue'), + ('Nightcrawler') +ON CONFLICT DO NOTHING; +``` + +The generated connection string is appended to your `.env` file. As long as that file is loaded into `process.env` (with `dotenv`, or natively via `node --env-file`), later runs pick it up and reuse the same database instead of provisioning a new one. + +## Options + +Accepts every [Neon connector option](/connectors/neon#options), plus the [`neon-new` parameters](https://www.npmjs.com/package/neon-new) below. + +### `url` or `connectionString` + +- **Type:** `string` _(optional)_ +- Connection string to an existing Neon database. +- If provided, no database is provisioned. + +### `seed` + +- **Type:** `{ type: "sql-script", path: string }` _(optional)_ +- **Default:** `undefined` +- Path to a `.sql` file for seeding the database schema and initial data. + +### `dotEnvFile` / `dotEnvKey` + +- **Type:** `string` _(optional)_ +- **Default:** `".env"` and `"DATABASE_URL"` +- File the generated connection string is written to, and the variable name it is written under. `dotEnvKey` is also the environment variable read to reuse an existing database. + +### `envPrefix` + +- **Type:** `string` _(optional)_ +- **Default:** `"PUBLIC_"` +- Prefix used for the public environment variables written alongside the connection string. + +### `settings` + +- **Type:** `{ logicalReplication?: boolean }` _(optional)_ +- Extra settings for the provisioned database. + +### `referrer` + +- **Type:** `string` _(optional)_ +- **Default:** `"db0/neon-connector"` +- Referrer name Neon uses for tracking. diff --git a/docs/2.connectors/neon.md b/docs/2.connectors/neon.md index 695b6762..32c22591 100644 --- a/docs/2.connectors/neon.md +++ b/docs/2.connectors/neon.md @@ -4,11 +4,45 @@ icon: cbi:neon # NEON -> Connect DB0 to Neon Serverless Postgres. +> Very similar to [Postgres connector](/connectors/postgresql), but optimized for serverless environments. -:read-more{to="https://neon.tech/"} +:read-more{to="https://neon.com"} -::read-more{to="https://github.com/unjs/db0/issues/32"} -This connector is planned to be supported. Follow up via [unjs/db0#32](https://github.com/unjs/db0/issues/32). +## Why Neon Connector? + +The fundamental difference is that Postgres Connector uses the [node-postgres](https://node-postgres.com/) driver, which needs a raw TCP connection, while Neon uses [neondatabase/serverless](https://neon.com/docs/serverless/serverless-driver), whose `Client` speaks postgres over WebSockets. The drivers have feature parity, but the connection type creates some runtime differences. + +A WebSocket connection is usually preferred over TCP for serverless environments because many of those runtimes cannot open raw TCP sockets at all. + +## Usage + +Install the Neon Serverless Driver for the postgres connection. + +:pm-install{name="@neondatabase/serverless"} + +This connector always connects to an existing database, so a connection string is required. + +```ts +import { createDatabase } from "db0"; +import neon from "db0/connectors/neon"; + +const db = createDatabase( + neon({ + url: process.env.DATABASE_URL, + }), +); +``` + +::tip +Want a database provisioned for you in development, without bringing your own connection string? Use the [Neon Instant connector](/connectors/neon-instant). :: +## Options + +Options are passed through to the underlying [`Client`](https://neon.com/docs/serverless/serverless-driver), so any `ClientConfig` field is accepted in addition to the following. + +### `url` or `connectionString` + +- **Type:** `string` +- Connection string to your Neon database. +- Optional if the database is identified another way, such as a `host` in the `ClientConfig`. If neither is present, the first query throws (the client connects lazily). diff --git a/docs/2.connectors/vercel.md b/docs/2.connectors/vercel.md index 28cb2084..44c4be67 100644 --- a/docs/2.connectors/vercel.md +++ b/docs/2.connectors/vercel.md @@ -4,25 +4,9 @@ icon: radix-icons:vercel-logo # Vercel -> Connect DB0 to Vercel Postgres +> Vercel Postgres has migrated to Vercel Marketplace. -:read-more{to="https://vercel.com/docs/storage/vercel-postgres"} +Existing Vercel Postgres instances were migrated to [Neon](https://neon.com). +For best integration with db0, use the [Neon Connector](/connectors/neon). -::read-more{to="https://github.com/unjs/db0/issues/32"} -A dedicated `vercel` connector is planned to be supported. Follow up via [unjs/db0#32](https://github.com/unjs/db0/issues/32). -:: - -## Usage - -Use [`postgres`](/connectors/postgresql) connector: - -```js -import { createDatabase } from "db0"; -import postgres from "db0/connectors/postgres"; - -const db = createDatabase( - postgres({ - /* options */ - }), -); -``` +:read-more{to="https://neon.com/docs/guides/vercel-postgres-transition-guide"} diff --git a/package.json b/package.json index 845dd00a..36e85e08 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@cloudflare/workers-types": "^5", "@electric-sql/pglite": "^0.5.4", "@libsql/client": "^0.17.4", + "@neondatabase/serverless": "^1.1.0", "@planetscale/database": "^1.20.1", "@types/better-sqlite3": "^7.6.13", "@types/bun": "^1.3.14", @@ -62,6 +63,7 @@ "kysely": "^0.29.3", "mlly": "^1.8.2", "mysql2": "^3.22.6", + "neon-new": "^0.15.0", "obuild": "^0.4.38", "pathe": "^2.0.3", "pg": "^8.22.0", @@ -74,16 +76,24 @@ "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", + "@neondatabase/serverless": "*", "better-sqlite3": "*", "drizzle-orm": "*", "kysely": "*", "mysql2": "*", + "neon-new": "*", "sqlite3": "*" }, "peerDependenciesMeta": { "@libsql/client": { "optional": true }, + "@neondatabase/serverless": { + "optional": true + }, + "neon-new": { + "optional": true + }, "better-sqlite3": { "optional": true }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44dc14d4..a8047ad3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: '@libsql/client': specifier: ^0.17.4 version: 0.17.4 + '@neondatabase/serverless': + specifier: ^1.1.0 + version: 1.1.0 '@planetscale/database': specifier: ^1.20.1 version: 1.20.1 @@ -47,13 +50,13 @@ importers: version: 0.6.2(magicast@0.5.3) db0: specifier: ^0.3.4 - version: 0.3.4(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260711.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1))(mysql2@3.22.6(@types/node@26.1.1))(sqlite3@6.0.1) + version: 0.3.4(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260711.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@neondatabase/serverless@1.1.0)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1))(mysql2@3.22.6(@types/node@26.1.1))(sqlite3@6.0.1) dotenv: specifier: ^17.4.2 version: 17.4.2 drizzle-orm: specifier: ^0.45.2 - version: 0.45.2(@cloudflare/workers-types@5.20260711.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1) + version: 0.45.2(@cloudflare/workers-types@5.20260711.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@neondatabase/serverless@1.1.0)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1) eslint: specifier: ^10.7.0 version: 10.7.0(jiti@2.7.0) @@ -72,6 +75,9 @@ importers: mysql2: specifier: ^3.22.6 version: 3.22.6(@types/node@26.1.1) + neon-new: + specifier: ^0.15.0 + version: 0.15.0 obuild: specifier: ^0.4.38 version: 0.4.38(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.0)(jiti@2.7.0)(magicast@0.5.3)(typescript@7.0.2) @@ -101,13 +107,13 @@ importers: devDependencies: db0: specifier: latest - version: 0.3.4(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(better-sqlite3@12.11.1)(drizzle-orm@0.29.5(@cloudflare/workers-types@5.20260711.1)(@libsql/client@0.17.4)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1))(mysql2@3.22.6(@types/node@26.1.1))(sqlite3@6.0.1) + version: 0.3.4(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(better-sqlite3@12.11.1)(drizzle-orm@0.29.5(@cloudflare/workers-types@5.20260711.1)(@libsql/client@0.17.4)(@neondatabase/serverless@1.1.0)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@types/react@19.1.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1))(mysql2@3.22.6(@types/node@26.1.1))(sqlite3@6.0.1) drizzle-kit: specifier: ^0.20.14 version: 0.20.18 drizzle-orm: specifier: ^0.29.4 - version: 0.29.5(@cloudflare/workers-types@5.20260711.1)(@libsql/client@0.17.4)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1) + version: 0.29.5(@cloudflare/workers-types@5.20260711.1)(@libsql/client@0.17.4)(@neondatabase/serverless@1.1.0)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@types/react@19.1.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1) jiti: specifier: ^1.21.0 version: 1.21.7 @@ -139,6 +145,12 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@clack/core@0.4.2': + resolution: {integrity: sha512-NYQfcEy8MWIxrT5Fj8nIVchfRFA26yYKJcvBS7WlUIlw2OmQOY9DhGGXMovyI5J5PpxrCPGkgUi207EBrjpBvg==} + + '@clack/prompts@0.10.1': + resolution: {integrity: sha512-Q0T02vx8ZM9XSv9/Yde0jTmmBQufZhPJfYAg2XrrrxWWaZgq1rr8nU8Hv710BQ1dhoP8rtY7YUdpGej2Qza/cw==} + '@cloudflare/kv-asset-handler@0.5.0': resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} engines: {node: '>=22.0.0'} @@ -952,6 +964,10 @@ packages: '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + '@neondatabase/serverless@1.1.0': + resolution: {integrity: sha512-r3ZZhRjEcfEdKIZnoB1RusNgvHuaBRqfCzV4Gi+5A9yUX0S4HTws/ASWqt13wL4y4I+0rqsWGdA2w7EQXHi3+Q==} + engines: {node: '>=19.0.0'} + '@oxc-project/types@0.139.0': resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} @@ -1203,6 +1219,12 @@ packages: '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/react@19.1.13': + resolution: {integrity: sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==} + + '@types/tinycolor2@1.4.6': + resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -1568,6 +1590,14 @@ packages: ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1725,6 +1755,10 @@ packages: resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==} engines: {node: '>=0.10'} + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + commander@9.5.0: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} @@ -1760,6 +1794,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d@1.0.2: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} @@ -1854,6 +1891,10 @@ packages: difflib@0.2.4: resolution: {integrity: sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} @@ -2041,6 +2082,9 @@ packages: electron-to-chromium@1.5.389: resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -2254,6 +2298,14 @@ packages: generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} @@ -2291,6 +2343,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gradient-string@3.0.0: + resolution: {integrity: sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg==} + engines: {node: '>=14'} + hanji@0.0.5: resolution: {integrity: sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw==} @@ -2760,6 +2816,11 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + neon-new@0.15.0: + resolution: {integrity: sha512-/h4JPj2199ll5BYML6AuzTFXK8UpBghnCzGtbGIvEbyUdxVZt7fMorwz+7ge3dtU3/TXRRrOYiUTXbv2ibOA1g==} + engines: {node: '>=20.19.0'} + hasBin: true + next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} @@ -2824,6 +2885,14 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-timeout@6.1.4: + resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} + engines: {node: '>=14.16'} + + p-wait-for@5.0.2: + resolution: {integrity: sha512-lwx6u1CotQYPVju77R+D0vFomni/AqRfqLmqQ8hekklqZ6gAY9rONh7lBQ0uxWMkC2AuX9b2DVAl8To0NyP1JA==} + engines: {node: '>=12'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2912,6 +2981,9 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} + postgres-semicolons@0.1.2: + resolution: {integrity: sha512-8WQBfSojaERe6y1falp7o05NZav48MHd3VUCtKdEkRyzgrQL/3tbdEmnhHfXbl/23ajym2CSSI6vA2NCwCXG+w==} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -3065,9 +3137,17 @@ packages: std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-indent@4.1.1: resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} engines: {node: '>=12'} @@ -3106,6 +3186,9 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@1.2.4: resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} @@ -3114,6 +3197,9 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinygradient@1.1.5: + resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} @@ -3315,6 +3401,10 @@ packages: '@cloudflare/workers-types': optional: true + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -3338,14 +3428,30 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + youch-core@0.3.3: resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} @@ -3386,6 +3492,17 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@clack/core@0.4.2': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.10.1': + dependencies: + '@clack/core': 0.4.2 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@cloudflare/kv-asset-handler@0.5.0': {} '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260708.1)': @@ -3922,6 +4039,8 @@ snapshots: '@neon-rs/load@0.0.4': {} + '@neondatabase/serverless@1.1.0': {} + '@oxc-project/types@0.139.0': {} '@parcel/watcher-android-arm64@2.5.6': @@ -4101,6 +4220,13 @@ snapshots: pg-protocol: 1.15.0 pg-types: 2.2.0 + '@types/react@19.1.13': + dependencies: + csstype: 3.2.3 + optional: true + + '@types/tinycolor2@1.4.6': {} + '@types/unist@3.0.3': {} '@types/ws@8.18.1': @@ -4397,6 +4523,10 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-regex@6.2.2: {} + + ansi-styles@6.2.3: {} + assertion-error@2.0.1: {} ast-v8-to-istanbul@1.0.4: @@ -4579,6 +4709,12 @@ snapshots: memoizee: 0.4.17 timers-ext: 0.1.8 + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + commander@9.5.0: {} confbox@0.1.8: {} @@ -4607,26 +4743,29 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + csstype@3.2.3: + optional: true + d@1.0.2: dependencies: es5-ext: 0.10.64 type: 2.7.3 - db0@0.3.4(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(better-sqlite3@12.11.1)(drizzle-orm@0.29.5(@cloudflare/workers-types@5.20260711.1)(@libsql/client@0.17.4)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1))(mysql2@3.22.6(@types/node@26.1.1))(sqlite3@6.0.1): + db0@0.3.4(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(better-sqlite3@12.11.1)(drizzle-orm@0.29.5(@cloudflare/workers-types@5.20260711.1)(@libsql/client@0.17.4)(@neondatabase/serverless@1.1.0)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@types/react@19.1.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1))(mysql2@3.22.6(@types/node@26.1.1))(sqlite3@6.0.1): optionalDependencies: '@electric-sql/pglite': 0.5.4 '@libsql/client': 0.17.4 better-sqlite3: 12.11.1 - drizzle-orm: 0.29.5(@cloudflare/workers-types@5.20260711.1)(@libsql/client@0.17.4)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1) + drizzle-orm: 0.29.5(@cloudflare/workers-types@5.20260711.1)(@libsql/client@0.17.4)(@neondatabase/serverless@1.1.0)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@types/react@19.1.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1) mysql2: 3.22.6(@types/node@26.1.1) sqlite3: 6.0.1 - db0@0.3.4(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260711.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1))(mysql2@3.22.6(@types/node@26.1.1))(sqlite3@6.0.1): + db0@0.3.4(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260711.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@neondatabase/serverless@1.1.0)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1))(mysql2@3.22.6(@types/node@26.1.1))(sqlite3@6.0.1): optionalDependencies: '@electric-sql/pglite': 0.5.4 '@libsql/client': 0.17.4 better-sqlite3: 12.11.1 - drizzle-orm: 0.45.2(@cloudflare/workers-types@5.20260711.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1) + drizzle-orm: 0.45.2(@cloudflare/workers-types@5.20260711.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@neondatabase/serverless@1.1.0)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1) mysql2: 3.22.6(@types/node@26.1.1) sqlite3: 6.0.1 @@ -4681,6 +4820,8 @@ snapshots: dependencies: heap: 0.2.7 + dotenv@16.6.1: {} + dotenv@17.4.2: {} dreamopt@0.8.0: @@ -4709,13 +4850,15 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.29.5(@cloudflare/workers-types@5.20260711.1)(@libsql/client@0.17.4)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1): + drizzle-orm@0.29.5(@cloudflare/workers-types@5.20260711.1)(@libsql/client@0.17.4)(@neondatabase/serverless@1.1.0)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@types/react@19.1.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1): optionalDependencies: '@cloudflare/workers-types': 5.20260711.1 '@libsql/client': 0.17.4 + '@neondatabase/serverless': 1.1.0 '@planetscale/database': 1.20.1 '@types/better-sqlite3': 7.6.13 '@types/pg': 8.20.0 + '@types/react': 19.1.13 better-sqlite3: 12.11.1 bun-types: 1.3.14 kysely: 0.29.3 @@ -4723,11 +4866,12 @@ snapshots: pg: 8.22.0 sqlite3: 6.0.1 - drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260711.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1): + drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260711.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@neondatabase/serverless@1.1.0)(@planetscale/database@1.20.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.29.3)(mysql2@3.22.6(@types/node@26.1.1))(pg@8.22.0)(sqlite3@6.0.1): optionalDependencies: '@cloudflare/workers-types': 5.20260711.1 '@electric-sql/pglite': 0.5.4 '@libsql/client': 0.17.4 + '@neondatabase/serverless': 1.1.0 '@planetscale/database': 1.20.1 '@types/better-sqlite3': 7.6.13 '@types/pg': 8.20.0 @@ -4742,6 +4886,8 @@ snapshots: electron-to-chromium@1.5.389: {} + emoji-regex@10.6.0: {} + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -5053,6 +5199,10 @@ snapshots: dependencies: is-property: 1.0.2 + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -5086,6 +5236,11 @@ snapshots: graceful-fs@4.2.11: optional: true + gradient-string@3.0.0: + dependencies: + chalk: 5.6.2 + tinygradient: 1.1.5 + hanji@0.0.5: dependencies: lodash.throttle: 4.1.1 @@ -5698,6 +5853,18 @@ snapshots: natural-compare@1.4.0: {} + neon-new@0.15.0: + dependencies: + '@clack/prompts': 0.10.1 + '@neondatabase/serverless': 1.1.0 + dotenv: 16.6.1 + gradient-string: 3.0.0 + open: 10.2.0 + p-wait-for: 5.0.2 + postgres-semicolons: 0.1.2 + yargs: 18.0.0 + yoctocolors: 2.1.2 + next-tick@1.1.0: {} node-abi@3.94.0: @@ -5792,6 +5959,12 @@ snapshots: dependencies: p-limit: 3.1.0 + p-timeout@6.1.4: {} + + p-wait-for@5.0.2: + dependencies: + p-timeout: 6.1.4 + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -5871,6 +6044,8 @@ snapshots: dependencies: xtend: 4.0.2 + postgres-semicolons@0.1.2: {} + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -6052,10 +6227,20 @@ snapshots: std-env@4.2.0: {} + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-indent@4.1.1: {} strip-json-comments@2.0.1: {} @@ -6100,6 +6285,8 @@ snapshots: tinybench@2.9.0: {} + tinycolor2@1.6.0: {} + tinyexec@1.2.4: {} tinyglobby@0.2.17: @@ -6107,6 +6294,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 + tinygradient@1.1.5: + dependencies: + '@types/tinycolor2': 1.4.6 + tinycolor2: 1.6.0 + tinyrainbow@3.1.0: {} ts-api-utils@2.5.0(typescript@7.0.2): @@ -6296,6 +6488,12 @@ snapshots: - bufferutil - utf-8-validate + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrappy@1.0.2: {} ws@8.21.0: {} @@ -6306,10 +6504,25 @@ snapshots: xtend@4.0.2: {} + y18n@5.0.8: {} + yallist@5.0.0: {} + yargs-parser@22.0.0: {} + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + yocto-queue@0.1.0: {} + yoctocolors@2.1.2: {} + youch-core@0.3.3: dependencies: '@poppinss/exception': 1.2.3 diff --git a/src/_connectors.ts b/src/_connectors.ts index e0171c60..781a97c3 100644 --- a/src/_connectors.ts +++ b/src/_connectors.ts @@ -10,13 +10,15 @@ import type { ConnectorOptions as LibSQLHttpOptions } from "db0/connectors/libsq import type { ConnectorOptions as LibSQLNodeOptions } from "db0/connectors/libsql/node"; import type { ConnectorOptions as LibSQLWebOptions } from "db0/connectors/libsql/web"; import type { ConnectorOptions as MySQL2Options } from "db0/connectors/mysql2"; +import type { ConnectorOptions as NeonOptions } from "db0/connectors/neon"; +import type { ConnectorOptions as NeonInstantOptions } from "db0/connectors/neon-instant"; import type { ConnectorOptions as NodeSQLiteOptions } from "db0/connectors/node-sqlite"; import type { ConnectorOptions as PgliteOptions } from "db0/connectors/pglite"; import type { ConnectorOptions as PlanetscaleOptions } from "db0/connectors/planetscale"; import type { ConnectorOptions as PostgreSQLOptions } from "db0/connectors/postgresql"; import type { ConnectorOptions as SQLite3Options } from "db0/connectors/sqlite3"; -export type ConnectorName = "better-sqlite3" | "bun-sqlite" | "bun" | "cloudflare-d1" | "cloudflare-hyperdrive-mysql" | "cloudflare-hyperdrive-postgresql" | "libsql-core" | "libsql-http" | "libsql-node" | "libsql" | "libsql-web" | "mysql2" | "node-sqlite" | "sqlite" | "pglite" | "planetscale" | "postgresql" | "sqlite3"; +export type ConnectorName = "better-sqlite3" | "bun-sqlite" | "bun" | "cloudflare-d1" | "cloudflare-hyperdrive-mysql" | "cloudflare-hyperdrive-postgresql" | "libsql-core" | "libsql-http" | "libsql-node" | "libsql" | "libsql-web" | "mysql2" | "neon" | "neon-instant" | "node-sqlite" | "sqlite" | "pglite" | "planetscale" | "postgresql" | "sqlite3"; export type ConnectorOptions = { "better-sqlite3": BetterSQLite3Options; @@ -33,6 +35,8 @@ export type ConnectorOptions = { "libsql": LibSQLNodeOptions; "libsql-web": LibSQLWebOptions; "mysql2": MySQL2Options; + "neon": NeonOptions; + "neon-instant": NeonInstantOptions; "node-sqlite": NodeSQLiteOptions; /** alias of node-sqlite */ "sqlite": NodeSQLiteOptions; @@ -57,6 +61,8 @@ export const connectors: Record = Object.freeze({ "libsql": "db0/connectors/libsql/node", "libsql-web": "db0/connectors/libsql/web", "mysql2": "db0/connectors/mysql2", + "neon": "db0/connectors/neon", + "neon-instant": "db0/connectors/neon-instant", "node-sqlite": "db0/connectors/node-sqlite", /** alias of node-sqlite */ "sqlite": "db0/connectors/node-sqlite", diff --git a/src/connectors/_internal/neon.ts b/src/connectors/_internal/neon.ts new file mode 100644 index 00000000..3d104350 --- /dev/null +++ b/src/connectors/_internal/neon.ts @@ -0,0 +1,181 @@ +import * as pg from "@neondatabase/serverless"; +import type { Connector, Primitive } from "db0"; + +import { BoundableStatement } from "./statement.ts"; + +export type NeonClientOptions = { url?: string } | pg.ClientConfig; + +type InternalQuery = ( + sql: string, + params?: Primitive[], +) => Promise; + +/** + * Resolves the connection string to connect with, called lazily on first use. + */ +export type ConnectionStringResolver = ( + opts: NeonClientOptions | undefined, +) => string | undefined | Promise; + +export function resolveStaticConnectionString( + opts: NeonClientOptions | undefined, +): string | undefined { + const { url, connectionString } = (opts || {}) as { + url?: string; + connectionString?: string; + }; + return url || connectionString; +} + +function toClientConfig(opts: NeonClientOptions | undefined): pg.ClientConfig { + const { url: _url, ...config } = (opts || {}) as { + url?: string; + } & pg.ClientConfig; + return config; +} + +export function createNeonConnector( + name: string, + opts: NeonClientOptions | undefined, + resolveConnectionString: ConnectionStringResolver = resolveStaticConnectionString, +): Connector { + let _client: undefined | Promise; + + async function connect(): Promise { + const config = toClientConfig(opts); + const connectionString = await resolveConnectionString(opts); + + if (connectionString) { + config.connectionString = connectionString; + } + + // `pg.ClientConfig` can identify a database without a connection string. + if (!config.connectionString && !config.host) { + throw new Error( + `[db0] [${name}] Missing connection string. Pass \`url\` or a \`host\` to the connector.`, + ); + } + + const client = new pg.Client(config); + await client.connect(); + return client; + } + + function getClient(): Promise { + if (!_client) { + // Assigned synchronously so concurrent callers share one client. + _client = connect().catch((error) => { + _client = undefined; // Let the next call retry instead of caching the failure. + throw error; + }); + } + return _client; + } + + const query: InternalQuery = async (sql, params) => { + const client = await getClient(); + return client.query(normalizeParams(sql), params); + }; + + return { + name, + dialect: "postgresql", + getInstance: () => getClient(), + exec: (sql) => query(sql), + prepare: (sql) => new StatementWrapper(sql, query), + dispose: async () => { + const client = _client; + _client = undefined; + await (await client?.catch(() => undefined))?.end?.(); + }, + }; +} + +/** + * Rewrites `?` placeholders into postgres' `$n` form, leaving `?` occurrences + * that are not placeholders alone: those inside string literals or quoted + * identifiers, inside comments, and the jsonb operators `?|`, `?&` and `??`. + * + * https://www.postgresql.org/docs/9.3/sql-prepare.html + */ +export function normalizeParams(sql: string): string { + let result = ""; + let index = 0; + + for (let i = 0; i < sql.length; i++) { + const char = sql[i]; + + // Quoted string ('...', including E'..' bodies) or identifier ("..."). + if (char === "'" || char === '"') { + const end = sql.indexOf(char, i + 1); + if (end === -1) { + result += sql.slice(i); + break; + } + result += sql.slice(i, end + 1); + i = end; + continue; + } + + if (char === "-" && sql[i + 1] === "-") { + const end = sql.indexOf("\n", i); + const stop = end === -1 ? sql.length : end; + result += sql.slice(i, stop); + i = stop - 1; + continue; + } + + if (char === "/" && sql[i + 1] === "*") { + const end = sql.indexOf("*/", i + 2); + const stop = end === -1 ? sql.length : end + 2; + result += sql.slice(i, stop); + i = stop - 1; + continue; + } + + if (char === "?") { + const next = sql[i + 1]; + // jsonb operators, not placeholders. + if (next === "|" || next === "&" || next === "?") { + result += char + next; + i++; + continue; + } + result += `$${++index}`; + continue; + } + + result += char; + } + + return result; +} + +class StatementWrapper extends BoundableStatement { + #query: InternalQuery; + #sql: string; + + constructor(sql: string, query: InternalQuery) { + super(); + this.#sql = sql; + this.#query = query; + } + + async all(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows; + } + + async run(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return { + success: true, + ...res, + }; + } + + async get(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows[0]; + } +} diff --git a/src/connectors/neon-instant.ts b/src/connectors/neon-instant.ts new file mode 100644 index 00000000..3ee9b675 --- /dev/null +++ b/src/connectors/neon-instant.ts @@ -0,0 +1,59 @@ +import type * as pg from "@neondatabase/serverless"; +import type { Connector } from "db0"; +import type { InstantPostgresParams } from "neon-new"; + +import { + createNeonConnector, + resolveStaticConnectionString, + type NeonClientOptions, +} from "./_internal/neon.ts"; + +export type ConnectorOptions = NeonClientOptions & + Partial; + +export default function neonInstantConnector( + opts?: ConnectorOptions, +): Connector { + const { + referrer = "db0/neon-connector", + dotEnvFile, + dotEnvKey = "DATABASE_URL", + seed, + envPrefix, + settings, + ...clientOpts + } = (opts || {}) as Partial & pg.ClientConfig; + + return createNeonConnector("neon-instant", clientOpts, async (clientOpts) => { + const connectionString = resolveStaticConnectionString(clientOpts); + if (connectionString) { + return connectionString; + } + + // Reuse the database provisioned by an earlier run, if it is still around. + const fromEnv = globalThis.process?.env?.[dotEnvKey]; + if (fromEnv) { + return fromEnv; + } + + // Provisioning a claimable database is a development-time affordance. + if (globalThis.process?.env?.NODE_ENV === "production") { + throw new Error( + "[db0] [neon-instant] Refusing to provision a database in production. Pass a connection string, or use the `neon` connector.", + ); + } + + const { instantPostgres } = await import("neon-new"); + + const { databaseUrl } = await instantPostgres({ + referrer, + dotEnvFile, + dotEnvKey, + seed, + envPrefix, + settings, + }); + + return databaseUrl; + }); +} diff --git a/src/connectors/neon.ts b/src/connectors/neon.ts new file mode 100644 index 00000000..ea2b4a03 --- /dev/null +++ b/src/connectors/neon.ts @@ -0,0 +1,12 @@ +import type * as pg from "@neondatabase/serverless"; +import type { Connector } from "db0"; + +import { createNeonConnector } from "./_internal/neon.ts"; + +export type ConnectorOptions = { url?: string } | pg.ClientConfig; + +export default function neonConnector( + opts?: ConnectorOptions, +): Connector { + return createNeonConnector("neon", opts); +} diff --git a/test/connectors/neon-instant.test.ts b/test/connectors/neon-instant.test.ts new file mode 100644 index 00000000..dae72f33 --- /dev/null +++ b/test/connectors/neon-instant.test.ts @@ -0,0 +1,12 @@ +import { describe } from "vitest"; +import neonInstantConnector from "../../src/connectors/neon-instant"; +import { testConnector } from "./_tests"; + +describe.runIf(process.env.NEON_URL)("connectors: Neon Instant", () => { + testConnector({ + dialect: "postgresql", + connector: neonInstantConnector({ + connectionString: process.env.NEON_URL!, + }), + }); +}); diff --git a/test/connectors/neon.test.ts b/test/connectors/neon.test.ts new file mode 100644 index 00000000..b496955e --- /dev/null +++ b/test/connectors/neon.test.ts @@ -0,0 +1,12 @@ +import { describe } from "vitest"; +import neonConnector from "../../src/connectors/neon"; +import { testConnector } from "./_tests"; + +describe.runIf(process.env.NEON_URL)("connectors: Neon", () => { + testConnector({ + dialect: "postgresql", + connector: neonConnector({ + connectionString: process.env.NEON_URL!, + }), + }); +}); diff --git a/test/connectors/neon.unit.test.ts b/test/connectors/neon.unit.test.ts new file mode 100644 index 00000000..205cca67 --- /dev/null +++ b/test/connectors/neon.unit.test.ts @@ -0,0 +1,215 @@ +import { describe, test, expect, vi, beforeEach } from "vitest"; + +const { instantPostgres, clients, state, MockClient } = vi.hoisted(() => { + const clients: any[] = []; + const state = { connect: () => Promise.resolve() }; + + class MockClient { + config: any; + connects = 0; + ends = 0; + queries: { sql: string; params?: unknown[] }[] = []; + + constructor(config: any) { + this.config = config; + clients.push(this); + } + + connect() { + this.connects++; + return state.connect(); + } + + query(sql: string, params?: unknown[]) { + this.queries.push({ sql, params }); + return Promise.resolve({ rows: [], rowCount: 0 }); + } + + end() { + this.ends++; + return Promise.resolve(); + } + } + + return { instantPostgres: vi.fn(), clients, state, MockClient }; +}); + +vi.mock("neon-new", () => ({ instantPostgres })); +vi.mock("@neondatabase/serverless", () => ({ Client: MockClient })); + +import neonConnector from "../../src/connectors/neon"; +import neonInstantConnector from "../../src/connectors/neon-instant"; +import { normalizeParams } from "../../src/connectors/_internal/neon"; +import { createDatabase } from "../../src"; + +beforeEach(() => { + clients.length = 0; + state.connect = () => Promise.resolve(); + instantPostgres.mockReset(); + instantPostgres.mockResolvedValue({ + databaseUrl: "postgres://provisioned/db", + }); + // The connector reuses an already-provisioned database from the environment. + vi.stubEnv("DATABASE_URL", ""); + vi.stubEnv("NODE_ENV", "test"); +}); + +describe("connectors: neon (sdk only)", () => { + test("connects with the given connection string and never provisions", async () => { + const db = createDatabase( + neonConnector({ url: "postgres://user@host/db" }), + ); + await db.getInstance(); + + expect(clients[0].config.connectionString).toBe("postgres://user@host/db"); + expect(instantPostgres).not.toHaveBeenCalled(); + }); + + test("accepts a ClientConfig without a connection string", async () => { + const db = createDatabase( + neonConnector({ host: "localhost", user: "u", database: "d" }), + ); + await db.getInstance(); + + expect(clients[0].config).toMatchObject({ host: "localhost", user: "u" }); + }); + + test("throws when no connection string is available", async () => { + const db = createDatabase(neonConnector()); + await expect(db.getInstance()).rejects.toThrow(/Missing connection string/); + expect(instantPostgres).not.toHaveBeenCalled(); + }); +}); + +describe("connectors: neon-instant", () => { + test("provisions a database when nothing else identifies one", async () => { + const db = createDatabase(neonInstantConnector()); + await db.getInstance(); + + expect(instantPostgres).toHaveBeenCalledOnce(); + expect(clients[0].config.connectionString).toBe( + "postgres://provisioned/db", + ); + }); + + test("forwards provisioning options and defaults the referrer", async () => { + const seed = { type: "sql-script", path: "init.sql" } as const; + const db = createDatabase(neonInstantConnector({ seed })); + await db.getInstance(); + + expect(instantPostgres).toHaveBeenCalledWith( + expect.objectContaining({ referrer: "db0/neon-connector", seed }), + ); + }); + + test("keeps provisioning options out of the client config", async () => { + const db = createDatabase( + neonInstantConnector({ + seed: { type: "sql-script", path: "init.sql" }, + dotEnvFile: ".env.local", + }), + ); + await db.getInstance(); + + expect(clients[0].config).not.toHaveProperty("seed"); + expect(clients[0].config).not.toHaveProperty("dotEnvFile"); + expect(clients[0].config).not.toHaveProperty("referrer"); + }); + + test("reuses an already-provisioned database from the environment", async () => { + vi.stubEnv("DATABASE_URL", "postgres://existing/db"); + + const db = createDatabase(neonInstantConnector()); + await db.getInstance(); + + expect(instantPostgres).not.toHaveBeenCalled(); + expect(clients[0].config.connectionString).toBe("postgres://existing/db"); + }); + + test("prefers an explicit connection string over provisioning", async () => { + const db = createDatabase( + neonInstantConnector({ url: "postgres://user@host/db" }), + ); + await db.getInstance(); + + expect(instantPostgres).not.toHaveBeenCalled(); + }); + + test("refuses to provision in production", async () => { + vi.stubEnv("NODE_ENV", "production"); + + const db = createDatabase(neonInstantConnector()); + await expect(db.getInstance()).rejects.toThrow(/production/); + expect(instantPostgres).not.toHaveBeenCalled(); + }); +}); + +describe("connectors: neon client lifecycle", () => { + test("concurrent first queries share one client and provision once", async () => { + const db = createDatabase(neonInstantConnector()); + + await Promise.all([db.sql`SELECT 1`, db.sql`SELECT 2`, db.sql`SELECT 3`]); + + expect(instantPostgres).toHaveBeenCalledOnce(); + expect(clients).toHaveLength(1); + expect(clients[0].connects).toBe(1); + }); + + test("disposing closes the client", async () => { + const db = createDatabase( + neonConnector({ url: "postgres://user@host/db" }), + ); + await db.getInstance(); + await db.dispose(); + + expect(clients[0].ends).toBe(1); + }); + + test("a failed connect is retried rather than cached", async () => { + state.connect = () => Promise.reject(new Error("boom")); + + const connector = neonConnector({ url: "postgres://user@host/db" }); + await expect(connector.getInstance()).rejects.toThrow("boom"); + + // A later attempt reconnects rather than replaying the cached failure. + state.connect = () => Promise.resolve(); + await expect(connector.getInstance()).resolves.toBeInstanceOf(MockClient); + expect(clients).toHaveLength(2); + }); + + test("disposing after a failed connect does not re-throw", async () => { + state.connect = () => Promise.reject(new Error("boom")); + + const db = createDatabase( + neonConnector({ url: "postgres://user@host/db" }), + ); + await expect(db.getInstance()).rejects.toThrow("boom"); + await expect(db.dispose()).resolves.toBeUndefined(); + }); +}); + +describe("neon: normalizeParams", () => { + test("rewrites placeholders into $n", () => { + expect(normalizeParams("SELECT * FROM t WHERE a = ? AND b = ?")).toBe( + "SELECT * FROM t WHERE a = $1 AND b = $2", + ); + }); + + test("leaves `?` inside string literals alone", () => { + expect(normalizeParams("SELECT ? WHERE note = 'why?'")).toBe( + "SELECT $1 WHERE note = 'why?'", + ); + }); + + test("leaves jsonb operators alone", () => { + expect(normalizeParams("SELECT * FROM t WHERE data ?| ? AND d ?& ?")).toBe( + "SELECT * FROM t WHERE data ?| $1 AND d ?& $2", + ); + }); + + test("leaves `?` inside comments alone", () => { + expect(normalizeParams("SELECT ? -- why?\nFROM t")).toBe( + "SELECT $1 -- why?\nFROM t", + ); + }); +});