Skip to content

Commit 0ddab65

Browse files
Merge pull request #2081 from drizzle-team/feature/pglite
Feature/pglite
2 parents f6de0d5 + dfa923a commit 0ddab65

File tree

11 files changed

+4370
-2
lines changed

11 files changed

+4370
-2
lines changed

.github/workflows/release-feature-branch.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ jobs:
114114
MYSQL_CONNECTION_STRING: mysql://root:root@localhost:3306/drizzle
115115
PLANETSCALE_CONNECTION_STRING: ${{ secrets.PLANETSCALE_CONNECTION_STRING }}
116116
NEON_CONNECTION_STRING: ${{ secrets.NEON_CONNECTION_STRING }}
117+
XATA_API_KEY: ${{ secrets.XATA_API_KEY }}
117118
LIBSQL_URL: file:local.db
118119
run: |
119120
if [[ ${{ github.event_name }} != "push" && "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then

changelogs/drizzle-orm/0.30.6.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
## New Features
2+
3+
### 🎉 PGlite driver Support
4+
5+
PGlite is a WASM Postgres build packaged into a TypeScript client library that enables you to run Postgres in the browser, Node.js and Bun, with no need to install any other dependencies. It is only 2.6mb gzipped.
6+
7+
It can be used as an ephemeral in-memory database, or with persistence either to the file system (Node/Bun) or indexedDB (Browser).
8+
9+
Unlike previous "Postgres in the browser" projects, PGlite does not use a Linux virtual machine - it is simply Postgres in WASM.
10+
11+
Usage Example
12+
```ts
13+
import { PGlite } from '@electric-sql/pglite';
14+
import { drizzle } from 'drizzle-orm/pglite';
15+
16+
// In-memory Postgres
17+
const client = new PGlite();
18+
const db = drizzle(client);
19+
20+
await db.select().from(users);
21+
```
22+
---
23+
There are currently 2 limitations, that should be fixed on Pglite side:
24+
25+
- [Attempting to refresh a materialised view throws error](https://github.com/electric-sql/pglite/issues/63)
26+
27+
- [Attempting to SET TIME ZONE throws error](https://github.com/electric-sql/pglite/issues/62)

drizzle-orm/package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "drizzle-orm",
3-
"version": "0.30.5",
3+
"version": "0.30.6",
44
"description": "Drizzle ORM package for SQL databases",
55
"type": "module",
66
"scripts": {
@@ -66,7 +66,8 @@
6666
"postgres": ">=3",
6767
"react": ">=18",
6868
"sql.js": ">=1",
69-
"sqlite3": ">=5"
69+
"sqlite3": ">=5",
70+
"@electric-sql/pglite": ">=0.1.1"
7071
},
7172
"peerDependenciesMeta": {
7273
"mysql2": {
@@ -140,11 +141,15 @@
140141
},
141142
"@types/react": {
142143
"optional": true
144+
},
145+
"@electric-sql/pglite": {
146+
"optional": true
143147
}
144148
},
145149
"devDependencies": {
146150
"@aws-sdk/client-rds-data": "^3.344.0",
147151
"@cloudflare/workers-types": "^4.20230904.0",
152+
"@electric-sql/pglite": "^0.1.1",
148153
"@libsql/client": "^0.5.6",
149154
"@neondatabase/serverless": "^0.9.0",
150155
"@op-engineering/op-sqlite": "^2.0.16",

drizzle-orm/src/pglite/driver.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { entityKind } from '~/entity.ts';
2+
import type { Logger } from '~/logger.ts';
3+
import { DefaultLogger } from '~/logger.ts';
4+
import { PgDatabase } from '~/pg-core/db.ts';
5+
import { PgDialect } from '~/pg-core/dialect.ts';
6+
import {
7+
createTableRelationsHelpers,
8+
extractTablesRelationalConfig,
9+
type RelationalSchemaConfig,
10+
type TablesRelationalConfig,
11+
} from '~/relations.ts';
12+
import type { DrizzleConfig } from '~/utils.ts';
13+
import type { PgliteClient, PgliteQueryResultHKT } from './session.ts';
14+
import { PgliteSession } from './session.ts';
15+
16+
export interface PgDriverOptions {
17+
logger?: Logger;
18+
}
19+
20+
export class PgliteDriver {
21+
static readonly [entityKind]: string = 'PgliteDriver';
22+
23+
constructor(
24+
private client: PgliteClient,
25+
private dialect: PgDialect,
26+
private options: PgDriverOptions = {},
27+
) {
28+
}
29+
30+
createSession(
31+
schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined,
32+
): PgliteSession<Record<string, unknown>, TablesRelationalConfig> {
33+
return new PgliteSession(this.client, this.dialect, schema, { logger: this.options.logger });
34+
}
35+
}
36+
37+
export type PgliteDatabase<
38+
TSchema extends Record<string, unknown> = Record<string, never>,
39+
> = PgDatabase<PgliteQueryResultHKT, TSchema>;
40+
41+
export function drizzle<TSchema extends Record<string, unknown> = Record<string, never>>(
42+
client: PgliteClient,
43+
config: DrizzleConfig<TSchema> = {},
44+
): PgliteDatabase<TSchema> {
45+
const dialect = new PgDialect();
46+
let logger;
47+
if (config.logger === true) {
48+
logger = new DefaultLogger();
49+
} else if (config.logger !== false) {
50+
logger = config.logger;
51+
}
52+
53+
let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
54+
if (config.schema) {
55+
const tablesConfig = extractTablesRelationalConfig(
56+
config.schema,
57+
createTableRelationsHelpers,
58+
);
59+
schema = {
60+
fullSchema: config.schema,
61+
schema: tablesConfig.tables,
62+
tableNamesMap: tablesConfig.tableNamesMap,
63+
};
64+
}
65+
66+
const driver = new PgliteDriver(client, dialect, { logger });
67+
const session = driver.createSession(schema);
68+
return new PgDatabase(dialect, session, schema) as PgliteDatabase<TSchema>;
69+
}

drizzle-orm/src/pglite/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './driver.ts';
2+
export * from './session.ts';

drizzle-orm/src/pglite/migrator.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { MigrationConfig } from '~/migrator.ts';
2+
import { readMigrationFiles } from '~/migrator.ts';
3+
import type { PgliteDatabase } from './driver.ts';
4+
5+
export async function migrate<TSchema extends Record<string, unknown>>(
6+
db: PgliteDatabase<TSchema>,
7+
config: string | MigrationConfig,
8+
) {
9+
const migrations = readMigrationFiles(config);
10+
await db.dialect.migrate(migrations, db.session, config);
11+
}

drizzle-orm/src/pglite/session.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import type { PGlite, QueryOptions, Results, Row, Transaction } from '@electric-sql/pglite';
2+
import { entityKind } from '~/entity.ts';
3+
import { type Logger, NoopLogger } from '~/logger.ts';
4+
import type { PgDialect } from '~/pg-core/dialect.ts';
5+
import { PgTransaction } from '~/pg-core/index.ts';
6+
import type { SelectedFieldsOrdered } from '~/pg-core/query-builders/select.types.ts';
7+
import type { PgTransactionConfig, PreparedQueryConfig, QueryResultHKT } from '~/pg-core/session.ts';
8+
import { PgPreparedQuery, PgSession } from '~/pg-core/session.ts';
9+
import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts';
10+
import { fillPlaceholders, type Query, sql } from '~/sql/sql.ts';
11+
import { type Assume, mapResultRow } from '~/utils.ts';
12+
13+
import { types } from '@electric-sql/pglite';
14+
15+
export type PgliteClient = PGlite;
16+
17+
export class PglitePreparedQuery<T extends PreparedQueryConfig> extends PgPreparedQuery<T> {
18+
static readonly [entityKind]: string = 'PglitePreparedQuery';
19+
20+
private rawQueryConfig: QueryOptions;
21+
private queryConfig: QueryOptions;
22+
23+
constructor(
24+
private client: PgliteClient | Transaction,
25+
private queryString: string,
26+
private params: unknown[],
27+
private logger: Logger,
28+
private fields: SelectedFieldsOrdered | undefined,
29+
name: string | undefined,
30+
private _isResponseInArrayMode: boolean,
31+
private customResultMapper?: (rows: unknown[][]) => T['execute'],
32+
) {
33+
super({ sql: queryString, params });
34+
this.rawQueryConfig = {
35+
rowMode: 'object',
36+
parsers: {
37+
[types.TIMESTAMP]: (value) => value,
38+
[types.TIMESTAMPTZ]: (value) => value,
39+
[types.INTERVAL]: (value) => value,
40+
[types.DATE]: (value) => value,
41+
},
42+
};
43+
this.queryConfig = {
44+
rowMode: 'array',
45+
parsers: {
46+
[types.TIMESTAMP]: (value) => value,
47+
[types.TIMESTAMPTZ]: (value) => value,
48+
[types.INTERVAL]: (value) => value,
49+
[types.DATE]: (value) => value,
50+
},
51+
};
52+
}
53+
54+
async execute(placeholderValues: Record<string, unknown> | undefined = {}): Promise<T['execute']> {
55+
const params = fillPlaceholders(this.params, placeholderValues);
56+
57+
this.logger.logQuery(this.queryString, params);
58+
59+
const { fields, rawQueryConfig, client, queryConfig, joinsNotNullableMap, customResultMapper, queryString } = this;
60+
61+
if (!fields && !customResultMapper) {
62+
return client.query<any[]>(queryString, params, rawQueryConfig);
63+
}
64+
65+
const result = await client.query<any[][]>(queryString, params, queryConfig);
66+
67+
return customResultMapper
68+
? customResultMapper(result.rows)
69+
: result.rows.map((row) => mapResultRow<T['execute']>(fields!, row, joinsNotNullableMap));
70+
}
71+
72+
all(placeholderValues: Record<string, unknown> | undefined = {}): Promise<T['all']> {
73+
const params = fillPlaceholders(this.params, placeholderValues);
74+
this.logger.logQuery(this.queryString, params);
75+
return this.client.query(this.queryString, params, this.rawQueryConfig).then((result) => result.rows);
76+
}
77+
78+
/** @internal */
79+
isResponseInArrayMode(): boolean {
80+
return this._isResponseInArrayMode;
81+
}
82+
}
83+
84+
export interface PgliteSessionOptions {
85+
logger?: Logger;
86+
}
87+
88+
export class PgliteSession<
89+
TFullSchema extends Record<string, unknown>,
90+
TSchema extends TablesRelationalConfig,
91+
> extends PgSession<PgliteQueryResultHKT, TFullSchema, TSchema> {
92+
static readonly [entityKind]: string = 'PgliteSession';
93+
94+
private logger: Logger;
95+
96+
constructor(
97+
private client: PgliteClient | Transaction,
98+
dialect: PgDialect,
99+
private schema: RelationalSchemaConfig<TSchema> | undefined,
100+
private options: PgliteSessionOptions = {},
101+
) {
102+
super(dialect);
103+
this.logger = options.logger ?? new NoopLogger();
104+
}
105+
106+
prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
107+
query: Query,
108+
fields: SelectedFieldsOrdered | undefined,
109+
name: string | undefined,
110+
isResponseInArrayMode: boolean,
111+
customResultMapper?: (rows: unknown[][]) => T['execute'],
112+
): PgPreparedQuery<T> {
113+
return new PglitePreparedQuery(
114+
this.client,
115+
query.sql,
116+
query.params,
117+
this.logger,
118+
fields,
119+
name,
120+
isResponseInArrayMode,
121+
customResultMapper,
122+
);
123+
}
124+
125+
override async transaction<T>(
126+
transaction: (tx: PgliteTransaction<TFullSchema, TSchema>) => Promise<T>,
127+
config?: PgTransactionConfig | undefined,
128+
): Promise<T> {
129+
return (this.client as PgliteClient).transaction(async (client) => {
130+
const session = new PgliteSession<TFullSchema, TSchema>(
131+
client,
132+
this.dialect,
133+
this.schema,
134+
this.options,
135+
);
136+
const tx = new PgliteTransaction(this.dialect, session, this.schema);
137+
if (config) {
138+
await tx.setTransaction(config);
139+
}
140+
return transaction(tx);
141+
}) as Promise<T>;
142+
}
143+
}
144+
145+
export class PgliteTransaction<
146+
TFullSchema extends Record<string, unknown>,
147+
TSchema extends TablesRelationalConfig,
148+
> extends PgTransaction<PgliteQueryResultHKT, TFullSchema, TSchema> {
149+
static readonly [entityKind]: string = 'PgliteTransaction';
150+
151+
override async transaction<T>(transaction: (tx: PgliteTransaction<TFullSchema, TSchema>) => Promise<T>): Promise<T> {
152+
const savepointName = `sp${this.nestedIndex + 1}`;
153+
const tx = new PgliteTransaction(this.dialect, this.session, this.schema, this.nestedIndex + 1);
154+
await tx.execute(sql.raw(`savepoint ${savepointName}`));
155+
try {
156+
const result = await transaction(tx);
157+
await tx.execute(sql.raw(`release savepoint ${savepointName}`));
158+
return result;
159+
} catch (err) {
160+
await tx.execute(sql.raw(`rollback to savepoint ${savepointName}`));
161+
throw err;
162+
}
163+
}
164+
}
165+
166+
export interface PgliteQueryResultHKT extends QueryResultHKT {
167+
type: Results<Assume<this['row'], Row>>;
168+
}

integration-tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"dependencies": {
6464
"@aws-sdk/client-rds-data": "^3.345.0",
6565
"@aws-sdk/credential-providers": "^3.345.0",
66+
"@electric-sql/pglite": "^0.1.1",
6667
"@libsql/client": "^0.5.6",
6768
"@miniflare/d1": "^2.14.0",
6869
"@miniflare/shared": "^2.14.0",

integration-tests/tests/imports/index.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ it('dynamic imports check for CommonJS', async () => {
1818
const promises: ProcessPromise[] = [];
1919
for (const [i, key] of Object.keys(pj['exports']).entries()) {
2020
const o1 = path.join('drizzle-orm', key);
21+
if (o1.startsWith('drizzle-orm/pglite')) {
22+
continue;
23+
}
2124
fs.writeFileSync(`${IMPORTS_FOLDER}/imports_${i}.cjs`, 'requ');
2225
fs.appendFileSync(`${IMPORTS_FOLDER}/imports_${i}.cjs`, 'ire("' + o1 + '");\n', {});
2326

0 commit comments

Comments
 (0)