@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
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
2
|
-
import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
|
|
3
|
-
import { api, ParseSyncRulesOptions, ReplicationHeadCallback } from '@powersync/service-core';
|
|
4
|
-
import * as pgwire from '@powersync/service-jpgwire';
|
|
5
|
-
import * as sync_rules from '@powersync/service-sync-rules';
|
|
6
|
-
import * as service_types from '@powersync/service-types';
|
|
7
|
-
import * as replication_utils from '../replication/replication-utils.js';
|
|
8
|
-
import { getDebugTableInfo } from '../replication/replication-utils.js';
|
|
9
|
-
import { KEEPALIVE_STATEMENT, PUBLICATION_NAME } from '../replication/WalStream.js';
|
|
10
|
-
import * as types from '../types/types.js';
|
|
11
|
-
import { getApplicationName } from '../utils/application-name.js';
|
|
12
|
-
import { CustomTypeRegistry } from '../types/registry.js';
|
|
13
|
-
import { PostgresTypeResolver } from '../types/resolver.js';
|
|
14
|
-
|
|
15
|
-
export class PostgresRouteAPIAdapter implements api.RouteAPI {
|
|
16
|
-
private typeCache: PostgresTypeResolver;
|
|
17
|
-
connectionTag: string;
|
|
18
|
-
// TODO this should probably be configurable one day
|
|
19
|
-
publicationName = PUBLICATION_NAME;
|
|
20
|
-
|
|
21
|
-
static withConfig(config: types.ResolvedConnectionConfig) {
|
|
22
|
-
const pool = pgwire.connectPgWirePool(config, {
|
|
23
|
-
idleTimeout: 30_000,
|
|
24
|
-
applicationName: getApplicationName()
|
|
25
|
-
});
|
|
26
|
-
return new PostgresRouteAPIAdapter(pool, config.tag, config);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @param config - Required for the service; optional for tests.
|
|
31
|
-
*/
|
|
32
|
-
constructor(
|
|
33
|
-
protected pool: pgwire.PgClient,
|
|
34
|
-
connectionTag?: string,
|
|
35
|
-
private config?: types.ResolvedConnectionConfig
|
|
36
|
-
) {
|
|
37
|
-
this.typeCache = new PostgresTypeResolver(pool);
|
|
38
|
-
this.connectionTag = connectionTag ?? sync_rules.DEFAULT_TAG;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
getParseSyncRulesOptions(): ParseSyncRulesOptions {
|
|
42
|
-
return {
|
|
43
|
-
defaultSchema: 'public'
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async shutdown(): Promise<void> {
|
|
48
|
-
await this.pool.end();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async getSourceConfig(): Promise<service_types.configFile.ResolvedDataSourceConfig> {
|
|
52
|
-
return this.config!;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async getConnectionStatus(): Promise<service_types.ConnectionStatusV2> {
|
|
56
|
-
const base = {
|
|
57
|
-
id: this.config?.id ?? '',
|
|
58
|
-
uri: this.config == null ? '' : types.baseUri(this.config)
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
await lib_postgres.retriedQuery(this.pool, `SELECT 'PowerSync connection test'`);
|
|
63
|
-
} catch (e) {
|
|
64
|
-
return {
|
|
65
|
-
...base,
|
|
66
|
-
connected: false,
|
|
67
|
-
errors: [{ level: 'fatal', message: e.message }]
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
await replication_utils.checkSourceConfiguration(this.pool, this.publicationName);
|
|
73
|
-
} catch (e) {
|
|
74
|
-
return {
|
|
75
|
-
...base,
|
|
76
|
-
connected: true,
|
|
77
|
-
errors: [{ level: 'fatal', message: e.message }]
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
...base,
|
|
83
|
-
connected: true,
|
|
84
|
-
errors: []
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async executeQuery(query: string, params: any[]): Promise<service_types.internal_routes.ExecuteSqlResponse> {
|
|
89
|
-
if (!this.config?.debug_api) {
|
|
90
|
-
return service_types.internal_routes.ExecuteSqlResponse.encode({
|
|
91
|
-
results: {
|
|
92
|
-
columns: [],
|
|
93
|
-
rows: []
|
|
94
|
-
},
|
|
95
|
-
success: false,
|
|
96
|
-
error: 'SQL querying is not enabled'
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const result = await this.pool.query({
|
|
102
|
-
statement: query,
|
|
103
|
-
params: params.map(lib_postgres.autoParameter)
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
return service_types.internal_routes.ExecuteSqlResponse.encode({
|
|
107
|
-
success: true,
|
|
108
|
-
results: {
|
|
109
|
-
columns: result.columns.map((c) => c.name),
|
|
110
|
-
rows: result.rows.map((row) => {
|
|
111
|
-
return row.raw.map((raw, i) => {
|
|
112
|
-
const value = pgwire.PgType.decode(raw, row.columns[i].typeOid);
|
|
113
|
-
const sqlValue = sync_rules.applyValueContext(
|
|
114
|
-
sync_rules.toSyncRulesValue(value),
|
|
115
|
-
sync_rules.CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY
|
|
116
|
-
);
|
|
117
|
-
if (typeof sqlValue == 'bigint') {
|
|
118
|
-
return Number(value);
|
|
119
|
-
} else if (sync_rules.isJsonValue(sqlValue)) {
|
|
120
|
-
return sqlValue;
|
|
121
|
-
} else {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
})
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
} catch (e) {
|
|
129
|
-
return service_types.internal_routes.ExecuteSqlResponse.encode({
|
|
130
|
-
results: {
|
|
131
|
-
columns: [],
|
|
132
|
-
rows: []
|
|
133
|
-
},
|
|
134
|
-
success: false,
|
|
135
|
-
error: e.message
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async getDebugTablesInfo(
|
|
141
|
-
tablePatterns: sync_rules.TablePattern[],
|
|
142
|
-
sqlSyncRules: sync_rules.SqlSyncRules
|
|
143
|
-
): Promise<api.PatternResult[]> {
|
|
144
|
-
let result: api.PatternResult[] = [];
|
|
145
|
-
|
|
146
|
-
for (let tablePattern of tablePatterns) {
|
|
147
|
-
const schema = tablePattern.schema;
|
|
148
|
-
|
|
149
|
-
let patternResult: api.PatternResult = {
|
|
150
|
-
schema: schema,
|
|
151
|
-
pattern: tablePattern.tablePattern,
|
|
152
|
-
wildcard: tablePattern.isWildcard
|
|
153
|
-
};
|
|
154
|
-
result.push(patternResult);
|
|
155
|
-
|
|
156
|
-
if (tablePattern.isWildcard) {
|
|
157
|
-
patternResult.tables = [];
|
|
158
|
-
const prefix = tablePattern.tablePrefix;
|
|
159
|
-
const results = await lib_postgres.retriedQuery(this.pool, {
|
|
160
|
-
statement: `SELECT c.oid AS relid, c.relname AS table_name
|
|
161
|
-
FROM pg_class c
|
|
162
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
163
|
-
WHERE n.nspname = $1
|
|
164
|
-
AND c.relkind = 'r'
|
|
165
|
-
AND c.relname LIKE $2`,
|
|
166
|
-
params: [
|
|
167
|
-
{ type: 'varchar', value: schema },
|
|
168
|
-
{ type: 'varchar', value: tablePattern.tablePattern }
|
|
169
|
-
]
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
for (let row of pgwire.pgwireRows(results)) {
|
|
173
|
-
const name = row.table_name as string;
|
|
174
|
-
const relationId = row.relid as number;
|
|
175
|
-
if (!name.startsWith(prefix)) {
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
const details = await this.getDebugTableInfo(tablePattern, name, relationId, sqlSyncRules);
|
|
179
|
-
patternResult.tables.push(details);
|
|
180
|
-
}
|
|
181
|
-
} else {
|
|
182
|
-
const results = await lib_postgres.retriedQuery(this.pool, {
|
|
183
|
-
statement: `SELECT c.oid AS relid, c.relname AS table_name
|
|
184
|
-
FROM pg_class c
|
|
185
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
186
|
-
WHERE n.nspname = $1
|
|
187
|
-
AND c.relkind = 'r'
|
|
188
|
-
AND c.relname = $2`,
|
|
189
|
-
params: [
|
|
190
|
-
{ type: 'varchar', value: schema },
|
|
191
|
-
{ type: 'varchar', value: tablePattern.tablePattern }
|
|
192
|
-
]
|
|
193
|
-
});
|
|
194
|
-
if (results.rows.length == 0) {
|
|
195
|
-
// Table not found
|
|
196
|
-
patternResult.table = await this.getDebugTableInfo(tablePattern, tablePattern.name, null, sqlSyncRules);
|
|
197
|
-
} else {
|
|
198
|
-
const row = pgwire.pgwireRows(results)[0];
|
|
199
|
-
const name = row.table_name as string;
|
|
200
|
-
const relationId = row.relid as number;
|
|
201
|
-
patternResult.table = await this.getDebugTableInfo(tablePattern, name, relationId, sqlSyncRules);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return result;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
protected async getDebugTableInfo(
|
|
209
|
-
tablePattern: sync_rules.TablePattern,
|
|
210
|
-
name: string,
|
|
211
|
-
relationId: number | null,
|
|
212
|
-
syncRules: sync_rules.SqlSyncRules
|
|
213
|
-
): Promise<service_types.TableInfo> {
|
|
214
|
-
return getDebugTableInfo({
|
|
215
|
-
db: this.pool,
|
|
216
|
-
name: name,
|
|
217
|
-
publicationName: this.publicationName,
|
|
218
|
-
connectionTag: this.connectionTag,
|
|
219
|
-
tablePattern: tablePattern,
|
|
220
|
-
relationId: relationId,
|
|
221
|
-
syncRules: syncRules
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
async getReplicationLagBytes(options: api.ReplicationLagOptions): Promise<number | undefined> {
|
|
226
|
-
const { bucketStorage } = options;
|
|
227
|
-
const slotName = bucketStorage.slot_name;
|
|
228
|
-
const results = await lib_postgres.retriedQuery(this.pool, {
|
|
229
|
-
statement: `SELECT
|
|
230
|
-
slot_name,
|
|
231
|
-
confirmed_flush_lsn,
|
|
232
|
-
pg_current_wal_lsn(),
|
|
233
|
-
(pg_current_wal_lsn() - confirmed_flush_lsn) AS lsn_distance
|
|
234
|
-
FROM pg_replication_slots WHERE slot_name = $1 LIMIT 1;`,
|
|
235
|
-
params: [{ type: 'varchar', value: slotName }]
|
|
236
|
-
});
|
|
237
|
-
const [row] = pgwire.pgwireRows(results);
|
|
238
|
-
if (row) {
|
|
239
|
-
return Number(row.lsn_distance);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
throw new ServiceError({
|
|
243
|
-
status: 500,
|
|
244
|
-
code: ErrorCode.PSYNC_S4001,
|
|
245
|
-
description: `Could not determine replication lag for slot ${slotName}`
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async getReplicationHead(): Promise<string> {
|
|
250
|
-
// On most Postgres versions, pg_logical_emit_message() returns the correct LSN.
|
|
251
|
-
// However, on Aurora (Postgres compatible), it can return an entirely different LSN,
|
|
252
|
-
// causing the write checkpoints to never be replicated back to the client.
|
|
253
|
-
// For those, we need to use pg_current_wal_lsn() instead.
|
|
254
|
-
const { results } = await lib_postgres.retriedQuery(this.pool, `SELECT pg_current_wal_lsn() as lsn`);
|
|
255
|
-
|
|
256
|
-
const lsn = results[0].rows[0].decodeWithoutCustomTypes(0);
|
|
257
|
-
return String(lsn);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async createReplicationHead<T>(callback: ReplicationHeadCallback<T>): Promise<T> {
|
|
261
|
-
const currentLsn = await this.getReplicationHead();
|
|
262
|
-
|
|
263
|
-
const r = await callback(currentLsn);
|
|
264
|
-
|
|
265
|
-
// Note: This may not reliably trigger a new replication message on Postgres 11 or 12,
|
|
266
|
-
// in which case there could be a delay in the client receiving the write checkpoint acknowledgement.
|
|
267
|
-
// Postgres 12 already reached EOL, and this is not a critical issue, so we're not fixing it.
|
|
268
|
-
// On postgres 13+, this works reliably.
|
|
269
|
-
await lib_postgres.retriedQuery(this.pool, KEEPALIVE_STATEMENT);
|
|
270
|
-
|
|
271
|
-
return r;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async getConnectionSchema(): Promise<service_types.DatabaseSchema[]> {
|
|
275
|
-
// https://github.com/Borvik/vscode-postgres/blob/88ec5ed061a0c9bced6c5d4ec122d0759c3f3247/src/language/server.ts
|
|
276
|
-
const results = await lib_postgres.retriedQuery(
|
|
277
|
-
this.pool,
|
|
278
|
-
`SELECT
|
|
279
|
-
tbl.schemaname,
|
|
280
|
-
tbl.tablename,
|
|
281
|
-
tbl.quoted_name,
|
|
282
|
-
json_agg(a ORDER BY attnum) as columns
|
|
283
|
-
FROM
|
|
284
|
-
(
|
|
285
|
-
SELECT
|
|
286
|
-
n.nspname as schemaname,
|
|
287
|
-
c.relname as tablename,
|
|
288
|
-
(quote_ident(n.nspname) || '.' || quote_ident(c.relname)) as quoted_name
|
|
289
|
-
FROM
|
|
290
|
-
pg_catalog.pg_class c
|
|
291
|
-
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
292
|
-
WHERE
|
|
293
|
-
c.relkind = 'r'
|
|
294
|
-
AND n.nspname not in ('information_schema', 'pg_catalog', 'pg_toast')
|
|
295
|
-
AND n.nspname not like 'pg_temp_%'
|
|
296
|
-
AND n.nspname not like 'pg_toast_temp_%'
|
|
297
|
-
AND c.relnatts > 0
|
|
298
|
-
AND has_schema_privilege(n.oid, 'USAGE') = true
|
|
299
|
-
AND has_table_privilege(quote_ident(n.nspname) || '.' || quote_ident(c.relname), 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') = true
|
|
300
|
-
) as tbl
|
|
301
|
-
LEFT JOIN (
|
|
302
|
-
SELECT
|
|
303
|
-
attrelid,
|
|
304
|
-
attname,
|
|
305
|
-
atttypid,
|
|
306
|
-
format_type(atttypid, atttypmod) as data_type,
|
|
307
|
-
(SELECT typname FROM pg_catalog.pg_type WHERE oid = atttypid) as pg_type,
|
|
308
|
-
attnum,
|
|
309
|
-
attisdropped
|
|
310
|
-
FROM
|
|
311
|
-
pg_attribute
|
|
312
|
-
) as a ON (
|
|
313
|
-
a.attrelid = tbl.quoted_name::regclass
|
|
314
|
-
AND a.attnum > 0
|
|
315
|
-
AND NOT a.attisdropped
|
|
316
|
-
AND has_column_privilege(tbl.quoted_name, a.attname, 'SELECT, INSERT, UPDATE, REFERENCES')
|
|
317
|
-
)
|
|
318
|
-
GROUP BY schemaname, tablename, quoted_name`
|
|
319
|
-
);
|
|
320
|
-
await this.typeCache.fetchTypesForSchema();
|
|
321
|
-
const rows = pgwire.pgwireRows(results);
|
|
322
|
-
|
|
323
|
-
let schemas: Record<string, service_types.DatabaseSchema> = {};
|
|
324
|
-
|
|
325
|
-
for (let row of rows) {
|
|
326
|
-
const schema = (schemas[row.schemaname] ??= {
|
|
327
|
-
name: row.schemaname,
|
|
328
|
-
tables: []
|
|
329
|
-
});
|
|
330
|
-
const table: service_types.TableSchema = {
|
|
331
|
-
name: row.tablename,
|
|
332
|
-
columns: [] as any[]
|
|
333
|
-
};
|
|
334
|
-
schema.tables.push(table);
|
|
335
|
-
|
|
336
|
-
const columnInfo = JSON.parse(row.columns);
|
|
337
|
-
for (let column of columnInfo) {
|
|
338
|
-
let pg_type = column.pg_type as string;
|
|
339
|
-
if (pg_type.startsWith('_')) {
|
|
340
|
-
pg_type = `${pg_type.substring(1)}[]`;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const knownType = this.typeCache.registry.lookupType(Number(column.atttypid));
|
|
344
|
-
table.columns.push({
|
|
345
|
-
name: column.attname,
|
|
346
|
-
sqlite_type: sync_rules.ExpressionType.fromTypeText(knownType.sqliteType()).typeFlags,
|
|
347
|
-
type: column.data_type,
|
|
348
|
-
internal_type: column.data_type,
|
|
349
|
-
pg_type: pg_type
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return Object.values(schemas);
|
|
355
|
-
}
|
|
356
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './module/PostgresModule.js';
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { baseUri, NormalizedBasePostgresConnectionConfig } from '@powersync/lib-service-postgres';
|
|
2
|
-
import {
|
|
3
|
-
api,
|
|
4
|
-
ConfigurationFileSyncRulesProvider,
|
|
5
|
-
ConnectionTestResult,
|
|
6
|
-
modules,
|
|
7
|
-
replication,
|
|
8
|
-
system
|
|
9
|
-
} from '@powersync/service-core';
|
|
10
|
-
import * as jpgwire from '@powersync/service-jpgwire';
|
|
11
|
-
import { ReplicationMetric } from '@powersync/service-types';
|
|
12
|
-
import { PostgresRouteAPIAdapter } from '../api/PostgresRouteAPIAdapter.js';
|
|
13
|
-
import { ConnectionManagerFactory } from '../replication/ConnectionManagerFactory.js';
|
|
14
|
-
import { PgManager } from '../replication/PgManager.js';
|
|
15
|
-
import { PostgresErrorRateLimiter } from '../replication/PostgresErrorRateLimiter.js';
|
|
16
|
-
import { checkSourceConfiguration, cleanUpReplicationSlot } from '../replication/replication-utils.js';
|
|
17
|
-
import { PUBLICATION_NAME } from '../replication/WalStream.js';
|
|
18
|
-
import { WalStreamReplicator } from '../replication/WalStreamReplicator.js';
|
|
19
|
-
import * as types from '../types/types.js';
|
|
20
|
-
import { PostgresConnectionConfig } from '../types/types.js';
|
|
21
|
-
import { getApplicationName } from '../utils/application-name.js';
|
|
22
|
-
import { CustomTypeRegistry } from '../types/registry.js';
|
|
23
|
-
|
|
24
|
-
export class PostgresModule extends replication.ReplicationModule<types.PostgresConnectionConfig> {
|
|
25
|
-
constructor() {
|
|
26
|
-
super({
|
|
27
|
-
name: 'Postgres',
|
|
28
|
-
type: types.POSTGRES_CONNECTION_TYPE,
|
|
29
|
-
configSchema: types.PostgresConnectionConfig
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async onInitialized(context: system.ServiceContextContainer): Promise<void> {
|
|
34
|
-
// Record replicated bytes using global jpgwire metrics. Only registered if this module is replicating
|
|
35
|
-
if (context.replicationEngine) {
|
|
36
|
-
jpgwire.setMetricsRecorder({
|
|
37
|
-
addBytesRead(bytes) {
|
|
38
|
-
context.metricsEngine.getCounter(ReplicationMetric.DATA_REPLICATED_BYTES).add(bytes);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
this.logger.info('Successfully set up connection metrics recorder for PostgresModule.');
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
protected createRouteAPIAdapter(): api.RouteAPI {
|
|
46
|
-
return PostgresRouteAPIAdapter.withConfig(this.resolveConfig(this.decodedConfig!));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
protected createReplicator(context: system.ServiceContext): replication.AbstractReplicator {
|
|
50
|
-
const normalisedConfig = this.resolveConfig(this.decodedConfig!);
|
|
51
|
-
const syncRuleProvider = new ConfigurationFileSyncRulesProvider(context.configuration.sync_rules);
|
|
52
|
-
const connectionFactory = new ConnectionManagerFactory(normalisedConfig);
|
|
53
|
-
|
|
54
|
-
return new WalStreamReplicator({
|
|
55
|
-
id: this.getDefaultId(normalisedConfig.database),
|
|
56
|
-
syncRuleProvider: syncRuleProvider,
|
|
57
|
-
storageEngine: context.storageEngine,
|
|
58
|
-
metricsEngine: context.metricsEngine,
|
|
59
|
-
connectionFactory: connectionFactory,
|
|
60
|
-
rateLimiter: new PostgresErrorRateLimiter()
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Combines base config with normalized connection settings
|
|
66
|
-
*/
|
|
67
|
-
private resolveConfig(config: types.PostgresConnectionConfig): types.ResolvedConnectionConfig {
|
|
68
|
-
return {
|
|
69
|
-
...config,
|
|
70
|
-
...types.normalizeConnectionConfig(config)
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async teardown(options: modules.TearDownOptions): Promise<void> {
|
|
75
|
-
const normalisedConfig = this.resolveConfig(this.decodedConfig!);
|
|
76
|
-
const connectionManager = new PgManager(normalisedConfig, {
|
|
77
|
-
idleTimeout: 30_000,
|
|
78
|
-
maxSize: 1,
|
|
79
|
-
applicationName: getApplicationName()
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
if (options.syncRules) {
|
|
84
|
-
// TODO: In the future, once we have more replication types, we will need to check if these syncRules are for Postgres
|
|
85
|
-
for (let syncRules of options.syncRules) {
|
|
86
|
-
try {
|
|
87
|
-
await cleanUpReplicationSlot(syncRules.slot_name, connectionManager.pool);
|
|
88
|
-
} catch (e) {
|
|
89
|
-
// Not really much we can do here for failures, most likely the database is no longer accessible
|
|
90
|
-
this.logger.warn(`Failed to fully clean up Postgres replication slot: ${syncRules.slot_name}`, e);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
} finally {
|
|
95
|
-
await connectionManager.end();
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async testConnection(config: PostgresConnectionConfig): Promise<ConnectionTestResult> {
|
|
100
|
-
this.decodeConfig(config);
|
|
101
|
-
const normalizedConfig = this.resolveConfig(this.decodedConfig!);
|
|
102
|
-
return await this.testConnection(normalizedConfig);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
static async testConnection(normalizedConfig: NormalizedBasePostgresConnectionConfig): Promise<ConnectionTestResult> {
|
|
106
|
-
// FIXME: This is not a complete implementation yet.
|
|
107
|
-
const connectionManager = new PgManager(normalizedConfig, {
|
|
108
|
-
idleTimeout: 30_000,
|
|
109
|
-
maxSize: 1,
|
|
110
|
-
applicationName: getApplicationName()
|
|
111
|
-
});
|
|
112
|
-
const connection = await connectionManager.snapshotConnection();
|
|
113
|
-
try {
|
|
114
|
-
await checkSourceConfiguration(connection, PUBLICATION_NAME);
|
|
115
|
-
} finally {
|
|
116
|
-
await connectionManager.end();
|
|
117
|
-
}
|
|
118
|
-
return {
|
|
119
|
-
connectionDescription: baseUri(normalizedConfig)
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { logger } from '@powersync/lib-services-framework';
|
|
2
|
-
import { PgPoolOptions } from '@powersync/service-jpgwire';
|
|
3
|
-
import { NormalizedPostgresConnectionConfig } from '../types/types.js';
|
|
4
|
-
import { PgManager } from './PgManager.js';
|
|
5
|
-
|
|
6
|
-
export class ConnectionManagerFactory {
|
|
7
|
-
private readonly connectionManagers = new Set<PgManager>();
|
|
8
|
-
public readonly dbConnectionConfig: NormalizedPostgresConnectionConfig;
|
|
9
|
-
|
|
10
|
-
constructor(dbConnectionConfig: NormalizedPostgresConnectionConfig) {
|
|
11
|
-
this.dbConnectionConfig = dbConnectionConfig;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
create(poolOptions: PgPoolOptions) {
|
|
15
|
-
const manager = new PgManager(this.dbConnectionConfig, { ...poolOptions });
|
|
16
|
-
this.connectionManagers.add(manager);
|
|
17
|
-
|
|
18
|
-
manager.registerListener({
|
|
19
|
-
onEnded: () => {
|
|
20
|
-
this.connectionManagers.delete(manager);
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
return manager;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async shutdown() {
|
|
27
|
-
logger.info('Shutting down Postgres connection Managers...');
|
|
28
|
-
for (const manager of [...this.connectionManagers]) {
|
|
29
|
-
await manager.end();
|
|
30
|
-
}
|
|
31
|
-
logger.info('Postgres connection Managers shutdown completed.');
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { BaseObserver } from '@powersync/lib-services-framework';
|
|
2
|
-
import * as pgwire from '@powersync/service-jpgwire';
|
|
3
|
-
import semver from 'semver';
|
|
4
|
-
import { PostgresTypeResolver } from '../types/resolver.js';
|
|
5
|
-
import { NormalizedPostgresConnectionConfig } from '../types/types.js';
|
|
6
|
-
import { getApplicationName } from '../utils/application-name.js';
|
|
7
|
-
import { getServerVersion } from '../utils/postgres_version.js';
|
|
8
|
-
|
|
9
|
-
export interface PgManagerOptions extends pgwire.PgPoolOptions {}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Shorter timeout for snapshot connections than for replication connections.
|
|
13
|
-
*/
|
|
14
|
-
const SNAPSHOT_SOCKET_TIMEOUT = 30_000;
|
|
15
|
-
|
|
16
|
-
export interface PgManagerListener {
|
|
17
|
-
onEnded(): void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class PgManager extends BaseObserver<PgManagerListener> {
|
|
21
|
-
/**
|
|
22
|
-
* Do not use this for any transactions.
|
|
23
|
-
*/
|
|
24
|
-
public readonly pool: pgwire.PgClient;
|
|
25
|
-
|
|
26
|
-
public readonly types: PostgresTypeResolver;
|
|
27
|
-
|
|
28
|
-
private connectionPromises: Promise<pgwire.PgConnection>[] = [];
|
|
29
|
-
|
|
30
|
-
constructor(
|
|
31
|
-
public options: NormalizedPostgresConnectionConfig,
|
|
32
|
-
public poolOptions: PgManagerOptions
|
|
33
|
-
) {
|
|
34
|
-
super();
|
|
35
|
-
// The pool is lazy - no connections are opened until a query is performed.
|
|
36
|
-
this.pool = pgwire.connectPgWirePool(this.options, poolOptions);
|
|
37
|
-
this.types = new PostgresTypeResolver(this.pool);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
public get connectionTag() {
|
|
41
|
-
return this.options.tag;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Create a new replication connection.
|
|
46
|
-
*/
|
|
47
|
-
async replicationConnection(): Promise<pgwire.PgConnection> {
|
|
48
|
-
const p = pgwire.connectPgWire(this.options, { type: 'replication', applicationName: getApplicationName() });
|
|
49
|
-
this.connectionPromises.push(p);
|
|
50
|
-
return await p;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* @returns The Postgres server version in a parsed Semver instance
|
|
55
|
-
*/
|
|
56
|
-
async getServerVersion(): Promise<semver.SemVer | null> {
|
|
57
|
-
return await getServerVersion(this.pool);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Create a new standard connection, used for initial snapshot.
|
|
62
|
-
*
|
|
63
|
-
* This connection must not be shared between multiple async contexts.
|
|
64
|
-
*/
|
|
65
|
-
async snapshotConnection(): Promise<pgwire.PgConnection> {
|
|
66
|
-
const p = pgwire.connectPgWire(this.options, { type: 'standard', applicationName: getApplicationName() });
|
|
67
|
-
this.connectionPromises.push(p);
|
|
68
|
-
const connection = await p;
|
|
69
|
-
|
|
70
|
-
// Use an shorter timeout for snapshot connections.
|
|
71
|
-
// This is to detect broken connections early, instead of waiting
|
|
72
|
-
// for the full 6 minutes.
|
|
73
|
-
// This we are constantly using the connection, we don't need any
|
|
74
|
-
// custom keepalives.
|
|
75
|
-
|
|
76
|
-
(connection as any)._socket.setTimeout(SNAPSHOT_SOCKET_TIMEOUT);
|
|
77
|
-
|
|
78
|
-
// Disable statement timeout for snapshot queries.
|
|
79
|
-
// On Supabase, the default is 2 minutes.
|
|
80
|
-
await connection.query(`set session statement_timeout = 0`);
|
|
81
|
-
|
|
82
|
-
return connection;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async end(): Promise<void> {
|
|
86
|
-
for (let result of await Promise.allSettled([
|
|
87
|
-
this.pool.end(),
|
|
88
|
-
...this.connectionPromises.map(async (promise) => {
|
|
89
|
-
// Wait for connection attempts to finish, but do not throw connection errors here
|
|
90
|
-
const connection = await promise.catch((_) => {});
|
|
91
|
-
return await connection?.end();
|
|
92
|
-
})
|
|
93
|
-
])) {
|
|
94
|
-
// Throw the first error, if any
|
|
95
|
-
if (result.status == 'rejected') {
|
|
96
|
-
throw result.reason;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
this.iterateListeners((listener) => {
|
|
100
|
-
listener.onEnded?.();
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async destroy() {
|
|
105
|
-
this.pool.destroy();
|
|
106
|
-
for (let result of await Promise.allSettled([
|
|
107
|
-
...this.connectionPromises.map(async (promise) => {
|
|
108
|
-
// Wait for connection attempts to finish, but do not throw connection errors here
|
|
109
|
-
const connection = await promise.catch((_) => {});
|
|
110
|
-
return connection?.destroy();
|
|
111
|
-
})
|
|
112
|
-
])) {
|
|
113
|
-
// Throw the first error, if any
|
|
114
|
-
if (result.status == 'rejected') {
|
|
115
|
-
throw result.reason;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
this.iterateListeners((listener) => {
|
|
119
|
-
listener.onEnded?.();
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
2
|
-
import { storage } from '@powersync/service-core';
|
|
3
|
-
import { PgoutputRelation } from '@powersync/service-jpgwire';
|
|
4
|
-
|
|
5
|
-
export type ReplicationIdentity = 'default' | 'nothing' | 'full' | 'index';
|
|
6
|
-
|
|
7
|
-
export function getReplicaIdColumns(relation: PgoutputRelation): storage.ColumnDescriptor[] {
|
|
8
|
-
if (relation.replicaIdentity == 'nothing') {
|
|
9
|
-
return [];
|
|
10
|
-
} else {
|
|
11
|
-
return relation.columns
|
|
12
|
-
.filter((c) => (c.flags & 0b1) != 0)
|
|
13
|
-
.map((c) => ({ name: c.name, typeId: c.typeOid }) satisfies storage.ColumnDescriptor);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
export function getRelId(source: PgoutputRelation): number {
|
|
17
|
-
// Source types are wrong here
|
|
18
|
-
const relId = (source as any).relationOid as number;
|
|
19
|
-
if (!relId) {
|
|
20
|
-
throw new ReplicationAssertionError(`No relation id found`);
|
|
21
|
-
}
|
|
22
|
-
return relId;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function getPgOutputRelation(source: PgoutputRelation): storage.SourceEntityDescriptor {
|
|
26
|
-
return {
|
|
27
|
-
name: source.name,
|
|
28
|
-
schema: source.schema,
|
|
29
|
-
objectId: getRelId(source),
|
|
30
|
-
replicaIdColumns: getReplicaIdColumns(source)
|
|
31
|
-
} satisfies storage.SourceEntityDescriptor;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function referencedColumnTypeIds(source: PgoutputRelation): number[] {
|
|
35
|
-
const oids = new Set<number>();
|
|
36
|
-
for (const column of source.columns) {
|
|
37
|
-
oids.add(column.typeOid);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return [...oids];
|
|
41
|
-
}
|