diff --git a/mayfly_go_web/src/views/ops/db/SqlExec.vue b/mayfly_go_web/src/views/ops/db/SqlExec.vue index eb6c31a8..963c58df 100644 --- a/mayfly_go_web/src/views/ops/db/SqlExec.vue +++ b/mayfly_go_web/src/views/ops/db/SqlExec.vue @@ -151,6 +151,16 @@ + @@ -171,6 +181,7 @@ import { TagResourceTypeEnum } from '@/common/commonEnum'; import { Pane, Splitpanes } from 'splitpanes'; import { useEventListener } from '@vueuse/core'; +const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue')); const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue')); const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue')); const DbTablesOp = defineAsyncComponent(() => import('./component/table/DbTablesOp.vue')); @@ -258,9 +269,9 @@ const NodeTypeDb = new NodeType(SqlExecNodeType.Db) .withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))]) .withLoadNodesFunc(async (parentNode: TagTreeNode) => { const params = parentNode.params; + params.parentKey = parentNode.key; // pg类数据库会多一层schema if (params.type == DbType.postgresql || params.type === DbType.dm || params.type === DbType.oracle) { - const params = parentNode.params; const { id, db } = params; const schemaNames = await dbApi.pgSchemas.request({ id, db }); return schemaNames.map((sn: any) => { @@ -277,23 +288,29 @@ const NodeTypeDb = new NodeType(SqlExecNodeType.Db) .withNodeClickFunc(nodeClickChangeDb); const NodeTypeTables = (params: any) => { + let tableKey = `${params.id}.${params.db}.table-menu`; + let sqlKey = getSqlMenuNodeKey(params.id, params.db); return [ - new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon), - new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon), + new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams({...params, key:tableKey}).withIcon(TableIcon), + new TagTreeNode(sqlKey, 'SQL', NodeTypeSqlMenu).withParams({...params, key:sqlKey}).withIcon(SqlIcon), ]; }; // postgres schema模式 const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema) .withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))]) - .withLoadNodesFunc(async (parentNode: TagTreeNode) => NodeTypeTables(parentNode.params)) + .withLoadNodesFunc(async (parentNode: TagTreeNode) => { + const params = parentNode.params; + params.parentKey = parentNode.key; + return NodeTypeTables(params); + }) .withNodeClickFunc(nodeClickChangeDb); // 数据库表菜单节点 const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu) .withContextMenuItems([ new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key)), - + new ContextmenuItem('createTable', '创建表').withIcon('Plus').withOnClick((data: any) => onEditTable(data)), new ContextmenuItem('tablesOp', '表操作').withIcon('Setting').withOnClick((data: any) => { const params = data.params; addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: data.key }); @@ -301,7 +318,7 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu) ]) .withLoadNodesFunc(async (parentNode: TagTreeNode) => { const params = parentNode.params; - let { id, db } = params; + let { id, db, type } = params; // 获取当前库的所有表信息 let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus); state.reloadStatus = false; @@ -309,11 +326,15 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu) const tablesNode = tables.map((x: any) => { const tableSize = x.dataLength + x.indexLength; dbTableSize += tableSize; - return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeTypeTable) + const key = `${id}.${db}.${x.tableName}`; + return new TagTreeNode(key, x.tableName, NodeTypeTable) .withIsLeaf(true) .withParams({ id, db, + type, + key: key, + parentKey: parentNode.key, tableName: x.tableName, tableComment: x.tableComment, size: tableSize == 0 ? '' : formatByteSize(tableSize, 1), @@ -339,22 +360,23 @@ const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu) return sqls.map((x: any) => { return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql) .withIsLeaf(true) - .withParams({ - id, - db, - dbs, - sqlName: x.name, - }) + .withParams({ id, db, dbs, sqlName: x.name }) .withIcon(SqlIcon); }); }) .withNodeClickFunc(nodeClickChangeDb); // 表节点类型 -const NodeTypeTable = new NodeType(SqlExecNodeType.Table).withNodeClickFunc((nodeData: TagTreeNode) => { - const params = nodeData.params; - loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName); -}); +const NodeTypeTable = new NodeType(SqlExecNodeType.Table) + .withContextMenuItems([ + new ContextmenuItem('copyTable', '复制表').withIcon('copyDocument').withOnClick((data: any) => onCopyTable(data)), + new ContextmenuItem('editTable', '编辑表').withIcon('edit').withOnClick((data: any) => onEditTable(data)), + new ContextmenuItem('delTable', '删除表').withIcon('Delete').withOnClick((data: any) => onDeleteTable(data)), + ]) + .withNodeClickFunc((nodeData: TagTreeNode) => { + const params = nodeData.params; + loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName); + }); // sql模板节点类型 const NodeTypeSql = new NodeType(SqlExecNodeType.Sql) @@ -384,9 +406,19 @@ const state = reactive({ loading: true, version: '', }, + tableCreateDialog: { + visible: false, + title: '', + activeName: '', + dbId: 0, + db: '', + dbType: '', + data: {}, + parentKey: '', + }, }); -const { nowDbInst } = toRefs(state); +const { nowDbInst, tableCreateDialog } = toRefs(state); const serverInfoReqParam = ref({ instanceId: 0, @@ -602,6 +634,63 @@ const reloadNode = (nodeKey: string) => { tagTreeRef.value.reloadNode(nodeKey); }; +const onEditTable = async (data: any) => { + let { db, id, tableName, tableComment, type, parentKey, key } = data.params; + // data.label就是表名 + if (tableName) { + state.tableCreateDialog.title = '修改表'; + let indexs = await dbApi.tableIndex.request({ id, db, tableName }); + let columns = await dbApi.columnMetadata.request({ id, db, tableName }); + let row = { tableName, tableComment }; + state.tableCreateDialog.data = { edit: true, row, indexs, columns }; + state.tableCreateDialog.parentKey = parentKey; + } else { + state.tableCreateDialog.title = '创建表'; + state.tableCreateDialog.data = { edit: false, row: {} }; + state.tableCreateDialog.parentKey = key; + } + + state.tableCreateDialog.visible = true; + state.tableCreateDialog.activeName = '1'; + state.tableCreateDialog.dbId = id; + state.tableCreateDialog.db = db; + state.tableCreateDialog.dbType = type; +}; + +const onDeleteTable = async (data: any) => { + let { db, id, tableName, parentKey } = data.params; + await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning', + }); + // 执行sql + dbApi.sqlExec.request({ id, db, sql: `drop table ${tableName}` }).then(() => { + ElMessage.success('删除成功'); + setTimeout(() => { + parentKey && reloadNode(parentKey); + }, 1000); + }); +}; + +const onCopyTable = async (data: any) => { + let { db, id, tableName, parentKey } = data.params; + + // 执行sql + dbApi.copyTable.request({ id, db, tableName, copyData:true }).then(() => { + ElMessage.success('复制成功'); + setTimeout(() => { + parentKey && reloadNode(parentKey); + }, 1000); + }); +}; + +const onSubmitEditTableSql = () => { + state.tableCreateDialog.visible = false; + state.tableCreateDialog.data = { edit: false, row: {} }; + reloadNode(state.tableCreateDialog.parentKey); +}; + /** * 获取当前操作的数据库信息 */ diff --git a/mayfly_go_web/src/views/ops/db/api.ts b/mayfly_go_web/src/views/ops/db/api.ts index 9e01c568..560460fc 100644 --- a/mayfly_go_web/src/views/ops/db/api.ts +++ b/mayfly_go_web/src/views/ops/db/api.ts @@ -11,6 +11,7 @@ export const dbApi = { tableInfos: Api.newGet('/dbs/{id}/t-infos'), tableIndex: Api.newGet('/dbs/{id}/t-index'), tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'), + copyTable: Api.newPost('/dbs/{id}/copy-table'), columnMetadata: Api.newGet('/dbs/{id}/c-metadata'), pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'), // 获取表即列提示 diff --git a/mayfly_go_web/src/views/ops/db/component/table/DbTableOp.vue b/mayfly_go_web/src/views/ops/db/component/table/DbTableOp.vue index 9bf2adef..6fbf6b4f 100644 --- a/mayfly_go_web/src/views/ops/db/component/table/DbTableOp.vue +++ b/mayfly_go_web/src/views/ops/db/component/table/DbTableOp.vue @@ -132,7 +132,7 @@ import { reactive, ref, toRefs, watch } from 'vue'; import { ElMessage } from 'element-plus'; import SqlExecBox from '../sqleditor/SqlExecBox'; -import { DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index'; +import { DbDialect, DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index'; const props = defineProps({ visible: { @@ -158,7 +158,7 @@ const props = defineProps({ //定义事件 const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']); -const dbDialect = getDbDialect(props.dbType); +let dbDialect = getDbDialect(props.dbType); type ColName = { prop: string; @@ -272,6 +272,7 @@ const { dialogVisible, btnloading, activeName, indexTypeList, tableData } = toRe watch(props, async (newValue) => { state.dialogVisible = newValue.visible; + dbDialect = getDbDialect(newValue.dbType); }); const cancel = () => { diff --git a/mayfly_go_web/src/views/ops/db/component/table/DbTablesOp.vue b/mayfly_go_web/src/views/ops/db/component/table/DbTablesOp.vue index c4b2f58a..cf321b67 100644 --- a/mayfly_go_web/src/views/ops/db/component/table/DbTablesOp.vue +++ b/mayfly_go_web/src/views/ops/db/component/table/DbTablesOp.vue @@ -68,9 +68,7 @@ @@ -127,7 +125,7 @@ import SqlExecBox from '../sqleditor/SqlExecBox'; import config from '@/common/config'; import { joinClientParams } from '@/common/request'; import { isTrue } from '@/common/assert'; -import { compatibleMysql, DbType } from '../../dialect/index'; +import { compatibleMysql, DbType, editDbTypes } from '../../dialect/index'; const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue')); diff --git a/mayfly_go_web/src/views/ops/db/dialect/index.ts b/mayfly_go_web/src/views/ops/db/dialect/index.ts index 8a92f7dd..35a0e696 100644 --- a/mayfly_go_web/src/views/ops/db/dialect/index.ts +++ b/mayfly_go_web/src/views/ops/db/dialect/index.ts @@ -115,6 +115,8 @@ export const DbType = { sqlite: 'sqlite', }; +export const editDbTypes = [DbType.mysql, DbType.mariadb, DbType.postgresql, DbType.dm, DbType.oracle, DbType.sqlite]; + export const compatibleMysql = (dbType: string): boolean => { switch (dbType) { case DbType.mysql: diff --git a/mayfly_go_web/src/views/ops/db/dialect/postgres_dialect.ts b/mayfly_go_web/src/views/ops/db/dialect/postgres_dialect.ts index b6001ec2..19931690 100644 --- a/mayfly_go_web/src/views/ops/db/dialect/postgres_dialect.ts +++ b/mayfly_go_web/src/views/ops/db/dialect/postgres_dialect.ts @@ -228,7 +228,7 @@ class PostgresqlDialect implements DbDialect { let marks = false; if (this.matchType(cl.type, ['char', 'time', 'date', 'text'])) { // 默认值是now()的time或date不需要加引号 - if (cl.value.toLowerCase() === 'pg_systimestamp()' && this.matchType(cl.type, ['time', 'date'])) { + if (['pg_systimestamp()', 'current_timestamp'].includes(cl.value.toLowerCase()) && this.matchType(cl.type, ['time', 'date'])) { marks = false; } else { marks = true; diff --git a/mayfly_go_web/src/views/ops/db/dialect/sqlite_dialect.ts b/mayfly_go_web/src/views/ops/db/dialect/sqlite_dialect.ts index d4657bac..57a7bf03 100644 --- a/mayfly_go_web/src/views/ops/db/dialect/sqlite_dialect.ts +++ b/mayfly_go_web/src/views/ops/db/dialect/sqlite_dialect.ts @@ -145,7 +145,7 @@ class SqliteDialect implements DbDialect { getDefaultRows(): RowDefinition[] { return [ - { name: 'id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' }, + { name: 'id', type: 'integer', length: '', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' }, { name: 'creator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' }, { name: 'creator', diff --git a/server/internal/db/api/db.go b/server/internal/db/api/db.go index 71f453a6..1e318ea2 100644 --- a/server/internal/db/api/db.go +++ b/server/internal/db/api/db.go @@ -462,6 +462,20 @@ func (d *Db) GetSchemas(rc *req.Ctx) { rc.ResData = res } +func (d *Db) CopyTable(rc *req.Ctx) { + form := &form.DbCopyTableForm{} + copy := ginx.BindJsonAndCopyTo[*dbi.DbCopyTable](rc.GinCtx, form, new(dbi.DbCopyTable)) + + conn, err := d.DbApp.GetDbConn(form.Id, form.Db) + biz.ErrIsNilAppendErr(err, "拷贝表失败: %s") + + err = conn.GetDialect().CopyTable(copy) + if err != nil { + logx.Errorf("拷贝表失败: %s", err.Error()) + } + biz.ErrIsNilAppendErr(err, "拷贝表失败: %s") +} + func getDbId(g *gin.Context) uint64 { dbId, _ := strconv.Atoi(g.Param("dbId")) biz.IsTrue(dbId > 0, "dbId错误") diff --git a/server/internal/db/api/form/db.go b/server/internal/db/api/form/db.go index f09f14da..cd7f0fd8 100644 --- a/server/internal/db/api/form/db.go +++ b/server/internal/db/api/form/db.go @@ -23,3 +23,11 @@ type DbSqlExecForm struct { Sql string `binding:"required" json:"sql"` // 执行sql Remark string `json:"remark"` // 执行备注 } + +// 数据库复制表 +type DbCopyTableForm struct { + Id uint64 `binding:"required" json:"id"` + Db string `binding:"required" json:"db" ` + TableName string `binding:"required" json:"tableName"` + CopyData bool `binding:"required" json:"copyData"` // 是否复制数据 +} diff --git a/server/internal/db/dbm/dbi/dialect.go b/server/internal/db/dbm/dbi/dialect.go index bd4435ed..d4c780c4 100644 --- a/server/internal/db/dbm/dbi/dialect.go +++ b/server/internal/db/dbm/dbi/dialect.go @@ -59,6 +59,13 @@ type Index struct { NonUnique int `json:"nonUnique"` } +type DbCopyTable struct { + Id uint64 `json:"id"` + Db string `json:"db" ` + TableName string `json:"tableName"` + CopyData bool `json:"copyData"` // 是否复制数据 +} + // -----------------------------------元数据接口定义------------------------------------------ // 数据库方言、元信息接口(表、列、获取表数据等元信息) type Dialect interface { @@ -97,6 +104,8 @@ type Dialect interface { GetDataType(dbColumnType string) DataType FormatStrData(dbColumnValue string, dataType DataType) string + + CopyTable(copy *DbCopyTable) error } // ------------------------- 元数据sql操作 ------------------------- diff --git a/server/internal/db/dbm/dm/dialect.go b/server/internal/db/dbm/dm/dialect.go index 74c7f25c..690e0395 100644 --- a/server/internal/db/dbm/dm/dialect.go +++ b/server/internal/db/dbm/dm/dialect.go @@ -4,11 +4,13 @@ import ( "context" "database/sql" "fmt" + "github.com/kanzihuang/vitess/go/vt/sqlparser" "mayfly-go/internal/db/dbm/dbi" "mayfly-go/pkg/errorx" "mayfly-go/pkg/logx" "mayfly-go/pkg/utils/anyx" "mayfly-go/pkg/utils/collx" + "mayfly-go/pkg/utils/stringx" "regexp" "strings" "time" @@ -311,3 +313,44 @@ func (dd *DMDialect) FormatStrData(dbColumnValue string, dataType dbi.DataType) } return dbColumnValue } + +func (dd *DMDialect) CopyTable(copy *dbi.DbCopyTable) error { + tableName := copy.TableName + ddl, err := dd.GetTableDDL(tableName) + if err != nil { + return err + } + // 生成新表名,为老表明+_copy_时间戳 + newTableName := tableName + "_copy_" + time.Now().Format("20060102150405") + + // 替换新表名 + ddl = strings.ReplaceAll(ddl, fmt.Sprintf("\"%s\"", strings.ToUpper(tableName)), fmt.Sprintf("\"%s\"", strings.ToUpper(newTableName))) + // 去除空格换行 + ddl = stringx.TrimSpaceAndBr(ddl) + sqls, err := sqlparser.SplitStatementToPieces(ddl, sqlparser.WithDialect(dd.dc.Info.Type.Dialect())) + for _, sql := range sqls { + _, _ = dd.dc.Exec(sql) + } + + // 复制数据 + if copy.CopyData { + go func() { + // 设置允许填充自增列之后,显示指定列名可以插入自增列 + _, _ = dd.dc.Exec(fmt.Sprintf("set identity_insert \"%s\" on", newTableName)) + // 获取列名 + columns, _ := dd.GetColumns(tableName) + columnArr := make([]string, 0) + for _, column := range columns { + columnArr = append(columnArr, fmt.Sprintf("\"%s\"", column.ColumnName)) + } + columnStr := strings.Join(columnArr, ",") + // 插入新数据并显示指定列 + _, _ = dd.dc.Exec(fmt.Sprintf("insert into \"%s\" (%s) select %s from \"%s\"", newTableName, columnStr, columnStr, tableName)) + + // 执行完成后关闭允许填充自增列 + _, _ = dd.dc.Exec(fmt.Sprintf("set identity_insert \"%s\" off", newTableName)) + }() + } + + return err +} diff --git a/server/internal/db/dbm/mysql/dialect.go b/server/internal/db/dbm/mysql/dialect.go index 86ca3c3f..7d126078 100644 --- a/server/internal/db/dbm/mysql/dialect.go +++ b/server/internal/db/dbm/mysql/dialect.go @@ -10,6 +10,7 @@ import ( "mayfly-go/pkg/utils/collx" "regexp" "strings" + "time" ) const ( @@ -224,3 +225,25 @@ func (md *MysqlDialect) FormatStrData(dbColumnValue string, dataType dbi.DataTyp // mysql不需要格式化时间日期等 return dbColumnValue } + +func (md *MysqlDialect) CopyTable(copy *dbi.DbCopyTable) error { + + tableName := copy.TableName + + // 生成新表名,为老表明+_copy_时间戳 + newTableName := tableName + "_copy_" + time.Now().Format("20060102150405") + + // 复制表结构创建表 + _, err := md.dc.Exec(fmt.Sprintf("create table %s like %s", newTableName, tableName)) + if err != nil { + return err + } + + // 复制数据 + if copy.CopyData { + go func() { + _, _ = md.dc.Exec(fmt.Sprintf("insert into %s select * from %s", newTableName, tableName)) + }() + } + return err +} diff --git a/server/internal/db/dbm/oracle/dialect.go b/server/internal/db/dbm/oracle/dialect.go index b9d73c22..9e607688 100644 --- a/server/internal/db/dbm/oracle/dialect.go +++ b/server/internal/db/dbm/oracle/dialect.go @@ -340,3 +340,14 @@ func (od *OracleDialect) FormatStrData(dbColumnValue string, dataType dbi.DataTy } return dbColumnValue } + +func (od *OracleDialect) CopyTable(copy *dbi.DbCopyTable) error { + // 生成新表名,为老表明+_copy_时间戳 + newTableName := strings.ToUpper(copy.TableName + "_copy_" + time.Now().Format("20060102150405")) + condition := "" + if copy.CopyData { + condition = " where 1 = 2" + } + _, err := od.dc.Exec(fmt.Sprintf("create table \"%s\" as select * from \"%s\" %s", newTableName, copy.TableName, condition)) + return err +} diff --git a/server/internal/db/dbm/postgres/dialect.go b/server/internal/db/dbm/postgres/dialect.go index 289441ad..64776721 100644 --- a/server/internal/db/dbm/postgres/dialect.go +++ b/server/internal/db/dbm/postgres/dialect.go @@ -255,3 +255,65 @@ func (pd *PgsqlDialect) FormatStrData(dbColumnValue string, dataType dbi.DataTyp } return dbColumnValue } + +func (pd *PgsqlDialect) IsGauss() bool { + return strings.Contains(pd.dc.Info.Params, "gauss") +} + +func (pd *PgsqlDialect) CopyTable(copy *dbi.DbCopyTable) error { + tableName := copy.TableName + // 生成新表名,为老表明+_copy_时间戳 + newTableName := tableName + "_copy_" + time.Now().Format("20060102150405") + // 执行根据旧表创建新表 + _, err := pd.dc.Exec(fmt.Sprintf("create table %s (like %s)", newTableName, tableName)) + if err != nil { + return err + } + + // 复制数据 + if copy.CopyData { + go func() { + _, _ = pd.dc.Exec(fmt.Sprintf("insert into %s select * from %s", newTableName, tableName)) + }() + } + + // 查询旧表的自增字段名 重新设置新表的序列序列器 + _, res, err := pd.dc.Query(fmt.Sprintf("select column_name from information_schema.columns where table_name = '%s' and column_default like 'nextval%%'", tableName)) + if err != nil { + return err + } + + for _, re := range res { + colName := anyx.ConvString(re["column_name"]) + if colName != "" { + + // 查询自增列当前最大值 + _, maxRes, err := pd.dc.Query(fmt.Sprintf("select max(%s) max_val from %s", colName, tableName)) + if err != nil { + return err + } + maxVal := anyx.ConvInt(maxRes[0]["max_val"]) + // 序列起始值为1或当前最大值+1 + if maxVal <= 0 { + maxVal = 1 + } else { + maxVal += 1 + } + + // 之所以不用tableName_colName_seq是因为gauss会自动创建同名的序列,且无法修改序列起始值,所以直接使用新序列值 + newSeqName := fmt.Sprintf("%s_%s_copy_seq", newTableName, colName) + + // 创建自增序列,当前最大值为旧表最大值 + _, err = pd.dc.Exec(fmt.Sprintf("CREATE SEQUENCE %s START %d INCREMENT 1", newSeqName, maxVal)) + if err != nil { + return err + } + // 将新表的自增主键序列与主键列相关联 + _, err = pd.dc.Exec(fmt.Sprintf("alter table %s alter column %s set default nextval('%s')", newTableName, colName, newSeqName)) + if err != nil { + return err + } + } + } + return err +} diff --git a/server/internal/db/dbm/sqlite/dialect.go b/server/internal/db/dbm/sqlite/dialect.go index c6ed2f09..c7ba27fd 100644 --- a/server/internal/db/dbm/sqlite/dialect.go +++ b/server/internal/db/dbm/sqlite/dialect.go @@ -58,7 +58,7 @@ func (sd *SqliteDialect) GetTables() ([]dbi.Table, error) { tables := make([]dbi.Table, 0) for _, re := range res { tables = append(tables, dbi.Table{ - TableName: re["tableName"].(string), + TableName: anyx.ConvString(re["tableName"]), TableComment: anyx.ConvString(re["tableComment"]), CreateTime: anyx.ConvString(re["createTime"]), TableRows: anyx.ConvInt(re["tableRows"]), @@ -97,7 +97,7 @@ func (sd *SqliteDialect) GetColumns(tableNames ...string) ([]dbi.Column, error) } columns = append(columns, dbi.Column{ TableName: tableName, - ColumnName: re["name"].(string), + ColumnName: anyx.ConvString(re["name"]), ColumnType: strings.ToLower(anyx.ConvString(re["type"])), ColumnComment: "", Nullable: nullable, @@ -117,7 +117,7 @@ func (sd *SqliteDialect) GetPrimaryKey(tableName string) (string, error) { } for _, re := range res { if anyx.ConvInt(re["pk"]) == 1 { - return re["name"].(string), nil + return anyx.ConvString(re["name"]), nil } } @@ -149,7 +149,7 @@ func (sd *SqliteDialect) GetTableIndex(tableName string) ([]dbi.Index, error) { indexs := make([]dbi.Index, 0) for _, re := range res { - indexSql := re["indexSql"].(string) + indexSql := anyx.ConvString(re["indexSql"]) isUnique := strings.Contains(indexSql, "CREATE UNIQUE INDEX") nonUnique := 1 if isUnique { @@ -157,7 +157,7 @@ func (sd *SqliteDialect) GetTableIndex(tableName string) ([]dbi.Index, error) { } indexs = append(indexs, dbi.Index{ - IndexName: re["indexName"].(string), + IndexName: anyx.ConvString(re["indexName"]), ColumnName: extractIndexFields(indexSql), IndexType: anyx.ConvString(re["indexType"]), IndexComment: anyx.ConvString(re["indexComment"]), @@ -171,13 +171,13 @@ func (sd *SqliteDialect) GetTableIndex(tableName string) ([]dbi.Index, error) { // 获取建表ddl func (sd *SqliteDialect) GetTableDDL(tableName string) (string, error) { - _, res, err := sd.dc.Query("select sql from sqlite_master WHERE name=? order by type desc", tableName) + _, res, err := sd.dc.Query("select sql from sqlite_master WHERE tbl_name=? order by type desc", tableName) if err != nil { return "", err } var builder strings.Builder for _, re := range res { - builder.WriteString(re["sql"].(string)) + builder.WriteString(anyx.ConvString(re["sql"]) + "; \n\n") } return builder.String(), nil @@ -245,3 +245,35 @@ func (sd *SqliteDialect) FormatStrData(dbColumnValue string, dataType dbi.DataTy } return dbColumnValue } + +func (sd *SqliteDialect) CopyTable(copy *dbi.DbCopyTable) error { + tableName := copy.TableName + + // 生成新表名,为老表明+_copy_时间戳 + newTableName := tableName + "_copy_" + time.Now().Format("20060102150405") + ddl, err := sd.GetTableDDL(tableName) + if err != nil { + return err + } + // 生成建表语句 + // 替换表名 + ddl = strings.ReplaceAll(ddl, fmt.Sprintf("CREATE TABLE \"%s\"", tableName), fmt.Sprintf("CREATE TABLE \"%s\"", newTableName)) + // 替换索引名,索引名为按照规范生成的,才能替换,否则未知索引名,无法替换 + ddl = strings.ReplaceAll(ddl, fmt.Sprintf("CREATE INDEX \"%s", tableName), fmt.Sprintf("CREATE INDEX \"%s", newTableName)) + + // 执行建表语句 + _, err = sd.dc.Exec(ddl) + if err != nil { + return err + } + + // 使用异步线程插入数据 + if copy.CopyData { + go func() { + // 执行插入语句 + _, _ = sd.dc.Exec(fmt.Sprintf("INSERT INTO \"%s\" SELECT * FROM \"%s\"", newTableName, tableName)) + }() + } + + return err +} diff --git a/server/internal/db/router/db.go b/server/internal/db/router/db.go index 1f8da676..7ae16ff1 100644 --- a/server/internal/db/router/db.go +++ b/server/internal/db/router/db.go @@ -42,9 +42,13 @@ func InitDbRouter(router *gin.RouterGroup) { req.NewGet(":dbId/hint-tables", d.HintTables), req.NewGet(":dbId/restore-task", d.GetRestoreTask), + req.NewPost(":dbId/restore-task", d.SaveRestoreTask). Log(req.NewLogSave("db-保存数据库恢复任务")), + req.NewGet(":dbId/restore-histories", d.GetRestoreHistories), + + req.NewPost(":dbId/copy-table", d.CopyTable), } req.BatchSetGroup(db, reqs[:])