Files
2026-07-13 12:10:05 +08:00

895 lines
20 KiB
TypeScript

import chalk from 'chalk';
import Docker from 'dockerode';
import { getTableConfig, index, int, singlestoreTable, text } from 'drizzle-orm/singlestore-core';
import fs from 'fs';
import getPort from 'get-port';
import { Connection, createConnection } from 'mysql2/promise';
import { diffTestSchemasPushSingleStore } from 'tests/schemaDiffer';
import { v4 as uuid } from 'uuid';
import { afterAll, beforeAll, expect, test } from 'vitest';
let client: Connection;
let singlestoreContainer: Docker.Container;
async function createDockerDB(): Promise<string> {
const docker = new Docker();
const port = await getPort({ port: 3306 });
const image = 'ghcr.io/singlestore-labs/singlestoredb-dev:latest';
const pullStream = await docker.pull(image);
await new Promise((resolve, reject) =>
docker.modem.followProgress(pullStream, (err) => err ? reject(err) : resolve(err))
);
singlestoreContainer = await docker.createContainer({
Image: image,
Env: ['ROOT_PASSWORD=singlestore'],
name: `drizzle-integration-tests-${uuid()}`,
HostConfig: {
AutoRemove: true,
PortBindings: {
'3306/tcp': [{ HostPort: `${port}` }],
},
},
});
await singlestoreContainer.start();
await new Promise((resolve) => setTimeout(resolve, 4000));
return `singlestore://root:singlestore@localhost:${port}/`;
}
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 singlestoreContainer?.stop().catch(console.error);
throw lastError;
}
await client.query('DROP DATABASE IF EXISTS drizzle;');
await client.query('CREATE DATABASE drizzle;');
await client.query('USE drizzle;');
});
afterAll(async () => {
await client?.end().catch(console.error);
await singlestoreContainer?.stop().catch(console.error);
});
if (!fs.existsSync('tests/push/singlestore')) {
fs.mkdirSync('tests/push/singlestore');
}
test('db has checks. Push with same names', async () => {
const schema1 = {
test: singlestoreTable('test', {
id: int('id').primaryKey(),
values: int('values').default(1),
}),
};
const schema2 = {
test: singlestoreTable('test', {
id: int('id').primaryKey(),
values: int('values').default(1),
}),
};
const { statements, sqlStatements } = await diffTestSchemasPushSingleStore(
client,
schema1,
schema2,
[],
'drizzle',
);
expect(statements).toStrictEqual([]);
expect(sqlStatements).toStrictEqual([]);
await client.query(`DROP TABLE \`test\`;`);
});
// TODO: Unskip this test when views are implemented
/* test.skip.skip('create view', async () => {
const table = singlestoreTable('test', {
id: int('id').primaryKey(),
});
const schema1 = {
test: table,
};
const schema2 = {
test: table,
view: singlestoreView('view').as((qb) => qb.select().from(table)),
};
const { statements, sqlStatements } = await diffTestSchemasPushSingleStore(
client,
schema1,
schema2,
[],
'drizzle',
false,
);
expect(statements).toStrictEqual([
{
definition: 'select `id` from `test`',
name: 'view',
type: 'singlestore_create_view',
replace: false,
sqlSecurity: 'definer',
withCheckOption: undefined,
algorithm: 'undefined',
},
]);
expect(sqlStatements).toStrictEqual([
`CREATE ALGORITHM = undefined
SQL SECURITY definer
VIEW \`view\` AS (select \`id\` from \`test\`);`,
]);
await client.query(`DROP TABLE \`test\`;`);
}); */
// TODO: Unskip this test when views are implemented
/* test.skip('drop view', async () => {
const table = singlestoreTable('test', {
id: int('id').primaryKey(),
});
const schema1 = {
test: table,
view: singlestoreView('view').as((qb) => qb.select().from(table)),
};
const schema2 = {
test: table,
};
const { statements, sqlStatements } = await diffTestSchemasPushSingleStore(
client,
schema1,
schema2,
[],
'drizzle',
false,
);
expect(statements).toStrictEqual([
{
name: 'view',
type: 'drop_view',
},
]);
expect(sqlStatements).toStrictEqual(['DROP VIEW `view`;']);
await client.query(`DROP TABLE \`test\`;`);
await client.query(`DROP VIEW \`view\`;`);
}); */
// TODO: Unskip this test when views are implemented
/* test.skip('alter view ".as"', async () => {
const table = singlestoreTable('test', {
id: int('id').primaryKey(),
});
const schema1 = {
test: table,
view: singlestoreView('view').as((qb) =>
qb
.select()
.from(table)
.where(sql`${table.id} = 1`)
),
};
const schema2 = {
test: table,
view: singlestoreView('view').as((qb) => qb.select().from(table)),
};
const { statements, sqlStatements } = await diffTestSchemasPushSingleStore(
client,
schema1,
schema2,
[],
'drizzle',
false,
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
await client.query(`DROP TABLE \`test\`;`);
await client.query(`DROP VIEW \`view\`;`);
}); */
// TODO: Unskip this test when views are implemented
/* test.skip('alter meta options with distinct in definition', async () => {
const table = singlestoreTable('test', {
id: int('id').primaryKey(),
});
const schema1 = {
test: table,
view: singlestoreView('view')
.withCheckOption('cascaded')
.sqlSecurity('definer')
.algorithm('merge')
.as((qb) =>
qb
.selectDistinct()
.from(table)
.where(sql`${table.id} = 1`)
),
};
const schema2 = {
test: table,
view: singlestoreView('view')
.withCheckOption('cascaded')
.sqlSecurity('definer')
.algorithm('undefined')
.as((qb) => qb.selectDistinct().from(table)),
};
await expect(
diffTestSchemasPushSingleStore(
client,
schema1,
schema2,
[],
'drizzle',
false,
),
).rejects.toThrowError();
await client.query(`DROP TABLE \`test\`;`);
}); */
test('added column not null and without default to table with data', async (t) => {
const schema1 = {
companies: singlestoreTable('companies', {
id: int('id'),
name: text('name'),
}),
};
const schema2 = {
companies: singlestoreTable('companies', {
id: int('id'),
name: text('name'),
age: int('age').notNull(),
}),
};
const table = getTableConfig(schema1.companies);
const seedStatements = [
`INSERT INTO \`${table.name}\` (\`${schema1.companies.name.name}\`) VALUES ('drizzle');`,
`INSERT INTO \`${table.name}\` (\`${schema1.companies.name.name}\`) VALUES ('turso');`,
];
const {
statements,
sqlStatements,
columnsToRemove,
infoToPrint,
shouldAskForApprove,
tablesToRemove,
tablesToTruncate,
} = await diffTestSchemasPushSingleStore(
client,
schema1,
schema2,
[],
'drizzle',
false,
undefined,
{
after: seedStatements,
},
);
expect(statements.length).toBe(1);
expect(statements[0]).toStrictEqual({
type: 'alter_table_add_column',
tableName: 'companies',
column: {
name: 'age',
type: 'int',
primaryKey: false,
notNull: true,
autoincrement: false,
},
schema: '',
});
expect(sqlStatements.length).toBe(2);
expect(sqlStatements[0]).toBe(`truncate table companies;`);
expect(sqlStatements[1]).toBe(
`ALTER TABLE \`companies\` ADD \`age\` int NOT NULL;`,
);
expect(columnsToRemove!.length).toBe(0);
expect(infoToPrint!.length).toBe(1);
expect(infoToPrint![0]).toBe(
`· You're about to add not-null ${
chalk.underline(
'age',
)
} column without default value, which contains 2 items`,
);
expect(shouldAskForApprove).toBe(true);
expect(tablesToRemove!.length).toBe(0);
expect(tablesToTruncate!.length).toBe(1);
expect(tablesToTruncate![0]).toBe('companies');
await client.query(`DROP TABLE \`companies\`;`);
});
test('added column not null and without default to table without data', async (t) => {
const schema1 = {
companies: singlestoreTable('companies', {
id: int('id').primaryKey(),
name: text('name').notNull(),
}),
};
const schema2 = {
companies: singlestoreTable('companies', {
id: int('id').primaryKey(),
name: text('name').notNull(),
age: int('age').notNull(),
}),
};
const {
statements,
sqlStatements,
columnsToRemove,
infoToPrint,
shouldAskForApprove,
tablesToRemove,
tablesToTruncate,
} = await diffTestSchemasPushSingleStore(
client,
schema1,
schema2,
[],
'drizzle',
false,
undefined,
);
expect(statements.length).toBe(1);
expect(statements[0]).toStrictEqual({
type: 'alter_table_add_column',
tableName: 'companies',
column: {
name: 'age',
type: 'int',
primaryKey: false,
notNull: true,
autoincrement: false,
},
schema: '',
});
expect(sqlStatements.length).toBe(1);
expect(sqlStatements[0]).toBe(
`ALTER TABLE \`companies\` ADD \`age\` int NOT NULL;`,
);
expect(infoToPrint!.length).toBe(0);
expect(columnsToRemove!.length).toBe(0);
expect(shouldAskForApprove).toBe(false);
expect(tablesToRemove!.length).toBe(0);
expect(tablesToTruncate!.length).toBe(0);
await client.query(`DROP TABLE \`companies\`;`);
});
test('drop not null, add not null', async (t) => {
const schema1 = {
users: singlestoreTable('users', {
id: int('id').primaryKey(),
name: text('name').notNull(),
}),
posts: singlestoreTable(
'posts',
{
id: int('id').primaryKey(),
name: text('name'),
userId: int('user_id'),
},
),
};
const schema2 = {
users: singlestoreTable('users', {
id: int('id').primaryKey(),
name: text('name'),
}),
posts: singlestoreTable(
'posts',
{
id: int('id').primaryKey(),
name: text('name').notNull(),
userId: int('user_id'),
},
),
};
const {
statements,
sqlStatements,
columnsToRemove,
infoToPrint,
shouldAskForApprove,
tablesToRemove,
tablesToTruncate,
} = await diffTestSchemasPushSingleStore(
client,
schema1,
schema2,
[],
'drizzle',
false,
undefined,
);
expect(statements!.length).toBe(2);
expect(statements![0]).toStrictEqual({
columns: [
{
autoincrement: false,
generated: undefined,
name: 'id',
notNull: true,
onUpdate: undefined,
primaryKey: false,
type: 'int',
},
{
autoincrement: false,
generated: undefined,
name: 'name',
notNull: true,
onUpdate: undefined,
primaryKey: false,
type: 'text',
},
{
autoincrement: false,
generated: undefined,
name: 'user_id',
notNull: false,
onUpdate: undefined,
primaryKey: false,
type: 'int',
},
],
compositePKs: [
'posts_id;id',
],
tableName: 'posts',
type: 'singlestore_recreate_table',
uniqueConstraints: [],
});
expect(statements![1]).toStrictEqual({
columns: [
{
autoincrement: false,
generated: undefined,
name: 'id',
notNull: true,
onUpdate: undefined,
primaryKey: false,
type: 'int',
},
{
autoincrement: false,
generated: undefined,
name: 'name',
notNull: false,
onUpdate: undefined,
primaryKey: false,
type: 'text',
},
],
compositePKs: [
'users_id;id',
],
tableName: 'users',
type: 'singlestore_recreate_table',
uniqueConstraints: [],
});
expect(sqlStatements!.length).toBe(8);
expect(sqlStatements![0]).toBe(`CREATE TABLE \`__new_posts\` (
\t\`id\` int NOT NULL,
\t\`name\` text NOT NULL,
\t\`user_id\` int,
\tCONSTRAINT \`posts_id\` PRIMARY KEY(\`id\`)
);\n`);
expect(sqlStatements![1]).toBe(
`INSERT INTO \`__new_posts\`(\`id\`, \`name\`, \`user_id\`) SELECT \`id\`, \`name\`, \`user_id\` FROM \`posts\`;`,
);
expect(sqlStatements![2]).toBe(`DROP TABLE \`posts\`;`);
expect(sqlStatements![3]).toBe(`ALTER TABLE \`__new_posts\` RENAME TO \`posts\`;`);
expect(sqlStatements![4]).toBe(`CREATE TABLE \`__new_users\` (
\t\`id\` int NOT NULL,
\t\`name\` text,
\tCONSTRAINT \`users_id\` PRIMARY KEY(\`id\`)
);\n`);
expect(sqlStatements![5]).toBe(
`INSERT INTO \`__new_users\`(\`id\`, \`name\`) SELECT \`id\`, \`name\` FROM \`users\`;`,
);
expect(sqlStatements![6]).toBe(
`DROP TABLE \`users\`;`,
);
expect(sqlStatements![7]).toBe(`ALTER TABLE \`__new_users\` RENAME TO \`users\`;`);
expect(columnsToRemove!.length).toBe(0);
expect(infoToPrint!.length).toBe(0);
expect(shouldAskForApprove).toBe(false);
expect(tablesToRemove!.length).toBe(0);
expect(tablesToTruncate!.length).toBe(0);
await client.query(`DROP TABLE \`users\`;`);
await client.query(`DROP TABLE \`posts\`;`);
});
test('drop table with data', async (t) => {
const schema1 = {
users: singlestoreTable('users', {
id: int('id').primaryKey(),
name: text('name').notNull(),
}),
posts: singlestoreTable(
'posts',
{
id: int('id').primaryKey(),
name: text('name'),
userId: int('user_id'),
},
),
};
const schema2 = {
posts: singlestoreTable(
'posts',
{
id: int('id').primaryKey(),
name: text('name'),
userId: int('user_id'),
},
),
};
const seedStatements = [
`INSERT INTO \`users\` (\`id\`, \`name\`) VALUES (1, 'drizzle')`,
];
const {
statements,
sqlStatements,
columnsToRemove,
infoToPrint,
shouldAskForApprove,
tablesToRemove,
tablesToTruncate,
} = await diffTestSchemasPushSingleStore(
client,
schema1,
schema2,
[],
'drizzle',
false,
undefined,
{ after: seedStatements },
);
expect(statements!.length).toBe(1);
expect(statements![0]).toStrictEqual({
policies: [],
schema: undefined,
tableName: 'users',
type: 'drop_table',
});
expect(sqlStatements!.length).toBe(1);
expect(sqlStatements![0]).toBe(`DROP TABLE \`users\`;`);
expect(columnsToRemove!.length).toBe(0);
expect(infoToPrint!.length).toBe(1);
expect(infoToPrint![0]).toBe(`· You're about to delete ${chalk.underline('users')} table with 1 items`);
expect(shouldAskForApprove).toBe(true);
expect(tablesToRemove!.length).toBe(1);
expect(tablesToRemove![0]).toBe('users');
expect(tablesToTruncate!.length).toBe(0);
await client.query(`DROP TABLE \`users\`;`);
await client.query(`DROP TABLE \`posts\`;`);
});
test('change data type. db has indexes. table does not have values', async (t) => {
const schema1 = {
users: singlestoreTable('users', {
id: int('id').primaryKey(),
name: int('name').notNull(),
}, (table) => [index('index').on(table.name)]),
};
const schema2 = {
users: singlestoreTable('users', {
id: int('id').primaryKey(),
name: text('name').notNull(),
}, (table) => [index('index').on(table.name)]),
};
const seedStatements = [`INSERT INTO users VALUES (1, 12)`];
const {
statements,
sqlStatements,
columnsToRemove,
infoToPrint,
shouldAskForApprove,
tablesToRemove,
tablesToTruncate,
} = await diffTestSchemasPushSingleStore(
client,
schema1,
schema2,
[],
'drizzle',
false,
undefined,
);
expect(statements!.length).toBe(2);
expect(statements![0]).toStrictEqual({
columns: [
{
autoincrement: false,
generated: undefined,
name: 'id',
notNull: true,
onUpdate: undefined,
primaryKey: false,
type: 'int',
},
{
autoincrement: false,
generated: undefined,
name: 'name',
notNull: true,
onUpdate: undefined,
primaryKey: false,
type: 'text',
},
],
compositePKs: [
'users_id;id',
],
tableName: 'users',
type: 'singlestore_recreate_table',
uniqueConstraints: [],
});
expect(statements![1]).toStrictEqual({
data: 'index;name;false;;;',
internal: undefined,
schema: '',
tableName: 'users',
type: 'create_index',
});
expect(sqlStatements!.length).toBe(5);
expect(sqlStatements![0]).toBe(`CREATE TABLE \`__new_users\` (
\t\`id\` int NOT NULL,
\t\`name\` text NOT NULL,
\tCONSTRAINT \`users_id\` PRIMARY KEY(\`id\`)
);\n`);
expect(sqlStatements![1]).toBe(
`INSERT INTO \`__new_users\`(\`id\`, \`name\`) SELECT \`id\`, \`name\` FROM \`users\`;`,
);
expect(sqlStatements![2]).toBe(`DROP TABLE \`users\`;`);
expect(sqlStatements![3]).toBe(`ALTER TABLE \`__new_users\` RENAME TO \`users\`;`);
expect(sqlStatements![4]).toBe(`CREATE INDEX \`index\` ON \`users\` (\`name\`);`);
expect(columnsToRemove!.length).toBe(0);
expect(infoToPrint!.length).toBe(0);
expect(shouldAskForApprove).toBe(false);
expect(tablesToRemove!.length).toBe(0);
expect(tablesToTruncate!.length).toBe(0);
await client.query(`DROP TABLE \`users\`;`);
});
test('change data type. db has indexes. table has values', async (t) => {
const schema1 = {
users: singlestoreTable('users', {
id: int('id').primaryKey(),
name: int('name'),
}, (table) => [index('index').on(table.name)]),
};
const schema2 = {
users: singlestoreTable('users', {
id: int('id').primaryKey(),
name: text('name'),
}, (table) => [index('index').on(table.name)]),
};
const seedStatements = [`INSERT INTO users VALUES (1, 12);`, `INSERT INTO users (id) VALUES (2);`];
const {
statements,
sqlStatements,
columnsToRemove,
infoToPrint,
shouldAskForApprove,
tablesToRemove,
tablesToTruncate,
} = await diffTestSchemasPushSingleStore(
client,
schema1,
schema2,
[],
'drizzle',
false,
undefined,
{ after: seedStatements },
);
expect(statements!.length).toBe(2);
expect(statements![0]).toStrictEqual({
columns: [
{
autoincrement: false,
generated: undefined,
name: 'id',
notNull: true,
onUpdate: undefined,
primaryKey: false,
type: 'int',
},
{
autoincrement: false,
generated: undefined,
name: 'name',
notNull: false,
onUpdate: undefined,
primaryKey: false,
type: 'text',
},
],
compositePKs: [
'users_id;id',
],
tableName: 'users',
type: 'singlestore_recreate_table',
uniqueConstraints: [],
});
expect(statements![1]).toStrictEqual({
data: 'index;name;false;;;',
internal: undefined,
schema: '',
tableName: 'users',
type: 'create_index',
});
expect(sqlStatements!.length).toBe(6);
expect(sqlStatements![0]).toBe(`TRUNCATE TABLE \`users\`;`);
expect(sqlStatements![1]).toBe(`CREATE TABLE \`__new_users\` (
\t\`id\` int NOT NULL,
\t\`name\` text,
\tCONSTRAINT \`users_id\` PRIMARY KEY(\`id\`)
);\n`);
expect(sqlStatements![2]).toBe(
`INSERT INTO \`__new_users\`(\`id\`, \`name\`) SELECT \`id\`, \`name\` FROM \`users\`;`,
);
expect(sqlStatements![3]).toBe(`DROP TABLE \`users\`;`);
expect(sqlStatements![4]).toBe(`ALTER TABLE \`__new_users\` RENAME TO \`users\`;`);
expect(sqlStatements![5]).toBe(`CREATE INDEX \`index\` ON \`users\` (\`name\`);`);
expect(columnsToRemove!.length).toBe(0);
expect(infoToPrint!.length).toBe(1);
expect(infoToPrint![0]).toBe(
`· You're about recreate ${chalk.underline('users')} table with data type changing for ${
chalk.underline('name')
} column, which contains 1 items`,
);
expect(shouldAskForApprove).toBe(true);
expect(tablesToRemove!.length).toBe(0);
expect(tablesToTruncate!.length).toBe(1);
expect(tablesToTruncate![0]).toBe(`users`);
await client.query(`DROP TABLE \`users\`;`);
});
test('add column. add default to column without not null', async (t) => {
const schema1 = {
users: singlestoreTable('users', {
id: int('id').primaryKey(),
name: text('name'),
}),
};
const schema2 = {
users: singlestoreTable('users', {
id: int('id').primaryKey(),
name: text('name').default('drizzle'),
age: int('age'),
}),
};
const {
statements,
sqlStatements,
columnsToRemove,
infoToPrint,
shouldAskForApprove,
tablesToRemove,
tablesToTruncate,
} = await diffTestSchemasPushSingleStore(
client,
schema1,
schema2,
[],
'drizzle',
false,
undefined,
);
expect(statements!.length).toBe(2);
expect(statements![0]).toStrictEqual({
columnAutoIncrement: false,
columnName: 'name',
columnNotNull: false,
columnOnUpdate: undefined,
columnPk: false,
newDataType: 'text',
newDefaultValue: "'drizzle'",
schema: '',
tableName: 'users',
type: 'alter_table_alter_column_set_default',
});
expect(statements![1]).toStrictEqual({
type: 'alter_table_add_column',
tableName: 'users',
schema: '',
column: {
notNull: false,
primaryKey: false,
autoincrement: false,
name: 'age',
type: 'int',
},
});
expect(sqlStatements!.length).toBe(2);
expect(sqlStatements![0]).toBe(`ALTER TABLE \`users\` MODIFY COLUMN \`name\` text DEFAULT 'drizzle';`);
expect(sqlStatements![1]).toBe(`ALTER TABLE \`users\` ADD \`age\` int;`);
expect(columnsToRemove!.length).toBe(0);
expect(infoToPrint!.length).toBe(0);
expect(shouldAskForApprove).toBe(false);
expect(tablesToRemove!.length).toBe(0);
expect(tablesToTruncate!.length).toBe(0);
await client.query(`DROP TABLE \`users\`;`);
});