@powersync/service-module-postgres 0.19.3 → 0.19.4
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/api/PostgresRouteAPIAdapter.d.ts +1 -1
- package/dist/api/PostgresRouteAPIAdapter.js +63 -72
- package/dist/api/PostgresRouteAPIAdapter.js.map +1 -1
- package/dist/module/PostgresModule.js.map +1 -1
- package/dist/replication/MissingReplicationSlotError.d.ts +41 -0
- package/dist/replication/MissingReplicationSlotError.js +33 -0
- package/dist/replication/MissingReplicationSlotError.js.map +1 -0
- package/dist/replication/PostgresErrorRateLimiter.js +1 -1
- package/dist/replication/PostgresErrorRateLimiter.js.map +1 -1
- package/dist/replication/SnapshotQuery.js +2 -2
- package/dist/replication/SnapshotQuery.js.map +1 -1
- package/dist/replication/WalStream.d.ts +35 -3
- package/dist/replication/WalStream.js +135 -9
- package/dist/replication/WalStream.js.map +1 -1
- package/dist/replication/WalStreamReplicationJob.js +6 -3
- package/dist/replication/WalStreamReplicationJob.js.map +1 -1
- package/dist/replication/replication-index.d.ts +3 -1
- package/dist/replication/replication-index.js +3 -1
- package/dist/replication/replication-index.js.map +1 -1
- package/dist/replication/replication-utils.d.ts +3 -11
- package/dist/replication/replication-utils.js +101 -164
- package/dist/replication/replication-utils.js.map +1 -1
- package/dist/replication/wal-budget-utils.d.ts +23 -0
- package/dist/replication/wal-budget-utils.js +57 -0
- package/dist/replication/wal-budget-utils.js.map +1 -0
- package/dist/types/registry.js +1 -1
- package/dist/types/registry.js.map +1 -1
- package/package.json +15 -11
- package/sql/check-source-configuration.plpgsql +13 -0
- package/sql/debug-tables-info-batched.plpgsql +230 -0
- package/CHANGELOG.md +0 -858
- package/src/api/PostgresRouteAPIAdapter.ts +0 -356
- package/src/index.ts +0 -1
- package/src/module/PostgresModule.ts +0 -122
- package/src/replication/ConnectionManagerFactory.ts +0 -33
- package/src/replication/PgManager.ts +0 -122
- package/src/replication/PgRelation.ts +0 -41
- package/src/replication/PostgresErrorRateLimiter.ts +0 -48
- package/src/replication/SnapshotQuery.ts +0 -213
- package/src/replication/WalStream.ts +0 -1137
- package/src/replication/WalStreamReplicationJob.ts +0 -138
- package/src/replication/WalStreamReplicator.ts +0 -53
- package/src/replication/replication-index.ts +0 -5
- package/src/replication/replication-utils.ts +0 -398
- package/src/types/registry.ts +0 -275
- package/src/types/resolver.ts +0 -227
- package/src/types/types.ts +0 -44
- package/src/utils/application-name.ts +0 -8
- package/src/utils/migration_lib.ts +0 -80
- package/src/utils/populate_test_data.ts +0 -37
- package/src/utils/populate_test_data_worker.ts +0 -53
- package/src/utils/postgres_version.ts +0 -8
- package/test/src/checkpoints.test.ts +0 -86
- package/test/src/chunked_snapshots.test.ts +0 -161
- package/test/src/env.ts +0 -11
- package/test/src/large_batch.test.ts +0 -241
- package/test/src/pg_test.test.ts +0 -729
- package/test/src/resuming_snapshots.test.ts +0 -160
- package/test/src/route_api_adapter.test.ts +0 -62
- package/test/src/schema_changes.test.ts +0 -655
- package/test/src/setup.ts +0 -12
- package/test/src/slow_tests.test.ts +0 -519
- package/test/src/storage_combination.test.ts +0 -35
- package/test/src/types/registry.test.ts +0 -149
- package/test/src/util.ts +0 -151
- package/test/src/validation.test.ts +0 -63
- package/test/src/wal_stream.test.ts +0 -607
- package/test/src/wal_stream_utils.ts +0 -284
- package/test/tsconfig.json +0 -27
- package/tsconfig.json +0 -34
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -3
package/src/types/registry.ts
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
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 | 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 type.separatorCharCode == pgwire.CHAR_CODE_COMMA && pgwire.ARRAY_TO_ELEM_OID.has(type.innerId);
|
|
260
|
-
default:
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
decodeDatabaseValue(value: string, oid: number): DatabaseInputValue {
|
|
266
|
-
const resolved = this.lookupType(oid);
|
|
267
|
-
// For backwards-compatibility, some types are only properly parsed with a compatibility option. Others are synced
|
|
268
|
-
// in the raw text representation by default, and are only parsed as JSON values when necessary.
|
|
269
|
-
if (this.isParsedWithoutCustomTypesSupport(resolved)) {
|
|
270
|
-
return pgwire.PgType.decode(value, oid);
|
|
271
|
-
} else {
|
|
272
|
-
return new CustomTypeValue(oid, this, value);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
package/src/types/resolver.ts
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import * as pgwire from '@powersync/service-jpgwire';
|
|
2
|
-
import { DatabaseInputRow, SqliteInputRow, toSyncRulesRow } from '@powersync/service-sync-rules';
|
|
3
|
-
import semver from 'semver';
|
|
4
|
-
import { getServerVersion } from '../utils/postgres_version.js';
|
|
5
|
-
import { CustomTypeRegistry } from './registry.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
|
-
readonly registry: CustomTypeRegistry;
|
|
15
|
-
|
|
16
|
-
constructor(private readonly pool: pgwire.PgClient) {
|
|
17
|
-
this.registry = new CustomTypeRegistry();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
private async fetchVersion(): Promise<semver.SemVer> {
|
|
21
|
-
if (this.cachedVersion == null) {
|
|
22
|
-
this.cachedVersion = (await getServerVersion(this.pool)) ?? semver.parse('0.0.1');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return this.cachedVersion!;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @returns Whether the Postgres instance this type cache is connected to has support for the multirange type (which
|
|
30
|
-
* is the case for Postgres 14 and later).
|
|
31
|
-
*/
|
|
32
|
-
async supportsMultiRanges() {
|
|
33
|
-
const version = await this.fetchVersion();
|
|
34
|
-
return version.compare(PostgresTypeResolver.minVersionForMultirange) >= 0;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Fetches information about indicated types.
|
|
39
|
-
*
|
|
40
|
-
* If a type references another custom type (e.g. because it's a composite type with a custom field), these are
|
|
41
|
-
* automatically crawled as well.
|
|
42
|
-
*/
|
|
43
|
-
public async fetchTypes(oids: number[]) {
|
|
44
|
-
const multiRangeSupport = await this.supportsMultiRanges();
|
|
45
|
-
|
|
46
|
-
let pending = oids.filter((id) => !this.registry.knows(id));
|
|
47
|
-
// For details on columns, see https://www.postgresql.org/docs/current/catalog-pg-type.html
|
|
48
|
-
const multiRangeDesc = `WHEN 'm' THEN json_build_object('inner', (SELECT rngsubtype FROM pg_range WHERE rngmultitypid = t.oid))`;
|
|
49
|
-
const statement = `
|
|
50
|
-
SELECT oid, t.typtype,
|
|
51
|
-
CASE t.typtype
|
|
52
|
-
WHEN 'b' THEN json_build_object('element_type', t.typelem, 'delim', (SELECT typdelim FROM pg_type i WHERE i.oid = t.typelem))
|
|
53
|
-
WHEN 'd' THEN json_build_object('type', t.typbasetype)
|
|
54
|
-
WHEN 'c' THEN json_build_object(
|
|
55
|
-
'elements',
|
|
56
|
-
(SELECT json_agg(json_build_object('name', a.attname, 'type', a.atttypid))
|
|
57
|
-
FROM pg_attribute a
|
|
58
|
-
WHERE a.attrelid = t.typrelid)
|
|
59
|
-
)
|
|
60
|
-
WHEN 'r' THEN json_build_object('inner', (SELECT rngsubtype FROM pg_range WHERE rngtypid = t.oid))
|
|
61
|
-
${multiRangeSupport ? multiRangeDesc : ''}
|
|
62
|
-
ELSE NULL
|
|
63
|
-
END AS desc
|
|
64
|
-
FROM pg_type t
|
|
65
|
-
WHERE t.oid = ANY($1)
|
|
66
|
-
`;
|
|
67
|
-
|
|
68
|
-
while (pending.length != 0) {
|
|
69
|
-
// 1016: int8 array
|
|
70
|
-
const query = await this.pool.query({ statement, params: [{ type: 1016, value: pending }] });
|
|
71
|
-
const stillPending: number[] = [];
|
|
72
|
-
|
|
73
|
-
const requireType = (oid: number) => {
|
|
74
|
-
if (!this.registry.knows(oid) && !pending.includes(oid) && !stillPending.includes(oid)) {
|
|
75
|
-
stillPending.push(oid);
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
for (const row of pgwire.pgwireRows(query)) {
|
|
80
|
-
const oid = Number(row.oid);
|
|
81
|
-
const desc = JSON.parse(row.desc);
|
|
82
|
-
|
|
83
|
-
switch (row.typtype) {
|
|
84
|
-
case 'b':
|
|
85
|
-
const { element_type, delim } = desc;
|
|
86
|
-
|
|
87
|
-
if (!this.registry.knows(oid)) {
|
|
88
|
-
// This type is an array of another custom type.
|
|
89
|
-
const inner = Number(element_type);
|
|
90
|
-
if (inner != 0) {
|
|
91
|
-
// Some array types like macaddr[] don't seem to have their inner type set properly - skip!
|
|
92
|
-
requireType(inner);
|
|
93
|
-
this.registry.set(oid, {
|
|
94
|
-
type: 'array',
|
|
95
|
-
innerId: inner,
|
|
96
|
-
separatorCharCode: (delim as string).charCodeAt(0),
|
|
97
|
-
sqliteType: () => 'text' // Since it's JSON
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
break;
|
|
102
|
-
case 'c':
|
|
103
|
-
// For composite types, we sync the JSON representation.
|
|
104
|
-
const elements: { name: string; typeId: number }[] = [];
|
|
105
|
-
for (const { name, type } of desc.elements) {
|
|
106
|
-
const typeId = Number(type);
|
|
107
|
-
elements.push({ name, typeId });
|
|
108
|
-
requireType(typeId);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
this.registry.set(oid, {
|
|
112
|
-
type: 'composite',
|
|
113
|
-
members: elements,
|
|
114
|
-
sqliteType: () => 'text' // Since it's JSON
|
|
115
|
-
});
|
|
116
|
-
break;
|
|
117
|
-
case 'd':
|
|
118
|
-
// For domain values like CREATE DOMAIN api.rating_value AS FLOAT CHECK (VALUE BETWEEN 0 AND 5), we sync
|
|
119
|
-
// the inner type (pg_type.typbasetype).
|
|
120
|
-
const inner = Number(desc.type);
|
|
121
|
-
this.registry.setDomainType(oid, inner);
|
|
122
|
-
requireType(inner);
|
|
123
|
-
break;
|
|
124
|
-
case 'r':
|
|
125
|
-
case 'm': {
|
|
126
|
-
const inner = Number(desc.inner);
|
|
127
|
-
this.registry.set(oid, {
|
|
128
|
-
type: row.typtype == 'r' ? 'range' : 'multirange',
|
|
129
|
-
innerId: inner,
|
|
130
|
-
sqliteType: () => 'text' // Since it's JSON
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
pending = stillPending;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Crawls all custom types referenced by table columns in the current database.
|
|
142
|
-
*/
|
|
143
|
-
public async fetchTypesForSchema() {
|
|
144
|
-
const sql = `
|
|
145
|
-
SELECT DISTINCT a.atttypid AS type_oid
|
|
146
|
-
FROM pg_attribute a
|
|
147
|
-
JOIN pg_class c ON c.oid = a.attrelid
|
|
148
|
-
JOIN pg_namespace cn ON cn.oid = c.relnamespace
|
|
149
|
-
JOIN pg_type t ON t.oid = a.atttypid
|
|
150
|
-
JOIN pg_namespace tn ON tn.oid = t.typnamespace
|
|
151
|
-
WHERE a.attnum > 0
|
|
152
|
-
AND NOT a.attisdropped
|
|
153
|
-
AND cn.nspname not in ('information_schema', 'pg_catalog', 'pg_toast')
|
|
154
|
-
`;
|
|
155
|
-
|
|
156
|
-
const query = await this.pool.query(sql);
|
|
157
|
-
let ids: number[] = [];
|
|
158
|
-
for (const row of pgwire.pgwireRows(query)) {
|
|
159
|
-
ids.push(Number(row.type_oid));
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
await this.fetchTypes(ids);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* pgwire message -> SQLite row.
|
|
167
|
-
* @param message
|
|
168
|
-
*/
|
|
169
|
-
constructAfterRecord(message: pgwire.PgoutputInsert | pgwire.PgoutputUpdate): SqliteInputRow {
|
|
170
|
-
const rawData = (message as any).afterRaw;
|
|
171
|
-
|
|
172
|
-
const record = this.decodeTuple(message.relation, rawData);
|
|
173
|
-
return toSyncRulesRow(record);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* pgwire message -> SQLite row.
|
|
178
|
-
* @param message
|
|
179
|
-
*/
|
|
180
|
-
constructBeforeRecord(message: pgwire.PgoutputDelete | pgwire.PgoutputUpdate): SqliteInputRow | undefined {
|
|
181
|
-
const rawData = (message as any).beforeRaw;
|
|
182
|
-
if (rawData == null) {
|
|
183
|
-
return undefined;
|
|
184
|
-
}
|
|
185
|
-
const record = this.decodeTuple(message.relation, rawData);
|
|
186
|
-
return toSyncRulesRow(record);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* We need a high level of control over how values are decoded, to make sure there is no loss
|
|
191
|
-
* of precision in the process.
|
|
192
|
-
*/
|
|
193
|
-
decodeTuple(relation: pgwire.PgoutputRelation, tupleRaw: Record<string, any>): DatabaseInputRow {
|
|
194
|
-
let result: Record<string, any> = {};
|
|
195
|
-
for (const column of relation.columns) {
|
|
196
|
-
const rawval = tupleRaw[column.name];
|
|
197
|
-
result[column.name] =
|
|
198
|
-
rawval == null
|
|
199
|
-
? // We can't decode null values, but it's important that null and undefined stay distinct because undefined
|
|
200
|
-
// represents a TOASTed value.
|
|
201
|
-
rawval
|
|
202
|
-
: this.registry.decodeDatabaseValue(rawval, column.typeOid);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return result;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* We need a high level of control over how values are decoded, to make sure there is no loss
|
|
210
|
-
* of precision in the process.
|
|
211
|
-
*/
|
|
212
|
-
private decodeTupleForTable(columnMap: Record<string, number>, tupleRaw: Record<string, any>): DatabaseInputRow {
|
|
213
|
-
let result: Record<string, any> = {};
|
|
214
|
-
for (let columnName in tupleRaw) {
|
|
215
|
-
const rawval = tupleRaw[columnName];
|
|
216
|
-
const typeOid = columnMap[columnName];
|
|
217
|
-
if (typeof rawval == 'string' && typeOid) {
|
|
218
|
-
result[columnName] = this.registry.decodeDatabaseValue(rawval, typeOid);
|
|
219
|
-
} else {
|
|
220
|
-
result[columnName] = rawval;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return result;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
private static minVersionForMultirange: semver.SemVer = semver.parse('14.0.0')!;
|
|
227
|
-
}
|
package/src/types/types.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
2
|
-
import * as service_types from '@powersync/service-types';
|
|
3
|
-
import * as t from 'ts-codec';
|
|
4
|
-
|
|
5
|
-
// Maintain backwards compatibility by exporting these
|
|
6
|
-
export const validatePort = lib_postgres.validatePort;
|
|
7
|
-
export const baseUri = lib_postgres.baseUri;
|
|
8
|
-
export type NormalizedPostgresConnectionConfig = lib_postgres.NormalizedBasePostgresConnectionConfig;
|
|
9
|
-
export const POSTGRES_CONNECTION_TYPE = lib_postgres.POSTGRES_CONNECTION_TYPE;
|
|
10
|
-
|
|
11
|
-
export const PostgresConnectionConfig = service_types.configFile.DataSourceConfig.and(
|
|
12
|
-
lib_postgres.BasePostgresConnectionConfig
|
|
13
|
-
).and(
|
|
14
|
-
t.object({
|
|
15
|
-
// Add any replication connection specific config here in future
|
|
16
|
-
})
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Config input specified when starting services
|
|
21
|
-
*/
|
|
22
|
-
export type PostgresConnectionConfig = t.Decoded<typeof PostgresConnectionConfig>;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Resolved version of {@link PostgresConnectionConfig}
|
|
26
|
-
*/
|
|
27
|
-
export type ResolvedConnectionConfig = PostgresConnectionConfig & NormalizedPostgresConnectionConfig;
|
|
28
|
-
|
|
29
|
-
export function isPostgresConfig(
|
|
30
|
-
config: service_types.configFile.DataSourceConfig
|
|
31
|
-
): config is PostgresConnectionConfig {
|
|
32
|
-
return config.type == lib_postgres.POSTGRES_CONNECTION_TYPE;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Validate and normalize connection options.
|
|
37
|
-
*
|
|
38
|
-
* Returns destructured options.
|
|
39
|
-
*/
|
|
40
|
-
export function normalizeConnectionConfig(options: PostgresConnectionConfig) {
|
|
41
|
-
return {
|
|
42
|
-
...lib_postgres.normalizeConnectionConfig(options)
|
|
43
|
-
} satisfies NormalizedPostgresConnectionConfig;
|
|
44
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
2
|
-
import * as pgwire from '@powersync/service-jpgwire';
|
|
3
|
-
|
|
4
|
-
export type MigrationFunction = (db: pgwire.PgConnection) => Promise<void>;
|
|
5
|
-
|
|
6
|
-
interface Migration {
|
|
7
|
-
id: number;
|
|
8
|
-
name: string;
|
|
9
|
-
up: MigrationFunction;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Very loosely based on https://github.com/porsager/postgres-shift/
|
|
13
|
-
export class Migrations {
|
|
14
|
-
private migrations: Migration[] = [];
|
|
15
|
-
|
|
16
|
-
add(id: number, name: string, up: MigrationFunction) {
|
|
17
|
-
if (this.migrations.length > 0 && this.migrations[this.migrations.length - 1].id >= id) {
|
|
18
|
-
throw new ServiceAssertionError('Migration ids must be strictly incrementing');
|
|
19
|
-
}
|
|
20
|
-
this.migrations.push({ id, up, name });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async up(db: pgwire.PgConnection) {
|
|
24
|
-
await db.query('BEGIN');
|
|
25
|
-
try {
|
|
26
|
-
await this.ensureMigrationsTable(db);
|
|
27
|
-
const current = await this.getCurrentMigration(db);
|
|
28
|
-
let currentId = current ? current.id : 0;
|
|
29
|
-
|
|
30
|
-
for (let migration of this.migrations) {
|
|
31
|
-
if (migration.id <= currentId) {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
await migration.up(db);
|
|
35
|
-
|
|
36
|
-
await db.query({
|
|
37
|
-
statement: `
|
|
38
|
-
insert into migrations (
|
|
39
|
-
migration_id,
|
|
40
|
-
name
|
|
41
|
-
) values (
|
|
42
|
-
$1,
|
|
43
|
-
$2
|
|
44
|
-
)
|
|
45
|
-
`,
|
|
46
|
-
params: [
|
|
47
|
-
{ type: 'int4', value: migration.id },
|
|
48
|
-
{ type: 'varchar', value: migration.name }
|
|
49
|
-
]
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
await db.query('COMMIT');
|
|
54
|
-
} catch (e) {
|
|
55
|
-
await db.query('ROLLBACK');
|
|
56
|
-
throw e;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
getCurrentMigration(db: pgwire.PgConnection) {
|
|
61
|
-
return db
|
|
62
|
-
.query(
|
|
63
|
-
`
|
|
64
|
-
select migration_id as id from migrations
|
|
65
|
-
order by migration_id desc
|
|
66
|
-
limit 1
|
|
67
|
-
`
|
|
68
|
-
)
|
|
69
|
-
.then((results) => ({ id: results.rows[0].decodeWithoutCustomTypes(0) as number }));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async ensureMigrationsTable(db: pgwire.PgConnection) {
|
|
73
|
-
await db.query(`create table if not exists migrations (
|
|
74
|
-
migration_id serial primary key,
|
|
75
|
-
created_at timestamp with time zone not null default now(),
|
|
76
|
-
name text
|
|
77
|
-
)
|
|
78
|
-
`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { Worker } from 'node:worker_threads';
|
|
2
|
-
|
|
3
|
-
import * as pgwire from '@powersync/service-jpgwire';
|
|
4
|
-
|
|
5
|
-
// This util is actually for tests only, but we need it compiled to JS for the service to work, so it's placed in the service.
|
|
6
|
-
|
|
7
|
-
export interface PopulateDataOptions {
|
|
8
|
-
connection: pgwire.NormalizedConnectionConfig;
|
|
9
|
-
num_transactions: number;
|
|
10
|
-
per_transaction: number;
|
|
11
|
-
size: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function populateData(options: PopulateDataOptions) {
|
|
15
|
-
const WORKER_TIMEOUT = 30_000;
|
|
16
|
-
|
|
17
|
-
const worker = new Worker(new URL('./populate_test_data_worker.js', import.meta.url), {
|
|
18
|
-
workerData: options
|
|
19
|
-
});
|
|
20
|
-
const timeout = setTimeout(() => {
|
|
21
|
-
// Exits with code 1 below
|
|
22
|
-
worker.terminate();
|
|
23
|
-
}, WORKER_TIMEOUT);
|
|
24
|
-
try {
|
|
25
|
-
return await new Promise<number>((resolve, reject) => {
|
|
26
|
-
worker.on('message', resolve);
|
|
27
|
-
worker.on('error', reject);
|
|
28
|
-
worker.on('exit', (code) => {
|
|
29
|
-
if (code !== 0) {
|
|
30
|
-
reject(new Error(`Populating data failed with exit code ${code}`));
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
} finally {
|
|
35
|
-
clearTimeout(timeout);
|
|
36
|
-
}
|
|
37
|
-
}
|