If you’ve ever written raw SQL queries inside TypeScript template literals, you probably love Treesitter’s syntax highlighting for embedded languages like:
const result = await sql`SELECT * FROM users`;
But what happens when your code starts getting fancy — say, using generic calls, non-null assertions, or different helper functions like tx.sql or sql<User>?
By default, Treesitter doesn’t inject SQL highlighting in these more complex patterns.
In this post, we’ll fix that by writing a custom Treesitter injection query to handle generic TypeScript expressions with sql and tx.
The Problem
Neovim’s Treesitter injection for SQL works great in the simplest case:
const users = await sql`SELECT * FROM users`;
…but once you introduce TypeScript generics or non-null assertions, highlighting breaks:
// Generic instantiation
const result = await sql<User>`SELECT * FROM users`;
// Non-null expression
const result = (await sql<User>!)`SELECT * FROM users`;
The reason?
Treesitter doesn’t recognize that sql<User> (an instantiation_expression) still represents a function call for the purpose of language injection.
The Fix: Custom Treesitter Injection Query
To solve this, we extend Neovim’s built-in SQL injection rules using a custom .scm query file.
Create a file at:
~/.config/nvim/queries/typescript/injections.scm
Paste this code:
;; Extend built-in injections
(call_expression
function: (non_null_expression
(instantiation_expression
(await_expression
(identifier) @_name)))
arguments: (template_string) @injection.content
(#eq? @_name "sql")
(#set! injection.language "sql")
(#set! injection.include-children))
;; Handle simpler variants like sql`...` and await sql`...`
(call_expression
function: [
(identifier) @_name
(await_expression (identifier) @_name)
(instantiation_expression function: (identifier) @_name)
]
arguments: (template_string) @injection.content
(#eq? @_name "sql")
(#set! injection.language "sql")
(#set! injection.include-children))
;; Support tx`...` and tx<User>`...`
(call_expression
function: (non_null_expression
(instantiation_expression
(await_expression
(identifier) @_name)))
arguments: (template_string) @injection.content
(#eq? @_name "tx")
(#set! injection.language "sql")
(#set! injection.include-children))
;; Handle simpler variants for tx as well
(call_expression
function: [
(identifier) @_name
(await_expression (identifier) @_name)
(instantiation_expression function: (identifier) @_name)
]
arguments: (template_string) @injection.content
(#eq? @_name "tx")
(#set! injection.language "sql")
(#set! injection.include-children))
What This Query Does
Let’s break it down.
Treesitter parses TypeScript code into syntax nodes like:
-
call_expression— a function call (sql\...``) -
instantiation_expression— a generic function call (sql<User>) -
await_expression— forawait sql\...`` -
template_string— your backticked SQL string
We’re telling Treesitter:
“Whenever you see a
call_expressionwhose function name issqlortx, treat the argument (a template string) as SQL content.”
The #set! injection.language "sql" command tells Neovim to apply SQL highlighting within that string.
We also include multiple variations to cover:
-
sql\...`` -
await sql\...`` -
sql<User>\...`` -
await sql<User>\...`` -
tx\...`` -
await tx<User>\...``
Testing It
Once you’ve saved the query, restart Neovim and open a TypeScript file containing SQL template strings.
Try all these patterns:
const a = sql`SELECT * FROM users`;
const b = await sql`SELECT * FROM posts`;
const c = await sql<User>`SELECT * FROM users`;
const d = await tx<Post>`SELECT * FROM posts`;
const e = tx`SELECT * FROM comments`;
If everything worked, your SQL inside backticks should now be properly highlighted in all cases.
Bonus Tip: Reload Queries Without Restarting Neovim
You can quickly reload queries after editing them using:
:TSBufReload
or restart Treesitter highlighting manually with:
:edit
Conclusion
By extending Treesitter’s injection rules, we’ve made Neovim smart enough to recognize complex TypeScript SQL calls — even with generics, awaits, or alternate function names.
This small tweak can massively improve readability and developer experience when working with query-heavy TypeScript codebases.
TL;DR
- Create
queries/typescript/injections.scm - Paste the SQL injection query above
- Enjoy perfect SQL highlighting for all your
sqlandtxtemplate strings
Would you like me to include a short section on how to debug Treesitter queries (e.g., using :InspectTree or :TSPlaygroundToggle) before you publish it? It’s a good addition for readers who want to tweak it further.
Top comments (1)
Neovim’s default Treesitter SQL highlighting breaks with TypeScript generics or non-null assertions like sql or await sql!. Fix it by creating queries/typescript/injections.scm with a custom injection query that tells Treesitter to treat sql or tx template strings as SQL, covering all variants including generics and await calls. After saving, reload with :TSBufReload to get full SQL highlighting.