@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/api/PostgresRouteAPIAdapter.js +15 -10
  3. package/dist/api/PostgresRouteAPIAdapter.js.map +1 -1
  4. package/dist/auth/SupabaseKeyCollector.js +2 -2
  5. package/dist/auth/SupabaseKeyCollector.js.map +1 -1
  6. package/dist/module/PostgresModule.d.ts +4 -2
  7. package/dist/module/PostgresModule.js +11 -3
  8. package/dist/module/PostgresModule.js.map +1 -1
  9. package/dist/replication/ConnectionManagerFactory.d.ts +1 -1
  10. package/dist/replication/PgManager.js.map +1 -1
  11. package/dist/replication/PgRelation.js +2 -1
  12. package/dist/replication/PgRelation.js.map +1 -1
  13. package/dist/replication/WalStream.js +9 -7
  14. package/dist/replication/WalStream.js.map +1 -1
  15. package/dist/replication/WalStreamReplicator.d.ts +1 -0
  16. package/dist/replication/WalStreamReplicator.js +4 -0
  17. package/dist/replication/WalStreamReplicator.js.map +1 -1
  18. package/dist/replication/replication-utils.d.ts +1 -1
  19. package/dist/replication/replication-utils.js +14 -16
  20. package/dist/replication/replication-utils.js.map +1 -1
  21. package/dist/types/types.d.ts +55 -52
  22. package/dist/types/types.js +11 -98
  23. package/dist/types/types.js.map +1 -1
  24. package/dist/utils/migration_lib.js +2 -1
  25. package/dist/utils/migration_lib.js.map +1 -1
  26. package/dist/utils/pgwire_utils.d.ts +6 -5
  27. package/dist/utils/pgwire_utils.js +14 -41
  28. package/dist/utils/pgwire_utils.js.map +1 -1
  29. package/package.json +10 -8
  30. package/src/api/PostgresRouteAPIAdapter.ts +15 -11
  31. package/src/auth/SupabaseKeyCollector.ts +2 -2
  32. package/src/module/PostgresModule.ts +22 -5
  33. package/src/replication/ConnectionManagerFactory.ts +1 -1
  34. package/src/replication/PgManager.ts +1 -0
  35. package/src/replication/PgRelation.ts +2 -1
  36. package/src/replication/WalStream.ts +16 -7
  37. package/src/replication/WalStreamReplicator.ts +5 -0
  38. package/src/replication/replication-utils.ts +20 -17
  39. package/src/types/types.ts +16 -136
  40. package/src/utils/migration_lib.ts +2 -1
  41. package/src/utils/pgwire_utils.ts +15 -42
  42. package/test/src/__snapshots__/schema_changes.test.ts.snap +5 -0
  43. package/test/src/env.ts +4 -1
  44. package/test/src/large_batch.test.ts +17 -2
  45. package/test/src/schema_changes.test.ts +8 -3
  46. package/test/src/setup.ts +5 -1
  47. package/test/src/slow_tests.test.ts +120 -32
  48. package/test/src/util.ts +7 -2
  49. package/test/src/wal_stream.test.ts +7 -2
  50. package/test/src/wal_stream_utils.ts +1 -0
  51. package/tsconfig.json +3 -0
  52. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,12 @@
1
- import { api, auth, ConfigurationFileSyncRulesProvider, modules, replication, system } from '@powersync/service-core';
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<void> {
136
+ async testConnection(config: PostgresConnectionConfig): Promise<ConnectionTestResult> {
128
137
  this.decodeConfig(config);
129
- const normalisedConfig = this.resolveConfig(this.decodedConfig!);
130
- const connectionManager = new PgManager(normalisedConfig, {
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
- return await checkSourceConfiguration(connection, PUBLICATION_NAME);
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
- private readonly dbConnectionConfig: NormalizedPostgresConnectionConfig;
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 Error(`No relation id!`);
20
+ throw new ReplicationAssertionError(`No relation id found`);
20
21
  }
21
22
  return relId;
22
23
  }
@@ -1,8 +1,16 @@
1
- import { container, errors, logger } from '@powersync/lib-services-framework';
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 = pg_utils.retriedQuery(
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 Error(`missing relid for ${name}`);
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 Error('Unreachable');
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 Error(`Aborted initial replication of ${this.slot_name}`);
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 Error('objectId expected');
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 Error(`Missing relation cache for ${relationId}`);
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 * as pg_utils from '../utils/pgwire_utils.js';
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 pgwire_utils.retriedQuery(db, {
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 pgwire_utils.retriedQuery(db, {
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 pgwire_utils.retriedQuery(db, {
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 pgwire_utils.retriedQuery(
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 pgwire_utils.retriedQuery(db, {
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 Error(
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 Error(
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 Error(`'${publicationName}' uses publish_via_partition_root, which is not supported.`);
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 util.retriedQuery(db, {
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 util.retriedQuery(db, {
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 pg_utils.retriedQuery(db, `SELECT * FROM ${sourceTable.escapedIdentifier} LIMIT 1`);
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 pg_utils.retriedQuery(db, {
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 },
@@ -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
- export const POSTGRES_CONNECTION_TYPE = 'postgresql' as const;
6
-
7
- export interface NormalizedPostgresConnectionConfig {
8
- id: string;
9
- tag: string;
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
- type: t.literal(POSTGRES_CONNECTION_TYPE),
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
- * Construct a postgres URI, without username, password or ssl options.
36
+ * Validate and normalize connection options.
159
37
  *
160
- * Only contains hostname, port, database.
38
+ * Returns destructured options.
161
39
  */
162
- export function baseUri(options: NormalizedPostgresConnectionConfig) {
163
- return `postgresql://${options.hostname}:${options.port}/${options.database}`;
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 Error('Migration ids must be strictly incrementing');
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 { SqliteJsonValue, SqliteRow, toSyncRulesRow } from '@powersync/service-sync-rules';
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 = pgwire.decodeTuple(message.relation, rawData);
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 = pgwire.decodeTuple(message.relation, rawData);
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
- * Retry a simple query - up to 2 attempts total.
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 async function retriedQuery(db: pgwire.PgClient, ...args: any[]) {
63
- for (let tries = 2; ; tries--) {
64
- try {
65
- return await db.query(...args);
66
- } catch (e) {
67
- if (tries == 1) {
68
- throw e;
69
- }
70
- logger.warn('Query error, retrying', e);
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
  }
@@ -0,0 +1,5 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`schema changes - mongodb > add to publication (not in sync rules) 1`] = `0`;
4
+
5
+ exports[`schema changes - postgres > add to publication (not in sync rules) 1`] = `16384`;
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 { INITIALIZED_MONGO_STORAGE_FACTORY, TEST_CONNECTION_OPTIONS } from './util.js';
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 { INITIALIZED_MONGO_STORAGE_FACTORY } from './util.js';
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).toEqual(0);
440
+ expect(metrics.replication_size_bytes).toMatchSnapshot();
436
441
  });
437
442
 
438
443
  test('replica identity nothing', async () => {