@powersync/service-module-postgres 0.16.2 → 0.16.3
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/CHANGELOG.md +14 -0
- package/dist/api/PostgresRouteAPIAdapter.d.ts +1 -0
- package/dist/api/PostgresRouteAPIAdapter.js +8 -1
- package/dist/api/PostgresRouteAPIAdapter.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/module/PostgresModule.d.ts +1 -0
- package/dist/module/PostgresModule.js +9 -4
- package/dist/module/PostgresModule.js.map +1 -1
- package/dist/replication/ConnectionManagerFactory.d.ts +3 -1
- package/dist/replication/ConnectionManagerFactory.js +4 -2
- package/dist/replication/ConnectionManagerFactory.js.map +1 -1
- package/dist/replication/PgManager.d.ts +8 -2
- package/dist/replication/PgManager.js +5 -4
- package/dist/replication/PgManager.js.map +1 -1
- package/dist/replication/PgRelation.d.ts +1 -0
- package/dist/replication/PgRelation.js +7 -0
- package/dist/replication/PgRelation.js.map +1 -1
- package/dist/replication/WalStream.d.ts +6 -1
- package/dist/replication/WalStream.js +45 -33
- package/dist/replication/WalStream.js.map +1 -1
- package/dist/types/registry.d.ts +69 -0
- package/dist/types/registry.js +196 -0
- package/dist/types/registry.js.map +1 -0
- package/dist/types/resolver.d.ts +47 -0
- package/dist/types/resolver.js +191 -0
- package/dist/types/resolver.js.map +1 -0
- package/dist/types/types.d.ts +4 -1
- package/dist/types/types.js.map +1 -1
- package/dist/utils/postgres_version.d.ts +3 -0
- package/dist/utils/postgres_version.js +7 -0
- package/dist/utils/postgres_version.js.map +1 -0
- package/package.json +10 -10
- package/src/api/PostgresRouteAPIAdapter.ts +9 -1
- package/src/index.ts +0 -2
- package/src/module/PostgresModule.ts +10 -4
- package/src/replication/ConnectionManagerFactory.ts +6 -2
- package/src/replication/PgManager.ts +12 -4
- package/src/replication/PgRelation.ts +9 -0
- package/src/replication/WalStream.ts +49 -30
- package/src/types/registry.ts +278 -0
- package/src/types/resolver.ts +210 -0
- package/src/types/types.ts +5 -1
- package/src/utils/postgres_version.ts +8 -0
- package/test/src/pg_test.test.ts +152 -5
- package/test/src/route_api_adapter.test.ts +60 -0
- package/test/src/schema_changes.test.ts +32 -0
- package/test/src/slow_tests.test.ts +3 -2
- package/test/src/types/registry.test.ts +149 -0
- package/test/src/util.ts +16 -0
- package/test/src/wal_stream.test.ts +24 -0
- package/test/src/wal_stream_utils.ts +2 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/pgwire_utils.d.ts +0 -17
- package/dist/utils/pgwire_utils.js +0 -43
- package/dist/utils/pgwire_utils.js.map +0 -1
- package/src/utils/pgwire_utils.ts +0 -48
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyValueContext,
|
|
3
|
+
CompatibilityContext,
|
|
4
|
+
CompatibilityOption,
|
|
5
|
+
CustomSqliteValue,
|
|
6
|
+
DatabaseInputValue,
|
|
7
|
+
SqliteValue,
|
|
8
|
+
SqliteValueType,
|
|
9
|
+
toSyncRulesValue
|
|
10
|
+
} from '@powersync/service-sync-rules';
|
|
11
|
+
import * as pgwire from '@powersync/service-jpgwire';
|
|
12
|
+
|
|
13
|
+
interface BaseType {
|
|
14
|
+
sqliteType: () => SqliteValueType;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A type natively supported by {@link pgwire.PgType.decode}. */
|
|
18
|
+
interface BuiltinType extends BaseType {
|
|
19
|
+
type: 'builtin';
|
|
20
|
+
oid: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An array type.
|
|
25
|
+
*/
|
|
26
|
+
interface ArrayType extends BaseType {
|
|
27
|
+
type: 'array';
|
|
28
|
+
innerId: number;
|
|
29
|
+
separatorCharCode: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
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
|
+
*/
|
|
37
|
+
interface DomainType extends BaseType {
|
|
38
|
+
type: 'domain';
|
|
39
|
+
innerId: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
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
|
+
*/
|
|
47
|
+
interface CompositeType extends BaseType {
|
|
48
|
+
type: 'composite';
|
|
49
|
+
members: { name: string; typeId: number }[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A type created with `CREATE TYPE AS RANGE`.
|
|
54
|
+
*
|
|
55
|
+
* Ranges are represented as {@link pgwire.Range}. Multiranges are represented as arrays thereof.
|
|
56
|
+
*/
|
|
57
|
+
interface RangeType extends BaseType {
|
|
58
|
+
type: 'range' | 'multirange';
|
|
59
|
+
innerId: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type KnownType = BuiltinType | ArrayType | DomainType | DomainType | CompositeType | RangeType;
|
|
63
|
+
|
|
64
|
+
interface UnknownType extends BaseType {
|
|
65
|
+
type: 'unknown';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type MaybeKnownType = KnownType | UnknownType;
|
|
69
|
+
|
|
70
|
+
const UNKNOWN_TYPE: UnknownType = {
|
|
71
|
+
type: 'unknown',
|
|
72
|
+
sqliteType: () => 'text'
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
class CustomTypeValue extends CustomSqliteValue {
|
|
76
|
+
constructor(
|
|
77
|
+
readonly oid: number,
|
|
78
|
+
readonly cache: CustomTypeRegistry,
|
|
79
|
+
readonly rawValue: string
|
|
80
|
+
) {
|
|
81
|
+
super();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private lookup(): KnownType | UnknownType {
|
|
85
|
+
return this.cache.lookupType(this.oid);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private decodeToDatabaseInputValue(context: CompatibilityContext): DatabaseInputValue {
|
|
89
|
+
if (context.isEnabled(CompatibilityOption.customTypes)) {
|
|
90
|
+
try {
|
|
91
|
+
return this.cache.decodeWithCustomTypes(this.rawValue, this.oid);
|
|
92
|
+
} catch (_e) {
|
|
93
|
+
return this.rawValue;
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
return pgwire.PgType.decode(this.rawValue, this.oid);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
toSqliteValue(context: CompatibilityContext): SqliteValue {
|
|
101
|
+
const value = toSyncRulesValue(this.decodeToDatabaseInputValue(context));
|
|
102
|
+
return applyValueContext(value, context);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get sqliteType(): SqliteValueType {
|
|
106
|
+
return this.lookup().sqliteType();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A registry of custom types.
|
|
112
|
+
*
|
|
113
|
+
* These extend the builtin decoding behavior in {@link pgwire.PgType.decode} for user-defined types like `DOMAIN`s or
|
|
114
|
+
* composite types.
|
|
115
|
+
*/
|
|
116
|
+
export class CustomTypeRegistry {
|
|
117
|
+
private readonly byOid: Map<number, KnownType>;
|
|
118
|
+
|
|
119
|
+
constructor() {
|
|
120
|
+
this.byOid = new Map();
|
|
121
|
+
|
|
122
|
+
for (const builtin of Object.values(pgwire.PgTypeOid)) {
|
|
123
|
+
if (typeof builtin == 'number') {
|
|
124
|
+
// We need to know the SQLite type of builtins to implement CustomSqliteValue.sqliteType for DOMAIN types.
|
|
125
|
+
let sqliteType: SqliteValueType;
|
|
126
|
+
switch (builtin) {
|
|
127
|
+
case pgwire.PgTypeOid.TEXT:
|
|
128
|
+
case pgwire.PgTypeOid.UUID:
|
|
129
|
+
case pgwire.PgTypeOid.VARCHAR:
|
|
130
|
+
case pgwire.PgTypeOid.DATE:
|
|
131
|
+
case pgwire.PgTypeOid.TIMESTAMP:
|
|
132
|
+
case pgwire.PgTypeOid.TIMESTAMPTZ:
|
|
133
|
+
case pgwire.PgTypeOid.TIME:
|
|
134
|
+
case pgwire.PgTypeOid.JSON:
|
|
135
|
+
case pgwire.PgTypeOid.JSONB:
|
|
136
|
+
case pgwire.PgTypeOid.PG_LSN:
|
|
137
|
+
sqliteType = 'text';
|
|
138
|
+
break;
|
|
139
|
+
case pgwire.PgTypeOid.BYTEA:
|
|
140
|
+
sqliteType = 'blob';
|
|
141
|
+
break;
|
|
142
|
+
case pgwire.PgTypeOid.BOOL:
|
|
143
|
+
case pgwire.PgTypeOid.INT2:
|
|
144
|
+
case pgwire.PgTypeOid.INT4:
|
|
145
|
+
case pgwire.PgTypeOid.OID:
|
|
146
|
+
case pgwire.PgTypeOid.INT8:
|
|
147
|
+
sqliteType = 'integer';
|
|
148
|
+
break;
|
|
149
|
+
case pgwire.PgTypeOid.FLOAT4:
|
|
150
|
+
case pgwire.PgTypeOid.FLOAT8:
|
|
151
|
+
sqliteType = 'real';
|
|
152
|
+
break;
|
|
153
|
+
default:
|
|
154
|
+
sqliteType = 'text';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.byOid.set(builtin, {
|
|
158
|
+
type: 'builtin',
|
|
159
|
+
oid: builtin,
|
|
160
|
+
sqliteType: () => sqliteType
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const [arrayId, innerId] of pgwire.ARRAY_TO_ELEM_OID.entries()) {
|
|
166
|
+
// We can just use the default decoder, except for box[] because those use a different delimiter. We don't fix
|
|
167
|
+
// this in PgType._decodeArray for backwards-compatibility.
|
|
168
|
+
if (innerId == 603) {
|
|
169
|
+
this.byOid.set(arrayId, {
|
|
170
|
+
type: 'array',
|
|
171
|
+
innerId,
|
|
172
|
+
sqliteType: () => 'text', // these get encoded as JSON arrays
|
|
173
|
+
separatorCharCode: 0x3b // ";"
|
|
174
|
+
});
|
|
175
|
+
} else {
|
|
176
|
+
this.byOid.set(arrayId, {
|
|
177
|
+
type: 'builtin',
|
|
178
|
+
oid: arrayId,
|
|
179
|
+
sqliteType: () => 'text' // these get encoded as JSON arrays
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
knows(oid: number): boolean {
|
|
186
|
+
return this.byOid.has(oid);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
set(oid: number, value: KnownType) {
|
|
190
|
+
this.byOid.set(oid, value);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
setDomainType(oid: number, inner: number) {
|
|
194
|
+
this.set(oid, {
|
|
195
|
+
type: 'domain',
|
|
196
|
+
innerId: inner,
|
|
197
|
+
sqliteType: () => this.lookupType(inner).sqliteType()
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
decodeWithCustomTypes(raw: string, oid: number): DatabaseInputValue {
|
|
202
|
+
const resolved = this.lookupType(oid);
|
|
203
|
+
switch (resolved.type) {
|
|
204
|
+
case 'builtin':
|
|
205
|
+
case 'unknown':
|
|
206
|
+
return pgwire.PgType.decode(raw, oid);
|
|
207
|
+
case 'domain':
|
|
208
|
+
return this.decodeWithCustomTypes(raw, resolved.innerId);
|
|
209
|
+
case 'composite': {
|
|
210
|
+
const parsed: [string, any][] = [];
|
|
211
|
+
|
|
212
|
+
new pgwire.StructureParser(raw).parseComposite((raw) => {
|
|
213
|
+
const nextMember = resolved.members[parsed.length];
|
|
214
|
+
if (nextMember) {
|
|
215
|
+
const value = raw == null ? null : this.decodeWithCustomTypes(raw, nextMember.typeId);
|
|
216
|
+
parsed.push([nextMember.name, value]);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
return Object.fromEntries(parsed);
|
|
220
|
+
}
|
|
221
|
+
case 'array': {
|
|
222
|
+
// Nornalize "array of array of T" types into just "array of T", because Postgres arrays are natively multi-
|
|
223
|
+
// dimensional. This may be required when we have a DOMAIN wrapper around an array followed by another array
|
|
224
|
+
// around that domain.
|
|
225
|
+
let innerId = resolved.innerId;
|
|
226
|
+
while (true) {
|
|
227
|
+
const resolvedInner = this.lookupType(innerId);
|
|
228
|
+
if (resolvedInner.type == 'domain') {
|
|
229
|
+
innerId = resolvedInner.innerId;
|
|
230
|
+
} else if (resolvedInner.type == 'array') {
|
|
231
|
+
innerId = resolvedInner.innerId;
|
|
232
|
+
} else {
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return new pgwire.StructureParser(raw).parseArray(
|
|
238
|
+
(source) => this.decodeWithCustomTypes(source, innerId),
|
|
239
|
+
resolved.separatorCharCode
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
case 'range':
|
|
243
|
+
return new pgwire.StructureParser(raw).parseRange((s) => this.decodeWithCustomTypes(s, resolved.innerId));
|
|
244
|
+
case 'multirange':
|
|
245
|
+
return new pgwire.StructureParser(raw).parseMultiRange((s) => this.decodeWithCustomTypes(s, resolved.innerId));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
lookupType(type: number): KnownType | UnknownType {
|
|
250
|
+
return this.byOid.get(type) ?? UNKNOWN_TYPE;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private isParsedWithoutCustomTypesSupport(type: MaybeKnownType): boolean {
|
|
254
|
+
switch (type.type) {
|
|
255
|
+
case 'builtin':
|
|
256
|
+
case 'unknown':
|
|
257
|
+
return true;
|
|
258
|
+
case 'array':
|
|
259
|
+
return (
|
|
260
|
+
type.separatorCharCode == pgwire.CHAR_CODE_COMMA &&
|
|
261
|
+
this.isParsedWithoutCustomTypesSupport(this.lookupType(type.innerId))
|
|
262
|
+
);
|
|
263
|
+
default:
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
decodeDatabaseValue(value: string, oid: number): DatabaseInputValue {
|
|
269
|
+
const resolved = this.lookupType(oid);
|
|
270
|
+
// For backwards-compatibility, some types are only properly parsed with a compatibility option. Others are synced
|
|
271
|
+
// in the raw text representation by default, and are only parsed as JSON values when necessary.
|
|
272
|
+
if (this.isParsedWithoutCustomTypesSupport(resolved)) {
|
|
273
|
+
return pgwire.PgType.decode(value, oid);
|
|
274
|
+
} else {
|
|
275
|
+
return new CustomTypeValue(oid, this, value);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { DatabaseInputRow, SqliteInputRow, toSyncRulesRow } from '@powersync/service-sync-rules';
|
|
2
|
+
import * as pgwire from '@powersync/service-jpgwire';
|
|
3
|
+
import { CustomTypeRegistry } from './registry.js';
|
|
4
|
+
import semver from 'semver';
|
|
5
|
+
import { getServerVersion } from '../utils/postgres_version.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolves descriptions used to decode values for custom postgres types.
|
|
9
|
+
*
|
|
10
|
+
* Custom types are resolved from the source database, which also involves crawling inner types (e.g. for composites).
|
|
11
|
+
*/
|
|
12
|
+
export class PostgresTypeResolver {
|
|
13
|
+
private cachedVersion: semver.SemVer | null = null;
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
readonly registry: CustomTypeRegistry,
|
|
17
|
+
private readonly pool: pgwire.PgClient
|
|
18
|
+
) {
|
|
19
|
+
this.registry = new CustomTypeRegistry();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private async fetchVersion(): Promise<semver.SemVer> {
|
|
23
|
+
if (this.cachedVersion == null) {
|
|
24
|
+
this.cachedVersion = (await getServerVersion(this.pool)) ?? semver.parse('0.0.1');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return this.cachedVersion!;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @returns Whether the Postgres instance this type cache is connected to has support for the multirange type (which
|
|
32
|
+
* is the case for Postgres 14 and later).
|
|
33
|
+
*/
|
|
34
|
+
async supportsMultiRanges() {
|
|
35
|
+
const version = await this.fetchVersion();
|
|
36
|
+
return version.compare(PostgresTypeResolver.minVersionForMultirange) >= 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fetches information about indicated types.
|
|
41
|
+
*
|
|
42
|
+
* If a type references another custom type (e.g. because it's a composite type with a custom field), these are
|
|
43
|
+
* automatically crawled as well.
|
|
44
|
+
*/
|
|
45
|
+
public async fetchTypes(oids: number[]) {
|
|
46
|
+
const multiRangeSupport = await this.supportsMultiRanges();
|
|
47
|
+
|
|
48
|
+
let pending = oids.filter((id) => !this.registry.knows(id));
|
|
49
|
+
// For details on columns, see https://www.postgresql.org/docs/current/catalog-pg-type.html
|
|
50
|
+
const multiRangeDesc = `WHEN 'm' THEN json_build_object('inner', (SELECT rngsubtype FROM pg_range WHERE rngmultitypid = t.oid))`;
|
|
51
|
+
const statement = `
|
|
52
|
+
SELECT oid, t.typtype,
|
|
53
|
+
CASE t.typtype
|
|
54
|
+
WHEN 'b' THEN json_build_object('element_type', t.typelem, 'delim', (SELECT typdelim FROM pg_type i WHERE i.oid = t.typelem))
|
|
55
|
+
WHEN 'd' THEN json_build_object('type', t.typbasetype)
|
|
56
|
+
WHEN 'c' THEN json_build_object(
|
|
57
|
+
'elements',
|
|
58
|
+
(SELECT json_agg(json_build_object('name', a.attname, 'type', a.atttypid))
|
|
59
|
+
FROM pg_attribute a
|
|
60
|
+
WHERE a.attrelid = t.typrelid)
|
|
61
|
+
)
|
|
62
|
+
WHEN 'r' THEN json_build_object('inner', (SELECT rngsubtype FROM pg_range WHERE rngtypid = t.oid))
|
|
63
|
+
${multiRangeSupport ? multiRangeDesc : ''}
|
|
64
|
+
ELSE NULL
|
|
65
|
+
END AS desc
|
|
66
|
+
FROM pg_type t
|
|
67
|
+
WHERE t.oid = ANY($1)
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
while (pending.length != 0) {
|
|
71
|
+
// 1016: int8 array
|
|
72
|
+
const query = await this.pool.query({ statement, params: [{ type: 1016, value: pending }] });
|
|
73
|
+
const stillPending: number[] = [];
|
|
74
|
+
|
|
75
|
+
const requireType = (oid: number) => {
|
|
76
|
+
if (!this.registry.knows(oid) && !pending.includes(oid) && !stillPending.includes(oid)) {
|
|
77
|
+
stillPending.push(oid);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
for (const row of pgwire.pgwireRows(query)) {
|
|
82
|
+
const oid = Number(row.oid);
|
|
83
|
+
const desc = JSON.parse(row.desc);
|
|
84
|
+
|
|
85
|
+
switch (row.typtype) {
|
|
86
|
+
case 'b':
|
|
87
|
+
const { element_type, delim } = desc;
|
|
88
|
+
|
|
89
|
+
if (!this.registry.knows(oid)) {
|
|
90
|
+
// This type is an array of another custom type.
|
|
91
|
+
const inner = Number(element_type);
|
|
92
|
+
if (inner != 0) {
|
|
93
|
+
// Some array types like macaddr[] don't seem to have their inner type set properly - skip!
|
|
94
|
+
requireType(inner);
|
|
95
|
+
this.registry.set(oid, {
|
|
96
|
+
type: 'array',
|
|
97
|
+
innerId: inner,
|
|
98
|
+
separatorCharCode: (delim as string).charCodeAt(0),
|
|
99
|
+
sqliteType: () => 'text' // Since it's JSON
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
case 'c':
|
|
105
|
+
// For composite types, we sync the JSON representation.
|
|
106
|
+
const elements: { name: string; typeId: number }[] = [];
|
|
107
|
+
for (const { name, type } of desc.elements) {
|
|
108
|
+
const typeId = Number(type);
|
|
109
|
+
elements.push({ name, typeId });
|
|
110
|
+
requireType(typeId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.registry.set(oid, {
|
|
114
|
+
type: 'composite',
|
|
115
|
+
members: elements,
|
|
116
|
+
sqliteType: () => 'text' // Since it's JSON
|
|
117
|
+
});
|
|
118
|
+
break;
|
|
119
|
+
case 'd':
|
|
120
|
+
// For domain values like CREATE DOMAIN api.rating_value AS FLOAT CHECK (VALUE BETWEEN 0 AND 5), we sync
|
|
121
|
+
// the inner type (pg_type.typbasetype).
|
|
122
|
+
const inner = Number(desc.type);
|
|
123
|
+
this.registry.setDomainType(oid, inner);
|
|
124
|
+
requireType(inner);
|
|
125
|
+
break;
|
|
126
|
+
case 'r':
|
|
127
|
+
case 'm': {
|
|
128
|
+
const inner = Number(desc.inner);
|
|
129
|
+
this.registry.set(oid, {
|
|
130
|
+
type: row.typtype == 'r' ? 'range' : 'multirange',
|
|
131
|
+
innerId: inner,
|
|
132
|
+
sqliteType: () => 'text' // Since it's JSON
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pending = stillPending;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Crawls all custom types referenced by table columns in the current database.
|
|
144
|
+
*/
|
|
145
|
+
public async fetchTypesForSchema() {
|
|
146
|
+
const sql = `
|
|
147
|
+
SELECT DISTINCT a.atttypid AS type_oid
|
|
148
|
+
FROM pg_attribute a
|
|
149
|
+
JOIN pg_class c ON c.oid = a.attrelid
|
|
150
|
+
JOIN pg_namespace cn ON cn.oid = c.relnamespace
|
|
151
|
+
JOIN pg_type t ON t.oid = a.atttypid
|
|
152
|
+
JOIN pg_namespace tn ON tn.oid = t.typnamespace
|
|
153
|
+
WHERE a.attnum > 0
|
|
154
|
+
AND NOT a.attisdropped
|
|
155
|
+
AND cn.nspname not in ('information_schema', 'pg_catalog', 'pg_toast')
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
const query = await this.pool.query({ statement: sql });
|
|
159
|
+
let ids: number[] = [];
|
|
160
|
+
for (const row of pgwire.pgwireRows(query)) {
|
|
161
|
+
ids.push(Number(row.type_oid));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await this.fetchTypes(ids);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* pgwire message -> SQLite row.
|
|
169
|
+
* @param message
|
|
170
|
+
*/
|
|
171
|
+
constructAfterRecord(message: pgwire.PgoutputInsert | pgwire.PgoutputUpdate): SqliteInputRow {
|
|
172
|
+
const rawData = (message as any).afterRaw;
|
|
173
|
+
|
|
174
|
+
const record = this.decodeTuple(message.relation, rawData);
|
|
175
|
+
return toSyncRulesRow(record);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* pgwire message -> SQLite row.
|
|
180
|
+
* @param message
|
|
181
|
+
*/
|
|
182
|
+
constructBeforeRecord(message: pgwire.PgoutputDelete | pgwire.PgoutputUpdate): SqliteInputRow | undefined {
|
|
183
|
+
const rawData = (message as any).beforeRaw;
|
|
184
|
+
if (rawData == null) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const record = this.decodeTuple(message.relation, rawData);
|
|
188
|
+
return toSyncRulesRow(record);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* We need a high level of control over how values are decoded, to make sure there is no loss
|
|
193
|
+
* of precision in the process.
|
|
194
|
+
*/
|
|
195
|
+
decodeTuple(relation: pgwire.PgoutputRelation, tupleRaw: Record<string, any>): DatabaseInputRow {
|
|
196
|
+
let result: Record<string, any> = {};
|
|
197
|
+
for (let columnName in tupleRaw) {
|
|
198
|
+
const rawval = tupleRaw[columnName];
|
|
199
|
+
const typeOid = (relation as any)._tupleDecoder._typeOids.get(columnName);
|
|
200
|
+
if (typeof rawval == 'string' && typeOid) {
|
|
201
|
+
result[columnName] = this.registry.decodeDatabaseValue(rawval, typeOid);
|
|
202
|
+
} else {
|
|
203
|
+
result[columnName] = rawval;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private static minVersionForMultirange: semver.SemVer = semver.parse('14.0.0')!;
|
|
210
|
+
}
|
package/src/types/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
2
2
|
import * as service_types from '@powersync/service-types';
|
|
3
3
|
import * as t from 'ts-codec';
|
|
4
|
+
import { CustomTypeRegistry } from './registry.js';
|
|
4
5
|
|
|
5
6
|
// Maintain backwards compatibility by exporting these
|
|
6
7
|
export const validatePort = lib_postgres.validatePort;
|
|
@@ -24,7 +25,10 @@ export type PostgresConnectionConfig = t.Decoded<typeof PostgresConnectionConfig
|
|
|
24
25
|
/**
|
|
25
26
|
* Resolved version of {@link PostgresConnectionConfig}
|
|
26
27
|
*/
|
|
27
|
-
export type ResolvedConnectionConfig = PostgresConnectionConfig &
|
|
28
|
+
export type ResolvedConnectionConfig = PostgresConnectionConfig &
|
|
29
|
+
NormalizedPostgresConnectionConfig & {
|
|
30
|
+
typeRegistry: CustomTypeRegistry;
|
|
31
|
+
};
|
|
28
32
|
|
|
29
33
|
export function isPostgresConfig(
|
|
30
34
|
config: service_types.configFile.DataSourceConfig
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as pgwire from '@powersync/service-jpgwire';
|
|
2
|
+
import semver, { type SemVer } from 'semver';
|
|
3
|
+
|
|
4
|
+
export async function getServerVersion(db: pgwire.PgClient): Promise<SemVer | null> {
|
|
5
|
+
const result = await db.query(`SHOW server_version;`);
|
|
6
|
+
// The result is usually of the form "16.2 (Debian 16.2-1.pgdg120+2)"
|
|
7
|
+
return semver.coerce(result.rows[0][0].split(' ')[0]);
|
|
8
|
+
}
|