Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Gajus Kuizinas
Gajus Kuizinas

Posted on

     

Dynamically generating SQL queries using Node.js

Ever since I have releasedSlonik (PostgreSQL client for Node.js) and written a controversialStop using Knex.js article (tl;dr; query builders are designed to be building blocks for ORMs; they do not add value when majority of the query is static.), I have been asked a lot – then how do I generate dynamic queries? I will answer this by sharing a couple of real-life examples.

All of the queries in this article are actual queries used in real-life business,Applaudience whichheavily relies on PostgreSQL.

Disclaimer: (1) All examples discuss only SQL injection threats. Authorization logic (e.g. whitelisting columns user is authorized to access) is not in the scope of this article. (2) All statements assume there are no bugs in Slonik implementation.

Static query with dynamic value bindings

If your query logic does not change depending on user's input, then simply construct SQL query usingsql tagged template literal, e.g.

sql`  SELECT c1.country_id  FROM cinema_movie_name cmn1  INNER JOIN cinema c1 ON c1.id = cmn1.cinema_id  WHERE cmn1.id =${cinemaMovieNameId}`;
Enter fullscreen modeExit fullscreen mode

If you are using Slonik, it issafe to pass values as template literal placeholders.sql will interpret all placeholder tokens and construct final SQL query. In this case, the only dynamic part of the query is the value bindings themselves, therefore the final query is:

SELECTc1.country_idFROMcinema_movie_namecmn1INNERJOINcinemac1ONc1.id=cmn1.cinema_idWHEREcmn1.id=$1
Enter fullscreen modeExit fullscreen mode

Query and bound values will be sent to PostgreSQL separately: no risk of SQL injection.

Binding a list of values

When your query input is a list of values (e.g. such as when retrieving rows matching multiple identifiers), then youmay usesql.valueList, e.g.

sql`  SELECT m1.*  FROM movie m1  WHERE m1.id IN (${sql.valueList(movieIds)})`;
Enter fullscreen modeExit fullscreen mode

This will generate a query with dynamic set of value bindings, i.e. ifmovieIds is[1, 2, 3] the query that is sent to PostgreSQL will be:

SELECTm1.*FROMmoviem1WHEREm1.idIN($1,$2,$3)
Enter fullscreen modeExit fullscreen mode

However, despite this being a common pattern, I do not advise to use this pattern. Instead, usesql.array, e.g.

sql`  SELECT m1.*  FROM movie m1  WHERE m1.id = ANY(${sql.array(movieIds,'int4')})`;
Enter fullscreen modeExit fullscreen mode

This will generate a fixed-length query that does not change based on its inputs, i.e.

SELECTm1.*FROMmoviem1WHEREm1.id=ANY($1::"int4"[])
Enter fullscreen modeExit fullscreen mode

Continue readingsql.array vssql.valueList.

Query with dynamic columns

If your query result refers to columns that depend on user's input, then usesql.identifier to generate SQL that identifies those columns, e.g.

(Note: Not an actual query used in business. See next paragraph.)

sql`  SELECT m1.id,${sql.identifier(['m1',movieTableColumnName])}  FROM movie m1  WHERE    m1.id =${moveId}`;
Enter fullscreen modeExit fullscreen mode

This query will produce a query that selects exactly 1 dynamically identified column. There is no risk of SQL injection, i.e. even if logic leading to generation ofmovieTableColumnName was somehow compromised, the worst that can happen is that query attacker will be able to return any column underm1 alias or execute query with invalid column identifier values (both carry risk; business logic is not in scope of this article).

Just because you can do this, you probably shouldn't. When your application requires to return different columns depending on user's query, it is better to select all columns that are in scope of the business logic and pick value of the needed column, i.e. If the intent of the latter query was to return a different movie identifier based onmovieTableColumnName, then it is better to write a static query:

sql`  SELECT    m1.id,    m1.foreign_comscore_id,    m1.foreign_imdb_id,    m1.foreign_metacritic_id    m1.foreign_rottentomatoes_id,    m1.foreign_tmdb_id,    m1.foreign_webedia_id  FROM movie m1  WHERE    m1.id =${moveId}`;
Enter fullscreen modeExit fullscreen mode

The latter has does return some superfluous data on every query, but it has several advantages:

  1. It reduces risk of SQL injection (regardless of how much you trust code generation logic, static code is always safer than dynamic code).
  2. It produces only one entrypg_stat_statements. You will learn to appreciate as few as possible queries inpg_stat_statements as your application scales.

Query with multiple dynamic columns

Same as the above, butsql.identifierList.

Nesting dynamic SQL queries

sql tagged template literals can be nested, e.g.

(Note: Simplified version of an actual query used in business.)

constfutureEventEventChangeSqlToken=sql`  SELECT    ec1.event_id,    ec1.seat_count,    ec1.seat_sold_count  FROM event_change_future_event_view ec1`;sql`  SELECT    event_id,    seat_count,    seat_sold_count  FROM (${futureEventEventChangeSqlToken}  ) AS haystack  WHERE${paginatedWhereSqlToken}  ORDER BY${orderSqlToken}  LIMIT${limitSqlToken}`
Enter fullscreen modeExit fullscreen mode

This allows to pass pre-bound SQL queries as first-class citizens across your program. This is handy when the intent is to isolate SQL generation logic for testing or when large SQL fragments are shared between queries or when the intent is to simply reduce concentration of code complexity in one place.

Injecting dynamic SQL fragments

sql.raw is used to inject dynamic SQL fragments, i.e.

sql`  SELECT${sql.raw('foo bar baz')}`
Enter fullscreen modeExit fullscreen mode

translates to (invalid) query:

SELECTfoobarbaz
Enter fullscreen modeExit fullscreen mode

Unlike the previous example usingsql tagged template,sql.raw is not safe – it allows to create dynamic SQL using user input.

There are no known use cases for generating queries usingsql.raw that aren't covered by nesting boundsql expressions (described in "Nesting dynamic SQL queries") or by one of the other existingquery building methods.sql.raw exists as a mechanism to execute externally storedstatic (e.g. queries stored in files).

Query with a dynamic comparison predicate members or operator

If an operator of a comparison predicate present in your query is dynamic, then usesql.comparisonPredicate, e.g.

(Note: Not an actual query used in business.)

sql`  SELECT    c1.id,    c1.nid,    c1.name  FROM cinema c1  WHERE${sql.comparisonPredicate(sql`c1.name`,nameComparisonOperator,nameComparisonValue)}`;
Enter fullscreen modeExit fullscreen mode

nameComparisonOperator can be values such as=,>,<, etc. AssumingnameComparisonOperator is "=", then the resulting query is going to be:

SELECTc1.id,c1.nid,c1.nameFROMcinemac1WHEREc1.name=$1
Enter fullscreen modeExit fullscreen mode

The latter is an extremely rare use case, reserved almost entirely to building higher level SQL abstraction tools (such as ORMs). It may be useful for "advance search" scenarios, however continue reading to familiarise with alternative patterns (seesql.booleanExpression).

Query with dynamic WHERE clause members

If presence ofWHERE clause members is dynamic, then usesql.booleanExpression.

constfindCinemas=(root,parameters,context)=>{constbooleanExpressions=[sql`TRUE`,];if(parameters.input.query){constquery=parameters.input.query;if(query.countryId!==undefined){booleanExpressions.push(sql`c2.id =${query.countryId}`);}if(query.nid!==undefined){booleanExpressions.push(sql`c1.nid %${query.nid}`);}if(query.name!==undefined){booleanExpressions.push(sql`c1.name %${query.name}`);}}constwhereSqlToken=sql.booleanExpression(booleanExpressions,'AND');returncontext.pool.any(sql`    SELECT      c1.id,      c1.nid,      c1.name,      c2.code_alpha_2 country_code,      c2.name country_name    FROM cinema c1    INNER JOIN country c2 ON c2.id = c1.country_id    WHERE${whereSqlToken}  `);},
Enter fullscreen modeExit fullscreen mode

findCinemas is an implementation of a GraphQL resolver. WHERE clause of the query is constructed using a combination of 3 possible boolean expressions. As is the case with all the other query building methods in Slonik, all expressions can be nested: you can have other boolean expressions as members of a boolean expression or even SQL expression constructed usingsql tagged template literal.

Summary

These examples cover every common dynamic SQL building scenario and provide enough knowledge of how Slonik works to enable reader to continue journey of familiarising withother query building methods provided by Slonik. The primary intent of this article was to demonstrate that Slonik provides a safe abstraction for constructing SQL queries keeping the static parts of the query intact.

If you value my work and want to see Slonik andmany other of my Open-Source projects to be continuously improved, then please consider becoming a patron:

Buy Me A Coffee
Become a Patron

Finally, I missed a use case scenario that you would like me to cover, mention it in the comments and I will happily include it.

Top comments(26)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
ivan_paqmind profile image
Ivan Kleshnin
Experienced software engineer, mentor and entrepreneur. 12+ years in webdev. Generalist. Proponent of minimalism and 改善 (kaizen).
  • Location
    Poland
  • Education
    Computer Systems and Networks
  • Joined
• Edited on• Edited

Is it ok to usesql.raw to create a fully dynamicORDER BY query?

letfield="createdAt"letdir="DESC"// generated like field.startsWith("+") ? "ASC" : "DESC"letorderToken=sql`  ORDER BY${sql.identifier(field)}${sql.raw(dir)}'`letq=sql`  SELECT * FROM "post"${orderToken}  LIMIT 3`
Enter fullscreen modeExit fullscreen mode
CollapseExpand
 
gajus profile image
Gajus Kuizinas
Open-source engineer interested in JavaScript, PostgreSQL and DevOps. Follow me on Twitter for outbursts about startups & engineering.
• Edited on• Edited

You don't need to:sql tags can be nested.

letfield='createdAt';letdir=field.startsWith('+')?sql`ASC`:sql`DESC`;letorderToken=sql`  ORDER BY${sql.identifier([field])}${dir}'`;letq=sql`  SELECT * FROM "post"${orderToken}  LIMIT 3`;
CollapseExpand
 
ivan_paqmind profile image
Ivan Kleshnin
Experienced software engineer, mentor and entrepreneur. 12+ years in webdev. Generalist. Proponent of minimalism and 改善 (kaizen).
  • Location
    Poland
  • Education
    Computer Systems and Networks
  • Joined
• Edited on• Edited

Thanks. What about optionalORDER BY? ForWHERE you started withTRUE so the absence of conditions results inWHERE (true) which is syntactically correct and has no performance implications. I can't find anything than can be used to order by default:

ORDER BY ??? default ???
Enter fullscreen modeExit fullscreen mode

Another attempt with?: breaks the placeholder ordering in query:

SELECT ${makeProjection(pick)} FROM ${sql.identifier([table])}WHERE ${makeWhere(filter)}${sort ? sql`ORDER BY ${makeOrder(sort)}` : ``} -- doesn't workLIMIT ${makeLimit(limit)}OFFSET ${makeOffset(offset)}
Enter fullscreen modeExit fullscreen mode
syntax error at or near "$1"
Enter fullscreen modeExit fullscreen mode
Thread Thread
 
gajus profile image
Gajus Kuizinas
Open-source engineer interested in JavaScript, PostgreSQL and DevOps. Follow me on Twitter for outbursts about startups & engineering.
${sort ? sql`ORDER BY ${makeOrder(sort)}` : sql``}
Enter fullscreen modeExit fullscreen mode
Thread Thread
 
ivan_paqmind profile image
Ivan Kleshnin
Experienced software engineer, mentor and entrepreneur. 12+ years in webdev. Generalist. Proponent of minimalism and 改善 (kaizen).
  • Location
    Poland
  • Education
    Computer Systems and Networks
  • Joined

Causes

InvalidInputError: Unexpected SQL input. Query cannot be empty.
Thread Thread
 
gajus profile image
Gajus Kuizinas
Open-source engineer interested in JavaScript, PostgreSQL and DevOps. Follow me on Twitter for outbursts about startups & engineering.
sql`--`
Thread Thread
 
gajus profile image
Gajus Kuizinas
Open-source engineer interested in JavaScript, PostgreSQL and DevOps. Follow me on Twitter for outbursts about startups & engineering.

I cannot recall the original reason for this constraint. Will likely remove this constraint. Trackgithub.com/gajus/slonik/issues/93.

CollapseExpand
 
ivan_paqmind profile image
Ivan Kleshnin
Experienced software engineer, mentor and entrepreneur. 12+ years in webdev. Generalist. Proponent of minimalism and 改善 (kaizen).
  • Location
    Poland
  • Education
    Computer Systems and Networks
  • Joined
• Edited on• Edited

Another question. WhyvalueList and other similar functions support only primitive values?

Is TypeScript a reason? The thing I like aboutpg driver is that, unlike MySQL, I canSELECT orINSERT a date, a boolean, an object, etc. value an it will do "the right thing" without an extra hassle.

Maybe it's not entirely safe or somehow "not a good practice" – I dunno.
For now, the API feels limiting for no obvious reason.

CollapseExpand
 
gajus profile image
Gajus Kuizinas
Open-source engineer interested in JavaScript, PostgreSQL and DevOps. Follow me on Twitter for outbursts about startups & engineering.

Value token can be any primitive that is shared between pg and JavaScript (string, integer, float, boolean, null).

If you have a requirement for automatic coalescing of other object types (such as Date), I suggest raising an issue. I cannot think of other types that would be relatively safe to cast, though.

CollapseExpand
 
ivan_paqmind profile image
Ivan Kleshnin
Experienced software engineer, mentor and entrepreneur. 12+ years in webdev. Generalist. Proponent of minimalism and 改善 (kaizen).
  • Location
    Poland
  • Education
    Computer Systems and Networks
  • Joined

JS Object withJSON.stringify?

Thread Thread
 
gajus profile image
Gajus Kuizinas
Open-source engineer interested in JavaScript, PostgreSQL and DevOps. Follow me on Twitter for outbursts about startups & engineering.

Too many edges cases where an object could be passed accidentally inserting potentially sensitive data to the database.

There issql.json() for that, though.

CollapseExpand
 
cvh23 profile image
cvh23
  • Joined

Slonik's approach is very interesting. Although it's maybe not directly related to Slonik, but because we don't use an ORM:What would be the best way for transforming a SELECT query result of rows into an array of nested objects? For instance if we are joining multiple tables and have a master-detail-relationship. The web client usually expects some kind of nested JSON structure.

CollapseExpand
 
gajus profile image
Gajus Kuizinas
Open-source engineer interested in JavaScript, PostgreSQL and DevOps. Follow me on Twitter for outbursts about startups & engineering.

You can return JSON from PostgreSQL query.

CollapseExpand
 
cvh23 profile image
cvh23
  • Joined

So your recommendation is to do everything in PostgreSQL with its JSON functions? So we already have the ready-to-use object or array of objects as query result?

Thread Thread
 
gajus profile image
Gajus Kuizinas
Open-source engineer interested in JavaScript, PostgreSQL and DevOps. Follow me on Twitter for outbursts about startups & engineering.

Not at all. I think it is entirely unnecessary. Keep the queries simple. If you need to nest objects, either use frameworks that automate nesting (e.g. GraphQL), or write simple iteration routines to amend the data structures.

Thread Thread
 
cvh23 profile image
cvh23
  • Joined

Sounds good, thank you! Do you know of any application which uses Slonik + GraphQL and is open source, so that I can study this approach?

CollapseExpand
 
5422m4n profile image
Sven Kanoldt
polyglot software engineer | #rustlang enthusiast | co-organizer at @RustMunich | maintainer of #cargogenerate | creator of @t_rec_rs and stegano-rs
  • Location
    Munich
  • Education
    MSc in Computer Science
  • Joined

Seems thatsql.valueList does not exist anymore. Also the link above providedContinue reading sql.array vs sql.valueList. does not lead to the right section of the README in the repo.

Was this feature removed lately?

CollapseExpand
 
paulovieira profile image
Paulo Vieira
  • Location
    Lisbon, Portugal
  • Joined

Hello Gajus,

I've been experimenting with Slonik for the first time today. Seems interesting, I'm very much aligned with the philosophy of "just write plain old sql". This article was useful, especially the final example. Thanks.

CollapseExpand
 
ivan_paqmind profile image
Ivan Kleshnin
Experienced software engineer, mentor and entrepreneur. 12+ years in webdev. Generalist. Proponent of minimalism and 改善 (kaizen).
  • Location
    Poland
  • Education
    Computer Systems and Networks
  • Joined
• Edited on• Edited

What's the difference betweentuple andvalueList? The only one I see is thattuple adds( and) parens around its values... The implementation and types are almost identical tovalueList, to the point I wonder why not to express one in terms of another or just ditchtuple as unnecessary. Most probably I miss something – that's just my first impression.

CollapseExpand
 
gajus profile image
Gajus Kuizinas
Open-source engineer interested in JavaScript, PostgreSQL and DevOps. Follow me on Twitter for outbursts about startups & engineering.

Eventuallysql.valueList,sql.rawList,sql.tupleList,sql.identifierList andsql.booleanExpression will be removed in favour of a single methodsql.expressionList.

github.com/gajus/slonik/issues/91

The original intention for separation was to force semantic resemblance and type strictness specific to the code fragment being generated. However, since then types have evolved for all of these helpers to allow a lot broader spectrum of allowed values.

CollapseExpand
 
baerrach profile image
Barrie Treloar
  • Joined

sql.booleanExpression was removed in favour ofsql.join but the article has not been updated.

Seefeat: remove multiple methods in favor of sql.join

The doc link is nowgithub.com/gajus/slonik#slonik-que...

CollapseExpand
 
sajedulkarim profile image
Sajedul karim
Software Engineer
  • Joined

I am fetching issue where I have to generate dynamic ORDER BY clause for multiple columns.
Here is my sample query:

SELECT *FROM userWHERE gender = 'male'  ORDER BY created_at ASC, updated_at DESCLIMIT 10 OFFSET 0;
Enter fullscreen modeExit fullscreen mode

My Try:

const orderBy: string = 'ORDER BY created_at ASC, updated_at DESC';SELECT *FROM userWHERE gender = 'male'  ${orderBy}LIMIT 10 OFFSET 0;
Enter fullscreen modeExit fullscreen mode

Here, if I pass ORDER BY clause as ${orderBy}, then it take it as a value. It shows error.

The generated sql is like below:

{  sql: "\n  SELECT *\nFROM user\nWHERE gender = 'male'\n  $1\nLIMIT 10 OFFSET 0;\n",  type: 'SLONIK_TOKEN_SQL',  values: [ 'ORDER BY created_at ASC, updated_at DESC' ]}
Enter fullscreen modeExit fullscreen mode

Please help me.

CollapseExpand
 
craigmichaelmartin profile image
craig martin
  • Joined

Hey Garjus - this is the first time I'm coming across your articles/project. Looks really interesting! I look foward to digging in :) I would love to hear your thoughts ongithub.com/craigmichaelmartin/pure... - apure ORM (no query builder dimension) which allows you to write regular native SQL and receive back properly nested/structured nestedpure (not db-aware stateful) business objects.

CollapseExpand
 
ackvf profile image
Vítězslav Ackermann Ferko
React.js developer since 2015
  • Location
    Czech Republic
  • Joined

Where have all the sql builder functions go and how to replace them?
comparisonPredicate, valueList, etc.

I am trying to construct multiple WHERE statements dynamically, but I get all sorts of errors. The thing is, some of them might be undefined and thus ommited, but then sql builder yells that it's not a valid value

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Open-source engineer interested in JavaScript, PostgreSQL and DevOps. Follow me on Twitter for outbursts about startups & engineering.
  • Location
    Miami, FL
  • Work
    Contra
  • Joined

More fromGajus Kuizinas

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp