diff --git a/packages/adapter-mariadb/src/conversion.ts b/packages/adapter-mariadb/src/conversion.ts index 8b7daca414cb..fa133c12da9b 100644 --- a/packages/adapter-mariadb/src/conversion.ts +++ b/packages/adapter-mariadb/src/conversion.ts @@ -2,7 +2,8 @@ import { ArgType, ColumnType, ColumnTypeEnum, ResultValue } from '@prisma/driver import * as mariadb from 'mariadb' const UNSIGNED_FLAG = 1 << 5 -const BINARY_FLAG = 1 << 7 +// https://github.com/mariadb-corporation/mariadb-connector-nodejs/blob/be72ebf9fee6e0bd153b6ff6e0bb252f794dbf0e/lib/const/collations.js#L150 +const BINARY_COLLATION_INDEX = 63 const enum MariaDbColumnType { DECIMAL = 'DECIMAL', @@ -83,7 +84,10 @@ export function mapColumnType(field: mariadb.FieldInfo): ColumnType { // https://github.com/mariadb-corporation/mariadb-connector-nodejs/blob/1bbbb41e92d2123948c2322a4dbb5021026f2d05/lib/cmd/column-definition.js#L27 if (field['dataTypeFormat'] === 'json') { return ColumnTypeEnum.Json - } else if (field.flags.valueOf() & BINARY_FLAG) { + } + // The Binary flag of column definition applies to both text and binary data. To distinguish them, check if collation == 'binary' instead of checking the flag. + // https://github.com/mariadb-corporation/mariadb-connector-nodejs/blob/be72ebf9fee6e0bd153b6ff6e0bb252f794dbf0e/lib/cmd/decoder/text-decoder.js#L44 + else if (field.collation.index === BINARY_COLLATION_INDEX) { return ColumnTypeEnum.Bytes } else { return ColumnTypeEnum.Text diff --git a/packages/adapter-planetscale/src/conversion.ts b/packages/adapter-planetscale/src/conversion.ts index b96a89664b6b..bcdd14ac8e55 100644 --- a/packages/adapter-planetscale/src/conversion.ts +++ b/packages/adapter-planetscale/src/conversion.ts @@ -1,10 +1,13 @@ -import { cast as defaultCast } from '@planetscale/database' +import { cast as defaultCast, type Field } from '@planetscale/database' import { ArgType, type ColumnType, ColumnTypeEnum } from '@prisma/driver-adapter-utils' import { decodeUtf8 } from './text' +// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_character_set.html +const BINARY_COLLATION_INDEX = 63 + // See: https://github.com/planetscale/vitess-types/blob/06235e372d2050b4c0fff49972df8111e696c564/src/vitess/query/v16/query.proto#L108-L218 -export type PlanetScaleColumnType = +type PlanetScaleColumnType = | 'NULL' | 'INT8' | 'UINT8' @@ -46,8 +49,9 @@ export type PlanetScaleColumnType = * module to see how other attributes of the field packet such as the field length are used to infer * the correct quaint::Value variant. */ -export function fieldToColumnType(field: PlanetScaleColumnType): ColumnType { - switch (field) { +export function fieldToColumnType(field: Field): ColumnType { + const type = field.type as PlanetScaleColumnType + switch (type) { case 'INT8': case 'UINT8': case 'INT16': @@ -85,6 +89,14 @@ export function fieldToColumnType(field: PlanetScaleColumnType): ColumnType { case 'BLOB': case 'BINARY': case 'VARBINARY': + // vitess converts CHAR/VARCHAR/TEXT columns with a binary collation to BINARY/VARBINARY/BLOB respectively before returning them to @planetscale/database driver. + // https://github.com/vitessio/vitess/blob/a94fa13f2ab53c98aad07a56eb15fe20b5ea7ade/go/sqltypes/type.go#L269 + // Therefore, we check the collation to distinguish between text and binary data. + // https://github.com/planetscale/database-js/blob/de78eebfaec8cd88c670b8c644fc5a3fd69e664c/src/cast.ts#L92 + if (field.charset && field.charset !== BINARY_COLLATION_INDEX) { + return ColumnTypeEnum.Text + } + return ColumnTypeEnum.Bytes case 'BIT': case 'BITNUM': case 'HEXNUM': @@ -95,7 +107,7 @@ export function fieldToColumnType(field: PlanetScaleColumnType): ColumnType { // Fall back to Int32 for consistency with quaint. return ColumnTypeEnum.Int32 default: - throw new Error(`Unsupported column type: ${field}`) + throw new Error(`Unsupported column type: ${type}`) } } diff --git a/packages/adapter-planetscale/src/planetscale.ts b/packages/adapter-planetscale/src/planetscale.ts index 5e62d7498fea..36a80145adf4 100644 --- a/packages/adapter-planetscale/src/planetscale.ts +++ b/packages/adapter-planetscale/src/planetscale.ts @@ -18,7 +18,7 @@ import { Debug, DriverAdapterError } from '@prisma/driver-adapter-utils' import { Mutex } from 'async-mutex' import { name as packageName } from '../package.json' -import { cast, fieldToColumnType, mapArg, type PlanetScaleColumnType } from './conversion' +import { cast, fieldToColumnType, mapArg } from './conversion' import { createDeferred, Deferred } from './deferred' import { convertDriverError } from './errors' @@ -54,7 +54,7 @@ class PlanetScaleQueryable field.name) return { columnNames: columns, - columnTypes: fields.map((field) => fieldToColumnType(field.type as PlanetScaleColumnType)), + columnTypes: fields.map((field) => fieldToColumnType(field)), rows: rows as SqlResultSet['rows'], lastInsertId, } diff --git a/packages/client/tests/functional/raw-queries/mysql-column-type/_matrix.ts b/packages/client/tests/functional/raw-queries/mysql-column-type/_matrix.ts new file mode 100644 index 000000000000..3585f7b579f9 --- /dev/null +++ b/packages/client/tests/functional/raw-queries/mysql-column-type/_matrix.ts @@ -0,0 +1,4 @@ +import { defineMatrix } from '../../_utils/defineMatrix' +import { Providers } from '../../_utils/providers' + +export default defineMatrix(() => [[{ provider: Providers.MYSQL }]]) diff --git a/packages/client/tests/functional/raw-queries/mysql-column-type/prisma/_schema.ts b/packages/client/tests/functional/raw-queries/mysql-column-type/prisma/_schema.ts new file mode 100644 index 000000000000..f65abf2a17a9 --- /dev/null +++ b/packages/client/tests/functional/raw-queries/mysql-column-type/prisma/_schema.ts @@ -0,0 +1,22 @@ +import { idForProvider } from '../../../_utils/idForProvider' +import testMatrix from '../_matrix' + +export default testMatrix.setupSchema(({ provider }) => { + return /* Prisma */ ` + generator client { + provider = "prisma-client-js" + output = "../generated/prisma/client" + } + + datasource db { + provider = "${provider}" + } + + model User { + id ${idForProvider(provider)} + char_bin_collation String @db.Char(191) + varchar_bin_collation String @db.VarChar(191) + text_bin_collation String @db.Text + } + ` +}) diff --git a/packages/client/tests/functional/raw-queries/mysql-column-type/test.ts b/packages/client/tests/functional/raw-queries/mysql-column-type/test.ts new file mode 100644 index 000000000000..e3933b09476f --- /dev/null +++ b/packages/client/tests/functional/raw-queries/mysql-column-type/test.ts @@ -0,0 +1,47 @@ +import { Providers } from '../../_utils/providers' +import testMatrix from './_matrix' +// @ts-ignore +import type { PrismaClient } from './generated/prisma/client' + +declare let prisma: PrismaClient + +testMatrix.setupTestSuite( + () => { + beforeAll(async () => { + // Prisma Schema does not support specifying column collation + await prisma.$executeRaw`ALTER TABLE \`User\` MODIFY \`char_bin_collation\` CHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL` + await prisma.$executeRaw`ALTER TABLE \`User\` MODIFY \`varchar_bin_collation\` VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL` + await prisma.$executeRaw`ALTER TABLE \`User\` MODIFY \`text_bin_collation\` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL` + }) + + beforeEach(async () => { + await prisma.user.deleteMany() + }) + + test('columns with _bin collation return strings, not Uint8Array', async () => { + await prisma.user.create({ + data: { + char_bin_collation: 'hello', + varchar_bin_collation: 'hello', + text_bin_collation: 'hello', + }, + }) + + const result = + (await prisma.$queryRaw`SELECT \`char_bin_collation\`, \`varchar_bin_collation\`, \`text_bin_collation\` FROM \`User\``) as Array< + Record + > + + expect(result).toHaveLength(1) + expect(result[0].char_bin_collation).toBe('hello') + expect(result[0].varchar_bin_collation).toBe('hello') + expect(result[0].text_bin_collation).toBe('hello') + }) + }, + { + optOut: { + from: [Providers.POSTGRESQL, Providers.SQLITE, Providers.MONGODB, Providers.COCKROACHDB, Providers.SQLSERVER], + reason: 'This test is for MySQL-specific column type detection', + }, + }, +)