drizzle-orm icon indicating copy to clipboard operation
drizzle-orm copied to clipboard

[BUG]: `Decimal` type gets casted to `number` when queried through `with` (include relations)

Open ghost opened this issue 1 year ago • 12 comments

What version of drizzle-orm are you using?

0.28.6

What version of drizzle-kit are you using?

0.19.13

Describe the Bug

With those two tables and relations:

export const processes = mysqlTable('processes', {
    id: varchar('id', { length: 128 })
        .primaryKey()
        .$defaultFn(() => createId()),
});

export const processesRelations = relations(processes, ({ one, many }) => ({
    processesInputs: many(processesInputs),
}));

export const processesInputs = mysqlTable('processesInputs', {
    id: varchar('id', { length: 128 })
        .primaryKey()
        .$defaultFn(() => createId()),
    percentage: decimal('percentage', { precision: 20, scale: 10 }).notNull(),
});

export const processesInputsRelations = relations(processesInputs, ({ one }) => ({
    process: one(processes, {
        fields: [processesInputs.processId],
        references: [processes.id],
    }),
}));

If I query the processesInputs table directly, I get percentage as a string:

const inputs = await db.query.processesInputs.findMany({
        where: eq(processesInputs.processId, processId),
    });

// inputs is something like:
[{
  id: 'pm8q3gbyb9uc6kw7klog7zs3',
  percentage: '100.0000000000',
  processId: 'mb9fzdtnhz81bwngwouj0vb1',
}]

But if I query the processes table including the related processesInputs, I get percentage as a number:

const process = await db.query.processes.findFirst({
        where: eq(processes.id, processId),
        with: {
            processesInputs: true,
        },
    });

const inputs = process.processesInputs;

//  inputs is something like:
[{
  id: 'pm8q3gbyb9uc6kw7klog7zs3',
  percentage: 100,
  processId: 'mb9fzdtnhz81bwngwouj0vb1',
}]

Expected behavior

I would expect percentage to be of type string in both cases

Environment & setup

No response

ghost avatar Sep 24 '23 10:09 ghost

Same problem here. Numeric column value gets somehow transformed to number when accessed through with, but the type still says string (as it should be indeed). This is also problematic when precision is important...

Christophevvv avatar Dec 11 '23 14:12 Christophevvv

+1

AChangXD avatar Dec 16 '23 16:12 AChangXD

What driver is giving you this problem?

Angelelz avatar Dec 16 '23 18:12 Angelelz

pg

Christophevvv avatar Dec 16 '23 18:12 Christophevvv

mysql

ghost avatar Dec 17 '23 13:12 ghost

Related to an issue i opened for postgres #1453

cloleb avatar Feb 06 '24 16:02 cloleb

this also happens in numeric with postgres. Seems to be related how drizzle handle relations using with: in the executed query, it uses JSON_AGG and JSON_BUILD_ARRAY, for example:

COALESCE(JSON_AGG(JSON_BUILD_ARRAY(
   "product_variations"."id", 
   "product_variations"."price", 
   "product_variations"."stock", 
   "product_variations"."weight", 
   "product_variations"."thickness_id", 
   "product_variations"."product_id", 
   "product_variations_thickness"."data"
 )), '[]'::json) AS "data" 
 seems that when it is casted to JSON it is converted to JSON numbers.
 

hlspablo avatar Apr 18 '24 15:04 hlspablo

@Angelelz facing this same issue in mysql. I see that there is a PR that has been open for a while to solve this issue. Any reason why it hasn't been merged ? If there is anything that need to be done before it can be merged, I'd like to help.

adiSuper94 avatar May 02 '24 13:05 adiSuper94

+1. This is a bummer with drizzle-zod as Zod schema validation fails against retreived data due to an invalid_type error ("Expected string, received number"); I'm using tRPC and my procedures fail due to this. The only workaround I can think of is .extend()-ing the fields to z.number().or(z.string())[.nullable()]. Drizzle shoots itself in the foot here. Please prioritize this.

hstevanoski avatar Jun 05 '24 17:06 hstevanoski

@Angelelz, spotting the same issue when using numeric(78, 0) with pg driver.

100000000000000000000000000 in the database turns to 100000000000000004764729344 when queried via db.query.

https://github.com/drizzle-team/drizzle-orm/issues/1290#issuecomment-2064307614, @hlspablo's comment seems to point the exact reason why that happens.

dalechyn avatar Jul 06 '24 23:07 dalechyn

this also happens in numeric with postgres. Seems to be related how drizzle handle relations using with: in the executed query, it uses JSON_AGG and JSON_BUILD_ARRAY, for example:

COALESCE(JSON_AGG(JSON_BUILD_ARRAY(
   "product_variations"."id", 
   "product_variations"."price", 
   "product_variations"."stock", 
   "product_variations"."weight", 
   "product_variations"."thickness_id", 
   "product_variations"."product_id", 
   "product_variations_thickness"."data"
 )), '[]'::json) AS "data" 
 seems that when it is casted to JSON it is converted to JSON numbers.

I think you are right. But the problem is in Drizzle, not PG. PG is actually returning the full resolution number in json, but drizzle internally uses JSON.parse for subrelations. Json.parse can't handle high precision numbers. My guess is it's happening here:

https://github.com/drizzle-team/drizzle-orm/blob/c8359a16fff4b05aff09445edd63fc65a7430ce9/drizzle-orm/src/relations.ts#L682

johanneskares avatar Sep 11 '24 15:09 johanneskares

Is there any known workaround?

dennismuench avatar Oct 03 '24 12:10 dennismuench

Overriding the numeric columns inside the extras object and casting everything to text worked for me (pg).

    const invoiceQuery = database.query.invoices.findFirst({
      columns: invoiceParams,
      with: {
        items: {
          columns: invoiceItemParams,
          extras: {
            amount: sql`cast (${invoiceItems.amount} as text)`.as('amount'),
            discount: sql`cast (${invoiceItems.discount} as text)`.as('discount'),
            discountAmount: sql`cast (${invoiceItems.discountAmount} as text)`.as('discountAmount'),
            unitAmount: sql`cast (${invoiceItems.unitAmount} as text)`.as('unitAmount'),
            total: sql`cast (${invoiceItems.total} as text)`.as('total'),
          },
        },
        ...
      },
      where: (t, { and, eq }) => and(
        eq(t.id, sql.placeholder('invoiceId')),
        eq(t.tenantId, sql.placeholder('tenantId')),
      ),
    });

dennismuench avatar Oct 04 '24 04:10 dennismuench