diff --git a/packages/plugins/@hera/plugin-external-data-source/.npmignore b/packages/plugins/@tachybase/plugin-external-data-source/.npmignore
similarity index 100%
rename from packages/plugins/@hera/plugin-external-data-source/.npmignore
rename to packages/plugins/@tachybase/plugin-external-data-source/.npmignore
diff --git a/packages/plugins/@hera/plugin-external-data-source/README.md b/packages/plugins/@tachybase/plugin-external-data-source/README.md
similarity index 100%
rename from packages/plugins/@hera/plugin-external-data-source/README.md
rename to packages/plugins/@tachybase/plugin-external-data-source/README.md
diff --git a/packages/plugins/@hera/plugin-external-data-source/client.d.ts b/packages/plugins/@tachybase/plugin-external-data-source/client.d.ts
similarity index 100%
rename from packages/plugins/@hera/plugin-external-data-source/client.d.ts
rename to packages/plugins/@tachybase/plugin-external-data-source/client.d.ts
diff --git a/packages/plugins/@hera/plugin-external-data-source/client.js b/packages/plugins/@tachybase/plugin-external-data-source/client.js
similarity index 100%
rename from packages/plugins/@hera/plugin-external-data-source/client.js
rename to packages/plugins/@tachybase/plugin-external-data-source/client.js
diff --git a/packages/plugins/@hera/plugin-external-data-source/package.json b/packages/plugins/@tachybase/plugin-external-data-source/package.json
similarity index 90%
rename from packages/plugins/@hera/plugin-external-data-source/package.json
rename to packages/plugins/@tachybase/plugin-external-data-source/package.json
index 595263c42..391e0cb31 100644
--- a/packages/plugins/@hera/plugin-external-data-source/package.json
+++ b/packages/plugins/@tachybase/plugin-external-data-source/package.json
@@ -1,5 +1,5 @@
 {
-  "name": "@hera/plugin-external-data-source",
+  "name": "@tachybase/plugin-external-data-source",
   "version": "0.0.13",
   "main": "dist/server/index.js",
   "dependencies": {
diff --git a/packages/plugins/@hera/plugin-external-data-source/server.d.ts b/packages/plugins/@tachybase/plugin-external-data-source/server.d.ts
similarity index 100%
rename from packages/plugins/@hera/plugin-external-data-source/server.d.ts
rename to packages/plugins/@tachybase/plugin-external-data-source/server.d.ts
diff --git a/packages/plugins/@hera/plugin-external-data-source/server.js b/packages/plugins/@tachybase/plugin-external-data-source/server.js
similarity index 100%
rename from packages/plugins/@hera/plugin-external-data-source/server.js
rename to packages/plugins/@tachybase/plugin-external-data-source/server.js
diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/forms/msql.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/forms/msql.tsx
new file mode 100644
index 000000000..c50e557e2
--- /dev/null
+++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/forms/msql.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import { SchemaComponent } from '@tachybase/client';
+import { tval, usePluginTranslation } from '../locale';
+
+export const MysqlDataSourceSettingsForm = () => {
+  const { t } = usePluginTranslation();
+  return (
+    
+  );
+};
diff --git a/packages/plugins/@hera/plugin-external-data-source/src/client/index.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/forms/pg.tsx
similarity index 78%
rename from packages/plugins/@hera/plugin-external-data-source/src/client/index.tsx
rename to packages/plugins/@tachybase/plugin-external-data-source/src/client/forms/pg.tsx
index 3ea0edaba..6840f36d7 100644
--- a/packages/plugins/@hera/plugin-external-data-source/src/client/index.tsx
+++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/forms/pg.tsx
@@ -1,11 +1,9 @@
 import React from 'react';
-import { Plugin, SchemaComponent } from '@tachybase/client';
-import {} from '@tachybase/client';
-import { PluginDataSourceManagerClient } from '@nocobase/plugin-data-source-manager/client';
 import { Space } from 'antd';
-import { generateNTemplate as tval, usePluginTranslation } from './locale';
+import { SchemaComponent } from '@tachybase/client';
+import { tval, usePluginTranslation } from '../locale';
 
-const DataSourceSettingsForm = () => {
+export const PgDataSourceSettingsForm = () => {
   const { t } = usePluginTranslation();
   return (
      {
           displayName: {
             type: 'string',
             title: tval('Data source display name'),
-            required: !0,
+            required: true,
             'x-decorator': 'FormItem',
             'x-component': 'Input',
           },
@@ -39,7 +37,7 @@ const DataSourceSettingsForm = () => {
               host: {
                 type: 'string',
                 title: tval('Host'),
-                required: !0,
+                required: true,
                 'x-decorator': 'FormItem',
                 'x-component': 'Input',
                 default: 'localhost',
@@ -47,7 +45,7 @@ const DataSourceSettingsForm = () => {
               port: {
                 type: 'string',
                 title: tval('Port'),
-                required: !0,
+                required: true,
                 'x-decorator': 'FormItem',
                 'x-component': 'Input',
                 default: 5432,
@@ -55,14 +53,14 @@ const DataSourceSettingsForm = () => {
               database: {
                 type: 'string',
                 title: tval('Database'),
-                required: !0,
+                required: true,
                 'x-decorator': 'FormItem',
                 'x-component': 'Input',
               },
               username: {
                 type: 'string',
                 title: tval('Username'),
-                required: !0,
+                required: true,
                 'x-decorator': 'FormItem',
                 'x-component': 'Input',
               },
@@ -98,13 +96,3 @@ const DataSourceSettingsForm = () => {
     />
   );
 };
-
-export class PluginExternalDataSourceClient extends Plugin {
-  async load() {
-    this.app.pm
-      .get(PluginDataSourceManagerClient)
-      .registerType('postgres', { DataSourceSettingsForm, label: tval('PostgreSQL') });
-  }
-}
-
-export default PluginExternalDataSourceClient;
diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/index.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/index.tsx
new file mode 100644
index 000000000..4d15e16cc
--- /dev/null
+++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/index.tsx
@@ -0,0 +1,19 @@
+import { Plugin } from '@tachybase/client';
+import { PluginDataSourceManagerClient } from '@nocobase/plugin-data-source-manager/client';
+import { tval } from './locale';
+import { PgDataSourceSettingsForm } from './forms/pg';
+import { MysqlDataSourceSettingsForm } from './forms/msql';
+
+export class PluginExternalDataSourceClient extends Plugin {
+  async load() {
+    this.app.pm
+      .get(PluginDataSourceManagerClient)
+      .registerType('postgres', { DataSourceSettingsForm: PgDataSourceSettingsForm, label: tval('PostgreSQL') });
+
+    this.app.pm
+      .get(PluginDataSourceManagerClient)
+      .registerType('mysql', { DataSourceSettingsForm: MysqlDataSourceSettingsForm, label: tval('MySQL') });
+  }
+}
+
+export default PluginExternalDataSourceClient;
diff --git a/packages/plugins/@hera/plugin-external-data-source/src/client/locale.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/client/locale.ts
similarity index 89%
rename from packages/plugins/@hera/plugin-external-data-source/src/client/locale.ts
rename to packages/plugins/@tachybase/plugin-external-data-source/src/client/locale.ts
index 4aefde70b..fa6b8c21f 100644
--- a/packages/plugins/@hera/plugin-external-data-source/src/client/locale.ts
+++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/locale.ts
@@ -6,7 +6,7 @@ export const NAMESPACE = '@hera/plugin-external-data-source';
 export function lang(key: string, options = {}) {
   return i18n.t(key, { ...options, ns: NAMESPACE });
 }
-export function generateNTemplate(key: string) {
+export function tval(key: string) {
   return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`;
 }
 export function usePluginTranslation() {
diff --git a/packages/plugins/@hera/plugin-external-data-source/src/index.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/index.ts
similarity index 100%
rename from packages/plugins/@hera/plugin-external-data-source/src/index.ts
rename to packages/plugins/@tachybase/plugin-external-data-source/src/index.ts
diff --git a/packages/plugins/@hera/plugin-external-data-source/src/locale/en-US.json b/packages/plugins/@tachybase/plugin-external-data-source/src/locale/en-US.json
similarity index 100%
rename from packages/plugins/@hera/plugin-external-data-source/src/locale/en-US.json
rename to packages/plugins/@tachybase/plugin-external-data-source/src/locale/en-US.json
diff --git a/packages/plugins/@hera/plugin-external-data-source/src/locale/zh-CN.json b/packages/plugins/@tachybase/plugin-external-data-source/src/locale/zh-CN.json
similarity index 100%
rename from packages/plugins/@hera/plugin-external-data-source/src/locale/zh-CN.json
rename to packages/plugins/@tachybase/plugin-external-data-source/src/locale/zh-CN.json
diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/collections/.gitkeep b/packages/plugins/@tachybase/plugin-external-data-source/src/server/collections/.gitkeep
similarity index 100%
rename from packages/plugins/@hera/plugin-external-data-source/src/server/collections/.gitkeep
rename to packages/plugins/@tachybase/plugin-external-data-source/src/server/collections/.gitkeep
diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/index.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/index.ts
similarity index 100%
rename from packages/plugins/@hera/plugin-external-data-source/src/server/index.ts
rename to packages/plugins/@tachybase/plugin-external-data-source/src/server/index.ts
diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/server/mysql/database-introspector.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/mysql/database-introspector.ts
new file mode 100644
index 000000000..8e451f556
--- /dev/null
+++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/mysql/database-introspector.ts
@@ -0,0 +1,219 @@
+import { ViewFieldInference } from '@tachybase/database';
+import { mergeOptions } from '@nocobase/plugin-data-source-manager';
+import { EventEmitter } from 'events';
+import _ from 'lodash';
+
+export class DatabaseIntrospector extends EventEmitter {
+  db;
+  typeInterfaceMap;
+  constructor(options) {
+    super();
+    this.db = options.db;
+    this.typeInterfaceMap = options.typeInterfaceMap;
+  }
+  async getCollections(options: any = {}) {
+    let tableList = await this.db.sequelize.getQueryInterface().showAllTables();
+    const views = (await this.db.queryInterface.listViews()).map((view) => view.name);
+    tableList = tableList.concat(views);
+    if (this.db.options.tablePrefix) {
+      tableList = tableList.filter((tableName) => {
+        return tableName.startsWith(this.db.options.tablePrefix);
+      });
+    }
+    const batchSize = 5;
+    const results = [];
+    for (let i = 0; i < tableList.length; i += batchSize) {
+      const batch = tableList.slice(i, i + batchSize);
+      const batchResults = await Promise.all(
+        batch.map(async (tableName) => {
+          const tableInfo = {
+            tableName,
+          };
+          this.emit('loadMessage', {
+            message: `load table ${tableName}`,
+          });
+          const collectionOptions = this.tableInfoToCollectionOptions(tableInfo);
+          const localOptions = options.localData?.[collectionOptions.name];
+          try {
+            return await this.getCollection({
+              tableInfo,
+              localOptions,
+              mergedOptions: views.includes(tableName) ? { view: true, template: 'view' } : {},
+            });
+          } catch (e) {
+            if (e.message.includes('No description found for')) {
+              return false;
+            }
+            throw e;
+          }
+        }),
+      );
+      results.push(...batchResults);
+    }
+    return results.filter(Boolean);
+  }
+  async getCollection(options) {
+    const { tableInfo } = options;
+    const columnsInfo = await this.db.sequelize.getQueryInterface().describeTable(tableInfo);
+    const collectionOptions = this.tableInfoToCollectionOptions(tableInfo);
+    const constraints = await this.db.sequelize
+      .getQueryInterface()
+      .showIndex(this.db.inDialect('postgres') ? tableInfo : tableInfo.tableName);
+    try {
+      const fields = Object.keys(columnsInfo).map((columnName) => {
+        return this.columnInfoToFieldOptions(columnsInfo, columnName, constraints);
+      });
+      const unsupportedFields = fields.filter((field: any) => {
+        return field.supported === false;
+      });
+      const supportFields = fields.filter((field: any) => {
+        return field.supported !== false;
+      });
+      const remoteCollectionInfo = {
+        ...collectionOptions,
+        ...((options == null ? void 0 : options.mergedOptions) || {}),
+        ...this.collectionOptionsByFields(supportFields),
+        fields: supportFields,
+      };
+      if (unsupportedFields.length) {
+        remoteCollectionInfo.unsupportedFields = unsupportedFields;
+      }
+      const finalOptions = this.mergeLocalDataIntoCollectionOptions(remoteCollectionInfo, options.localOptions);
+      if (finalOptions.view && !finalOptions.filterTargetKey && supportFields.find((field) => field.name === 'id')) {
+        finalOptions.filterTargetKey = 'id';
+      }
+      return finalOptions;
+    } catch (e) {
+      throw new Error(`table ${tableInfo.tableName} introspection error: ${e.message}`);
+    }
+  }
+  loadCollection(options) {
+    this.db.collection({
+      ...options,
+      introspected: true,
+    });
+  }
+  loadCollections(options) {
+    options.collections.forEach((collection) => {
+      this.loadCollection(collection);
+    });
+  }
+  tableInfoToCollectionOptions(tableInfo) {
+    const tableName = tableInfo.tableName;
+    let name = tableName;
+    if (this.db.options.tablePrefix) {
+      name = tableName.replace(this.db.options.tablePrefix, '');
+    }
+    return {
+      name,
+      title: name,
+      schema: tableInfo.schema,
+      tableName,
+    };
+  }
+  collectionOptionsByFields(fields) {
+    const options: any = {
+      timestamps: false,
+      autoGenId: false,
+    };
+    const autoIncrementField = fields.find((field) => field.autoIncrement);
+    if (autoIncrementField) {
+      options.filterTargetKey = autoIncrementField.name;
+    }
+    const primaryKeys = fields.filter((field) => field.primaryKey);
+    if (!options.filterTargetKey && primaryKeys.length == 1) {
+      options.filterTargetKey = primaryKeys[0].name;
+    }
+    const uniques = fields.filter((field) => field.unique);
+    if (!options.filterTargetKey && uniques.length == 1) {
+      options.filterTargetKey = uniques[0].name;
+    }
+    return options;
+  }
+  mergeLocalDataIntoCollectionOptions(collectionOptions, localData) {
+    if (!localData) {
+      return collectionOptions;
+    }
+    const collectionFields = collectionOptions.fields || [];
+    const localFieldsAsObject = _.keyBy(localData.fields, 'name');
+    const newFields = collectionFields.map((field) => {
+      const localField = localFieldsAsObject[field.name];
+      if (!localField) {
+        return field;
+      }
+      return mergeOptions(field, localField);
+    });
+    const localAssociationFields = localData.fields?.filter((field) => {
+      return ['belongsTo', 'belongsToMany', 'hasMany', 'hasOne'].includes(field.type);
+    });
+    if (localAssociationFields) {
+      newFields.push(...localAssociationFields);
+    }
+    return {
+      ...collectionOptions,
+      ..._.omit(localData, ['fields']),
+      fields: newFields,
+    };
+  }
+  columnInfoToFieldOptions(columnsInfo, columnName, indexes) {
+    const columnInfo = columnsInfo[columnName];
+    let fieldOptions: any = {
+      ...this.columnAttribute(columnsInfo, columnName, indexes),
+      ...ViewFieldInference.inferToFieldType({
+        dialect: this.db.options.dialect,
+        type: columnInfo.type,
+        name: columnName,
+      }),
+      rawType: columnInfo.type,
+      name: columnName,
+    };
+    if (!fieldOptions.type) {
+      return {
+        rawType: columnInfo.type,
+        name: columnName,
+        supported: false,
+      };
+    }
+    const interfaceConfig = this.getDefaultInterfaceByType(columnsInfo, columnName, fieldOptions.type);
+    if (typeof interfaceConfig === 'string') {
+      fieldOptions.interface = interfaceConfig;
+    } else {
+      fieldOptions = {
+        ...fieldOptions,
+        ...interfaceConfig,
+      };
+    }
+    _.set(fieldOptions, 'uiSchema.title', columnName);
+    return fieldOptions;
+  }
+  getDefaultInterfaceByType(columnsInfo, columnName, type) {
+    const interfaceConfig = this.typeInterfaceMap[type];
+    if (typeof interfaceConfig === 'function') {
+      return interfaceConfig(columnsInfo[columnName]);
+    }
+    return interfaceConfig;
+  }
+  columnAttribute(columnsInfo, columnName, indexes) {
+    const columnInfo = columnsInfo[columnName];
+    const attr = {
+      type: columnInfo.type,
+      allowNull: columnInfo.allowNull,
+      primaryKey: columnInfo.primaryKey,
+      unique: false,
+      autoIncrement: columnInfo.autoIncrement,
+    };
+    if (columnInfo.defaultValue && typeof columnInfo.defaultValue === 'string') {
+      const isSerial = columnInfo.defaultValue.match(/^nextval\(/);
+      const isUUID = columnInfo.defaultValue.match(/^uuid_generate_v4\(/);
+      if (isSerial || isUUID) {
+        attr.autoIncrement = true;
+      }
+    }
+    for (const index of indexes) {
+      if (index.fields.length == 1 && index.fields[0].attribute == columnName && index.unique) {
+        attr.unique = true;
+      }
+    }
+    return attr;
+  }
+}
diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/server/mysql/mysql-collection-manager.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/mysql/mysql-collection-manager.ts
new file mode 100644
index 000000000..4335b889e
--- /dev/null
+++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/mysql/mysql-collection-manager.ts
@@ -0,0 +1,2 @@
+import { SequelizeCollectionManager } from '@tachybase/data-source-manager';
+export class MySQLCollectionManager extends SequelizeCollectionManager {}
diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/server/mysql/mysql-data-source.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/mysql/mysql-data-source.ts
new file mode 100644
index 000000000..d4c962ad4
--- /dev/null
+++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/mysql/mysql-data-source.ts
@@ -0,0 +1,76 @@
+import { DataSource } from '@tachybase/data-source-manager';
+import { Database } from '@tachybase/database';
+import { MySQLCollectionManager } from './mysql-collection-manager';
+import { DatabaseIntrospector } from './database-introspector';
+import typeInterfaceMap from './type-interface-map';
+
+export class MySQLDataSource extends DataSource {
+  static async testConnection(options) {
+    const database = new Database({
+      dialect: 'mysql',
+      ...options,
+    });
+    try {
+      await database.sequelize.authenticate();
+    } catch (e) {
+      throw e;
+    } finally {
+      await database.close();
+    }
+    return true;
+  }
+  async load(options) {
+    const db = this.collectionManager.db;
+    const introspector = new DatabaseIntrospector({
+      db,
+      typeInterfaceMap,
+    });
+    introspector.on('loadMessage', ({ message }) => {
+      this.emit('loadMessage', { message });
+    });
+    const { localData } = options;
+    const collections = await introspector.getCollections({
+      localData,
+    });
+    const delayFields = new Map();
+    for (const collection of collections) {
+      try {
+        const fields = collection.fields;
+        for (const field of fields) {
+          if (field.type === 'belongsToMany') {
+            if (!delayFields.has(collection.name)) {
+              delayFields.set(collection.name, []);
+            }
+            delayFields.get(collection.name).push(field);
+          }
+        }
+        this.collectionManager.defineCollection({
+          ...collection,
+          fields: fields.filter((field) => field.type !== 'belongsToMany'),
+          introspected: true,
+        });
+      } catch (e) {
+        db.logger.error(`load collection failed, ${e}`);
+        delayFields.delete(collection.name);
+      }
+    }
+    for (const [name, fields] of delayFields.entries()) {
+      try {
+        for (const field of fields) {
+          this.collectionManager.getCollection(name).setField(field.name, field);
+        }
+      } catch (e) {
+        db.logger.error(`load belongs to many field failed, ${e}`);
+      }
+    }
+  }
+  createCollectionManager(options) {
+    const database = new Database({
+      dialect: 'mysql',
+      ...options,
+    });
+    return new MySQLCollectionManager({
+      database,
+    });
+  }
+}
diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/server/mysql/type-interface-map.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/mysql/type-interface-map.ts
new file mode 100644
index 000000000..daae201e1
--- /dev/null
+++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/mysql/type-interface-map.ts
@@ -0,0 +1,241 @@
+const typeInterfaceMap = {
+  array: '',
+  belongsTo: '',
+  belongsToMany: '',
+  boolean: () => {
+    return {
+      interface: 'checkbox',
+      uiSchema: {
+        type: 'boolean',
+        'x-component': 'Checkbox',
+      },
+    };
+  },
+  context: '',
+  date: () => {
+    return {
+      interface: 'datetime',
+      uiSchema: {
+        'x-component': 'DatePicker',
+        'x-component-props': {
+          dateFormat: 'YYYY-MM-DD',
+          showTime: false,
+        },
+      },
+    };
+  },
+  hasMany: '',
+  hasOne: '',
+  json: () => {
+    return {
+      interface: 'json',
+      uiSchema: {
+        'x-component': 'Input.JSON',
+        'x-component-props': {
+          autoSize: {
+            minRows: 5,
+            // maxRows: 20,
+          },
+        },
+        default: null,
+      },
+    };
+  },
+  jsonb: () => {
+    return {
+      interface: 'json',
+      uiSchema: {
+        'x-component': 'Input.JSON',
+        'x-component-props': {
+          autoSize: {
+            minRows: 5,
+            // maxRows: 20,
+          },
+        },
+        default: null,
+      },
+    };
+  },
+  integer: () => ({
+    interface: 'integer',
+    // name,
+    uiSchema: {
+      type: 'number',
+      // title,
+      'x-component': 'InputNumber',
+      'x-component-props': {
+        stringMode: true,
+        step: '1',
+      },
+      'x-validator': 'integer',
+    },
+  }),
+  bigInt: (columnInfo) => {
+    return {
+      interface: 'integer',
+      uiSchema: {
+        'x-component': 'InputNumber',
+        'x-component-props': {
+          style: {
+            width: '100%',
+          },
+        },
+      },
+    };
+  },
+  float: () => {
+    return {
+      interface: 'number',
+      uiSchema: {
+        type: 'number',
+        // title,
+        'x-component': 'InputNumber',
+        'x-component-props': {
+          stringMode: true,
+          step: '1',
+        },
+      },
+    };
+  },
+  double: () => {
+    return {
+      interface: 'number',
+      uiSchema: {
+        type: 'number',
+        // title,
+        'x-component': 'InputNumber',
+        'x-component-props': {
+          stringMode: true,
+          step: '1',
+        },
+      },
+    };
+  },
+  real: () => {
+    return {
+      interface: 'number',
+      uiSchema: {
+        type: 'number',
+        // title,
+        'x-component': 'InputNumber',
+        'x-component-props': {
+          stringMode: true,
+          step: '1',
+        },
+      },
+    };
+  },
+  decimal: () => {
+    return {
+      interface: 'number',
+      uiSchema: {
+        type: 'number',
+        // title,
+        'x-component': 'InputNumber',
+        'x-component-props': {
+          stringMode: true,
+          step: '1',
+        },
+      },
+    };
+  },
+  password: () => ({
+    interface: 'password',
+    hidden: true,
+    // name,
+    uiSchema: {
+      type: 'string',
+      // title,
+      'x-component': 'Password',
+    },
+  }),
+  radio: '',
+  set: '',
+  sort: '',
+  string: (columnInfo) => {
+    const type = columnInfo.type;
+    if (type.startsWith('ENUM')) {
+      return {
+        interface: 'select',
+        uiSchema: {
+          type: 'string',
+          'x-component': 'Select',
+          enum: type
+            .replace('ENUM(', '')
+            .replace(')', '')
+            .replace(/'/g, '')
+            .split(',')
+            .map((item) => ({
+              label: item,
+              value: item,
+            })),
+        },
+      };
+    }
+    return {
+      interface: 'input',
+      uiSchema: {
+        'x-component': 'Input',
+        'x-component-props': {
+          style: {
+            width: '100%',
+          },
+        },
+      },
+    };
+  },
+  text: () => {
+    return {
+      interface: 'textarea',
+      // name,
+      uiSchema: {
+        type: 'string',
+        'x-component': 'Input.TextArea',
+      },
+    };
+  },
+  time: () => ({
+    interface: 'time',
+    // name,
+    uiSchema: {
+      type: 'string',
+      'x-component': 'TimePicker',
+      'x-component-props': {
+        format: 'HH:mm:ss',
+      },
+    },
+  }),
+  uid: () => {
+    return {
+      interface: 'input',
+      uiSchema: {
+        'x-component': 'Input',
+        'x-component-props': {
+          style: {
+            width: '100%',
+          },
+        },
+      },
+    };
+  },
+  uuid: () => {
+    return {
+      interface: 'uuid',
+      uiSchema: {
+        'x-component': 'Input',
+        'x-component-props': {
+          style: {
+            width: '100%',
+          },
+        },
+      },
+    };
+  },
+  virtual: '',
+  point: '',
+  polygon: '',
+  lineString: '',
+  circle: '',
+};
+
+export default typeInterfaceMap;
diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/services/database-introspector.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/pg/database-introspector.ts
similarity index 100%
rename from packages/plugins/@hera/plugin-external-data-source/src/server/services/database-introspector.ts
rename to packages/plugins/@tachybase/plugin-external-data-source/src/server/pg/database-introspector.ts
diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/postgres-collection-manager.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/pg/postgres-collection-manager.ts
similarity index 100%
rename from packages/plugins/@hera/plugin-external-data-source/src/server/postgres-collection-manager.ts
rename to packages/plugins/@tachybase/plugin-external-data-source/src/server/pg/postgres-collection-manager.ts
diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/postgres-data-source.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/pg/postgres-data-source.ts
similarity index 95%
rename from packages/plugins/@hera/plugin-external-data-source/src/server/postgres-data-source.ts
rename to packages/plugins/@tachybase/plugin-external-data-source/src/server/pg/postgres-data-source.ts
index 7ffd97b7d..bf051ac9d 100644
--- a/packages/plugins/@hera/plugin-external-data-source/src/server/postgres-data-source.ts
+++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/pg/postgres-data-source.ts
@@ -1,8 +1,8 @@
 import { DataSource } from '@tachybase/data-source-manager';
 import { Database } from '@tachybase/database';
 import { PostgresCollectionManager } from './postgres-collection-manager';
-import { DatabaseIntrospector } from './services/database-introspector';
-import typeInterfaceMap from './services/type-interface-map';
+import { DatabaseIntrospector } from './database-introspector';
+import typeInterfaceMap from './type-interface-map';
 
 export class PostgresDataSource extends DataSource {
   static async testConnection(options) {
diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/services/type-interface-map.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/pg/type-interface-map.ts
similarity index 100%
rename from packages/plugins/@hera/plugin-external-data-source/src/server/services/type-interface-map.ts
rename to packages/plugins/@tachybase/plugin-external-data-source/src/server/pg/type-interface-map.ts
diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/services/utils.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/pg/utils.ts
similarity index 100%
rename from packages/plugins/@hera/plugin-external-data-source/src/server/services/utils.ts
rename to packages/plugins/@tachybase/plugin-external-data-source/src/server/pg/utils.ts
diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/plugin.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/plugin.ts
similarity index 75%
rename from packages/plugins/@hera/plugin-external-data-source/src/server/plugin.ts
rename to packages/plugins/@tachybase/plugin-external-data-source/src/server/plugin.ts
index 5d779c28b..69f0b1d11 100644
--- a/packages/plugins/@hera/plugin-external-data-source/src/server/plugin.ts
+++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/plugin.ts
@@ -1,11 +1,13 @@
 import { Plugin } from '@tachybase/server';
-import { PostgresDataSource } from './postgres-data-source';
+import { PostgresDataSource } from './pg/postgres-data-source';
+import { MySQLDataSource } from './mysql/mysql-data-source';
 
 export class PluginExternalDataSourceServer extends Plugin {
   async afterAdd() {}
 
   async beforeLoad() {
     this.app.dataSourceManager.factory.register('postgres', PostgresDataSource);
+    this.app.dataSourceManager.factory.register('mysql', MySQLDataSource);
   }
   async beforeEnable() {
     const plugin = this.pm.get('data-source-manager');
diff --git a/packages/presets/rental/package.json b/packages/presets/rental/package.json
index fb7d85866..8769992d1 100644
--- a/packages/presets/rental/package.json
+++ b/packages/presets/rental/package.json
@@ -51,6 +51,7 @@
     "@nocobase/plugin-users": "workspace:*",
     "@nocobase/plugin-verification": "workspace:*",
     "@tachybase/client": "workspace:*",
+    "@tachybase/plugin-external-data-source": "workspace:*",
     "@tachybase/plugin-mobile-client": "workspace:*",
     "@tachybase/plugin-system-settings": "workspace:*",
     "@tachybase/plugin-workflow": "workspace:*",
diff --git a/packages/presets/rental/src/server/index.ts b/packages/presets/rental/src/server/index.ts
index c637bd9cf..b0e51b63f 100644
--- a/packages/presets/rental/src/server/index.ts
+++ b/packages/presets/rental/src/server/index.ts
@@ -2,7 +2,7 @@ import PresetTachyBase from '@tachybase/preset-tachybase';
 import _ from 'lodash';
 
 export class PluginRental extends PresetTachyBase {
-  #builtInPlugins = ['approval-mobile', 'core', 'homepage'];
+  #builtInPlugins = ['approval-mobile', 'core', 'homepage', 'external-data-source'];
 
   get builtInPlugins() {
     return super.builtInPlugins.concat(this.#builtInPlugins);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 275dd79d4..1d97e5467 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1638,40 +1638,6 @@ importers:
         specifier: ^8.13.0
         version: 8.15.1
 
-  packages/plugins/@hera/plugin-external-data-source:
-    dependencies:
-      '@tachybase/client':
-        specifier: workspace:*
-        version: link:../../../core/client
-      '@tachybase/data-source-manager':
-        specifier: workspace:*
-        version: link:../../../core/data-source-manager
-      '@tachybase/database':
-        specifier: workspace:*
-        version: link:../../../core/database
-      '@tachybase/server':
-        specifier: workspace:*
-        version: link:../../../core/server
-      '@tachybase/test':
-        specifier: workspace:*
-        version: link:../../../core/test
-      antd:
-        specifier: 5.16.1
-        version: 5.16.1(react-dom@18.2.0)(react@18.2.0)
-      lodash:
-        specifier: ^4.17.21
-        version: 4.17.21
-      pg:
-        specifier: ^8.11.3
-        version: 8.11.3
-      react-i18next:
-        specifier: ^11.15.1
-        version: 11.18.6(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0)
-    devDependencies:
-      '@types/lodash':
-        specifier: ^4.17.0
-        version: 4.17.0
-
   packages/plugins/@hera/plugin-homepage:
     dependencies:
       '@tachybase/client':
@@ -3819,6 +3785,40 @@ importers:
         specifier: ^4.0.525
         version: 4.0.759
 
+  packages/plugins/@tachybase/plugin-external-data-source:
+    dependencies:
+      '@tachybase/client':
+        specifier: workspace:*
+        version: link:../../../core/client
+      '@tachybase/data-source-manager':
+        specifier: workspace:*
+        version: link:../../../core/data-source-manager
+      '@tachybase/database':
+        specifier: workspace:*
+        version: link:../../../core/database
+      '@tachybase/server':
+        specifier: workspace:*
+        version: link:../../../core/server
+      '@tachybase/test':
+        specifier: workspace:*
+        version: link:../../../core/test
+      antd:
+        specifier: 5.16.1
+        version: 5.16.1(react-dom@18.2.0)(react@18.2.0)
+      lodash:
+        specifier: ^4.17.21
+        version: 4.17.21
+      pg:
+        specifier: ^8.11.3
+        version: 8.11.3
+      react-i18next:
+        specifier: ^11.15.1
+        version: 11.18.6(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0)
+    devDependencies:
+      '@types/lodash':
+        specifier: ^4.17.0
+        version: 4.17.0
+
   packages/plugins/@tachybase/plugin-field-markdown-vditor:
     dependencies:
       '@tachybase/client':
@@ -4357,6 +4357,9 @@ importers:
       '@tachybase/client':
         specifier: workspace:*
         version: link:../../core/client
+      '@tachybase/plugin-external-data-source':
+        specifier: workspace:*
+        version: link:../../plugins/@tachybase/plugin-external-data-source
       '@tachybase/plugin-mobile-client':
         specifier: workspace:*
         version: link:../../plugins/@tachybase/plugin-mobile-client
@@ -4847,7 +4850,7 @@ packages:
       react-dom: '>=17.0.0'
     dependencies:
       '@ant-design/cssinjs': 1.20.0(react-dom@18.2.0)(react@18.2.0)
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       '@ctrl/tinycolor': 3.6.1
       antd: 5.16.1(react-dom@18.2.0)(react@18.2.0)
       rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
@@ -4864,7 +4867,7 @@ packages:
     dependencies:
       '@ant-design/icons': 5.3.6(react-dom@18.2.0)(react@18.2.0)
       '@ant-design/pro-provider': 2.13.5(antd@5.16.1)(react-dom@18.2.0)(react@18.2.0)
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       antd: 5.16.1(react-dom@18.2.0)(react@18.2.0)
       classnames: 2.5.1
       dayjs: 1.11.10
@@ -4880,7 +4883,7 @@ packages:
     peerDependencies:
       react: '>=16.9.0'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       classnames: 2.5.1
       json2mq: 0.2.0
       react: 18.2.0
@@ -8056,7 +8059,7 @@ packages:
     resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==}
     dependencies:
       '@babel/helper-module-imports': 7.22.15
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       '@emotion/hash': 0.9.1
       '@emotion/memoize': 0.8.1
       '@emotion/serialize': 1.1.2
@@ -8109,7 +8112,7 @@ packages:
       '@types/react':
         optional: true
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       '@emotion/babel-plugin': 11.11.0
       '@emotion/cache': 11.11.0
       '@emotion/serialize': 1.1.2
@@ -10530,7 +10533,7 @@ packages:
       react: '>=16.9.0'
       react-dom: '>=16.9.0'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0)
       '@rc-component/trigger': 2.1.0(react-dom@18.2.0)(react@18.2.0)
       classnames: 2.5.1
@@ -10562,7 +10565,7 @@ packages:
       react: '>=16.9.0'
       react-dom: '>=16.9.0'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0)
       classnames: 2.5.1
       rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
@@ -25641,7 +25644,7 @@ packages:
       react: '>=16.9.0'
       react-dom: '>=16.9.0'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       array-tree-filter: 2.1.0
       classnames: 2.5.1
       rc-select: 14.13.0(react-dom@18.2.0)(react@18.2.0)
@@ -25681,7 +25684,7 @@ packages:
       react: '>=16.9.0'
       react-dom: '>=16.9.0'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0)
       classnames: 2.5.1
       rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
@@ -25695,7 +25698,7 @@ packages:
       react: '>=16.9.0'
       react-dom: '>=16.9.0'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0)
       classnames: 2.5.1
       rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
@@ -25736,7 +25739,7 @@ packages:
       react: '>=16.9.0'
       react-dom: '>=16.9.0'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       async-validator: 4.2.5
       rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
       react: 18.2.0
@@ -25748,7 +25751,7 @@ packages:
       react: '>=16.9.0'
       react-dom: '>=16.9.0'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0)
       classnames: 2.5.1
       rc-dialog: 9.4.0(react-dom@18.2.0)(react@18.2.0)
@@ -25763,7 +25766,7 @@ packages:
       react: '>=16.9.0'
       react-dom: '>=16.9.0'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       '@rc-component/mini-decimal': 1.1.0
       classnames: 2.5.1
       rc-input: 1.4.5(react-dom@18.2.0)(react@18.2.0)
@@ -25805,7 +25808,7 @@ packages:
       react: '>=16.9.0'
       react-dom: '>=16.9.0'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       '@rc-component/trigger': 2.1.0(react-dom@18.2.0)(react@18.2.0)
       classnames: 2.5.1
       rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
@@ -25885,7 +25888,7 @@ packages:
       moment:
         optional: true
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       '@rc-component/trigger': 2.1.0(react-dom@18.2.0)(react@18.2.0)
       classnames: 2.5.1
       dayjs: 1.11.10
@@ -25971,7 +25974,7 @@ packages:
       react: '*'
       react-dom: '*'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       '@rc-component/trigger': 1.18.3(react-dom@18.2.0)(react@18.2.0)
       classnames: 2.5.1
       rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
@@ -25989,7 +25992,7 @@ packages:
       react: '*'
       react-dom: '*'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       '@rc-component/trigger': 2.1.0(react-dom@18.2.0)(react@18.2.0)
       classnames: 2.5.1
       rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
@@ -26044,7 +26047,7 @@ packages:
       react: '>=16.9.0'
       react-dom: '>=16.9.0'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       '@rc-component/context': 1.4.0(react-dom@18.2.0)(react@18.2.0)
       classnames: 2.5.1
       rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0)
@@ -26117,7 +26120,7 @@ packages:
       react: '*'
       react-dom: '*'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       classnames: 2.5.1
       rc-select: 14.13.0(react-dom@18.2.0)(react@18.2.0)
       rc-tree: 5.8.5(react-dom@18.2.0)(react@18.2.0)
@@ -26163,7 +26166,7 @@ packages:
       react: '*'
       react-dom: '*'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       classnames: 2.5.1
       rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
       rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
@@ -26179,7 +26182,7 @@ packages:
       react: '*'
       react-dom: '*'
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       classnames: 2.5.1
       rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
       rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
@@ -26585,7 +26588,7 @@ packages:
       react-native:
         optional: true
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
       '@types/react-redux': 7.1.33
       hoist-non-react-statics: 3.3.2
       loose-envify: 1.4.0
@@ -26943,7 +26946,7 @@ packages:
   /redux@4.2.1:
     resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
     dependencies:
-      '@babel/runtime': 7.24.4
+      '@babel/runtime': 7.24.5
 
   /reflect-metadata@0.1.14:
     resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==}