1import type * as runtime from "@cloudflare/workers-types";
2import * as Effect from "effect/Effect";
3import * as Binding from "../../Binding.ts";
4import type { RuntimeContext } from "../../RuntimeContext.ts";
5import type { Database } from "./Database.ts";
6
7/**
8 * Bind a {@link Database} to a Worker and obtain the Effect-native D1
9 * connection client (`prepare`, `exec`, `batch`, `raw`).
10 *
11 * `QueryDatabase` is a single identifier that is simultaneously the binding's
12 * Context tag, its type, and the callable —
13 * `yield* Cloudflare.D1.QueryDatabase(db)`.
14 *
15 * **Example:** Querying a database inside a Worker
16 * ```typescript
17 * const db = yield* Cloudflare.D1.QueryDatabase(MyDatabase);
18 * const rows = yield* db.prepare("SELECT * FROM users").all();
19 * ```
20 *
21 * @binding
22 * @product D1
23 * @category Storage & Databases
24 */
25export interface QueryDatabase extends Binding.Service<
26 QueryDatabase,
27 "Cloudflare.D1.QueryDatabase",
28 (database: Database) => Effect.Effect<QueryDatabaseClient>
29> {}
30
31export const QueryDatabase = Binding.Service<QueryDatabase>(
32 "Cloudflare.D1.QueryDatabase",
33);
34
35/**
36 * Effect-native wrapper around a Cloudflare D1 prepared statement.
37 *
38 * Construction (`prepare`) and binding (`bind`) are synchronous —
39 * they're plan builders and don't touch D1. Only the executors
40 * (`all`, `first`, `run`, `raw`) round-trip to the database, and
41 * those return Effects so they participate in the Effect runtime.
42 */
43export class PreparedStatement {
44 /** @internal */
45 constructor(
46 private readonly query: string,
47 private readonly binds: ReadonlyArray<unknown>,
48 private readonly rawEff: Effect.Effect<runtime.D1Database>,
49 ) {}
50
51 /**
52 * Return a new prepared statement bound to `values`. Subsequent
53 * calls replace the previous binding, matching the underlying
54 * Cloudflare runtime semantics.
55 */
56 bind(...values: unknown[]): PreparedStatement {
57 return new PreparedStatement(this.query, values, this.rawEff);
58 }
59
60 /** Run the query and return all matching rows. */
61 all<T = unknown>(): Effect.Effect<
62 runtime.D1Result<T>,
63 never,
64 RuntimeContext
65 > {
66 return this.withRuntime((stmt) => stmt.all<T>());
67 }
68
69 /** Run the query and return the first row, or `null` if no rows. */
70 first<T = unknown>(): Effect.Effect<T | null, never, RuntimeContext>;
71 first<T = unknown>(
72 column: string,
73 ): Effect.Effect<T | null, never, RuntimeContext>;
74 first<T = unknown>(
75 column?: string,
76 ): Effect.Effect<T | null, never, RuntimeContext> {
77 return this.withRuntime((stmt) =>
78 column !== undefined ? stmt.first<T>(column) : stmt.first<T>(),
79 );
80 }
81
82 /** Run the query as a mutation; returns row metadata. */
83 run<T = unknown>(): Effect.Effect<
84 runtime.D1Result<T>,
85 never,
86 RuntimeContext
87 > {
88 return this.withRuntime((stmt) => stmt.run<T>());
89 }
90
91 /** Run the query and return rows as flat arrays. */
92 raw<T = unknown[]>(): Effect.Effect<T[], never, RuntimeContext>;
93 raw<T = unknown[]>(options: {
94 columnNames: true;
95 }): Effect.Effect<[string[], ...T[]], never, RuntimeContext>;
96 raw<T = unknown[]>(options?: {
97 columnNames: true;
98 }): Effect.Effect<T[] | [string[], ...T[]], never, RuntimeContext> {
99 return this.withRuntime((stmt) =>
100 options
101 ? stmt.raw<T>(options)
102 : (stmt.raw<T>() as Promise<T[] | [string[], ...T[]]>),
103 );
104 }
105
106 /**
107 * Materialize the underlying Cloudflare prepared statement against
108 * a concrete D1 binding. Used by `QueryDatabaseClient.batch` to
109 * collect statements for a single transactional call.
110 *
111 * @internal
112 */
113 _build(raw: runtime.D1Database): runtime.D1PreparedStatement {
114 const stmt = raw.prepare(this.query);
115 return this.binds.length > 0 ? stmt.bind(...this.binds) : stmt;
116 }
117
118 private withRuntime<A>(
119 fn: (stmt: runtime.D1PreparedStatement) => Promise<A>,
120 ): Effect.Effect<A, never, RuntimeContext> {
121 return Effect.flatMap(this.rawEff, (raw) =>
122 Effect.promise(() => fn(this._build(raw))),
123 ) as Effect.Effect<A, never, RuntimeContext>;
124 }
125}
126
127export interface QueryDatabaseClient {
128 /**
129 * An Effect that resolves to the raw underlying Cloudflare D1Database binding.
130 * Use this when you need direct access for libraries like Better Auth.
131 */
132 raw: Effect.Effect<runtime.D1Database, never, RuntimeContext>;
133 /**
134 * Prepare a SQL statement. Returns synchronously — the network
135 * round-trip happens when you yield one of the statement's
136 * executors (`all`, `first`, `run`, `raw`).
137 */
138 prepare: (query: string) => PreparedStatement;
139 /**
140 * Execute raw SQL without prepared statements.
141 */
142 exec: (
143 query: string,
144 ) => Effect.Effect<runtime.D1ExecResult, never, RuntimeContext>;
145 /**
146 * Send multiple prepared statements in a single call.
147 * Statements execute sequentially and are rolled back on failure.
148 */
149 batch: <T = unknown>(
150 statements: PreparedStatement[],
151 ) => Effect.Effect<runtime.D1Result<T>[], never, RuntimeContext>;
152}