@powersync/service-module-postgres 0.3.0 → 0.5.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/CHANGELOG.md +38 -0
- package/dist/api/PostgresRouteAPIAdapter.js +15 -10
- package/dist/api/PostgresRouteAPIAdapter.js.map +1 -1
- package/dist/auth/SupabaseKeyCollector.js +2 -2
- package/dist/auth/SupabaseKeyCollector.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 +9 -7
- 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.d.ts +1 -1
- package/dist/replication/replication-utils.js +14 -16
- package/dist/replication/replication-utils.js.map +1 -1
- package/dist/types/types.d.ts +55 -52
- package/dist/types/types.js +11 -98
- package/dist/types/types.js.map +1 -1
- 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 -5
- package/dist/utils/pgwire_utils.js +14 -41
- package/dist/utils/pgwire_utils.js.map +1 -1
- package/package.json +10 -8
- package/src/api/PostgresRouteAPIAdapter.ts +15 -11
- package/src/auth/SupabaseKeyCollector.ts +2 -2
- 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 +16 -7
- package/src/replication/WalStreamReplicator.ts +5 -0
- package/src/replication/replication-utils.ts +20 -17
- package/src/types/types.ts +16 -136
- package/src/utils/migration_lib.ts +2 -1
- package/src/utils/pgwire_utils.ts +15 -42
- package/test/src/__snapshots__/schema_changes.test.ts.snap +5 -0
- package/test/src/env.ts +4 -1
- package/test/src/large_batch.test.ts +17 -2
- package/test/src/schema_changes.test.ts +8 -3
- package/test/src/setup.ts +5 -1
- package/test/src/slow_tests.test.ts +120 -32
- package/test/src/util.ts +7 -2
- package/test/src/wal_stream.test.ts +7 -2
- package/test/src/wal_stream_utils.ts +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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,8 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
2
|
+
import {
|
|
3
|
+
container,
|
|
4
|
+
errors,
|
|
5
|
+
logger,
|
|
6
|
+
ReplicationAbortedError,
|
|
7
|
+
ReplicationAssertionError
|
|
8
|
+
} from '@powersync/lib-services-framework';
|
|
2
9
|
import { getUuidReplicaIdentityBson, Metrics, SourceEntityDescriptor, storage } from '@powersync/service-core';
|
|
3
10
|
import * as pgwire from '@powersync/service-jpgwire';
|
|
4
11
|
import { DatabaseInputRow, SqliteRow, SqlSyncRules, TablePattern, toSyncRulesRow } from '@powersync/service-sync-rules';
|
|
5
12
|
import * as pg_utils from '../utils/pgwire_utils.js';
|
|
13
|
+
|
|
6
14
|
import { PgManager } from './PgManager.js';
|
|
7
15
|
import { getPgOutputRelation, getRelId } from './PgRelation.js';
|
|
8
16
|
import { checkSourceConfiguration, getReplicationIdentityColumns } from './replication-utils.js';
|
|
@@ -63,7 +71,7 @@ export class WalStream {
|
|
|
63
71
|
// Ping to speed up cancellation of streaming replication
|
|
64
72
|
// We're not using pg_snapshot here, since it could be in the middle of
|
|
65
73
|
// an initial replication transaction.
|
|
66
|
-
const promise =
|
|
74
|
+
const promise = lib_postgres.retriedQuery(
|
|
67
75
|
this.connections.pool,
|
|
68
76
|
`SELECT * FROM pg_logical_emit_message(false, 'powersync', 'ping')`
|
|
69
77
|
);
|
|
@@ -131,7 +139,7 @@ export class WalStream {
|
|
|
131
139
|
for (let row of tableRows) {
|
|
132
140
|
const name = row.table_name as string;
|
|
133
141
|
if (typeof row.relid != 'bigint') {
|
|
134
|
-
throw new
|
|
142
|
+
throw new ReplicationAssertionError(`Missing relid for ${name}`);
|
|
135
143
|
}
|
|
136
144
|
const relid = Number(row.relid as bigint);
|
|
137
145
|
|
|
@@ -292,7 +300,7 @@ export class WalStream {
|
|
|
292
300
|
}
|
|
293
301
|
}
|
|
294
302
|
|
|
295
|
-
throw new
|
|
303
|
+
throw new ReplicationAssertionError('Unreachable');
|
|
296
304
|
}
|
|
297
305
|
|
|
298
306
|
async estimatedCount(db: pgwire.PgConnection, table: storage.SourceTable): Promise<string> {
|
|
@@ -413,7 +421,7 @@ WHERE oid = $1::regclass`,
|
|
|
413
421
|
lastLogIndex = at;
|
|
414
422
|
}
|
|
415
423
|
if (this.abort_signal.aborted) {
|
|
416
|
-
throw new
|
|
424
|
+
throw new ReplicationAbortedError(`Aborted initial replication of ${this.slot_name}`);
|
|
417
425
|
}
|
|
418
426
|
|
|
419
427
|
for (const record of WalStream.getQueryData(rows)) {
|
|
@@ -439,7 +447,7 @@ WHERE oid = $1::regclass`,
|
|
|
439
447
|
|
|
440
448
|
async handleRelation(batch: storage.BucketStorageBatch, descriptor: SourceEntityDescriptor, snapshot: boolean) {
|
|
441
449
|
if (!descriptor.objectId && typeof descriptor.objectId != 'number') {
|
|
442
|
-
throw new
|
|
450
|
+
throw new ReplicationAssertionError(`objectId expected, got ${typeof descriptor.objectId}`);
|
|
443
451
|
}
|
|
444
452
|
const result = await this.storage.resolveTable({
|
|
445
453
|
group_id: this.group_id,
|
|
@@ -482,6 +490,7 @@ WHERE oid = $1::regclass`,
|
|
|
482
490
|
await db.query('COMMIT');
|
|
483
491
|
} catch (e) {
|
|
484
492
|
await db.query('ROLLBACK');
|
|
493
|
+
// TODO: Wrap with custom error type
|
|
485
494
|
throw e;
|
|
486
495
|
}
|
|
487
496
|
} finally {
|
|
@@ -499,7 +508,7 @@ WHERE oid = $1::regclass`,
|
|
|
499
508
|
if (table == null) {
|
|
500
509
|
// We should always receive a replication message before the relation is used.
|
|
501
510
|
// If we can't find it, it's a bug.
|
|
502
|
-
throw new
|
|
511
|
+
throw new ReplicationAssertionError(`Missing relation cache for ${relationId}`);
|
|
503
512
|
}
|
|
504
513
|
return table;
|
|
505
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,13 +1,11 @@
|
|
|
1
1
|
import * as pgwire from '@powersync/service-jpgwire';
|
|
2
2
|
|
|
3
|
+
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
4
|
+
import { ErrorCode, logger, ServiceError } from '@powersync/lib-services-framework';
|
|
3
5
|
import { PatternResult, storage } from '@powersync/service-core';
|
|
4
|
-
import * as pgwire_utils from '../utils/pgwire_utils.js';
|
|
5
|
-
import { ReplicationIdentity } from './PgRelation.js';
|
|
6
6
|
import * as sync_rules from '@powersync/service-sync-rules';
|
|
7
7
|
import * as service_types from '@powersync/service-types';
|
|
8
|
-
import
|
|
9
|
-
import * as util from '../utils/pgwire_utils.js';
|
|
10
|
-
import { logger } from '@powersync/lib-services-framework';
|
|
8
|
+
import { ReplicationIdentity } from './PgRelation.js';
|
|
11
9
|
|
|
12
10
|
export interface ReplicaIdentityResult {
|
|
13
11
|
replicationColumns: storage.ColumnDescriptor[];
|
|
@@ -20,7 +18,7 @@ export async function getPrimaryKeyColumns(
|
|
|
20
18
|
mode: 'primary' | 'replident'
|
|
21
19
|
): Promise<storage.ColumnDescriptor[]> {
|
|
22
20
|
const indexFlag = mode == 'primary' ? `i.indisprimary` : `i.indisreplident`;
|
|
23
|
-
const attrRows = await
|
|
21
|
+
const attrRows = await lib_postgres.retriedQuery(db, {
|
|
24
22
|
statement: `SELECT a.attname as name, a.atttypid as typeid, t.typname as type, a.attnum as attnum
|
|
25
23
|
FROM pg_index i
|
|
26
24
|
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY (i.indkey)
|
|
@@ -41,7 +39,7 @@ export async function getPrimaryKeyColumns(
|
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
export async function getAllColumns(db: pgwire.PgClient, relationId: number): Promise<storage.ColumnDescriptor[]> {
|
|
44
|
-
const attrRows = await
|
|
42
|
+
const attrRows = await lib_postgres.retriedQuery(db, {
|
|
45
43
|
statement: `SELECT a.attname as name, a.atttypid as typeid, t.typname as type, a.attnum as attnum
|
|
46
44
|
FROM pg_attribute a
|
|
47
45
|
JOIN pg_type t ON a.atttypid = t.oid
|
|
@@ -62,7 +60,7 @@ export async function getReplicationIdentityColumns(
|
|
|
62
60
|
db: pgwire.PgClient,
|
|
63
61
|
relationId: number
|
|
64
62
|
): Promise<ReplicaIdentityResult> {
|
|
65
|
-
const rows = await
|
|
63
|
+
const rows = await lib_postgres.retriedQuery(db, {
|
|
66
64
|
statement: `SELECT CASE relreplident
|
|
67
65
|
WHEN 'd' THEN 'default'
|
|
68
66
|
WHEN 'n' THEN 'nothing'
|
|
@@ -95,7 +93,7 @@ WHERE oid = $1::oid LIMIT 1`,
|
|
|
95
93
|
|
|
96
94
|
export async function checkSourceConfiguration(db: pgwire.PgClient, publicationName: string): Promise<void> {
|
|
97
95
|
// Check basic config
|
|
98
|
-
await
|
|
96
|
+
await lib_postgres.retriedQuery(
|
|
99
97
|
db,
|
|
100
98
|
`DO $$
|
|
101
99
|
BEGIN
|
|
@@ -113,23 +111,28 @@ $$ LANGUAGE plpgsql;`
|
|
|
113
111
|
);
|
|
114
112
|
|
|
115
113
|
// Check that publication exists
|
|
116
|
-
const rs = await
|
|
114
|
+
const rs = await lib_postgres.retriedQuery(db, {
|
|
117
115
|
statement: `SELECT * FROM pg_publication WHERE pubname = $1`,
|
|
118
116
|
params: [{ type: 'varchar', value: publicationName }]
|
|
119
117
|
});
|
|
120
118
|
const row = pgwire.pgwireRows(rs)[0];
|
|
121
119
|
if (row == null) {
|
|
122
|
-
throw new
|
|
120
|
+
throw new ServiceError(
|
|
121
|
+
ErrorCode.PSYNC_S1141,
|
|
123
122
|
`Publication '${publicationName}' does not exist. Run: \`CREATE PUBLICATION ${publicationName} FOR ALL TABLES\`, or read the documentation for details.`
|
|
124
123
|
);
|
|
125
124
|
}
|
|
126
125
|
if (row.pubinsert == false || row.pubupdate == false || row.pubdelete == false || row.pubtruncate == false) {
|
|
127
|
-
throw new
|
|
126
|
+
throw new ServiceError(
|
|
127
|
+
ErrorCode.PSYNC_S1142,
|
|
128
128
|
`Publication '${publicationName}' does not publish all changes. Create a publication using \`WITH (publish = "insert, update, delete, truncate")\` (the default).`
|
|
129
129
|
);
|
|
130
130
|
}
|
|
131
131
|
if (row.pubviaroot) {
|
|
132
|
-
throw new
|
|
132
|
+
throw new ServiceError(
|
|
133
|
+
ErrorCode.PSYNC_S1143,
|
|
134
|
+
`'${publicationName}' uses publish_via_partition_root, which is not supported.`
|
|
135
|
+
);
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
138
|
|
|
@@ -158,7 +161,7 @@ export async function getDebugTablesInfo(options: GetDebugTablesInfoOptions): Pr
|
|
|
158
161
|
if (tablePattern.isWildcard) {
|
|
159
162
|
patternResult.tables = [];
|
|
160
163
|
const prefix = tablePattern.tablePrefix;
|
|
161
|
-
const results = await
|
|
164
|
+
const results = await lib_postgres.retriedQuery(db, {
|
|
162
165
|
statement: `SELECT c.oid AS relid, c.relname AS table_name
|
|
163
166
|
FROM pg_class c
|
|
164
167
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
@@ -189,7 +192,7 @@ export async function getDebugTablesInfo(options: GetDebugTablesInfoOptions): Pr
|
|
|
189
192
|
patternResult.tables.push(details);
|
|
190
193
|
}
|
|
191
194
|
} else {
|
|
192
|
-
const results = await
|
|
195
|
+
const results = await lib_postgres.retriedQuery(db, {
|
|
193
196
|
statement: `SELECT c.oid AS relid, c.relname AS table_name
|
|
194
197
|
FROM pg_class c
|
|
195
198
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
@@ -284,14 +287,14 @@ export async function getDebugTableInfo(options: GetDebugTableInfoOptions): Prom
|
|
|
284
287
|
|
|
285
288
|
let selectError = null;
|
|
286
289
|
try {
|
|
287
|
-
await
|
|
290
|
+
await lib_postgres.retriedQuery(db, `SELECT * FROM ${sourceTable.escapedIdentifier} LIMIT 1`);
|
|
288
291
|
} catch (e) {
|
|
289
292
|
selectError = { level: 'fatal', message: e.message };
|
|
290
293
|
}
|
|
291
294
|
|
|
292
295
|
let replicateError = null;
|
|
293
296
|
|
|
294
|
-
const publications = await
|
|
297
|
+
const publications = await lib_postgres.retriedQuery(db, {
|
|
295
298
|
statement: `SELECT tablename FROM pg_publication_tables WHERE pubname = $1 AND schemaname = $2 AND tablename = $3`,
|
|
296
299
|
params: [
|
|
297
300
|
{ type: 'varchar', value: publicationName },
|
package/src/types/types.ts
CHANGED
|
@@ -1,56 +1,18 @@
|
|
|
1
|
+
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
1
2
|
import * as service_types from '@powersync/service-types';
|
|
2
3
|
import * as t from 'ts-codec';
|
|
3
|
-
import * as urijs from 'uri-js';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
hostname: string;
|
|
12
|
-
port: number;
|
|
13
|
-
database: string;
|
|
14
|
-
|
|
15
|
-
username: string;
|
|
16
|
-
password: string;
|
|
17
|
-
|
|
18
|
-
sslmode: 'verify-full' | 'verify-ca' | 'disable';
|
|
19
|
-
cacert: string | undefined;
|
|
20
|
-
|
|
21
|
-
client_certificate: string | undefined;
|
|
22
|
-
client_private_key: string | undefined;
|
|
23
|
-
}
|
|
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;
|
|
24
10
|
|
|
25
11
|
export const PostgresConnectionConfig = service_types.configFile.DataSourceConfig.and(
|
|
12
|
+
lib_postgres.BasePostgresConnectionConfig
|
|
13
|
+
).and(
|
|
26
14
|
t.object({
|
|
27
|
-
|
|
28
|
-
/** Unique identifier for the connection - optional when a single connection is present. */
|
|
29
|
-
id: t.string.optional(),
|
|
30
|
-
/** Tag used as reference in sync rules. Defaults to "default". Does not have to be unique. */
|
|
31
|
-
tag: t.string.optional(),
|
|
32
|
-
uri: t.string.optional(),
|
|
33
|
-
hostname: t.string.optional(),
|
|
34
|
-
port: service_types.configFile.portCodec.optional(),
|
|
35
|
-
username: t.string.optional(),
|
|
36
|
-
password: t.string.optional(),
|
|
37
|
-
database: t.string.optional(),
|
|
38
|
-
|
|
39
|
-
/** Defaults to verify-full */
|
|
40
|
-
sslmode: t.literal('verify-full').or(t.literal('verify-ca')).or(t.literal('disable')).optional(),
|
|
41
|
-
/** Required for verify-ca, optional for verify-full */
|
|
42
|
-
cacert: t.string.optional(),
|
|
43
|
-
|
|
44
|
-
client_certificate: t.string.optional(),
|
|
45
|
-
client_private_key: t.string.optional(),
|
|
46
|
-
|
|
47
|
-
/** Expose database credentials */
|
|
48
|
-
demo_database: t.boolean.optional(),
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Prefix for the slot name. Defaults to "powersync_"
|
|
52
|
-
*/
|
|
53
|
-
slot_name_prefix: t.string.optional()
|
|
15
|
+
// Add any replication connection specific config here in future
|
|
54
16
|
})
|
|
55
17
|
);
|
|
56
18
|
|
|
@@ -64,101 +26,19 @@ export type PostgresConnectionConfig = t.Decoded<typeof PostgresConnectionConfig
|
|
|
64
26
|
*/
|
|
65
27
|
export type ResolvedConnectionConfig = PostgresConnectionConfig & NormalizedPostgresConnectionConfig;
|
|
66
28
|
|
|
67
|
-
/**
|
|
68
|
-
* Validate and normalize connection options.
|
|
69
|
-
*
|
|
70
|
-
* Returns destructured options.
|
|
71
|
-
*/
|
|
72
|
-
export function normalizeConnectionConfig(options: PostgresConnectionConfig): NormalizedPostgresConnectionConfig {
|
|
73
|
-
let uri: urijs.URIComponents;
|
|
74
|
-
if (options.uri) {
|
|
75
|
-
uri = urijs.parse(options.uri);
|
|
76
|
-
if (uri.scheme != 'postgresql' && uri.scheme != 'postgres') {
|
|
77
|
-
`Invalid URI - protocol must be postgresql, got ${uri.scheme}`;
|
|
78
|
-
} else if (uri.scheme != 'postgresql') {
|
|
79
|
-
uri.scheme = 'postgresql';
|
|
80
|
-
}
|
|
81
|
-
} else {
|
|
82
|
-
uri = urijs.parse('postgresql:///');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const hostname = options.hostname ?? uri.host ?? '';
|
|
86
|
-
const port = validatePort(options.port ?? uri.port ?? 5432);
|
|
87
|
-
|
|
88
|
-
const database = options.database ?? uri.path?.substring(1) ?? '';
|
|
89
|
-
|
|
90
|
-
const [uri_username, uri_password] = (uri.userinfo ?? '').split(':');
|
|
91
|
-
|
|
92
|
-
const username = options.username ?? uri_username ?? '';
|
|
93
|
-
const password = options.password ?? uri_password ?? '';
|
|
94
|
-
|
|
95
|
-
const sslmode = options.sslmode ?? 'verify-full'; // Configuration not supported via URI
|
|
96
|
-
const cacert = options.cacert;
|
|
97
|
-
|
|
98
|
-
if (sslmode == 'verify-ca' && cacert == null) {
|
|
99
|
-
throw new Error('Explicit cacert is required for sslmode=verify-ca');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (hostname == '') {
|
|
103
|
-
throw new Error(`hostname required`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (username == '') {
|
|
107
|
-
throw new Error(`username required`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (password == '') {
|
|
111
|
-
throw new Error(`password required`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (database == '') {
|
|
115
|
-
throw new Error(`database required`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
id: options.id ?? 'default',
|
|
120
|
-
tag: options.tag ?? 'default',
|
|
121
|
-
|
|
122
|
-
hostname,
|
|
123
|
-
port,
|
|
124
|
-
database,
|
|
125
|
-
|
|
126
|
-
username,
|
|
127
|
-
password,
|
|
128
|
-
sslmode,
|
|
129
|
-
cacert,
|
|
130
|
-
|
|
131
|
-
client_certificate: options.client_certificate ?? undefined,
|
|
132
|
-
client_private_key: options.client_private_key ?? undefined
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
29
|
export function isPostgresConfig(
|
|
137
30
|
config: service_types.configFile.DataSourceConfig
|
|
138
31
|
): config is PostgresConnectionConfig {
|
|
139
|
-
return config.type == POSTGRES_CONNECTION_TYPE;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Check whether the port is in a "safe" range.
|
|
144
|
-
*
|
|
145
|
-
* We do not support connecting to "privileged" ports.
|
|
146
|
-
*/
|
|
147
|
-
export function validatePort(port: string | number): number {
|
|
148
|
-
if (typeof port == 'string') {
|
|
149
|
-
port = parseInt(port);
|
|
150
|
-
}
|
|
151
|
-
if (port < 1024) {
|
|
152
|
-
throw new Error(`Port ${port} not supported`);
|
|
153
|
-
}
|
|
154
|
-
return port;
|
|
32
|
+
return config.type == lib_postgres.POSTGRES_CONNECTION_TYPE;
|
|
155
33
|
}
|
|
156
34
|
|
|
157
35
|
/**
|
|
158
|
-
*
|
|
36
|
+
* Validate and normalize connection options.
|
|
159
37
|
*
|
|
160
|
-
*
|
|
38
|
+
* Returns destructured options.
|
|
161
39
|
*/
|
|
162
|
-
export function
|
|
163
|
-
return
|
|
40
|
+
export function normalizeConnectionConfig(options: PostgresConnectionConfig) {
|
|
41
|
+
return {
|
|
42
|
+
...lib_postgres.normalizeConnectionConfig(options)
|
|
43
|
+
} satisfies NormalizedPostgresConnectionConfig;
|
|
164
44
|
}
|
|
@@ -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,9 +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 {
|
|
5
|
-
|
|
6
|
-
import { logger } from '@powersync/lib-services-framework';
|
|
4
|
+
import { DatabaseInputRow, SqliteRow, toSyncRulesRow } from '@powersync/service-sync-rules';
|
|
7
5
|
|
|
8
6
|
/**
|
|
9
7
|
* pgwire message -> SQLite row.
|
|
@@ -12,7 +10,7 @@ import { logger } from '@powersync/lib-services-framework';
|
|
|
12
10
|
export function constructAfterRecord(message: pgwire.PgoutputInsert | pgwire.PgoutputUpdate): SqliteRow {
|
|
13
11
|
const rawData = (message as any).afterRaw;
|
|
14
12
|
|
|
15
|
-
const record =
|
|
13
|
+
const record = decodeTuple(message.relation, rawData);
|
|
16
14
|
return toSyncRulesRow(record);
|
|
17
15
|
}
|
|
18
16
|
|
|
@@ -25,49 +23,24 @@ export function constructBeforeRecord(message: pgwire.PgoutputDelete | pgwire.Pg
|
|
|
25
23
|
if (rawData == null) {
|
|
26
24
|
return undefined;
|
|
27
25
|
}
|
|
28
|
-
const record =
|
|
26
|
+
const record = decodeTuple(message.relation, rawData);
|
|
29
27
|
return toSyncRulesRow(record);
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
export function escapeIdentifier(identifier: string) {
|
|
33
|
-
return `"${identifier.replace(/"/g, '""').replace(/\./g, '"."')}"`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function autoParameter(arg: SqliteJsonValue | boolean): pgwire.StatementParam {
|
|
37
|
-
if (arg == null) {
|
|
38
|
-
return { type: 'varchar', value: null };
|
|
39
|
-
} else if (typeof arg == 'string') {
|
|
40
|
-
return { type: 'varchar', value: arg };
|
|
41
|
-
} else if (typeof arg == 'number') {
|
|
42
|
-
if (Number.isInteger(arg)) {
|
|
43
|
-
return { type: 'int8', value: arg };
|
|
44
|
-
} else {
|
|
45
|
-
return { type: 'float8', value: arg };
|
|
46
|
-
}
|
|
47
|
-
} else if (typeof arg == 'boolean') {
|
|
48
|
-
return { type: 'bool', value: arg };
|
|
49
|
-
} else if (typeof arg == 'bigint') {
|
|
50
|
-
return { type: 'int8', value: arg };
|
|
51
|
-
} else {
|
|
52
|
-
throw new Error(`Unsupported query parameter: ${typeof arg}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function retriedQuery(db: pgwire.PgClient, ...statements: pgwire.Statement[]): Promise<pgwire.PgResult>;
|
|
57
|
-
export async function retriedQuery(db: pgwire.PgClient, query: string): Promise<pgwire.PgResult>;
|
|
58
|
-
|
|
59
30
|
/**
|
|
60
|
-
*
|
|
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.
|
|
61
33
|
*/
|
|
62
|
-
export
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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;
|
|
71
43
|
}
|
|
72
44
|
}
|
|
45
|
+
return result;
|
|
73
46
|
}
|
package/test/src/env.ts
CHANGED
|
@@ -2,7 +2,10 @@ import { utils } from '@powersync/lib-services-framework';
|
|
|
2
2
|
|
|
3
3
|
export const env = utils.collectEnvironmentVariables({
|
|
4
4
|
PG_TEST_URL: utils.type.string.default('postgres://postgres:postgres@localhost:5432/powersync_test'),
|
|
5
|
+
PG_STORAGE_TEST_URL: utils.type.string.default('postgres://postgres:postgres@localhost:5431/powersync_storage_test'),
|
|
5
6
|
MONGO_TEST_URL: utils.type.string.default('mongodb://localhost:27017/powersync_test'),
|
|
6
7
|
CI: utils.type.boolean.default('false'),
|
|
7
|
-
SLOW_TESTS: utils.type.boolean.default('false')
|
|
8
|
+
SLOW_TESTS: utils.type.boolean.default('false'),
|
|
9
|
+
TEST_MONGO_STORAGE: utils.type.boolean.default('true'),
|
|
10
|
+
TEST_POSTGRES_STORAGE: utils.type.boolean.default('true')
|
|
8
11
|
});
|
|
@@ -3,10 +3,14 @@ import * as timers from 'timers/promises';
|
|
|
3
3
|
import { describe, expect, test } from 'vitest';
|
|
4
4
|
import { populateData } from '../../dist/utils/populate_test_data.js';
|
|
5
5
|
import { env } from './env.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
INITIALIZED_MONGO_STORAGE_FACTORY,
|
|
8
|
+
INITIALIZED_POSTGRES_STORAGE_FACTORY,
|
|
9
|
+
TEST_CONNECTION_OPTIONS
|
|
10
|
+
} from './util.js';
|
|
7
11
|
import { WalStreamTestContext } from './wal_stream_utils.js';
|
|
8
12
|
|
|
9
|
-
describe('batch replication tests - mongodb', { timeout: 120_000 }, function () {
|
|
13
|
+
describe.skipIf(!env.TEST_MONGO_STORAGE)('batch replication tests - mongodb', { timeout: 120_000 }, function () {
|
|
10
14
|
// These are slow but consistent tests.
|
|
11
15
|
// Not run on every test run, but we do run on CI, or when manually debugging issues.
|
|
12
16
|
if (env.CI || env.SLOW_TESTS) {
|
|
@@ -17,6 +21,17 @@ describe('batch replication tests - mongodb', { timeout: 120_000 }, function ()
|
|
|
17
21
|
}
|
|
18
22
|
});
|
|
19
23
|
|
|
24
|
+
describe.skipIf(!env.TEST_POSTGRES_STORAGE)('batch replication tests - postgres', { timeout: 240_000 }, function () {
|
|
25
|
+
// These are slow but consistent tests.
|
|
26
|
+
// Not run on every test run, but we do run on CI, or when manually debugging issues.
|
|
27
|
+
if (env.CI || env.SLOW_TESTS) {
|
|
28
|
+
defineBatchTests(INITIALIZED_POSTGRES_STORAGE_FACTORY);
|
|
29
|
+
} else {
|
|
30
|
+
// Need something in this file.
|
|
31
|
+
test('no-op', () => {});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
20
35
|
const BASIC_SYNC_RULES = `bucket_definitions:
|
|
21
36
|
global:
|
|
22
37
|
data:
|
|
@@ -3,13 +3,18 @@ import * as timers from 'timers/promises';
|
|
|
3
3
|
import { describe, expect, test } from 'vitest';
|
|
4
4
|
|
|
5
5
|
import { storage } from '@powersync/service-core';
|
|
6
|
-
import {
|
|
6
|
+
import { env } from './env.js';
|
|
7
|
+
import { INITIALIZED_MONGO_STORAGE_FACTORY, INITIALIZED_POSTGRES_STORAGE_FACTORY } from './util.js';
|
|
7
8
|
import { WalStreamTestContext } from './wal_stream_utils.js';
|
|
8
9
|
|
|
9
|
-
describe('schema changes', { timeout: 20_000 }, function () {
|
|
10
|
+
describe.skipIf(!env.TEST_MONGO_STORAGE)('schema changes - mongodb', { timeout: 20_000 }, function () {
|
|
10
11
|
defineTests(INITIALIZED_MONGO_STORAGE_FACTORY);
|
|
11
12
|
});
|
|
12
13
|
|
|
14
|
+
describe.skipIf(!env.TEST_POSTGRES_STORAGE)('schema changes - postgres', { timeout: 20_000 }, function () {
|
|
15
|
+
defineTests(INITIALIZED_POSTGRES_STORAGE_FACTORY);
|
|
16
|
+
});
|
|
17
|
+
|
|
13
18
|
const BASIC_SYNC_RULES = `
|
|
14
19
|
bucket_definitions:
|
|
15
20
|
global:
|
|
@@ -432,7 +437,7 @@ function defineTests(factory: storage.TestStorageFactory) {
|
|
|
432
437
|
expect(data).toMatchObject([]);
|
|
433
438
|
|
|
434
439
|
const metrics = await storage.factory.getStorageMetrics();
|
|
435
|
-
expect(metrics.replication_size_bytes).
|
|
440
|
+
expect(metrics.replication_size_bytes).toMatchSnapshot();
|
|
436
441
|
});
|
|
437
442
|
|
|
438
443
|
test('replica identity nothing', async () => {
|