Skip to content

Commit f678672

Browse files
committed
Compile IR to SQL
1 parent dd6cdf7 commit f678672

File tree

2 files changed

+181
-6
lines changed

2 files changed

+181
-6
lines changed

packages/electric-db-collection/src/electric.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import {
1313
ExpectedNumberInAwaitTxIdError,
1414
TimeoutWaitingForTxIdError,
1515
} from "./errors"
16+
import { compileSQL } from "./sql-compiler"
1617
import type {
1718
BaseCollectionConfig,
1819
CollectionConfig,
1920
DeleteMutationFnParams,
2021
Fn,
2122
InsertMutationFnParams,
23+
OnLoadMoreOptions,
2224
SyncConfig,
2325
UpdateMutationFnParams,
2426
UtilsRecord,
@@ -494,15 +496,42 @@ function createElectricSync<T extends Row<unknown>>(
494496
}
495497
})
496498

497-
// Return the unsubscribe function
498-
return () => {
499-
// Unsubscribe from the stream
500-
unsubscribeStream()
501-
// Abort the abort controller to stop the stream
502-
abortController.abort()
499+
return {
500+
onLoadMore: (opts) => onLoadMore(params, opts),
501+
cleanup: () => {
502+
// Unsubscribe from the stream
503+
unsubscribeStream()
504+
// Abort the abort controller to stop the stream
505+
abortController.abort()
506+
},
503507
}
504508
},
505509
// Expose the getSyncMetadata function
506510
getSyncMetadata,
507511
}
508512
}
513+
514+
async function onLoadMore<T extends Row<unknown>>(
515+
syncParams: Parameters<SyncConfig<T>[`sync`]>[0],
516+
options: OnLoadMoreOptions
517+
) {
518+
const { begin, write, commit } = syncParams
519+
520+
// TODO: optimize this by keeping track of which snapshot have been loaded already
521+
// and only load this one if it's not a subset of the ones that have been loaded already
522+
523+
const snapshotParams = compileSQL<T>(options)
524+
525+
const snapshot = await requestSnapshot(snapshotParams)
526+
527+
begin()
528+
529+
snapshot.data.forEach((row) => {
530+
write({
531+
type: `insert`,
532+
value: row.value,
533+
})
534+
})
535+
536+
commit()
537+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import type { IR, OnLoadMoreOptions } from "@tanstack/db"
2+
3+
export function compileSQL<T>(
4+
options: OnLoadMoreOptions
5+
): ExternalSubsetParamsRecord {
6+
const { where, orderBy, limit } = options
7+
8+
const params: Array<T> = []
9+
const compiledSQL: ExternalSubsetParamsRecord = { params }
10+
11+
if (where) {
12+
// TODO: this only works when the where expression's PropRefs directly reference a column of the collection
13+
// doesn't work if it goes through aliases because then we need to know the entire query to be able to follow the reference until the base collection (cf. followRef function)
14+
compiledSQL.where = compileBasicExpression(where, params)
15+
}
16+
17+
if (orderBy) {
18+
compiledSQL.orderBy = compileOrderBy(orderBy, params)
19+
}
20+
21+
if (limit) {
22+
compiledSQL.limit = limit
23+
}
24+
25+
return compiledSQL
26+
}
27+
28+
/**
29+
* Compiles the expression to a SQL string and mutates the params array with the values.
30+
* @param exp - The expression to compile
31+
* @param params - The params array
32+
* @returns The compiled SQL string
33+
*/
34+
function compileBasicExpression(
35+
exp: IR.BasicExpression<unknown>,
36+
params: Array<unknown>
37+
): string {
38+
switch (exp.type) {
39+
case `val`:
40+
params.push(exp.value)
41+
return `$${params.length}`
42+
case `ref`:
43+
if (exp.path.length !== 1) {
44+
throw new Error(
45+
`Compiler can't handle nested properties: ${exp.path.join(`.`)}`
46+
)
47+
}
48+
return exp.path[0]!
49+
case `func`:
50+
return compileFunction(exp, params)
51+
}
52+
}
53+
54+
function compileOrderBy(orderBy: IR.OrderBy, params: Array<unknown>): string {
55+
const compiledOrderByClauses = orderBy.map((clause: IR.OrderByClause) =>
56+
compileOrderByClause(clause, params)
57+
)
58+
return compiledOrderByClauses.join(`,`)
59+
}
60+
61+
function compileOrderByClause(
62+
clause: IR.OrderByClause,
63+
params: Array<unknown>
64+
): string {
65+
// TODO: what to do with stringSort and locale?
66+
// Correctly supporting them is tricky as it depends on Postgres' collation
67+
const { expression, compareOptions } = clause
68+
let sql = compileBasicExpression(expression, params)
69+
70+
if (compareOptions.direction === `desc`) {
71+
sql = `${sql} DESC`
72+
}
73+
74+
if (compareOptions.nulls === `first`) {
75+
sql = `${sql} NULLS FIRST`
76+
}
77+
78+
if (compareOptions.nulls === `last`) {
79+
sql = `${sql} NULLS LAST`
80+
}
81+
82+
return sql
83+
}
84+
85+
function compileFunction(
86+
exp: IR.Func<unknown>,
87+
params: Array<unknown> = []
88+
): string {
89+
const { name, args } = exp
90+
91+
const opName = getOpName(name)
92+
93+
const compiledArgs = args.map((arg: IR.BasicExpression) =>
94+
compileBasicExpression(arg, params)
95+
)
96+
97+
if (isBinaryOp(name)) {
98+
if (compiledArgs.length !== 2) {
99+
throw new Error(`Binary operator ${name} expects 2 arguments`)
100+
}
101+
const [lhs, rhs] = compiledArgs
102+
return `${lhs} ${opName} ${rhs}`
103+
}
104+
105+
return `${opName}(${compiledArgs.join(`,`)})`
106+
}
107+
108+
function isBinaryOp(name: string): boolean {
109+
const binaryOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `and`, `or`]
110+
return binaryOps.includes(name)
111+
}
112+
113+
function getOpName(name: string): string {
114+
const opNames = {
115+
eq: `=`,
116+
gt: `>`,
117+
gte: `>=`,
118+
lt: `<`,
119+
lte: `<=`,
120+
add: `+`,
121+
and: `AND`,
122+
or: `OR`,
123+
not: `NOT`,
124+
isUndefined: `IS NULL`,
125+
isNull: `IS NULL`,
126+
in: `IN`,
127+
like: `LIKE`,
128+
ilike: `ILIKE`,
129+
upper: `UPPER`,
130+
lower: `LOWER`,
131+
length: `LENGTH`,
132+
concat: `CONCAT`,
133+
coalesce: `COALESCE`,
134+
}
135+
return opNames[name as keyof typeof opNames] || name
136+
}
137+
138+
// TODO: remove this type once we rebase on top of Ilia's PR
139+
// that type will be exported by Ilia's PR
140+
export type ExternalSubsetParamsRecord = {
141+
where?: string
142+
params?: Record<string, any>
143+
limit?: number
144+
offset?: number
145+
orderBy?: string
146+
}

0 commit comments

Comments
 (0)