import 'dotenv/config'; import Docker from 'dockerode'; import { SQL, sql } from 'drizzle-orm'; import { bigint, char, check, decimal, double, float, int, mediumint, mysqlEnum, mysqlTable, mysqlView, serial, smallint, text, tinyint, varchar, } from 'drizzle-orm/mysql-core'; import * as fs from 'fs'; import getPort from 'get-port'; import { Connection, createConnection } from 'mysql2/promise'; import { introspectMySQLToFile } from 'tests/schemaDiffer'; import { v4 as uuid } from 'uuid'; import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest'; let client: Connection; let mysqlContainer: Docker.Container; async function createDockerDB(): Promise { const docker = new Docker(); const port = await getPort({ port: 3306 }); const image = 'mysql:8'; const pullStream = await docker.pull(image); await new Promise((resolve, reject) => // eslint-disable-next-line @typescript-eslint/no-unsafe-argument docker.modem.followProgress(pullStream, (err) => err ? reject(err) : resolve(err)) ); mysqlContainer = await docker.createContainer({ Image: image, Env: ['MYSQL_ROOT_PASSWORD=mysql', 'MYSQL_DATABASE=drizzle'], name: `drizzle-integration-tests-${uuid()}`, HostConfig: { AutoRemove: true, PortBindings: { '3306/tcp': [{ HostPort: `${port}` }], }, }, }); await mysqlContainer.start(); return `mysql://root:mysql@127.0.0.1:${port}/drizzle`; } beforeAll(async () => { const connectionString = process.env.MYSQL_CONNECTION_STRING ?? await createDockerDB(); const sleep = 1000; let timeLeft = 20000; let connected = false; let lastError: unknown | undefined; do { try { client = await createConnection(connectionString); await client.connect(); connected = true; break; } catch (e) { lastError = e; await new Promise((resolve) => setTimeout(resolve, sleep)); timeLeft -= sleep; } } while (timeLeft > 0); if (!connected) { console.error('Cannot connect to MySQL'); await client?.end().catch(console.error); await mysqlContainer?.stop().catch(console.error); throw lastError; } }); afterAll(async () => { await client?.end().catch(console.error); await mysqlContainer?.stop().catch(console.error); }); beforeEach(async () => { await client.query(`drop database if exists \`drizzle\`;`); await client.query(`create database \`drizzle\`;`); await client.query(`use \`drizzle\`;`); }); if (!fs.existsSync('tests/introspect/mysql')) { fs.mkdirSync('tests/introspect/mysql'); } test('generated always column: link to another column', async () => { const schema = { users: mysqlTable('users', { id: int('id'), email: text('email'), generatedEmail: text('generatedEmail').generatedAlwaysAs( (): SQL => sql`\`email\``, ), }), }; const { statements, sqlStatements } = await introspectMySQLToFile( client, schema, 'generated-link-column', 'drizzle', ); expect(statements.length).toBe(0); expect(sqlStatements.length).toBe(0); }); test('generated always column virtual: link to another column', async () => { const schema = { users: mysqlTable('users', { id: int('id'), email: text('email'), generatedEmail: text('generatedEmail').generatedAlwaysAs( (): SQL => sql`\`email\``, { mode: 'virtual' }, ), }), }; const { statements, sqlStatements } = await introspectMySQLToFile( client, schema, 'generated-link-column-virtual', 'drizzle', ); expect(statements.length).toBe(0); expect(sqlStatements.length).toBe(0); }); test('Default value of character type column: char', async () => { const schema = { users: mysqlTable('users', { id: int('id'), sortKey: char('sortKey', { length: 255 }).default('0'), }), }; const { statements, sqlStatements } = await introspectMySQLToFile( client, schema, 'default-value-char-column', 'drizzle', ); expect(statements.length).toBe(0); expect(sqlStatements.length).toBe(0); }); test('Default value of character type column: varchar', async () => { const schema = { users: mysqlTable('users', { id: int('id'), sortKey: varchar('sortKey', { length: 255 }).default('0'), }), }; const { statements, sqlStatements } = await introspectMySQLToFile( client, schema, 'default-value-varchar-column', 'drizzle', ); expect(statements.length).toBe(0); expect(sqlStatements.length).toBe(0); }); test('introspect checks', async () => { const schema = { users: mysqlTable('users', { id: serial('id'), name: varchar('name', { length: 255 }), age: int('age'), }, (table) => ({ someCheck: check('some_check', sql`${table.age} > 21`), })), }; const { statements, sqlStatements } = await introspectMySQLToFile( client, schema, 'introspect-checks', 'drizzle', ); expect(statements.length).toBe(0); expect(sqlStatements.length).toBe(0); }); test('view #1', async () => { const users = mysqlTable('users', { id: int('id') }); const testView = mysqlView('some_view', { id: int('id') }).as( sql`select \`drizzle\`.\`users\`.\`id\` AS \`id\` from \`drizzle\`.\`users\``, ); const schema = { users: users, testView, }; const { statements, sqlStatements } = await introspectMySQLToFile( client, schema, 'view-1', 'drizzle', ); expect(statements.length).toBe(0); expect(sqlStatements.length).toBe(0); }); test('view #2', async () => { const users = mysqlTable('some_users', { id: int('id') }); const testView = mysqlView('some_view', { id: int('id') }).algorithm('temptable').sqlSecurity('definer').as( sql`SELECT * FROM ${users}`, ); const schema = { users: users, testView, }; const { statements, sqlStatements } = await introspectMySQLToFile( client, schema, 'view-2', 'drizzle', ); expect(statements.length).toBe(0); expect(sqlStatements.length).toBe(0); }); test('handle float type', async () => { const schema = { table: mysqlTable('table', { col1: float(), col2: float({ precision: 2 }), col3: float({ precision: 2, scale: 1 }), }), }; const { statements, sqlStatements } = await introspectMySQLToFile( client, schema, 'handle-float-type', 'drizzle', ); expect(statements.length).toBe(0); expect(sqlStatements.length).toBe(0); }); test('handle unsigned numerical types', async () => { const schema = { table: mysqlTable('table', { col1: int({ unsigned: true }), col2: tinyint({ unsigned: true }), col3: smallint({ unsigned: true }), col4: mediumint({ unsigned: true }), col5: bigint({ mode: 'number', unsigned: true }), col6: float({ unsigned: true }), col7: float({ precision: 2, scale: 1, unsigned: true }), col8: double({ unsigned: true }), col9: double({ precision: 2, scale: 1, unsigned: true }), col10: decimal({ unsigned: true }), col11: decimal({ precision: 2, scale: 1, unsigned: true }), }), }; const { statements, sqlStatements } = await introspectMySQLToFile( client, schema, 'handle-unsigned-numerical-types', 'drizzle', ); expect(statements.length).toBe(0); expect(sqlStatements.length).toBe(0); }); test('instrospect strings with single quotes', async () => { const schema = { columns: mysqlTable('columns', { enum: mysqlEnum('my_enum', ['escape\'s quotes "', 'escape\'s quotes 2 "']).default('escape\'s quotes "'), text: text('text').default('escape\'s quotes " '), varchar: varchar('varchar', { length: 255 }).default('escape\'s quotes " '), }), }; const { statements, sqlStatements } = await introspectMySQLToFile( client, schema, 'introspect-strings-with-single-quotes', 'drizzle', ); expect(statements.length).toBe(0); expect(sqlStatements.length).toBe(0); await client.query(`drop table columns;`); });