orez 0.1.13 → 0.1.15
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/bench/serial-mutations.bench.d.ts +10 -0
- package/dist/bench/serial-mutations.bench.d.ts.map +1 -0
- package/dist/bench/serial-mutations.bench.js +228 -0
- package/dist/bench/serial-mutations.bench.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -1
- package/dist/index.js.map +1 -1
- package/dist/mutex.d.ts +1 -0
- package/dist/mutex.d.ts.map +1 -1
- package/dist/mutex.js +8 -0
- package/dist/mutex.js.map +1 -1
- package/dist/pg-proxy.d.ts.map +1 -1
- package/dist/pg-proxy.js +38 -36
- package/dist/pg-proxy.js.map +1 -1
- package/dist/replication/change-tracker.d.ts.map +1 -1
- package/dist/replication/change-tracker.js +6 -1
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts +1 -0
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +94 -38
- package/dist/replication/handler.js.map +1 -1
- package/dist/replication/pgoutput-encoder.d.ts +5 -0
- package/dist/replication/pgoutput-encoder.d.ts.map +1 -1
- package/dist/replication/pgoutput-encoder.js +85 -59
- package/dist/replication/pgoutput-encoder.js.map +1 -1
- package/package.json +2 -2
- package/src/bench/serial-mutations.bench.ts +270 -0
- package/src/index.ts +44 -1
- package/src/integration/integration.test.ts +27 -14
- package/src/mutex.ts +9 -0
- package/src/pg-proxy.ts +40 -38
- package/src/replication/change-tracker.ts +5 -1
- package/src/replication/handler.ts +97 -39
- package/src/replication/pgoutput-encoder.ts +101 -60
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { log } from '../log.js'
|
|
10
|
+
|
|
11
|
+
const textEncoder = new TextEncoder()
|
|
10
12
|
import {
|
|
11
13
|
getChangesSince,
|
|
12
|
-
getCurrentWatermark,
|
|
13
14
|
purgeConsumedChanges,
|
|
14
15
|
installTriggersOnShardTables,
|
|
15
16
|
type ChangeRecord,
|
|
@@ -22,11 +23,9 @@ import {
|
|
|
22
23
|
encodeUpdate,
|
|
23
24
|
encodeDelete,
|
|
24
25
|
encodeKeepalive,
|
|
25
|
-
|
|
26
|
-
wrapCopyData,
|
|
26
|
+
encodeWrappedChange,
|
|
27
27
|
getTableOid,
|
|
28
28
|
inferColumns,
|
|
29
|
-
type ColumnInfo,
|
|
30
29
|
} from './pgoutput-encoder.js'
|
|
31
30
|
|
|
32
31
|
import type { Mutex } from '../mutex.js'
|
|
@@ -34,6 +33,7 @@ import type { PGlite } from '@electric-sql/pglite'
|
|
|
34
33
|
|
|
35
34
|
export interface ReplicationWriter {
|
|
36
35
|
write(data: Uint8Array): void
|
|
36
|
+
readonly closed?: boolean
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// current lsn counter
|
|
@@ -56,7 +56,7 @@ function nowMicros(): bigint {
|
|
|
56
56
|
// build a wire protocol row description + data row response
|
|
57
57
|
function buildSimpleResponse(columns: string[], values: string[]): Uint8Array {
|
|
58
58
|
const parts: Uint8Array[] = []
|
|
59
|
-
const encoder =
|
|
59
|
+
const encoder = textEncoder
|
|
60
60
|
|
|
61
61
|
// RowDescription (0x54)
|
|
62
62
|
let rdSize = 6 // int32 len + int16 numFields
|
|
@@ -140,7 +140,7 @@ function buildSimpleResponse(columns: string[], values: string[]): Uint8Array {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
function buildCommandComplete(tag: string): Uint8Array {
|
|
143
|
-
const encoder =
|
|
143
|
+
const encoder = textEncoder
|
|
144
144
|
const tagBytes = encoder.encode(tag + '\0')
|
|
145
145
|
const cc = new Uint8Array(1 + 4 + tagBytes.length)
|
|
146
146
|
cc[0] = 0x43
|
|
@@ -159,7 +159,7 @@ function buildCommandComplete(tag: string): Uint8Array {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
function buildErrorResponse(message: string): Uint8Array {
|
|
162
|
-
const encoder =
|
|
162
|
+
const encoder = textEncoder
|
|
163
163
|
const msgBytes = encoder.encode(message)
|
|
164
164
|
// S(severity) + M(message) + null terminator
|
|
165
165
|
const fields = new Uint8Array(2 + 6 + 2 + msgBytes.length + 1 + 1) // S + ERROR\0 + M + msg\0 + terminator
|
|
@@ -334,6 +334,9 @@ export async function handleStartReplication(
|
|
|
334
334
|
FOR EACH STATEMENT EXECUTE FUNCTION public._zero_notify_change();
|
|
335
335
|
`)
|
|
336
336
|
}
|
|
337
|
+
if (tables.length > 0) {
|
|
338
|
+
log.proxy(`installed notify triggers on ${tables.length} public table(s)`)
|
|
339
|
+
}
|
|
337
340
|
|
|
338
341
|
// discover shard schemas (e.g. chat_0) and install NOTIFY triggers
|
|
339
342
|
const shardSchemas = await db.query<{ nspname: string }>(
|
|
@@ -463,34 +466,76 @@ export async function handleStartReplication(
|
|
|
463
466
|
const sentRelations = new Set<string>()
|
|
464
467
|
let txCounter = 1
|
|
465
468
|
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
const pollIntervalIdle =
|
|
469
|
-
const pollIntervalCatchUp = 20
|
|
469
|
+
// event-driven replication with promise-based wakeup
|
|
470
|
+
// uses pg_notify to wake up immediately, polling only as fallback
|
|
471
|
+
const pollIntervalIdle = 200
|
|
470
472
|
const batchSize = 2000
|
|
471
|
-
const purgeEveryN =
|
|
473
|
+
const purgeEveryN = 5
|
|
472
474
|
const shardRescanEveryN = 20
|
|
473
475
|
let running = true
|
|
474
476
|
let pollsSincePurge = 0
|
|
475
477
|
let pollsSinceShardRescan = 0
|
|
476
478
|
|
|
479
|
+
// promise-based wakeup mechanism
|
|
480
|
+
let wakeupResolve: (() => void) | null = null
|
|
481
|
+
const wakeup = () => {
|
|
482
|
+
if (wakeupResolve) {
|
|
483
|
+
wakeupResolve()
|
|
484
|
+
wakeupResolve = null
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const waitForWakeup = (timeoutMs: number): Promise<void> => {
|
|
488
|
+
return new Promise((resolve) => {
|
|
489
|
+
const timer = setTimeout(() => {
|
|
490
|
+
wakeupResolve = null
|
|
491
|
+
resolve()
|
|
492
|
+
}, timeoutMs)
|
|
493
|
+
wakeupResolve = () => {
|
|
494
|
+
clearTimeout(timer)
|
|
495
|
+
resolve()
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// set up LISTEN to wake up immediately on changes
|
|
501
|
+
let unsubscribe: (() => Promise<void>) | null = null
|
|
502
|
+
try {
|
|
503
|
+
unsubscribe = await db.listen('_zero_changes', wakeup)
|
|
504
|
+
log.debug.proxy('replication: listening for _zero_changes notifications')
|
|
505
|
+
} catch {
|
|
506
|
+
log.debug.proxy('replication: LISTEN not available, using polling only')
|
|
507
|
+
}
|
|
508
|
+
|
|
477
509
|
const poll = async () => {
|
|
478
510
|
while (running) {
|
|
511
|
+
// check if the connection was closed
|
|
512
|
+
if (writer.closed) {
|
|
513
|
+
log.debug.proxy('replication: writer closed, exiting poll loop')
|
|
514
|
+
running = false
|
|
515
|
+
break
|
|
516
|
+
}
|
|
517
|
+
|
|
479
518
|
try {
|
|
480
519
|
// periodically re-scan for new shard schemas (e.g. chat_0 created by zero-cache)
|
|
481
520
|
pollsSinceShardRescan++
|
|
482
521
|
if (pollsSinceShardRescan >= shardRescanEveryN) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
522
|
+
if (mutex.tryAcquire()) {
|
|
523
|
+
pollsSinceShardRescan = 0
|
|
524
|
+
try {
|
|
525
|
+
await installTriggersOnShardTables(db)
|
|
526
|
+
} finally {
|
|
527
|
+
mutex.release()
|
|
528
|
+
}
|
|
489
529
|
}
|
|
490
530
|
}
|
|
491
531
|
|
|
492
|
-
// acquire mutex
|
|
493
|
-
|
|
532
|
+
// try to acquire mutex without blocking proxy connections.
|
|
533
|
+
// if the mutex is busy (proxy handling zero-cache queries), skip
|
|
534
|
+
// this iteration to avoid starving initial sync.
|
|
535
|
+
if (!mutex.tryAcquire()) {
|
|
536
|
+
await waitForWakeup(pollIntervalIdle)
|
|
537
|
+
continue
|
|
538
|
+
}
|
|
494
539
|
let changes: Awaited<ReturnType<typeof getChangesSince>>
|
|
495
540
|
try {
|
|
496
541
|
changes = await getChangesSince(db, lastWatermark, batchSize)
|
|
@@ -531,9 +576,8 @@ export async function handleStartReplication(
|
|
|
531
576
|
|
|
532
577
|
// purge consumed changes periodically to free wasm memory
|
|
533
578
|
pollsSincePurge++
|
|
534
|
-
if (pollsSincePurge >= purgeEveryN) {
|
|
579
|
+
if (pollsSincePurge >= purgeEveryN && mutex.tryAcquire()) {
|
|
535
580
|
pollsSincePurge = 0
|
|
536
|
-
await mutex.acquire()
|
|
537
581
|
try {
|
|
538
582
|
const purged = await purgeConsumedChanges(db, lastWatermark)
|
|
539
583
|
if (purged > 0) {
|
|
@@ -543,15 +587,17 @@ export async function handleStartReplication(
|
|
|
543
587
|
mutex.release()
|
|
544
588
|
}
|
|
545
589
|
}
|
|
590
|
+
|
|
591
|
+
// got changes - continue immediately to check for more
|
|
592
|
+
continue
|
|
546
593
|
}
|
|
547
594
|
|
|
548
595
|
// send keepalive
|
|
549
596
|
const ts = nowMicros()
|
|
550
597
|
writer.write(encodeKeepalive(currentLsn, ts, false))
|
|
551
598
|
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
599
|
+
// no changes: wait for notify signal or poll interval
|
|
600
|
+
await waitForWakeup(pollIntervalIdle)
|
|
555
601
|
} catch (err: unknown) {
|
|
556
602
|
const msg = err instanceof Error ? err.message : String(err)
|
|
557
603
|
log.debug.proxy(`replication poll error: ${msg}`)
|
|
@@ -565,10 +611,19 @@ export async function handleStartReplication(
|
|
|
565
611
|
}
|
|
566
612
|
|
|
567
613
|
log.debug.proxy('replication: starting poll loop')
|
|
568
|
-
|
|
614
|
+
try {
|
|
615
|
+
await poll()
|
|
616
|
+
} finally {
|
|
617
|
+
if (unsubscribe) {
|
|
618
|
+
await unsubscribe().catch(() => {})
|
|
619
|
+
}
|
|
620
|
+
}
|
|
569
621
|
log.debug.proxy('replication: poll loop exited')
|
|
570
622
|
}
|
|
571
623
|
|
|
624
|
+
// cache column info per table to avoid per-change allocation
|
|
625
|
+
const cachedColumns = new Map<string, ReturnType<typeof inferColumns>>()
|
|
626
|
+
|
|
572
627
|
async function streamChanges(
|
|
573
628
|
changes: ChangeRecord[],
|
|
574
629
|
writer: ReplicationWriter,
|
|
@@ -582,8 +637,7 @@ async function streamChanges(
|
|
|
582
637
|
const lsn = nextLsn()
|
|
583
638
|
|
|
584
639
|
// BEGIN
|
|
585
|
-
|
|
586
|
-
writer.write(wrapCopyData(beginMsg))
|
|
640
|
+
writer.write(encodeWrappedChange(lsn, lsn, ts, encodeBegin(lsn, ts, txId)))
|
|
587
641
|
|
|
588
642
|
for (const change of changes) {
|
|
589
643
|
// parse schema-qualified name (schema.table or bare table)
|
|
@@ -626,18 +680,23 @@ async function streamChanges(
|
|
|
626
680
|
const row = rowData || oldData
|
|
627
681
|
if (!row) continue
|
|
628
682
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
683
|
+
// use cached columns or build and cache them
|
|
684
|
+
let columns = cachedColumns.get(qualifiedKey)
|
|
685
|
+
if (!columns) {
|
|
686
|
+
const keySet = tableKeyColumns.get(qualifiedKey)
|
|
687
|
+
const typeOids = columnTypeOids.get(qualifiedKey)
|
|
688
|
+
columns = inferColumns(row).map((col) => ({
|
|
689
|
+
...col,
|
|
690
|
+
typeOid: typeOids?.get(col.name) ?? col.typeOid,
|
|
691
|
+
isKey: keySet?.has(col.name) ?? false,
|
|
692
|
+
}))
|
|
693
|
+
cachedColumns.set(qualifiedKey, columns)
|
|
694
|
+
}
|
|
636
695
|
|
|
637
696
|
// send RELATION if not yet sent
|
|
638
697
|
if (!sentRelations.has(qualifiedKey)) {
|
|
639
698
|
const relMsg = encodeRelation(tableOid, schema, tableName, 0x64, columns)
|
|
640
|
-
writer.write(
|
|
699
|
+
writer.write(encodeWrappedChange(lsn, lsn, ts, relMsg))
|
|
641
700
|
sentRelations.add(qualifiedKey)
|
|
642
701
|
}
|
|
643
702
|
|
|
@@ -660,13 +719,12 @@ async function streamChanges(
|
|
|
660
719
|
continue
|
|
661
720
|
}
|
|
662
721
|
|
|
663
|
-
writer.write(
|
|
722
|
+
writer.write(encodeWrappedChange(lsn, lsn, ts, changeMsg))
|
|
664
723
|
}
|
|
665
724
|
|
|
666
725
|
// COMMIT
|
|
667
726
|
const endLsn = nextLsn()
|
|
668
|
-
|
|
669
|
-
writer.write(wrapCopyData(commitMsg))
|
|
727
|
+
writer.write(encodeWrappedChange(endLsn, endLsn, ts, encodeCommit(0, lsn, endLsn, ts)))
|
|
670
728
|
}
|
|
671
729
|
|
|
672
730
|
function normalizeShardClientsRow(
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
// postgres epoch: 2000-01-01 in microseconds from unix epoch
|
|
11
11
|
const PG_EPOCH_MICROS = 946684800000000n
|
|
12
12
|
|
|
13
|
+
// shared encoder instance - avoids per-call allocation
|
|
14
|
+
const encoder = new TextEncoder()
|
|
15
|
+
|
|
13
16
|
// table oid tracking
|
|
14
17
|
const tableOids = new Map<string, number>()
|
|
15
18
|
let nextOid = 16384
|
|
@@ -39,10 +42,27 @@ export function inferColumns(row: Record<string, unknown>): ColumnInfo[] {
|
|
|
39
42
|
}))
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
// reusable scratch buffer for building messages (64KB, grows if needed)
|
|
46
|
+
let scratch = new Uint8Array(65536)
|
|
47
|
+
let scratchView = new DataView(scratch.buffer)
|
|
48
|
+
|
|
49
|
+
function ensureScratch(size: number): void {
|
|
50
|
+
if (scratch.length < size) {
|
|
51
|
+
const newSize = Math.max(size, scratch.length * 2)
|
|
52
|
+
scratch = new Uint8Array(newSize)
|
|
53
|
+
scratchView = new DataView(scratch.buffer)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeInt16At(offset: number, val: number): void {
|
|
58
|
+
scratchView.setInt16(offset, val)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function writeInt32At(offset: number, val: number): void {
|
|
62
|
+
scratchView.setInt32(offset, val)
|
|
44
63
|
}
|
|
45
64
|
|
|
65
|
+
// legacy helpers for standalone buffers
|
|
46
66
|
function writeInt16(buf: Uint8Array, offset: number, val: number): void {
|
|
47
67
|
new DataView(buf.buffer, buf.byteOffset).setInt16(offset, val)
|
|
48
68
|
}
|
|
@@ -89,14 +109,14 @@ export function encodeRelation(
|
|
|
89
109
|
replicaIdentity: number,
|
|
90
110
|
columns: ColumnInfo[]
|
|
91
111
|
): Uint8Array {
|
|
92
|
-
const schemaBytes =
|
|
93
|
-
const nameBytes =
|
|
112
|
+
const schemaBytes = encoder.encode(schema)
|
|
113
|
+
const nameBytes = encoder.encode(tableName)
|
|
94
114
|
|
|
95
115
|
// calculate column sizes
|
|
96
116
|
let columnsSize = 0
|
|
97
117
|
const colNameBytes: Uint8Array[] = []
|
|
98
118
|
for (const col of columns) {
|
|
99
|
-
const nb =
|
|
119
|
+
const nb = encoder.encode(col.name)
|
|
100
120
|
colNameBytes.push(nb)
|
|
101
121
|
columnsSize += 1 + nb.length + 1 + 4 + 4 // flags + name + null + typeOid + typeMod
|
|
102
122
|
}
|
|
@@ -133,19 +153,25 @@ export function encodeRelation(
|
|
|
133
153
|
return buf
|
|
134
154
|
}
|
|
135
155
|
|
|
136
|
-
|
|
156
|
+
// encode tuple data directly into scratch buffer starting at given offset.
|
|
157
|
+
// returns the number of bytes written.
|
|
158
|
+
function encodeTupleDataInto(
|
|
137
159
|
row: Record<string, unknown>,
|
|
138
|
-
columns: ColumnInfo[]
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
let
|
|
160
|
+
columns: ColumnInfo[],
|
|
161
|
+
startOffset: number
|
|
162
|
+
): number {
|
|
163
|
+
let pos = startOffset
|
|
164
|
+
|
|
165
|
+
// reserve space for ncolumns
|
|
166
|
+
ensureScratch(pos + 2 + columns.length * 32)
|
|
167
|
+
writeInt16At(pos, columns.length)
|
|
168
|
+
pos += 2
|
|
142
169
|
|
|
143
|
-
const values: (Uint8Array | null)[] = []
|
|
144
170
|
for (const col of columns) {
|
|
145
171
|
const val = row[col.name]
|
|
146
172
|
if (val === null || val === undefined) {
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
ensureScratch(pos + 1)
|
|
174
|
+
scratch[pos++] = 0x6e // 'n' for null
|
|
149
175
|
} else {
|
|
150
176
|
// convert to postgresql text format
|
|
151
177
|
let strVal: string
|
|
@@ -156,28 +182,48 @@ function encodeTupleData(
|
|
|
156
182
|
} else {
|
|
157
183
|
strVal = String(val)
|
|
158
184
|
}
|
|
159
|
-
const bytes =
|
|
160
|
-
|
|
161
|
-
|
|
185
|
+
const bytes = encoder.encode(strVal)
|
|
186
|
+
ensureScratch(pos + 1 + 4 + bytes.length)
|
|
187
|
+
scratch[pos++] = 0x74 // 't' for text
|
|
188
|
+
writeInt32At(pos, bytes.length)
|
|
189
|
+
pos += 4
|
|
190
|
+
scratch.set(bytes, pos)
|
|
191
|
+
pos += bytes.length
|
|
162
192
|
}
|
|
163
193
|
}
|
|
164
194
|
|
|
195
|
+
return pos - startOffset
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* encode a complete change message wrapped in CopyData(XLogData(...)).
|
|
200
|
+
* avoids intermediate buffer allocations by writing directly into one buffer.
|
|
201
|
+
*/
|
|
202
|
+
export function encodeWrappedChange(
|
|
203
|
+
walStart: bigint,
|
|
204
|
+
walEnd: bigint,
|
|
205
|
+
timestamp: bigint,
|
|
206
|
+
changeData: Uint8Array
|
|
207
|
+
): Uint8Array {
|
|
208
|
+
// CopyData header: 'd' + int32 len
|
|
209
|
+
// XLogData header: 'w' + int64 walStart + int64 walEnd + int64 timestamp
|
|
210
|
+
// then changeData
|
|
211
|
+
const xlogSize = 1 + 8 + 8 + 8 + changeData.length
|
|
212
|
+
const totalSize = 1 + 4 + xlogSize
|
|
165
213
|
const buf = new Uint8Array(totalSize)
|
|
166
|
-
let pos = 0
|
|
167
|
-
writeInt16(buf, pos, columns.length)
|
|
168
|
-
pos += 2
|
|
169
214
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
215
|
+
// CopyData
|
|
216
|
+
buf[0] = 0x64 // 'd'
|
|
217
|
+
writeInt32(buf, 1, 4 + xlogSize)
|
|
218
|
+
|
|
219
|
+
// XLogData
|
|
220
|
+
buf[5] = 0x77 // 'w'
|
|
221
|
+
writeInt64(buf, 6, walStart)
|
|
222
|
+
writeInt64(buf, 14, walEnd)
|
|
223
|
+
writeInt64(buf, 22, timestamp - PG_EPOCH_MICROS)
|
|
224
|
+
|
|
225
|
+
// change payload
|
|
226
|
+
buf.set(changeData, 30)
|
|
181
227
|
|
|
182
228
|
return buf
|
|
183
229
|
}
|
|
@@ -188,13 +234,14 @@ export function encodeInsert(
|
|
|
188
234
|
row: Record<string, unknown>,
|
|
189
235
|
columns: ColumnInfo[]
|
|
190
236
|
): Uint8Array {
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
237
|
+
// write header + tuple directly into scratch
|
|
238
|
+
const headerSize = 1 + 4 + 1 // 'I' + oid + 'N'
|
|
239
|
+
ensureScratch(headerSize + 2 + columns.length * 64)
|
|
240
|
+
scratch[0] = 0x49 // 'I'
|
|
241
|
+
writeInt32At(1, tableOid)
|
|
242
|
+
scratch[5] = 0x4e // 'N' for new tuple
|
|
243
|
+
const tupleLen = encodeTupleDataInto(row, columns, 6)
|
|
244
|
+
return scratch.slice(0, 6 + tupleLen)
|
|
198
245
|
}
|
|
199
246
|
|
|
200
247
|
// encode an UPDATE message
|
|
@@ -204,26 +251,21 @@ export function encodeUpdate(
|
|
|
204
251
|
oldRow: Record<string, unknown> | null,
|
|
205
252
|
columns: ColumnInfo[]
|
|
206
253
|
): Uint8Array {
|
|
207
|
-
|
|
254
|
+
ensureScratch(1 + 4 + 1 + columns.length * 128)
|
|
255
|
+
scratch[0] = 0x55 // 'U'
|
|
256
|
+
writeInt32At(1, tableOid)
|
|
208
257
|
|
|
209
258
|
if (oldRow) {
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
buf.set(oldTuple, 6)
|
|
216
|
-
buf[6 + oldTuple.length] = 0x4e // 'N' for new tuple
|
|
217
|
-
buf.set(newTuple, 7 + oldTuple.length)
|
|
218
|
-
return buf
|
|
259
|
+
scratch[5] = 0x4f // 'O' for old tuple
|
|
260
|
+
const oldLen = encodeTupleDataInto(oldRow, columns, 6)
|
|
261
|
+
scratch[6 + oldLen] = 0x4e // 'N' for new tuple
|
|
262
|
+
const newLen = encodeTupleDataInto(row, columns, 7 + oldLen)
|
|
263
|
+
return scratch.slice(0, 7 + oldLen + newLen)
|
|
219
264
|
}
|
|
220
265
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
buf[5] = 0x4e // 'N'
|
|
225
|
-
buf.set(newTuple, 6)
|
|
226
|
-
return buf
|
|
266
|
+
scratch[5] = 0x4e // 'N'
|
|
267
|
+
const newLen = encodeTupleDataInto(row, columns, 6)
|
|
268
|
+
return scratch.slice(0, 6 + newLen)
|
|
227
269
|
}
|
|
228
270
|
|
|
229
271
|
// encode a DELETE message
|
|
@@ -232,13 +274,12 @@ export function encodeDelete(
|
|
|
232
274
|
oldRow: Record<string, unknown>,
|
|
233
275
|
columns: ColumnInfo[]
|
|
234
276
|
): Uint8Array {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return buf
|
|
277
|
+
ensureScratch(1 + 4 + 1 + columns.length * 64)
|
|
278
|
+
scratch[0] = 0x44 // 'D'
|
|
279
|
+
writeInt32At(1, tableOid)
|
|
280
|
+
scratch[5] = 0x4b // 'K' for key tuple
|
|
281
|
+
const tupleLen = encodeTupleDataInto(oldRow, columns, 6)
|
|
282
|
+
return scratch.slice(0, 6 + tupleLen)
|
|
242
283
|
}
|
|
243
284
|
|
|
244
285
|
// wrap a pgoutput message in XLogData format
|