Skip to content
1 change: 1 addition & 0 deletions docs/2.connectors/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Currently supported connectors:
- [PlanetScale](/connectors/planetscale)
- [PostgreSQL](/connectors/postgresql)
- [MySQL](/connectors/mysql)
- [Neon](/connectors/neon)
- [SQLite](/connectors/sqlite)

::read-more{to="https://github.com/unjs/db0/issues/32"}
Expand Down
32 changes: 28 additions & 4 deletions docs/2.connectors/neon.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,33 @@ icon: cbi:neon

> Connect DB0 to Neon Serverless Postgres.

:read-more{to="https://neon.tech/"}
:read-more{to="https://neon.tech"}

::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).
::
## Usage

For this connector, you need to install [`@neondatabase/serverless`](https://www.npmjs.com/package/@neondatabase/serverless) dependency:

:pm-install{name="@neondatabase/serverless"}

Use `neon` connector:

```js
import { createDatabase } from "db0";
import neonConnector from "db0/connectors/neon";

const db = createDatabase(
neonConnector({
/* options */
}),
);
```

## Options

### `url`

The URL of the Neon Serverless Postgres instance.

**Type:** `string`

:read-more{to="https://neon.tech/docs/serverless/serverless-driver#neon-function-configuration-options"}
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@cloudflare/workers-types": "^4.20260316.1",
"@electric-sql/pglite": "^0.3.16",
"@libsql/client": "^0.17.0",
"@neondatabase/serverless": "^1.0.2",
"@planetscale/database": "^1.19.0",
"@types/better-sqlite3": "^7.6.13",
"@types/bun": "^1.3.10",
Expand Down Expand Up @@ -74,6 +75,7 @@
"peerDependencies": {
"@electric-sql/pglite": "*",
"@libsql/client": "*",
"@neondatabase/serverless": "*",
"better-sqlite3": "*",
"drizzle-orm": "*",
"mysql2": "*",
Expand All @@ -83,6 +85,9 @@
"@libsql/client": {
"optional": true
},
"@neondatabase/serverless": {
"optional": true
},
"better-sqlite3": {
"optional": true
},
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/_connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ 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 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" | "node-sqlite" | "sqlite" | "pglite" | "planetscale" | "postgresql" | "sqlite3";

export type ConnectorOptions = {
"better-sqlite3": BetterSQLite3Options;
Expand All @@ -33,6 +34,7 @@ export type ConnectorOptions = {
"libsql": LibSQLNodeOptions;
"libsql-web": LibSQLWebOptions;
"mysql2": MySQL2Options;
"neon": NeonOptions;
"node-sqlite": NodeSQLiteOptions;
/** alias of node-sqlite */
"sqlite": NodeSQLiteOptions;
Expand All @@ -57,6 +59,7 @@ export const connectors: Record<ConnectorName, string> = Object.freeze({
"libsql": "db0/connectors/libsql/node",
"libsql-web": "db0/connectors/libsql/web",
"mysql2": "db0/connectors/mysql2",
"neon": "db0/connectors/neon",
"node-sqlite": "db0/connectors/node-sqlite",
/** alias of node-sqlite */
"sqlite": "db0/connectors/node-sqlite",
Expand Down
80 changes: 80 additions & 0 deletions src/connectors/neon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { neon } from "@neondatabase/serverless";
import type {
NeonQueryFunction,
HTTPTransactionOptions,
} from "@neondatabase/serverless";
import type { Connector, Primitive } from "db0";
import { BoundableStatement } from "./_internal/statement.ts";

export interface ConnectorOptions extends HTTPTransactionOptions<false, false> {
/**
* The URL of the Neon Serverless Postgres instance.
*
* @required
*/
url: string;
}

type InternalQuery = (sql: string, params?: Primitive[]) => Promise<unknown[]>;

export default function neonConnector(
opts: ConnectorOptions,
): Connector<NeonQueryFunction<false, false>> {
let _connection: NeonQueryFunction<false, false>;

function getConnection() {
if (_connection) {
return _connection;
}
const { url, ...transactionOptions } = opts;
_connection = neon(url, transactionOptions);
return _connection;
}

const query: InternalQuery = async (sql, params) => {
const connection = getConnection();
return connection.query(normalizeParams(sql), params);
};

return {
name: "neon",
dialect: "postgresql",
getInstance: () => getConnection(),
exec: (sql) => query(sql),
prepare: (sql) => new StatementWrapper(sql, query),
};
}

// https://www.postgresql.org/docs/9.3/sql-prepare.html
function normalizeParams(sql: string) {
let i = 0;
return sql.replace(/\?/g, () => `$${++i}`);
}

class StatementWrapper extends BoundableStatement<void> {
#query: InternalQuery;
#sql: string;

constructor(sql: string, query: InternalQuery) {
super();
this.#sql = sql;
this.#query = query;
}

async all(...params: Primitive[]) {
return this.#query(this.#sql, params);
}

async run(...params: Primitive[]) {
const res = await this.#query(this.#sql, params);
return {
success: true,
...res,
};
}
Comment on lines +68 to +74
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Spreading an array into an object produces unexpected results.

The res variable is an array (unknown[]), so spreading it into an object with ...res will produce numeric keys (0, 1, 2...) rather than meaningful properties. The run() method should only return { success: boolean } per the BoundableStatement interface contract.

πŸ› Proposed fix
   async run(...params: Primitive[]) {
-    const res = await this.#query(this.#sql, params);
+    await this.#query(this.#sql, params);
     return {
       success: true,
-      ...res,
     };
   }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async run(...params: Primitive[]) {
const res = await this.#query(this.#sql, params);
return {
success: true,
...res,
};
}
async run(...params: Primitive[]) {
await this.#query(this.#sql, params);
return {
success: true,
};
}
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/connectors/neon.ts` around lines 71 - 77, The run method spreads the
array result (res from this.#query) into the returned object, which creates
numeric keys and violates the BoundableStatement contract that run() should
return only { success: boolean }; update the run method (function run in this
class using this.#query and this.#sql) to stop spreading ...res and instead
return an object with only success: true (or success: false on error), and if
you need to expose rows keep them in a separate explicit property (e.g., rows)
rather than spreading the array into the object.


async get(...params: Primitive[]) {
const res = await this.#query(this.#sql, params);
return res[0];
}
}
28 changes: 28 additions & 0 deletions test/connectors/neon.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { vi, describe } from "vitest";
import { testConnector } from "./_tests";

vi.mock("@neondatabase/serverless", async () => {
const { PGlite } = await import("@electric-sql/pglite");
const pglite = await PGlite.create();
return {
neon: () => {
const queryFn = async (sql: string, params?: unknown[]) => {
const result = await pglite.query(sql, params);
return result.rows;
};
queryFn.query = queryFn;
return queryFn;
},
};
});

const { default: neonConnector } = await import("../../src/connectors/neon");

describe("connectors: neon (mocked with pglite)", () => {
testConnector({
dialect: "postgresql",
connector: neonConnector({
url: "postgresql://mock@localhost/mock",
}),
});
});
Loading