From a07990de977fb60ab4af1e8f3a2250454dedbb34 Mon Sep 17 00:00:00 2001 From: Maxwell Brown Date: Wed, 14 Aug 2024 21:35:15 -0400 Subject: [PATCH] Add support for obtaining raw SQL query results from the underlying SQL client (#3457) --- .changeset/metal-geese-move.md | 59 +++++++++++++++++++ packages/sql-d1/src/D1Client.ts | 25 +++++--- packages/sql-mssql/src/MssqlClient.ts | 5 +- packages/sql-mysql2/src/MysqlClient.ts | 33 ++++++++--- packages/sql-pg/src/PgClient.ts | 5 +- packages/sql-sqlite-bun/src/SqliteClient.ts | 5 +- packages/sql-sqlite-node/src/SqliteClient.ts | 9 ++- .../src/SqliteClient.ts | 5 +- packages/sql-sqlite-wasm/src/SqliteClient.ts | 5 +- packages/sql/src/SqlConnection.ts | 11 +++- packages/sql/src/Statement.ts | 1 + packages/sql/src/internal/client.ts | 8 ++- packages/sql/src/internal/statement.ts | 13 +++- 13 files changed, 155 insertions(+), 29 deletions(-) create mode 100644 .changeset/metal-geese-move.md diff --git a/.changeset/metal-geese-move.md b/.changeset/metal-geese-move.md new file mode 100644 index 0000000000..b8f022290f --- /dev/null +++ b/.changeset/metal-geese-move.md @@ -0,0 +1,59 @@ +--- +"@effect/sql-sqlite-react-native": minor +"@effect/sql-sqlite-node": minor +"@effect/sql-sqlite-wasm": minor +"@effect/sql-sqlite-bun": minor +"@effect/sql-mysql2": minor +"@effect/sql-mssql": minor +"@effect/sql-d1": minor +"@effect/sql-pg": minor +"@effect/sql": minor +--- + +Add support for executing raw SQL queries with the underlying SQL client. + +This is primarily useful when the SQL client returns special results for certain +query types. + +For example, because MySQL does not support the `RETURNING` clause, the `mysql2` +client will return a [`ResultSetHeader`](https://sidorares.github.io/node-mysql2/docs/documentation/typescript-examples#resultsetheader) +for `INSERT`, `UPDATE`, `DELETE`, and `TRUNCATE` operations. + +To gain access to the raw results of a query, you can use the `.raw` property on +the `Statement`: + +```ts +import * as Effect from "effect/Effect" +import * as SqlClient from "@effect/sql/SqlClient" +import * as MysqlClient from "@effect/sql/MysqlClient" + +const DatabaseLive = MysqlClient.layer({ + database: Config.succeed("database"), + username: Config.succeed("root"), + password: Config.succeed(Redacted.make("password")), +}) + +const program = Effect.gen(function*() { + const sql = yield* SqlClient.SqlClient + + const result = yield* sql`INSERT INTO usernames VALUES ("Bob")`.raw + + console.log(result) + /** + * ResultSetHeader { + * fieldCount: 0, + * affectedRows: 1, + * insertId: 0, + * info: '', + * serverStatus: 2, + * warningStatus: 0, + * changedRows: 0 + * } + */ +}) + +program.pipe( + Effect.provide(DatabaseLive), + Effect.runPromise +) +``` diff --git a/packages/sql-d1/src/D1Client.ts b/packages/sql-d1/src/D1Client.ts index 95d9b55cf3..17b0cf72e6 100644 --- a/packages/sql-d1/src/D1Client.ts +++ b/packages/sql-d1/src/D1Client.ts @@ -102,19 +102,25 @@ export const make = ( catch: (cause) => new SqlError({ cause, message: `Failed to execute statement` }) }) - const run = ( + const runRaw = ( + sql: string, + params: ReadonlyArray = [] + ) => runStatement(db.prepare(sql), params) + + const runCached = ( sql: string, params: ReadonlyArray = [] ) => Effect.flatMap(prepareCache.get(sql), (s) => runStatement(s, params)) - const runRaw = ( + const runUncached = ( sql: string, params: ReadonlyArray = [] - ) => Effect.map(runStatement(db.prepare(sql), params), transformRows) + ) => Effect.map(runRaw(sql, params), transformRows) const runTransform = options.transformResultNames - ? (sql: string, params?: ReadonlyArray) => Effect.map(run(sql, params), transformRows) - : run + ? (sql: string, params?: ReadonlyArray) => + Effect.map(runCached(sql, params), transformRows) + : runCached const runValues = ( sql: string, @@ -139,14 +145,17 @@ export const make = ( execute(sql, params) { return runTransform(sql, params) }, + executeRaw(sql, params) { + return runRaw(sql, params) + }, executeValues(sql, params) { return runValues(sql, params) }, executeWithoutTransform(sql, params) { - return run(sql, params) + return runCached(sql, params) }, - executeRaw(sql, params) { - return runRaw(sql, params) + executeUnprepared(sql, params) { + return runUncached(sql, params) }, executeStream(_sql, _params) { return Effect.dieMessage("executeStream not implemented") diff --git a/packages/sql-mssql/src/MssqlClient.ts b/packages/sql-mssql/src/MssqlClient.ts index 124523ff1b..4cc6660888 100644 --- a/packages/sql-mssql/src/MssqlClient.ts +++ b/packages/sql-mssql/src/MssqlClient.ts @@ -260,13 +260,16 @@ export const make = ( execute(sql, params) { return run(sql, params) }, + executeRaw(sql, params) { + return run(sql, params, false) + }, executeWithoutTransform(sql, params) { return run(sql, params, false) }, executeValues(sql, params) { return run(sql, params, true, true) }, - executeRaw(sql, params) { + executeUnprepared(sql, params) { return run(sql, params) }, executeStream() { diff --git a/packages/sql-mysql2/src/MysqlClient.ts b/packages/sql-mysql2/src/MysqlClient.ts index 0ac58807f9..1473bca5d9 100644 --- a/packages/sql-mysql2/src/MysqlClient.ts +++ b/packages/sql-mysql2/src/MysqlClient.ts @@ -89,40 +89,57 @@ export const make = ( class ConnectionImpl implements Connection { constructor(private readonly conn: Mysql.PoolConnection | Mysql.Pool) {} - private run( + private runRaw( sql: string, values?: ReadonlyArray, - transform = true, rowsAsArray = false, method: "execute" | "query" = "execute" ) { - return Effect.async, SqlError>((resume) => { + return Effect.async((resume) => { ;(this.conn as any)[method]({ sql, values, rowsAsArray - }, (cause: unknown | null, results: ReadonlyArray, _fields: any) => { + }, (cause: unknown | null, results: unknown, _fields: any) => { if (cause) { resume(Effect.fail(new SqlError({ cause, message: "Failed to execute statement" }))) - } else if (transform && !rowsAsArray && options.transformResultNames) { - resume(Effect.succeed(transformRows(results))) } else { - resume(Effect.succeed(Array.isArray(results) ? results : [])) + resume(Effect.succeed(results)) } }) }) } + private run( + sql: string, + values?: ReadonlyArray, + transform = true, + rowsAsArray = false, + method: "execute" | "query" = "execute" + ) { + return this.runRaw(sql, values, rowsAsArray, method).pipe( + Effect.map((results) => { + if (transform && !rowsAsArray && options.transformResultNames) { + return transformRows(results as ReadonlyArray) + } + return Array.isArray(results) ? results : [] + }) + ) + } + execute(sql: string, params: ReadonlyArray) { return this.run(sql, params) } + executeRaw(sql: string, params: ReadonlyArray) { + return this.runRaw(sql, params, true) + } executeWithoutTransform(sql: string, params: ReadonlyArray) { return this.run(sql, params, false) } executeValues(sql: string, params: ReadonlyArray) { return this.run(sql, params, true, true) } - executeRaw(sql: string, params?: ReadonlyArray) { + executeUnprepared(sql: string, params?: ReadonlyArray) { return this.run(sql, params, true, false, "query") } executeStream(sql: string, params: ReadonlyArray) { diff --git a/packages/sql-pg/src/PgClient.ts b/packages/sql-pg/src/PgClient.ts index 097580d90e..f401165b3d 100644 --- a/packages/sql-pg/src/PgClient.ts +++ b/packages/sql-pg/src/PgClient.ts @@ -192,13 +192,16 @@ export const make = ( execute(sql: string, params: ReadonlyArray) { return this.runTransform(this.pg.unsafe(sql, params as any)) } + executeRaw(sql: string, params: ReadonlyArray) { + return this.run(this.pg.unsafe(sql, params as any)) + } executeWithoutTransform(sql: string, params: ReadonlyArray) { return this.run(this.pg.unsafe(sql, params as any)) } executeValues(sql: string, params: ReadonlyArray) { return this.run(this.pg.unsafe(sql, params as any).values()) } - executeRaw(sql: string, params?: ReadonlyArray) { + executeUnprepared(sql: string, params?: ReadonlyArray) { return this.runTransform(this.pg.unsafe(sql, params as any)) } executeStream(sql: string, params: ReadonlyArray) { diff --git a/packages/sql-sqlite-bun/src/SqliteClient.ts b/packages/sql-sqlite-bun/src/SqliteClient.ts index 58b05137fb..aea95b27b0 100644 --- a/packages/sql-sqlite-bun/src/SqliteClient.ts +++ b/packages/sql-sqlite-bun/src/SqliteClient.ts @@ -120,13 +120,16 @@ export const make = ( execute(sql, params) { return runTransform(sql, params) }, + executeRaw(sql, params) { + return run(sql, params) + }, executeValues(sql, params) { return runValues(sql, params) }, executeWithoutTransform(sql, params) { return run(sql, params) }, - executeRaw(sql, params) { + executeUnprepared(sql, params) { return runTransform(sql, params) }, executeStream(_sql, _params) { diff --git a/packages/sql-sqlite-node/src/SqliteClient.ts b/packages/sql-sqlite-node/src/SqliteClient.ts index a4468d7df7..d18a49921b 100644 --- a/packages/sql-sqlite-node/src/SqliteClient.ts +++ b/packages/sql-sqlite-node/src/SqliteClient.ts @@ -136,7 +136,7 @@ export const make = ( params: ReadonlyArray = [] ) => Effect.flatMap(prepareCache.get(sql), (s) => runStatement(s, params)) - const runRaw = ( + const runUnprepared = ( sql: string, params: ReadonlyArray = [] ) => Effect.map(runStatement(db.prepare(sql), params), transformRows) @@ -172,14 +172,17 @@ export const make = ( execute(sql, params) { return runTransform(sql, params) }, + executeRaw(sql, params) { + return run(sql, params) + }, executeValues(sql, params) { return runValues(sql, params) }, executeWithoutTransform(sql, params) { return run(sql, params) }, - executeRaw(sql, params) { - return runRaw(sql, params) + executeUnprepared(sql, params) { + return runUnprepared(sql, params) }, executeStream(_sql, _params) { return Effect.dieMessage("executeStream not implemented") diff --git a/packages/sql-sqlite-react-native/src/SqliteClient.ts b/packages/sql-sqlite-react-native/src/SqliteClient.ts index b4484dcda4..1b40ad1254 100644 --- a/packages/sql-sqlite-react-native/src/SqliteClient.ts +++ b/packages/sql-sqlite-react-native/src/SqliteClient.ts @@ -149,6 +149,9 @@ export const make = ( execute(sql, params) { return runTransform(sql, params) }, + executeRaw(sql, params) { + return run(sql, params) + }, executeValues(sql, params) { return Effect.map(run(sql, params), (results) => { if (results.length === 0) { @@ -161,7 +164,7 @@ export const make = ( executeWithoutTransform(sql, params) { return run(sql, params) }, - executeRaw(sql, params) { + executeUnprepared(sql, params) { return runTransform(sql, params) }, executeStream() { diff --git a/packages/sql-sqlite-wasm/src/SqliteClient.ts b/packages/sql-sqlite-wasm/src/SqliteClient.ts index baa69cd335..60aafc4e25 100644 --- a/packages/sql-sqlite-wasm/src/SqliteClient.ts +++ b/packages/sql-sqlite-wasm/src/SqliteClient.ts @@ -132,13 +132,16 @@ export const make = ( execute(sql, params) { return runTransform(sql, params) }, + executeRaw(sql, params) { + return run(sql, params) + }, executeValues(sql, params) { return run(sql, params, "array") }, executeWithoutTransform(sql, params) { return run(sql, params) }, - executeRaw(sql, params) { + executeUnprepared(sql, params) { return runTransform(sql, params) }, executeStream() { diff --git a/packages/sql/src/SqlConnection.ts b/packages/sql/src/SqlConnection.ts index 68272a7367..c083419c6e 100644 --- a/packages/sql/src/SqlConnection.ts +++ b/packages/sql/src/SqlConnection.ts @@ -18,6 +18,15 @@ export interface Connection { params: ReadonlyArray ) => Effect, SqlError> + /** + * Execute the specified SQL query and return the raw results directly from + * underlying SQL client. + */ + readonly executeRaw: ( + sql: string, + params: ReadonlyArray + ) => Effect + readonly executeStream: ( sql: string, params: ReadonlyArray @@ -33,7 +42,7 @@ export interface Connection { params: ReadonlyArray ) => Effect>, SqlError> - readonly executeRaw: ( + readonly executeUnprepared: ( sql: string, params?: ReadonlyArray | undefined ) => Effect, SqlError> diff --git a/packages/sql/src/Statement.ts b/packages/sql/src/Statement.ts index 9b6ac8c770..921dd8616b 100644 --- a/packages/sql/src/Statement.ts +++ b/packages/sql/src/Statement.ts @@ -45,6 +45,7 @@ export type Dialect = "sqlite" | "pg" | "mysql" | "mssql" * @since 1.0.0 */ export interface Statement extends Fragment, Effect, SqlError>, Pipeable { + readonly raw: Effect readonly withoutTransform: Effect, SqlError> readonly stream: Stream.Stream readonly values: Effect>, SqlError> diff --git a/packages/sql/src/internal/client.ts b/packages/sql/src/internal/client.ts index ff023c1afb..7505da7062 100644 --- a/packages/sql/src/internal/client.ts +++ b/packages/sql/src/internal/client.ts @@ -76,7 +76,7 @@ export function make({ ( [scope, conn] ) => - conn.executeRaw(id === 0 ? beginTransaction : savepoint(`effect_sql_${id}`)).pipe( + conn.executeUnprepared(id === 0 ? beginTransaction : savepoint(`effect_sql_${id}`)).pipe( Effect.zipRight(Effect.locally( restore(effect), FiberRef.currentContext, @@ -90,7 +90,7 @@ export function make({ if (Exit.isSuccess(exit)) { if (id === 0) { span.event("db.transaction.commit", clock.unsafeCurrentTimeNanos()) - effect = Effect.orDie(conn.executeRaw(commit)) + effect = Effect.orDie(conn.executeUnprepared(commit)) } else { span.event("db.transaction.savepoint", clock.unsafeCurrentTimeNanos()) effect = Effect.void @@ -98,7 +98,9 @@ export function make({ } else { span.event("db.transaction.rollback", clock.unsafeCurrentTimeNanos()) effect = Effect.orDie( - id > 0 ? conn.executeRaw(rollbackSavepoint(`effect_sql_${id}`)) : conn.executeRaw(rollback) + id > 0 + ? conn.executeUnprepared(rollbackSavepoint(`effect_sql_${id}`)) + : conn.executeUnprepared(rollback) ) } const withScope = scope !== undefined ? Effect.ensuring(effect, Scope.close(scope, exit)) : effect diff --git a/packages/sql/src/internal/statement.ts b/packages/sql/src/internal/statement.ts index 629be891fc..162a70a180 100644 --- a/packages/sql/src/internal/statement.ts +++ b/packages/sql/src/internal/statement.ts @@ -130,6 +130,14 @@ export class StatementPrimitive extends Effectable.Class, Er ) } + get raw(): Effect.Effect { + return this.withConnection( + "executeRaw", + (connection, sql, params) => connection.executeRaw(sql, params), + true + ) + } + get stream(): Stream.Stream { return Stream.unwrapScoped(Effect.flatMap( Effect.makeSpanScoped("sql.execute", { kind: "client", captureStackTrace: false }), @@ -154,7 +162,10 @@ export class StatementPrimitive extends Effectable.Class, Er } get unprepared(): Effect.Effect, Error.SqlError> { - return this.withConnection("executeRaw", (connection, sql, params) => connection.executeRaw(sql, params)) + return this.withConnection( + "executeUnprepared", + (connection, sql, params) => connection.executeUnprepared(sql, params) + ) } compile(withoutTransform?: boolean | undefined) {