JavaScript in MySQL: Extending SQL, Not Moving the Application
At Percona Live Europe 2026, Dmitry Lenev and I presented JavaScript in My Database? Over My Dead Query, the story of component_js_lang, the V8-based JavaScript engine for Percona Server for MySQL 8.4. That experience told us few things:
- The presentation was seen as “for developers”
- What most DBA or DevOps heard was "JavaScript in the database" = "application logic in the database"
This let most of the people pictured developers shipping their business logic into stored procedures. Order workflows, pricing rules, REST calls, all running inside mysqld.
That picture is wrong, and in this post I want to replace it with the right one, using the same four demos we showed on stage.
The fear is reasonable, the target is wrong
DBAs have earned their skepticism. Many of us have inherited a schema where thousands of lines of stored procedures were the application: impossible to test, impossible to version, impossible to scale out, and owned by nobody who still works there.
So when a new language arrives inside the server, the reflex is to protect the database from becoming an application server again.
We share that reflex.
The point of this post is that JavaScript routines, used as designed, do not move us in that direction. They move the opposite way: they make SQL stronger so that less logic needs to be pulled out of the database and re-implemented somewhere else.
The engine itself also enforces the boundary. Inside component_js_lang a routine has:
- No network I/O. No fetch(), no sockets. A routine cannot call your payment provider or another microservice.
- No file system access. It cannot read configs, keys or files.
- No Node.js or DOM APIs. No require(), no process, no npm ecosystem.
- No useful async. Everything runs synchronously inside the statement.
You cannot build an application with that toolbox. However you can build very good functions!
What JavaScript routines are actually for
A JavaScript routine extends the SQL vocabulary. It is a new function you call from SELECT, JOIN, WHERE, GROUP BY or a trigger, exactly like LOWER() or JSON_EXTRACT(). The only new syntax is LANGUAGE JS:
CREATE FUNCTION norm_keys(d JSON) RETURNS JSON
DETERMINISTIC NO SQL LANGUAGE JS AS $$
const walk = (j) => {
if (j === null || typeof j !== 'object') return j
if (Array.isArray(j)) return j.map(walk)
const out = {}
for (const [k, v] of Object.entries(j))
out[k.toLowerCase()] = walk(v)
return out
}
return walk(typeof d === 'string' ? JSON.parse(d) : d)
$$;
The rule we kept repeating on stage is simple: SQL owns the set, JavaScript owns the value. SQL is unbeatable at filtering, joining, ordering, windowing and aggregating. Where SQL and its procedural language stop is a short, very specific list:
- Recursion over a shape not known in advance. JSON_TABLE needs every path as a literal.
- Arrays. SQL has no array type, so any algorithm that fills a grid has nowhere to live.
- Big integers. BIGINT stops at 19 digits; an IBAN checksum needs 29.
- Real iteration with aggregates. A recursive CTE cannot contain an aggregate or window function (ERROR 3575), and a WHILE loop in PL pays an executor round trip on every statement.
- Rich string handling. UPPER() and LOWER() do not know that McDonald has a capital D.
None of these is business logic. They are missing building blocks. JavaScript supplies them and hands the result straight back to SQL.
The JavaScript routine lives inside the SQL layer, not beside the application: a query calls it, and it returns a value to that query.
Four demos, one pattern
Each demo in the pleu_2026 repository follows the same shape: SQL does the set work, a small JavaScript function does the one step SQL cannot phrase, and the decision that matters to the business stays in the application.
| Demo | What SQL could not do | The JavaScript piece | Result | What stays in the application |
| Normalize JSON keys | Walk a JSON tree of unknown shape; four partners spell customer.id four ways | norm_keys(), 9 lines | One report went from 51 lines with four UNION ALL branches to 19 lines with no partner names | Which partners to onboard and what the report means |
| Validate input at the door | Proper-case names, normalise phones, run an IBAN mod-97 on a 29-digit number | proper_name(), clean_email(), clean_phone(), valid_iban(), clean_contact() | 460 of 2,000 rows caught, each with a list of what was wrong | What to do with a rejected contact: fix, call the customer, quarantine |
| Deduplicate customers | Levenshtein distance needs a grid, and SQL has no arrays | lev_distance() inside a JOIN predicate | Name equality found 21.7% of duplicates; blocking in SQL plus two distances found about 80% at 99.7% precision | Whether to merge two records, and how |
| Where should Federico live? | Iterate to a geometric median; recursive CTEs refuse aggregates | Weiszfeld procedure | 11.00 s in procedural SQL vs 0.52 s in JavaScript; 327,033 statements vs 6 | Whether Federico actually moves |
A few details are worth calling out, because they are exactly where DBAs and developers should meet.
Dedupe: SQL narrows, JavaScript decides the distance. Comparing 3,000 customers with 2,000 signups is six million pairs. We never let JavaScript see them all. Ordinary join predicates on city and initials cut the work to 21,352 pairs, and only those reach lev_distance(). The function returns a number. Merging two customers is a business decision, and it stays in the application.
Validation: the function reports, the policy decides. clean_contact() returns the cleaned values and an array of errors. A trigger uses it to guarantee that nothing dirty lands in contacts, whoever does the insert: web app, mobile app, nightly import, or someone at a mysql prompt at 2am. What happens next to a rejected row is still the application's job.
Federico: three steps, one of them JavaScript. Turning 44,347 raw stops into 16,232 weighted endpoints is two window functions, and nothing beats SQL there. Turning the resulting coordinate into a district is plain joins. JavaScript only runs the inner loop in the middle. Note that this demo uses session.runSql(), so today it runs on MySQL Enterprise; SQL from JavaScript is the top roadmap item for Percona Server.
Where we draw the line
A JavaScript routine belongs in the database when it makes a data operation possible or cheaper. It belongs in the application when it decides what the business does. In practice we use five questions before accepting a routine:
- Does it work on values the database already holds? If it needs an external service, a file or a user session, it is application code.
- Is it deterministic, with no side effects outside the transaction? DETERMINISTIC NO SQL is not only a binlog requirement; it is a good design filter.
- Is it called from SQL? A good routine sits inside a SELECT, a JOIN, a JSON_TABLE or a trigger. If nothing in SQL calls it, ask why it is in the database.
- Is the result smaller than the input, or does it save a round trip? Two numbers out instead of 5,000 names, emails and cities is the kind of win we want.
- Would every client need the same rule? Validation that the web app, the mobile app and the importer all must apply is a data rule. Pricing for a campaign that changes next week is business policy.
| Belongs in a JS routine | Stays in the application |
| Normalising and validating values (keys, names, phones, IBAN) | Workflows, approvals, notifications |
| Distance, similarity, checksum and parsing functions | Decisions based on those scores: merge, reject, refund |
| The inner loop of a numeric algorithm over rows SQL selected | Orchestration across services and external APIs |
| Integrity rules every writer must obey | UI, sessions, authentication flows |
| Reshaping JSON so SQL can query it | Business rules owned by product and changed often |
If a routine starts to need configuration tables, feature flags or calls to another system, that is our signal that it has crossed the line.
Why developers should ask for it
For developers this is not about giving up control of the application. It is about no longer having to pull data out of MySQL just because SQL could not express one step.
- It is the language you already write. ECMAScript on V8, the same runtime as Chrome and Node.js. JSON columns arrive as native JS objects, no JSON.parse() needed.
- Fewer round trips. In the commute demo one CALL read 16,232 rows and wrote 2,000 answers with zero rows crossing the network. The speed-up from 11.00 s to 0.52 s came from not crossing the executor boundary 327,033 times, not from JavaScript being a faster language.
- Less personal data in motion. The dedupe query returns two numbers instead of shipping 5,000 names, emails and cities to an app server, and into its logs. Your GDPR scope stays in the database.
- One rule, written once. Validation that lives in five clients becomes four out-of-date copies. A function and a trigger are one copy nobody can bypass.
- Simpler queries in your code. norm_keys() turned a 51-line, partner-specific report into 19 lines. Partner five becomes a data change, not a code change.
- Pure functions are easy to test. Deterministic, no I/O, no hidden state: the same unit-testing habits you already have.
How we suggest developers and DBAs work together:
- The developer proposes the function, with the SQL that will call it.
- The DBA reviews it against the five questions above, and checks if the call site uses blocking or filtering so the function runs on as few rows as possible.
- Grant CREATE_JS_ROUTINE deliberately; it is required on top of CREATE ROUTINE, so who can write JavaScript is an explicit choice.
- Keep the routine in version control next to the schema migrations.
SQL did not get worse, it got a companion
JavaScript in MySQL is not an invitation to rebuild the application inside the server.
It is a set of sharp tools for the few places where SQL and PL run out: recursion, arrays, big numbers and tight loops. The set work stays in SQL, the business logic stays in the application, and JavaScript fills the gap between them.
If you want to try it, component_js_lang is available as a tech preview in Percona Server for MySQL 8.4.5-5 and later, free and open source, while the equivalent feature from Oracle requires MySQL Enterprise.
You can easily use the code available here Demo code from Percona Live Europe 2026 to perform some tests, just doing cut and paste.
Tell us what you think on the Percona Community Forum or in Jira with the label PS-JS-LANG. Your feedback decides what goes into GA, starting with SQL execution from JavaScript routines.
Resources


