@electric-sql/client 1.3.1 → 1.4.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/index.cjs +166 -11
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +67 -2
- package/dist/index.browser.mjs +3 -3
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +67 -2
- package/dist/index.legacy-esm.js +164 -11
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +164 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +109 -3
- package/src/constants.ts +6 -0
- package/src/error.ts +7 -0
- package/src/expression-compiler.ts +132 -0
- package/src/index.ts +1 -0
- package/src/types.ts +28 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-sql/client",
|
|
3
3
|
"description": "Postgres everywhere - your data, in sync, wherever you need it.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.1",
|
|
5
5
|
"author": "ElectricSQL team and contributors.",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/electric-sql/electric/issues"
|
package/src/client.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
MissingShapeHandleError,
|
|
25
25
|
ReservedParamError,
|
|
26
26
|
MissingHeadersError,
|
|
27
|
+
StaleCacheError,
|
|
27
28
|
} from './error'
|
|
28
29
|
import {
|
|
29
30
|
BackoffDefaults,
|
|
@@ -59,7 +60,11 @@ import {
|
|
|
59
60
|
SUBSET_PARAM_LIMIT,
|
|
60
61
|
SUBSET_PARAM_OFFSET,
|
|
61
62
|
SUBSET_PARAM_ORDER_BY,
|
|
63
|
+
SUBSET_PARAM_WHERE_EXPR,
|
|
64
|
+
SUBSET_PARAM_ORDER_BY_EXPR,
|
|
65
|
+
CACHE_BUSTER_QUERY_PARAM,
|
|
62
66
|
} from './constants'
|
|
67
|
+
import { compileExpression, compileOrderBy } from './expression-compiler'
|
|
63
68
|
import {
|
|
64
69
|
EventSourceMessage,
|
|
65
70
|
fetchEventSource,
|
|
@@ -73,6 +78,7 @@ const RESERVED_PARAMS: Set<ReservedParamKeys> = new Set([
|
|
|
73
78
|
SHAPE_HANDLE_QUERY_PARAM,
|
|
74
79
|
LIVE_QUERY_PARAM,
|
|
75
80
|
OFFSET_QUERY_PARAM,
|
|
81
|
+
CACHE_BUSTER_QUERY_PARAM,
|
|
76
82
|
])
|
|
77
83
|
|
|
78
84
|
type Replica = `full` | `default`
|
|
@@ -138,6 +144,7 @@ type ReservedParamKeys =
|
|
|
138
144
|
| typeof SHAPE_HANDLE_QUERY_PARAM
|
|
139
145
|
| typeof LIVE_QUERY_PARAM
|
|
140
146
|
| typeof OFFSET_QUERY_PARAM
|
|
147
|
+
| typeof CACHE_BUSTER_QUERY_PARAM
|
|
141
148
|
| `subset__${string}`
|
|
142
149
|
|
|
143
150
|
/**
|
|
@@ -570,6 +577,9 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
570
577
|
#sseBackoffBaseDelay = 100 // Base delay for exponential backoff (ms)
|
|
571
578
|
#sseBackoffMaxDelay = 5000 // Maximum delay cap (ms)
|
|
572
579
|
#unsubscribeFromVisibilityChanges?: () => void
|
|
580
|
+
#staleCacheBuster?: string // Cache buster set when stale CDN response detected, used on retry requests to bypass cache
|
|
581
|
+
#staleCacheRetryCount = 0
|
|
582
|
+
#maxStaleCacheRetries = 3
|
|
573
583
|
|
|
574
584
|
// Derived state: we're in replay mode if we have a last seen cursor
|
|
575
585
|
get #replayMode(): boolean {
|
|
@@ -669,7 +679,8 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
669
679
|
if (this.#onError) {
|
|
670
680
|
const retryOpts = await this.#onError(err as Error)
|
|
671
681
|
// Guard against null (typeof null === "object" in JavaScript)
|
|
672
|
-
|
|
682
|
+
const isRetryable = !(err instanceof MissingHeadersError)
|
|
683
|
+
if (retryOpts && typeof retryOpts === `object` && isRetryable) {
|
|
673
684
|
// Update params/headers but don't reset offset
|
|
674
685
|
// We want to continue from where we left off, not refetch everything
|
|
675
686
|
if (retryOpts.params) {
|
|
@@ -780,6 +791,15 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
780
791
|
}
|
|
781
792
|
return // interrupted
|
|
782
793
|
}
|
|
794
|
+
|
|
795
|
+
if (e instanceof StaleCacheError) {
|
|
796
|
+
// Received a stale cached response from CDN with an expired handle.
|
|
797
|
+
// The #staleCacheBuster has been set in #onInitialResponse, so retry
|
|
798
|
+
// the request which will include a random cache buster to bypass the
|
|
799
|
+
// misconfigured CDN cache.
|
|
800
|
+
return this.#requestShape()
|
|
801
|
+
}
|
|
802
|
+
|
|
783
803
|
if (!(e instanceof FetchError)) throw e // should never happen
|
|
784
804
|
|
|
785
805
|
if (e.status == 409) {
|
|
@@ -890,13 +910,29 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
890
910
|
}
|
|
891
911
|
|
|
892
912
|
if (subsetParams) {
|
|
893
|
-
|
|
913
|
+
// Prefer structured expressions when available (allows proper columnMapper application)
|
|
914
|
+
// Fall back to legacy string format for backwards compatibility
|
|
915
|
+
if (subsetParams.whereExpr) {
|
|
916
|
+
// Compile structured expression with columnMapper applied
|
|
917
|
+
const compiledWhere = compileExpression(
|
|
918
|
+
subsetParams.whereExpr,
|
|
919
|
+
this.options.columnMapper?.encode
|
|
920
|
+
)
|
|
921
|
+
setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, compiledWhere)
|
|
922
|
+
// Also send the structured expression for servers that support it
|
|
923
|
+
fetchUrl.searchParams.set(
|
|
924
|
+
SUBSET_PARAM_WHERE_EXPR,
|
|
925
|
+
JSON.stringify(subsetParams.whereExpr)
|
|
926
|
+
)
|
|
927
|
+
} else if (subsetParams.where && typeof subsetParams.where === `string`) {
|
|
928
|
+
// Legacy string format (no columnMapper applied to already-compiled SQL)
|
|
894
929
|
const encodedWhere = encodeWhereClause(
|
|
895
930
|
subsetParams.where,
|
|
896
931
|
this.options.columnMapper?.encode
|
|
897
932
|
)
|
|
898
933
|
setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, encodedWhere)
|
|
899
934
|
}
|
|
935
|
+
|
|
900
936
|
if (subsetParams.params)
|
|
901
937
|
// Serialize params as JSON to keep the parameter name constant for proxy configs
|
|
902
938
|
fetchUrl.searchParams.set(
|
|
@@ -907,7 +943,25 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
907
943
|
setQueryParam(fetchUrl, SUBSET_PARAM_LIMIT, subsetParams.limit)
|
|
908
944
|
if (subsetParams.offset)
|
|
909
945
|
setQueryParam(fetchUrl, SUBSET_PARAM_OFFSET, subsetParams.offset)
|
|
910
|
-
|
|
946
|
+
|
|
947
|
+
// Prefer structured ORDER BY expressions when available
|
|
948
|
+
if (subsetParams.orderByExpr) {
|
|
949
|
+
// Compile structured ORDER BY with columnMapper applied
|
|
950
|
+
const compiledOrderBy = compileOrderBy(
|
|
951
|
+
subsetParams.orderByExpr,
|
|
952
|
+
this.options.columnMapper?.encode
|
|
953
|
+
)
|
|
954
|
+
setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, compiledOrderBy)
|
|
955
|
+
// Also send the structured expression for servers that support it
|
|
956
|
+
fetchUrl.searchParams.set(
|
|
957
|
+
SUBSET_PARAM_ORDER_BY_EXPR,
|
|
958
|
+
JSON.stringify(subsetParams.orderByExpr)
|
|
959
|
+
)
|
|
960
|
+
} else if (
|
|
961
|
+
subsetParams.orderBy &&
|
|
962
|
+
typeof subsetParams.orderBy === `string`
|
|
963
|
+
) {
|
|
964
|
+
// Legacy string format
|
|
911
965
|
const encodedOrderBy = encodeWhereClause(
|
|
912
966
|
subsetParams.orderBy,
|
|
913
967
|
this.options.columnMapper?.encode
|
|
@@ -948,6 +1002,15 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
948
1002
|
fetchUrl.searchParams.set(EXPIRED_HANDLE_QUERY_PARAM, expiredHandle)
|
|
949
1003
|
}
|
|
950
1004
|
|
|
1005
|
+
// Add random cache buster if we received a stale response from CDN
|
|
1006
|
+
// This forces a fresh request bypassing the misconfigured CDN cache
|
|
1007
|
+
if (this.#staleCacheBuster) {
|
|
1008
|
+
fetchUrl.searchParams.set(
|
|
1009
|
+
CACHE_BUSTER_QUERY_PARAM,
|
|
1010
|
+
this.#staleCacheBuster
|
|
1011
|
+
)
|
|
1012
|
+
}
|
|
1013
|
+
|
|
951
1014
|
// sort query params in-place for stable URLs and improved cache hits
|
|
952
1015
|
fetchUrl.searchParams.sort()
|
|
953
1016
|
|
|
@@ -993,6 +1056,46 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
993
1056
|
: null
|
|
994
1057
|
if (shapeHandle !== expiredHandle) {
|
|
995
1058
|
this.#shapeHandle = shapeHandle
|
|
1059
|
+
// Clear cache buster after successful response with valid handle
|
|
1060
|
+
if (this.#staleCacheBuster) {
|
|
1061
|
+
this.#staleCacheBuster = undefined
|
|
1062
|
+
this.#staleCacheRetryCount = 0
|
|
1063
|
+
}
|
|
1064
|
+
} else if (this.#shapeHandle === undefined) {
|
|
1065
|
+
// We received a stale response from cache and don't have a handle yet.
|
|
1066
|
+
// Instead of accepting the stale handle, throw an error to trigger a retry
|
|
1067
|
+
// with a random cache buster to bypass the CDN cache.
|
|
1068
|
+
this.#staleCacheRetryCount++
|
|
1069
|
+
// Cancel the response body to release the connection before retrying
|
|
1070
|
+
await response.body?.cancel()
|
|
1071
|
+
if (this.#staleCacheRetryCount > this.#maxStaleCacheRetries) {
|
|
1072
|
+
throw new FetchError(
|
|
1073
|
+
502,
|
|
1074
|
+
undefined,
|
|
1075
|
+
undefined,
|
|
1076
|
+
{},
|
|
1077
|
+
this.#currentFetchUrl?.toString() ?? ``,
|
|
1078
|
+
`CDN continues serving stale cached responses after ${this.#maxStaleCacheRetries} retry attempts. ` +
|
|
1079
|
+
`This indicates a severe proxy/CDN misconfiguration. ` +
|
|
1080
|
+
`Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key. ` +
|
|
1081
|
+
`For more information visit the troubleshooting guide: https://electric-sql.com/docs/guides/troubleshooting`
|
|
1082
|
+
)
|
|
1083
|
+
}
|
|
1084
|
+
console.warn(
|
|
1085
|
+
`[Electric] Received stale cached response with expired shape handle. ` +
|
|
1086
|
+
`This should not happen and indicates a proxy/CDN caching misconfiguration. ` +
|
|
1087
|
+
`The response contained handle "${shapeHandle}" which was previously marked as expired. ` +
|
|
1088
|
+
`Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key. ` +
|
|
1089
|
+
`For more information visit the troubleshooting guide: https://electric-sql.com/docs/guides/troubleshooting ` +
|
|
1090
|
+
`Retrying with a random cache buster to bypass the stale cache (attempt ${this.#staleCacheRetryCount}/${this.#maxStaleCacheRetries}).`
|
|
1091
|
+
)
|
|
1092
|
+
// Generate a random cache buster for the retry
|
|
1093
|
+
this.#staleCacheBuster = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
|
|
1094
|
+
throw new StaleCacheError(
|
|
1095
|
+
`Received stale cached response with expired handle "${shapeHandle}". ` +
|
|
1096
|
+
`This indicates a proxy/CDN caching misconfiguration. ` +
|
|
1097
|
+
`Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key.`
|
|
1098
|
+
)
|
|
996
1099
|
} else {
|
|
997
1100
|
console.warn(
|
|
998
1101
|
`[Electric] Received stale cached response with expired shape handle. ` +
|
|
@@ -1453,6 +1556,9 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1453
1556
|
// Reset SSE fallback state to try SSE again after reset
|
|
1454
1557
|
this.#consecutiveShortSseConnections = 0
|
|
1455
1558
|
this.#sseFallbackToLongPolling = false
|
|
1559
|
+
// Reset stale cache retry state
|
|
1560
|
+
this.#staleCacheBuster = undefined
|
|
1561
|
+
this.#staleCacheRetryCount = 0
|
|
1456
1562
|
}
|
|
1457
1563
|
|
|
1458
1564
|
/**
|
package/src/constants.ts
CHANGED
|
@@ -26,6 +26,9 @@ export const SUBSET_PARAM_LIMIT = `subset__limit`
|
|
|
26
26
|
export const SUBSET_PARAM_OFFSET = `subset__offset`
|
|
27
27
|
export const SUBSET_PARAM_ORDER_BY = `subset__order_by`
|
|
28
28
|
export const SUBSET_PARAM_WHERE_PARAMS = `subset__params`
|
|
29
|
+
export const SUBSET_PARAM_WHERE_EXPR = `subset__where_expr`
|
|
30
|
+
export const SUBSET_PARAM_ORDER_BY_EXPR = `subset__order_by_expr`
|
|
31
|
+
export const CACHE_BUSTER_QUERY_PARAM = `cache-buster` // Random cache buster to bypass stale CDN responses
|
|
29
32
|
|
|
30
33
|
// Query parameters that should be passed through when proxying Electric requests
|
|
31
34
|
export const ELECTRIC_PROTOCOL_QUERY_PARAMS: Array<string> = [
|
|
@@ -41,4 +44,7 @@ export const ELECTRIC_PROTOCOL_QUERY_PARAMS: Array<string> = [
|
|
|
41
44
|
SUBSET_PARAM_OFFSET,
|
|
42
45
|
SUBSET_PARAM_ORDER_BY,
|
|
43
46
|
SUBSET_PARAM_WHERE_PARAMS,
|
|
47
|
+
SUBSET_PARAM_WHERE_EXPR,
|
|
48
|
+
SUBSET_PARAM_ORDER_BY_EXPR,
|
|
49
|
+
CACHE_BUSTER_QUERY_PARAM,
|
|
44
50
|
]
|
package/src/error.ts
CHANGED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { SerializedExpression, SerializedOrderByClause } from './types'
|
|
2
|
+
import { quoteIdentifier } from './column-mapper'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compiles a serialized expression into a SQL string.
|
|
6
|
+
* Applies columnMapper transformations to column references.
|
|
7
|
+
*
|
|
8
|
+
* @param expr - The serialized expression to compile
|
|
9
|
+
* @param columnMapper - Optional function to transform column names (e.g., camelCase to snake_case)
|
|
10
|
+
* @returns The compiled SQL string
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const expr = { type: 'ref', column: 'userId' }
|
|
15
|
+
* compileExpression(expr, camelToSnake) // '"user_id"'
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function compileExpression(
|
|
19
|
+
expr: SerializedExpression,
|
|
20
|
+
columnMapper?: (col: string) => string
|
|
21
|
+
): string {
|
|
22
|
+
switch (expr.type) {
|
|
23
|
+
case `ref`: {
|
|
24
|
+
// Apply columnMapper, then quote
|
|
25
|
+
const mappedColumn = columnMapper
|
|
26
|
+
? columnMapper(expr.column)
|
|
27
|
+
: expr.column
|
|
28
|
+
return quoteIdentifier(mappedColumn)
|
|
29
|
+
}
|
|
30
|
+
case `val`:
|
|
31
|
+
return `$${expr.paramIndex}`
|
|
32
|
+
case `func`:
|
|
33
|
+
return compileFunction(expr, columnMapper)
|
|
34
|
+
default: {
|
|
35
|
+
// TypeScript exhaustiveness check
|
|
36
|
+
const _exhaustive: never = expr
|
|
37
|
+
throw new Error(`Unknown expression type: ${JSON.stringify(_exhaustive)}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Compiles a function expression into SQL.
|
|
44
|
+
*/
|
|
45
|
+
function compileFunction(
|
|
46
|
+
expr: { type: `func`; name: string; args: SerializedExpression[] },
|
|
47
|
+
columnMapper?: (col: string) => string
|
|
48
|
+
): string {
|
|
49
|
+
const args = expr.args.map((arg) => compileExpression(arg, columnMapper))
|
|
50
|
+
|
|
51
|
+
switch (expr.name) {
|
|
52
|
+
// Binary comparison operators
|
|
53
|
+
case `eq`:
|
|
54
|
+
return `${args[0]} = ${args[1]}`
|
|
55
|
+
case `gt`:
|
|
56
|
+
return `${args[0]} > ${args[1]}`
|
|
57
|
+
case `gte`:
|
|
58
|
+
return `${args[0]} >= ${args[1]}`
|
|
59
|
+
case `lt`:
|
|
60
|
+
return `${args[0]} < ${args[1]}`
|
|
61
|
+
case `lte`:
|
|
62
|
+
return `${args[0]} <= ${args[1]}`
|
|
63
|
+
|
|
64
|
+
// Logical operators
|
|
65
|
+
case `and`:
|
|
66
|
+
return args.map((a) => `(${a})`).join(` AND `)
|
|
67
|
+
case `or`:
|
|
68
|
+
return args.map((a) => `(${a})`).join(` OR `)
|
|
69
|
+
case `not`:
|
|
70
|
+
return `NOT (${args[0]})`
|
|
71
|
+
|
|
72
|
+
// Special operators
|
|
73
|
+
case `in`:
|
|
74
|
+
return `${args[0]} = ANY(${args[1]})`
|
|
75
|
+
case `like`:
|
|
76
|
+
return `${args[0]} LIKE ${args[1]}`
|
|
77
|
+
case `ilike`:
|
|
78
|
+
return `${args[0]} ILIKE ${args[1]}`
|
|
79
|
+
case `isNull`:
|
|
80
|
+
case `isUndefined`:
|
|
81
|
+
return `${args[0]} IS NULL`
|
|
82
|
+
|
|
83
|
+
// String functions
|
|
84
|
+
case `upper`:
|
|
85
|
+
return `UPPER(${args[0]})`
|
|
86
|
+
case `lower`:
|
|
87
|
+
return `LOWER(${args[0]})`
|
|
88
|
+
case `length`:
|
|
89
|
+
return `LENGTH(${args[0]})`
|
|
90
|
+
case `concat`:
|
|
91
|
+
return `CONCAT(${args.join(`, `)})`
|
|
92
|
+
|
|
93
|
+
// Other functions
|
|
94
|
+
case `coalesce`:
|
|
95
|
+
return `COALESCE(${args.join(`, `)})`
|
|
96
|
+
|
|
97
|
+
default:
|
|
98
|
+
throw new Error(`Unknown function: ${expr.name}`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Compiles serialized ORDER BY clauses into a SQL string.
|
|
104
|
+
* Applies columnMapper transformations to column references.
|
|
105
|
+
*
|
|
106
|
+
* @param clauses - The serialized ORDER BY clauses to compile
|
|
107
|
+
* @param columnMapper - Optional function to transform column names
|
|
108
|
+
* @returns The compiled SQL ORDER BY string
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* const clauses = [{ column: 'createdAt', direction: 'desc', nulls: 'first' }]
|
|
113
|
+
* compileOrderBy(clauses, camelToSnake) // '"created_at" DESC NULLS FIRST'
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export function compileOrderBy(
|
|
117
|
+
clauses: SerializedOrderByClause[],
|
|
118
|
+
columnMapper?: (col: string) => string
|
|
119
|
+
): string {
|
|
120
|
+
return clauses
|
|
121
|
+
.map((clause) => {
|
|
122
|
+
const mappedColumn = columnMapper
|
|
123
|
+
? columnMapper(clause.column)
|
|
124
|
+
: clause.column
|
|
125
|
+
let sql = quoteIdentifier(mappedColumn)
|
|
126
|
+
if (clause.direction === `desc`) sql += ` DESC`
|
|
127
|
+
if (clause.nulls === `first`) sql += ` NULLS FIRST`
|
|
128
|
+
if (clause.nulls === `last`) sql += ` NULLS LAST`
|
|
129
|
+
return sql
|
|
130
|
+
})
|
|
131
|
+
.join(`, `)
|
|
132
|
+
}
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -68,12 +68,40 @@ export type MoveTag = string
|
|
|
68
68
|
*/
|
|
69
69
|
export type MoveOutPattern = { pos: number; value: string }
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Serialized expression types for structured subset queries.
|
|
73
|
+
* These allow Electric to properly apply columnMapper transformations
|
|
74
|
+
* before generating the final SQL.
|
|
75
|
+
*/
|
|
76
|
+
export type SerializedExpression =
|
|
77
|
+
| { type: `ref`; column: string } // Column reference
|
|
78
|
+
| { type: `val`; paramIndex: number } // Parameter placeholder ($1, $2, etc.)
|
|
79
|
+
| { type: `func`; name: string; args: SerializedExpression[] } // Operator/function
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Serialized ORDER BY clause for structured subset queries.
|
|
83
|
+
*/
|
|
84
|
+
export type SerializedOrderByClause = {
|
|
85
|
+
column: string
|
|
86
|
+
direction?: `asc` | `desc` // omitted means 'asc'
|
|
87
|
+
nulls?: `first` | `last`
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
export type SubsetParams = {
|
|
91
|
+
/** Legacy string format WHERE clause */
|
|
72
92
|
where?: string
|
|
93
|
+
/** Positional parameter values for WHERE clause */
|
|
73
94
|
params?: Record<string, string>
|
|
95
|
+
/** Maximum number of rows to return */
|
|
74
96
|
limit?: number
|
|
97
|
+
/** Number of rows to skip */
|
|
75
98
|
offset?: number
|
|
99
|
+
/** Legacy string format ORDER BY clause */
|
|
76
100
|
orderBy?: string
|
|
101
|
+
/** Structured WHERE expression (preferred when available) */
|
|
102
|
+
whereExpr?: SerializedExpression
|
|
103
|
+
/** Structured ORDER BY clauses (preferred when available) */
|
|
104
|
+
orderByExpr?: SerializedOrderByClause[]
|
|
77
105
|
}
|
|
78
106
|
|
|
79
107
|
export type ControlMessage = {
|