@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,152 @@
1
+ import { ActiveCheckpoint, BucketStorageFactory, OpId, SyncRulesBucketStorage } from '@powersync/service-core';
2
+ import { TEST_CONNECTION_OPTIONS, clearAndRecreateTestDb } from './util.js';
3
+ import { fromAsync } from '@core-tests/stream_utils.js';
4
+ import { BinLogStream, BinLogStreamOptions } from '@module/replication/BinLogStream.js';
5
+ import { MySQLConnectionManager } from '@module/replication/MySQLConnectionManager.js';
6
+ import mysqlPromise from 'mysql2/promise';
7
+ import { readExecutedGtid } from '@module/common/read-executed-gtid.js';
8
+ import { logger } from '@powersync/lib-services-framework';
9
+
10
+ /**
11
+ * Tests operating on the binlog stream need to configure the stream and manage asynchronous
12
+ * replication, which gets a little tricky.
13
+ *
14
+ * This wraps a test in a function that configures all the context, and tears it down afterward.
15
+ */
16
+ export function binlogStreamTest(
17
+ factory: () => Promise<BucketStorageFactory>,
18
+ test: (context: BinlogStreamTestContext) => Promise<void>
19
+ ): () => Promise<void> {
20
+ return async () => {
21
+ const f = await factory();
22
+ const connectionManager = new MySQLConnectionManager(TEST_CONNECTION_OPTIONS, {});
23
+
24
+ const connection = await connectionManager.getConnection();
25
+ await clearAndRecreateTestDb(connection);
26
+ connection.release();
27
+ const context = new BinlogStreamTestContext(f, connectionManager);
28
+ try {
29
+ await test(context);
30
+ } finally {
31
+ await context.dispose();
32
+ }
33
+ };
34
+ }
35
+
36
+ export class BinlogStreamTestContext {
37
+ private _binlogStream?: BinLogStream;
38
+ private abortController = new AbortController();
39
+ private streamPromise?: Promise<void>;
40
+ public storage?: SyncRulesBucketStorage;
41
+ private replicationDone = false;
42
+
43
+ constructor(
44
+ public factory: BucketStorageFactory,
45
+ public connectionManager: MySQLConnectionManager
46
+ ) {}
47
+
48
+ async dispose() {
49
+ this.abortController.abort();
50
+ await this.streamPromise;
51
+ await this.connectionManager.end();
52
+ }
53
+
54
+ get connectionTag() {
55
+ return this.connectionManager.connectionTag;
56
+ }
57
+
58
+ async updateSyncRules(content: string) {
59
+ const syncRules = await this.factory.updateSyncRules({ content: content });
60
+ this.storage = this.factory.getInstance(syncRules);
61
+ return this.storage!;
62
+ }
63
+
64
+ get binlogStream() {
65
+ if (this.storage == null) {
66
+ throw new Error('updateSyncRules() first');
67
+ }
68
+ if (this._binlogStream) {
69
+ return this._binlogStream;
70
+ }
71
+ const options: BinLogStreamOptions = {
72
+ storage: this.storage,
73
+ connections: this.connectionManager,
74
+ abortSignal: this.abortController.signal
75
+ };
76
+ this._binlogStream = new BinLogStream(options);
77
+ return this._binlogStream!;
78
+ }
79
+
80
+ async replicateSnapshot() {
81
+ await this.binlogStream.initReplication();
82
+ this.replicationDone = true;
83
+ }
84
+
85
+ startStreaming() {
86
+ if (!this.replicationDone) {
87
+ throw new Error('Call replicateSnapshot() before startStreaming()');
88
+ }
89
+ this.streamPromise = this.binlogStream.streamChanges();
90
+ }
91
+
92
+ async getCheckpoint(options?: { timeout?: number }) {
93
+ const connection = await this.connectionManager.getConnection();
94
+ let checkpoint = await Promise.race([
95
+ getClientCheckpoint(connection, this.factory, { timeout: options?.timeout ?? 60_000 }),
96
+ this.streamPromise
97
+ ]);
98
+ connection.release();
99
+ if (typeof checkpoint == undefined) {
100
+ // This indicates an issue with the test setup - streamingPromise completed instead
101
+ // of getClientCheckpoint()
102
+ throw new Error('Test failure - streamingPromise completed');
103
+ }
104
+ return checkpoint as string;
105
+ }
106
+
107
+ async getBucketsDataBatch(buckets: Record<string, string>, options?: { timeout?: number }) {
108
+ let checkpoint = await this.getCheckpoint(options);
109
+ const map = new Map<string, string>(Object.entries(buckets));
110
+ return fromAsync(this.storage!.getBucketDataBatch(checkpoint, map));
111
+ }
112
+
113
+ async getBucketData(bucket: string, start?: string, options?: { timeout?: number }) {
114
+ start ??= '0';
115
+ let checkpoint = await this.getCheckpoint(options);
116
+ const map = new Map<string, string>([[bucket, start]]);
117
+ const batch = this.storage!.getBucketDataBatch(checkpoint, map);
118
+ const batches = await fromAsync(batch);
119
+ return batches[0]?.batch.data ?? [];
120
+ }
121
+ }
122
+
123
+ export async function getClientCheckpoint(
124
+ connection: mysqlPromise.Connection,
125
+ bucketStorage: BucketStorageFactory,
126
+ options?: { timeout?: number }
127
+ ): Promise<OpId> {
128
+ const start = Date.now();
129
+ const gtid = await readExecutedGtid(connection);
130
+ // This old API needs a persisted checkpoint id.
131
+ // Since we don't use LSNs anymore, the only way to get that is to wait.
132
+
133
+ const timeout = options?.timeout ?? 50_000;
134
+ let lastCp: ActiveCheckpoint | null = null;
135
+
136
+ logger.info('Expected Checkpoint: ' + gtid.comparable);
137
+ while (Date.now() - start < timeout) {
138
+ const cp = await bucketStorage.getActiveCheckpoint();
139
+ lastCp = cp;
140
+ //logger.info('Last Checkpoint: ' + lastCp.lsn);
141
+ if (!cp.hasSyncRules()) {
142
+ throw new Error('No sync rules available');
143
+ }
144
+ if (cp.lsn && cp.lsn >= gtid.comparable) {
145
+ return cp.checkpoint;
146
+ }
147
+
148
+ await new Promise((resolve) => setTimeout(resolve, 30));
149
+ }
150
+
151
+ throw new Error(`Timeout while waiting for checkpoint ${gtid.comparable}. Last checkpoint: ${lastCp?.lsn}`);
152
+ }
@@ -0,0 +1,7 @@
1
+ import { utils } from '@powersync/lib-services-framework';
2
+
3
+ export const env = utils.collectEnvironmentVariables({
4
+ MYSQL_TEST_URI: utils.type.string.default('mysql://myuser:mypassword@localhost:3306/mydatabase'),
5
+ CI: utils.type.boolean.default('false'),
6
+ SLOW_TESTS: utils.type.boolean.default('false')
7
+ });
@@ -0,0 +1,7 @@
1
+ import { container } from '@powersync/lib-services-framework';
2
+ import { beforeAll } from 'vitest';
3
+
4
+ beforeAll(() => {
5
+ // Executes for every test file
6
+ container.registerDefaults();
7
+ });
@@ -0,0 +1,62 @@
1
+ import * as types from '@module/types/types.js';
2
+ import { BucketStorageFactory, Metrics, MongoBucketStorage } from '@powersync/service-core';
3
+ import { env } from './env.js';
4
+ import mysqlPromise from 'mysql2/promise';
5
+ import { connectMongo } from '@core-tests/util.js';
6
+ import { getMySQLVersion } from '@module/common/check-source-configuration.js';
7
+ import { gte } from 'semver';
8
+ import { RowDataPacket } from 'mysql2';
9
+
10
+ // The metrics need to be initialized before they can be used
11
+ await Metrics.initialise({
12
+ disable_telemetry_sharing: true,
13
+ powersync_instance_id: 'test',
14
+ internal_metrics_endpoint: 'unused.for.tests.com'
15
+ });
16
+ Metrics.getInstance().resetCounters();
17
+
18
+ export const TEST_URI = env.MYSQL_TEST_URI;
19
+
20
+ export const TEST_CONNECTION_OPTIONS = types.normalizeConnectionConfig({
21
+ type: 'mysql',
22
+ uri: TEST_URI
23
+ });
24
+
25
+ export type StorageFactory = () => Promise<BucketStorageFactory>;
26
+
27
+ export const INITIALIZED_MONGO_STORAGE_FACTORY: StorageFactory = async () => {
28
+ const db = await connectMongo();
29
+
30
+ // None of the tests insert data into this collection, so it was never created
31
+ if (!(await db.db.listCollections({ name: db.bucket_parameters.collectionName }).hasNext())) {
32
+ await db.db.createCollection('bucket_parameters');
33
+ }
34
+
35
+ await db.clear();
36
+
37
+ return new MongoBucketStorage(db, { slot_name_prefix: 'test_' });
38
+ };
39
+
40
+ export async function clearAndRecreateTestDb(connection: mysqlPromise.Connection) {
41
+ const version = await getMySQLVersion(connection);
42
+ if (gte(version, '8.4.0')) {
43
+ await connection.query('RESET BINARY LOGS AND GTIDS');
44
+ } else {
45
+ await connection.query('RESET MASTER');
46
+ }
47
+
48
+ // await connection.query(`DROP DATABASE IF EXISTS ${TEST_CONNECTION_OPTIONS.database}`);
49
+ //
50
+ // await connection.query(`CREATE DATABASE IF NOT EXISTS ${TEST_CONNECTION_OPTIONS.database}`);
51
+
52
+ const [result] = await connection.query<RowDataPacket[]>(
53
+ `SELECT TABLE_NAME FROM information_schema.tables
54
+ WHERE TABLE_SCHEMA = '${TEST_CONNECTION_OPTIONS.database}'`
55
+ );
56
+ for (let row of result) {
57
+ const name = row.TABLE_NAME;
58
+ if (name.startsWith('test_')) {
59
+ await connection.query(`DROP TABLE ${name}`);
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "baseUrl": "./",
6
+ "noEmit": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "sourceMap": true,
10
+ "paths": {
11
+ "@/*": ["../../../packages/service-core/src/*"],
12
+ "@module/*": ["../src/*"],
13
+ "@core-tests/*": ["../../../packages/service-core/test/src/*"]
14
+ }
15
+ },
16
+ "include": ["src"],
17
+ "references": [
18
+ {
19
+ "path": "../"
20
+ },
21
+ {
22
+ "path": "../../../packages/service-core/test"
23
+ },
24
+ {
25
+ "path": "../../../packages/service-core/"
26
+ }
27
+ ]
28
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "esModuleInterop": true,
7
+ "skipLibCheck": true,
8
+ "sourceMap": true,
9
+ "typeRoots": ["./node_modules/@types", "./src/replication/zongji.d.ts"]
10
+ },
11
+ "include": ["src"],
12
+ "references": [
13
+ {
14
+ "path": "../../packages/types"
15
+ },
16
+ {
17
+ "path": "../../packages/sync-rules"
18
+ },
19
+ {
20
+ "path": "../../packages/service-core"
21
+ },
22
+ {
23
+ "path": "../../libs/lib-services"
24
+ }
25
+ ]
26
+ }