修正issue及界面错误

This commit is contained in:
inrgihc
2024-05-31 20:55:22 +08:00
parent 8629801641
commit 8d8477641f
26 changed files with 215 additions and 204 deletions

152
README.md
View File

@@ -224,157 +224,7 @@ dbswitch:
> 提示:如果要将源端所有表名(或者字段名)添加前缀,可以配置```"from-pattern": "^","to-value": "T_"```;
- 5支持的数据库产品及其JDBC驱动连接示例如下
**MySQL/MariaDB数据库**
```
jdbc连接地址jdbc:mysql://172.17.2.10:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&tinyInt1isBit=false&rewriteBatchedStatements=true&useCompression=true
jdbc驱动名称 com.mysql.jdbc.Driver
```
与:
```
jdbc连接地址jdbc:mariadb://172.17.2.10:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&tinyInt1isBit=false&rewriteBatchedStatements=true&useCompression=true
jdbc驱动名称 org.mariadb.jdbc.Driver
```
**Oracle数据库**
```
jdbc连接地址jdbc:oracle:thin:@172.17.2.10:1521:ORCL 或 jdbc:oracle:thin:@//172.17.2.10:1521/ORCL
jdbc驱动名称oracle.jdbc.driver.OracleDriver
```
**SQL Server(>=2005)数据库**
```
jdbc连接地址jdbc:sqlserver://172.17.2.10:1433;DatabaseName=test
jdbc驱动名称com.microsoft.sqlserver.jdbc.SQLServerDriver
```
**Sybase数据库**
```
jdbc连接地址jdbc:sybase:Tds:172.17.2.10:5000/test?charset=cp936
jdbc驱动名称com.sybase.jdbc4.jdbc.SybDriver
```
> JDBC连接Sybase数据库使用中文时只能使用CP936这个字符集
**PostgreSQL/Greenplum数据库**
```
jdbc连接地址jdbc:postgresql://172.17.2.10:5432/test
jdbc驱动名称org.postgresql.Driver
```
**DB2数据库**
```
jdbc连接地址jdbc:db2://172.17.2.10:50000/testdb:driverType=4;fullyMaterializeLobData=true;fullyMaterializeInputStreams=true;progressiveStreaming=2;progresssiveLocators=2;
jdbc驱动名称com.ibm.db2.jcc.DB2Driver
```
**达梦DM数据库**
```
jdbc连接地址jdbc:dm://172.17.2.10:5236
jdbc驱动名称dm.jdbc.driver.DmDriver
```
**人大金仓Kingbase8数据库**
```
jdbc连接地址jdbc:kingbase8://172.17.2.10:54321/MYTEST
jdbc驱动名称com.kingbase8.Driver
```
**神通Oscar数据库**
```
jdbc连接地址jdbc:oscar://172.17.2.10:2003/OSRDB
jdbc驱动名称com.oscar.Driver
```
**南大通用GBase8a数据库**
```
jdbc连接地址jdbc:gbase://172.17.2.10:5258/gbase
jdbc驱动名称com.gbase.jdbc.Driver
```
**翰高HighGo数据库(可按PostgreSQL使用)**
```
jdbc连接地址jdbc:highgo://172.17.2.10:5866/highgo
jdbc驱动名称com.highgo.jdbc.Driver
```
**Apache Hive数据库**
```
jdbc连接地址jdbc:hive2://172.17.2.12:10000/default
jdbc驱动名称org.apache.hive.jdbc.HiveDriver
```
注意当前只支持hive version 3.x的账号密码认证方式。
**OpenGauss数据库**
```
jdbc连接地址jdbc:opengauss://172.17.2.10:5866/test
jdbc驱动名称org.opengauss.Driver
```
**ClickHouse数据库**
```
jdbc连接地址jdbc:clickhouse://172.17.2.10:8123/default
jdbc驱动名称com.clickhouse.jdbc.ClickHouseDriver
```
**SQLite数据库**
```
jdbc连接地址jdbc:sqlite:/tmp/test.db 或者 jdbc:sqlite::resource:http://172.17.2.12:8080/test.db
jdbc驱动名称org.sqlite.JDBC
```
注意:
> (a) 本地文件方式jdbc:sqlite:/tmp/test.db , 该方式适用于dbswitch为实体机器部署的场景。
>
> (b) 远程文件方式: jdbc:sqlite::resource:http://172.17.2.12:8080/test.db ,该方式适用于容器方式部署的场景, 搭建文件服务器的方法可使
> 用如下docker方式快速部署(/home/files为服务器上存放sqlite数据库文件的目录)
>
> ```docker run -d --name http_file_server -p 8080:8080 -v /home/files:/data inrgihc/http_file_server:latest```
>
> 说明远程服务器文件将会被下载到本地System.getProperty("java.io.tmpdir")所指定的目录下(linux为/tmp/Windows为C:/temp/),并以
> sqlite-jdbc-tmp-{XXX}.db的方式进行文件命名其中{XXX}为文件网络地址(例如上述为http://192.168.31.57:8080/test.db) 的字符串哈希值,
> 如果本地文件已经存在则不会再次进行下载而是直接使用该文件(当已经下载过文件后远程服务器即使关闭了该sqlite的jdbc-url任然可
> 用直至本地的sqlite-jdbc-tmp-XXX.db文件被人为手动删除)
>
> (c) 不支持内存及其他方式;本地文件方式可以作为源端和目的端,而远程服务器方式只能作为源端。
>
> (d) SQLite为单写多读方式禁止人为方式造成多写导致锁表。
**MongoDB数据库**
```
jdbc连接地址jdbc:mongodb://172.17.2.12:27017/admin?authSource=admin&authMechanism=SCRAM-SHA-1
jdbc驱动名称com.gitee.jdbc.mongodb.JdbcDriver
```
> 项目地址https://gitee.com/inrgihc/jdbc-mongodb-driver
**ElasticSearch数据库**
```
jdbc连接地址jdbc:jest://172.17.2.12:9200?useHttps=false
jdbc驱动名称com.gitee.jdbc.elasticsearch.JdbcDriver
```
> 项目地址https://gitee.com/inrgihc/jdbc-jest-driver
- 5支持的数据库产品详见:[DBSWITCH对数据库产品的支持列表](SUPPORTED_PRODUCTS.md)
#### (2)、启动方法

153
SUPPORTED_PRODUCTS.md Normal file
View File

@@ -0,0 +1,153 @@
## DBSWITCH对数据库产品的支持列表
支持的数据库产品及其JDBC驱动连接示例如下
**MySQL/MariaDB/StarRocks数据库**
```
jdbc连接地址jdbc:mysql://172.17.2.10:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&tinyInt1isBit=false&rewriteBatchedStatements=true&useCompression=true
jdbc驱动名称 com.mysql.jdbc.Driver
```
与:
```
jdbc连接地址jdbc:mariadb://172.17.2.10:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&tinyInt1isBit=false&rewriteBatchedStatements=true&useCompression=true
jdbc驱动名称 org.mariadb.jdbc.Driver
```
**Oracle数据库**
```
jdbc连接地址jdbc:oracle:thin:@172.17.2.10:1521:ORCL 或 jdbc:oracle:thin:@//172.17.2.10:1521/ORCL
jdbc驱动名称oracle.jdbc.driver.OracleDriver
```
**SQL Server(>=2005)数据库**
```
jdbc连接地址jdbc:sqlserver://172.17.2.10:1433;DatabaseName=test
jdbc驱动名称com.microsoft.sqlserver.jdbc.SQLServerDriver
```
**Sybase数据库**
```
jdbc连接地址jdbc:sybase:Tds:172.17.2.10:5000/test?charset=cp936
jdbc驱动名称com.sybase.jdbc4.jdbc.SybDriver
```
> JDBC连接Sybase数据库使用中文时只能使用CP936这个字符集
**PostgreSQL/Greenplum数据库**
```
jdbc连接地址jdbc:postgresql://172.17.2.10:5432/test
jdbc驱动名称org.postgresql.Driver
```
**DB2数据库**
```
jdbc连接地址jdbc:db2://172.17.2.10:50000/testdb:driverType=4;fullyMaterializeLobData=true;fullyMaterializeInputStreams=true;progressiveStreaming=2;progresssiveLocators=2;
jdbc驱动名称com.ibm.db2.jcc.DB2Driver
```
**达梦DM数据库**
```
jdbc连接地址jdbc:dm://172.17.2.10:5236
jdbc驱动名称dm.jdbc.driver.DmDriver
```
**人大金仓Kingbase8数据库**
```
jdbc连接地址jdbc:kingbase8://172.17.2.10:54321/MYTEST
jdbc驱动名称com.kingbase8.Driver
```
**神通Oscar数据库**
```
jdbc连接地址jdbc:oscar://172.17.2.10:2003/OSRDB
jdbc驱动名称com.oscar.Driver
```
**南大通用GBase8a数据库**
```
jdbc连接地址jdbc:gbase://172.17.2.10:5258/gbase
jdbc驱动名称com.gbase.jdbc.Driver
```
**翰高HighGo数据库(可按PostgreSQL使用)**
```
jdbc连接地址jdbc:highgo://172.17.2.10:5866/highgo
jdbc驱动名称com.highgo.jdbc.Driver
```
**Apache Hive数据库**
```
jdbc连接地址jdbc:hive2://172.17.2.12:10000/default
jdbc驱动名称org.apache.hive.jdbc.HiveDriver
```
注意当前只支持hive version 3.x的账号密码认证方式。
**OpenGauss数据库**
```
jdbc连接地址jdbc:opengauss://172.17.2.10:5866/test
jdbc驱动名称org.opengauss.Driver
```
**ClickHouse数据库**
```
jdbc连接地址jdbc:clickhouse://172.17.2.10:8123/default
jdbc驱动名称com.clickhouse.jdbc.ClickHouseDriver
```
**SQLite数据库**
```
jdbc连接地址jdbc:sqlite:/tmp/test.db 或者 jdbc:sqlite::resource:http://172.17.2.12:8080/test.db
jdbc驱动名称org.sqlite.JDBC
```
注意:
> (a) 本地文件方式jdbc:sqlite:/tmp/test.db , 该方式适用于dbswitch为实体机器部署的场景。
>
> (b) 远程文件方式: jdbc:sqlite::resource:http://172.17.2.12:8080/test.db ,该方式适用于容器方式部署的场景, 搭建文件服务器的方法可使
> 用如下docker方式快速部署(/home/files为服务器上存放sqlite数据库文件的目录)
>
> ```docker run -d --name http_file_server -p 8080:8080 -v /home/files:/data inrgihc/http_file_server:latest```
>
> 说明远程服务器文件将会被下载到本地System.getProperty("java.io.tmpdir")所指定的目录下(linux为/tmp/Windows为C:/temp/),并以
> sqlite-jdbc-tmp-{XXX}.db的方式进行文件命名其中{XXX}为文件网络地址(例如上述为http://192.168.31.57:8080/test.db) 的字符串哈希值,
> 如果本地文件已经存在则不会再次进行下载而是直接使用该文件(当已经下载过文件后远程服务器即使关闭了该sqlite的jdbc-url任然可
> 直至本地的sqlite-jdbc-tmp-XXX.db文件被人为手动删除)
>
> (c) 不支持内存及其他方式;本地文件方式可以作为源端和目的端,而远程服务器方式只能作为源端。
>
> (d) SQLite为单写多读方式禁止人为方式造成多写导致锁表。
**MongoDB数据库**
```
jdbc连接地址jdbc:mongodb://172.17.2.12:27017/admin?authSource=admin&authMechanism=SCRAM-SHA-1
jdbc驱动名称com.gitee.jdbc.mongodb.JdbcDriver
```
> 项目地址https://gitee.com/inrgihc/jdbc-mongodb-driver
**ElasticSearch数据库**
```
jdbc连接地址jdbc:jest://172.17.2.12:9200?useHttps=false
jdbc驱动名称com.gitee.jdbc.elasticsearch.JdbcDriver
```
> 项目地址https://gitee.com/inrgihc/jdbc-jest-driver

View File

@@ -4,6 +4,8 @@
基于Vue.js 2.0编写的dbswitch操作管理web端。
> 项目地址https://gitee.com/inrgihc/dbswitch
## 二、环境
**node** : >= v14.15.4

View File

@@ -86,6 +86,8 @@
</li>
<li>ClickHouse
</li>
<li>StarRocks
</li>
<li>MongoDB(只支持数据加载写入不支持变化量同步)
</li>
<li>ElasticSearch(只支持数据加载写入不支持变化量同步)
@@ -128,6 +130,7 @@
dbswitch-product-hive // -> hive方言实现类
dbswitch-product-sqlite // -> sqlite方言实现类
dbswitch-product-clickhouse // -> clickhouse方言实现类
dbswitch-product-starrocks // -> starrocks方言实现类
dbswitch-product-mongodb // -> mongodb方言实现类
dbswitch-product-elasticsearch // -> elasticsearch方言实现类
dbswitch-data // 工具入口模块,读取配置文件中的参数执行异构迁移同步

View File

@@ -175,10 +175,12 @@
<el-select size="small"
placeholder="请选择数据源"
v-model="sqlDataSourceId">
<el-option v-for="(item,index) in connectionList"
:key="index"
:label="`[${item.id}]${item.name}`"
:value="item.id"></el-option>
<template v-for="item in connectionList">
<el-option v-if="item.useSql"
:key="item.id"
:label="`[${item.id}]${item.name}`"
:value="item.id"></el-option>
</template>
</el-select>
</div>
</el-col>

View File

@@ -21,9 +21,6 @@
<el-description-item label="地址"
:span='15'
:value="userinfo.address"></el-description-item>
<el-description-item label="锁定"
:span='15'
:value="userinfo.locked"></el-description-item>
<el-description-item label="创建时间"
:span='15'
:value="userinfo.createTime"></el-description-item>

View File

@@ -27,4 +27,6 @@ public class DbConnectionNameResponse {
@ApiModelProperty("名称")
private String name;
@ApiModelProperty("类型")
private Boolean useSql;
}

View File

@@ -246,7 +246,7 @@ public class ConnectionService {
Supplier<List<DbConnectionNameResponse>> method = () -> {
List<DatabaseConnectionEntity> lists = databaseConnectionDAO.listAll(null);
return lists.stream()
.map(c -> new DbConnectionNameResponse(c.getId(), c.getName()))
.map(c -> new DbConnectionNameResponse(c.getId(), c.getName(), c.getType().isUseSql()))
.collect(Collectors.toList());
};

View File

@@ -1 +1 @@
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>异构数据迁移工具</title><link href=/static/css/app.e8f93316f0672fafa7fc92fac082f131.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=/static/js/manifest.a75c86cba0d382d89449.js></script><script type=text/javascript src=/static/js/vendor.8200341f98478c8f7552.js></script><script type=text/javascript src=/static/js/app.6479f74f2c348b271749.js></script></body></html>
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>异构数据迁移工具</title><link href=/static/css/app.36d9ab08e3bf950d3ba24d86b2782e88.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=/static/js/manifest.73292c52c8a3905d5901.js></script><script type=text/javascript src=/static/js/vendor.8200341f98478c8f7552.js></script><script type=text/javascript src=/static/js/app.6479f74f2c348b271749.js></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +1,2 @@
!function(e){var n=window.webpackJsonp;window.webpackJsonp=function(r,c,f){for(var o,d,b,i=0,u=[];i<r.length;i++)d=r[i],t[d]&&u.push(t[d][0]),t[d]=0;for(o in c)Object.prototype.hasOwnProperty.call(c,o)&&(e[o]=c[o]);for(n&&n(r,c,f);u.length;)u.shift()();if(f)for(i=0;i<f.length;i++)b=a(a.s=f[i]);return b};var r={},t={25:0};function a(n){if(r[n])return r[n].exports;var t=r[n]={i:n,l:!1,exports:{}};return e[n].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var n=t[e];if(0===n)return new Promise(function(e){e()});if(n)return n[2];var r=new Promise(function(r,a){n=t[e]=[r,a]});n[2]=r;var c=document.getElementsByTagName("head")[0],f=document.createElement("script");f.type="text/javascript",f.charset="utf-8",f.async=!0,f.timeout=12e4,a.nc&&f.setAttribute("nonce",a.nc),f.src=a.p+"static/js/"+e+"."+{0:"b25ba583c782da8df088",1:"431ebdfd77ec151f12e7",2:"5d102c48d5747fb106e0",3:"ef5e983cf9ad9dc27899",4:"c08b2c8b16b2d0d50f62",5:"de32c8bd9fcd7cba61b2",6:"d7f3aa182a9403e7c6f6",7:"2c3ef6028bafee6fbaca",8:"535ff4d6dbb1131433ea",9:"4590fe5acd44e077ba19",10:"1d1a99a89d92bca35121",11:"9ea92898e3919b2a671f",12:"9511a964c93be4d9ab2c",13:"522b8e9b509523953170",14:"c04809c50fa291ee4b57",15:"b413da284ca8a4266148",16:"d0a4d814849a4cb8dde5",17:"68d3bb07f9efaf16b653",18:"b6bf70f4f372e952d31f",19:"60fcf0f9bb311b02607d",20:"51523f2d886fb89f48dc",21:"f9fd831656eab671bdc4",22:"b623f8b56b9a9aa1c02b"}[e]+".js";var o=setTimeout(d,12e4);function d(){f.onerror=f.onload=null,clearTimeout(o);var n=t[e];0!==n&&(n&&n[1](new Error("Loading chunk "+e+" failed.")),t[e]=void 0)}return f.onerror=f.onload=d,c.appendChild(f),r},a.m=e,a.c=r,a.d=function(e,n,r){a.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},a.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(n,"a",n),n},a.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},a.p="/",a.oe=function(e){throw console.error(e),e}}([]);
//# sourceMappingURL=manifest.a75c86cba0d382d89449.js.map
!function(e){var n=window.webpackJsonp;window.webpackJsonp=function(r,f,c){for(var o,d,b,i=0,u=[];i<r.length;i++)d=r[i],t[d]&&u.push(t[d][0]),t[d]=0;for(o in f)Object.prototype.hasOwnProperty.call(f,o)&&(e[o]=f[o]);for(n&&n(r,f,c);u.length;)u.shift()();if(c)for(i=0;i<c.length;i++)b=a(a.s=c[i]);return b};var r={},t={25:0};function a(n){if(r[n])return r[n].exports;var t=r[n]={i:n,l:!1,exports:{}};return e[n].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var n=t[e];if(0===n)return new Promise(function(e){e()});if(n)return n[2];var r=new Promise(function(r,a){n=t[e]=[r,a]});n[2]=r;var f=document.getElementsByTagName("head")[0],c=document.createElement("script");c.type="text/javascript",c.charset="utf-8",c.async=!0,c.timeout=12e4,a.nc&&c.setAttribute("nonce",a.nc),c.src=a.p+"static/js/"+e+"."+{0:"b25ba583c782da8df088",1:"257d60784bfec6f635e2",2:"22e821ec8c9909429f09",3:"ef5e983cf9ad9dc27899",4:"f4d7f6cfff84a6ebe507",5:"de32c8bd9fcd7cba61b2",6:"d7f3aa182a9403e7c6f6",7:"2c3ef6028bafee6fbaca",8:"535ff4d6dbb1131433ea",9:"4590fe5acd44e077ba19",10:"1d1a99a89d92bca35121",11:"9ea92898e3919b2a671f",12:"9511a964c93be4d9ab2c",13:"522b8e9b509523953170",14:"c04809c50fa291ee4b57",15:"b413da284ca8a4266148",16:"d0a4d814849a4cb8dde5",17:"68d3bb07f9efaf16b653",18:"b6bf70f4f372e952d31f",19:"60fcf0f9bb311b02607d",20:"51523f2d886fb89f48dc",21:"f9fd831656eab671bdc4",22:"b623f8b56b9a9aa1c02b"}[e]+".js";var o=setTimeout(d,12e4);function d(){c.onerror=c.onload=null,clearTimeout(o);var n=t[e];0!==n&&(n&&n[1](new Error("Loading chunk "+e+" failed.")),t[e]=void 0)}return c.onerror=c.onload=d,f.appendChild(c),r},a.m=e,a.c=r,a.d=function(e,n,r){a.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},a.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(n,"a",n),n},a.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},a.p="/",a.oe=function(e){throw console.error(e),e}}([]);
//# sourceMappingURL=manifest.73292c52c8a3905d5901.js.map

View File

@@ -24,6 +24,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.experimental.UtilityClass;
import org.apache.commons.collections4.CollectionUtils;
@@ -70,9 +71,11 @@ public final class GenerateSqlUtils {
Map<String, String> tblProperties) {
ProductTypeEnum type = provider.getProductType();
StringBuilder sb = new StringBuilder();
List<String> pks = fieldNames.stream()
.filter((cd) -> primaryKeys.contains(cd.getFieldName()))
.map((cd) -> cd.getFieldName())
Set<String> fieldNameSets = fieldNames.stream()
.map(ColumnDescription::getFieldName)
.collect(Collectors.toSet());
List<String> pks = primaryKeys.stream()
.filter(fieldNameSets::contains)
.collect(Collectors.toList());
sb.append(Constants.CREATE_TABLE);
@@ -88,17 +91,16 @@ public final class GenerateSqlUtils {
Integer fieldIndex = 0;
for (int i = 0; i < fieldNames.size(); i++) {
ColumnDescription cd = fieldNames.get(i);
if (primaryKeys.contains(cd.getFieldName())){
copyFieldNames.add(fieldIndex,cd);
fieldIndex = fieldIndex +1;
}else{
if (primaryKeys.contains(cd.getFieldName())) {
copyFieldNames.add(fieldIndex, cd);
fieldIndex = fieldIndex + 1;
} else {
copyFieldNames.add(cd);
}
}
fieldNames = copyFieldNames;
}
for (int i = 0; i < fieldNames.size(); i++) {
if (i > 0) {
sb.append(", ");
@@ -147,10 +149,10 @@ public final class GenerateSqlUtils {
sb.append("ORDER BY tuple()");
}
if (withRemarks && StringUtils.isNotBlank(tableRemarks)) {
//sb.append(Constants.CR);
//sb.append(String.format("COMMENT='%s' ", tableRemarks.replace("'", "\\'")));
sb.append(Constants.CR);
sb.append(String.format("COMMENT '%s' ", tableRemarks.replace("'", "\\'")));
}
}else if (type.isLikeStarRocks()){
} else if (type.isLikeStarRocks()) {
String pk = provider.getPrimaryKeyAsString(pks);
sb.append("PRIMARY KEY (").append(pk).append(")");
sb.append("\n DISTRIBUTED BY HASH(").append(pk).append(")");