@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.
Files changed (96) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +67 -0
  3. package/README.md +3 -0
  4. package/dev/.env.template +2 -0
  5. package/dev/README.md +9 -0
  6. package/dev/config/sync_rules.yaml +12 -0
  7. package/dev/docker/mysql/docker-compose.yaml +17 -0
  8. package/dev/docker/mysql/init-scripts/my.cnf +9 -0
  9. package/dev/docker/mysql/init-scripts/mysql.sql +38 -0
  10. package/dist/api/MySQLRouteAPIAdapter.d.ts +24 -0
  11. package/dist/api/MySQLRouteAPIAdapter.js +311 -0
  12. package/dist/api/MySQLRouteAPIAdapter.js.map +1 -0
  13. package/dist/common/ReplicatedGTID.d.ts +59 -0
  14. package/dist/common/ReplicatedGTID.js +110 -0
  15. package/dist/common/ReplicatedGTID.js.map +1 -0
  16. package/dist/common/check-source-configuration.d.ts +3 -0
  17. package/dist/common/check-source-configuration.js +46 -0
  18. package/dist/common/check-source-configuration.js.map +1 -0
  19. package/dist/common/common-index.d.ts +6 -0
  20. package/dist/common/common-index.js +7 -0
  21. package/dist/common/common-index.js.map +1 -0
  22. package/dist/common/get-replication-columns.d.ts +12 -0
  23. package/dist/common/get-replication-columns.js +103 -0
  24. package/dist/common/get-replication-columns.js.map +1 -0
  25. package/dist/common/get-tables-from-pattern.d.ts +7 -0
  26. package/dist/common/get-tables-from-pattern.js +28 -0
  27. package/dist/common/get-tables-from-pattern.js.map +1 -0
  28. package/dist/common/mysql-to-sqlite.d.ts +4 -0
  29. package/dist/common/mysql-to-sqlite.js +56 -0
  30. package/dist/common/mysql-to-sqlite.js.map +1 -0
  31. package/dist/common/read-executed-gtid.d.ts +6 -0
  32. package/dist/common/read-executed-gtid.js +40 -0
  33. package/dist/common/read-executed-gtid.js.map +1 -0
  34. package/dist/index.d.ts +3 -0
  35. package/dist/index.js +4 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/module/MySQLModule.d.ts +13 -0
  38. package/dist/module/MySQLModule.js +46 -0
  39. package/dist/module/MySQLModule.js.map +1 -0
  40. package/dist/replication/BinLogReplicationJob.d.ts +14 -0
  41. package/dist/replication/BinLogReplicationJob.js +88 -0
  42. package/dist/replication/BinLogReplicationJob.js.map +1 -0
  43. package/dist/replication/BinLogReplicator.d.ts +13 -0
  44. package/dist/replication/BinLogReplicator.js +25 -0
  45. package/dist/replication/BinLogReplicator.js.map +1 -0
  46. package/dist/replication/BinLogStream.d.ts +43 -0
  47. package/dist/replication/BinLogStream.js +421 -0
  48. package/dist/replication/BinLogStream.js.map +1 -0
  49. package/dist/replication/MySQLConnectionManager.d.ts +43 -0
  50. package/dist/replication/MySQLConnectionManager.js +81 -0
  51. package/dist/replication/MySQLConnectionManager.js.map +1 -0
  52. package/dist/replication/MySQLConnectionManagerFactory.d.ts +10 -0
  53. package/dist/replication/MySQLConnectionManagerFactory.js +21 -0
  54. package/dist/replication/MySQLConnectionManagerFactory.js.map +1 -0
  55. package/dist/replication/MySQLErrorRateLimiter.d.ts +10 -0
  56. package/dist/replication/MySQLErrorRateLimiter.js +43 -0
  57. package/dist/replication/MySQLErrorRateLimiter.js.map +1 -0
  58. package/dist/replication/zongji/zongji-utils.d.ts +7 -0
  59. package/dist/replication/zongji/zongji-utils.js +19 -0
  60. package/dist/replication/zongji/zongji-utils.js.map +1 -0
  61. package/dist/types/types.d.ts +50 -0
  62. package/dist/types/types.js +61 -0
  63. package/dist/types/types.js.map +1 -0
  64. package/dist/utils/mysql_utils.d.ts +14 -0
  65. package/dist/utils/mysql_utils.js +38 -0
  66. package/dist/utils/mysql_utils.js.map +1 -0
  67. package/package.json +51 -0
  68. package/src/api/MySQLRouteAPIAdapter.ts +357 -0
  69. package/src/common/ReplicatedGTID.ts +158 -0
  70. package/src/common/check-source-configuration.ts +59 -0
  71. package/src/common/common-index.ts +6 -0
  72. package/src/common/get-replication-columns.ts +124 -0
  73. package/src/common/get-tables-from-pattern.ts +44 -0
  74. package/src/common/mysql-to-sqlite.ts +59 -0
  75. package/src/common/read-executed-gtid.ts +43 -0
  76. package/src/index.ts +5 -0
  77. package/src/module/MySQLModule.ts +53 -0
  78. package/src/replication/BinLogReplicationJob.ts +97 -0
  79. package/src/replication/BinLogReplicator.ts +35 -0
  80. package/src/replication/BinLogStream.ts +547 -0
  81. package/src/replication/MySQLConnectionManager.ts +104 -0
  82. package/src/replication/MySQLConnectionManagerFactory.ts +28 -0
  83. package/src/replication/MySQLErrorRateLimiter.ts +44 -0
  84. package/src/replication/zongji/zongji-utils.ts +32 -0
  85. package/src/replication/zongji/zongji.d.ts +98 -0
  86. package/src/types/types.ts +102 -0
  87. package/src/utils/mysql_utils.ts +47 -0
  88. package/test/src/binlog_stream.test.ts +288 -0
  89. package/test/src/binlog_stream_utils.ts +152 -0
  90. package/test/src/env.ts +7 -0
  91. package/test/src/setup.ts +7 -0
  92. package/test/src/util.ts +62 -0
  93. package/test/tsconfig.json +28 -0
  94. package/tsconfig.json +26 -0
  95. package/tsconfig.tsbuildinfo +1 -0
  96. 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,5 @@
1
+ import { MySQLModule } from './module/MySQLModule.js';
2
+
3
+ export const module = new MySQLModule();
4
+
5
+ export default module;
@@ -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
+ }