Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature request: add on conflict set to dynamic insert query helper #217

Closed
alextes opened this issue Aug 24, 2021 · 15 comments
Closed

feature request: add on conflict set to dynamic insert query helper #217

alextes opened this issue Aug 24, 2021 · 15 comments

Comments

@alextes
Copy link
Contributor

alextes commented Aug 24, 2021

I have a query like so:

const insertAnalysisT = () => sql`
	  INSERT INTO base_fees_per_block (
		    hash,
		    number
	  )
	  VALUES (
		    ${block.hash},
		    ${block.number}
	  )
	  ON CONFLICT (number) DO UPDATE
	  SET
		    hash = ${block.hash},
		    number = ${block.number}
`;

Because of the conflict but I can't use the dynamic query helper. A helper that simply sets all columns that are inserted on a conflict with a specifiable list of columns (in this case just number but could be multiple), would be quite flexible and for me helpful. This query is a lot longer in actuality.

If there are others (👍 ) it may be interesting to implement!

@porsager
Copy link
Owner

porsager commented Aug 27, 2021

Should by possible by doing this 🙂

const insertAnalysisT = () => sql`
  INSERT INTO base_fees_per_block 
    ${ sql(block) }
  ON CONFLICT (number) DO UPDATE
  SET ${ sql(block) }
`

@antonbrams
Copy link

unfortunatelly it doesn't work for me

const key = 'id'
const table = 'orders_status'
const rows = [
	{id: 2, title: 'test1'},
	{id: 3, title: 'test2'},
]
sql`
	INSERT INTO ${sql(table)} ${sql(rows)}
	ON CONFLICT (${key}) DO UPDATE
	SET ${sql(rows)}
`

even if i hard code the conflict id, it throws an error
"PostgresError: column "0" of relation "orders_status" does not exist"

what am i doing wrong?

@alextes
Copy link
Contributor Author

alextes commented Jun 15, 2022

I'm not sure but I think the issue is with the conflict clause:

ON CONFLICT (${key}) DO UPDATE

you're supposed to provide the name of the column, I believe for column names the syntax would be

`${sql(key)}`

Maybe. You can try ON CONFLICT (id) DO UPDATE to confirm that's what is going wrong. You can also use the debug option to see what SQL is actually being sent which might make the problem obvious. You can see a short description of it here: https://github.com/porsager/postgres#all-postgres-options

@antonbrams
Copy link

even if i hard code the conflict id, it throws an error

i've done it already :( and this was my error

"PostgresError: column "0" of relation "orders_status" does not exist"

@porsager
Copy link
Owner

You're missing sql() around key 😊

@antonbrams
Copy link

You're missing sql() around key 😊

i've tested it too... but please read carefully, what i've wrote - even this configuration

sql`
	INSERT INTO ${sql(table)} ${sql(rows)}
	ON CONFLICT (id) DO UPDATE
	SET ${sql(rows)}
`

throws an error "PostgresError: column "0" of relation "orders_status" does not exist"

@porsager
Copy link
Owner

porsager commented Jun 15, 2022 via email

@antonbrams
Copy link

no prob <3
"postgres": "^3.2.4"

@porsager
Copy link
Owner

porsager commented Jun 15, 2022

Ah I see.. you can't supply an array (multiple rows) to update set like for inserts. You can only supply an object for a single row, so your query will work like that if you just have one row - eg.

sql`
  INSERT INTO ${sql(table)} ${ sql(row) }
  ON CONFLICT (${ sql(key) }) DO UPDATE
  SET ${ sql(row) }
`

If you want multiple rows to work and still be completely dynamic around columns you have to use fragments and then you can do it like this by using excluded.:

sql`
  INSERT INTO ${sql('test')} ${sql(rows)}
  ON CONFLICT (${ sql(key) }) DO UPDATE
  SET ${ 
    Object.keys(rows[0]).map((x, i) =>
      sql`${ i ? sql`,` : sql``}${ sql(x) } = excluded.${ sql(x) }`
    )
  }
`

@antonbrams
Copy link

antonbrams commented Jun 15, 2022

If you want multiple rows to work and still be completely dynamic around columns you have to use fragments and then you can do it like this by using excluded.:

sql`
  INSERT INTO ${sql('test')} ${sql(rows)}
  ON CONFLICT (${ sql(key) }) DO UPDATE
  SET ${ 
    Object.keys(rows[0]).map((x, i) =>
      sql`${ i ? sql`,` : sql``}${ sql(x) } = excluded.${ sql(x) }`
    )
  }
`

jesus..... this is such a crazy syntax :D
thanks, i'll try tomorrow!

@porsager
Copy link
Owner

porsager commented Jun 15, 2022

Crazy indeed, but it's whats keeping you safe from sql injections 😝

For your sample above it generates the following query:

INSERT INTO "test" ("id","title")values($1,$2),($3,$4)
ON CONFLICT ("id") DO UPDATE
SET  "id" = excluded."id", "title" = excluded."title"

@antonbrams
Copy link

thanks! It works!

@karlhorky
Copy link
Contributor

karlhorky commented Jun 11, 2023

Nice, this syntax for excluded. keys is great for INSERT INTO ... ON CONFLICT queries (upserts)

Here's another version with more expressive variable names and TypeScript:

export async function upsertBenefitsUnauthenticated(benefits: BenefitInput[]) {
  return await sql<Benefit[]>`
    INSERT INTO benefits
      ${sql(benefits)}
    ON CONFLICT
      (id)
    DO UPDATE SET
      ${Object.keys(benefits[0]!).map((field, count) =>
        sql`${count > 0 ? sql`,` : sql``} ${sql(field)} = excluded.${sql(field)}`,
      )}
    RETURNING
      *
  `;
}

@karlhorky
Copy link
Contributor

karlhorky commented Jun 11, 2023

Oh interesting, this query above is giving me a TypeScript error though, hm... 🤔 Wonder if the types for ParameterOrFragment or sql() are missing something cc @Minigugus

Argument of type 'PendingQuery<Row[]>[]' is not assignable to parameter of type 'ParameterOrFragment<never>'.
  Type 'PendingQuery<Row[]>[]' is not assignable to type 'readonly SerializableParameter<never>[]'.
    Type 'PendingQuery<Row[]>' is not assignable to type 'SerializableParameter<never>'.
      Type 'PendingQuery<Row[]>' is missing the following properties from type 'Uint8Array': BYTES_PER_ELEMENT, buffer, byteLength, byteOffset, and 26 more.ts(2345)
Screenshot 2023-06-11 at 15 55 46

@karlhorky
Copy link
Contributor

Apparently it's a problem with the ParameterOrFragment type, opened PR #613 to fix

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants