chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:10:05 +08:00
commit d37d8d293b
1388 changed files with 484182 additions and 0 deletions
@@ -0,0 +1,379 @@
import { eq, getTableName, is, sql, Table } from 'drizzle-orm';
import type { MutationOption } from 'drizzle-orm/cache/core';
import { Cache } from 'drizzle-orm/cache/core';
import type { CacheConfig } from 'drizzle-orm/cache/core/types';
import type { MySqlDatabase } from 'drizzle-orm/mysql-core';
import { alias, boolean, int, json, mysqlTable, serial, text, timestamp } from 'drizzle-orm/mysql-core';
import Keyv from 'keyv';
import { beforeEach, describe, expect, test, vi } from 'vitest';
// eslint-disable-next-line drizzle-internal/require-entity-kind
export class TestGlobalCache extends Cache {
private globalTtl: number = 1000;
private usedTablesPerKey: Record<string, string[]> = {};
constructor(private kv: Keyv = new Keyv()) {
super();
}
override strategy(): 'explicit' | 'all' {
return 'all';
}
override async get(key: string, _tables: string[], _isTag: boolean): Promise<any[] | undefined> {
const res = await this.kv.get(key) ?? undefined;
return res;
}
override async put(
key: string,
response: any,
tables: string[],
isTag: boolean,
config?: CacheConfig,
): Promise<void> {
await this.kv.set(key, response, config ? config.ex : this.globalTtl);
for (const table of tables) {
const keys = this.usedTablesPerKey[table];
if (keys === undefined) {
this.usedTablesPerKey[table] = [key];
} else {
keys.push(key);
}
}
}
override async onMutate(params: MutationOption): Promise<void> {
const tagsArray = params.tags ? Array.isArray(params.tags) ? params.tags : [params.tags] : [];
const tablesArray = params.tables ? Array.isArray(params.tables) ? params.tables : [params.tables] : [];
const keysToDelete = new Set<string>();
for (const table of tablesArray) {
const tableName = is(table, Table) ? getTableName(table) : table as string;
const keys = this.usedTablesPerKey[tableName] ?? [];
for (const key of keys) keysToDelete.add(key);
}
if (keysToDelete.size > 0 || tagsArray.length > 0) {
for (const tag of tagsArray) {
await this.kv.delete(tag);
}
for (const key of keysToDelete) {
await this.kv.delete(key);
for (const table of tablesArray) {
const tableName = is(table, Table) ? getTableName(table) : table as string;
this.usedTablesPerKey[tableName] = [];
}
}
}
}
}
// eslint-disable-next-line drizzle-internal/require-entity-kind
export class TestCache extends TestGlobalCache {
override strategy(): 'explicit' | 'all' {
return 'explicit';
}
}
declare module 'vitest' {
interface TestContext {
cachedMySQL: {
db: MySqlDatabase<any, any>;
dbGlobalCached: MySqlDatabase<any, any>;
};
}
}
const usersTable = mysqlTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
verified: boolean('verified').notNull().default(false),
jsonb: json('jsonb').$type<string[]>(),
createdAt: timestamp('created_at', { fsp: 2 }).notNull().defaultNow(),
});
const postsTable = mysqlTable('posts', {
id: serial().primaryKey(),
description: text().notNull(),
userId: int('city_id').references(() => usersTable.id),
});
export function tests() {
describe('common_cache', () => {
beforeEach(async (ctx) => {
const { db, dbGlobalCached } = ctx.cachedMySQL;
await db.execute(sql`drop table if exists users`);
await db.execute(sql`drop table if exists posts`);
await db.$cache?.invalidate({ tables: 'users' });
await dbGlobalCached.$cache?.invalidate({ tables: 'users' });
// public users
await db.execute(
sql`
create table users (
id serial primary key,
name text not null,
verified boolean not null default false,
jsonb json,
created_at timestamp not null default now()
)
`,
);
await db.execute(
sql`
create table posts (
id serial primary key,
description text not null,
user_id int
)
`,
);
});
test('test force invalidate', async (ctx) => {
const { db } = ctx.cachedMySQL;
const spyInvalidate = vi.spyOn(db.$cache, 'invalidate');
await db.$cache?.invalidate({ tables: 'users' });
expect(spyInvalidate).toHaveBeenCalledTimes(1);
});
test('default global config - no cache should be hit', async (ctx) => {
const { db } = ctx.cachedMySQL;
// @ts-expect-error
const spyPut = vi.spyOn(db.$cache, 'put');
// @ts-expect-error
const spyGet = vi.spyOn(db.$cache, 'get');
// @ts-expect-error
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
await db.select().from(usersTable);
expect(spyPut).toHaveBeenCalledTimes(0);
expect(spyGet).toHaveBeenCalledTimes(0);
expect(spyInvalidate).toHaveBeenCalledTimes(0);
});
test('default global config + enable cache on select: get, put', async (ctx) => {
const { db } = ctx.cachedMySQL;
// @ts-expect-error
const spyPut = vi.spyOn(db.$cache, 'put');
// @ts-expect-error
const spyGet = vi.spyOn(db.$cache, 'get');
// @ts-expect-error
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
await db.select().from(usersTable).$withCache();
expect(spyPut).toHaveBeenCalledTimes(1);
expect(spyGet).toHaveBeenCalledTimes(1);
expect(spyInvalidate).toHaveBeenCalledTimes(0);
});
test('default global config + enable cache on select + write: get, put, onMutate', async (ctx) => {
const { db } = ctx.cachedMySQL;
// @ts-expect-error
const spyPut = vi.spyOn(db.$cache, 'put');
// @ts-expect-error
const spyGet = vi.spyOn(db.$cache, 'get');
// @ts-expect-error
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
await db.select().from(usersTable).$withCache({ config: { ex: 1 } });
expect(spyPut).toHaveBeenCalledTimes(1);
expect(spyGet).toHaveBeenCalledTimes(1);
expect(spyInvalidate).toHaveBeenCalledTimes(0);
spyPut.mockClear();
spyGet.mockClear();
spyInvalidate.mockClear();
await db.insert(usersTable).values({ name: 'John' });
expect(spyPut).toHaveBeenCalledTimes(0);
expect(spyGet).toHaveBeenCalledTimes(0);
expect(spyInvalidate).toHaveBeenCalledTimes(1);
});
test('default global config + enable cache on select + disable invalidate: get, put', async (ctx) => {
const { db } = ctx.cachedMySQL;
// @ts-expect-error
const spyPut = vi.spyOn(db.$cache, 'put');
// @ts-expect-error
const spyGet = vi.spyOn(db.$cache, 'get');
// @ts-expect-error
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
await db.select().from(usersTable).$withCache({ tag: 'custom', autoInvalidate: false, config: { ex: 1 } });
expect(spyPut).toHaveBeenCalledTimes(1);
expect(spyGet).toHaveBeenCalledTimes(1);
expect(spyInvalidate).toHaveBeenCalledTimes(0);
await db.insert(usersTable).values({ name: 'John' });
// invalidate force
await db.$cache?.invalidate({ tags: ['custom'] });
});
test('global: true + disable cache', async (ctx) => {
const { dbGlobalCached: db } = ctx.cachedMySQL;
// @ts-expect-error
const spyPut = vi.spyOn(db.$cache, 'put');
// @ts-expect-error
const spyGet = vi.spyOn(db.$cache, 'get');
// @ts-expect-error
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
await db.select().from(usersTable).$withCache(false);
expect(spyPut).toHaveBeenCalledTimes(0);
expect(spyGet).toHaveBeenCalledTimes(0);
expect(spyInvalidate).toHaveBeenCalledTimes(0);
});
test('global: true - cache should be hit', async (ctx) => {
const { dbGlobalCached: db } = ctx.cachedMySQL;
// @ts-expect-error
const spyPut = vi.spyOn(db.$cache, 'put');
// @ts-expect-error
const spyGet = vi.spyOn(db.$cache, 'get');
// @ts-expect-error
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
await db.select().from(usersTable);
expect(spyPut).toHaveBeenCalledTimes(1);
expect(spyGet).toHaveBeenCalledTimes(1);
expect(spyInvalidate).toHaveBeenCalledTimes(0);
});
test('global: true - cache: false on select - no cache hit', async (ctx) => {
const { dbGlobalCached: db } = ctx.cachedMySQL;
// @ts-expect-error
const spyPut = vi.spyOn(db.$cache, 'put');
// @ts-expect-error
const spyGet = vi.spyOn(db.$cache, 'get');
// @ts-expect-error
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
await db.select().from(usersTable).$withCache(false);
expect(spyPut).toHaveBeenCalledTimes(0);
expect(spyGet).toHaveBeenCalledTimes(0);
expect(spyInvalidate).toHaveBeenCalledTimes(0);
});
test('global: true - disable invalidate - cache hit + no invalidate', async (ctx) => {
const { dbGlobalCached: db } = ctx.cachedMySQL;
// @ts-expect-error
const spyPut = vi.spyOn(db.$cache, 'put');
// @ts-expect-error
const spyGet = vi.spyOn(db.$cache, 'get');
// @ts-expect-error
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
await db.select().from(usersTable).$withCache({ autoInvalidate: false });
expect(spyPut).toHaveBeenCalledTimes(1);
expect(spyGet).toHaveBeenCalledTimes(1);
expect(spyInvalidate).toHaveBeenCalledTimes(0);
spyPut.mockClear();
spyGet.mockClear();
spyInvalidate.mockClear();
await db.insert(usersTable).values({ name: 'John' });
expect(spyPut).toHaveBeenCalledTimes(0);
expect(spyGet).toHaveBeenCalledTimes(0);
expect(spyInvalidate).toHaveBeenCalledTimes(1);
});
test('global: true - with custom tag', async (ctx) => {
const { dbGlobalCached: db } = ctx.cachedMySQL;
// @ts-expect-error
const spyPut = vi.spyOn(db.$cache, 'put');
// @ts-expect-error
const spyGet = vi.spyOn(db.$cache, 'get');
// @ts-expect-error
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
await db.select().from(usersTable).$withCache({ tag: 'custom', autoInvalidate: false });
expect(spyPut).toHaveBeenCalledTimes(1);
expect(spyGet).toHaveBeenCalledTimes(1);
expect(spyInvalidate).toHaveBeenCalledTimes(0);
await db.insert(usersTable).values({ name: 'John' });
// invalidate force
await db.$cache?.invalidate({ tags: ['custom'] });
});
// check select used tables
test('check simple select used tables', (ctx) => {
const { db } = ctx.cachedMySQL;
// @ts-expect-error
expect(db.select().from(usersTable).getUsedTables()).toStrictEqual(['users']);
// @ts-expect-error
expect(db.select().from(sql`${usersTable}`).getUsedTables()).toStrictEqual(['users']);
});
// check select+join used tables
test('select+join', (ctx) => {
const { db } = ctx.cachedMySQL;
// @ts-expect-error
expect(db.select().from(usersTable).leftJoin(postsTable, eq(usersTable.id, postsTable.userId)).getUsedTables())
.toStrictEqual(['users', 'posts']);
expect(
// @ts-expect-error
db.select().from(sql`${usersTable}`).leftJoin(postsTable, eq(usersTable.id, postsTable.userId)).getUsedTables(),
).toStrictEqual(['users', 'posts']);
});
// check select+2join used tables
test('select+2joins', (ctx) => {
const { db } = ctx.cachedMySQL;
expect(
db.select().from(usersTable).leftJoin(
postsTable,
eq(usersTable.id, postsTable.userId),
).leftJoin(
alias(postsTable, 'post2'),
eq(usersTable.id, postsTable.userId),
)
// @ts-expect-error
.getUsedTables(),
)
.toStrictEqual(['users', 'posts']);
expect(
db.select().from(sql`${usersTable}`).leftJoin(postsTable, eq(usersTable.id, postsTable.userId)).leftJoin(
alias(postsTable, 'post2'),
eq(usersTable.id, postsTable.userId),
// @ts-expect-error
).getUsedTables(),
).toStrictEqual(['users', 'posts']);
});
// select subquery used tables
test('select+join', (ctx) => {
const { db } = ctx.cachedMySQL;
const sq = db.select().from(usersTable).where(eq(usersTable.id, 42)).as('sq');
db.select().from(sq);
// @ts-expect-error
expect(db.select().from(sq).getUsedTables()).toStrictEqual(['users']);
});
});
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,817 @@
import retry from 'async-retry';
import type Docker from 'dockerode';
import { asc, eq, Name, sql } from 'drizzle-orm';
import {
alias,
binary,
customType,
date,
datetime,
mysqlEnum,
mysqlTable,
mysqlTableCreator,
serial,
text,
time,
varchar,
year,
} from 'drizzle-orm/mysql-core';
import type { MySql2Database } from 'drizzle-orm/mysql2';
import { drizzle } from 'drizzle-orm/mysql2';
import { migrate } from 'drizzle-orm/mysql2/migrator';
import * as mysql from 'mysql2/promise';
import { v4 as uuid } from 'uuid';
import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest';
import { toLocalDate } from '~/utils';
import { createDockerDB } from './mysql-common';
const ENABLE_LOGGING = false;
let db: MySql2Database;
let client: mysql.Connection;
let container: Docker.Container | undefined;
beforeAll(async () => {
let connectionString;
if (process.env['MYSQL_CONNECTION_STRING']) {
connectionString = process.env['MYSQL_CONNECTION_STRING'];
} else {
const { connectionString: conStr, container: contrainerObj } = await createDockerDB();
connectionString = conStr;
container = contrainerObj;
}
client = await retry(async () => {
client = await mysql.createConnection(connectionString);
await client.connect();
return client;
}, {
retries: 20,
factor: 1,
minTimeout: 250,
maxTimeout: 250,
randomize: false,
onRetry() {
client?.end();
},
});
db = drizzle(client, { logger: ENABLE_LOGGING });
});
afterAll(async () => {
await client?.end();
await container?.stop().catch(console.error);
});
beforeEach((ctx) => {
ctx.mysql = {
db,
};
});
const customSerial = customType<{ data: number; notNull: true; default: true }>({
dataType() {
return 'serial';
},
});
const customText = customType<{ data: string }>({
dataType() {
return 'text';
},
});
const customBoolean = customType<{ data: boolean }>({
dataType() {
return 'boolean';
},
fromDriver(value) {
if (typeof value === 'boolean') {
return value;
}
return value === 1;
},
});
const customJson = <TData>(name: string) =>
customType<{ data: TData; driverData: string }>({
dataType() {
return 'json';
},
toDriver(value: TData): string {
return JSON.stringify(value);
},
})(name);
const customTimestamp = customType<
{ data: Date; driverData: string; config: { fsp: number } }
>({
dataType(config) {
const precision = config?.fsp === undefined ? '' : ` (${config.fsp})`;
return `timestamp${precision}`;
},
fromDriver(value: string): Date {
return new Date(value);
},
});
const customBinary = customType<{ data: string; driverData: Buffer; config: { length: number } }>({
dataType(config) {
return config?.length === undefined
? `binary`
: `binary(${config.length})`;
},
toDriver(value) {
return sql`UNHEX(${value})`;
},
fromDriver(value) {
return value.toString('hex');
},
});
const usersTable = mysqlTable('userstest', {
id: customSerial('id').primaryKey(),
name: customText('name').notNull(),
verified: customBoolean('verified').notNull().default(false),
jsonb: customJson<string[]>('jsonb'),
createdAt: customTimestamp('created_at', { fsp: 2 }).notNull().default(sql`now()`),
});
const datesTable = mysqlTable('datestable', {
date: date('date'),
dateAsString: date('date_as_string', { mode: 'string' }),
time: time('time', { fsp: 1 }),
datetime: datetime('datetime', { fsp: 2 }),
datetimeAsString: datetime('datetime_as_string', { fsp: 2, mode: 'string' }),
year: year('year'),
});
export const testTable = mysqlTable('test_table', {
id: customBinary('id', { length: 16 }).primaryKey(),
sqlId: binary('sql_id', { length: 16 }),
rawId: varchar('raw_id', { length: 64 }),
});
const usersMigratorTable = mysqlTable('users12', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull(),
});
beforeEach(async () => {
await db.execute(sql`drop table if exists \`userstest\``);
await db.execute(sql`drop table if exists \`datestable\``);
await db.execute(sql`drop table if exists \`test_table\``);
// await ctx.db.execute(sql`create schema public`);
await db.execute(
sql`
create table \`userstest\` (
\`id\` serial primary key,
\`name\` text not null,
\`verified\` boolean not null default false,
\`jsonb\` json,
\`created_at\` timestamp not null default now()
)
`,
);
await db.execute(
sql`
create table \`datestable\` (
\`date\` date,
\`date_as_string\` date,
\`time\` time,
\`datetime\` datetime,
\`datetime_as_string\` datetime,
\`year\` year
)
`,
);
await db.execute(
sql`
create table \`test_table\` (
\`id\` binary(16) primary key,
\`sql_id\` binary(16),
\`raw_id\` varchar(64)
)
`,
);
});
test('select all fields', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values({ name: 'John' });
const result = await db.select().from(usersTable);
expect(result[0]!.createdAt).toBeInstanceOf(Date);
// not timezone based timestamp, thats why it should not work here
// t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 2000);
expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]);
});
test('select sql', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values({ name: 'John' });
const users = await db.select({
name: sql`upper(${usersTable.name})`,
}).from(usersTable);
expect(users).toEqual([{ name: 'JOHN' }]);
});
test('select typed sql', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values({ name: 'John' });
const users = await db.select({
name: sql<string>`upper(${usersTable.name})`,
}).from(usersTable);
expect(users).toEqual([{ name: 'JOHN' }]);
});
test('insert returning sql', async (ctx) => {
const { db } = ctx.mysql;
const [result, _] = await db.insert(usersTable).values({ name: 'John' });
expect(result.insertId).toBe(1);
});
test('delete returning sql', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values({ name: 'John' });
const users = await db.delete(usersTable).where(eq(usersTable.name, 'John'));
expect(users[0].affectedRows).toBe(1);
});
test('update returning sql', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values({ name: 'John' });
const users = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John'));
expect(users[0].changedRows).toBe(1);
});
test('update with returning all fields', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values({ name: 'John' });
const updatedUsers = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John'));
const users = await db.select().from(usersTable).where(eq(usersTable.id, 1));
expect(updatedUsers[0].changedRows).toBe(1);
expect(users[0]!.createdAt).toBeInstanceOf(Date);
// not timezone based timestamp, thats why it should not work here
// t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 2000);
expect(users).toEqual([{ id: 1, name: 'Jane', verified: false, jsonb: null, createdAt: users[0]!.createdAt }]);
});
test('update with returning partial', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values({ name: 'John' });
const updatedUsers = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John'));
const users = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where(
eq(usersTable.id, 1),
);
expect(updatedUsers[0].changedRows).toBe(1);
expect(users).toEqual([{ id: 1, name: 'Jane' }]);
});
test('delete with returning all fields', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values({ name: 'John' });
const deletedUser = await db.delete(usersTable).where(eq(usersTable.name, 'John'));
expect(deletedUser[0].affectedRows).toBe(1);
});
test('delete with returning partial', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values({ name: 'John' });
const deletedUser = await db.delete(usersTable).where(eq(usersTable.name, 'John'));
expect(deletedUser[0].affectedRows).toBe(1);
});
test('insert + select', async (ctx) => {
const { db } = ctx.mysql;
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 }]);
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 },
]);
});
test('json insert', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values({ name: 'John', jsonb: ['foo', 'bar'] });
const result = await db.select({
id: usersTable.id,
name: usersTable.name,
jsonb: usersTable.jsonb,
}).from(usersTable);
expect(result).toEqual([{ id: 1, name: 'John', jsonb: ['foo', 'bar'] }]);
});
test('insert with overridden default values', async (ctx) => {
const { db } = ctx.mysql;
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 }]);
});
test('insert many', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values([
{ name: 'John' },
{ name: 'Bruce', jsonb: ['foo', 'bar'] },
{ name: 'Jane' },
{ name: 'Austin', verified: true },
]);
const result = await db.select({
id: usersTable.id,
name: usersTable.name,
jsonb: usersTable.jsonb,
verified: usersTable.verified,
}).from(usersTable);
expect(result).toEqual([
{ id: 1, name: 'John', jsonb: null, verified: false },
{ id: 2, name: 'Bruce', jsonb: ['foo', 'bar'], verified: false },
{ id: 3, name: 'Jane', jsonb: null, verified: false },
{ id: 4, name: 'Austin', jsonb: null, verified: true },
]);
});
test('insert many with returning', async (ctx) => {
const { db } = ctx.mysql;
const result = await db.insert(usersTable).values([
{ name: 'John' },
{ name: 'Bruce', jsonb: ['foo', 'bar'] },
{ name: 'Jane' },
{ name: 'Austin', verified: true },
]);
expect(result[0].affectedRows).toBe(4);
});
test('select with group by as field', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]);
const result = await db.select({ name: usersTable.name }).from(usersTable)
.groupBy(usersTable.name);
expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }]);
});
test('select with group by as sql', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]);
const result = await db.select({ name: usersTable.name }).from(usersTable)
.groupBy(sql`${usersTable.name}`);
expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }]);
});
test('select with group by as sql + column', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]);
const result = await db.select({ name: usersTable.name }).from(usersTable)
.groupBy(sql`${usersTable.name}`, usersTable.id);
expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]);
});
test('select with group by as column + sql', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]);
const result = await db.select({ name: usersTable.name }).from(usersTable)
.groupBy(usersTable.id, sql`${usersTable.name}`);
expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]);
});
test('select with group by complex query', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]);
const result = await db.select({ name: usersTable.name }).from(usersTable)
.groupBy(usersTable.id, sql`${usersTable.name}`)
.orderBy(asc(usersTable.name))
.limit(1);
expect(result).toEqual([{ name: 'Jane' }]);
});
test('build query', async (ctx) => {
const { db } = ctx.mysql;
const query = db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable)
.groupBy(usersTable.id, usersTable.name)
.toSQL();
expect(query).toEqual({
sql: `select \`id\`, \`name\` from \`userstest\` group by \`userstest\`.\`id\`, \`userstest\`.\`name\``,
params: [],
});
});
test('build query insert with onDuplicate', async (ctx) => {
const { db } = ctx.mysql;
const query = db.insert(usersTable)
.values({ name: 'John', jsonb: ['foo', 'bar'] })
.onDuplicateKeyUpdate({ set: { name: 'John1' } })
.toSQL();
expect(query).toEqual({
sql:
'insert into `userstest` (`id`, `name`, `verified`, `jsonb`, `created_at`) values (default, ?, default, ?, default) on duplicate key update `name` = ?',
params: ['John', '["foo","bar"]', 'John1'],
});
});
test('insert with onDuplicate', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable)
.values({ name: 'John' });
await db.insert(usersTable)
.values({ id: 1, name: 'John' })
.onDuplicateKeyUpdate({ set: { name: 'John1' } });
const res = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where(
eq(usersTable.id, 1),
);
expect(res).toEqual([{ id: 1, name: 'John1' }]);
});
test('insert conflict', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable)
.values({ name: 'John' });
await expect((async () => {
db.insert(usersTable).values({ id: 1, name: 'John1' });
})()).resolves.not.toThrowError();
});
test('insert conflict with ignore', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable)
.values({ name: 'John' });
await db.insert(usersTable)
.ignore()
.values({ id: 1, name: 'John1' });
const res = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where(
eq(usersTable.id, 1),
);
expect(res).toEqual([{ id: 1, name: 'John' }]);
});
test('insert sql', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values({ name: sql`${'John'}` });
const result = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable);
expect(result).toEqual([{ id: 1, name: 'John' }]);
});
test('partial join with alias', async (ctx) => {
const { db } = ctx.mysql;
const customerAlias = alias(usersTable, 'customer');
await db.insert(usersTable).values([{ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }]);
const result = await db
.select({
user: {
id: usersTable.id,
name: usersTable.name,
},
customer: {
id: customerAlias.id,
name: customerAlias.name,
},
}).from(usersTable)
.leftJoin(customerAlias, eq(customerAlias.id, 11))
.where(eq(usersTable.id, 10));
expect(result).toEqual([{
user: { id: 10, name: 'Ivan' },
customer: { id: 11, name: 'Hans' },
}]);
});
test('full join with alias', async (ctx) => {
const { db } = ctx.mysql;
const mysqlTable = mysqlTableCreator((name) => `prefixed_${name}`);
const users = mysqlTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
});
await db.execute(sql`drop table if exists ${users}`);
await db.execute(sql`create table ${users} (id serial primary key, name text not null)`);
const customers = alias(users, 'customer');
await db.insert(users).values([{ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }]);
const result = await db
.select().from(users)
.leftJoin(customers, eq(customers.id, 11))
.where(eq(users.id, 10));
expect(result).toEqual([{
users: {
id: 10,
name: 'Ivan',
},
customer: {
id: 11,
name: 'Hans',
},
}]);
await db.execute(sql`drop table ${users}`);
});
test('select from alias', async (ctx) => {
const { db } = ctx.mysql;
const mysqlTable = mysqlTableCreator((name) => `prefixed_${name}`);
const users = mysqlTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
});
await db.execute(sql`drop table if exists ${users}`);
await db.execute(sql`create table ${users} (id serial primary key, name text not null)`);
const user = alias(users, 'user');
const customers = alias(users, 'customer');
await db.insert(users).values([{ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }]);
const result = await db
.select()
.from(user)
.leftJoin(customers, eq(customers.id, 11))
.where(eq(user.id, 10));
expect(result).toEqual([{
user: {
id: 10,
name: 'Ivan',
},
customer: {
id: 11,
name: 'Hans',
},
}]);
await db.execute(sql`drop table ${users}`);
});
test('insert with spaces', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values({ name: sql`'Jo h n'` });
const result = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable);
expect(result).toEqual([{ id: 1, name: 'Jo h n' }]);
});
test('prepared statement', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values({ name: 'John' });
const statement = db.select({
id: usersTable.id,
name: usersTable.name,
}).from(usersTable)
.prepare();
const result = await statement.execute();
expect(result).toEqual([{ id: 1, name: 'John' }]);
});
test('prepared statement reuse', async (ctx) => {
const { db } = ctx.mysql;
const stmt = db.insert(usersTable).values({
verified: true,
name: sql.placeholder('name'),
}).prepare();
for (let i = 0; i < 10; i++) {
await stmt.execute({ name: `John ${i}` });
}
const result = await db.select({
id: usersTable.id,
name: usersTable.name,
verified: usersTable.verified,
}).from(usersTable);
expect(result).toEqual([
{ id: 1, name: 'John 0', verified: true },
{ id: 2, name: 'John 1', verified: true },
{ id: 3, name: 'John 2', verified: true },
{ id: 4, name: 'John 3', verified: true },
{ id: 5, name: 'John 4', verified: true },
{ id: 6, name: 'John 5', verified: true },
{ id: 7, name: 'John 6', verified: true },
{ id: 8, name: 'John 7', verified: true },
{ id: 9, name: 'John 8', verified: true },
{ id: 10, name: 'John 9', verified: true },
]);
});
test('prepared statement with placeholder in .where', async (ctx) => {
const { db } = ctx.mysql;
await db.insert(usersTable).values({ name: 'John' });
const stmt = db.select({
id: usersTable.id,
name: usersTable.name,
}).from(usersTable)
.where(eq(usersTable.id, sql.placeholder('id')))
.prepare();
const result = await stmt.execute({ id: 1 });
expect(result).toEqual([{ id: 1, name: 'John' }]);
});
test('migrator', async (ctx) => {
const { db } = ctx.mysql;
await db.execute(sql`drop table if exists cities_migration`);
await db.execute(sql`drop table if exists users_migration`);
await db.execute(sql`drop table if exists users12`);
await db.execute(sql`drop table if exists __drizzle_migrations`);
await migrate(db, { migrationsFolder: './drizzle2/mysql' });
await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' });
const result = await db.select().from(usersMigratorTable);
expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]);
await db.execute(sql`drop table cities_migration`);
await db.execute(sql`drop table users_migration`);
await db.execute(sql`drop table users12`);
await db.execute(sql`drop table __drizzle_migrations`);
});
test('insert via db.execute + select via db.execute', async (ctx) => {
const { db } = ctx.mysql;
await db.execute(sql`insert into ${usersTable} (${new Name(usersTable.name.name)}) values (${'John'})`);
const result = await db.execute<{ id: number; name: string }>(sql`select id, name from ${usersTable}`);
expect(result[0]).toEqual([{ id: 1, name: 'John' }]);
});
test('insert via db.execute w/ query builder', async (ctx) => {
const { db } = ctx.mysql;
const inserted = await db.execute(
db.insert(usersTable).values({ name: 'John' }),
);
expect(inserted[0].affectedRows).toBe(1);
});
test('insert + select all possible dates', async (ctx) => {
const { db } = ctx.mysql;
const date = new Date('2022-11-11');
await db.insert(datesTable).values({
date: date,
dateAsString: '2022-11-11',
time: '12:12:12',
datetime: date,
year: 22,
datetimeAsString: '2022-11-11 12:12:12',
});
const res = await db.select().from(datesTable);
expect(res[0]?.date).toBeInstanceOf(Date);
expect(res[0]?.datetime).toBeInstanceOf(Date);
expect(res[0]?.dateAsString).toBeTypeOf('string');
expect(res[0]?.datetimeAsString).toBeTypeOf('string');
expect(res).toEqual([{
date: toLocalDate(new Date('2022-11-11')),
dateAsString: '2022-11-11',
time: '12:12:12',
datetime: new Date('2022-11-11'),
year: 2022,
datetimeAsString: '2022-11-11 12:12:12',
}]);
});
const tableWithEnums = mysqlTable('enums_test_case', {
id: serial('id').primaryKey(),
enum1: mysqlEnum('enum1', ['a', 'b', 'c']).notNull(),
enum2: mysqlEnum('enum2', ['a', 'b', 'c']).default('a'),
enum3: mysqlEnum('enum3', ['a', 'b', 'c']).notNull().default('b'),
});
test('Mysql enum test case #1', async (ctx) => {
const { db } = ctx.mysql;
await db.execute(sql`drop table if exists \`enums_test_case\``);
await db.execute(sql`
create table \`enums_test_case\` (
\`id\` serial primary key,
\`enum1\` ENUM('a', 'b', 'c') not null,
\`enum2\` ENUM('a', 'b', 'c') default 'a',
\`enum3\` ENUM('a', 'b', 'c') not null default 'b'
)
`);
await db.insert(tableWithEnums).values([
{ id: 1, enum1: 'a', enum2: 'b', enum3: 'c' },
{ id: 2, enum1: 'a', enum3: 'c' },
{ id: 3, enum1: 'a' },
]);
const res = await db.select().from(tableWithEnums);
await db.execute(sql`drop table \`enums_test_case\``);
expect(res).toEqual([
{ id: 1, enum1: 'a', enum2: 'b', enum3: 'c' },
{ id: 2, enum1: 'a', enum2: 'a', enum3: 'c' },
{ id: 3, enum1: 'a', enum2: 'a', enum3: 'b' },
]);
});
test('custom binary', async (ctx) => {
const { db } = ctx.mysql;
const id = uuid().replace(/-/g, '');
await db.insert(testTable).values({
id,
sqlId: sql`UNHEX(${id})`,
rawId: id,
});
const res = await db.select().from(testTable);
expect(res).toEqual([{
id,
sqlId: Buffer.from(id, 'hex').toString(),
rawId: id,
}]);
});
@@ -0,0 +1,88 @@
import { Client } from '@planetscale/database';
import type { PlanetScaleDatabase } from 'drizzle-orm/planetscale-serverless';
import { drizzle } from 'drizzle-orm/planetscale-serverless';
import { beforeAll, beforeEach } from 'vitest';
import { skipTests } from '~/common';
import { tests } from './mysql-common';
import { TestCache, TestGlobalCache, tests as cacheTests } from './mysql-common-cache';
const ENABLE_LOGGING = false;
let db: PlanetScaleDatabase;
let dbGlobalCached: PlanetScaleDatabase;
let cachedDb: PlanetScaleDatabase;
beforeAll(async () => {
const client = new Client({ url: process.env['PLANETSCALE_CONNECTION_STRING']! });
db = drizzle(client, { logger: ENABLE_LOGGING });
cachedDb = drizzle(client, { logger: ENABLE_LOGGING, cache: new TestCache() });
dbGlobalCached = drizzle(client, { logger: ENABLE_LOGGING, cache: new TestGlobalCache() });
});
beforeEach((ctx) => {
ctx.mysql = {
db,
};
ctx.cachedMySQL = {
db: cachedDb,
dbGlobalCached,
};
});
skipTests([
'mySchema :: view',
'mySchema :: select from tables with same name from different schema using alias',
'mySchema :: prepared statement with placeholder in .where',
'mySchema :: insert with spaces',
'mySchema :: select with group by as column + sql',
'mySchema :: select with group by as field',
'mySchema :: insert many',
'mySchema :: insert with overridden default values',
'mySchema :: insert + select',
'mySchema :: delete with returning all fields',
'mySchema :: update with returning partial',
'mySchema :: delete returning sql',
'mySchema :: insert returning sql',
'mySchema :: select typed sql',
'mySchema :: select sql',
'mySchema :: select all fields',
'test $onUpdateFn and $onUpdate works updating',
'test $onUpdateFn and $onUpdate works as $default',
'set operations (mixed all) as function with subquery',
'set operations (mixed) from query builder',
'set operations (except all) as function',
'set operations (except all) from query builder',
'set operations (except) as function',
'set operations (except) from query builder',
'set operations (intersect all) as function',
'set operations (intersect all) from query builder',
'set operations (intersect) as function',
'set operations (intersect) from query builder',
'select iterator w/ prepared statement',
'select iterator',
'subquery with view',
'join on aliased sql from with clause',
'with ... delete',
'with ... update',
'with ... select',
// to redefine in this file
'utc config for datetime',
'transaction',
'transaction with options (set isolationLevel)',
'having',
'select count()',
'insert via db.execute w/ query builder',
'insert via db.execute + select via db.execute',
'insert many with returning',
'delete with returning partial',
'delete with returning all fields',
'update with returning partial',
'update with returning all fields',
'update returning sql',
'delete returning sql',
'insert returning sql',
]);
tests('planetscale');
cacheTests();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,139 @@
import retry from 'async-retry';
import type { MySqlRemoteDatabase } from 'drizzle-orm/mysql-proxy';
import { drizzle as proxyDrizzle } from 'drizzle-orm/mysql-proxy';
import * as mysql from 'mysql2/promise';
import { afterAll, beforeAll, beforeEach } from 'vitest';
import { skipTests } from '~/common';
import { createDockerDB, tests } from './mysql-common';
const ENABLE_LOGGING = false;
// eslint-disable-next-line drizzle-internal/require-entity-kind
class ServerSimulator {
constructor(private db: mysql.Connection) {}
async query(sql: string, params: any[], method: 'all' | 'execute') {
if (method === 'all') {
try {
const result = await this.db.query({
sql,
values: params,
rowsAsArray: true,
typeCast: function(field: any, next: any) {
if (field.type === 'TIMESTAMP' || field.type === 'DATETIME' || field.type === 'DATE') {
return field.string();
}
return next();
},
});
return { data: result[0] as any };
} catch (e: any) {
return { error: e };
}
} else if (method === 'execute') {
try {
const result = await this.db.query({
sql,
values: params,
typeCast: function(field: any, next: any) {
if (field.type === 'TIMESTAMP' || field.type === 'DATETIME' || field.type === 'DATE') {
return field.string();
}
return next();
},
});
return { data: result as any };
} catch (e: any) {
return { error: e };
}
} else {
return { error: 'Unknown method value' };
}
}
async migrations(queries: string[]) {
await this.db.query('START TRANSACTION');
try {
for (const query of queries) {
await this.db.query(query);
}
await this.db.query('COMMIT');
} catch (e) {
await this.db.query('ROLLBACK');
throw e;
}
return {};
}
}
let db: MySqlRemoteDatabase;
let client: mysql.Connection;
let serverSimulator: ServerSimulator;
beforeAll(async () => {
let connectionString;
if (process.env['MYSQL_CONNECTION_STRING']) {
connectionString = process.env['MYSQL_CONNECTION_STRING'];
} else {
const { connectionString: conStr } = await createDockerDB();
connectionString = conStr;
}
client = await retry(async () => {
client = await mysql.createConnection({
uri: connectionString,
supportBigNumbers: true,
});
await client.connect();
return client;
}, {
retries: 20,
factor: 1,
minTimeout: 250,
maxTimeout: 250,
randomize: false,
onRetry() {
client?.end();
},
});
serverSimulator = new ServerSimulator(client);
db = proxyDrizzle(async (sql, params, method) => {
try {
const response = await serverSimulator.query(sql, params, method);
if (response.error !== undefined) {
throw response.error;
}
return { rows: response.data };
} catch (e: any) {
console.error('Error from mysql proxy server:', e.message);
throw e;
}
}, { logger: ENABLE_LOGGING });
});
afterAll(async () => {
await client?.end();
});
beforeEach((ctx) => {
ctx.mysql = {
db,
};
});
skipTests([
'select iterator w/ prepared statement',
'select iterator',
'nested transaction rollback',
'nested transaction',
'transaction rollback',
'transaction',
'transaction with options (set isolationLevel)',
'migrator',
]);
tests();
@@ -0,0 +1,61 @@
import retry from 'async-retry';
import type { MySql2Database } from 'drizzle-orm/mysql2';
import { drizzle } from 'drizzle-orm/mysql2';
import * as mysql from 'mysql2/promise';
import { afterAll, beforeAll, beforeEach } from 'vitest';
import { createDockerDB, tests } from './mysql-common';
import { TestCache, TestGlobalCache, tests as cacheTests } from './mysql-common-cache';
const ENABLE_LOGGING = false;
let db: MySql2Database;
let dbGlobalCached: MySql2Database;
let cachedDb: MySql2Database;
let client: mysql.Connection;
beforeAll(async () => {
let connectionString;
if (process.env['MYSQL_CONNECTION_STRING']) {
connectionString = process.env['MYSQL_CONNECTION_STRING'];
} else {
const { connectionString: conStr } = await createDockerDB();
connectionString = conStr;
}
client = await retry(async () => {
client = await mysql.createConnection({
uri: connectionString!,
supportBigNumbers: true,
});
await client.connect();
return client;
}, {
retries: 20,
factor: 1,
minTimeout: 250,
maxTimeout: 250,
randomize: false,
onRetry() {
client?.end();
},
});
db = drizzle(client, { logger: ENABLE_LOGGING });
cachedDb = drizzle(client, { logger: ENABLE_LOGGING, cache: new TestCache() });
dbGlobalCached = drizzle(client, { logger: ENABLE_LOGGING, cache: new TestGlobalCache() });
});
afterAll(async () => {
await client?.end();
});
beforeEach((ctx) => {
ctx.mysql = {
db,
};
ctx.cachedMySQL = {
db: cachedDb,
dbGlobalCached,
};
});
cacheTests();
tests();
@@ -0,0 +1,76 @@
import 'dotenv/config';
import { connect } from '@tidbcloud/serverless';
import type { TiDBServerlessDatabase } from 'drizzle-orm/tidb-serverless';
import { drizzle } from 'drizzle-orm/tidb-serverless';
import { beforeAll, beforeEach } from 'vitest';
import { skipTests } from '~/common.ts';
import { tests } from './mysql-common.ts';
const ENABLE_LOGGING = false;
let db: TiDBServerlessDatabase;
beforeAll(async () => {
const connectionString = process.env['TIDB_CONNECTION_STRING'];
if (!connectionString) {
throw new Error('TIDB_CONNECTION_STRING is not set');
}
const client = connect({ url: connectionString });
db = drizzle(client!, { logger: ENABLE_LOGGING });
});
beforeEach((ctx) => {
ctx.mysql = {
db,
};
});
skipTests([
'mySchema :: select with group by as field',
'mySchema :: delete with returning all fields',
'mySchema :: update with returning partial',
'mySchema :: delete returning sql',
'mySchema :: insert returning sql',
'test $onUpdateFn and $onUpdate works updating',
'set operations (mixed all) as function with subquery',
'set operations (union) from query builder with subquery',
'join on aliased sql from with clause',
'join on aliased sql from select',
'select from raw sql with joins',
'select from raw sql',
'having',
'select count()',
'with ... select',
'insert via db.execute w/ query builder',
'insert via db.execute + select via db.execute',
'select with group by as sql',
'select with group by as field',
'insert many with returning',
'delete with returning partial',
'delete with returning all fields',
'update with returning partial',
'update with returning all fields',
'update returning sql',
'delete returning sql',
'insert returning sql',
// not supported
'set operations (except all) as function',
'set operations (except all) from query builder',
'set operations (intersect all) as function',
'set operations (intersect all) from query builder',
'set operations (union all) as function',
'tc config for datetime',
'select iterator w/ prepared statement',
'select iterator',
'transaction',
'transaction with options (set isolationLevel)',
'Insert all defaults in multiple rows',
'Insert all defaults in 1 row',
'$default with empty array',
'utc config for datetime',
]);
tests();