Skip to content

Commit 16b4268

Browse files
committed
Docs
1 parent 9e3bead commit 16b4268

File tree

5 files changed

+62
-11
lines changed

5 files changed

+62
-11
lines changed

modules/module-postgres/src/types/cache.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@ import { DatabaseInputRow, SqliteInputRow, toSyncRulesRow } from '@powersync/ser
22
import * as pgwire from '@powersync/service-jpgwire';
33
import { CustomTypeRegistry } from './registry.js';
44

5+
/**
6+
* A cache of custom types for which information can be crawled from the source database.
7+
*/
58
export class PostgresTypeCache {
69
readonly registry: CustomTypeRegistry;
710

811
constructor(private readonly pool: pgwire.PgClient) {
912
this.registry = new CustomTypeRegistry();
1013
}
1114

15+
/**
16+
* Fetches information about indicated types.
17+
*
18+
* If a type references another custom type (e.g. because it's a composite type with a custom field), these are
19+
* automatically crawled as well.
20+
*/
1221
public async fetchTypes(oids: number[]) {
1322
let pending = oids.filter((id) => !this.registry.knows(id));
1423
// For details on columns, see https://www.postgresql.org/docs/current/catalog-pg-type.html
@@ -30,6 +39,7 @@ WHERE t.oid = ANY($1)
3039
`;
3140

3241
while (pending.length != 0) {
42+
// 1016: int8 array
3343
const query = await this.pool.query({ statement, params: [{ type: 1016, value: pending }] });
3444
const stillPending: number[] = [];
3545

modules/module-postgres/src/types/registry.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,41 @@ import {
99
toSyncRulesValue
1010
} from '@powersync/service-sync-rules';
1111
import * as pgwire from '@powersync/service-jpgwire';
12-
import { JsonContainer } from '@powersync/service-jsonbig';
1312

1413
interface BaseType {
1514
sqliteType: () => SqliteValueType;
1615
}
1716

17+
/** A type natively supported by {@link pgwire.PgType.decode}. */
1818
interface BuiltinType extends BaseType {
1919
type: 'builtin';
2020
oid: number;
2121
}
2222

23+
/**
24+
* An array type.
25+
*/
2326
interface ArrayType extends BaseType {
2427
type: 'array';
2528
innerId: number;
2629
separatorCharCode: number;
2730
}
2831

32+
/**
33+
* A domain type, like `CREATE DOMAIN api.rating_value AS FLOAT CHECK (VALUE BETWEEN 0 AND 5);`
34+
*
35+
* This type gets decoded and synced as the inner type (`FLOAT` in the example above).
36+
*/
2937
interface DomainType extends BaseType {
3038
type: 'domain';
3139
innerId: number;
3240
}
3341

42+
/**
43+
* A composite type as created by `CREATE TYPE AS`.
44+
*
45+
* These types are encoded as a tuple of values, so we recover attribute names to restore them as a JSON object.
46+
*/
3447
interface CompositeType extends BaseType {
3548
type: 'composite';
3649
members: { name: string; typeId: number }[];
@@ -81,6 +94,12 @@ class CustomTypeValue extends CustomSqliteValue {
8194
}
8295
}
8396

97+
/**
98+
* A registry of custom types.
99+
*
100+
* These extend the builtin decoding behavior in {@link pgwire.PgType.decode} for user-defined types like `DOMAIN`s or
101+
* composite types.
102+
*/
84103
export class CustomTypeRegistry {
85104
private readonly byOid: Map<number, KnownType>;
86105

@@ -89,6 +108,7 @@ export class CustomTypeRegistry {
89108

90109
for (const builtin of Object.values(pgwire.PgTypeOid)) {
91110
if (typeof builtin == 'number') {
111+
// We need to know the SQLite type of builtins to implement CustomSqliteValue.sqliteType for DOMAIN types.
92112
let sqliteType: SqliteValueType;
93113
switch (builtin) {
94114
case pgwire.PgTypeOid.TEXT:
@@ -168,12 +188,6 @@ export class CustomTypeRegistry {
168188
return pgwire.PgType.decode(raw, oid);
169189
case 'domain':
170190
return this.decodeWithCustomTypes(raw, resolved.innerId);
171-
case 'array':
172-
return pgwire.decodeArray({
173-
source: raw,
174-
decodeElement: (source) => this.decodeWithCustomTypes(source, resolved.innerId),
175-
delimiterCharCode: resolved.separatorCharCode
176-
});
177191
case 'composite': {
178192
const parsed: [string, any][] = [];
179193

@@ -196,6 +210,28 @@ export class CustomTypeRegistry {
196210

197211
return Object.fromEntries(parsed);
198212
}
213+
case 'array': {
214+
// Nornalize "array of array of T" types into just "array of T", because Postgres arrays are natively multi-
215+
// dimensional. This may be required when we have a DOMAIN wrapper around an array followed by another array
216+
// around that domain.
217+
let innerId = resolved.innerId;
218+
while (true) {
219+
const resolvedInner = this.lookupType(innerId);
220+
if (resolvedInner.type == 'domain') {
221+
innerId = resolvedInner.innerId;
222+
} else if (resolvedInner.type == 'array') {
223+
innerId = resolvedInner.innerId;
224+
} else {
225+
break;
226+
}
227+
}
228+
229+
return pgwire.decodeArray({
230+
source: raw,
231+
decodeElement: (source) => this.decodeWithCustomTypes(source, innerId),
232+
delimiterCharCode: resolved.separatorCharCode
233+
});
234+
}
199235
}
200236
}
201237

@@ -217,6 +253,8 @@ export class CustomTypeRegistry {
217253

218254
decodeDatabaseValue(value: string, oid: number): DatabaseInputValue {
219255
const resolved = this.lookupType(oid);
256+
// For backwards-compatibility, some types are only properly parsed with a compatibility option. Others are synced
257+
// in the raw text representation by default, and are only parsed as JSON values when necessary.
220258
if (this.isParsedWithoutCustomTypesSupport(resolved)) {
221259
return pgwire.PgType.decode(value, oid);
222260
} else {

modules/module-postgres/test/src/types/registry.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ describe('custom type registry', () => {
4343
registry.setDomainType(1339, 1338);
4444

4545
checkResult('{1,2,3}', 1339, '{1,2,3}', '[1,2,3]');
46+
47+
registry.set(1400, { type: 'array', separatorCharCode: CHAR_CODE_COMMA, innerId: 1339, sqliteType: () => 'text' });
48+
checkResult('{{1,2,3}}', 1400, '{{1,2,3}}', '[[1,2,3]]');
4649
});
4750

4851
test('structure', () => {

packages/jpgwire/src/sequence_tokenizer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export interface SequenceListener {
88
* Invoked whenever the tokenizer has finished parsing a value that isn't a nested structure.
99
*
1010
* @param value the raw value, with escape characters related to the outer structure being removed. `null` for the
11-
* literal text `NULL`.
11+
* literal text {@link Delimiters.nullLiteral}.
1212
*/
1313
onValue: (value: string | null) => void;
1414
/**

packages/sync-rules/src/compatibility.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ export class CompatibilityOption {
2323
);
2424

2525
static customTypes = new CompatibilityOption(
26-
'custom_types',
27-
'Map custom types into appropriate structures.',
26+
'custom_postgres_types',
27+
'Map custom Postgres types into appropriate structures instead of syncing the raw string.',
2828
CompatibilityEdition.SYNC_STREAMS
2929
);
3030

3131
static byName: Record<string, CompatibilityOption> = Object.freeze({
3232
timestamps_iso8601: this.timestampsIso8601,
33-
custom_types: this.customTypes
33+
custom_postgres_types: this.customTypes
3434
});
3535
}
3636

0 commit comments

Comments
 (0)