diff --git a/.changeset/fair-kings-hope.md b/.changeset/fair-kings-hope.md new file mode 100644 index 0000000000..243163660e --- /dev/null +++ b/.changeset/fair-kings-hope.md @@ -0,0 +1,5 @@ +--- +"@effect/sql": patch +--- + +fix mysql support for Model.makeRepository diff --git a/.changeset/rare-ducks-clean.md b/.changeset/rare-ducks-clean.md new file mode 100644 index 0000000000..6770bddd26 --- /dev/null +++ b/.changeset/rare-ducks-clean.md @@ -0,0 +1,5 @@ +--- +"@effect/sql": patch +--- + +add insertVoid & updateVoid to Model repository diff --git a/packages/sql-mysql2/test/Model.test.ts b/packages/sql-mysql2/test/Model.test.ts new file mode 100644 index 0000000000..29a4dc4b1a --- /dev/null +++ b/packages/sql-mysql2/test/Model.test.ts @@ -0,0 +1,69 @@ +import { Schema } from "@effect/schema" +import { Model, SqlClient } from "@effect/sql" +import { assert, describe, it } from "@effect/vitest" +import { Effect } from "effect" +import { MysqlContainer } from "./utils.js" + +class User extends Model.Class("User")({ + id: Model.Generated(Schema.Int), + name: Schema.String, + age: Schema.Int +}) {} + +describe("Model", () => { + it.effect("insert returns result", () => + Effect.gen(function*() { + const repo = yield* Model.makeRepository(User, { + tableName: "users", + idColumn: "id", + spanPrefix: "UserRepository" + }) + const sql = yield* SqlClient.SqlClient + yield* sql`CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)` + + const result = yield* repo.insert(User.insert.make({ name: "Alice", age: 30 })) + assert.deepStrictEqual(result, new User({ id: 1, name: "Alice", age: 30 })) + }).pipe( + Effect.provide(MysqlContainer.ClientLive), + Effect.catchTag("ContainerError", () => Effect.void) + ), { timeout: 60_000 }) + + it.effect("insertVoid", () => + Effect.gen(function*() { + const repo = yield* Model.makeRepository(User, { + tableName: "users", + idColumn: "id", + spanPrefix: "UserRepository" + }) + const sql = yield* SqlClient.SqlClient + yield* sql`CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)` + + const result = yield* repo.insertVoid(User.insert.make({ name: "Alice", age: 30 })) + assert.strictEqual(result, void 0) + }).pipe( + Effect.provide(MysqlContainer.ClientLive), + Effect.catchTag("ContainerError", () => Effect.void) + ), { timeout: 60_000 }) + + it.scopedLive("insert data loader returns result", () => + Effect.gen(function*() { + const repo = yield* Model.makeDataLoaders(User, { + tableName: "users", + idColumn: "id", + spanPrefix: "UserRepository", + window: 10 + }) + const sql = yield* SqlClient.SqlClient + yield* sql`CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)` + + const [alice, john] = yield* Effect.all([ + repo.insert(User.insert.make({ name: "Alice", age: 30 })), + repo.insert(User.insert.make({ name: "John", age: 30 })) + ], { batching: true }) + assert.deepStrictEqual(alice.name, "Alice") + assert.deepStrictEqual(john.name, "John") + }).pipe( + Effect.provide(MysqlContainer.ClientLive), + Effect.catchTag("ContainerError", () => Effect.void) + ), { timeout: 20_000 }) +}) diff --git a/packages/sql-mysql2/test/utils.ts b/packages/sql-mysql2/test/utils.ts index ad15fe4db6..4842d90269 100644 --- a/packages/sql-mysql2/test/utils.ts +++ b/packages/sql-mysql2/test/utils.ts @@ -23,8 +23,8 @@ export class MysqlContainer extends Context.Tag("test/MysqlContainer")< ) static ClientLive = Layer.unwrapEffect( - Effect.gen(function*(_) { - const container = yield* _(MysqlContainer) + Effect.gen(function*() { + const container = yield* MysqlContainer return MysqlClient.layer({ url: Config.succeed(Redacted.make(container.getConnectionUri())) }) diff --git a/packages/sql/src/Model.ts b/packages/sql/src/Model.ts index 11fcc6f5c9..6f65997c70 100644 --- a/packages/sql/src/Model.ts +++ b/packages/sql/src/Model.ts @@ -621,9 +621,15 @@ export const makeRepository = < readonly insert: ( insert: S["insert"]["Type"] ) => Effect.Effect + readonly insertVoid: ( + insert: S["insert"]["Type"] + ) => Effect.Effect readonly update: ( update: S["update"]["Type"] ) => Effect.Effect + readonly updateVoid: ( + update: S["update"]["Type"] + ) => Effect.Effect readonly findById: ( id: Schema.Schema.Type ) => Effect.Effect, never, S["Context"] | Schema.Schema.Context> @@ -637,11 +643,20 @@ export const makeRepository = < Effect.gen(function*() { const sql = yield* SqlClient const idSchema = Model.fields[options.idColumn] as Schema.Schema.Any + const idColumn = options.idColumn as string const insertSchema = SqlSchema.single({ Request: Model.insert, Result: Model, - execute: (request) => sql`insert into ${sql(options.tableName)} ${sql.insert(request).returning("*")}` + execute: (request) => + sql.onDialectOrElse({ + mysql: () => + sql`insert into ${sql(options.tableName)} ${sql.insert(request)}; +select * from ${sql(options.tableName)} where ${sql(idColumn)} = LAST_INSERT_ID();`.unprepared.pipe( + Effect.map(([, results]) => results as any) + ), + orElse: () => sql`insert into ${sql(options.tableName)} ${sql.insert(request).returning("*")}` + }) }) const insert = ( insert: S["insert"]["Type"] @@ -654,13 +669,42 @@ export const makeRepository = < }) ) as any + const insertVoidSchema = SqlSchema.void({ + Request: Model.insert, + execute: (request) => sql`insert into ${sql(options.tableName)} ${sql.insert(request)}` + }) + const insertVoid = ( + insert: S["insert"]["Type"] + ): Effect.Effect => + insertVoidSchema(insert).pipe( + Effect.orDie, + Effect.withSpan(`${options.spanPrefix}.insertVoid`, { + captureStackTrace: false, + attributes: { insert } + }) + ) as any + const updateSchema = SqlSchema.single({ Request: Model.update, Result: Model, execute: (request) => - sql`update ${sql(options.tableName)} set ${sql.update(request, [options.idColumn])} where ${ - sql(options.idColumn as string) - } = ${sql(request[options.idColumn])} returning *` + sql.onDialectOrElse({ + mysql: () => + sql`update ${sql(options.tableName)} set ${sql.update(request, [idColumn])} where ${sql(idColumn)} = ${ + request[idColumn] + }`.raw.pipe( + Effect.zipRight( + sql`select * from ${sql(options.tableName)} where ${sql(options.idColumn as string)} = ${ + request[idColumn] + }` + ), + sql.withTransaction + ), + orElse: () => + sql`update ${sql(options.tableName)} set ${sql.update(request, [idColumn])} where ${sql(idColumn)} = ${ + request[idColumn] + } returning *` + }) }) const update = ( update: S["update"]["Type"] @@ -673,10 +717,28 @@ export const makeRepository = < }) ) as any + const updateVoidSchema = SqlSchema.void({ + Request: Model.update, + execute: (request) => + sql`update ${sql(options.tableName)} set ${sql.update(request, [idColumn])} where ${sql(idColumn)} = ${ + request[idColumn] + }` + }) + const updateVoid = ( + update: S["update"]["Type"] + ): Effect.Effect => + updateVoidSchema(update).pipe( + Effect.orDie, + Effect.withSpan(`${options.spanPrefix}.updateVoid`, { + captureStackTrace: false, + attributes: { update } + }) + ) as any + const findByIdSchema = SqlSchema.findOne({ Request: idSchema, Result: Model, - execute: (id) => sql`select * from ${sql(options.tableName)} where ${sql(options.idColumn as string)} = ${id}` + execute: (id) => sql`select * from ${sql(options.tableName)} where ${sql(idColumn)} = ${id}` }) const findById = ( id: Schema.Schema.Type @@ -691,7 +753,7 @@ export const makeRepository = < const deleteSchema = SqlSchema.void({ Request: idSchema, - execute: (id) => sql`delete from ${sql(options.tableName)} where ${sql(options.idColumn as string)} = ${id}` + execute: (id) => sql`delete from ${sql(options.tableName)} where ${sql(idColumn)} = ${id}` }) const delete_ = ( id: Schema.Schema.Type @@ -704,7 +766,7 @@ export const makeRepository = < }) ) as any - return { insert, update, findById, delete: delete_ } as const + return { insert, insertVoid, update, updateVoid, findById, delete: delete_ } as const }) /** @@ -728,6 +790,7 @@ export const makeDataLoaders = < ): Effect.Effect< { readonly insert: (insert: S["insert"]["Type"]) => Effect.Effect + readonly insertVoid: (insert: S["insert"]["Type"]) => Effect.Effect readonly findById: (id: Schema.Schema.Type) => Effect.Effect> readonly delete: (id: Schema.Schema.Type) => Effect.Effect }, @@ -737,11 +800,21 @@ export const makeDataLoaders = < Effect.gen(function*() { const sql = yield* SqlClient const idSchema = Model.fields[options.idColumn] as Schema.Schema.Any + const idColumn = options.idColumn as string const insertResolver = yield* SqlResolver.ordered(`${options.spanPrefix}/insert`, { Request: Model.insert, Result: Model, - execute: (request) => sql`insert into ${sql(options.tableName)} ${sql.insert(request).returning("*")}` + execute: (request) => + sql.onDialectOrElse({ + mysql: () => + Effect.forEach(request, (request) => + sql`insert into ${sql(options.tableName)} ${sql.insert(request)}; +select * from ${sql(options.tableName)} where ${sql(idColumn)} = LAST_INSERT_ID();`.unprepared.pipe( + Effect.map(([, results]) => results[0] as any) + ), { concurrency: 10 }), + orElse: () => sql`insert into ${sql(options.tableName)} ${sql.insert(request).returning("*")}` + }) }) const insertLoader = yield* RRX.dataLoader(insertResolver, { window: options.window, @@ -759,6 +832,26 @@ export const makeDataLoaders = < }) ) as any + const insertVoidResolver = yield* SqlResolver.void(`${options.spanPrefix}/insertVoid`, { + Request: Model.insert, + execute: (request) => sql`insert into ${sql(options.tableName)} ${sql.insert(request)}` + }) + const insertVoidLoader = yield* RRX.dataLoader(insertVoidResolver, { + window: options.window, + maxBatchSize: options.maxBatchSize! + }) + const insertVoidExecute = insertVoidResolver.makeExecute(insertVoidLoader) + const insertVoid = ( + insert: S["insert"]["Type"] + ): Effect.Effect => + insertVoidExecute(insert).pipe( + Effect.orDie, + Effect.withSpan(`${options.spanPrefix}.insertVoid`, { + captureStackTrace: false, + attributes: { insert } + }) + ) as any + const findByIdResolver = yield* SqlResolver.grouped(`${options.spanPrefix}/findById`, { Request: idSchema, RequestGroupKey(id) { @@ -766,9 +859,9 @@ export const makeDataLoaders = < }, Result: Model, ResultGroupKey(request) { - return request[options.idColumn] + return request[idColumn] }, - execute: (ids) => sql`select * from ${sql(options.tableName)} where ${sql.in(options.idColumn as string, ids)}` + execute: (ids) => sql`select * from ${sql(options.tableName)} where ${sql.in(idColumn, ids)}` }) const findByIdLoader = yield* RRX.dataLoader(findByIdResolver, { window: options.window, @@ -786,7 +879,7 @@ export const makeDataLoaders = < const deleteResolver = yield* SqlResolver.void(`${options.spanPrefix}/delete`, { Request: idSchema, - execute: (ids) => sql`delete from ${sql(options.tableName)} where ${sql.in(options.idColumn as string, ids)}` + execute: (ids) => sql`delete from ${sql(options.tableName)} where ${sql.in(idColumn, ids)}` }) const deleteLoader = yield* RRX.dataLoader(deleteResolver, { window: options.window, @@ -802,5 +895,5 @@ export const makeDataLoaders = < }) ) as any - return { insert, findById, delete: delete_ } as const + return { insert, insertVoid, findById, delete: delete_ } as const })