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

[Pg] Add insert/update array support in aws-data-api #1911

Merged
merged 9 commits into from
Apr 10, 2024
13 changes: 13 additions & 0 deletions drizzle-orm/src/aws-data-api/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ export function getValueFromDataApi(field: Field) {
if (field.arrayValue.stringValues !== undefined) {
return field.arrayValue.stringValues;
}
if (field.arrayValue.longValues !== undefined) {
return field.arrayValue.longValues;
}
if (field.arrayValue.doubleValues !== undefined) {
return field.arrayValue.doubleValues;
}
if (field.arrayValue.booleanValues !== undefined) {
return field.arrayValue.booleanValues;
}
if (field.arrayValue.arrayValues !== undefined) {
return field.arrayValue.arrayValues;
}

throw new Error('Unknown array type');
} else {
throw new Error('Unknown type');
Expand Down
48 changes: 45 additions & 3 deletions drizzle-orm/src/aws-data-api/pg/driver.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { entityKind } from '~/entity.ts';
import type { SQLWrapper } from '~/index.ts';
import { entityKind, is } from '~/entity.ts';
import type { SQL, SQLWrapper } from '~/index.ts';
import { Param, sql, Table } from '~/index.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 { PgColumn, PgInsertConfig, PgTable, TableConfig } from '~/pg-core/index.ts';
import { PgArray } from '~/pg-core/index.ts';
import type { PgRaw } from '~/pg-core/query-builders/raw.ts';
import {
createTableRelationsHelpers,
extractTablesRelationalConfig,
type RelationalSchemaConfig,
type TablesRelationalConfig,
} from '~/relations.ts';
import type { DrizzleConfig } from '~/utils.ts';
import type { DrizzleConfig, UpdateSet } from '~/utils.ts';
import type { AwsDataApiClient, AwsDataApiPgQueryResult, AwsDataApiPgQueryResultHKT } from './session.ts';
import { AwsDataApiSession } from './session.ts';

Expand Down Expand Up @@ -48,6 +51,45 @@ export class AwsPgDialect extends PgDialect {
override escapeParam(num: number): string {
return `:${num + 1}`;
}

override buildInsertQuery(
{ table, values, onConflict, returning }: PgInsertConfig<PgTable<TableConfig>>,
): SQL<unknown> {
const columns: Record<string, PgColumn> = table[Table.Symbol.Columns];
const colEntries: [string, PgColumn][] = Object.entries(columns);
for (const value of values) {
for (const [fieldName, col] of colEntries) {
const colValue = value[fieldName];
if (
is(colValue, Param) && colValue.value !== undefined && is(colValue.encoder, PgArray)
&& Array.isArray(colValue.value)
) {
value[fieldName] = sql`cast(${col.mapToDriverValue(colValue.value)} as ${
sql.raw(colValue.encoder.getSQLType())
})`;
}
}
}

return super.buildInsertQuery({ table, values, onConflict, returning });
}

override buildUpdateSet(table: PgTable<TableConfig>, set: UpdateSet): SQL<unknown> {
const columns: Record<string, PgColumn> = table[Table.Symbol.Columns];

for (const [colName, colValue] of Object.entries(set)) {
const currentColumn = columns[colName];
if (
currentColumn && is(colValue, Param) && colValue.value !== undefined && is(colValue.encoder, PgArray)
&& Array.isArray(colValue.value)
) {
set[colName] = sql`cast(${currentColumn?.mapToDriverValue(colValue.value)} as ${
sql.raw(colValue.encoder.getSQLType())
})`;
}
}
return super.buildUpdateSet(table, set);
}
}

export function drizzle<TSchema extends Record<string, unknown> = Record<string, never>>(
Expand Down
124 changes: 113 additions & 11 deletions integration-tests/tests/awsdatapi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const usersTable = pgTable('users', {
name: text('name').notNull(),
verified: boolean('verified').notNull().default(false),
jsonb: jsonb('jsonb').$type<string[]>(),
bestTexts: text('best_texts').array().default(sql`'{}'`).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
});

Expand Down Expand Up @@ -69,6 +70,7 @@ beforeEach(async () => {
name text not null,
verified boolean not null default false,
jsonb jsonb,
best_texts text[] not null default '{}',
created_at timestamptz not null default now()
)
`,
Expand All @@ -84,7 +86,14 @@ test('select all fields', async () => {

expect(result[0]!.createdAt).toBeInstanceOf(Date);
// t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 100);
expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]);
expect(result).toEqual([{
bestTexts: [],
id: 1,
name: 'John',
verified: false,
jsonb: null,
createdAt: result[0]!.createdAt,
}]);
});

test('select sql', async () => {
Expand Down Expand Up @@ -176,7 +185,14 @@ test('update with returning all fields', async () => {

expect(users[0]!.createdAt).toBeInstanceOf(Date);
// t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 100);
expect(users).toEqual([{ id: 1, name: 'Jane', verified: false, jsonb: null, createdAt: users[0]!.createdAt }]);
expect(users).toEqual([{
id: 1,
bestTexts: [],
name: 'Jane',
verified: false,
jsonb: null,
createdAt: users[0]!.createdAt,
}]);
});

test('update with returning partial', async () => {
Expand All @@ -195,7 +211,14 @@ test('delete with returning all fields', async () => {

expect(users[0]!.createdAt).toBeInstanceOf(Date);
// t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 100);
expect(users).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: users[0]!.createdAt }]);
expect(users).toEqual([{
bestTexts: [],
id: 1,
name: 'John',
verified: false,
jsonb: null,
createdAt: users[0]!.createdAt,
}]);
});

test('delete with returning partial', async () => {
Expand All @@ -211,13 +234,20 @@ test('delete with returning partial', async () => {
test('insert + select', async () => {
await db.insert(usersTable).values({ name: 'John' });
const result = await db.select().from(usersTable);
expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]);
expect(result).toEqual([{
bestTexts: [],
id: 1,
name: 'John',
verified: false,
jsonb: null,
createdAt: result[0]!.createdAt,
}]);

await db.insert(usersTable).values({ name: 'Jane' });
const result2 = await db.select().from(usersTable);
expect(result2).toEqual([
{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result2[0]!.createdAt },
{ id: 2, name: 'Jane', verified: false, jsonb: null, createdAt: result2[1]!.createdAt },
{ bestTexts: [], id: 1, name: 'John', verified: false, jsonb: null, createdAt: result2[0]!.createdAt },
{ bestTexts: [], id: 2, name: 'Jane', verified: false, jsonb: null, createdAt: result2[1]!.createdAt },
]);
});

Expand All @@ -236,7 +266,14 @@ test('insert with overridden default values', async () => {
await db.insert(usersTable).values({ name: 'John', verified: true });
const result = await db.select().from(usersTable);

expect(result).toEqual([{ id: 1, name: 'John', verified: true, jsonb: null, createdAt: result[0]!.createdAt }]);
expect(result).toEqual([{
bestTexts: [],
id: 1,
name: 'John',
verified: true,
jsonb: null,
createdAt: result[0]!.createdAt,
}]);
});

test('insert many', async () => {
Expand Down Expand Up @@ -385,12 +422,14 @@ test('full join with alias', async () => {
expect(result).toEqual([{
users: {
id: 10,
bestTexts: [],
name: 'Ivan',
verified: false,
jsonb: null,
createdAt: result[0]!.users.createdAt,
},
customer: {
bestTexts: [],
id: 11,
name: 'Hans',
verified: false,
Expand Down Expand Up @@ -627,7 +666,7 @@ test('build query insert with onConflict do update', async () => {

expect(query).toEqual({
sql:
'insert into "users" ("id", "name", "verified", "jsonb", "created_at") values (default, :1, default, :2, default) on conflict ("id") do update set "name" = :3',
'insert into "users" ("id", "name", "verified", "jsonb", "best_texts", "created_at") values (default, :1, default, :2, default, default) on conflict ("id") do update set "name" = :3',
params: ['John', '["foo","bar"]', 'John1'],
// typings: ['none', 'json', 'none']
});
Expand All @@ -641,7 +680,7 @@ test('build query insert with onConflict do update / multiple columns', async ()

expect(query).toEqual({
sql:
'insert into "users" ("id", "name", "verified", "jsonb", "created_at") values (default, :1, default, :2, default) on conflict ("id","name") do update set "name" = :3',
'insert into "users" ("id", "name", "verified", "jsonb", "best_texts", "created_at") values (default, :1, default, :2, default, default) on conflict ("id","name") do update set "name" = :3',
params: ['John', '["foo","bar"]', 'John1'],
// typings: ['none', 'json', 'none']
});
Expand All @@ -655,7 +694,7 @@ test('build query insert with onConflict do nothing', async () => {

expect(query).toEqual({
sql:
'insert into "users" ("id", "name", "verified", "jsonb", "created_at") values (default, :1, default, :2, default) on conflict do nothing',
'insert into "users" ("id", "name", "verified", "jsonb", "best_texts", "created_at") values (default, :1, default, :2, default, default) on conflict do nothing',
params: ['John', '["foo","bar"]'],
// typings: ['none', 'json']
});
Expand All @@ -669,7 +708,7 @@ test('build query insert with onConflict do nothing + target', async () => {

expect(query).toEqual({
sql:
'insert into "users" ("id", "name", "verified", "jsonb", "created_at") values (default, :1, default, :2, default) on conflict ("id") do nothing',
'insert into "users" ("id", "name", "verified", "jsonb", "best_texts", "created_at") values (default, :1, default, :2, default, default) on conflict ("id") do nothing',
params: ['John', '["foo","bar"]'],
// typings: ['none', 'json']
});
Expand Down Expand Up @@ -857,6 +896,69 @@ test('select from raw sql with mapped values', async () => {
]);
});

test('insert with array values works', async () => {
const bestTexts = ['text1', 'text2', 'text3'];
const [insertResult] = await db.insert(usersTable).values({
name: 'John',
bestTexts,
}).returning();

expect(insertResult?.bestTexts).toEqual(bestTexts);
});

test('update with array values works', async () => {
const [newUser] = await db.insert(usersTable).values({ name: 'John' }).returning();

const bestTexts = ['text4', 'text5', 'text6'];
const [insertResult] = await db.update(usersTable).set({
bestTexts,
}).where(eq(usersTable.id, newUser!.id)).returning();

expect(insertResult?.bestTexts).toEqual(bestTexts);
});

test('insert with array values works', async () => {
const bestTexts = ['text1', 'text2', 'text3'];
const [insertResult] = await db.insert(usersTable).values({
name: 'John',
bestTexts,
}).returning();

expect(insertResult?.bestTexts).toEqual(bestTexts);
});

test('update with array values works', async () => {
const [newUser] = await db.insert(usersTable).values({ name: 'John' }).returning();

const bestTexts = ['text4', 'text5', 'text6'];
const [insertResult] = await db.update(usersTable).set({
bestTexts,
}).where(eq(usersTable.id, newUser!.id)).returning();

expect(insertResult?.bestTexts).toEqual(bestTexts);
});

test('insert with array values works', async () => {
const bestTexts = ['text1', 'text2', 'text3'];
const [insertResult] = await db.insert(usersTable).values({
name: 'John',
bestTexts,
}).returning();

expect(insertResult?.bestTexts).toEqual(bestTexts);
});

test('update with array values works', async () => {
const [newUser] = await db.insert(usersTable).values({ name: 'John' }).returning();

const bestTexts = ['text4', 'text5', 'text6'];
const [insertResult] = await db.update(usersTable).set({
bestTexts,
}).where(eq(usersTable.id, newUser!.id)).returning();

expect(insertResult?.bestTexts).toEqual(bestTexts);
});

test('all date and time columns', async () => {
const table = pgTable('all_columns', {
id: serial('id').primaryKey(),
Expand Down