Skip to content

Commit

Permalink
Merge pull request #2081 from drizzle-team/feature/pglite
Browse files Browse the repository at this point in the history
Feature/pglite
  • Loading branch information
AndriiSherman committed Mar 28, 2024
2 parents f6de0d5 + dfa923a commit 0ddab65
Show file tree
Hide file tree
Showing 11 changed files with 4,370 additions and 2 deletions.
1 change: 1 addition & 0 deletions .github/workflows/release-feature-branch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ jobs:
MYSQL_CONNECTION_STRING: mysql://root:root@localhost:3306/drizzle
PLANETSCALE_CONNECTION_STRING: ${{ secrets.PLANETSCALE_CONNECTION_STRING }}
NEON_CONNECTION_STRING: ${{ secrets.NEON_CONNECTION_STRING }}
XATA_API_KEY: ${{ secrets.XATA_API_KEY }}
LIBSQL_URL: file:local.db
run: |
if [[ ${{ github.event_name }} != "push" && "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then
Expand Down
27 changes: 27 additions & 0 deletions changelogs/drizzle-orm/0.30.6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## New Features

### 馃帀 PGlite driver Support

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.

It can be used as an ephemeral in-memory database, or with persistence either to the file system (Node/Bun) or indexedDB (Browser).

Unlike previous "Postgres in the browser" projects, PGlite does not use a Linux virtual machine - it is simply Postgres in WASM.

Usage Example
```ts
import { PGlite } from '@electric-sql/pglite';
import { drizzle } from 'drizzle-orm/pglite';

// In-memory Postgres
const client = new PGlite();
const db = drizzle(client);

await db.select().from(users);
```
---
There are currently 2 limitations, that should be fixed on Pglite side:

- [Attempting to refresh a materialised view throws error](https://github.com/electric-sql/pglite/issues/63)

- [Attempting to SET TIME ZONE throws error](https://github.com/electric-sql/pglite/issues/62)
9 changes: 7 additions & 2 deletions drizzle-orm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "drizzle-orm",
"version": "0.30.5",
"version": "0.30.6",
"description": "Drizzle ORM package for SQL databases",
"type": "module",
"scripts": {
Expand Down Expand Up @@ -66,7 +66,8 @@
"postgres": ">=3",
"react": ">=18",
"sql.js": ">=1",
"sqlite3": ">=5"
"sqlite3": ">=5",
"@electric-sql/pglite": ">=0.1.1"
},
"peerDependenciesMeta": {
"mysql2": {
Expand Down Expand Up @@ -140,11 +141,15 @@
},
"@types/react": {
"optional": true
},
"@electric-sql/pglite": {
"optional": true
}
},
"devDependencies": {
"@aws-sdk/client-rds-data": "^3.344.0",
"@cloudflare/workers-types": "^4.20230904.0",
"@electric-sql/pglite": "^0.1.1",
"@libsql/client": "^0.5.6",
"@neondatabase/serverless": "^0.9.0",
"@op-engineering/op-sqlite": "^2.0.16",
Expand Down
69 changes: 69 additions & 0 deletions drizzle-orm/src/pglite/driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { entityKind } from '~/entity.ts';
import type { Logger } from '~/logger.ts';
import { DefaultLogger } from '~/logger.ts';
import { PgDatabase } from '~/pg-core/db.ts';
import { PgDialect } from '~/pg-core/dialect.ts';
import {
createTableRelationsHelpers,
extractTablesRelationalConfig,
type RelationalSchemaConfig,
type TablesRelationalConfig,
} from '~/relations.ts';
import type { DrizzleConfig } from '~/utils.ts';
import type { PgliteClient, PgliteQueryResultHKT } from './session.ts';
import { PgliteSession } from './session.ts';

export interface PgDriverOptions {
logger?: Logger;
}

export class PgliteDriver {
static readonly [entityKind]: string = 'PgliteDriver';

constructor(
private client: PgliteClient,
private dialect: PgDialect,
private options: PgDriverOptions = {},
) {
}

createSession(
schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined,
): PgliteSession<Record<string, unknown>, TablesRelationalConfig> {
return new PgliteSession(this.client, this.dialect, schema, { logger: this.options.logger });
}
}

export type PgliteDatabase<
TSchema extends Record<string, unknown> = Record<string, never>,
> = PgDatabase<PgliteQueryResultHKT, TSchema>;

export function drizzle<TSchema extends Record<string, unknown> = Record<string, never>>(
client: PgliteClient,
config: DrizzleConfig<TSchema> = {},
): PgliteDatabase<TSchema> {
const dialect = new PgDialect();
let logger;
if (config.logger === true) {
logger = new DefaultLogger();
} else if (config.logger !== false) {
logger = config.logger;
}

let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
if (config.schema) {
const tablesConfig = extractTablesRelationalConfig(
config.schema,
createTableRelationsHelpers,
);
schema = {
fullSchema: config.schema,
schema: tablesConfig.tables,
tableNamesMap: tablesConfig.tableNamesMap,
};
}

const driver = new PgliteDriver(client, dialect, { logger });
const session = driver.createSession(schema);
return new PgDatabase(dialect, session, schema) as PgliteDatabase<TSchema>;
}
2 changes: 2 additions & 0 deletions drizzle-orm/src/pglite/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './driver.ts';
export * from './session.ts';
11 changes: 11 additions & 0 deletions drizzle-orm/src/pglite/migrator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { MigrationConfig } from '~/migrator.ts';
import { readMigrationFiles } from '~/migrator.ts';
import type { PgliteDatabase } from './driver.ts';

export async function migrate<TSchema extends Record<string, unknown>>(
db: PgliteDatabase<TSchema>,
config: string | MigrationConfig,
) {
const migrations = readMigrationFiles(config);
await db.dialect.migrate(migrations, db.session, config);
}
168 changes: 168 additions & 0 deletions drizzle-orm/src/pglite/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import type { PGlite, QueryOptions, Results, Row, Transaction } from '@electric-sql/pglite';
import { entityKind } from '~/entity.ts';
import { type Logger, NoopLogger } from '~/logger.ts';
import type { PgDialect } from '~/pg-core/dialect.ts';
import { PgTransaction } from '~/pg-core/index.ts';
import type { SelectedFieldsOrdered } from '~/pg-core/query-builders/select.types.ts';
import type { PgTransactionConfig, PreparedQueryConfig, QueryResultHKT } from '~/pg-core/session.ts';
import { PgPreparedQuery, PgSession } from '~/pg-core/session.ts';
import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts';
import { fillPlaceholders, type Query, sql } from '~/sql/sql.ts';
import { type Assume, mapResultRow } from '~/utils.ts';

import { types } from '@electric-sql/pglite';

export type PgliteClient = PGlite;

export class PglitePreparedQuery<T extends PreparedQueryConfig> extends PgPreparedQuery<T> {
static readonly [entityKind]: string = 'PglitePreparedQuery';

private rawQueryConfig: QueryOptions;
private queryConfig: QueryOptions;

constructor(
private client: PgliteClient | Transaction,
private queryString: string,
private params: unknown[],
private logger: Logger,
private fields: SelectedFieldsOrdered | undefined,
name: string | undefined,
private _isResponseInArrayMode: boolean,
private customResultMapper?: (rows: unknown[][]) => T['execute'],
) {
super({ sql: queryString, params });
this.rawQueryConfig = {
rowMode: 'object',
parsers: {
[types.TIMESTAMP]: (value) => value,
[types.TIMESTAMPTZ]: (value) => value,
[types.INTERVAL]: (value) => value,
[types.DATE]: (value) => value,
},
};
this.queryConfig = {
rowMode: 'array',
parsers: {
[types.TIMESTAMP]: (value) => value,
[types.TIMESTAMPTZ]: (value) => value,
[types.INTERVAL]: (value) => value,
[types.DATE]: (value) => value,
},
};
}

async execute(placeholderValues: Record<string, unknown> | undefined = {}): Promise<T['execute']> {
const params = fillPlaceholders(this.params, placeholderValues);

this.logger.logQuery(this.queryString, params);

const { fields, rawQueryConfig, client, queryConfig, joinsNotNullableMap, customResultMapper, queryString } = this;

if (!fields && !customResultMapper) {
return client.query<any[]>(queryString, params, rawQueryConfig);
}

const result = await client.query<any[][]>(queryString, params, queryConfig);

return customResultMapper
? customResultMapper(result.rows)
: result.rows.map((row) => mapResultRow<T['execute']>(fields!, row, joinsNotNullableMap));
}

all(placeholderValues: Record<string, unknown> | undefined = {}): Promise<T['all']> {
const params = fillPlaceholders(this.params, placeholderValues);
this.logger.logQuery(this.queryString, params);
return this.client.query(this.queryString, params, this.rawQueryConfig).then((result) => result.rows);
}

/** @internal */
isResponseInArrayMode(): boolean {
return this._isResponseInArrayMode;
}
}

export interface PgliteSessionOptions {
logger?: Logger;
}

export class PgliteSession<
TFullSchema extends Record<string, unknown>,
TSchema extends TablesRelationalConfig,
> extends PgSession<PgliteQueryResultHKT, TFullSchema, TSchema> {
static readonly [entityKind]: string = 'PgliteSession';

private logger: Logger;

constructor(
private client: PgliteClient | Transaction,
dialect: PgDialect,
private schema: RelationalSchemaConfig<TSchema> | undefined,
private options: PgliteSessionOptions = {},
) {
super(dialect);
this.logger = options.logger ?? new NoopLogger();
}

prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
query: Query,
fields: SelectedFieldsOrdered | undefined,
name: string | undefined,
isResponseInArrayMode: boolean,
customResultMapper?: (rows: unknown[][]) => T['execute'],
): PgPreparedQuery<T> {
return new PglitePreparedQuery(
this.client,
query.sql,
query.params,
this.logger,
fields,
name,
isResponseInArrayMode,
customResultMapper,
);
}

override async transaction<T>(
transaction: (tx: PgliteTransaction<TFullSchema, TSchema>) => Promise<T>,
config?: PgTransactionConfig | undefined,
): Promise<T> {
return (this.client as PgliteClient).transaction(async (client) => {
const session = new PgliteSession<TFullSchema, TSchema>(
client,
this.dialect,
this.schema,
this.options,
);
const tx = new PgliteTransaction(this.dialect, session, this.schema);
if (config) {
await tx.setTransaction(config);
}
return transaction(tx);
}) as Promise<T>;
}
}

export class PgliteTransaction<
TFullSchema extends Record<string, unknown>,
TSchema extends TablesRelationalConfig,
> extends PgTransaction<PgliteQueryResultHKT, TFullSchema, TSchema> {
static readonly [entityKind]: string = 'PgliteTransaction';

override async transaction<T>(transaction: (tx: PgliteTransaction<TFullSchema, TSchema>) => Promise<T>): Promise<T> {
const savepointName = `sp${this.nestedIndex + 1}`;
const tx = new PgliteTransaction(this.dialect, this.session, this.schema, this.nestedIndex + 1);
await tx.execute(sql.raw(`savepoint ${savepointName}`));
try {
const result = await transaction(tx);
await tx.execute(sql.raw(`release savepoint ${savepointName}`));
return result;
} catch (err) {
await tx.execute(sql.raw(`rollback to savepoint ${savepointName}`));
throw err;
}
}
}

export interface PgliteQueryResultHKT extends QueryResultHKT {
type: Results<Assume<this['row'], Row>>;
}
1 change: 1 addition & 0 deletions integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"dependencies": {
"@aws-sdk/client-rds-data": "^3.345.0",
"@aws-sdk/credential-providers": "^3.345.0",
"@electric-sql/pglite": "^0.1.1",
"@libsql/client": "^0.5.6",
"@miniflare/d1": "^2.14.0",
"@miniflare/shared": "^2.14.0",
Expand Down
3 changes: 3 additions & 0 deletions integration-tests/tests/imports/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ it('dynamic imports check for CommonJS', async () => {
const promises: ProcessPromise[] = [];
for (const [i, key] of Object.keys(pj['exports']).entries()) {
const o1 = path.join('drizzle-orm', key);
if (o1.startsWith('drizzle-orm/pglite')) {
continue;
}
fs.writeFileSync(`${IMPORTS_FOLDER}/imports_${i}.cjs`, 'requ');
fs.appendFileSync(`${IMPORTS_FOLDER}/imports_${i}.cjs`, 'ire("' + o1 + '");\n', {});

Expand Down

0 comments on commit 0ddab65

Please sign in to comment.