Merge commit '3c60637c1a27da8ba66888de518d58d5707801f2' as 'repos/effect-smol'
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
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<ReasonCase>
|
||||
|
||||
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)
|
||||
}
|
||||
}))
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user