@tanstack/electric-db-collection 0.4.10 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/electric.cjs +146 -103
- package/dist/cjs/electric.cjs.map +1 -1
- package/dist/cjs/electric.d.cts +94 -25
- package/dist/cjs/sql-compiler.cjs +43 -92
- package/dist/cjs/sql-compiler.cjs.map +1 -1
- package/dist/esm/electric.d.ts +94 -25
- package/dist/esm/electric.js +147 -104
- package/dist/esm/electric.js.map +1 -1
- package/dist/esm/sql-compiler.js +43 -92
- package/dist/esm/sql-compiler.js.map +1 -1
- package/package.json +2 -2
- package/src/electric.ts +295 -166
- package/src/sql-compiler.ts +57 -134
package/dist/cjs/electric.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseCollectionConfig, CollectionConfig, DeleteMutationFnParams, InsertMutationFnParams, SyncMode, UpdateMutationFnParams
|
|
1
|
+
import { BaseCollectionConfig, CollectionConfig, DeleteMutationFnParams, InsertMutationFnParams, SyncMode, UpdateMutationFnParams } from '@tanstack/db';
|
|
2
2
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
3
3
|
import { GetExtensions, Message, Row, ShapeStreamOptions } from '@electric-sql/client';
|
|
4
4
|
export { isChangeMessage, isControlMessage } from '@electric-sql/client';
|
|
@@ -28,11 +28,11 @@ export type MatchFunction<T extends Row<unknown>> = (message: Message<T>) => boo
|
|
|
28
28
|
/**
|
|
29
29
|
* Matching strategies for Electric synchronization
|
|
30
30
|
* Handlers can return:
|
|
31
|
-
* - Txid strategy: { txid: number | number[], timeout?: number } (
|
|
31
|
+
* - Txid strategy: { txid: number | number[], timeout?: number } (deprecated compatibility behavior)
|
|
32
32
|
* - Void (no return value) - mutation completes without waiting
|
|
33
33
|
*
|
|
34
34
|
* The optional timeout property specifies how long to wait for the txid(s) in milliseconds.
|
|
35
|
-
* If not specified, defaults to
|
|
35
|
+
* If not specified, defaults to 15000ms.
|
|
36
36
|
*/
|
|
37
37
|
export type MatchingStrategy = {
|
|
38
38
|
txid: Txid | Array<Txid>;
|
|
@@ -61,7 +61,7 @@ export type ElectricSyncMode = SyncMode | `progressive`;
|
|
|
61
61
|
* @template T - The type of items in the collection
|
|
62
62
|
* @template TSchema - The schema type for validation
|
|
63
63
|
*/
|
|
64
|
-
export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never> extends Omit<BaseCollectionConfig<T, string | number, TSchema, ElectricCollectionUtils<T
|
|
64
|
+
export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never> extends Omit<BaseCollectionConfig<T, string | number, TSchema, ElectricCollectionUtils<T>>, `onInsert` | `onUpdate` | `onDelete` | `syncMode`> {
|
|
65
65
|
/**
|
|
66
66
|
* Configuration options for the ElectricSQL ShapeStream
|
|
67
67
|
*/
|
|
@@ -74,43 +74,82 @@ export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>,
|
|
|
74
74
|
[ELECTRIC_TEST_HOOKS]?: ElectricTestHooks;
|
|
75
75
|
/**
|
|
76
76
|
* Optional asynchronous handler function called before an insert operation
|
|
77
|
+
*
|
|
78
|
+
* **IMPORTANT - Electric Synchronization:**
|
|
79
|
+
* This handler **must not resolve** until synchronization is confirmed.
|
|
80
|
+
* Await one of these synchronization utilities before the handler completes:
|
|
81
|
+
* 1. `await collection.utils.awaitTxId(txid)` (recommended for most cases)
|
|
82
|
+
* 2. `await collection.utils.awaitMatch(fn)` for custom matching logic
|
|
83
|
+
*
|
|
84
|
+
* Simply returning without waiting for sync will drop optimistic state too early, causing UI glitches.
|
|
85
|
+
*
|
|
77
86
|
* @param params Object containing transaction and collection information
|
|
78
|
-
* @returns Promise
|
|
87
|
+
* @returns Promise that should resolve after synchronization is complete
|
|
88
|
+
*
|
|
89
|
+
* **Deprecation notice:** Returning `{ txid }` from handlers is deprecated and will be removed in v1.0.
|
|
90
|
+
* Use `await collection.utils.awaitTxId(txid)` within the handler instead.
|
|
91
|
+
*
|
|
79
92
|
* @example
|
|
80
|
-
* //
|
|
81
|
-
* onInsert: async ({ transaction }) => {
|
|
93
|
+
* // Recommended: Wait for txid to sync
|
|
94
|
+
* onInsert: async ({ transaction, collection }) => {
|
|
82
95
|
* const newItem = transaction.mutations[0].modified
|
|
83
96
|
* const result = await api.todos.create({
|
|
84
97
|
* data: newItem
|
|
85
98
|
* })
|
|
86
|
-
*
|
|
99
|
+
* // Wait for txid to sync before handler completes
|
|
100
|
+
* await collection.utils.awaitTxId(result.txid)
|
|
87
101
|
* }
|
|
88
102
|
*
|
|
89
103
|
* @example
|
|
90
104
|
* // Insert handler with custom timeout
|
|
91
|
-
* onInsert: async ({ transaction }) => {
|
|
105
|
+
* onInsert: async ({ transaction, collection }) => {
|
|
106
|
+
* const newItem = transaction.mutations[0].modified
|
|
107
|
+
* const result = await api.todos.create({
|
|
108
|
+
* data: newItem
|
|
109
|
+
* })
|
|
110
|
+
* // Wait up to 10 seconds for txid
|
|
111
|
+
* await collection.utils.awaitTxId(result.txid, 10000)
|
|
112
|
+
* }
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* // Insert handler with timeout error handling
|
|
116
|
+
* onInsert: async ({ transaction, collection }) => {
|
|
92
117
|
* const newItem = transaction.mutations[0].modified
|
|
93
118
|
* const result = await api.todos.create({
|
|
94
119
|
* data: newItem
|
|
95
120
|
* })
|
|
96
|
-
*
|
|
121
|
+
*
|
|
122
|
+
* try {
|
|
123
|
+
* await collection.utils.awaitTxId(result.txid, 5000)
|
|
124
|
+
* } catch (error) {
|
|
125
|
+
* // Decide sync timeout policy:
|
|
126
|
+
* // - Throw to rollback optimistic state
|
|
127
|
+
* // - Catch to keep optimistic state (eventual consistency)
|
|
128
|
+
* // - Schedule background retry
|
|
129
|
+
* console.warn('Sync timeout, keeping optimistic state:', error)
|
|
130
|
+
* // Don't throw - allow optimistic state to persist
|
|
131
|
+
* }
|
|
97
132
|
* }
|
|
98
133
|
*
|
|
99
134
|
* @example
|
|
100
|
-
* // Insert handler with multiple items
|
|
101
|
-
* onInsert: async ({ transaction }) => {
|
|
135
|
+
* // Insert handler with multiple items
|
|
136
|
+
* onInsert: async ({ transaction, collection }) => {
|
|
102
137
|
* const items = transaction.mutations.map(m => m.modified)
|
|
103
138
|
* const results = await Promise.all(
|
|
104
139
|
* items.map(item => api.todos.create({ data: item }))
|
|
105
140
|
* )
|
|
106
|
-
*
|
|
141
|
+
* // Wait for all txids to sync
|
|
142
|
+
* await Promise.all(
|
|
143
|
+
* results.map(r => collection.utils.awaitTxId(r.txid))
|
|
144
|
+
* )
|
|
107
145
|
* }
|
|
108
146
|
*
|
|
109
147
|
* @example
|
|
110
|
-
* // Use awaitMatch utility for custom matching
|
|
148
|
+
* // Alternative: Use awaitMatch utility for custom matching logic
|
|
111
149
|
* onInsert: async ({ transaction, collection }) => {
|
|
112
150
|
* const newItem = transaction.mutations[0].modified
|
|
113
151
|
* await api.todos.create({ data: newItem })
|
|
152
|
+
* // Wait for specific change to appear in sync stream
|
|
114
153
|
* await collection.utils.awaitMatch(
|
|
115
154
|
* (message) => isChangeMessage(message) &&
|
|
116
155
|
* message.headers.operation === 'insert' &&
|
|
@@ -121,24 +160,39 @@ export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>,
|
|
|
121
160
|
onInsert?: (params: InsertMutationFnParams<T, string | number, ElectricCollectionUtils<T>>) => Promise<MatchingStrategy>;
|
|
122
161
|
/**
|
|
123
162
|
* Optional asynchronous handler function called before an update operation
|
|
163
|
+
*
|
|
164
|
+
* **IMPORTANT - Electric Synchronization:**
|
|
165
|
+
* This handler **must not resolve** until synchronization is confirmed.
|
|
166
|
+
* Await one of these synchronization utilities before the handler completes:
|
|
167
|
+
* 1. `await collection.utils.awaitTxId(txid)` (recommended for most cases)
|
|
168
|
+
* 2. `await collection.utils.awaitMatch(fn)` for custom matching logic
|
|
169
|
+
*
|
|
170
|
+
* Simply returning without waiting for sync will drop optimistic state too early, causing UI glitches.
|
|
171
|
+
*
|
|
124
172
|
* @param params Object containing transaction and collection information
|
|
125
|
-
* @returns Promise
|
|
173
|
+
* @returns Promise that should resolve after synchronization is complete
|
|
174
|
+
*
|
|
175
|
+
* **Deprecation notice:** Returning `{ txid }` from handlers is deprecated and will be removed in v1.0.
|
|
176
|
+
* Use `await collection.utils.awaitTxId(txid)` within the handler instead.
|
|
177
|
+
*
|
|
126
178
|
* @example
|
|
127
|
-
* //
|
|
128
|
-
* onUpdate: async ({ transaction }) => {
|
|
179
|
+
* // Recommended: Wait for txid to sync
|
|
180
|
+
* onUpdate: async ({ transaction, collection }) => {
|
|
129
181
|
* const { original, changes } = transaction.mutations[0]
|
|
130
182
|
* const result = await api.todos.update({
|
|
131
183
|
* where: { id: original.id },
|
|
132
184
|
* data: changes
|
|
133
185
|
* })
|
|
134
|
-
*
|
|
186
|
+
* // Wait for txid to sync before handler completes
|
|
187
|
+
* await collection.utils.awaitTxId(result.txid)
|
|
135
188
|
* }
|
|
136
189
|
*
|
|
137
190
|
* @example
|
|
138
|
-
* // Use awaitMatch utility for custom matching
|
|
191
|
+
* // Alternative: Use awaitMatch utility for custom matching logic
|
|
139
192
|
* onUpdate: async ({ transaction, collection }) => {
|
|
140
193
|
* const { original, changes } = transaction.mutations[0]
|
|
141
194
|
* await api.todos.update({ where: { id: original.id }, data: changes })
|
|
195
|
+
* // Wait for specific change to appear in sync stream
|
|
142
196
|
* await collection.utils.awaitMatch(
|
|
143
197
|
* (message) => isChangeMessage(message) &&
|
|
144
198
|
* message.headers.operation === 'update' &&
|
|
@@ -149,23 +203,38 @@ export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>,
|
|
|
149
203
|
onUpdate?: (params: UpdateMutationFnParams<T, string | number, ElectricCollectionUtils<T>>) => Promise<MatchingStrategy>;
|
|
150
204
|
/**
|
|
151
205
|
* Optional asynchronous handler function called before a delete operation
|
|
206
|
+
*
|
|
207
|
+
* **IMPORTANT - Electric Synchronization:**
|
|
208
|
+
* This handler **must not resolve** until synchronization is confirmed.
|
|
209
|
+
* Await one of these synchronization utilities before the handler completes:
|
|
210
|
+
* 1. `await collection.utils.awaitTxId(txid)` (recommended for most cases)
|
|
211
|
+
* 2. `await collection.utils.awaitMatch(fn)` for custom matching logic
|
|
212
|
+
*
|
|
213
|
+
* Simply returning without waiting for sync will drop optimistic state too early, causing UI glitches.
|
|
214
|
+
*
|
|
152
215
|
* @param params Object containing transaction and collection information
|
|
153
|
-
* @returns Promise
|
|
216
|
+
* @returns Promise that should resolve after synchronization is complete
|
|
217
|
+
*
|
|
218
|
+
* **Deprecation notice:** Returning `{ txid }` from handlers is deprecated and will be removed in v1.0.
|
|
219
|
+
* Use `await collection.utils.awaitTxId(txid)` within the handler instead.
|
|
220
|
+
*
|
|
154
221
|
* @example
|
|
155
|
-
* //
|
|
156
|
-
* onDelete: async ({ transaction }) => {
|
|
222
|
+
* // Recommended: Wait for txid to sync
|
|
223
|
+
* onDelete: async ({ transaction, collection }) => {
|
|
157
224
|
* const mutation = transaction.mutations[0]
|
|
158
225
|
* const result = await api.todos.delete({
|
|
159
226
|
* id: mutation.original.id
|
|
160
227
|
* })
|
|
161
|
-
*
|
|
228
|
+
* // Wait for txid to sync before handler completes
|
|
229
|
+
* await collection.utils.awaitTxId(result.txid)
|
|
162
230
|
* }
|
|
163
231
|
*
|
|
164
232
|
* @example
|
|
165
|
-
* // Use awaitMatch utility for custom matching
|
|
233
|
+
* // Alternative: Use awaitMatch utility for custom matching logic
|
|
166
234
|
* onDelete: async ({ transaction, collection }) => {
|
|
167
235
|
* const mutation = transaction.mutations[0]
|
|
168
236
|
* await api.todos.delete({ id: mutation.original.id })
|
|
237
|
+
* // Wait for specific change to appear in sync stream
|
|
169
238
|
* await collection.utils.awaitMatch(
|
|
170
239
|
* (message) => isChangeMessage(message) &&
|
|
171
240
|
* message.headers.operation === 'delete' &&
|
|
@@ -186,7 +255,7 @@ export type AwaitMatchFn<T extends Row<unknown>> = (matchFn: MatchFunction<T>, t
|
|
|
186
255
|
/**
|
|
187
256
|
* Electric collection utilities type
|
|
188
257
|
*/
|
|
189
|
-
export interface ElectricCollectionUtils<T extends Row<unknown> = Row<unknown>>
|
|
258
|
+
export interface ElectricCollectionUtils<T extends Row<unknown> = Row<unknown>> {
|
|
190
259
|
awaitTxId: AwaitTxIdFn;
|
|
191
260
|
awaitMatch: AwaitMatchFn<T>;
|
|
192
261
|
}
|
|
@@ -35,7 +35,7 @@ function compileSQL(options, compileOptions) {
|
|
|
35
35
|
}
|
|
36
36
|
function quoteIdentifier(name, encodeColumnName) {
|
|
37
37
|
const columnName = encodeColumnName ? encodeColumnName(name) : name;
|
|
38
|
-
return `"${columnName}"`;
|
|
38
|
+
return `"${columnName.replace(/"/g, `""`)}"`;
|
|
39
39
|
}
|
|
40
40
|
function compileBasicExpression(exp, params, encodeColumnName) {
|
|
41
41
|
switch (exp.type) {
|
|
@@ -91,6 +91,40 @@ function compileFunction(exp, params = [], encodeColumnName) {
|
|
|
91
91
|
);
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
if (name === `not`) {
|
|
95
|
+
if (args.length !== 1) throw new Error(`NOT expects 1 argument`);
|
|
96
|
+
const arg = args[0];
|
|
97
|
+
if (arg?.type === `func` && (arg.name === `isNull` || arg.name === `isUndefined`)) {
|
|
98
|
+
if (arg.args.length !== 1) {
|
|
99
|
+
throw new Error(`${arg.name} expects 1 argument`);
|
|
100
|
+
}
|
|
101
|
+
const innerArg = arg.args[0];
|
|
102
|
+
const compiled = compileBasicExpression(
|
|
103
|
+
innerArg,
|
|
104
|
+
params,
|
|
105
|
+
encodeColumnName
|
|
106
|
+
);
|
|
107
|
+
return `${innerArg.type === `func` ? `(${compiled})` : compiled} IS NOT NULL`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const booleanLiteralIndex = args.findIndex(
|
|
111
|
+
(arg) => arg.type === `val` && typeof arg.value === `boolean`
|
|
112
|
+
);
|
|
113
|
+
if (args.length === 2 && isBooleanComparisonOp(name) && booleanLiteralIndex !== -1) {
|
|
114
|
+
const literalValue = args[booleanLiteralIndex].value;
|
|
115
|
+
const valueArg = args[booleanLiteralIndex === 0 ? 1 : 0];
|
|
116
|
+
const compiled = compileBasicExpression(valueArg, params, encodeColumnName);
|
|
117
|
+
const value = `(${compiled})`;
|
|
118
|
+
const lessThan = (name === `lt` || name === `lte`) === (booleanLiteralIndex === 1);
|
|
119
|
+
const inclusive = name === `lte` || name === `gte`;
|
|
120
|
+
if (inclusive && literalValue === lessThan) {
|
|
121
|
+
return `${value} = ${value}`;
|
|
122
|
+
}
|
|
123
|
+
if (!inclusive && literalValue !== lessThan) {
|
|
124
|
+
return `${value} <> ${value}`;
|
|
125
|
+
}
|
|
126
|
+
return `${value} = ${lessThan ? `FALSE` : `TRUE`}`;
|
|
127
|
+
}
|
|
94
128
|
const compiledArgs = args.map((arg) => {
|
|
95
129
|
const compiled = compileBasicExpression(arg, params, encodeColumnName);
|
|
96
130
|
return arg.type === `func` && (arg.name === `and` || arg.name === `or` || name !== `and` && name !== `or` && name !== `not` && (isBinaryOp(arg.name) || arg.name === `not` || arg.name === `isNull` || arg.name === `isUndefined`)) ? `(${compiled})` : compiled;
|
|
@@ -102,21 +136,6 @@ function compileFunction(exp, params = [], encodeColumnName) {
|
|
|
102
136
|
return `${compiledArgs[0]} ${opName}`;
|
|
103
137
|
}
|
|
104
138
|
if (name === `not`) {
|
|
105
|
-
if (compiledArgs.length !== 1) {
|
|
106
|
-
throw new Error(`NOT expects 1 argument`);
|
|
107
|
-
}
|
|
108
|
-
const arg = args[0];
|
|
109
|
-
if (arg && arg.type === `func`) {
|
|
110
|
-
const funcArg = arg;
|
|
111
|
-
if (funcArg.name === `isNull` || funcArg.name === `isUndefined`) {
|
|
112
|
-
const innerArg = compileBasicExpression(
|
|
113
|
-
funcArg.args[0],
|
|
114
|
-
params,
|
|
115
|
-
encodeColumnName
|
|
116
|
-
);
|
|
117
|
-
return `${innerArg} IS NOT NULL`;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
139
|
return `${opName} (${compiledArgs[0]})`;
|
|
121
140
|
}
|
|
122
141
|
if (isBinaryOp(name)) {
|
|
@@ -127,83 +146,15 @@ function compileFunction(exp, params = [], encodeColumnName) {
|
|
|
127
146
|
throw new Error(`Binary operator ${name} expects 2 arguments`);
|
|
128
147
|
}
|
|
129
148
|
const [lhs, rhs] = compiledArgs;
|
|
130
|
-
if (
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
params.pop();
|
|
136
|
-
if (name === `lt`) {
|
|
137
|
-
if (boolValue === true) {
|
|
138
|
-
params.push(false);
|
|
139
|
-
return `${lhs} = $${params.length}`;
|
|
140
|
-
} else {
|
|
141
|
-
return `false`;
|
|
142
|
-
}
|
|
143
|
-
} else if (name === `gt`) {
|
|
144
|
-
if (boolValue === false) {
|
|
145
|
-
params.push(true);
|
|
146
|
-
return `${lhs} = $${params.length}`;
|
|
147
|
-
} else {
|
|
148
|
-
return `false`;
|
|
149
|
-
}
|
|
150
|
-
} else if (name === `lte`) {
|
|
151
|
-
if (boolValue === true) {
|
|
152
|
-
return `true`;
|
|
153
|
-
} else {
|
|
154
|
-
params.push(false);
|
|
155
|
-
return `${lhs} = $${params.length}`;
|
|
156
|
-
}
|
|
157
|
-
} else if (name === `gte`) {
|
|
158
|
-
if (boolValue === false) {
|
|
159
|
-
return `true`;
|
|
160
|
-
} else {
|
|
161
|
-
params.push(true);
|
|
162
|
-
return `${lhs} = $${params.length}`;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
149
|
+
if (name === `in`) {
|
|
150
|
+
const valueArg = args[0];
|
|
151
|
+
const arrayArg = args[1];
|
|
152
|
+
if (valueArg.type === `val` && Array.isArray(valueArg.value)) {
|
|
153
|
+
throw new Error(`Array-valued 'in' left operand; expected a scalar`);
|
|
165
154
|
}
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
params.pop();
|
|
169
|
-
params.pop();
|
|
170
|
-
const rhsCompiled = compileBasicExpression(
|
|
171
|
-
rhsArg,
|
|
172
|
-
params,
|
|
173
|
-
encodeColumnName
|
|
174
|
-
);
|
|
175
|
-
if (name === `lt`) {
|
|
176
|
-
if (boolValue === true) {
|
|
177
|
-
return `false`;
|
|
178
|
-
} else {
|
|
179
|
-
params.push(true);
|
|
180
|
-
return `${rhsCompiled} = $${params.length}`;
|
|
181
|
-
}
|
|
182
|
-
} else if (name === `gt`) {
|
|
183
|
-
if (boolValue === true) {
|
|
184
|
-
params.push(false);
|
|
185
|
-
return `${rhsCompiled} = $${params.length}`;
|
|
186
|
-
} else {
|
|
187
|
-
return `false`;
|
|
188
|
-
}
|
|
189
|
-
} else if (name === `lte`) {
|
|
190
|
-
if (boolValue === false) {
|
|
191
|
-
return `true`;
|
|
192
|
-
} else {
|
|
193
|
-
params.push(true);
|
|
194
|
-
return `${rhsCompiled} = $${params.length}`;
|
|
195
|
-
}
|
|
196
|
-
} else if (name === `gte`) {
|
|
197
|
-
if (boolValue === true) {
|
|
198
|
-
return `true`;
|
|
199
|
-
} else {
|
|
200
|
-
params.push(false);
|
|
201
|
-
return `${rhsCompiled} = $${params.length}`;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
155
|
+
if (arrayArg.type === `ref` && valueArg.type === `val`) {
|
|
156
|
+
return `${lhs} = ANY(${rhs}) AND ${rhs} @> ARRAY[${lhs}] AND ${rhs} IS NOT NULL`;
|
|
204
157
|
}
|
|
205
|
-
}
|
|
206
|
-
if (name === `in`) {
|
|
207
158
|
return `${lhs} ${opName}(${rhs})`;
|
|
208
159
|
}
|
|
209
160
|
return `${lhs} ${opName} ${rhs}`;
|
|
@@ -226,7 +177,7 @@ function isBinaryOp(name) {
|
|
|
226
177
|
return binaryOps.includes(name);
|
|
227
178
|
}
|
|
228
179
|
function isComparisonOp(name) {
|
|
229
|
-
const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`];
|
|
180
|
+
const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`, `in`];
|
|
230
181
|
return comparisonOps.includes(name);
|
|
231
182
|
}
|
|
232
183
|
function isBooleanComparisonOp(name) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql-compiler.cjs","sources":["../../src/sql-compiler.ts"],"sourcesContent":["import { serialize } from './pg-serializer'\nimport type { SubsetParams } from '@electric-sql/client'\nimport type { IR, LoadSubsetOptions } from '@tanstack/db'\n\nexport type CompiledSqlRecord = Omit<SubsetParams, `params`> & {\n params?: Array<unknown>\n}\n\n/**\n * Optional function to encode column names (e.g., camelCase to snake_case)\n * This is typically the `encode` function from a columnMapper\n */\nexport type ColumnEncoder = (columnName: string) => string\n\n/**\n * Options for SQL compilation\n */\nexport interface CompileSQLOptions {\n /**\n * Optional function to encode column names before quoting.\n * Used to transform property names (e.g., camelCase) to database column names (e.g., snake_case).\n * This should be the `encode` function from shapeOptions.columnMapper.\n */\n encodeColumnName?: ColumnEncoder\n}\n\nexport function compileSQL<T>(\n options: LoadSubsetOptions,\n compileOptions?: CompileSQLOptions,\n): SubsetParams {\n const { where, orderBy, limit } = options\n const encodeColumnName = compileOptions?.encodeColumnName\n\n const params: Array<T> = []\n const compiledSQL: CompiledSqlRecord = { params }\n\n if (where) {\n // TODO: this only works when the where expression's PropRefs directly reference a column of the collection\n // 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)\n compiledSQL.where = compileBasicExpression(where, params, encodeColumnName)\n }\n\n if (orderBy) {\n compiledSQL.orderBy = compileOrderBy(orderBy, params, encodeColumnName)\n }\n\n if (limit) {\n compiledSQL.limit = limit\n }\n\n // WORKAROUND for Electric bug: Empty subset requests don't load data\n // Add dummy \"true = true\" predicate when there's no where clause\n // This is always true so doesn't filter data, just tricks Electric into loading\n if (!where) {\n compiledSQL.where = `true = true`\n }\n\n // Serialize the values in the params array into PG formatted strings\n // and transform the array into a Record<string, string>\n const paramsRecord = params.reduce(\n (acc, param, index) => {\n const serialized = serialize(param)\n // Empty strings are valid query values (e.g., WHERE column = '')\n // Only omit null/undefined values from params\n if (param != null) {\n acc[`${index + 1}`] = serialized\n }\n return acc\n },\n {} as Record<string, string>,\n )\n\n return {\n ...compiledSQL,\n params: paramsRecord,\n }\n}\n\n/**\n * Quote PostgreSQL identifiers to handle mixed case column names correctly.\n * Electric/Postgres requires quotes for case-sensitive identifiers.\n * @param name - The identifier to quote\n * @param encodeColumnName - Optional function to encode the column name before quoting (e.g., camelCase to snake_case)\n * @returns The quoted identifier\n */\nfunction quoteIdentifier(\n name: string,\n encodeColumnName?: ColumnEncoder,\n): string {\n const columnName = encodeColumnName ? encodeColumnName(name) : name\n return `\"${columnName}\"`\n}\n\n/**\n * Compiles the expression to a SQL string and mutates the params array with the values.\n * @param exp - The expression to compile\n * @param params - The params array\n * @param encodeColumnName - Optional function to encode column names (e.g., camelCase to snake_case)\n * @returns The compiled SQL string\n */\nfunction compileBasicExpression(\n exp: IR.BasicExpression<unknown>,\n params: Array<unknown>,\n encodeColumnName?: ColumnEncoder,\n): string {\n switch (exp.type) {\n case `val`:\n params.push(exp.value)\n return `$${params.length}`\n case `ref`:\n // TODO: doesn't yet support JSON(B) values which could be accessed with nested props\n if (exp.path.length !== 1) {\n throw new Error(\n `Compiler can't handle nested properties: ${exp.path.join(`.`)}`,\n )\n }\n return quoteIdentifier(exp.path[0]!, encodeColumnName)\n case `func`:\n return compileFunction(exp, params, encodeColumnName)\n default:\n throw new Error(`Unknown expression type`)\n }\n}\n\nfunction compileOrderBy(\n orderBy: IR.OrderBy,\n params: Array<unknown>,\n encodeColumnName?: ColumnEncoder,\n): string {\n const compiledOrderByClauses = orderBy.map((clause: IR.OrderByClause) =>\n compileOrderByClause(clause, params, encodeColumnName),\n )\n return compiledOrderByClauses.join(`,`)\n}\n\nfunction compileOrderByClause(\n clause: IR.OrderByClause,\n params: Array<unknown>,\n encodeColumnName?: ColumnEncoder,\n): string {\n // FIXME: We should handle stringSort and locale.\n // Correctly supporting them is tricky as it depends on Postgres' collation\n const { expression, compareOptions } = clause\n let sql = compileBasicExpression(expression, params, encodeColumnName)\n\n if (compareOptions.direction === `desc`) {\n sql = `${sql} DESC`\n }\n\n if (compareOptions.nulls === `first`) {\n sql = `${sql} NULLS FIRST`\n }\n\n if (compareOptions.nulls === `last`) {\n sql = `${sql} NULLS LAST`\n }\n\n return sql\n}\n\n/**\n * Check if a BasicExpression represents a null/undefined value\n */\nfunction isNullValue(exp: IR.BasicExpression<unknown>): boolean {\n return exp.type === `val` && (exp.value === null || exp.value === undefined)\n}\n\nfunction compileFunction(\n exp: IR.Func<unknown>,\n params: Array<unknown> = [],\n encodeColumnName?: ColumnEncoder,\n): string {\n const { name, args } = exp\n\n const opName = getOpName(name)\n\n // Handle comparison operators with null/undefined values\n // These would create invalid queries with missing params (e.g., \"col = $1\" with empty params)\n // In SQL, all comparisons with NULL return UNKNOWN, so these are almost always mistakes\n if (isComparisonOp(name)) {\n const nullArgIndex = args.findIndex((arg: IR.BasicExpression) =>\n isNullValue(arg),\n )\n\n if (nullArgIndex !== -1) {\n // All comparison operators (including eq) throw an error for null values\n // Users should use isNull() or isUndefined() to check for null values\n throw new Error(\n `Cannot use null/undefined value with '${name}' operator. ` +\n `Comparisons with null always evaluate to UNKNOWN in SQL. ` +\n `Use isNull() or isUndefined() to check for null values, ` +\n `or filter out null values before building the query.`,\n )\n }\n }\n\n const compiledArgs = args.map((arg: IR.BasicExpression) => {\n const compiled = compileBasicExpression(arg, params, encodeColumnName)\n // AND/OR group their children by precedence; NOT already wraps its operand.\n // In value positions, preserve any nested operator as a single expression.\n return arg.type === `func` &&\n (arg.name === `and` ||\n arg.name === `or` ||\n (name !== `and` &&\n name !== `or` &&\n name !== `not` &&\n (isBinaryOp(arg.name) ||\n arg.name === `not` ||\n arg.name === `isNull` ||\n arg.name === `isUndefined`)))\n ? `(${compiled})`\n : compiled\n })\n\n // Special case for IS NULL / IS NOT NULL - these are postfix operators\n if (name === `isNull` || name === `isUndefined`) {\n if (compiledArgs.length !== 1) {\n throw new Error(`${name} expects 1 argument`)\n }\n return `${compiledArgs[0]} ${opName}`\n }\n\n // Special case for NOT - unary prefix operator\n if (name === `not`) {\n if (compiledArgs.length !== 1) {\n throw new Error(`NOT expects 1 argument`)\n }\n // Check if the argument is IS NULL to generate IS NOT NULL\n const arg = args[0]\n if (arg && arg.type === `func`) {\n const funcArg = arg\n if (funcArg.name === `isNull` || funcArg.name === `isUndefined`) {\n const innerArg = compileBasicExpression(\n funcArg.args[0]!,\n params,\n encodeColumnName,\n )\n return `${innerArg} IS NOT NULL`\n }\n }\n return `${opName} (${compiledArgs[0]})`\n }\n\n if (isBinaryOp(name)) {\n // Special handling for AND/OR which can be variadic\n if ((name === `and` || name === `or`) && compiledArgs.length > 2) {\n // Chain multiple arguments: (a AND b AND c) or (a OR b OR c)\n return compiledArgs.join(` ${opName} `)\n }\n\n if (compiledArgs.length !== 2) {\n throw new Error(`Binary operator ${name} expects 2 arguments`)\n }\n const [lhs, rhs] = compiledArgs\n\n // Special case for comparison operators with boolean values\n // PostgreSQL doesn't support < > <= >= on booleans\n // Transform to equivalent equality checks or constant expressions\n if (isBooleanComparisonOp(name)) {\n const lhsArg = args[0]\n const rhsArg = args[1]\n\n // Check if RHS is a boolean literal value\n if (\n rhsArg &&\n rhsArg.type === `val` &&\n typeof rhsArg.value === `boolean`\n ) {\n const boolValue = rhsArg.value\n // Remove the boolean param we just added since we'll transform the expression\n params.pop()\n\n // Transform based on operator and boolean value\n // Boolean ordering: false < true\n if (name === `lt`) {\n if (boolValue === true) {\n // lt(col, true) → col = false (only false is less than true)\n params.push(false)\n return `${lhs} = $${params.length}`\n } else {\n // lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `gt`) {\n if (boolValue === false) {\n // gt(col, false) → col = true (only true is greater than false)\n params.push(true)\n return `${lhs} = $${params.length}`\n } else {\n // gt(col, true) → nothing is greater than true\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === true) {\n // lte(col, true) → everything is ≤ true\n return `true`\n } else {\n // lte(col, false) → col = false\n params.push(false)\n return `${lhs} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === false) {\n // gte(col, false) → everything is ≥ false\n return `true`\n } else {\n // gte(col, true) → col = true\n params.push(true)\n return `${lhs} = $${params.length}`\n }\n }\n }\n\n // Check if LHS is a boolean literal value (less common but handle it)\n if (\n lhsArg &&\n lhsArg.type === `val` &&\n typeof lhsArg.value === `boolean`\n ) {\n const boolValue = lhsArg.value\n // Remove params for this expression and rebuild\n params.pop() // remove RHS\n params.pop() // remove LHS (boolean)\n\n // Recompile RHS to get fresh param\n const rhsCompiled = compileBasicExpression(\n rhsArg!,\n params,\n encodeColumnName,\n )\n\n // Transform: flip the comparison (val op col → col flipped_op val)\n if (name === `lt`) {\n // lt(true, col) → gt(col, true) → col > true → nothing is greater than true\n if (boolValue === true) {\n return `false`\n } else {\n // lt(false, col) → gt(col, false) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gt`) {\n // gt(true, col) → lt(col, true) → col = false\n if (boolValue === true) {\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n } else {\n // gt(false, col) → lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === false) {\n // lte(false, col) → gte(col, false) → everything\n return `true`\n } else {\n // lte(true, col) → gte(col, true) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === true) {\n // gte(true, col) → lte(col, true) → everything\n return `true`\n } else {\n // gte(false, col) → lte(col, false) → col = false\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n }\n }\n }\n }\n\n // Special case for = ANY operator which needs parentheses around the array parameter\n if (name === `in`) {\n return `${lhs} ${opName}(${rhs})`\n }\n return `${lhs} ${opName} ${rhs}`\n }\n\n return `${opName}(${compiledArgs.join(`,`)})`\n}\n\nfunction isBinaryOp(name: string): boolean {\n const binaryOps = [\n `eq`,\n `gt`,\n `gte`,\n `lt`,\n `lte`,\n `and`,\n `or`,\n `in`,\n `like`,\n `ilike`,\n ]\n return binaryOps.includes(name)\n}\n\n/**\n * Check if operator is a comparison operator that takes two values\n * These operators cannot accept null/undefined as values\n * (null comparisons in SQL always evaluate to UNKNOWN)\n */\nfunction isComparisonOp(name: string): boolean {\n const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`]\n return comparisonOps.includes(name)\n}\n\n/**\n * Checks if the operator is a comparison operator (excluding eq)\n * These operators don't work on booleans in PostgreSQL without casting\n */\nfunction isBooleanComparisonOp(name: string): boolean {\n return [`gt`, `gte`, `lt`, `lte`].includes(name)\n}\n\nfunction getOpName(name: string): string {\n const opNames = {\n eq: `=`,\n gt: `>`,\n gte: `>=`,\n lt: `<`,\n lte: `<=`,\n add: `+`,\n and: `AND`,\n or: `OR`,\n not: `NOT`,\n isUndefined: `IS NULL`,\n isNull: `IS NULL`,\n in: `= ANY`, // Use = ANY syntax for array parameters\n like: `LIKE`,\n ilike: `ILIKE`,\n upper: `UPPER`,\n lower: `LOWER`,\n length: `LENGTH`,\n concat: `CONCAT`,\n coalesce: `COALESCE`,\n }\n\n const opName = opNames[name as keyof typeof opNames]\n\n if (!opName) {\n throw new Error(`Unknown operator/function: ${name}`)\n }\n\n return opName\n}\n"],"names":["serialize"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sql-compiler.cjs","sources":["../../src/sql-compiler.ts"],"sourcesContent":["import { serialize } from './pg-serializer'\nimport type { SubsetParams } from '@electric-sql/client'\nimport type { IR, LoadSubsetOptions } from '@tanstack/db'\n\nexport type CompiledSqlRecord = Omit<SubsetParams, `params`> & {\n params?: Array<unknown>\n}\n\n/**\n * Optional function to encode column names (e.g., camelCase to snake_case)\n * This is typically the `encode` function from a columnMapper\n */\nexport type ColumnEncoder = (columnName: string) => string\n\n/**\n * Options for SQL compilation\n */\nexport interface CompileSQLOptions {\n /**\n * Optional function to encode column names before quoting.\n * Used to transform property names (e.g., camelCase) to database column names (e.g., snake_case).\n * This should be the `encode` function from shapeOptions.columnMapper.\n */\n encodeColumnName?: ColumnEncoder\n}\n\nexport function compileSQL<T>(\n options: LoadSubsetOptions,\n compileOptions?: CompileSQLOptions,\n): SubsetParams {\n const { where, orderBy, limit } = options\n const encodeColumnName = compileOptions?.encodeColumnName\n\n const params: Array<T> = []\n const compiledSQL: CompiledSqlRecord = { params }\n\n if (where) {\n // TODO: this only works when the where expression's PropRefs directly reference a column of the collection\n // 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)\n compiledSQL.where = compileBasicExpression(where, params, encodeColumnName)\n }\n\n if (orderBy) {\n compiledSQL.orderBy = compileOrderBy(orderBy, params, encodeColumnName)\n }\n\n if (limit) {\n compiledSQL.limit = limit\n }\n\n // WORKAROUND for Electric bug: Empty subset requests don't load data\n // Add dummy \"true = true\" predicate when there's no where clause\n // This is always true so doesn't filter data, just tricks Electric into loading\n if (!where) {\n compiledSQL.where = `true = true`\n }\n\n // Serialize the values in the params array into PG formatted strings\n // and transform the array into a Record<string, string>\n const paramsRecord = params.reduce(\n (acc, param, index) => {\n const serialized = serialize(param)\n // Empty strings are valid query values (e.g., WHERE column = '')\n // Only omit null/undefined values from params\n if (param != null) {\n acc[`${index + 1}`] = serialized\n }\n return acc\n },\n {} as Record<string, string>,\n )\n\n return {\n ...compiledSQL,\n params: paramsRecord,\n }\n}\n\n/**\n * Quote PostgreSQL identifiers to handle mixed case column names correctly.\n * Electric/Postgres requires quotes for case-sensitive identifiers.\n * @param name - The identifier to quote\n * @param encodeColumnName - Optional function to encode the column name before quoting (e.g., camelCase to snake_case)\n * @returns The quoted identifier\n */\nfunction quoteIdentifier(\n name: string,\n encodeColumnName?: ColumnEncoder,\n): string {\n const columnName = encodeColumnName ? encodeColumnName(name) : name\n return `\"${columnName.replace(/\"/g, `\"\"`)}\"`\n}\n\n/**\n * Compiles the expression to a SQL string and mutates the params array with the values.\n * @param exp - The expression to compile\n * @param params - The params array\n * @param encodeColumnName - Optional function to encode column names (e.g., camelCase to snake_case)\n * @returns The compiled SQL string\n */\nfunction compileBasicExpression(\n exp: IR.BasicExpression<unknown>,\n params: Array<unknown>,\n encodeColumnName?: ColumnEncoder,\n): string {\n switch (exp.type) {\n case `val`:\n params.push(exp.value)\n return `$${params.length}`\n case `ref`:\n if (exp.path.length !== 1) {\n throw new Error(\n `Compiler can't handle nested properties: ${exp.path.join(`.`)}`,\n )\n }\n return quoteIdentifier(exp.path[0]!, encodeColumnName)\n case `func`:\n return compileFunction(exp, params, encodeColumnName)\n default:\n throw new Error(`Unknown expression type`)\n }\n}\n\nfunction compileOrderBy(\n orderBy: IR.OrderBy,\n params: Array<unknown>,\n encodeColumnName?: ColumnEncoder,\n): string {\n const compiledOrderByClauses = orderBy.map((clause: IR.OrderByClause) =>\n compileOrderByClause(clause, params, encodeColumnName),\n )\n return compiledOrderByClauses.join(`,`)\n}\n\nfunction compileOrderByClause(\n clause: IR.OrderByClause,\n params: Array<unknown>,\n encodeColumnName?: ColumnEncoder,\n): string {\n // FIXME: We should handle stringSort and locale.\n // Correctly supporting them is tricky as it depends on Postgres' collation\n const { expression, compareOptions } = clause\n let sql = compileBasicExpression(expression, params, encodeColumnName)\n\n if (compareOptions.direction === `desc`) {\n sql = `${sql} DESC`\n }\n\n if (compareOptions.nulls === `first`) {\n sql = `${sql} NULLS FIRST`\n }\n\n if (compareOptions.nulls === `last`) {\n sql = `${sql} NULLS LAST`\n }\n\n return sql\n}\n\n/**\n * Check if a BasicExpression represents a null/undefined value\n */\nfunction isNullValue(exp: IR.BasicExpression<unknown>): boolean {\n return exp.type === `val` && (exp.value === null || exp.value === undefined)\n}\n\nfunction compileFunction(\n exp: IR.Func<unknown>,\n params: Array<unknown> = [],\n encodeColumnName?: ColumnEncoder,\n): string {\n const { name, args } = exp\n\n const opName = getOpName(name)\n\n // Handle comparison operators with null/undefined values\n // These would create invalid queries with missing params (e.g., \"col = $1\" with empty params)\n // In SQL, all comparisons with NULL return UNKNOWN, so these are almost always mistakes\n if (isComparisonOp(name)) {\n const nullArgIndex = args.findIndex((arg: IR.BasicExpression) =>\n isNullValue(arg),\n )\n\n if (nullArgIndex !== -1) {\n // All comparison operators (including eq) throw an error for null values\n // Users should use isNull() or isUndefined() to check for null values\n throw new Error(\n `Cannot use null/undefined value with '${name}' operator. ` +\n `Comparisons with null always evaluate to UNKNOWN in SQL. ` +\n `Use isNull() or isUndefined() to check for null values, ` +\n `or filter out null values before building the query.`,\n )\n }\n }\n\n if (name === `not`) {\n if (args.length !== 1) throw new Error(`NOT expects 1 argument`)\n const arg = args[0]\n if (\n arg?.type === `func` &&\n (arg.name === `isNull` || arg.name === `isUndefined`)\n ) {\n if (arg.args.length !== 1) {\n throw new Error(`${arg.name} expects 1 argument`)\n }\n const innerArg = arg.args[0]!\n const compiled = compileBasicExpression(\n innerArg,\n params,\n encodeColumnName,\n )\n return `${innerArg.type === `func` ? `(${compiled})` : compiled} IS NOT NULL`\n }\n }\n\n const booleanLiteralIndex = args.findIndex(\n (arg) => arg.type === `val` && typeof arg.value === `boolean`,\n )\n if (\n args.length === 2 &&\n isBooleanComparisonOp(name) &&\n booleanLiteralIndex !== -1\n ) {\n const literalValue = (args[booleanLiteralIndex] as IR.Value<boolean>).value\n const valueArg = args[booleanLiteralIndex === 0 ? 1 : 0]!\n const compiled = compileBasicExpression(valueArg, params, encodeColumnName)\n const value = `(${compiled})`\n const lessThan =\n (name === `lt` || name === `lte`) === (booleanLiteralIndex === 1)\n const inclusive = name === `lte` || name === `gte`\n\n if (inclusive && literalValue === lessThan) {\n return `${value} = ${value}`\n }\n if (!inclusive && literalValue !== lessThan) {\n return `${value} <> ${value}`\n }\n return `${value} = ${lessThan ? `FALSE` : `TRUE`}`\n }\n\n const compiledArgs = args.map((arg: IR.BasicExpression) => {\n const compiled = compileBasicExpression(arg, params, encodeColumnName)\n // AND/OR group their children by precedence; NOT already wraps its operand.\n // In value positions, preserve any nested operator as a single expression.\n return arg.type === `func` &&\n (arg.name === `and` ||\n arg.name === `or` ||\n (name !== `and` &&\n name !== `or` &&\n name !== `not` &&\n (isBinaryOp(arg.name) ||\n arg.name === `not` ||\n arg.name === `isNull` ||\n arg.name === `isUndefined`)))\n ? `(${compiled})`\n : compiled\n })\n\n // Special case for IS NULL / IS NOT NULL - these are postfix operators\n if (name === `isNull` || name === `isUndefined`) {\n if (compiledArgs.length !== 1) {\n throw new Error(`${name} expects 1 argument`)\n }\n return `${compiledArgs[0]} ${opName}`\n }\n\n // Special case for NOT - unary prefix operator\n if (name === `not`) {\n return `${opName} (${compiledArgs[0]})`\n }\n\n if (isBinaryOp(name)) {\n // Special handling for AND/OR which can be variadic\n if ((name === `and` || name === `or`) && compiledArgs.length > 2) {\n // Chain multiple arguments: (a AND b AND c) or (a OR b OR c)\n return compiledArgs.join(` ${opName} `)\n }\n\n if (compiledArgs.length !== 2) {\n throw new Error(`Binary operator ${name} expects 2 arguments`)\n }\n const [lhs, rhs] = compiledArgs\n\n if (name === `in`) {\n const valueArg = args[0]!\n const arrayArg = args[1]!\n\n if (valueArg.type === `val` && Array.isArray(valueArg.value)) {\n throw new Error(`Array-valued 'in' left operand; expected a scalar`)\n }\n\n if (arrayArg.type === `ref` && valueArg.type === `val`) {\n // Resolve literal parameters from the array element type before containment.\n return `${lhs} = ANY(${rhs}) AND ${rhs} @> ARRAY[${lhs}] AND ${rhs} IS NOT NULL`\n }\n\n // Literal value lists and ref/ref membership retain the original = ANY form.\n return `${lhs} ${opName}(${rhs})`\n }\n return `${lhs} ${opName} ${rhs}`\n }\n\n return `${opName}(${compiledArgs.join(`,`)})`\n}\n\nfunction isBinaryOp(name: string): boolean {\n const binaryOps = [\n `eq`,\n `gt`,\n `gte`,\n `lt`,\n `lte`,\n `and`,\n `or`,\n `in`,\n `like`,\n `ilike`,\n ]\n return binaryOps.includes(name)\n}\n\n/**\n * Check if operator is a comparison operator that takes two values\n * These operators cannot accept null/undefined as values\n * (null comparisons in SQL always evaluate to UNKNOWN)\n */\nfunction isComparisonOp(name: string): boolean {\n const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`, `in`]\n return comparisonOps.includes(name)\n}\n\n/**\n * Checks if the operator is a comparison operator (excluding eq)\n * These operators don't work on booleans in PostgreSQL without casting\n */\nfunction isBooleanComparisonOp(name: string): boolean {\n return [`gt`, `gte`, `lt`, `lte`].includes(name)\n}\n\nfunction getOpName(name: string): string {\n const opNames = {\n eq: `=`,\n gt: `>`,\n gte: `>=`,\n lt: `<`,\n lte: `<=`,\n add: `+`,\n and: `AND`,\n or: `OR`,\n not: `NOT`,\n isUndefined: `IS NULL`,\n isNull: `IS NULL`,\n in: `= ANY`, // Use = ANY syntax for array parameters\n like: `LIKE`,\n ilike: `ILIKE`,\n upper: `UPPER`,\n lower: `LOWER`,\n length: `LENGTH`,\n concat: `CONCAT`,\n coalesce: `COALESCE`,\n }\n\n const opName = opNames[name as keyof typeof opNames]\n\n if (!opName) {\n throw new Error(`Unknown operator/function: ${name}`)\n }\n\n return opName\n}\n"],"names":["serialize"],"mappings":"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"}
|