@powersync/service-module-mysql 0.0.0-dev-20241015210820
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 +20 -0
- package/LICENSE +67 -0
- package/README.md +3 -0
- package/dev/.env.template +2 -0
- package/dev/README.md +9 -0
- package/dev/config/sync_rules.yaml +12 -0
- package/dev/docker/mysql/docker-compose.yaml +17 -0
- package/dev/docker/mysql/init-scripts/my.cnf +9 -0
- package/dev/docker/mysql/init-scripts/mysql.sql +38 -0
- package/dist/api/MySQLRouteAPIAdapter.d.ts +24 -0
- package/dist/api/MySQLRouteAPIAdapter.js +311 -0
- package/dist/api/MySQLRouteAPIAdapter.js.map +1 -0
- package/dist/common/ReplicatedGTID.d.ts +59 -0
- package/dist/common/ReplicatedGTID.js +110 -0
- package/dist/common/ReplicatedGTID.js.map +1 -0
- package/dist/common/check-source-configuration.d.ts +3 -0
- package/dist/common/check-source-configuration.js +46 -0
- package/dist/common/check-source-configuration.js.map +1 -0
- package/dist/common/common-index.d.ts +6 -0
- package/dist/common/common-index.js +7 -0
- package/dist/common/common-index.js.map +1 -0
- package/dist/common/get-replication-columns.d.ts +12 -0
- package/dist/common/get-replication-columns.js +103 -0
- package/dist/common/get-replication-columns.js.map +1 -0
- package/dist/common/get-tables-from-pattern.d.ts +7 -0
- package/dist/common/get-tables-from-pattern.js +28 -0
- package/dist/common/get-tables-from-pattern.js.map +1 -0
- package/dist/common/mysql-to-sqlite.d.ts +4 -0
- package/dist/common/mysql-to-sqlite.js +56 -0
- package/dist/common/mysql-to-sqlite.js.map +1 -0
- package/dist/common/read-executed-gtid.d.ts +6 -0
- package/dist/common/read-executed-gtid.js +40 -0
- package/dist/common/read-executed-gtid.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/module/MySQLModule.d.ts +13 -0
- package/dist/module/MySQLModule.js +46 -0
- package/dist/module/MySQLModule.js.map +1 -0
- package/dist/replication/BinLogReplicationJob.d.ts +14 -0
- package/dist/replication/BinLogReplicationJob.js +88 -0
- package/dist/replication/BinLogReplicationJob.js.map +1 -0
- package/dist/replication/BinLogReplicator.d.ts +13 -0
- package/dist/replication/BinLogReplicator.js +25 -0
- package/dist/replication/BinLogReplicator.js.map +1 -0
- package/dist/replication/BinLogStream.d.ts +43 -0
- package/dist/replication/BinLogStream.js +421 -0
- package/dist/replication/BinLogStream.js.map +1 -0
- package/dist/replication/MySQLConnectionManager.d.ts +43 -0
- package/dist/replication/MySQLConnectionManager.js +81 -0
- package/dist/replication/MySQLConnectionManager.js.map +1 -0
- package/dist/replication/MySQLConnectionManagerFactory.d.ts +10 -0
- package/dist/replication/MySQLConnectionManagerFactory.js +21 -0
- package/dist/replication/MySQLConnectionManagerFactory.js.map +1 -0
- package/dist/replication/MySQLErrorRateLimiter.d.ts +10 -0
- package/dist/replication/MySQLErrorRateLimiter.js +43 -0
- package/dist/replication/MySQLErrorRateLimiter.js.map +1 -0
- package/dist/replication/zongji/zongji-utils.d.ts +7 -0
- package/dist/replication/zongji/zongji-utils.js +19 -0
- package/dist/replication/zongji/zongji-utils.js.map +1 -0
- package/dist/types/types.d.ts +50 -0
- package/dist/types/types.js +61 -0
- package/dist/types/types.js.map +1 -0
- package/dist/utils/mysql_utils.d.ts +14 -0
- package/dist/utils/mysql_utils.js +38 -0
- package/dist/utils/mysql_utils.js.map +1 -0
- package/package.json +51 -0
- package/src/api/MySQLRouteAPIAdapter.ts +357 -0
- package/src/common/ReplicatedGTID.ts +158 -0
- package/src/common/check-source-configuration.ts +59 -0
- package/src/common/common-index.ts +6 -0
- package/src/common/get-replication-columns.ts +124 -0
- package/src/common/get-tables-from-pattern.ts +44 -0
- package/src/common/mysql-to-sqlite.ts +59 -0
- package/src/common/read-executed-gtid.ts +43 -0
- package/src/index.ts +5 -0
- package/src/module/MySQLModule.ts +53 -0
- package/src/replication/BinLogReplicationJob.ts +97 -0
- package/src/replication/BinLogReplicator.ts +35 -0
- package/src/replication/BinLogStream.ts +547 -0
- package/src/replication/MySQLConnectionManager.ts +104 -0
- package/src/replication/MySQLConnectionManagerFactory.ts +28 -0
- package/src/replication/MySQLErrorRateLimiter.ts +44 -0
- package/src/replication/zongji/zongji-utils.ts +32 -0
- package/src/replication/zongji/zongji.d.ts +98 -0
- package/src/types/types.ts +102 -0
- package/src/utils/mysql_utils.ts +47 -0
- package/test/src/binlog_stream.test.ts +288 -0
- package/test/src/binlog_stream_utils.ts +152 -0
- package/test/src/env.ts +7 -0
- package/test/src/setup.ts +7 -0
- package/test/src/util.ts +62 -0
- package/test/tsconfig.json +28 -0
- package/tsconfig.json +26 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as sync_rules from '@powersync/service-sync-rules';
|
|
2
|
+
import mysql from 'mysql2/promise';
|
|
3
|
+
|
|
4
|
+
export type GetDebugTablesInfoOptions = {
|
|
5
|
+
connection: mysql.Connection;
|
|
6
|
+
tablePattern: sync_rules.TablePattern;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export async function getTablesFromPattern(options: GetDebugTablesInfoOptions): Promise<Set<string>> {
|
|
10
|
+
const { connection, tablePattern } = options;
|
|
11
|
+
const schema = tablePattern.schema;
|
|
12
|
+
|
|
13
|
+
if (tablePattern.isWildcard) {
|
|
14
|
+
const [results] = await connection.query<mysql.RowDataPacket[]>(
|
|
15
|
+
`SELECT
|
|
16
|
+
TABLE_NAME AS table_name
|
|
17
|
+
FROM
|
|
18
|
+
INFORMATION_SCHEMA.TABLES
|
|
19
|
+
WHERE
|
|
20
|
+
TABLE_SCHEMA = ?
|
|
21
|
+
AND TABLE_NAME LIKE ?`,
|
|
22
|
+
[schema, tablePattern.tablePattern]
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return new Set(
|
|
26
|
+
results
|
|
27
|
+
.filter((result) => result.table_name.startsWith(tablePattern.tablePrefix))
|
|
28
|
+
.map((result) => result.table_name)
|
|
29
|
+
);
|
|
30
|
+
} else {
|
|
31
|
+
const [[match]] = await connection.query<mysql.RowDataPacket[]>(
|
|
32
|
+
`SELECT
|
|
33
|
+
TABLE_NAME AS table_name
|
|
34
|
+
FROM
|
|
35
|
+
INFORMATION_SCHEMA.TABLES
|
|
36
|
+
WHERE
|
|
37
|
+
TABLE_SCHEMA = ?
|
|
38
|
+
AND TABLE_NAME = ?`,
|
|
39
|
+
[tablePattern.schema, tablePattern.tablePattern]
|
|
40
|
+
);
|
|
41
|
+
// Only return the first result
|
|
42
|
+
return new Set([match.table_name]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as sync_rules from '@powersync/service-sync-rules';
|
|
2
|
+
import { ExpressionType } from '@powersync/service-sync-rules';
|
|
3
|
+
|
|
4
|
+
export function toSQLiteRow(row: Record<string, any>): sync_rules.SqliteRow {
|
|
5
|
+
for (let key in row) {
|
|
6
|
+
if (row[key] instanceof Date) {
|
|
7
|
+
row[key] = row[key].toISOString();
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return sync_rules.toSyncRulesRow(row);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function toExpressionTypeFromMySQLType(mysqlType: string | undefined): ExpressionType {
|
|
14
|
+
if (!mysqlType) {
|
|
15
|
+
return ExpressionType.TEXT;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const upperCaseType = mysqlType.toUpperCase();
|
|
19
|
+
// Handle type with parameters like VARCHAR(255), DECIMAL(10,2), etc.
|
|
20
|
+
const baseType = upperCaseType.split('(')[0];
|
|
21
|
+
|
|
22
|
+
switch (baseType) {
|
|
23
|
+
case 'BIT':
|
|
24
|
+
case 'BOOL':
|
|
25
|
+
case 'BOOLEAN':
|
|
26
|
+
case 'TINYINT':
|
|
27
|
+
case 'SMALLINT':
|
|
28
|
+
case 'MEDIUMINT':
|
|
29
|
+
case 'INT':
|
|
30
|
+
case 'INTEGER':
|
|
31
|
+
case 'BIGINT':
|
|
32
|
+
case 'UNSIGNED BIGINT':
|
|
33
|
+
return ExpressionType.INTEGER;
|
|
34
|
+
case 'BINARY':
|
|
35
|
+
case 'VARBINARY':
|
|
36
|
+
case 'TINYBLOB':
|
|
37
|
+
case 'MEDIUMBLOB':
|
|
38
|
+
case 'LONGBLOB':
|
|
39
|
+
case 'BLOB':
|
|
40
|
+
case 'GEOMETRY':
|
|
41
|
+
case 'POINT':
|
|
42
|
+
case 'LINESTRING':
|
|
43
|
+
case 'POLYGON':
|
|
44
|
+
case 'MULTIPOINT':
|
|
45
|
+
case 'MULTILINESTRING':
|
|
46
|
+
case 'MULTIPOLYGON':
|
|
47
|
+
case 'GEOMETRYCOLLECTION':
|
|
48
|
+
return ExpressionType.BLOB;
|
|
49
|
+
case 'FLOAT':
|
|
50
|
+
case 'DOUBLE':
|
|
51
|
+
case 'REAL':
|
|
52
|
+
return ExpressionType.REAL;
|
|
53
|
+
case 'JSON':
|
|
54
|
+
return ExpressionType.TEXT;
|
|
55
|
+
default:
|
|
56
|
+
// In addition to the normal text types, includes: DECIMAL, NUMERIC, DATE, TIME, DATETIME, TIMESTAMP, YEAR, ENUM, SET
|
|
57
|
+
return ExpressionType.TEXT;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import mysqlPromise from 'mysql2/promise';
|
|
2
|
+
import * as mysql_utils from '../utils/mysql_utils.js';
|
|
3
|
+
import { gte } from 'semver';
|
|
4
|
+
|
|
5
|
+
import { ReplicatedGTID } from './ReplicatedGTID.js';
|
|
6
|
+
import { getMySQLVersion } from './check-source-configuration.js';
|
|
7
|
+
import { logger } from '@powersync/lib-services-framework';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Gets the current master HEAD GTID
|
|
11
|
+
*/
|
|
12
|
+
export async function readExecutedGtid(connection: mysqlPromise.Connection): Promise<ReplicatedGTID> {
|
|
13
|
+
const version = await getMySQLVersion(connection);
|
|
14
|
+
let binlogStatus: mysqlPromise.RowDataPacket;
|
|
15
|
+
if (gte(version, '8.4.0')) {
|
|
16
|
+
// Get the BinLog status
|
|
17
|
+
const [[binLogResult]] = await mysql_utils.retriedQuery({
|
|
18
|
+
connection,
|
|
19
|
+
query: `SHOW BINARY LOG STATUS`
|
|
20
|
+
});
|
|
21
|
+
binlogStatus = binLogResult;
|
|
22
|
+
} else {
|
|
23
|
+
// TODO Check if this works for version 5.7
|
|
24
|
+
// Get the BinLog status
|
|
25
|
+
const [[binLogResult]] = await mysql_utils.retriedQuery({
|
|
26
|
+
connection,
|
|
27
|
+
query: `SHOW MASTER STATUS`
|
|
28
|
+
});
|
|
29
|
+
binlogStatus = binLogResult;
|
|
30
|
+
}
|
|
31
|
+
const position = {
|
|
32
|
+
filename: binlogStatus.File,
|
|
33
|
+
offset: parseInt(binlogStatus.Position)
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
logger.info('Succesfully read executed GTID', { position });
|
|
37
|
+
|
|
38
|
+
return new ReplicatedGTID({
|
|
39
|
+
// The head always points to the next position to start replication from
|
|
40
|
+
position,
|
|
41
|
+
raw_gtid: binlogStatus.Executed_Gtid_Set
|
|
42
|
+
});
|
|
43
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { api, ConfigurationFileSyncRulesProvider, replication, system, TearDownOptions } from '@powersync/service-core';
|
|
2
|
+
|
|
3
|
+
import { MySQLRouteAPIAdapter } from '../api/MySQLRouteAPIAdapter.js';
|
|
4
|
+
import { BinLogReplicator } from '../replication/BinLogReplicator.js';
|
|
5
|
+
import { MySQLErrorRateLimiter } from '../replication/MySQLErrorRateLimiter.js';
|
|
6
|
+
import * as types from '../types/types.js';
|
|
7
|
+
import { MySQLConnectionManagerFactory } from '../replication/MySQLConnectionManagerFactory.js';
|
|
8
|
+
|
|
9
|
+
export class MySQLModule extends replication.ReplicationModule<types.MySQLConnectionConfig> {
|
|
10
|
+
constructor() {
|
|
11
|
+
super({
|
|
12
|
+
name: 'MySQL',
|
|
13
|
+
type: types.MYSQL_CONNECTION_TYPE,
|
|
14
|
+
configSchema: types.MySQLConnectionConfig
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async initialize(context: system.ServiceContextContainer): Promise<void> {
|
|
19
|
+
await super.initialize(context);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected createRouteAPIAdapter(): api.RouteAPI {
|
|
23
|
+
return new MySQLRouteAPIAdapter(this.resolveConfig(this.decodedConfig!));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
protected createReplicator(context: system.ServiceContext): replication.AbstractReplicator {
|
|
27
|
+
const normalisedConfig = this.resolveConfig(this.decodedConfig!);
|
|
28
|
+
const syncRuleProvider = new ConfigurationFileSyncRulesProvider(context.configuration.sync_rules);
|
|
29
|
+
const connectionFactory = new MySQLConnectionManagerFactory(normalisedConfig);
|
|
30
|
+
|
|
31
|
+
return new BinLogReplicator({
|
|
32
|
+
id: this.getDefaultId(normalisedConfig.database),
|
|
33
|
+
syncRuleProvider: syncRuleProvider,
|
|
34
|
+
storageEngine: context.storageEngine,
|
|
35
|
+
connectionFactory: connectionFactory,
|
|
36
|
+
rateLimiter: new MySQLErrorRateLimiter()
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Combines base config with normalized connection settings
|
|
42
|
+
*/
|
|
43
|
+
private resolveConfig(config: types.MySQLConnectionConfig): types.ResolvedConnectionConfig {
|
|
44
|
+
return {
|
|
45
|
+
...config,
|
|
46
|
+
...types.normalizeConnectionConfig(config)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async teardown(options: TearDownOptions): Promise<void> {
|
|
51
|
+
// No specific teardown required for MySQL
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { container } from '@powersync/lib-services-framework';
|
|
2
|
+
import { replication } from '@powersync/service-core';
|
|
3
|
+
import { BinLogStream } from './BinLogStream.js';
|
|
4
|
+
import { MySQLConnectionManagerFactory } from './MySQLConnectionManagerFactory.js';
|
|
5
|
+
|
|
6
|
+
export interface BinLogReplicationJobOptions extends replication.AbstractReplicationJobOptions {
|
|
7
|
+
connectionFactory: MySQLConnectionManagerFactory;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class BinLogReplicationJob extends replication.AbstractReplicationJob {
|
|
11
|
+
private connectionFactory: MySQLConnectionManagerFactory;
|
|
12
|
+
|
|
13
|
+
constructor(options: BinLogReplicationJobOptions) {
|
|
14
|
+
super(options);
|
|
15
|
+
this.connectionFactory = options.connectionFactory;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get slot_name() {
|
|
19
|
+
return this.options.storage.slot_name;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async keepAlive() {}
|
|
23
|
+
|
|
24
|
+
async replicate() {
|
|
25
|
+
try {
|
|
26
|
+
await this.replicateLoop();
|
|
27
|
+
} catch (e) {
|
|
28
|
+
// Fatal exception
|
|
29
|
+
container.reporter.captureException(e, {
|
|
30
|
+
metadata: {
|
|
31
|
+
replication_slot: this.slot_name
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
this.logger.error(`Replication failed on ${this.slot_name}`, e);
|
|
35
|
+
|
|
36
|
+
// Slot removal type error logic goes here
|
|
37
|
+
// if (e) {
|
|
38
|
+
// // This stops replication on this slot, and creates a new slot
|
|
39
|
+
// await this.options.storage.factory.slotRemoved(this.slot_name);
|
|
40
|
+
// }
|
|
41
|
+
} finally {
|
|
42
|
+
this.abortController.abort();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async replicateLoop() {
|
|
47
|
+
while (!this.isStopped) {
|
|
48
|
+
await this.replicateOnce();
|
|
49
|
+
|
|
50
|
+
if (!this.isStopped) {
|
|
51
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async replicateOnce() {
|
|
57
|
+
// New connections on every iteration (every error with retry),
|
|
58
|
+
// otherwise we risk repeating errors related to the connection,
|
|
59
|
+
// such as caused by cached PG schemas.
|
|
60
|
+
const connectionManager = this.connectionFactory.create({
|
|
61
|
+
// Pool connections are only used intermittently.
|
|
62
|
+
idleTimeout: 30_000
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
await this.rateLimiter?.waitUntilAllowed({ signal: this.abortController.signal });
|
|
66
|
+
if (this.isStopped) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const stream = new BinLogStream({
|
|
70
|
+
abortSignal: this.abortController.signal,
|
|
71
|
+
storage: this.options.storage,
|
|
72
|
+
connections: connectionManager
|
|
73
|
+
});
|
|
74
|
+
await stream.replicate();
|
|
75
|
+
} catch (e) {
|
|
76
|
+
this.logger.error(`Replication error`, e);
|
|
77
|
+
if (e.cause != null) {
|
|
78
|
+
this.logger.error(`cause`, e.cause);
|
|
79
|
+
}
|
|
80
|
+
// TODO not recoverable error
|
|
81
|
+
if (false) {
|
|
82
|
+
throw e;
|
|
83
|
+
} else {
|
|
84
|
+
// Report the error if relevant, before retrying
|
|
85
|
+
container.reporter.captureException(e, {
|
|
86
|
+
metadata: {
|
|
87
|
+
replication_slot: this.slot_name
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// This sets the retry delay
|
|
91
|
+
this.rateLimiter?.reportError(e);
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
await connectionManager.end();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { replication, storage } from '@powersync/service-core';
|
|
2
|
+
import { BinLogReplicationJob } from './BinLogReplicationJob.js';
|
|
3
|
+
import { MySQLConnectionManagerFactory } from './MySQLConnectionManagerFactory.js';
|
|
4
|
+
|
|
5
|
+
export interface BinLogReplicatorOptions extends replication.AbstractReplicatorOptions {
|
|
6
|
+
connectionFactory: MySQLConnectionManagerFactory;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class BinLogReplicator extends replication.AbstractReplicator<BinLogReplicationJob> {
|
|
10
|
+
private readonly connectionFactory: MySQLConnectionManagerFactory;
|
|
11
|
+
|
|
12
|
+
constructor(options: BinLogReplicatorOptions) {
|
|
13
|
+
super(options);
|
|
14
|
+
this.connectionFactory = options.connectionFactory;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
createJob(options: replication.CreateJobOptions): BinLogReplicationJob {
|
|
18
|
+
return new BinLogReplicationJob({
|
|
19
|
+
id: this.createJobId(options.storage.group_id),
|
|
20
|
+
storage: options.storage,
|
|
21
|
+
lock: options.lock,
|
|
22
|
+
connectionFactory: this.connectionFactory,
|
|
23
|
+
rateLimiter: this.rateLimiter
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async cleanUp(syncRulesStorage: storage.SyncRulesBucketStorage): Promise<void> {
|
|
28
|
+
// The MySQL module does not create anything which requires cleanup on the MySQL server.
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async stop(): Promise<void> {
|
|
32
|
+
await super.stop();
|
|
33
|
+
await this.connectionFactory.shutdown();
|
|
34
|
+
}
|
|
35
|
+
}
|