Skip to content

Commit

Permalink
Merge pull request #156 from drizzle-team/aws-dataapi
Browse files Browse the repository at this point in the history
Aws dataapi
  • Loading branch information
AndriiSherman authored Jan 31, 2023
2 parents ed722e7 + afeab5b commit dd587f5
Show file tree
Hide file tree
Showing 27 changed files with 2,484 additions and 42 deletions.
22 changes: 22 additions & 0 deletions changelogs/drizzle-orm/0.17.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
We have released [AWS Data API support](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html) for PostgreSQL

---

Connection example

```typescript
import { drizzle, migrate } from 'drizzle-orm/aws-data-api/pg';

const rdsClient = new RDSDataClient({});

const db = drizzle(rdsClient, {
database: '',
secretArn: '',
resourceArn: '',
});

await migrate(db, { migrationsFolder: '' });
```

> **Note**:
> All drizzle pg data types are working well with data api, except of `interval`. This type is not yet mapped in proper way
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.17.2",
"version": "0.17.3",
"description": "Drizzle ORM package for SQL databases",
"scripts": {
"build": "tsc && resolve-tspaths && cp ../README.md package.json dist/",
Expand Down Expand Up @@ -39,12 +39,13 @@
"@types/pg": "*",
"@types/sql.js": "*",
"better-sqlite3": ">=7 <9",
"bun-types": "*",
"mysql2": ">=2 <3",
"pg": ">=8 <9",
"postgres": ">=3 <4",
"sql.js": ">=1 <2",
"sqlite3": ">=5 <6",
"bun-types": "*"
"@aws-sdk/client-rds-data": ">=3 <4"
},
"peerDependenciesMeta": {
"mysql2": {
Expand Down Expand Up @@ -82,9 +83,13 @@
},
"bun-types": {
"optional": true
},
"@aws-sdk/client-rds-data": {
"optional": true
}
},
"devDependencies": {
"@aws-sdk/client-rds-data": "^3.257.0",
"@cloudflare/workers-types": "^3.18.0",
"@neondatabase/serverless": "^0.1.13",
"@types/better-sqlite3": "^7.6.2",
Expand Down
72 changes: 72 additions & 0 deletions drizzle-orm/src/aws-data-api/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Field, TypeHint } from "@aws-sdk/client-rds-data";
import { QueryTypingsValue } from "~/sql";

export function getValueFromDataApi(row: Field) {
if (typeof row.stringValue !== 'undefined') {
return row.stringValue;
} else if (typeof row.booleanValue !== 'undefined') {
return row.booleanValue;
} else if (typeof row.doubleValue !== 'undefined') {
return row.doubleValue;
} else if (typeof row.isNull !== 'undefined') {
return null;
} else if (typeof row.longValue !== 'undefined') {
return row.longValue;
} else if (typeof row.blobValue !== 'undefined') {
return row.blobValue;
} else if (typeof row.arrayValue !== 'undefined') {
if (typeof row.arrayValue.stringValues !== 'undefined') {
return row.arrayValue.stringValues;
}
throw Error('Unknown array type');
} else {
throw Error('Unknown type');
}
}

export function typingsToAwsTypeHint(typings?: QueryTypingsValue): TypeHint | undefined {
if (typings === 'date') {
return TypeHint.DATE;
} else if (typings === 'decimal') {
return TypeHint.DECIMAL;
} else if (typings === 'json') {
return TypeHint.JSON;
} else if (typings === 'time') {
return TypeHint.TIME;
} else if (typings === 'timestamp') {
return TypeHint.TIMESTAMP;
} else if (typings === 'uuid') {
return TypeHint.UUID;
} else {
return undefined;
}
}

export function toValueParam(value: any, typings?: QueryTypingsValue): { value: Field; typeHint?: TypeHint } {
let response: { value: Field; typeHint?: TypeHint } = {
value: {} as any,
typeHint: typingsToAwsTypeHint(typings),
};

if (value === null) {
response.value = { isNull: true };
} else if (typeof value === 'string') {
if (response.typeHint === 'DATE') {
response.value = { stringValue: value.split('T')[0]! };
} else {
response.value = { stringValue: value };
}
} else if (typeof value === 'number' && Number.isInteger(value)) {
response.value = { longValue: value };
} else if (typeof value === 'number' && !Number.isInteger(value)) {
response.value = { doubleValue: value };
} else if (typeof value === 'boolean') {
response.value = { booleanValue: value };
} else if (value instanceof Date) {
response.value = { stringValue: value.toISOString().replace('T', ' ').replace('Z', '') };
} else {
throw Error(`Unknown type for ${value}`);
}

return response;
}
50 changes: 50 additions & 0 deletions drizzle-orm/src/aws-data-api/pg/driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Logger } from '~/logger';
import { PgDatabase } from '~/pg-core/db';
import { PgDialect } from '~/pg-core/dialect';
import { AwsDataApiClient, AwsDataApiPgQueryResultHKT, AwsDataApiSession } from './session';

export interface PgDriverOptions {
logger?: Logger;
database: string;
resourceArn: string;
secretArn: string;
}

export class AwsDataApiDriver {
constructor(
private client: AwsDataApiClient,
private dialect: PgDialect,
private options: PgDriverOptions,
) {
}

createSession(): AwsDataApiSession {
return new AwsDataApiSession(this.client, this.dialect, this.options);
}
}

export interface DrizzleConfig {
logger?: Logger;
database: string;
resourceArn: string;
secretArn: string;
}

export type AwsDataApiPgDatabase = PgDatabase<AwsDataApiPgQueryResultHKT, AwsDataApiSession>;

export class AwsPgDialect extends PgDialect {
override escapeName(name: string): string {
return `"${name}"`;
}

override escapeParam(num: number): string {
return `:${num + 1}`;
}
}

export function drizzle(client: AwsDataApiClient, config: DrizzleConfig): AwsDataApiPgDatabase {
const dialect = new AwsPgDialect();
const driver = new AwsDataApiDriver(client, dialect, config);
const session = driver.createSession();
return new PgDatabase(dialect, session);
}
2 changes: 2 additions & 0 deletions drizzle-orm/src/aws-data-api/pg/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './driver';
export * from './session';
46 changes: 46 additions & 0 deletions drizzle-orm/src/aws-data-api/pg/migrator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { MigrationConfig, readMigrationFiles } from '~/migrator';
import { sql } from '~/sql';
import { AwsDataApiPgDatabase } from './driver';
import { AwsDataApiSession } from './session';

export async function migrate(db: AwsDataApiPgDatabase, config: string | MigrationConfig) {
const migrations = readMigrationFiles(config);

// Write own aws datapi migrator
const session = db.session as AwsDataApiSession;

const migrationTableCreate = sql`CREATE TABLE IF NOT EXISTS "drizzle"."__drizzle_migrations" (
id SERIAL PRIMARY KEY,
hash text NOT NULL,
created_at bigint
)`;
await session.execute(sql`CREATE SCHEMA IF NOT EXISTS "drizzle"`);
await session.execute(migrationTableCreate);

const dbMigrations = await session.execute<{ id: number; hash: string; created_at: string }[]>(
sql`SELECT id, hash, created_at FROM "drizzle"."__drizzle_migrations" ORDER BY created_at DESC LIMIT 1`,
);

const lastDbMigration = dbMigrations[0];
const transactionId = await session.beginTransaction();

try {
for await (const migration of migrations) {
if (
!lastDbMigration
|| parseInt(lastDbMigration.created_at, 10) < migration.folderMillis
) {
await session.executeWithTransaction(sql.raw(migration.sql), transactionId);
await session.executeWithTransaction(
sql`INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at") VALUES(${migration.hash}, ${migration.folderMillis})`,
transactionId,
);
}
}

await session.commitTransaction(transactionId!);
} catch (e) {
await session.rollbackTransaction(transactionId!);
throw e;
}
}
Loading

0 comments on commit dd587f5

Please sign in to comment.