-
-
Notifications
You must be signed in to change notification settings - Fork 134
Description
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