Skip to content

$unuseAll() and $unuse() break transaction isolation #2494

@pkudinov

Description

@pkudinov

Description

Calling $unuseAll() or $unuse() on a transaction client returns a new client that runs queries outside the active transaction.

Reproduction

const client = new ZenStackClient(schema, {
  dialect: new PostgresDialect({ pool }),
  plugins: [new PolicyPlugin()],
});

await client.$transaction(async (tx) => {
  // Get txid inside the transaction
  const [{ txid: txTxid }] = await tx.$unuseAll().$queryRawUnsafe<{ txid: string }>(
    'SELECT txid_current() AS txid'
  );

  // Get txid via $unuseAll()
  const [{ txid: unuseTxid }] = await tx.$unuseAll().$queryRawUnsafe<{ txid: string }>(
    'SELECT txid_current() AS txid'
  );

  console.log('tx txid:', txTxid);
  console.log('$unuseAll() txid:', unuseTxid);
  // These are DIFFERENT — $unuseAll() escaped the transaction
});

Output:

tx txid: 17474959
$unuseAll() txid: 17474960

Root cause

In client-impl.ts, $unuseAll() creates a new ClientImpl via new ClientImpl(this.schema, newOptions, this). The constructor always runs this.kysely = new Kysely(this.kyselyProps) (line 131), which creates a fresh Kysely instance from kyselyProps.

However, when inside a transaction, the transaction state lives in txClient.kysely (set to the Kysely Transaction object on line 257 of interactiveTransaction). The kyselyProps don't carry the transaction context, so the new client gets a non-transactional connection.

The same issue applies to $unuse() and $use().

Suggested fix

After constructing the new client, propagate the transaction Kysely instance:

$unuseAll() {
    const newOptions = { ...this.options, plugins: [] };
    const newClient = new ClientImpl(this.schema, newOptions, this);
    newClient.inputValidator = new InputValidator(newClient, {
        enabled: newOptions.validateInput !== false,
    });
    // Preserve transaction state
    if (this.kysely.isTransaction) {
        newClient.kysely = this.kysely;
    }
    return newClient;
}

Same pattern for $unuse(), $use(), and $setAuth().

Version

@zenstackhq/orm 3.4.6

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions