CASEswitch over dialect names.import { countDistinct, dateTrunc, rowNumber, col } from 'ts-prorm-orm';
const dialect = prorm.getDialect(); // 'postgres' | 'mysql' | 'sqlite' | 'mssql' | …
countDistinct(dialect, 'id'); // COUNT(DISTINCT "id") on PostgreSQL
dateTrunc(dialect, 'month', col('createdAt')); // DATE_TRUNC('month', "createdAt")
rowNumber().forDialect(dialect).partitionBy('userId').orderBy('total', 'DESC').toSQL();
// ROW_NUMBER() OVER (PARTITION BY "userId" ORDER BY "total" DESC)orders table (id, userId, status, total, createdAt, shippedAt) withusers and products alongside it.UnsafeSqlArgumentError ratherimport { col, count, dateTrunc, sqlSum } from 'ts-prorm-orm';
const d = prorm.getDialect();
const month = dateTrunc(d, 'month', col('createdAt'));
const rows = await Order.findAll({
attributes: [
[month, 'month'],
[count(d), 'orders'],
[sqlSum(d, col('total'), { filter: { status: 'paid' } }), 'paidRevenue'],
],
group: [month],
order: [['month', 'ASC']],
});filter is a native FILTER (WHERE …) where the engine has one (PostgreSQL,CASE inside the aggregate elsewhere. The two give the same| Position | PostgreSQL | MySQL | SQLite | SQL Server |
|---|---|---|---|---|
attributes: [[expr, 'alias']] | yes | yes | yes | yes |
order: [['alias', 'ASC']] (by the attribute's alias) | yes | yes | yes | yes |
group: [expr] | yes | yes | yes | yes |
order: [[expr, 'ASC']] (the expression itself) | yes | yes | yes | yes |
where: expr (a predicate as the whole condition) | yes | refused | yes | refused |
ORDER BY:await Order.findAll({ attributes: [[month, 'month']], group: [month], order: [[month, 'ASC']] });order rendered [object Object] on PostgreSQLgroup: [expr] rendered [[object Object]]] on SQLUnsupportedWhereError and sends nothing. It never drops the condition.col() is a columncol():import { concat, col } from 'ts-prorm-orm';
concat(dialect, 'first', ' ', 'last');
// 'first' || ' ' || 'last' — three literals
concat(dialect, col('firstName'), ' ', col('lastName'));
// "firstName" || ' ' || "lastName" — two columns and a literalimport { countDistinct, sqlSum, col } from 'ts-prorm-orm';
countDistinct(dialect, 'id'); // COUNT(DISTINCT "id")
sqlSum(dialect, col('total')); // SUM("total") — col() also workscol() everywhere and the distinction stops mattering. fullTextMatch andfullTextRank are the exception in the other direction: they take columncol() there throws UnsafeSqlArgumentError.| Function | PostgreSQL | MySQL | SQLite | SQL Server | Oracle |
|---|---|---|---|---|---|
count(d) | COUNT(*) | COUNT(*) | COUNT(*) | COUNT(*) | COUNT(*) |
countDistinct(d, 'id') | COUNT(DISTINCT "id") | COUNT(DISTINCT `id`) | COUNT(DISTINCT "id") | COUNT(DISTINCT [id]) | COUNT(DISTINCT "id") |
sqlSum(d, col('total')), avg, min, max | SUM("total") | SUM(`total`) | SUM("total") | SUM([total]) | SUM("total") |
stringAgg(d, col('name'), ', ') | STRING_AGG("name", ', ') | GROUP_CONCAT(`name` SEPARATOR ', ') | GROUP_CONCAT("name", ', ') | STRING_AGG([name], ', ') | LISTAGG("name", ', ') |
arrayAgg(d, col('tag')) | ARRAY_AGG("tag") | JSON_ARRAYAGG(`tag`) | json_group_array("tag") | throws | JSON_ARRAYAGG("tag") |
percentileCont(d, 0.5, { orderBy: 'total' }) | PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY "total") | throws | throws | PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY [total]) OVER () | PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY "total") |
sqlMode(d, col('status')) | MODE() WITHIN GROUP (ORDER BY "status") | throws | throws | throws | STATS_MODE("status") |
arrayAgg on SQL Server throws with the reason: FOR JSON PATH shapes a wholeError: percentileCont() is not supported on dialect "mysql": mysql has no PERCENTILE_CONT
function; compute the percentile in a subquery with ORDER BY and LIMIT/OFFSET insteadsum is exported as sqlSum at the package root, because sum is alreadysqlRandom, sqlTruncate,sqlSubstring, sqlTrim, sqlLtrim, sqlRtrim, sqlJsonContains,sqlJsonHasKey, sqlFormatDate, sqlNow, sqlRepeat, sqlReverse,sqlCoalesce, sqlFormat, sqlLike, sqlParseDate and sqlMode.filter (a where-style condition). stringAgg also takesdistinct and orderBy:import { stringAgg, col } from 'ts-prorm-orm';
stringAgg(dialect, col('name'), ', ', { orderBy: 'name', distinct: true });Error: stringAgg() is not supported on dialect "sqlite": SQLite's GROUP_CONCAT does not
support an ORDER BY clause; pre-sort via a subquery insteadError: stringAgg() is not supported on dialect "mssql": MSSQL STRING_AGG does not support DISTINCTimport { countDistinct, col } from 'ts-prorm-orm';
countDistinct(dialect, [col('userId'), col('productId')]);| Engine | Output |
|---|---|
| PostgreSQL | COUNT(DISTINCT CASE WHEN "userId" IS NOT NULL AND "productId" IS NOT NULL THEN ("userId", "productId") END) |
| MySQL | COUNT(DISTINCT `userId`, `productId`) |
| SQLite, SQL Server, Oracle | throws, and the message suggests a DISTINCT subquery |
NULL test because the row value(NULL, 1) is not itself NULL and would otherwise be counted.WindowFunctionBuilder is chainable. Set the dialect with forDialect(), thentoSQL(), toLiteral() or as(alias):import { rowNumber, lag, ntile } from 'ts-prorm-orm';
rowNumber().forDialect('postgres').partitionBy('userId').orderBy('total', 'DESC').toSQL();
// ROW_NUMBER() OVER (PARTITION BY "userId" ORDER BY "total" DESC)
lag('total', 1).forDialect('mysql').partitionBy('userId').orderBy('createdAt').toSQL();
// LAG(`total`, 1) OVER (PARTITION BY `userId` ORDER BY `createdAt`)
ntile(4).forDialect('postgres').orderBy('total', 'DESC').toSQL();
// NTILE(4) OVER (ORDER BY "total" DESC)[!CAUTION]
WithoutforDialect(), identifiers are double-quoted (the ANSI form). On
MySQL, a double-quoted word is a string, soPARTITION BY "userId"
partitions by a constant and the query still runs. Always callforDialect(prorm.getDialect()).
as(alias) returns the [expression, alias] pair that attributes expects.import { rowNumber } from 'ts-prorm-orm';
const d = prorm.getDialect();
await Order.findAll({
attributes: [
'id',
'userId',
'total',
rowNumber().forDialect(d).partitionBy('userId').orderBy('total', 'DESC').as('rankInUser'),
],
});rowNumber, rank, denseRank, percentRank, cumeDist, ntile,lag, lead, firstValue, lastValue, nthValue, and the aggregate-oversumOver, avgOver, countOver, minOver, maxOver. windowFn(name,
…args) builds one the list does not cover.import { sumOver, avgOver } from 'ts-prorm-orm';
sumOver('total').forDialect('postgres').orderBy('id')
.rows({ start: 'unboundedPreceding', end: 'currentRow' })
.toSQL();
// SUM("total") OVER (ORDER BY "id" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
avgOver('total').forDialect('mysql').partitionBy('userId')
.rows({ start: { preceding: 2 }, end: { following: 2 } })
.toSQL();
// AVG(`total`) OVER (PARTITION BY `userId` ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING)'unboundedPreceding', 'unboundedFollowing', 'currentRow',{ preceding: n } or { following: n }. range(frame) is the RANGErows(frame).exclude() adds a frame exclusion, which only some engines have. A movingimport { avgOver } from 'ts-prorm-orm';
avgOver('total').forDialect(dialect).orderBy('id')
.rows({ start: { preceding: 2 }, end: { following: 2 } })
.exclude('currentRow')
.toSQL();Error: Window frame exclusion (EXCLUDE CURRENT ROW) is not supported on dialect "mysql";
no other frame specification expresses itError: Window frame exclusion (EXCLUDE CURRENT ROW) is not supported on dialect "mssql";
no other frame specification expresses itnamedWindow() and reference it with over():import { namedWindow, sumOver } from 'ts-prorm-orm';
const w = namedWindow('w').forDialect('postgres').partitionBy('userId').orderBy('createdAt');
w.toSQL(); // "w" AS (PARTITION BY "userId" ORDER BY "createdAt") — for the WINDOW clause
sumOver('total').forDialect('postgres').over(w).toSQL(); // SUM("total") OVER "w"orderBy() or apartitionBy() on top of a named window throws, as SQL| Call | PostgreSQL | MySQL | SQLite | SQL Server | Oracle |
|---|---|---|---|---|---|
dateTrunc(d, 'month', col('createdAt')) | DATE_TRUNC('month', "createdAt") | DATE_FORMAT(`createdAt`, '%Y-%m-01 00:00:00') | strftime('%Y-%m-01 00:00:00', "createdAt") | DATETRUNC(month, [createdAt]) | TRUNC("createdAt", 'MM') |
dateDiff(d, 'day', col('createdAt'), col('shippedAt')) | (CAST("shippedAt" AS DATE) - CAST("createdAt" AS DATE)) | TIMESTAMPDIFF(DAY, `createdAt`, `shippedAt`) | ((CAST(strftime('%s', "shippedAt") AS INTEGER) - CAST(strftime('%s', "createdAt") AS INTEGER)) / 86400) | DATEDIFF(day, [createdAt], [shippedAt]) | (TRUNC("shippedAt") - TRUNC("createdAt")) |
dateAdd(d, col('createdAt'), 7, 'day') | "createdAt" + INTERVAL '7 day' | DATE_ADD(`createdAt`, INTERVAL 7 DAY) | datetime("createdAt", '+7 days') | DATEADD(day, 7, [createdAt]) | ("createdAt" + 7) |
sqlFormatDate(d, col('createdAt'), 'YYYY-MM-DD') | TO_CHAR("createdAt", 'YYYY-MM-DD') | DATE_FORMAT(`createdAt`, '%Y-%m-%d') | strftime('%Y-%m-%d', "createdAt") | FORMAT([createdAt], 'yyyy-MM-dd') |
DATE_TRUNC does not exist on MySQL, and the DATE_FORMAT string thatsqlFormatDate alsoYYYY-MM-DD to %Y-%m-%d oryyyy-MM-dd.currentTimestamp() is the portable SQL-standard form. sqlNow() andcurrentDate() use each engine's own spelling:[!NOTE]
SQLite'sdatetime('now')andCURRENT_TIMESTAMPare UTC text
(YYYY-MM-DD HH:MM:SS). SQL Server'sGETDATE()is the server's local time.
Store UTC and convert at the edge if the engines must agree.
import { jsonExtract, jsonObject, col } from 'ts-prorm-orm';
jsonExtract(dialect, col('shipping'), '$.address.city');
jsonObject(dialect, { id: col('id'), total: col('total') });JSON_EXTRACT alone would return "value" with the quotes. On Oracle,jsonExtract renders JSON_VALUE("shipping", '$.address.city').jsonValue, jsonQuery, jsonExists, isJson, jsonArray, jsonSet,jsonAgg, jsonObjectAgg, jsonKeys, jsonTypeOf, sqlJsonContains andsqlJsonHasKey.import { fullTextMatch, fullTextRank } from 'ts-prorm-orm';
fullTextMatch(dialect, ['title', 'body'], 'database index');
fullTextRank(dialect, ['title', 'body'], 'database index');| Dialect | fullTextMatch | fullTextRank |
|---|---|---|
| PostgreSQL | to_tsvector('english', "title" || ' ' || "body") @@ plainto_tsquery('english', 'database index') | ts_rank(to_tsvector('english', "title" || ' ' || "body"), plainto_tsquery('english', 'database index')) |
| MySQL | MATCH(`title`, `body`) AGAINST('database index' IN NATURAL LANGUAGE MODE) | MATCH(`title`, `body`) AGAINST('database index') |
| SQLite | ("title" MATCH 'database index' OR "body" MATCH 'database index') | throws: select bm25() from the FTS5 table in a raw query |
| SQL Server | FREETEXT(([title], [body]), 'database index') | throws: use CONTAINSTABLE()/FREETEXTTABLE() in a raw query |
where on PostgreSQL and SQLite:import { fullTextMatch } from 'ts-prorm-orm';
await Product.findAll({ where: fullTextMatch(prorm.getDialect(), ['name'], 'wireless mouse') });
// PostgreSQL: SELECT * FROM "products" WHERE to_tsvector('english', "name") @@ plainto_tsquery('english', 'wireless mouse')
// SQLite: SELECT * FROM "products" WHERE ("name" MATCH 'wireless mouse')findAll throws UnsupportedWhereError beforeconcat, upper, lower, length, replace, lpad, rpad,split, splitPart, position, overlay, regexpReplace, sqlSubstring,sqlTrim, sqlLtrim, sqlRtrim, sqlRepeat, sqlReverse, sqlFormat,sqlLike.round, ceil, floor, power, sqrt, abs, sign, mod,log, ln, exp, greatest, least, sqlRandom, sqlTruncate, and thebitAnd, bitOr, bitXor, bitNot, bitShiftLeft, bitShiftRight.sqlCoalesce, nullif, tryCast, collate.import { sqlCoalesce, nullif, round, col } from 'ts-prorm-orm';
sqlCoalesce(dialect, col('nickname'), col('name'), 'anonymous'); // COALESCE("nickname", "name", 'anonymous')
nullif(dialect, col('discount'), 0); // NULLIF("discount", 0)
round(dialect, col('total'), 2); // ROUND("total", 2)greatest / least are worth singling out. The same call produces threeimport { greatest, col } from 'ts-prorm-orm';
greatest(dialect, col('a'), col('b'));GREATEST before 2022, so the builder expands it into theCASE. The expansion compares each candidate with every argument,import { caseWhen, caseOf } from 'ts-prorm-orm';
caseWhen()
.when({ status: 'active' }, 1)
.else(0)
.toSQL('postgres');
// CASE WHEN "status" = 'active' THEN 1 ELSE 0 END
caseOf('status').when('a', 1).else(0).toSQL('mysql');
// CASE `status` WHEN 'a' THEN 1 ELSE 0 END — the simple (switch) formcaseWhen() takes a full where-style condition percaseOf(column) is the simple form that compares one expression againstcaseWhen(dialect)),forDialect(dialect) or in toSQL(dialect), and support as(alias) andtoLiteral().import { caseWhen, count, sqlSum } from 'ts-prorm-orm';
const d = prorm.getDialect();
await Order.findAll({
attributes: [
[count(d), 'total'],
[sqlSum(d, caseWhen(d).when({ status: 'paid' }, 1).else(0).toLiteral()), 'paid'],
],
});[!TIP] count(d, undefined, { filter: { status: 'paid' } })expresses the same count
directly, and uses the nativeFILTER (WHERE …)clause where the engine has
one.
query-builders — how these fit the compilerattributes, group, havingwhere vocabulary