From 6d91c86f55c0b6758b45ffa3c5f5fc150dfd3d88 Mon Sep 17 00:00:00 2001 From: Zubair Zafar Date: Mon, 24 Aug 2026 11:47:20 -0700 Subject: [PATCH 1/9] test(mysql): reproduce binary buffer mapping bug --- drizzle-orm/tests/mysql-binary-buffer.test.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 drizzle-orm/tests/mysql-binary-buffer.test.ts diff --git a/drizzle-orm/tests/mysql-binary-buffer.test.ts b/drizzle-orm/tests/mysql-binary-buffer.test.ts new file mode 100644 index 0000000000..dde0fa6b10 --- /dev/null +++ b/drizzle-orm/tests/mysql-binary-buffer.test.ts @@ -0,0 +1,30 @@ +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('hello world'); + + 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 string values returned by string-based adapters 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')); + }); +}); From 3f87d2580266090e3956dba6ef2a23dafad11226 Mon Sep 17 00:00:00 2001 From: Zubair Zafar Date: Mon, 24 Aug 2026 11:52:13 -0700 Subject: [PATCH 2/9] test(mysql): assert binary columns infer Buffer --- drizzle-orm/type-tests/mysql/binary-buffer.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 drizzle-orm/type-tests/mysql/binary-buffer.ts diff --git a/drizzle-orm/type-tests/mysql/binary-buffer.ts b/drizzle-orm/type-tests/mysql/binary-buffer.ts new file mode 100644 index 0000000000..6971d471e1 --- /dev/null +++ b/drizzle-orm/type-tests/mysql/binary-buffer.ts @@ -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>; +Expect>; +Expect>; +Expect>; + +// 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(''); From a7286c358cefad6c174ac53fcb15363ce6e7829d Mon Sep 17 00:00:00 2001 From: Zubair Zafar Date: Mon, 24 Aug 2026 11:52:37 -0700 Subject: [PATCH 3/9] test(mysql): cover Uint8Array binary values --- drizzle-orm/tests/mysql-binary-buffer.test.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/drizzle-orm/tests/mysql-binary-buffer.test.ts b/drizzle-orm/tests/mysql-binary-buffer.test.ts index dde0fa6b10..6ddf10a652 100644 --- a/drizzle-orm/tests/mysql-binary-buffer.test.ts +++ b/drizzle-orm/tests/mysql-binary-buffer.test.ts @@ -9,7 +9,7 @@ const table = mysqlTable('binary_buffer_test', { describe.concurrent('MySQL binary columns', () => { test('preserve Buffer values returned by mysql2', ({ expect }) => { - const value = Buffer.from('hello world'); + const value = Buffer.from([0x00, 0xff, 0x80, 0x31]); const binaryValue = table.binary.mapFromDriverValue(value); const varbinaryValue = table.varbinary.mapFromDriverValue(value); @@ -20,7 +20,19 @@ describe.concurrent('MySQL binary columns', () => { expect(varbinaryValue).toEqual(value); }); - test('convert string values returned by string-based adapters to Buffer', ({ expect }) => { + 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'); From e149d1254ffa090060e53d28327fa5e012d62595 Mon Sep 17 00:00:00 2001 From: Zubair Zafar Date: Mon, 24 Aug 2026 11:52:49 -0700 Subject: [PATCH 4/9] fix(mysql): map binary columns to Buffer --- drizzle-orm/src/mysql-core/columns/binary.ts | 37 +++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/drizzle-orm/src/mysql-core/columns/binary.ts b/drizzle-orm/src/mysql-core/columns/binary.ts index f94da26c5f..01bf54c42e 100644 --- a/drizzle-orm/src/mysql-core/columns/binary.ts +++ b/drizzle-orm/src/mysql-core/columns/binary.ts @@ -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 = MySqlBinaryBuilder<{ name: TName; - dataType: 'string'; + dataType: 'buffer'; columnType: 'MySqlBinary'; - data: string; - driverParam: string; + data: Buffer; + driverParam: Buffer; enumValues: undefined; }>; -export class MySqlBinaryBuilder> extends MySqlColumnBuilder< +export class MySqlBinaryBuilder> 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 { + return super.default(value as Buffer | SQL); + } + /** @internal */ override build( table: AnyMySqlTable<{ name: TTableName }>, @@ -33,7 +43,7 @@ export class MySqlBinaryBuilder> extends MySqlColumn< +export class MySqlBinary> extends MySqlColumn< T, MySqlBinaryConfig > { @@ -41,16 +51,9 @@ export class MySqlBinary> ex 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 { From 4004e13fdddda1a9bbd4b2b254ef9998efcaa5c5 Mon Sep 17 00:00:00 2001 From: Zubair Zafar Date: Mon, 24 Aug 2026 11:53:00 -0700 Subject: [PATCH 5/9] fix(mysql): map varbinary columns to Buffer --- .../src/mysql-core/columns/varbinary.ts | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/drizzle-orm/src/mysql-core/columns/varbinary.ts b/drizzle-orm/src/mysql-core/columns/varbinary.ts index 25b6864031..2813be308d 100644 --- a/drizzle-orm/src/mysql-core/columns/varbinary.ts +++ b/drizzle-orm/src/mysql-core/columns/varbinary.ts @@ -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 = MySqlVarBinaryBuilder<{ name: TName; - dataType: 'string'; + dataType: 'buffer'; columnType: 'MySqlVarBinary'; - data: string; - driverParam: string; + data: Buffer; + driverParam: Buffer; enumValues: undefined; }>; -export class MySqlVarBinaryBuilder> +export class MySqlVarBinaryBuilder> extends MySqlColumnBuilder { 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 { + return super.default(value as Buffer | SQL); + } + /** @internal */ override build( table: AnyMySqlTable<{ name: TTableName }>, @@ -37,22 +47,15 @@ export class MySqlVarBinaryBuilder, + T extends ColumnBaseConfig<'buffer', 'MySqlVarBinary'>, > extends MySqlColumn { 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 { From 0c8e49d9ca2ccaada61415e8ed85f9410c00fd25 Mon Sep 17 00:00:00 2001 From: Zubair Zafar Date: Mon, 24 Aug 2026 23:52:33 -0700 Subject: [PATCH 6/9] test(mysql): cover PlanetScale binary buffer round-trip --- .../tests/mysql/mysql-planetscale.test.ts | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/integration-tests/tests/mysql/mysql-planetscale.test.ts b/integration-tests/tests/mysql/mysql-planetscale.test.ts index fc97ea254a..315c3579a9 100644 --- a/integration-tests/tests/mysql/mysql-planetscale.test.ts +++ b/integration-tests/tests/mysql/mysql-planetscale.test.ts @@ -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'; @@ -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(); +cacheTests(); \ No newline at end of file From 92949683db01e289f7354eec30465e1ee4076d08 Mon Sep 17 00:00:00 2001 From: Zubair Zafar Date: Mon, 24 Aug 2026 23:52:45 -0700 Subject: [PATCH 7/9] test(mysql2): cover binary buffer round-trip --- integration-tests/tests/mysql/mysql.test.ts | 27 +++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/integration-tests/tests/mysql/mysql.test.ts b/integration-tests/tests/mysql/mysql.test.ts index 6641e2d14f..5b46c46720 100644 --- a/integration-tests/tests/mysql/mysql.test.ts +++ b/integration-tests/tests/mysql/mysql.test.ts @@ -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'; @@ -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(); +tests(); \ No newline at end of file From aba735779b643106a066ffa16074698646501348 Mon Sep 17 00:00:00 2001 From: Zubair Zafar Date: Mon, 24 Aug 2026 23:53:42 -0700 Subject: [PATCH 8/9] test(mysql2): clean binary regression DDL --- integration-tests/tests/mysql/mysql.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/tests/mysql/mysql.test.ts b/integration-tests/tests/mysql/mysql.test.ts index 5b46c46720..a290d7924c 100644 --- a/integration-tests/tests/mysql/mysql.test.ts +++ b/integration-tests/tests/mysql/mysql.test.ts @@ -67,7 +67,7 @@ test('binary and varbinary preserve non-UTF-8 bytes', async () => { 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.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); @@ -81,4 +81,4 @@ test('binary and varbinary preserve non-UTF-8 bytes', async () => { }); cacheTests(); -tests(); \ No newline at end of file +tests(); From d7e18153a1997ab86a128fdedfe23f1934903d83 Mon Sep 17 00:00:00 2001 From: Zubair Zafar Date: Mon, 24 Aug 2026 23:53:59 -0700 Subject: [PATCH 9/9] test(mysql): clean PlanetScale binary regression DDL --- integration-tests/tests/mysql/mysql-planetscale.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/tests/mysql/mysql-planetscale.test.ts b/integration-tests/tests/mysql/mysql-planetscale.test.ts index 315c3579a9..77df3c36ef 100644 --- a/integration-tests/tests/mysql/mysql-planetscale.test.ts +++ b/integration-tests/tests/mysql/mysql-planetscale.test.ts @@ -94,7 +94,7 @@ test('binary and varbinary preserve non-UTF-8 bytes', async () => { 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.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); @@ -108,4 +108,4 @@ test('binary and varbinary preserve non-UTF-8 bytes', async () => { }); tests('planetscale'); -cacheTests(); \ No newline at end of file +cacheTests();