Skip to content

Commit 18c4a34

Browse files
authored
Merge pull request #422 from drizzle-team/beta
2 parents f891008 + 59509eb commit 18c4a34

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+2523
-729
lines changed

.eslintrc.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ rules:
99
'@typescript-eslint/consistent-type-imports':
1010
- error
1111
- disallowTypeAnnotations: false
12+
fixStyle: inline-type-imports
1213
import/no-cycle: error
1314
import/no-self-import: error
1415
import/no-empty-named-blocks: error

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
19

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<h1>Drizzle ORM <a href=""><img alt="npm" src="https://img.shields.io/npm/v/drizzle-orm?label="></a></h1>
33
<img alt="npm" src="https://img.shields.io/npm/dm/drizzle-orm">
44
<img alt="npm bundle size" src="https://img.shields.io/bundlephobia/min/drizzle-orm">
5-
<a href="https://discord.gg/yfjTbVXMW4"><img alt="Discord" src="https://img.shields.io/discord/1043890932593987624"></a>
5+
<a href="https://discord.gg/yfjTbVXMW4"><img alt="Discord" src="https://img.shields.io/discord/1043890932593987624?label=Discord"></a>
66
<img alt="License" src="https://img.shields.io/npm/l/drizzle-orm">
77
<h6><i>If you know SQL, you know Drizzle ORM</i></h6>
88
<hr />
@@ -41,6 +41,7 @@ Drizzle ORM is being battle-tested on production projects by multiple teams 🚀
4141
| CockroachDB || |
4242

4343
## Our sponsors ❤️
44+
4445
<p align="center">
4546
<a href="https://drizzle.team" target="_blank">
4647
<img src='https://api.drizzle.team/github/sponsors/svg'/>

changelogs/drizzle-orm/0.23.9.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Transactions support 🎉
2+
3+
You can now use transactions with all the supported databases and drivers.
4+
5+
`node-postgres` example:
6+
7+
```ts
8+
await db.transaction(async (tx) => {
9+
await tx.insert(users).values(newUser);
10+
await tx.update(users).set({ name: 'Mr. Dan' }).where(eq(users.name, 'Dan'));
11+
await tx.delete(users).where(eq(users.name, 'Dan'));
12+
});
13+
```
14+
15+
For more information, see transactions docs:
16+
17+
- [PostgreSQL](/drizzle-orm/src/pg-core/README.md#transactions)
18+
- [MySQL](/drizzle-orm/src/mysql-core/README.md#transactions)
19+
- [SQLite](/drizzle-orm/src/sqlite-core/README.md#transactions)

drizzle-orm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "drizzle-orm",
3-
"version": "0.23.8",
3+
"version": "0.23.9",
44
"description": "Drizzle ORM package for SQL databases",
55
"scripts": {
66
"build": "tsc && resolve-tspaths && cp ../README.md package.json dist/",

drizzle-orm/src/aws-data-api/pg/driver.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Logger } from '~/logger';
22
import { DefaultLogger } from '~/logger';
33
import { PgDatabase } from '~/pg-core/db';
44
import { PgDialect } from '~/pg-core/dialect';
5-
import type { AwsDataApiClient, AwsDataApiPgQueryResultHKT} from './session';
5+
import type { AwsDataApiClient, AwsDataApiPgQueryResultHKT } from './session';
66
import { AwsDataApiSession } from './session';
77

88
export interface PgDriverOptions {
@@ -21,7 +21,7 @@ export class AwsDataApiDriver {
2121
}
2222

2323
createSession(): AwsDataApiSession {
24-
return new AwsDataApiSession(this.client, this.dialect, this.options);
24+
return new AwsDataApiSession(this.client, this.dialect, this.options, undefined);
2525
}
2626
}
2727

@@ -32,7 +32,7 @@ export interface DrizzleConfig {
3232
secretArn: string;
3333
}
3434

35-
export type AwsDataApiPgDatabase = PgDatabase<AwsDataApiPgQueryResultHKT, AwsDataApiSession>;
35+
export type AwsDataApiPgDatabase = PgDatabase<AwsDataApiPgQueryResultHKT>;
3636

3737
export class AwsPgDialect extends PgDialect {
3838
override escapeName(name: string): string {
Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,8 @@
1-
import type { MigrationConfig} from '~/migrator';
1+
import type { MigrationConfig } from '~/migrator';
22
import { readMigrationFiles } from '~/migrator';
3-
import { sql } from '~/sql';
43
import type { AwsDataApiPgDatabase } from './driver';
54

65
export async function migrate(db: AwsDataApiPgDatabase, config: string | MigrationConfig) {
76
const migrations = readMigrationFiles(config);
8-
9-
// TODO: Write own aws datapi migrator
10-
const { session } = db;
11-
12-
const migrationTableCreate = sql`CREATE TABLE IF NOT EXISTS "drizzle"."__drizzle_migrations" (
13-
id SERIAL PRIMARY KEY,
14-
hash text NOT NULL,
15-
created_at bigint
16-
)`;
17-
await session.execute(sql`CREATE SCHEMA IF NOT EXISTS "drizzle"`);
18-
await session.execute(migrationTableCreate);
19-
20-
const dbMigrations = await session.execute<{ id: number; hash: string; created_at: string }[]>(
21-
sql`SELECT id, hash, created_at FROM "drizzle"."__drizzle_migrations" ORDER BY created_at DESC LIMIT 1`,
22-
);
23-
24-
const lastDbMigration = dbMigrations[0];
25-
const transactionId = await session.beginTransaction();
26-
27-
try {
28-
for await (const migration of migrations) {
29-
if (
30-
!lastDbMigration
31-
|| parseInt(lastDbMigration.created_at, 10) < migration.folderMillis
32-
) {
33-
for (const stmnt of migration.sql) {
34-
await session.executeWithTransaction(sql.raw(stmnt), transactionId);
35-
}
36-
await session.executeWithTransaction(
37-
sql`INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at") VALUES(${migration.hash}, ${migration.folderMillis})`,
38-
transactionId,
39-
);
40-
}
41-
}
42-
43-
await session.commitTransaction(transactionId!);
44-
} catch (e) {
45-
await session.rollbackTransaction(transactionId!);
46-
throw e;
47-
}
7+
await db.dialect.migrate(migrations, db.session);
488
}

drizzle-orm/src/aws-data-api/pg/session.ts

Lines changed: 60 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
1-
import type {
2-
BeginTransactionCommandInput,
3-
CommitTransactionCommandInput,
4-
ExecuteStatementCommandOutput,
5-
RDSDataClient,
6-
RollbackTransactionCommandInput,
7-
} from '@aws-sdk/client-rds-data';
1+
import type { ExecuteStatementCommandOutput, RDSDataClient } from '@aws-sdk/client-rds-data';
82
import {
93
BeginTransactionCommand,
104
CommitTransactionCommand,
115
ExecuteStatementCommand,
126
RollbackTransactionCommand,
137
} from '@aws-sdk/client-rds-data';
148
import type { Logger } from '~/logger';
15-
import type { PgDialect, PreparedQueryConfig, QueryResultHKT } from '~/pg-core';
16-
import { PgSession, PreparedQuery } from '~/pg-core';
9+
import {
10+
type PgDialect,
11+
PgSession,
12+
PgTransaction,
13+
type PgTransactionConfig,
14+
PreparedQuery,
15+
type PreparedQueryConfig,
16+
type QueryResultHKT,
17+
} from '~/pg-core';
1718
import type { SelectedFieldsOrdered } from '~/pg-core/query-builders/select.types';
18-
import type { Query, QueryTypingsValue, SQL } from '~/sql';
19-
import { fillPlaceholders } from '~/sql';
19+
import { fillPlaceholders, type Query, type QueryTypingsValue, type SQL, sql } from '~/sql';
2020
import { mapResultRow } from '~/utils';
2121
import { getValueFromDataApi, toValueParam } from '../common';
2222

@@ -32,8 +32,8 @@ export class AwsDataApiPreparedQuery<T extends PreparedQueryConfig> extends Prep
3232
private typings: QueryTypingsValue[],
3333
private options: AwsDataApiSessionOptions,
3434
private fields: SelectedFieldsOrdered | undefined,
35-
name: string | undefined,
36-
transactionId: string | undefined,
35+
/** @internal */
36+
readonly transactionId: string | undefined,
3737
) {
3838
super();
3939
this.rawQuery = new ExecuteStatementCommand({
@@ -68,14 +68,6 @@ export class AwsDataApiPreparedQuery<T extends PreparedQueryConfig> extends Prep
6868
return mapResultRow<T['execute']>(fields, mappedResult, this.joinsNotNullableMap);
6969
});
7070
}
71-
72-
all(placeholderValues?: Record<string, unknown> | undefined): Promise<T['all']> {
73-
throw new Error('Method not implemented.');
74-
}
75-
76-
values(placeholderValues?: Record<string, unknown> | undefined): Promise<T['values']> {
77-
throw new Error('Method not implemented.');
78-
}
7971
}
8072

8173
export interface AwsDataApiSessionOptions {
@@ -85,13 +77,23 @@ export interface AwsDataApiSessionOptions {
8577
secretArn: string;
8678
}
8779

88-
export class AwsDataApiSession extends PgSession {
89-
private rawQuery: BeginTransactionCommandInput | CommitTransactionCommandInput | RollbackTransactionCommandInput;
80+
interface AwsDataApiQueryBase {
81+
resourceArn: string;
82+
secretArn: string;
83+
database: string;
84+
}
85+
86+
export class AwsDataApiSession extends PgSession<AwsDataApiPgQueryResultHKT> {
87+
/** @internal */
88+
readonly rawQuery: AwsDataApiQueryBase;
9089

9190
constructor(
92-
private client: AwsDataApiClient,
91+
/** @internal */
92+
readonly client: AwsDataApiClient,
9393
dialect: PgDialect,
9494
private options: AwsDataApiSessionOptions,
95+
/** @internal */
96+
readonly transactionId: string | undefined,
9597
) {
9698
super(dialect);
9799
this.rawQuery = {
@@ -104,25 +106,7 @@ export class AwsDataApiSession extends PgSession {
104106
prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
105107
query: Query,
106108
fields: SelectedFieldsOrdered | undefined,
107-
name: string | undefined,
108-
): PreparedQuery<T> {
109-
return new AwsDataApiPreparedQuery(
110-
this.client,
111-
query.sql,
112-
query.params,
113-
query.typings ?? [],
114-
this.options,
115-
fields,
116-
name,
117-
undefined,
118-
);
119-
}
120-
121-
prepareQueryWithTransaction<T extends PreparedQueryConfig = PreparedQueryConfig>(
122-
query: Query,
123-
fields: SelectedFieldsOrdered | undefined,
124-
name: string | undefined,
125-
transactionId: string | undefined,
109+
transactionId?: string,
126110
): PreparedQuery<T> {
127111
return new AwsDataApiPreparedQuery(
128112
this.client,
@@ -131,39 +115,52 @@ export class AwsDataApiSession extends PgSession {
131115
query.typings ?? [],
132116
this.options,
133117
fields,
134-
name,
135118
transactionId,
136119
);
137120
}
138121

139-
executeWithTransaction<T>(query: SQL, transactionId: string | undefined): Promise<T> {
140-
return this.prepareQueryWithTransaction<PreparedQueryConfig & { execute: T }>(
141-
this.dialect.sqlToQuery(query),
142-
undefined,
143-
undefined,
144-
transactionId,
145-
).execute();
146-
}
147-
148122
override execute<T>(query: SQL): Promise<T> {
149123
return this.prepareQuery<PreparedQueryConfig & { execute: T }>(
150124
this.dialect.sqlToQuery(query),
151125
undefined,
152-
undefined,
126+
this.transactionId,
153127
).execute();
154128
}
155129

156-
async beginTransaction(): Promise<string | undefined> {
157-
const transactionRes = await this.client.send(new BeginTransactionCommand(this.rawQuery));
158-
return transactionRes.transactionId;
159-
}
160-
161-
async commitTransaction(transactionId: string): Promise<void> {
162-
await this.client.send(new CommitTransactionCommand({ ...this.rawQuery, transactionId }));
130+
override async transaction<T>(
131+
transaction: (tx: AwsDataApiTransaction) => Promise<T>,
132+
config?: PgTransactionConfig | undefined,
133+
): Promise<T> {
134+
const { transactionId } = await this.client.send(new BeginTransactionCommand(this.rawQuery));
135+
const session = new AwsDataApiSession(this.client, this.dialect, this.options, transactionId);
136+
const tx = new AwsDataApiTransaction(this.dialect, session);
137+
if (config) {
138+
await tx.setTransaction(config);
139+
}
140+
try {
141+
const result = await transaction(tx);
142+
await this.client.send(new CommitTransactionCommand({ ...this.rawQuery, transactionId }));
143+
return result;
144+
} catch (e) {
145+
await this.client.send(new RollbackTransactionCommand({ ...this.rawQuery, transactionId }));
146+
throw e;
147+
}
163148
}
149+
}
164150

165-
async rollbackTransaction(transactionId: string): Promise<void> {
166-
await this.client.send(new RollbackTransactionCommand({ ...this.rawQuery, transactionId }));
151+
export class AwsDataApiTransaction extends PgTransaction<AwsDataApiPgQueryResultHKT> {
152+
override transaction<T>(transaction: (tx: AwsDataApiTransaction) => Promise<T>): Promise<T> {
153+
const savepointName = `sp${this.nestedIndex + 1}`;
154+
const tx = new AwsDataApiTransaction(this.dialect, this.session, this.nestedIndex + 1);
155+
this.session.execute(sql`savepoint ${savepointName}`);
156+
try {
157+
const result = transaction(tx);
158+
this.session.execute(sql`release savepoint ${savepointName}`);
159+
return result;
160+
} catch (e) {
161+
this.session.execute(sql`rollback to savepoint ${savepointName}`);
162+
throw e;
163+
}
167164
}
168165
}
169166

drizzle-orm/src/better-sqlite3/session.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { Database, RunResult, Statement } from 'better-sqlite3';
22
import type { Logger } from '~/logger';
33
import { NoopLogger } from '~/logger';
4-
import type { Query } from '~/sql';
5-
import { fillPlaceholders } from '~/sql';
4+
import { fillPlaceholders, type Query, sql } from '~/sql';
5+
import { SQLiteTransaction } from '~/sqlite-core';
66
import type { SQLiteSyncDialect } from '~/sqlite-core/dialect';
77
import type { SelectedFieldsOrdered } from '~/sqlite-core/query-builders/select.types';
8-
import type { PreparedQueryConfig as PreparedQueryConfigBase, Transaction } from '~/sqlite-core/session';
8+
import type { PreparedQueryConfig as PreparedQueryConfigBase, SQLiteTransactionConfig } from '~/sqlite-core/session';
99
import { PreparedQuery as PreparedQueryBase, SQLiteSession } from '~/sqlite-core/session';
1010
import { mapResultRow } from '~/utils';
1111

@@ -27,10 +27,6 @@ export class BetterSQLiteSession extends SQLiteSession<'sync', RunResult> {
2727
this.logger = options.logger ?? new NoopLogger();
2828
}
2929

30-
exec(query: string): void {
31-
this.client.exec(query);
32-
}
33-
3430
prepareQuery<T extends Omit<PreparedQueryConfig, 'run'>>(
3531
query: Query,
3632
fields: SelectedFieldsOrdered | undefined,
@@ -39,8 +35,26 @@ export class BetterSQLiteSession extends SQLiteSession<'sync', RunResult> {
3935
return new PreparedQuery(stmt, query.sql, query.params, this.logger, fields);
4036
}
4137

42-
override transaction(): Transaction<'sync', RunResult> {
43-
throw new Error('Method not implemented.');
38+
override transaction<T>(transaction: (tx: BetterSQLiteTransaction) => T, config: SQLiteTransactionConfig = {}): T {
39+
const tx = new BetterSQLiteTransaction(this.dialect, this);
40+
const nativeTx = this.client.transaction(transaction);
41+
return nativeTx[config.behavior ?? 'deferred'](tx);
42+
}
43+
}
44+
45+
export class BetterSQLiteTransaction extends SQLiteTransaction<'sync', RunResult> {
46+
override transaction<T>(transaction: (tx: BetterSQLiteTransaction) => T): T {
47+
const savepointName = `sp${this.nestedIndex}`;
48+
const tx = new BetterSQLiteTransaction(this.dialect, this.session, this.nestedIndex + 1);
49+
this.session.run(sql.raw(`savepoint ${savepointName}`));
50+
try {
51+
const result = transaction(tx);
52+
this.session.run(sql.raw(`release savepoint ${savepointName}`));
53+
return result;
54+
} catch (err) {
55+
this.session.run(sql.raw(`rollback to savepoint ${savepointName}`));
56+
throw err;
57+
}
4458
}
4559
}
4660

0 commit comments

Comments
 (0)