@syncular/core 0.20.1 → 0.21.0
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/constants.d.ts +5 -3
- package/dist/constants.js +5 -3
- package/dist/message.d.ts +7 -0
- package/dist/message.js +2 -0
- package/dist/render.d.ts +7 -0
- package/dist/render.js +28 -5
- package/dist/row-codec.d.ts +21 -0
- package/dist/row-codec.js +121 -2
- package/package.json +1 -1
- package/src/constants.ts +5 -3
- package/src/message.ts +9 -0
- package/src/render.ts +33 -6
- package/src/row-codec.ts +157 -2
package/dist/constants.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** Protocol constants (SPEC.md §1.2, §9 are normative). */
|
|
2
2
|
/** Latest SSP2 envelope wire version emitted by current clients. */
|
|
3
|
-
export declare const PROTOCOL_WIRE_VERSION =
|
|
4
|
-
/** Oldest SSP2 envelope wire version accepted
|
|
5
|
-
|
|
3
|
+
export declare const PROTOCOL_WIRE_VERSION = 3;
|
|
4
|
+
/** Oldest SSP2 envelope wire version accepted. Wire version 3 changed the
|
|
5
|
+
* push payload to the sparse row codec (§2.4) and the conflict record shape
|
|
6
|
+
* (§6.3); versions 1 and 2 are rejected as unknown (§9). */
|
|
7
|
+
export declare const MINIMUM_PROTOCOL_WIRE_VERSION = 3;
|
|
6
8
|
/** Wire versions implemented by the reference codec, oldest first. */
|
|
7
9
|
export declare const SUPPORTED_PROTOCOL_WIRE_VERSIONS: readonly number[];
|
|
8
10
|
/** Whether the reference codec implements this exact wire version. */
|
package/dist/constants.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** Protocol constants (SPEC.md §1.2, §9 are normative). */
|
|
2
2
|
/** Latest SSP2 envelope wire version emitted by current clients. */
|
|
3
|
-
export const PROTOCOL_WIRE_VERSION =
|
|
4
|
-
/** Oldest SSP2 envelope wire version accepted
|
|
5
|
-
|
|
3
|
+
export const PROTOCOL_WIRE_VERSION = 3;
|
|
4
|
+
/** Oldest SSP2 envelope wire version accepted. Wire version 3 changed the
|
|
5
|
+
* push payload to the sparse row codec (§2.4) and the conflict record shape
|
|
6
|
+
* (§6.3); versions 1 and 2 are rejected as unknown (§9). */
|
|
7
|
+
export const MINIMUM_PROTOCOL_WIRE_VERSION = 3;
|
|
6
8
|
/** Wire versions implemented by the reference codec, oldest first. */
|
|
7
9
|
export const SUPPORTED_PROTOCOL_WIRE_VERSIONS = Object.freeze(Array.from({
|
|
8
10
|
length: PROTOCOL_WIRE_VERSION - MINIMUM_PROTOCOL_WIRE_VERSION + 1,
|
package/dist/message.d.ts
CHANGED
|
@@ -71,6 +71,13 @@ export type PushOperationResult = {
|
|
|
71
71
|
message: string;
|
|
72
72
|
serverVersion: number;
|
|
73
73
|
serverRow: Uint8Array;
|
|
74
|
+
/**
|
|
75
|
+
* §6.3 (wire version 3): presence-bitmap layout over the row's columns,
|
|
76
|
+
* marking the present columns whose `column_version` exceeded the
|
|
77
|
+
* operation's `baseVersion`. A `baseVersion = 0` lost-insert conflict
|
|
78
|
+
* marks every present non-crdt column.
|
|
79
|
+
*/
|
|
80
|
+
conflictColumns: Uint8Array;
|
|
74
81
|
} | {
|
|
75
82
|
opIndex: number;
|
|
76
83
|
status: 'error';
|
package/dist/message.js
CHANGED
|
@@ -185,6 +185,7 @@ function decodePushResult(r) {
|
|
|
185
185
|
message: r.str(),
|
|
186
186
|
serverVersion: r.i64(),
|
|
187
187
|
serverRow: r.bytes(),
|
|
188
|
+
conflictColumns: r.bytes(),
|
|
188
189
|
});
|
|
189
190
|
}
|
|
190
191
|
else if (recordStatus === 3) {
|
|
@@ -709,6 +710,7 @@ function encodeFrame(frame, wireVersion) {
|
|
|
709
710
|
w.str(result.message);
|
|
710
711
|
w.i64(result.serverVersion);
|
|
711
712
|
w.bytes(result.serverRow);
|
|
713
|
+
w.bytes(result.conflictColumns);
|
|
712
714
|
}
|
|
713
715
|
else {
|
|
714
716
|
w.u8(3);
|
package/dist/render.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* fields embedded as parsed JSON.
|
|
9
9
|
*/
|
|
10
10
|
import type { SyncMessage } from './message.js';
|
|
11
|
+
import type { RowColumn, SparseRowValue } from './row-codec.js';
|
|
11
12
|
import type { RowsSegment } from './segment.js';
|
|
12
13
|
export type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
13
14
|
[key: string]: JsonValue;
|
|
@@ -20,3 +21,9 @@ export declare function renderMessage(bytes: Uint8Array): JsonValue;
|
|
|
20
21
|
export declare function renderRowsSegmentValue(segment: RowsSegment): JsonValue;
|
|
21
22
|
/** `render(bytes) → json` for standalone SSG2 rows segments (§11 rule 8). */
|
|
22
23
|
export declare function renderRowsSegment(bytes: Uint8Array): JsonValue;
|
|
24
|
+
/**
|
|
25
|
+
* Render a decoded sparse row (RFC §6.1, §11 rule 11): the column table plus
|
|
26
|
+
* `values` keyed by column name, carrying only the present columns, so an
|
|
27
|
+
* absent column has no key and a present NULL renders as JSON `null`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function renderSparseRowValue(columns: readonly RowColumn[], values: readonly SparseRowValue[]): JsonValue;
|
package/dist/render.js
CHANGED
|
@@ -67,6 +67,7 @@ function renderResult(result) {
|
|
|
67
67
|
message: result.message,
|
|
68
68
|
serverVersion: result.serverVersion,
|
|
69
69
|
serverRow: bytesToBase64(result.serverRow),
|
|
70
|
+
conflictColumns: bytesToBase64(result.conflictColumns),
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
73
|
return {
|
|
@@ -274,17 +275,20 @@ function renderRowValue(column, value) {
|
|
|
274
275
|
}
|
|
275
276
|
return value;
|
|
276
277
|
}
|
|
278
|
+
function renderColumnTable(columns) {
|
|
279
|
+
return columns.map((column) => ({
|
|
280
|
+
name: column.name,
|
|
281
|
+
type: column.type,
|
|
282
|
+
nullable: column.nullable,
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
277
285
|
export function renderRowsSegmentValue(segment) {
|
|
278
286
|
return {
|
|
279
287
|
magic: 'SSG2',
|
|
280
288
|
formatVersion: ROWS_SEGMENT_FORMAT_VERSION,
|
|
281
289
|
table: segment.table,
|
|
282
290
|
schemaVersion: segment.schemaVersion,
|
|
283
|
-
columns: segment.columns
|
|
284
|
-
name: column.name,
|
|
285
|
-
type: column.type,
|
|
286
|
-
nullable: column.nullable,
|
|
287
|
-
})),
|
|
291
|
+
columns: renderColumnTable(segment.columns),
|
|
288
292
|
blocks: segment.blocks.map((block) => block.map((row) => ({
|
|
289
293
|
serverVersion: row.serverVersion,
|
|
290
294
|
values: Object.fromEntries(segment.columns.map((column, i) => [
|
|
@@ -298,3 +302,22 @@ export function renderRowsSegmentValue(segment) {
|
|
|
298
302
|
export function renderRowsSegment(bytes) {
|
|
299
303
|
return renderRowsSegmentValue(decodeRowsSegment(bytes));
|
|
300
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Render a decoded sparse row (RFC §6.1, §11 rule 11): the column table plus
|
|
307
|
+
* `values` keyed by column name, carrying only the present columns, so an
|
|
308
|
+
* absent column has no key and a present NULL renders as JSON `null`.
|
|
309
|
+
*/
|
|
310
|
+
export function renderSparseRowValue(columns, values) {
|
|
311
|
+
const present = [];
|
|
312
|
+
for (let i = 0; i < columns.length; i++) {
|
|
313
|
+
const column = columns[i];
|
|
314
|
+
const value = values[i];
|
|
315
|
+
if (column === undefined || value === undefined)
|
|
316
|
+
continue;
|
|
317
|
+
present.push([column.name, renderRowValue(column, value)]);
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
columns: renderColumnTable(columns),
|
|
321
|
+
values: Object.fromEntries(present),
|
|
322
|
+
};
|
|
323
|
+
}
|
package/dist/row-codec.d.ts
CHANGED
|
@@ -31,6 +31,27 @@ export interface RowColumn {
|
|
|
31
31
|
export type RowValue = string | number | boolean | Uint8Array | null;
|
|
32
32
|
export declare function columnTypeTag(type: ColumnType): number;
|
|
33
33
|
export declare function columnTypeFromTag(tag: number): ColumnType;
|
|
34
|
+
/**
|
|
35
|
+
* A sparse-row slot (RFC §6.1): `undefined` when the column is absent from
|
|
36
|
+
* the payload (an apply leaves the stored value untouched), `null` when the
|
|
37
|
+
* column is present and NULL.
|
|
38
|
+
*/
|
|
39
|
+
export type SparseRowValue = RowValue | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Encode one standalone sparse row (RFC §6.1, SPEC.md §2.4).
|
|
42
|
+
*
|
|
43
|
+
* `primaryKeyIndex` is the row's primary-key column; a payload without it is
|
|
44
|
+
* encoder misuse and throws, like a NULL in a non-nullable column.
|
|
45
|
+
*/
|
|
46
|
+
export declare function encodeSparseRow(columns: readonly RowColumn[], primaryKeyIndex: number, values: readonly SparseRowValue[]): Uint8Array;
|
|
47
|
+
/**
|
|
48
|
+
* Decode one standalone sparse row; the bytes must contain exactly one row.
|
|
49
|
+
*
|
|
50
|
+
* Decode checks, in order (each a decode error, RFC §6.1): a set presence
|
|
51
|
+
* padding bit, an absent primary-key column, a set null-bitmap padding bit
|
|
52
|
+
* (a null for an absent column), a null bit for a non-nullable column.
|
|
53
|
+
*/
|
|
54
|
+
export declare function decodeSparseRow(columns: readonly RowColumn[], primaryKeyIndex: number, bytes: Uint8Array): SparseRowValue[];
|
|
34
55
|
export declare function writeRow(writer: ByteWriter, columns: readonly RowColumn[], values: readonly RowValue[]): void;
|
|
35
56
|
export declare function readRow(reader: ByteReader, columns: readonly RowColumn[]): RowValue[];
|
|
36
57
|
/** Encode one standalone row (e.g. a push payload or conflict serverRow). */
|
package/dist/row-codec.js
CHANGED
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
* A row is a null bitmap of ceil(columnCount / 8) bytes (bit i set =
|
|
5
5
|
* column i is NULL; LSB-first within each byte, byte i/8), followed by the
|
|
6
6
|
* non-null values encoded positionally in schema-IR declaration order.
|
|
7
|
-
* Used for `COMMIT` change payloads, rows-segment row data,
|
|
8
|
-
* payloads
|
|
7
|
+
* Used for `COMMIT` change payloads, rows-segment row data, and conflict
|
|
8
|
+
* `serverRow` values; push operation payloads use the sparse row codec
|
|
9
|
+
* (`encodeSparseRow` / `decodeSparseRow`) instead.
|
|
10
|
+
*
|
|
11
|
+
* A sparse row (RFC §6.1) is the column subset a push operation writes: a
|
|
12
|
+
* presence bitmap over all columns, a null bitmap over the present columns,
|
|
13
|
+
* then the non-null present values. Absent columns keep their stored value.
|
|
9
14
|
*/
|
|
10
15
|
import { parseBlobRef } from './blob-ref.js';
|
|
11
16
|
import { ByteReader, ByteWriter } from './bytes.js';
|
|
@@ -109,6 +114,120 @@ function readValue(reader, column) {
|
|
|
109
114
|
return reader.bytes();
|
|
110
115
|
}
|
|
111
116
|
}
|
|
117
|
+
/** One bit of a bitmap: LSB-first within byte `index / 8`. */
|
|
118
|
+
function bitmapBit(bitmap, index) {
|
|
119
|
+
return (((bitmap[index >> 3] ?? 0) >> (index & 7)) & 1) !== 0;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Encode one standalone sparse row (RFC §6.1, SPEC.md §2.4).
|
|
123
|
+
*
|
|
124
|
+
* `primaryKeyIndex` is the row's primary-key column; a payload without it is
|
|
125
|
+
* encoder misuse and throws, like a NULL in a non-nullable column.
|
|
126
|
+
*/
|
|
127
|
+
export function encodeSparseRow(columns, primaryKeyIndex, values) {
|
|
128
|
+
if (values.length !== columns.length) {
|
|
129
|
+
throw new Error(`row value count ${values.length} does not match column count ${columns.length}`);
|
|
130
|
+
}
|
|
131
|
+
const presence = new Uint8Array(Math.ceil(columns.length / 8));
|
|
132
|
+
let presentCount = 0;
|
|
133
|
+
for (let i = 0; i < columns.length; i++) {
|
|
134
|
+
if (values[i] === undefined)
|
|
135
|
+
continue;
|
|
136
|
+
presence[i >> 3] = (presence[i >> 3] ?? 0) | (1 << (i & 7));
|
|
137
|
+
presentCount++;
|
|
138
|
+
}
|
|
139
|
+
const primaryKey = columns[primaryKeyIndex];
|
|
140
|
+
if (primaryKey === undefined) {
|
|
141
|
+
throw new Error(`primary-key column index ${primaryKeyIndex} is outside the column table`);
|
|
142
|
+
}
|
|
143
|
+
if (!bitmapBit(presence, primaryKeyIndex)) {
|
|
144
|
+
throw new Error(`sparse row is missing the primary-key column ${primaryKey.name}`);
|
|
145
|
+
}
|
|
146
|
+
const nulls = new Uint8Array(Math.ceil(presentCount / 8));
|
|
147
|
+
let present = 0;
|
|
148
|
+
for (let i = 0; i < columns.length; i++) {
|
|
149
|
+
const column = columns[i];
|
|
150
|
+
const value = values[i];
|
|
151
|
+
if (column === undefined || value === undefined)
|
|
152
|
+
continue;
|
|
153
|
+
if (value === null) {
|
|
154
|
+
if (!column.nullable) {
|
|
155
|
+
throw new Error(`column ${column.name} is not nullable`);
|
|
156
|
+
}
|
|
157
|
+
nulls[present >> 3] = (nulls[present >> 3] ?? 0) | (1 << (present & 7));
|
|
158
|
+
}
|
|
159
|
+
present++;
|
|
160
|
+
}
|
|
161
|
+
const writer = new ByteWriter();
|
|
162
|
+
writer.raw(presence);
|
|
163
|
+
writer.raw(nulls);
|
|
164
|
+
for (let i = 0; i < columns.length; i++) {
|
|
165
|
+
const column = columns[i];
|
|
166
|
+
const value = values[i];
|
|
167
|
+
if (column === undefined || value === undefined || value === null) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
writeValue(writer, column, value);
|
|
171
|
+
}
|
|
172
|
+
return writer.finish();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Decode one standalone sparse row; the bytes must contain exactly one row.
|
|
176
|
+
*
|
|
177
|
+
* Decode checks, in order (each a decode error, RFC §6.1): a set presence
|
|
178
|
+
* padding bit, an absent primary-key column, a set null-bitmap padding bit
|
|
179
|
+
* (a null for an absent column), a null bit for a non-nullable column.
|
|
180
|
+
*/
|
|
181
|
+
export function decodeSparseRow(columns, primaryKeyIndex, bytes) {
|
|
182
|
+
const reader = new ByteReader(bytes);
|
|
183
|
+
const presenceLength = Math.ceil(columns.length / 8);
|
|
184
|
+
const presence = reader.raw(presenceLength);
|
|
185
|
+
for (let bit = columns.length; bit < presenceLength * 8; bit++) {
|
|
186
|
+
if (bitmapBit(presence, bit)) {
|
|
187
|
+
throw new DecodeError('sync.invalid_request', 'sparse row presence bitmap has a set padding bit (non-canonical encoding)');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const primaryKey = columns[primaryKeyIndex];
|
|
191
|
+
if (primaryKey === undefined) {
|
|
192
|
+
throw new DecodeError('sync.invalid_request', `primary-key column index ${primaryKeyIndex} is outside the column table`);
|
|
193
|
+
}
|
|
194
|
+
if (!bitmapBit(presence, primaryKeyIndex)) {
|
|
195
|
+
throw new DecodeError('sync.invalid_request', `sparse row is missing the primary-key column ${primaryKey.name}`);
|
|
196
|
+
}
|
|
197
|
+
let presentCount = 0;
|
|
198
|
+
for (let i = 0; i < columns.length; i++) {
|
|
199
|
+
if (bitmapBit(presence, i))
|
|
200
|
+
presentCount++;
|
|
201
|
+
}
|
|
202
|
+
const nullLength = Math.ceil(presentCount / 8);
|
|
203
|
+
const nulls = reader.raw(nullLength);
|
|
204
|
+
for (let bit = presentCount; bit < nullLength * 8; bit++) {
|
|
205
|
+
if (bitmapBit(nulls, bit)) {
|
|
206
|
+
throw new DecodeError('sync.invalid_request', 'sparse row null bitmap sets a null bit for an absent column');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const values = [];
|
|
210
|
+
let present = 0;
|
|
211
|
+
for (let i = 0; i < columns.length; i++) {
|
|
212
|
+
const column = columns[i];
|
|
213
|
+
if (column === undefined || !bitmapBit(presence, i)) {
|
|
214
|
+
values.push(undefined);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const isNull = bitmapBit(nulls, present);
|
|
218
|
+
present++;
|
|
219
|
+
if (!isNull) {
|
|
220
|
+
values.push(readValue(reader, column));
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (!column.nullable) {
|
|
224
|
+
throw new DecodeError('sync.invalid_request', `null bit set for non-nullable column ${column.name}`);
|
|
225
|
+
}
|
|
226
|
+
values.push(null);
|
|
227
|
+
}
|
|
228
|
+
reader.expectFullyConsumed('sparse row payload');
|
|
229
|
+
return values;
|
|
230
|
+
}
|
|
112
231
|
export function writeRow(writer, columns, values) {
|
|
113
232
|
if (values.length !== columns.length) {
|
|
114
233
|
throw new Error(`row value count ${values.length} does not match column count ${columns.length}`);
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/** Protocol constants (SPEC.md §1.2, §9 are normative). */
|
|
2
2
|
|
|
3
3
|
/** Latest SSP2 envelope wire version emitted by current clients. */
|
|
4
|
-
export const PROTOCOL_WIRE_VERSION =
|
|
4
|
+
export const PROTOCOL_WIRE_VERSION = 3;
|
|
5
5
|
|
|
6
|
-
/** Oldest SSP2 envelope wire version accepted
|
|
7
|
-
|
|
6
|
+
/** Oldest SSP2 envelope wire version accepted. Wire version 3 changed the
|
|
7
|
+
* push payload to the sparse row codec (§2.4) and the conflict record shape
|
|
8
|
+
* (§6.3); versions 1 and 2 are rejected as unknown (§9). */
|
|
9
|
+
export const MINIMUM_PROTOCOL_WIRE_VERSION = 3;
|
|
8
10
|
|
|
9
11
|
/** Wire versions implemented by the reference codec, oldest first. */
|
|
10
12
|
export const SUPPORTED_PROTOCOL_WIRE_VERSIONS: readonly number[] =
|
package/src/message.ts
CHANGED
|
@@ -122,6 +122,13 @@ export type PushOperationResult =
|
|
|
122
122
|
message: string;
|
|
123
123
|
serverVersion: number;
|
|
124
124
|
serverRow: Uint8Array;
|
|
125
|
+
/**
|
|
126
|
+
* §6.3 (wire version 3): presence-bitmap layout over the row's columns,
|
|
127
|
+
* marking the present columns whose `column_version` exceeded the
|
|
128
|
+
* operation's `baseVersion`. A `baseVersion = 0` lost-insert conflict
|
|
129
|
+
* marks every present non-crdt column.
|
|
130
|
+
*/
|
|
131
|
+
conflictColumns: Uint8Array;
|
|
125
132
|
}
|
|
126
133
|
| {
|
|
127
134
|
opIndex: number;
|
|
@@ -422,6 +429,7 @@ function decodePushResult(r: ByteReader): PushResultFrame {
|
|
|
422
429
|
message: r.str(),
|
|
423
430
|
serverVersion: r.i64(),
|
|
424
431
|
serverRow: r.bytes(),
|
|
432
|
+
conflictColumns: r.bytes(),
|
|
425
433
|
});
|
|
426
434
|
} else if (recordStatus === 3) {
|
|
427
435
|
results.push({
|
|
@@ -971,6 +979,7 @@ function encodeFrame(
|
|
|
971
979
|
w.str(result.message);
|
|
972
980
|
w.i64(result.serverVersion);
|
|
973
981
|
w.bytes(result.serverRow);
|
|
982
|
+
w.bytes(result.conflictColumns);
|
|
974
983
|
} else {
|
|
975
984
|
w.u8(3);
|
|
976
985
|
w.str(result.code);
|
package/src/render.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
SyncMessage,
|
|
17
17
|
} from './message';
|
|
18
18
|
import { decodeMessage } from './message';
|
|
19
|
-
import type { RowColumn, RowValue } from './row-codec';
|
|
19
|
+
import type { RowColumn, RowValue, SparseRowValue } from './row-codec';
|
|
20
20
|
import type { RowsSegment } from './segment';
|
|
21
21
|
import { decodeRowsSegment, ROWS_SEGMENT_FORMAT_VERSION } from './segment';
|
|
22
22
|
|
|
@@ -106,6 +106,7 @@ function renderResult(result: PushOperationResult): JsonValue {
|
|
|
106
106
|
message: result.message,
|
|
107
107
|
serverVersion: result.serverVersion,
|
|
108
108
|
serverRow: bytesToBase64(result.serverRow),
|
|
109
|
+
conflictColumns: bytesToBase64(result.conflictColumns),
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
112
|
return {
|
|
@@ -321,17 +322,21 @@ function renderRowValue(column: RowColumn, value: RowValue): JsonValue {
|
|
|
321
322
|
return value;
|
|
322
323
|
}
|
|
323
324
|
|
|
325
|
+
function renderColumnTable(columns: readonly RowColumn[]): JsonValue {
|
|
326
|
+
return columns.map((column) => ({
|
|
327
|
+
name: column.name,
|
|
328
|
+
type: column.type,
|
|
329
|
+
nullable: column.nullable,
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
|
|
324
333
|
export function renderRowsSegmentValue(segment: RowsSegment): JsonValue {
|
|
325
334
|
return {
|
|
326
335
|
magic: 'SSG2',
|
|
327
336
|
formatVersion: ROWS_SEGMENT_FORMAT_VERSION,
|
|
328
337
|
table: segment.table,
|
|
329
338
|
schemaVersion: segment.schemaVersion,
|
|
330
|
-
columns: segment.columns
|
|
331
|
-
name: column.name,
|
|
332
|
-
type: column.type,
|
|
333
|
-
nullable: column.nullable,
|
|
334
|
-
})),
|
|
339
|
+
columns: renderColumnTable(segment.columns),
|
|
335
340
|
blocks: segment.blocks.map((block) =>
|
|
336
341
|
block.map((row) => ({
|
|
337
342
|
serverVersion: row.serverVersion,
|
|
@@ -350,3 +355,25 @@ export function renderRowsSegmentValue(segment: RowsSegment): JsonValue {
|
|
|
350
355
|
export function renderRowsSegment(bytes: Uint8Array): JsonValue {
|
|
351
356
|
return renderRowsSegmentValue(decodeRowsSegment(bytes));
|
|
352
357
|
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Render a decoded sparse row (RFC §6.1, §11 rule 11): the column table plus
|
|
361
|
+
* `values` keyed by column name, carrying only the present columns, so an
|
|
362
|
+
* absent column has no key and a present NULL renders as JSON `null`.
|
|
363
|
+
*/
|
|
364
|
+
export function renderSparseRowValue(
|
|
365
|
+
columns: readonly RowColumn[],
|
|
366
|
+
values: readonly SparseRowValue[],
|
|
367
|
+
): JsonValue {
|
|
368
|
+
const present: Array<[string, JsonValue]> = [];
|
|
369
|
+
for (let i = 0; i < columns.length; i++) {
|
|
370
|
+
const column = columns[i];
|
|
371
|
+
const value = values[i];
|
|
372
|
+
if (column === undefined || value === undefined) continue;
|
|
373
|
+
present.push([column.name, renderRowValue(column, value)]);
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
columns: renderColumnTable(columns),
|
|
377
|
+
values: Object.fromEntries(present),
|
|
378
|
+
};
|
|
379
|
+
}
|
package/src/row-codec.ts
CHANGED
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
* A row is a null bitmap of ceil(columnCount / 8) bytes (bit i set =
|
|
5
5
|
* column i is NULL; LSB-first within each byte, byte i/8), followed by the
|
|
6
6
|
* non-null values encoded positionally in schema-IR declaration order.
|
|
7
|
-
* Used for `COMMIT` change payloads, rows-segment row data,
|
|
8
|
-
* payloads
|
|
7
|
+
* Used for `COMMIT` change payloads, rows-segment row data, and conflict
|
|
8
|
+
* `serverRow` values; push operation payloads use the sparse row codec
|
|
9
|
+
* (`encodeSparseRow` / `decodeSparseRow`) instead.
|
|
10
|
+
*
|
|
11
|
+
* A sparse row (RFC §6.1) is the column subset a push operation writes: a
|
|
12
|
+
* presence bitmap over all columns, a null bitmap over the present columns,
|
|
13
|
+
* then the non-null present values. Absent columns keep their stored value.
|
|
9
14
|
*/
|
|
10
15
|
import { parseBlobRef } from './blob-ref';
|
|
11
16
|
import { ByteReader, ByteWriter } from './bytes';
|
|
@@ -178,6 +183,156 @@ function readValue(reader: ByteReader, column: RowColumn): RowValue {
|
|
|
178
183
|
}
|
|
179
184
|
}
|
|
180
185
|
|
|
186
|
+
/** One bit of a bitmap: LSB-first within byte `index / 8`. */
|
|
187
|
+
function bitmapBit(bitmap: Uint8Array, index: number): boolean {
|
|
188
|
+
return (((bitmap[index >> 3] ?? 0) >> (index & 7)) & 1) !== 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A sparse-row slot (RFC §6.1): `undefined` when the column is absent from
|
|
193
|
+
* the payload (an apply leaves the stored value untouched), `null` when the
|
|
194
|
+
* column is present and NULL.
|
|
195
|
+
*/
|
|
196
|
+
export type SparseRowValue = RowValue | undefined;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Encode one standalone sparse row (RFC §6.1, SPEC.md §2.4).
|
|
200
|
+
*
|
|
201
|
+
* `primaryKeyIndex` is the row's primary-key column; a payload without it is
|
|
202
|
+
* encoder misuse and throws, like a NULL in a non-nullable column.
|
|
203
|
+
*/
|
|
204
|
+
export function encodeSparseRow(
|
|
205
|
+
columns: readonly RowColumn[],
|
|
206
|
+
primaryKeyIndex: number,
|
|
207
|
+
values: readonly SparseRowValue[],
|
|
208
|
+
): Uint8Array {
|
|
209
|
+
if (values.length !== columns.length) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`row value count ${values.length} does not match column count ${columns.length}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const presence = new Uint8Array(Math.ceil(columns.length / 8));
|
|
215
|
+
let presentCount = 0;
|
|
216
|
+
for (let i = 0; i < columns.length; i++) {
|
|
217
|
+
if (values[i] === undefined) continue;
|
|
218
|
+
presence[i >> 3] = (presence[i >> 3] ?? 0) | (1 << (i & 7));
|
|
219
|
+
presentCount++;
|
|
220
|
+
}
|
|
221
|
+
const primaryKey = columns[primaryKeyIndex];
|
|
222
|
+
if (primaryKey === undefined) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`primary-key column index ${primaryKeyIndex} is outside the column table`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (!bitmapBit(presence, primaryKeyIndex)) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`sparse row is missing the primary-key column ${primaryKey.name}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const nulls = new Uint8Array(Math.ceil(presentCount / 8));
|
|
233
|
+
let present = 0;
|
|
234
|
+
for (let i = 0; i < columns.length; i++) {
|
|
235
|
+
const column = columns[i];
|
|
236
|
+
const value = values[i];
|
|
237
|
+
if (column === undefined || value === undefined) continue;
|
|
238
|
+
if (value === null) {
|
|
239
|
+
if (!column.nullable) {
|
|
240
|
+
throw new Error(`column ${column.name} is not nullable`);
|
|
241
|
+
}
|
|
242
|
+
nulls[present >> 3] = (nulls[present >> 3] ?? 0) | (1 << (present & 7));
|
|
243
|
+
}
|
|
244
|
+
present++;
|
|
245
|
+
}
|
|
246
|
+
const writer = new ByteWriter();
|
|
247
|
+
writer.raw(presence);
|
|
248
|
+
writer.raw(nulls);
|
|
249
|
+
for (let i = 0; i < columns.length; i++) {
|
|
250
|
+
const column = columns[i];
|
|
251
|
+
const value = values[i];
|
|
252
|
+
if (column === undefined || value === undefined || value === null) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
writeValue(writer, column, value);
|
|
256
|
+
}
|
|
257
|
+
return writer.finish();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Decode one standalone sparse row; the bytes must contain exactly one row.
|
|
262
|
+
*
|
|
263
|
+
* Decode checks, in order (each a decode error, RFC §6.1): a set presence
|
|
264
|
+
* padding bit, an absent primary-key column, a set null-bitmap padding bit
|
|
265
|
+
* (a null for an absent column), a null bit for a non-nullable column.
|
|
266
|
+
*/
|
|
267
|
+
export function decodeSparseRow(
|
|
268
|
+
columns: readonly RowColumn[],
|
|
269
|
+
primaryKeyIndex: number,
|
|
270
|
+
bytes: Uint8Array,
|
|
271
|
+
): SparseRowValue[] {
|
|
272
|
+
const reader = new ByteReader(bytes);
|
|
273
|
+
const presenceLength = Math.ceil(columns.length / 8);
|
|
274
|
+
const presence = reader.raw(presenceLength);
|
|
275
|
+
for (let bit = columns.length; bit < presenceLength * 8; bit++) {
|
|
276
|
+
if (bitmapBit(presence, bit)) {
|
|
277
|
+
throw new DecodeError(
|
|
278
|
+
'sync.invalid_request',
|
|
279
|
+
'sparse row presence bitmap has a set padding bit (non-canonical encoding)',
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const primaryKey = columns[primaryKeyIndex];
|
|
284
|
+
if (primaryKey === undefined) {
|
|
285
|
+
throw new DecodeError(
|
|
286
|
+
'sync.invalid_request',
|
|
287
|
+
`primary-key column index ${primaryKeyIndex} is outside the column table`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (!bitmapBit(presence, primaryKeyIndex)) {
|
|
291
|
+
throw new DecodeError(
|
|
292
|
+
'sync.invalid_request',
|
|
293
|
+
`sparse row is missing the primary-key column ${primaryKey.name}`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
let presentCount = 0;
|
|
297
|
+
for (let i = 0; i < columns.length; i++) {
|
|
298
|
+
if (bitmapBit(presence, i)) presentCount++;
|
|
299
|
+
}
|
|
300
|
+
const nullLength = Math.ceil(presentCount / 8);
|
|
301
|
+
const nulls = reader.raw(nullLength);
|
|
302
|
+
for (let bit = presentCount; bit < nullLength * 8; bit++) {
|
|
303
|
+
if (bitmapBit(nulls, bit)) {
|
|
304
|
+
throw new DecodeError(
|
|
305
|
+
'sync.invalid_request',
|
|
306
|
+
'sparse row null bitmap sets a null bit for an absent column',
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const values: SparseRowValue[] = [];
|
|
311
|
+
let present = 0;
|
|
312
|
+
for (let i = 0; i < columns.length; i++) {
|
|
313
|
+
const column = columns[i];
|
|
314
|
+
if (column === undefined || !bitmapBit(presence, i)) {
|
|
315
|
+
values.push(undefined);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const isNull = bitmapBit(nulls, present);
|
|
319
|
+
present++;
|
|
320
|
+
if (!isNull) {
|
|
321
|
+
values.push(readValue(reader, column));
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (!column.nullable) {
|
|
325
|
+
throw new DecodeError(
|
|
326
|
+
'sync.invalid_request',
|
|
327
|
+
`null bit set for non-nullable column ${column.name}`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
values.push(null);
|
|
331
|
+
}
|
|
332
|
+
reader.expectFullyConsumed('sparse row payload');
|
|
333
|
+
return values;
|
|
334
|
+
}
|
|
335
|
+
|
|
181
336
|
export function writeRow(
|
|
182
337
|
writer: ByteWriter,
|
|
183
338
|
columns: readonly RowColumn[],
|