Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/adapter-better-sqlite3/src/conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,10 @@ export function mapRow(row: Row, columnTypes: ColumnType[]): ResultValue[] {
continue
}

// Convert bigint to string as we can only use JSON-encodable types here.
// Convert bigint values to numbers when safe, otherwise use strings.
if (typeof value === 'bigint') {
result[i] = value.toString()
const asNumber = Number(value)
result[i] = Number.isSafeInteger(asNumber) ? asNumber : value.toString()
continue
}

Expand Down
38 changes: 27 additions & 11 deletions packages/adapter-mariadb/src/mariadb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ class MariaDbQueryable<Connection extends mariadb.Pool | mariadb.Connection> imp
typeCast,
}
const values = args.map((arg, i) => mapArg(arg, query.argTypes[i]))
return await this.client.query(req, values)
// We intentionally use `execute` here, because it uses the binary protocol, unlike `query`.
return await this.client.execute(req, values)
} catch (e) {
const error = e as Error
this.onError(error)
Expand All @@ -76,9 +77,11 @@ class MariaDbQueryable<Connection extends mariadb.Pool | mariadb.Connection> imp
}
}

// All transaction operations use `client.query` instead of `client.execute` to avoid using the
// binary protocol, which does not support transactions in the MariaDB driver.
class MariaDbTransaction extends MariaDbQueryable<mariadb.Connection> implements Transaction {
constructor(
conn: mariadb.Connection,
readonly conn: mariadb.Connection,
readonly options: TransactionOptions,
readonly cleanup?: () => void,
) {
Expand All @@ -88,27 +91,39 @@ class MariaDbTransaction extends MariaDbQueryable<mariadb.Connection> implements
async commit(): Promise<void> {
debug(`[js::commit]`)

this.cleanup?.()
await this.client.end()
try {
await this.client.query({ sql: 'COMMIT' })
} catch (err) {
this.onError(err)
} finally {
this.cleanup?.()
await this.client.end()
}
}

async rollback(): Promise<void> {
debug(`[js::rollback]`)

this.cleanup?.()
await this.client.end()
try {
await this.client.query({ sql: 'ROLLBACK' })
} catch (err) {
this.onError(err)
} finally {
this.cleanup?.()
await this.client.end()
}
}

async createSavepoint(name: string): Promise<void> {
await this.executeRaw({ sql: `SAVEPOINT ${name}`, args: [], argTypes: [] })
await this.client.query({ sql: `SAVEPOINT ${name}` }).catch(this.onError.bind(this))
}

async rollbackToSavepoint(name: string): Promise<void> {
await this.executeRaw({ sql: `ROLLBACK TO ${name}`, args: [], argTypes: [] })
await this.client.query({ sql: `ROLLBACK TO ${name}` }).catch(this.onError.bind(this))
}

async releaseSavepoint(name: string): Promise<void> {
await this.executeRaw({ sql: `RELEASE SAVEPOINT ${name}`, args: [], argTypes: [] })
await this.client.query({ sql: `RELEASE SAVEPOINT ${name}` }).catch(this.onError.bind(this))
}
}

Expand Down Expand Up @@ -143,7 +158,7 @@ export class PrismaMariaDbAdapter extends MariaDbQueryable<mariadb.Pool> impleme

async startTransaction(isolationLevel?: IsolationLevel): Promise<Transaction> {
const options: TransactionOptions = {
usePhantomQuery: false,
usePhantomQuery: true,
}

const tag = '[js::startTransaction]'
Expand All @@ -169,7 +184,8 @@ export class PrismaMariaDbAdapter extends MariaDbQueryable<mariadb.Pool> impleme
argTypes: [],
})
}
await tx.executeRaw({ sql: 'BEGIN', args: [], argTypes: [] })
// Uses `query` instead of `execute` to avoid the binary protocol.
await tx.conn.query({ sql: 'BEGIN' }).catch(this.onError.bind(this))
return tx
} catch (error) {
await conn.end()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineMatrix } from '../../_utils/defineMatrix'
import { Providers } from '../../_utils/providers'

export default defineMatrix(() => [[{ provider: Providers.MYSQL }]])
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { idForProvider } from '../../../_utils/idForProvider'
import testMatrix from '../_matrix'

export default testMatrix.setupSchema(({ provider }) => {
return /* Prisma */ `
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "${provider}"
}

model AssetAccount {
assetId ${idForProvider(provider)}
amount Decimal @default(0) @db.Decimal(30, 0)
}
`
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import testMatrix from './_matrix'
// @ts-ignore
import type { Prisma as PrismaNamespace, PrismaClient } from './generated/prisma/client'

declare let prisma: PrismaClient
declare let Prisma: typeof PrismaNamespace

testMatrix.setupTestSuite(
() => {
test('preserves precision for large decimal values', async () => {
await prisma.assetAccount.create({
data: {
assetId: '1',
amount: new Prisma.Decimal('0'),
},
})

await expect(
prisma.assetAccount.update({
where: { assetId: '1' },
data: { amount: { increment: new Prisma.Decimal('5000000000000000000000000000') } },
}),
).resolves.toMatchObject({
assetId: '1',
amount: new Prisma.Decimal('5000000000000000000000000000'),
})

// First withdrawal
await expect(
prisma.assetAccount.update({
where: { assetId: '1' },
data: { amount: { decrement: new Prisma.Decimal('1000000000000000000000') } },
}),
).resolves.toMatchObject({
assetId: '1',
amount: new Prisma.Decimal('4999999000000000000000000000'),
})

await expect(
prisma.assetAccount.update({
where: { assetId: '1' },
data: { amount: { decrement: new Prisma.Decimal('1000000000000000000000') } },
}),
).resolves.toMatchObject({
assetId: '1',
amount: new Prisma.Decimal('4999998000000000000000000000'),
})
})
},
{
optOut: {
from: ['sqlserver', 'cockroachdb', 'mongodb', 'postgresql', 'sqlite'],
reason:
'This test is specific to MySQL/MariaDB and the precision loss issue with large decimal values. It does not apply to other databases that do not have this issue.',
},
skipDriverAdapter: {
from: ['js_planetscale'],
reason: 'This issue currently cannot be addressed in Planetscale due to limitations of their API',
},
},
)
5 changes: 3 additions & 2 deletions packages/client/tests/functional/tracing/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,9 @@ testMatrix.setupTestSuite(
const usesJsDrivers = driverAdapter !== undefined || clientEngineExecutor === 'remote'

const usesSyntheticTxQueries =
(driverAdapter !== undefined && ['js_d1', 'js_libsql', 'js_planetscale', 'js_mssql'].includes(driverAdapter)) ||
(clientEngineExecutor === 'remote' && provider === Providers.SQLSERVER)
(driverAdapter !== undefined &&
['js_d1', 'js_libsql', 'js_planetscale', 'js_mssql', 'js_mariadb'].includes(driverAdapter)) ||
(clientEngineExecutor === 'remote' && [Providers.SQLSERVER, Providers.MYSQL].includes(provider))

beforeEach(async () => {
await prisma.$connect()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ testMatrix.setupTestSuite(

test('float - output', async () => {
const result = await prisma.$queryRawTyped(sql.getFloat(id))
expect(result[0].float).toBe(12.3)
// Account for potential precision loss when storing the value as a FLOAT
expect(result[0].float).toBeOneOf([new Float32Array([12.3])[0], 12.3])
expectTypeOf(result[0].float).toEqualTypeOf<number | null>()
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ testMatrix.setupTestSuite(

test('float - output', async () => {
const result = await prisma.$queryRawTyped(sql.getFloat(id))
expect(result[0].float).toBe(12.3)
// Account for potential precision loss when storing the value as a FLOAT
expect(result[0].float).toBeOneOf([new Float32Array([12.3])[0], 12.3])
expectTypeOf(result[0].float).toBeNumber()
})

Expand Down
91 changes: 91 additions & 0 deletions packages/client/tests/functional/unixepoch-ms-datetime/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,97 @@ testMatrix.setupTestSuite(
// These two queries are going to be compacted together and run as one.
await expect(Promise.all([find(), find()])).resolves.toMatchObject([created, created])
})

test('findUnique() returns valid Date when createdAt is stored as unix millis directly', async () => {
const prisma = createClient(info, driverAdapter)

const uuid = randomUUID()
const nowMillis = Date.now()

await prisma.$executeRaw`
INSERT INTO Event (name, uuid, createdAt)
VALUES ('event', ${uuid}, ${nowMillis})
`

const found = await prisma.event.findFirst({
where: {
uuid,
},
})

expect(found?.createdAt).toBeInstanceOf(Date)
expect(isNaN(found!.createdAt.getTime())).toBe(false)
})

test('aggregate() returns valid Date when unix millis are stored directly', async () => {
const prisma = createClient(info, driverAdapter)

const uuid = randomUUID()
const nowMillis = Date.now()

await prisma.$executeRaw`
INSERT INTO Event (name, uuid, createdAt)
VALUES ('event', ${uuid}, ${nowMillis})
`

const agg = await prisma.event.aggregate({
_min: { createdAt: true },
_max: { createdAt: true },
})

expect(agg._min.createdAt).toBeInstanceOf(Date)
expect(isNaN(agg._min.createdAt!.getTime())).toBe(false)
expect(agg._max.createdAt).toBeInstanceOf(Date)
expect(isNaN(agg._max.createdAt!.getTime())).toBe(false)
})

test('manually created INTEGER DateTime column returns valid Date values', async () => {
const prisma = createClient(info, driverAdapter)

await prisma.$executeRaw`
DROP TABLE IF EXISTS Event
`

await prisma.$executeRaw`
CREATE TABLE Event (
name TEXT NOT NULL,
uuid TEXT NOT NULL,
createdAt INTEGER NOT NULL DEFAULT (unixepoch('now') * 1000),
PRIMARY KEY (uuid, createdAt)
)
`

const created = await prisma.event.create({
data: {
name: 'event',
},
})

expect(created.createdAt).toBeInstanceOf(Date)
expect(isNaN(created.createdAt.getTime())).toBe(false)

const found = await prisma.event.findUnique({
where: {
uuid_createdAt: {
uuid: created.uuid,
createdAt: created.createdAt,
},
},
})

expect(found?.createdAt).toBeInstanceOf(Date)
expect(isNaN(found!.createdAt.getTime())).toBe(false)

const agg = await prisma.event.aggregate({
_min: { createdAt: true },
_max: { createdAt: true },
})

expect(agg._min.createdAt).toBeInstanceOf(Date)
expect(isNaN(agg._min.createdAt!.getTime())).toBe(false)
expect(agg._max.createdAt).toBeInstanceOf(Date)
expect(isNaN(agg._max.createdAt!.getTime())).toBe(false)
})
},
{
optOut: {
Expand Down
Loading