@powersync/service-module-mysql 0.0.0-dev-20241021185145 → 0.0.0-dev-20241023191639

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.
@@ -2,15 +2,16 @@ import { logger } from '@powersync/lib-services-framework';
2
2
  import * as sync_rules from '@powersync/service-sync-rules';
3
3
  import async from 'async';
4
4
 
5
- import { framework, getUuidReplicaIdentityBson, Metrics, storage } from '@powersync/service-core';
6
- import mysql from 'mysql2';
5
+ import { ColumnDescriptor, framework, getUuidReplicaIdentityBson, Metrics, storage } from '@powersync/service-core';
6
+ import mysql, { FieldPacket } from 'mysql2';
7
7
 
8
- import { BinLogEvent } from '@powersync/mysql-zongji';
8
+ import { BinLogEvent, TableMapEntry } from '@powersync/mysql-zongji';
9
9
  import * as common from '../common/common-index.js';
10
10
  import * as zongji_utils from './zongji/zongji-utils.js';
11
11
  import { MySQLConnectionManager } from './MySQLConnectionManager.js';
12
- import { ReplicatedGTID } from '../common/common-index.js';
12
+ import { isBinlogStillAvailable, ReplicatedGTID } from '../common/common-index.js';
13
13
  import mysqlPromise from 'mysql2/promise';
14
+ import { MySQLTypesMap } from '../utils/mysql_utils.js';
14
15
 
15
16
  export interface BinLogStreamOptions {
16
17
  connections: MySQLConnectionManager;
@@ -30,6 +31,7 @@ interface WriteChangePayload {
30
31
  database: string;
31
32
  table: string;
32
33
  sourceTable: storage.SourceTable;
34
+ columns: Map<string, ColumnDescriptor>;
33
35
  }
34
36
 
35
37
  export type Data = Record<string, any>;
@@ -211,10 +213,23 @@ AND table_type = 'BASE TABLE';`,
211
213
  */
212
214
  protected async checkInitialReplicated(): Promise<boolean> {
213
215
  const status = await this.storage.getStatus();
216
+ const lastKnowGTID = status.checkpoint_lsn ? common.ReplicatedGTID.fromSerialized(status.checkpoint_lsn) : null;
214
217
  if (status.snapshot_done && status.checkpoint_lsn) {
215
- logger.info(`Initial replication already done. MySQL appears healthy`);
218
+ logger.info(`Initial replication already done.`);
219
+
220
+ if (lastKnowGTID) {
221
+ // Check if the binlog is still available. If it isn't we need to snapshot again.
222
+ const connection = await this.connections.getConnection();
223
+ try {
224
+ return await isBinlogStillAvailable(connection, lastKnowGTID.position.filename);
225
+ } finally {
226
+ connection.release();
227
+ }
228
+ }
229
+
216
230
  return true;
217
231
  }
232
+
218
233
  return false;
219
234
  }
220
235
 
@@ -270,17 +285,24 @@ AND table_type = 'BASE TABLE';`,
270
285
  logger.info(`Replicating ${table.qualifiedName}`);
271
286
  // TODO count rows and log progress at certain batch sizes
272
287
 
288
+ const columns = new Map<string, ColumnDescriptor>();
273
289
  return new Promise<void>((resolve, reject) => {
274
290
  // MAX_EXECUTION_TIME(0) hint disables execution timeout for this query
275
291
  connection
276
292
  .query(`SELECT /*+ MAX_EXECUTION_TIME(0) */ * FROM ${table.schema}.${table.table}`)
277
- .stream()
278
293
  .on('error', (err) => {
279
294
  reject(err);
280
295
  })
281
- .on('data', async (row) => {
296
+ .on('fields', (fields: FieldPacket[]) => {
297
+ // Map the columns and their types
298
+ fields.forEach((field) => {
299
+ const columnType = MySQLTypesMap[field.type as number];
300
+ columns.set(field.name, { name: field.name, type: columnType, typeId: field.type });
301
+ });
302
+ })
303
+ .on('result', async (row) => {
282
304
  connection.pause();
283
- const record = common.toSQLiteRow(row);
305
+ const record = common.toSQLiteRow(row, columns);
284
306
 
285
307
  await batch.save({
286
308
  tag: storage.SaveOperationTag.INSERT,
@@ -291,7 +313,6 @@ AND table_type = 'BASE TABLE';`,
291
313
  afterReplicaId: getUuidReplicaIdentityBson(record, table.replicaIdColumns)
292
314
  });
293
315
  connection.resume();
294
- // TODO: These metrics can probably be reported in batches
295
316
  Metrics.getInstance().rows_replicated_total.add(1);
296
317
  })
297
318
  .on('end', async function () {
@@ -382,19 +403,11 @@ AND table_type = 'BASE TABLE';`,
382
403
  binLogPositionState.offset = evt.position;
383
404
  break;
384
405
  case zongji_utils.eventIsWriteMutation(evt):
385
- // TODO, can multiple tables be present?
386
406
  const writeTableInfo = evt.tableMap[evt.tableId];
387
407
  await this.writeChanges(batch, {
388
408
  type: storage.SaveOperationTag.INSERT,
389
409
  data: evt.rows,
390
- database: writeTableInfo.parentSchema,
391
- table: writeTableInfo.tableName,
392
- sourceTable: this.getTable(
393
- getMysqlRelId({
394
- schema: writeTableInfo.parentSchema,
395
- name: writeTableInfo.tableName
396
- })
397
- )
410
+ tableEntry: writeTableInfo
398
411
  });
399
412
  break;
400
413
  case zongji_utils.eventIsUpdateMutation(evt):
@@ -403,31 +416,15 @@ AND table_type = 'BASE TABLE';`,
403
416
  type: storage.SaveOperationTag.UPDATE,
404
417
  data: evt.rows.map((row) => row.after),
405
418
  previous_data: evt.rows.map((row) => row.before),
406
- database: updateTableInfo.parentSchema,
407
- table: updateTableInfo.tableName,
408
- sourceTable: this.getTable(
409
- getMysqlRelId({
410
- schema: updateTableInfo.parentSchema,
411
- name: updateTableInfo.tableName
412
- })
413
- )
419
+ tableEntry: updateTableInfo
414
420
  });
415
421
  break;
416
422
  case zongji_utils.eventIsDeleteMutation(evt):
417
- // TODO, can multiple tables be present?
418
423
  const deleteTableInfo = evt.tableMap[evt.tableId];
419
424
  await this.writeChanges(batch, {
420
425
  type: storage.SaveOperationTag.DELETE,
421
426
  data: evt.rows,
422
- database: deleteTableInfo.parentSchema,
423
- table: deleteTableInfo.tableName,
424
- // TODO cleanup
425
- sourceTable: this.getTable(
426
- getMysqlRelId({
427
- schema: deleteTableInfo.parentSchema,
428
- name: deleteTableInfo.tableName
429
- })
430
- )
427
+ tableEntry: deleteTableInfo
431
428
  });
432
429
  break;
433
430
  case zongji_utils.eventIsXid(evt):
@@ -503,14 +500,26 @@ AND table_type = 'BASE TABLE';`,
503
500
  type: storage.SaveOperationTag;
504
501
  data: Data[];
505
502
  previous_data?: Data[];
506
- database: string;
507
- table: string;
508
- sourceTable: storage.SourceTable;
503
+ tableEntry: TableMapEntry;
509
504
  }
510
505
  ): Promise<storage.FlushedResult | null> {
506
+ const columns = new Map<string, ColumnDescriptor>();
507
+ msg.tableEntry.columns.forEach((column) => {
508
+ columns.set(column.name, { name: column.name, typeId: column.type });
509
+ });
510
+
511
511
  for (const [index, row] of msg.data.entries()) {
512
512
  await this.writeChange(batch, {
513
- ...msg,
513
+ type: msg.type,
514
+ database: msg.tableEntry.parentSchema,
515
+ sourceTable: this.getTable(
516
+ getMysqlRelId({
517
+ schema: msg.tableEntry.parentSchema,
518
+ name: msg.tableEntry.tableName
519
+ })
520
+ ),
521
+ table: msg.tableEntry.tableName,
522
+ columns: columns,
514
523
  data: row,
515
524
  previous_data: msg.previous_data?.[index]
516
525
  });
@@ -525,7 +534,7 @@ AND table_type = 'BASE TABLE';`,
525
534
  switch (payload.type) {
526
535
  case storage.SaveOperationTag.INSERT:
527
536
  Metrics.getInstance().rows_replicated_total.add(1);
528
- const record = common.toSQLiteRow(payload.data);
537
+ const record = common.toSQLiteRow(payload.data, payload.columns);
529
538
  return await batch.save({
530
539
  tag: storage.SaveOperationTag.INSERT,
531
540
  sourceTable: payload.sourceTable,
@@ -4,6 +4,7 @@ declare module '@powersync/mysql-zongji' {
4
4
  user: string;
5
5
  password: string;
6
6
  dateStrings?: boolean;
7
+ timeZone?: string;
7
8
  };
8
9
 
9
10
  interface DatabaseFilter {
@@ -31,6 +32,11 @@ declare module '@powersync/mysql-zongji' {
31
32
  * BinLog position offset to start reading events from in file specified
32
33
  */
33
34
  position?: number;
35
+
36
+ /**
37
+ * Unique server ID for this replication client.
38
+ */
39
+ serverId?: number;
34
40
  };
35
41
 
36
42
  export type ColumnSchema = {
@@ -49,10 +55,10 @@ declare module '@powersync/mysql-zongji' {
49
55
  };
50
56
 
51
57
  export type TableMapEntry = {
52
- columnSchemas: Array<ColumnSchema>;
58
+ columnSchemas: ColumnSchema[];
53
59
  parentSchema: string;
54
60
  tableName: string;
55
- columns: Array<ColumnDefinition>;
61
+ columns: ColumnDefinition[];
56
62
  };
57
63
 
58
64
  export type BaseBinLogEvent = {
@@ -90,14 +96,14 @@ declare module '@powersync/mysql-zongji' {
90
96
  tableId: number;
91
97
  numberOfColumns: number;
92
98
  tableMap: Record<string, TableMapEntry>;
93
- rows: Array<Record<string, any>>;
99
+ rows: Record<string, any>[];
94
100
  };
95
101
 
96
102
  export type BinLogUpdateEvent = Omit<BinLogMutationEvent, 'rows'> & {
97
- rows: Array<{
103
+ rows: {
98
104
  before: Record<string, any>;
99
105
  after: Record<string, any>;
100
- }>;
106
+ }[];
101
107
  };
102
108
 
103
109
  export type BinLogEvent = BinLogRotationEvent | BinLogGTIDLogEvent | BinLogXidEvent | BinLogMutationEvent;
@@ -14,6 +14,7 @@ export interface NormalizedMySQLConnectionConfig {
14
14
 
15
15
  username: string;
16
16
  password: string;
17
+ server_id: number;
17
18
 
18
19
  cacert?: string;
19
20
  client_certificate?: string;
@@ -29,6 +30,7 @@ export const MySQLConnectionConfig = service_types.configFile.DataSourceConfig.a
29
30
  username: t.string.optional(),
30
31
  password: t.string.optional(),
31
32
  database: t.string.optional(),
33
+ server_id: t.number.optional(),
32
34
 
33
35
  cacert: t.string.optional(),
34
36
  client_certificate: t.string.optional(),
@@ -97,6 +99,8 @@ export function normalizeConnectionConfig(options: MySQLConnectionConfig): Norma
97
99
  database,
98
100
 
99
101
  username,
100
- password
102
+ password,
103
+
104
+ server_id: options.server_id ?? 1
101
105
  };
102
106
  }
@@ -1,8 +1,13 @@
1
1
  import { logger } from '@powersync/lib-services-framework';
2
- import mysql from 'mysql2';
2
+ import mysql, { Types } from 'mysql2';
3
3
  import mysqlPromise from 'mysql2/promise';
4
4
  import * as types from '../types/types.js';
5
5
 
6
+ export const MySQLTypesMap: { [key: number]: string } = {};
7
+ for (const [name, code] of Object.entries(Types)) {
8
+ MySQLTypesMap[code as number] = name;
9
+ }
10
+
6
11
  export type RetriedQueryOptions = {
7
12
  connection: mysqlPromise.Connection;
8
13
  query: string;
@@ -35,13 +40,14 @@ export function createPool(config: types.NormalizedMySQLConnectionConfig, option
35
40
  cert: config.client_certificate
36
41
  };
37
42
  const hasSSLOptions = Object.values(sslOptions).some((v) => !!v);
38
- // TODO confirm if default options are fine for Powersync use case
39
43
  return mysql.createPool({
40
44
  host: config.hostname,
41
45
  user: config.username,
42
46
  password: config.password,
43
47
  database: config.database,
44
48
  ssl: hasSSLOptions ? sslOptions : undefined,
49
+ supportBigNumbers: true,
50
+ timezone: 'Z', // Ensure no auto timezone manipulation of the dates occur
45
51
  ...(options || {})
46
52
  });
47
53
  }