Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add drivers for Xata #1902

Merged
merged 19 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions changelogs/drizzle-orm/0.30.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## New Features

### 🎉 xata-http driver support

According their **[official website](https://xata.io)**, Xata is a Postgres data platform with a focus on reliability, scalability, and developer experience. The Xata Postgres service is currently in beta, please see the [Xata docs](https://xata.io/docs/postgres) on how to enable it in your account.

Drizzle ORM natively supports both the `xata` driver with `drizzle-orm/xata` package and the **[`postgres`](#postgresjs)** or **[`pg`](#node-postgres)** drivers for accessing a Xata Postgres database.

The following example use the Xata generated client, which you obtain by running the [xata init](https://xata.io/docs/getting-started/installation) CLI command.

```bash
pnpm add drizzle-orm @xata.io/client
```

```ts
import { drizzle } from 'drizzle-orm/xata-http';
import { getXataClient } from './xata'; // Generated client

const xata = getXataClient();
const db = drizzle(xata);

const result = await db.select().from(...);
```

You can also connect to Xata using `pg` or `postgres.js` drivers
7 changes: 6 additions & 1 deletion drizzle-orm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "drizzle-orm",
"version": "0.30.3",
"version": "0.30.4",
"description": "Drizzle ORM package for SQL databases",
"type": "module",
"scripts": {
Expand Down Expand Up @@ -55,6 +55,7 @@
"@types/react": ">=18",
"@types/sql.js": "*",
"@vercel/postgres": "*",
"@xata.io/client": "*",
"better-sqlite3": ">=7",
"bun-types": "*",
"expo-sqlite": ">=13.2.0",
Expand All @@ -74,6 +75,9 @@
"@vercel/postgres": {
"optional": true
},
"@xata.io/client": {
"optional": true
},
"better-sqlite3": {
"optional": true
},
Expand Down Expand Up @@ -153,6 +157,7 @@
"@types/react": "^18.2.45",
"@types/sql.js": "^1.4.4",
"@vercel/postgres": "^0.3.0",
"@xata.io/client": "^0.29.3",
"better-sqlite3": "^8.4.0",
"bun-types": "^0.6.6",
"cpy": "^10.1.0",
Expand Down
79 changes: 79 additions & 0 deletions drizzle-orm/src/xata-http/driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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 type { ExtractTablesWithRelations, RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts';
import { createTableRelationsHelpers, extractTablesRelationalConfig } from '~/relations.ts';
import type { DrizzleConfig } from '~/utils.ts';
import type { XataHttpClient, XataHttpQueryResultHKT } from './session.ts';
import { XataHttpSession } from './session.ts';

export interface XataDriverOptions {
logger?: Logger;
}

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

constructor(
private client: XataHttpClient,
private dialect: PgDialect,
private options: XataDriverOptions = {},
) {
this.initMappers();
}

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

initMappers() {
// TODO: Add custom type parsers
}
}

export class XataHttpDatabase<TSchema extends Record<string, unknown> = Record<string, never>>
extends PgDatabase<XataHttpQueryResultHKT, TSchema>
{
static readonly [entityKind]: string = 'XataHttpDatabase';

/** @internal */
declare readonly session: XataHttpSession<TSchema, ExtractTablesWithRelations<TSchema>>;
}

export function drizzle<TSchema extends Record<string, unknown> = Record<string, never>>(
client: XataHttpClient,
config: DrizzleConfig<TSchema> = {},
): XataHttpDatabase<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 XataHttpDriver(client, dialect, { logger });
const session = driver.createSession(schema);

return new XataHttpDatabase(
dialect,
session,
schema as RelationalSchemaConfig<ExtractTablesWithRelations<TSchema>> | undefined,
);
}
2 changes: 2 additions & 0 deletions drizzle-orm/src/xata-http/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './driver.ts';
export * from './session.ts';
57 changes: 57 additions & 0 deletions drizzle-orm/src/xata-http/migrator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { readMigrationFiles } from '~/migrator.ts';
import { sql } from '~/sql/sql.ts';
import type { XataHttpDatabase } from './driver.ts';

export interface MigrationConfig {
migrationsFolder: string;
migrationsTable?: string;
}

/**
* This function reads migrationFolder and execute each unapplied migration and mark it as executed in database
*
* NOTE: The Xata HTTP driver does not support transactions. This means that if any part of a migration fails,
* no rollback will be executed. Currently, you will need to handle unsuccessful migration yourself.
* @param db - drizzle db instance
* @param config - path to migration folder generated by drizzle-kit
*/ export async function migrate<TSchema extends Record<string, unknown>>(
db: XataHttpDatabase<TSchema>,
config: string | MigrationConfig,
) {
const migrations = readMigrationFiles(config);
const migrationsTable = typeof config === 'string'
? '__drizzle_migrations'
: config.migrationsTable ?? '__drizzle_migrations';
const migrationTableCreate = sql`
CREATE TABLE IF NOT EXISTS ${sql.identifier(migrationsTable)} (
id SERIAL PRIMARY KEY,
hash text NOT NULL,
created_at bigint
)
`;
await db.session.execute(migrationTableCreate);

const dbMigrations = await db.session.all<{
id: number;
hash: string;
created_at: string;
}>(
sql`select id, hash, created_at from ${sql.identifier(migrationsTable)} order by created_at desc limit 1`,
);

const lastDbMigration = dbMigrations[0];

for await (const migration of migrations) {
if (!lastDbMigration || Number(lastDbMigration.created_at) < migration.folderMillis) {
for (const stmt of migration.sql) {
await db.session.execute(sql.raw(stmt));
}

await db.session.execute(
sql`insert into ${
sql.identifier(migrationsTable)
} ("hash", "created_at") values(${migration.hash}, ${migration.folderMillis})`,
);
}
}
}
164 changes: 164 additions & 0 deletions drizzle-orm/src/xata-http/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import type { SQLPluginResult, SQLQueryResult } from '@xata.io/client';
import { entityKind } from '~/entity.ts';
import type { Logger } from '~/logger.ts';
import { 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 } from '~/sql/sql.ts';
import { mapResultRow } from '~/utils.ts';

export type XataHttpClient = {
sql: SQLPluginResult;
};

export interface QueryResults<ArrayMode extends 'json' | 'array'> {
rowCount: number;
rows: ArrayMode extends 'array' ? any[][] : Record<string, any>[];
rowAsArray: ArrayMode extends 'array' ? true : false;
}

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

constructor(
private client: XataHttpClient,
query: Query,
private logger: Logger,
private fields: SelectedFieldsOrdered | undefined,
private _isResponseInArrayMode: boolean,
private customResultMapper?: (rows: unknown[][]) => T['execute'],
) {
super(query);
}

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

this.logger.logQuery(this.query.sql, params);

const { fields, client, query, customResultMapper, joinsNotNullableMap } = this;

if (!fields && !customResultMapper) {
return await client.sql<Record<string, any>>({ statement: query.sql, params });
// return { rowCount: result.records.length, rows: result.records, rowAsArray: false };
}

const { rows, warning } = await client.sql({ statement: query.sql, params, responseType: 'array' });
if (warning) console.warn(warning);

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

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

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

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

export interface XataHttpSessionOptions {
logger?: Logger;
}

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

private logger: Logger;

constructor(
private client: XataHttpClient,
dialect: PgDialect,
private schema: RelationalSchemaConfig<TSchema> | undefined,
private options: XataHttpSessionOptions = {},
) {
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 XataHttpPreparedQuery(
this.client,
query,
this.logger,
fields,
isResponseInArrayMode,
customResultMapper,
);
}

async query(query: string, params: unknown[]): Promise<QueryResults<'array'>> {
this.logger.logQuery(query, params);
const result = await this.client.sql({ statement: query, params, responseType: 'array' });

return {
rowCount: result.rows.length,
rows: result.rows,
rowAsArray: true,
};
}

async queryObjects(query: string, params: unknown[]): Promise<QueryResults<'json'>> {
const result = await this.client.sql<Record<string, any>>({ statement: query, params });

return {
rowCount: result.records.length,
rows: result.records,
rowAsArray: false,
};
}

override async transaction<T>(
_transaction: (tx: XataTransaction<TFullSchema, TSchema>) => Promise<T>,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_config: PgTransactionConfig = {},
): Promise<T> {
throw new Error('No transactions support in Xata Http driver');
}
}

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

override async transaction<T>(_transaction: (tx: XataTransaction<TFullSchema, TSchema>) => Promise<T>): Promise<T> {
throw new Error('No transactions support in Xata Http driver');
}
}

export interface XataHttpQueryResultHKT extends QueryResultHKT {
type: SQLQueryResult<this['row']>;
}
1 change: 1 addition & 0 deletions integration-tests/.xata/migrations/.ledger
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

4 changes: 4 additions & 0 deletions integration-tests/.xata/version/compatibility.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"@xata.io/cli": { "latest": "0.15.10", "compatibility": [{ "range": ">=0.0.0" }] },
"@xata.io/client": { "latest": "0.29.3", "compatibility": [{ "range": ">=0.0.0" }] }
}
6 changes: 6 additions & 0 deletions integration-tests/.xatarc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"databaseURL": "https://Andrii-Sherman-s-workspace-2r5ujp.us-east-1.xata.sh/db/integration-tests",
"codegen": {
"output": "tests/xata/xata.ts"
}
}
Loading