Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'

This commit is contained in:
-Puter
2026-07-19 03:25:10 +05:30
parent dd1071cc10
commit 2daf979036
2214 changed files with 673090 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
import { assert, describe, it } from "@effect/vitest"
import { Effect, Layer, Logger } from "effect"
import { SqlClient, Statement } from "effect/unstable/sql"
import { MysqlContainer } from "./utils.ts"
const SqlLogger = Layer.succeed(Statement.CurrentTransformer)((statement) => {
const [query, params] = statement.compile()
return Effect.log("executing sql").pipe(
Effect.annotateLogs({ query, params }),
Effect.as(statement)
)
})
describe("sql", () => {
it.effect("tranformers", () =>
Effect.gen(function*() {
const logs: Array<unknown> = []
const sql = yield* SqlClient.SqlClient
const result = yield* sql<{ result: number }>`SELECT 1 + 1 AS result`.pipe(
Effect.provide(Logger.layer([
Logger.make((log) => {
logs.push(log.message)
})
]))
)
assert.deepStrictEqual(result, [{ result: 2 }])
assert.deepStrictEqual(logs, [["executing sql"]])
}).pipe(
Effect.provide(MysqlContainer.layerClient.pipe(
Layer.provideMerge(SqlLogger)
)),
Effect.catchTag("ContainerError", () => Effect.void)
), { timeout: 60_000 })
})

View File

@@ -0,0 +1,16 @@
import { Layer } from "effect"
import * as KeyValueStoreTest from "effect-test/unstable/persistence/KeyValueStoreTest"
import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"
import { MysqlContainer } from "./utils.ts"
KeyValueStoreTest.suite(
"sql-mysql2",
KeyValueStore.layerSql().pipe(Layer.provide(MysqlContainer.layerClient))
)
KeyValueStoreTest.suite(
"sql-mysql2-custom-table",
KeyValueStore.layerSql({ table: "effect_key_value_store_custom" }).pipe(
Layer.provide(MysqlContainer.layerClient)
)
)

View File

@@ -0,0 +1,227 @@
import { assert, describe, it } from "@effect/vitest"
import { Cause, Effect, Schema } from "effect"
import { Model } from "effect/unstable/schema"
import { SqlClient, SqlModel, SqlResolver } from "effect/unstable/sql"
import { MysqlContainer } from "./utils.ts"
class User extends Model.Class<User>("User")({
id: Schema.Int.pipe(
Model.FieldExcept(["insert"])
),
name: Schema.String,
age: Schema.Int
}) {}
class SoftDeleteUser extends Model.Class<SoftDeleteUser>("SoftDeleteUser")({
id: Schema.Int.pipe(
Model.FieldExcept(["insert"])
),
name: Schema.String,
deletedAt: Schema.NullOr(Schema.String).pipe(
Model.FieldOnly(["select", "update"])
)
}) {}
describe("SqlModel", () => {
it.effect("insert returns result", () =>
Effect.gen(function*() {
const repo = yield* SqlModel.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.layerClient),
Effect.catchTag("ContainerError", () => Effect.void)
), { timeout: 60_000 })
it.effect("insert returns result with transforms", () =>
Effect.gen(function*() {
const repo = yield* SqlModel.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.layerClientWithTransforms),
Effect.catchTag("ContainerError", () => Effect.void)
), { timeout: 60_000 })
it.effect("insertVoid", () =>
Effect.gen(function*() {
const repo = yield* SqlModel.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.layerClient),
Effect.catchTag("ContainerError", () => Effect.void)
), { timeout: 60_000 })
it.live("insert data loader returns result", () =>
Effect.gen(function*() {
const repo = yield* SqlModel.makeResolvers(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 [alice, john] = yield* Effect.all([
SqlResolver.request(User.insert.make({ name: "Alice", age: 30 }), repo.insert),
SqlResolver.request(User.insert.make({ name: "John", age: 30 }), repo.insert)
], { concurrency: "unbounded" })
assert.deepStrictEqual(alice.name, "Alice")
assert.deepStrictEqual(john.name, "John")
}).pipe(
Effect.provide(MysqlContainer.layerClient),
Effect.catchTag("ContainerError", () => Effect.void)
), { timeout: 60_000 })
it.live("findById data loader", () =>
Effect.gen(function*() {
const repo = yield* SqlModel.makeResolvers(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 alice = yield* SqlResolver.request(User.insert.make({ name: "Alice", age: 30 }), repo.insert)
const john = yield* SqlResolver.request(User.insert.make({ name: "John", age: 30 }), repo.insert)
const [alice2, john2] = yield* Effect.all([
SqlResolver.request(alice.id, repo.findById),
SqlResolver.request(john.id, repo.findById)
], { concurrency: "unbounded" })
assert.deepStrictEqual(alice2.name, "Alice")
assert.deepStrictEqual(john2.name, "John")
}).pipe(
Effect.provide(MysqlContainer.layerClient),
Effect.catchTag("ContainerError", () => Effect.void)
), { timeout: 60_000 })
it.effect("findById ignores soft deleted rows", () =>
Effect.gen(function*() {
const repo = yield* SqlModel.makeRepository(SoftDeleteUser, {
tableName: "soft_delete_users",
idColumn: "id",
spanPrefix: "SoftDeleteUserRepository",
softDeleteColumn: "deletedAt"
})
const sql = yield* SqlClient.SqlClient
yield* sql`CREATE TABLE soft_delete_users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), deletedAt VARCHAR(255) NULL)`
const alice = yield* repo.insert(SoftDeleteUser.insert.make({ name: "Alice" }))
const bob = yield* repo.insert(SoftDeleteUser.insert.make({ name: "Bob" }))
yield* sql`UPDATE soft_delete_users SET deletedAt = CURRENT_TIMESTAMP WHERE id = ${bob.id}`
const aliceResult = yield* repo.findById(alice.id)
assert.deepStrictEqual(aliceResult, new SoftDeleteUser({ id: alice.id, name: "Alice", deletedAt: null }))
const error = yield* Effect.flip(repo.findById(bob.id))
assert.isTrue(Cause.isNoSuchElementError(error))
yield* repo.delete(alice.id)
const rows = yield* sql<
{ deletedAt: string | null }
>`SELECT deletedAt FROM soft_delete_users WHERE id = ${alice.id}`
assert.strictEqual(rows.length, 1)
assert.isNotNull(rows[0]!.deletedAt)
const deletedError = yield* Effect.flip(repo.findById(alice.id))
assert.isTrue(Cause.isNoSuchElementError(deletedError))
}).pipe(
Effect.provide(MysqlContainer.layerClient),
Effect.catchTag("ContainerError", () => Effect.void)
), { timeout: 60_000 })
it.live("findById data loader ignores soft deleted rows", () =>
Effect.gen(function*() {
const repo = yield* SqlModel.makeResolvers(SoftDeleteUser, {
tableName: "soft_delete_user_resolvers",
idColumn: "id",
spanPrefix: "SoftDeleteUserRepository",
softDeleteColumn: "deletedAt"
})
const sql = yield* SqlClient.SqlClient
yield* sql`CREATE TABLE soft_delete_user_resolvers (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), deletedAt VARCHAR(255) NULL)`
const alice = yield* SqlResolver.request(SoftDeleteUser.insert.make({ name: "Alice" }), repo.insert)
const bob = yield* SqlResolver.request(SoftDeleteUser.insert.make({ name: "Bob" }), repo.insert)
yield* sql`UPDATE soft_delete_user_resolvers SET deletedAt = CURRENT_TIMESTAMP WHERE id = ${bob.id}`
const aliceResult = yield* SqlResolver.request(alice.id, repo.findById)
assert.deepStrictEqual(aliceResult, new SoftDeleteUser({ id: alice.id, name: "Alice", deletedAt: null }))
const error = yield* Effect.flip(SqlResolver.request(bob.id, repo.findById))
assert.isTrue(Cause.isNoSuchElementError(error))
yield* SqlResolver.request(alice.id, repo.delete)
const rows = yield* sql<
{ deletedAt: string | null }
>`SELECT deletedAt FROM soft_delete_user_resolvers WHERE id = ${alice.id}`
assert.strictEqual(rows.length, 1)
assert.isNotNull(rows[0]!.deletedAt)
const deletedError = yield* Effect.flip(SqlResolver.request(alice.id, repo.findById))
assert.isTrue(Cause.isNoSuchElementError(deletedError))
}).pipe(
Effect.provide(MysqlContainer.layerClient),
Effect.catchTag("ContainerError", () => Effect.void)
), { timeout: 60_000 })
it.effect("update returns result", () =>
Effect.gen(function*() {
const repo = yield* SqlModel.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)`
let result = yield* repo.insert(User.insert.make({ name: "Alice", age: 30 }))
result = yield* repo.update(User.update.make({ ...result, name: "Bob" }))
assert.deepStrictEqual(result, new User({ id: 1, name: "Bob", age: 30 }))
}).pipe(
Effect.provide(MysqlContainer.layerClient),
Effect.catchTag("ContainerError", () => Effect.void)
), { timeout: 60_000 })
it.effect("update returns result with transforms", () =>
Effect.gen(function*() {
const repo = yield* SqlModel.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)`
let result = yield* repo.insert(User.insert.make({ name: "Alice", age: 30 }))
result = yield* repo.update(User.update.make({ ...result, name: "Bob" }))
assert.deepStrictEqual(result, new User({ id: 1, name: "Bob", age: 30 }))
}).pipe(
Effect.provide(MysqlContainer.layerClientWithTransforms),
Effect.catchTag("ContainerError", () => Effect.void)
), { timeout: 60_000 })
})

View File

@@ -0,0 +1,35 @@
import { MysqlClient } from "@effect/sql-mysql2"
import { assert, describe, it } from "@effect/vitest"
import { Effect, Stream } from "effect"
import { MysqlContainer } from "./utils.ts"
describe("MysqlClient", () => {
it.effect("stream returns same rows as direct execution", () =>
Effect.gen(function*() {
const sql = yield* MysqlClient.MysqlClient
yield* sql`DROP TABLE IF EXISTS stream_regression_test`
yield* sql`CREATE TABLE stream_regression_test (id INT PRIMARY KEY AUTO_INCREMENT, value VARCHAR(255))`
const testData = Array.from({ length: 100 }, (_, i) => ({ value: `row_${i}` }))
yield* sql`INSERT INTO stream_regression_test ${sql.insert(testData)}`
const directResults = yield* sql`SELECT * FROM stream_regression_test`
const streamResults = yield* Stream.runCollect(
sql`SELECT * FROM stream_regression_test`.stream
)
yield* sql`DROP TABLE stream_regression_test`
assert.strictEqual(
streamResults.length,
directResults.length,
`Stream returned ${streamResults.length} rows, expected ${directResults.length}`
)
}).pipe(
Effect.provide(MysqlContainer.layerClient),
Effect.catchTag("ContainerError", () => Effect.void)
), { timeout: 60_000 })
})

View File

@@ -0,0 +1,22 @@
import { Layer } from "effect"
import * as PersistedCacheTest from "effect-test/unstable/persistence/PersistedCacheTest"
import * as PersistedQueueTest from "effect-test/unstable/persistence/PersistedQueueTest"
import { PersistedQueue, Persistence } from "effect/unstable/persistence"
import { MysqlContainer } from "./utils.ts"
PersistedCacheTest.suite(
"sql-mysql2-multi",
Persistence.layerSqlMultiTable.pipe(Layer.provide(MysqlContainer.layerClient))
)
PersistedCacheTest.suite(
"sql-mysql2-single",
Persistence.layerSql.pipe(Layer.provide(MysqlContainer.layerClient))
)
PersistedQueueTest.suite(
"sql-mysql2",
PersistedQueue.layerStoreSql().pipe(
Layer.provide(MysqlContainer.layerClient)
)
)

View File

@@ -0,0 +1,133 @@
import { assert, describe, it } from "@effect/vitest"
import { Effect } from "effect"
import * as Reactivity from "effect/unstable/reactivity/Reactivity"
import type * as SqlError from "effect/unstable/sql/SqlError"
import { vi } from "vitest"
const state: {
connectCause: unknown
} = {
connectCause: null
}
vi.mock("mysql2", () => ({
createPool: () => ({
query: (_sql: string, cb: (cause: unknown) => void) => cb(state.connectCause),
end: (cb: () => void) => cb(),
getConnection: (_cb: () => void) => undefined
})
}))
const connectFailureReason = (cause: unknown) =>
Effect.gen(function*() {
state.connectCause = cause
const { MysqlClient } = yield* Effect.promise(() => import("@effect/sql-mysql2"))
const error = yield* Effect.flip(MysqlClient.make({}))
return error.reason
}).pipe(
Effect.scoped,
Effect.provide(Reactivity.layer)
)
const connectFailureReasonTag = (errno: number) => Effect.map(connectFailureReason({ errno }), (reason) => reason._tag)
const assertUniqueViolation = (reason: SqlError.SqlErrorReason, constraint: string) => {
assert.strictEqual(reason._tag, "UniqueViolation")
if (reason._tag === "UniqueViolation") {
assert.strictEqual(reason.constraint, constraint)
}
}
describe("MysqlClient SqlError classification", () => {
it.effect("maps representative errno codes to reasons", () =>
Effect.gen(function*() {
const cases = [
[1040, "ConnectionError"],
[1045, "AuthenticationError"],
[1142, "AuthorizationError"],
[1064, "SqlSyntaxError"],
[1062, "UniqueViolation"],
[1048, "ConstraintError"],
[1213, "DeadlockError"],
[1205, "LockTimeoutError"],
[3024, "StatementTimeoutError"]
] as const
for (const [errno, expectedTag] of cases) {
const tag = yield* connectFailureReasonTag(errno)
assert.strictEqual(tag, expectedTag)
}
}))
it.effect("falls back to UnknownError for unmapped errno", () =>
Effect.gen(function*() {
const tag = yield* connectFailureReasonTag(9999)
assert.strictEqual(tag, "UnknownError")
}))
it.effect("classifies duplicate-entry errno 1062 as UniqueViolation", () =>
Effect.gen(function*() {
const reason = yield* connectFailureReason({ errno: 1062 })
assertUniqueViolation(reason, "unknown")
}))
it.effect("extracts duplicate-entry constraints from structured fields and common messages", () =>
Effect.gen(function*() {
const explicit = yield* connectFailureReason({
errno: 1062,
constraint: " users_email_key ",
sqlMessage: "Duplicate entry 'user@example.com' for key 'ignored_key'"
})
assertUniqueViolation(explicit, "users_email_key")
const singleQuoted = yield* connectFailureReason({
errno: 1062,
sqlMessage: "Duplicate entry 'user@example.com' for key 'users_email_key'"
})
assertUniqueViolation(singleQuoted, "users_email_key")
const backtickQuoted = yield* connectFailureReason({
errno: 1062,
message: "Duplicate entry '1' for key `PRIMARY`"
})
assertUniqueViolation(backtickQuoted, "PRIMARY")
const unquoted = yield* connectFailureReason({
errno: 1062,
sqlMessage: "Duplicate entry 'user@example.com' for key users_email_key"
})
assertUniqueViolation(unquoted, "users_email_key")
}))
it.effect("uses unknown for blank, missing, malformed, or non-string duplicate-entry metadata", () =>
Effect.gen(function*() {
const missing = yield* connectFailureReason({ errno: 1062 })
assertUniqueViolation(missing, "unknown")
const blank = yield* connectFailureReason({
errno: 1062,
constraint: " ",
sqlMessage: "Duplicate entry 'user@example.com' for key ' '"
})
assertUniqueViolation(blank, "unknown")
const malformed = yield* connectFailureReason({
errno: 1062,
sqlMessage: "Duplicate entry 'user@example.com'"
})
assertUniqueViolation(malformed, "unknown")
const nonString = yield* connectFailureReason({
errno: 1062,
sqlMessage: 1062,
message: { text: "Duplicate entry 'user@example.com' for key 'users_email_key'" }
})
assertUniqueViolation(nonString, "unknown")
}))
it.effect("keeps non-unique constraint errno values classified as ConstraintError", () =>
Effect.gen(function*() {
const tag = yield* connectFailureReasonTag(1452)
assert.strictEqual(tag, "ConstraintError")
}))
})

View File

@@ -0,0 +1,7 @@
import * as SqlEventLogServerUnencryptedStorageTest from "effect-test/unstable/eventlog/SqlEventLogServerUnencryptedStorageTest"
import { MysqlContainer } from "./utils.ts"
SqlEventLogServerUnencryptedStorageTest.suite(
"sql-mysql2",
MysqlContainer.layerClient
)

View File

@@ -0,0 +1,64 @@
import { MysqlClient } from "@effect/sql-mysql2"
import type { StartedMySqlContainer } from "@testcontainers/mysql"
import { MySqlContainer } from "@testcontainers/mysql"
import { Context, Data, Effect, Layer, Redacted, String } from "effect"
export class ContainerError extends Data.TaggedError("ContainerError")<{
cause: unknown
}> {}
export class MysqlContainer extends Context.Service<
MysqlContainer,
StartedMySqlContainer
>()("test/MysqlContainer") {
static readonly layer = Layer.effect(this)(
Effect.acquireRelease(
Effect.tryPromise({
try: () => new MySqlContainer("mysql:lts").start(),
catch: (cause) => new ContainerError({ cause })
}),
(container) => Effect.promise(() => container.stop())
)
)
static client = Layer.unwrap(
Effect.gen(function*() {
const container = yield* MysqlContainer
return MysqlClient.layer({
url: Redacted.make(container.getConnectionUri())
})
})
)
static layerClient = this.client.pipe(Layer.provide(this.layer))
static layerClientWithTransforms = Layer.unwrap(
Effect.gen(function*() {
const container = yield* MysqlContainer
return MysqlClient.layer({
url: Redacted.make(container.getConnectionUri()),
transformQueryNames: String.camelToSnake,
transformResultNames: String.snakeToCamel
})
})
).pipe(Layer.provide(this.layer))
static layerVitest = Layer.effect(
this,
Effect.acquireRelease(
Effect.tryPromise({
try: () =>
new MySqlContainer("vitess/vttestserver:mysql80").withEnvironment({
KEYSPACES: "test,unsharded",
NUM_SHARDS: "1,1",
MYSQL_BIND_HOST: "0.0.0.0",
PORT: "3303"
}).start(),
catch: (cause) => new ContainerError({ cause })
}),
(container) => Effect.promise(() => container.stop())
)
)
static layerClientVitess = this.client.pipe(Layer.provide(this.layerVitest))
}