/** * Advisory Locks Module for postgres.do * * This module handles: * - Lock key validation (single and two-key) * - Advisory lock SQL generation * - Transaction-scoped locks (xact variants) * - Shared vs exclusive locks * * PostgreSQL advisory locks are application-level cooperative locks. * They do not affect table access and are entirely user-defined. * * @see https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS */ import { PostgresError } from './types' // ============================================================================ // Constants // ============================================================================ /** PostgreSQL bigint max value: 2^63 - 1 */ export const BIGINT_MAX = BigInt('9223372036854775807') /** PostgreSQL int4 max value: 2^31 - 1 */ export const INT4_MAX = 2147483647 // ============================================================================ // Lock Key Validation // ============================================================================ /** Default PostgreSQL error code for invalid parameter values */ const INVALID_PARAMETER_CODE = '22023' /** * Unified integer range validation utility for advisory lock keys. * * This utility consolidates common validation logic used across all advisory * lock key types, ensuring consistent error handling and messages. * * Validation rules: * 1. Value must be an integer (for number types) * 2. Value must be non-negative (>= 0) * 3. Value must not exceed the specified maximum * * @param value - The value to validate (number or bigint) * @param max - Maximum allowed value (inclusive), as a bigint * @param typeName - Human-readable type name for error messages (e.g., 'key', 'classId', 'objId') * @param errorCode - PostgreSQL error code for thrown errors (defaults to '22023') * * @throws {PostgresError} If the value is not an integer (code: errorCode) * @throws {PostgresError} If the value is negative (code: errorCode) * @throws {PostgresError} If the value exceeds the maximum (code: errorCode) * * @example * ```typescript * // Validate a bigint key (0 to 2^63 - 1) * validateIntRange(lockKey, BIGINT_MAX, 'key') * * // Validate an int4 classId (0 to 2^31 - 1) * validateIntRange(classId, BigInt(INT4_MAX), 'classId') * ``` * * @internal This is an internal utility. Use validateLockKey or validateTwoKeyLock instead. */ function validateIntRange( value: number | bigint, max: bigint, typeName: string, errorCode: string = INVALID_PARAMETER_CODE ): void { // Check for non-integer (only for numbers, as bigints are always integers) if (typeof value === 'number' && !Number.isInteger(value)) { throw new PostgresError(`Advisory lock ${typeName} must be an integer`, { code: errorCode, severity: 'ERROR', }) } const valueBigInt = BigInt(value) // Check for negative values if (valueBigInt < BigInt(0)) { throw new PostgresError(`Advisory lock ${typeName} must be non-negative`, { code: errorCode, severity: 'ERROR', }) } // Check for maximum boundary overflow if (valueBigInt > max) { throw new PostgresError(`Advisory lock ${typeName} exceeds PostgreSQL maximum`, { code: errorCode, severity: 'ERROR', }) } } /** * Validate a single advisory lock key. * * Single-key advisory locks use PostgreSQL's bigint type, which allows * values from 0 to 2^63 - 1 (9,223,372,036,854,775,807). * * Validation rules: * - Must be an integer (no fractional values) * - Must be non-negative (>= 0) * - Must not exceed PostgreSQL bigint maximum (2^63 - 1) * * @param key - The lock key to validate (number or bigint) * * @throws {PostgresError} If the key is not an integer (code: '22023') * @throws {PostgresError} If the key is negative (code: '22023') * @throws {PostgresError} If the key exceeds bigint maximum (code: '22023') * * @example * ```typescript * // Valid keys * validateLockKey(0) * validateLockKey(12345) * validateLockKey(BigInt('9223372036854775807')) * * // Invalid keys (will throw PostgresError) * validateLockKey(-1) // negative * validateLockKey(12.5) // non-integer * validateLockKey(BIGINT_MAX + 1n) // exceeds maximum * ``` * * @see https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS */ export function validateLockKey(key: number | bigint): void { validateIntRange(key, BIGINT_MAX, 'key') } /** * Validate a two-key advisory lock pair. * * Two-key advisory locks use PostgreSQL's int4 type for both classId and objId, * which allows values from 0 to 2^31 - 1 (2,147,483,647). * * The two-key form is useful for namespacing locks: * - classId: Identifies the lock class/namespace (e.g., table OID) * - objId: Identifies the specific object within that namespace * * Validation rules (applied to both keys): * - Must be an integer (no fractional values) * - Must be non-negative (>= 0) * - Must not exceed PostgreSQL int4 maximum (2^31 - 1) * * @param classId - The lock class/namespace identifier * @param objId - The object identifier within the namespace * * @throws {PostgresError} If classId is invalid (code: '22023') * @throws {PostgresError} If objId is invalid (code: '22023') * * @example * ```typescript * // Valid key pairs * validateTwoKeyLock(0, 0) * validateTwoKeyLock(1, 42) * validateTwoKeyLock(2147483647, 2147483647) * * // Invalid key pairs (will throw PostgresError) * validateTwoKeyLock(-1, 42) // negative classId * validateTwoKeyLock(1, -1) // negative objId * validateTwoKeyLock(2147483648, 0) // classId exceeds int4 max * ``` * * @see https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS */ export function validateTwoKeyLock(classId: number, objId: number): void { validateIntRange(classId, BigInt(INT4_MAX), 'classId') validateIntRange(objId, BigInt(INT4_MAX), 'objId') } // ============================================================================ // SQL Building // ============================================================================ /** * Build the SQL for an advisory lock operation. * * @param lockFn - The PostgreSQL lock function name * @param args - The lock key arguments (single bigint or two int4s) * @returns The SQL string for the lock operation * * @example * ```typescript * buildAdvisoryLockSql('pg_advisory_xact_lock', 12345) * // Returns: "SELECT pg_advisory_xact_lock(12345)" * * buildAdvisoryLockSql('pg_advisory_xact_lock', 1, 42) * // Returns: "SELECT pg_advisory_xact_lock(1, 42)" * ``` */ export function buildAdvisoryLockSql(lockFn: string, ...args: Array): string { if (args.length === 1) { return `SELECT ${lockFn}(${BigInt(args[0]!)})` } return `SELECT ${lockFn}(${args[0]}, ${args[1]})` } // ============================================================================ // Advisory Lock Functions // ============================================================================ /** * Advisory lock function types for PostgreSQL. * These are the PostgreSQL function names used for different lock operations. */ export const AdvisoryLockFunctions = { /** Exclusive lock, transaction-scoped (released on commit/rollback) */ XACT_LOCK: 'pg_advisory_xact_lock', /** Shared lock, transaction-scoped */ XACT_LOCK_SHARED: 'pg_advisory_xact_lock_shared', /** Try exclusive lock, transaction-scoped (non-blocking) */ TRY_XACT_LOCK: 'pg_try_advisory_xact_lock', /** Try shared lock, transaction-scoped (non-blocking) */ TRY_XACT_LOCK_SHARED: 'pg_try_advisory_xact_lock_shared', /** Exclusive lock, session-scoped (must be explicitly unlocked) */ SESSION_LOCK: 'pg_advisory_lock', /** Shared lock, session-scoped */ SESSION_LOCK_SHARED: 'pg_advisory_lock_shared', /** Try exclusive lock, session-scoped (non-blocking) */ TRY_SESSION_LOCK: 'pg_try_advisory_lock', /** Try shared lock, session-scoped (non-blocking) */ TRY_SESSION_LOCK_SHARED: 'pg_try_advisory_lock_shared', /** Unlock session-scoped lock */ UNLOCK: 'pg_advisory_unlock', /** Unlock shared session-scoped lock */ UNLOCK_SHARED: 'pg_advisory_unlock_shared', /** Unlock all session-scoped locks */ UNLOCK_ALL: 'pg_advisory_unlock_all', } as const export type AdvisoryLockFunction = typeof AdvisoryLockFunctions[keyof typeof AdvisoryLockFunctions]