import { assert, describe, it } from "@effect/vitest" import { Effect, Schema } from "effect" import * as SqlError from "effect/unstable/sql/SqlError" type ReasonCase = { readonly tag: SqlError.SqlErrorReason["_tag"] readonly isRetryable: boolean readonly make: (args: { readonly cause: unknown readonly message?: string | undefined readonly operation?: string | undefined }) => SqlError.SqlErrorReason } const uniqueViolationConstraint = "users_email_key" const assertUniqueViolation = (reason: SqlError.SqlErrorReason, constraint: string) => { assert.strictEqual(reason._tag, "UniqueViolation") if (reason._tag === "UniqueViolation") { assert.strictEqual(reason.constraint, constraint) } } const reasonCases = [ { tag: "ConnectionError", isRetryable: true, make: (args) => new SqlError.ConnectionError(args) }, { tag: "AuthenticationError", isRetryable: false, make: (args) => new SqlError.AuthenticationError(args) }, { tag: "AuthorizationError", isRetryable: false, make: (args) => new SqlError.AuthorizationError(args) }, { tag: "SqlSyntaxError", isRetryable: false, make: (args) => new SqlError.SqlSyntaxError(args) }, { tag: "UniqueViolation", isRetryable: false, make: (args) => new SqlError.UniqueViolation({ ...args, constraint: uniqueViolationConstraint }) }, { tag: "ConstraintError", isRetryable: false, make: (args) => new SqlError.ConstraintError(args) }, { tag: "DeadlockError", isRetryable: true, make: (args) => new SqlError.DeadlockError(args) }, { tag: "SerializationError", isRetryable: true, make: (args) => new SqlError.SerializationError(args) }, { tag: "LockTimeoutError", isRetryable: true, make: (args) => new SqlError.LockTimeoutError(args) }, { tag: "StatementTimeoutError", isRetryable: true, make: (args) => new SqlError.StatementTimeoutError(args) }, { tag: "UnknownError", isRetryable: false, make: (args) => new SqlError.UnknownError(args) } ] as const satisfies ReadonlyArray describe("SqlError", () => { it("reason classes expose expected tags and retryability", () => { for (const reasonCase of reasonCases) { const reason = reasonCase.make({ cause: { tag: reasonCase.tag }, message: `${reasonCase.tag} message`, operation: "execute" }) assert.strictEqual(reason._tag, reasonCase.tag) assert.strictEqual(reason.isRetryable, reasonCase.isRetryable) assert.strictEqual(reason.message, `${reasonCase.tag} message`) assert.strictEqual(reason.operation, "execute") assert.deepStrictEqual(reason.cause, { tag: reasonCase.tag }) } }) it("delegates message, cause and retryability for every reason type", () => { for (const reasonCase of reasonCases) { const withoutMessage = reasonCase.make({ cause: { tag: `${reasonCase.tag}-fallback` } }) const withMessage = reasonCase.make({ cause: { tag: `${reasonCase.tag}-custom` }, message: `${reasonCase.tag} custom`, operation: "execute" }) const withEmptyMessage = reasonCase.make({ cause: { tag: `${reasonCase.tag}-empty` }, message: "" }) const fallbackError = new SqlError.SqlError({ reason: withoutMessage }) const explicitMessageError = new SqlError.SqlError({ reason: withMessage }) const emptyMessageError = new SqlError.SqlError({ reason: withEmptyMessage }) assert.strictEqual(fallbackError.message, reasonCase.tag) assert.strictEqual(fallbackError.cause, withoutMessage) assert.strictEqual(fallbackError.isRetryable, reasonCase.isRetryable) assert.strictEqual(explicitMessageError.message, `${reasonCase.tag} custom`) assert.strictEqual(explicitMessageError.cause, withMessage) assert.strictEqual(explicitMessageError.isRetryable, reasonCase.isRetryable) assert.strictEqual(emptyMessageError.message, reasonCase.tag) assert.strictEqual(emptyMessageError.cause, withEmptyMessage) assert.strictEqual(emptyMessageError.isRetryable, reasonCase.isRetryable) } }) it("isSqlError only matches the SqlError wrapper", () => { const reason = new SqlError.UnknownError({ cause: new Error("boom") }) const error = new SqlError.SqlError({ reason }) const mismatch = new SqlError.ResultLengthMismatch({ expected: 1, actual: 0 }) assert.strictEqual(SqlError.isSqlError(error), true) assert.strictEqual(SqlError.isSqlError(reason), false) assert.strictEqual(SqlError.isSqlError(mismatch), false) }) it("isSqlErrorReason only matches reason values", () => { const reason = new SqlError.UnknownError({ cause: new Error("boom") }) const error = new SqlError.SqlError({ reason }) assert.strictEqual(SqlError.isSqlErrorReason(reason), true) assert.strictEqual(SqlError.isSqlErrorReason(error), false) }) it("constructs and recognizes UniqueViolation reasons", () => { const reason = new SqlError.UniqueViolation({ cause: { code: "23505" }, message: "duplicate key value violates unique constraint", operation: "insert", constraint: uniqueViolationConstraint }) assert.strictEqual(SqlError.isSqlErrorReason(reason), true) assert.strictEqual(reason._tag, "UniqueViolation") assert.strictEqual(reason.isRetryable, false) assert.strictEqual(reason.constraint, uniqueViolationConstraint) }) it("classifySqliteError maps sqlite unique constraint codes to UniqueViolation", () => { const byString = SqlError.classifySqliteError({ code: "SQLITE_CONSTRAINT_UNIQUE", constraint: " " + uniqueViolationConstraint + " " }) const byNumericCode = SqlError.classifySqliteError({ code: 2067, constraint: uniqueViolationConstraint }) const byErrno = SqlError.classifySqliteError({ errno: 2067, constraint: uniqueViolationConstraint }) const byExtendedErrno = SqlError.classifySqliteError({ code: 19, errno: 2067, constraint: uniqueViolationConstraint }) const unknown = SqlError.classifySqliteError({ code: "NOT_SQLITE" }) assertUniqueViolation(byString, uniqueViolationConstraint) assertUniqueViolation(byNumericCode, uniqueViolationConstraint) assertUniqueViolation(byErrno, uniqueViolationConstraint) assertUniqueViolation(byExtendedErrno, uniqueViolationConstraint) assert.strictEqual(unknown._tag, "UnknownError") }) it("classifySqliteError extracts sqlite unique descriptors from messages", () => { const reason = SqlError.classifySqliteError({ code: "SQLITE_CONSTRAINT_UNIQUE", message: "UNIQUE constraint failed: users.email" }) assertUniqueViolation(reason, "users.email") }) it("classifySqliteError prefers explicit sqlite unique constraint identifiers", () => { const reason = SqlError.classifySqliteError({ code: "SQLITE_CONSTRAINT_UNIQUE", constraint: " users_email_key ", message: "UNIQUE constraint failed: users.email" }) assertUniqueViolation(reason, "users_email_key") }) it("classifySqliteError falls back to unknown for blank or missing sqlite unique identifiers", () => { const blank = SqlError.classifySqliteError({ code: "SQLITE_CONSTRAINT_UNIQUE", constraint: " ", message: "UNIQUE constraint failed: users.email" }) const missing = SqlError.classifySqliteError({ code: "SQLITE_CONSTRAINT_UNIQUE" }) const nonString = SqlError.classifySqliteError({ code: "SQLITE_CONSTRAINT_UNIQUE", constraint: 123 }) const malformedMessage = SqlError.classifySqliteError({ code: "SQLITE_CONSTRAINT_UNIQUE", message: { text: "UNIQUE constraint failed: users.email" } }) assertUniqueViolation(blank, "unknown") assertUniqueViolation(missing, "unknown") assertUniqueViolation(nonString, "unknown") assertUniqueViolation(malformedMessage, "unknown") }) it("classifySqliteError keeps generic sqlite constraints as ConstraintError", () => { const byString = SqlError.classifySqliteError({ code: "SQLITE_CONSTRAINT" }) const byNumericCode = SqlError.classifySqliteError({ code: 19 }) const byErrno = SqlError.classifySqliteError({ errno: 19 }) const primaryKeyString = SqlError.classifySqliteError({ code: "SQLITE_CONSTRAINT_PRIMARYKEY" }) const primaryKeyNumber = SqlError.classifySqliteError({ code: 1555 }) assert.strictEqual(byString._tag, "ConstraintError") assert.strictEqual(byNumericCode._tag, "ConstraintError") assert.strictEqual(byErrno._tag, "ConstraintError") assert.strictEqual(primaryKeyString._tag, "ConstraintError") assert.strictEqual(primaryKeyNumber._tag, "ConstraintError") }) for (const reasonCase of reasonCases) { it.effect(`schema roundtrip for SqlError wrapping ${reasonCase.tag}`, () => Effect.gen(function*() { const cause = { tag: reasonCase.tag } const error = new SqlError.SqlError({ reason: reasonCase.make({ cause, message: `${reasonCase.tag} message`, operation: "execute" }) }) const encoded = yield* Schema.encodeEffect(SqlError.SqlError)(error) const decoded = yield* Schema.decodeEffect(SqlError.SqlError)(encoded) assert.strictEqual(decoded._tag, "SqlError") assert.strictEqual(decoded.reason._tag, reasonCase.tag) assert.strictEqual(decoded.reason.message, `${reasonCase.tag} message`) assert.strictEqual(decoded.reason.operation, "execute") assert.deepStrictEqual(decoded.reason.cause, cause) assert.strictEqual(decoded.message, `${reasonCase.tag} message`) assert.strictEqual(decoded.isRetryable, reasonCase.isRetryable) assert.strictEqual(decoded.cause, decoded.reason) if (decoded.reason._tag === "UniqueViolation") { assert.strictEqual(decoded.reason.constraint, uniqueViolationConstraint) } })) } })