Skip to content
37 changes: 20 additions & 17 deletions drizzle-orm/src/mysql-core/columns/binary.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
import type {
ColumnBuilderBaseConfig,
ColumnBuilderRuntimeConfig,
HasDefault,
MakeColumnConfig,
} from '~/column-builder.ts';
import type { ColumnBaseConfig } from '~/column.ts';
import { entityKind } from '~/entity.ts';
import type { AnyMySqlTable } from '~/mysql-core/table.ts';
import type { SQL } from '~/sql/sql.ts';
import { getColumnNameAndConfig } from '~/utils.ts';
import { MySqlColumn, MySqlColumnBuilder } from './common.ts';

export type MySqlBinaryBuilderInitial<TName extends string> = MySqlBinaryBuilder<{
name: TName;
dataType: 'string';
dataType: 'buffer';
columnType: 'MySqlBinary';
data: string;
driverParam: string;
data: Buffer;
driverParam: Buffer;
enumValues: undefined;
}>;

export class MySqlBinaryBuilder<T extends ColumnBuilderBaseConfig<'string', 'MySqlBinary'>> extends MySqlColumnBuilder<
export class MySqlBinaryBuilder<T extends ColumnBuilderBaseConfig<'buffer', 'MySqlBinary'>> extends MySqlColumnBuilder<
T,
MySqlBinaryConfig
> {
static override readonly [entityKind]: string = 'MySqlBinaryBuilder';

constructor(name: T['name'], length: number | undefined) {
super(name, 'string', 'MySqlBinary');
super(name, 'buffer', 'MySqlBinary');
this.config.length = length;
}

override default(value: Buffer | string | SQL): HasDefault<this> {
return super.default(value as Buffer | SQL);
}

/** @internal */
override build<TTableName extends string>(
table: AnyMySqlTable<{ name: TTableName }>,
Expand All @@ -33,24 +43,17 @@ export class MySqlBinaryBuilder<T extends ColumnBuilderBaseConfig<'string', 'MyS
}
}

export class MySqlBinary<T extends ColumnBaseConfig<'string', 'MySqlBinary'>> extends MySqlColumn<
export class MySqlBinary<T extends ColumnBaseConfig<'buffer', 'MySqlBinary'>> extends MySqlColumn<
T,
MySqlBinaryConfig
> {
static override readonly [entityKind]: string = 'MySqlBinary';

length: number | undefined = this.config.length;

override mapFromDriverValue(value: string | Buffer | Uint8Array): string {
if (typeof value === 'string') return value;
if (Buffer.isBuffer(value)) return value.toString();

const str: string[] = [];
for (const v of value) {
str.push(v === 49 ? '1' : '0');
}

return str.join('');
override mapFromDriverValue(value: string | Buffer | Uint8Array): Buffer {
if (Buffer.isBuffer(value)) return value;
return Buffer.from(value);
}

getSQLType(): string {
Expand Down
37 changes: 20 additions & 17 deletions drizzle-orm/src/mysql-core/columns/varbinary.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
import type {
ColumnBuilderBaseConfig,
ColumnBuilderRuntimeConfig,
HasDefault,
MakeColumnConfig,
} from '~/column-builder.ts';
import type { ColumnBaseConfig } from '~/column.ts';
import { entityKind } from '~/entity.ts';
import type { AnyMySqlTable } from '~/mysql-core/table.ts';
import type { SQL } from '~/sql/sql.ts';
import { getColumnNameAndConfig } from '~/utils.ts';
import { MySqlColumn, MySqlColumnBuilder } from './common.ts';

export type MySqlVarBinaryBuilderInitial<TName extends string> = MySqlVarBinaryBuilder<{
name: TName;
dataType: 'string';
dataType: 'buffer';
columnType: 'MySqlVarBinary';
data: string;
driverParam: string;
data: Buffer;
driverParam: Buffer;
enumValues: undefined;
}>;

export class MySqlVarBinaryBuilder<T extends ColumnBuilderBaseConfig<'string', 'MySqlVarBinary'>>
export class MySqlVarBinaryBuilder<T extends ColumnBuilderBaseConfig<'buffer', 'MySqlVarBinary'>>
extends MySqlColumnBuilder<T, MySqlVarbinaryOptions>
{
static override readonly [entityKind]: string = 'MySqlVarBinaryBuilder';

/** @internal */
constructor(name: T['name'], config: MySqlVarbinaryOptions) {
super(name, 'string', 'MySqlVarBinary');
super(name, 'buffer', 'MySqlVarBinary');
this.config.length = config?.length;
}

override default(value: Buffer | string | SQL): HasDefault<this> {
return super.default(value as Buffer | SQL);
}

/** @internal */
override build<TTableName extends string>(
table: AnyMySqlTable<{ name: TTableName }>,
Expand All @@ -37,22 +47,15 @@ export class MySqlVarBinaryBuilder<T extends ColumnBuilderBaseConfig<'string', '
}

export class MySqlVarBinary<
T extends ColumnBaseConfig<'string', 'MySqlVarBinary'>,
T extends ColumnBaseConfig<'buffer', 'MySqlVarBinary'>,
> extends MySqlColumn<T, MySqlVarbinaryOptions> {
static override readonly [entityKind]: string = 'MySqlVarBinary';

length: number | undefined = this.config.length;

override mapFromDriverValue(value: string | Buffer | Uint8Array): string {
if (typeof value === 'string') return value;
if (Buffer.isBuffer(value)) return value.toString();

const str: string[] = [];
for (const v of value) {
str.push(v === 49 ? '1' : '0');
}

return str.join('');
override mapFromDriverValue(value: string | Buffer | Uint8Array): Buffer {
if (Buffer.isBuffer(value)) return value;
return Buffer.from(value);
}

getSQLType(): string {
Expand Down
42 changes: 42 additions & 0 deletions drizzle-orm/tests/mysql-binary-buffer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Buffer } from 'node:buffer';
import { describe, test } from 'vitest';
import { binary, mysqlTable, varbinary } from '~/mysql-core/index.ts';

const table = mysqlTable('binary_buffer_test', {
binary: binary('binary', { length: 11 }).notNull(),
varbinary: varbinary('varbinary', { length: 11 }).notNull(),
});

describe.concurrent('MySQL binary columns', () => {
test('preserve Buffer values returned by mysql2', ({ expect }) => {
const value = Buffer.from([0x00, 0xff, 0x80, 0x31]);

const binaryValue = table.binary.mapFromDriverValue(value);
const varbinaryValue = table.varbinary.mapFromDriverValue(value);

expect(Buffer.isBuffer(binaryValue)).toBe(true);
expect(Buffer.isBuffer(varbinaryValue)).toBe(true);
expect(binaryValue).toEqual(value);
expect(varbinaryValue).toEqual(value);
});

test('convert Uint8Array values returned by binary adapters to Buffer without changing bytes', ({ expect }) => {
const value = new Uint8Array([0x00, 0xff, 0x80, 0x31]);

const binaryValue = table.binary.mapFromDriverValue(value);
const varbinaryValue = table.varbinary.mapFromDriverValue(value);

expect(Buffer.isBuffer(binaryValue)).toBe(true);
expect(Buffer.isBuffer(varbinaryValue)).toBe(true);
expect(binaryValue).toEqual(Buffer.from(value));
expect(varbinaryValue).toEqual(Buffer.from(value));
});

test('convert legacy string driver values to Buffer', ({ expect }) => {
const binaryValue = table.binary.mapFromDriverValue('hello world');
const varbinaryValue = table.varbinary.mapFromDriverValue('hello world');

expect(binaryValue).toEqual(Buffer.from('hello world'));
expect(varbinaryValue).toEqual(Buffer.from('hello world'));
});
});
17 changes: 17 additions & 0 deletions drizzle-orm/type-tests/mysql/binary-buffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { type Equal, Expect } from 'type-tests/utils.ts';
import { binary, mysqlTable, varbinary } from '~/mysql-core/index.ts';

const table = mysqlTable('binary_buffer_types', {
binary: binary('binary').notNull(),
varbinary: varbinary('varbinary', { length: 255 }).notNull(),
});

Expect<Equal<typeof table.$inferSelect.binary, Buffer>>;
Expect<Equal<typeof table.$inferSelect.varbinary, Buffer>>;
Expect<Equal<typeof table.$inferInsert.binary, Buffer>>;
Expect<Equal<typeof table.$inferInsert.varbinary, Buffer>>;

// Keep accepting string SQL defaults for backwards compatibility while
// the selected/inserted application data type is correctly inferred as Buffer.
binary('binary_default').default('');
varbinary('varbinary_default', { length: 255 }).default('');
25 changes: 24 additions & 1 deletion integration-tests/tests/mysql/mysql-planetscale.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Client } from '@planetscale/database';
import { sql } from 'drizzle-orm';
import { binary, mysqlTable, varbinary } from 'drizzle-orm/mysql-core';
import type { PlanetScaleDatabase } from 'drizzle-orm/planetscale-serverless';
import { drizzle } from 'drizzle-orm/planetscale-serverless';
import { beforeAll, beforeEach } from 'vitest';
import { beforeAll, beforeEach, expect, test } from 'vitest';
import { skipTests } from '~/common';
import { tests } from './mysql-common';
import { TestCache, TestGlobalCache, tests as cacheTests } from './mysql-common-cache';
Expand Down Expand Up @@ -84,5 +86,26 @@ skipTests([
'insert returning sql',
]);

test('binary and varbinary preserve non-UTF-8 bytes', async () => {
const table = mysqlTable('binary_buffer_planetscale', {
binary: binary('binary', { length: 4 }).notNull(),
varbinary: varbinary('varbinary', { length: 4 }).notNull(),
});
const value = Buffer.from([0x00, 0xff, 0x80, 0x31]);

await db.execute(sql`drop table if exists ${table}`);
await db.execute(sql`create table ${table} (\`binary\` binary(4) not null, \`varbinary\` varbinary(4) not null)`);
await db.insert(table).values({ binary: value, varbinary: value });

const [row] = await db.select().from(table);

expect(Buffer.isBuffer(row!.binary)).toBe(true);
expect(Buffer.isBuffer(row!.varbinary)).toBe(true);
expect(row!.binary).toEqual(value);
expect(row!.varbinary).toEqual(value);

await db.execute(sql`drop table ${table}`);
});

tests('planetscale');
cacheTests();
25 changes: 24 additions & 1 deletion integration-tests/tests/mysql/mysql.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import retry from 'async-retry';
import { sql } from 'drizzle-orm';
import { binary, mysqlTable, varbinary } from 'drizzle-orm/mysql-core';
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 { afterAll, beforeAll, beforeEach, expect, test } from 'vitest';
import { createDockerDB, tests } from './mysql-common';
import { TestCache, TestGlobalCache, tests as cacheTests } from './mysql-common-cache';

Expand Down Expand Up @@ -57,5 +59,26 @@ beforeEach((ctx) => {
};
});

test('binary and varbinary preserve non-UTF-8 bytes', async () => {
const table = mysqlTable('binary_buffer_mysql2', {
binary: binary('binary', { length: 4 }).notNull(),
varbinary: varbinary('varbinary', { length: 4 }).notNull(),
});
const value = Buffer.from([0x00, 0xff, 0x80, 0x31]);

await db.execute(sql`drop table if exists ${table}`);
await db.execute(sql`create table ${table} (\`binary\` binary(4) not null, \`varbinary\` varbinary(4) not null)`);
await db.insert(table).values({ binary: value, varbinary: value });

const [row] = await db.select().from(table);

expect(Buffer.isBuffer(row!.binary)).toBe(true);
expect(Buffer.isBuffer(row!.varbinary)).toBe(true);
expect(row!.binary).toEqual(value);
expect(row!.varbinary).toEqual(value);

await db.execute(sql`drop table ${table}`);
});

cacheTests();
tests();