diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBObjectOperatorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBObjectOperatorFactory.java
index 74c883b23d..c27738be72 100644
--- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBObjectOperatorFactory.java
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBObjectOperatorFactory.java
@@ -23,6 +23,7 @@
import com.oceanbase.tools.dbbrowser.AbstractDBBrowserFactory;
import com.oceanbase.tools.dbbrowser.editor.mysql.MySQLObjectOperator;
import com.oceanbase.tools.dbbrowser.editor.oracle.OracleObjectOperator;
+import com.oceanbase.tools.dbbrowser.editor.postgre.PostgresObjectOperator;
import com.oceanbase.tools.dbbrowser.editor.sqlserver.SqlServerObjectOperator;
import lombok.Setter;
@@ -67,7 +68,7 @@ public DBObjectOperator buildForOdpSharding() {
@Override
public DBObjectOperator buildForPostgres() {
- throw new UnsupportedOperationException("Not supported yet");
+ return new PostgresObjectOperator(getJdbcOperations());
}
@Override
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableColumnEditorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableColumnEditorFactory.java
index 050c65557d..b4958bc3d8 100644
--- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableColumnEditorFactory.java
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableColumnEditorFactory.java
@@ -18,6 +18,7 @@
import com.oceanbase.tools.dbbrowser.AbstractDBBrowserFactory;
import com.oceanbase.tools.dbbrowser.editor.mysql.MySQLColumnEditor;
import com.oceanbase.tools.dbbrowser.editor.oracle.OracleColumnEditor;
+import com.oceanbase.tools.dbbrowser.editor.postgre.PostgresColumnEditor;
import com.oceanbase.tools.dbbrowser.editor.sqlserver.SqlServerColumnEditor;
public class DBTableColumnEditorFactory extends AbstractDBBrowserFactory {
@@ -54,7 +55,7 @@ public DBTableColumnEditor buildForOdpSharding() {
@Override
public DBTableColumnEditor buildForPostgres() {
- throw new UnsupportedOperationException("Not supported yet");
+ return new PostgresColumnEditor();
}
@Override
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableConstraintEditorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableConstraintEditorFactory.java
index 495cfdca84..eef54c0f5f 100644
--- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableConstraintEditorFactory.java
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableConstraintEditorFactory.java
@@ -22,6 +22,7 @@
import com.oceanbase.tools.dbbrowser.editor.mysql.OBMySQLLessThan400ConstraintEditor;
import com.oceanbase.tools.dbbrowser.editor.oracle.OBOracleLessThan400ConstraintEditor;
import com.oceanbase.tools.dbbrowser.editor.oracle.OracleConstraintEditor;
+import com.oceanbase.tools.dbbrowser.editor.postgre.PostgresConstraintEditor;
import com.oceanbase.tools.dbbrowser.editor.sqlserver.SqlServerConstraintEditor;
import com.oceanbase.tools.dbbrowser.util.VersionUtils;
@@ -74,7 +75,7 @@ public DBTableConstraintEditor buildForOdpSharding() {
@Override
public DBTableConstraintEditor buildForPostgres() {
- throw new UnsupportedOperationException("Not supported yet");
+ return new PostgresConstraintEditor();
}
@Override
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableEditorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableEditorFactory.java
index de0eea4cf2..8ea0b9ad3c 100644
--- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableEditorFactory.java
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableEditorFactory.java
@@ -22,6 +22,7 @@
import com.oceanbase.tools.dbbrowser.editor.mysql.OBMySQLLessThan400TableEditor;
import com.oceanbase.tools.dbbrowser.editor.mysql.OBMySQLTableEditor;
import com.oceanbase.tools.dbbrowser.editor.oracle.OracleTableEditor;
+import com.oceanbase.tools.dbbrowser.editor.postgre.PostgresTableEditor;
import com.oceanbase.tools.dbbrowser.editor.sqlserver.SqlServerTableEditor;
import com.oceanbase.tools.dbbrowser.util.VersionUtils;
@@ -81,7 +82,10 @@ public DBTableEditor buildForOdpSharding() {
@Override
public DBTableEditor buildForPostgres() {
- throw new UnsupportedOperationException("Not supported yet");
+ return new PostgresTableEditor(getTableIndexEditor(),
+ getTableColumnEditor(),
+ getTableConstraintEditor(),
+ getTablePartitionEditor());
}
@Override
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableIndexEditorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableIndexEditorFactory.java
index 95a9d93c34..54f2c14226 100644
--- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableIndexEditorFactory.java
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableIndexEditorFactory.java
@@ -20,6 +20,7 @@
import com.oceanbase.tools.dbbrowser.editor.mysql.OBMySQLIndexEditor;
import com.oceanbase.tools.dbbrowser.editor.oracle.OBOracleIndexEditor;
import com.oceanbase.tools.dbbrowser.editor.oracle.OracleIndexEditor;
+import com.oceanbase.tools.dbbrowser.editor.postgre.PostgresIndexEditor;
import com.oceanbase.tools.dbbrowser.editor.sqlserver.SqlServerIndexEditor;
import lombok.Setter;
@@ -61,7 +62,7 @@ public DBTableIndexEditor buildForOdpSharding() {
@Override
public DBTableIndexEditor buildForPostgres() {
- throw new UnsupportedOperationException("Not supported yet");
+ return new PostgresIndexEditor();
}
@Override
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTablePartitionEditorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTablePartitionEditorFactory.java
index b9cf8f9624..ae81185c82 100644
--- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTablePartitionEditorFactory.java
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTablePartitionEditorFactory.java
@@ -24,6 +24,7 @@
import com.oceanbase.tools.dbbrowser.editor.mysql.OBMySQLLessThan400DBTablePartitionEditor;
import com.oceanbase.tools.dbbrowser.editor.oracle.OBOracleLessThan400DBTablePartitionEditor;
import com.oceanbase.tools.dbbrowser.editor.oracle.OracleDBTablePartitionEditor;
+import com.oceanbase.tools.dbbrowser.editor.postgre.PostgresPartitionEditor;
import com.oceanbase.tools.dbbrowser.editor.sqlserver.SqlServerPartitionEditor;
import com.oceanbase.tools.dbbrowser.util.VersionUtils;
@@ -79,7 +80,7 @@ public DBTablePartitionEditor buildForOdpSharding() {
@Override
public DBTablePartitionEditor buildForPostgres() {
- throw new UnsupportedOperationException("Not supported yet");
+ return new PostgresPartitionEditor();
}
@Override
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresColumnEditor.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresColumnEditor.java
new file mode 100644
index 0000000000..ee8e19c2a8
--- /dev/null
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresColumnEditor.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.editor.postgre;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+import javax.validation.constraints.NotNull;
+
+import com.oceanbase.tools.dbbrowser.editor.DBTableColumnEditor;
+import com.oceanbase.tools.dbbrowser.model.DBTableColumn;
+import com.oceanbase.tools.dbbrowser.util.PostgresSqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.SqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.StringUtils;
+
+/**
+ * PostgreSQL 列编辑器
+ *
+ *
+ * PostgreSQL 的 ALTER COLUMN 语法与 MySQL/Oracle 有较大差异:
+ *
+ *
+ * 修改类型:ALTER TABLE ... ALTER COLUMN ... TYPE new_type
+ * 设置默认值:ALTER TABLE ... ALTER COLUMN ... SET DEFAULT value
+ * 删除默认值:ALTER TABLE ... ALTER COLUMN ... DROP DEFAULT
+ * 设置非空:ALTER TABLE ... ALTER COLUMN ... SET NOT NULL
+ * 删除非空:ALTER TABLE ... ALTER COLUMN ... DROP NOT NULL
+ * 重命名列:ALTER TABLE ... RENAME COLUMN old TO new
+ * 列注释:COMMENT ON COLUMN ... IS 'comment'
+ *
+ *
+ *
+ * 注意:PostgreSQL 的 ALTER COLUMN 每次只能修改一个属性,不能合并为一条语句。
+ *
+ *
+ * @author odc
+ * @since ODC_release_4.3.4
+ */
+public class PostgresColumnEditor extends DBTableColumnEditor {
+
+ @Override
+ protected SqlBuilder sqlBuilder() {
+ return new PostgresSqlBuilder();
+ }
+
+ @Override
+ protected boolean appendColumnKeyWord() {
+ return false;
+ }
+
+ @Override
+ protected List getSupportColumnModifiers() {
+ return Arrays.asList(
+ new PostgresDataTypeModifier(),
+ new PostgresNullNotNullModifier(),
+ new PostgresDefaultOptionModifier());
+ }
+
+ /**
+ * 生成添加列的 DDL 语句
+ *
+ *
+ * PostgreSQL 添加列语法: ALTER TABLE "schema"."table" ADD COLUMN "column_name" type [NOT NULL] [DEFAULT
+ * value];
+ *
+ *
+ * @param column 要添加的列定义
+ * @return 生成的 DDL 语句字符串
+ */
+ @Override
+ public String generateCreateObjectDDL(@NotNull DBTableColumn column) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+
+ // 生成 ALTER TABLE ... ADD COLUMN 语句
+ sqlBuilder.append("ALTER TABLE ").append(getFullyQualifiedTableName(column))
+ .append(" ADD COLUMN ");
+ appendColumnDefinition(column, sqlBuilder);
+ sqlBuilder.append(";").line();
+
+ // 生成列注释语句
+ generateColumnComment(column, sqlBuilder);
+
+ String ddl = sqlBuilder.toString();
+ if (!ddl.trim().endsWith(";")) {
+ ddl += ";\n";
+ }
+ return ddl;
+ }
+
+ /**
+ * 生成更新列的 DDL 语句
+ *
+ *
+ * PostgreSQL 的 ALTER COLUMN 每次只能修改一个属性,因此需要对比新旧列, 对每个变更属性分别生成对应的 ALTER 语句。
+ *
+ *
+ * @param oldColumn 修改前的列定义
+ * @param newColumn 修改后的列定义
+ * @return 生成的 DDL 语句字符串
+ */
+ @Override
+ public String generateUpdateObjectDDL(@NotNull DBTableColumn oldColumn,
+ @NotNull DBTableColumn newColumn) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+
+ // 步骤1:检查列名是否改变
+ if (!StringUtils.equals(oldColumn.getName(), newColumn.getName())) {
+ sqlBuilder.append(generateRenameObjectDDL(oldColumn, newColumn)).append("\n");
+ }
+
+ // 步骤2:检查数据类型是否改变
+ if (!isTypeEqual(oldColumn, newColumn)) {
+ generateAlterColumnType(newColumn, sqlBuilder);
+ }
+
+ // 步骤3:检查默认值是否改变
+ if (!Objects.equals(oldColumn.getDefaultValue(), newColumn.getDefaultValue())) {
+ generateAlterDefaultValue(newColumn, sqlBuilder);
+ }
+
+ // 步骤4:检查 NOT NULL 是否改变
+ if (!Objects.equals(oldColumn.getNullable(), newColumn.getNullable())) {
+ generateAlterNullable(newColumn, sqlBuilder);
+ }
+
+ // 步骤5:检查列注释是否改变
+ if (!Objects.equals(oldColumn.getComment(), newColumn.getComment())) {
+ generateColumnComment(newColumn, sqlBuilder);
+ }
+
+ return sqlBuilder.toString();
+ }
+
+ /**
+ * 生成删除列的 DDL 语句
+ *
+ *
+ * PostgreSQL 删除列语法:ALTER TABLE "schema"."table" DROP COLUMN "column_name";
+ *
+ *
+ * @param column 要删除的列定义
+ * @return 生成的 DDL 语句字符串
+ */
+ @Override
+ public String generateDropObjectDDL(@NotNull DBTableColumn column) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+ sqlBuilder.append("ALTER TABLE ").append(getFullyQualifiedTableName(column))
+ .append(" DROP COLUMN ").identifier(column.getName()).append(";\n");
+ return sqlBuilder.toString();
+ }
+
+ /**
+ * 生成重命名列的 DDL 语句
+ *
+ *
+ * PostgreSQL 重命名列语法:ALTER TABLE "schema"."table" RENAME COLUMN "old" TO "new";
+ *
+ *
+ * @param oldColumn 重命名前的列定义
+ * @param newColumn 重命名后的列定义
+ * @return 生成的 DDL 语句字符串
+ */
+ @Override
+ public String generateRenameObjectDDL(@NotNull DBTableColumn oldColumn,
+ @NotNull DBTableColumn newColumn) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+ sqlBuilder.append("ALTER TABLE ").append(getFullyQualifiedTableName(oldColumn))
+ .append(" RENAME COLUMN ").identifier(oldColumn.getName())
+ .append(" TO ").identifier(newColumn.getName()).append(";");
+ return sqlBuilder.toString();
+ }
+
+ /**
+ * 生成列注释的 DDL 语句
+ *
+ *
+ * PostgreSQL 列注释语法:COMMENT ON COLUMN "schema"."table"."column" IS 'comment';
+ *
+ *
+ * @param column 列定义
+ * @param sqlBuilder SQL 构建器
+ */
+ @Override
+ protected void generateColumnComment(DBTableColumn column, SqlBuilder sqlBuilder) {
+ if (StringUtils.isBlank(column.getComment())) {
+ return;
+ }
+ sqlBuilder.append("COMMENT ON COLUMN ")
+ .append(getFullyQualifiedTableName(column)).append(".")
+ .identifier(column.getName())
+ .append(" IS ").value(column.getComment()).append(";\n");
+ }
+
+ /**
+ * 生成修改列类型的 DDL 语句
+ *
+ *
+ * PostgreSQL 语法:ALTER TABLE "schema"."table" ALTER COLUMN "column" TYPE new_type;
+ *
+ */
+ private void generateAlterColumnType(DBTableColumn column, SqlBuilder sqlBuilder) {
+ sqlBuilder.append("ALTER TABLE ").append(getFullyQualifiedTableName(column))
+ .append(" ALTER COLUMN ").identifier(column.getName())
+ .append(" TYPE ");
+ appendColumnType(column, sqlBuilder);
+ sqlBuilder.append(";\n");
+ }
+
+ /**
+ * 生成修改列默认值的 DDL 语句
+ *
+ *
+ * PostgreSQL 语法:
+ *
+ * 设置默认值:ALTER TABLE ... ALTER COLUMN ... SET DEFAULT value;
+ * 删除默认值:ALTER TABLE ... ALTER COLUMN ... DROP DEFAULT;
+ *
+ *
+ */
+ private void generateAlterDefaultValue(DBTableColumn column, SqlBuilder sqlBuilder) {
+ sqlBuilder.append("ALTER TABLE ").append(getFullyQualifiedTableName(column))
+ .append(" ALTER COLUMN ").identifier(column.getName());
+ if (StringUtils.isNotBlank(column.getDefaultValue())) {
+ sqlBuilder.append(" SET DEFAULT ").append(column.getDefaultValue());
+ } else {
+ sqlBuilder.append(" DROP DEFAULT");
+ }
+ sqlBuilder.append(";\n");
+ }
+
+ /**
+ * 生成修改列 NOT NULL 的 DDL 语句
+ *
+ *
+ * PostgreSQL 语法:
+ *
+ * 设置非空:ALTER TABLE ... ALTER COLUMN ... SET NOT NULL;
+ * 删除非空:ALTER TABLE ... ALTER COLUMN ... DROP NOT NULL;
+ *
+ *
+ */
+ private void generateAlterNullable(DBTableColumn column, SqlBuilder sqlBuilder) {
+ sqlBuilder.append("ALTER TABLE ").append(getFullyQualifiedTableName(column))
+ .append(" ALTER COLUMN ").identifier(column.getName());
+ if (Boolean.FALSE.equals(column.getNullable())) {
+ sqlBuilder.append(" SET NOT NULL");
+ } else {
+ sqlBuilder.append(" DROP NOT NULL");
+ }
+ sqlBuilder.append(";\n");
+ }
+
+ /**
+ * 判断两个列的数据类型是否相等
+ */
+ private boolean isTypeEqual(DBTableColumn oldColumn, DBTableColumn newColumn) {
+ if (!Objects.equals(oldColumn.getTypeName(), newColumn.getTypeName())) {
+ return false;
+ }
+ if (!Objects.equals(oldColumn.getPrecision(), newColumn.getPrecision())) {
+ return false;
+ }
+ if (!Objects.equals(oldColumn.getScale(), newColumn.getScale())) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * 追加列类型定义
+ */
+ private void appendColumnType(DBTableColumn column, SqlBuilder sqlBuilder) {
+ String typeName = column.getTypeName();
+ Long precision = column.getPrecision();
+ Integer scale = column.getScale();
+
+ sqlBuilder.append(typeName);
+ if (needsPrecision(typeName)) {
+ if (precision != null) {
+ sqlBuilder.append("(").append(precision);
+ if (scale != null) {
+ sqlBuilder.append(",").append(scale);
+ }
+ sqlBuilder.append(")");
+ }
+ }
+ }
+
+ /**
+ * 判断数据类型是否需要精度参数
+ */
+ private boolean needsPrecision(String typeName) {
+ if (StringUtils.isBlank(typeName)) {
+ return false;
+ }
+ String lowerTypeName = typeName.toLowerCase();
+ return lowerTypeName.equals("varchar") || lowerTypeName.equals("char")
+ || lowerTypeName.equals("character varying") || lowerTypeName.equals("character")
+ || lowerTypeName.equals("numeric") || lowerTypeName.equals("decimal")
+ || lowerTypeName.equals("bit") || lowerTypeName.equals("varbit");
+ }
+
+ /**
+ * PostgreSQL 数据类型修饰符
+ */
+ protected class PostgresDataTypeModifier implements DBColumnModifier {
+ @Override
+ public void appendModifier(DBTableColumn column, SqlBuilder sqlBuilder) {
+ sqlBuilder.space();
+ appendColumnType(column, sqlBuilder);
+ }
+ }
+
+ /**
+ * PostgreSQL NULL/NOT NULL 修饰符
+ */
+ protected class PostgresNullNotNullModifier implements DBColumnModifier {
+ @Override
+ public void appendModifier(DBTableColumn column, SqlBuilder sqlBuilder) {
+ sqlBuilder.append(column.getNullable() ? " NULL" : " NOT NULL");
+ }
+ }
+
+ /**
+ * PostgreSQL 默认值修饰符
+ */
+ protected class PostgresDefaultOptionModifier implements DBColumnModifier {
+ @Override
+ public void appendModifier(DBTableColumn column, SqlBuilder sqlBuilder) {
+ if (StringUtils.isNotBlank(column.getDefaultValue())) {
+ sqlBuilder.append(" DEFAULT ").append(column.getDefaultValue());
+ }
+ }
+ }
+}
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresConstraintEditor.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresConstraintEditor.java
new file mode 100644
index 0000000000..e59da58f0b
--- /dev/null
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresConstraintEditor.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.editor.postgre;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.validation.constraints.NotNull;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import com.oceanbase.tools.dbbrowser.editor.DBTableConstraintEditor;
+import com.oceanbase.tools.dbbrowser.model.DBConstraintType;
+import com.oceanbase.tools.dbbrowser.model.DBForeignKeyModifyRule;
+import com.oceanbase.tools.dbbrowser.model.DBTableConstraint;
+import com.oceanbase.tools.dbbrowser.util.PostgresSqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.SqlBuilder;
+
+/**
+ * PostgreSQL 约束编辑器
+ *
+ *
+ * PostgreSQL 约束语法特点:
+ *
+ *
+ * 添加约束:ALTER TABLE "schema"."table" ADD CONSTRAINT "name" PRIMARY KEY/UNIQUE/CHECK/FOREIGN KEY
+ * (...);
+ * 删除约束:ALTER TABLE "schema"."table" DROP CONSTRAINT "name";
+ * 重命名约束:ALTER TABLE "schema"."table" RENAME CONSTRAINT "old" TO "new";
+ *
+ *
+ *
+ * PostgreSQL 支持以下约束类型:
+ *
+ * PRIMARY KEY - 主键约束
+ * UNIQUE - 唯一约束
+ * FOREIGN KEY - 外键约束
+ * CHECK - 检查约束
+ *
+ *
+ *
+ * @author odc
+ * @since ODC_release_4.3.4
+ */
+public class PostgresConstraintEditor extends DBTableConstraintEditor {
+
+ @Override
+ protected SqlBuilder sqlBuilder() {
+ return new PostgresSqlBuilder();
+ }
+
+ /**
+ * 生成添加约束的 DDL 语句
+ *
+ *
+ * PostgreSQL 添加约束语法: ALTER TABLE "schema"."table" ADD CONSTRAINT "name" PRIMARY KEY ("col1",
+ * "col2");
+ *
+ *
+ * @param constraint 约束定义
+ * @return 生成的 DDL 语句字符串
+ */
+ @Override
+ public String generateCreateObjectDDL(@NotNull DBTableConstraint constraint) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+ sqlBuilder.append("ALTER TABLE ").append(getFullyQualifiedTableName(constraint))
+ .append(" ADD CONSTRAINT ").identifier(constraint.getName()).space();
+
+ appendConstraintType(constraint, sqlBuilder);
+ appendConstraintColumns(constraint, sqlBuilder);
+ appendConstraintOptions(constraint, sqlBuilder);
+
+ return sqlBuilder.toString().trim() + ";\n";
+ }
+
+ /**
+ * 生成删除约束的 DDL 语句
+ *
+ *
+ * PostgreSQL 删除约束语法:ALTER TABLE "schema"."table" DROP CONSTRAINT "name";
+ *
+ *
+ * @param constraint 约束定义
+ * @return 生成的 DDL 语句字符串
+ */
+ @Override
+ public String generateDropObjectDDL(@NotNull DBTableConstraint constraint) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+ sqlBuilder.append("ALTER TABLE ").append(getFullyQualifiedTableName(constraint))
+ .append(" DROP CONSTRAINT ").identifier(constraint.getName());
+ return sqlBuilder.toString().trim() + ";\n";
+ }
+
+ /**
+ * 生成重命名约束的 DDL 语句
+ *
+ *
+ * PostgreSQL 重命名约束语法:ALTER TABLE "schema"."table" RENAME CONSTRAINT "old" TO "new";
+ *
+ *
+ * @param oldConstraint 重命名前的约束定义
+ * @param newConstraint 重命名后的约束定义
+ * @return 生成的 DDL 语句字符串
+ */
+ @Override
+ public String generateRenameObjectDDL(@NotNull DBTableConstraint oldConstraint,
+ @NotNull DBTableConstraint newConstraint) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+ sqlBuilder.append("ALTER TABLE ").append(getFullyQualifiedTableName(oldConstraint))
+ .append(" RENAME CONSTRAINT ").identifier(oldConstraint.getName())
+ .append(" TO ").identifier(newConstraint.getName()).append(";");
+ return sqlBuilder.toString();
+ }
+
+ /**
+ * 生成更新约束的 DDL 语句
+ *
+ *
+ * PostgreSQL 约束修改策略:
+ *
+ * 结构性变更:需要 DROP + CREATE
+ * 仅名称变更:使用 ALTER TABLE ... RENAME CONSTRAINT
+ *
+ *
+ *
+ * @param oldConstraint 修改前的约束定义
+ * @param newConstraint 修改后的约束定义
+ * @return 生成的 DDL 语句字符串
+ */
+ @Override
+ public String generateUpdateObjectDDL(@NotNull DBTableConstraint oldConstraint,
+ @NotNull DBTableConstraint newConstraint) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+
+ // 检查是否有结构性变更
+ boolean hasStructuralChange = hasStructuralChange(oldConstraint, newConstraint);
+
+ if (hasStructuralChange) {
+ // 结构性变更需要 DROP + CREATE
+ String drop = generateDropObjectDDL(oldConstraint);
+ String create = generateCreateObjectDDL(newConstraint);
+
+ String dropTrimmed = drop.trim();
+ String createTrimmed = create.trim();
+
+ sqlBuilder.append(dropTrimmed);
+ if (!dropTrimmed.endsWith(";")) {
+ sqlBuilder.append("; ");
+ } else {
+ sqlBuilder.append(" ");
+ }
+ sqlBuilder.append(createTrimmed);
+ } else if (!StringUtils.equals(oldConstraint.getName(), newConstraint.getName())) {
+ // 仅名称变更
+ sqlBuilder.append(generateRenameObjectDDL(oldConstraint, newConstraint)).append("\n");
+ }
+
+ return sqlBuilder.toString();
+ }
+
+ /**
+ * 批量更新约束的 DDL 生成
+ *
+ *
+ * 使用 ordinalPosition 进行匹配。
+ *
+ */
+ @Override
+ public String generateUpdateObjectListDDL(Collection oldConstraints,
+ Collection newConstraints) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+
+ if (CollectionUtils.isEmpty(oldConstraints)) {
+ if (CollectionUtils.isNotEmpty(newConstraints)) {
+ newConstraints.forEach(constraint -> sqlBuilder.append(generateCreateObjectDDL(constraint)));
+ }
+ return sqlBuilder.toString();
+ }
+
+ if (CollectionUtils.isEmpty(newConstraints)) {
+ if (CollectionUtils.isNotEmpty(oldConstraints)) {
+ oldConstraints.forEach(constraint -> sqlBuilder.append(generateDropObjectDDL(constraint)));
+ }
+ return sqlBuilder.toString();
+ }
+
+ Map position2OldConstraint = new HashMap<>();
+ Map position2NewConstraint = new HashMap<>();
+
+ oldConstraints.forEach(
+ oldConstraint -> position2OldConstraint.put(oldConstraint.getOrdinalPosition(), oldConstraint));
+ newConstraints.forEach(newConstraint -> {
+ if (Objects.nonNull(newConstraint.getOrdinalPosition())) {
+ position2NewConstraint.put(newConstraint.getOrdinalPosition(), newConstraint);
+ }
+ });
+
+ // 处理新增和修改的约束
+ for (DBTableConstraint newConstraint : newConstraints) {
+ if (Objects.isNull(newConstraint.getOrdinalPosition())) {
+ // ordinalPosition 为空表示新约束
+ sqlBuilder.append(generateCreateObjectDDL(newConstraint));
+ } else if (position2OldConstraint.containsKey(newConstraint.getOrdinalPosition())) {
+ // 已存在的约束,检查是否需要更新
+ String ddl = generateUpdateObjectDDL(
+ position2OldConstraint.get(newConstraint.getOrdinalPosition()),
+ newConstraint);
+ if (StringUtils.isNotEmpty(ddl)) {
+ sqlBuilder.append(ddl);
+ }
+ }
+ }
+
+ // 处理删除的约束
+ for (DBTableConstraint oldConstraint : oldConstraints) {
+ if (!position2NewConstraint.containsKey(oldConstraint.getOrdinalPosition())) {
+ sqlBuilder.append(generateDropObjectDDL(oldConstraint));
+ }
+ }
+
+ return sqlBuilder.toString();
+ }
+
+ /**
+ * 追加约束选项
+ *
+ *
+ * 对于外键约束,追加 ON DELETE 和 ON UPDATE 规则。
+ *
+ */
+ @Override
+ protected void appendConstraintOptions(DBTableConstraint constraint, SqlBuilder sqlBuilder) {
+ // 外键约束的参照动作
+ if (constraint.getType() == DBConstraintType.FOREIGN_KEY) {
+ if (Objects.nonNull(constraint.getOnDeleteRule())
+ && constraint.getOnDeleteRule() != DBForeignKeyModifyRule.NO_ACTION) {
+ sqlBuilder.append(" ON DELETE ").append(constraint.getOnDeleteRule().getValue());
+ }
+ if (Objects.nonNull(constraint.getOnUpdateRule())
+ && constraint.getOnUpdateRule() != DBForeignKeyModifyRule.NO_ACTION) {
+ sqlBuilder.append(" ON UPDATE ").append(constraint.getOnUpdateRule().getValue());
+ }
+ }
+ }
+
+ /**
+ * 检查是否有结构性变更
+ *
+ *
+ * 比较约束类型、列名、外键引用等属性。
+ *
+ */
+ private boolean hasStructuralChange(DBTableConstraint oldConstraint, DBTableConstraint newConstraint) {
+ // 比较约束类型
+ if (!Objects.equals(oldConstraint.getType(), newConstraint.getType())) {
+ return true;
+ }
+
+ // 比较列名
+ if (!Objects.equals(oldConstraint.getColumnNames(), newConstraint.getColumnNames())) {
+ return true;
+ }
+
+ // 对于外键约束,比较引用表和列
+ if (oldConstraint.getType() == DBConstraintType.FOREIGN_KEY) {
+ if (!Objects.equals(oldConstraint.getReferenceSchemaName(), newConstraint.getReferenceSchemaName())) {
+ return true;
+ }
+ if (!Objects.equals(oldConstraint.getReferenceTableName(), newConstraint.getReferenceTableName())) {
+ return true;
+ }
+ if (!Objects.equals(oldConstraint.getReferenceColumnNames(), newConstraint.getReferenceColumnNames())) {
+ return true;
+ }
+ if (!Objects.equals(oldConstraint.getOnDeleteRule(), newConstraint.getOnDeleteRule())) {
+ return true;
+ }
+ if (!Objects.equals(oldConstraint.getOnUpdateRule(), newConstraint.getOnUpdateRule())) {
+ return true;
+ }
+ }
+
+ // 对于 CHECK 约束,比较检查子句
+ if (oldConstraint.getType() == DBConstraintType.CHECK) {
+ if (!Objects.equals(oldConstraint.getCheckClause(), newConstraint.getCheckClause())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresIndexEditor.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresIndexEditor.java
new file mode 100644
index 0000000000..c5d21fd9f7
--- /dev/null
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresIndexEditor.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.editor.postgre;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.validation.constraints.NotNull;
+
+import org.apache.commons.collections4.CollectionUtils;
+
+import com.oceanbase.tools.dbbrowser.editor.DBTableIndexEditor;
+import com.oceanbase.tools.dbbrowser.model.DBIndexType;
+import com.oceanbase.tools.dbbrowser.model.DBTableIndex;
+import com.oceanbase.tools.dbbrowser.util.PostgresSqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.SqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.StringUtils;
+
+/**
+ * PostgreSQL 索引编辑器
+ *
+ *
+ * PostgreSQL 索引语法特点:
+ *
+ *
+ * 创建索引:CREATE [UNIQUE] INDEX "idx_name" ON "schema"."table" USING btree ("col1", "col2");
+ * 删除索引:DROP INDEX "schema"."idx_name";(索引是 Schema 级别的独立对象,不需要 ON table)
+ * 重命名索引:ALTER INDEX "schema"."old_name" RENAME TO "new_name";
+ *
+ *
+ *
+ * PostgreSQL 支持多种索引类型:btree(默认)、hash、gin、gist、spgist、brin
+ *
+ *
+ * @author odc
+ * @since ODC_release_4.3.4
+ */
+public class PostgresIndexEditor extends DBTableIndexEditor {
+
+ @Override
+ protected SqlBuilder sqlBuilder() {
+ return new PostgresSqlBuilder();
+ }
+
+ @Override
+ public boolean editable() {
+ return true;
+ }
+
+ /**
+ * 生成创建索引的 DDL 语句
+ *
+ *
+ * PostgreSQL 创建索引语法: CREATE [UNIQUE] INDEX "idx_name" ON "schema"."table" USING btree ("col1",
+ * "col2");
+ *
+ *
+ * @param index 索引定义
+ * @return 生成的 DDL 语句字符串
+ */
+ @Override
+ public String generateCreateObjectDDL(@NotNull DBTableIndex index) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+ sqlBuilder.append("CREATE ");
+
+ // UNIQUE 关键字
+ if (index.getType() == DBIndexType.UNIQUE || Boolean.TRUE.equals(index.getUnique())) {
+ sqlBuilder.append("UNIQUE ");
+ }
+
+ sqlBuilder.append("INDEX ").identifier(index.getName())
+ .append(" ON ").append(getFullyQualifiedTableName(index));
+
+ // 索引类型(USING 子句)
+ appendIndexType(index, sqlBuilder);
+
+ sqlBuilder.append(" (");
+ appendIndexColumns(index, sqlBuilder);
+ sqlBuilder.append(")");
+
+ // 索引选项
+ appendIndexOptions(index, sqlBuilder);
+
+ sqlBuilder.append(";\n");
+ return sqlBuilder.toString();
+ }
+
+ /**
+ * 生成删除索引的 DDL 语句
+ *
+ *
+ * PostgreSQL 中索引是 Schema 级别的独立对象,DROP INDEX 不需要指定 ON table。 语法:DROP INDEX "schema"."index_name";
+ *
+ *
+ * @param index 索引定义
+ * @return 生成的 DDL 语句字符串
+ */
+ @Override
+ public String generateDropObjectDDL(@NotNull DBTableIndex index) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+ sqlBuilder.append("DROP INDEX ");
+
+ // PostgreSQL 索引是 Schema 级别对象,使用 schema.index_name 格式
+ if (StringUtils.isNotEmpty(index.getSchemaName())) {
+ sqlBuilder.identifier(index.getSchemaName()).append(".");
+ }
+ sqlBuilder.identifier(index.getName()).append(";\n");
+
+ return sqlBuilder.toString();
+ }
+
+ /**
+ * 生成重命名索引的 DDL 语句
+ *
+ *
+ * PostgreSQL 重命名索引语法:ALTER INDEX "schema"."old_name" RENAME TO "new_name";
+ *
+ *
+ * @param oldIndex 重命名前的索引定义
+ * @param newIndex 重命名后的索引定义
+ * @return 生成的 DDL 语句字符串
+ */
+ @Override
+ public String generateRenameObjectDDL(@NotNull DBTableIndex oldIndex, @NotNull DBTableIndex newIndex) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+ sqlBuilder.append("ALTER INDEX ");
+
+ // 使用 schema.index_name 格式
+ if (StringUtils.isNotEmpty(oldIndex.getSchemaName())) {
+ sqlBuilder.identifier(oldIndex.getSchemaName()).append(".");
+ }
+ sqlBuilder.identifier(oldIndex.getName())
+ .append(" RENAME TO ").identifier(newIndex.getName()).append(";");
+
+ return sqlBuilder.toString();
+ }
+
+ /**
+ * 生成更新索引的 DDL 语句
+ *
+ *
+ * PostgreSQL 的索引修改策略:
+ *
+ * 结构性变更(列、类型、唯一性等):需要 DROP + CREATE
+ * 仅名称变更:使用 ALTER INDEX ... RENAME TO
+ *
+ *
+ *
+ * @param oldIndex 修改前的索引定义
+ * @param newIndex 修改后的索引定义
+ * @return 生成的 DDL 语句字符串
+ */
+ @Override
+ public String generateUpdateObjectDDL(@NotNull DBTableIndex oldIndex, @NotNull DBTableIndex newIndex) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+
+ // 检查是否有结构性变更
+ boolean needsRebuild = hasStructuralChange(oldIndex, newIndex);
+
+ if (needsRebuild) {
+ // 结构性变更需要 DROP + CREATE
+ String drop = generateDropObjectDDL(oldIndex);
+ String create = generateCreateObjectDDL(newIndex);
+ sqlBuilder.append(drop).append(create);
+ } else if (!StringUtils.equals(oldIndex.getName(), newIndex.getName())) {
+ // 仅名称变更,使用 RENAME
+ sqlBuilder.append(generateRenameObjectDDL(oldIndex, newIndex)).append("\n");
+ }
+
+ return sqlBuilder.toString();
+ }
+
+ /**
+ * 批量更新索引的 DDL 生成
+ *
+ *
+ * 使用索引名称进行匹配,而不是 ordinalPosition,因为名称更稳定可靠。
+ *
+ */
+ @Override
+ public String generateUpdateObjectListDDL(Collection oldIndexes,
+ Collection newIndexes) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+
+ if (CollectionUtils.isEmpty(oldIndexes)) {
+ if (CollectionUtils.isNotEmpty(newIndexes)) {
+ newIndexes.forEach(index -> sqlBuilder.append(generateCreateObjectDDL(index)));
+ }
+ return sqlBuilder.toString();
+ }
+
+ if (CollectionUtils.isEmpty(newIndexes)) {
+ if (CollectionUtils.isNotEmpty(oldIndexes)) {
+ oldIndexes.forEach(index -> sqlBuilder.append(generateDropObjectDDL(index)));
+ }
+ return sqlBuilder.toString();
+ }
+
+ // 使用名称匹配
+ Map name2OldIndex = new HashMap<>();
+ Map name2NewIndex = new HashMap<>();
+ Map structure2OldIndex = new HashMap<>();
+
+ oldIndexes.forEach(oldIndex -> {
+ if (StringUtils.isNotEmpty(oldIndex.getName())) {
+ name2OldIndex.put(oldIndex.getName(), oldIndex);
+ }
+ String structureKey = buildIndexStructureKey(oldIndex);
+ if (StringUtils.isNotEmpty(structureKey)) {
+ structure2OldIndex.put(structureKey, oldIndex);
+ }
+ });
+
+ newIndexes.forEach(newIndex -> {
+ if (StringUtils.isNotEmpty(newIndex.getName())) {
+ name2NewIndex.put(newIndex.getName(), newIndex);
+ }
+ });
+
+ // 处理新增和修改的索引
+ for (DBTableIndex newIndex : newIndexes) {
+ if (StringUtils.isEmpty(newIndex.getName())) {
+ // 无名称视为新索引
+ sqlBuilder.append(generateCreateObjectDDL(newIndex));
+ } else if (!name2OldIndex.containsKey(newIndex.getName())) {
+ // 检查是否为重命名
+ String structureKey = buildIndexStructureKey(newIndex);
+ if (StringUtils.isNotEmpty(structureKey) && structure2OldIndex.containsKey(structureKey)) {
+ // 结构相同,名称不同 -> 重命名
+ DBTableIndex oldIndex = structure2OldIndex.get(structureKey);
+ sqlBuilder.append(generateRenameObjectDDL(oldIndex, newIndex)).append(";\n");
+ } else {
+ // 新索引
+ sqlBuilder.append(generateCreateObjectDDL(newIndex));
+ }
+ } else {
+ // 已存在的索引,检查是否需要更新
+ String ddl = generateUpdateObjectDDL(name2OldIndex.get(newIndex.getName()), newIndex);
+ if (StringUtils.isNotEmpty(ddl)) {
+ sqlBuilder.append(ddl);
+ }
+ }
+ }
+
+ // 处理删除的索引
+ for (DBTableIndex oldIndex : oldIndexes) {
+ if (StringUtils.isEmpty(oldIndex.getName())
+ || (!name2NewIndex.containsKey(oldIndex.getName())
+ && !isIndexRenamed(oldIndex, newIndexes))) {
+ sqlBuilder.append(generateDropObjectDDL(oldIndex));
+ }
+ }
+
+ return sqlBuilder.toString();
+ }
+
+ /**
+ * 追加索引类型(USING 子句)
+ *
+ *
+ * PostgreSQL 支持多种索引类型:btree(默认)、hash、gin、gist、spgist、brin
+ *
+ */
+ @Override
+ protected void appendIndexType(DBTableIndex index, SqlBuilder sqlBuilder) {
+ // 从 DBIndexType 获取 PostgreSQL 索引方法名
+ String indexMethod = getIndexMethod(index);
+ if (StringUtils.isNotEmpty(indexMethod)) {
+ sqlBuilder.append(" USING ").append(indexMethod);
+ }
+ }
+
+ /**
+ * 追加索引列修饰符
+ */
+ @Override
+ protected void appendIndexColumnModifiers(DBTableIndex index, SqlBuilder sqlBuilder) {
+ // PostgreSQL 基本不需要额外的列修饰符
+ // 如需支持表达式索引或排序(ASC/DESC),可在此扩展
+ }
+
+ /**
+ * 追加索引选项
+ */
+ @Override
+ protected void appendIndexOptions(DBTableIndex index, SqlBuilder sqlBuilder) {
+ // PostgreSQL 索引选项如 WITH (fillfactor = 70), TABLESPACE 等
+ // 目前暂不实现,可根据需要扩展
+ }
+
+ /**
+ * 检查是否有结构性变更
+ *
+ *
+ * 比较影响 DDL 结构的字段:索引类型、列名、唯一性
+ *
+ */
+ private boolean hasStructuralChange(DBTableIndex oldIndex, DBTableIndex newIndex) {
+ // 比较索引类型
+ if (!Objects.equals(oldIndex.getType(), newIndex.getType())) {
+ return true;
+ }
+
+ // 比较列名
+ if (!Objects.equals(oldIndex.getColumnNames(), newIndex.getColumnNames())) {
+ return true;
+ }
+
+ // 比较唯一性
+ if (!Objects.equals(oldIndex.getUnique(), newIndex.getUnique())
+ || oldIndex.isNonUnique() != newIndex.isNonUnique()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 获取 PostgreSQL 索引方法名
+ */
+ private String getIndexMethod(DBTableIndex index) {
+ if (index.getType() == null) {
+ return "btree"; // 默认使用 btree
+ }
+ switch (index.getType()) {
+ case UNIQUE:
+ return "btree"; // UNIQUE INDEX 默认也使用 btree
+ case FULLTEXT:
+ return "gin"; // 全文索引使用 gin
+ case NORMAL:
+ default:
+ return "btree";
+ }
+ }
+
+ /**
+ * 构建索引结构键,用于匹配重命名的索引
+ */
+ private String buildIndexStructureKey(DBTableIndex index) {
+ if (index == null) {
+ return null;
+ }
+ StringBuilder key = new StringBuilder();
+ key.append(index.getType() != null ? index.getType().name() : "NORMAL");
+ key.append("|");
+ if (index.getColumnNames() != null) {
+ key.append(String.join(",", index.getColumnNames()));
+ }
+ key.append("|");
+ key.append(index.getUnique() != null ? index.getUnique() : false);
+ return key.toString();
+ }
+
+ /**
+ * 检查索引是否被重命名
+ */
+ private boolean isIndexRenamed(DBTableIndex oldIndex, Collection newIndexes) {
+ String oldStructureKey = buildIndexStructureKey(oldIndex);
+ if (StringUtils.isEmpty(oldStructureKey)) {
+ return false;
+ }
+ for (DBTableIndex newIndex : newIndexes) {
+ String newStructureKey = buildIndexStructureKey(newIndex);
+ if (oldStructureKey.equals(newStructureKey)
+ && !StringUtils.equals(oldIndex.getName(), newIndex.getName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresObjectOperator.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresObjectOperator.java
new file mode 100644
index 0000000000..156a4dacc4
--- /dev/null
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresObjectOperator.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.editor.postgre;
+
+import org.springframework.jdbc.core.JdbcOperations;
+
+import com.oceanbase.tools.dbbrowser.editor.DBObjectOperator;
+import com.oceanbase.tools.dbbrowser.editor.GeneralSqlStatementBuilder;
+import com.oceanbase.tools.dbbrowser.model.DBObjectType;
+import com.oceanbase.tools.dbbrowser.util.PostgresSqlBuilder;
+
+/**
+ * PostgreSQL 数据库对象操作器
+ *
+ *
+ * 提供删除数据库对象的能力,使用 PostgreSQL 特有的 SQL 语法。
+ *
+ *
+ * @author odc
+ * @since ODC_release_4.3.4
+ */
+public class PostgresObjectOperator implements DBObjectOperator {
+
+ protected final JdbcOperations syncJdbcExecutor;
+
+ public PostgresObjectOperator(JdbcOperations syncJdbcExecutor) {
+ this.syncJdbcExecutor = syncJdbcExecutor;
+ }
+
+ @Override
+ public void drop(DBObjectType objectType, String schemaName, String objectName) {
+ String sql = GeneralSqlStatementBuilder.drop(new PostgresSqlBuilder(), objectType, schemaName, objectName);
+ syncJdbcExecutor.execute(sql);
+ }
+
+}
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresPartitionEditor.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresPartitionEditor.java
new file mode 100644
index 0000000000..fc008f1500
--- /dev/null
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresPartitionEditor.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.editor.postgre;
+
+import java.util.List;
+
+import javax.validation.constraints.NotNull;
+
+import com.oceanbase.tools.dbbrowser.editor.DBTablePartitionEditor;
+import com.oceanbase.tools.dbbrowser.model.DBTablePartition;
+import com.oceanbase.tools.dbbrowser.model.DBTablePartitionDefinition;
+import com.oceanbase.tools.dbbrowser.model.DBTablePartitionOption;
+import com.oceanbase.tools.dbbrowser.util.PostgresSqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.SqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.StringUtils;
+
+/**
+ * PostgreSQL 分区编辑器
+ *
+ *
+ * PostgreSQL 声明式分区语法特点:
+ *
+ *
+ * 创建分区表:CREATE TABLE ... PARTITION BY RANGE/LIST/HASH (column);
+ * 添加分区:ALTER TABLE ... ATTACH PARTITION ...;
+ * 删除分区:ALTER TABLE ... DETACH PARTITION ...;
+ *
+ *
+ *
+ * 注意:PostgreSQL 分区表需要先创建父表,再创建子分区表。本期仅提供基本的分区识别能力, 分区的可视化编辑不在本期范围。
+ *
+ *
+ * @author odc
+ * @since ODC_release_4.3.4
+ */
+public class PostgresPartitionEditor extends DBTablePartitionEditor {
+
+ @Override
+ protected SqlBuilder sqlBuilder() {
+ return new PostgresSqlBuilder();
+ }
+
+ @Override
+ public String generateCreateObjectDDL(@NotNull DBTablePartition partition) {
+ // PostgreSQL 分区需要创建父表后再创建子分区表
+ // 这里仅返回空字符串,因为完整 DDL 在 createDefinitionDDL 中处理
+ return "";
+ }
+
+ @Override
+ public String generateCreateDefinitionDDL(@NotNull DBTablePartition partition) {
+ if (partition.getPartitionOption() == null
+ || partition.getPartitionOption().getType() == null) {
+ return "";
+ }
+
+ SqlBuilder sqlBuilder = sqlBuilder();
+ sqlBuilder.append(" PARTITION BY ");
+
+ switch (partition.getPartitionOption().getType()) {
+ case RANGE:
+ sqlBuilder.append("RANGE");
+ break;
+ case LIST:
+ sqlBuilder.append("LIST");
+ break;
+ case HASH:
+ sqlBuilder.append("HASH");
+ break;
+ default:
+ return "";
+ }
+
+ sqlBuilder.append("(");
+ List columnNames = partition.getPartitionOption().getColumnNames();
+ if (columnNames != null && !columnNames.isEmpty()) {
+ for (int i = 0; i < columnNames.size(); i++) {
+ if (i > 0) {
+ sqlBuilder.append(", ");
+ }
+ sqlBuilder.identifier(columnNames.get(i));
+ }
+ }
+ sqlBuilder.append(")");
+
+ return sqlBuilder.toString();
+ }
+
+ @Override
+ protected void appendDefinitions(DBTablePartition partition, SqlBuilder sqlBuilder) {
+ // PostgreSQL 分区定义在子表中,不在父表 DDL 中
+ }
+
+ @Override
+ protected void appendDefinition(DBTablePartitionOption option, DBTablePartitionDefinition definition,
+ SqlBuilder sqlBuilder) {
+ // PostgreSQL 分区定义在子表中
+ }
+
+ @Override
+ protected String modifyPartitionType(@NotNull DBTablePartition oldPartition,
+ @NotNull DBTablePartition newPartition) {
+ // PostgreSQL 不支持直接修改分区类型,需要重建表
+ return "-- PostgreSQL does not support direct PARTITION BY modification\n"
+ + "-- Please recreate the table with the new partition type\n";
+ }
+
+ @Override
+ public String generateAddPartitionDefinitionDDL(@NotNull DBTablePartitionDefinition definition,
+ @NotNull DBTablePartitionOption option, String fullyQualifiedTableName) {
+ if (definition == null || StringUtils.isBlank(fullyQualifiedTableName)) {
+ return "";
+ }
+
+ SqlBuilder sqlBuilder = sqlBuilder();
+ sqlBuilder.append("-- PostgreSQL requires creating a separate partition table:\n");
+ sqlBuilder.append("-- CREATE TABLE ").append(fullyQualifiedTableName).append("_")
+ .append(definition.getName());
+
+ if (option.getType() != null) {
+ switch (option.getType()) {
+ case RANGE:
+ sqlBuilder.append(" PARTITION OF ").append(fullyQualifiedTableName)
+ .append(" FOR VALUES FROM (")
+ .append(definition.getMaxValues() != null && !definition.getMaxValues().isEmpty()
+ ? definition.getMaxValues().get(0)
+ : "MINVALUE")
+ .append(") TO (")
+ .append(definition.getMaxValues() != null && definition.getMaxValues().size() > 1
+ ? definition.getMaxValues().get(1)
+ : "MAXVALUE")
+ .append(")");
+ break;
+ case LIST:
+ sqlBuilder.append(" PARTITION OF ").append(fullyQualifiedTableName)
+ .append(" FOR VALUES IN (")
+ .append(definition.getMaxValues() != null
+ ? String.join(", ", definition.getMaxValues())
+ : "")
+ .append(")");
+ break;
+ case HASH:
+ sqlBuilder.append(" PARTITION OF ").append(fullyQualifiedTableName)
+ .append(" FOR VALUES WITH (MODULUS ")
+ .append(option.getPartitionsNum() != null ? option.getPartitionsNum() : 1)
+ .append(", REMAINDER ")
+ .append(definition.getOrdinalPosition() != null ? definition.getOrdinalPosition() : 0)
+ .append(")");
+ break;
+ default:
+ break;
+ }
+ }
+ sqlBuilder.append(";\n");
+
+ return sqlBuilder.toString();
+ }
+
+ @Override
+ public String generateAddPartitionDefinitionDDL(String schemaName, @NotNull String tableName,
+ @NotNull DBTablePartitionOption option,
+ List definitions) {
+ if (definitions == null || definitions.isEmpty()) {
+ return "";
+ }
+
+ SqlBuilder sqlBuilder = sqlBuilder();
+ for (DBTablePartitionDefinition definition : definitions) {
+ sqlBuilder.append(generateAddPartitionDefinitionDDL(definition, option,
+ getFullyQualifiedTableName(schemaName, tableName)));
+ }
+ return sqlBuilder.toString();
+ }
+
+ @Override
+ public String generateDropObjectDDL(@NotNull DBTablePartition partition) {
+ // PostgreSQL 删除分区需要 DETACH 分区表
+ return "-- Use ALTER TABLE ... DETACH PARTITION to remove a partition\n";
+ }
+
+ private String getFullyQualifiedTableName(String schemaName, String tableName) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+ if (StringUtils.isNotBlank(schemaName)) {
+ sqlBuilder.identifier(schemaName).append(".");
+ }
+ sqlBuilder.identifier(tableName);
+ return sqlBuilder.toString();
+ }
+}
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresTableEditor.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresTableEditor.java
new file mode 100644
index 0000000000..0d86baab58
--- /dev/null
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresTableEditor.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.editor.postgre;
+
+import java.util.Objects;
+
+import javax.validation.constraints.NotNull;
+
+import com.oceanbase.tools.dbbrowser.editor.DBObjectEditor;
+import com.oceanbase.tools.dbbrowser.editor.DBTableEditor;
+import com.oceanbase.tools.dbbrowser.model.DBTable;
+import com.oceanbase.tools.dbbrowser.model.DBTableColumn;
+import com.oceanbase.tools.dbbrowser.model.DBTableConstraint;
+import com.oceanbase.tools.dbbrowser.model.DBTableIndex;
+import com.oceanbase.tools.dbbrowser.model.DBTablePartition;
+import com.oceanbase.tools.dbbrowser.util.PostgresSqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.SqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.StringUtils;
+
+/**
+ * PostgreSQL 表编辑器
+ *
+ *
+ * PostgreSQL 表 DDL 特点:
+ *
+ *
+ * 表注释:COMMENT ON TABLE "schema"."table" IS 'comment';
+ * 列注释:COMMENT ON COLUMN "schema"."table"."column" IS 'comment';
+ * 重命名表:ALTER TABLE "schema"."old_name" RENAME TO "new_name";
+ *
+ *
+ * @author odc
+ * @since ODC_release_4.3.4
+ */
+public class PostgresTableEditor extends DBTableEditor {
+
+ public PostgresTableEditor(DBObjectEditor indexEditor,
+ DBObjectEditor columnEditor,
+ DBObjectEditor constraintEditor,
+ DBObjectEditor partitionEditor) {
+ super(indexEditor, columnEditor, constraintEditor, partitionEditor);
+ }
+
+ @Override
+ protected void appendColumnComment(DBTable table, SqlBuilder sqlBuilder) {
+ if (Objects.isNull(table.getColumns())) {
+ return;
+ }
+ for (DBTableColumn column : table.getColumns()) {
+ column.setSchemaName(table.getSchemaName());
+ column.setTableName(table.getName());
+ ((PostgresColumnEditor) columnEditor).generateColumnComment(column, sqlBuilder);
+ }
+ }
+
+ @Override
+ protected void appendTableComment(DBTable table, SqlBuilder sqlBuilder) {
+ if (Objects.isNull(table.getTableOptions()) || StringUtils.isBlank(table.getTableOptions().getComment())) {
+ return;
+ }
+ sqlBuilder.append("COMMENT ON TABLE ").append(getFullyQualifiedTableName(table))
+ .append(" IS ").value(table.getTableOptions().getComment()).append(";\n");
+ }
+
+ @Override
+ protected boolean createIndexWhenCreatingTable() {
+ // PostgreSQL 索引需要在表创建后单独创建
+ return false;
+ }
+
+ @Override
+ protected void appendTableOptions(DBTable table, SqlBuilder sqlBuilder) {
+ // PostgreSQL 表选项较少,基本不在此处理
+ }
+
+ @Override
+ public void generateUpdateTableOptionDDL(DBTable oldTable, DBTable newTable, SqlBuilder sqlBuilder) {
+ String oldComment =
+ Objects.nonNull(oldTable.getTableOptions()) ? oldTable.getTableOptions().getComment() : null;
+ String newComment =
+ Objects.nonNull(newTable.getTableOptions()) ? newTable.getTableOptions().getComment() : null;
+ if (!Objects.equals(oldComment, newComment)) {
+ appendTableComment(newTable, sqlBuilder);
+ }
+ }
+
+ @Override
+ public String generateRenameObjectDDL(@NotNull DBTable oldTable, @NotNull DBTable newTable) {
+ SqlBuilder sqlBuilder = sqlBuilder();
+ sqlBuilder.append("ALTER TABLE ").append(getFullyQualifiedTableName(oldTable))
+ .append(" RENAME TO ").identifier(newTable.getName()).append(";");
+ return sqlBuilder.toString();
+ }
+
+ @Override
+ protected SqlBuilder sqlBuilder() {
+ return new PostgresSqlBuilder();
+ }
+
+}
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/model/DBTrigger.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/model/DBTrigger.java
index fbc133481d..9133cf87fa 100644
--- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/model/DBTrigger.java
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/model/DBTrigger.java
@@ -38,8 +38,10 @@ public class DBTrigger implements DBObject {
private List triggerEvents;
// 等效于之前的 tableOwner
private String schemaMode;
- // 等效于之前的 tableName
+ // 等效于之前的 tableName,用于存储关联的表名
private String schemaName;
+ // 表名,用于触发器关联的表(独立字段,避免与 schemaName 混淆)
+ private String tableName;
private Boolean rowLevel = true;
private boolean enable;
private String sqlExpression;
@@ -59,8 +61,16 @@ public DBObjectType type() {
return DBObjectType.TRIGGER;
}
+ /**
+ * 获取表名
+ *
+ * 返回 tableName 字段(如果设置了),否则返回 schemaName 字段以保持向后兼容
+ *
+ *
+ * @return 表名
+ */
public String getTableName() {
- return this.schemaName;
+ return this.tableName != null ? this.tableName : this.schemaName;
}
public String getTableOwner() {
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/schema/postgre/PostgresSchemaAccessor.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/schema/postgre/PostgresSchemaAccessor.java
index e70b5ed03e..0fb139efde 100644
--- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/schema/postgre/PostgresSchemaAccessor.java
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/schema/postgre/PostgresSchemaAccessor.java
@@ -15,9 +15,13 @@
*/
package com.oceanbase.tools.dbbrowser.schema.postgre;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
@@ -25,13 +29,17 @@
import org.springframework.jdbc.core.JdbcOperations;
import com.oceanbase.tools.dbbrowser.model.DBColumnGroupElement;
+import com.oceanbase.tools.dbbrowser.model.DBConstraintType;
import com.oceanbase.tools.dbbrowser.model.DBDatabase;
+import com.oceanbase.tools.dbbrowser.model.DBForeignKeyModifyRule;
import com.oceanbase.tools.dbbrowser.model.DBFunction;
+import com.oceanbase.tools.dbbrowser.model.DBIndexType;
import com.oceanbase.tools.dbbrowser.model.DBMViewRefreshParameter;
import com.oceanbase.tools.dbbrowser.model.DBMViewRefreshRecord;
import com.oceanbase.tools.dbbrowser.model.DBMViewRefreshRecordParam;
import com.oceanbase.tools.dbbrowser.model.DBMaterializedView;
import com.oceanbase.tools.dbbrowser.model.DBObjectIdentity;
+import com.oceanbase.tools.dbbrowser.model.DBObjectType;
import com.oceanbase.tools.dbbrowser.model.DBPLObjectIdentity;
import com.oceanbase.tools.dbbrowser.model.DBPackage;
import com.oceanbase.tools.dbbrowser.model.DBProcedure;
@@ -44,11 +52,15 @@
import com.oceanbase.tools.dbbrowser.model.DBTableConstraint;
import com.oceanbase.tools.dbbrowser.model.DBTableIndex;
import com.oceanbase.tools.dbbrowser.model.DBTablePartition;
+import com.oceanbase.tools.dbbrowser.model.DBTablePartitionDefinition;
+import com.oceanbase.tools.dbbrowser.model.DBTablePartitionOption;
+import com.oceanbase.tools.dbbrowser.model.DBTablePartitionType;
import com.oceanbase.tools.dbbrowser.model.DBTableSubpartitionDefinition;
import com.oceanbase.tools.dbbrowser.model.DBTrigger;
import com.oceanbase.tools.dbbrowser.model.DBType;
import com.oceanbase.tools.dbbrowser.model.DBVariable;
import com.oceanbase.tools.dbbrowser.model.DBView;
+import com.oceanbase.tools.dbbrowser.model.DBViewCheckOption;
import com.oceanbase.tools.dbbrowser.schema.DBSchemaAccessor;
import com.oceanbase.tools.dbbrowser.util.StringUtils;
@@ -136,9 +148,73 @@ public List showTablesLike(String schemaName, String tableNameLike) {
throw new UnsupportedOperationException("Not supported yet");
}
+ /**
+ * 列出指定 schema 下的表
+ *
+ * PostgreSQL 使用 pg_class 系统表查询表信息,relkind='r' 表示普通表,relkind='p' 表示分区表
+ *
+ *
+ * @param schemaName schema 名称
+ * @param tableNameLike 表名匹配模式(可选)
+ * @return 表对象列表
+ */
@Override
public List listTables(String schemaName, String tableNameLike) {
- throw new UnsupportedOperationException("Not supported yet");
+ if (StringUtils.isBlank(schemaName)) {
+ return listAllTables(tableNameLike);
+ }
+
+ StringBuilder sql = new StringBuilder();
+ sql.append("SELECT c.relname AS table_name, ");
+ sql.append(" obj_description(c.oid) AS table_comment ");
+ sql.append("FROM pg_catalog.pg_class c ");
+ sql.append("INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid ");
+ sql.append("WHERE n.nspname = ? ");
+ sql.append(" AND c.relkind IN ('r', 'p') "); // r=普通表, p=分区表
+ sql.append(" AND c.relispartition = false "); // 排除分区子表,只显示父表
+
+ List params = new ArrayList<>();
+ params.add(schemaName);
+
+ if (StringUtils.isNotBlank(tableNameLike)) {
+ sql.append(" AND c.relname LIKE ? ESCAPE '\\' ");
+ params.add(StringUtils.escapeLike(tableNameLike));
+ }
+
+ sql.append("ORDER BY c.relname");
+
+ try {
+ return jdbcOperations.query(sql.toString(), params.toArray(), (rs, rowNum) -> {
+ DBObjectIdentity identity = new DBObjectIdentity();
+ identity.setSchemaName(schemaName);
+ identity.setName(rs.getString("table_name"));
+ identity.setType(DBObjectType.TABLE);
+ return identity;
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return Collections.emptyList();
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * 列出所有用户 schema 下的表
+ */
+ private List listAllTables(String tableNameLike) {
+ List results = new ArrayList<>();
+ List schemas = showDatabases();
+
+ for (String schema : schemas) {
+ try {
+ List tables = listTables(schema, tableNameLike);
+ results.addAll(tables);
+ } catch (Exception e) {
+ log.warn("Failed to list tables for schema: " + schema, e);
+ }
+ }
+ return results;
}
@Override
@@ -161,29 +237,168 @@ public boolean syncExternalTableFiles(String schemaName, String tableName) {
throw new UnsupportedOperationException("Not supported yet");
}
+ /**
+ * 列出指定 schema 下的视图
+ *
+ * PostgreSQL 使用 information_schema.views 或 pg_class 查询视图信息
+ *
+ *
+ * @param schemaName schema 名称
+ * @return 视图对象列表
+ */
@Override
public List listViews(String schemaName) {
- throw new UnsupportedOperationException("Not supported yet");
+ if (StringUtils.isBlank(schemaName)) {
+ return listAllUserViews(null);
+ }
+
+ String sql = "SELECT c.relname AS view_name " +
+ "FROM pg_catalog.pg_class c " +
+ "INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid " +
+ "WHERE n.nspname = ? " +
+ " AND c.relkind = 'v' " + // v = view
+ "ORDER BY c.relname";
+
+ try {
+ return jdbcOperations.query(sql, new Object[] {schemaName}, (rs, rowNum) -> {
+ DBObjectIdentity identity = new DBObjectIdentity();
+ identity.setSchemaName(schemaName);
+ identity.setName(rs.getString("view_name"));
+ identity.setType(DBObjectType.VIEW);
+ return identity;
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return Collections.emptyList();
+ }
+ throw e;
+ }
}
+ /**
+ * 列出所有视图(用户视图 + 系统视图)
+ *
+ * @param viewNameLike 视图名匹配模式(可选)
+ * @return 视图对象列表
+ */
@Override
public List listAllViews(String viewNameLike) {
- throw new UnsupportedOperationException("Not supported yet");
- }
-
+ List results = new ArrayList<>();
+ results.addAll(listAllUserViews(viewNameLike));
+ results.addAll(listAllSystemViews(viewNameLike));
+ return results;
+ }
+
+ /**
+ * 列出所有用户视图
+ *
+ * 过滤系统 schema:pg_catalog, information_schema
+ *
+ *
+ * @param viewNameLike 视图名匹配模式(可选)
+ * @return 用户视图对象列表
+ */
@Override
public List listAllUserViews(String viewNameLike) {
- throw new UnsupportedOperationException("Not supported yet");
+ StringBuilder sql = new StringBuilder();
+ sql.append("SELECT c.relname AS view_name, n.nspname AS schema_name ");
+ sql.append("FROM pg_catalog.pg_class c ");
+ sql.append("INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid ");
+ sql.append("WHERE c.relkind = 'v' ");
+ sql.append(" AND n.nspname NOT LIKE 'pg_%' ");
+ sql.append(" AND n.nspname <> 'information_schema' ");
+
+ List params = new ArrayList<>();
+ if (StringUtils.isNotBlank(viewNameLike)) {
+ sql.append(" AND c.relname LIKE ? ESCAPE '\\'");
+ params.add(StringUtils.escapeLike(viewNameLike));
+ }
+ sql.append("ORDER BY n.nspname, c.relname");
+
+ try {
+ return jdbcOperations.query(sql.toString(), params.toArray(), (rs, rowNum) -> {
+ DBObjectIdentity identity = new DBObjectIdentity();
+ identity.setSchemaName(rs.getString("schema_name"));
+ identity.setName(rs.getString("view_name"));
+ identity.setType(DBObjectType.VIEW);
+ return identity;
+ });
+ } catch (BadSqlGrammarException e) {
+ log.warn("Failed to list user views", e);
+ return Collections.emptyList();
+ }
}
+ /**
+ * 列出所有系统视图
+ *
+ * 系统视图位于 pg_catalog 和 information_schema 中
+ *
+ *
+ * @param viewNameLike 视图名匹配模式(可选)
+ * @return 系统视图对象列表
+ */
@Override
public List listAllSystemViews(String viewNameLike) {
- throw new UnsupportedOperationException("Not supported yet");
+ StringBuilder sql = new StringBuilder();
+ sql.append("SELECT c.relname AS view_name, n.nspname AS schema_name ");
+ sql.append("FROM pg_catalog.pg_class c ");
+ sql.append("INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid ");
+ sql.append("WHERE c.relkind = 'v' ");
+ sql.append(" AND (n.nspname LIKE 'pg_%' OR n.nspname = 'information_schema') ");
+
+ List params = new ArrayList<>();
+ if (StringUtils.isNotBlank(viewNameLike)) {
+ sql.append(" AND c.relname LIKE ? ESCAPE '\\'");
+ params.add(StringUtils.escapeLike(viewNameLike));
+ }
+ sql.append("ORDER BY n.nspname, c.relname");
+
+ try {
+ return jdbcOperations.query(sql.toString(), params.toArray(), (rs, rowNum) -> {
+ DBObjectIdentity identity = new DBObjectIdentity();
+ identity.setSchemaName(rs.getString("schema_name"));
+ identity.setName(rs.getString("view_name"));
+ identity.setType(DBObjectType.VIEW);
+ return identity;
+ });
+ } catch (BadSqlGrammarException e) {
+ log.warn("Failed to list system views", e);
+ return Collections.emptyList();
+ }
}
+ /**
+ * 显示指定 schema 下的系统视图名称列表
+ *
+ * @param schemaName schema 名称
+ * @return 系统视图名称列表
+ */
@Override
public List showSystemViews(String schemaName) {
- throw new UnsupportedOperationException("Not supported yet");
+ StringBuilder sql = new StringBuilder();
+ sql.append("SELECT c.relname AS view_name ");
+ sql.append("FROM pg_catalog.pg_class c ");
+ sql.append("INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid ");
+ sql.append("WHERE c.relkind = 'v' ");
+
+ List params = new ArrayList<>();
+ if (StringUtils.isNotBlank(schemaName)) {
+ sql.append(" AND n.nspname = ?");
+ params.add(schemaName);
+ } else {
+ sql.append(" AND (n.nspname LIKE 'pg_%' OR n.nspname = 'information_schema')");
+ }
+ sql.append("ORDER BY c.relname");
+
+ try {
+ return jdbcOperations.query(sql.toString(), params.toArray(), (rs, rowNum) -> rs.getString("view_name"));
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return Collections.emptyList();
+ }
+ throw e;
+ }
}
@Override
@@ -221,19 +436,87 @@ public List listMViewIndexes(String schemaName, String mViewName)
throw new UnsupportedOperationException("not support yet");
}
+ /**
+ * 列出所有变量
+ *
+ * PostgreSQL 使用 pg_settings 视图查询所有变量
+ *
+ *
+ * @return 变量列表
+ */
@Override
public List showVariables() {
- throw new UnsupportedOperationException("Not supported yet");
+ String sql = "SELECT name, setting AS value, short_desc AS description " +
+ "FROM pg_catalog.pg_settings " +
+ "ORDER BY name";
+
+ try {
+ return jdbcOperations.query(sql, (rs, rowNum) -> {
+ DBVariable variable = new DBVariable();
+ variable.setName(rs.getString("name"));
+ variable.setValue(rs.getString("value"));
+ return variable;
+ });
+ } catch (Exception e) {
+ log.warn("Failed to show variables", e);
+ return Collections.emptyList();
+ }
}
+ /**
+ * 列出会话级变量
+ *
+ * context 为 'user' 或 'superuser' 的变量是会话级变量
+ *
+ *
+ * @return 会话变量列表
+ */
@Override
public List showSessionVariables() {
- throw new UnsupportedOperationException("Not supported yet");
+ String sql = "SELECT name, setting AS value, short_desc AS description " +
+ "FROM pg_catalog.pg_settings " +
+ "WHERE context IN ('user', 'superuser') " +
+ "ORDER BY name";
+
+ try {
+ return jdbcOperations.query(sql, (rs, rowNum) -> {
+ DBVariable variable = new DBVariable();
+ variable.setName(rs.getString("name"));
+ variable.setValue(rs.getString("value"));
+ return variable;
+ });
+ } catch (Exception e) {
+ log.warn("Failed to show session variables", e);
+ return Collections.emptyList();
+ }
}
+ /**
+ * 列出全局级变量
+ *
+ * context 为 'postmaster' 的变量是全局级变量(需要重启才能生效)
+ *
+ *
+ * @return 全局变量列表
+ */
@Override
public List showGlobalVariables() {
- throw new UnsupportedOperationException("Not supported yet");
+ String sql = "SELECT name, setting AS value, short_desc AS description " +
+ "FROM pg_catalog.pg_settings " +
+ "WHERE context = 'postmaster' " +
+ "ORDER BY name";
+
+ try {
+ return jdbcOperations.query(sql, (rs, rowNum) -> {
+ DBVariable variable = new DBVariable();
+ variable.setName(rs.getString("name"));
+ variable.setValue(rs.getString("value"));
+ return variable;
+ });
+ } catch (Exception e) {
+ log.warn("Failed to show global variables", e);
+ return Collections.emptyList();
+ }
}
@Override
@@ -248,76 +531,615 @@ public List showCollation() {
return jdbcOperations.queryForList(sql, String.class);
}
+ /**
+ * 列出指定 schema 下的函数
+ *
+ * PostgreSQL 使用 pg_proc 查询函数,prokind='f' 表示函数
+ *
+ *
+ * @param schemaName schema 名称
+ * @return 函数对象列表
+ */
@Override
public List listFunctions(String schemaName) {
- throw new UnsupportedOperationException("Not supported yet");
+ if (StringUtils.isBlank(schemaName)) {
+ return Collections.emptyList();
+ }
+
+ String sql = "SELECT p.proname AS function_name, " +
+ " pg_get_function_arguments(p.oid) AS arguments, " +
+ " pg_get_function_result(p.oid) AS return_type " +
+ "FROM pg_catalog.pg_proc p " +
+ "INNER JOIN pg_catalog.pg_namespace n ON p.pronamespace = n.oid " +
+ "WHERE n.nspname = ? " +
+ " AND p.prokind = 'f' " + // f = function
+ "ORDER BY p.proname";
+
+ try {
+ return jdbcOperations.query(sql, new Object[] {schemaName}, (rs, rowNum) -> {
+ DBPLObjectIdentity identity = new DBPLObjectIdentity();
+ identity.setSchemaName(schemaName);
+ identity.setName(rs.getString("function_name"));
+ identity.setType(DBObjectType.FUNCTION);
+ return identity;
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return Collections.emptyList();
+ }
+ throw e;
+ }
}
+ /**
+ * 列出指定 schema 下的存储过程
+ *
+ * PostgreSQL 11+ 支持存储过程,prokind='p' 表示存储过程
+ *
+ *
+ * @param schemaName schema 名称
+ * @return 存储过程对象列表
+ */
@Override
public List listProcedures(String schemaName) {
- throw new UnsupportedOperationException("Not supported yet");
+ if (StringUtils.isBlank(schemaName)) {
+ return Collections.emptyList();
+ }
+
+ String sql = "SELECT p.proname AS procedure_name " +
+ "FROM pg_catalog.pg_proc p " +
+ "INNER JOIN pg_catalog.pg_namespace n ON p.pronamespace = n.oid " +
+ "WHERE n.nspname = ? " +
+ " AND p.prokind = 'p' " + // p = procedure (PG 11+)
+ "ORDER BY p.proname";
+
+ try {
+ return jdbcOperations.query(sql, new Object[] {schemaName}, (rs, rowNum) -> {
+ DBPLObjectIdentity identity = new DBPLObjectIdentity();
+ identity.setSchemaName(schemaName);
+ identity.setName(rs.getString("procedure_name"));
+ identity.setType(DBObjectType.PROCEDURE);
+ return identity;
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return Collections.emptyList();
+ }
+ // 如果 prokind 列不存在(PG 11 之前版本),返回空列表
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "prokind")) {
+ log.debug("PostgreSQL version does not support stored procedures (requires PG 11+)");
+ return Collections.emptyList();
+ }
+ throw e;
+ }
}
+ /**
+ * 列出指定 schema 下的包
+ *
+ * PostgreSQL 不支持 Oracle 风格的包概念
+ *
+ */
@Override
public List listPackages(String schemaName) {
- throw new UnsupportedOperationException("Not supported yet");
+ // PostgreSQL 不支持包,返回空列表
+ return Collections.emptyList();
}
+ /**
+ * 列出指定 schema 下的包体
+ *
+ * PostgreSQL 不支持 Oracle 风格的包概念
+ *
+ */
@Override
public List listPackageBodies(String schemaName) {
- throw new UnsupportedOperationException("Not supported yet");
+ // PostgreSQL 不支持包体,返回空列表
+ return Collections.emptyList();
}
+ /**
+ * 列出指定 schema 下的触发器
+ *
+ * @param schemaName schema 名称
+ * @return 触发器对象列表
+ */
@Override
public List listTriggers(String schemaName) {
- throw new UnsupportedOperationException("Not supported yet");
+ if (StringUtils.isBlank(schemaName)) {
+ return Collections.emptyList();
+ }
+
+ String sql = "SELECT t.tgname AS trigger_name, " +
+ " c.relname AS table_name " +
+ "FROM pg_catalog.pg_trigger t " +
+ "INNER JOIN pg_catalog.pg_class c ON t.tgrelid = c.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid " +
+ "WHERE n.nspname = ? " +
+ " AND NOT t.tgisinternal " + // 排除内部触发器
+ "ORDER BY t.tgname";
+
+ try {
+ return jdbcOperations.query(sql, new Object[] {schemaName}, (rs, rowNum) -> {
+ DBPLObjectIdentity identity = new DBPLObjectIdentity();
+ identity.setSchemaName(schemaName);
+ identity.setName(rs.getString("trigger_name"));
+ identity.setType(DBObjectType.TRIGGER);
+ return identity;
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return Collections.emptyList();
+ }
+ throw e;
+ }
}
+ /**
+ * 列出指定 schema 下的类型
+ *
+ * @param schemaName schema 名称
+ * @return 类型对象列表
+ */
@Override
public List listTypes(String schemaName) {
- throw new UnsupportedOperationException("Not supported yet");
+ if (StringUtils.isBlank(schemaName)) {
+ return Collections.emptyList();
+ }
+
+ String sql = "SELECT t.typname AS type_name " +
+ "FROM pg_catalog.pg_type t " +
+ "INNER JOIN pg_catalog.pg_namespace n ON t.typnamespace = n.oid " +
+ "WHERE n.nspname = ? " +
+ " AND t.typtype = 'c' " + // c = composite type
+ "ORDER BY t.typname";
+
+ try {
+ return jdbcOperations.query(sql, new Object[] {schemaName}, (rs, rowNum) -> {
+ DBPLObjectIdentity identity = new DBPLObjectIdentity();
+ identity.setSchemaName(schemaName);
+ identity.setName(rs.getString("type_name"));
+ identity.setType(DBObjectType.TYPE);
+ return identity;
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return Collections.emptyList();
+ }
+ throw e;
+ }
}
+ /**
+ * 列出指定 schema 下的序列
+ *
+ * PostgreSQL 使用 pg_class 查询序列,relkind='S' 表示序列
+ *
+ *
+ * @param schemaName schema 名称
+ * @return 序列对象列表
+ */
@Override
public List listSequences(String schemaName) {
- throw new UnsupportedOperationException("Not supported yet");
+ if (StringUtils.isBlank(schemaName)) {
+ return Collections.emptyList();
+ }
+
+ String sql = "SELECT c.relname AS sequence_name " +
+ "FROM pg_catalog.pg_class c " +
+ "INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid " +
+ "WHERE n.nspname = ? " +
+ " AND c.relkind = 'S' " + // S = sequence
+ "ORDER BY c.relname";
+
+ try {
+ return jdbcOperations.query(sql, new Object[] {schemaName}, (rs, rowNum) -> {
+ DBObjectIdentity identity = new DBObjectIdentity();
+ identity.setSchemaName(schemaName);
+ identity.setName(rs.getString("sequence_name"));
+ identity.setType(DBObjectType.SEQUENCE);
+ return identity;
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return Collections.emptyList();
+ }
+ throw e;
+ }
}
+ /**
+ * 列出指定 schema 下的同义词
+ *
+ * PostgreSQL 不原生支持同义词,返回空列表
+ *
+ */
@Override
- public List listSynonyms(String schemaName,
- DBSynonymType synonymType) {
- throw new UnsupportedOperationException("Not supported yet");
+ public List listSynonyms(String schemaName, DBSynonymType synonymType) {
+ // PostgreSQL 不原生支持同义词,返回空列表
+ return Collections.emptyList();
}
+ /**
+ * 批量列出指定表的列信息
+ */
@Override
public Map> listTableColumns(
String schemaName, List tableNames) {
- throw new UnsupportedOperationException("Not supported yet");
+ if (tableNames == null || tableNames.isEmpty()) {
+ return Collections.emptyMap();
+ }
+
+ // 构建 IN 子句
+ StringBuilder inClause = new StringBuilder();
+ List params = new ArrayList<>();
+ params.add(schemaName);
+ for (int i = 0; i < tableNames.size(); i++) {
+ if (i > 0) {
+ inClause.append(",");
+ }
+ inClause.append("?");
+ params.add(tableNames.get(i));
+ }
+
+ String sql = "SELECT " +
+ " c.relname AS table_name, " +
+ " a.attnum AS ordinal_position, " +
+ " a.attname AS column_name, " +
+ " pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type, " +
+ " t.typname AS type_name, " +
+ " a.attnotnull AS not_null, " +
+ " pg_get_expr(d.adbin, d.adrelid) AS default_value, " +
+ " col_description(a.attrelid, a.attnum) AS column_comment, " +
+ " CASE WHEN a.atttypmod > 0 AND t.typname IN ('varchar', 'char', 'bpchar') " +
+ " THEN a.atttypmod - 4 " +
+ " ELSE NULL END AS char_length, " +
+ " CASE WHEN a.atttypmod > 0 AND t.typname = 'numeric' " +
+ " THEN ((a.atttypmod - 4) >> 16) & 65535 " +
+ " ELSE NULL END AS numeric_precision, " +
+ " CASE WHEN a.atttypmod > 0 AND t.typname = 'numeric' " +
+ " THEN (a.atttypmod - 4) & 65535 " +
+ " ELSE NULL END AS numeric_scale " +
+ "FROM pg_catalog.pg_attribute a " +
+ "INNER JOIN pg_catalog.pg_class c ON a.attrelid = c.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid " +
+ "INNER JOIN pg_catalog.pg_type t ON a.atttypid = t.oid " +
+ "LEFT JOIN pg_catalog.pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum " +
+ "WHERE n.nspname = ? " +
+ " AND c.relname IN (" + inClause.toString() + ") " +
+ " AND a.attnum > 0 " +
+ " AND NOT a.attisdropped " +
+ "ORDER BY c.relname, a.attnum";
+
+ try {
+ List columns = jdbcOperations.query(sql, params.toArray(), (rs, rowNum) -> {
+ DBTableColumn column = new DBTableColumn();
+ column.setSchemaName(schemaName);
+ String tableName = rs.getString("table_name");
+ column.setTableName(tableName);
+ column.setOrdinalPosition(rs.getInt("ordinal_position"));
+ column.setName(rs.getString("column_name"));
+ column.setTypeName(rs.getString("type_name"));
+ column.setFullTypeName(rs.getString("data_type"));
+ column.setNullable(!rs.getBoolean("not_null"));
+
+ String defaultValue = rs.getString("default_value");
+ if (StringUtils.isNotBlank(defaultValue)) {
+ column.fillDefaultValue(defaultValue);
+ }
+
+ String comment = rs.getString("column_comment");
+ if (StringUtils.isNotBlank(comment)) {
+ column.setComment(comment);
+ }
+
+ // 处理字符长度
+ Object charLengthObj = rs.getObject("char_length");
+ if (charLengthObj != null) {
+ column.setMaxLength(rs.getLong("char_length"));
+ }
+
+ // 处理数值精度
+ Object precisionObj = rs.getObject("numeric_precision");
+ if (precisionObj != null) {
+ column.setPrecision(rs.getLong("numeric_precision"));
+ }
+
+ Object scaleObj = rs.getObject("numeric_scale");
+ if (scaleObj != null) {
+ column.setScale(rs.getInt("numeric_scale"));
+ }
+
+ return column;
+ });
+ return columns.stream()
+ .filter(col -> col.getTableName() != null)
+ .collect(Collectors.groupingBy(DBTableColumn::getTableName));
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return Collections.emptyMap();
+ }
+ throw e;
+ }
}
- @Override
- public List listTableColumns(String schemeName, String tableName) {
- throw new UnsupportedOperationException("Not supported yet");
+ /**
+ * 列出指定表的完整列信息
+ *
+ * 查询 pg_attribute 获取列信息,pg_attrdef 获取默认值,col_description 获取注释
+ *
+ *
+ * @param schemaName schema 名称
+ * @param tableName 表名
+ * @return 列信息列表
+ */
+ @Override
+ public List listTableColumns(String schemaName, String tableName) {
+ String sql = "SELECT " +
+ " a.attnum AS ordinal_position, " +
+ " a.attname AS column_name, " +
+ " pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type, " +
+ " t.typname AS type_name, " +
+ " a.attnotnull AS not_null, " +
+ " pg_get_expr(d.adbin, d.adrelid) AS default_value, " +
+ " col_description(a.attrelid, a.attnum) AS column_comment, " +
+ " CASE WHEN a.atttypmod > 0 AND t.typname IN ('varchar', 'char', 'bpchar') " +
+ " THEN a.atttypmod - 4 " +
+ " ELSE NULL END AS char_length, " +
+ " CASE WHEN a.atttypmod > 0 AND t.typname = 'numeric' " +
+ " THEN ((a.atttypmod - 4) >> 16) & 65535 " +
+ " ELSE NULL END AS numeric_precision, " +
+ " CASE WHEN a.atttypmod > 0 AND t.typname = 'numeric' " +
+ " THEN (a.atttypmod - 4) & 65535 " +
+ " ELSE NULL END AS numeric_scale " +
+ "FROM pg_catalog.pg_attribute a " +
+ "INNER JOIN pg_catalog.pg_class c ON a.attrelid = c.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid " +
+ "INNER JOIN pg_catalog.pg_type t ON a.atttypid = t.oid " +
+ "LEFT JOIN pg_catalog.pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum " +
+ "WHERE n.nspname = ? " +
+ " AND c.relname = ? " +
+ " AND a.attnum > 0 " +
+ " AND NOT a.attisdropped " +
+ "ORDER BY a.attnum";
+
+ try {
+ return jdbcOperations.query(sql, new Object[] {schemaName, tableName}, (rs, rowNum) -> {
+ DBTableColumn column = new DBTableColumn();
+ column.setSchemaName(schemaName);
+ column.setTableName(tableName);
+ column.setOrdinalPosition(rs.getInt("ordinal_position"));
+ column.setName(rs.getString("column_name"));
+ column.setTypeName(rs.getString("type_name"));
+ column.setFullTypeName(rs.getString("data_type"));
+ column.setNullable(!rs.getBoolean("not_null"));
+
+ String defaultValue = rs.getString("default_value");
+ if (StringUtils.isNotBlank(defaultValue)) {
+ column.fillDefaultValue(defaultValue);
+ }
+
+ String comment = rs.getString("column_comment");
+ if (StringUtils.isNotBlank(comment)) {
+ column.setComment(comment);
+ }
+
+ // 处理字符长度
+ Object charLengthObj = rs.getObject("char_length");
+ if (charLengthObj != null) {
+ column.setMaxLength(rs.getLong("char_length"));
+ }
+
+ // 处理数值精度
+ Object precisionObj = rs.getObject("numeric_precision");
+ if (precisionObj != null) {
+ column.setPrecision(rs.getLong("numeric_precision"));
+ }
+
+ Object scaleObj = rs.getObject("numeric_scale");
+ if (scaleObj != null) {
+ column.setScale(rs.getInt("numeric_scale"));
+ }
+
+ return column;
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema") ||
+ StringUtils.containsIgnoreCase(e.getMessage(), "relation")) {
+ return Collections.emptyList();
+ }
+ throw e;
+ }
}
+ /**
+ * 列出指定 schema 下所有表的基本列信息
+ *
+ * 使用 pg_attribute 批量查询所有表的列信息,用于对象树展开场景
+ *
+ *
+ * @param schemaName schema 名称
+ * @return 表名到列列表的映射
+ */
@Override
public Map> listBasicTableColumns(String schemaName) {
- throw new UnsupportedOperationException("Not supported yet");
+ String sql = "SELECT " +
+ " c.relname AS table_name, " +
+ " a.attnum AS ordinal_position, " +
+ " a.attname AS column_name, " +
+ " pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type, " +
+ " t.typname AS type_name, " +
+ " col_description(a.attrelid, a.attnum) AS column_comment " +
+ "FROM pg_catalog.pg_attribute a " +
+ "INNER JOIN pg_catalog.pg_class c ON a.attrelid = c.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid " +
+ "INNER JOIN pg_catalog.pg_type t ON a.atttypid = t.oid " +
+ "WHERE n.nspname = ? " +
+ " AND c.relkind IN ('r', 'p') " +
+ " AND a.attnum > 0 " +
+ " AND NOT a.attisdropped " +
+ "ORDER BY c.relname, a.attnum";
+
+ try {
+ List columns = jdbcOperations.query(sql, new Object[] {schemaName}, (rs, rowNum) -> {
+ DBTableColumn column = new DBTableColumn();
+ column.setSchemaName(schemaName);
+ column.setTableName(rs.getString("table_name"));
+ column.setOrdinalPosition(rs.getInt("ordinal_position"));
+ column.setName(rs.getString("column_name"));
+ column.setTypeName(rs.getString("type_name"));
+ column.setFullTypeName(rs.getString("data_type"));
+ String comment = rs.getString("column_comment");
+ if (StringUtils.isNotBlank(comment)) {
+ column.setComment(comment);
+ }
+ return column;
+ });
+ return columns.stream()
+ .filter(col -> col.getTableName() != null)
+ .collect(Collectors.groupingBy(DBTableColumn::getTableName));
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return Collections.emptyMap();
+ }
+ throw e;
+ }
}
+ /**
+ * 列出指定表的基本列信息
+ */
@Override
public List listBasicTableColumns(String schemaName, String tableName) {
- throw new UnsupportedOperationException("Not supported yet");
+ String sql = "SELECT " +
+ " a.attnum AS ordinal_position, " +
+ " a.attname AS column_name, " +
+ " pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type, " +
+ " t.typname AS type_name, " +
+ " col_description(a.attrelid, a.attnum) AS column_comment " +
+ "FROM pg_catalog.pg_attribute a " +
+ "INNER JOIN pg_catalog.pg_class c ON a.attrelid = c.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid " +
+ "INNER JOIN pg_catalog.pg_type t ON a.atttypid = t.oid " +
+ "WHERE n.nspname = ? " +
+ " AND c.relname = ? " +
+ " AND a.attnum > 0 " +
+ " AND NOT a.attisdropped " +
+ "ORDER BY a.attnum";
+
+ try {
+ return jdbcOperations.query(sql, new Object[] {schemaName, tableName}, (rs, rowNum) -> {
+ DBTableColumn column = new DBTableColumn();
+ column.setSchemaName(schemaName);
+ column.setTableName(tableName);
+ column.setOrdinalPosition(rs.getInt("ordinal_position"));
+ column.setName(rs.getString("column_name"));
+ column.setTypeName(rs.getString("type_name"));
+ column.setFullTypeName(rs.getString("data_type"));
+ String comment = rs.getString("column_comment");
+ if (StringUtils.isNotBlank(comment)) {
+ column.setComment(comment);
+ }
+ return column;
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema") ||
+ StringUtils.containsIgnoreCase(e.getMessage(), "relation")) {
+ return Collections.emptyList();
+ }
+ throw e;
+ }
}
+ /**
+ * 获取指定 schema 下所有视图的列信息
+ *
+ * @param schemaName schema 名称
+ * @return 视图名到列列表的映射
+ */
@Override
public Map> listBasicViewColumns(String schemaName) {
- throw new UnsupportedOperationException("Not supported yet");
+ String sql = "SELECT " +
+ " c.relname AS view_name, " +
+ " a.attnum AS ordinal_position, " +
+ " a.attname AS column_name, " +
+ " pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type, " +
+ " t.typname AS type_name " +
+ "FROM pg_catalog.pg_attribute a " +
+ "INNER JOIN pg_catalog.pg_class c ON a.attrelid = c.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid " +
+ "INNER JOIN pg_catalog.pg_type t ON a.atttypid = t.oid " +
+ "WHERE n.nspname = ? " +
+ " AND c.relkind = 'v' " +
+ " AND a.attnum > 0 " +
+ " AND NOT a.attisdropped " +
+ "ORDER BY c.relname, a.attnum";
+
+ try {
+ List columns = jdbcOperations.query(sql, new Object[] {schemaName}, (rs, rowNum) -> {
+ DBTableColumn column = new DBTableColumn();
+ column.setSchemaName(schemaName);
+ column.setTableName(rs.getString("view_name"));
+ column.setOrdinalPosition(rs.getInt("ordinal_position"));
+ column.setName(rs.getString("column_name"));
+ column.setTypeName(rs.getString("type_name"));
+ column.setFullTypeName(rs.getString("data_type"));
+ return column;
+ });
+ return columns.stream()
+ .filter(col -> col.getTableName() != null)
+ .collect(Collectors.groupingBy(DBTableColumn::getTableName));
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return Collections.emptyMap();
+ }
+ throw e;
+ }
}
+ /**
+ * 获取指定视图的列信息
+ *
+ * @param schemaName schema 名称
+ * @param viewName 视图名
+ * @return 列信息列表
+ */
@Override
public List listBasicViewColumns(String schemaName, String viewName) {
- throw new UnsupportedOperationException("Not supported yet");
+ String sql = "SELECT " +
+ " a.attnum AS ordinal_position, " +
+ " a.attname AS column_name, " +
+ " pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type, " +
+ " t.typname AS type_name " +
+ "FROM pg_catalog.pg_attribute a " +
+ "INNER JOIN pg_catalog.pg_class c ON a.attrelid = c.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid " +
+ "INNER JOIN pg_catalog.pg_type t ON a.atttypid = t.oid " +
+ "WHERE n.nspname = ? " +
+ " AND c.relname = ? " +
+ " AND c.relkind = 'v' " +
+ " AND a.attnum > 0 " +
+ " AND NOT a.attisdropped " +
+ "ORDER BY a.attnum";
+
+ try {
+ return jdbcOperations.query(sql, new Object[] {schemaName, viewName}, (rs, rowNum) -> {
+ DBTableColumn column = new DBTableColumn();
+ column.setSchemaName(schemaName);
+ column.setTableName(viewName);
+ column.setOrdinalPosition(rs.getInt("ordinal_position"));
+ column.setName(rs.getString("column_name"));
+ column.setTypeName(rs.getString("type_name"));
+ column.setFullTypeName(rs.getString("data_type"));
+ return column;
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema") ||
+ StringUtils.containsIgnoreCase(e.getMessage(), "relation")) {
+ return Collections.emptyList();
+ }
+ throw e;
+ }
}
@Override
@@ -345,21 +1167,634 @@ public Map> listBasicColumnsInfo(String schemaName)
throw new UnsupportedOperationException("Not supported yet");
}
+ /**
+ * 列出指定 schema 下所有表的索引信息
+ *
+ * @param schemaName schema 名称
+ * @return 表名到索引列表的映射
+ */
@Override
public Map> listTableIndexes(String schemaName) {
- throw new UnsupportedOperationException("Not supported yet");
+ String sql = "SELECT " +
+ " t.relname AS table_name, " +
+ " i.relname AS index_name, " +
+ " ix.indisunique AS is_unique, " +
+ " ix.indisprimary AS is_primary, " +
+ " am.amname AS index_type, " +
+ " a.attname AS column_name, " +
+ " array_position(ix.indkey, a.attnum) AS column_position " +
+ "FROM pg_catalog.pg_index ix " +
+ "INNER JOIN pg_catalog.pg_class i ON ix.indexrelid = i.oid " +
+ "INNER JOIN pg_catalog.pg_class t ON ix.indrelid = t.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON t.relnamespace = n.oid " +
+ "INNER JOIN pg_catalog.pg_am am ON i.relam = am.oid " +
+ "INNER JOIN pg_catalog.pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) " +
+ "WHERE n.nspname = ? " +
+ "ORDER BY t.relname, i.relname, column_position";
+
+ try {
+ Map> tableIndexMap = new LinkedHashMap<>();
+ Map tableIndexCounterMap = new HashMap<>();
+
+ jdbcOperations.query(sql, new Object[] {schemaName}, (rs, rowNum) -> {
+ String tableName = rs.getString("table_name");
+ String indexName = rs.getString("index_name");
+
+ Map indexMap = tableIndexMap.computeIfAbsent(tableName,
+ k -> new LinkedHashMap<>());
+ DBTableIndex index = indexMap.get(indexName);
+
+ if (index == null) {
+ index = new DBTableIndex();
+ index.setSchemaName(schemaName);
+ index.setTableName(tableName);
+ index.setName(indexName);
+ index.setOrdinalPosition(tableIndexCounterMap.computeIfAbsent(tableName, k -> new AtomicInteger(1))
+ .getAndIncrement());
+ index.setUnique(rs.getBoolean("is_unique"));
+ index.setPrimary(rs.getBoolean("is_primary"));
+ index.setNonUnique(!index.getUnique());
+ index.setColumnNames(new ArrayList<>());
+
+ String indexType = rs.getString("index_type");
+ if ("btree".equalsIgnoreCase(indexType)) {
+ if (index.getUnique()) {
+ index.setType(DBIndexType.UNIQUE);
+ } else {
+ index.setType(DBIndexType.NORMAL);
+ }
+ } else if ("hash".equalsIgnoreCase(indexType)) {
+ index.setType(DBIndexType.NORMAL);
+ } else if ("gin".equalsIgnoreCase(indexType)) {
+ index.setType(DBIndexType.FULLTEXT);
+ } else if ("gist".equalsIgnoreCase(indexType)) {
+ index.setType(DBIndexType.SPATIAL);
+ } else {
+ index.setType(DBIndexType.UNKNOWN);
+ }
+
+ indexMap.put(indexName, index);
+ }
+
+ String columnName = rs.getString("column_name");
+ if (columnName != null) {
+ index.getColumnNames().add(columnName);
+ }
+
+ return null;
+ });
+
+ // 转换为 Map>
+ Map> result = new LinkedHashMap<>();
+ for (Map.Entry> entry : tableIndexMap.entrySet()) {
+ result.put(entry.getKey(), new ArrayList<>(entry.getValue().values()));
+ }
+ return result;
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return Collections.emptyMap();
+ }
+ throw e;
+ }
}
+ /**
+ * 列出指定表的索引信息
+ *
+ * @param schemaName schema 名称
+ * @param tableName 表名
+ * @return 索引列表
+ */
+ @Override
+ public List listTableIndexes(String schemaName, String tableName) {
+ String sql = "SELECT " +
+ " i.relname AS index_name, " +
+ " ix.indisunique AS is_unique, " +
+ " ix.indisprimary AS is_primary, " +
+ " am.amname AS index_type, " +
+ " pg_get_indexdef(ix.indexrelid) AS index_definition, " +
+ " obj_description(ix.indexrelid) AS index_comment, " +
+ " a.attname AS column_name, " +
+ " array_position(ix.indkey, a.attnum) AS column_position " +
+ "FROM pg_catalog.pg_index ix " +
+ "INNER JOIN pg_catalog.pg_class i ON ix.indexrelid = i.oid " +
+ "INNER JOIN pg_catalog.pg_class t ON ix.indrelid = t.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON t.relnamespace = n.oid " +
+ "INNER JOIN pg_catalog.pg_am am ON i.relam = am.oid " +
+ "INNER JOIN pg_catalog.pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) " +
+ "WHERE n.nspname = ? AND t.relname = ? " +
+ "ORDER BY i.relname, column_position";
+
+ try {
+ Map indexMap = new LinkedHashMap<>();
+ AtomicInteger ordinalCounter = new AtomicInteger(1);
+
+ jdbcOperations.query(sql, new Object[] {schemaName, tableName}, (rs, rowNum) -> {
+ String indexName = rs.getString("index_name");
+ DBTableIndex index = indexMap.get(indexName);
+
+ if (index == null) {
+ index = new DBTableIndex();
+ index.setSchemaName(schemaName);
+ index.setTableName(tableName);
+ index.setName(indexName);
+ index.setOrdinalPosition(ordinalCounter.getAndIncrement());
+ index.setUnique(rs.getBoolean("is_unique"));
+ index.setPrimary(rs.getBoolean("is_primary"));
+ index.setNonUnique(!index.getUnique());
+ index.setColumnNames(new ArrayList<>());
+
+ String indexType = rs.getString("index_type");
+ if ("btree".equalsIgnoreCase(indexType)) {
+ if (index.getUnique()) {
+ index.setType(DBIndexType.UNIQUE);
+ } else {
+ index.setType(DBIndexType.NORMAL);
+ }
+ } else if ("hash".equalsIgnoreCase(indexType)) {
+ index.setType(DBIndexType.NORMAL);
+ } else if ("gin".equalsIgnoreCase(indexType)) {
+ index.setType(DBIndexType.FULLTEXT);
+ } else if ("gist".equalsIgnoreCase(indexType)) {
+ index.setType(DBIndexType.SPATIAL);
+ } else {
+ index.setType(DBIndexType.UNKNOWN);
+ }
+
+ String definition = rs.getString("index_definition");
+ if (StringUtils.isNotBlank(definition)) {
+ index.setDdl(definition);
+ }
+
+ String comment = rs.getString("index_comment");
+ if (StringUtils.isNotBlank(comment)) {
+ index.setComment(comment);
+ }
+
+ indexMap.put(indexName, index);
+ }
+
+ String columnName = rs.getString("column_name");
+ if (columnName != null) {
+ index.getColumnNames().add(columnName);
+ }
+
+ return null;
+ });
+
+ return new ArrayList<>(indexMap.values());
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema") ||
+ StringUtils.containsIgnoreCase(e.getMessage(), "relation")) {
+ return Collections.emptyList();
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * 列出指定 schema 下所有表的约束信息
+ *
+ * @param schemaName schema 名称
+ * @return 表名到约束列表的映射
+ */
@Override
public Map> listTableConstraints(
String schemaName) {
- throw new UnsupportedOperationException("Not supported yet");
+ Map> tableConstraintMap = new LinkedHashMap<>();
+ AtomicInteger constraintCounter = new AtomicInteger(1);
+
+ // 查询主键和唯一约束
+ String pkUniqueSql = "SELECT " +
+ " t.relname AS table_name, " +
+ " con.conname AS constraint_name, " +
+ " con.contype AS constraint_type, " +
+ " a.attname AS column_name " +
+ "FROM pg_catalog.pg_constraint con " +
+ "INNER JOIN pg_catalog.pg_class t ON con.conrelid = t.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON t.relnamespace = n.oid " +
+ "INNER JOIN pg_catalog.pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(con.conkey) " +
+ "WHERE n.nspname = ? " +
+ " AND con.contype IN ('p', 'u') " +
+ "ORDER BY t.relname, con.conname, a.attnum";
+
+ jdbcOperations.query(pkUniqueSql, new Object[] {schemaName}, (rs, rowNum) -> {
+ String tableName = rs.getString("table_name");
+ String constraintName = rs.getString("constraint_name");
+ String constraintType = rs.getString("constraint_type");
+
+ Map constraintMap = tableConstraintMap.computeIfAbsent(tableName,
+ k -> new LinkedHashMap<>());
+ DBTableConstraint constraint = constraintMap.get(constraintName);
+
+ if (constraint == null) {
+ constraint = new DBTableConstraint();
+ constraint.setName(constraintName);
+ constraint.setSchemaName(schemaName);
+ constraint.setTableName(tableName);
+ constraint.setOwner(schemaName);
+ constraint.setOrdinalPosition(constraintCounter.getAndIncrement());
+ constraint.setColumnNames(new ArrayList<>());
+
+ if ("p".equals(constraintType)) {
+ constraint.setType(DBConstraintType.PRIMARY_KEY);
+ } else if ("u".equals(constraintType)) {
+ constraint.setType(DBConstraintType.UNIQUE);
+ }
+
+ constraintMap.put(constraintName, constraint);
+ }
+
+ String columnName = rs.getString("column_name");
+ if (columnName != null) {
+ constraint.getColumnNames().add(columnName);
+ }
+
+ return null;
+ });
+
+ // 查询外键约束
+ String fkSql = "SELECT " +
+ " t.relname AS table_name, " +
+ " con.conname AS constraint_name, " +
+ " a.attname AS column_name, " +
+ " ref_ns.nspname AS referenced_schema_name, " +
+ " ref_t.relname AS referenced_table_name, " +
+ " ref_a.attname AS referenced_column_name, " +
+ " con.confupdtype AS update_action, " +
+ " con.confdeltype AS delete_action " +
+ "FROM pg_catalog.pg_constraint con " +
+ "INNER JOIN pg_catalog.pg_class t ON con.conrelid = t.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON t.relnamespace = n.oid " +
+ "INNER JOIN pg_catalog.pg_class ref_t ON con.confrelid = ref_t.oid " +
+ "INNER JOIN pg_catalog.pg_namespace ref_ns ON ref_t.relnamespace = ref_ns.oid " +
+ "INNER JOIN pg_catalog.pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(con.conkey) " +
+ "INNER JOIN pg_catalog.pg_attribute ref_a ON ref_a.attrelid = ref_t.oid AND ref_a.attnum = ANY(con.confkey) "
+ +
+ "WHERE n.nspname = ? " +
+ " AND con.contype = 'f' " +
+ "ORDER BY t.relname, con.conname, a.attnum";
+
+ jdbcOperations.query(fkSql, new Object[] {schemaName}, (rs, rowNum) -> {
+ String tableName = rs.getString("table_name");
+ String constraintName = rs.getString("constraint_name");
+
+ Map constraintMap = tableConstraintMap.computeIfAbsent(tableName,
+ k -> new LinkedHashMap<>());
+ DBTableConstraint constraint = constraintMap.get(constraintName);
+
+ if (constraint == null) {
+ constraint = new DBTableConstraint();
+ constraint.setName(constraintName);
+ constraint.setSchemaName(schemaName);
+ constraint.setTableName(tableName);
+ constraint.setOwner(schemaName);
+ constraint.setType(DBConstraintType.FOREIGN_KEY);
+ constraint.setOrdinalPosition(constraintCounter.getAndIncrement());
+ constraint.setColumnNames(new ArrayList<>());
+ constraint.setReferenceColumnNames(new ArrayList<>());
+ constraint.setReferenceSchemaName(rs.getString("referenced_schema_name"));
+ constraint.setReferenceTableName(rs.getString("referenced_table_name"));
+
+ // 解析 ON UPDATE 和 ON DELETE 规则
+ constraint.setOnUpdateRule(mapPgConstraintAction(rs.getString("update_action")));
+ constraint.setOnDeleteRule(mapPgConstraintAction(rs.getString("delete_action")));
+
+ constraintMap.put(constraintName, constraint);
+ }
+
+ String columnName = rs.getString("column_name");
+ if (columnName != null && !constraint.getColumnNames().contains(columnName)) {
+ constraint.getColumnNames().add(columnName);
+ }
+
+ String refColumnName = rs.getString("referenced_column_name");
+ if (refColumnName != null && !constraint.getReferenceColumnNames().contains(refColumnName)) {
+ constraint.getReferenceColumnNames().add(refColumnName);
+ }
+
+ return null;
+ });
+
+ // 查询检查约束
+ String checkSql = "SELECT " +
+ " t.relname AS table_name, " +
+ " con.conname AS constraint_name, " +
+ " pg_get_constraintdef(con.oid) AS constraint_definition " +
+ "FROM pg_catalog.pg_constraint con " +
+ "INNER JOIN pg_catalog.pg_class t ON con.conrelid = t.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON t.relnamespace = n.oid " +
+ "WHERE n.nspname = ? " +
+ " AND con.contype = 'c' " +
+ "ORDER BY t.relname, con.conname";
+
+ jdbcOperations.query(checkSql, new Object[] {schemaName}, (rs, rowNum) -> {
+ String tableName = rs.getString("table_name");
+ String constraintName = rs.getString("constraint_name");
+ String definition = rs.getString("constraint_definition");
+
+ Map constraintMap = tableConstraintMap.computeIfAbsent(tableName,
+ k -> new LinkedHashMap<>());
+ DBTableConstraint constraint = new DBTableConstraint();
+ constraint.setName(constraintName);
+ constraint.setSchemaName(schemaName);
+ constraint.setTableName(tableName);
+ constraint.setOwner(schemaName);
+ constraint.setType(DBConstraintType.CHECK);
+ constraint.setOrdinalPosition(constraintCounter.getAndIncrement());
+ constraint.setColumnNames(new ArrayList<>());
+
+ if (StringUtils.isNotBlank(definition)) {
+ // 从定义中提取 CHECK 子句
+ int checkStart = definition.toUpperCase().indexOf("CHECK");
+ if (checkStart >= 0) {
+ constraint.setCheckClause(definition.substring(checkStart));
+ } else {
+ constraint.setCheckClause(definition);
+ }
+ }
+
+ constraintMap.put(constraintName, constraint);
+ return null;
+ });
+
+ // 转换为 Map>
+ Map> result = new LinkedHashMap<>();
+ for (Map.Entry> entry : tableConstraintMap.entrySet()) {
+ result.put(entry.getKey(), new ArrayList<>(entry.getValue().values()));
+ }
+ return result;
+ }
+
+ /**
+ * 列出指定表的约束信息
+ *
+ * @param schemaName schema 名称
+ * @param tableName 表名
+ * @return 约束列表
+ */
+ @Override
+ public List listTableConstraints(String schemaName, String tableName) {
+ List constraints = new ArrayList<>();
+ AtomicInteger ordinalCounter = new AtomicInteger(1);
+
+ // 查询主键和唯一约束
+ String pkUniqueSql = "SELECT " +
+ " con.conname AS constraint_name, " +
+ " con.contype AS constraint_type, " +
+ " a.attname AS column_name " +
+ "FROM pg_catalog.pg_constraint con " +
+ "INNER JOIN pg_catalog.pg_class t ON con.conrelid = t.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON t.relnamespace = n.oid " +
+ "INNER JOIN pg_catalog.pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(con.conkey) " +
+ "WHERE n.nspname = ? AND t.relname = ? " +
+ " AND con.contype IN ('p', 'u') " +
+ "ORDER BY con.conname, a.attnum";
+
+ Map constraintMap = new LinkedHashMap<>();
+
+ jdbcOperations.query(pkUniqueSql, new Object[] {schemaName, tableName}, (rs, rowNum) -> {
+ String constraintName = rs.getString("constraint_name");
+ String constraintType = rs.getString("constraint_type");
+
+ DBTableConstraint constraint = constraintMap.get(constraintName);
+
+ if (constraint == null) {
+ constraint = new DBTableConstraint();
+ constraint.setName(constraintName);
+ constraint.setSchemaName(schemaName);
+ constraint.setTableName(tableName);
+ constraint.setOwner(schemaName);
+ constraint.setOrdinalPosition(ordinalCounter.getAndIncrement());
+ constraint.setColumnNames(new ArrayList<>());
+
+ if ("p".equals(constraintType)) {
+ constraint.setType(DBConstraintType.PRIMARY_KEY);
+ } else if ("u".equals(constraintType)) {
+ constraint.setType(DBConstraintType.UNIQUE);
+ }
+
+ constraintMap.put(constraintName, constraint);
+ }
+
+ String columnName = rs.getString("column_name");
+ if (columnName != null) {
+ constraint.getColumnNames().add(columnName);
+ }
+
+ return null;
+ });
+
+ // 查询外键约束
+ String fkSql = "SELECT " +
+ " con.conname AS constraint_name, " +
+ " a.attname AS column_name, " +
+ " ref_ns.nspname AS referenced_schema_name, " +
+ " ref_t.relname AS referenced_table_name, " +
+ " ref_a.attname AS referenced_column_name, " +
+ " con.confupdtype AS update_action, " +
+ " con.confdeltype AS delete_action " +
+ "FROM pg_catalog.pg_constraint con " +
+ "INNER JOIN pg_catalog.pg_class t ON con.conrelid = t.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON t.relnamespace = n.oid " +
+ "INNER JOIN pg_catalog.pg_class ref_t ON con.confrelid = ref_t.oid " +
+ "INNER JOIN pg_catalog.pg_namespace ref_ns ON ref_t.relnamespace = ref_ns.oid " +
+ "INNER JOIN pg_catalog.pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(con.conkey) " +
+ "INNER JOIN pg_catalog.pg_attribute ref_a ON ref_a.attrelid = ref_t.oid AND ref_a.attnum = ANY(con.confkey) "
+ +
+ "WHERE n.nspname = ? AND t.relname = ? " +
+ " AND con.contype = 'f' " +
+ "ORDER BY con.conname, a.attnum";
+
+ jdbcOperations.query(fkSql, new Object[] {schemaName, tableName}, (rs, rowNum) -> {
+ String constraintName = rs.getString("constraint_name");
+
+ DBTableConstraint constraint = constraintMap.get(constraintName);
+
+ if (constraint == null) {
+ constraint = new DBTableConstraint();
+ constraint.setName(constraintName);
+ constraint.setSchemaName(schemaName);
+ constraint.setTableName(tableName);
+ constraint.setOwner(schemaName);
+ constraint.setType(DBConstraintType.FOREIGN_KEY);
+ constraint.setOrdinalPosition(ordinalCounter.getAndIncrement());
+ constraint.setColumnNames(new ArrayList<>());
+ constraint.setReferenceColumnNames(new ArrayList<>());
+ constraint.setReferenceSchemaName(rs.getString("referenced_schema_name"));
+ constraint.setReferenceTableName(rs.getString("referenced_table_name"));
+
+ // 解析 ON UPDATE 和 ON DELETE 规则
+ constraint.setOnUpdateRule(mapPgConstraintAction(rs.getString("update_action")));
+ constraint.setOnDeleteRule(mapPgConstraintAction(rs.getString("delete_action")));
+
+ constraintMap.put(constraintName, constraint);
+ }
+
+ String columnName = rs.getString("column_name");
+ if (columnName != null && !constraint.getColumnNames().contains(columnName)) {
+ constraint.getColumnNames().add(columnName);
+ }
+
+ String refColumnName = rs.getString("referenced_column_name");
+ if (refColumnName != null && !constraint.getReferenceColumnNames().contains(refColumnName)) {
+ constraint.getReferenceColumnNames().add(refColumnName);
+ }
+
+ return null;
+ });
+
+ // 查询检查约束
+ String checkSql = "SELECT " +
+ " con.conname AS constraint_name, " +
+ " pg_get_constraintdef(con.oid) AS constraint_definition " +
+ "FROM pg_catalog.pg_constraint con " +
+ "INNER JOIN pg_catalog.pg_class t ON con.conrelid = t.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON t.relnamespace = n.oid " +
+ "WHERE n.nspname = ? AND t.relname = ? " +
+ " AND con.contype = 'c' " +
+ "ORDER BY con.conname";
+
+ jdbcOperations.query(checkSql, new Object[] {schemaName, tableName}, (rs, rowNum) -> {
+ String constraintName = rs.getString("constraint_name");
+ String definition = rs.getString("constraint_definition");
+
+ DBTableConstraint constraint = new DBTableConstraint();
+ constraint.setName(constraintName);
+ constraint.setSchemaName(schemaName);
+ constraint.setTableName(tableName);
+ constraint.setOwner(schemaName);
+ constraint.setType(DBConstraintType.CHECK);
+ constraint.setOrdinalPosition(ordinalCounter.getAndIncrement());
+ constraint.setColumnNames(new ArrayList<>());
+
+ if (StringUtils.isNotBlank(definition)) {
+ // 从定义中提取 CHECK 子句
+ int checkStart = definition.toUpperCase().indexOf("CHECK");
+ if (checkStart >= 0) {
+ constraint.setCheckClause(definition.substring(checkStart));
+ } else {
+ constraint.setCheckClause(definition);
+ }
+ }
+
+ constraintMap.put(constraintName, constraint);
+ return null;
+ });
+
+ constraints.addAll(constraintMap.values());
+ return constraints;
+ }
+
+ /**
+ * 映射 PostgreSQL 约束动作到外部模型 PostgreSQL action codes: 'a' = NO ACTION, 'r' = RESTRICT, 'c' = CASCADE,
+ * 'n' = SET NULL, 'd' = SET DEFAULT
+ */
+ private DBForeignKeyModifyRule mapPgConstraintAction(String action) {
+ if (action == null || action.isEmpty()) {
+ return DBForeignKeyModifyRule.NO_ACTION;
+ }
+ switch (action.charAt(0)) {
+ case 'c':
+ return DBForeignKeyModifyRule.CASCADE;
+ case 'n':
+ return DBForeignKeyModifyRule.SET_NULL;
+ case 'd':
+ return DBForeignKeyModifyRule.SET_DEFAULT;
+ case 'r':
+ return DBForeignKeyModifyRule.NO_ACTION; // RESTRICT 类似 NO ACTION
+ case 'a':
+ default:
+ return DBForeignKeyModifyRule.NO_ACTION;
+ }
}
+ /**
+ * 列出指定 schema 下所有表的选项信息
+ *
+ * PostgreSQL 表选项包括:表注释、创建时间、修改时间等
+ *
+ *
+ * @param schemaName schema 名称
+ * @return 表名到表选项的映射
+ */
@Override
public Map listTableOptions(
String schemaName) {
- throw new UnsupportedOperationException("Not supported yet");
+ String sql = "SELECT " +
+ " c.relname AS table_name, " +
+ " obj_description(c.oid) AS table_comment, " +
+ " pg_catalog.pg_size_pretty(pg_catalog.pg_total_relation_size(c.oid)) AS total_size " +
+ "FROM pg_catalog.pg_class c " +
+ "INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid " +
+ "WHERE n.nspname = ? " +
+ " AND c.relkind IN ('r', 'p') " +
+ "ORDER BY c.relname";
+
+ try {
+ Map result = new LinkedHashMap<>();
+ jdbcOperations.query(sql, new Object[] {schemaName}, (rs, rowNum) -> {
+ String tableName = rs.getString("table_name");
+ DBTableOptions options = new DBTableOptions();
+
+ String comment = rs.getString("table_comment");
+ if (StringUtils.isNotBlank(comment)) {
+ options.setComment(comment);
+ }
+
+ result.put(tableName, options);
+ return null;
+ });
+ return result;
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return Collections.emptyMap();
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * 获取指定表的选项信息
+ *
+ * PostgreSQL 表选项包括:表注释等
+ *
+ *
+ * @param schemaName schema 名称
+ * @param tableName 表名
+ * @return 表选项
+ */
+ @Override
+ public DBTableOptions getTableOptions(String schemaName, String tableName) {
+ String sql = "SELECT " +
+ " obj_description(c.oid) AS table_comment " +
+ "FROM pg_catalog.pg_class c " +
+ "INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid " +
+ "WHERE n.nspname = ? AND c.relname = ?";
+
+ DBTableOptions options = new DBTableOptions();
+ try {
+ jdbcOperations.query(sql, new Object[] {schemaName, tableName}, rs -> {
+ if (rs.next()) {
+ String comment = rs.getString("table_comment");
+ if (StringUtils.isNotBlank(comment)) {
+ options.setComment(comment);
+ }
+ }
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema") ||
+ StringUtils.containsIgnoreCase(e.getMessage(), "relation")) {
+ return options;
+ }
+ throw e;
+ }
+ return options;
+ }
+
+ @Override
+ public DBTableOptions getTableOptions(String schemaName, String tableName, String ddl) {
+ return getTableOptions(schemaName, tableName);
}
@Override
@@ -389,34 +1824,371 @@ public List listPartitionTables(String partitionMethod) {
throw new UnsupportedOperationException("Not supported yet");
}
- @Override
- public List listTableConstraints(String schemaName, String tableName) {
- throw new UnsupportedOperationException("Not supported yet");
- }
-
+ /**
+ * 获取表的分区信息
+ *
+ * PostgreSQL 从 10 版本开始支持声明式分区,通过 pg_class.relkind = 'p' 识别分区表 分区类型包括:RANGE, LIST, HASH
+ *
+ *
+ * @param schemaName schema 名称
+ * @param tableName 表名
+ * @return 分区信息,如果不是分区表则返回 null
+ */
@Override
public DBTablePartition getPartition(String schemaName, String tableName) {
- throw new UnsupportedOperationException("Not supported yet");
+ // 首先检查是否是分区表
+ String checkPartitionSql = "SELECT c.relkind, p.partstrat " +
+ "FROM pg_catalog.pg_class c " +
+ "INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid " +
+ "LEFT JOIN pg_catalog.pg_partitioned_table p ON c.oid = p.partrelid " +
+ "WHERE n.nspname = ? AND c.relname = ?";
+
+ AtomicReference partitionStrategy = new AtomicReference<>();
+ AtomicReference relKind = new AtomicReference<>();
+
+ try {
+ jdbcOperations.query(checkPartitionSql, new Object[] {schemaName, tableName}, rs -> {
+ relKind.set(rs.getString("relkind"));
+ partitionStrategy.set(rs.getString("partstrat"));
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema") ||
+ StringUtils.containsIgnoreCase(e.getMessage(), "relation")) {
+ return null;
+ }
+ throw e;
+ }
+
+ // 如果不是分区表,返回 null
+ if (!"p".equals(relKind.get()) || partitionStrategy.get() == null) {
+ return null;
+ }
+
+ DBTablePartition partition = new DBTablePartition();
+ partition.setSchemaName(schemaName);
+ partition.setTableName(tableName);
+
+ // 设置分区选项
+ DBTablePartitionOption option = new DBTablePartitionOption();
+ String strategy = partitionStrategy.get();
+ if ("r".equalsIgnoreCase(strategy)) {
+ option.setType(DBTablePartitionType.RANGE);
+ } else if ("l".equalsIgnoreCase(strategy)) {
+ option.setType(DBTablePartitionType.LIST);
+ } else if ("h".equalsIgnoreCase(strategy)) {
+ option.setType(DBTablePartitionType.HASH);
+ }
+ partition.setPartitionOption(option);
+
+ // 获取分区键列
+ String partitionKeySql = "SELECT a.attname " +
+ "FROM pg_catalog.pg_partitioned_table p " +
+ "INNER JOIN pg_catalog.pg_class c ON p.partrelid = c.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid " +
+ "INNER JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid AND a.attnum = p.partkey[1] " +
+ "WHERE n.nspname = ? AND c.relname = ?";
+
+ List partitionColumns = new ArrayList<>();
+ try {
+ jdbcOperations.query(partitionKeySql, new Object[] {schemaName, tableName}, rs -> {
+ partitionColumns.add(rs.getString("attname"));
+ });
+ } catch (Exception e) {
+ log.warn("Failed to get partition key for table: " + schemaName + "." + tableName, e);
+ }
+
+ if (!partitionColumns.isEmpty()) {
+ option.setColumnNames(partitionColumns);
+ }
+
+ // 获取分区定义列表
+ String partitionDefSql = "SELECT " +
+ " c.relname AS partition_name, " +
+ " pg_get_expr(c.relpartbound, c.oid) AS partition_bound, " +
+ " obj_description(c.oid) AS partition_comment " +
+ "FROM pg_catalog.pg_class c " +
+ "INNER JOIN pg_catalog.pg_inherits i ON c.oid = i.inhrelid " +
+ "INNER JOIN pg_catalog.pg_class parent ON i.inhparent = parent.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON parent.relnamespace = n.oid " +
+ "WHERE n.nspname = ? AND parent.relname = ? " +
+ "ORDER BY c.relname";
+
+ List partitionDefinitions = new ArrayList<>();
+ try {
+ jdbcOperations.query(partitionDefSql, new Object[] {schemaName, tableName}, rs -> {
+ DBTablePartitionDefinition def = new DBTablePartitionDefinition();
+ def.setName(rs.getString("partition_name"));
+ def.setType(option.getType());
+
+ String bound = rs.getString("partition_bound");
+ if (StringUtils.isNotBlank(bound)) {
+ def.fillValues(bound);
+ }
+
+ String comment = rs.getString("partition_comment");
+ if (StringUtils.isNotBlank(comment)) {
+ def.setComment(comment);
+ }
+
+ partitionDefinitions.add(def);
+ });
+ } catch (Exception e) {
+ log.warn("Failed to get partition definitions for table: " + schemaName + "." + tableName, e);
+ }
+
+ partition.setPartitionDefinitions(partitionDefinitions);
+
+ return partition;
}
- @Override
- public List listTableIndexes(String schemaName, String tableName) {
- throw new UnsupportedOperationException("Not supported yet");
+ /**
+ * 解析分区边界表达式 PostgreSQL 返回的边界格式如:FOR VALUES FROM ('a') TO ('z') 或 FOR VALUES IN ('a', 'b')
+ */
+ private List parsePartitionBound(String bound, DBTablePartitionType type) {
+ List values = new ArrayList<>();
+ if (StringUtils.isBlank(bound)) {
+ return values;
+ }
+
+ // 简单解析,提取括号中的值
+ int start = bound.indexOf('(');
+ int end = bound.lastIndexOf(')');
+ if (start >= 0 && end > start) {
+ String content = bound.substring(start + 1, end);
+ // 分割多个值(如果有)
+ String[] parts = content.split(",");
+ for (String part : parts) {
+ String trimmed = part.trim();
+ // 移除引号
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
+ trimmed = trimmed.substring(1, trimmed.length() - 1);
+ }
+ values.add(trimmed);
+ }
+ }
+ return values;
}
+ /**
+ * 获取表的 DDL
+ *
+ * PostgreSQL 没有类似 MySQL SHOW CREATE TABLE 的内置函数,需要程序化拼装 DDL
+ *
+ *
+ * @param schemaName schema 名称
+ * @param tableName 表名
+ * @return CREATE TABLE DDL 语句
+ */
@Override
public String getTableDDL(String schemaName, String tableName) {
- throw new UnsupportedOperationException("Not supported yet");
+ StringBuilder ddl = new StringBuilder();
+
+ // 1. 获取列信息并生成列定义
+ List columns = listTableColumns(schemaName, tableName);
+ if (columns.isEmpty()) {
+ return "";
+ }
+
+ ddl.append("CREATE TABLE \"").append(schemaName).append("\".\"").append(tableName).append("\" (\n");
+
+ // 生成列定义
+ List columnDefs = new ArrayList<>();
+ for (DBTableColumn column : columns) {
+ columnDefs.add(buildColumnDefinition(column));
+ }
+ ddl.append(" ").append(String.join(",\n ", columnDefs));
+
+ // 2. 获取主键约束并添加到列定义后
+ List constraints = listTableConstraints(schemaName, tableName);
+ for (DBTableConstraint constraint : constraints) {
+ if (constraint.getType() == DBConstraintType.PRIMARY_KEY) {
+ ddl.append(",\n ");
+ ddl.append("CONSTRAINT \"").append(constraint.getName()).append("\" PRIMARY KEY (");
+ ddl.append(constraint.getColumnNames().stream()
+ .map(col -> "\"" + col + "\"")
+ .collect(Collectors.joining(", ")));
+ ddl.append(")");
+ }
+ }
+
+ ddl.append("\n);\n");
+
+ // 3. 添加表注释
+ DBTableOptions options = getTableOptions(schemaName, tableName);
+ if (StringUtils.isNotBlank(options.getComment())) {
+ ddl.append("\nCOMMENT ON TABLE \"").append(schemaName).append("\".\"").append(tableName)
+ .append("\" IS '").append(escapeString(options.getComment())).append("';\n");
+ }
+
+ // 4. 添加列注释
+ for (DBTableColumn column : columns) {
+ if (StringUtils.isNotBlank(column.getComment())) {
+ ddl.append("COMMENT ON COLUMN \"").append(schemaName).append("\".\"").append(tableName)
+ .append("\".\"").append(column.getName()).append("\" IS '")
+ .append(escapeString(column.getComment())).append("';\n");
+ }
+ }
+
+ // 5. 添加索引(非主键索引)
+ List indexes = listTableIndexes(schemaName, tableName);
+ for (DBTableIndex index : indexes) {
+ if (!Boolean.TRUE.equals(index.getPrimary())) {
+ ddl.append("\n").append(buildIndexDDL(schemaName, tableName, index));
+ }
+ }
+
+ // 6. 添加其他约束(外键、唯一约束、检查约束)
+ for (DBTableConstraint constraint : constraints) {
+ if (constraint.getType() == DBConstraintType.FOREIGN_KEY) {
+ ddl.append("\n").append(buildForeignKeyDDL(schemaName, tableName, constraint));
+ } else if (constraint.getType() == DBConstraintType.UNIQUE) {
+ // 唯一约束如果已有对应唯一索引,则不需要额外创建
+ } else if (constraint.getType() == DBConstraintType.CHECK) {
+ ddl.append("\n").append(buildCheckConstraintDDL(schemaName, tableName, constraint));
+ }
+ }
+
+ return ddl.toString();
}
- @Override
- public DBTableOptions getTableOptions(String schemaName, String tableName) {
- throw new UnsupportedOperationException("Not supported yet");
+ /**
+ * 构建列定义
+ */
+ private String buildColumnDefinition(DBTableColumn column) {
+ StringBuilder def = new StringBuilder();
+ def.append("\"").append(column.getName()).append("\" ");
+ def.append(column.getFullTypeName() != null ? column.getFullTypeName() : column.getTypeName());
+
+ // NOT NULL
+ if (Boolean.FALSE.equals(column.getNullable())) {
+ def.append(" NOT NULL");
+ }
+
+ // DEFAULT
+ if (StringUtils.isNotBlank(column.getDefaultValue())) {
+ def.append(" DEFAULT ").append(column.getDefaultValue());
+ }
+
+ return def.toString();
}
- @Override
- public DBTableOptions getTableOptions(String schemaName, String tableName, String ddl) {
- throw new UnsupportedOperationException("Not supported yet");
+ /**
+ * 构建索引 DDL
+ */
+ private String buildIndexDDL(String schemaName, String tableName, DBTableIndex index) {
+ StringBuilder ddl = new StringBuilder();
+ ddl.append("CREATE ");
+ if (Boolean.TRUE.equals(index.getUnique())) {
+ ddl.append("UNIQUE ");
+ }
+ ddl.append("INDEX \"").append(index.getName()).append("\" ON \"")
+ .append(schemaName).append("\".\"").append(tableName).append("\"");
+ if (index.getType() != null) {
+ String indexMethod = mapIndexTypeToMethod(index.getType());
+ ddl.append(" USING ").append(indexMethod);
+ }
+ if (index.getColumnNames() != null && !index.getColumnNames().isEmpty()) {
+ ddl.append(" (").append(index.getColumnNames().stream()
+ .map(col -> "\"" + col + "\"")
+ .collect(Collectors.joining(", "))).append(")");
+ }
+ ddl.append(";");
+ return ddl.toString();
+ }
+
+ /**
+ * 映射索引类型到 PostgreSQL 索引方法
+ */
+ private String mapIndexTypeToMethod(DBIndexType type) {
+ switch (type) {
+ case BITMAP:
+ return "bitmap";
+ case FULLTEXT:
+ return "gin"; // GIN 用于全文搜索
+ case SPATIAL:
+ return "gist";
+ case NORMAL:
+ case UNIQUE:
+ default:
+ return "btree";
+ }
+ }
+
+ /**
+ * 构建外键约束 DDL
+ */
+ private String buildForeignKeyDDL(String schemaName, String tableName, DBTableConstraint constraint) {
+ StringBuilder ddl = new StringBuilder();
+ ddl.append("ALTER TABLE \"").append(schemaName).append("\".\"").append(tableName).append("\" ");
+ ddl.append("ADD CONSTRAINT \"").append(constraint.getName()).append("\" ");
+ ddl.append("FOREIGN KEY (");
+ if (constraint.getColumnNames() != null) {
+ ddl.append(constraint.getColumnNames().stream()
+ .map(col -> "\"" + col + "\"")
+ .collect(Collectors.joining(", ")));
+ }
+ ddl.append(") REFERENCES \"");
+ if (StringUtils.isNotBlank(constraint.getReferenceSchemaName())) {
+ ddl.append(constraint.getReferenceSchemaName()).append("\".\"");
+ }
+ ddl.append(constraint.getReferenceTableName()).append("\"(");
+ if (constraint.getReferenceColumnNames() != null) {
+ ddl.append(constraint.getReferenceColumnNames().stream()
+ .map(col -> "\"" + col + "\"")
+ .collect(Collectors.joining(", ")));
+ }
+ ddl.append(")");
+
+ // ON DELETE
+ if (constraint.getOnDeleteRule() != null && constraint.getOnDeleteRule() != DBForeignKeyModifyRule.NO_ACTION) {
+ ddl.append(" ON DELETE ").append(mapForeignKeyRule(constraint.getOnDeleteRule()));
+ }
+
+ // ON UPDATE
+ if (constraint.getOnUpdateRule() != null && constraint.getOnUpdateRule() != DBForeignKeyModifyRule.NO_ACTION) {
+ ddl.append(" ON UPDATE ").append(mapForeignKeyRule(constraint.getOnUpdateRule()));
+ }
+
+ ddl.append(";");
+ return ddl.toString();
+ }
+
+ /**
+ * 构建检查约束 DDL
+ */
+ private String buildCheckConstraintDDL(String schemaName, String tableName, DBTableConstraint constraint) {
+ StringBuilder ddl = new StringBuilder();
+ ddl.append("ALTER TABLE \"").append(schemaName).append("\".\"").append(tableName).append("\" ");
+ ddl.append("ADD CONSTRAINT \"").append(constraint.getName()).append("\" ");
+ ddl.append("CHECK (").append(constraint.getCheckClause()).append(");");
+ return ddl.toString();
+ }
+
+ /**
+ * 映射外键规则到 PostgreSQL 语法
+ */
+ private String mapForeignKeyRule(DBForeignKeyModifyRule rule) {
+ switch (rule) {
+ case CASCADE:
+ return "CASCADE";
+ case SET_NULL:
+ return "SET NULL";
+ case SET_DEFAULT:
+ return "SET DEFAULT";
+ case NO_ACTION:
+ default:
+ return "NO ACTION";
+ }
+ }
+
+ /**
+ * 转义字符串中的单引号
+ */
+ private String escapeString(String str) {
+ if (str == null) {
+ return "";
+ }
+ return str.replace("'", "''");
}
@Override
@@ -425,50 +2197,370 @@ public List listTableColumnGroups(String schemaName,
throw new UnsupportedOperationException("Not supported yet");
}
+ /**
+ * 获取视图详情
+ *
+ * PostgreSQL 使用 information_schema.views 和 pg_class 查询视图定义
+ *
+ *
+ * @param schemaName schema 名称
+ * @param viewName 视图名
+ * @return 视图详情
+ */
@Override
public DBView getView(String schemaName, String viewName) {
- throw new UnsupportedOperationException("Not supported yet");
+ DBView view = new DBView();
+ view.setViewName(viewName);
+ view.setSchemaName(schemaName);
+
+ // 查询视图基本信息
+ String infoSql = "SELECT " +
+ " v.table_schema, " +
+ " v.check_option, " +
+ " v.is_updatable, " +
+ " pg_get_viewdef(c.oid, true) AS view_definition " +
+ "FROM information_schema.views v " +
+ "INNER JOIN pg_catalog.pg_class c ON c.relname = v.table_name " +
+ "INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid AND n.nspname = v.table_schema " +
+ "WHERE v.table_schema = ? AND v.table_name = ?";
+
+ try {
+ jdbcOperations.query(infoSql, new Object[] {schemaName, viewName}, rs -> {
+ view.setDefiner(rs.getString("table_schema"));
+
+ String checkOption = rs.getString("check_option");
+ // PostgreSQL 的 check_option 可以是 NONE, CASCADED 或 LOCAL
+ // 但 DBViewCheckOption 只有 NONE 和 READ_ONLY
+ // CASCADED 和 LOCAL 在 PostgreSQL 中表示视图的检查选项级联方式
+ // 将 CASCADED 映射为 READ_ONLY(更严格的检查),其他映射为 NONE
+ if ("CASCADED".equalsIgnoreCase(checkOption) || "LOCAL".equalsIgnoreCase(checkOption)) {
+ view.setCheckOption(DBViewCheckOption.READ_ONLY.name());
+ } else {
+ view.setCheckOption(DBViewCheckOption.NONE.name());
+ }
+
+ String isUpdatable = rs.getString("is_updatable");
+ view.setUpdatable("YES".equalsIgnoreCase(isUpdatable));
+
+ String viewDefinition = rs.getString("view_definition");
+ if (StringUtils.isNotBlank(viewDefinition)) {
+ StringBuilder ddl = new StringBuilder();
+ ddl.append("CREATE OR REPLACE VIEW \"").append(schemaName).append("\".\"").append(viewName)
+ .append("\" AS ").append(viewDefinition);
+ view.setDdl(ddl.toString());
+ }
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema") ||
+ StringUtils.containsIgnoreCase(e.getMessage(), "relation")) {
+ return null;
+ }
+ throw e;
+ }
+
+ // 获取视图列信息
+ view.setColumns(listBasicViewColumns(schemaName, viewName));
+
+ return view;
}
+ /**
+ * 获取函数详情
+ *
+ * PostgreSQL 使用 pg_get_functiondef 获取函数定义
+ *
+ *
+ * @param schemaName schema 名称
+ * @param functionName 函数名
+ * @return 函数详情
+ */
@Override
public DBFunction getFunction(String schemaName, String functionName) {
- throw new UnsupportedOperationException("Not supported yet");
+ DBFunction function = new DBFunction();
+ function.setFunName(functionName);
+
+ // 查询函数基本信息
+ String sql = "SELECT " +
+ " p.proname AS function_name, " +
+ " n.nspname AS schema_name, " +
+ " pg_get_functiondef(p.oid) AS function_ddl, " +
+ " pg_get_function_arguments(p.oid) AS arguments, " +
+ " pg_get_function_result(p.oid) AS return_type, " +
+ " p.prosrc AS body, " +
+ " l.lanname AS language, " +
+ " d.description AS comment " +
+ "FROM pg_catalog.pg_proc p " +
+ "INNER JOIN pg_catalog.pg_namespace n ON p.pronamespace = n.oid " +
+ "INNER JOIN pg_catalog.pg_language l ON p.prolang = l.oid " +
+ "LEFT JOIN pg_catalog.pg_description d ON d.objoid = p.oid AND d.classoid = 'pg_proc'::regclass " +
+ "WHERE n.nspname = ? AND p.proname = ? " +
+ " AND p.prokind = 'f'";
+
+ try {
+ jdbcOperations.query(sql, new Object[] {schemaName, functionName}, rs -> {
+ function.setDdl(rs.getString("function_ddl"));
+ function.setReturnType(rs.getString("return_type"));
+ function.setStatus("VALID");
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return null;
+ }
+ throw e;
+ }
+
+ return function;
}
+ /**
+ * 获取存储过程详情
+ *
+ * PostgreSQL 11+ 使用 pg_get_functiondef 获取存储过程定义
+ *
+ *
+ * @param schemaName schema 名称
+ * @param procedureName 存储过程名
+ * @return 存储过程详情
+ */
@Override
public DBProcedure getProcedure(String schemaName, String procedureName) {
- throw new UnsupportedOperationException("Not supported yet");
+ DBProcedure procedure = new DBProcedure();
+ procedure.setProName(procedureName);
+
+ // 查询存储过程基本信息
+ String sql = "SELECT " +
+ " p.proname AS procedure_name, " +
+ " n.nspname AS schema_name, " +
+ " pg_get_functiondef(p.oid) AS procedure_ddl, " +
+ " pg_get_function_arguments(p.oid) AS arguments, " +
+ " p.prosrc AS body, " +
+ " l.lanname AS language, " +
+ " d.description AS comment " +
+ "FROM pg_catalog.pg_proc p " +
+ "INNER JOIN pg_catalog.pg_namespace n ON p.pronamespace = n.oid " +
+ "INNER JOIN pg_catalog.pg_language l ON p.prolang = l.oid " +
+ "LEFT JOIN pg_catalog.pg_description d ON d.objoid = p.oid AND d.classoid = 'pg_proc'::regclass " +
+ "WHERE n.nspname = ? AND p.proname = ? " +
+ " AND p.prokind = 'p'";
+
+ try {
+ jdbcOperations.query(sql, new Object[] {schemaName, procedureName}, rs -> {
+ procedure.setDdl(rs.getString("procedure_ddl"));
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return null;
+ }
+ // 如果 prokind 列不存在(PG 11 之前版本),返回 null
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "prokind")) {
+ log.debug("PostgreSQL version does not support stored procedures (requires PG 11+)");
+ return null;
+ }
+ throw e;
+ }
+
+ return procedure;
}
+ /**
+ * 获取包详情
+ *
+ * PostgreSQL 不支持 Oracle 风格的包概念
+ *
+ */
@Override
public DBPackage getPackage(String schemaName, String packageName) {
- throw new UnsupportedOperationException("Not supported yet");
- }
+ // PostgreSQL 不支持包,返回 null
+ return null;
+ }
+
+ /**
+ * 获取触发器详情
+ *
+ * @param schemaName schema 名称
+ * @param triggerName 触发器名称(注意:接口参数名为 packageName,实际表示触发器名)
+ * @return 触发器详情
+ */
+ @Override
+ public DBTrigger getTrigger(String schemaName, String triggerName) {
+ DBTrigger trigger = new DBTrigger();
+ trigger.setTriggerName(triggerName);
+
+ // 查询触发器基本信息
+ String sql = "SELECT " +
+ " t.tgname AS trigger_name, " +
+ " c.relname AS table_name, " +
+ " n.nspname AS schema_name, " +
+ " pg_get_triggerdef(t.oid) AS trigger_ddl, " +
+ " t.tgenabled AS enabled, " +
+ " pg_catalog.obj_description(t.oid, 'pg_trigger') AS comment " +
+ "FROM pg_catalog.pg_trigger t " +
+ "INNER JOIN pg_catalog.pg_class c ON t.tgrelid = c.oid " +
+ "INNER JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid " +
+ "WHERE n.nspname = ? AND t.tgname = ? " +
+ " AND NOT t.tgisinternal";
- @Override
- public DBTrigger getTrigger(String schemaName, String packageName) {
- throw new UnsupportedOperationException("Not supported yet");
+ try {
+ jdbcOperations.query(sql, new Object[] {schemaName, triggerName}, rs -> {
+ trigger.setSchemaName(rs.getString("schema_name"));
+ trigger.setSchemaMode(rs.getString("table_name"));
+ trigger.setTableName(rs.getString("table_name"));
+
+ String ddl = rs.getString("trigger_ddl");
+ trigger.setDdl(ddl);
+
+ // tgenabled: 'O' = enabled, 'D' = disabled, 'R' = replica
+ char enabled = rs.getString("enabled") != null ? rs.getString("enabled").charAt(0) : 'O';
+ if (enabled == 'O') {
+ trigger.setEnable(true);
+ trigger.setStatus("ENABLED");
+ } else {
+ trigger.setEnable(false);
+ trigger.setStatus("DISABLED");
+ }
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return null;
+ }
+ throw e;
+ }
+
+ return trigger;
}
+ /**
+ * 获取类型详情
+ *
+ * @param schemaName schema 名称
+ * @param typeName 类型名
+ * @return 类型详情
+ */
@Override
public DBType getType(String schemaName, String typeName) {
- throw new UnsupportedOperationException("Not supported yet");
+ DBType type = new DBType();
+ type.setTypeName(typeName);
+ type.setOwner(schemaName);
+
+ // 查询类型基本信息
+ String sql = "SELECT " +
+ " t.typname AS type_name, " +
+ " n.nspname AS schema_name, " +
+ " t.typtype AS type_kind, " +
+ " pg_catalog.format_type(t.oid, NULL) AS formatted_type " +
+ "FROM pg_catalog.pg_type t " +
+ "INNER JOIN pg_catalog.pg_namespace n ON t.typnamespace = n.oid " +
+ "WHERE n.nspname = ? AND t.typname = ?";
+
+ try {
+ jdbcOperations.query(sql, new Object[] {schemaName, typeName}, rs -> {
+ String typeKind = rs.getString("type_kind");
+ if ("c".equals(typeKind)) {
+ type.setType("COMPOSITE");
+ } else if ("e".equals(typeKind)) {
+ type.setType("ENUM");
+ } else if ("d".equals(typeKind)) {
+ type.setType("DOMAIN");
+ } else {
+ type.setType("OTHER");
+ }
+ type.setStatus("VALID");
+ });
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema")) {
+ return null;
+ }
+ throw e;
+ }
+
+ return type;
}
+ /**
+ * 获取序列详情
+ *
+ * PostgreSQL 使用 pg_sequences 或查询序列值获取序列信息
+ *
+ *
+ * @param schemaName schema 名称
+ * @param sequenceName 序列名
+ * @return 序列详情
+ */
@Override
public DBSequence getSequence(String schemaName, String sequenceName) {
- throw new UnsupportedOperationException("Not supported yet");
+ DBSequence sequence = new DBSequence();
+ sequence.setName(sequenceName);
+ sequence.setUser(schemaName);
+
+ // 查询序列属性(PG 10+ 使用 pg_sequence)
+ // 使用兼容性更好的方式查询序列信息
+ String sql = "SELECT " +
+ " s.relname AS sequence_name, " +
+ " n.nspname AS schema_name " +
+ "FROM pg_catalog.pg_class s " +
+ "INNER JOIN pg_catalog.pg_namespace n ON s.relnamespace = n.oid " +
+ "WHERE n.nspname = ? AND s.relname = ? AND s.relkind = 'S'";
+
+ try {
+ jdbcOperations.query(sql, new Object[] {schemaName, sequenceName}, rs -> {
+ // 序列存在
+ });
+
+ // 查询序列当前值和属性
+ try {
+ String valueSql = "SELECT last_value, is_called FROM \"" + schemaName + "\".\"" + sequenceName + "\"";
+ jdbcOperations.query(valueSql, rs -> {
+ if (rs.next()) {
+ Long lastValue = rs.getLong("last_value");
+ boolean isCalled = rs.getBoolean("is_called");
+ if (isCalled) {
+ sequence.setNextCacheValue(String.valueOf(lastValue));
+ } else {
+ sequence.setStartValue(String.valueOf(lastValue));
+ }
+ }
+ });
+ } catch (Exception e) {
+ log.debug("Failed to get sequence current value: " + e.getMessage());
+ }
+
+ // 构建 DDL
+ StringBuilder ddl = new StringBuilder();
+ ddl.append("CREATE SEQUENCE IF NOT EXISTS \"").append(schemaName).append("\".\"").append(sequenceName)
+ .append("\";\n");
+ ddl.append("ALTER SEQUENCE \"").append(schemaName).append("\".\"").append(sequenceName).append("\"");
+ ddl.append(" OWNED BY NONE;");
+ sequence.setDdl(ddl.toString());
+
+ } catch (BadSqlGrammarException e) {
+ if (StringUtils.containsIgnoreCase(e.getMessage(), "Unknown schema") ||
+ StringUtils.containsIgnoreCase(e.getMessage(), "relation")) {
+ return null;
+ }
+ throw e;
+ }
+
+ return sequence;
}
+ /**
+ * 获取同义词详情
+ *
+ * PostgreSQL 不原生支持同义词,返回 null
+ *
+ */
@Override
- public DBSynonym getSynonym(String schemaName, String synonymName,
- DBSynonymType synonymType) {
- throw new UnsupportedOperationException("Not supported yet");
+ public DBSynonym getSynonym(String schemaName, String synonymName, DBSynonymType synonymType) {
+ // PostgreSQL 不原生支持同义词,返回 null
+ return null;
}
+ /**
+ * 批量获取表详情
+ */
@Override
- public Map getTables(String schemaName,
- List tableNames) {
+ public Map getTables(String schemaName, List tableNames) {
+ // 暂不实现,留作后续扩展
throw new UnsupportedOperationException("Not supported yet");
}
}
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/stats/DBStatsAccessorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/stats/DBStatsAccessorFactory.java
index 7be619e696..3714c5053a 100644
--- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/stats/DBStatsAccessorFactory.java
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/stats/DBStatsAccessorFactory.java
@@ -33,6 +33,7 @@
import com.oceanbase.tools.dbbrowser.stats.oracle.OBOracleNoLessThan2270StatsAccessor;
import com.oceanbase.tools.dbbrowser.stats.oracle.OBOracleNoLessThan400StatsAccessor;
import com.oceanbase.tools.dbbrowser.stats.oracle.OracleStatsAccessor;
+import com.oceanbase.tools.dbbrowser.stats.postgres.PostgresStatsAccessor;
import com.oceanbase.tools.dbbrowser.stats.sqlserver.SqlServerStatsAccessor;
import com.oceanbase.tools.dbbrowser.util.VersionUtils;
@@ -109,7 +110,7 @@ public DBStatsAccessor buildForOdpSharding() {
@Override
public DBStatsAccessor buildForPostgres() {
- throw new UnsupportedOperationException("Not supported yet");
+ return new PostgresStatsAccessor(getJdbcOperations());
}
@Override
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/stats/postgres/PostgresStatsAccessor.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/stats/postgres/PostgresStatsAccessor.java
new file mode 100644
index 0000000000..f023a65c26
--- /dev/null
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/stats/postgres/PostgresStatsAccessor.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.stats.postgres;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.jdbc.core.JdbcOperations;
+import org.springframework.jdbc.core.RowMapper;
+
+import com.oceanbase.tools.dbbrowser.model.DBSession;
+import com.oceanbase.tools.dbbrowser.model.DBSession.DBTransState;
+import com.oceanbase.tools.dbbrowser.model.DBTableStats;
+import com.oceanbase.tools.dbbrowser.stats.DBStatsAccessor;
+import com.oceanbase.tools.dbbrowser.util.PostgresSqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.SqlBuilder;
+
+import lombok.NonNull;
+
+/**
+ * PostgreSQL implementation of {@link DBStatsAccessor}.
+ *
+ *
+ * Provides table statistics and session information using PostgreSQL system catalogs:
+ *
+ * Table stats: {@code pg_stat_user_tables}, {@code pg_total_relation_size()}
+ * Sessions: {@code pg_stat_activity}
+ *
+ *
+ *
+ * @author odc
+ * @since ODC_release_4.3.4
+ */
+public class PostgresStatsAccessor implements DBStatsAccessor {
+
+ protected final JdbcOperations jdbcOperations;
+
+ /**
+ * Query for listing all sessions from pg_stat_activity. Maps pg_stat_activity columns to DBSession
+ * fields.
+ */
+ private static final String QUERY_ALL_SESSIONS =
+ "SELECT pid, usename, datname, state, query, client_addr, "
+ + "EXTRACT(EPOCH FROM (now() - query_start))::bigint AS execute_time "
+ + "FROM pg_stat_activity";
+
+ /**
+ * Query for current session using pg_backend_pid().
+ */
+ private static final String QUERY_CURRENT_SESSION =
+ QUERY_ALL_SESSIONS + " WHERE pid = pg_backend_pid()";
+
+ public PostgresStatsAccessor(@NonNull JdbcOperations jdbcOperations) {
+ this.jdbcOperations = jdbcOperations;
+ }
+
+ @Override
+ public DBTableStats getTableStats(@NonNull String schema, @NonNull String tableName) {
+ String sql = sqlBuilder()
+ .append("SELECT COALESCE(s.n_live_tup, c.reltuples::bigint) AS row_count, ")
+ .append("pg_total_relation_size(c.oid) AS data_size_in_bytes ")
+ .append("FROM pg_class c ")
+ .append("LEFT JOIN pg_stat_user_tables s ON c.oid = s.relid ")
+ .append("JOIN pg_namespace n ON c.relnamespace = n.oid ")
+ .append("WHERE n.nspname = ").value(schema)
+ .append(" AND c.relname = ").value(tableName)
+ .toString();
+
+ DBTableStats stats = new DBTableStats();
+ jdbcOperations.query(sql, rs -> {
+ stats.setRowCount(parseLongSafely(rs, "row_count"));
+ stats.setDataSizeInBytes(parseLongSafely(rs, "data_size_in_bytes"));
+ });
+ return stats;
+ }
+
+ @Override
+ public List listAllSessions() {
+ return jdbcOperations.query(QUERY_ALL_SESSIONS, new PostgresDBSessionRowMapper());
+ }
+
+ @Override
+ public DBSession currentSession() {
+ List sessions = jdbcOperations.query(QUERY_CURRENT_SESSION, new PostgresDBSessionRowMapper());
+ return CollectionUtils.isEmpty(sessions) ? DBSession.unknown() : sessions.get(0);
+ }
+
+ protected SqlBuilder sqlBuilder() {
+ return new PostgresSqlBuilder();
+ }
+
+ /**
+ * Parse Long value safely, handling null values.
+ */
+ private Long parseLongSafely(ResultSet rs, String columnName) throws SQLException {
+ Object value = rs.getObject(columnName);
+ if (value == null) {
+ return 0L;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).longValue();
+ }
+ try {
+ return Long.parseLong(value.toString());
+ } catch (NumberFormatException e) {
+ return 0L;
+ }
+ }
+
+ /**
+ * Custom RowMapper for PostgreSQL DBSession.
+ *
+ *
+ * Maps pg_stat_activity columns to DBSession fields:
+ *
+ * pid -> id (backend process ID)
+ * usename -> username
+ * datname -> databaseName
+ * state -> state
+ * query -> latestQueries
+ * client_addr -> host
+ * execute_time (computed) -> executeTime
+ *
+ *
+ *
+ *
+ * Transaction state mapping:
+ *
+ * idle -> IDLE
+ * active -> ACTIVE
+ * idle in transaction -> ACTIVE
+ * others -> UNKNOWN
+ *
+ *
+ */
+ private static class PostgresDBSessionRowMapper implements RowMapper {
+
+ @Override
+ public DBSession mapRow(ResultSet rs, int rowNum) throws SQLException {
+ DBSession session = new DBSession();
+ session.setId(String.valueOf(rs.getLong("pid")));
+ session.setUsername(rs.getString("usename"));
+ session.setDatabaseName(rs.getString("datname"));
+ session.setState(rs.getString("state"));
+ session.setLatestQueries(rs.getString("query"));
+
+ // Handle client_addr - PostgreSQL returns java.sql.SQLException for null
+ try {
+ Object clientAddr = rs.getObject("client_addr");
+ if (clientAddr != null) {
+ session.setHost(clientAddr.toString());
+ }
+ } catch (SQLException e) {
+ // client_addr might be null, ignore
+ session.setHost(null);
+ }
+
+ // Execute time in seconds
+ Long executeTime = (Long) rs.getObject("execute_time");
+ session.setExecuteTime(executeTime != null ? executeTime.intValue() : 0);
+
+ // Map PostgreSQL state to DBTransState
+ session.setTransState(mapTransState(session.getState()));
+
+ // PostgreSQL doesn't have these concepts
+ session.setTransId(null);
+ session.setSqlId(null);
+ session.setTraceId(null);
+ session.setActiveQueries(null);
+ session.setSvrIp(null);
+ session.setProxyHost(null);
+
+ // Command is not directly available, could use query type but set null for simplicity
+ session.setCommand(null);
+
+ return session;
+ }
+
+ /**
+ * Map PostgreSQL session state to DBTransState.
+ *
+ * @param state PostgreSQL session state from pg_stat_activity
+ * @return corresponding DBTransState
+ */
+ private DBTransState mapTransState(String state) {
+ if (state == null) {
+ return DBTransState.UNKNOWN;
+ }
+ switch (state.toLowerCase()) {
+ case "idle":
+ return DBTransState.IDLE;
+ case "active":
+ case "idle in transaction":
+ case "idle in transaction (aborted)":
+ return DBTransState.ACTIVE;
+ default:
+ return DBTransState.UNKNOWN;
+ }
+ }
+ }
+}
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBFunctionTemplateFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBFunctionTemplateFactory.java
index 5532f4b7b9..579ca09a41 100644
--- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBFunctionTemplateFactory.java
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBFunctionTemplateFactory.java
@@ -19,6 +19,7 @@
import com.oceanbase.tools.dbbrowser.model.DBFunction;
import com.oceanbase.tools.dbbrowser.template.mysql.MySQLFunctionTemplate;
import com.oceanbase.tools.dbbrowser.template.oracle.OracleFunctionTemplate;
+import com.oceanbase.tools.dbbrowser.template.postgre.PostgresFunctionTemplate;
import com.oceanbase.tools.dbbrowser.template.sqlserver.SqlServerFunctionTemplate;
public class DBFunctionTemplateFactory extends AbstractDBBrowserFactory> {
@@ -55,7 +56,7 @@ public DBObjectTemplate buildForOdpSharding() {
@Override
public DBObjectTemplate buildForPostgres() {
- throw new UnsupportedOperationException("Not supported yet");
+ return new PostgresFunctionTemplate();
}
@Override
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBProcedureTemplateFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBProcedureTemplateFactory.java
index e9d00d038f..473a7fa468 100644
--- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBProcedureTemplateFactory.java
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBProcedureTemplateFactory.java
@@ -19,6 +19,7 @@
import com.oceanbase.tools.dbbrowser.model.DBProcedure;
import com.oceanbase.tools.dbbrowser.template.mysql.MySQLProcedureTemplate;
import com.oceanbase.tools.dbbrowser.template.oracle.OracleProcedureTemplate;
+import com.oceanbase.tools.dbbrowser.template.postgre.PostgresProcedureTemplate;
import com.oceanbase.tools.dbbrowser.template.sqlserver.SqlServerProcedureTemplate;
public class DBProcedureTemplateFactory extends AbstractDBBrowserFactory> {
@@ -55,7 +56,7 @@ public DBObjectTemplate buildForOdpSharding() {
@Override
public DBObjectTemplate buildForPostgres() {
- throw new UnsupportedOperationException("Not supported yet");
+ return new PostgresProcedureTemplate();
}
@Override
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBViewTemplateFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBViewTemplateFactory.java
index 23020d5d7a..da49082657 100644
--- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBViewTemplateFactory.java
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBViewTemplateFactory.java
@@ -19,6 +19,7 @@
import com.oceanbase.tools.dbbrowser.model.DBView;
import com.oceanbase.tools.dbbrowser.template.mysql.MySQLViewTemplate;
import com.oceanbase.tools.dbbrowser.template.oracle.OracleViewTemplate;
+import com.oceanbase.tools.dbbrowser.template.postgre.PostgresViewTemplate;
import com.oceanbase.tools.dbbrowser.template.sqlserver.SqlServerViewTemplate;
public class DBViewTemplateFactory extends AbstractDBBrowserFactory> {
@@ -55,7 +56,7 @@ public DBObjectTemplate buildForOdpSharding() {
@Override
public DBObjectTemplate buildForPostgres() {
- throw new UnsupportedOperationException("Not supported yet");
+ return new PostgresViewTemplate();
}
@Override
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/postgre/PostgresFunctionTemplate.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/postgre/PostgresFunctionTemplate.java
new file mode 100644
index 0000000000..6b89ace50c
--- /dev/null
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/postgre/PostgresFunctionTemplate.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.template.postgre;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.validation.constraints.NotNull;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.Validate;
+
+import com.oceanbase.tools.dbbrowser.model.DBFunction;
+import com.oceanbase.tools.dbbrowser.model.DBPLParam;
+import com.oceanbase.tools.dbbrowser.model.DBPLParamMode;
+import com.oceanbase.tools.dbbrowser.template.DBObjectTemplate;
+import com.oceanbase.tools.dbbrowser.util.PostgresSqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.SqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.StringUtils;
+
+/**
+ * PostgreSQL function template for generating CREATE FUNCTION statements.
+ *
+ *
+ * PostgreSQL uses PL/pgSQL as the procedural language and supports dollar-quoting for function body
+ * to avoid escaping single quotes.
+ *
+ *
+ *
+ * Template output example:
+ *
+ *
+ *
+ * CREATE OR REPLACE FUNCTION "function_name" (
+ * p_param1 INTEGER
+ * )
+ * RETURNS INTEGER
+ * LANGUAGE plpgsql
+ * AS $$
+ * BEGIN
+ * RETURN p_param1;
+ * END;
+ * $$;
+ *
+ *
+ * @author odc
+ * @since ODC_release_4.3.4
+ */
+public class PostgresFunctionTemplate implements DBObjectTemplate {
+
+ @Override
+ public String generateCreateObjectTemplate(@NotNull DBFunction dbObject) {
+ Validate.notBlank(dbObject.getFunName(), "Function name can not be blank");
+
+ SqlBuilder sqlBuilder = new PostgresSqlBuilder();
+
+ // Generate CREATE OR REPLACE FUNCTION
+ sqlBuilder.append("CREATE OR REPLACE FUNCTION ").identifier(dbObject.getFunName()).append(" (");
+
+ // Generate parameters
+ List paramList = dbObject.getParams();
+ if (CollectionUtils.isNotEmpty(paramList)) {
+ String params = paramList.stream()
+ .map(p -> formatParameter(p))
+ .collect(Collectors.joining(",\n\t"));
+ sqlBuilder.append("\n\t").append(params).append("\n");
+ }
+
+ sqlBuilder.append(")").line();
+
+ // Generate RETURNS clause
+ String returnType = dbObject.getReturnType();
+ if (StringUtils.isBlank(returnType)) {
+ returnType = "INTEGER"; // Default return type
+ }
+ sqlBuilder.append("RETURNS ").append(returnType).line();
+
+ // Generate LANGUAGE clause
+ sqlBuilder.append("LANGUAGE plpgsql").line();
+
+ // Generate function body using dollar-quoting
+ sqlBuilder.append("AS $$").line();
+ sqlBuilder.append("BEGIN").line();
+ sqlBuilder.append("\t-- Enter your function code here").line();
+
+ // Add a default return statement
+ sqlBuilder.append("\tRETURN NULL;").line();
+
+ sqlBuilder.append("END;").line();
+ sqlBuilder.append("$$;");
+
+ return sqlBuilder.toString();
+ }
+
+ /**
+ * Format a single PL/pgSQL function parameter.
+ *
+ *
+ * PostgreSQL function parameter format: [IN|OUT|INOUT] param_name data_type [DEFAULT default_value]
+ *
+ *
+ * @param param the parameter to format
+ * @return formatted parameter string
+ */
+ private String formatParameter(DBPLParam param) {
+ StringBuilder sb = new StringBuilder();
+
+ // Add parameter mode (IN/OUT/INOUT) if specified
+ DBPLParamMode paramMode = param.getParamMode();
+ if (Objects.nonNull(paramMode) && paramMode != DBPLParamMode.UNKNOWN) {
+ sb.append(paramMode.name()).append(" ");
+ }
+
+ // Add parameter name (handle special characters)
+ sb.append(StringUtils.quoteOracleIdentifier(param.getParamName()));
+
+ // Add data type
+ if (StringUtils.isNotBlank(param.getDataType())) {
+ sb.append(" ").append(param.getDataType());
+ } else {
+ sb.append(" INTEGER"); // Default type
+ }
+
+ // Add default value if specified (only for IN parameters)
+ if (StringUtils.isNotBlank(param.getDefaultValue())
+ && (paramMode == null || paramMode == DBPLParamMode.IN)) {
+ sb.append(" DEFAULT ").append(param.getDefaultValue());
+ }
+
+ return sb.toString();
+ }
+
+}
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/postgre/PostgresProcedureTemplate.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/postgre/PostgresProcedureTemplate.java
new file mode 100644
index 0000000000..819ca63181
--- /dev/null
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/postgre/PostgresProcedureTemplate.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.template.postgre;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.validation.constraints.NotNull;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.Validate;
+
+import com.oceanbase.tools.dbbrowser.model.DBPLParam;
+import com.oceanbase.tools.dbbrowser.model.DBPLParamMode;
+import com.oceanbase.tools.dbbrowser.model.DBProcedure;
+import com.oceanbase.tools.dbbrowser.template.DBObjectTemplate;
+import com.oceanbase.tools.dbbrowser.util.PostgresSqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.SqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.StringUtils;
+
+/**
+ * PostgreSQL procedure template for generating CREATE PROCEDURE statements.
+ *
+ *
+ * PostgreSQL 11+ supports stored procedures. Like functions, procedures use PL/pgSQL as the
+ * procedural language and dollar-quoting for the body.
+ *
+ *
+ *
+ * Template output example:
+ *
+ *
+ *
+ * CREATE OR REPLACE PROCEDURE "procedure_name" (
+ * p_param1 INTEGER
+ * )
+ * LANGUAGE plpgsql
+ * AS $$
+ * BEGIN
+ * -- procedure body
+ * NULL;
+ * END;
+ * $$;
+ *
+ *
+ *
+ * Note: CREATE PROCEDURE is available in PostgreSQL 11 and later versions.
+ *
+ *
+ * @author odc
+ * @since ODC_release_4.3.4
+ */
+public class PostgresProcedureTemplate implements DBObjectTemplate {
+
+ @Override
+ public String generateCreateObjectTemplate(@NotNull DBProcedure dbObject) {
+ Validate.notBlank(dbObject.getProName(), "Procedure name can not be blank");
+
+ SqlBuilder sqlBuilder = new PostgresSqlBuilder();
+
+ // Generate CREATE OR REPLACE PROCEDURE
+ sqlBuilder.append("CREATE OR REPLACE PROCEDURE ").identifier(dbObject.getProName()).append(" (");
+
+ // Generate parameters
+ List paramList = dbObject.getParams();
+ if (CollectionUtils.isNotEmpty(paramList)) {
+ String params = paramList.stream()
+ .map(p -> formatParameter(p))
+ .collect(Collectors.joining(",\n\t"));
+ sqlBuilder.append("\n\t").append(params).append("\n");
+ }
+
+ sqlBuilder.append(")").line();
+
+ // Generate LANGUAGE clause
+ sqlBuilder.append("LANGUAGE plpgsql").line();
+
+ // Generate procedure body using dollar-quoting
+ sqlBuilder.append("AS $$").line();
+ sqlBuilder.append("BEGIN").line();
+ sqlBuilder.append("\t-- Enter your procedure code here").line();
+ sqlBuilder.append("\tNULL;").line();
+ sqlBuilder.append("END;").line();
+ sqlBuilder.append("$$;");
+
+ return sqlBuilder.toString();
+ }
+
+ /**
+ * Format a single PL/pgSQL procedure parameter.
+ *
+ *
+ * PostgreSQL procedure parameter format: [IN|OUT|INOUT] param_name data_type [DEFAULT
+ * default_value]
+ *
+ *
+ *
+ * Note: Unlike functions, procedures support OUT parameters but they work differently - they are
+ * assigned values within the procedure body.
+ *
+ *
+ * @param param the parameter to format
+ * @return formatted parameter string
+ */
+ private String formatParameter(DBPLParam param) {
+ StringBuilder sb = new StringBuilder();
+
+ // Add parameter mode (IN/OUT/INOUT) if specified
+ DBPLParamMode paramMode = param.getParamMode();
+ if (Objects.nonNull(paramMode) && paramMode != DBPLParamMode.UNKNOWN) {
+ sb.append(paramMode.name()).append(" ");
+ }
+
+ // Add parameter name (handle special characters)
+ sb.append(StringUtils.quoteOracleIdentifier(param.getParamName()));
+
+ // Add data type
+ if (StringUtils.isNotBlank(param.getDataType())) {
+ sb.append(" ").append(param.getDataType());
+ } else {
+ sb.append(" INTEGER"); // Default type
+ }
+
+ // Add default value if specified (only for IN parameters)
+ if (StringUtils.isNotBlank(param.getDefaultValue())
+ && (paramMode == null || paramMode == DBPLParamMode.IN)) {
+ sb.append(" DEFAULT ").append(param.getDefaultValue());
+ }
+
+ return sb.toString();
+ }
+
+}
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/postgre/PostgresViewTemplate.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/postgre/PostgresViewTemplate.java
new file mode 100644
index 0000000000..c45c19dab8
--- /dev/null
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/postgre/PostgresViewTemplate.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.template.postgre;
+
+import javax.validation.constraints.NotNull;
+
+import org.apache.commons.lang3.Validate;
+
+import com.oceanbase.tools.dbbrowser.model.DBView;
+import com.oceanbase.tools.dbbrowser.template.BaseViewTemplate;
+import com.oceanbase.tools.dbbrowser.util.PostgresSqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.SqlBuilder;
+import com.oceanbase.tools.dbbrowser.util.StringUtils;
+
+/**
+ * PostgreSQL view template for generating CREATE VIEW statements.
+ *
+ *
+ * PostgreSQL uses double quotes for identifiers and supports CREATE OR REPLACE VIEW syntax. Unlike
+ * SQL Server, PostgreSQL does not require USE statement before CREATE VIEW.
+ *
+ *
+ *
+ * Template output example:
+ *
+ *
+ *
+ * CREATE OR REPLACE VIEW "schema_name"."view_name" AS
+ * SELECT column1, column2
+ * FROM "schema_name"."table_name"
+ * WHERE condition;
+ *
+ *
+ * @author odc
+ * @since ODC_release_4.3.4
+ */
+public class PostgresViewTemplate extends BaseViewTemplate {
+
+ @Override
+ protected String preHandle(String str) {
+ return str.toLowerCase();
+ }
+
+ @Override
+ protected SqlBuilder sqlBuilder() {
+ return new PostgresSqlBuilder();
+ }
+
+ /**
+ * Override to support schema prefix for PostgreSQL.
+ *
+ *
+ * PostgreSQL view names can be qualified with schema: "schema"."view_name"
+ *
+ */
+ @Override
+ public String generateCreateObjectTemplate(@NotNull DBView dbObject) {
+ Validate.notBlank(dbObject.getViewName(), "View name can not be blank");
+ validOperations(dbObject);
+
+ SqlBuilder sqlBuilder = sqlBuilder();
+
+ // Generate CREATE OR REPLACE VIEW with optional schema prefix
+ sqlBuilder.append(preHandle("create or replace view "));
+
+ if (StringUtils.isNotBlank(dbObject.getSchemaName())) {
+ sqlBuilder.identifier(dbObject.getSchemaName()).append(".");
+ }
+
+ sqlBuilder.identifier(dbObject.getViewName())
+ .append(preHandle(" as"));
+
+ // Generate query statement using base class logic
+ generateQueryStatement(dbObject, sqlBuilder);
+
+ return sqlBuilder.toString();
+ }
+
+ @Override
+ protected String doGenerateCreateObjectTemplate(SqlBuilder sqlBuilder, DBView dbObject) {
+ return sqlBuilder.toString();
+ }
+
+}
diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/util/PostgresSqlBuilder.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/util/PostgresSqlBuilder.java
new file mode 100644
index 0000000000..2eec2a1690
--- /dev/null
+++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/util/PostgresSqlBuilder.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.util;
+
+/**
+ * PostgreSQL SQL builder.
+ *
+ *
+ * PostgreSQL uses double quotes for identifiers and single quotes for string values. The escaping
+ * rules are:
+ *
+ * Identifiers: use double quotes, internal double quotes are escaped by doubling: "column"
+ * Values: use single quotes, internal single quotes are escaped by doubling: 'value'
+ *
+ *
+ *
+ *
+ * Note: PostgreSQL identifier quoting is similar to Oracle (both use double quotes), but the LIKE
+ * clause escaping behavior differs. Oracle requires explicit ESCAPE clause, while PostgreSQL treats
+ * backslash as a literal character by default in LIKE patterns (unless standard_conforming_strings
+ * is off).
+ *
+ *
+ * @author odc
+ */
+public class PostgresSqlBuilder extends SqlBuilder {
+
+ public PostgresSqlBuilder() {
+ super();
+ }
+
+ /**
+ * Append identifier with PostgreSQL quoting rules.
+ *
+ *
+ * PostgreSQL uses double quotes for identifiers. Internal double quotes are escaped by doubling
+ * them.
+ *
+ *
+ * @param identifier the identifier to quote
+ * @return this SqlBuilder
+ */
+ @Override
+ public SqlBuilder identifier(String identifier) {
+ if (StringUtils.isBlank(identifier)) {
+ return this;
+ }
+ // PostgreSQL uses double quotes for identifiers, same as Oracle
+ return append(StringUtils.quoteOracleIdentifier(identifier));
+ }
+
+ /**
+ * Append value with PostgreSQL quoting rules.
+ *
+ *
+ * PostgreSQL uses single quotes for string values. Internal single quotes are escaped by doubling
+ * them.
+ *
+ *
+ * @param value the value to quote
+ * @return this SqlBuilder
+ */
+ @Override
+ public SqlBuilder value(String value) {
+ if (value == null) {
+ return append("NULL");
+ }
+ // PostgreSQL uses single quotes for values, same as Oracle
+ return append(StringUtils.quoteOracleValue(value));
+ }
+
+ /**
+ * Append default value.
+ *
+ *
+ * Default values in PostgreSQL are typically function calls or literals, so they are appended
+ * as-is.
+ *
+ *
+ * @param value the default value
+ * @return this SqlBuilder
+ */
+ @Override
+ public SqlBuilder defaultValue(String value) {
+ return append(value);
+ }
+
+ /**
+ * Append LIKE clause.
+ *
+ *
+ * PostgreSQL LIKE clause uses backslash for escaping by default (when standard_conforming_strings
+ * is off) or can use the ESCAPE clause explicitly. For simplicity, we use the base implementation
+ * without explicit ESCAPE clause, which differs from Oracle that always appends ESCAPE '\'.
+ *
+ *
+ * @param fieldKey the field name
+ * @param fieldLikeValue the like pattern value
+ * @return this SqlBuilder
+ */
+ @Override
+ public SqlBuilder like(String fieldKey, String fieldLikeValue) {
+ // Use base implementation without explicit ESCAPE clause
+ // PostgreSQL's LIKE behavior depends on standard_conforming_strings setting
+ return super.like(fieldKey, fieldLikeValue);
+ }
+}
diff --git a/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresColumnEditorTest.java b/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresColumnEditorTest.java
new file mode 100644
index 0000000000..2f3dc96d77
--- /dev/null
+++ b/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresColumnEditorTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.editor.postgre;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.oceanbase.tools.dbbrowser.model.DBTableColumn;
+
+/**
+ * PostgreSQL 列编辑器测试类
+ *
+ *
+ * 测试覆盖以下场景:
+ *
+ * 添加列(CREATE)
+ * 删除列(DROP)
+ * 重命名列(RENAME)
+ * 修改列类型
+ * 修改列默认值
+ * 修改列 NULL/NOT NULL
+ * 修改列注释
+ *
+ *
+ */
+public class PostgresColumnEditorTest {
+
+ private PostgresColumnEditor editor;
+
+ @Before
+ public void setUp() {
+ editor = new PostgresColumnEditor();
+ }
+
+ // ==================== 添加列测试 ====================
+
+ @Test
+ public void testGenerateCreateObjectDDL_basicColumn() {
+ DBTableColumn column = createColumn("public", "users", "name", "varchar", 100L, null, true, null, null);
+ String ddl = editor.generateCreateObjectDDL(column);
+
+ Assert.assertTrue(ddl.contains("ALTER TABLE \"public\".\"users\""));
+ Assert.assertTrue(ddl.contains("ADD COLUMN \"name\""));
+ Assert.assertTrue(ddl.contains("varchar(100)"));
+ Assert.assertTrue(ddl.contains("NULL"));
+ }
+
+ @Test
+ public void testGenerateCreateObjectDDL_notNullWithDefault() {
+ DBTableColumn column = createColumn("public", "users", "age", "integer", null, null, false, "0", null);
+ String ddl = editor.generateCreateObjectDDL(column);
+
+ Assert.assertTrue(ddl.contains("NOT NULL"));
+ Assert.assertTrue(ddl.contains("DEFAULT 0"));
+ }
+
+ @Test
+ public void testGenerateCreateObjectDDL_withComment() {
+ DBTableColumn column = createColumn("public", "users", "email", "varchar", 255L, null, true, null, "用户邮箱");
+ String ddl = editor.generateCreateObjectDDL(column);
+
+ Assert.assertTrue(ddl.contains("COMMENT ON COLUMN"));
+ Assert.assertTrue(ddl.contains("\"public\".\"users\".\"email\""));
+ Assert.assertTrue(ddl.contains("IS '用户邮箱'"));
+ }
+
+ @Test
+ public void testGenerateCreateObjectDDL_numericWithScale() {
+ DBTableColumn column = createColumn("public", "products", "price", "numeric", 10L, 2, false, "0.00", null);
+ String ddl = editor.generateCreateObjectDDL(column);
+
+ Assert.assertTrue(ddl.contains("numeric(10,2)"));
+ }
+
+ // ==================== 删除列测试 ====================
+
+ @Test
+ public void testGenerateDropObjectDDL_basic() {
+ DBTableColumn column = createColumn("public", "users", "old_column", "varchar", 100L, null, true, null, null);
+ String ddl = editor.generateDropObjectDDL(column);
+
+ Assert.assertEquals("ALTER TABLE \"public\".\"users\" DROP COLUMN \"old_column\";\n", ddl);
+ }
+
+ // ==================== 重命名列测试 ====================
+
+ @Test
+ public void testGenerateRenameObjectDDL_basic() {
+ DBTableColumn oldColumn = createColumn("public", "users", "old_name", "varchar", 100L, null, true, null, null);
+ DBTableColumn newColumn = createColumn("public", "users", "new_name", "varchar", 100L, null, true, null, null);
+ String ddl = editor.generateRenameObjectDDL(oldColumn, newColumn);
+
+ Assert.assertTrue(ddl.contains("ALTER TABLE \"public\".\"users\""));
+ Assert.assertTrue(ddl.contains("RENAME COLUMN \"old_name\" TO \"new_name\""));
+ }
+
+ // ==================== 修改列测试 ====================
+
+ @Test
+ public void testGenerateUpdateObjectDDL_changeType() {
+ DBTableColumn oldColumn = createColumn("public", "users", "age", "integer", null, null, true, null, null);
+ DBTableColumn newColumn = createColumn("public", "users", "age", "bigint", null, null, true, null, null);
+ String ddl = editor.generateUpdateObjectDDL(oldColumn, newColumn);
+
+ Assert.assertTrue(ddl.contains("ALTER COLUMN \"age\" TYPE bigint"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_changeTypeWithPrecision() {
+ DBTableColumn oldColumn = createColumn("public", "users", "name", "varchar", 50L, null, true, null, null);
+ DBTableColumn newColumn = createColumn("public", "users", "name", "varchar", 100L, null, true, null, null);
+ String ddl = editor.generateUpdateObjectDDL(oldColumn, newColumn);
+
+ Assert.assertTrue(ddl.contains("ALTER COLUMN \"name\" TYPE varchar(100)"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_setDefaultValue() {
+ DBTableColumn oldColumn = createColumn("public", "users", "status", "varchar", 20L, null, true, null, null);
+ DBTableColumn newColumn =
+ createColumn("public", "users", "status", "varchar", 20L, null, true, "'active'", null);
+ String ddl = editor.generateUpdateObjectDDL(oldColumn, newColumn);
+
+ Assert.assertTrue(ddl.contains("SET DEFAULT 'active'"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_dropDefaultValue() {
+ DBTableColumn oldColumn =
+ createColumn("public", "users", "status", "varchar", 20L, null, true, "'active'", null);
+ DBTableColumn newColumn = createColumn("public", "users", "status", "varchar", 20L, null, true, null, null);
+ String ddl = editor.generateUpdateObjectDDL(oldColumn, newColumn);
+
+ Assert.assertTrue(ddl.contains("DROP DEFAULT"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_setNotNull() {
+ DBTableColumn oldColumn = createColumn("public", "users", "email", "varchar", 255L, null, true, null, null);
+ DBTableColumn newColumn = createColumn("public", "users", "email", "varchar", 255L, null, false, null, null);
+ String ddl = editor.generateUpdateObjectDDL(oldColumn, newColumn);
+
+ Assert.assertTrue(ddl.contains("SET NOT NULL"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_dropNotNull() {
+ DBTableColumn oldColumn = createColumn("public", "users", "email", "varchar", 255L, null, false, null, null);
+ DBTableColumn newColumn = createColumn("public", "users", "email", "varchar", 255L, null, true, null, null);
+ String ddl = editor.generateUpdateObjectDDL(oldColumn, newColumn);
+
+ Assert.assertTrue(ddl.contains("DROP NOT NULL"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_changeComment() {
+ DBTableColumn oldColumn = createColumn("public", "users", "name", "varchar", 100L, null, true, null, "旧注释");
+ DBTableColumn newColumn = createColumn("public", "users", "name", "varchar", 100L, null, true, null, "新注释");
+ String ddl = editor.generateUpdateObjectDDL(oldColumn, newColumn);
+
+ Assert.assertTrue(ddl.contains("COMMENT ON COLUMN"));
+ Assert.assertTrue(ddl.contains("IS '新注释'"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_multipleChanges() {
+ // 修改类型 + 设置默认值 + 设置 NOT NULL + 修改注释
+ DBTableColumn oldColumn = createColumn("public", "users", "age", "integer", null, null, true, null, "年龄");
+ DBTableColumn newColumn = createColumn("public", "users", "age", "bigint", null, null, false, "0", "用户年龄");
+ String ddl = editor.generateUpdateObjectDDL(oldColumn, newColumn);
+
+ Assert.assertTrue("Should change type", ddl.contains("ALTER COLUMN \"age\" TYPE bigint"));
+ Assert.assertTrue("Should set default", ddl.contains("SET DEFAULT 0"));
+ Assert.assertTrue("Should set not null", ddl.contains("SET NOT NULL"));
+ Assert.assertTrue("Should update comment", ddl.contains("IS '用户年龄'"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_renameOnly() {
+ DBTableColumn oldColumn = createColumn("public", "users", "old_name", "varchar", 100L, null, true, null, null);
+ DBTableColumn newColumn = createColumn("public", "users", "new_name", "varchar", 100L, null, true, null, null);
+ String ddl = editor.generateUpdateObjectDDL(oldColumn, newColumn);
+
+ Assert.assertTrue(ddl.contains("RENAME COLUMN \"old_name\" TO \"new_name\""));
+ // 不应该包含 ALTER COLUMN TYPE
+ Assert.assertFalse(ddl.contains("ALTER COLUMN \"new_name\" TYPE"));
+ }
+
+ // ==================== 标识符转义测试 ====================
+
+ @Test
+ public void testIdentifierWithSpecialChars() {
+ // 列名包含特殊字符(需要转义)
+ DBTableColumn column =
+ createColumn("my_schema", "my_table", "col\"umn", "varchar", 50L, null, true, null, null);
+ String ddl = editor.generateCreateObjectDDL(column);
+
+ // 验证双引号被正确转义
+ Assert.assertTrue(ddl.contains("\"col\"\"umn\""));
+ }
+
+ @Test
+ public void testSchemaNameWithSpecialChars() {
+ DBTableColumn column = createColumn("my\"schema", "users", "name", "varchar", 100L, null, true, null, null);
+ String ddl = editor.generateCreateObjectDDL(column);
+
+ Assert.assertTrue(ddl.contains("\"my\"\"schema\""));
+ }
+
+ // ==================== 空值处理测试 ====================
+
+ @Test
+ public void testGenerateCreateObjectDDL_nullComment() {
+ DBTableColumn column = createColumn("public", "users", "name", "varchar", 100L, null, true, null, null);
+ String ddl = editor.generateCreateObjectDDL(column);
+
+ // 不应该包含 COMMENT ON COLUMN
+ Assert.assertFalse(ddl.contains("COMMENT ON COLUMN"));
+ }
+
+ // ==================== 辅助方法 ====================
+
+ private DBTableColumn createColumn(String schemaName, String tableName, String name,
+ String typeName, Long precision, Integer scale, Boolean nullable,
+ String defaultValue, String comment) {
+ DBTableColumn column = new DBTableColumn();
+ column.setSchemaName(schemaName);
+ column.setTableName(tableName);
+ column.setName(name);
+ column.setTypeName(typeName);
+ column.setPrecision(precision);
+ column.setScale(scale);
+ column.setNullable(nullable);
+ column.setDefaultValue(defaultValue);
+ column.setComment(comment);
+ return column;
+ }
+}
diff --git a/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresConstraintEditorTest.java b/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresConstraintEditorTest.java
new file mode 100644
index 0000000000..b4a78ccfe0
--- /dev/null
+++ b/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresConstraintEditorTest.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.editor.postgre;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.oceanbase.tools.dbbrowser.model.DBConstraintType;
+import com.oceanbase.tools.dbbrowser.model.DBForeignKeyModifyRule;
+import com.oceanbase.tools.dbbrowser.model.DBTableConstraint;
+
+/**
+ * PostgreSQL 约束编辑器测试类
+ *
+ *
+ * 测试覆盖以下场景:
+ *
+ * 创建主键约束
+ * 创建唯一约束
+ * 创建外键约束
+ * 创建检查约束
+ * 删除约束
+ * 重命名约束
+ * 修改约束
+ *
+ *
+ */
+public class PostgresConstraintEditorTest {
+
+ private PostgresConstraintEditor editor;
+
+ @Before
+ public void setUp() {
+ editor = new PostgresConstraintEditor();
+ }
+
+ // ==================== 创建约束测试 ====================
+
+ @Test
+ public void testGenerateCreateObjectDDL_primaryKey() {
+ DBTableConstraint constraint = createConstraint("public", "users", "pk_users", "id",
+ DBConstraintType.PRIMARY_KEY, null, null, null, null, null, null);
+ String ddl = editor.generateCreateObjectDDL(constraint);
+
+ Assert.assertTrue(ddl.contains("ALTER TABLE \"public\".\"users\""));
+ Assert.assertTrue(ddl.contains("ADD CONSTRAINT \"pk_users\""));
+ Assert.assertTrue(ddl.contains("PRIMARY KEY"));
+ Assert.assertTrue(ddl.contains("(\"id\")"));
+ }
+
+ @Test
+ public void testGenerateCreateObjectDDL_compositePrimaryKey() {
+ DBTableConstraint constraint = createConstraint("public", "order_items", "pk_order_items",
+ null, DBConstraintType.PRIMARY_KEY, null, null, null, null, null, null);
+ constraint.setColumnNames(Arrays.asList("order_id", "item_id"));
+
+ String ddl = editor.generateCreateObjectDDL(constraint);
+
+ Assert.assertTrue(ddl.contains("PRIMARY KEY"));
+ Assert.assertTrue(ddl.contains("(\"order_id\", \"item_id\")"));
+ }
+
+ @Test
+ public void testGenerateCreateObjectDDL_uniqueConstraint() {
+ DBTableConstraint constraint = createConstraint("public", "users", "uk_users_email", "email",
+ DBConstraintType.UNIQUE, null, null, null, null, null, null);
+ String ddl = editor.generateCreateObjectDDL(constraint);
+
+ Assert.assertTrue(ddl.contains("ADD CONSTRAINT \"uk_users_email\""));
+ Assert.assertTrue(ddl.contains("UNIQUE"));
+ Assert.assertTrue(ddl.contains("(\"email\")"));
+ }
+
+ @Test
+ public void testGenerateCreateObjectDDL_foreignKey() {
+ DBTableConstraint constraint = createConstraint("public", "orders", "fk_orders_user",
+ "user_id", DBConstraintType.FOREIGN_KEY, "public", "users", "id",
+ null, null, null);
+ String ddl = editor.generateCreateObjectDDL(constraint);
+
+ Assert.assertTrue(ddl.contains("FOREIGN KEY"));
+ Assert.assertTrue(ddl.contains("(\"user_id\")"));
+ Assert.assertTrue(ddl.contains("REFERENCES \"public\".\"users\""));
+ Assert.assertTrue(ddl.contains("(\"id\")"));
+ }
+
+ @Test
+ public void testGenerateCreateObjectDDL_foreignKeyWithCascade() {
+ DBTableConstraint constraint = createConstraint("public", "orders", "fk_orders_user",
+ "user_id", DBConstraintType.FOREIGN_KEY, "public", "users", "id",
+ DBForeignKeyModifyRule.CASCADE, null, null);
+ String ddl = editor.generateCreateObjectDDL(constraint);
+
+ Assert.assertTrue(ddl.contains("ON DELETE CASCADE"));
+ }
+
+ @Test
+ public void testGenerateCreateObjectDDL_checkConstraint() {
+ DBTableConstraint constraint = createConstraint("public", "products", "ck_products_price",
+ null, DBConstraintType.CHECK, null, null, null, null, null, "price > 0");
+ String ddl = editor.generateCreateObjectDDL(constraint);
+
+ Assert.assertTrue(ddl.contains("ADD CONSTRAINT \"ck_products_price\""));
+ Assert.assertTrue(ddl.contains("CHECK"));
+ Assert.assertTrue(ddl.contains("(price > 0)"));
+ }
+
+ // ==================== 删除约束测试 ====================
+
+ @Test
+ public void testGenerateDropObjectDDL_basic() {
+ DBTableConstraint constraint = createConstraint("public", "users", "pk_users", "id",
+ DBConstraintType.PRIMARY_KEY, null, null, null, null, null, null);
+ String ddl = editor.generateDropObjectDDL(constraint);
+
+ Assert.assertTrue(ddl.contains("ALTER TABLE \"public\".\"users\""));
+ Assert.assertTrue(ddl.contains("DROP CONSTRAINT \"pk_users\""));
+ }
+
+ // ==================== 重命名约束测试 ====================
+
+ @Test
+ public void testGenerateRenameObjectDDL_basic() {
+ DBTableConstraint oldConstraint = createConstraint("public", "users", "old_name", "id",
+ DBConstraintType.PRIMARY_KEY, null, null, null, null, null, null);
+ DBTableConstraint newConstraint = createConstraint("public", "users", "new_name", "id",
+ DBConstraintType.PRIMARY_KEY, null, null, null, null, null, null);
+ String ddl = editor.generateRenameObjectDDL(oldConstraint, newConstraint);
+
+ Assert.assertTrue(ddl.contains("ALTER TABLE \"public\".\"users\""));
+ Assert.assertTrue(ddl.contains("RENAME CONSTRAINT \"old_name\" TO \"new_name\""));
+ }
+
+ // ==================== 修改约束测试 ====================
+
+ @Test
+ public void testGenerateUpdateObjectDDL_renameOnly() {
+ DBTableConstraint oldConstraint = createConstraint("public", "users", "old_name", "id",
+ DBConstraintType.PRIMARY_KEY, null, null, null, null, null, null);
+ DBTableConstraint newConstraint = createConstraint("public", "users", "new_name", "id",
+ DBConstraintType.PRIMARY_KEY, null, null, null, null, null, null);
+ String ddl = editor.generateUpdateObjectDDL(oldConstraint, newConstraint);
+
+ // 仅重命名,不应该包含 DROP + CREATE
+ Assert.assertTrue(ddl.contains("RENAME CONSTRAINT"));
+ Assert.assertFalse(ddl.contains("DROP CONSTRAINT"));
+ Assert.assertFalse(ddl.contains("ADD CONSTRAINT"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_changeColumns() {
+ DBTableConstraint oldConstraint = createConstraint("public", "users", "uk_users", "email",
+ DBConstraintType.UNIQUE, null, null, null, null, null, null);
+ DBTableConstraint newConstraint = createConstraint("public", "users", "uk_users", "username",
+ DBConstraintType.UNIQUE, null, null, null, null, null, null);
+ String ddl = editor.generateUpdateObjectDDL(oldConstraint, newConstraint);
+
+ // 结构性变更需要 DROP + CREATE
+ Assert.assertTrue(ddl.contains("DROP CONSTRAINT"));
+ Assert.assertTrue(ddl.contains("ADD CONSTRAINT"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_changeType() {
+ DBTableConstraint oldConstraint = createConstraint("public", "users", "constr", "col",
+ DBConstraintType.UNIQUE, null, null, null, null, null, null);
+ DBTableConstraint newConstraint = createConstraint("public", "users", "constr", "col",
+ DBConstraintType.PRIMARY_KEY, null, null, null, null, null, null);
+ String ddl = editor.generateUpdateObjectDDL(oldConstraint, newConstraint);
+
+ // 约束类型变更需要 DROP + CREATE
+ Assert.assertTrue(ddl.contains("DROP CONSTRAINT"));
+ Assert.assertTrue(ddl.contains("ADD CONSTRAINT"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_changeForeignKeyReference() {
+ DBTableConstraint oldConstraint = createConstraint("public", "orders", "fk_orders",
+ "user_id", DBConstraintType.FOREIGN_KEY, "public", "users", "id",
+ null, null, null);
+ DBTableConstraint newConstraint = createConstraint("public", "orders", "fk_orders",
+ "user_id", DBConstraintType.FOREIGN_KEY, "public", "customers", "id",
+ null, null, null);
+ String ddl = editor.generateUpdateObjectDDL(oldConstraint, newConstraint);
+
+ // 外键引用变更需要 DROP + CREATE
+ Assert.assertTrue(ddl.contains("DROP CONSTRAINT"));
+ Assert.assertTrue(ddl.contains("REFERENCES \"public\".\"customers\""));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_changeCheckClause() {
+ DBTableConstraint oldConstraint = createConstraint("public", "products", "ck_price",
+ null, DBConstraintType.CHECK, null, null, null, null, null, "price > 0");
+ DBTableConstraint newConstraint = createConstraint("public", "products", "ck_price",
+ null, DBConstraintType.CHECK, null, null, null, null, null, "price >= 0");
+ String ddl = editor.generateUpdateObjectDDL(oldConstraint, newConstraint);
+
+ // CHECK 子句变更需要 DROP + CREATE
+ Assert.assertTrue(ddl.contains("DROP CONSTRAINT"));
+ Assert.assertTrue(ddl.contains("CHECK (price >= 0)"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_noChange() {
+ DBTableConstraint oldConstraint = createConstraint("public", "users", "pk_users", "id",
+ DBConstraintType.PRIMARY_KEY, null, null, null, null, null, null);
+ DBTableConstraint newConstraint = createConstraint("public", "users", "pk_users", "id",
+ DBConstraintType.PRIMARY_KEY, null, null, null, null, null, null);
+ String ddl = editor.generateUpdateObjectDDL(oldConstraint, newConstraint);
+
+ // 无变化,应返回空字符串
+ Assert.assertEquals("", ddl);
+ }
+
+ // ==================== 标识符转义测试 ====================
+
+ @Test
+ public void testIdentifierWithSpecialChars() {
+ DBTableConstraint constraint = createConstraint("my_schema", "my_table", "con\"str", "col\"umn",
+ DBConstraintType.PRIMARY_KEY, null, null, null, null, null, null);
+ String ddl = editor.generateCreateObjectDDL(constraint);
+
+ // 验证双引号被正确转义
+ Assert.assertTrue(ddl.contains("\"con\"\"str\""));
+ Assert.assertTrue(ddl.contains("\"col\"\"umn\""));
+ }
+
+ // ==================== 批量操作测试 ====================
+
+ @Test
+ public void testGenerateUpdateObjectListDDL_addNewConstraint() {
+ List oldConstraints = new ArrayList<>();
+ List newConstraints = Arrays.asList(
+ createConstraint("public", "users", "pk_users", "id",
+ DBConstraintType.PRIMARY_KEY, null, null, null, null, null, null));
+
+ String ddl = editor.generateUpdateObjectListDDL(oldConstraints, newConstraints);
+
+ Assert.assertTrue(ddl.contains("ADD CONSTRAINT"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectListDDL_dropConstraint() {
+ List oldConstraints = Arrays.asList(
+ createConstraint("public", "users", "uk_users_email", "email",
+ DBConstraintType.UNIQUE, null, null, null, null, null, null));
+ List newConstraints = new ArrayList<>();
+
+ String ddl = editor.generateUpdateObjectListDDL(oldConstraints, newConstraints);
+
+ Assert.assertTrue(ddl.contains("DROP CONSTRAINT"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectListDDL_mixedOperations() {
+ // 混合操作:删除一个、修改一个、新增一个
+ DBTableConstraint toDrop = createConstraint("public", "users", "to_drop", "col1",
+ DBConstraintType.UNIQUE, null, null, null, null, null, null);
+ toDrop.setOrdinalPosition(1);
+
+ DBTableConstraint oldToModify = createConstraint("public", "users", "to_modify", "col2",
+ DBConstraintType.UNIQUE, null, null, null, null, null, null);
+ oldToModify.setOrdinalPosition(2);
+
+ DBTableConstraint newToModify = createConstraint("public", "users", "to_modify", null,
+ DBConstraintType.UNIQUE, null, null, null, null, null, null);
+ newToModify.setOrdinalPosition(2);
+ newToModify.setColumnNames(Arrays.asList("col2", "col3"));
+
+ DBTableConstraint toAdd = createConstraint("public", "users", "to_add", "col4",
+ DBConstraintType.UNIQUE, null, null, null, null, null, null);
+ // ordinalPosition 为 null 表示新增
+
+ List oldConstraints = Arrays.asList(toDrop, oldToModify);
+ List newConstraints = Arrays.asList(newToModify, toAdd);
+
+ String ddl = editor.generateUpdateObjectListDDL(oldConstraints, newConstraints);
+
+ // 验证包含所有操作
+ Assert.assertTrue("Should contain DROP CONSTRAINT", ddl.contains("DROP CONSTRAINT"));
+ Assert.assertTrue("Should contain ADD CONSTRAINT", ddl.contains("ADD CONSTRAINT"));
+ }
+
+ // ==================== 辅助方法 ====================
+
+ private DBTableConstraint createConstraint(String schemaName, String tableName, String constraintName,
+ String columnName, DBConstraintType type,
+ String refSchemaName, String refTableName, String refColumnName,
+ DBForeignKeyModifyRule onDeleteRule, DBForeignKeyModifyRule onUpdateRule,
+ String checkClause) {
+ DBTableConstraint constraint = new DBTableConstraint();
+ constraint.setSchemaName(schemaName);
+ constraint.setTableName(tableName);
+ constraint.setName(constraintName);
+ if (columnName != null) {
+ constraint.setColumnNames(Arrays.asList(columnName));
+ }
+ constraint.setType(type);
+ constraint.setReferenceSchemaName(refSchemaName);
+ constraint.setReferenceTableName(refTableName);
+ if (refColumnName != null) {
+ constraint.setReferenceColumnNames(Arrays.asList(refColumnName));
+ }
+ constraint.setOnDeleteRule(onDeleteRule);
+ constraint.setOnUpdateRule(onUpdateRule);
+ constraint.setCheckClause(checkClause);
+ return constraint;
+ }
+}
diff --git a/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresIndexEditorTest.java b/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresIndexEditorTest.java
new file mode 100644
index 0000000000..34f9f50bba
--- /dev/null
+++ b/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/editor/postgre/PostgresIndexEditorTest.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.editor.postgre;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.oceanbase.tools.dbbrowser.model.DBIndexType;
+import com.oceanbase.tools.dbbrowser.model.DBTableIndex;
+
+/**
+ * PostgreSQL 索引编辑器测试类
+ *
+ *
+ * 测试覆盖以下场景:
+ *
+ * 创建普通索引
+ * 创建唯一索引
+ * 删除索引
+ * 重命名索引
+ * 修改索引结构
+ * 批量索引操作
+ *
+ *
+ */
+public class PostgresIndexEditorTest {
+
+ private PostgresIndexEditor editor;
+
+ @Before
+ public void setUp() {
+ editor = new PostgresIndexEditor();
+ }
+
+ // ==================== 创建索引测试 ====================
+
+ @Test
+ public void testGenerateCreateObjectDDL_basicIndex() {
+ DBTableIndex index =
+ createIndex("public", "users", "idx_users_name", Arrays.asList("name"), DBIndexType.NORMAL, false);
+ String ddl = editor.generateCreateObjectDDL(index);
+
+ Assert.assertTrue(ddl.contains("CREATE INDEX \"idx_users_name\""));
+ Assert.assertTrue(ddl.contains("ON \"public\".\"users\""));
+ Assert.assertTrue(ddl.contains("USING btree"));
+ Assert.assertTrue(ddl.contains("(\"name\")"));
+ }
+
+ @Test
+ public void testGenerateCreateObjectDDL_uniqueIndex() {
+ DBTableIndex index =
+ createIndex("public", "users", "idx_users_email", Arrays.asList("email"), DBIndexType.UNIQUE, true);
+ String ddl = editor.generateCreateObjectDDL(index);
+
+ Assert.assertTrue(ddl.contains("CREATE UNIQUE INDEX"));
+ Assert.assertTrue(ddl.contains("\"idx_users_email\""));
+ }
+
+ @Test
+ public void testGenerateCreateObjectDDL_multiColumnIndex() {
+ DBTableIndex index = createIndex("public", "orders", "idx_orders_composite",
+ Arrays.asList("user_id", "created_at"), DBIndexType.NORMAL, false);
+ String ddl = editor.generateCreateObjectDDL(index);
+
+ Assert.assertTrue(ddl.contains("\"user_id\""));
+ Assert.assertTrue(ddl.contains("\"created_at\""));
+ }
+
+ @Test
+ public void testGenerateCreateObjectDDL_fulltextIndex() {
+ DBTableIndex index = createIndex("public", "articles", "idx_articles_content",
+ Arrays.asList("content"), DBIndexType.FULLTEXT, false);
+ String ddl = editor.generateCreateObjectDDL(index);
+
+ // 全文索引使用 gin
+ Assert.assertTrue(ddl.contains("USING gin"));
+ }
+
+ // ==================== 删除索引测试 ====================
+
+ @Test
+ public void testGenerateDropObjectDDL_basic() {
+ DBTableIndex index =
+ createIndex("public", "users", "idx_users_name", Arrays.asList("name"), DBIndexType.NORMAL, false);
+ String ddl = editor.generateDropObjectDDL(index);
+
+ // PostgreSQL 索引是 Schema 级别对象,使用 DROP INDEX "schema"."index_name" 格式
+ Assert.assertTrue(ddl.contains("DROP INDEX \"public\".\"idx_users_name\""));
+ // 不应该包含 ON table
+ Assert.assertFalse(ddl.contains("ON"));
+ }
+
+ // ==================== 重命名索引测试 ====================
+
+ @Test
+ public void testGenerateRenameObjectDDL_basic() {
+ DBTableIndex oldIndex =
+ createIndex("public", "users", "old_idx_name", Arrays.asList("name"), DBIndexType.NORMAL, false);
+ DBTableIndex newIndex =
+ createIndex("public", "users", "new_idx_name", Arrays.asList("name"), DBIndexType.NORMAL, false);
+ String ddl = editor.generateRenameObjectDDL(oldIndex, newIndex);
+
+ Assert.assertTrue(ddl.contains("ALTER INDEX \"public\".\"old_idx_name\""));
+ Assert.assertTrue(ddl.contains("RENAME TO \"new_idx_name\""));
+ }
+
+ // ==================== 修改索引测试 ====================
+
+ @Test
+ public void testGenerateUpdateObjectDDL_renameOnly() {
+ DBTableIndex oldIndex =
+ createIndex("public", "users", "old_name", Arrays.asList("name"), DBIndexType.NORMAL, false);
+ DBTableIndex newIndex =
+ createIndex("public", "users", "new_name", Arrays.asList("name"), DBIndexType.NORMAL, false);
+ String ddl = editor.generateUpdateObjectDDL(oldIndex, newIndex);
+
+ // 仅重命名,不应该是 DROP + CREATE
+ Assert.assertTrue(ddl.contains("ALTER INDEX"));
+ Assert.assertTrue(ddl.contains("RENAME TO"));
+ Assert.assertFalse(ddl.contains("DROP INDEX"));
+ Assert.assertFalse(ddl.contains("CREATE INDEX"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_changeColumns() {
+ // 修改索引列:从单列变为多列
+ DBTableIndex oldIndex =
+ createIndex("public", "users", "idx_users", Arrays.asList("name"), DBIndexType.NORMAL, false);
+ DBTableIndex newIndex =
+ createIndex("public", "users", "idx_users", Arrays.asList("name", "email"), DBIndexType.NORMAL, false);
+ String ddl = editor.generateUpdateObjectDDL(oldIndex, newIndex);
+
+ // 结构性变更需要 DROP + CREATE
+ Assert.assertTrue(ddl.contains("DROP INDEX"));
+ Assert.assertTrue(ddl.contains("CREATE INDEX"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_changeToUnique() {
+ // 修改为唯一索引
+ DBTableIndex oldIndex =
+ createIndex("public", "users", "idx_users_email", Arrays.asList("email"), DBIndexType.NORMAL, false);
+ DBTableIndex newIndex =
+ createIndex("public", "users", "idx_users_email", Arrays.asList("email"), DBIndexType.UNIQUE, true);
+ String ddl = editor.generateUpdateObjectDDL(oldIndex, newIndex);
+
+ // 结构性变更需要 DROP + CREATE
+ Assert.assertTrue(ddl.contains("DROP INDEX"));
+ Assert.assertTrue(ddl.contains("CREATE UNIQUE INDEX"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectDDL_noChange() {
+ DBTableIndex oldIndex =
+ createIndex("public", "users", "idx_users_name", Arrays.asList("name"), DBIndexType.NORMAL, false);
+ DBTableIndex newIndex =
+ createIndex("public", "users", "idx_users_name", Arrays.asList("name"), DBIndexType.NORMAL, false);
+ String ddl = editor.generateUpdateObjectDDL(oldIndex, newIndex);
+
+ // 无变化,应返回空字符串
+ Assert.assertEquals("", ddl);
+ }
+
+ // ==================== 标识符转义测试 ====================
+
+ @Test
+ public void testIdentifierWithSpecialChars() {
+ DBTableIndex index =
+ createIndex("my_schema", "my_table", "idx\"name", Arrays.asList("col\"umn"), DBIndexType.NORMAL, false);
+ String ddl = editor.generateCreateObjectDDL(index);
+
+ // 验证双引号被正确转义
+ Assert.assertTrue(ddl.contains("\"idx\"\"name\""));
+ Assert.assertTrue(ddl.contains("\"col\"\"umn\""));
+ }
+
+ // ==================== 批量操作测试 ====================
+
+ @Test
+ public void testGenerateUpdateObjectListDDL_addNewIndex() {
+ List oldIndexes = new ArrayList<>();
+ List newIndexes = Arrays.asList(
+ createIndex("public", "users", "idx_users_name", Arrays.asList("name"), DBIndexType.NORMAL, false));
+
+ String ddl = editor.generateUpdateObjectListDDL(oldIndexes, newIndexes);
+
+ Assert.assertTrue(ddl.contains("CREATE INDEX"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectListDDL_dropIndex() {
+ List oldIndexes = Arrays.asList(
+ createIndex("public", "users", "idx_users_name", Arrays.asList("name"), DBIndexType.NORMAL, false));
+ List newIndexes = new ArrayList<>();
+
+ String ddl = editor.generateUpdateObjectListDDL(oldIndexes, newIndexes);
+
+ Assert.assertTrue(ddl.contains("DROP INDEX"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectListDDL_renameIndex() {
+ // 重命名索引:结构相同,名称不同
+ List oldIndexes = Arrays.asList(
+ createIndex("public", "users", "old_idx_name", Arrays.asList("name"), DBIndexType.NORMAL, false));
+ List newIndexes = Arrays.asList(
+ createIndex("public", "users", "new_idx_name", Arrays.asList("name"), DBIndexType.NORMAL, false));
+
+ String ddl = editor.generateUpdateObjectListDDL(oldIndexes, newIndexes);
+
+ // 应该生成 RENAME 语句
+ Assert.assertTrue(ddl.contains("ALTER INDEX"));
+ Assert.assertTrue(ddl.contains("RENAME TO"));
+ }
+
+ @Test
+ public void testGenerateUpdateObjectListDDL_mixedOperations() {
+ // 混合操作:删除一个、修改一个、新增一个
+ List oldIndexes = Arrays.asList(
+ createIndex("public", "users", "idx_to_drop", Arrays.asList("col1"), DBIndexType.NORMAL, false),
+ createIndex("public", "users", "idx_to_modify", Arrays.asList("col2"), DBIndexType.NORMAL, false));
+ List newIndexes = Arrays.asList(
+ createIndex("public", "users", "idx_to_modify", Arrays.asList("col2", "col3"), DBIndexType.NORMAL,
+ false),
+ createIndex("public", "users", "idx_to_add", Arrays.asList("col4"), DBIndexType.NORMAL, false));
+
+ String ddl = editor.generateUpdateObjectListDDL(oldIndexes, newIndexes);
+
+ // 验证包含所有操作
+ Assert.assertTrue("Should contain DROP INDEX", ddl.contains("DROP INDEX"));
+ Assert.assertTrue("Should contain CREATE INDEX", ddl.contains("CREATE INDEX"));
+ }
+
+ // ==================== 辅助方法 ====================
+
+ private DBTableIndex createIndex(String schemaName, String tableName, String indexName,
+ List columnNames, DBIndexType type, Boolean unique) {
+ DBTableIndex index = new DBTableIndex();
+ index.setSchemaName(schemaName);
+ index.setTableName(tableName);
+ index.setName(indexName);
+ index.setColumnNames(columnNames);
+ index.setType(type);
+ index.setUnique(unique);
+ return index;
+ }
+}
diff --git a/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/schema/postgre/PostgresSchemaAccessorTest.java b/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/schema/postgre/PostgresSchemaAccessorTest.java
new file mode 100644
index 0000000000..8c35a4a830
--- /dev/null
+++ b/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/schema/postgre/PostgresSchemaAccessorTest.java
@@ -0,0 +1,939 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.schema.postgre;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.jdbc.core.JdbcOperations;
+import org.springframework.jdbc.core.RowCallbackHandler;
+import org.springframework.jdbc.core.RowMapper;
+
+import com.oceanbase.tools.dbbrowser.model.DBConstraintType;
+import com.oceanbase.tools.dbbrowser.model.DBFunction;
+import com.oceanbase.tools.dbbrowser.model.DBIndexType;
+import com.oceanbase.tools.dbbrowser.model.DBObjectIdentity;
+import com.oceanbase.tools.dbbrowser.model.DBObjectType;
+import com.oceanbase.tools.dbbrowser.model.DBPLObjectIdentity;
+import com.oceanbase.tools.dbbrowser.model.DBProcedure;
+import com.oceanbase.tools.dbbrowser.model.DBSequence;
+import com.oceanbase.tools.dbbrowser.model.DBTable.DBTableOptions;
+import com.oceanbase.tools.dbbrowser.model.DBTableColumn;
+import com.oceanbase.tools.dbbrowser.model.DBTableConstraint;
+import com.oceanbase.tools.dbbrowser.model.DBTableIndex;
+import com.oceanbase.tools.dbbrowser.model.DBTablePartition;
+import com.oceanbase.tools.dbbrowser.model.DBTablePartitionType;
+import com.oceanbase.tools.dbbrowser.model.DBVariable;
+import com.oceanbase.tools.dbbrowser.model.DBView;
+
+/**
+ * Unit tests for {@link PostgresSchemaAccessor}
+ *
+ * This test uses Mockito to mock JdbcOperations, so it doesn't require a real PostgreSQL database.
+ */
+public class PostgresSchemaAccessorTest {
+
+ private JdbcOperations jdbcOperations;
+ private PostgresSchemaAccessor accessor;
+ private String testSchemaName = "public";
+
+ @Before
+ public void setUp() {
+ jdbcOperations = mock(JdbcOperations.class);
+ accessor = new PostgresSchemaAccessor(jdbcOperations);
+ }
+
+ // ============== listTables Tests ==============
+
+ @Test
+ public void listTables_Success() throws Exception {
+ List> mockData = new ArrayList<>();
+ Map table1 = new HashMap<>();
+ table1.put("table_name", "users");
+ table1.put("table_comment", "User table");
+ mockData.add(table1);
+
+ Map table2 = new HashMap<>();
+ table2.put("table_name", "orders");
+ table2.put("table_comment", "Order table");
+ mockData.add(table2);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ when(jdbcOperations.query(anyString(), any(Object[].class), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ RowMapper mapper = invocation.getArgument(2);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ List tables = accessor.listTables(testSchemaName, null);
+
+ Assert.assertNotNull(tables);
+ Assert.assertEquals(2, tables.size());
+ Assert.assertEquals("users", tables.get(0).getName());
+ Assert.assertEquals(DBObjectType.TABLE, tables.get(0).getType());
+ }
+
+ @Test
+ public void listTables_WithLike_Success() throws Exception {
+ List> mockData = new ArrayList<>();
+ Map table1 = new HashMap<>();
+ table1.put("table_name", "user_accounts");
+ table1.put("table_comment", null);
+ mockData.add(table1);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ when(jdbcOperations.query(anyString(), any(Object[].class), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ RowMapper mapper = invocation.getArgument(2);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ List tables = accessor.listTables(testSchemaName, "user%");
+
+ Assert.assertNotNull(tables);
+ Assert.assertEquals(1, tables.size());
+ }
+
+ // ============== listTableColumns Tests ==============
+
+ @Test
+ public void listTableColumns_Success() throws Exception {
+ List> mockData = new ArrayList<>();
+
+ Map col1 = new HashMap<>();
+ col1.put("ordinal_position", 1);
+ col1.put("column_name", "id");
+ col1.put("data_type", "integer");
+ col1.put("type_name", "int4");
+ col1.put("not_null", true);
+ col1.put("default_value", "nextval('users_id_seq'::regclass)");
+ col1.put("column_comment", "Primary key");
+ col1.put("char_length", null);
+ col1.put("numeric_precision", null);
+ col1.put("numeric_scale", null);
+ mockData.add(col1);
+
+ Map col2 = new HashMap<>();
+ col2.put("ordinal_position", 2);
+ col2.put("column_name", "name");
+ col2.put("data_type", "character varying(100)");
+ col2.put("type_name", "varchar");
+ col2.put("not_null", false);
+ col2.put("default_value", null);
+ col2.put("column_comment", "User name");
+ col2.put("char_length", 100);
+ col2.put("numeric_precision", null);
+ col2.put("numeric_scale", null);
+ mockData.add(col2);
+
+ Map col3 = new HashMap<>();
+ col3.put("ordinal_position", 3);
+ col3.put("column_name", "price");
+ col3.put("data_type", "numeric(10,2)");
+ col3.put("type_name", "numeric");
+ col3.put("not_null", false);
+ col3.put("default_value", null);
+ col3.put("column_comment", null);
+ col3.put("char_length", null);
+ col3.put("numeric_precision", 10);
+ col3.put("numeric_scale", 2);
+ mockData.add(col3);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ when(jdbcOperations.query(anyString(), any(Object[].class), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ RowMapper mapper = invocation.getArgument(2);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ List columns = accessor.listTableColumns(testSchemaName, "users");
+
+ Assert.assertNotNull(columns);
+ Assert.assertEquals(3, columns.size());
+
+ Assert.assertEquals("id", columns.get(0).getName());
+ Assert.assertEquals("int4", columns.get(0).getTypeName());
+ Assert.assertFalse(columns.get(0).getNullable());
+ Assert.assertEquals("Primary key", columns.get(0).getComment());
+
+ Assert.assertEquals("name", columns.get(1).getName());
+ Assert.assertTrue(columns.get(1).getNullable());
+ Assert.assertEquals(Long.valueOf(100), columns.get(1).getMaxLength());
+
+ Assert.assertEquals("price", columns.get(2).getName());
+ Assert.assertEquals(Long.valueOf(10), columns.get(2).getPrecision());
+ Assert.assertEquals(Integer.valueOf(2), columns.get(2).getScale());
+ }
+
+ @Test
+ public void listBasicTableColumns_Success() throws Exception {
+ List> mockData = new ArrayList<>();
+
+ Map col1 = new HashMap<>();
+ col1.put("table_name", "users");
+ col1.put("ordinal_position", 1);
+ col1.put("column_name", "id");
+ col1.put("data_type", "integer");
+ col1.put("type_name", "int4");
+ col1.put("column_comment", null);
+ mockData.add(col1);
+
+ Map col2 = new HashMap<>();
+ col2.put("table_name", "users");
+ col2.put("ordinal_position", 2);
+ col2.put("column_name", "name");
+ col2.put("data_type", "varchar");
+ col2.put("type_name", "varchar");
+ col2.put("column_comment", "Name");
+ mockData.add(col2);
+
+ Map col3 = new HashMap<>();
+ col3.put("table_name", "orders");
+ col3.put("ordinal_position", 1);
+ col3.put("column_name", "id");
+ col3.put("data_type", "bigint");
+ col3.put("type_name", "int8");
+ col3.put("column_comment", null);
+ mockData.add(col3);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ when(jdbcOperations.query(anyString(), any(Object[].class), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ RowMapper mapper = invocation.getArgument(2);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ Map> tableColumns = accessor.listBasicTableColumns(testSchemaName);
+
+ Assert.assertNotNull(tableColumns);
+ Assert.assertEquals(2, tableColumns.size());
+ Assert.assertTrue(tableColumns.containsKey("users"));
+ Assert.assertTrue(tableColumns.containsKey("orders"));
+ Assert.assertEquals(2, tableColumns.get("users").size());
+ }
+
+ // ============== getTableOptions Tests ==============
+
+ @Test
+ public void getTableOptions_Success() throws Exception {
+ doAnswer(invocation -> {
+ ResultSet mockResultSet = mock(ResultSet.class);
+ when(mockResultSet.next()).thenReturn(true).thenReturn(false);
+ when(mockResultSet.getString("table_comment")).thenReturn("User information table");
+ RowCallbackHandler handler = invocation.getArgument(2);
+ handler.processRow(mockResultSet);
+ return null;
+ }).when(jdbcOperations).query(anyString(), any(Object[].class), any(RowCallbackHandler.class));
+
+ DBTableOptions options = accessor.getTableOptions(testSchemaName, "users");
+
+ Assert.assertNotNull(options);
+ Assert.assertEquals("User information table", options.getComment());
+ }
+
+ @Test
+ public void getTableOptions_NoComment_Success() throws Exception {
+ doAnswer(invocation -> {
+ ResultSet mockResultSet = mock(ResultSet.class);
+ when(mockResultSet.next()).thenReturn(true).thenReturn(false);
+ when(mockResultSet.getString("table_comment")).thenReturn(null);
+ RowCallbackHandler handler = invocation.getArgument(2);
+ handler.processRow(mockResultSet);
+ return null;
+ }).when(jdbcOperations).query(anyString(), any(Object[].class), any(RowCallbackHandler.class));
+
+ DBTableOptions options = accessor.getTableOptions(testSchemaName, "users");
+
+ Assert.assertNotNull(options);
+ Assert.assertNull(options.getComment());
+ }
+
+ // ============== listTableIndexes Tests ==============
+
+ @Test
+ public void listTableIndexes_Success() throws Exception {
+ List> mockData = new ArrayList<>();
+
+ Map idx1Col1 = new HashMap<>();
+ idx1Col1.put("index_name", "users_pkey");
+ idx1Col1.put("is_unique", true);
+ idx1Col1.put("is_primary", true);
+ idx1Col1.put("index_type", "btree");
+ idx1Col1.put("index_definition", "CREATE UNIQUE INDEX users_pkey ON public.users USING btree (id)");
+ idx1Col1.put("index_comment", null);
+ idx1Col1.put("column_name", "id");
+ idx1Col1.put("column_position", 1);
+ mockData.add(idx1Col1);
+
+ Map idx2Col1 = new HashMap<>();
+ idx2Col1.put("index_name", "users_name_idx");
+ idx2Col1.put("is_unique", false);
+ idx2Col1.put("is_primary", false);
+ idx2Col1.put("index_type", "btree");
+ idx2Col1.put("index_definition", "CREATE INDEX users_name_idx ON public.users USING btree (name)");
+ idx2Col1.put("index_comment", "Name index");
+ idx2Col1.put("column_name", "name");
+ idx2Col1.put("column_position", 1);
+ mockData.add(idx2Col1);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ when(jdbcOperations.query(anyString(), any(Object[].class), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ RowMapper mapper = invocation.getArgument(2);
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ mapper.mapRow(mockResultSet, rowNum++);
+ }
+ return null;
+ });
+
+ List indexes = accessor.listTableIndexes(testSchemaName, "users");
+
+ Assert.assertNotNull(indexes);
+ Assert.assertEquals(2, indexes.size());
+
+ DBTableIndex pkIndex = indexes.stream()
+ .filter(i -> "users_pkey".equals(i.getName()))
+ .findFirst().orElse(null);
+ Assert.assertNotNull(pkIndex);
+ Assert.assertTrue(pkIndex.getPrimary());
+ Assert.assertTrue(pkIndex.getUnique());
+ Assert.assertEquals(DBIndexType.UNIQUE, pkIndex.getType());
+
+ DBTableIndex normalIndex = indexes.stream()
+ .filter(i -> "users_name_idx".equals(i.getName()))
+ .findFirst().orElse(null);
+ Assert.assertNotNull(normalIndex);
+ Assert.assertFalse(normalIndex.getPrimary());
+ Assert.assertFalse(normalIndex.getUnique());
+ }
+
+ // ============== listTableConstraints Tests ==============
+
+ @Test
+ public void listTableConstraints_PrimaryKey_Success() throws Exception {
+ List> pkData = new ArrayList<>();
+ Map pkCol = new HashMap<>();
+ pkCol.put("constraint_name", "users_pkey");
+ pkCol.put("constraint_type", "p");
+ pkCol.put("column_name", "id");
+ pkData.add(pkCol);
+
+ ResultSet pkResultSet = createMockResultSet(pkData);
+ ResultSet emptyResultSet = createMockResultSet(new ArrayList<>());
+
+ when(jdbcOperations.query(anyString(), any(Object[].class), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ String sql = invocation.getArgument(0);
+ RowMapper mapper = invocation.getArgument(2);
+ ResultSet rs = sql.contains("contype IN") ? pkResultSet : emptyResultSet;
+ int rowNum = 0;
+ while (rs.next()) {
+ mapper.mapRow(rs, rowNum++);
+ }
+ return null;
+ });
+
+ List constraints = accessor.listTableConstraints(testSchemaName, "users");
+
+ Assert.assertNotNull(constraints);
+ Assert.assertEquals(1, constraints.size());
+
+ DBTableConstraint pk = constraints.get(0);
+ Assert.assertEquals("users_pkey", pk.getName());
+ Assert.assertEquals(DBConstraintType.PRIMARY_KEY, pk.getType());
+ Assert.assertEquals(1, pk.getColumnNames().size());
+ Assert.assertEquals("id", pk.getColumnNames().get(0));
+ }
+
+ @Test
+ public void listTableConstraints_ForeignKey_Success() throws Exception {
+ List> fkData = new ArrayList<>();
+ Map fkCol = new HashMap<>();
+ fkCol.put("constraint_name", "orders_user_id_fkey");
+ fkCol.put("column_name", "user_id");
+ fkCol.put("referenced_schema_name", "public");
+ fkCol.put("referenced_table_name", "users");
+ fkCol.put("referenced_column_name", "id");
+ fkCol.put("update_action", "a");
+ fkCol.put("delete_action", "c");
+ fkData.add(fkCol);
+
+ ResultSet fkResultSet = createMockResultSet(fkData);
+ ResultSet emptyResultSet = createMockResultSet(new ArrayList<>());
+
+ when(jdbcOperations.query(anyString(), any(Object[].class), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ String sql = invocation.getArgument(0);
+ RowMapper mapper = invocation.getArgument(2);
+ ResultSet rs = sql.contains("contype = 'f'") ? fkResultSet : emptyResultSet;
+ int rowNum = 0;
+ while (rs.next()) {
+ mapper.mapRow(rs, rowNum++);
+ }
+ return null;
+ });
+
+ List constraints = accessor.listTableConstraints(testSchemaName, "orders");
+
+ Assert.assertNotNull(constraints);
+ Assert.assertEquals(1, constraints.size());
+
+ DBTableConstraint fk = constraints.get(0);
+ Assert.assertEquals("orders_user_id_fkey", fk.getName());
+ Assert.assertEquals(DBConstraintType.FOREIGN_KEY, fk.getType());
+ Assert.assertEquals("users", fk.getReferenceTableName());
+ }
+
+ @Test
+ public void listTableConstraints_Check_Success() throws Exception {
+ List> checkData = new ArrayList<>();
+ Map checkConstraint = new HashMap<>();
+ checkConstraint.put("constraint_name", "users_age_check");
+ checkConstraint.put("constraint_definition", "CHECK ((age >= 0))");
+ checkData.add(checkConstraint);
+
+ ResultSet checkResultSet = createMockResultSet(checkData);
+ ResultSet emptyResultSet = createMockResultSet(new ArrayList<>());
+
+ when(jdbcOperations.query(anyString(), any(Object[].class), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ String sql = invocation.getArgument(0);
+ RowMapper mapper = invocation.getArgument(2);
+ ResultSet rs = sql.contains("contype = 'c'") ? checkResultSet : emptyResultSet;
+ int rowNum = 0;
+ while (rs.next()) {
+ mapper.mapRow(rs, rowNum++);
+ }
+ return null;
+ });
+
+ List constraints = accessor.listTableConstraints(testSchemaName, "users");
+
+ Assert.assertNotNull(constraints);
+
+ DBTableConstraint check = constraints.stream()
+ .filter(c -> c.getType() == DBConstraintType.CHECK)
+ .findFirst().orElse(null);
+ Assert.assertNotNull(check);
+ Assert.assertEquals("users_age_check", check.getName());
+ }
+
+ // ============== getPartition Tests ==============
+
+ @Test
+ public void getPartition_RangePartition_Success() throws Exception {
+ AtomicInteger queryCount = new AtomicInteger(0);
+
+ doAnswer(invocation -> {
+ int count = queryCount.getAndIncrement();
+ ResultSet mockResultSet = mock(ResultSet.class);
+ if (count == 0) {
+ // First query: partition check
+ when(mockResultSet.next()).thenReturn(true).thenReturn(false);
+ when(mockResultSet.getString("relkind")).thenReturn("p");
+ when(mockResultSet.getString("partstrat")).thenReturn("r");
+ } else {
+ // Subsequent queries: partition key and definitions
+ when(mockResultSet.next()).thenReturn(false);
+ }
+ RowCallbackHandler handler = invocation.getArgument(2);
+ handler.processRow(mockResultSet);
+ return null;
+ }).when(jdbcOperations).query(anyString(), any(Object[].class), any(RowCallbackHandler.class));
+
+ // Mock partition definitions query with RowMapper
+ when(jdbcOperations.query(anyString(), any(Object[].class), any(RowMapper.class)))
+ .thenReturn(null);
+
+ DBTablePartition partition = accessor.getPartition(testSchemaName, "events");
+
+ Assert.assertNotNull(partition);
+ Assert.assertEquals(DBTablePartitionType.RANGE, partition.getPartitionOption().getType());
+ }
+
+ @Test
+ public void getPartition_NotPartitioned_ReturnsNull() throws Exception {
+ doAnswer(invocation -> {
+ ResultSet mockResultSet = mock(ResultSet.class);
+ when(mockResultSet.next()).thenReturn(true).thenReturn(false);
+ when(mockResultSet.getString("relkind")).thenReturn("r");
+ when(mockResultSet.getString("partstrat")).thenReturn(null);
+ RowCallbackHandler handler = invocation.getArgument(2);
+ handler.processRow(mockResultSet);
+ return null;
+ }).when(jdbcOperations).query(anyString(), any(Object[].class), any(RowCallbackHandler.class));
+
+ DBTablePartition partition = accessor.getPartition(testSchemaName, "users");
+
+ Assert.assertNull(partition);
+ }
+
+ // ============== getTableDDL Tests ==============
+
+ @Test
+ public void getTableDDL_Success() throws Exception {
+ List> columnsData = new ArrayList<>();
+ Map col1 = new HashMap<>();
+ col1.put("ordinal_position", 1);
+ col1.put("column_name", "id");
+ col1.put("data_type", "integer");
+ col1.put("type_name", "int4");
+ col1.put("not_null", true);
+ col1.put("default_value", "nextval('users_id_seq'::regclass)");
+ col1.put("column_comment", null);
+ col1.put("char_length", null);
+ col1.put("numeric_precision", null);
+ col1.put("numeric_scale", null);
+ columnsData.add(col1);
+
+ ResultSet columnsRs = createMockResultSet(columnsData);
+
+ when(jdbcOperations.query(anyString(), any(Object[].class), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ String sql = invocation.getArgument(0);
+ if (sql.contains("pg_attribute")) {
+ RowMapper mapper = invocation.getArgument(2);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (columnsRs.next()) {
+ result.add(mapper.mapRow(columnsRs, rowNum++));
+ }
+ return result;
+ }
+ return new ArrayList();
+ });
+
+ doAnswer(invocation -> {
+ ResultSet mockResultSet = mock(ResultSet.class);
+ when(mockResultSet.next()).thenReturn(true).thenReturn(false);
+ when(mockResultSet.getString("table_comment")).thenReturn("User table");
+ RowCallbackHandler handler = invocation.getArgument(2);
+ handler.processRow(mockResultSet);
+ return null;
+ }).when(jdbcOperations).query(anyString(), any(Object[].class), any(RowCallbackHandler.class));
+
+ String ddl = accessor.getTableDDL(testSchemaName, "users");
+
+ Assert.assertNotNull(ddl);
+ Assert.assertTrue(ddl.contains("CREATE TABLE"));
+ Assert.assertTrue(ddl.contains("\"id\""));
+ Assert.assertTrue(ddl.contains("integer"));
+ Assert.assertTrue(ddl.contains("NOT NULL"));
+ }
+
+ // ============== listViews Tests ==============
+
+ @Test
+ public void listViews_Success() throws Exception {
+ List> mockData = new ArrayList<>();
+ Map view1 = new HashMap<>();
+ view1.put("view_name", "user_view");
+ mockData.add(view1);
+
+ Map view2 = new HashMap<>();
+ view2.put("view_name", "order_view");
+ mockData.add(view2);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ when(jdbcOperations.query(anyString(), any(Object[].class), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ RowMapper mapper = invocation.getArgument(2);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ List views = accessor.listViews(testSchemaName);
+
+ Assert.assertNotNull(views);
+ Assert.assertEquals(2, views.size());
+ Assert.assertEquals("user_view", views.get(0).getName());
+ Assert.assertEquals(DBObjectType.VIEW, views.get(0).getType());
+ }
+
+ // ============== listFunctions Tests ==============
+
+ @Test
+ public void listFunctions_Success() throws Exception {
+ List> mockData = new ArrayList<>();
+ Map func1 = new HashMap<>();
+ func1.put("function_name", "calculate_total");
+ func1.put("arguments", "order_id integer");
+ func1.put("return_type", "numeric");
+ mockData.add(func1);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ when(jdbcOperations.query(anyString(), any(Object[].class), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ RowMapper mapper = invocation.getArgument(2);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ List functions = accessor.listFunctions(testSchemaName);
+
+ Assert.assertNotNull(functions);
+ Assert.assertEquals(1, functions.size());
+ Assert.assertEquals("calculate_total", functions.get(0).getName());
+ Assert.assertEquals(DBObjectType.FUNCTION, functions.get(0).getType());
+ }
+
+ // ============== listProcedures Tests ==============
+
+ @Test
+ public void listProcedures_Success() throws Exception {
+ List> mockData = new ArrayList<>();
+ Map proc1 = new HashMap<>();
+ proc1.put("procedure_name", "process_order");
+ mockData.add(proc1);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ when(jdbcOperations.query(anyString(), any(Object[].class), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ RowMapper mapper = invocation.getArgument(2);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ List procedures = accessor.listProcedures(testSchemaName);
+
+ Assert.assertNotNull(procedures);
+ Assert.assertEquals(1, procedures.size());
+ Assert.assertEquals("process_order", procedures.get(0).getName());
+ Assert.assertEquals(DBObjectType.PROCEDURE, procedures.get(0).getType());
+ }
+
+ // ============== listSequences Tests ==============
+
+ @Test
+ public void listSequences_Success() throws Exception {
+ List> mockData = new ArrayList<>();
+ Map seq1 = new HashMap<>();
+ seq1.put("sequence_name", "user_id_seq");
+ mockData.add(seq1);
+
+ Map seq2 = new HashMap<>();
+ seq2.put("sequence_name", "order_id_seq");
+ mockData.add(seq2);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ when(jdbcOperations.query(anyString(), any(Object[].class), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ RowMapper mapper = invocation.getArgument(2);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ List sequences = accessor.listSequences(testSchemaName);
+
+ Assert.assertNotNull(sequences);
+ Assert.assertEquals(2, sequences.size());
+ Assert.assertEquals("user_id_seq", sequences.get(0).getName());
+ Assert.assertEquals(DBObjectType.SEQUENCE, sequences.get(0).getType());
+ }
+
+ // ============== showVariables Tests ==============
+
+ @Test
+ public void showVariables_Success() throws Exception {
+ List> mockData = new ArrayList<>();
+ Map var1 = new HashMap<>();
+ var1.put("name", "work_mem");
+ var1.put("value", "4MB");
+ mockData.add(var1);
+
+ Map var2 = new HashMap<>();
+ var2.put("name", "shared_buffers");
+ var2.put("value", "128MB");
+ mockData.add(var2);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ when(jdbcOperations.query(anyString(), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ RowMapper mapper = invocation.getArgument(1);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ List variables = accessor.showVariables();
+
+ Assert.assertNotNull(variables);
+ Assert.assertEquals(2, variables.size());
+ Assert.assertEquals("work_mem", variables.get(0).getName());
+ Assert.assertEquals("4MB", variables.get(0).getValue());
+ }
+
+ // ============== showSessionVariables Tests ==============
+
+ @Test
+ public void showSessionVariables_Success() throws Exception {
+ List> mockData = new ArrayList<>();
+ Map var1 = new HashMap<>();
+ var1.put("name", "work_mem");
+ var1.put("value", "4MB");
+ mockData.add(var1);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ when(jdbcOperations.query(anyString(), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ RowMapper mapper = invocation.getArgument(1);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ List variables = accessor.showSessionVariables();
+
+ Assert.assertNotNull(variables);
+ Assert.assertEquals(1, variables.size());
+ Assert.assertEquals("work_mem", variables.get(0).getName());
+ }
+
+ // ============== getFunction Tests ==============
+
+ @Test
+ public void getFunction_Success() throws Exception {
+ doAnswer(invocation -> {
+ ResultSet mockResultSet = mock(ResultSet.class);
+ when(mockResultSet.next()).thenReturn(true).thenReturn(false);
+ when(mockResultSet.getString("function_name")).thenReturn("calculate_total");
+ when(mockResultSet.getString("function_ddl")).thenReturn(
+ "CREATE FUNCTION calculate_total() RETURNS numeric AS $$ BEGIN RETURN 0; END; $$ LANGUAGE plpgsql");
+ when(mockResultSet.getString("return_type")).thenReturn("numeric");
+ when(mockResultSet.getString("arguments")).thenReturn("");
+ RowCallbackHandler handler = invocation.getArgument(2);
+ handler.processRow(mockResultSet);
+ return null;
+ }).when(jdbcOperations).query(anyString(), any(Object[].class), any(RowCallbackHandler.class));
+
+ DBFunction function = accessor.getFunction(testSchemaName, "calculate_total");
+
+ Assert.assertNotNull(function);
+ Assert.assertEquals("calculate_total", function.getFunName());
+ Assert.assertEquals("numeric", function.getReturnType());
+ }
+
+ // ============== getProcedure Tests ==============
+
+ @Test
+ public void getProcedure_Success() throws Exception {
+ doAnswer(invocation -> {
+ ResultSet mockResultSet = mock(ResultSet.class);
+ when(mockResultSet.next()).thenReturn(true).thenReturn(false);
+ when(mockResultSet.getString("procedure_name")).thenReturn("process_order");
+ when(mockResultSet.getString("procedure_ddl"))
+ .thenReturn("CREATE PROCEDURE process_order() AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql");
+ when(mockResultSet.getString("arguments")).thenReturn("");
+ RowCallbackHandler handler = invocation.getArgument(2);
+ handler.processRow(mockResultSet);
+ return null;
+ }).when(jdbcOperations).query(anyString(), any(Object[].class), any(RowCallbackHandler.class));
+
+ DBProcedure procedure = accessor.getProcedure(testSchemaName, "process_order");
+
+ Assert.assertNotNull(procedure);
+ Assert.assertEquals("process_order", procedure.getProName());
+ }
+
+ // ============== getView Tests ==============
+
+ @Test
+ public void getView_Success() throws Exception {
+ doAnswer(invocation -> {
+ ResultSet mockResultSet = mock(ResultSet.class);
+ when(mockResultSet.next()).thenReturn(true).thenReturn(false);
+ when(mockResultSet.getString("table_schema")).thenReturn(testSchemaName);
+ when(mockResultSet.getString("check_option")).thenReturn("NONE");
+ when(mockResultSet.getString("is_updatable")).thenReturn("YES");
+ when(mockResultSet.getString("view_definition")).thenReturn("SELECT * FROM users");
+ RowCallbackHandler handler = invocation.getArgument(2);
+ handler.processRow(mockResultSet);
+ return null;
+ }).when(jdbcOperations).query(anyString(), any(Object[].class), any(RowCallbackHandler.class));
+
+ when(jdbcOperations.query(anyString(), any(Object[].class), any(RowMapper.class)))
+ .thenReturn(new ArrayList<>());
+
+ DBView view = accessor.getView(testSchemaName, "user_view");
+
+ Assert.assertNotNull(view);
+ Assert.assertEquals("user_view", view.getViewName());
+ Assert.assertTrue(view.isUpdatable());
+ }
+
+ // ============== getSequence Tests ==============
+
+ @Test
+ public void getSequence_Success() throws Exception {
+ // Mock first query to check sequence exists
+ doAnswer(invocation -> {
+ ResultSet mockResultSet = mock(ResultSet.class);
+ when(mockResultSet.next()).thenReturn(true).thenReturn(false);
+ RowCallbackHandler handler = invocation.getArgument(2);
+ handler.processRow(mockResultSet);
+ return null;
+ }).when(jdbcOperations).query(anyString(), any(Object[].class), any(RowCallbackHandler.class));
+
+ DBSequence sequence = accessor.getSequence(testSchemaName, "user_id_seq");
+
+ Assert.assertNotNull(sequence);
+ Assert.assertEquals("user_id_seq", sequence.getName());
+ }
+
+ // ============== Helper Methods ==============
+
+ private ResultSet createMockResultSet(List> data) throws Exception {
+ ResultSet rs = mock(ResultSet.class);
+ ResultSetMetaData rsmd = mock(ResultSetMetaData.class);
+
+ if (data == null || data.isEmpty()) {
+ when(rs.next()).thenReturn(false);
+ return rs;
+ }
+
+ String[] columnNames = data.get(0).keySet().toArray(new String[0]);
+ when(rsmd.getColumnCount()).thenReturn(columnNames.length);
+ for (int i = 0; i < columnNames.length; i++) {
+ when(rsmd.getColumnName(i + 1)).thenReturn(columnNames[i]);
+ when(rsmd.getColumnLabel(i + 1)).thenReturn(columnNames[i]);
+ }
+ when(rs.getMetaData()).thenReturn(rsmd);
+
+ AtomicInteger rowIndex = new AtomicInteger(0);
+ when(rs.next()).thenAnswer(invocation -> {
+ int current = rowIndex.get();
+ if (current < data.size()) {
+ rowIndex.incrementAndGet();
+ return true;
+ }
+ return false;
+ });
+
+ when(rs.getObject(anyString())).thenAnswer(invocation -> {
+ String columnName = invocation.getArgument(0);
+ int currentRow = rowIndex.get() - 1;
+ if (currentRow >= 0 && currentRow < data.size()) {
+ return data.get(currentRow).get(columnName);
+ }
+ return null;
+ });
+
+ when(rs.getString(anyString())).thenAnswer(invocation -> {
+ Object value = rs.getObject(invocation.getArgument(0));
+ return value != null ? value.toString() : null;
+ });
+
+ when(rs.getBoolean(anyString())).thenAnswer(invocation -> {
+ Object value = rs.getObject(invocation.getArgument(0));
+ if (value instanceof Boolean) {
+ return (Boolean) value;
+ }
+ return false;
+ });
+
+ when(rs.getInt(anyString())).thenAnswer(invocation -> {
+ Object value = rs.getObject(invocation.getArgument(0));
+ if (value instanceof Integer) {
+ return (Integer) value;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).intValue();
+ }
+ return 0;
+ });
+
+ when(rs.getLong(anyString())).thenAnswer(invocation -> {
+ Object value = rs.getObject(invocation.getArgument(0));
+ if (value instanceof Long) {
+ return (Long) value;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).longValue();
+ }
+ return 0L;
+ });
+
+ return rs;
+ }
+}
diff --git a/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/stats/PostgresStatsAccessorTest.java b/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/stats/PostgresStatsAccessorTest.java
new file mode 100644
index 0000000000..65217fda64
--- /dev/null
+++ b/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/stats/PostgresStatsAccessorTest.java
@@ -0,0 +1,552 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.stats;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.jdbc.core.JdbcOperations;
+import org.springframework.jdbc.core.RowCallbackHandler;
+import org.springframework.jdbc.core.RowMapper;
+
+import com.oceanbase.tools.dbbrowser.model.DBSession;
+import com.oceanbase.tools.dbbrowser.model.DBSession.DBTransState;
+import com.oceanbase.tools.dbbrowser.model.DBTableStats;
+import com.oceanbase.tools.dbbrowser.stats.postgres.PostgresStatsAccessor;
+
+/**
+ * Unit tests for {@link PostgresStatsAccessor}
+ *
+ *
+ * This test uses Mockito to mock JdbcOperations, so it doesn't require a real PostgreSQL database.
+ *
+ *
+ * @author odc
+ * @since ODC_release_4.3.4
+ */
+public class PostgresStatsAccessorTest {
+
+ private JdbcOperations jdbcOperations;
+ private PostgresStatsAccessor accessor;
+
+ @Before
+ public void setUp() {
+ jdbcOperations = mock(JdbcOperations.class);
+ accessor = new PostgresStatsAccessor(jdbcOperations);
+ }
+
+ // ============== getTableStats Tests ==============
+
+ @Test
+ public void getTableStats_Success() throws Exception {
+ List> mockData = new ArrayList<>();
+ Map row = new HashMap<>();
+ row.put("row_count", 1000L);
+ row.put("data_size_in_bytes", 81920L);
+ mockData.add(row);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ doAnswer(invocation -> {
+ RowCallbackHandler handler = invocation.getArgument(1);
+ while (mockResultSet.next()) {
+ handler.processRow(mockResultSet);
+ }
+ return null;
+ }).when(jdbcOperations).query(anyString(), any(RowCallbackHandler.class));
+
+ DBTableStats stats = accessor.getTableStats("public", "users");
+
+ Assert.assertNotNull(stats);
+ Assert.assertEquals(Long.valueOf(1000L), stats.getRowCount());
+ Assert.assertEquals(Long.valueOf(81920L), stats.getDataSizeInBytes());
+ }
+
+ @Test
+ public void getTableStats_NullValues_ReturnsZero() throws Exception {
+ List> mockData = new ArrayList<>();
+ Map row = new HashMap<>();
+ row.put("row_count", null);
+ row.put("data_size_in_bytes", null);
+ mockData.add(row);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ doAnswer(invocation -> {
+ RowCallbackHandler handler = invocation.getArgument(1);
+ while (mockResultSet.next()) {
+ handler.processRow(mockResultSet);
+ }
+ return null;
+ }).when(jdbcOperations).query(anyString(), any(RowCallbackHandler.class));
+
+ DBTableStats stats = accessor.getTableStats("public", "users");
+
+ Assert.assertNotNull(stats);
+ Assert.assertEquals(Long.valueOf(0L), stats.getRowCount());
+ Assert.assertEquals(Long.valueOf(0L), stats.getDataSizeInBytes());
+ }
+
+ @Test
+ public void getTableStats_EmptyResult_ReturnsEmptyStats() throws Exception {
+ ResultSet mockResultSet = createMockResultSet(new ArrayList<>());
+
+ doAnswer(invocation -> {
+ RowCallbackHandler handler = invocation.getArgument(1);
+ while (mockResultSet.next()) {
+ handler.processRow(mockResultSet);
+ }
+ return null;
+ }).when(jdbcOperations).query(anyString(), any(RowCallbackHandler.class));
+
+ DBTableStats stats = accessor.getTableStats("public", "nonexistent_table");
+
+ Assert.assertNotNull(stats);
+ Assert.assertNull(stats.getRowCount());
+ Assert.assertNull(stats.getDataSizeInBytes());
+ }
+
+ @Test
+ public void getTableStats_WithNumericValues() throws Exception {
+ List> mockData = new ArrayList<>();
+ Map row = new HashMap<>();
+ // Test with integer values (PostgreSQL may return different numeric types)
+ row.put("row_count", 500);
+ row.put("data_size_in_bytes", 40960);
+ mockData.add(row);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ doAnswer(invocation -> {
+ RowCallbackHandler handler = invocation.getArgument(1);
+ while (mockResultSet.next()) {
+ handler.processRow(mockResultSet);
+ }
+ return null;
+ }).when(jdbcOperations).query(anyString(), any(RowCallbackHandler.class));
+
+ DBTableStats stats = accessor.getTableStats("public", "orders");
+
+ Assert.assertNotNull(stats);
+ Assert.assertEquals(Long.valueOf(500L), stats.getRowCount());
+ Assert.assertEquals(Long.valueOf(40960L), stats.getDataSizeInBytes());
+ }
+
+ // ============== listAllSessions Tests ==============
+
+ @Test
+ public void listAllSessions_Success() throws Exception {
+ List> mockData = new ArrayList<>();
+
+ Map session1 = new HashMap<>();
+ session1.put("pid", 12345L);
+ session1.put("usename", "postgres");
+ session1.put("datname", "testdb");
+ session1.put("state", "active");
+ session1.put("query", "SELECT * FROM users");
+ session1.put("client_addr", "192.168.1.100");
+ session1.put("execute_time", 10L);
+ mockData.add(session1);
+
+ Map session2 = new HashMap<>();
+ session2.put("pid", 12346L);
+ session2.put("usename", "admin");
+ session2.put("datname", "mydb");
+ session2.put("state", "idle");
+ session2.put("query", "SELECT 1");
+ session2.put("client_addr", "192.168.1.101");
+ session2.put("execute_time", 0L);
+ mockData.add(session2);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ when(jdbcOperations.query(anyString(), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ @SuppressWarnings("unchecked")
+ RowMapper mapper = invocation.getArgument(1);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ List sessions = accessor.listAllSessions();
+
+ Assert.assertNotNull(sessions);
+ Assert.assertEquals(2, sessions.size());
+
+ DBSession s1 = sessions.get(0);
+ Assert.assertEquals("12345", s1.getId());
+ Assert.assertEquals("postgres", s1.getUsername());
+ Assert.assertEquals("testdb", s1.getDatabaseName());
+ Assert.assertEquals("active", s1.getState());
+ Assert.assertEquals("SELECT * FROM users", s1.getLatestQueries());
+ Assert.assertEquals("192.168.1.100", s1.getHost());
+ Assert.assertEquals(Integer.valueOf(10), s1.getExecuteTime());
+ Assert.assertEquals(DBTransState.ACTIVE, s1.getTransState());
+
+ DBSession s2 = sessions.get(1);
+ Assert.assertEquals("12346", s2.getId());
+ Assert.assertEquals("idle", s2.getState());
+ Assert.assertEquals(DBTransState.IDLE, s2.getTransState());
+ }
+
+ @Test
+ public void listAllSessions_StateTransitions() throws Exception {
+ List> mockData = new ArrayList<>();
+
+ // idle in transaction -> ACTIVE
+ Map session1 = new HashMap<>();
+ session1.put("pid", 111L);
+ session1.put("usename", "user1");
+ session1.put("datname", "db1");
+ session1.put("state", "idle in transaction");
+ session1.put("query", "BEGIN");
+ session1.put("client_addr", null);
+ session1.put("execute_time", 30L);
+ mockData.add(session1);
+
+ // idle in transaction (aborted) -> ACTIVE
+ Map session2 = new HashMap<>();
+ session2.put("pid", 112L);
+ session2.put("usename", "user2");
+ session2.put("datname", "db2");
+ session2.put("state", "idle in transaction (aborted)");
+ session2.put("query", "BEGIN; SELECT 1/0;");
+ session2.put("client_addr", null);
+ session2.put("execute_time", 60L);
+ mockData.add(session2);
+
+ // null state -> UNKNOWN
+ Map session3 = new HashMap<>();
+ session3.put("pid", 113L);
+ session3.put("usename", "user3");
+ session3.put("datname", "db3");
+ session3.put("state", null);
+ session3.put("query", null);
+ session3.put("client_addr", null);
+ session3.put("execute_time", null);
+ mockData.add(session3);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ when(jdbcOperations.query(anyString(), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ @SuppressWarnings("unchecked")
+ RowMapper mapper = invocation.getArgument(1);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ List sessions = accessor.listAllSessions();
+
+ Assert.assertEquals(3, sessions.size());
+ Assert.assertEquals(DBTransState.ACTIVE, sessions.get(0).getTransState());
+ Assert.assertEquals(DBTransState.ACTIVE, sessions.get(1).getTransState());
+ Assert.assertEquals(DBTransState.UNKNOWN, sessions.get(2).getTransState());
+ }
+
+ @Test
+ public void listAllSessions_EmptyList() throws Exception {
+ ResultSet mockResultSet = createMockResultSet(new ArrayList<>());
+
+ when(jdbcOperations.query(anyString(), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ @SuppressWarnings("unchecked")
+ RowMapper mapper = invocation.getArgument(1);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ List sessions = accessor.listAllSessions();
+
+ Assert.assertNotNull(sessions);
+ Assert.assertTrue(sessions.isEmpty());
+ }
+
+ // ============== currentSession Tests ==============
+
+ @Test
+ public void currentSession_Success() throws Exception {
+ List> mockData = new ArrayList<>();
+ Map session = new HashMap<>();
+ session.put("pid", 99999L);
+ session.put("usename", "testuser");
+ session.put("datname", "testdb");
+ session.put("state", "active");
+ session.put("query", "SELECT pg_backend_pid()");
+ session.put("client_addr", "127.0.0.1");
+ session.put("execute_time", 0L);
+ mockData.add(session);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ when(jdbcOperations.query(anyString(), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ @SuppressWarnings("unchecked")
+ RowMapper mapper = invocation.getArgument(1);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ DBSession currentSession = accessor.currentSession();
+
+ Assert.assertNotNull(currentSession);
+ Assert.assertEquals("99999", currentSession.getId());
+ Assert.assertEquals("testuser", currentSession.getUsername());
+ Assert.assertEquals("testdb", currentSession.getDatabaseName());
+ Assert.assertEquals("active", currentSession.getState());
+ Assert.assertEquals(DBTransState.ACTIVE, currentSession.getTransState());
+ }
+
+ @Test
+ public void currentSession_NoResult_ReturnsUnknown() throws Exception {
+ ResultSet mockResultSet = createMockResultSet(new ArrayList<>());
+
+ when(jdbcOperations.query(anyString(), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ @SuppressWarnings("unchecked")
+ RowMapper mapper = invocation.getArgument(1);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ DBSession currentSession = accessor.currentSession();
+
+ Assert.assertNotNull(currentSession);
+ Assert.assertEquals(DBTransState.UNKNOWN, currentSession.getTransState());
+ }
+
+ @Test
+ public void currentSession_NullFields() throws Exception {
+ List> mockData = new ArrayList<>();
+ Map session = new HashMap<>();
+ session.put("pid", 123L);
+ session.put("usename", null);
+ session.put("datname", null);
+ session.put("state", null);
+ session.put("query", null);
+ session.put("client_addr", null);
+ session.put("execute_time", null);
+ mockData.add(session);
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ when(jdbcOperations.query(anyString(), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ @SuppressWarnings("unchecked")
+ RowMapper mapper = invocation.getArgument(1);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ DBSession currentSession = accessor.currentSession();
+
+ Assert.assertNotNull(currentSession);
+ Assert.assertEquals("123", currentSession.getId());
+ Assert.assertNull(currentSession.getUsername());
+ Assert.assertNull(currentSession.getDatabaseName());
+ Assert.assertEquals(Integer.valueOf(0), currentSession.getExecuteTime());
+ Assert.assertEquals(DBTransState.UNKNOWN, currentSession.getTransState());
+ }
+
+ // ============== SQL Generation Tests ==============
+
+ @Test
+ public void getTableStats_SqlContainsExpectedKeywords() throws Exception {
+ List> mockData = new ArrayList<>();
+ mockData.add(new HashMap<>());
+
+ ResultSet mockResultSet = createMockResultSet(mockData);
+
+ final String[] capturedSql = new String[1];
+
+ doAnswer(invocation -> {
+ capturedSql[0] = invocation.getArgument(0);
+ RowCallbackHandler handler = invocation.getArgument(1);
+ while (mockResultSet.next()) {
+ handler.processRow(mockResultSet);
+ }
+ return null;
+ }).when(jdbcOperations).query(anyString(), any(RowCallbackHandler.class));
+
+ accessor.getTableStats("public", "users");
+
+ Assert.assertNotNull(capturedSql[0]);
+ // Verify SQL uses PostgreSQL system tables
+ Assert.assertTrue("SQL should contain pg_class", capturedSql[0].contains("pg_class"));
+ Assert.assertTrue("SQL should contain pg_stat_user_tables", capturedSql[0].contains("pg_stat_user_tables"));
+ Assert.assertTrue("SQL should contain pg_total_relation_size",
+ capturedSql[0].contains("pg_total_relation_size"));
+ Assert.assertTrue("SQL should contain pg_namespace", capturedSql[0].contains("pg_namespace"));
+ }
+
+ @Test
+ public void listAllSessions_SqlContainsPgStatActivity() throws Exception {
+ ResultSet mockResultSet = createMockResultSet(new ArrayList<>());
+
+ final String[] capturedSql = new String[1];
+
+ when(jdbcOperations.query(anyString(), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ capturedSql[0] = invocation.getArgument(0);
+ @SuppressWarnings("unchecked")
+ RowMapper mapper = invocation.getArgument(1);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ accessor.listAllSessions();
+
+ Assert.assertNotNull(capturedSql[0]);
+ Assert.assertTrue("SQL should contain pg_stat_activity", capturedSql[0].contains("pg_stat_activity"));
+ Assert.assertTrue("SQL should contain pg_backend_pid", capturedSql[0].contains("pg_backend_pid()") == false);
+ }
+
+ @Test
+ public void currentSession_SqlContainsPgBackendPid() throws Exception {
+ ResultSet mockResultSet = createMockResultSet(new ArrayList<>());
+
+ final String[] capturedSql = new String[1];
+
+ when(jdbcOperations.query(anyString(), any(RowMapper.class)))
+ .thenAnswer(invocation -> {
+ capturedSql[0] = invocation.getArgument(0);
+ @SuppressWarnings("unchecked")
+ RowMapper mapper = invocation.getArgument(1);
+ List result = new ArrayList<>();
+ int rowNum = 0;
+ while (mockResultSet.next()) {
+ result.add(mapper.mapRow(mockResultSet, rowNum++));
+ }
+ return result;
+ });
+
+ accessor.currentSession();
+
+ Assert.assertNotNull(capturedSql[0]);
+ Assert.assertTrue("SQL should contain pg_stat_activity", capturedSql[0].contains("pg_stat_activity"));
+ Assert.assertTrue("SQL should filter by pg_backend_pid()", capturedSql[0].contains("pg_backend_pid()"));
+ }
+
+ // ============== Helper Methods ==============
+
+ private ResultSet createMockResultSet(List> data) throws Exception {
+ ResultSet rs = mock(ResultSet.class);
+ ResultSetMetaData rsmd = mock(ResultSetMetaData.class);
+
+ if (data == null || data.isEmpty()) {
+ when(rs.next()).thenReturn(false);
+ return rs;
+ }
+
+ String[] columnNames = data.get(0).keySet().toArray(new String[0]);
+ when(rsmd.getColumnCount()).thenReturn(columnNames.length);
+ for (int i = 0; i < columnNames.length; i++) {
+ when(rsmd.getColumnName(i + 1)).thenReturn(columnNames[i]);
+ when(rsmd.getColumnLabel(i + 1)).thenReturn(columnNames[i]);
+ }
+ when(rs.getMetaData()).thenReturn(rsmd);
+
+ AtomicInteger rowIndex = new AtomicInteger(0);
+ when(rs.next()).thenAnswer(invocation -> {
+ int current = rowIndex.get();
+ if (current < data.size()) {
+ rowIndex.incrementAndGet();
+ return true;
+ }
+ return false;
+ });
+
+ when(rs.getObject(anyString())).thenAnswer(invocation -> {
+ String columnName = invocation.getArgument(0);
+ int currentRow = rowIndex.get() - 1;
+ if (currentRow >= 0 && currentRow < data.size()) {
+ return data.get(currentRow).get(columnName);
+ }
+ return null;
+ });
+
+ when(rs.getString(anyString())).thenAnswer(invocation -> {
+ Object value = rs.getObject(invocation.getArgument(0));
+ return value != null ? value.toString() : null;
+ });
+
+ when(rs.getLong(anyString())).thenAnswer(invocation -> {
+ Object value = rs.getObject(invocation.getArgument(0));
+ if (value instanceof Long) {
+ return (Long) value;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).longValue();
+ }
+ return 0L;
+ });
+
+ when(rs.getInt(anyString())).thenAnswer(invocation -> {
+ Object value = rs.getObject(invocation.getArgument(0));
+ if (value instanceof Integer) {
+ return (Integer) value;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).intValue();
+ }
+ return 0;
+ });
+
+ return rs;
+ }
+}
diff --git a/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/template/PostgresFunctionTemplateTest.java b/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/template/PostgresFunctionTemplateTest.java
new file mode 100644
index 0000000000..ddb65e28af
--- /dev/null
+++ b/libs/db-browser/src/test/java/com/oceanbase/tools/dbbrowser/template/PostgresFunctionTemplateTest.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.tools.dbbrowser.template;
+
+import java.util.Arrays;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import com.oceanbase.tools.dbbrowser.model.DBFunction;
+import com.oceanbase.tools.dbbrowser.model.DBPLParam;
+import com.oceanbase.tools.dbbrowser.model.DBPLParamMode;
+import com.oceanbase.tools.dbbrowser.template.postgre.PostgresFunctionTemplate;
+
+/**
+ * {@link PostgresFunctionTemplateTest}
+ *
+ * @author odc
+ * @since ODC_release_4.3.4
+ */
+public class PostgresFunctionTemplateTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void generateCreateObjectTemplate_basicFunction_generateSucceed() {
+ DBObjectTemplate template = new PostgresFunctionTemplate();
+ DBFunction function = DBFunction.of("f_test", "INTEGER");
+
+ String result = template.generateCreateObjectTemplate(function);
+
+ Assert.assertTrue(result.contains("CREATE OR REPLACE FUNCTION"));
+ Assert.assertTrue(result.contains("\"f_test\""));
+ Assert.assertTrue(result.contains("RETURNS INTEGER"));
+ Assert.assertTrue(result.contains("LANGUAGE plpgsql"));
+ Assert.assertTrue(result.contains("AS $$"));
+ Assert.assertTrue(result.contains("BEGIN"));
+ Assert.assertTrue(result.contains("END;"));
+ Assert.assertTrue(result.contains("$$;"));
+ }
+
+ @Test
+ public void generateCreateObjectTemplate_functionWithoutName_expThrown() {
+ DBObjectTemplate template = new PostgresFunctionTemplate();
+ DBFunction function = new DBFunction();
+
+ thrown.expectMessage("Function name can not be blank");
+ thrown.expect(NullPointerException.class);
+ template.generateCreateObjectTemplate(function);
+ }
+
+ @Test
+ public void generateCreateObjectTemplate_functionWithReturnTypeVarChar_generateSucceed() {
+ DBObjectTemplate template = new PostgresFunctionTemplate();
+ DBFunction function = DBFunction.of("f_get_name", "VARCHAR(100)");
+
+ String result = template.generateCreateObjectTemplate(function);
+
+ Assert.assertTrue(result.contains("RETURNS VARCHAR(100)"));
+ }
+
+ @Test
+ public void generateCreateObjectTemplate_functionWithParameters_generateSucceed() {
+ DBObjectTemplate template = new PostgresFunctionTemplate();
+ DBFunction function = DBFunction.of("f_add", "INTEGER");
+
+ DBPLParam param1 = new DBPLParam();
+ param1.setParamName("p_a");
+ param1.setDataType("INTEGER");
+ param1.setParamMode(DBPLParamMode.IN);
+
+ DBPLParam param2 = new DBPLParam();
+ param2.setParamName("p_b");
+ param2.setDataType("INTEGER");
+ param2.setParamMode(DBPLParamMode.IN);
+
+ function.setParams(Arrays.asList(param1, param2));
+
+ String result = template.generateCreateObjectTemplate(function);
+
+ Assert.assertTrue(result.contains("IN \"p_a\" INTEGER"));
+ Assert.assertTrue(result.contains("IN \"p_b\" INTEGER"));
+ }
+
+ @Test
+ public void generateCreateObjectTemplate_functionWithDefaultParameter_generateSucceed() {
+ DBObjectTemplate template = new PostgresFunctionTemplate();
+ DBFunction function = DBFunction.of("f_greet", "VARCHAR");
+
+ DBPLParam param = new DBPLParam();
+ param.setParamName("p_name");
+ param.setDataType("VARCHAR");
+ param.setParamMode(DBPLParamMode.IN);
+ param.setDefaultValue("'World'");
+
+ function.setParams(Arrays.asList(param));
+
+ String result = template.generateCreateObjectTemplate(function);
+
+ Assert.assertTrue(result.contains("DEFAULT 'World'"));
+ }
+
+ @Test
+ public void generateCreateObjectTemplate_functionWithOutParameter_generateSucceed() {
+ DBObjectTemplate template = new PostgresFunctionTemplate();
+ DBFunction function = DBFunction.of("f_get_user", "RECORD");
+
+ DBPLParam param1 = new DBPLParam();
+ param1.setParamName("p_id");
+ param1.setDataType("INTEGER");
+ param1.setParamMode(DBPLParamMode.IN);
+
+ DBPLParam param2 = new DBPLParam();
+ param2.setParamName("p_name");
+ param2.setDataType("VARCHAR");
+ param2.setParamMode(DBPLParamMode.OUT);
+
+ function.setParams(Arrays.asList(param1, param2));
+
+ String result = template.generateCreateObjectTemplate(function);
+
+ Assert.assertTrue(result.contains("IN \"p_id\" INTEGER"));
+ Assert.assertTrue(result.contains("OUT \"p_name\" VARCHAR"));
+ }
+
+ @Test
+ public void generateCreateObjectTemplate_functionWithInOutParameter_generateSucceed() {
+ DBObjectTemplate