@powersync/service-module-postgres 0.4.0 → 0.5.1
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 +31 -0
- package/dist/api/PostgresRouteAPIAdapter.js +6 -1
- package/dist/api/PostgresRouteAPIAdapter.js.map +1 -1
- package/dist/module/PostgresModule.d.ts +4 -2
- package/dist/module/PostgresModule.js +11 -3
- package/dist/module/PostgresModule.js.map +1 -1
- package/dist/replication/ConnectionManagerFactory.d.ts +1 -1
- package/dist/replication/PgManager.js.map +1 -1
- package/dist/replication/PgRelation.js +2 -1
- package/dist/replication/PgRelation.js.map +1 -1
- package/dist/replication/WalStream.js +7 -6
- package/dist/replication/WalStream.js.map +1 -1
- package/dist/replication/WalStreamReplicator.d.ts +1 -0
- package/dist/replication/WalStreamReplicator.js +4 -0
- package/dist/replication/WalStreamReplicator.js.map +1 -1
- package/dist/replication/replication-utils.js +4 -4
- package/dist/replication/replication-utils.js.map +1 -1
- package/dist/types/types.d.ts +6 -2
- package/dist/utils/migration_lib.js +2 -1
- package/dist/utils/migration_lib.js.map +1 -1
- package/dist/utils/pgwire_utils.d.ts +6 -1
- package/dist/utils/pgwire_utils.js +20 -2
- package/dist/utils/pgwire_utils.js.map +1 -1
- package/package.json +10 -10
- package/src/api/PostgresRouteAPIAdapter.ts +6 -1
- package/src/module/PostgresModule.ts +22 -5
- package/src/replication/ConnectionManagerFactory.ts +1 -1
- package/src/replication/PgManager.ts +1 -0
- package/src/replication/PgRelation.ts +2 -1
- package/src/replication/WalStream.ts +13 -6
- package/src/replication/WalStreamReplicator.ts +5 -0
- package/src/replication/replication-utils.ts +9 -4
- package/src/utils/migration_lib.ts +2 -1
- package/src/utils/pgwire_utils.ts +21 -3
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
|
-
"version": "0.
|
|
8
|
+
"version": "0.5.1",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"license": "FSL-1.1-Apache-2.0",
|
|
11
11
|
"type": "module",
|
|
@@ -27,19 +27,19 @@
|
|
|
27
27
|
"ts-codec": "^1.3.0",
|
|
28
28
|
"uuid": "^9.0.1",
|
|
29
29
|
"uri-js": "^4.4.1",
|
|
30
|
-
"@powersync/lib-services-framework": "0.
|
|
31
|
-
"@powersync/lib-service-postgres": "0.
|
|
32
|
-
"@powersync/service-core": "0.
|
|
33
|
-
"@powersync/service-jpgwire": "0.
|
|
30
|
+
"@powersync/lib-services-framework": "0.5.0",
|
|
31
|
+
"@powersync/lib-service-postgres": "0.2.0",
|
|
32
|
+
"@powersync/service-core": "0.16.1",
|
|
33
|
+
"@powersync/service-jpgwire": "0.19.0",
|
|
34
34
|
"@powersync/service-jsonbig": "0.17.10",
|
|
35
|
-
"@powersync/service-sync-rules": "0.23.
|
|
36
|
-
"@powersync/service-types": "0.7.
|
|
35
|
+
"@powersync/service-sync-rules": "0.23.3",
|
|
36
|
+
"@powersync/service-types": "0.7.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/uuid": "^9.0.4",
|
|
40
|
-
"@powersync/service-core-tests": "0.3.
|
|
41
|
-
"@powersync/service-module-mongodb-storage": "0.3.
|
|
42
|
-
"@powersync/service-module-postgres-storage": "0.1.
|
|
40
|
+
"@powersync/service-core-tests": "0.3.2",
|
|
41
|
+
"@powersync/service-module-mongodb-storage": "0.3.2",
|
|
42
|
+
"@powersync/service-module-postgres-storage": "0.1.2"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsc -b",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
2
|
+
import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
|
|
2
3
|
import { api, ParseSyncRulesOptions } from '@powersync/service-core';
|
|
3
4
|
import * as pgwire from '@powersync/service-jpgwire';
|
|
4
5
|
import * as sync_rules from '@powersync/service-sync-rules';
|
|
@@ -228,7 +229,11 @@ FROM pg_replication_slots WHERE slot_name = $1 LIMIT 1;`,
|
|
|
228
229
|
return Number(row.lsn_distance);
|
|
229
230
|
}
|
|
230
231
|
|
|
231
|
-
throw new
|
|
232
|
+
throw new ServiceError({
|
|
233
|
+
status: 500,
|
|
234
|
+
code: ErrorCode.PSYNC_S4001,
|
|
235
|
+
description: `Could not determine replication lag for slot ${slotName}`
|
|
236
|
+
});
|
|
232
237
|
}
|
|
233
238
|
|
|
234
239
|
async getReplicationHead(): Promise<string> {
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
api,
|
|
3
|
+
auth,
|
|
4
|
+
ConfigurationFileSyncRulesProvider,
|
|
5
|
+
ConnectionTestResult,
|
|
6
|
+
modules,
|
|
7
|
+
replication,
|
|
8
|
+
system
|
|
9
|
+
} from '@powersync/service-core';
|
|
2
10
|
import * as jpgwire from '@powersync/service-jpgwire';
|
|
3
11
|
import { PostgresRouteAPIAdapter } from '../api/PostgresRouteAPIAdapter.js';
|
|
4
12
|
import { SupabaseKeyCollector } from '../auth/SupabaseKeyCollector.js';
|
|
@@ -10,6 +18,7 @@ import { PUBLICATION_NAME } from '../replication/WalStream.js';
|
|
|
10
18
|
import { WalStreamReplicator } from '../replication/WalStreamReplicator.js';
|
|
11
19
|
import * as types from '../types/types.js';
|
|
12
20
|
import { PostgresConnectionConfig } from '../types/types.js';
|
|
21
|
+
import { baseUri, NormalizedBasePostgresConnectionConfig } from '@powersync/lib-service-postgres';
|
|
13
22
|
|
|
14
23
|
export class PostgresModule extends replication.ReplicationModule<types.PostgresConnectionConfig> {
|
|
15
24
|
constructor() {
|
|
@@ -124,18 +133,26 @@ export class PostgresModule extends replication.ReplicationModule<types.Postgres
|
|
|
124
133
|
});
|
|
125
134
|
}
|
|
126
135
|
|
|
127
|
-
async testConnection(config: PostgresConnectionConfig): Promise<
|
|
136
|
+
async testConnection(config: PostgresConnectionConfig): Promise<ConnectionTestResult> {
|
|
128
137
|
this.decodeConfig(config);
|
|
129
|
-
const
|
|
130
|
-
|
|
138
|
+
const normalizedConfig = this.resolveConfig(this.decodedConfig!);
|
|
139
|
+
return await this.testConnection(normalizedConfig);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
static async testConnection(normalizedConfig: NormalizedBasePostgresConnectionConfig): Promise<ConnectionTestResult> {
|
|
143
|
+
// FIXME: This is not a complete implementation yet.
|
|
144
|
+
const connectionManager = new PgManager(normalizedConfig, {
|
|
131
145
|
idleTimeout: 30_000,
|
|
132
146
|
maxSize: 1
|
|
133
147
|
});
|
|
134
148
|
const connection = await connectionManager.snapshotConnection();
|
|
135
149
|
try {
|
|
136
|
-
|
|
150
|
+
await checkSourceConfiguration(connection, PUBLICATION_NAME);
|
|
137
151
|
} finally {
|
|
138
152
|
await connectionManager.end();
|
|
139
153
|
}
|
|
154
|
+
return {
|
|
155
|
+
connectionDescription: baseUri(normalizedConfig)
|
|
156
|
+
};
|
|
140
157
|
}
|
|
141
158
|
}
|
|
@@ -5,7 +5,7 @@ import { logger } from '@powersync/lib-services-framework';
|
|
|
5
5
|
|
|
6
6
|
export class ConnectionManagerFactory {
|
|
7
7
|
private readonly connectionManagers: PgManager[];
|
|
8
|
-
|
|
8
|
+
public readonly dbConnectionConfig: NormalizedPostgresConnectionConfig;
|
|
9
9
|
|
|
10
10
|
constructor(dbConnectionConfig: NormalizedPostgresConnectionConfig) {
|
|
11
11
|
this.dbConnectionConfig = dbConnectionConfig;
|
|
@@ -50,6 +50,7 @@ export class PgManager {
|
|
|
50
50
|
// for the full 6 minutes.
|
|
51
51
|
// This we are constantly using the connection, we don't need any
|
|
52
52
|
// custom keepalives.
|
|
53
|
+
|
|
53
54
|
(connection as any)._socket.setTimeout(SNAPSHOT_SOCKET_TIMEOUT);
|
|
54
55
|
|
|
55
56
|
// Disable statement timeout for snapshot queries.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ReplicationAssertionError, ServiceError } from '@powersync/lib-services-framework';
|
|
1
2
|
import { storage } from '@powersync/service-core';
|
|
2
3
|
import { PgoutputRelation } from '@powersync/service-jpgwire';
|
|
3
4
|
|
|
@@ -16,7 +17,7 @@ export function getRelId(source: PgoutputRelation): number {
|
|
|
16
17
|
// Source types are wrong here
|
|
17
18
|
const relId = (source as any).relationOid as number;
|
|
18
19
|
if (!relId) {
|
|
19
|
-
throw new
|
|
20
|
+
throw new ReplicationAssertionError(`No relation id found`);
|
|
20
21
|
}
|
|
21
22
|
return relId;
|
|
22
23
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
container,
|
|
4
|
+
errors,
|
|
5
|
+
logger,
|
|
6
|
+
ReplicationAbortedError,
|
|
7
|
+
ReplicationAssertionError
|
|
8
|
+
} from '@powersync/lib-services-framework';
|
|
3
9
|
import { getUuidReplicaIdentityBson, Metrics, SourceEntityDescriptor, storage } from '@powersync/service-core';
|
|
4
10
|
import * as pgwire from '@powersync/service-jpgwire';
|
|
5
11
|
import { DatabaseInputRow, SqliteRow, SqlSyncRules, TablePattern, toSyncRulesRow } from '@powersync/service-sync-rules';
|
|
@@ -133,7 +139,7 @@ export class WalStream {
|
|
|
133
139
|
for (let row of tableRows) {
|
|
134
140
|
const name = row.table_name as string;
|
|
135
141
|
if (typeof row.relid != 'bigint') {
|
|
136
|
-
throw new
|
|
142
|
+
throw new ReplicationAssertionError(`Missing relid for ${name}`);
|
|
137
143
|
}
|
|
138
144
|
const relid = Number(row.relid as bigint);
|
|
139
145
|
|
|
@@ -294,7 +300,7 @@ export class WalStream {
|
|
|
294
300
|
}
|
|
295
301
|
}
|
|
296
302
|
|
|
297
|
-
throw new
|
|
303
|
+
throw new ReplicationAssertionError('Unreachable');
|
|
298
304
|
}
|
|
299
305
|
|
|
300
306
|
async estimatedCount(db: pgwire.PgConnection, table: storage.SourceTable): Promise<string> {
|
|
@@ -415,7 +421,7 @@ WHERE oid = $1::regclass`,
|
|
|
415
421
|
lastLogIndex = at;
|
|
416
422
|
}
|
|
417
423
|
if (this.abort_signal.aborted) {
|
|
418
|
-
throw new
|
|
424
|
+
throw new ReplicationAbortedError(`Aborted initial replication of ${this.slot_name}`);
|
|
419
425
|
}
|
|
420
426
|
|
|
421
427
|
for (const record of WalStream.getQueryData(rows)) {
|
|
@@ -441,7 +447,7 @@ WHERE oid = $1::regclass`,
|
|
|
441
447
|
|
|
442
448
|
async handleRelation(batch: storage.BucketStorageBatch, descriptor: SourceEntityDescriptor, snapshot: boolean) {
|
|
443
449
|
if (!descriptor.objectId && typeof descriptor.objectId != 'number') {
|
|
444
|
-
throw new
|
|
450
|
+
throw new ReplicationAssertionError(`objectId expected, got ${typeof descriptor.objectId}`);
|
|
445
451
|
}
|
|
446
452
|
const result = await this.storage.resolveTable({
|
|
447
453
|
group_id: this.group_id,
|
|
@@ -484,6 +490,7 @@ WHERE oid = $1::regclass`,
|
|
|
484
490
|
await db.query('COMMIT');
|
|
485
491
|
} catch (e) {
|
|
486
492
|
await db.query('ROLLBACK');
|
|
493
|
+
// TODO: Wrap with custom error type
|
|
487
494
|
throw e;
|
|
488
495
|
}
|
|
489
496
|
} finally {
|
|
@@ -501,7 +508,7 @@ WHERE oid = $1::regclass`,
|
|
|
501
508
|
if (table == null) {
|
|
502
509
|
// We should always receive a replication message before the relation is used.
|
|
503
510
|
// If we can't find it, it's a bug.
|
|
504
|
-
throw new
|
|
511
|
+
throw new ReplicationAssertionError(`Missing relation cache for ${relationId}`);
|
|
505
512
|
}
|
|
506
513
|
return table;
|
|
507
514
|
}
|
|
@@ -2,6 +2,7 @@ import { replication, storage } from '@powersync/service-core';
|
|
|
2
2
|
import { ConnectionManagerFactory } from './ConnectionManagerFactory.js';
|
|
3
3
|
import { cleanUpReplicationSlot } from './replication-utils.js';
|
|
4
4
|
import { WalStreamReplicationJob } from './WalStreamReplicationJob.js';
|
|
5
|
+
import { PostgresModule } from '../module/PostgresModule.js';
|
|
5
6
|
|
|
6
7
|
export interface WalStreamReplicatorOptions extends replication.AbstractReplicatorOptions {
|
|
7
8
|
connectionFactory: ConnectionManagerFactory;
|
|
@@ -42,4 +43,8 @@ export class WalStreamReplicator extends replication.AbstractReplicator<WalStrea
|
|
|
42
43
|
await super.stop();
|
|
43
44
|
await this.connectionFactory.shutdown();
|
|
44
45
|
}
|
|
46
|
+
|
|
47
|
+
async testConnection() {
|
|
48
|
+
return await PostgresModule.testConnection(this.connectionFactory.dbConnectionConfig);
|
|
49
|
+
}
|
|
45
50
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as pgwire from '@powersync/service-jpgwire';
|
|
2
2
|
|
|
3
3
|
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
4
|
-
import { logger } from '@powersync/lib-services-framework';
|
|
4
|
+
import { ErrorCode, logger, ServiceError } from '@powersync/lib-services-framework';
|
|
5
5
|
import { PatternResult, storage } from '@powersync/service-core';
|
|
6
6
|
import * as sync_rules from '@powersync/service-sync-rules';
|
|
7
7
|
import * as service_types from '@powersync/service-types';
|
|
@@ -117,17 +117,22 @@ $$ LANGUAGE plpgsql;`
|
|
|
117
117
|
});
|
|
118
118
|
const row = pgwire.pgwireRows(rs)[0];
|
|
119
119
|
if (row == null) {
|
|
120
|
-
throw new
|
|
120
|
+
throw new ServiceError(
|
|
121
|
+
ErrorCode.PSYNC_S1141,
|
|
121
122
|
`Publication '${publicationName}' does not exist. Run: \`CREATE PUBLICATION ${publicationName} FOR ALL TABLES\`, or read the documentation for details.`
|
|
122
123
|
);
|
|
123
124
|
}
|
|
124
125
|
if (row.pubinsert == false || row.pubupdate == false || row.pubdelete == false || row.pubtruncate == false) {
|
|
125
|
-
throw new
|
|
126
|
+
throw new ServiceError(
|
|
127
|
+
ErrorCode.PSYNC_S1142,
|
|
126
128
|
`Publication '${publicationName}' does not publish all changes. Create a publication using \`WITH (publish = "insert, update, delete, truncate")\` (the default).`
|
|
127
129
|
);
|
|
128
130
|
}
|
|
129
131
|
if (row.pubviaroot) {
|
|
130
|
-
throw new
|
|
132
|
+
throw new ServiceError(
|
|
133
|
+
ErrorCode.PSYNC_S1143,
|
|
134
|
+
`'${publicationName}' uses publish_via_partition_root, which is not supported.`
|
|
135
|
+
);
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
138
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
1
2
|
import * as pgwire from '@powersync/service-jpgwire';
|
|
2
3
|
|
|
3
4
|
export type MigrationFunction = (db: pgwire.PgConnection) => Promise<void>;
|
|
@@ -14,7 +15,7 @@ export class Migrations {
|
|
|
14
15
|
|
|
15
16
|
add(id: number, name: string, up: MigrationFunction) {
|
|
16
17
|
if (this.migrations.length > 0 && this.migrations[this.migrations.length - 1].id >= id) {
|
|
17
|
-
throw new
|
|
18
|
+
throw new ServiceAssertionError('Migration ids must be strictly incrementing');
|
|
18
19
|
}
|
|
19
20
|
this.migrations.push({ id, up, name });
|
|
20
21
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Adapted from https://github.com/kagis/pgwire/blob/0dc927f9f8990a903f238737326e53ba1c8d094f/mod.js#L2218
|
|
2
2
|
|
|
3
3
|
import * as pgwire from '@powersync/service-jpgwire';
|
|
4
|
-
import { SqliteRow, toSyncRulesRow } from '@powersync/service-sync-rules';
|
|
4
|
+
import { DatabaseInputRow, SqliteRow, toSyncRulesRow } from '@powersync/service-sync-rules';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* pgwire message -> SQLite row.
|
|
@@ -10,7 +10,7 @@ import { SqliteRow, toSyncRulesRow } from '@powersync/service-sync-rules';
|
|
|
10
10
|
export function constructAfterRecord(message: pgwire.PgoutputInsert | pgwire.PgoutputUpdate): SqliteRow {
|
|
11
11
|
const rawData = (message as any).afterRaw;
|
|
12
12
|
|
|
13
|
-
const record =
|
|
13
|
+
const record = decodeTuple(message.relation, rawData);
|
|
14
14
|
return toSyncRulesRow(record);
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -23,6 +23,24 @@ export function constructBeforeRecord(message: pgwire.PgoutputDelete | pgwire.Pg
|
|
|
23
23
|
if (rawData == null) {
|
|
24
24
|
return undefined;
|
|
25
25
|
}
|
|
26
|
-
const record =
|
|
26
|
+
const record = decodeTuple(message.relation, rawData);
|
|
27
27
|
return toSyncRulesRow(record);
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* We need a high level of control over how values are decoded, to make sure there is no loss
|
|
32
|
+
* of precision in the process.
|
|
33
|
+
*/
|
|
34
|
+
export function decodeTuple(relation: pgwire.PgoutputRelation, tupleRaw: Record<string, any>): DatabaseInputRow {
|
|
35
|
+
let result: Record<string, any> = {};
|
|
36
|
+
for (let columnName in tupleRaw) {
|
|
37
|
+
const rawval = tupleRaw[columnName];
|
|
38
|
+
const typeOid = (relation as any)._tupleDecoder._typeOids.get(columnName);
|
|
39
|
+
if (typeof rawval == 'string' && typeOid) {
|
|
40
|
+
result[columnName] = pgwire.PgType.decode(rawval, typeOid);
|
|
41
|
+
} else {
|
|
42
|
+
result[columnName] = rawval;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|