import { TransactionError } from "./errors.js";
/**
 * Represents a PostgreSQL transaction with support for savepoints.
 *
 * Transactions are created via `PostgresClient.begin()` and support
 * tagged template literal syntax for queries.
 */
export class Transaction {
    _sql;
    // biome-ignore lint/correctness/noUnusedPrivateClassMembers: held to prevent GC of the reserved connection
    _connection;
    _committed = false;
    _rolledBack = false;
    _savepointCounter = 0;
    constructor(sql, connection) {
        this._sql = sql;
        this._connection = connection;
    }
    /**
     * Whether the transaction has been committed.
     */
    get committed() {
        return this._committed;
    }
    /**
     * Whether the transaction has been rolled back.
     */
    get rolledBack() {
        return this._rolledBack;
    }
    /**
     * Whether the transaction is still active (not committed or rolled back).
     */
    get active() {
        return !this._committed && !this._rolledBack;
    }
    /**
     * Execute a query within this transaction using tagged template literal syntax.
     *
     * @example
     * ```typescript
     * const tx = await client.begin();
     * const result = await tx`SELECT * FROM users WHERE id = ${userId}`;
     * await tx.commit();
     * ```
     */
    query(strings, ...values) {
        this._ensureActive('query');
        return this._sql(strings, ...values);
    }
    /**
     * Create a savepoint within this transaction.
     *
     * @param name - Optional name for the savepoint. If not provided, a unique name is generated.
     *               If provided, must be a valid SQL identifier (alphanumeric and underscores only,
     *               starting with a letter or underscore).
     * @returns A Savepoint object that can be used to rollback to this point.
     * @throws {TransactionError} If the provided name is not a valid SQL identifier.
     *
     * @example
     * ```typescript
     * const tx = await client.begin();
     * await tx`INSERT INTO users (name) VALUES ('Alice')`;
     *
     * const sp = await tx.savepoint();
     * await tx`INSERT INTO users (name) VALUES ('Bob')`;
     *
     * // Oops, rollback Bob but keep Alice
     * await sp.rollback();
     *
     * await tx.commit(); // Only Alice is committed
     * ```
     */
    async savepoint(name) {
        this._ensureActive('savepoint');
        const savepointName = name ?? `sp_${++this._savepointCounter}`;
        // Validate savepoint name to prevent SQL injection
        // Must be a valid SQL identifier: starts with letter or underscore, contains only alphanumeric and underscores
        const validIdentifierPattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
        if (!validIdentifierPattern.test(savepointName)) {
            throw new TransactionError({
                message: `Invalid savepoint name "${savepointName}": must be a valid SQL identifier (alphanumeric and underscores only, starting with a letter or underscore)`,
                phase: 'savepoint',
            });
        }
        try {
            await this._sql `SAVEPOINT ${this._sql.unsafe(savepointName)}`;
            return new Savepoint(this._sql, savepointName);
        }
        catch (error) {
            throw new TransactionError({
                message: `Failed to create savepoint: ${error instanceof Error ? error.message : String(error)}`,
                phase: 'savepoint',
                cause: error,
            });
        }
    }
    /**
     * Commit the transaction.
     *
     * @throws {TransactionError} If the transaction is not active or commit fails.
     */
    async commit() {
        this._ensureActive('commit');
        try {
            await this._sql `COMMIT`;
            this._committed = true;
        }
        catch (error) {
            throw new TransactionError({
                message: `Failed to commit transaction: ${error instanceof Error ? error.message : String(error)}`,
                phase: 'commit',
                cause: error,
            });
        }
        finally {
            this._releaseConnection();
        }
    }
    /**
     * Rollback the transaction.
     *
     * @throws {TransactionError} If the transaction is not active or rollback fails.
     */
    async rollback() {
        this._ensureActive('rollback');
        try {
            await this._sql `ROLLBACK`;
            this._rolledBack = true;
        }
        catch (error) {
            throw new TransactionError({
                message: `Failed to rollback transaction: ${error instanceof Error ? error.message : String(error)}`,
                phase: 'rollback',
                cause: error,
            });
        }
        finally {
            this._releaseConnection();
        }
    }
    /**
     * Releases the underlying reserved connection back to the pool.
     * Called automatically on commit or rollback. Safe to call multiple times.
     */
    _releaseConnection() {
        const sql = this._sql;
        if (typeof sql.release === 'function') {
            sql.release();
        }
    }
    /**
     * Ensures the transaction is still active.
     */
    _ensureActive(operation) {
        if (this._committed) {
            throw new TransactionError({
                message: `Cannot ${operation}: transaction has been committed`,
                phase: operation,
            });
        }
        if (this._rolledBack) {
            throw new TransactionError({
                message: `Cannot ${operation}: transaction has been rolled back`,
                phase: operation,
            });
        }
    }
}
/**
 * Represents a savepoint within a transaction.
 */
export class Savepoint {
    _sql;
    _name;
    _released = false;
    _rolledBack = false;
    constructor(sql, name) {
        this._sql = sql;
        this._name = name;
    }
    /**
     * The name of this savepoint.
     */
    get name() {
        return this._name;
    }
    /**
     * Whether the savepoint has been released.
     */
    get released() {
        return this._released;
    }
    /**
     * Whether the savepoint has been rolled back to.
     */
    get rolledBack() {
        return this._rolledBack;
    }
    /**
     * Rollback to this savepoint.
     * All changes made after this savepoint was created will be undone.
     *
     * @throws {TransactionError} If the savepoint has been released or already rolled back.
     */
    async rollback() {
        if (this._released) {
            throw new TransactionError({
                message: `Cannot rollback: savepoint "${this._name}" has been released`,
                phase: 'savepoint',
            });
        }
        if (this._rolledBack) {
            throw new TransactionError({
                message: `Cannot rollback: savepoint "${this._name}" has already been rolled back`,
                phase: 'savepoint',
            });
        }
        try {
            await this._sql `ROLLBACK TO SAVEPOINT ${this._sql.unsafe(this._name)}`;
            this._rolledBack = true;
        }
        catch (error) {
            throw new TransactionError({
                message: `Failed to rollback to savepoint: ${error instanceof Error ? error.message : String(error)}`,
                phase: 'savepoint',
                cause: error,
            });
        }
    }
    /**
     * Release this savepoint.
     * The savepoint is destroyed but changes are kept.
     */
    async release() {
        if (this._released) {
            return; // Already released, no-op
        }
        try {
            await this._sql `RELEASE SAVEPOINT ${this._sql.unsafe(this._name)}`;
            this._released = true;
        }
        catch (error) {
            throw new TransactionError({
                message: `Failed to release savepoint: ${error instanceof Error ? error.message : String(error)}`,
                phase: 'savepoint',
                cause: error,
            });
        }
    }
}
/**
 * Represents a reserved (exclusive) connection from the pool.
 *
 * Reserved connections are created via `PostgresClient.reserve()` and support
 * tagged template literal syntax for queries.
 */
export class ReservedConnection {
    _sql;
    _released = false;
    constructor(sql) {
        this._sql = sql;
    }
    /**
     * Whether the connection has been released back to the pool.
     */
    get released() {
        return this._released;
    }
    /**
     * Execute a query on this reserved connection using tagged template literal syntax.
     *
     * @example
     * ```typescript
     * const conn = await client.reserve();
     * try {
     *   await conn`SET LOCAL timezone = 'UTC'`;
     *   const result = await conn`SELECT NOW()`;
     * } finally {
     *   conn.release();
     * }
     * ```
     */
    query(strings, ...values) {
        if (this._released) {
            throw new TransactionError({
                message: 'Cannot query: connection has been released',
            });
        }
        return this._sql(strings, ...values);
    }
    /**
     * Release the connection back to the pool.
     */
    release() {
        if (this._released) {
            return; // Already released, no-op
        }
        this._released = true;
        // The underlying SQL connection will be returned to the pool
        // when it goes out of scope or is explicitly closed
    }
}
//# sourceMappingURL=transaction.js.map