@powersync/service-module-mysql 0.7.4 → 0.9.0
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 +61 -0
- package/LICENSE +3 -3
- package/dev/docker/mysql/init-scripts/my.cnf +1 -3
- package/dist/api/MySQLRouteAPIAdapter.js +12 -4
- package/dist/api/MySQLRouteAPIAdapter.js.map +1 -1
- package/dist/common/ReplicatedGTID.js +4 -0
- package/dist/common/ReplicatedGTID.js.map +1 -1
- package/dist/common/common-index.d.ts +1 -2
- package/dist/common/common-index.js +1 -2
- package/dist/common/common-index.js.map +1 -1
- package/dist/common/mysql-to-sqlite.d.ts +1 -1
- package/dist/common/mysql-to-sqlite.js +4 -0
- package/dist/common/mysql-to-sqlite.js.map +1 -1
- package/dist/common/schema-utils.d.ts +20 -0
- package/dist/common/{get-replication-columns.js → schema-utils.js} +73 -30
- package/dist/common/schema-utils.js.map +1 -0
- package/dist/replication/BinLogReplicationJob.js +4 -1
- package/dist/replication/BinLogReplicationJob.js.map +1 -1
- package/dist/replication/BinLogStream.d.ts +9 -6
- package/dist/replication/BinLogStream.js +117 -73
- package/dist/replication/BinLogStream.js.map +1 -1
- package/dist/replication/zongji/BinLogListener.d.ts +60 -6
- package/dist/replication/zongji/BinLogListener.js +347 -89
- package/dist/replication/zongji/BinLogListener.js.map +1 -1
- package/dist/replication/zongji/zongji-utils.d.ts +4 -1
- package/dist/replication/zongji/zongji-utils.js +9 -0
- package/dist/replication/zongji/zongji-utils.js.map +1 -1
- package/dist/types/node-sql-parser-extended-types.d.ts +31 -0
- package/dist/types/node-sql-parser-extended-types.js +2 -0
- package/dist/types/node-sql-parser-extended-types.js.map +1 -0
- package/dist/utils/mysql-utils.d.ts +4 -2
- package/dist/utils/mysql-utils.js +15 -3
- package/dist/utils/mysql-utils.js.map +1 -1
- package/dist/utils/parser-utils.d.ts +16 -0
- package/dist/utils/parser-utils.js +58 -0
- package/dist/utils/parser-utils.js.map +1 -0
- package/package.json +12 -11
- package/src/api/MySQLRouteAPIAdapter.ts +15 -4
- package/src/common/ReplicatedGTID.ts +6 -1
- package/src/common/common-index.ts +1 -2
- package/src/common/mysql-to-sqlite.ts +7 -1
- package/src/common/{get-replication-columns.ts → schema-utils.ts} +96 -37
- package/src/replication/BinLogReplicationJob.ts +4 -1
- package/src/replication/BinLogStream.ts +139 -94
- package/src/replication/zongji/BinLogListener.ts +421 -100
- package/src/replication/zongji/zongji-utils.ts +16 -1
- package/src/types/node-sql-parser-extended-types.ts +25 -0
- package/src/utils/mysql-utils.ts +19 -4
- package/src/utils/parser-utils.ts +73 -0
- package/test/src/BinLogListener.test.ts +421 -77
- package/test/src/BinLogStream.test.ts +128 -52
- package/test/src/BinlogStreamUtils.ts +12 -2
- package/test/src/mysql-to-sqlite.test.ts +5 -5
- package/test/src/parser-utils.test.ts +24 -0
- package/test/src/schema-changes.test.ts +659 -0
- package/test/src/util.ts +87 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/common/get-replication-columns.d.ts +0 -12
- package/dist/common/get-replication-columns.js.map +0 -1
- package/dist/common/get-tables-from-pattern.d.ts +0 -7
- package/dist/common/get-tables-from-pattern.js +0 -28
- package/dist/common/get-tables-from-pattern.js.map +0 -1
- package/src/common/get-tables-from-pattern.ts +0 -44
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import 'node-sql-parser';
|
|
2
|
+
/**
|
|
3
|
+
* Missing Type definitions for the node-sql-parser
|
|
4
|
+
*/
|
|
5
|
+
declare module 'node-sql-parser' {
|
|
6
|
+
interface RenameStatement {
|
|
7
|
+
type: 'rename';
|
|
8
|
+
table: {
|
|
9
|
+
db: string | null;
|
|
10
|
+
table: string;
|
|
11
|
+
}[][];
|
|
12
|
+
}
|
|
13
|
+
interface TruncateStatement {
|
|
14
|
+
type: 'truncate';
|
|
15
|
+
keyword: 'table';
|
|
16
|
+
name: {
|
|
17
|
+
db: string | null;
|
|
18
|
+
table: string;
|
|
19
|
+
as: string | null;
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
interface DropIndexStatement {
|
|
23
|
+
type: 'drop';
|
|
24
|
+
keyword: 'index';
|
|
25
|
+
table: {
|
|
26
|
+
db: string | null;
|
|
27
|
+
table: string;
|
|
28
|
+
};
|
|
29
|
+
name: any[];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-sql-parser-extended-types.js","sourceRoot":"","sources":["../../src/types/node-sql-parser-extended-types.ts"],"names":[],"mappings":"AAAA,OAAO,iBAAiB,CAAC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import mysql from 'mysql2';
|
|
2
2
|
import mysqlPromise from 'mysql2/promise';
|
|
3
3
|
import * as types from '../types/types.js';
|
|
4
|
-
import {
|
|
4
|
+
import { SourceEntityDescriptor } from '@powersync/service-core';
|
|
5
5
|
export type RetriedQueryOptions = {
|
|
6
6
|
connection: mysqlPromise.Connection;
|
|
7
7
|
query: string;
|
|
@@ -29,4 +29,6 @@ export declare function getMySQLVersion(connection: mysqlPromise.Connection): Pr
|
|
|
29
29
|
* @param minimumVersion
|
|
30
30
|
*/
|
|
31
31
|
export declare function isVersionAtLeast(version: string, minimumVersion: string): boolean;
|
|
32
|
-
export declare function
|
|
32
|
+
export declare function satisfiesVersion(version: string, targetVersion: string): boolean;
|
|
33
|
+
export declare function qualifiedMySQLTable(table: SourceEntityDescriptor): string;
|
|
34
|
+
export declare function qualifiedMySQLTable(table: string, schema: string): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { logger } from '@powersync/lib-services-framework';
|
|
2
2
|
import mysql from 'mysql2';
|
|
3
|
-
import { coerce, gte } from 'semver';
|
|
3
|
+
import { coerce, gte, satisfies } from 'semver';
|
|
4
4
|
/**
|
|
5
5
|
* Retry a simple query - up to 2 attempts total.
|
|
6
6
|
*/
|
|
@@ -69,7 +69,19 @@ export function isVersionAtLeast(version, minimumVersion) {
|
|
|
69
69
|
const coercedMinimumVersion = coerce(minimumVersion);
|
|
70
70
|
return gte(coercedVersion, coercedMinimumVersion, { loose: true });
|
|
71
71
|
}
|
|
72
|
-
export function
|
|
73
|
-
|
|
72
|
+
export function satisfiesVersion(version, targetVersion) {
|
|
73
|
+
const coercedVersion = coerce(version);
|
|
74
|
+
return satisfies(coercedVersion, targetVersion, { loose: true });
|
|
75
|
+
}
|
|
76
|
+
export function qualifiedMySQLTable(table, schema) {
|
|
77
|
+
if (typeof table === 'object') {
|
|
78
|
+
return `\`${table.schema.replaceAll('`', '``')}\`.\`${table.name.replaceAll('`', '``')}\``;
|
|
79
|
+
}
|
|
80
|
+
else if (schema) {
|
|
81
|
+
return `\`${schema.replaceAll('`', '``')}\`.\`${table.replaceAll('`', '``')}\``;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
return `\`${table.replaceAll('`', '``')}\``;
|
|
85
|
+
}
|
|
74
86
|
}
|
|
75
87
|
//# sourceMappingURL=mysql-utils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mysql-utils.js","sourceRoot":"","sources":["../../src/utils/mysql-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AAC3D,OAAO,KAAK,MAAM,QAAQ,CAAC;AAG3B,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"mysql-utils.js","sourceRoot":"","sources":["../../src/utils/mysql-utils.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Alter, AST, Create, Drop, TruncateStatement, RenameStatement, DropIndexStatement } from 'node-sql-parser';
|
|
2
|
+
/**
|
|
3
|
+
* Check if a query is a DDL statement that applies to tables matching any of the provided matcher functions.
|
|
4
|
+
* @param query
|
|
5
|
+
* @param matchers
|
|
6
|
+
*/
|
|
7
|
+
export declare function matchedSchemaChangeQuery(query: string, matchers: ((table: string) => boolean)[]): boolean;
|
|
8
|
+
export declare function isTruncate(statement: AST): statement is TruncateStatement;
|
|
9
|
+
export declare function isRenameTable(statement: AST): statement is RenameStatement;
|
|
10
|
+
export declare function isAlterTable(statement: AST): statement is Alter;
|
|
11
|
+
export declare function isRenameExpression(expression: any): boolean;
|
|
12
|
+
export declare function isColumnExpression(expression: any): boolean;
|
|
13
|
+
export declare function isConstraintExpression(expression: any): boolean;
|
|
14
|
+
export declare function isDropTable(statement: AST): statement is Drop;
|
|
15
|
+
export declare function isDropIndex(statement: AST): statement is DropIndexStatement;
|
|
16
|
+
export declare function isCreateUniqueIndex(statement: AST): statement is Create;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// We ignore create table statements, since even in the worst case we will pick up the changes when row events for that
|
|
2
|
+
// table are received.
|
|
3
|
+
const DDL_KEYWORDS = ['alter table', 'drop table', 'truncate table', 'rename table'];
|
|
4
|
+
/**
|
|
5
|
+
* Check if a query is a DDL statement that applies to tables matching any of the provided matcher functions.
|
|
6
|
+
* @param query
|
|
7
|
+
* @param matchers
|
|
8
|
+
*/
|
|
9
|
+
export function matchedSchemaChangeQuery(query, matchers) {
|
|
10
|
+
// Normalize case and remove backticks for matching
|
|
11
|
+
const normalizedQuery = query.toLowerCase().replace(/`/g, '');
|
|
12
|
+
const isDDLQuery = DDL_KEYWORDS.some((keyword) => normalizedQuery.includes(keyword));
|
|
13
|
+
if (isDDLQuery) {
|
|
14
|
+
const tokens = normalizedQuery.split(/[^a-zA-Z0-9_`]+/);
|
|
15
|
+
// Check if any matched table names appear in the query
|
|
16
|
+
for (const token of tokens) {
|
|
17
|
+
const matchFound = matchers.some((matcher) => matcher(token));
|
|
18
|
+
if (matchFound) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
// @ts-ignore
|
|
26
|
+
export function isTruncate(statement) {
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
return statement.type === 'truncate';
|
|
29
|
+
}
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
export function isRenameTable(statement) {
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
return statement.type === 'rename';
|
|
34
|
+
}
|
|
35
|
+
export function isAlterTable(statement) {
|
|
36
|
+
return statement.type === 'alter';
|
|
37
|
+
}
|
|
38
|
+
export function isRenameExpression(expression) {
|
|
39
|
+
return expression.resource === 'table' && expression.action === 'rename';
|
|
40
|
+
}
|
|
41
|
+
export function isColumnExpression(expression) {
|
|
42
|
+
return expression.resource === 'column';
|
|
43
|
+
}
|
|
44
|
+
export function isConstraintExpression(expression) {
|
|
45
|
+
return ((expression.resource === 'key' && expression.keyword === 'primary key') ||
|
|
46
|
+
expression.resource === 'constraint' ||
|
|
47
|
+
(expression.resource === 'index' && expression.action === 'drop'));
|
|
48
|
+
}
|
|
49
|
+
export function isDropTable(statement) {
|
|
50
|
+
return statement.type === 'drop' && statement.keyword === 'table';
|
|
51
|
+
}
|
|
52
|
+
export function isDropIndex(statement) {
|
|
53
|
+
return statement.type === 'drop' && statement.keyword === 'index';
|
|
54
|
+
}
|
|
55
|
+
export function isCreateUniqueIndex(statement) {
|
|
56
|
+
return statement.type === 'create' && statement.keyword === 'index' && statement.index_type === 'unique';
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=parser-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser-utils.js","sourceRoot":"","sources":["../../src/utils/parser-utils.ts"],"names":[],"mappings":"AAEA,uHAAuH;AACvH,sBAAsB;AACtB,MAAM,YAAY,GAAG,CAAC,aAAa,EAAE,YAAY,EAAE,gBAAgB,EAAE,cAAc,CAAC,CAAC;AAErF;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAa,EAAE,QAAwC;IAC9F,mDAAmD;IACnD,MAAM,eAAe,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAE9D,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,eAAe,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IACrF,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;QACxD,uDAAuD;QACvD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;YAC9D,IAAI,UAAU,EAAE,CAAC;gBACf,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,aAAa;AACb,MAAM,UAAU,UAAU,CAAC,SAAc;IACvC,aAAa;IACb,OAAO,SAAS,CAAC,IAAI,KAAK,UAAU,CAAC;AACvC,CAAC;AAED,aAAa;AACb,MAAM,UAAU,aAAa,CAAC,SAAc;IAC1C,aAAa;IACb,OAAO,SAAS,CAAC,IAAI,KAAK,QAAQ,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,SAAc;IACzC,OAAO,SAAS,CAAC,IAAI,KAAK,OAAO,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,UAAe;IAChD,OAAO,UAAU,CAAC,QAAQ,KAAK,OAAO,IAAI,UAAU,CAAC,MAAM,KAAK,QAAQ,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,UAAe;IAChD,OAAO,UAAU,CAAC,QAAQ,KAAK,QAAQ,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,UAAe;IACpD,OAAO,CACL,CAAC,UAAU,CAAC,QAAQ,KAAK,KAAK,IAAI,UAAU,CAAC,OAAO,KAAK,aAAa,CAAC;QACvE,UAAU,CAAC,QAAQ,KAAK,YAAY;QACpC,CAAC,UAAU,CAAC,QAAQ,KAAK,OAAO,IAAI,UAAU,CAAC,MAAM,KAAK,MAAM,CAAC,CAClE,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,SAAc;IACxC,OAAO,SAAS,CAAC,IAAI,KAAK,MAAM,IAAI,SAAS,CAAC,OAAO,KAAK,OAAO,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,SAAc;IACxC,OAAO,SAAS,CAAC,IAAI,KAAK,MAAM,IAAI,SAAS,CAAC,OAAO,KAAK,OAAO,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,SAAc;IAChD,OAAO,SAAS,CAAC,IAAI,KAAK,QAAQ,IAAI,SAAS,CAAC,OAAO,KAAK,OAAO,IAAI,SAAS,CAAC,UAAU,KAAK,QAAQ,CAAC;AAC3G,CAAC"}
|
package/package.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"name": "@powersync/service-module-mysql",
|
|
3
3
|
"repository": "https://github.com/powersync-ja/powersync-service",
|
|
4
4
|
"types": "dist/index.d.ts",
|
|
5
|
-
"version": "0.
|
|
6
|
-
"license": "FSL-1.1-
|
|
5
|
+
"version": "0.9.0",
|
|
6
|
+
"license": "FSL-1.1-ALv2",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"publishConfig": {
|
|
@@ -22,25 +22,26 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@powersync/mysql-zongji": "0.
|
|
25
|
+
"@powersync/mysql-zongji": "^0.5.0",
|
|
26
26
|
"async": "^3.2.4",
|
|
27
27
|
"mysql2": "^3.11.0",
|
|
28
|
+
"node-sql-parser": "^5.3.9",
|
|
28
29
|
"semver": "^7.5.4",
|
|
29
30
|
"ts-codec": "^1.3.0",
|
|
30
31
|
"uri-js": "^4.4.1",
|
|
31
32
|
"uuid": "^11.1.0",
|
|
32
|
-
"@powersync/lib-services-framework": "0.7.
|
|
33
|
-
"@powersync/service-core": "1.
|
|
34
|
-
"@powersync/service-sync-rules": "0.
|
|
35
|
-
"@powersync/service-types": "0.
|
|
36
|
-
"@powersync/service-jsonbig": "0.17.
|
|
33
|
+
"@powersync/lib-services-framework": "0.7.3",
|
|
34
|
+
"@powersync/service-core": "1.15.0",
|
|
35
|
+
"@powersync/service-sync-rules": "0.29.0",
|
|
36
|
+
"@powersync/service-types": "0.13.0",
|
|
37
|
+
"@powersync/service-jsonbig": "0.17.11"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@types/async": "^3.2.24",
|
|
40
41
|
"@types/semver": "^7.5.4",
|
|
41
|
-
"@powersync/service-core-tests": "0.
|
|
42
|
-
"@powersync/service-module-mongodb-storage": "0.
|
|
43
|
-
"@powersync/service-module-postgres-storage": "0.
|
|
42
|
+
"@powersync/service-core-tests": "0.12.0",
|
|
43
|
+
"@powersync/service-module-mongodb-storage": "0.12.0",
|
|
44
|
+
"@powersync/service-module-postgres-storage": "0.10.0"
|
|
44
45
|
},
|
|
45
46
|
"scripts": {
|
|
46
47
|
"build": "tsc -b",
|
|
@@ -102,7 +102,10 @@ export class MySQLRouteAPIAdapter implements api.RouteAPI {
|
|
|
102
102
|
*/
|
|
103
103
|
return fields.map((c) => {
|
|
104
104
|
const value = row[c.name];
|
|
105
|
-
const sqlValue = sync_rules.
|
|
105
|
+
const sqlValue = sync_rules.applyValueContext(
|
|
106
|
+
sync_rules.toSyncRulesValue(value),
|
|
107
|
+
sync_rules.CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY
|
|
108
|
+
);
|
|
106
109
|
if (typeof sqlValue == 'bigint') {
|
|
107
110
|
return Number(value);
|
|
108
111
|
} else if (value instanceof Date) {
|
|
@@ -208,7 +211,7 @@ export class MySQLRouteAPIAdapter implements api.RouteAPI {
|
|
|
208
211
|
idColumnsResult = await common.getReplicationIdentityColumns({
|
|
209
212
|
connection: connection,
|
|
210
213
|
schema,
|
|
211
|
-
|
|
214
|
+
tableName: tableName
|
|
212
215
|
});
|
|
213
216
|
} catch (ex) {
|
|
214
217
|
idColumnsError = { level: 'fatal', message: ex.message };
|
|
@@ -217,7 +220,15 @@ export class MySQLRouteAPIAdapter implements api.RouteAPI {
|
|
|
217
220
|
}
|
|
218
221
|
|
|
219
222
|
const idColumns = idColumnsResult?.columns ?? [];
|
|
220
|
-
const sourceTable = new storage.SourceTable(
|
|
223
|
+
const sourceTable = new storage.SourceTable({
|
|
224
|
+
id: 0,
|
|
225
|
+
connectionTag: this.config.tag,
|
|
226
|
+
objectId: tableName,
|
|
227
|
+
schema: schema,
|
|
228
|
+
name: tableName,
|
|
229
|
+
replicaIdColumns: idColumns,
|
|
230
|
+
snapshotComplete: true
|
|
231
|
+
});
|
|
221
232
|
const syncData = syncRules.tableSyncsData(sourceTable);
|
|
222
233
|
const syncParameters = syncRules.tableSyncsParameters(sourceTable);
|
|
223
234
|
|
|
@@ -232,7 +243,7 @@ export class MySQLRouteAPIAdapter implements api.RouteAPI {
|
|
|
232
243
|
let selectError: service_types.ReplicationError | null = null;
|
|
233
244
|
try {
|
|
234
245
|
await this.retriedQuery({
|
|
235
|
-
query: `SELECT * FROM ${sourceTable.
|
|
246
|
+
query: `SELECT * FROM ${sourceTable.name} LIMIT 1`
|
|
236
247
|
});
|
|
237
248
|
} catch (e) {
|
|
238
249
|
selectError = { level: 'fatal', message: e.message };
|
|
@@ -92,10 +92,15 @@ export class ReplicatedGTID {
|
|
|
92
92
|
* @returns A comparable string in the format
|
|
93
93
|
* `padded_end_transaction|raw_gtid|binlog_filename|binlog_position`
|
|
94
94
|
*/
|
|
95
|
-
get comparable() {
|
|
95
|
+
get comparable(): string {
|
|
96
96
|
const { raw, position } = this;
|
|
97
97
|
const [, transactionRanges] = this.raw.split(':');
|
|
98
98
|
|
|
99
|
+
// This means no transactions have been executed on the database yet
|
|
100
|
+
if (!transactionRanges) {
|
|
101
|
+
return ReplicatedGTID.ZERO.comparable;
|
|
102
|
+
}
|
|
103
|
+
|
|
99
104
|
let maxTransactionId = 0;
|
|
100
105
|
|
|
101
106
|
for (const range of transactionRanges.split(',')) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export * from './check-source-configuration.js';
|
|
2
|
-
export * from './
|
|
3
|
-
export * from './get-tables-from-pattern.js';
|
|
2
|
+
export * from './schema-utils.js';
|
|
4
3
|
export * from './mysql-to-sqlite.js';
|
|
5
4
|
export * from './read-executed-gtid.js';
|
|
6
5
|
export * from './ReplicatedGTID.js';
|
|
@@ -103,7 +103,10 @@ export function toColumnDescriptorFromDefinition(column: ColumnDefinition): Colu
|
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
export function toSQLiteRow(
|
|
106
|
+
export function toSQLiteRow(
|
|
107
|
+
row: Record<string, any>,
|
|
108
|
+
columns: Map<string, ColumnDescriptor>
|
|
109
|
+
): sync_rules.SqliteInputRow {
|
|
107
110
|
let result: sync_rules.DatabaseInputRow = {};
|
|
108
111
|
for (let key in row) {
|
|
109
112
|
// We are very much expecting the column to be there
|
|
@@ -183,6 +186,9 @@ export function toSQLiteRow(row: Record<string, any>, columns: Map<string, Colum
|
|
|
183
186
|
result[key] = row[key];
|
|
184
187
|
break;
|
|
185
188
|
}
|
|
189
|
+
} else {
|
|
190
|
+
// If the value is null, we just set it to null
|
|
191
|
+
result[key] = null;
|
|
186
192
|
}
|
|
187
193
|
}
|
|
188
194
|
return sync_rules.toSyncRulesRow(result);
|
|
@@ -1,23 +1,64 @@
|
|
|
1
|
-
import { storage } from '@powersync/service-core';
|
|
2
1
|
import mysqlPromise from 'mysql2/promise';
|
|
3
2
|
import * as mysql_utils from '../utils/mysql-utils.js';
|
|
3
|
+
import { ColumnDescriptor } from '@powersync/service-core';
|
|
4
|
+
import { TablePattern } from '@powersync/service-sync-rules';
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
+
export interface GetColumnsOptions {
|
|
6
7
|
connection: mysqlPromise.Connection;
|
|
7
8
|
schema: string;
|
|
8
|
-
|
|
9
|
-
}
|
|
9
|
+
tableName: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function getColumns(options: GetColumnsOptions): Promise<ColumnDescriptor[]> {
|
|
13
|
+
const { connection, schema, tableName } = options;
|
|
14
|
+
|
|
15
|
+
const [allColumns] = await mysql_utils.retriedQuery({
|
|
16
|
+
connection: connection,
|
|
17
|
+
query: `
|
|
18
|
+
SELECT
|
|
19
|
+
s.COLUMN_NAME AS name,
|
|
20
|
+
c.DATA_TYPE as type
|
|
21
|
+
FROM
|
|
22
|
+
INFORMATION_SCHEMA.COLUMNS s
|
|
23
|
+
JOIN
|
|
24
|
+
INFORMATION_SCHEMA.COLUMNS c
|
|
25
|
+
ON
|
|
26
|
+
s.TABLE_SCHEMA = c.TABLE_SCHEMA
|
|
27
|
+
AND s.TABLE_NAME = c.TABLE_NAME
|
|
28
|
+
AND s.COLUMN_NAME = c.COLUMN_NAME
|
|
29
|
+
WHERE
|
|
30
|
+
s.TABLE_SCHEMA = ?
|
|
31
|
+
AND s.TABLE_NAME = ?
|
|
32
|
+
ORDER BY
|
|
33
|
+
s.ORDINAL_POSITION;
|
|
34
|
+
`,
|
|
35
|
+
params: [schema, tableName]
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return allColumns.map((row) => {
|
|
39
|
+
return {
|
|
40
|
+
name: row.name,
|
|
41
|
+
type: row.type
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface GetReplicationIdentityColumnsOptions {
|
|
47
|
+
connection: mysqlPromise.Connection;
|
|
48
|
+
schema: string;
|
|
49
|
+
tableName: string;
|
|
50
|
+
}
|
|
10
51
|
|
|
11
|
-
export
|
|
12
|
-
columns:
|
|
52
|
+
export interface ReplicationIdentityColumnsResult {
|
|
53
|
+
columns: ColumnDescriptor[];
|
|
13
54
|
// TODO maybe export an enum from the core package
|
|
14
55
|
identity: string;
|
|
15
|
-
}
|
|
56
|
+
}
|
|
16
57
|
|
|
17
58
|
export async function getReplicationIdentityColumns(
|
|
18
|
-
options:
|
|
59
|
+
options: GetReplicationIdentityColumnsOptions
|
|
19
60
|
): Promise<ReplicationIdentityColumnsResult> {
|
|
20
|
-
const { connection, schema,
|
|
61
|
+
const { connection, schema, tableName } = options;
|
|
21
62
|
const [primaryKeyColumns] = await mysql_utils.retriedQuery({
|
|
22
63
|
connection: connection,
|
|
23
64
|
query: `
|
|
@@ -39,7 +80,7 @@ export async function getReplicationIdentityColumns(
|
|
|
39
80
|
ORDER BY
|
|
40
81
|
s.SEQ_IN_INDEX;
|
|
41
82
|
`,
|
|
42
|
-
params: [schema,
|
|
83
|
+
params: [schema, tableName]
|
|
43
84
|
});
|
|
44
85
|
|
|
45
86
|
if (primaryKeyColumns.length) {
|
|
@@ -52,8 +93,7 @@ export async function getReplicationIdentityColumns(
|
|
|
52
93
|
};
|
|
53
94
|
}
|
|
54
95
|
|
|
55
|
-
//
|
|
56
|
-
// No primary key, find the first valid unique key
|
|
96
|
+
// No primary key, check if any of the columns have a unique constraint we can use
|
|
57
97
|
const [uniqueKeyColumns] = await mysql_utils.retriedQuery({
|
|
58
98
|
connection: connection,
|
|
59
99
|
query: `
|
|
@@ -78,7 +118,7 @@ export async function getReplicationIdentityColumns(
|
|
|
78
118
|
AND s.NON_UNIQUE = 0
|
|
79
119
|
ORDER BY s.SEQ_IN_INDEX;
|
|
80
120
|
`,
|
|
81
|
-
params: [schema,
|
|
121
|
+
params: [schema, tableName]
|
|
82
122
|
});
|
|
83
123
|
|
|
84
124
|
if (uniqueKeyColumns.length > 0) {
|
|
@@ -91,34 +131,53 @@ export async function getReplicationIdentityColumns(
|
|
|
91
131
|
};
|
|
92
132
|
}
|
|
93
133
|
|
|
94
|
-
const
|
|
134
|
+
const allColumns = await getColumns({
|
|
95
135
|
connection: connection,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
s.COLUMN_NAME AS name,
|
|
99
|
-
c.DATA_TYPE as type
|
|
100
|
-
FROM
|
|
101
|
-
INFORMATION_SCHEMA.COLUMNS s
|
|
102
|
-
JOIN
|
|
103
|
-
INFORMATION_SCHEMA.COLUMNS c
|
|
104
|
-
ON
|
|
105
|
-
s.TABLE_SCHEMA = c.TABLE_SCHEMA
|
|
106
|
-
AND s.TABLE_NAME = c.TABLE_NAME
|
|
107
|
-
AND s.COLUMN_NAME = c.COLUMN_NAME
|
|
108
|
-
WHERE
|
|
109
|
-
s.TABLE_SCHEMA = ?
|
|
110
|
-
AND s.TABLE_NAME = ?
|
|
111
|
-
ORDER BY
|
|
112
|
-
s.ORDINAL_POSITION;
|
|
113
|
-
`,
|
|
114
|
-
params: [schema, table_name]
|
|
136
|
+
schema: schema,
|
|
137
|
+
tableName: tableName
|
|
115
138
|
});
|
|
116
139
|
|
|
117
140
|
return {
|
|
118
|
-
columns: allColumns
|
|
119
|
-
name: row.name,
|
|
120
|
-
type: row.type
|
|
121
|
-
})),
|
|
141
|
+
columns: allColumns,
|
|
122
142
|
identity: 'full'
|
|
123
143
|
};
|
|
124
144
|
}
|
|
145
|
+
|
|
146
|
+
export async function getTablesFromPattern(
|
|
147
|
+
connection: mysqlPromise.Connection,
|
|
148
|
+
tablePattern: TablePattern
|
|
149
|
+
): Promise<string[]> {
|
|
150
|
+
const schema = tablePattern.schema;
|
|
151
|
+
|
|
152
|
+
if (tablePattern.isWildcard) {
|
|
153
|
+
const [results] = await mysql_utils.retriedQuery({
|
|
154
|
+
connection: connection,
|
|
155
|
+
query: `
|
|
156
|
+
SELECT TABLE_NAME
|
|
157
|
+
FROM information_schema.tables
|
|
158
|
+
WHERE TABLE_SCHEMA = ?
|
|
159
|
+
AND TABLE_NAME LIKE ?
|
|
160
|
+
AND table_type = 'BASE TABLE'
|
|
161
|
+
`,
|
|
162
|
+
params: [schema, tablePattern.tablePattern]
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return results
|
|
166
|
+
.map((row) => row.TABLE_NAME)
|
|
167
|
+
.filter((tableName: string) => tableName.startsWith(tablePattern.tablePrefix));
|
|
168
|
+
} else {
|
|
169
|
+
const [results] = await mysql_utils.retriedQuery({
|
|
170
|
+
connection: connection,
|
|
171
|
+
query: `
|
|
172
|
+
SELECT TABLE_NAME
|
|
173
|
+
FROM information_schema.tables
|
|
174
|
+
WHERE TABLE_SCHEMA = ?
|
|
175
|
+
AND TABLE_NAME = ?
|
|
176
|
+
AND table_type = 'BASE TABLE'
|
|
177
|
+
`,
|
|
178
|
+
params: [schema, tablePattern.tablePattern]
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return results.map((row) => row.TABLE_NAME);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -21,7 +21,9 @@ export class BinLogReplicationJob extends replication.AbstractReplicationJob {
|
|
|
21
21
|
return this.options.storage.slot_name;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
async keepAlive() {
|
|
24
|
+
async keepAlive() {
|
|
25
|
+
// Keepalives are handled by the binlog heartbeat mechanism
|
|
26
|
+
}
|
|
25
27
|
|
|
26
28
|
async replicate() {
|
|
27
29
|
try {
|
|
@@ -56,6 +58,7 @@ export class BinLogReplicationJob extends replication.AbstractReplicationJob {
|
|
|
56
58
|
const connectionManager = this.connectionFactory.create({
|
|
57
59
|
// Pool connections are only used intermittently.
|
|
58
60
|
idleTimeout: 30_000,
|
|
61
|
+
connectionLimit: 2,
|
|
59
62
|
|
|
60
63
|
connectAttributes: {
|
|
61
64
|
// https://dev.mysql.com/doc/refman/8.0/en/performance-schema-connection-attribute-tables.html
|