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

[Pg] Fix jsonb field escaping data before insert

Open LeonAlvarez opened this issue 1 year ago • 2 comments

RN Jsonb column is incorrectly escaping data before inserting it which end up in "{\"external_id\":40}" instead of {"external_id": 40}. Because of that queries using jsonb syntax are not working for example the following query:

SELECT * FROM categories
WHERE (meta_data->>'external_id')::int = 164;

on current drizzle status we are forced to first cast column to jsonb as escaped data is just a string (which can be stored on json be but it really isnt jsonb)

SELECT * FROM categories
WERE ((meta_data #>> '{}')::jsonb->>'external_id')::int= 164;

LeonAlvarez avatar May 30 '23 21:05 LeonAlvarez

The integration tests fail with this change.

I have fixed postgresjs test, now should I extend insertjson with a proper jsonb query like

test.serial('json insert', async (t) => {
	const { db } = t.context;

	await db.insert(usersTable).values({ name: 'John', jsonb: ['foo', 'bar'] });
	const result = await db.select({
		id: usersTable.id,
		name: usersTable.name,
		jsonb: usersTable.jsonb,
	}).from(usersTable);

	t.deepEqual(result, [{ id: 1, name: 'John', jsonb: ['foo', 'bar'] }]);
	const foo = await db.select({
		id: usersTable.id,
		name: usersTable.name,
		jsonb: usersTable.jsonb,
	}).from(usersTable).where(
		and(
			eq(usersTable.name, 'John'),
			sql.raw(`jsonb->>0 = 'foo'`),
		)
	);
	t.deepEqual(foo, [{ id: 1, name: 'John', jsonb: ['foo', 'bar'] }]);
});

Or better to leave just the jsonb query?

test.serial('json insert', async (t) => {
	const { db } = t.context;
	
	await db.insert(usersTable).values({ name: 'John', jsonb: ['foo', 'bar'] });
	const result = await db.select({
		id: usersTable.id,
		name: usersTable.name,
		jsonb: usersTable.jsonb,
	}).from(usersTable).where(
		and(
			eq(usersTable.name, 'John'),
			sql.raw(`jsonb->>0 = 'foo'`),
		)
	);
	
	t.deepEqual(result, [{ id: 1, name: 'John', jsonb: ['foo', 'bar'] }]);
});

This test will fail on by branch for pg driver Is there a way to do mapToDriver by driver?

LeonAlvarez avatar Jun 04 '23 08:06 LeonAlvarez

now should I extend insertjson with a proper jsonb query

sure, go ahead @LeonAlvarez

dankochetov avatar Jun 07 '23 18:06 dankochetov

great! jsonb support is a must <3

oswaldoacauan avatar Jun 10 '23 21:06 oswaldoacauan

For now I'm solving it on user land as there is no simple way to change behaviour of type for each driver on core.

If you are using postgresjs and jsonb simply create a custom jsonb type and use it isntead of the one from pg-core

import { customType } from 'drizzle-orm/pg-core';

const jsonb = customType<{ data: any }>({
  dataType() {
    return 'jsonb';
  },
  toDriver(val) {
    return val as any;
  },
  fromDriver(value) {
    if (typeof value === 'string') {
      try {
        return JSON.parse(value) as any;
      } catch {}
    }
    return value as any;
  },
});

export default jsonb;

LeonAlvarez avatar Jun 22 '23 15:06 LeonAlvarez

Using your workaround @LeonAlvarez , its working great.

stunaz avatar Jun 22 '23 18:06 stunaz

This is what I'm using based on @LeonAlvarez's solution with TS types and based on Drizzle's own example: https://orm.drizzle.team/docs/custom-types (except driverData should be TData not string for it to work)

I assume it will work with 'jsonb' too.

export const customJson = <TData,>(name: string) =>
    customType<{ data: TData; driverData: TData }>({
        dataType() {
            return 'json'
        },
        toDriver(val: TData): TData {
            return val
        },
        fromDriver(value): TData {
            if (typeof value === 'string') {
                try {
                    return JSON.parse(value) as TData
                } catch {}
            }
            return value as TData
        },
    })(name)

ivanfeli avatar Jul 22 '23 21:07 ivanfeli

Any updates? 🥇

nikosantis avatar Sep 26 '23 21:09 nikosantis

Has anyone managed to use the customType or an alternative approach to insert when the column type is jsonb[] ? The customType approach worked for jsonb, but I'm stuck on jsonb[]...

create table test ( id serial primary key, data jsonb, list jsonb[] );

export const test = pgTable("test", { id: serial("id").primaryKey().notNull(), data: jsonb("data"), list: jsonb("list").array(), });

await db.insert(test).values({ data: { name: 'Ali', age: 34 }, list: [ { animal: 'pig', color: 'pink' }, { animal: 'bear', color: 'brown' }, ] });

I can write/read data fine. But getting below on list...

Query: insert into "test" ("id", "data", "list") values (default, $1, $2, $3) -- params: [{"name":"Ali","age":34}, "{[object Object],[object Object]}"] invalid input syntax for type json

realverse avatar Oct 18 '23 17:10 realverse

I don't know if there is a better way but this works fine for me

export const customJsonb = <TData>(name: string) =>
  customType<{ data: TData; driverData: TData }>({
    dataType() {
      return 'jsonb'
    },
    toDriver(val: TData) {
      return sql`(((${JSON.stringify(val)})::jsonb)#>> '{}')::jsonb`
    },
    fromDriver(value): TData {
      return value as TData
    },
  })(name)

and this can convert all the stringifyed JSON

UPDATE table_name
SET column1 = (column1#>> '{}')::jsonb,
    column2 = (column2#>> '{}')::jsonb
;

xlc avatar Nov 14 '23 00:11 xlc

I was going to work on this issue but I noticed this PR. What's the status @LeonAlvarez ? Let me know if you can complete the fix or if you'll prefer me to take over.

Angelelz avatar Nov 28 '23 13:11 Angelelz

Closing this in favor of #1641

Angelelz avatar Dec 13 '23 06:12 Angelelz