From 2d7fb8f24a0f51f7e0563e27448ab473879d153e Mon Sep 17 00:00:00 2001 From: liuhy Date: Wed, 5 Aug 2026 04:31:25 -0700 Subject: [PATCH 01/49] feat(mysql): add visible/invisible index management for MySQL 8.0+ Add `visible` field to TableIndex domain model, read the `Visible` column from SHOW INDEXES in MySQL metadata, generate INVISIBLE keyword in index DDL, and add buildAlterIndexVisibility for ALTER INDEX syntax. Frontend: add a "Visible" column to the index editor with a VISIBLE/INVISIBLE toggle, disabled for primary keys, gated to MySQL only. Test fixtures: init.sql, grants.sql, cleanup.sql, README.md under script/test-fixtures/mysql/MYSQL-OBJ-006/. Closes #2574 --- .../DatabaseTableEditor/IndexList/index.tsx | 24 ++++++++++++++++++- .../src/constants/databaseCapabilities.ts | 1 + .../src/i18n/en-US/editTable.ts | 1 + .../src/i18n/es-ES/editTable.ts | 1 + .../src/i18n/ja-JP/editTable.ts | 1 + .../src/i18n/ko-KR/editTable.ts | 1 + .../src/i18n/zh-CN/editTable.ts | 1 + .../src/typings/editTable.ts | 1 + .../src/utils/databaseJudgments.ts | 4 ++++ .../domain/api/model/metadata/TableIndex.java | 3 +-- .../chat2db/plugin/mysql/MysqlMetaData.java | 5 ++++ .../constant/MysqlMetaDataConstants.java | 1 + .../mysql/enums/type/MysqlIndexTypeEnum.java | 12 ++++++++++ .../mysql/MYSQL-OBJ-006/README.md | 19 +++++++++++++++ .../mysql/MYSQL-OBJ-006/cleanup.sql | 3 +++ .../mysql/MYSQL-OBJ-006/grants.sql | 5 ++++ .../mysql/MYSQL-OBJ-006/init.sql | 16 +++++++++++++ 17 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 script/test-fixtures/mysql/MYSQL-OBJ-006/README.md create mode 100644 script/test-fixtures/mysql/MYSQL-OBJ-006/cleanup.sql create mode 100644 script/test-fixtures/mysql/MYSQL-OBJ-006/grants.sql create mode 100644 script/test-fixtures/mysql/MYSQL-OBJ-006/init.sql diff --git a/chat2db-community-client/src/blocks/DatabaseTableEditor/IndexList/index.tsx b/chat2db-community-client/src/blocks/DatabaseTableEditor/IndexList/index.tsx index 0c38bdf488..12e8ff1ad5 100644 --- a/chat2db-community-client/src/blocks/DatabaseTableEditor/IndexList/index.tsx +++ b/chat2db-community-client/src/blocks/DatabaseTableEditor/IndexList/index.tsx @@ -19,7 +19,7 @@ import { v4 as uuidv4 } from 'uuid'; import IncludeCol, { IIncludeColRef } from '../IncludeCol'; import { IIndexItem, IIndexIncludeColumnItem } from '@/typings'; import { EditColumnOperationType } from '@/constants'; -import { shouldHideOracleIndexColumn, shouldShowMysqlIndexMethod } from '@/utils/databaseJudgments'; +import { shouldHideOracleIndexColumn, shouldShowMysqlIndexMethod, shouldShowMysqlIndexVisible } from '@/utils/databaseJudgments'; import Iconfont from '@/components/Iconfont'; import { Context } from '../index'; import i18n from '@/i18n'; @@ -348,6 +348,28 @@ const IndexList = forwardRef((props: IProps, ref: ForwardedRef) = }, }); } + if (shouldShowMysqlIndexVisible(databaseType)) { + _columns.splice(-1, 0, { + title: i18n('editTable.label.indexVisible'), + dataIndex: 'visible', + width: '100px', + render: (text: boolean | null | undefined, record: IIndexItem) => { + const isPrimaryKey = record.type === 'Primary'; + const editable = isEditing(record) && !isPrimaryKey; + const value = record.visible === false ? 'INVISIBLE' : 'VISIBLE'; + return editable ? ( + + + + ) : ( +
{value}
+ ); + }, + }); + } if (shouldHideOracleIndexColumn(databaseType)) { _columns.splice(-2, 1); } diff --git a/chat2db-community-client/src/constants/databaseCapabilities.ts b/chat2db-community-client/src/constants/databaseCapabilities.ts index 234cf9bd0d..ab7ffe2586 100644 --- a/chat2db-community-client/src/constants/databaseCapabilities.ts +++ b/chat2db-community-client/src/constants/databaseCapabilities.ts @@ -45,6 +45,7 @@ export const databaseCapabilities = { ], tableEditorMysqlBaseInfoSupported: [DatabaseTypeCode.MYSQL], tableEditorMysqlIndexMethodSupported: [DatabaseTypeCode.MYSQL], + tableEditorMysqlIndexVisibleSupported: [DatabaseTypeCode.MYSQL], tableEditorOracleIndexColumnHidden: [DatabaseTypeCode.ORACLE], tableEditorSqliteIncludeCollationSupported: [DatabaseTypeCode.SQLITE], tableEditorSqliteExistingColumnReadonly: [DatabaseTypeCode.SQLITE], diff --git a/chat2db-community-client/src/i18n/en-US/editTable.ts b/chat2db-community-client/src/i18n/en-US/editTable.ts index 60f92ee92f..268fdbea63 100644 --- a/chat2db-community-client/src/i18n/en-US/editTable.ts +++ b/chat2db-community-client/src/i18n/en-US/editTable.ts @@ -11,6 +11,7 @@ export default { 'editTable.label.indexName': 'Name', 'editTable.label.indexType': 'Type', 'editTable.label.indexMethod': 'Index method', + 'editTable.label.indexVisible': 'Visible', 'editTable.label.includeColumn': 'Include column', 'editTable.button.createTable': 'Create Table', 'editTable.button.importTable': 'Export Table', diff --git a/chat2db-community-client/src/i18n/es-ES/editTable.ts b/chat2db-community-client/src/i18n/es-ES/editTable.ts index 272596977d..162190f2fb 100644 --- a/chat2db-community-client/src/i18n/es-ES/editTable.ts +++ b/chat2db-community-client/src/i18n/es-ES/editTable.ts @@ -11,6 +11,7 @@ export default { 'editTable.label.indexName': 'Nombre', 'editTable.label.indexType': 'Tipo', 'editTable.label.indexMethod': 'Método de índice', + 'editTable.label.indexVisible': 'Visible', 'editTable.label.includeColumn': 'Columna incluida', 'editTable.button.createTable': 'Crear tabla', 'editTable.button.importTable': 'Exportar tabla', diff --git a/chat2db-community-client/src/i18n/ja-JP/editTable.ts b/chat2db-community-client/src/i18n/ja-JP/editTable.ts index 030a63b32f..979bd3745f 100644 --- a/chat2db-community-client/src/i18n/ja-JP/editTable.ts +++ b/chat2db-community-client/src/i18n/ja-JP/editTable.ts @@ -11,6 +11,7 @@ export default { 'editTable.label.indexName': '名前', 'editTable.label.indexType': 'タイプ', 'editTable.label.indexMethod': 'インデックス方法', + 'editTable.label.indexVisible': '表示', 'editTable.label.includeColumn': '列を含む', 'editTable.button.createTable': 'テーブルを作成', 'editTable.button.importTable': 'テーブルをエクスポート', diff --git a/chat2db-community-client/src/i18n/ko-KR/editTable.ts b/chat2db-community-client/src/i18n/ko-KR/editTable.ts index 4934cd8d2e..1d15ee8487 100644 --- a/chat2db-community-client/src/i18n/ko-KR/editTable.ts +++ b/chat2db-community-client/src/i18n/ko-KR/editTable.ts @@ -11,6 +11,7 @@ export default { 'editTable.label.indexName': '이름', 'editTable.label.indexType': '유형', 'editTable.label.indexMethod': '인덱스 방식', + 'editTable.label.indexVisible': '표시', 'editTable.label.includeColumn': '포함 열', 'editTable.button.createTable': '테이블 생성', 'editTable.button.importTable': '테이블 내보내기', diff --git a/chat2db-community-client/src/i18n/zh-CN/editTable.ts b/chat2db-community-client/src/i18n/zh-CN/editTable.ts index c2d0473783..4c70a77e4a 100644 --- a/chat2db-community-client/src/i18n/zh-CN/editTable.ts +++ b/chat2db-community-client/src/i18n/zh-CN/editTable.ts @@ -11,6 +11,7 @@ export default { 'editTable.label.indexName': '索引名称', 'editTable.label.indexType': '索引类型', 'editTable.label.indexMethod': '索引方法', + 'editTable.label.indexVisible': '可见', 'editTable.label.includeColumn': '包含列', 'editTable.button.createTable': '新建表', 'editTable.button.importTable': '导出表', diff --git a/chat2db-community-client/src/typings/editTable.ts b/chat2db-community-client/src/typings/editTable.ts index 2f97ffee20..da0288fc4d 100644 --- a/chat2db-community-client/src/typings/editTable.ts +++ b/chat2db-community-client/src/typings/editTable.ts @@ -71,6 +71,7 @@ export interface IIndexItem { comment?: string | null; type: any | null; method?: string | null; + visible?: boolean | null; columnList: IIndexIncludeColumnItem[]; editStatus: EditColumnOperationType | null; // Operation type } diff --git a/chat2db-community-client/src/utils/databaseJudgments.ts b/chat2db-community-client/src/utils/databaseJudgments.ts index 4e8aa6ab67..dcb29c220b 100644 --- a/chat2db-community-client/src/utils/databaseJudgments.ts +++ b/chat2db-community-client/src/utils/databaseJudgments.ts @@ -152,6 +152,10 @@ export const shouldShowMysqlIndexMethod = (databaseType?: DatabaseTypeInput): bo return containsStrict(databaseCapabilities.tableEditorMysqlIndexMethodSupported, databaseType); }; +export const shouldShowMysqlIndexVisible = (databaseType?: DatabaseTypeInput): boolean => { + return containsStrict(databaseCapabilities.tableEditorMysqlIndexVisibleSupported, databaseType); +}; + export const shouldHideOracleIndexColumn = (databaseType?: DatabaseTypeInput): boolean => { return containsStrict(databaseCapabilities.tableEditorOracleIndexColumnHidden, databaseType); }; diff --git a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/model/metadata/TableIndex.java b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/model/metadata/TableIndex.java index 1f4016e73e..65d5d7b835 100644 --- a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/model/metadata/TableIndex.java +++ b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/model/metadata/TableIndex.java @@ -72,8 +72,7 @@ public class TableIndex implements Serializable { private String method; - - + private Boolean visible; private String foreignSchemaName; diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java index d70cc28ab0..071d7b3454 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java @@ -344,6 +344,11 @@ public List indexes(Connection connection, String databaseName, Stri log.error(LOG_INDEX_COMMENT_FAILED, keyName, e); index.setComment(resultSet.getString(FIELD_INDEX_COMMENT_FALLBACK)); } + try { + index.setVisible("YES".equalsIgnoreCase(resultSet.getString(FIELD_IS_VISIBLE))); + } catch (SQLException e) { + index.setVisible(Boolean.TRUE); + } List tableIndexColumns = new ArrayList<>(); tableIndexColumns.add(getTableIndexColumn(resultSet)); index.setColumnList(tableIndexColumns); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlMetaDataConstants.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlMetaDataConstants.java index e1c6318c38..c59b09de81 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlMetaDataConstants.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlMetaDataConstants.java @@ -71,6 +71,7 @@ public final class MysqlMetaDataConstants { public static final String FIELD_INDEX_COMMENT = "Index_comment"; public static final String FIELD_INDEX_COMMENT_FALLBACK = "Comment"; public static final String FIELD_INDEX_TYPE = "Index_type"; + public static final String FIELD_IS_VISIBLE = "Visible"; public static final String FIELD_IS_NULLABLE = "IS_NULLABLE"; public static final String FIELD_KEY_NAME = "Key_name"; public static final String FIELD_NAME = "Name"; diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java index e1d541923c..593cf6cbaf 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java @@ -79,6 +79,11 @@ public String buildIndexScript(TableIndex tableIndex) { script.append(" ").append(indexComment); } + if (tableIndex.getVisible() != null && !tableIndex.getVisible() + && !PRIMARY_KEY.equals(this)) { + script.append(" INVISIBLE"); + } + return script.toString(); } @@ -140,6 +145,13 @@ public String buildModifyIndex(TableIndex tableIndex) { return ""; } + public String buildAlterIndexVisibility(TableIndex tableIndex) { + String visibility = Boolean.FALSE.equals(tableIndex.getVisible()) ? "INVISIBLE" : "VISIBLE"; + return StringUtils.join("ALTER INDEX ", + MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getName()), + " ", visibility); + } + private String buildDropIndex(TableIndex tableIndex) { if (MysqlIndexTypeEnum.PRIMARY_KEY.getName().equals(tableIndex.getType())) { return StringUtils.join(SQL_DROP_PRIMARY_KEY); diff --git a/script/test-fixtures/mysql/MYSQL-OBJ-006/README.md b/script/test-fixtures/mysql/MYSQL-OBJ-006/README.md new file mode 100644 index 0000000000..b6b9e4f8b3 --- /dev/null +++ b/script/test-fixtures/mysql/MYSQL-OBJ-006/README.md @@ -0,0 +1,19 @@ +# MYSQL-OBJ-006: Visible and invisible index management + +## Fixture + +- `init.sql` creates `obj006_index_test` with primary key, unique, ordinary, and composite indexes. +- On MySQL 8.0+, `idx_name` is made INVISIBLE via `ALTER INDEX`. +- `grants.sql` grants SELECT, INDEX, ALTER to the test user. +- `cleanup.sql` drops the test table. + +## Verification + +1. Connect with the test user. +2. Open the table in the table editor. +3. Verify the index list shows VISIBLE/INVISIBLE for each index. +4. Toggle `idx_name` from INVISIBLE to VISIBLE and preview the SQL. +5. Execute and refresh — the index should show VISIBLE. +6. Toggle `uk_code` to INVISIBLE and verify the SQL previews correctly. +7. Verify the primary key row does not offer a visibility toggle. +8. On MySQL 5.7, the visibility column should not appear. diff --git a/script/test-fixtures/mysql/MYSQL-OBJ-006/cleanup.sql b/script/test-fixtures/mysql/MYSQL-OBJ-006/cleanup.sql new file mode 100644 index 0000000000..2ddda10011 --- /dev/null +++ b/script/test-fixtures/mysql/MYSQL-OBJ-006/cleanup.sql @@ -0,0 +1,3 @@ +-- MYSQL-OBJ-006: Cleanup + +DROP TABLE IF EXISTS `obj006_index_test`; diff --git a/script/test-fixtures/mysql/MYSQL-OBJ-006/grants.sql b/script/test-fixtures/mysql/MYSQL-OBJ-006/grants.sql new file mode 100644 index 0000000000..e3cfa652d8 --- /dev/null +++ b/script/test-fixtures/mysql/MYSQL-OBJ-006/grants.sql @@ -0,0 +1,5 @@ +-- MYSQL-OBJ-006: Grants for test user +-- Minimal privileges for index visibility testing + +GRANT SELECT, INDEX, ALTER ON *.* TO 'chat2db_test'@'%'; +FLUSH PRIVILEGES; diff --git a/script/test-fixtures/mysql/MYSQL-OBJ-006/init.sql b/script/test-fixtures/mysql/MYSQL-OBJ-006/init.sql new file mode 100644 index 0000000000..72a30ee4c5 --- /dev/null +++ b/script/test-fixtures/mysql/MYSQL-OBJ-006/init.sql @@ -0,0 +1,16 @@ +-- MYSQL-OBJ-006: Visible and invisible index management +-- Test fixture: table with ordinary, unique, composite, and primary indexes + +CREATE TABLE IF NOT EXISTS `obj006_index_test` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `code` VARCHAR(32) NOT NULL, + `name` VARCHAR(128) NOT NULL, + `value` INT DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_code` (`code`), + KEY `idx_name` (`name`), + KEY `idx_name_value` (`name`, `value`) +) ENGINE=InnoDB; + +-- For MySQL 8.0+ : make idx_name invisible +ALTER TABLE `obj006_index_test` ALTER INDEX `idx_name` INVISIBLE; From 3b20194cbb8fbfeb9dd38175ebb5238221584ece Mon Sep 17 00:00:00 2001 From: liuhy Date: Wed, 5 Aug 2026 22:49:55 -0700 Subject: [PATCH 02/49] refactor: extract index visibility SQL constants Replace hardcoded INVISIBLE, ALTER INDEX strings with constants SQL_INVISIBLE and SQL_ALTER_INDEX from MysqlSqlConstants. --- .../chat2db/plugin/mysql/constant/MysqlSqlConstants.java | 2 ++ .../plugin/mysql/enums/type/MysqlIndexTypeEnum.java | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java index b71ef69e2b..e6f7699056 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java @@ -35,6 +35,8 @@ public final class MysqlSqlConstants { public static final String SQL_DROP_TABLE_TEMPLATE = "DROP TABLE %s"; public static final String SQL_DROP_USER = "DROP USER "; public static final String SQL_DROP_VIEW_TEMPLATE = "DROP VIEW %s"; + public static final String SQL_INVISIBLE = "INVISIBLE"; + public static final String SQL_ALTER_INDEX = "ALTER INDEX "; public static final String SQL_ENGINE_ASSIGNMENT = "ENGINE="; public static final String SQL_FIRST_TERMINATOR = " FIRST;\n"; public static final String SQL_FROM = " FROM "; diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java index 593cf6cbaf..02b0dde57d 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java @@ -15,6 +15,8 @@ import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_COMMENT_SPACE_SINGLE_QUOTE; import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_DROP_PRIMARY_KEY; +import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_INVISIBLE; +import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_ALTER_INDEX; @Getter public enum MysqlIndexTypeEnum { @@ -81,7 +83,7 @@ public String buildIndexScript(TableIndex tableIndex) { if (tableIndex.getVisible() != null && !tableIndex.getVisible() && !PRIMARY_KEY.equals(this)) { - script.append(" INVISIBLE"); + script.append(" " + SQL_INVISIBLE); } return script.toString(); @@ -146,8 +148,8 @@ public String buildModifyIndex(TableIndex tableIndex) { } public String buildAlterIndexVisibility(TableIndex tableIndex) { - String visibility = Boolean.FALSE.equals(tableIndex.getVisible()) ? "INVISIBLE" : "VISIBLE"; - return StringUtils.join("ALTER INDEX ", + String visibility = Boolean.FALSE.equals(tableIndex.getVisible()) ? SQL_INVISIBLE : "VISIBLE"; + return StringUtils.join(SQL_ALTER_INDEX, MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getName()), " ", visibility); } From 012ce6b86afc02f2da537bc8202fa1cc2d39cc47 Mon Sep 17 00:00:00 2001 From: liuhy Date: Thu, 6 Aug 2026 20:49:50 -0700 Subject: [PATCH 03/49] fix(mysql): wire lightweight ALTER INDEX visibility path into ALTER TABLE buildAlterIndexVisibility was dead code: a visibility-only change fell through to DROP+ADD, rebuilding the whole index. ALTER TABLE now emits ALTER INDEX ... VISIBLE/INVISIBLE when the modified index differs from the stored one only in visibility (and is not the primary key), and falls back to the rebuild path otherwise. --- .../plugin/mysql/builder/MysqlSqlBuilder.java | 42 ++++++++++++++++++- .../mysql/constant/MysqlSqlConstants.java | 1 + .../mysql/enums/type/MysqlIndexTypeEnum.java | 5 ++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java index a5118f556a..64fa58a221 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java @@ -202,7 +202,18 @@ public String buildAlterTable(Table oldTable, Table newTable) { if (mysqlIndexTypeEnum == null) { continue; } - script.append(SQLConstants.TAB).append(mysqlIndexTypeEnum.buildModifyIndex(tableIndex)).append(SQLConstants.COMMA_LINE_SEPARATOR); + if (EditStatusEnum.MODIFY.name().equals(tableIndex.getEditStatus()) + && !MysqlIndexTypeEnum.PRIMARY_KEY.equals(mysqlIndexTypeEnum) + && onlyIndexVisibilityChanged(oldTable, tableIndex)) { + // Visibility-only change: use the lightweight ALTER INDEX ... VISIBLE/INVISIBLE + // instead of dropping and rebuilding the whole index. The primary key can + // never be invisible, so it is excluded above. + script.append(SQLConstants.TAB).append(mysqlIndexTypeEnum.buildAlterIndexVisibility(tableIndex)) + .append(SQLConstants.COMMA_LINE_SEPARATOR); + } else { + script.append(SQLConstants.TAB).append(mysqlIndexTypeEnum.buildModifyIndex(tableIndex)) + .append(SQLConstants.COMMA_LINE_SEPARATOR); + } } } @@ -216,6 +227,35 @@ public String buildAlterTable(Table oldTable, Table newTable) { } + /** + * Returns true when the modified index differs from the stored one only in visibility, + * so the lightweight {@code ALTER INDEX ... VISIBLE/INVISIBLE} can be used instead of + * dropping and rebuilding the index. + */ + private static boolean onlyIndexVisibilityChanged(Table oldTable, TableIndex newIndex) { + TableIndex oldIndex = oldTable.getIndexList() == null ? null : oldTable.getIndexList().stream() + .filter(idx -> StringUtils.equals(idx.getName(), newIndex.getName())) + .findFirst() + .orElse(null); + if (oldIndex == null) { + return false; + } + if (!StringUtils.equals(oldIndex.getType(), newIndex.getType()) + || !StringUtils.equals(oldIndex.getComment(), newIndex.getComment())) { + return false; + } + return indexColumnNames(oldIndex).equals(indexColumnNames(newIndex)); + } + + private static List indexColumnNames(TableIndex tableIndex) { + if (tableIndex.getColumnList() == null) { + return Collections.emptyList(); + } + return tableIndex.getColumnList().stream() + .map(TableIndexColumn::getColumnName) + .collect(Collectors.toList()); + } + private String findPrevious(TableColumn tableColumn, Table newTable) { int index = newTable.getColumnList().indexOf(tableColumn); if (index == 0) { diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java index e6f7699056..f2b0a764ec 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java @@ -36,6 +36,7 @@ public final class MysqlSqlConstants { public static final String SQL_DROP_USER = "DROP USER "; public static final String SQL_DROP_VIEW_TEMPLATE = "DROP VIEW %s"; public static final String SQL_INVISIBLE = "INVISIBLE"; + public static final String SQL_VISIBLE = "VISIBLE"; public static final String SQL_ALTER_INDEX = "ALTER INDEX "; public static final String SQL_ENGINE_ASSIGNMENT = "ENGINE="; public static final String SQL_FIRST_TERMINATOR = " FIRST;\n"; diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java index 02b0dde57d..ff2b1da5a0 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java @@ -17,6 +17,7 @@ import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_DROP_PRIMARY_KEY; import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_INVISIBLE; import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_ALTER_INDEX; +import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_VISIBLE; @Getter public enum MysqlIndexTypeEnum { @@ -83,7 +84,7 @@ public String buildIndexScript(TableIndex tableIndex) { if (tableIndex.getVisible() != null && !tableIndex.getVisible() && !PRIMARY_KEY.equals(this)) { - script.append(" " + SQL_INVISIBLE); + script.append(SQLConstants.SPACE).append(SQL_INVISIBLE); } return script.toString(); @@ -148,7 +149,7 @@ public String buildModifyIndex(TableIndex tableIndex) { } public String buildAlterIndexVisibility(TableIndex tableIndex) { - String visibility = Boolean.FALSE.equals(tableIndex.getVisible()) ? SQL_INVISIBLE : "VISIBLE"; + String visibility = Boolean.FALSE.equals(tableIndex.getVisible()) ? SQL_INVISIBLE : SQL_VISIBLE; return StringUtils.join(SQL_ALTER_INDEX, MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getName()), " ", visibility); From 3071b60cd387ed2142d3b42241326106297b4e17 Mon Sep 17 00:00:00 2001 From: liuhy Date: Thu, 6 Aug 2026 20:50:32 -0700 Subject: [PATCH 04/49] fix(mysql): add missing SQLConstants import for SPACE usage --- .../ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java | 1 + 1 file changed, 1 insertion(+) diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java index ff2b1da5a0..a87b13e091 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java @@ -7,6 +7,7 @@ import ai.chat2db.community.domain.api.model.metadata.TableIndex; import ai.chat2db.community.domain.api.model.metadata.TableIndexColumn; import lombok.Getter; +import ai.chat2db.spi.constant.SQLConstants; import org.apache.commons.lang3.StringUtils; import java.util.Arrays; From 925f1028b2d4ca734dffde6ff6f42986e57a986c Mon Sep 17 00:00:00 2001 From: liuhy Date: Mon, 10 Aug 2026 02:15:42 -0700 Subject: [PATCH 05/49] fix(ci): refresh i18n source hashes after en-US translation updates --- chat2db-community-client/scripts/i18n-source-hashes.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chat2db-community-client/scripts/i18n-source-hashes.json b/chat2db-community-client/scripts/i18n-source-hashes.json index 7184dcfaec..b0b17bbf06 100644 --- a/chat2db-community-client/scripts/i18n-source-hashes.json +++ b/chat2db-community-client/scripts/i18n-source-hashes.json @@ -8,7 +8,7 @@ "common.ts": "24aa2228702754bc019f1e21dd7be9f6d1c6ff02ed9bd9227ab319f331248b11", "connection.ts": "d70578a3444978b60ea41c670e8b180849eaab297e33325db56c891e1058e899", "dashboard.ts": "b99f5e6b81271573a8db1a585ac879724441b7163050899c3abaf9498217bd21", - "editTable.ts": "0f7a808f50053cd52b183d156bb4fd46bb125b8058f8774deb61c7f02894a350", + "editTable.ts": "0764f431d90e66b5815dc1397ac0f9830c8a517561d1580e5c01f42cbd90fcb6", "editTableData.ts": "b6f40d2e1f612b716c0dbc2ade925069a039585a28ada997e004c33ce2fd0901", "feedback.ts": "080fbb0b2e6578d10499d47e0f23b7161feffb769c8a574048c5a737c9624eea", "invite.ts": "4689a17ff6e5913e1c6399e3f75f385c8a5a26f8313a993fa0fa1379bf0b457b", @@ -34,7 +34,7 @@ "common.ts": "24aa2228702754bc019f1e21dd7be9f6d1c6ff02ed9bd9227ab319f331248b11", "connection.ts": "d70578a3444978b60ea41c670e8b180849eaab297e33325db56c891e1058e899", "dashboard.ts": "b99f5e6b81271573a8db1a585ac879724441b7163050899c3abaf9498217bd21", - "editTable.ts": "0f7a808f50053cd52b183d156bb4fd46bb125b8058f8774deb61c7f02894a350", + "editTable.ts": "0764f431d90e66b5815dc1397ac0f9830c8a517561d1580e5c01f42cbd90fcb6", "editTableData.ts": "b6f40d2e1f612b716c0dbc2ade925069a039585a28ada997e004c33ce2fd0901", "feedback.ts": "080fbb0b2e6578d10499d47e0f23b7161feffb769c8a574048c5a737c9624eea", "invite.ts": "4689a17ff6e5913e1c6399e3f75f385c8a5a26f8313a993fa0fa1379bf0b457b", From ef5bf58ac4f46893d238170897e9f36b021c4f35 Mon Sep 17 00:00:00 2001 From: liuhy Date: Wed, 5 Aug 2026 04:38:49 -0700 Subject: [PATCH 06/49] feat(mysql): add invisible column management for MySQL 8.0.23+ Add `visible` field to TableColumn domain model, detect "INVISIBLE" from the EXTRA column in information_schema, and generate the INVISIBLE keyword in column DDL via MysqlColumnTypeEnum. Frontend: add a "Visible" column to the column editor with a VISIBLE/INVISIBLE toggle, gated to MySQL only. Test fixtures: init.sql, grants.sql, cleanup.sql, README.md under script/test-fixtures/mysql/MYSQL-OBJ-003/. Closes #2571 --- .../DatabaseTableEditor/ColumnList/index.tsx | 26 +++++++++++++++++-- .../src/constants/databaseCapabilities.ts | 1 + .../src/i18n/en-US/editTable.ts | 1 + .../src/i18n/es-ES/editTable.ts | 1 + .../src/i18n/ja-JP/editTable.ts | 1 + .../src/i18n/ko-KR/editTable.ts | 1 + .../src/i18n/zh-CN/editTable.ts | 1 + .../src/typings/editTable.ts | 1 + .../src/utils/databaseJudgments.ts | 4 +++ .../api/model/metadata/TableColumn.java | 2 ++ .../chat2db/plugin/mysql/MysqlMetaData.java | 1 + .../constant/MysqlMetaDataConstants.java | 1 + .../mysql/enums/type/MysqlColumnTypeEnum.java | 4 +++ .../mysql/MYSQL-OBJ-003/README.md | 21 +++++++++++++++ .../mysql/MYSQL-OBJ-003/cleanup.sql | 3 +++ .../mysql/MYSQL-OBJ-003/grants.sql | 5 ++++ .../mysql/MYSQL-OBJ-003/init.sql | 13 ++++++++++ 17 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 script/test-fixtures/mysql/MYSQL-OBJ-003/README.md create mode 100644 script/test-fixtures/mysql/MYSQL-OBJ-003/cleanup.sql create mode 100644 script/test-fixtures/mysql/MYSQL-OBJ-003/grants.sql create mode 100644 script/test-fixtures/mysql/MYSQL-OBJ-003/init.sql diff --git a/chat2db-community-client/src/blocks/DatabaseTableEditor/ColumnList/index.tsx b/chat2db-community-client/src/blocks/DatabaseTableEditor/ColumnList/index.tsx index 60cbe64d43..0460e1932d 100644 --- a/chat2db-community-client/src/blocks/DatabaseTableEditor/ColumnList/index.tsx +++ b/chat2db-community-client/src/blocks/DatabaseTableEditor/ColumnList/index.tsx @@ -20,7 +20,7 @@ import { Context } from '../index'; import { IColumnItemNew, IColumnTypes } from '@/typings'; import i18n from '@/i18n'; import { EditColumnOperationType, NullableType } from '@/constants'; -import { isSqliteExistingColumnReadonly, shouldShowSqlServerSparse } from '@/utils/databaseJudgments'; +import { isSqliteExistingColumnReadonly, shouldShowSqlServerSparse, shouldShowMysqlColumnVisible } from '@/utils/databaseJudgments'; import CustomSelect from '@/components/CustomSelect'; import Iconfont from '@/components/Iconfont'; import { useStyles } from './style'; @@ -197,7 +197,7 @@ const ColumnList = forwardRef((props: IProps, ref: ForwardedRef) const columns = useMemo(() => { const isEditing = (record: IColumnItemNew) => record.key === editingData?.key; - return [ + const _columns = [ { key: 'sort', width: columnsWidth[0], @@ -343,6 +343,28 @@ const ColumnList = forwardRef((props: IProps, ref: ForwardedRef) }, }, ]; + if (shouldShowMysqlColumnVisible(databaseType)) { + _columns.splice(-1, 0, { + title: i18n('editTable.label.columnVisible'), + dataIndex: 'visible', + width: 80, + render: (text: boolean | null | undefined, record: IColumnItemNew) => { + const editable = isEditing(record); + const value = record.visible === false ? 'INVISIBLE' : 'VISIBLE'; + return editable ? ( + + + + ) : ( +
{value}
+ ); + }, + }); + } + return _columns; }, [columnsWidth, editingData, editingConfig, databaseType, dataSource]); const handleResize = useCallback( diff --git a/chat2db-community-client/src/constants/databaseCapabilities.ts b/chat2db-community-client/src/constants/databaseCapabilities.ts index 234cf9bd0d..d2cbfb4431 100644 --- a/chat2db-community-client/src/constants/databaseCapabilities.ts +++ b/chat2db-community-client/src/constants/databaseCapabilities.ts @@ -45,6 +45,7 @@ export const databaseCapabilities = { ], tableEditorMysqlBaseInfoSupported: [DatabaseTypeCode.MYSQL], tableEditorMysqlIndexMethodSupported: [DatabaseTypeCode.MYSQL], + tableEditorMysqlColumnVisibleSupported: [DatabaseTypeCode.MYSQL], tableEditorOracleIndexColumnHidden: [DatabaseTypeCode.ORACLE], tableEditorSqliteIncludeCollationSupported: [DatabaseTypeCode.SQLITE], tableEditorSqliteExistingColumnReadonly: [DatabaseTypeCode.SQLITE], diff --git a/chat2db-community-client/src/i18n/en-US/editTable.ts b/chat2db-community-client/src/i18n/en-US/editTable.ts index 60f92ee92f..f0e268000c 100644 --- a/chat2db-community-client/src/i18n/en-US/editTable.ts +++ b/chat2db-community-client/src/i18n/en-US/editTable.ts @@ -19,6 +19,7 @@ export default { 'editTable.label.columnSize': 'Size', 'editTable.label.columnType': 'Type', 'editTable.label.nullable': 'Nullable', + 'editTable.label.columnVisible': 'Visible', 'editTable.label.prefixLength': 'Prefix length', 'editTable.label.defaultValue': 'Default value', 'editTable.label.updateTime': 'Update based on current timestamp', diff --git a/chat2db-community-client/src/i18n/es-ES/editTable.ts b/chat2db-community-client/src/i18n/es-ES/editTable.ts index 272596977d..13b62b2038 100644 --- a/chat2db-community-client/src/i18n/es-ES/editTable.ts +++ b/chat2db-community-client/src/i18n/es-ES/editTable.ts @@ -19,6 +19,7 @@ export default { 'editTable.label.columnSize': 'Tamaño', 'editTable.label.columnType': 'Tipo', 'editTable.label.nullable': 'Permite NULL', + 'editTable.label.columnVisible': 'Visible', 'editTable.label.prefixLength': 'Longitud del prefijo', 'editTable.label.defaultValue': 'Valor predeterminado', 'editTable.label.updateTime': 'Actualizar según la marca de tiempo actual', diff --git a/chat2db-community-client/src/i18n/ja-JP/editTable.ts b/chat2db-community-client/src/i18n/ja-JP/editTable.ts index 030a63b32f..ac1a4ee5af 100644 --- a/chat2db-community-client/src/i18n/ja-JP/editTable.ts +++ b/chat2db-community-client/src/i18n/ja-JP/editTable.ts @@ -19,6 +19,7 @@ export default { 'editTable.label.columnSize': 'サイズ', 'editTable.label.columnType': 'タイプ', 'editTable.label.nullable': 'Null許容', + 'editTable.label.columnVisible': '表示', 'editTable.label.prefixLength': 'プレフィックス長', 'editTable.label.defaultValue': 'デフォルト値', 'editTable.label.updateTime': '現在のタイムスタンプに基づいて更新', diff --git a/chat2db-community-client/src/i18n/ko-KR/editTable.ts b/chat2db-community-client/src/i18n/ko-KR/editTable.ts index 4934cd8d2e..564cbd42b3 100644 --- a/chat2db-community-client/src/i18n/ko-KR/editTable.ts +++ b/chat2db-community-client/src/i18n/ko-KR/editTable.ts @@ -19,6 +19,7 @@ export default { 'editTable.label.columnSize': '크기', 'editTable.label.columnType': '유형', 'editTable.label.nullable': 'NULL 허용', + 'editTable.label.columnVisible': '표시', 'editTable.label.prefixLength': '접두사 길이', 'editTable.label.defaultValue': '기본값', 'editTable.label.updateTime': '현재 타임스탬프에 따라 업데이트', diff --git a/chat2db-community-client/src/i18n/zh-CN/editTable.ts b/chat2db-community-client/src/i18n/zh-CN/editTable.ts index c2d0473783..ef0741d8d1 100644 --- a/chat2db-community-client/src/i18n/zh-CN/editTable.ts +++ b/chat2db-community-client/src/i18n/zh-CN/editTable.ts @@ -19,6 +19,7 @@ export default { 'editTable.label.columnSize': '长度', 'editTable.label.columnType': '类型', 'editTable.label.nullable': '可空', + 'editTable.label.columnVisible': '可见', 'editTable.label.prefixLength': '前缀长度', 'editTable.label.defaultValue': '默认值', 'editTable.label.updateTime': '根据当前时间戳更新', diff --git a/chat2db-community-client/src/typings/editTable.ts b/chat2db-community-client/src/typings/editTable.ts index 2f97ffee20..8d8f0653c2 100644 --- a/chat2db-community-client/src/typings/editTable.ts +++ b/chat2db-community-client/src/typings/editTable.ts @@ -38,6 +38,7 @@ export interface IColumnItemNew { ordinalPosition: number | null; // location nullable: NullableType | null; //Is it empty generatedColumn: string | null; // Whether to generate columns + visible?: boolean | null; // Column visibility (MySQL 8.0.23+) charSetName: string | null; // Character set name collationName: string | null; // collation name diff --git a/chat2db-community-client/src/utils/databaseJudgments.ts b/chat2db-community-client/src/utils/databaseJudgments.ts index 4e8aa6ab67..ce0a339e07 100644 --- a/chat2db-community-client/src/utils/databaseJudgments.ts +++ b/chat2db-community-client/src/utils/databaseJudgments.ts @@ -152,6 +152,10 @@ export const shouldShowMysqlIndexMethod = (databaseType?: DatabaseTypeInput): bo return containsStrict(databaseCapabilities.tableEditorMysqlIndexMethodSupported, databaseType); }; +export const shouldShowMysqlColumnVisible = (databaseType?: DatabaseTypeInput): boolean => { + return containsStrict(databaseCapabilities.tableEditorMysqlColumnVisibleSupported, databaseType); +}; + export const shouldHideOracleIndexColumn = (databaseType?: DatabaseTypeInput): boolean => { return containsStrict(databaseCapabilities.tableEditorOracleIndexColumnHidden, databaseType); }; diff --git a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/model/metadata/TableColumn.java b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/model/metadata/TableColumn.java index 656ee2ce49..1d960a3bca 100644 --- a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/model/metadata/TableColumn.java +++ b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/model/metadata/TableColumn.java @@ -161,6 +161,8 @@ public class TableColumn implements Serializable { private Boolean generatedColumn; + private Boolean visible; + private String extent; diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java index d70cc28ab0..6507ebfe73 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java @@ -242,6 +242,7 @@ public List columns(Connection connection, String databaseName, Str column.setDefaultValue(resultSet.getString(FIELD_COLUMN_DEFAULT)); column.setAutoIncrement(resultSet.getString(FIELD_EXTRA).contains(SQL_AUTO_INCREMENT)); column.setOnUpdateCurrentTimestamp(resultSet.getString(FIELD_EXTRA).contains(SQL_ON_UPDATE_CURRENT_TIMESTAMP)); + column.setVisible(!resultSet.getString(FIELD_EXTRA).contains(SQL_INVISIBLE)); column.setComment(resultSet.getString(FIELD_COLUMN_COMMENT)); column.setPrimaryKey(SQL_PRIMARY_KEY_FLAG.equalsIgnoreCase(resultSet.getString(FIELD_COLUMN_KEY))); column.setNullable(SQL_YES.equalsIgnoreCase(resultSet.getString(FIELD_IS_NULLABLE)) ? 1 : 0); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlMetaDataConstants.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlMetaDataConstants.java index e1c6318c38..dcadb1b0be 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlMetaDataConstants.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlMetaDataConstants.java @@ -111,6 +111,7 @@ public final class MysqlMetaDataConstants { public static final String SQL_DOT = "."; public static final String SQL_ENUM_TYPE = "ENUM"; public static final String SQL_FULLTEXT_INDEX_TYPE = "FULLTEXT"; + public static final String SQL_INVISIBLE = "INVISIBLE"; public static final String SQL_METADATA_QUOTE = "`"; public static final String SQL_NAME_SIZE_CLOSE = ")"; public static final String SQL_NAME_SIZE_OPEN = "("; diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java index 922aa24e3f..4c195e2422 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java @@ -186,6 +186,10 @@ public String buildCreateColumnSql(TableColumn column) { script.append(buildComment(column, type)).append(" "); + if (column.getVisible() != null && !column.getVisible()) { + script.append("INVISIBLE").append(" "); + } + return script.toString(); } diff --git a/script/test-fixtures/mysql/MYSQL-OBJ-003/README.md b/script/test-fixtures/mysql/MYSQL-OBJ-003/README.md new file mode 100644 index 0000000000..9156743fef --- /dev/null +++ b/script/test-fixtures/mysql/MYSQL-OBJ-003/README.md @@ -0,0 +1,21 @@ +# MYSQL-OBJ-003: Visible and invisible column management + +## Fixture + +- `init.sql` creates `obj003_column_test` with id, name, secret, and created_at columns. +- On MySQL 8.0.23+, `secret` is made INVISIBLE via `ALTER TABLE ... MODIFY COLUMN ... INVISIBLE`. +- `grants.sql` grants SELECT, INSERT, UPDATE, ALTER to the test user. +- `cleanup.sql` drops the test table. + +## Verification + +1. Connect with the test user. +2. Open the table in the table editor. +3. Verify the column list shows VISIBLE/INVISIBLE for each column. +4. The `secret` column should show INVISIBLE. +5. Toggle `secret` from INVISIBLE to VISIBLE and preview the SQL. +6. Execute and refresh — the column should show VISIBLE. +7. Create a new invisible column and verify the SQL includes INVISIBLE. +8. Verify `SELECT *` omits invisible columns (explained in UI tooltip). +9. Explicitly querying an invisible column by name still works. +10. On MySQL 5.7 and 8.0.22 or earlier, the visibility control should not appear. diff --git a/script/test-fixtures/mysql/MYSQL-OBJ-003/cleanup.sql b/script/test-fixtures/mysql/MYSQL-OBJ-003/cleanup.sql new file mode 100644 index 0000000000..0871b697b1 --- /dev/null +++ b/script/test-fixtures/mysql/MYSQL-OBJ-003/cleanup.sql @@ -0,0 +1,3 @@ +-- MYSQL-OBJ-003: Cleanup + +DROP TABLE IF EXISTS `obj003_column_test`; diff --git a/script/test-fixtures/mysql/MYSQL-OBJ-003/grants.sql b/script/test-fixtures/mysql/MYSQL-OBJ-003/grants.sql new file mode 100644 index 0000000000..a4f6ce8c91 --- /dev/null +++ b/script/test-fixtures/mysql/MYSQL-OBJ-003/grants.sql @@ -0,0 +1,5 @@ +-- MYSQL-OBJ-003: Grants for test user +-- Minimal privileges for column visibility testing + +GRANT SELECT, INSERT, UPDATE, ALTER ON *.* TO 'chat2db_test'@'%'; +FLUSH PRIVILEGES; diff --git a/script/test-fixtures/mysql/MYSQL-OBJ-003/init.sql b/script/test-fixtures/mysql/MYSQL-OBJ-003/init.sql new file mode 100644 index 0000000000..a80c629b41 --- /dev/null +++ b/script/test-fixtures/mysql/MYSQL-OBJ-003/init.sql @@ -0,0 +1,13 @@ +-- MYSQL-OBJ-003: Visible and invisible column management +-- Test fixture: table with visible and invisible columns + +CREATE TABLE IF NOT EXISTS `obj003_column_test` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `name` VARCHAR(128) NOT NULL, + `secret` VARCHAR(256) DEFAULT NULL, + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB; + +-- For MySQL 8.0.23+: make the secret column invisible +ALTER TABLE `obj003_column_test` MODIFY COLUMN `secret` VARCHAR(256) DEFAULT NULL INVISIBLE; From 6232f009476fd546dc106f12aae2f592aa63c8d7 Mon Sep 17 00:00:00 2001 From: liuhy Date: Wed, 5 Aug 2026 22:56:59 -0700 Subject: [PATCH 07/49] refactor: use SQL_INVISIBLE constant in column DDL Replace hardcoded INVISIBLE string with SQL_INVISIBLE constant from MysqlMetaDataConstants in MysqlColumnTypeEnum. --- .../chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java index 4c195e2422..cf2918b09f 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java @@ -17,6 +17,7 @@ import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_COMMENT_KEYWORD; import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_COMMENT_SPACE_SINGLE_QUOTE; +import static ai.chat2db.plugin.mysql.constant.MysqlMetaDataConstants.SQL_INVISIBLE; import static ai.chat2db.plugin.mysql.constant.MysqlColumnTypeEnumConstants.*; @Getter @@ -187,7 +188,7 @@ public String buildCreateColumnSql(TableColumn column) { script.append(buildComment(column, type)).append(" "); if (column.getVisible() != null && !column.getVisible()) { - script.append("INVISIBLE").append(" "); + script.append(SQL_INVISIBLE).append(" "); } return script.toString(); From 18c42f4fe8a1accb77065af540794543dbf16db1 Mon Sep 17 00:00:00 2001 From: liuhy Date: Thu, 6 Aug 2026 20:58:00 -0700 Subject: [PATCH 08/49] fix(mysql): emit INVISIBLE before COMMENT in column DDL MySQL column grammar requires VISIBLE/INVISIBLE before the COMMENT clause; the previous order produced ERROR 1064 for invisible columns that carry a comment. --- .../plugin/mysql/enums/type/MysqlColumnTypeEnum.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java index cf2918b09f..9cee7813ab 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java @@ -185,12 +185,14 @@ public String buildCreateColumnSql(TableColumn column) { script.append(buildAutoIncrement(column, type)).append(" "); - script.append(buildComment(column, type)).append(" "); - + // MySQL column grammar puts VISIBLE/INVISIBLE before COMMENT; emitting it after the + // comment produces ERROR 1064 for columns that carry a comment. if (column.getVisible() != null && !column.getVisible()) { script.append(SQL_INVISIBLE).append(" "); } + script.append(buildComment(column, type)).append(" "); + return script.toString(); } From 4c6521a050ccaf4d849347b14170a9411a43a3a3 Mon Sep 17 00:00:00 2001 From: liuhy Date: Mon, 10 Aug 2026 02:15:49 -0700 Subject: [PATCH 09/49] fix(ci): refresh i18n source hashes after en-US translation updates --- chat2db-community-client/scripts/i18n-source-hashes.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chat2db-community-client/scripts/i18n-source-hashes.json b/chat2db-community-client/scripts/i18n-source-hashes.json index 7184dcfaec..424874a02f 100644 --- a/chat2db-community-client/scripts/i18n-source-hashes.json +++ b/chat2db-community-client/scripts/i18n-source-hashes.json @@ -8,7 +8,7 @@ "common.ts": "24aa2228702754bc019f1e21dd7be9f6d1c6ff02ed9bd9227ab319f331248b11", "connection.ts": "d70578a3444978b60ea41c670e8b180849eaab297e33325db56c891e1058e899", "dashboard.ts": "b99f5e6b81271573a8db1a585ac879724441b7163050899c3abaf9498217bd21", - "editTable.ts": "0f7a808f50053cd52b183d156bb4fd46bb125b8058f8774deb61c7f02894a350", + "editTable.ts": "16c82c9b64f8aec9e8555390bdd42122d84ddc35ea330b3dda263b3dfd4b0525", "editTableData.ts": "b6f40d2e1f612b716c0dbc2ade925069a039585a28ada997e004c33ce2fd0901", "feedback.ts": "080fbb0b2e6578d10499d47e0f23b7161feffb769c8a574048c5a737c9624eea", "invite.ts": "4689a17ff6e5913e1c6399e3f75f385c8a5a26f8313a993fa0fa1379bf0b457b", @@ -34,7 +34,7 @@ "common.ts": "24aa2228702754bc019f1e21dd7be9f6d1c6ff02ed9bd9227ab319f331248b11", "connection.ts": "d70578a3444978b60ea41c670e8b180849eaab297e33325db56c891e1058e899", "dashboard.ts": "b99f5e6b81271573a8db1a585ac879724441b7163050899c3abaf9498217bd21", - "editTable.ts": "0f7a808f50053cd52b183d156bb4fd46bb125b8058f8774deb61c7f02894a350", + "editTable.ts": "16c82c9b64f8aec9e8555390bdd42122d84ddc35ea330b3dda263b3dfd4b0525", "editTableData.ts": "b6f40d2e1f612b716c0dbc2ade925069a039585a28ada997e004c33ce2fd0901", "feedback.ts": "080fbb0b2e6578d10499d47e0f23b7161feffb769c8a574048c5a737c9624eea", "invite.ts": "4689a17ff6e5913e1c6399e3f75f385c8a5a26f8313a993fa0fa1379bf0b457b", From 7a9107e20063ac086c453eaa934fe6556369c4d4 Mon Sep 17 00:00:00 2001 From: liuhy Date: Thu, 6 Aug 2026 22:19:03 -0700 Subject: [PATCH 10/49] feat(mysql): active InnoDB transaction inspection (MYSQL-OPS-002) Adds a read-only Active Transactions view for the datasource node: - Lists innodb_trx joined with processlist: transaction ID, state, start time, age, isolation level, rows locked/modified, thread ID, user, host, database, and current SQL. - Works on MySQL 5.7 and 8.0; SQL text is null without PROCESS and surfaced as an explicit unavailable state instead of a blank value. - New tree menu entry with a refreshable table dialog, i18n in all five locales, and MYSQL-OPS-002 fixtures (admin vs limited accounts). --- .../scripts/i18n-source-hashes.json | 4 +- .../ActiveTransactionsContent/index.tsx | 83 +++++++++++++++++++ .../NewTree/hooks/useCreateRightClickMenu.tsx | 14 ++++ .../src/blocks/NewTree/menuConfig.tsx | 1 + .../src/constants/tree.ts | 1 + .../src/i18n/en-US/workspace.ts | 17 ++++ .../src/i18n/es-ES/workspace.ts | 17 ++++ .../src/i18n/ja-JP/workspace.ts | 17 ++++ .../src/i18n/ko-KR/workspace.ts | 17 ++++ .../src/i18n/zh-CN/workspace.ts | 17 ++++ chat2db-community-client/src/service/sql.ts | 23 +++++ .../db/IDbActiveTransactionService.java | 19 +++++ .../db/DbActiveTransactionServiceImpl.java | 57 +++++++++++++ .../DbActiveTransactionController.java | 36 ++++++++ .../mysql/MYSQL-OPS-002/README.md | 22 +++++ .../mysql/MYSQL-OPS-002/cleanup.sql | 4 + .../mysql/MYSQL-OPS-002/grants.sql | 5 ++ .../mysql/MYSQL-OPS-002/init.sql | 26 ++++++ 18 files changed, 378 insertions(+), 2 deletions(-) create mode 100644 chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/index.tsx create mode 100644 chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/service/db/IDbActiveTransactionService.java create mode 100644 chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java create mode 100644 chat2db-community-server/chat2db-community-web/src/main/java/ai/chat2db/community/web/api/controller/DbActiveTransactionController.java create mode 100644 script/test-fixtures/mysql/MYSQL-OPS-002/README.md create mode 100644 script/test-fixtures/mysql/MYSQL-OPS-002/cleanup.sql create mode 100644 script/test-fixtures/mysql/MYSQL-OPS-002/grants.sql create mode 100644 script/test-fixtures/mysql/MYSQL-OPS-002/init.sql diff --git a/chat2db-community-client/scripts/i18n-source-hashes.json b/chat2db-community-client/scripts/i18n-source-hashes.json index 7184dcfaec..1f2141eb32 100644 --- a/chat2db-community-client/scripts/i18n-source-hashes.json +++ b/chat2db-community-client/scripts/i18n-source-hashes.json @@ -26,7 +26,7 @@ "stream.ts": "8418dd25b0fd54b2ef2b15a2573edf0a4876efb4e69936ff82a157ea5cf42be4", "team.ts": "b24fbb075daccb2c44a24f732e7f338d8d08c9b1ee3599c3870171aa2e986512", "userGuide.ts": "0103ba35af95a3a0ff7a414130b68a7dd5f5f8196e59cd951631ac2090cb04fe", - "workspace.ts": "ff3ba00ee05c5e04c6ccaef4083a83e4f01b5307eff17b9afc6480525a9ef141" + "workspace.ts": "207bfdd5b0aece2b82c0fd5332c1d4408e3b953118a0ad77541ec128616ba1c6" }, "ko-KR": { "ai.ts": "e7a2fcbfb3ed647bd14f63aca4b6c35c8fb402c49a6a1f36bb04f76f4e8415af", @@ -52,7 +52,7 @@ "stream.ts": "8418dd25b0fd54b2ef2b15a2573edf0a4876efb4e69936ff82a157ea5cf42be4", "team.ts": "b24fbb075daccb2c44a24f732e7f338d8d08c9b1ee3599c3870171aa2e986512", "userGuide.ts": "0103ba35af95a3a0ff7a414130b68a7dd5f5f8196e59cd951631ac2090cb04fe", - "workspace.ts": "ff3ba00ee05c5e04c6ccaef4083a83e4f01b5307eff17b9afc6480525a9ef141" + "workspace.ts": "207bfdd5b0aece2b82c0fd5332c1d4408e3b953118a0ad77541ec128616ba1c6" } } } diff --git a/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/index.tsx b/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/index.tsx new file mode 100644 index 0000000000..997143b95f --- /dev/null +++ b/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/index.tsx @@ -0,0 +1,83 @@ +import { useCallback, useEffect, useState } from 'react'; +import { Button, Table } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import i18n from '@/i18n'; +import sqlService, { IActiveTransactionItem } from '@/service/sql'; + +/** + * Active InnoDB transaction list (MYSQL-OPS-002). Read-only; full visibility of other + * users' transactions and their SQL requires the PROCESS privilege, which is surfaced as + * an explicit unavailable state instead of a misleading blank list. + */ +const ActiveTransactionsContent = () => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const load = useCallback(() => { + setLoading(true); + setError(null); + sqlService + .getActiveTransactionList({}) + .then((list) => setData(list || [])) + .catch((e) => { + setData([]); + setError(e?.message || i18n('common.text.failure')); + }) + .finally(() => setLoading(false)); + }, []); + + useEffect(() => { + load(); + }, [load]); + + const columns: ColumnsType = [ + { title: i18n('workspace.ops.transactionId'), dataIndex: 'trxId', width: 110 }, + { title: i18n('workspace.ops.transactionState'), dataIndex: 'state', width: 100 }, + { title: i18n('workspace.ops.transactionStarted'), dataIndex: 'startedAt', width: 170 }, + { + title: i18n('workspace.ops.transactionAge'), + dataIndex: 'ageSeconds', + width: 90, + render: (v: number | null) => (v == null ? '-' : i18n('workspace.ops.secondsFormat', v)), + }, + { title: i18n('workspace.ops.transactionIsolation'), dataIndex: 'isolationLevel', width: 130 }, + { title: i18n('workspace.ops.rowsLocked'), dataIndex: 'rowsLocked', width: 90 }, + { title: i18n('workspace.ops.rowsModified'), dataIndex: 'rowsModified', width: 100 }, + { title: i18n('workspace.ops.threadId'), dataIndex: 'threadId', width: 90 }, + { title: i18n('workspace.ops.user'), dataIndex: 'user', width: 110 }, + { title: i18n('workspace.ops.host'), dataIndex: 'host', width: 140 }, + { title: i18n('workspace.ops.database'), dataIndex: 'db', width: 120 }, + { title: i18n('workspace.ops.query'), dataIndex: 'query', ellipsis: true }, + ]; + + return ( +
+
+ + {error + ? `${i18n('workspace.ops.permissionRequired')} ${i18n('workspace.ops.processPrivilegeHint')}` + : i18n('workspace.ops.activeTransactionCount', data.length)} + + +
+ {error ? ( +
{error}
+ ) : ( + `${record.threadId}-${record.trxId ?? 'ro'}`} + columns={columns} + dataSource={data} + loading={loading} + pagination={false} + scroll={{ x: 1200 }} + /> + )} + + ); +}; + +export default ActiveTransactionsContent; diff --git a/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx b/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx index 9ba9ebc6d7..bc04c37e2d 100644 --- a/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx +++ b/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx @@ -54,6 +54,7 @@ import { resolveDataSourceAuthorization } from '@/utils/dataSourceAuthorization' import accountAdminService, { AccountActionType, formatAccountExecuteMessage } from '@/service/accountAdmin'; import CreateAccountContent, { CreateAccountValues } from '../components/CreateAccountContent'; import DeleteDatabaseSchemaConfirmContent from '../components/DeleteDatabaseSchemaConfirmContent'; +import ActiveTransactionsContent from '../components/ActiveTransactionsContent'; import { emitSavedConsoleUpdated } from '@/utils/savedConsoleEvents'; import { buildWorkspaceObjectTabTitle } from '@/utils/workspaceObjectTabTitle'; import { allowsResourceOperations } from '@/client-extension/resourceOperationCapabilities'; @@ -406,6 +407,19 @@ export const useCreateRightClickMenu = () => { }, }, + [OperationColumn.ActiveTransactions]: { + text: i18n('workspace.ops.activeTransactions'), + icon: 'icon-file-text', + handle: () => { + staticModal.confirm({ + title: i18n('workspace.ops.activeTransactions'), + content: , + footer: null, + width: 1100, + }); + }, + }, + [OperationColumn.CreateAccount]: { text: i18n('workspace.databaseAccount.createUser'), icon: 'icon-users', diff --git a/chat2db-community-client/src/blocks/NewTree/menuConfig.tsx b/chat2db-community-client/src/blocks/NewTree/menuConfig.tsx index f662aa53ee..d5e5e153a8 100644 --- a/chat2db-community-client/src/blocks/NewTree/menuConfig.tsx +++ b/chat2db-community-client/src/blocks/NewTree/menuConfig.tsx @@ -100,6 +100,7 @@ export const dropMenuConfig = { OperationColumn.Divider, // Management OperationColumn.MoveToGroup, + OperationColumn.ActiveTransactions, OperationColumn.Refresh, OperationColumn.Divider, OperationColumn.RemoveDataSource, diff --git a/chat2db-community-client/src/constants/tree.ts b/chat2db-community-client/src/constants/tree.ts index 94049852b4..c4f0de2ac0 100644 --- a/chat2db-community-client/src/constants/tree.ts +++ b/chat2db-community-client/src/constants/tree.ts @@ -50,6 +50,7 @@ export enum OperationColumn { // Universal DeleteTreeNode = 'deleteTreeNode', // delete tree node Refresh = 'refresh', // Refresh menus at all levels + ActiveTransactions = 'activeTransactions', // Active InnoDB transactions (MYSQL-OPS-002) CreateConsole = 'createConsole', // Create a new console Rename = 'rename', // Rename diff --git a/chat2db-community-client/src/i18n/en-US/workspace.ts b/chat2db-community-client/src/i18n/en-US/workspace.ts index d442f6d718..c9bc37475d 100644 --- a/chat2db-community-client/src/i18n/en-US/workspace.ts +++ b/chat2db-community-client/src/i18n/en-US/workspace.ts @@ -295,6 +295,23 @@ export default { 'workspace.databaseAccount.revokePrivilege': 'Revoke privileges', 'workspace.databaseAccount.lockUnsupported': 'Account lock is not supported by this MySQL version or current account privileges', + 'workspace.ops.activeTransactions': 'Active Transactions', + 'workspace.ops.transactionId': 'Transaction ID', + 'workspace.ops.transactionState': 'State', + 'workspace.ops.transactionStarted': 'Started', + 'workspace.ops.transactionAge': 'Age', + 'workspace.ops.secondsFormat': '{0}s', + 'workspace.ops.transactionIsolation': 'Isolation', + 'workspace.ops.rowsLocked': 'Rows Locked', + 'workspace.ops.rowsModified': 'Rows Modified', + 'workspace.ops.threadId': 'Thread ID', + 'workspace.ops.user': 'User', + 'workspace.ops.host': 'Host', + 'workspace.ops.database': 'Database', + 'workspace.ops.query': 'Query', + 'workspace.ops.permissionRequired': 'Insufficient permissions.', + 'workspace.ops.processPrivilegeHint': 'PROCESS privilege is required for full transaction visibility.', + 'workspace.ops.activeTransactionCount': '{0} active transaction(s)', 'workspace.routine.button.invoke': 'Invoke routine', 'workspace.routine.button.apply': 'Apply changes', 'workspace.routine.button.refresh': 'Reload DDL', diff --git a/chat2db-community-client/src/i18n/es-ES/workspace.ts b/chat2db-community-client/src/i18n/es-ES/workspace.ts index 361249e8fb..a5bf96997c 100644 --- a/chat2db-community-client/src/i18n/es-ES/workspace.ts +++ b/chat2db-community-client/src/i18n/es-ES/workspace.ts @@ -297,6 +297,23 @@ export default { 'workspace.databaseAccount.revokePrivilege': 'Revocar privilegios', 'workspace.databaseAccount.lockUnsupported': 'Esta versión de MySQL o los privilegios de la cuenta actual no admiten el bloqueo de cuentas', + 'workspace.ops.activeTransactions': 'Transacciones activas', + 'workspace.ops.transactionId': 'ID de transacción', + 'workspace.ops.transactionState': 'Estado', + 'workspace.ops.transactionStarted': 'Inicio', + 'workspace.ops.transactionAge': 'Edad', + 'workspace.ops.secondsFormat': '{0}s', + 'workspace.ops.transactionIsolation': 'Aislamiento', + 'workspace.ops.rowsLocked': 'Filas bloqueadas', + 'workspace.ops.rowsModified': 'Filas modificadas', + 'workspace.ops.threadId': 'ID de hilo', + 'workspace.ops.user': 'Usuario', + 'workspace.ops.host': 'Host', + 'workspace.ops.database': 'Base de datos', + 'workspace.ops.query': 'Consulta', + 'workspace.ops.permissionRequired': 'Permisos insuficientes.', + 'workspace.ops.processPrivilegeHint': 'Se requiere el privilegio PROCESS para ver las transacciones.', + 'workspace.ops.activeTransactionCount': '{0} transacción(es) activa(s)', 'workspace.routine.button.invoke': 'Invocar rutina', 'workspace.routine.button.apply': 'Aplicar cambios', 'workspace.routine.button.refresh': 'Volver a cargar DDL', diff --git a/chat2db-community-client/src/i18n/ja-JP/workspace.ts b/chat2db-community-client/src/i18n/ja-JP/workspace.ts index c69f096cf8..1ad1eef177 100644 --- a/chat2db-community-client/src/i18n/ja-JP/workspace.ts +++ b/chat2db-community-client/src/i18n/ja-JP/workspace.ts @@ -295,6 +295,23 @@ export default { 'workspace.databaseAccount.revokePrivilege': '権限を取り消し', 'workspace.databaseAccount.lockUnsupported': 'この MySQL バージョンまたは現在の権限ではアカウントロックを利用できません', + 'workspace.ops.activeTransactions': 'アクティブなトランザクション', + 'workspace.ops.transactionId': 'トランザクション ID', + 'workspace.ops.transactionState': '状態', + 'workspace.ops.transactionStarted': '開始時刻', + 'workspace.ops.transactionAge': '経過時間', + 'workspace.ops.secondsFormat': '{0}秒', + 'workspace.ops.transactionIsolation': '分離レベル', + 'workspace.ops.rowsLocked': 'ロック行数', + 'workspace.ops.rowsModified': '変更行数', + 'workspace.ops.threadId': 'スレッド ID', + 'workspace.ops.user': 'ユーザー', + 'workspace.ops.host': 'ホスト', + 'workspace.ops.database': 'データベース', + 'workspace.ops.query': 'SQL', + 'workspace.ops.permissionRequired': '権限が不足しています。', + 'workspace.ops.processPrivilegeHint': 'トランザクションの完全な表示には PROCESS 権限が必要です。', + 'workspace.ops.activeTransactionCount': 'アクティブなトランザクション: {0} 件', 'workspace.routine.button.invoke': 'ルーチンを呼び出す', 'workspace.routine.button.apply': '変更を適用', 'workspace.routine.button.refresh': 'DDL を再読み込み', diff --git a/chat2db-community-client/src/i18n/ko-KR/workspace.ts b/chat2db-community-client/src/i18n/ko-KR/workspace.ts index d021394a25..b03a9b78a6 100644 --- a/chat2db-community-client/src/i18n/ko-KR/workspace.ts +++ b/chat2db-community-client/src/i18n/ko-KR/workspace.ts @@ -293,6 +293,23 @@ export default { 'workspace.databaseAccount.grantPrivilege': '권한 부여', 'workspace.databaseAccount.revokePrivilege': '권한 회수', 'workspace.databaseAccount.lockUnsupported': '이 MySQL 버전 또는 현재 계정 권한에서는 계정 잠금을 지원하지 않습니다', + 'workspace.ops.activeTransactions': '활성 트랜잭션', + 'workspace.ops.transactionId': '트랜잭션 ID', + 'workspace.ops.transactionState': '상태', + 'workspace.ops.transactionStarted': '시작 시간', + 'workspace.ops.transactionAge': '경과 시간', + 'workspace.ops.secondsFormat': '{0}초', + 'workspace.ops.transactionIsolation': '격리 수준', + 'workspace.ops.rowsLocked': '잠긴 행', + 'workspace.ops.rowsModified': '수정된 행', + 'workspace.ops.threadId': '스레드 ID', + 'workspace.ops.user': '사용자', + 'workspace.ops.host': '호스트', + 'workspace.ops.database': '데이터베이스', + 'workspace.ops.query': 'SQL', + 'workspace.ops.permissionRequired': '권한이 부족합니다.', + 'workspace.ops.processPrivilegeHint': '트랜잭션 전체 보기에는 PROCESS 권한이 필요합니다.', + 'workspace.ops.activeTransactionCount': '활성 트랜잭션 {0}개', 'workspace.routine.button.invoke': '루틴 호출', 'workspace.routine.button.apply': '변경 사항 적용', 'workspace.routine.button.refresh': 'DDL 다시 불러오기', diff --git a/chat2db-community-client/src/i18n/zh-CN/workspace.ts b/chat2db-community-client/src/i18n/zh-CN/workspace.ts index 55f7b50f86..aa19df5478 100644 --- a/chat2db-community-client/src/i18n/zh-CN/workspace.ts +++ b/chat2db-community-client/src/i18n/zh-CN/workspace.ts @@ -286,6 +286,23 @@ export default { 'workspace.databaseAccount.grantPrivilege': '授予权限', 'workspace.databaseAccount.revokePrivilege': '撤销权限', 'workspace.databaseAccount.lockUnsupported': '当前 MySQL 版本或账号权限不支持账号锁定', + 'workspace.ops.activeTransactions': '活动事务', + 'workspace.ops.transactionId': '事务 ID', + 'workspace.ops.transactionState': '状态', + 'workspace.ops.transactionStarted': '开始时间', + 'workspace.ops.transactionAge': '时长', + 'workspace.ops.secondsFormat': '{0} 秒', + 'workspace.ops.transactionIsolation': '隔离级别', + 'workspace.ops.rowsLocked': '锁定行数', + 'workspace.ops.rowsModified': '修改行数', + 'workspace.ops.threadId': '线程 ID', + 'workspace.ops.user': '用户', + 'workspace.ops.host': '主机', + 'workspace.ops.database': '数据库', + 'workspace.ops.query': 'SQL', + 'workspace.ops.permissionRequired': '权限不足。', + 'workspace.ops.processPrivilegeHint': '完整事务可见性需要 PROCESS 权限。', + 'workspace.ops.activeTransactionCount': '{0} 个活动事务', 'workspace.routine.button.invoke': '调用例程', 'workspace.routine.button.apply': '提交变更', 'workspace.routine.button.refresh': '重新加载 DDL', diff --git a/chat2db-community-client/src/service/sql.ts b/chat2db-community-client/src/service/sql.ts index 61940b2859..a6873ea7ab 100644 --- a/chat2db-community-client/src/service/sql.ts +++ b/chat2db-community-client/src/service/sql.ts @@ -429,6 +429,28 @@ export interface ICopyTableParams extends ITableParams { // Copy table const copyTable = createRequest('/api/rdb/table/copy', { method: 'post' }); + +/** Active InnoDB transactions (MYSQL-OPS-002). */ +export interface IActiveTransactionItem { + trxId: string | null; + state: string | null; + startedAt: string | null; + ageSeconds: number | null; + isolationLevel: string | null; + rowsLocked: number | null; + rowsModified: number | null; + lockStructs: number | null; + threadId: number | null; + user: string | null; + host: string | null; + db: string | null; + query: string | null; +} + +const getActiveTransactionList = createRequest, IActiveTransactionItem[]>( + '/api/rdb/active_transaction/list', + { method: 'get' }, +); const checkIsSelectSQL = createRequest<{ sql: string; dbType: DatabaseTypeCode }, boolean>('/api/sql/valid_select'); const getDataSourceList = createRequest>( @@ -487,5 +509,6 @@ export default { getAllTableList, getAllFieldByTable, checkIsSelectSQL, + getActiveTransactionList, getDataSourceList, }; diff --git a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/service/db/IDbActiveTransactionService.java b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/service/db/IDbActiveTransactionService.java new file mode 100644 index 0000000000..5b4e91aed4 --- /dev/null +++ b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/service/db/IDbActiveTransactionService.java @@ -0,0 +1,19 @@ +package ai.chat2db.community.domain.api.service.db; + +import java.util.List; +import java.util.Map; + +/** + * Inspects active InnoDB transactions (MYSQL-OPS-002). Read-only; requires + * {@code PROCESS} for full visibility of other users' transactions and SQL text. + */ +public interface IDbActiveTransactionService { + + /** + * Lists active InnoDB transactions with their state, age, isolation level, lock + * counters, owning thread, user, host, database, and current SQL (when visible). + * + * @return a list of transaction maps, empty when no transaction is active. + */ + List> activeTransactions(); +} diff --git a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java new file mode 100644 index 0000000000..49bbefdcb3 --- /dev/null +++ b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java @@ -0,0 +1,57 @@ +package ai.chat2db.community.domain.core.impl.db; + +import ai.chat2db.community.domain.api.service.db.IDbActiveTransactionService; +import ai.chat2db.spi.DefaultSQLExecutor; +import ai.chat2db.spi.sql.Chat2DBContext; +import org.springframework.stereotype.Service; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Service +public class DbActiveTransactionServiceImpl implements IDbActiveTransactionService { + + /** + * InnoDB transaction metadata joined with the owning processlist row. Works on MySQL + * 5.7 and 8.0; {@code trx_query} is null without the PROCESS privilege (or for + * read-only transactions on 8.0, where the query text is intentionally not captured). + */ + private static final String SQL_ACTIVE_TRANSACTIONS = + "SELECT t.trx_id, t.trx_state, t.trx_started, " + + "TIMESTAMPDIFF(SECOND, t.trx_started, NOW()) AS trx_age_seconds, " + + "t.trx_isolation_level, t.trx_rows_locked, t.trx_rows_modified, " + + "t.trx_lock_structs, t.trx_mysql_thread_id, " + + "p.USER, p.HOST, p.DB, t.trx_query " + + "FROM information_schema.innodb_trx t " + + "LEFT JOIN information_schema.processlist p ON t.trx_mysql_thread_id = p.ID " + + "ORDER BY t.trx_started"; + + @Override + public List> activeTransactions() { + Connection connection = Chat2DBContext.getConnection(); + return DefaultSQLExecutor.getInstance().execute(connection, SQL_ACTIVE_TRANSACTIONS, resultSet -> { + List> transactions = new ArrayList<>(); + while (resultSet.next()) { + Map row = new LinkedHashMap<>(); + row.put("trxId", resultSet.getString("trx_id")); + row.put("state", resultSet.getString("trx_state")); + row.put("startedAt", resultSet.getTimestamp("trx_started")); + row.put("ageSeconds", resultSet.getLong("trx_age_seconds")); + row.put("isolationLevel", resultSet.getString("trx_isolation_level")); + row.put("rowsLocked", resultSet.getLong("trx_rows_locked")); + row.put("rowsModified", resultSet.getLong("trx_rows_modified")); + row.put("lockStructs", resultSet.getLong("trx_lock_structs")); + row.put("threadId", resultSet.getLong("trx_mysql_thread_id")); + row.put("user", resultSet.getString("USER")); + row.put("host", resultSet.getString("HOST")); + row.put("db", resultSet.getString("DB")); + row.put("query", resultSet.getString("trx_query")); + transactions.add(row); + } + return transactions; + }); + } +} diff --git a/chat2db-community-server/chat2db-community-web/src/main/java/ai/chat2db/community/web/api/controller/DbActiveTransactionController.java b/chat2db-community-server/chat2db-community-web/src/main/java/ai/chat2db/community/web/api/controller/DbActiveTransactionController.java new file mode 100644 index 0000000000..1bd845cdcf --- /dev/null +++ b/chat2db-community-server/chat2db-community-web/src/main/java/ai/chat2db/community/web/api/controller/DbActiveTransactionController.java @@ -0,0 +1,36 @@ +package ai.chat2db.community.web.api.controller; + +import ai.chat2db.community.domain.api.service.db.IDbActiveTransactionService; +import ai.chat2db.community.tools.wrapper.result.DataResult; +import ai.chat2db.community.web.api.aspect.connection.ConnectionInfoAspect; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +/** + * Exposes active InnoDB transaction inspection (MYSQL-OPS-002). + */ +@ConnectionInfoAspect +@RequestMapping("/api/rdb/active_transaction") +@RestController +public class DbActiveTransactionController { + + @Autowired + private IDbActiveTransactionService activeTransactionService; + + /** + * Lists active InnoDB transactions. + *

+ * Endpoint: {@code GET /api/rdb/active_transaction/list}. + * + * @return data result containing a list of transaction maps. + */ + @GetMapping("/list") + public DataResult>> list() { + return DataResult.of(activeTransactionService.activeTransactions()); + } +} diff --git a/script/test-fixtures/mysql/MYSQL-OPS-002/README.md b/script/test-fixtures/mysql/MYSQL-OPS-002/README.md new file mode 100644 index 0000000000..4cb3b32f30 --- /dev/null +++ b/script/test-fixtures/mysql/MYSQL-OPS-002/README.md @@ -0,0 +1,22 @@ +# MYSQL-OPS-002: Active transaction inspection + +## Fixture + +- `init.sql` creates: + - `ops002_admin` — administrator with PROCESS (full transaction visibility) + - `ops002_user` — limited account (no PROCESS; sees NULL SQL text for others) + - `ops002_test` database with `ops002_accounts` and sample data +- `grants.sql` grants PROCESS to the admin account only +- `cleanup.sql` drops test objects and users + +## Verification + +1. Connect as `ops002_admin`. +2. Open the Active Transactions view (right-click the datasource node). +3. Open a second connection and run `START TRANSACTION; UPDATE ops002_accounts SET balance = balance - 100 WHERE id = 1;` — leave it open. +4. Refresh the view — verify the transaction appears with state RUNNING, a growing age, isolation level REPEATABLE READ, thread ID, user, host, database, and the UPDATE SQL text. +5. Open a third connection and run a second open transaction — verify both are listed, ordered by start time. +6. On the same connection run a lock wait (`UPDATE ops002_accounts ... WHERE id = 1` while the first transaction holds the row lock) — verify the waiting transaction's state is LOCK WAIT. +7. Commit the first transaction — refresh — verify the row disappears. +8. Connect as `ops002_user`, open a transaction, and refresh the view — verify the current user's transaction appears, and other users' transactions show NULL/empty SQL text (explicit unavailable state). +9. With no open transactions, refresh — verify the empty state is shown normally. diff --git a/script/test-fixtures/mysql/MYSQL-OPS-002/cleanup.sql b/script/test-fixtures/mysql/MYSQL-OPS-002/cleanup.sql new file mode 100644 index 0000000000..adce63a526 --- /dev/null +++ b/script/test-fixtures/mysql/MYSQL-OPS-002/cleanup.sql @@ -0,0 +1,4 @@ +-- MYSQL-OPS-002: Cleanup +DROP DATABASE IF EXISTS `ops002_test`; +DROP USER IF EXISTS 'ops002_admin'@'%'; +DROP USER IF EXISTS 'ops002_user'@'%'; diff --git a/script/test-fixtures/mysql/MYSQL-OPS-002/grants.sql b/script/test-fixtures/mysql/MYSQL-OPS-002/grants.sql new file mode 100644 index 0000000000..0183fd6ccc --- /dev/null +++ b/script/test-fixtures/mysql/MYSQL-OPS-002/grants.sql @@ -0,0 +1,5 @@ +-- MYSQL-OPS-002: Grants for test user +-- ops002_admin has PROCESS for full transaction visibility (all users + SQL text). +GRANT PROCESS ON *.* TO 'ops002_admin'@'%'; +-- ops002_user intentionally has NO PROCESS: transactions appear with NULL query text. +FLUSH PRIVILEGES; diff --git a/script/test-fixtures/mysql/MYSQL-OPS-002/init.sql b/script/test-fixtures/mysql/MYSQL-OPS-002/init.sql new file mode 100644 index 0000000000..547fff78dd --- /dev/null +++ b/script/test-fixtures/mysql/MYSQL-OPS-002/init.sql @@ -0,0 +1,26 @@ +-- MYSQL-OPS-002: Active transaction inspection +-- Test fixture: users, a test table, and a stored helper that opens transactions. +-- The admin account can see all transactions (PROCESS); the limited account can only +-- see its own transactions and NULL SQL text. + +CREATE DATABASE IF NOT EXISTS `ops002_test`; +USE `ops002_test`; + +CREATE TABLE IF NOT EXISTS `ops002_accounts` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `name` VARCHAR(64) NOT NULL, + `balance` DECIMAL(12,2) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB; + +INSERT INTO `ops002_accounts` (`name`, `balance`) VALUES + ('alice', 1000.00), + ('bob', 500.00), + ('carol', 250.00); + +-- Two test users: full visibility vs limited visibility. +CREATE USER IF NOT EXISTS 'ops002_admin'@'%' IDENTIFIED BY 'Ops002_admin_2026'; +CREATE USER IF NOT EXISTS 'ops002_user'@'%' IDENTIFIED BY 'Ops002_user_2026'; + +GRANT SELECT, INSERT, UPDATE, DELETE ON `ops002_test`.* TO 'ops002_admin'@'%'; +GRANT SELECT, INSERT, UPDATE, DELETE ON `ops002_test`.* TO 'ops002_user'@'%'; From 0f3c3e5453fd73f81e849832208c3eaf90544441 Mon Sep 17 00:00:00 2001 From: liuhy Date: Thu, 6 Aug 2026 23:09:07 -0700 Subject: [PATCH 11/49] fix(client): use {1}-based i18n placeholders (0-based placeholders were not replaced) --- chat2db-community-client/scripts/i18n-source-hashes.json | 4 ++-- chat2db-community-client/src/i18n/en-US/workspace.ts | 4 ++-- chat2db-community-client/src/i18n/es-ES/workspace.ts | 4 ++-- chat2db-community-client/src/i18n/ja-JP/workspace.ts | 4 ++-- chat2db-community-client/src/i18n/ko-KR/workspace.ts | 4 ++-- chat2db-community-client/src/i18n/zh-CN/workspace.ts | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/chat2db-community-client/scripts/i18n-source-hashes.json b/chat2db-community-client/scripts/i18n-source-hashes.json index 1f2141eb32..fdf22e9802 100644 --- a/chat2db-community-client/scripts/i18n-source-hashes.json +++ b/chat2db-community-client/scripts/i18n-source-hashes.json @@ -26,7 +26,7 @@ "stream.ts": "8418dd25b0fd54b2ef2b15a2573edf0a4876efb4e69936ff82a157ea5cf42be4", "team.ts": "b24fbb075daccb2c44a24f732e7f338d8d08c9b1ee3599c3870171aa2e986512", "userGuide.ts": "0103ba35af95a3a0ff7a414130b68a7dd5f5f8196e59cd951631ac2090cb04fe", - "workspace.ts": "207bfdd5b0aece2b82c0fd5332c1d4408e3b953118a0ad77541ec128616ba1c6" + "workspace.ts": "1ea86e53e79b66a458c3c54397659ffa65f4dbc9a0eb80a41e569bebf7ba058a" }, "ko-KR": { "ai.ts": "e7a2fcbfb3ed647bd14f63aca4b6c35c8fb402c49a6a1f36bb04f76f4e8415af", @@ -52,7 +52,7 @@ "stream.ts": "8418dd25b0fd54b2ef2b15a2573edf0a4876efb4e69936ff82a157ea5cf42be4", "team.ts": "b24fbb075daccb2c44a24f732e7f338d8d08c9b1ee3599c3870171aa2e986512", "userGuide.ts": "0103ba35af95a3a0ff7a414130b68a7dd5f5f8196e59cd951631ac2090cb04fe", - "workspace.ts": "207bfdd5b0aece2b82c0fd5332c1d4408e3b953118a0ad77541ec128616ba1c6" + "workspace.ts": "1ea86e53e79b66a458c3c54397659ffa65f4dbc9a0eb80a41e569bebf7ba058a" } } } diff --git a/chat2db-community-client/src/i18n/en-US/workspace.ts b/chat2db-community-client/src/i18n/en-US/workspace.ts index c9bc37475d..0837438ad6 100644 --- a/chat2db-community-client/src/i18n/en-US/workspace.ts +++ b/chat2db-community-client/src/i18n/en-US/workspace.ts @@ -300,7 +300,7 @@ export default { 'workspace.ops.transactionState': 'State', 'workspace.ops.transactionStarted': 'Started', 'workspace.ops.transactionAge': 'Age', - 'workspace.ops.secondsFormat': '{0}s', + 'workspace.ops.secondsFormat': '{1}s', 'workspace.ops.transactionIsolation': 'Isolation', 'workspace.ops.rowsLocked': 'Rows Locked', 'workspace.ops.rowsModified': 'Rows Modified', @@ -311,7 +311,7 @@ export default { 'workspace.ops.query': 'Query', 'workspace.ops.permissionRequired': 'Insufficient permissions.', 'workspace.ops.processPrivilegeHint': 'PROCESS privilege is required for full transaction visibility.', - 'workspace.ops.activeTransactionCount': '{0} active transaction(s)', + 'workspace.ops.activeTransactionCount': '{1} active transaction(s)', 'workspace.routine.button.invoke': 'Invoke routine', 'workspace.routine.button.apply': 'Apply changes', 'workspace.routine.button.refresh': 'Reload DDL', diff --git a/chat2db-community-client/src/i18n/es-ES/workspace.ts b/chat2db-community-client/src/i18n/es-ES/workspace.ts index a5bf96997c..7428e15864 100644 --- a/chat2db-community-client/src/i18n/es-ES/workspace.ts +++ b/chat2db-community-client/src/i18n/es-ES/workspace.ts @@ -302,7 +302,7 @@ export default { 'workspace.ops.transactionState': 'Estado', 'workspace.ops.transactionStarted': 'Inicio', 'workspace.ops.transactionAge': 'Edad', - 'workspace.ops.secondsFormat': '{0}s', + 'workspace.ops.secondsFormat': '{1}s', 'workspace.ops.transactionIsolation': 'Aislamiento', 'workspace.ops.rowsLocked': 'Filas bloqueadas', 'workspace.ops.rowsModified': 'Filas modificadas', @@ -313,7 +313,7 @@ export default { 'workspace.ops.query': 'Consulta', 'workspace.ops.permissionRequired': 'Permisos insuficientes.', 'workspace.ops.processPrivilegeHint': 'Se requiere el privilegio PROCESS para ver las transacciones.', - 'workspace.ops.activeTransactionCount': '{0} transacción(es) activa(s)', + 'workspace.ops.activeTransactionCount': '{1} transacción(es) activa(s)', 'workspace.routine.button.invoke': 'Invocar rutina', 'workspace.routine.button.apply': 'Aplicar cambios', 'workspace.routine.button.refresh': 'Volver a cargar DDL', diff --git a/chat2db-community-client/src/i18n/ja-JP/workspace.ts b/chat2db-community-client/src/i18n/ja-JP/workspace.ts index 1ad1eef177..943019bebc 100644 --- a/chat2db-community-client/src/i18n/ja-JP/workspace.ts +++ b/chat2db-community-client/src/i18n/ja-JP/workspace.ts @@ -300,7 +300,7 @@ export default { 'workspace.ops.transactionState': '状態', 'workspace.ops.transactionStarted': '開始時刻', 'workspace.ops.transactionAge': '経過時間', - 'workspace.ops.secondsFormat': '{0}秒', + 'workspace.ops.secondsFormat': '{1}秒', 'workspace.ops.transactionIsolation': '分離レベル', 'workspace.ops.rowsLocked': 'ロック行数', 'workspace.ops.rowsModified': '変更行数', @@ -311,7 +311,7 @@ export default { 'workspace.ops.query': 'SQL', 'workspace.ops.permissionRequired': '権限が不足しています。', 'workspace.ops.processPrivilegeHint': 'トランザクションの完全な表示には PROCESS 権限が必要です。', - 'workspace.ops.activeTransactionCount': 'アクティブなトランザクション: {0} 件', + 'workspace.ops.activeTransactionCount': 'アクティブなトランザクション: {1} 件', 'workspace.routine.button.invoke': 'ルーチンを呼び出す', 'workspace.routine.button.apply': '変更を適用', 'workspace.routine.button.refresh': 'DDL を再読み込み', diff --git a/chat2db-community-client/src/i18n/ko-KR/workspace.ts b/chat2db-community-client/src/i18n/ko-KR/workspace.ts index b03a9b78a6..8263be2c91 100644 --- a/chat2db-community-client/src/i18n/ko-KR/workspace.ts +++ b/chat2db-community-client/src/i18n/ko-KR/workspace.ts @@ -298,7 +298,7 @@ export default { 'workspace.ops.transactionState': '상태', 'workspace.ops.transactionStarted': '시작 시간', 'workspace.ops.transactionAge': '경과 시간', - 'workspace.ops.secondsFormat': '{0}초', + 'workspace.ops.secondsFormat': '{1}초', 'workspace.ops.transactionIsolation': '격리 수준', 'workspace.ops.rowsLocked': '잠긴 행', 'workspace.ops.rowsModified': '수정된 행', @@ -309,7 +309,7 @@ export default { 'workspace.ops.query': 'SQL', 'workspace.ops.permissionRequired': '권한이 부족합니다.', 'workspace.ops.processPrivilegeHint': '트랜잭션 전체 보기에는 PROCESS 권한이 필요합니다.', - 'workspace.ops.activeTransactionCount': '활성 트랜잭션 {0}개', + 'workspace.ops.activeTransactionCount': '활성 트랜잭션 {1}개', 'workspace.routine.button.invoke': '루틴 호출', 'workspace.routine.button.apply': '변경 사항 적용', 'workspace.routine.button.refresh': 'DDL 다시 불러오기', diff --git a/chat2db-community-client/src/i18n/zh-CN/workspace.ts b/chat2db-community-client/src/i18n/zh-CN/workspace.ts index aa19df5478..5263b35a97 100644 --- a/chat2db-community-client/src/i18n/zh-CN/workspace.ts +++ b/chat2db-community-client/src/i18n/zh-CN/workspace.ts @@ -291,7 +291,7 @@ export default { 'workspace.ops.transactionState': '状态', 'workspace.ops.transactionStarted': '开始时间', 'workspace.ops.transactionAge': '时长', - 'workspace.ops.secondsFormat': '{0} 秒', + 'workspace.ops.secondsFormat': '{1} 秒', 'workspace.ops.transactionIsolation': '隔离级别', 'workspace.ops.rowsLocked': '锁定行数', 'workspace.ops.rowsModified': '修改行数', @@ -302,7 +302,7 @@ export default { 'workspace.ops.query': 'SQL', 'workspace.ops.permissionRequired': '权限不足。', 'workspace.ops.processPrivilegeHint': '完整事务可见性需要 PROCESS 权限。', - 'workspace.ops.activeTransactionCount': '{0} 个活动事务', + 'workspace.ops.activeTransactionCount': '{1} 个活动事务', 'workspace.routine.button.invoke': '调用例程', 'workspace.routine.button.apply': '提交变更', 'workspace.routine.button.refresh': '重新加载 DDL', From 5f7e7b58c91e8fc0edc17f9c2bea2bd613cdad8c Mon Sep 17 00:00:00 2001 From: dreamWork Date: Sat, 22 Aug 2026 17:16:32 +0800 Subject: [PATCH 12/49] refactor(terminal): refactor terminal shell startup logic with fault-tolerant fallback 1. Add `resolveShellCandidates` method to return available shell candidates based on the operating system 2. Modify `create` method to sequentially try starting shells from the candidate list, automatically falling back to the next on failure 3. Add unit tests to verify shell candidate resolution and exception scenarios 4. Add compatibility handling for scenarios where enterprise security software blocks the default shell --- .../jcef/terminal/TerminalSessionManager.java | 51 +++++++++++++++++-- .../terminal/TerminalSessionManagerTest.java | 14 +++++ 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/java/ai/chat2db/community/jcef/terminal/TerminalSessionManager.java b/chat2db-community-server/chat2db-community-jcef/src/main/java/ai/chat2db/community/jcef/terminal/TerminalSessionManager.java index 77755da6be..cc79794c4a 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/java/ai/chat2db/community/jcef/terminal/TerminalSessionManager.java +++ b/chat2db-community-server/chat2db-community-jcef/src/main/java/ai/chat2db/community/jcef/terminal/TerminalSessionManager.java @@ -51,7 +51,24 @@ public static Map create(Path directory, int columns, int rows) } public static Map create(Path directory, int columns, int rows, String shellId) throws IOException { - return create(directory, columns, rows, resolveShell(shellId)); + List candidates = resolveShellCandidates(shellId); + IOException lastFailure = null; + for (ShellCommand shell : candidates) { + try { + return create(directory, columns, rows, shell); + } catch (IOException failure) { + // Some corporate security suites (e.g. QiAnXin) deny process creation for + // powershell.exe even though the binary is present in PATH. Treat a spawn-time + // failure as "this shell cannot be invoked" and try the next candidate so the + // terminal still opens via, for example, cmd.exe. + lastFailure = failure; + log.warn("Unable to start terminal shell [{}], trying the next available shell", + shell.command(), failure); + } + } + throw lastFailure != null + ? lastFailure + : new IOException("No terminal shell is available"); } public static Map createInUserHome(int columns, int rows, String shellId) throws IOException { @@ -226,6 +243,19 @@ private static TerminalSession requireKnownSession(String sessionId) { return session; } + static List resolveShellCandidates(String requestedShellId) { + String shellId = requestedShellId == null || requestedShellId.isBlank() + ? "system" + : requestedShellId.toLowerCase(Locale.ROOT); + if ("system".equals(shellId)) { + // The default shell may be denied at runtime (e.g. powershell.exe blocked by a + // corporate security suite). Return the ordered fallback chain so create(...) can + // move on to cmd.exe when an earlier candidate cannot be started. + return resolveSystemShellCandidates(normalizedOsName()); + } + return List.of(resolveShell(shellId)); + } + private static ShellCommand resolveShell(String requestedShellId) { String shellId = requestedShellId == null || requestedShellId.isBlank() ? "system" @@ -254,22 +284,33 @@ private static ShellCommand resolveShell(String requestedShellId) { } private static ShellCommand resolveSystemShell(String os) { + return resolveSystemShellCandidates(os).get(0); + } + + private static List resolveSystemShellCandidates(String os) { if (isWindows(os)) { + List candidates = new ArrayList<>(); String pwsh = findCommand(os, "pwsh.exe", "pwsh"); if (pwsh != null) { - return powerShell("system", "PowerShell 7", pwsh); + candidates.add(powerShell("system", "PowerShell 7", pwsh)); } String powerShell = findCommand(os, "powershell.exe"); if (powerShell != null) { - return powerShell("system", "Windows PowerShell", powerShell); + candidates.add(powerShell("system", "Windows PowerShell", powerShell)); } - return new ShellCommand( + // Keep cmd.exe as the last resort so a blocked PowerShell still yields a usable shell. + candidates.add(new ShellCommand( "system", List.of(requireCommand(os, "Command Prompt", resolveCommandPrompt())), "Command Prompt", ShellFamily.CMD - ); + )); + return candidates; } + return List.of(resolveNonWindowsSystemShell(os)); + } + + private static ShellCommand resolveNonWindowsSystemShell(String os) { String configuredShell = System.getenv("SHELL"); if (configuredShell != null && Files.isExecutable(Path.of(configuredShell))) { return unixShell("system", Path.of(configuredShell).getFileName().toString(), configuredShell); diff --git a/chat2db-community-server/chat2db-community-jcef/src/test/java/ai/chat2db/community/jcef/terminal/TerminalSessionManagerTest.java b/chat2db-community-server/chat2db-community-jcef/src/test/java/ai/chat2db/community/jcef/terminal/TerminalSessionManagerTest.java index d8e7f3d371..0c7d57f485 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/test/java/ai/chat2db/community/jcef/terminal/TerminalSessionManagerTest.java +++ b/chat2db-community-server/chat2db-community-jcef/src/test/java/ai/chat2db/community/jcef/terminal/TerminalSessionManagerTest.java @@ -16,6 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class TerminalSessionManagerTest { @@ -88,6 +89,19 @@ void resolvesConfiguredUserHomeForDefaultTerminalDirectory() { assertEquals(directory, TerminalSessionManager.resolveUserHomeDirectory(directory.toString())); } + @Test + void resolvesDefaultShellCandidatesForCurrentOs() { + assertFalse(TerminalSessionManager.resolveShellCandidates("system").isEmpty()); + assertFalse(TerminalSessionManager.resolveShellCandidates(null).isEmpty()); + assertFalse(TerminalSessionManager.resolveShellCandidates("").isEmpty()); + } + + @Test + void rejectsUnknownShellCandidates() { + assertThrows(IllegalArgumentException.class, + () -> TerminalSessionManager.resolveShellCandidates("not-a-shell")); + } + @Test void exposesSystemDefaultAndInstalledShells() { Map capabilities = TerminalSessionManager.capabilities(); From 31672175b3c73744f0ea31491bfbfd60381e9962 Mon Sep 17 00:00:00 2001 From: dreamWork Date: Sat, 22 Aug 2026 17:43:45 +0800 Subject: [PATCH 13/49] refactor(terminal): refactor terminal session manager to support configurable default shell and improved shell candidate resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduce DEFAULT_SHELL_PROPERTY configuration property for setting the default shell - Add processFactory factory method to support custom process creation logic - Refactor create method to accept pre-resolved shell candidate list - Extract PtyProcess startup logic into a separate startProcess method - Add proper argument configuration for PowerShell and CMD commands (-NoLogo, -NoExit, -Command, etc.) - Improve shell candidate resolution order on Windows (pwsh → PowerShell → cmd) - Add support for zsh and bash colored prompts - Add test helper methods to reset factory and event publisher state - Add comprehensive unit tests covering shell configuration and failover scenarios - Change inner classes and enums to public to support external access - Add command parser interface to support custom command lookup logic --- .../jcef/terminal/TerminalSessionManager.java | 138 ++++++++++---- .../terminal/TerminalSessionManagerTest.java | 180 +++++++++++++++++- 2 files changed, 285 insertions(+), 33 deletions(-) diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/java/ai/chat2db/community/jcef/terminal/TerminalSessionManager.java b/chat2db-community-server/chat2db-community-jcef/src/main/java/ai/chat2db/community/jcef/terminal/TerminalSessionManager.java index cc79794c4a..d71185fffa 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/java/ai/chat2db/community/jcef/terminal/TerminalSessionManager.java +++ b/chat2db-community-server/chat2db-community-jcef/src/main/java/ai/chat2db/community/jcef/terminal/TerminalSessionManager.java @@ -43,6 +43,9 @@ public final class TerminalSessionManager { private static final TerminalEventPublisher JCEF_EVENT_PUBLISHER = TerminalSessionManager::publishToJcef; private static volatile TerminalEventPublisher eventPublisher = JCEF_EVENT_PUBLISHER; + static final String DEFAULT_SHELL_PROPERTY = "chat2db.community.terminal.default-shell"; + private static volatile TerminalProcessFactory processFactory = TerminalSessionManager::defaultProcessFactory; + private TerminalSessionManager() { } @@ -51,7 +54,10 @@ public static Map create(Path directory, int columns, int rows) } public static Map create(Path directory, int columns, int rows, String shellId) throws IOException { - List candidates = resolveShellCandidates(shellId); + return create(directory, columns, rows, resolveShellCandidates(shellId)); + } + + static Map create(Path directory, int columns, int rows, List candidates) throws IOException { IOException lastFailure = null; for (ShellCommand shell : candidates) { try { @@ -97,14 +103,8 @@ private static Map create(Path directory, int columns, int rows, applyShellColorEnvironment(environment, shell); } - PtyProcess process = new PtyProcessBuilder(shell.command().toArray(String[]::new)) - .setDirectory(cwd.toString()) - .setEnvironment(environment) - .setInitialColumns(Math.max(columns, 20)) - .setInitialRows(Math.max(rows, 5)) - .setRedirectErrorStream(true) - .setWindowsAnsiColorEnabled(true) - .start(); + PtyProcess process = startProcess(shell.command().toArray(String[]::new), cwd.toString(), environment, + Math.max(columns, 20), Math.max(rows, 5)); String sessionId = UUID.randomUUID().toString(); TerminalSession session = new TerminalSession(sessionId, cwd, shell, process); @@ -119,6 +119,23 @@ private static Map create(Path directory, int columns, int rows, return result; } + private static PtyProcess startProcess(String[] command, String directory, Map environment, + int columns, int rows) throws IOException { + return processFactory.create(command, directory, environment, columns, rows); + } + + static PtyProcess defaultProcessFactory(String[] command, String directory, Map environment, + int columns, int rows) throws IOException { + return new PtyProcessBuilder(command) + .setDirectory(directory) + .setEnvironment(environment) + .setInitialColumns(columns) + .setInitialRows(rows) + .setRedirectErrorStream(true) + .setWindowsAnsiColorEnabled(true) + .start(); + } + static boolean applyColorEnvironment(Map environment) { // Selecting a Chat2DB terminal theme is an explicit color opt-in. Keep // the override scoped to this child process and avoid exposing CLI tools @@ -273,12 +290,7 @@ private static ShellCommand resolveShell(String requestedShellId) { "Windows PowerShell", requireCommand(os, "Windows PowerShell", "powershell.exe") ); - case "cmd" -> new ShellCommand( - "cmd", - List.of(requireCommand(os, "Command Prompt", resolveCommandPrompt())), - "Command Prompt", - ShellFamily.CMD - ); + case "cmd" -> commandPrompt("cmd", requireCommand(os, "Command Prompt", resolveCommandPrompt())); default -> throw new IllegalArgumentException("Unsupported terminal shell: " + shellId); }; } @@ -288,26 +300,69 @@ private static ShellCommand resolveSystemShell(String os) { } private static List resolveSystemShellCandidates(String os) { + return resolveSystemShellCandidates(os, TerminalSessionManager::findCommand); + } + + static List resolveSystemShellCandidates(String os, CommandResolver resolver) { + List candidates = new ArrayList<>(); + Set seenCommands = new HashSet<>(); + String configuredShell = configuredDefaultShell(); + if (configuredShell != null) { + ShellCommand leading = resolveConfiguredShell(configuredShell); + if (leading != null) { + candidates.add(leading); + seenCommands.add(leading.command().get(0)); + } + } if (isWindows(os)) { - List candidates = new ArrayList<>(); - String pwsh = findCommand(os, "pwsh.exe", "pwsh"); + String pwsh = resolver.find(os, "pwsh.exe", "pwsh"); if (pwsh != null) { - candidates.add(powerShell("system", "PowerShell 7", pwsh)); + addCandidate(candidates, seenCommands, powerShell("system", "PowerShell 7", pwsh)); } - String powerShell = findCommand(os, "powershell.exe"); + String powerShell = resolver.find(os, "powershell.exe"); if (powerShell != null) { - candidates.add(powerShell("system", "Windows PowerShell", powerShell)); + addCandidate(candidates, seenCommands, powerShell("system", "Windows PowerShell", powerShell)); } // Keep cmd.exe as the last resort so a blocked PowerShell still yields a usable shell. - candidates.add(new ShellCommand( - "system", - List.of(requireCommand(os, "Command Prompt", resolveCommandPrompt())), - "Command Prompt", - ShellFamily.CMD - )); + String cmd = resolver.find(os, resolveCommandPrompt()); + addCandidate(candidates, seenCommands, commandPrompt("system", cmd == null ? resolveCommandPrompt() : cmd)); return candidates; } - return List.of(resolveNonWindowsSystemShell(os)); + ShellCommand systemShell = resolveNonWindowsSystemShell(os); + if (seenCommands.add(systemShell.command().get(0))) { + candidates.add(systemShell); + } + return candidates; + } + + static ShellCommand commandPrompt(String id, String command) { + // /K keeps cmd.exe resident after launch, matching pwsh's -NoExit behaviour. + return new ShellCommand(id, List.of(command, "/K"), "Command Prompt", ShellFamily.CMD); + } + + static String configuredDefaultShell() { + String configured = System.getProperty(DEFAULT_SHELL_PROPERTY); + if (configured == null || configured.isBlank()) { + return null; + } + String normalized = configured.trim().toLowerCase(Locale.ROOT); + // "system" means auto resolution; treating it as a configured default would loop back here. + return "system".equals(normalized) ? null : normalized; + } + + private static ShellCommand resolveConfiguredShell(String shellId) { + try { + return resolveShell(shellId); + } catch (IllegalArgumentException exception) { + log.warn("Ignoring configured default terminal shell [{}]: {}", shellId, exception.getMessage()); + return null; + } + } + + private static void addCandidate(List candidates, Set seenCommands, ShellCommand shell) { + if (seenCommands.add(shell.command().get(0))) { + candidates.add(shell); + } } private static ShellCommand resolveNonWindowsSystemShell(String os) { @@ -319,7 +374,7 @@ private static ShellCommand resolveNonWindowsSystemShell(String os) { return unixShell("system", fallbackName, requireCommand(os, fallbackName, fallbackName)); } - private static void applyShellColorEnvironment(Map environment, ShellCommand shell) { + static void applyShellColorEnvironment(Map environment, ShellCommand shell) { switch (shell.family()) { case BASH -> environment.put("PS1", "\\[\\033[34m\\]\\u@\\h\\[\\033[0m\\]:" + "\\[\\033[32m\\]\\w\\[\\033[0m\\]\\$ "); @@ -331,7 +386,7 @@ private static void applyShellColorEnvironment(Map environment, } } - private static ShellCommand unixShell(String id, String displayName, String command) { + static ShellCommand unixShell(String id, String displayName, String command) { String executableName = Path.of(command).getFileName().toString().toLowerCase(Locale.ROOT); ShellFamily family = executableName.contains("zsh") ? ShellFamily.ZSH @@ -339,7 +394,7 @@ private static ShellCommand unixShell(String id, String displayName, String comm return new ShellCommand(id, List.of(command, "-l"), displayName, family); } - private static ShellCommand powerShell(String id, String displayName, String command) { + static ShellCommand powerShell(String id, String displayName, String command) { String colorSetup = "if (Get-Command Set-PSReadLineOption -ErrorAction SilentlyContinue) { " + "Set-PSReadLineOption -Colors @{ " + "Command = 'Blue'; Parameter = 'Cyan'; String = 'Green'; " @@ -428,12 +483,31 @@ static void resetEventPublisherForTests() { eventPublisher = JCEF_EVENT_PUBLISHER; } + static void setProcessFactoryForTests(TerminalProcessFactory factory) { + processFactory = factory == null ? TerminalSessionManager::defaultProcessFactory : factory; + } + + static void resetProcessFactoryForTests() { + processFactory = TerminalSessionManager::defaultProcessFactory; + } + + @FunctionalInterface + interface CommandResolver { + String find(String os, String... candidates); + } + + @FunctionalInterface + interface TerminalProcessFactory { + PtyProcess create(String[] command, String directory, Map environment, + int columns, int rows) throws IOException; + } + @FunctionalInterface interface TerminalEventPublisher { void publish(String sessionId, ActionTypeEnum actionType, Map message); } - private enum ShellFamily { + enum ShellFamily { BASH, ZSH, POWERSHELL, @@ -441,7 +515,7 @@ private enum ShellFamily { OTHER } - private record ShellCommand(String id, List command, String displayName, ShellFamily family) { + record ShellCommand(String id, List command, String displayName, ShellFamily family) { } private static final class TerminalSession { diff --git a/chat2db-community-server/chat2db-community-jcef/src/test/java/ai/chat2db/community/jcef/terminal/TerminalSessionManagerTest.java b/chat2db-community-server/chat2db-community-jcef/src/test/java/ai/chat2db/community/jcef/terminal/TerminalSessionManagerTest.java index 0c7d57f485..f888481c51 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/test/java/ai/chat2db/community/jcef/terminal/TerminalSessionManagerTest.java +++ b/chat2db-community-server/chat2db-community-jcef/src/test/java/ai/chat2db/community/jcef/terminal/TerminalSessionManagerTest.java @@ -5,17 +5,20 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import java.io.IOException; import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -24,8 +27,10 @@ class TerminalSessionManagerTest { Path directory; @AfterEach - void resetPublisher() { + void resetAfterEach() { TerminalSessionManager.resetEventPublisherForTests(); + TerminalSessionManager.resetProcessFactoryForTests(); + System.clearProperty(TerminalSessionManager.DEFAULT_SHELL_PROPERTY); } @Test @@ -175,4 +180,177 @@ void batchesLargeOutputAndWaitsForAcknowledgement() throws Exception { TerminalSessionManager.kill(sessionId); } } + + @Test + void powershellShellsConfigureNoLogoNoExitAndCommandArgs() { + TerminalSessionManager.ShellCommand pwsh = + TerminalSessionManager.powerShell("pwsh", "PowerShell 7", "pwsh.exe"); + assertEquals(TerminalSessionManager.ShellFamily.POWERSHELL, pwsh.family()); + assertEquals("pwsh.exe", pwsh.command().get(0)); + assertTrue(pwsh.command().contains("-NoLogo")); + assertTrue(pwsh.command().contains("-NoExit")); + assertTrue(pwsh.command().contains("-Command")); + assertTrue(pwsh.command().stream().anyMatch(arg -> arg.contains("Set-PSReadLineOption"))); + + TerminalSessionManager.ShellCommand windowsPowerShell = + TerminalSessionManager.powerShell("powershell", "Windows PowerShell", "powershell.exe"); + assertEquals(TerminalSessionManager.ShellFamily.POWERSHELL, windowsPowerShell.family()); + assertTrue(windowsPowerShell.command().contains("-NoExit")); + assertTrue(windowsPowerShell.command().contains("-Command")); + } + + @Test + void cmdUsesExplicitStayAliveArgument() { + TerminalSessionManager.ShellCommand cmd = TerminalSessionManager.commandPrompt("cmd", "cmd.exe"); + assertEquals(TerminalSessionManager.ShellFamily.CMD, cmd.family()); + assertEquals(List.of("cmd.exe", "/K"), cmd.command()); + assertFalse(cmd.command().contains("-NoLogo")); + assertFalse(cmd.command().contains("-NoExit")); + assertFalse(cmd.command().contains("-Command")); + } + + @Test + void bashSetsColoredPs1Prompt() { + Map environment = new HashMap<>(); + TerminalSessionManager.applyShellColorEnvironment(environment, + TerminalSessionManager.unixShell("system", "Bash", "bash")); + assertTrue(environment.containsKey("PS1")); + assertFalse(environment.containsKey("PROMPT")); + } + + @Test + void cmdSetsColoredPromptWhilePowershellUsesCommandColors() { + Map cmdEnvironment = new HashMap<>(); + TerminalSessionManager.applyShellColorEnvironment(cmdEnvironment, + new TerminalSessionManager.ShellCommand( + "cmd", List.of("cmd.exe"), "Command Prompt", TerminalSessionManager.ShellFamily.CMD)); + assertEquals("$E[34m$P$E[0m$G$S", cmdEnvironment.get("PROMPT")); + + Map powershellEnvironment = new HashMap<>(); + TerminalSessionManager.applyShellColorEnvironment(powershellEnvironment, + TerminalSessionManager.powerShell("system", "PowerShell 7", "pwsh.exe")); + assertFalse(powershellEnvironment.containsKey("PS1")); + assertFalse(powershellEnvironment.containsKey("PROMPT")); + } + + @Test + void configuredDefaultShellNormalizesProperty() { + System.clearProperty(TerminalSessionManager.DEFAULT_SHELL_PROPERTY); + assertNull(TerminalSessionManager.configuredDefaultShell()); + + System.setProperty(TerminalSessionManager.DEFAULT_SHELL_PROPERTY, " system "); + assertNull(TerminalSessionManager.configuredDefaultShell()); + + System.setProperty(TerminalSessionManager.DEFAULT_SHELL_PROPERTY, " PWSH "); + assertEquals("pwsh", TerminalSessionManager.configuredDefaultShell()); + } + + @Test + void windowsSystemShellsFallBackPwshThenPowerShellThenCmd() { + System.clearProperty(TerminalSessionManager.DEFAULT_SHELL_PROPERTY); + List candidates = + TerminalSessionManager.resolveSystemShellCandidates("windows 11", (os, names) -> { + for (String name : names) { + if (name.contains("pwsh")) { + return "C:/pwsh.exe"; + } + if (name.contains("powershell")) { + return "C:/powershell.exe"; + } + if (name.equals("cmd.exe")) { + return "C:/Windows/System32/cmd.exe"; + } + } + return null; + }); + assertEquals( + List.of("PowerShell 7", "Windows PowerShell", "Command Prompt"), + candidates.stream().map(TerminalSessionManager.ShellCommand::displayName) + .collect(java.util.stream.Collectors.toList())); + assertEquals(TerminalSessionManager.ShellFamily.CMD, + candidates.get(candidates.size() - 1).family()); + } + + @Test + void zshSetsColoredPrompt() { + Map environment = new HashMap<>(); + TerminalSessionManager.applyShellColorEnvironment(environment, + TerminalSessionManager.unixShell("system", "Zsh", "zsh")); + assertTrue(environment.containsKey("PROMPT")); + assertFalse(environment.containsKey("PS1")); + } + + @Test + void windowsSystemShellFallsBackToCmdWhenPowershellSpawnBlocked() throws Exception { + if (!System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win")) { + return; + } + System.clearProperty(TerminalSessionManager.DEFAULT_SHELL_PROPERTY); + AtomicInteger attempts = new AtomicInteger(); + TerminalSessionManager.setProcessFactoryForTests((command, dir, env, columns, rows) -> { + attempts.incrementAndGet(); + if (!command[0].toLowerCase(Locale.ROOT).contains("cmd")) { + throw new IOException("blocked"); + } + return TerminalSessionManager.defaultProcessFactory(command, dir, env, columns, rows); + }); + Path cwd = Path.of(System.getProperty("java.io.tmpdir")); + try { + Map session = TerminalSessionManager.create(cwd, 80, 24, "system"); + String sessionId = (String) session.get("sessionId"); + try { + assertNotNull(sessionId); + assertTrue(attempts.get() >= 2); + assertTrue((Boolean) TerminalSessionManager.status(sessionId).get("alive")); + } finally { + TerminalSessionManager.kill(sessionId); + } + } finally { + TerminalSessionManager.resetProcessFactoryForTests(); + } + } + + @Test + void fallsBackToNextShellWhenSpawnFails() throws Exception { + if (System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win")) { + return; + } + AtomicInteger attempts = new AtomicInteger(); + TerminalSessionManager.setProcessFactoryForTests((command, dir, env, columns, rows) -> { + if (attempts.getAndIncrement() == 0) { + throw new IOException("powershell blocked by security software"); + } + return TerminalSessionManager.defaultProcessFactory(command, dir, env, columns, rows); + }); + TerminalSessionManager.ShellCommand blocked = new TerminalSessionManager.ShellCommand( + "blocked", List.of("powershell.exe"), "Windows PowerShell", TerminalSessionManager.ShellFamily.POWERSHELL); + TerminalSessionManager.ShellCommand fallback = TerminalSessionManager.unixShell("bash", "Bash", "bash"); + Map session = TerminalSessionManager.create(directory, 80, 24, List.of(blocked, fallback)); + String sessionId = (String) session.get("sessionId"); + try { + assertNotNull(sessionId); + assertEquals("bash", session.get("shellId")); + assertEquals(2, attempts.get()); + assertTrue((Boolean) TerminalSessionManager.status(sessionId).get("alive")); + } finally { + TerminalSessionManager.kill(sessionId); + } + } + + @Test + void throwsWhenNoCandidateCanStart() { + AtomicInteger attempts = new AtomicInteger(); + TerminalSessionManager.setProcessFactoryForTests((command, dir, env, columns, rows) -> { + attempts.incrementAndGet(); + throw new IOException("blocked"); + }); + TerminalSessionManager.ShellCommand blocked = new TerminalSessionManager.ShellCommand( + "blocked", List.of("powershell.exe"), "Windows PowerShell", TerminalSessionManager.ShellFamily.POWERSHELL); + TerminalSessionManager.ShellCommand cmd = new TerminalSessionManager.ShellCommand( + "cmd", List.of("cmd.exe"), "Command Prompt", TerminalSessionManager.ShellFamily.CMD); + IOException exception = assertThrows(IOException.class, + () -> TerminalSessionManager.create(directory, 80, 24, List.of(blocked, cmd))); + assertEquals("blocked", exception.getMessage()); + assertEquals(2, attempts.get()); + } } From df3355612c4b66a2427d74c2d970b2d61ab3dbc8 Mon Sep 17 00:00:00 2001 From: hhz <1839462565@qq.com> Date: Wed, 26 Aug 2026 20:00:50 +0800 Subject: [PATCH 14/49] fix(dm): support EXPLAIN execution plans - route DM SQL through the DM command executor - add dedicated DM lexer and parser support - retrieve execution plans through DmdbConnection.getExplainInfo - preserve SQL type, metrics, streaming, and error behavior - cover explicit EXPLAIN, Explain actions, cancellation, and driver isolation Fixes #2762 --- .../chat2db-community-dm/pom.xml | 23 +- .../chat2db/plugin/dm/parser/base/DMLexer.g4 | 2524 +++++ .../chat2db/plugin/dm/parser/base/DMParser.g4 | 9730 +++++++++++++++++ .../chat2db/plugin/dm/DMCommandExecutor.java | 139 + .../ai/chat2db/plugin/dm/DMExplainClient.java | 76 + .../java/ai/chat2db/plugin/dm/DMMetaData.java | 6 + .../plugin/dm/builder/DMSqlBuilder.java | 10 + .../chat2db/plugin/dm/parser/DMDialect.java | 83 + .../plugin/dm/parser/DMExecutableSql.java | 10 + .../plugin/dm/parser/DMExecutionVisitor.java | 49 + .../chat2db/plugin/dm/parser/DMSqlParser.java | 485 +- .../plugin/dm/parser/base/DMLexerBase.java | 20 + .../plugin/dm/parser/base/DMParserBase.java | 31 + .../error/strategy/DMErrorStrategy.java | 29 + .../dm/parser/listener/DMSelectListener.java | 209 + .../dm/parser/rule/DMRulePredicate.java | 26 + .../dm/parser/rule/manager/DMRuleManager.java | 30 + .../rule/predicate/DMBeginBlockPredicate.java | 32 + .../predicate/DMCreateFunctionPredicate.java | 92 + .../predicate/DMCreatePackagePredicate.java | 38 + .../predicate/DMCreateProcedurePredicate.java | 106 + .../rule/predicate/DMIfBlockPredicate.java | 50 + .../parser/visitor/DMCreateTableVisitor.java | 13 + .../dm/parser/visitor/DMParserVisitor.java | 1075 ++ .../parser/visitor/DMSimpleParserVisitor.java | 464 + .../parser/visitor/DMValidTableVisitor.java | 1207 ++ .../plugin/dm/DMCommandExecutorTest.java | 638 ++ .../java/dm/jdbc/driver/DmdbConnection.java | 9 + .../ai/chat2db/spi/DefaultSQLExecutor.java | 25 + .../ai/chat2db/spi/sql/Chat2DBContext.java | 7 + 30 files changed, 17230 insertions(+), 6 deletions(-) create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/antlr4/ai/chat2db/plugin/dm/parser/base/DMLexer.g4 create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/antlr4/ai/chat2db/plugin/dm/parser/base/DMParser.g4 create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMCommandExecutor.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMExplainClient.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMDialect.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMExecutableSql.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMExecutionVisitor.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/base/DMLexerBase.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/base/DMParserBase.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/error/strategy/DMErrorStrategy.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/listener/DMSelectListener.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/DMRulePredicate.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/manager/DMRuleManager.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMBeginBlockPredicate.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMCreateFunctionPredicate.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMCreatePackagePredicate.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMCreateProcedurePredicate.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMIfBlockPredicate.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMCreateTableVisitor.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMParserVisitor.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMSimpleParserVisitor.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMValidTableVisitor.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMCommandExecutorTest.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/dm/jdbc/driver/DmdbConnection.java diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/pom.xml b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/pom.xml index 4a90701922..03a9e0ad88 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/pom.xml +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/pom.xml @@ -15,10 +15,6 @@ ai.chat2db chat2db-community-spi - - ai.chat2db - chat2db-community-oracle - org.junit.jupiter junit-jupiter @@ -40,5 +36,24 @@ src/main/resources + + + org.antlr + antlr4-maven-plugin + ${antlr4.version} + + ${project.basedir}/src/main/antlr4 + ${project.build.directory}/generated-sources/antlr4 + true + + + + + antlr4 + + + + + \ No newline at end of file diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/antlr4/ai/chat2db/plugin/dm/parser/base/DMLexer.g4 b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/antlr4/ai/chat2db/plugin/dm/parser/base/DMLexer.g4 new file mode 100644 index 0000000000..eeec06f463 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/antlr4/ai/chat2db/plugin/dm/parser/base/DMLexer.g4 @@ -0,0 +1,2524 @@ +/** + * Oracle(c) PL/SQL 11g Parser + * + * Adapted for Dameng (DM) SQL parsing in Chat2DB Community. + * Derived from the Oracle PL/SQL grammar; dialect-specific rules such as + * EXPLAIN may differ from the upstream Oracle grammar. + * + * Copyright (c) 2009-2011 Alexandre Porcelli + * Copyright (c) 2015-2019 Ivan Kochurkin (KvanTTT, kvanttt@gmail.com, Positive Technologies). + * Copyright (c) 2017 Mark Adams + * + * 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. + */ + +// $antlr-format alignTrailingComments true, columnLimit 150, maxEmptyLinesToKeep 1, reflowComments false, useTab false +// $antlr-format allowShortRulesOnASingleLine true, allowShortBlocksOnASingleLine true, minEmptyLines 0, alignSemicolons ownLine +// $antlr-format alignColons trailing, singleLineOverrulesHangingColon true, alignLexerCommands true, alignLabels true, alignTrailers true + +lexer grammar DMLexer; + +options { + superClass = DMLexerBase; + caseInsensitive = true; +} + +@lexer::postinclude { +#include +} + +ABORT : 'ABORT'; +ABS : 'ABS'; +ABSENT : 'ABSENT'; +ACCESS : 'ACCESS'; +ACCESSED : 'ACCESSED'; +ACCESSIBLE : 'ACCESSIBLE'; +ACCOUNT : 'ACCOUNT'; +ACL : 'ACL'; +ACOS : 'ACOS'; +ACROSS : 'ACROSS'; +ACTION : 'ACTION'; +ACTIONS : 'ACTIONS'; +ACTIVATE : 'ACTIVATE'; +ACTIVE : 'ACTIVE'; +ACTIVE_COMPONENT : 'ACTIVE_COMPONENT'; +ACTIVE_DATA : 'ACTIVE_DATA'; +ACTIVE_FUNCTION : 'ACTIVE_FUNCTION'; +ACTIVE_TAG : 'ACTIVE_TAG'; +ACTIVITY : 'ACTIVITY'; +ADAPTIVE_PLAN : 'ADAPTIVE_PLAN'; +ADD : 'ADD'; +ADD_COLUMN : 'ADD_COLUMN'; +ADD_GROUP : 'ADD_GROUP'; +ADD_MONTHS : 'ADD_MONTHS'; +ADJ_DATE : 'ADJ_DATE'; +ADMIN : 'ADMIN'; +ADMINISTER : 'ADMINISTER'; +ADMINISTRATOR : 'ADMINISTRATOR'; +ADVANCED : 'ADVANCED'; +ADVISE : 'ADVISE'; +ADVISOR : 'ADVISOR'; +AFD_DISKSTRING : 'AFD_DISKSTRING'; +AFTER : 'AFTER'; +AGENT : 'AGENT'; +AGGREGATE : 'AGGREGATE'; +A_LETTER : 'A'; +ALIAS : 'ALIAS'; +ALL : 'ALL'; +ALLOCATE : 'ALLOCATE'; +ALLOW : 'ALLOW'; +ALL_ROWS : 'ALL_ROWS'; +ALTER : 'ALTER'; +ALTERNATE : 'ALTERNATE'; +ALWAYS : 'ALWAYS'; +ANALYTIC : 'ANALYTIC'; +ANALYZE : 'ANALYZE'; +ANCESTOR : 'ANCESTOR'; +ANCILLARY : 'ANCILLARY'; +AND : 'AND'; +AND_EQUAL : 'AND_EQUAL'; +ANOMALY : 'ANOMALY'; +ANSI_REARCH : 'ANSI_REARCH'; +ANTIJOIN : 'ANTIJOIN'; +ANY : 'ANY'; +ANYSCHEMA : 'ANYSCHEMA'; +APPEND : 'APPEND'; +APPENDCHILDXML : 'APPENDCHILDXML'; +APPEND_VALUES : 'APPEND_VALUES'; +APPLICATION : 'APPLICATION'; +APPLY : 'APPLY'; +APPROX_COUNT_DISTINCT : 'APPROX_COUNT_DISTINCT'; +ARCHIVAL : 'ARCHIVAL'; +ARCHIVE : 'ARCHIVE'; +ARCHIVED : 'ARCHIVED'; +ARCHIVELOG : 'ARCHIVELOG'; +ARRAY : 'ARRAY'; +AS : 'AS'; +ASC : 'ASC'; +ASCII : 'ASCII'; +ASCIISTR : 'ASCIISTR'; +ASIN : 'ASIN'; +ASIS : 'ASIS'; +ASSEMBLY : 'ASSEMBLY'; +ASSIGN : 'ASSIGN'; +ASSOCIATE : 'ASSOCIATE'; +ASYNC : 'ASYNC'; +ASYNCHRONOUS : 'ASYNCHRONOUS'; +ATAN2 : 'ATAN2'; +ATAN : 'ATAN'; +AT : 'AT'; +ATTRIBUTE : 'ATTRIBUTE'; +ATTRIBUTES : 'ATTRIBUTES'; +AUDIT : 'AUDIT'; +AUTHENTICATED : 'AUTHENTICATED'; +AUTHENTICATION : 'AUTHENTICATION'; +AUTHID : 'AUTHID'; +AUTHORIZATION : 'AUTHORIZATION'; +AUTOALLOCATE : 'AUTOALLOCATE'; +AUTO : 'AUTO'; +AUTOBACKUP : 'AUTOBACKUP'; +AUTOEXTEND : 'AUTOEXTEND'; +AUTO_LOGIN : 'AUTO_LOGIN'; +AUTOMATIC : 'AUTOMATIC'; +AUTONOMOUS_TRANSACTION : 'AUTONOMOUS_TRANSACTION'; +AUTO_REOPTIMIZE : 'AUTO_REOPTIMIZE'; +AVAILABILITY : 'AVAILABILITY'; +AVRO : 'AVRO'; +BACKGROUND : 'BACKGROUND'; +BACKINGFILE : 'BACKINGFILE'; +BACKUP : 'BACKUP'; +BACKUPS : 'BACKUPS'; +BACKUPSET : 'BACKUPSET'; +BASIC : 'BASIC'; +BASICFILE : 'BASICFILE'; +BATCH : 'BATCH'; +BATCHSIZE : 'BATCHSIZE'; +BATCH_TABLE_ACCESS_BY_ROWID : 'BATCH_TABLE_ACCESS_BY_ROWID'; +BECOME : 'BECOME'; +BEFORE : 'BEFORE'; +BEGIN : 'BEGIN'; +BEGINNING : 'BEGINNING'; +BEGIN_OUTLINE_DATA : 'BEGIN_OUTLINE_DATA'; +BEHALF : 'BEHALF'; +BEQUEATH : 'BEQUEATH'; +BETWEEN : 'BETWEEN'; +BFILE : 'BFILE'; +BFILENAME : 'BFILENAME'; +BIGFILE : 'BIGFILE'; +BINARY : 'BINARY'; +BINARY_DOUBLE : 'BINARY_DOUBLE'; +BINARY_DOUBLE_INFINITY : 'BINARY_DOUBLE_INFINITY'; +BINARY_DOUBLE_NAN : 'BINARY_DOUBLE_NAN'; +BINARY_FLOAT : 'BINARY_FLOAT'; +BINARY_FLOAT_INFINITY : 'BINARY_FLOAT_INFINITY'; +BINARY_FLOAT_NAN : 'BINARY_FLOAT_NAN'; +BINARY_INTEGER : 'BINARY_INTEGER'; +BIND_AWARE : 'BIND_AWARE'; +BINDING : 'BINDING'; +BIN_TO_NUM : 'BIN_TO_NUM'; +BITAND : 'BITAND'; +BITMAP_AND : 'BITMAP_AND'; +BITMAP : 'BITMAP'; +BITMAPS : 'BITMAPS'; +BITMAP_TREE : 'BITMAP_TREE'; +BITS : 'BITS'; +BLOB : 'BLOB'; +BLOCK : 'BLOCK'; +BLOCK_RANGE : 'BLOCK_RANGE'; +BLOCKS : 'BLOCKS'; +BLOCKSIZE : 'BLOCKSIZE'; +BODY : 'BODY'; +BOOLEAN : 'BOOLEAN'; +BOTH : 'BOTH'; +BOUND : 'BOUND'; +BRANCH : 'BRANCH'; +BREADTH : 'BREADTH'; +BROADCAST : 'BROADCAST'; +BSON : 'BSON'; +BUFFER : 'BUFFER'; +BUFFER_CACHE : 'BUFFER_CACHE'; +BUFFER_POOL : 'BUFFER_POOL'; +BUILD : 'BUILD'; +BULK : 'BULK'; +BY : 'BY'; +BYPASS_RECURSIVE_CHECK : 'BYPASS_RECURSIVE_CHECK'; +BYPASS_UJVC : 'BYPASS_UJVC'; +BYTE : 'BYTE'; +CACHE : 'CACHE'; +CACHE_CB : 'CACHE_CB'; +CACHE_INSTANCES : 'CACHE_INSTANCES'; +CACHE_TEMP_TABLE : 'CACHE_TEMP_TABLE'; +CACHING : 'CACHING'; +CALCULATED : 'CALCULATED'; +CALLBACK : 'CALLBACK'; +CALL : 'CALL'; +CANCEL : 'CANCEL'; +CANONICAL : 'CANONICAL'; +CAPACITY : 'CAPACITY'; +CAPTION : 'CAPTION'; +CARDINALITY : 'CARDINALITY'; +CASCADE : 'CASCADE'; +CASE : 'CASE'; +CAST : 'CAST'; +CASESENSITIVE : 'CASE-SENSITIVE'; +CATEGORY : 'CATEGORY'; +CDBDEFAULT : 'CDB$DEFAULT'; +CEIL : 'CEIL'; +CELL_FLASH_CACHE : 'CELL_FLASH_CACHE'; +CERTIFICATE : 'CERTIFICATE'; +CFILE : 'CFILE'; +CHAINED : 'CHAINED'; +CHANGE : 'CHANGE'; +CHANGETRACKING : 'CHANGETRACKING'; +CHANGE_DUPKEY_ERROR_INDEX : 'CHANGE_DUPKEY_ERROR_INDEX'; +CHARACTER : 'CHARACTER'; +CHAR : 'CHAR'; +CHAR_CS : 'CHAR_CS'; +CHARTOROWID : 'CHARTOROWID'; +CHECK_ACL_REWRITE : 'CHECK_ACL_REWRITE'; +CHECK : 'CHECK'; +CHECKPOINT : 'CHECKPOINT'; +CHILD : 'CHILD'; +CHOOSE : 'CHOOSE'; +CHR : 'CHR'; +CHUNK : 'CHUNK'; +CLASS : 'CLASS'; +CLASSIFICATION : 'CLASSIFICATION'; +CLASSIFIER : 'CLASSIFIER'; +CLAUSE : 'CLAUSE'; +CLEAN : 'CLEAN'; +CLEANUP : 'CLEANUP'; +CLEAR : 'CLEAR'; +C_LETTER : 'C'; +CLIENT : 'CLIENT'; +CLOB : 'CLOB'; +CLONE : 'CLONE'; +CLOSE_CACHED_OPEN_CURSORS : 'CLOSE_CACHED_OPEN_CURSORS'; +CLOSE : 'CLOSE'; +CLUSTER_BY_ROWID : 'CLUSTER_BY_ROWID'; +CLUSTER : 'CLUSTER'; +CLUSTER_DETAILS : 'CLUSTER_DETAILS'; +CLUSTER_DISTANCE : 'CLUSTER_DISTANCE'; +CLUSTER_ID : 'CLUSTER_ID'; +CLUSTERING : 'CLUSTERING'; +CLUSTERING_FACTOR : 'CLUSTERING_FACTOR'; +CLUSTER_PROBABILITY : 'CLUSTER_PROBABILITY'; +CLUSTER_SET : 'CLUSTER_SET'; +COALESCE : 'COALESCE'; +COALESCE_SQ : 'COALESCE_SQ'; +COARSE : 'COARSE'; +CO_AUTH_IND : 'CO_AUTH_IND'; +COLD : 'COLD'; +COLLECT : 'COLLECT'; +COLUMNAR : 'COLUMNAR'; +COLUMN_AUTH_INDICATOR : 'COLUMN_AUTH_INDICATOR'; +COLUMN : 'COLUMN'; +COLUMNS : 'COLUMNS'; +COLUMN_STATS : 'COLUMN_STATS'; +COLUMN_VALUE : 'COLUMN_VALUE'; +COMMENT : 'COMMENT'; +COMMIT : 'COMMIT'; +COMMITTED : 'COMMITTED'; +COMMON : 'COMMON'; +COMMON_DATA : 'COMMON_DATA'; +COMPACT : 'COMPACT'; +COMPATIBILITY : 'COMPATIBILITY'; +COMPILE : 'COMPILE'; +COMPLETE : 'COMPLETE'; +COMPLIANCE : 'COMPLIANCE'; +COMPONENT : 'COMPONENT'; +COMPONENTS : 'COMPONENTS'; +COMPOSE : 'COMPOSE'; +COMPOSITE : 'COMPOSITE'; +COMPOSITE_LIMIT : 'COMPOSITE_LIMIT'; +COMPOUND : 'COMPOUND'; +COMPRESS : 'COMPRESS'; +COMPUTE : 'COMPUTE'; +CONCAT : 'CONCAT'; +CON_DBID_TO_ID : 'CON_DBID_TO_ID'; +CONDITIONAL : 'CONDITIONAL'; +CONDITION : 'CONDITION'; +CONFIRM : 'CONFIRM'; +CONFORMING : 'CONFORMING'; +CON_GUID_TO_ID : 'CON_GUID_TO_ID'; +CON_ID : 'CON_ID'; +CON_NAME_TO_ID : 'CON_NAME_TO_ID'; +CONNECT_BY_CB_WHR_ONLY : 'CONNECT_BY_CB_WHR_ONLY'; +CONNECT_BY_COMBINE_SW : 'CONNECT_BY_COMBINE_SW'; +CONNECT_BY_COST_BASED : 'CONNECT_BY_COST_BASED'; +CONNECT_BY_ELIM_DUPS : 'CONNECT_BY_ELIM_DUPS'; +CONNECT_BY_FILTERING : 'CONNECT_BY_FILTERING'; +CONNECT_BY_ISCYCLE : 'CONNECT_BY_ISCYCLE'; +CONNECT_BY_ISLEAF : 'CONNECT_BY_ISLEAF'; +CONNECT_BY_ROOT : 'CONNECT_BY_ROOT'; +CONNECT : 'CONNECT'; +CONNECT_TIME : 'CONNECT_TIME'; +CONSIDER : 'CONSIDER'; +CONSISTENT : 'CONSISTENT'; +CONSTANT : 'CONSTANT'; +CONST : 'CONST'; +CONSTRAINT : 'CONSTRAINT'; +CONSTRAINTS : 'CONSTRAINTS'; +CONSTRUCTOR : 'CONSTRUCTOR'; +CONTAINER : 'CONTAINER'; +CONTAINERS : 'CONTAINERS'; +CONTAINERS_DEFAULT : 'CONTAINERS_DEFAULT'; +CONTAINER_DATA : 'CONTAINER_DATA'; +CONTAINER_MAP : 'CONTAINER_MAP'; +CONTENT : 'CONTENT'; +CONTENTS : 'CONTENTS'; +CONTEXT : 'CONTEXT'; +CONTINUE : 'CONTINUE'; +CONTROLFILE : 'CONTROLFILE'; +CON_UID_TO_ID : 'CON_UID_TO_ID'; +CONVERT : 'CONVERT'; +CONVERSION : 'CONVERSION'; +COOKIE : 'COOKIE'; +COPY : 'COPY'; +CORR_K : 'CORR_K'; +CORR_S : 'CORR_S'; +CORRUPTION : 'CORRUPTION'; +CORRUPT_XID_ALL : 'CORRUPT_XID_ALL'; +CORRUPT_XID : 'CORRUPT_XID'; +COS : 'COS'; +COSH : 'COSH'; +COST : 'COST'; +COST_XML_QUERY_REWRITE : 'COST_XML_QUERY_REWRITE'; +COUNT : 'COUNT'; +COVAR_POP : 'COVAR_POP'; +COVAR_SAMP : 'COVAR_SAMP'; +CPU_COSTING : 'CPU_COSTING'; +CPU_PER_CALL : 'CPU_PER_CALL'; +CPU_PER_SESSION : 'CPU_PER_SESSION'; +CRASH : 'CRASH'; +CREATE : 'CREATE'; +CREATE_FILE_DEST : 'CREATE_FILE_DEST'; +CREATE_STORED_OUTLINES : 'CREATE_STORED_OUTLINES'; +CREATION : 'CREATION'; +CREDENTIAL : 'CREDENTIAL'; +CRITICAL : 'CRITICAL'; +CROSS : 'CROSS'; +CROSSEDITION : 'CROSSEDITION'; +CSCONVERT : 'CSCONVERT'; +CUBE_AJ : 'CUBE_AJ'; +CUBE : 'CUBE'; +CUBE_GB : 'CUBE_GB'; +CUBE_SJ : 'CUBE_SJ'; +CUME_DISTM : 'CUME_DISTM'; +CURRENT : 'CURRENT'; +CURRENT_DATE : 'CURRENT_DATE'; +CURRENT_SCHEMA : 'CURRENT_SCHEMA'; +CURRENT_TIME : 'CURRENT_TIME'; +CURRENT_TIMESTAMP : 'CURRENT_TIMESTAMP'; +CURRENT_USER : 'CURRENT_USER'; +CURRENTV : 'CURRENTV'; +CURSOR : 'CURSOR'; +CURSOR_SHARING_EXACT : 'CURSOR_SHARING_EXACT'; +CURSOR_SPECIFIC_SEGMENT : 'CURSOR_SPECIFIC_SEGMENT'; +CUSTOMDATUM : 'CUSTOMDATUM'; +CV : 'CV'; +CYCLE : 'CYCLE'; +DANGLING : 'DANGLING'; +DATABASE : 'DATABASE'; +DATA : 'DATA'; +DATAFILE : 'DATAFILE'; +DATAFILES : 'DATAFILES'; +DATAGUARDCONFIG : 'DATAGUARDCONFIG'; +DATAMOVEMENT : 'DATAMOVEMENT'; +DATAOBJNO : 'DATAOBJNO'; +DATAOBJ_TO_MAT_PARTITION : 'DATAOBJ_TO_MAT_PARTITION'; +DATAOBJ_TO_PARTITION : 'DATAOBJ_TO_PARTITION'; +DATAPUMP : 'DATAPUMP'; +DATA_SECURITY_REWRITE_LIMIT : 'DATA_SECURITY_REWRITE_LIMIT'; +DATE : 'DATE'; +DATE_MODE : 'DATE_MODE'; +DAY : 'DAY'; +DAYS : 'DAYS'; +DBA : 'DBA'; +DBA_RECYCLEBIN : 'DBA_RECYCLEBIN'; +DBMS_STATS : 'DBMS_STATS'; +DB_ROLE_CHANGE : 'DB_ROLE_CHANGE'; +DBTIMEZONE : 'DBTIMEZONE'; +DB_UNIQUE_NAME : 'DB_UNIQUE_NAME'; +DB_VERSION : 'DB_VERSION'; +DDL : 'DDL'; +DEALLOCATE : 'DEALLOCATE'; +DEBUG : 'DEBUG'; +DEBUGGER : 'DEBUGGER'; +DEC : 'DEC'; +DECIMAL : 'DECIMAL'; +DECLARE : 'DECLARE'; +DECOMPOSE : 'DECOMPOSE'; +DECORRELATE : 'DECORRELATE'; +DECR : 'DECR'; +DECREMENT : 'DECREMENT'; +DECRYPT : 'DECRYPT'; +DEDUPLICATE : 'DEDUPLICATE'; +DEFAULT : 'DEFAULT'; +DEFAULTS : 'DEFAULTS'; +DEFAULT_COLLATION : 'DEFAULT_COLLATION'; +DEFAULT_CREDENTIAL : 'DEFAULT_CREDENTIAL'; +DEFERRABLE : 'DEFERRABLE'; +DEFERRED : 'DEFERRED'; +DEFINED : 'DEFINED'; +DEFINE : 'DEFINE'; +DEFINER : 'DEFINER'; +DEGREE : 'DEGREE'; +DELAY : 'DELAY'; +DELEGATE : 'DELEGATE'; +DELETE_ALL : 'DELETE_ALL'; +DELETE : 'DELETE'; +DELETEXML : 'DELETEXML'; +DEMAND : 'DEMAND'; +DENSE_RANKM : 'DENSE_RANKM'; +DEPENDENT : 'DEPENDENT'; +DEPRECATE : 'DEPRECATE'; +DEPTH : 'DEPTH'; +DEQUEUE : 'DEQUEUE'; +DEREF : 'DEREF'; +DEREF_NO_REWRITE : 'DEREF_NO_REWRITE'; +DESC : 'DESC'; +DESCRIPTION : 'DESCRIPTION'; +DESTROY : 'DESTROY'; +DETACHED : 'DETACHED'; +DETERMINES : 'DETERMINES'; +DETERMINISTIC : 'DETERMINISTIC'; +DICTIONARY : 'DICTIONARY'; +DIMENSION : 'DIMENSION'; +DIMENSIONS : 'DIMENSIONS'; +DIRECT_LOAD : 'DIRECT_LOAD'; +DIRECTORY : 'DIRECTORY'; +DIRECT_PATH : 'DIRECT_PATH'; +DISABLE_ALL : 'DISABLE_ALL'; +DISABLE : 'DISABLE'; +DISABLE_PARALLEL_DML : 'DISABLE_PARALLEL_DML'; +DISABLE_PRESET : 'DISABLE_PRESET'; +DISABLE_RPKE : 'DISABLE_RPKE'; +DISALLOW : 'DISALLOW'; +DISASSOCIATE : 'DISASSOCIATE'; +DISCARD : 'DISCARD'; +DISCONNECT : 'DISCONNECT'; +DISK : 'DISK'; +DISKGROUP : 'DISKGROUP'; +DISKGROUP_PLUS : '\'+ DISKGROUP'; +DISKS : 'DISKS'; +DISMOUNT : 'DISMOUNT'; +DISTINCT : 'DISTINCT'; +DISTINGUISHED : 'DISTINGUISHED'; +DISTRIBUTED : 'DISTRIBUTED'; +DISTRIBUTE : 'DISTRIBUTE'; +DML : 'DML'; +DML_UPDATE : 'DML_UPDATE'; +DOCFIDELITY : 'DOCFIDELITY'; +DOCUMENT : 'DOCUMENT'; +DOLLAR_ELSE : '$ELSE'; +DOLLAR_ELSIF : '$ELSIF'; +DOLLAR_END : '$END'; +DOLLAR_ERROR : '$ERROR'; +DOLLAR_IF : '$IF'; +DOLLAR_THEN : '$THEN'; +DOMAIN_INDEX_FILTER : 'DOMAIN_INDEX_FILTER'; +DOMAIN_INDEX_NO_SORT : 'DOMAIN_INDEX_NO_SORT'; +DOMAIN_INDEX_SORT : 'DOMAIN_INDEX_SORT'; +DOUBLE : 'DOUBLE'; +DOWNGRADE : 'DOWNGRADE'; +DRIVING_SITE : 'DRIVING_SITE'; +DROP_COLUMN : 'DROP_COLUMN'; +DROP : 'DROP'; +DROP_GROUP : 'DROP_GROUP'; +DSINTERVAL_UNCONSTRAINED : 'DSINTERVAL_UNCONSTRAINED'; +DST_UPGRADE_INSERT_CONV : 'DST_UPGRADE_INSERT_CONV'; +DUMP : 'DUMP'; +DUMPSET : 'DUMPSET'; +DUPLICATE : 'DUPLICATE'; +DV : 'DV'; +DYNAMIC : 'DYNAMIC'; +DYNAMIC_SAMPLING : 'DYNAMIC_SAMPLING'; +DYNAMIC_SAMPLING_EST_CDN : 'DYNAMIC_SAMPLING_EST_CDN'; +E_LETTER : 'E'; +EACH : 'EACH'; +EDITIONABLE : 'EDITIONABLE'; +EDITION : 'EDITION'; +EDITIONING : 'EDITIONING'; +EDITIONS : 'EDITIONS'; +ELEMENT : 'ELEMENT'; +ELIM_GROUPBY : 'ELIM_GROUPBY'; +ELIMINATE_JOIN : 'ELIMINATE_JOIN'; +ELIMINATE_OBY : 'ELIMINATE_OBY'; +ELIMINATE_OUTER_JOIN : 'ELIMINATE_OUTER_JOIN'; +ELSE : 'ELSE'; +ELSIF : 'ELSIF'; +EM : 'EM'; +EMPTY_BLOB : 'EMPTY_BLOB'; +EMPTY_CLOB : 'EMPTY_CLOB'; +EMPTY_ : 'EMPTY'; +ENABLE_ALL : 'ENABLE_ALL'; +ENABLE : 'ENABLE'; +ENABLE_PARALLEL_DML : 'ENABLE_PARALLEL_DML'; +ENABLE_PRESET : 'ENABLE_PRESET'; +ENCODING : 'ENCODING'; +ENCRYPT : 'ENCRYPT'; +ENCRYPTION : 'ENCRYPTION'; +END : 'END'; +END_OUTLINE_DATA : 'END_OUTLINE_DATA'; +ENFORCED : 'ENFORCED'; +ENFORCE : 'ENFORCE'; +ENQUEUE : 'ENQUEUE'; +ENTERPRISE : 'ENTERPRISE'; +ENTITYESCAPING : 'ENTITYESCAPING'; +ENTRY : 'ENTRY'; +EQUIPART : 'EQUIPART'; +ERR : 'ERR'; +ERROR_ARGUMENT : 'ERROR_ARGUMENT'; +ERROR : 'ERROR'; +ERROR_ON_OVERLAP_TIME : 'ERROR_ON_OVERLAP_TIME'; +ERRORS : 'ERRORS'; +ERROR_INDEX : 'ERROR_INDEX'; +ERROR_CODE : 'ERROR_CODE'; +ESCAPE : 'ESCAPE'; +ESTIMATE : 'ESTIMATE'; +EVAL : 'EVAL'; +EVALNAME : 'EVALNAME'; +EVALUATE : 'EVALUATE'; +EVALUATION : 'EVALUATION'; +EVENTS : 'EVENTS'; +EVERY : 'EVERY'; +EXCEPT : 'EXCEPT'; +EXCEPTION : 'EXCEPTION'; +EXCEPTION_INIT : 'EXCEPTION_INIT'; +EXCEPTIONS : 'EXCEPTIONS'; +EXCHANGE : 'EXCHANGE'; +EXCLUDE : 'EXCLUDE'; +EXCLUDING : 'EXCLUDING'; +EXCLUSIVE : 'EXCLUSIVE'; +EXECUTE : 'EXECUTE'; +EXEMPT : 'EXEMPT'; +EXISTING : 'EXISTING'; +EXISTS : 'EXISTS'; +EXISTSNODE : 'EXISTSNODE'; +EXIT : 'EXIT'; +EXPAND_GSET_TO_UNION : 'EXPAND_GSET_TO_UNION'; +EXPAND_TABLE : 'EXPAND_TABLE'; +EXP : 'EXP'; +EXPIRE : 'EXPIRE'; +EXPLAIN : 'EXPLAIN'; +EXPLOSION : 'EXPLOSION'; +EXPORT : 'EXPORT'; +EXPR_CORR_CHECK : 'EXPR_CORR_CHECK'; +EXPRESS : 'EXPRESS'; +EXTENDS : 'EXTENDS'; +EXTENT : 'EXTENT'; +EXTENTS : 'EXTENTS'; +EXTERNAL : 'EXTERNAL'; +EXTERNALLY : 'EXTERNALLY'; +EXTRACTCLOBXML : 'EXTRACTCLOBXML'; +EXTRACT : 'EXTRACT'; +EXTRACTVALUE : 'EXTRACTVALUE'; +EXTRA : 'EXTRA'; +FACILITY : 'FACILITY'; +FACT : 'FACT'; +FACTOR : 'FACTOR'; +FACTORIZE_JOIN : 'FACTORIZE_JOIN'; +FAILED : 'FAILED'; +FAILED_LOGIN_ATTEMPTS : 'FAILED_LOGIN_ATTEMPTS'; +FAILGROUP : 'FAILGROUP'; +FAILOVER : 'FAILOVER'; +FAILURE : 'FAILURE'; +FALSE : 'FALSE'; +FAMILY : 'FAMILY'; +FAR : 'FAR'; +FAST : 'FAST'; +FASTSTART : 'FASTSTART'; +FBTSCAN : 'FBTSCAN'; +FEATURE : 'FEATURE'; +FEATURE_DETAILS : 'FEATURE_DETAILS'; +FEATURE_ID : 'FEATURE_ID'; +FEATURE_SET : 'FEATURE_SET'; +FEATURE_VALUE : 'FEATURE_VALUE'; +FETCH : 'FETCH'; +FILE : 'FILE'; +FILE_NAME_CONVERT : 'FILE_NAME_CONVERT'; +FILEGROUP : 'FILEGROUP'; +FILESTORE : 'FILESTORE'; +FILESYSTEM_LIKE_LOGGING : 'FILESYSTEM_LIKE_LOGGING'; +FILTER : 'FILTER'; +FINAL : 'FINAL'; +FINE : 'FINE'; +FINISH : 'FINISH'; +FIRST : 'FIRST'; +FIRSTM : 'FIRSTM'; +FIRST_ROWS : 'FIRST_ROWS'; +FIRST_VALUE : 'FIRST_VALUE'; +FIXED_VIEW_DATA : 'FIXED_VIEW_DATA'; +FLAGGER : 'FLAGGER'; +FLASHBACK : 'FLASHBACK'; +FLASH_CACHE : 'FLASH_CACHE'; +FLOAT : 'FLOAT'; +FLOB : 'FLOB'; +FLEX : 'FLEX'; +FLOOR : 'FLOOR'; +FLUSH : 'FLUSH'; +FOLDER : 'FOLDER'; +FOLLOWING : 'FOLLOWING'; +FOLLOWS : 'FOLLOWS'; +FORALL : 'FORALL'; +FORCE : 'FORCE'; +FORCE_XML_QUERY_REWRITE : 'FORCE_XML_QUERY_REWRITE'; +FOREIGN : 'FOREIGN'; +FOREVER : 'FOREVER'; +FOR : 'FOR'; +FORMAT : 'FORMAT'; +FORWARD : 'FORWARD'; +FRAGMENT_NUMBER : 'FRAGMENT_NUMBER'; +FREELIST : 'FREELIST'; +FREELISTS : 'FREELISTS'; +FREEPOOLS : 'FREEPOOLS'; +FRESH : 'FRESH'; +FROM : 'FROM'; +FROM_TZ : 'FROM_TZ'; +FULL : 'FULL'; +FULL_OUTER_JOIN_TO_OUTER : 'FULL_OUTER_JOIN_TO_OUTER'; +FUNCTION : 'FUNCTION'; +FUNCTIONS : 'FUNCTIONS'; +FTP : 'FTP'; +G_LETTER : 'G'; +GATHER_OPTIMIZER_STATISTICS : 'GATHER_OPTIMIZER_STATISTICS'; +GATHER_PLAN_STATISTICS : 'GATHER_PLAN_STATISTICS'; +GBY_CONC_ROLLUP : 'GBY_CONC_ROLLUP'; +GBY_PUSHDOWN : 'GBY_PUSHDOWN'; +GENERATED : 'GENERATED'; +GET : 'GET'; +GLOBAL : 'GLOBAL'; +GLOBALLY : 'GLOBALLY'; +GLOBAL_NAME : 'GLOBAL_NAME'; +GLOBAL_TOPIC_ENABLED : 'GLOBAL_TOPIC_ENABLED'; +GOTO : 'GOTO'; +GRANT : 'GRANT'; +GROUP_BY : 'GROUP_BY'; +GROUP : 'GROUP'; +GROUP_ID : 'GROUP_ID'; +GROUPING : 'GROUPING'; +GROUPING_ID : 'GROUPING_ID'; +GROUPS : 'GROUPS'; +GUARANTEED : 'GUARANTEED'; +GUARANTEE : 'GUARANTEE'; +GUARD : 'GUARD'; +HALF_YEARS : 'HALF_YEARS'; +HASH_AJ : 'HASH_AJ'; +HASH : 'HASH'; +HASHKEYS : 'HASHKEYS'; +HASH_SJ : 'HASH_SJ'; +HAVING : 'HAVING'; +HEADER : 'HEADER'; +HEAP : 'HEAP'; +HELP : 'HELP'; +HEXTORAW : 'HEXTORAW'; +HEXTOREF : 'HEXTOREF'; +HIDDEN_KEYWORD : 'HIDDEN'; +HIDE : 'HIDE'; +HIER_ORDER : 'HIER_ORDER'; +HIERARCHICAL : 'HIERARCHICAL'; +HIERARCHIES : 'HIERARCHIES'; +HIERARCHY : 'HIERARCHY'; +HIGH : 'HIGH'; +HINTSET_BEGIN : 'HINTSET_BEGIN'; +HINTSET_END : 'HINTSET_END'; +HOT : 'HOT'; +HOUR : 'HOUR'; +HOURS : 'HOURS'; +HTTP : 'HTTP'; +HWM_BROKERED : 'HWM_BROKERED'; +HYBRID : 'HYBRID'; +H_LETTER : 'H'; +IDENTIFIED : 'IDENTIFIED'; +IDENTIFIER : 'IDENTIFIER'; +IDENTITY : 'IDENTITY'; +IDGENERATORS : 'IDGENERATORS'; +ID : 'ID'; +IDLE_TIME : 'IDLE_TIME'; +IF : 'IF'; +IGNORE : 'IGNORE'; +IGNORE_OPTIM_EMBEDDED_HINTS : 'IGNORE_OPTIM_EMBEDDED_HINTS'; +IGNORE_ROW_ON_DUPKEY_INDEX : 'IGNORE_ROW_ON_DUPKEY_INDEX'; +IGNORE_WHERE_CLAUSE : 'IGNORE_WHERE_CLAUSE'; +ILM : 'ILM'; +IMMEDIATE : 'IMMEDIATE'; +IMPACT : 'IMPACT'; +IMPORT : 'IMPORT'; +INACTIVE : 'INACTIVE'; +INACTIVE_ACCOUNT_TIME : 'INACTIVE_ACCOUNT_TIME'; +INCLUDE : 'INCLUDE'; +INCLUDE_VERSION : 'INCLUDE_VERSION'; +INCLUDING : 'INCLUDING'; +INCREMENTAL : 'INCREMENTAL'; +INCREMENT : 'INCREMENT'; +INCR : 'INCR'; +INDENT : 'INDENT'; +INDEX_ASC : 'INDEX_ASC'; +INDEX_COMBINE : 'INDEX_COMBINE'; +INDEX_DESC : 'INDEX_DESC'; +INDEXED : 'INDEXED'; +INDEXES : 'INDEXES'; +INDEX_FFS : 'INDEX_FFS'; +INDEX_FILTER : 'INDEX_FILTER'; +INDEX : 'INDEX'; +INDEXING : 'INDEXING'; +INDEX_JOIN : 'INDEX_JOIN'; +INDEX_ROWS : 'INDEX_ROWS'; +INDEX_RRS : 'INDEX_RRS'; +INDEX_RS_ASC : 'INDEX_RS_ASC'; +INDEX_RS_DESC : 'INDEX_RS_DESC'; +INDEX_RS : 'INDEX_RS'; +INDEX_SCAN : 'INDEX_SCAN'; +INDEX_SKIP_SCAN : 'INDEX_SKIP_SCAN'; +INDEX_SS_ASC : 'INDEX_SS_ASC'; +INDEX_SS_DESC : 'INDEX_SS_DESC'; +INDEX_SS : 'INDEX_SS'; +INDEX_STATS : 'INDEX_STATS'; +INDEXTYPE : 'INDEXTYPE'; +INDEXTYPES : 'INDEXTYPES'; +INDICATOR : 'INDICATOR'; +INDICES : 'INDICES'; +INFINITE : 'INFINITE'; +INFORMATIONAL : 'INFORMATIONAL'; +INHERIT : 'INHERIT'; +IN : 'IN'; +INITCAP : 'INITCAP'; +INITIAL : 'INITIAL'; +INITIALIZED : 'INITIALIZED'; +INITIALLY : 'INITIALLY'; +INITRANS : 'INITRANS'; +INLINE : 'INLINE'; +INLINE_XMLTYPE_NT : 'INLINE_XMLTYPE_NT'; +INMEMORY : 'INMEMORY'; +IN_MEMORY_METADATA : 'IN_MEMORY_METADATA'; +INMEMORY_PRUNING : 'INMEMORY_PRUNING'; +INNER : 'INNER'; +INOUT : 'INOUT'; +INPLACE : 'INPLACE'; +INSERTCHILDXMLAFTER : 'INSERTCHILDXMLAFTER'; +INSERTCHILDXMLBEFORE : 'INSERTCHILDXMLBEFORE'; +INSERTCHILDXML : 'INSERTCHILDXML'; +INSERT : 'INSERT'; +INSERTXMLAFTER : 'INSERTXMLAFTER'; +INSERTXMLBEFORE : 'INSERTXMLBEFORE'; +INSTANCE : 'INSTANCE'; +INSTANCES : 'INSTANCES'; +INSTANTIABLE : 'INSTANTIABLE'; +INSTANTLY : 'INSTANTLY'; +INSTEAD : 'INSTEAD'; +INSTR2 : 'INSTR2'; +INSTR4 : 'INSTR4'; +INSTRB : 'INSTRB'; +INSTRC : 'INSTRC'; +INSTR : 'INSTR'; +INTEGER : 'INTEGER'; +INTERLEAVED : 'INTERLEAVED'; +INTERMEDIATE : 'INTERMEDIATE'; +INTERNAL_CONVERT : 'INTERNAL_CONVERT'; +INTERNAL_USE : 'INTERNAL_USE'; +INTERPRETED : 'INTERPRETED'; +INTERSECT : 'INTERSECT'; +INTERVAL : 'INTERVAL'; +INT : 'INT'; +INTO : 'INTO'; +INVALIDATE : 'INVALIDATE'; +INVALIDATION : 'INVALIDATION'; +INVISIBLE : 'INVISIBLE'; +IN_XQUERY : 'IN_XQUERY'; +IS : 'IS'; +IS_LEAF : 'IS_LEAF'; +ISOLATION : 'ISOLATION'; +ISOLATION_LEVEL : 'ISOLATION_LEVEL'; +ITERATE : 'ITERATE'; +ITERATION_NUMBER : 'ITERATION_NUMBER'; +JAVA : 'JAVA'; +JOB : 'JOB'; +JOIN : 'JOIN'; +JSON_ARRAYAGG : 'JSON_ARRAYAGG'; +JSON_ARRAY : 'JSON_ARRAY'; +JSON_EQUAL : 'JSON_EQUAL'; +JSON_EXISTS2 : 'JSON_EXISTS2'; +JSON_EXISTS : 'JSON_EXISTS'; +JSONGET : 'JSONGET'; +JSON : 'JSON'; +JSON_OBJECTAGG : 'JSON_OBJECTAGG'; +JSON_OBJECT : 'JSON_OBJECT'; +JSONPARSE : 'JSONPARSE'; +JSON_QUERY : 'JSON_QUERY'; +JSON_SERIALIZE : 'JSON_SERIALIZE'; +JSON_TABLE : 'JSON_TABLE'; +JSON_TEXTCONTAINS2 : 'JSON_TEXTCONTAINS2'; +JSON_TEXTCONTAINS : 'JSON_TEXTCONTAINS'; +JSON_TRANSFORM : 'JSON_TRANSFORM'; +JSON_VALUE : 'JSON_VALUE'; +K_LETTER : 'K'; +KEEP_DUPLICATES : 'KEEP_DUPLICATES'; +KEEP : 'KEEP'; +KERBEROS : 'KERBEROS'; +KEY : 'KEY'; +KEY_LENGTH : 'KEY_LENGTH'; +KEYSIZE : 'KEYSIZE'; +KEYS : 'KEYS'; +KEYSTORE : 'KEYSTORE'; +KILL : 'KILL'; +LABEL : 'LABEL'; +LANGUAGE : 'LANGUAGE'; +LAST_DAY : 'LAST_DAY'; +LAST : 'LAST'; +LAST_VALUE : 'LAST_VALUE'; +LATERAL : 'LATERAL'; +LAX : 'LAX'; +LAYER : 'LAYER'; +LDAP_REGISTRATION_ENABLED : 'LDAP_REGISTRATION_ENABLED'; +LDAP_REGISTRATION : 'LDAP_REGISTRATION'; +LDAP_REG_SYNC_INTERVAL : 'LDAP_REG_SYNC_INTERVAL'; +LEAF : 'LEAF'; +LEAD_CDB : 'LEAD_CDB'; +LEAD_CDB_URI : 'LEAD_CDB_URI'; +LEADING : 'LEADING'; +LEFT : 'LEFT'; +LENGTH2 : 'LENGTH2'; +LENGTH4 : 'LENGTH4'; +LENGTHB : 'LENGTHB'; +LENGTHC : 'LENGTHC'; +LENGTH : 'LENGTH'; +LESS : 'LESS'; +LEVEL : 'LEVEL'; +LEVEL_NAME : 'LEVEL_NAME'; +LEVELS : 'LEVELS'; +LIBRARY : 'LIBRARY'; +LIFECYCLE : 'LIFECYCLE'; +LIFE : 'LIFE'; +LIFETIME : 'LIFETIME'; +LIKE2 : 'LIKE2'; +LIKE4 : 'LIKE4'; +LIKEC : 'LIKEC'; +LIKE_EXPAND : 'LIKE_EXPAND'; +LIKE : 'LIKE'; +LIMIT : 'LIMIT'; +LINEAR : 'LINEAR'; +LINK : 'LINK'; +LIST : 'LIST'; +LN : 'LN'; +LNNVL : 'LNNVL'; +LOAD : 'LOAD'; +LOB : 'LOB'; +LOBNVL : 'LOBNVL'; +LOBS : 'LOBS'; +LOCAL_INDEXES : 'LOCAL_INDEXES'; +LOCAL : 'LOCAL'; +LOCALTIME : 'LOCALTIME'; +LOCALTIMESTAMP : 'LOCALTIMESTAMP'; +LOCATION : 'LOCATION'; +LOCATOR : 'LOCATOR'; +LOCKDOWN : 'LOCKDOWN'; +LOCKED : 'LOCKED'; +LOCKING : 'LOCKING'; +LOCK : 'LOCK'; +LOGFILE : 'LOGFILE'; +LOGFILES : 'LOGFILES'; +LOGGING : 'LOGGING'; +LOGICAL : 'LOGICAL'; +LOGICAL_READS_PER_CALL : 'LOGICAL_READS_PER_CALL'; +LOGICAL_READS_PER_SESSION : 'LOGICAL_READS_PER_SESSION'; +LOG : 'LOG'; +LOGMINING : 'LOGMINING'; +LOGOFF : 'LOGOFF'; +LOGON : 'LOGON'; +LOG_READ_ONLY_VIOLATIONS : 'LOG_READ_ONLY_VIOLATIONS'; +LONG : 'LONG'; +LOOP : 'LOOP'; +LOST : 'LOST'; +LOWER : 'LOWER'; +LOW : 'LOW'; +LPAD : 'LPAD'; +LTRIM : 'LTRIM'; +M_LETTER : 'M'; +MAIN : 'MAIN'; +MAKE_REF : 'MAKE_REF'; +MANAGED : 'MANAGED'; +MANAGE : 'MANAGE'; +MANAGEMENT : 'MANAGEMENT'; +MANAGER : 'MANAGER'; +MANDATORY : 'MANDATORY'; +MANUAL : 'MANUAL'; +MAP : 'MAP'; +MAPPING : 'MAPPING'; +MASTER : 'MASTER'; +MATCHED : 'MATCHED'; +MATCHES : 'MATCHES'; +MATCH : 'MATCH'; +MATCH_NUMBER : 'MATCH_NUMBER'; +MATCH_RECOGNIZE : 'MATCH_RECOGNIZE'; +MATERIALIZED : 'MATERIALIZED'; +MATERIALIZE : 'MATERIALIZE'; +MAXARCHLOGS : 'MAXARCHLOGS'; +MAXDATAFILES : 'MAXDATAFILES'; +MAXEXTENTS : 'MAXEXTENTS'; +MAXIMIZE : 'MAXIMIZE'; +MAXINSTANCES : 'MAXINSTANCES'; +MAXLOGFILES : 'MAXLOGFILES'; +MAXLOGHISTORY : 'MAXLOGHISTORY'; +MAXLOGMEMBERS : 'MAXLOGMEMBERS'; +MAX_SHARED_TEMP_SIZE : 'MAX_SHARED_TEMP_SIZE'; +MAXSIZE : 'MAXSIZE'; +MAXTRANS : 'MAXTRANS'; +MAXVALUE : 'MAXVALUE'; +MEASURE : 'MEASURE'; +MEASURES : 'MEASURES'; +MEDIUM : 'MEDIUM'; +MEMBER : 'MEMBER'; +MEMBER_CAPTION : 'MEMBER_CAPTION'; +MEMBER_DESCRIPTION : 'MEMBER_DESCRIPTION'; +MEMBER_NAME : 'MEMBER_NAME'; +MEMBER_UNIQUE_NAME : 'MEMBER_UNIQUE_NAME'; +MEMCOMPRESS : 'MEMCOMPRESS'; +MEMORY : 'MEMORY'; +MERGEACTIONS : 'MERGE$ACTIONS'; +MERGE_AJ : 'MERGE_AJ'; +MERGE_CONST_ON : 'MERGE_CONST_ON'; +MERGE : 'MERGE'; +MERGE_SJ : 'MERGE_SJ'; +METADATA : 'METADATA'; +METHOD : 'METHOD'; +MIGRATE : 'MIGRATE'; +MIGRATION : 'MIGRATION'; +MINEXTENTS : 'MINEXTENTS'; +MINIMIZE : 'MINIMIZE'; +MINIMUM : 'MINIMUM'; +MINING : 'MINING'; +MINUS : 'MINUS'; +MINUS_NULL : 'MINUS_NULL'; +MINUTE : 'MINUTE'; +MINUTES : 'MINUTES'; +MINVALUE : 'MINVALUE'; +MIRRORCOLD : 'MIRRORCOLD'; +MIRRORHOT : 'MIRRORHOT'; +MIRROR : 'MIRROR'; +MISSING : 'MISSING'; +MISMATCH : 'MISMATCH'; +MLSLABEL : 'MLSLABEL'; +MODEL_COMPILE_SUBQUERY : 'MODEL_COMPILE_SUBQUERY'; +MODEL_DONTVERIFY_UNIQUENESS : 'MODEL_DONTVERIFY_UNIQUENESS'; +MODEL_DYNAMIC_SUBQUERY : 'MODEL_DYNAMIC_SUBQUERY'; +MODEL_MIN_ANALYSIS : 'MODEL_MIN_ANALYSIS'; +MODEL : 'MODEL'; +MODEL_NB : 'MODEL_NB'; +MODEL_NO_ANALYSIS : 'MODEL_NO_ANALYSIS'; +MODEL_PBY : 'MODEL_PBY'; +MODEL_PUSH_REF : 'MODEL_PUSH_REF'; +MODEL_SV : 'MODEL_SV'; +MODE : 'MODE'; +MODIFICATION : 'MODIFICATION'; +MODIFY_COLUMN_TYPE : 'MODIFY_COLUMN_TYPE'; +MODIFY : 'MODIFY'; +MOD : 'MOD'; +MODULE : 'MODULE'; +MONITORING : 'MONITORING'; +MONITOR : 'MONITOR'; +MONTH : 'MONTH'; +MONTHS_BETWEEN : 'MONTHS_BETWEEN'; +MONTHS : 'MONTHS'; +MOUNT : 'MOUNT'; +MOUNTPATH : 'MOUNTPATH'; +MOUNTPOINT : 'MOUNTPOINT'; +MOVEMENT : 'MOVEMENT'; +MOVE : 'MOVE'; +MULTIDIMENSIONAL : 'MULTIDIMENSIONAL'; +MULTISET : 'MULTISET'; +MV_MERGE : 'MV_MERGE'; +NAMED : 'NAMED'; +NAME : 'NAME'; +NAMESPACE : 'NAMESPACE'; +NAN : 'NAN'; +NANVL : 'NANVL'; +NATIONAL : 'NATIONAL'; +NATIVE_FULL_OUTER_JOIN : 'NATIVE_FULL_OUTER_JOIN'; +NATIVE : 'NATIVE'; +NATURAL : 'NATURAL'; +NATURALN : 'NATURALN'; +NAV : 'NAV'; +NCHAR_CS : 'NCHAR_CS'; +NCHAR : 'NCHAR'; +NCHR : 'NCHR'; +NCLOB : 'NCLOB'; +NEEDED : 'NEEDED'; +NEG : 'NEG'; +NESTED : 'NESTED'; +NESTED_TABLE_FAST_INSERT : 'NESTED_TABLE_FAST_INSERT'; +NESTED_TABLE_GET_REFS : 'NESTED_TABLE_GET_REFS'; +NESTED_TABLE_ID : 'NESTED_TABLE_ID'; +NESTED_TABLE_SET_REFS : 'NESTED_TABLE_SET_REFS'; +NESTED_TABLE_SET_SETID : 'NESTED_TABLE_SET_SETID'; +NETWORK : 'NETWORK'; +NEVER : 'NEVER'; +NEW : 'NEW'; +NEW_TIME : 'NEW_TIME'; +NEXT_DAY : 'NEXT_DAY'; +NEXT : 'NEXT'; +NL_AJ : 'NL_AJ'; +NLJ_BATCHING : 'NLJ_BATCHING'; +NLJ_INDEX_FILTER : 'NLJ_INDEX_FILTER'; +NLJ_INDEX_SCAN : 'NLJ_INDEX_SCAN'; +NLJ_PREFETCH : 'NLJ_PREFETCH'; +NLS_CALENDAR : 'NLS_CALENDAR'; +NLS_CHARACTERSET : 'NLS_CHARACTERSET'; +NLS_CHARSET_DECL_LEN : 'NLS_CHARSET_DECL_LEN'; +NLS_CHARSET_ID : 'NLS_CHARSET_ID'; +NLS_CHARSET_NAME : 'NLS_CHARSET_NAME'; +NLS_COMP : 'NLS_COMP'; +NLS_CURRENCY : 'NLS_CURRENCY'; +NLS_DATE_FORMAT : 'NLS_DATE_FORMAT'; +NLS_DATE_LANGUAGE : 'NLS_DATE_LANGUAGE'; +NLS_INITCAP : 'NLS_INITCAP'; +NLS_ISO_CURRENCY : 'NLS_ISO_CURRENCY'; +NL_SJ : 'NL_SJ'; +NLS_LANG : 'NLS_LANG'; +NLS_LANGUAGE : 'NLS_LANGUAGE'; +NLS_LENGTH_SEMANTICS : 'NLS_LENGTH_SEMANTICS'; +NLS_LOWER : 'NLS_LOWER'; +NLS_NCHAR_CONV_EXCP : 'NLS_NCHAR_CONV_EXCP'; +NLS_NUMERIC_CHARACTERS : 'NLS_NUMERIC_CHARACTERS'; +NLS_SORT : 'NLS_SORT'; +NLSSORT : 'NLSSORT'; +NLS_SPECIAL_CHARS : 'NLS_SPECIAL_CHARS'; +NLS_TERRITORY : 'NLS_TERRITORY'; +NLS_UPPER : 'NLS_UPPER'; +NO_ACCESS : 'NO_ACCESS'; +NO_ADAPTIVE_PLAN : 'NO_ADAPTIVE_PLAN'; +NO_ANSI_REARCH : 'NO_ANSI_REARCH'; +NOAPPEND : 'NOAPPEND'; +NOARCHIVELOG : 'NOARCHIVELOG'; +NOAUDIT : 'NOAUDIT'; +NO_AUTO_REOPTIMIZE : 'NO_AUTO_REOPTIMIZE'; +NO_BASETABLE_MULTIMV_REWRITE : 'NO_BASETABLE_MULTIMV_REWRITE'; +NO_BATCH_TABLE_ACCESS_BY_ROWID : 'NO_BATCH_TABLE_ACCESS_BY_ROWID'; +NO_BIND_AWARE : 'NO_BIND_AWARE'; +NO_BUFFER : 'NO_BUFFER'; +NOCACHE : 'NOCACHE'; +NO_CARTESIAN : 'NO_CARTESIAN'; +NO_CHECK_ACL_REWRITE : 'NO_CHECK_ACL_REWRITE'; +NO_CLUSTER_BY_ROWID : 'NO_CLUSTER_BY_ROWID'; +NO_CLUSTERING : 'NO_CLUSTERING'; +NO_COALESCE_SQ : 'NO_COALESCE_SQ'; +NO_COMMON_DATA : 'NO_COMMON_DATA'; +NOCOMPRESS : 'NOCOMPRESS'; +NO_CONNECT_BY_CB_WHR_ONLY : 'NO_CONNECT_BY_CB_WHR_ONLY'; +NO_CONNECT_BY_COMBINE_SW : 'NO_CONNECT_BY_COMBINE_SW'; +NO_CONNECT_BY_COST_BASED : 'NO_CONNECT_BY_COST_BASED'; +NO_CONNECT_BY_ELIM_DUPS : 'NO_CONNECT_BY_ELIM_DUPS'; +NO_CONNECT_BY_FILTERING : 'NO_CONNECT_BY_FILTERING'; +NOCOPY : 'NOCOPY'; +NO_COST_XML_QUERY_REWRITE : 'NO_COST_XML_QUERY_REWRITE'; +NO_CPU_COSTING : 'NO_CPU_COSTING'; +NOCPU_COSTING : 'NOCPU_COSTING'; +NOCYCLE : 'NOCYCLE'; +NO_DATA_SECURITY_REWRITE : 'NO_DATA_SECURITY_REWRITE'; +NO_DECORRELATE : 'NO_DECORRELATE'; +NODELAY : 'NODELAY'; +NO_DOMAIN_INDEX_FILTER : 'NO_DOMAIN_INDEX_FILTER'; +NO_DST_UPGRADE_INSERT_CONV : 'NO_DST_UPGRADE_INSERT_CONV'; +NO_ELIM_GROUPBY : 'NO_ELIM_GROUPBY'; +NO_ELIMINATE_JOIN : 'NO_ELIMINATE_JOIN'; +NO_ELIMINATE_OBY : 'NO_ELIMINATE_OBY'; +NO_ELIMINATE_OUTER_JOIN : 'NO_ELIMINATE_OUTER_JOIN'; +NOENTITYESCAPING : 'NOENTITYESCAPING'; +NO_EXPAND_GSET_TO_UNION : 'NO_EXPAND_GSET_TO_UNION'; +NO_EXPAND : 'NO_EXPAND'; +NO_EXPAND_TABLE : 'NO_EXPAND_TABLE'; +NO_FACT : 'NO_FACT'; +NO_FACTORIZE_JOIN : 'NO_FACTORIZE_JOIN'; +NO_FILTERING : 'NO_FILTERING'; +NOFORCE : 'NOFORCE'; +NO_FULL_OUTER_JOIN_TO_OUTER : 'NO_FULL_OUTER_JOIN_TO_OUTER'; +NO_GATHER_OPTIMIZER_STATISTICS : 'NO_GATHER_OPTIMIZER_STATISTICS'; +NO_GBY_PUSHDOWN : 'NO_GBY_PUSHDOWN'; +NOGUARANTEE : 'NOGUARANTEE'; +NO_INDEX_FFS : 'NO_INDEX_FFS'; +NO_INDEX : 'NO_INDEX'; +NO_INDEX_SS : 'NO_INDEX_SS'; +NO_INMEMORY : 'NO_INMEMORY'; +NO_INMEMORY_PRUNING : 'NO_INMEMORY_PRUNING'; +NOKEEP : 'NOKEEP'; +NOSCALE : 'NOSCALE'; +NO_LOAD : 'NO_LOAD'; +NOLOCAL : 'NOLOCAL'; +NOLOGGING : 'NOLOGGING'; +NOMAPPING : 'NOMAPPING'; +NOMAXVALUE : 'NOMAXVALUE'; +NO_MERGE : 'NO_MERGE'; +NOMINIMIZE : 'NOMINIMIZE'; +NOMINVALUE : 'NOMINVALUE'; +NO_MODEL_PUSH_REF : 'NO_MODEL_PUSH_REF'; +NO_MONITORING : 'NO_MONITORING'; +NOMONITORING : 'NOMONITORING'; +NO_MONITOR : 'NO_MONITOR'; +NO_MULTIMV_REWRITE : 'NO_MULTIMV_REWRITE'; +NO_NATIVE_FULL_OUTER_JOIN : 'NO_NATIVE_FULL_OUTER_JOIN'; +NONBLOCKING : 'NONBLOCKING'; +NONEDITIONABLE : 'NONEDITIONABLE'; +NONE : 'NONE'; +NO_NLJ_BATCHING : 'NO_NLJ_BATCHING'; +NO_NLJ_PREFETCH : 'NO_NLJ_PREFETCH'; +NO : 'NO'; +NONSCHEMA : 'NONSCHEMA'; +NO_OBJECT_LINK : 'NO_OBJECT_LINK'; +NOORDER : 'NOORDER'; +NO_ORDER_ROLLUPS : 'NO_ORDER_ROLLUPS'; +NO_OUTER_JOIN_TO_ANTI : 'NO_OUTER_JOIN_TO_ANTI'; +NO_OUTER_JOIN_TO_INNER : 'NO_OUTER_JOIN_TO_INNER'; +NOOVERRIDE : 'NOOVERRIDE'; +NO_PARALLEL_INDEX : 'NO_PARALLEL_INDEX'; +NOPARALLEL_INDEX : 'NOPARALLEL_INDEX'; +NO_PARALLEL : 'NO_PARALLEL'; +NOPARALLEL : 'NOPARALLEL'; +NO_PARTIAL_COMMIT : 'NO_PARTIAL_COMMIT'; +NO_PARTIAL_JOIN : 'NO_PARTIAL_JOIN'; +NO_PARTIAL_ROLLUP_PUSHDOWN : 'NO_PARTIAL_ROLLUP_PUSHDOWN'; +NOPARTITION : 'NOPARTITION'; +NO_PLACE_DISTINCT : 'NO_PLACE_DISTINCT'; +NO_PLACE_GROUP_BY : 'NO_PLACE_GROUP_BY'; +NO_PQ_CONCURRENT_UNION : 'NO_PQ_CONCURRENT_UNION'; +NO_PQ_MAP : 'NO_PQ_MAP'; +NOPROMPT : 'NOPROMPT'; +NO_PQ_REPLICATE : 'NO_PQ_REPLICATE'; +NO_PQ_SKEW : 'NO_PQ_SKEW'; +NO_PRUNE_GSETS : 'NO_PRUNE_GSETS'; +NO_PULL_PRED : 'NO_PULL_PRED'; +NO_PUSH_PRED : 'NO_PUSH_PRED'; +NO_PUSH_SUBQ : 'NO_PUSH_SUBQ'; +NO_PX_FAULT_TOLERANCE : 'NO_PX_FAULT_TOLERANCE'; +NO_PX_JOIN_FILTER : 'NO_PX_JOIN_FILTER'; +NO_QKN_BUFF : 'NO_QKN_BUFF'; +NO_QUERY_TRANSFORMATION : 'NO_QUERY_TRANSFORMATION'; +NO_REF_CASCADE : 'NO_REF_CASCADE'; +NORELOCATE : 'NORELOCATE'; +NORELY : 'NORELY'; +NOREPAIR : 'NOREPAIR'; +NOREPLAY : 'NOREPLAY'; +NORESETLOGS : 'NORESETLOGS'; +NO_RESULT_CACHE : 'NO_RESULT_CACHE'; +NOREVERSE : 'NOREVERSE'; +NO_REWRITE : 'NO_REWRITE'; +NOREWRITE : 'NOREWRITE'; +NORMAL : 'NORMAL'; +NO_ROOT_SW_FOR_LOCAL : 'NO_ROOT_SW_FOR_LOCAL'; +NOROWDEPENDENCIES : 'NOROWDEPENDENCIES'; +NOSCHEMACHECK : 'NOSCHEMACHECK'; +NOSEGMENT : 'NOSEGMENT'; +NO_SEMIJOIN : 'NO_SEMIJOIN'; +NO_SEMI_TO_INNER : 'NO_SEMI_TO_INNER'; +NO_SET_TO_JOIN : 'NO_SET_TO_JOIN'; +NOSORT : 'NOSORT'; +NO_SQL_TRANSLATION : 'NO_SQL_TRANSLATION'; +NO_SQL_TUNE : 'NO_SQL_TUNE'; +NO_STAR_TRANSFORMATION : 'NO_STAR_TRANSFORMATION'; +NO_STATEMENT_QUEUING : 'NO_STATEMENT_QUEUING'; +NO_STATS_GSETS : 'NO_STATS_GSETS'; +NOSTRICT : 'NOSTRICT'; +NO_SUBQUERY_PRUNING : 'NO_SUBQUERY_PRUNING'; +NO_SUBSTRB_PAD : 'NO_SUBSTRB_PAD'; +NO_SWAP_JOIN_INPUTS : 'NO_SWAP_JOIN_INPUTS'; +NOSWITCH : 'NOSWITCH'; +NO_TABLE_LOOKUP_BY_NL : 'NO_TABLE_LOOKUP_BY_NL'; +NO_TEMP_TABLE : 'NO_TEMP_TABLE'; +NOTHING : 'NOTHING'; +NOTIFICATION : 'NOTIFICATION'; +NOT : 'NOT'; +NO_TRANSFORM_DISTINCT_AGG : 'NO_TRANSFORM_DISTINCT_AGG'; +NO_UNNEST : 'NO_UNNEST'; +NO_USE_CUBE : 'NO_USE_CUBE'; +NO_USE_HASH_AGGREGATION : 'NO_USE_HASH_AGGREGATION'; +NO_USE_HASH_GBY_FOR_PUSHDOWN : 'NO_USE_HASH_GBY_FOR_PUSHDOWN'; +NO_USE_HASH : 'NO_USE_HASH'; +NO_USE_INVISIBLE_INDEXES : 'NO_USE_INVISIBLE_INDEXES'; +NO_USE_MERGE : 'NO_USE_MERGE'; +NO_USE_NL : 'NO_USE_NL'; +NO_USE_VECTOR_AGGREGATION : 'NO_USE_VECTOR_AGGREGATION'; +NOVALIDATE : 'NOVALIDATE'; +NO_VECTOR_TRANSFORM_DIMS : 'NO_VECTOR_TRANSFORM_DIMS'; +NO_VECTOR_TRANSFORM_FACT : 'NO_VECTOR_TRANSFORM_FACT'; +NO_VECTOR_TRANSFORM : 'NO_VECTOR_TRANSFORM'; +NOWAIT : 'NOWAIT'; +NO_XDB_FASTPATH_INSERT : 'NO_XDB_FASTPATH_INSERT'; +NO_XML_DML_REWRITE : 'NO_XML_DML_REWRITE'; +NO_XMLINDEX_REWRITE_IN_SELECT : 'NO_XMLINDEX_REWRITE_IN_SELECT'; +NO_XMLINDEX_REWRITE : 'NO_XMLINDEX_REWRITE'; +NO_XML_QUERY_REWRITE : 'NO_XML_QUERY_REWRITE'; +NO_ZONEMAP : 'NO_ZONEMAP'; +NTH_VALUE : 'NTH_VALUE'; +NULLIF : 'NULLIF'; +NULL_ : 'NULL'; +NULLS : 'NULLS'; +NUMBER : 'NUMBER'; +NUMERIC : 'NUMERIC'; +NUM_INDEX_KEYS : 'NUM_INDEX_KEYS'; +NUMTODSINTERVAL : 'NUMTODSINTERVAL'; +NUMTOYMINTERVAL : 'NUMTOYMINTERVAL'; +NVARCHAR2 : 'NVARCHAR2'; +NVL2 : 'NVL2'; +OBJECT2XML : 'OBJECT2XML'; +OBJECT : 'OBJECT'; +OBJ_ID : 'OBJ_ID'; +OBJNO : 'OBJNO'; +OBJNO_REUSE : 'OBJNO_REUSE'; +OCCURENCES : 'OCCURENCES'; +OFFLINE : 'OFFLINE'; +OFF : 'OFF'; +OFFSET : 'OFFSET'; +OF : 'OF'; +OIDINDEX : 'OIDINDEX'; +OID : 'OID'; +OLAP : 'OLAP'; +OLD : 'OLD'; +OLD_PUSH_PRED : 'OLD_PUSH_PRED'; +OLS : 'OLS'; +OLTP : 'OLTP'; +OMIT : 'OMIT'; +ONE : 'ONE'; +ONLINE : 'ONLINE'; +ONLINELOG : 'ONLINELOG'; +ONLY : 'ONLY'; +ON : 'ON'; +OPAQUE : 'OPAQUE'; +OPAQUE_TRANSFORM : 'OPAQUE_TRANSFORM'; +OPAQUE_XCANONICAL : 'OPAQUE_XCANONICAL'; +OPCODE : 'OPCODE'; +OPEN : 'OPEN'; +OPERATIONS : 'OPERATIONS'; +OPERATOR : 'OPERATOR'; +OPT_ESTIMATE : 'OPT_ESTIMATE'; +OPTIMAL : 'OPTIMAL'; +OPTIMIZE : 'OPTIMIZE'; +OPTIMIZER_FEATURES_ENABLE : 'OPTIMIZER_FEATURES_ENABLE'; +OPTIMIZER_GOAL : 'OPTIMIZER_GOAL'; +OPTION : 'OPTION'; +OPT_PARAM : 'OPT_PARAM'; +ORA_BRANCH : 'ORA_BRANCH'; +ORA_CHECK_ACL : 'ORA_CHECK_ACL'; +ORA_CHECK_PRIVILEGE : 'ORA_CHECK_PRIVILEGE'; +ORA_CLUSTERING : 'ORA_CLUSTERING'; +ORADATA : 'ORADATA'; +ORADEBUG : 'ORADEBUG'; +ORA_DST_AFFECTED : 'ORA_DST_AFFECTED'; +ORA_DST_CONVERT : 'ORA_DST_CONVERT'; +ORA_DST_ERROR : 'ORA_DST_ERROR'; +ORA_GET_ACLIDS : 'ORA_GET_ACLIDS'; +ORA_GET_PRIVILEGES : 'ORA_GET_PRIVILEGES'; +ORA_HASH : 'ORA_HASH'; +ORA_INVOKING_USERID : 'ORA_INVOKING_USERID'; +ORA_INVOKING_USER : 'ORA_INVOKING_USER'; +ORA_INVOKING_XS_USER_GUID : 'ORA_INVOKING_XS_USER_GUID'; +ORA_INVOKING_XS_USER : 'ORA_INVOKING_XS_USER'; +ORA_RAWCOMPARE : 'ORA_RAWCOMPARE'; +ORA_RAWCONCAT : 'ORA_RAWCONCAT'; +ORA_ROWSCN : 'ORA_ROWSCN'; +ORA_ROWSCN_RAW : 'ORA_ROWSCN_RAW'; +ORA_ROWVERSION : 'ORA_ROWVERSION'; +ORA_TABVERSION : 'ORA_TABVERSION'; +ORA_WRITE_TIME : 'ORA_WRITE_TIME'; +ORDERED : 'ORDERED'; +ORDERED_PREDICATES : 'ORDERED_PREDICATES'; +ORDER : 'ORDER'; +ORDINALITY : 'ORDINALITY'; +OR_EXPAND : 'OR_EXPAND'; +ORGANIZATION : 'ORGANIZATION'; +OR : 'OR'; +OR_PREDICATES : 'OR_PREDICATES'; +OSERROR : 'OSERROR'; +OTHER : 'OTHER'; +OUTER_JOIN_TO_ANTI : 'OUTER_JOIN_TO_ANTI'; +OUTER_JOIN_TO_INNER : 'OUTER_JOIN_TO_INNER'; +OUTER : 'OUTER'; +OUTLINE_LEAF : 'OUTLINE_LEAF'; +OUTLINE : 'OUTLINE'; +OUT_OF_LINE : 'OUT_OF_LINE'; +OUT : 'OUT'; +OVERFLOW_NOMOVE : 'OVERFLOW_NOMOVE'; +OVERFLOW : 'OVERFLOW'; +OVERLAPS : 'OVERLAPS'; +OVER : 'OVER'; +OVERRIDING : 'OVERRIDING'; +OWNER : 'OWNER'; +OWNERSHIP : 'OWNERSHIP'; +OWN : 'OWN'; +P_LETTER : 'P'; +PACKAGE : 'PACKAGE'; +PACKAGES : 'PACKAGES'; +PARALLEL_ENABLE : 'PARALLEL_ENABLE'; +PARALLEL_INDEX : 'PARALLEL_INDEX'; +PARALLEL : 'PARALLEL'; +PARAMETERFILE : 'PARAMETERFILE'; +PARAMETERS : 'PARAMETERS'; +PARAM : 'PARAM'; +PARENT : 'PARENT'; +PARENT_LEVEL_NAME : 'PARENT_LEVEL_NAME'; +PARENT_UNIQUE_NAME : 'PARENT_UNIQUE_NAME'; +PARITY : 'PARITY'; +PARTIAL_JOIN : 'PARTIAL_JOIN'; +PARTIALLY : 'PARTIALLY'; +PARTIAL : 'PARTIAL'; +PARTIAL_ROLLUP_PUSHDOWN : 'PARTIAL_ROLLUP_PUSHDOWN'; +PARTITION_HASH : 'PARTITION_HASH'; +PARTITION_LIST : 'PARTITION_LIST'; +PARTITION : 'PARTITION'; +PARTITION_RANGE : 'PARTITION_RANGE'; +PARTITIONS : 'PARTITIONS'; +PARTNUMINST : 'PART$NUM$INST'; +PASSING : 'PASSING'; +PASSWORD_GRACE_TIME : 'PASSWORD_GRACE_TIME'; +PASSWORD_LIFE_TIME : 'PASSWORD_LIFE_TIME'; +PASSWORD_LOCK_TIME : 'PASSWORD_LOCK_TIME'; +PASSWORD : 'PASSWORD'; +PASSWORD_REUSE_MAX : 'PASSWORD_REUSE_MAX'; +PASSWORD_REUSE_TIME : 'PASSWORD_REUSE_TIME'; +PASSWORD_ROLLOVER_TIME : 'PASSWORD_ROLLOVER_TIME'; +PASSWORD_VERIFY_FUNCTION : 'PASSWORD_VERIFY_FUNCTION'; +PAST : 'PAST'; +PATCH : 'PATCH'; +PATH : 'PATH'; +PATH_PREFIX : 'PATH_PREFIX'; +PATHS : 'PATHS'; +PATTERN : 'PATTERN'; +PBL_HS_BEGIN : 'PBL_HS_BEGIN'; +PBL_HS_END : 'PBL_HS_END'; +PCTFREE : 'PCTFREE'; +PCTINCREASE : 'PCTINCREASE'; +PCTTHRESHOLD : 'PCTTHRESHOLD'; +PCTUSED : 'PCTUSED'; +PCTVERSION : 'PCTVERSION'; +PENDING : 'PENDING'; +PERCENT_FOUND : '%' SPACE* 'FOUND'; +PERCENT_ISOPEN : '%' SPACE* 'ISOPEN'; +PERCENT_NOTFOUND : '%' SPACE* 'NOTFOUND'; +PERCENT_KEYWORD : 'PERCENT'; +PERCENT_RANKM : 'PERCENT_RANKM'; +PERCENT_ROWCOUNT : '%' SPACE* 'ROWCOUNT'; +PERCENT_ROWTYPE : '%' SPACE* 'ROWTYPE'; +PERCENT_TYPE : '%' SPACE* 'TYPE'; +PERCENT_BULK_EXCEPTIONS : '%' SPACE* 'BULK_EXCEPTIONS'; +PERCENT_BULK_ROWCOUNT : '%' SPACE* 'BULK_ROWCOUNT'; +PERFORMANCE : 'PERFORMANCE'; +PERIOD_KEYWORD : 'PERIOD'; +PERMANENT : 'PERMANENT'; +PERMISSION : 'PERMISSION'; +PERMUTE : 'PERMUTE'; +PER : 'PER'; +PFILE : 'PFILE'; +PHYSICAL : 'PHYSICAL'; +PIKEY : 'PIKEY'; +PIPELINED : 'PIPELINED'; +PIPE : 'PIPE'; +PIV_GB : 'PIV_GB'; +PIVOT : 'PIVOT'; +PIV_SSF : 'PIV_SSF'; +PLACE_DISTINCT : 'PLACE_DISTINCT'; +PLACE_GROUP_BY : 'PLACE_GROUP_BY'; +PLAN : 'PLAN'; +PLSCOPE_SETTINGS : 'PLSCOPE_SETTINGS'; +PLS_INTEGER : 'PLS_INTEGER'; +PLSQL_CCFLAGS : 'PLSQL_CCFLAGS'; +PLSQL_CODE_TYPE : 'PLSQL_CODE_TYPE'; +PLSQL_DEBUG : 'PLSQL_DEBUG'; +PLSQL_OPTIMIZE_LEVEL : 'PLSQL_OPTIMIZE_LEVEL'; +PLSQL_WARNINGS : 'PLSQL_WARNINGS'; +PLUGGABLE : 'PLUGGABLE'; +PMEM : 'PMEM'; +POINT : 'POINT'; +POLICY : 'POLICY'; +POOL_16K : 'POOL_16K'; +POOL_2K : 'POOL_2K'; +POOL_32K : 'POOL_32K'; +POOL_4K : 'POOL_4K'; +POOL_8K : 'POOL_8K'; +POSITIVEN : 'POSITIVEN'; +POSITIVE : 'POSITIVE'; +POST_TRANSACTION : 'POST_TRANSACTION'; +POWERMULTISET_BY_CARDINALITY : 'POWERMULTISET_BY_CARDINALITY'; +POWERMULTISET : 'POWERMULTISET'; +POWER : 'POWER'; +PQ_CONCURRENT_UNION : 'PQ_CONCURRENT_UNION'; +PQ_DISTRIBUTE : 'PQ_DISTRIBUTE'; +PQ_DISTRIBUTE_WINDOW : 'PQ_DISTRIBUTE_WINDOW'; +PQ_FILTER : 'PQ_FILTER'; +PQ_MAP : 'PQ_MAP'; +PQ_NOMAP : 'PQ_NOMAP'; +PQ_REPLICATE : 'PQ_REPLICATE'; +PQ_SKEW : 'PQ_SKEW'; +PRAGMA : 'PRAGMA'; +PREBUILT : 'PREBUILT'; +PRECEDES : 'PRECEDES'; +PRECEDING : 'PRECEDING'; +PRECISION : 'PRECISION'; +PRECOMPUTE_SUBQUERY : 'PRECOMPUTE_SUBQUERY'; +PREDICATE_REORDERS : 'PREDICATE_REORDERS'; +PRELOAD : 'PRELOAD'; +PREPARE : 'PREPARE'; +PRESENTNNV : 'PRESENTNNV'; +PRESENT : 'PRESENT'; +PRESENTV : 'PRESENTV'; +PRESERVE_OID : 'PRESERVE_OID'; +PRESERVE : 'PRESERVE'; +PRETTY : 'PRETTY'; +PREVIOUS : 'PREVIOUS'; +PREV : 'PREV'; +PRIMARY : 'PRIMARY'; +PRINTBLOBTOCLOB : 'PRINTBLOBTOCLOB'; +PRIORITY : 'PRIORITY'; +PRIOR : 'PRIOR'; +PRIVATE : 'PRIVATE'; +PRIVATE_SGA : 'PRIVATE_SGA'; +PRIVILEGED : 'PRIVILEGED'; +PRIVILEGE : 'PRIVILEGE'; +PRIVILEGES : 'PRIVILEGES'; +PROCEDURAL : 'PROCEDURAL'; +PROCEDURE : 'PROCEDURE'; +PROCESS : 'PROCESS'; +PROFILE : 'PROFILE'; +PROGRAM : 'PROGRAM'; +PROJECT : 'PROJECT'; +PROPAGATE : 'PROPAGATE'; +PROPERTY : 'PROPERTY'; +PROTECTED : 'PROTECTED'; +PROTECTION : 'PROTECTION'; +PROTOCOL : 'PROTOCOL'; +PROXY : 'PROXY'; +PRUNING : 'PRUNING'; +PUBLIC : 'PUBLIC'; +PULL_PRED : 'PULL_PRED'; +PURGE : 'PURGE'; +PUSH_PRED : 'PUSH_PRED'; +PUSH_SUBQ : 'PUSH_SUBQ'; +PX_FAULT_TOLERANCE : 'PX_FAULT_TOLERANCE'; +PX_GRANULE : 'PX_GRANULE'; +PX_JOIN_FILTER : 'PX_JOIN_FILTER'; +QB_NAME : 'QB_NAME'; +QUARTERS : 'QUARTERS'; +QUERY_BLOCK : 'QUERY_BLOCK'; +QUERY : 'QUERY'; +QUEUE_CURR : 'QUEUE_CURR'; +QUEUE : 'QUEUE'; +QUEUE_ROWP : 'QUEUE_ROWP'; +QUIESCE : 'QUIESCE'; +QUORUM : 'QUORUM'; +QUOTA : 'QUOTA'; +QUOTAGROUP : 'QUOTAGROUP'; +RAISE : 'RAISE'; +RANDOM_LOCAL : 'RANDOM_LOCAL'; +RANDOM : 'RANDOM'; +RANGE : 'RANGE'; +RANKM : 'RANKM'; +RAPIDLY : 'RAPIDLY'; +RAW : 'RAW'; +RAWTOHEX : 'RAWTOHEX'; +RAWTONHEX : 'RAWTONHEX'; +RBA : 'RBA'; +RBO_OUTLINE : 'RBO_OUTLINE'; +RDBA : 'RDBA'; +READ : 'READ'; +READS : 'READS'; +REALM : 'REALM'; +REAL : 'REAL'; +REBALANCE : 'REBALANCE'; +REBUILD : 'REBUILD'; +RECORD : 'RECORD'; +RECORDS_PER_BLOCK : 'RECORDS_PER_BLOCK'; +RECOVERABLE : 'RECOVERABLE'; +RECOVER : 'RECOVER'; +RECOVERY : 'RECOVERY'; +RECYCLEBIN : 'RECYCLEBIN'; +RECYCLE : 'RECYCLE'; +REDACTION : 'REDACTION'; +REDEFINE : 'REDEFINE'; +REDO : 'REDO'; +REDUCED : 'REDUCED'; +REDUNDANCY : 'REDUNDANCY'; +REF_CASCADE_CURSOR : 'REF_CASCADE_CURSOR'; +REFERENCED : 'REFERENCED'; +REFERENCE : 'REFERENCE'; +REFERENCES : 'REFERENCES'; +REFERENCING : 'REFERENCING'; +REF : 'REF'; +REFRESH : 'REFRESH'; +REFTOHEX : 'REFTOHEX'; +REGEXP_COUNT : 'REGEXP_COUNT'; +REGEXP_INSTR : 'REGEXP_INSTR'; +REGEXP_LIKE : 'REGEXP_LIKE'; +REGEXP_REPLACE : 'REGEXP_REPLACE'; +REGEXP_SUBSTR : 'REGEXP_SUBSTR'; +REGISTER : 'REGISTER'; +REGR_AVGX : 'REGR_AVGX'; +REGR_AVGY : 'REGR_AVGY'; +REGR_COUNT : 'REGR_COUNT'; +REGR_INTERCEPT : 'REGR_INTERCEPT'; +REGR_R2 : 'REGR_R2'; +REGR_SLOPE : 'REGR_SLOPE'; +REGR_SXX : 'REGR_SXX'; +REGR_SXY : 'REGR_SXY'; +REGR_SYY : 'REGR_SYY'; +REGULAR : 'REGULAR'; +REJECT : 'REJECT'; +REKEY : 'REKEY'; +RELATIONAL : 'RELATIONAL'; +RELIES_ON : 'RELIES_ON'; +RELOCATE : 'RELOCATE'; +RELY : 'RELY'; +REMAINDER : 'REMAINDER'; +REMOTE : 'REMOTE'; +REMOTE_MAPPED : 'REMOTE_MAPPED'; +REMOVE : 'REMOVE'; +RENAME : 'RENAME'; +REPAIR : 'REPAIR'; +REPEAT : 'REPEAT'; +REPLACE : 'REPLACE'; +REPLICATION : 'REPLICATION'; +REQUIRED : 'REQUIRED'; +RESETLOGS : 'RESETLOGS'; +RESET : 'RESET'; +RESIZE : 'RESIZE'; +RESOLVE : 'RESOLVE'; +RESOLVER : 'RESOLVER'; +RESOURCE : 'RESOURCE'; +RESPECT : 'RESPECT'; +RESTART : 'RESTART'; +RESTORE_AS_INTERVALS : 'RESTORE_AS_INTERVALS'; +RESTORE : 'RESTORE'; +RESTRICT_ALL_REF_CONS : 'RESTRICT_ALL_REF_CONS'; +RESTRICTED : 'RESTRICTED'; +RESTRICT_REFERENCES : 'RESTRICT_REFERENCES'; +RESTRICT : 'RESTRICT'; +RESULT_CACHE : 'RESULT_CACHE'; +RESULT : 'RESULT'; +RESUMABLE : 'RESUMABLE'; +RESUME : 'RESUME'; +RETENTION : 'RETENTION'; +RETRY_ON_ROW_CHANGE : 'RETRY_ON_ROW_CHANGE'; +RETURNING : 'RETURNING'; +RETURN : 'RETURN'; +REUSE : 'REUSE'; +REVERSE : 'REVERSE'; +REVOKE : 'REVOKE'; +REWRITE_OR_ERROR : 'REWRITE_OR_ERROR'; +REWRITE : 'REWRITE'; +RIGHT : 'RIGHT'; +ROLE : 'ROLE'; +ROLESET : 'ROLESET'; +ROLES : 'ROLES'; +ROLLBACK : 'ROLLBACK'; +ROLLING : 'ROLLING'; +ROLLUP : 'ROLLUP'; +ROWDEPENDENCIES : 'ROWDEPENDENCIES'; +ROWID_MAPPING_TABLE : 'ROWID_MAPPING_TABLE'; +ROWID : 'ROWID'; +ROWIDTOCHAR : 'ROWIDTOCHAR'; +ROWIDTONCHAR : 'ROWIDTONCHAR'; +ROW_LENGTH : 'ROW_LENGTH'; +ROWNUM : 'ROWNUM'; +ROW : 'ROW'; +ROWS : 'ROWS'; +RPAD : 'RPAD'; +RTRIM : 'RTRIM'; +RULE : 'RULE'; +RULES : 'RULES'; +RUNNING : 'RUNNING'; +SALT : 'SALT'; +SAMPLE : 'SAMPLE'; +SAVE_AS_INTERVALS : 'SAVE_AS_INTERVALS'; +SAVEPOINT : 'SAVEPOINT'; +SAVE : 'SAVE'; +SB4 : 'SB4'; +SCALE_ROWS : 'SCALE_ROWS'; +SCALE : 'SCALE'; +SCAN_INSTANCES : 'SCAN_INSTANCES'; +SCAN : 'SCAN'; +SCHEDULER : 'SCHEDULER'; +SCHEMACHECK : 'SCHEMACHECK'; +SCHEMA : 'SCHEMA'; +SCN_ASCENDING : 'SCN_ASCENDING'; +SCN : 'SCN'; +SCOPE : 'SCOPE'; +SCRUB : 'SCRUB'; +SD_ALL : 'SD_ALL'; +SD_INHIBIT : 'SD_INHIBIT'; +SDO_GEOM_MBR : 'SDO_GEOM_MBR'; +SDO_GEOMETRY : 'SDO_GEOMETRY'; +SD_SHOW : 'SD_SHOW'; +SEARCH : 'SEARCH'; +SECOND : 'SECOND'; +SECONDS : 'SECONDS'; +SECRET : 'SECRET'; +SECUREFILE_DBA : 'SECUREFILE_DBA'; +SECUREFILE : 'SECUREFILE'; +SECURITY : 'SECURITY'; +SEED : 'SEED'; +SEG_BLOCK : 'SEG_BLOCK'; +SEG_FILE : 'SEG_FILE'; +SEGMENT : 'SEGMENT'; +SELECTIVITY : 'SELECTIVITY'; +SELECT : 'SELECT'; +SELF : 'SELF'; +SEMIJOIN_DRIVER : 'SEMIJOIN_DRIVER'; +SEMIJOIN : 'SEMIJOIN'; +SEMI_TO_INNER : 'SEMI_TO_INNER'; +SEQUENCED : 'SEQUENCED'; +SEQUENCE : 'SEQUENCE'; +SEQUENTIAL : 'SEQUENTIAL'; +SEQ : 'SEQ'; +SERIALIZABLE : 'SERIALIZABLE'; +SERIALLY_REUSABLE : 'SERIALLY_REUSABLE'; +SERIAL : 'SERIAL'; +SERVERERROR : 'SERVERERROR'; +SERVICE_NAME_CONVERT : 'SERVICE_NAME_CONVERT'; +SERVICE : 'SERVICE'; +SERVICES : 'SERVICES'; +SESSION_CACHED_CURSORS : 'SESSION_CACHED_CURSORS'; +SESSION : 'SESSION'; +SESSIONS_PER_USER : 'SESSIONS_PER_USER'; +SESSIONTIMEZONE : 'SESSIONTIMEZONE'; +SESSIONTZNAME : 'SESSIONTZNAME'; +SET : 'SET'; +SETS : 'SETS'; +SETTINGS : 'SETTINGS'; +SET_TO_JOIN : 'SET_TO_JOIN'; +SEVERE : 'SEVERE'; +SHARDSPACE : 'SHARDSPACE'; +SHARED_POOL : 'SHARED_POOL'; +SHARED : 'SHARED'; +SHARE : 'SHARE'; +SHARING : 'SHARING'; +SHELFLIFE : 'SHELFLIFE'; +SHOW : 'SHOW'; +SHRINK : 'SHRINK'; +SHUTDOWN : 'SHUTDOWN'; +SIBLINGS : 'SIBLINGS'; +SID : 'SID'; +SITE : 'SITE'; +SIGNAL_COMPONENT : 'SIGNAL_COMPONENT'; +SIGNAL_FUNCTION : 'SIGNAL_FUNCTION'; +SIGN : 'SIGN'; +SIGNTYPE : 'SIGNTYPE'; +SIMPLE_INTEGER : 'SIMPLE_INTEGER'; +SIMPLE : 'SIMPLE'; +SINGLE : 'SINGLE'; +SINGLETASK : 'SINGLETASK'; +SINH : 'SINH'; +SIN : 'SIN'; +SIZE : 'SIZE'; +SKIP_EXT_OPTIMIZER : 'SKIP_EXT_OPTIMIZER'; +SKIP_ : 'SKIP'; +SKIP_UNQ_UNUSABLE_IDX : 'SKIP_UNQ_UNUSABLE_IDX'; +SKIP_UNUSABLE_INDEXES : 'SKIP_UNUSABLE_INDEXES'; +SMALLFILE : 'SMALLFILE'; +SMALLINT : 'SMALLINT'; +SNAPSHOT : 'SNAPSHOT'; +SOME : 'SOME'; +SORT : 'SORT'; +SOUNDEX : 'SOUNDEX'; +SOURCE_FILE_DIRECTORY : 'SOURCE_FILE_DIRECTORY'; +SOURCE_FILE_NAME_CONVERT : 'SOURCE_FILE_NAME_CONVERT'; +SOURCE : 'SOURCE'; +SPACE_KEYWORD : 'SPACE'; +SPECIFICATION : 'SPECIFICATION'; +SPFILE : 'SPFILE'; +SPLIT : 'SPLIT'; +SPREADSHEET : 'SPREADSHEET'; +SQLDATA : 'SQLDATA'; +SQLERROR : 'SQLERROR'; +SQLLDR : 'SQLLDR'; +SQL : 'SQL'; +FILE_EXT : 'PKB' | 'PKS'; +SQL_MACRO : 'SQL_MACRO'; +SQL_TRACE : 'SQL_TRACE'; +SQL_TRANSLATION_PROFILE : 'SQL_TRANSLATION_PROFILE'; +SQRT : 'SQRT'; +STALE : 'STALE'; +STANDALONE : 'STANDALONE'; +STANDARD : 'STANDARD'; +STANDARD_HASH : 'STANDARD_HASH'; +STANDBY_MAX_DATA_DELAY : 'STANDBY_MAX_DATA_DELAY'; +STANDBYS : 'STANDBYS'; +STANDBY : 'STANDBY'; +STAR : 'STAR'; +STAR_TRANSFORMATION : 'STAR_TRANSFORMATION'; +START : 'START'; +STARTUP : 'STARTUP'; +STATEMENT_ID : 'STATEMENT_ID'; +STATEMENT_QUEUING : 'STATEMENT_QUEUING'; +STATEMENTS : 'STATEMENTS'; +STATEMENT : 'STATEMENT'; +STATE : 'STATE'; +STATIC : 'STATIC'; +STATISTICS : 'STATISTICS'; +STATS_BINOMIAL_TEST : 'STATS_BINOMIAL_TEST'; +STATS_CROSSTAB : 'STATS_CROSSTAB'; +STATS_F_TEST : 'STATS_F_TEST'; +STATS_KS_TEST : 'STATS_KS_TEST'; +STATS_MODE : 'STATS_MODE'; +STATS_MW_TEST : 'STATS_MW_TEST'; +STATS_ONE_WAY_ANOVA : 'STATS_ONE_WAY_ANOVA'; +STATS_T_TEST_INDEP : 'STATS_T_TEST_INDEP'; +STATS_T_TEST_INDEPU : 'STATS_T_TEST_INDEPU'; +STATS_T_TEST_ONE : 'STATS_T_TEST_ONE'; +STATS_T_TEST_PAIRED : 'STATS_T_TEST_PAIRED'; +STATS_WSR_TEST : 'STATS_WSR_TEST'; +STDDEV_POP : 'STDDEV_POP'; +STDDEV_SAMP : 'STDDEV_SAMP'; +STOP : 'STOP'; +STORAGE : 'STORAGE'; +STORE : 'STORE'; +STREAMS : 'STREAMS'; +STREAM : 'STREAM'; +STRICT : 'STRICT'; +STRING : 'STRING'; +STRIPE_COLUMNS : 'STRIPE_COLUMNS'; +STRIPE_WIDTH : 'STRIPE_WIDTH'; +STRIP : 'STRIP'; +STRUCTURE : 'STRUCTURE'; +SUBMULTISET : 'SUBMULTISET'; +SUBPARTITION_REL : 'SUBPARTITION_REL'; +SUBPARTITIONS : 'SUBPARTITIONS'; +SUBPARTITION : 'SUBPARTITION'; +SUBQUERIES : 'SUBQUERIES'; +SUBQUERY_PRUNING : 'SUBQUERY_PRUNING'; +SUBSCRIBE : 'SUBSCRIBE'; +SUBSET : 'SUBSET'; +SUBSTITUTABLE : 'SUBSTITUTABLE'; +SUBSTR2 : 'SUBSTR2'; +SUBSTR4 : 'SUBSTR4'; +SUBSTRB : 'SUBSTRB'; +SUBSTRC : 'SUBSTRC'; +SUBTYPE : 'SUBTYPE'; +SUCCESSFUL : 'SUCCESSFUL'; +SUCCESS : 'SUCCESS'; +SUMMARY : 'SUMMARY'; +SUPPLEMENTAL : 'SUPPLEMENTAL'; +SUSPEND : 'SUSPEND'; +SWAP_JOIN_INPUTS : 'SWAP_JOIN_INPUTS'; +SWITCHOVER : 'SWITCHOVER'; +SWITCH : 'SWITCH'; +SYNCHRONOUS : 'SYNCHRONOUS'; +SYNC : 'SYNC'; +SYNONYM : 'SYNONYM'; +SYS : 'SYS'; +SYSASM : 'SYSASM'; +SYS_AUDIT : 'SYS_AUDIT'; +SYSAUX : 'SYSAUX'; +SYSBACKUP : 'SYSBACKUP'; +SYS_CHECKACL : 'SYS_CHECKACL'; +SYS_CHECK_PRIVILEGE : 'SYS_CHECK_PRIVILEGE'; +SYS_CONNECT_BY_PATH : 'SYS_CONNECT_BY_PATH'; +SYS_CONTEXT : 'SYS_CONTEXT'; +SYSDATE : 'SYSDATE'; +SYSDBA : 'SYSDBA'; +SYS_DBURIGEN : 'SYS_DBURIGEN'; +SYSDG : 'SYSDG'; +SYS_DL_CURSOR : 'SYS_DL_CURSOR'; +SYS_DM_RXFORM_CHR : 'SYS_DM_RXFORM_CHR'; +SYS_DM_RXFORM_NUM : 'SYS_DM_RXFORM_NUM'; +SYS_DOM_COMPARE : 'SYS_DOM_COMPARE'; +SYS_DST_PRIM2SEC : 'SYS_DST_PRIM2SEC'; +SYS_DST_SEC2PRIM : 'SYS_DST_SEC2PRIM'; +SYS_ET_BFILE_TO_RAW : 'SYS_ET_BFILE_TO_RAW'; +SYS_ET_BLOB_TO_IMAGE : 'SYS_ET_BLOB_TO_IMAGE'; +SYS_ET_IMAGE_TO_BLOB : 'SYS_ET_IMAGE_TO_BLOB'; +SYS_ET_RAW_TO_BFILE : 'SYS_ET_RAW_TO_BFILE'; +SYS_EXTPDTXT : 'SYS_EXTPDTXT'; +SYS_EXTRACT_UTC : 'SYS_EXTRACT_UTC'; +SYS_FBT_INSDEL : 'SYS_FBT_INSDEL'; +SYS_FILTER_ACLS : 'SYS_FILTER_ACLS'; +SYS_FNMATCHES : 'SYS_FNMATCHES'; +SYS_FNREPLACE : 'SYS_FNREPLACE'; +SYS_GET_ACLIDS : 'SYS_GET_ACLIDS'; +SYS_GET_COL_ACLIDS : 'SYS_GET_COL_ACLIDS'; +SYS_GET_PRIVILEGES : 'SYS_GET_PRIVILEGES'; +SYS_GETTOKENID : 'SYS_GETTOKENID'; +SYS_GETXTIVAL : 'SYS_GETXTIVAL'; +SYS_GUID : 'SYS_GUID'; +SYSGUID : 'SYSGUID'; +SYSKM : 'SYSKM'; +SYS_MAKE_XMLNODEID : 'SYS_MAKE_XMLNODEID'; +SYS_MAKEXML : 'SYS_MAKEXML'; +SYS_MKXMLATTR : 'SYS_MKXMLATTR'; +SYS_MKXTI : 'SYS_MKXTI'; +SYSOBJ : 'SYSOBJ'; +SYS_OP_ADT2BIN : 'SYS_OP_ADT2BIN'; +SYS_OP_ADTCONS : 'SYS_OP_ADTCONS'; +SYS_OP_ALSCRVAL : 'SYS_OP_ALSCRVAL'; +SYS_OP_ATG : 'SYS_OP_ATG'; +SYS_OP_BIN2ADT : 'SYS_OP_BIN2ADT'; +SYS_OP_BITVEC : 'SYS_OP_BITVEC'; +SYS_OP_BL2R : 'SYS_OP_BL2R'; +SYS_OP_BLOOM_FILTER_LIST : 'SYS_OP_BLOOM_FILTER_LIST'; +SYS_OP_BLOOM_FILTER : 'SYS_OP_BLOOM_FILTER'; +SYS_OP_C2C : 'SYS_OP_C2C'; +SYS_OP_CAST : 'SYS_OP_CAST'; +SYS_OP_CEG : 'SYS_OP_CEG'; +SYS_OP_CL2C : 'SYS_OP_CL2C'; +SYS_OP_COMBINED_HASH : 'SYS_OP_COMBINED_HASH'; +SYS_OP_COMP : 'SYS_OP_COMP'; +SYS_OP_CONVERT : 'SYS_OP_CONVERT'; +SYS_OP_COUNTCHG : 'SYS_OP_COUNTCHG'; +SYS_OP_CSCONV : 'SYS_OP_CSCONV'; +SYS_OP_CSCONVTEST : 'SYS_OP_CSCONVTEST'; +SYS_OP_CSR : 'SYS_OP_CSR'; +SYS_OP_CSX_PATCH : 'SYS_OP_CSX_PATCH'; +SYS_OP_CYCLED_SEQ : 'SYS_OP_CYCLED_SEQ'; +SYS_OP_DECOMP : 'SYS_OP_DECOMP'; +SYS_OP_DESCEND : 'SYS_OP_DESCEND'; +SYS_OP_DISTINCT : 'SYS_OP_DISTINCT'; +SYS_OP_DRA : 'SYS_OP_DRA'; +SYS_OP_DUMP : 'SYS_OP_DUMP'; +SYS_OP_DV_CHECK : 'SYS_OP_DV_CHECK'; +SYS_OP_ENFORCE_NOT_NULL : 'SYS_OP_ENFORCE_NOT_NULL$'; +SYSOPER : 'SYSOPER'; +SYS_OP_EXTRACT : 'SYS_OP_EXTRACT'; +SYS_OP_GROUPING : 'SYS_OP_GROUPING'; +SYS_OP_GUID : 'SYS_OP_GUID'; +SYS_OP_HASH : 'SYS_OP_HASH'; +SYS_OP_IIX : 'SYS_OP_IIX'; +SYS_OP_ITR : 'SYS_OP_ITR'; +SYS_OP_KEY_VECTOR_CREATE : 'SYS_OP_KEY_VECTOR_CREATE'; +SYS_OP_KEY_VECTOR_FILTER_LIST : 'SYS_OP_KEY_VECTOR_FILTER_LIST'; +SYS_OP_KEY_VECTOR_FILTER : 'SYS_OP_KEY_VECTOR_FILTER'; +SYS_OP_KEY_VECTOR_SUCCEEDED : 'SYS_OP_KEY_VECTOR_SUCCEEDED'; +SYS_OP_KEY_VECTOR_USE : 'SYS_OP_KEY_VECTOR_USE'; +SYS_OP_LBID : 'SYS_OP_LBID'; +SYS_OP_LOBLOC2BLOB : 'SYS_OP_LOBLOC2BLOB'; +SYS_OP_LOBLOC2CLOB : 'SYS_OP_LOBLOC2CLOB'; +SYS_OP_LOBLOC2ID : 'SYS_OP_LOBLOC2ID'; +SYS_OP_LOBLOC2NCLOB : 'SYS_OP_LOBLOC2NCLOB'; +SYS_OP_LOBLOC2TYP : 'SYS_OP_LOBLOC2TYP'; +SYS_OP_LSVI : 'SYS_OP_LSVI'; +SYS_OP_LVL : 'SYS_OP_LVL'; +SYS_OP_MAKEOID : 'SYS_OP_MAKEOID'; +SYS_OP_MAP_NONNULL : 'SYS_OP_MAP_NONNULL'; +SYS_OP_MSR : 'SYS_OP_MSR'; +SYS_OP_NICOMBINE : 'SYS_OP_NICOMBINE'; +SYS_OP_NIEXTRACT : 'SYS_OP_NIEXTRACT'; +SYS_OP_NII : 'SYS_OP_NII'; +SYS_OP_NIX : 'SYS_OP_NIX'; +SYS_OP_NOEXPAND : 'SYS_OP_NOEXPAND'; +SYS_OP_NTCIMG : 'SYS_OP_NTCIMG$'; +SYS_OP_NUMTORAW : 'SYS_OP_NUMTORAW'; +SYS_OP_OIDVALUE : 'SYS_OP_OIDVALUE'; +SYS_OP_OPNSIZE : 'SYS_OP_OPNSIZE'; +SYS_OP_PAR_1 : 'SYS_OP_PAR_1'; +SYS_OP_PARGID_1 : 'SYS_OP_PARGID_1'; +SYS_OP_PARGID : 'SYS_OP_PARGID'; +SYS_OP_PAR : 'SYS_OP_PAR'; +SYS_OP_PART_ID : 'SYS_OP_PART_ID'; +SYS_OP_PIVOT : 'SYS_OP_PIVOT'; +SYS_OP_R2O : 'SYS_OP_R2O'; +SYS_OP_RAWTONUM : 'SYS_OP_RAWTONUM'; +SYS_OP_RDTM : 'SYS_OP_RDTM'; +SYS_OP_REF : 'SYS_OP_REF'; +SYS_OP_RMTD : 'SYS_OP_RMTD'; +SYS_OP_ROWIDTOOBJ : 'SYS_OP_ROWIDTOOBJ'; +SYS_OP_RPB : 'SYS_OP_RPB'; +SYS_OPTLOBPRBSC : 'SYS_OPTLOBPRBSC'; +SYS_OP_TOSETID : 'SYS_OP_TOSETID'; +SYS_OP_TPR : 'SYS_OP_TPR'; +SYS_OP_TRTB : 'SYS_OP_TRTB'; +SYS_OPTXICMP : 'SYS_OPTXICMP'; +SYS_OPTXQCASTASNQ : 'SYS_OPTXQCASTASNQ'; +SYS_OP_UNDESCEND : 'SYS_OP_UNDESCEND'; +SYS_OP_VECAND : 'SYS_OP_VECAND'; +SYS_OP_VECBIT : 'SYS_OP_VECBIT'; +SYS_OP_VECOR : 'SYS_OP_VECOR'; +SYS_OP_VECXOR : 'SYS_OP_VECXOR'; +SYS_OP_VERSION : 'SYS_OP_VERSION'; +SYS_OP_VREF : 'SYS_OP_VREF'; +SYS_OP_VVD : 'SYS_OP_VVD'; +SYS_OP_XMLCONS_FOR_CSX : 'SYS_OP_XMLCONS_FOR_CSX'; +SYS_OP_XPTHATG : 'SYS_OP_XPTHATG'; +SYS_OP_XPTHIDX : 'SYS_OP_XPTHIDX'; +SYS_OP_XPTHOP : 'SYS_OP_XPTHOP'; +SYS_OP_XTXT2SQLT : 'SYS_OP_XTXT2SQLT'; +SYS_OP_ZONE_ID : 'SYS_OP_ZONE_ID'; +SYS_ORDERKEY_DEPTH : 'SYS_ORDERKEY_DEPTH'; +SYS_ORDERKEY_MAXCHILD : 'SYS_ORDERKEY_MAXCHILD'; +SYS_ORDERKEY_PARENT : 'SYS_ORDERKEY_PARENT'; +SYS_PARALLEL_TXN : 'SYS_PARALLEL_TXN'; +SYS_PATHID_IS_ATTR : 'SYS_PATHID_IS_ATTR'; +SYS_PATHID_IS_NMSPC : 'SYS_PATHID_IS_NMSPC'; +SYS_PATHID_LASTNAME : 'SYS_PATHID_LASTNAME'; +SYS_PATHID_LASTNMSPC : 'SYS_PATHID_LASTNMSPC'; +SYS_PATH_REVERSE : 'SYS_PATH_REVERSE'; +SYS_PXQEXTRACT : 'SYS_PXQEXTRACT'; +SYS_RAW_TO_XSID : 'SYS_RAW_TO_XSID'; +SYS_RID_ORDER : 'SYS_RID_ORDER'; +SYS_ROW_DELTA : 'SYS_ROW_DELTA'; +SYS_SC_2_XMLT : 'SYS_SC_2_XMLT'; +SYS_SYNRCIREDO : 'SYS_SYNRCIREDO'; +SYSTEM_DEFINED : 'SYSTEM_DEFINED'; +SYSTEM : 'SYSTEM'; +SYSTIMESTAMP : 'SYSTIMESTAMP'; +SYS_TYPEID : 'SYS_TYPEID'; +SYS_UMAKEXML : 'SYS_UMAKEXML'; +SYS_XMLANALYZE : 'SYS_XMLANALYZE'; +SYS_XMLCONTAINS : 'SYS_XMLCONTAINS'; +SYS_XMLCONV : 'SYS_XMLCONV'; +SYS_XMLEXNSURI : 'SYS_XMLEXNSURI'; +SYS_XMLGEN : 'SYS_XMLGEN'; +SYS_XMLI_LOC_ISNODE : 'SYS_XMLI_LOC_ISNODE'; +SYS_XMLI_LOC_ISTEXT : 'SYS_XMLI_LOC_ISTEXT'; +SYS_XMLINSTR : 'SYS_XMLINSTR'; +SYS_XMLLOCATOR_GETSVAL : 'SYS_XMLLOCATOR_GETSVAL'; +SYS_XMLNODEID_GETCID : 'SYS_XMLNODEID_GETCID'; +SYS_XMLNODEID_GETLOCATOR : 'SYS_XMLNODEID_GETLOCATOR'; +SYS_XMLNODEID_GETOKEY : 'SYS_XMLNODEID_GETOKEY'; +SYS_XMLNODEID_GETPATHID : 'SYS_XMLNODEID_GETPATHID'; +SYS_XMLNODEID_GETPTRID : 'SYS_XMLNODEID_GETPTRID'; +SYS_XMLNODEID_GETRID : 'SYS_XMLNODEID_GETRID'; +SYS_XMLNODEID_GETSVAL : 'SYS_XMLNODEID_GETSVAL'; +SYS_XMLNODEID_GETTID : 'SYS_XMLNODEID_GETTID'; +SYS_XMLNODEID : 'SYS_XMLNODEID'; +SYS_XMLT_2_SC : 'SYS_XMLT_2_SC'; +SYS_XMLTRANSLATE : 'SYS_XMLTRANSLATE'; +SYS_XMLTYPE2SQL : 'SYS_XMLTYPE2SQL'; +SYS_XQ_ASQLCNV : 'SYS_XQ_ASQLCNV'; +SYS_XQ_ATOMCNVCHK : 'SYS_XQ_ATOMCNVCHK'; +SYS_XQBASEURI : 'SYS_XQBASEURI'; +SYS_XQCASTABLEERRH : 'SYS_XQCASTABLEERRH'; +SYS_XQCODEP2STR : 'SYS_XQCODEP2STR'; +SYS_XQCODEPEQ : 'SYS_XQCODEPEQ'; +SYS_XQCON2SEQ : 'SYS_XQCON2SEQ'; +SYS_XQCONCAT : 'SYS_XQCONCAT'; +SYS_XQDELETE : 'SYS_XQDELETE'; +SYS_XQDFLTCOLATION : 'SYS_XQDFLTCOLATION'; +SYS_XQDOC : 'SYS_XQDOC'; +SYS_XQDOCURI : 'SYS_XQDOCURI'; +SYS_XQDURDIV : 'SYS_XQDURDIV'; +SYS_XQED4URI : 'SYS_XQED4URI'; +SYS_XQENDSWITH : 'SYS_XQENDSWITH'; +SYS_XQERRH : 'SYS_XQERRH'; +SYS_XQERR : 'SYS_XQERR'; +SYS_XQESHTMLURI : 'SYS_XQESHTMLURI'; +SYS_XQEXLOBVAL : 'SYS_XQEXLOBVAL'; +SYS_XQEXSTWRP : 'SYS_XQEXSTWRP'; +SYS_XQEXTRACT : 'SYS_XQEXTRACT'; +SYS_XQEXTRREF : 'SYS_XQEXTRREF'; +SYS_XQEXVAL : 'SYS_XQEXVAL'; +SYS_XQFB2STR : 'SYS_XQFB2STR'; +SYS_XQFNBOOL : 'SYS_XQFNBOOL'; +SYS_XQFNCMP : 'SYS_XQFNCMP'; +SYS_XQFNDATIM : 'SYS_XQFNDATIM'; +SYS_XQFNLNAME : 'SYS_XQFNLNAME'; +SYS_XQFNNM : 'SYS_XQFNNM'; +SYS_XQFNNSURI : 'SYS_XQFNNSURI'; +SYS_XQFNPREDTRUTH : 'SYS_XQFNPREDTRUTH'; +SYS_XQFNQNM : 'SYS_XQFNQNM'; +SYS_XQFNROOT : 'SYS_XQFNROOT'; +SYS_XQFORMATNUM : 'SYS_XQFORMATNUM'; +SYS_XQFTCONTAIN : 'SYS_XQFTCONTAIN'; +SYS_XQFUNCR : 'SYS_XQFUNCR'; +SYS_XQGETCONTENT : 'SYS_XQGETCONTENT'; +SYS_XQINDXOF : 'SYS_XQINDXOF'; +SYS_XQINSERT : 'SYS_XQINSERT'; +SYS_XQINSPFX : 'SYS_XQINSPFX'; +SYS_XQIRI2URI : 'SYS_XQIRI2URI'; +SYS_XQLANG : 'SYS_XQLANG'; +SYS_XQLLNMFRMQNM : 'SYS_XQLLNMFRMQNM'; +SYS_XQMKNODEREF : 'SYS_XQMKNODEREF'; +SYS_XQNILLED : 'SYS_XQNILLED'; +SYS_XQNODENAME : 'SYS_XQNODENAME'; +SYS_XQNORMSPACE : 'SYS_XQNORMSPACE'; +SYS_XQNORMUCODE : 'SYS_XQNORMUCODE'; +SYS_XQ_NRNG : 'SYS_XQ_NRNG'; +SYS_XQNSP4PFX : 'SYS_XQNSP4PFX'; +SYS_XQNSPFRMQNM : 'SYS_XQNSPFRMQNM'; +SYS_XQPFXFRMQNM : 'SYS_XQPFXFRMQNM'; +SYS_XQ_PKSQL2XML : 'SYS_XQ_PKSQL2XML'; +SYS_XQPOLYABS : 'SYS_XQPOLYABS'; +SYS_XQPOLYADD : 'SYS_XQPOLYADD'; +SYS_XQPOLYCEL : 'SYS_XQPOLYCEL'; +SYS_XQPOLYCSTBL : 'SYS_XQPOLYCSTBL'; +SYS_XQPOLYCST : 'SYS_XQPOLYCST'; +SYS_XQPOLYDIV : 'SYS_XQPOLYDIV'; +SYS_XQPOLYFLR : 'SYS_XQPOLYFLR'; +SYS_XQPOLYMOD : 'SYS_XQPOLYMOD'; +SYS_XQPOLYMUL : 'SYS_XQPOLYMUL'; +SYS_XQPOLYRND : 'SYS_XQPOLYRND'; +SYS_XQPOLYSQRT : 'SYS_XQPOLYSQRT'; +SYS_XQPOLYSUB : 'SYS_XQPOLYSUB'; +SYS_XQPOLYUMUS : 'SYS_XQPOLYUMUS'; +SYS_XQPOLYUPLS : 'SYS_XQPOLYUPLS'; +SYS_XQPOLYVEQ : 'SYS_XQPOLYVEQ'; +SYS_XQPOLYVGE : 'SYS_XQPOLYVGE'; +SYS_XQPOLYVGT : 'SYS_XQPOLYVGT'; +SYS_XQPOLYVLE : 'SYS_XQPOLYVLE'; +SYS_XQPOLYVLT : 'SYS_XQPOLYVLT'; +SYS_XQPOLYVNE : 'SYS_XQPOLYVNE'; +SYS_XQREF2VAL : 'SYS_XQREF2VAL'; +SYS_XQRENAME : 'SYS_XQRENAME'; +SYS_XQREPLACE : 'SYS_XQREPLACE'; +SYS_XQRESVURI : 'SYS_XQRESVURI'; +SYS_XQRNDHALF2EVN : 'SYS_XQRNDHALF2EVN'; +SYS_XQRSLVQNM : 'SYS_XQRSLVQNM'; +SYS_XQRYENVPGET : 'SYS_XQRYENVPGET'; +SYS_XQRYVARGET : 'SYS_XQRYVARGET'; +SYS_XQRYWRP : 'SYS_XQRYWRP'; +SYS_XQSEQ2CON4XC : 'SYS_XQSEQ2CON4XC'; +SYS_XQSEQ2CON : 'SYS_XQSEQ2CON'; +SYS_XQSEQDEEPEQ : 'SYS_XQSEQDEEPEQ'; +SYS_XQSEQINSB : 'SYS_XQSEQINSB'; +SYS_XQSEQRM : 'SYS_XQSEQRM'; +SYS_XQSEQRVS : 'SYS_XQSEQRVS'; +SYS_XQSEQSUB : 'SYS_XQSEQSUB'; +SYS_XQSEQTYPMATCH : 'SYS_XQSEQTYPMATCH'; +SYS_XQSTARTSWITH : 'SYS_XQSTARTSWITH'; +SYS_XQSTATBURI : 'SYS_XQSTATBURI'; +SYS_XQSTR2CODEP : 'SYS_XQSTR2CODEP'; +SYS_XQSTRJOIN : 'SYS_XQSTRJOIN'; +SYS_XQSUBSTRAFT : 'SYS_XQSUBSTRAFT'; +SYS_XQSUBSTRBEF : 'SYS_XQSUBSTRBEF'; +SYS_XQTOKENIZE : 'SYS_XQTOKENIZE'; +SYS_XQTREATAS : 'SYS_XQTREATAS'; +SYS_XQ_UPKXML2SQL : 'SYS_XQ_UPKXML2SQL'; +SYS_XQXFORM : 'SYS_XQXFORM'; +SYS_XSID_TO_RAW : 'SYS_XSID_TO_RAW'; +SYS_ZMAP_FILTER : 'SYS_ZMAP_FILTER'; +SYS_ZMAP_REFRESH : 'SYS_ZMAP_REFRESH'; +T_LETTER : 'T'; +TABLE_LOOKUP_BY_NL : 'TABLE_LOOKUP_BY_NL'; +TABLESPACE_NO : 'TABLESPACE_NO'; +TABLESPACE : 'TABLESPACE'; +TABLES : 'TABLES'; +TABLE_STATS : 'TABLE_STATS'; +TABLE : 'TABLE'; +TABNO : 'TABNO'; +TAG : 'TAG'; +TANH : 'TANH'; +TAN : 'TAN'; +TBLORIDXPARTNUM : 'TBL$OR$IDX$PART$NUM'; +TEMPFILE : 'TEMPFILE'; +TEMPLATE : 'TEMPLATE'; +TEMPORARY : 'TEMPORARY'; +TEMP_TABLE : 'TEMP_TABLE'; +TEST : 'TEST'; +TEXT : 'TEXT'; +THAN : 'THAN'; +THEN : 'THEN'; +THE : 'THE'; +THREAD : 'THREAD'; +THROUGH : 'THROUGH'; +TIER : 'TIER'; +TIES : 'TIES'; +TIMEOUT : 'TIMEOUT'; +TIMESTAMP_LTZ_UNCONSTRAINED : 'TIMESTAMP_LTZ_UNCONSTRAINED'; +TIMESTAMP : 'TIMESTAMP'; +TIMESTAMP_TZ_UNCONSTRAINED : 'TIMESTAMP_TZ_UNCONSTRAINED'; +TIMESTAMP_UNCONSTRAINED : 'TIMESTAMP_UNCONSTRAINED'; +TIMES : 'TIMES'; +TIME : 'TIME'; +TIMEZONE : 'TIMEZONE'; +TIMEZONE_ABBR : 'TIMEZONE_ABBR'; +TIMEZONE_HOUR : 'TIMEZONE_HOUR'; +TIMEZONE_MINUTE : 'TIMEZONE_MINUTE'; +TIMEZONE_OFFSET : 'TIMEZONE_OFFSET'; +TIMEZONE_REGION : 'TIMEZONE_REGION'; +TIME_ZONE : 'TIME_ZONE'; +TIMING : 'TIMING'; +TIV_GB : 'TIV_GB'; +TIV_SSF : 'TIV_SSF'; +TO_ACLID : 'TO_ACLID'; +TO_BINARY_DOUBLE : 'TO_BINARY_DOUBLE'; +TO_BINARY_FLOAT : 'TO_BINARY_FLOAT'; +TO_BLOB : 'TO_BLOB'; +TO_CLOB : 'TO_CLOB'; +TO_DSINTERVAL : 'TO_DSINTERVAL'; +TO_LOB : 'TO_LOB'; +TO_MULTI_BYTE : 'TO_MULTI_BYTE'; +TO_NCHAR : 'TO_NCHAR'; +TO_NCLOB : 'TO_NCLOB'; +TO_NUMBER : 'TO_NUMBER'; +TOPLEVEL : 'TOPLEVEL'; +TO_SINGLE_BYTE : 'TO_SINGLE_BYTE'; +TO_TIMESTAMP : 'TO_TIMESTAMP'; +TO_TIMESTAMP_TZ : 'TO_TIMESTAMP_TZ'; +TO_TIME : 'TO_TIME'; +TO_TIME_TZ : 'TO_TIME_TZ'; +TO : 'TO'; +TO_YMINTERVAL : 'TO_YMINTERVAL'; +TRACE : 'TRACE'; +TRACING : 'TRACING'; +TRACKING : 'TRACKING'; +TRAILING : 'TRAILING'; +TRANSACTION : 'TRANSACTION'; +TRANSFORM : 'TRANSFORM'; +TRANSFORM_DISTINCT_AGG : 'TRANSFORM_DISTINCT_AGG'; +TRANSITIONAL : 'TRANSITIONAL'; +TRANSITION : 'TRANSITION'; +TRANSLATE : 'TRANSLATE'; +TRANSLATION : 'TRANSLATION'; +TREAT : 'TREAT'; +TRIGGERS : 'TRIGGERS'; +TRIGGER : 'TRIGGER'; +TRUE : 'TRUE'; +TRUNCATE : 'TRUNCATE'; +TRUNC : 'TRUNC'; +TRUSTED : 'TRUSTED'; +TRUST : 'TRUST'; +TUNING : 'TUNING'; +TX : 'TX'; +TYPES : 'TYPES'; +TYPE : 'TYPE'; +TZ_OFFSET : 'TZ_OFFSET'; +UB2 : 'UB2'; +UBA : 'UBA'; +UCS2 : 'UCS2'; +UID : 'UID'; +UNARCHIVED : 'UNARCHIVED'; +UNBOUNDED : 'UNBOUNDED'; +UNBOUND : 'UNBOUND'; +UNCONDITIONAL : 'UNCONDITIONAL'; +UNDER : 'UNDER'; +UNDO : 'UNDO'; +UNDROP : 'UNDROP'; +UNIFORM : 'UNIFORM'; +UNION : 'UNION'; +UNIQUE : 'UNIQUE'; +UNISTR : 'UNISTR'; +UNLIMITED : 'UNLIMITED'; +UNLOAD : 'UNLOAD'; +UNLOCK : 'UNLOCK'; +UNMATCHED : 'UNMATCHED'; +UNNEST_INNERJ_DISTINCT_VIEW : 'UNNEST_INNERJ_DISTINCT_VIEW'; +UNNEST_NOSEMIJ_NODISTINCTVIEW : 'UNNEST_NOSEMIJ_NODISTINCTVIEW'; +UNNEST_SEMIJ_VIEW : 'UNNEST_SEMIJ_VIEW'; +UNNEST : 'UNNEST'; +UNPACKED : 'UNPACKED'; +UNPIVOT : 'UNPIVOT'; +UNPLUG : 'UNPLUG'; +UNPROTECTED : 'UNPROTECTED'; +UNQUIESCE : 'UNQUIESCE'; +UNRECOVERABLE : 'UNRECOVERABLE'; +UNRESTRICTED : 'UNRESTRICTED'; +UNSUBSCRIBE : 'UNSUBSCRIBE'; +UNTIL : 'UNTIL'; +UNUSABLE : 'UNUSABLE'; +UNUSED : 'UNUSED'; +UPDATABLE : 'UPDATABLE'; +UPDATED : 'UPDATED'; +UPDATE : 'UPDATE'; +UPDATEXML : 'UPDATEXML'; +UPD_INDEXES : 'UPD_INDEXES'; +UPD_JOININDEX : 'UPD_JOININDEX'; +UPGRADE : 'UPGRADE'; +UPPER : 'UPPER'; +UPSERT : 'UPSERT'; +UROWID : 'UROWID'; +USABLE : 'USABLE'; +USAGE : 'USAGE'; +USE_ANTI : 'USE_ANTI'; +USE_CONCAT : 'USE_CONCAT'; +USE_CUBE : 'USE_CUBE'; +USE_HASH_AGGREGATION : 'USE_HASH_AGGREGATION'; +USE_HASH_GBY_FOR_PUSHDOWN : 'USE_HASH_GBY_FOR_PUSHDOWN'; +USE_HASH : 'USE_HASH'; +USE_HIDDEN_PARTITIONS : 'USE_HIDDEN_PARTITIONS'; +USE_INVISIBLE_INDEXES : 'USE_INVISIBLE_INDEXES'; +USE_MERGE_CARTESIAN : 'USE_MERGE_CARTESIAN'; +USE_MERGE : 'USE_MERGE'; +USE_NL : 'USE_NL'; +USE_NL_WITH_INDEX : 'USE_NL_WITH_INDEX'; +USE_PRIVATE_OUTLINES : 'USE_PRIVATE_OUTLINES'; +USER_DATA : 'USER_DATA'; +USER_DEFINED : 'USER_DEFINED'; +USERENV : 'USERENV'; +USERGROUP : 'USERGROUP'; +USER_RECYCLEBIN : 'USER_RECYCLEBIN'; +USERS : 'USERS'; +USER_TABLESPACES : 'USER_TABLESPACES'; +USER : 'USER'; +USE_SEMI : 'USE_SEMI'; +USE_STORED_OUTLINES : 'USE_STORED_OUTLINES'; +USE_TTT_FOR_GSETS : 'USE_TTT_FOR_GSETS'; +USE : 'USE'; +USE_VECTOR_AGGREGATION : 'USE_VECTOR_AGGREGATION'; +USE_WEAK_NAME_RESL : 'USE_WEAK_NAME_RESL'; +USING_NO_EXPAND : 'USING_NO_EXPAND'; +USING_NLS_COMP : 'USING_NLS_COMP'; +USING : 'USING'; +UTF16BE : 'UTF16BE'; +UTF16LE : 'UTF16LE'; +UTF32 : 'UTF32'; +UTF8 : 'UTF8'; +V1 : 'V1'; +V2 : 'V2'; +VALIDATE : 'VALIDATE'; +VALIDATE_CONVERSION : 'VALIDATE_CONVERSION'; +VALIDATION : 'VALIDATION'; +VALID_TIME_END : 'VALID_TIME_END'; +VALUES : 'VALUES'; +VALUE : 'VALUE'; +VARCHAR2 : 'VARCHAR2'; +VARCHAR : 'VARCHAR'; +VARIABLE : 'VARIABLE'; +VAR_POP : 'VAR_POP'; +VARRAYS : 'VARRAYS'; +VARRAY : 'VARRAY'; +VAR_SAMP : 'VAR_SAMP'; +VARYING : 'VARYING'; +VECTOR_READ_TRACE : 'VECTOR_READ_TRACE'; +VECTOR_READ : 'VECTOR_READ'; +VECTOR_TRANSFORM_DIMS : 'VECTOR_TRANSFORM_DIMS'; +VECTOR_TRANSFORM_FACT : 'VECTOR_TRANSFORM_FACT'; +VECTOR_TRANSFORM : 'VECTOR_TRANSFORM'; +VERIFIER : 'VERIFIER'; +VERIFY : 'VERIFY'; +VERSIONING : 'VERSIONING'; +VERSIONS_ENDSCN : 'VERSIONS_ENDSCN'; +VERSIONS_ENDTIME : 'VERSIONS_ENDTIME'; +VERSIONS_OPERATION : 'VERSIONS_OPERATION'; +VERSIONS_STARTSCN : 'VERSIONS_STARTSCN'; +VERSIONS_STARTTIME : 'VERSIONS_STARTTIME'; +VERSIONS : 'VERSIONS'; +VERSIONS_XID : 'VERSIONS_XID'; +VERSION : 'VERSION'; +VIEW : 'VIEW'; +VIOLATION : 'VIOLATION'; +VIRTUAL : 'VIRTUAL'; +VISIBILITY : 'VISIBILITY'; +VISIBLE : 'VISIBLE'; +VOLUME : 'VOLUME'; +VSIZE : 'VSIZE'; +WAIT : 'WAIT'; +WALLET : 'WALLET'; +WARNING : 'WARNING'; +WEEKS : 'WEEKS'; +WEEK : 'WEEK'; +WELLFORMED : 'WELLFORMED'; +WHENEVER : 'WHENEVER'; +WHEN : 'WHEN'; +WHERE : 'WHERE'; +WHILE : 'WHILE'; +WHITESPACE : 'WHITESPACE'; +WIDTH_BUCKET : 'WIDTH_BUCKET'; +WITHIN : 'WITHIN'; +WITHOUT : 'WITHOUT'; +WITH_PLSQL : 'WITH_PLSQL'; +WITH : 'WITH'; +WORK : 'WORK'; +WRAPPED : 'WRAPPED'; +WRAPPER : 'WRAPPER'; +WRITE : 'WRITE'; +XDB_FASTPATH_INSERT : 'XDB_FASTPATH_INSERT'; +XDB : 'XDB'; +X_DYN_PRUNE : 'X_DYN_PRUNE'; +XID : 'XID'; +XML2OBJECT : 'XML2OBJECT'; +XMLAGG : 'XMLAGG'; +XMLATTRIBUTES : 'XMLATTRIBUTES'; +XMLCAST : 'XMLCAST'; +XMLCDATA : 'XMLCDATA'; +XMLCOLATTVAL : 'XMLCOLATTVAL'; +XMLCOMMENT : 'XMLCOMMENT'; +XMLCONCAT : 'XMLCONCAT'; +XMLDIFF : 'XMLDIFF'; +XML_DML_RWT_STMT : 'XML_DML_RWT_STMT'; +XMLELEMENT : 'XMLELEMENT'; +XMLEXISTS2 : 'XMLEXISTS2'; +XMLEXISTS : 'XMLEXISTS'; +XMLFOREST : 'XMLFOREST'; +XMLINDEX : 'XMLINDEX'; +XMLINDEX_REWRITE_IN_SELECT : 'XMLINDEX_REWRITE_IN_SELECT'; +XMLINDEX_REWRITE : 'XMLINDEX_REWRITE'; +XMLINDEX_SEL_IDX_TBL : 'XMLINDEX_SEL_IDX_TBL'; +XMLISNODE : 'XMLISNODE'; +XMLISVALID : 'XMLISVALID'; +XMLNAMESPACES : 'XMLNAMESPACES'; +XMLPARSE : 'XMLPARSE'; +XMLPATCH : 'XMLPATCH'; +XMLPI : 'XMLPI'; +XMLQUERYVAL : 'XMLQUERYVAL'; +XMLQUERY : 'XMLQUERY'; +XMLROOT : 'XMLROOT'; +XMLSCHEMA : 'XMLSCHEMA'; +XMLSERIALIZE : 'XMLSERIALIZE'; +XMLTABLE : 'XMLTABLE'; +XMLTRANSFORMBLOB : 'XMLTRANSFORMBLOB'; +XMLTRANSFORM : 'XMLTRANSFORM'; +XMLTYPE : 'XMLTYPE'; +SYSXMLTYPE : '"SYS"."XMLTYPE"'; +XML : 'XML'; +XPATHTABLE : 'XPATHTABLE'; +XS_SYS_CONTEXT : 'XS_SYS_CONTEXT'; +XS : 'XS'; +XTRANSPORT : 'XTRANSPORT'; +YEARS : 'YEARS'; +YEAR : 'YEAR'; +YES : 'YES'; +YMINTERVAL_UNCONSTRAINED : 'YMINTERVAL_UNCONSTRAINED'; +ZONEMAP : 'ZONEMAP'; +ZONE : 'ZONE'; +PREDICTION : 'PREDICTION'; +PREDICTION_BOUNDS : 'PREDICTION_BOUNDS'; +PREDICTION_COST : 'PREDICTION_COST'; +PREDICTION_DETAILS : 'PREDICTION_DETAILS'; +PREDICTION_PROBABILITY : 'PREDICTION_PROBABILITY'; +PREDICTION_SET : 'PREDICTION_SET'; + +BLOCKCHAIN : 'BLOCKCHAIN'; +COLLATE : 'COLLATE'; +COLLATION : 'COLLATION'; +DEFINITION : 'DEFINITION'; +DUPLICATED : 'DUPLICATED'; +EXTENDED : 'EXTENDED'; +HASHING : 'HASHING'; +IDLE : 'IDLE'; +IMMUTABLE : 'IMMUTABLE'; +ORACLE_DATAPUMP : 'ORACLE_DATAPUMP'; +ORACLE_HDFS : 'ORACLE_HDFS'; +ORACLE_HIVE : 'ORACLE_HIVE'; +ORACLE_LOADER : 'ORACLE_LOADER'; +SHA2_512_Q : '"SHA2_512"'; +SHARDED : 'SHARDED'; +V1_Q : '"V1"'; + +ISOLATE : 'ISOLATE'; +ROOT : 'ROOT'; +UNITE : 'UNITE'; +ALGORITHM : 'ALGORITHM'; + +CUME_DIST : 'CUME_DIST'; +DENSE_RANK : 'DENSE_RANK'; +LISTAGG : 'LISTAGG'; +PERCENT_RANK : 'PERCENT_RANK'; +PERCENTILE_CONT : 'PERCENTILE_CONT'; +PERCENTILE_DISC : 'PERCENTILE_DISC'; +RANK : 'RANK'; + +AVG : 'AVG'; +CORR : 'CORR'; +COVAR_ : 'COVAR_'; +DECODE : 'DECODE'; +LAG : 'LAG'; +LAG_DIFF : 'LAG_DIFF'; +LAG_DIFF_PERCENT : 'LAG_DIFF_PERCENT'; +LEAD : 'LEAD'; +MAX : 'MAX'; +MEDIAN : 'MEDIAN'; +MEMOPTIMIZE : 'MEMOPTIMIZE'; +MIN : 'MIN'; +NTILE : 'NTILE'; +NVL : 'NVL'; +RATIO_TO_REPORT : 'RATIO_TO_REPORT'; +REGR_ : 'REGR_'; +ROUND : 'ROUND'; +ROW_NUMBER : 'ROW_NUMBER'; +SUBSTR : 'SUBSTR'; +TO_CHAR : 'TO_CHAR'; +TRIM : 'TRIM'; +SUM : 'SUM'; +STDDEV : 'STDDEV'; +VAR_ : 'VAR_'; +VARIANCE : 'VARIANCE'; +LEAST : 'LEAST'; +GREATEST : 'GREATEST'; +TO_DATE : 'TO_DATE'; + +CHARSETID : 'CHARSETID'; +CHARSETFORM : 'CHARSETFORM'; +DURATION : 'DURATION'; +EXTEND : 'EXTEND'; +MAXLEN : 'MAXLEN'; +PERSISTABLE : 'PERSISTABLE'; +POLYMORPHIC : 'POLYMORPHIC'; +STRUCT : 'STRUCT'; +TDO : 'TDO'; +WM_CONCAT : 'WM_CONCAT'; + +// Rule #358 - subtoken typecast in , it also incorporates +// Lowercase 'n' is a usual addition to the standard + +NATIONAL_CHAR_STRING_LIT: 'N' '\'' (~('\'' | '\r' | '\n') | '\'' '\'' | NEWLINE)* '\''; + +// Rule #040 - subtoken typecast in +// Lowercase 'b' is a usual addition to the standard + +BIT_STRING_LIT: 'B' ('\'' [01]* '\'')+; + +// Rule #284 - subtoken typecast in +// Lowercase 'x' is a usual addition to the standard + +HEX_STRING_LIT : 'X' ('\'' [A-F0-9]* '\'')+; +DOUBLE_PERIOD : '..'; +PERIOD : '.'; + +//{ Rule #238 +// This rule is a bit tricky - it resolves the ambiguity with +// It also incorporates and for the +// Rule #501 was incorporated directly in the token +// See also the rule #617 +/* + : ( + UNSIGNED_INTEGER + ( '.' UNSIGNED_INTEGER + | {$type = UNSIGNED_INTEGER;} + ) ( E ('+' | '-')? UNSIGNED_INTEGER {$type = APPROXIMATE_NUM_LIT;} )? + | '.' UNSIGNED_INTEGER ( E ('+' | '-')? UNSIGNED_INTEGER {$type = APPROXIMATE_NUM_LIT;} )? + ) + (D | F)? + ;*/ + +UNSIGNED_INTEGER : [0-9]+; +APPROXIMATE_NUM_LIT : FLOAT_FRAGMENT ('E' ('+' | '-')? (FLOAT_FRAGMENT | [0-9]+))? ('D' | 'F')?; + +// Rule #--- is a base for Rule #065 , it incorporates +// and a superfluous subtoken typecasting of the "QUOTE" +CHAR_STRING: '\'' (~('\'' | '\r' | '\n') | '\'' '\'' | NEWLINE)* '\''; + +// See https://livesql.oracle.com/apex/livesql/file/content_CIREYU9EA54EOKQ7LAMZKRF6P.html +// TODO: context sensitive string quotes (any characted after quote) +CHAR_STRING_PERL: + 'Q' '\'' ( + QS_ANGLE + | QS_BRACE + | QS_BRACK + | QS_PAREN + | QS_EXCLAM + | QS_SHARP + | QS_QUOTE + | QS_DQUOTE + | QS_TILDA + | QS_SOLIDUS + | QS_RSOLIDUS + ) '\'' -> type(CHAR_STRING) +; +fragment QS_ANGLE : '<' .*? '>'; +fragment QS_BRACE : '{' .*? '}'; +fragment QS_BRACK : '[' .*? ']'; +fragment QS_PAREN : '(' .*? ')'; +fragment QS_EXCLAM : '!' .*? '!'; +fragment QS_SHARP : '#' .*? '#'; +fragment QS_QUOTE : '\'' .*? '\''; +fragment QS_DQUOTE : '"' .*? '"'; +fragment QS_TILDA : '~' .*? '~'; +fragment QS_SOLIDUS : '/' .*? '/'; +fragment QS_RSOLIDUS : '\\' .*? '\\'; + +DELIMITED_ID: '"' (~ [\u0000"] | '"' '"')+ '"'; + +PERCENT : '%'; +AMPERSAND : '&'; +LEFT_PAREN : '('; +RIGHT_PAREN : ')'; +DOUBLE_ASTERISK : '**'; +ASTERISK : '*'; +PLUS_SIGN : '+'; +MINUS_SIGN : '-'; +COMMA : ','; +SOLIDUS : '/'; +AT_SIGN : '@'; +ASSIGN_OP : ':='; +HASH_OP : '#'; + +SQ: '\''; + +BINDVAR: + ':' SIMPLE_LETTER (SIMPLE_LETTER | [0-9] | '_')* + | ':' DELIMITED_ID // not used in SQL but spotted in v$sqltext when using cursor_sharing + | ':' UNSIGNED_INTEGER + | QUESTION_MARK // not in SQL, not in DM, not in OCI, use this for JDBC +; + +NOT_EQUAL_OP : '!=' | '<>' | '^=' | '~='; +CARRET_OPERATOR_PART : '^'; +TILDE_OPERATOR_PART : '~'; +EXCLAMATION_OPERATOR_PART : '!'; +GREATER_THAN_OP : '>'; +LESS_THAN_OP : '<'; +COLON : ':'; +SEMICOLON : ';'; + +BAR : '|'; +EQUALS_OP : '='; + +LEFT_BRACKET : '['; +RIGHT_BRACKET : ']'; + +INTRODUCER: '_'; + +// Comments https://docs.oracle.com/cd/E11882_01/server.112/e41084/sql_elements006.htm + +SINGLE_LINE_COMMENT : '--' ~('\r' | '\n')* NEWLINE_EOF -> channel(HIDDEN); +MULTI_LINE_COMMENT : '/*' .*? '*/' -> channel(HIDDEN); +// https://docs.oracle.com/cd/E11882_01/server.112/e16604/ch_twelve034.htm#SQPUG054 +REMARK_COMMENT: + 'REM' {this.IsNewlineAtPos(-4)}? 'ARK'? (' ' ~('\r' | '\n')*)? NEWLINE_EOF -> channel(HIDDEN) +; + +// https://docs.oracle.com/cd/E11882_01/server.112/e16604/ch_twelve032.htm#SQPUG052 +PROMPT_MESSAGE: 'PRO' {this.IsNewlineAtPos(-4)}? 'MPT'? (' ' ~('\r' | '\n')*)? NEWLINE_EOF; + +// TODO: should starts with newline +START_CMD: // https://docs.oracle.com/cd/B19306_01/server.102/b14357/ch12002.htm + '@' '@'? +; // https://docs.oracle.com/cd/B19306_01/server.102/b14357/ch12003.htm + +REGULAR_ID: SIMPLE_LETTER (SIMPLE_LETTER | '$' | '_' | '#' | [0-9])*; + +INQUIRY_DIRECTIVE: '$$' (SIMPLE_LETTER | '_')+; + +SPACES: [ \t\r\n]+ -> channel(HIDDEN); + +// Fragment rules + +fragment NEWLINE_EOF : NEWLINE | EOF; +fragment QUESTION_MARK : '?'; +fragment SIMPLE_LETTER : [\p{Letter}]; +fragment FLOAT_FRAGMENT : UNSIGNED_INTEGER* '.'? UNSIGNED_INTEGER+; +fragment NEWLINE : '\r'? '\n'; +fragment SPACE : [ \t]; \ No newline at end of file diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/antlr4/ai/chat2db/plugin/dm/parser/base/DMParser.g4 b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/antlr4/ai/chat2db/plugin/dm/parser/base/DMParser.g4 new file mode 100644 index 0000000000..9fedbcb821 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/antlr4/ai/chat2db/plugin/dm/parser/base/DMParser.g4 @@ -0,0 +1,9730 @@ +/** + * Oracle(c) PL/SQL 11g Parser + * + * Adapted for Dameng (DM) SQL parsing in Chat2DB Community. + * Derived from the Oracle PL/SQL grammar; dialect-specific rules such as + * EXPLAIN may differ from the upstream Oracle grammar. + * + * Copyright (c) 2009-2011 Alexandre Porcelli + * Copyright (c) 2015-2019 Ivan Kochurkin (KvanTTT, kvanttt@gmail.com, Positive Technologies). + * Copyright (c) 2017-2018 Mark Adams + * + * 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. + */ + +// $antlr-format alignTrailingComments true, columnLimit 150, minEmptyLines 1, maxEmptyLinesToKeep 1, reflowComments false, useTab false +// $antlr-format allowShortRulesOnASingleLine false, allowShortBlocksOnASingleLine true, alignSemicolons hanging, alignColons hanging + +parser grammar DMParser; + +options { + tokenVocab = DMLexer; + superClass = DMParserBase; +} + +@parser::postinclude { +#include +} + +sql_script + : sql_plus_command_no_semicolon? ( + (sql_plus_command | unit_statement) (SEMICOLON+ '/'* (sql_plus_command | unit_statement))* SEMICOLON* '/'* + ) EOF + ; + +unit_statement + : alter_analytic_view + | alter_attribute_dimension + | alter_audit_policy + | alter_cluster + | alter_database + | alter_database_link + | alter_dimension + | alter_diskgroup + | alter_flashback_archive + | alter_function + | alter_hierarchy + | alter_index + | alter_inmemory_join_group + | alter_java + | alter_library + | alter_lockdown_profile + | alter_materialized_view + | alter_materialized_view_log + | alter_materialized_zonemap + | alter_operator + | alter_outline + | alter_package + | alter_pmem_filestore + | alter_procedure + | alter_resource_cost + | alter_role + | alter_rollback_segment + | alter_sequence + | alter_session + | alter_synonym + | alter_table + | alter_tablespace + | alter_tablespace_set + | alter_trigger + | alter_type + | alter_user + | alter_view + | call_statement + | create_analytic_view + | create_attribute_dimension + | create_audit_policy + | create_cluster + | create_context + | create_controlfile + | create_database + | create_database_link + | create_dimension + | create_directory + | create_diskgroup + | create_edition + | create_flashback_archive + | create_function_body + | create_hierarchy + | create_index + | create_inmemory_join_group + | create_java + | create_library + | create_lockdown_profile + | create_materialized_view + | create_materialized_view_log + | create_materialized_zonemap + | create_operator + | create_outline + | create_package + | create_package_body + | create_pmem_filestore + | create_procedure_body + | create_profile + | create_restore_point + | create_role + | create_rollback_segment + | create_sequence + | create_spfile + | create_synonym + | create_table + | create_tablespace + | create_tablespace_set + | create_trigger + | create_type + | create_user + | create_view + | drop_analytic_view + | drop_attribute_dimension + | drop_audit_policy + | drop_cluster + | drop_context + | drop_database + | drop_database_link + | drop_directory + | drop_diskgroup + | drop_edition + | drop_flashback_archive + | drop_function + | drop_hierarchy + | drop_index + | drop_indextype + | drop_inmemory_join_group + | drop_java + | drop_library + | drop_lockdown_profile + | drop_materialized_view + | drop_materialized_zonemap + | drop_operator + | drop_outline + | drop_package + | drop_pmem_filestore + | drop_procedure + | drop_restore_point + | drop_role + | drop_rollback_segment + | drop_sequence + | drop_synonym + | drop_table + | drop_tablespace + | drop_tablespace_set + | drop_trigger + | drop_type + | drop_user + | drop_view + | administer_key_management + | analyze + | anonymous_block + | associate_statistics + | audit_traditional + | comment_on_column + | comment_on_materialized + | comment_on_table + | data_manipulation_language_statements + | disassociate_statistics + | flashback_table + | grant_statement + | noaudit_statement + | purge_statement + | rename_object + | revoke_statement + | transaction_control_statements + | truncate_cluster + | truncate_table + | unified_auditing + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/ALTER-DISKGROUP.html +alter_diskgroup + : ALTER DISKGROUP ( + id_expression ( + ((add_disk_clause | drop_disk_clause)+ | resize_disk_clause) rebalance_diskgroup_clause? + | ( + replace_disk_clause + | rename_disk_clause + | disk_online_clause + | disk_offline_clause + | rebalance_diskgroup_clause + | check_diskgroup_clause + | diskgroup_template_clauses + | diskgroup_directory_clauses + | diskgroup_alias_clauses + | diskgroup_volume_clauses + | diskgroup_attributes + | drop_diskgroup_file_clause + | convert_redundancy_clause + | usergroup_clauses + | user_clauses + | file_permissions_clause + | file_owner_clause + | scrub_clause + | quotagroup_clauses + | filegroup_clauses + ) + ) + | (id_expression (',' id_expression)* | ALL) ( + undrop_disk_clause + | diskgroup_availability + | enable_disable_volume + ) + ) + ; + +add_disk_clause + : ADD ( + (SITE sn = id_expression)? quorum_regular? (FAILGROUP fgn = id_expression)? DISK qualified_disk_clause ( + ',' qualified_disk_clause + )* + )+ + ; + +drop_disk_clause + : DROP ( + quorum_regular? DISK id_expression force_noforce? (',' id_expression force_noforce?)* + | DISKS IN quorum_regular? FAILGROUP id_expression force_noforce? ( + ',' id_expression force_noforce? + )* + ) + ; + +resize_disk_clause + : RESIZE ALL (SIZE size_clause)? + ; + +replace_disk_clause + : REPLACE DISK id_expression WITH CHAR_STRING force_noforce? ( + ',' id_expression WITH CHAR_STRING force_noforce? + )* (POWER numeric)? wait_nowait? + ; + +wait_nowait + : WAIT + | NOWAIT + ; + +rename_disk_clause + : RENAME ( + DISK id_expression TO id_expression (',' id_expression TO id_expression)* + | DISKS ALL + ) + ; + +disk_online_clause + : ONLINE ( + ( + quorum_regular? DISK id_expression (',' id_expression)* + | DISKS IN quorum_regular? FAILGROUP id_expression (',' id_expression)* + )+ + | ALL + ) (POWER numeric)? wait_nowait? + ; + +disk_offline_clause + : OFFLINE ( + quorum_regular? DISK id_expression (',' id_expression)* + | DISKS IN quorum_regular? FAILGROUP id_expression (',' id_expression)* + ) timeout_clause? + ; + +timeout_clause + : DROP AFTER numeric (M_LETTER | H_LETTER) + ; + +rebalance_diskgroup_clause + : REBALANCE ( + ((WITH | WITHOUT) phase+)? (POWER numeric) (WAIT | NOWAIT)? + | MODIFY POWER numeric? + ) + ; + +phase + : id_expression //TODO + ; + +check_diskgroup_clause + : CHECK ALL? (REPAIR | NOREPAIR)? //inconsistent documentation + ; + +diskgroup_template_clauses + : (ADD | MODIFY) TEMPLATE id_expression qualified_template_clause ( + ',' id_expression qualified_template_clause + )* + | DROP TEMPLATE id_expression (',' id_expression)* + ; + +qualified_template_clause + : ATTRIBUTES '(' redundancy_clause? striping_clause? ')' //inconsistent documentation + ; + +redundancy_clause + : MIRROR + | HIGH + | UNPROTECTED + | PARITY + | DOUBLE + ; + +striping_clause + : FINE + | COARSE + ; + +force_noforce + : FORCE + | NOFORCE + ; + +diskgroup_directory_clauses + : ADD DIRECTORY filename (',' filename)* + | DROP DIRECTORY filename force_noforce? (',' filename force_noforce?)* + | RENAME DIRECTORY dir_name TO dir_name (',' dir_name TO dir_name)* + ; + +dir_name + : CHAR_STRING + ; + +diskgroup_alias_clauses + : ADD ALIAS CHAR_STRING FOR CHAR_STRING (',' CHAR_STRING FOR CHAR_STRING)* + | DROP ALIAS CHAR_STRING (',' CHAR_STRING)* + | RENAME ALIAS CHAR_STRING TO CHAR_STRING (',' CHAR_STRING TO CHAR_STRING)* + ; + +diskgroup_volume_clauses + : add_volume_clause + | modify_volume_clause + | RESIZE VOLUME id_expression SIZE size_clause + | DROP VOLUME id_expression + ; + +add_volume_clause + : ADD VOLUME id_expression SIZE size_clause redundancy_clause? ( + STRIPE_WIDTH numeric (K_LETTER | M_LETTER) + )? (STRIPE_COLUMNS numeric)? + ; + +modify_volume_clause + : MODIFY VOLUME id_expression (MOUNTPATH CHAR_STRING)? (USAGE CHAR_STRING)? + ; + +diskgroup_attributes + : SET ATTRIBUTE CHAR_STRING '=' CHAR_STRING + ; + +modify_diskgroup_file + : MODIFY FILE CHAR_STRING ATTRIBUTE '(' disk_region_clause ')' ( + ',' CHAR_STRING ATTRIBUTE '(' disk_region_clause ')' + )* + ; + +disk_region_clause + : + ; + +drop_diskgroup_file_clause + : DROP FILE filename (',' filename)* + ; + +convert_redundancy_clause + : CONVERT REDUNDANCY TO FLEX + ; + +usergroup_clauses + : ADD USERGROUP CHAR_STRING WITH MEMBER CHAR_STRING (',' CHAR_STRING)* + | MODIFY USERGROUP CHAR_STRING (ADD | DROP) MEMBER CHAR_STRING (',' CHAR_STRING)* + | DROP USERGROUP CHAR_STRING + ; + +user_clauses + : ADD USER CHAR_STRING (',' CHAR_STRING)* + | DROP USER CHAR_STRING (',' CHAR_STRING)* CASCADE? + | REPLACE USER CHAR_STRING WITH CHAR_STRING (',' CHAR_STRING WITH CHAR_STRING)* + ; + +file_permissions_clause + : SET PERMISSION (OWNER | GROUP | OTHER) '=' (NONE | READ (ONLY | WRITE)) ( + ',' (OWNER | GROUP | OTHER) '=' (NONE | READ (ONLY | WRITE)) + )* FOR FILE CHAR_STRING (',' CHAR_STRING)* + ; + +file_owner_clause + : SET OWNERSHIP (OWNER | GROUP) '=' CHAR_STRING (',' (OWNER | GROUP) '=' CHAR_STRING)* FOR FILE CHAR_STRING ( + ',' CHAR_STRING + )* + ; + +scrub_clause + : SCRUB (FILE CHAR_STRING | DISK id_expression)? (REPAIR | NOREPAIR)? ( + POWER (AUTO | LOW | HIGH | MAX) + )? wait_nowait? force_noforce? STOP? + ; + +quotagroup_clauses + : ADD QUOTAGROUP id_expression (SET property_name '=' property_value)? + | MODIFY QUOTAGROUP id_expression SET property_name '=' property_value + | MOVE QUOTAGROUP id_expression TO id_expression + | DROP QUOTAGROUP id_expression + ; + +property_name + : id_expression + ; + +property_value + : id_expression + ; + +filegroup_clauses + : add_filegroup_clause + | modify_filegroup_clause + | move_to_filegroup_clause + | drop_filegroup_clause + ; + +add_filegroup_clause + : ADD FILEGROUP id_expression ((DATABASE | CLUSTER | VOLUME) id_expression | TEMPLATE) ( + FROM TEMPLATE id_expression + )? (SET CHAR_STRING '=' CHAR_STRING)? + ; + +modify_filegroup_clause + : MODIFY FILEGROUP id_expression SET CHAR_STRING '=' CHAR_STRING + ; + +move_to_filegroup_clause + : MOVE FILE CHAR_STRING TO FILEGROUP id_expression + ; + +drop_filegroup_clause + : DROP FILEGROUP id_expression CASCADE? + ; + +quorum_regular + : QUORUM + | REGULAR + ; + +undrop_disk_clause + : UNDROP DISKS + ; + +diskgroup_availability + : MOUNT (RESTRICTED | NORMAL)? (FORCE | NOFORCE)? + | DISMOUNT (FORCE | NOFORCE)? + ; + +enable_disable_volume + : (ENABLE | DISABLE) VOLUME (id_expression (',' id_expression)* | ALL) + ; + +// DDL -> SQL Statements for Stored PL/SQL Units + +// Function DDLs + +drop_function + : DROP FUNCTION function_name + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/ALTER-FLASHBACK-ARCHIVE.html +alter_flashback_archive + : ALTER FLASHBACK ARCHIVE fa = id_expression ( + SET DEFAULT + | (ADD | MODIFY) TABLESPACE ts = id_expression flashback_archive_quota? + | REMOVE TABLESPACE rts = id_expression + | MODIFY /*RETENTION*/ flashback_archive_retention // inconsistent documentation + | PURGE (ALL | BEFORE (SCN expression | TIMESTAMP expression)) + | NO? OPTIMIZE DATA + ) + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/ALTER-HIERARCHY.html +alter_hierarchy + : ALTER HIERARCHY (schema_name '.')? hn = id_expression ( + RENAME TO nhn = id_expression + | COMPILE + ) + ; + +alter_function + : ALTER FUNCTION function_name ( + EDITIONABLE + | NONEDITIONABLE + | COMPILE DEBUG? compiler_parameters_clause* (REUSE SETTINGS)? + ) + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/ALTER-JAVA.html +alter_java + : ALTER JAVA (SOURCE | CLASS) (schema_name '.')? o = id_expression ( + RESOLVER '(' ('(' match_string ','? (schema_name | '-') ')')+ ')' + )? (COMPILE | RESOLVE | invoker_rights_clause) + ; + +match_string + : DELIMITED_ID + | '*' + ; + +create_function_body + : CREATE (OR REPLACE)? (EDITIONABLE | NONEDITIONABLE)? FUNCTION function_name ( + '(' parameter (',' parameter)* ')' + )? RETURN type_spec (SHARING '=' (METADATA | NONE))? ( + invoker_rights_clause + | accessible_by_clause + | default_collation_clause + | parallel_enable_clause + | result_cache_clause + | PIPELINED + | DETERMINISTIC + )* ( + ((IS | AS) (DECLARE? seq_of_declare_specs? body | call_spec)) + | aggregate_clause + | pipelined_using_clause + | sql_macro_body + ) + ; + +sql_macro_body + : SQL_MACRO IS BEGIN RETURN quoted_string SEMICOLON END + ; + +// Creation Function - Specific Clauses + +parallel_enable_clause + : PARALLEL_ENABLE partition_by_clause? + ; + +partition_by_clause + : '(' PARTITION expression BY (ANY | (HASH | RANGE | LIST) paren_column_list) streaming_clause? ')' + ; + +result_cache_clause + : RESULT_CACHE relies_on_part? ('(' MODE (DEFAULT | FORCE ) ')')? + ; + +accessible_by_clause + : ACCESSIBLE BY '(' accessor (',' accessor)* ')' + ; + +default_collation_clause + : DEFAULT COLLATION USING_NLS_COMP + ; + +aggregate_clause + : AGGREGATE USING implementation_type_name + ; + +pipelined_using_clause + : PIPELINED ((ROW | TABLE) POLYMORPHIC)? USING implementation_type_name + ; + +accessor + : unitKind = (FUNCTION | PROCEDURE | PACKAGE | TRIGGER | TYPE) function_name + ; + +relies_on_part + : RELIES_ON '(' tableview_name (',' tableview_name)* ')' + ; + +streaming_clause + : (ORDER | CLUSTER) expression BY paren_column_list + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/ALTER-OUTLINE.html +alter_outline + : ALTER OUTLINE (PUBLIC | PRIVATE)? o = id_expression outline_options+ + ; + +outline_options + : REBUILD + | RENAME TO non = id_expression + | CHANGE CATEGORY TO ncn = id_expression + | ENABLE + | DISABLE + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/ALTER-LOCKDOWN-PROFILE.html +alter_lockdown_profile + : ALTER LOCKDOWN PROFILE id_expression ( + lockdown_feature + | lockdown_options + | lockdown_statements + ) (USERS '=' (ALL | COMMON | LOCAL))? + ; + +lockdown_feature + : disable_enable FEATURE ('=' '(' string_list ')' | ALL (EXCEPT '=' '(' string_list ')')?) + ; + +lockdown_options + : disable_enable OPTION ('=' '(' string_list ')' | ALL (EXCEPT '=' '(' string_list ')')?) + ; + +lockdown_statements + : disable_enable STATEMENT ( + '=' '(' string_list ')' + | '=' '(' CHAR_STRING ')' statement_clauses + | ALL (EXCEPT '=' '(' string_list ')')? + ) + ; + +statement_clauses + : CLAUSE ( + '=' '(' string_list ')' + | '=' '(' CHAR_STRING ')' clause_options + | ALL (EXCEPT '=' '(' string_list ')')? + ) + ; + +clause_options + : OPTION ( + '=' '(' string_list ')' + | '=' '(' CHAR_STRING ')' option_values+ + | ALL (EXCEPT '=' '(' string_list ')')? + ) + ; + +option_values + : VALUE '=' '(' string_list ')' + | (MINVALUE | MAXVALUE) '=' CHAR_STRING + ; + +string_list + : CHAR_STRING (',' CHAR_STRING)* + ; + +disable_enable + : DISABLE + | ENABLE + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-LOCKDOWN-PROFILE.html +drop_lockdown_profile + : DROP LOCKDOWN PROFILE p = id_expression + ; + +// Package DDLs + +drop_package + : DROP PACKAGE BODY? (schema_object_name '.')? package_name + ; + +alter_package + : ALTER PACKAGE package_name COMPILE DEBUG? (PACKAGE | BODY | SPECIFICATION)? compiler_parameters_clause* ( + REUSE SETTINGS + )? + ; + +create_package + : CREATE (OR REPLACE)? (EDITIONABLE | NONEDITIONABLE)? PACKAGE (schema_object_name '.')? package_name invoker_rights_clause? ( + IS + | AS + ) package_obj_spec* END package_name? + ; + +create_package_body + : CREATE (OR REPLACE)? (EDITIONABLE | NONEDITIONABLE)? PACKAGE BODY (schema_object_name '.')? package_name ( + IS + | AS + ) package_obj_body*? (BEGIN seq_of_statements (EXCEPTION exception_handler+)?)? END package_name? + ; + +// Create Package Specific Clauses + +package_obj_spec + : pragma_declaration + | exception_declaration + | procedure_spec + | function_spec + | variable_declaration + | subtype_declaration + | cursor_declaration + | type_declaration + ; + +procedure_spec + : PROCEDURE identifier ('(' parameter ( ',' parameter)* ')')? ( + accessible_by_clause + | PARALLEL_ENABLE + )* ';' + ; + +function_spec + : FUNCTION identifier ('(' parameter ( ',' parameter)* ')')? RETURN type_spec ( + DETERMINISTIC + | PIPELINED + | parallel_enable_clause + | RESULT_CACHE + | streaming_clause + )* ';' + ; + +package_obj_body + : pragma_declaration + | exception_declaration + | procedure_spec + | function_spec + | subtype_declaration + | cursor_declaration + | variable_declaration + | type_declaration + | procedure_body + | function_body + | selection_directive + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/alter-pmem-filestore.html +alter_pmem_filestore + : ALTER PMEM FILESTORE fsn = id_expression ( + RESIZE size_clause + | autoextend_clause + | MOUNT (MOUNTPOINT file_path)? (BACKINGFILE filename)? FORCE? //inconsistent documentation + | DISMOUNT + ) + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/drop-pmem-filestore.html +drop_pmem_filestore + : DROP PMEM FILESTORE fsn = id_expression ((FORCE? INCLUDING | EXCLUDING) CONTENTS)? + ; + +// Procedure DDLs + +drop_procedure + : DROP PROCEDURE procedure_name + ; + +alter_procedure + : ALTER PROCEDURE procedure_name COMPILE DEBUG? compiler_parameters_clause* (REUSE SETTINGS)? + ; + +function_body + : FUNCTION identifier ('(' parameter (',' parameter)* ')')? RETURN type_spec ( + PIPELINED + | DETERMINISTIC + | invoker_rights_clause + | parallel_enable_clause + | result_cache_clause + | streaming_clause + // see example in section "How Table Functions Stream their Input Data" on streaming_clause in DM 9i: https://docs.oracle.com/cd/B10501_01/appdev.920/a96624/08_subs.htm#20554 + )* ( + ( (IS | AS) (DECLARE? seq_of_declare_specs? body | call_spec)) + | (PIPELINED | AGGREGATE) USING implementation_type_name + ) ';' + ; + +procedure_body + : PROCEDURE identifier ('(' parameter (',' parameter)* ')')? ( + accessible_by_clause + | PARALLEL_ENABLE + )* (IS | AS) (DECLARE? seq_of_declare_specs? body | call_spec | EXTERNAL) ';' + ; + +create_procedure_body + : CREATE (OR REPLACE)? PROCEDURE procedure_name ('(' parameter (',' parameter)* ')')? invoker_rights_clause? PARALLEL_ENABLE? ( + IS + | AS + ) (DECLARE? seq_of_declare_specs? body | call_spec | EXTERNAL) + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/ALTER-RESOURCE-COST.html +alter_resource_cost + : ALTER RESOURCE COST ( + (CPU_PER_SESSION | CONNECT_TIME | LOGICAL_READS_PER_SESSION | PRIVATE_SGA) UNSIGNED_INTEGER + )+ + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-OUTLINE.html +drop_outline + : DROP OUTLINE o = id_expression + ; + +// Rollback Segment DDLs + +//https://docs.oracle.com/cd/E11882_01/server.112/e41084/statements_2011.htm#SQLRF00816 +alter_rollback_segment + : ALTER ROLLBACK SEGMENT rollback_segment_name ( + ONLINE + | OFFLINE + | storage_clause + | SHRINK (TO size_clause)? + ) + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-RESTORE-POINT.html +drop_restore_point + : DROP RESTORE POINT rp = id_expression (FOR PLUGGABLE DATABASE pdb = id_expression)? + ; + +drop_rollback_segment + : DROP ROLLBACK SEGMENT rollback_segment_name + ; + +drop_role + : DROP ROLE role_name + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/create-pmem-filestore.html +create_pmem_filestore + : CREATE PMEM FILESTORE fsn = id_expression pmem_filestore_options+ + ; + +pmem_filestore_options + : MOUNTPOINT file_path + | BACKINGFILE filename REUSE? + | (SIZE | BLOCKSIZE) size_clause + | autoextend_clause + ; + +file_path + : CHAR_STRING + ; + +create_rollback_segment + : CREATE PUBLIC? ROLLBACK SEGMENT rollback_segment_name ( + TABLESPACE tablespace + | storage_clause + )* + ; + +// Trigger DDLs + +drop_trigger + : DROP TRIGGER trigger_name + ; + +alter_trigger + : ALTER TRIGGER alter_trigger_name = trigger_name ( + (ENABLE | DISABLE) + | RENAME TO rename_trigger_name = trigger_name + | COMPILE DEBUG? compiler_parameters_clause* (REUSE SETTINGS)? + ) + ; + +create_trigger + : CREATE (OR REPLACE)? EDITIONABLE? TRIGGER trigger_name ( + simple_dml_trigger + | compound_dml_trigger + | non_dml_trigger + ) trigger_follows_clause? (ENABLE | DISABLE)? trigger_when_clause? trigger_body + ; + +trigger_follows_clause + : FOLLOWS trigger_name (',' trigger_name)* + ; + +trigger_when_clause + : WHEN '(' condition ')' + ; + +// Create Trigger Specific Clauses + +simple_dml_trigger + : (BEFORE | AFTER | INSTEAD OF) dml_event_clause referencing_clause? for_each_row? + ; + +for_each_row + : FOR EACH ROW + ; + +compound_dml_trigger + : FOR dml_event_clause referencing_clause? + ; + +non_dml_trigger + : (BEFORE | AFTER) non_dml_event (OR non_dml_event)* ON (DATABASE | (schema_name '.')? SCHEMA) + ; + +trigger_body + : compound_trigger_block + | CALL identifier + | trigger_block + ; + +routine_clause + : routine_name function_argument? + ; + +compound_trigger_block + : COMPOUND TRIGGER seq_of_declare_specs? timing_point_section+ END trigger_name? + ; + +timing_point_section + : bk = BEFORE STATEMENT IS tps_block BEFORE STATEMENT ';' + | bk = BEFORE EACH ROW IS tps_block BEFORE EACH ROW ';' + | ak = AFTER STATEMENT IS tps_block AFTER STATEMENT ';' + | ak = AFTER EACH ROW IS tps_block AFTER EACH ROW ';' + ; + +non_dml_event + : ALTER + | ANALYZE + | ASSOCIATE STATISTICS + | AUDIT + | COMMENT + | CREATE + | DISASSOCIATE STATISTICS + | DROP + | GRANT + | NOAUDIT + | RENAME + | REVOKE + | TRUNCATE + | DDL + | STARTUP + | SHUTDOWN + | DB_ROLE_CHANGE + | LOGON + | LOGOFF + | SERVERERROR + | SUSPEND + | DATABASE + | SCHEMA + | FOLLOWS + ; + +dml_event_clause + : dml_event_element (OR dml_event_element)* ON dml_event_nested_clause? tableview_name + ; + +dml_event_element + : (DELETE | INSERT | UPDATE) (OF column_list)? + ; + +dml_event_nested_clause + : NESTED TABLE tableview_name OF + ; + +referencing_clause + : (REFERENCING referencing_element | REFERENCES) referencing_element* + ; + +referencing_element + : (NEW | OLD | PARENT) column_alias + ; + +// DDLs + +drop_type + : DROP TYPE BODY? type_name (FORCE | VALIDATE)? + ; + +alter_type + : ALTER TYPE type_name ( + compile_type_clause + | replace_type_clause + //TODO | {input.LT(2).getText().equalsIgnoreCase("attribute")}? alter_attribute_definition + | alter_method_spec + | alter_collection_clauses + | modifier_clause + | overriding_subprogram_spec + ) dependent_handling_clause? + ; + +// Alter Type Specific Clauses + +compile_type_clause + : COMPILE DEBUG? (SPECIFICATION | BODY)? compiler_parameters_clause* (REUSE SETTINGS)? + ; + +replace_type_clause + : REPLACE invoker_rights_clause? AS OBJECT '(' object_member_spec (',' object_member_spec)* ')' + ; + +alter_method_spec + : alter_method_element (',' alter_method_element)* + ; + +alter_method_element + : (ADD | DROP) (map_order_function_spec | subprogram_spec) + ; + +alter_attribute_definition + : (ADD | MODIFY | DROP) ATTRIBUTE ( + attribute_definition + | '(' attribute_definition (',' attribute_definition)* ')' + ) + ; + +attribute_definition + : attribute_name type_spec? + ; + +alter_collection_clauses + : MODIFY (LIMIT expression | ELEMENT TYPE type_spec) + ; + +dependent_handling_clause + : INVALIDATE + | CASCADE (CONVERT TO SUBSTITUTABLE | NOT? INCLUDING TABLE DATA)? dependent_exceptions_part? + ; + +dependent_exceptions_part + : FORCE? EXCEPTIONS INTO tableview_name + ; + +create_type + : CREATE (OR REPLACE)? TYPE (type_definition | type_body) + ; + +// Create Type Specific Clauses + +type_definition + : type_name (OID CHAR_STRING)? FORCE? object_type_def? + ; + +object_type_def + : invoker_rights_clause? (object_as_part | object_under_part) sqlj_object_type? ( + '(' object_member_spec (',' object_member_spec)* ')' + )? modifier_clause* + ; + +object_as_part + : (IS | AS) (OBJECT | varray_type_def | nested_table_type_def) + ; + +object_under_part + : UNDER type_spec + ; + +nested_table_type_def + : TABLE OF type_spec (NOT NULL_)? + ; + +sqlj_object_type + : EXTERNAL NAME expression LANGUAGE JAVA USING (SQLDATA | CUSTOMDATUM | ORADATA) + ; + +type_body + : BODY type_name (IS | AS) (type_body_elements)+ END + ; + +type_body_elements + : map_order_func_declaration + | subprog_decl_in_type + | overriding_subprogram_spec + ; + +map_order_func_declaration + : (MAP | ORDER) MEMBER func_decl_in_type + ; + +subprog_decl_in_type + : (MEMBER | STATIC) (proc_decl_in_type | func_decl_in_type | constructor_declaration) + ; + +proc_decl_in_type + : PROCEDURE procedure_name '(' type_elements_parameter (',' type_elements_parameter)* ')' ( + IS + | AS + ) (call_spec | DECLARE? seq_of_declare_specs? body ';') + ; + +func_decl_in_type + : FUNCTION function_name ('(' type_elements_parameter (',' type_elements_parameter)* ')')? RETURN type_spec ( + IS + | AS + ) (call_spec | DECLARE? seq_of_declare_specs? body ';') + ; + +constructor_declaration + : FINAL? INSTANTIABLE? CONSTRUCTOR FUNCTION type_spec ( + '(' (SELF IN OUT type_spec ',') type_elements_parameter (',' type_elements_parameter)* ')' + )? RETURN SELF AS RESULT (IS | AS) (call_spec | DECLARE? seq_of_declare_specs? body ';') + ; + +// Common Type Clauses + +modifier_clause + : NOT? (INSTANTIABLE | FINAL | OVERRIDING) + ; + +object_member_spec + : identifier type_spec sqlj_object_type_attr? + | element_spec + ; + +sqlj_object_type_attr + : EXTERNAL NAME expression + ; + +element_spec + : modifier_clause? element_spec_options+ (',' pragma_clause)? + ; + +element_spec_options + : subprogram_spec + | constructor_spec + | map_order_function_spec + ; + +subprogram_spec + : (MEMBER | STATIC) (type_procedure_spec | type_function_spec) + ; + +// TODO: should be refactored such as Procedure body and Function body, maybe Type_Function_Body and overriding_function_body +overriding_subprogram_spec + : OVERRIDING MEMBER overriding_function_spec + ; + +overriding_function_spec + : FUNCTION function_name ('(' type_elements_parameter (',' type_elements_parameter)* ')')? RETURN ( + type_spec + | SELF AS RESULT + ) (PIPELINED? (IS | AS) (DECLARE? seq_of_declare_specs? body))? ';'? + ; + +type_procedure_spec + : PROCEDURE procedure_name '(' type_elements_parameter (',' type_elements_parameter)* ')' ( + (IS | AS) call_spec + )? + ; + +type_function_spec + : FUNCTION function_name ('(' type_elements_parameter (',' type_elements_parameter)* ')')? RETURN ( + type_spec + | SELF AS RESULT + ) ((IS | AS) call_spec | EXTERNAL VARIABLE? NAME expression)? + ; + +constructor_spec + : FINAL? INSTANTIABLE? CONSTRUCTOR FUNCTION type_spec ( + '(' (SELF IN OUT type_spec ',') type_elements_parameter (',' type_elements_parameter)* ')' + )? RETURN SELF AS RESULT ((IS | AS) call_spec)? + ; + +map_order_function_spec + : (MAP | ORDER) MEMBER type_function_spec + ; + +pragma_clause + : PRAGMA RESTRICT_REFERENCES '(' pragma_elements (',' pragma_elements)* ')' + ; + +pragma_elements + : identifier + | DEFAULT + ; + +type_elements_parameter + : parameter_name type_spec + ; + +// Sequence DDLs + +drop_sequence + : DROP SEQUENCE sequence_name + ; + +alter_sequence + : ALTER SEQUENCE sequence_name sequence_spec+ + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/ALTER-SESSION.html +alter_session + : ALTER SESSION ( + ADVISE ( COMMIT | ROLLBACK | NOTHING) + | CLOSE DATABASE LINK parameter_name + | enable_or_disable COMMIT IN PROCEDURE + | enable_or_disable GUARD + | (enable_or_disable | FORCE) PARALLEL (DML | DDL | QUERY) ( + PARALLEL (literal | parameter_name) + )? + | SET alter_session_set_clause + ) + ; + +alter_session_set_clause + : (parameter_name '=' parameter_value)+ + | EDITION '=' en = id_expression + | CONTAINER '=' cn = id_expression (SERVICE '=' sn = id_expression)? + | ROW ARCHIVAL VISIBILITY '=' (ACTIVE | ALL) + | DEFAULT_COLLATION '=' (c = id_expression | NONE) + ; + +create_sequence + : CREATE SEQUENCE sequence_name (sequence_start_clause | sequence_spec)* + ; + +// Common Sequence + +sequence_spec + : INCREMENT BY UNSIGNED_INTEGER + | MAXVALUE UNSIGNED_INTEGER + | NOMAXVALUE + | MINVALUE UNSIGNED_INTEGER + | NOMINVALUE + | CYCLE + | NOCYCLE + | CACHE UNSIGNED_INTEGER + | NOCACHE + | ORDER + | NOORDER + ; + +sequence_start_clause + : START WITH UNSIGNED_INTEGER + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-ANALYTIC-VIEW.html +create_analytic_view + : CREATE (OR REPLACE)? (NOFORCE | FORCE)? ANALYTIC VIEW av = id_expression ( + SHARING '=' (METADATA | NONE) + )? classification_clause* cav_using_clause? dim_by_clause? measures_clause? default_measure_clause? default_aggregate_clause? cache_clause? + fact_columns_clause? qry_transform_clause? + ; + +classification_clause + // : (CAPTION c=quoted_string)? (DESCRIPTION d=quoted_string)? classification_item* + // to handle - 'rule contains a closure with at least one alternative that can match an empty string' + : (caption_clause description_clause? | caption_clause? description_clause) classification_item* + | caption_clause? description_clause? classification_item+ + ; + +caption_clause + : CAPTION c = quoted_string + ; + +description_clause + : DESCRIPTION d = quoted_string + ; + +classification_item + : CLASSIFICATION cn = id_expression (VALUE cv = quoted_string)? (LANGUAGE language)? + ; + +language + : NULL_ + | nls = id_expression + ; + +cav_using_clause + : USING (schema_name '.')? t = id_expression REMOTE? (AS? ta = id_expression)? + ; + +dim_by_clause + : DIMENSION BY '(' dim_key (',' dim_key)* ')' + ; + +dim_key + : dim_ref classification_clause* KEY ( + '(' (a = id_expression '.')? f = column_name (',' (a = id_expression '.')? f = column_name)* ')' + | (a = id_expression '.')? f = column_name + ) REFERENCES DISTINCT? ('(' attribute_name (',' attribute_name) ')' | attribute_name) HIERARCHIES '(' hier_ref ( + ',' hier_ref + )* ')' + ; + +dim_ref + : (schema_name '.')? ad = id_expression (AS? da = id_expression)? + ; + +hier_ref + : (schema_name '.')? h = id_expression (AS? ha = id_expression)? DEFAULT? + ; + +measures_clause + : MEASURES '(' av_measure (',' av_measure)* ')' + ; + +av_measure + : mn = id_expression (base_meas_clause | calc_meas_clause)? //classification_clause* + ; + +base_meas_clause + : FACT /*FOR MEASURE*/ bm = id_expression meas_aggregate_clause? //FIXME inconsistent documentation + ; + +meas_aggregate_clause + : AGGREGATE BY aggregate_function_name + ; + +calc_meas_clause + : /*m=id_expression*/ AS '(' expression ')' //FIXME inconsistent documentation + ; + +default_measure_clause + : DEFAULT MEASURE m = id_expression + ; + +default_aggregate_clause + : DEFAULT AGGREGATE BY aggregate_function_name + ; + +cache_clause + : CACHE cache_specification (',' cache_specification)* + ; + +cache_specification + : MEASURE GROUP ( + ALL + | '(' id_expression (',' id_expression)* ')' levels_clause (',' levels_clause)* + ) + ; + +levels_clause + : LEVELS '(' level_specification (',' level_specification)* ')' level_group_type + ; + +level_specification + : '(' ((d = id_expression '.')? h = id_expression '.')? l = id_expression ')' + ; + +level_group_type + : DYNAMIC + | MATERIALIZED (USING (schema_name '.')? t = id_expression)? + ; + +fact_columns_clause + : FACT COLUMN f = column_name (AS? fa = id_expression (',' AS? fa = id_expression)*)? + ; + +qry_transform_clause + : ENABLE QUERY TRANSFORM (RELY | NORELY)? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-ATTRIBUTE-DIMENSION.html +create_attribute_dimension + : CREATE (OR REPLACE)? (NOFORCE | FORCE)? ATTRIBUTE DIMENSION (schema_name '.')? ad = id_expression ( + SHARING '=' (METADATA | NONE) + )? classification_clause* (DIMENSION TYPE (STANDARD | TIME))? ad_using_clause attributes_clause ad_level_clause+ all_clause? + ; + +ad_using_clause + : USING source_clause (',' source_clause)* join_path_clause* + ; + +source_clause + : (schema_name '.')? ftov = id_expression REMOTE? (AS? a = id_expression)? + ; + +join_path_clause + : JOIN PATH jpn = id_expression ON join_condition + ; + +join_condition + : join_condition_item (AND join_condition_item)* + ; + +join_condition_item + : (a = id_expression '.')? column_name '=' (b = id_expression '.')? column_name + ; + +attributes_clause + : ATTRIBUTES '(' ad_attributes_clause (',' ad_attributes_clause)* ')' + ; + +ad_attributes_clause + : (a = id_expression '.')? column_name (AS? an = id_expression)? classification_clause* + ; + +ad_level_clause + : LEVEL l = id_expression (NOT NULL_ | SKIP_ WHEN NULL_)? ( + LEVEL TYPE ( + STANDARD + | YEARS + | HALF_YEARS + | QUARTERS + | MONTHS + | WEEKS + | DAYS + | HOURS + | MINUTES + | SECONDS + ) + )? classification_clause* //inconsistent documentation - LEVEL TYPE goes after the classification_clause rule + key_clause alternate_key_clause? (MEMBER NAME expression)? (MEMBER CAPTION expression)? ( + MEMBER DESCRIPTION expression + )? (ORDER BY (MIN | MAX)? dim_order_clause (',' (MIN | MAX)? dim_order_clause)*)? ( + DETERMINES '(' id_expression (',' id_expression)* ')' + )? + ; + +key_clause + : KEY (a = id_expression | '(' id_expression (',' id_expression)* ')') + ; + +alternate_key_clause + : ALTERNATE key_clause + ; + +dim_order_clause + : a = id_expression (ASC | DESC)? (NULLS (FIRST | LAST))? + ; + +all_clause + : ALL MEMBER ( + NAME expression (MEMBER CAPTION expression)? + | CAPTION expression (MEMBER DESCRIPTION expression)? + | DESCRIPTION expression + ) + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-AUDIT-POLICY-Unified-Auditing.html +create_audit_policy + : CREATE AUDIT POLICY p = id_expression privilege_audit_clause? action_audit_clause? role_audit_clause? ( + WHEN quoted_string EVALUATE PER (STATEMENT | SESSION | INSTANCE) + )? (ONLY TOPLEVEL)? container_clause? + ; + +privilege_audit_clause + : PRIVILEGES system_privilege (',' system_privilege)* + ; + +action_audit_clause + : (standard_actions | component_actions | system_actions)+ + ; + +system_actions + : ACTIONS system_privilege (',' system_privilege)* + ; + +standard_actions + : ACTIONS actions_clause (',' actions_clause)* + ; + +actions_clause + : (object_action | ALL) ON ( + DIRECTORY directory_name + | (MINING MODEL)? (schema_name '.')? id_expression + ) + | (system_action | ALL) + ; + +object_action + : ALTER + | GRANT + | READ + | EXECUTE + | AUDIT + | COMMENT + | DELETE + | INDEX + | INSERT + | LOCK + | SELECT + | UPDATE + | FLASHBACK + | RENAME + ; + +system_action + : id_expression // SELECT name FROM AUDITABLE_SYSTEM_ACTIONS WHERE component = 'Standard'; + | (CREATE | ALTER | DROP) JAVA + | LOCK TABLE + | (READ | WRITE | EXECUTE) DIRECTORY + ; + +component_actions + : ACTIONS COMPONENT '=' ( + (DATAPUMP | DIRECT_LOAD | OLS | XS) component_action (',' component_action)* + | DV component_action ON id_expression (',' component_action ON id_expression)* + | PROTOCOL (FTP | HTTP | AUTHENTICATION) + ) + ; + +component_action + : id_expression // SELECT name FROM auditable_system_actions WHERE component = 'Datapump'; + ; + +role_audit_clause + : ROLES role_name (',' role_name)* + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-CONTROLFILE.html +create_controlfile + : CREATE CONTROLFILE REUSE? SET? DATABASE d = id_expression logfile_clause? ( + RESETLOGS + | NORESETLOGS + ) (DATAFILE file_specification (',' file_specification)*)? controlfile_options* character_set_clause? + ; + +controlfile_options + : MAXLOGFILES numeric + | MAXLOGMEMBERS numeric + | MAXLOGHISTORY numeric + | MAXDATAFILES numeric + | MAXINSTANCES numeric + | ARCHIVELOG + | NOARCHIVELOG + | FORCE LOGGING + | SET STANDBY NOLOGGING FOR (DATA AVAILABILITY | LOAD PERFORMANCE) + ; + +logfile_clause + : LOGFILE (GROUP? numeric)? file_specification (',' (GROUP? numeric)? file_specification)* + ; + +character_set_clause + : CHARACTER SET cs = id_expression + ; + +file_specification + : datafile_tempfile_spec + | redo_log_file_spec + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-DISKGROUP.html +create_diskgroup + : CREATE DISKGROUP id_expression ( + (HIGH | NORMAL | FLEX | EXTENDED (SITE sn = id_expression)? | EXTERNAL) REDUNDANCY + )? ( + quorum_regular? (FAILGROUP fg = id_expression)? DISK qualified_disk_clause ( + ',' qualified_disk_clause + )* + )+ (ATTRIBUTE an = CHAR_STRING '=' av = CHAR_STRING (',' CHAR_STRING '=' CHAR_STRING)*)? + ; + +qualified_disk_clause + : ss = CHAR_STRING (NAME dn = id_expression)? (SIZE size_clause)? force_noforce? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-EDITION.html +create_edition + : CREATE EDITION e = id_expression (AS CHILD OF pe = id_expression)? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-FLASHBACK-ARCHIVE.html +create_flashback_archive + : CREATE FLASHBACK ARCHIVE DEFAULT? fa = id_expression TABLESPACE ts = id_expression flashback_archive_quota? ( + NO? OPTIMIZE DATA + )? flashback_archive_retention + ; + +flashback_archive_quota + : QUOTA UNSIGNED_INTEGER (M_LETTER | G_LETTER | T_LETTER | P_LETTER | E_LETTER) + ; + +flashback_archive_retention + : RETENTION UNSIGNED_INTEGER (YEAR | MONTH | DAY) + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-HIERARCHY.html +create_hierarchy + : CREATE (OR REPLACE)? (NO? FORCE)? HIERARCHY (schema_name '.')? h = id_expression ( + SHARING '=' (METADATA | NONE) + )? classification_clause* hier_using_clause level_hier_clause hier_attrs_clause? + ; + +hier_using_clause + : USING (schema_name '.')? ad = id_expression + ; + +level_hier_clause + : '(' (l = id_expression (CHILD OF)?)+ ')' + ; + +hier_attrs_clause + : HIERARCHICAL ATTRIBUTES '(' hier_attr_clause ')' + ; + +hier_attr_clause + : hier_attr_name classification_clause* + ; + +hier_attr_name + : MEMBER_NAME + | MEMBER_UNIQUE_NAME + | MEMBER_CAPTION + | MEMBER_DESCRIPTION + | LEVEL_NAME + | HIER_ORDER + | DEPTH + | IS_LEAF + | PARENT_LEVEL_NAME + | PARENT_UNIQUE_NAME + ; + +create_index + : CREATE (UNIQUE | BITMAP)? INDEX index_name (IF NOT EXISTS)? ON ( + cluster_index_clause + | table_index_clause + | bitmap_join_index_clause + ) (USABLE | UNUSABLE)? ((DEFERRED | IMMEDIATE) INVALIDATION)? + ; + +cluster_index_clause + : CLUSTER cluster_name index_attributes? + ; + +cluster_name + : (id_expression '.')? id_expression + ; + +table_index_clause + : tableview_name table_alias? '(' index_expr (ASC | DESC)? (',' index_expr (ASC | DESC)?)* ')' index_properties? + ; + +bitmap_join_index_clause + : tableview_name '(' (tableview_name | table_alias)? column_name (ASC | DESC)? ( + ',' (tableview_name | table_alias)? column_name (ASC | DESC)? + )* ')' FROM tableview_name table_alias (',' tableview_name table_alias)* where_clause local_partitioned_index? index_attributes? + ; + +index_expr + : column_name + | expression + ; + +index_properties + : (global_partitioned_index | local_partitioned_index | index_attributes)+ + | INDEXTYPE IS (domain_index_clause | xmlindex_clause) + ; + +domain_index_clause + : indextype local_domain_index_clause? parallel_clause? (PARAMETERS '(' odci_parameters ')')? + ; + +local_domain_index_clause + : LOCAL ( + '(' PARTITION partition_name (PARAMETERS '(' odci_parameters ')')? ( + ',' PARTITION partition_name (PARAMETERS '(' odci_parameters ')')? + )* ')' + )? + ; + +xmlindex_clause + : (XDB '.')? XMLINDEX local_xmlindex_clause? parallel_clause? //TODO xmlindex_parameters_clause? + ; + +local_xmlindex_clause + : LOCAL ( + '(' PARTITION partition_name ( + ',' PARTITION partition_name //TODO xmlindex_parameters_clause? + )* ')' + )? + ; + +global_partitioned_index + : GLOBAL PARTITION BY ( + RANGE '(' column_name (',' column_name)* ')' '(' index_partitioning_clause ( + ',' index_partitioning_clause + )* ')' + | HASH '(' column_name (',' column_name)* ')' ( + individual_hash_partitions + | hash_partitions_by_quantity + ) + ) + ; + +index_partitioning_clause + : PARTITION partition_name? VALUES LESS THAN '(' index_partitioning_values_list ')' segment_attributes_clause? + ; + +index_partitioning_values_list + : literal (',' literal)* + | TIMESTAMP literal (',' TIMESTAMP literal)* + ; + +local_partitioned_index + : LOCAL ( + on_range_partitioned_table + | on_list_partitioned_table + | on_hash_partitioned_table + | on_comp_partitioned_table + )? + ; + +on_range_partitioned_table + : '(' partitioned_table (',' partitioned_table)* ')' + ; + +on_list_partitioned_table + : '(' partitioned_table (',' partitioned_table)* ')' + ; + +partitioned_table + : PARTITION partition_name? (segment_attributes_clause | key_compression)* UNUSABLE? + ; + +on_hash_partitioned_table + : STORE IN '(' tablespace (',' tablespace)* ')' + | '(' on_hash_partitioned_clause (',' on_hash_partitioned_clause)* ')' + ; + +on_hash_partitioned_clause + : PARTITION partition_name? (TABLESPACE tablespace)? key_compression? UNUSABLE? + ; + +on_comp_partitioned_table + : (STORE IN '(' tablespace (',' tablespace)* ')')? '(' on_comp_partitioned_clause ( + ',' on_comp_partitioned_clause + )* ')' + ; + +on_comp_partitioned_clause + : PARTITION partition_name? (segment_attributes_clause | key_compression)* UNUSABLE index_subpartition_clause? + ; + +index_subpartition_clause + : STORE IN '(' tablespace (',' tablespace)* ')' + | '(' index_subpartition_subclause (',' index_subpartition_subclause)* ')' + ; + +index_subpartition_subclause + : SUBPARTITION subpartition_name? (TABLESPACE tablespace)? key_compression? UNUSABLE? + ; + +odci_parameters + : CHAR_STRING + ; + +indextype + : (id_expression '.')? id_expression + ; + +//https://docs.oracle.com/cd/E11882_01/server.112/e41084/statements_1010.htm#SQLRF00805 +alter_index + : ALTER INDEX index_name (alter_index_ops_set1 | alter_index_ops_set2) + ; + +alter_index_ops_set1 + : ( + deallocate_unused_clause + | allocate_extent_clause + | shrink_clause + | parallel_clause + | physical_attributes_clause + | logging_clause + )+ + ; + +alter_index_ops_set2 + : rebuild_clause + | PARAMETERS '(' odci_parameters ')' + | COMPILE + | enable_or_disable + | UNUSABLE + | visible_or_invisible + | RENAME TO new_index_name + | COALESCE + | monitoring_nomonitoring USAGE + | UPDATE BLOCK REFERENCES + | alter_index_partitioning + ; + +visible_or_invisible + : VISIBLE + | INVISIBLE + ; + +monitoring_nomonitoring + : MONITORING + | NOMONITORING + ; + +rebuild_clause + : REBUILD (PARTITION partition_name | SUBPARTITION subpartition_name | REVERSE | NOREVERSE)? ( + parallel_clause + | TABLESPACE tablespace + | PARAMETERS '(' odci_parameters ')' + //TODO | xmlindex_parameters_clause + | ONLINE + | physical_attributes_clause + | key_compression + | logging_clause + )* + ; + +alter_index_partitioning + : modify_index_default_attrs + | add_hash_index_partition + | modify_index_partition + | rename_index_partition + | drop_index_partition + | split_index_partition + | coalesce_index_partition + | modify_index_subpartition + ; + +modify_index_default_attrs + : MODIFY DEFAULT ATTRIBUTES (FOR PARTITION partition_name)? ( + physical_attributes_clause + | TABLESPACE (tablespace | DEFAULT) + | logging_clause + ) + ; + +add_hash_index_partition + : ADD PARTITION partition_name? (TABLESPACE tablespace)? key_compression? parallel_clause? + ; + +coalesce_index_partition + : COALESCE PARTITION parallel_clause? + ; + +modify_index_partition + : MODIFY PARTITION partition_name ( + modify_index_partitions_ops+ + | PARAMETERS '(' odci_parameters ')' + | COALESCE + | UPDATE BLOCK REFERENCES + | UNUSABLE + ) + ; + +modify_index_partitions_ops + : deallocate_unused_clause + | allocate_extent_clause + | physical_attributes_clause + | logging_clause + | key_compression + ; + +rename_index_partition + : RENAME (PARTITION partition_name | SUBPARTITION subpartition_name) TO new_partition_name + ; + +drop_index_partition + : DROP PARTITION partition_name + ; + +split_index_partition + : SPLIT PARTITION partition_name_old AT '(' literal (',' literal)* ')' ( + INTO '(' index_partition_description ',' index_partition_description ')' + )? parallel_clause? + ; + +index_partition_description + : PARTITION ( + partition_name ( + (segment_attributes_clause | key_compression)+ + | PARAMETERS '(' odci_parameters ')' + ) UNUSABLE? + )? + ; + +modify_index_subpartition + : MODIFY SUBPARTITION subpartition_name ( + UNUSABLE + | allocate_extent_clause + | deallocate_unused_clause + ) + ; + +partition_name_old + : partition_name + ; + +new_partition_name + : partition_name + ; + +new_index_name + : index_name + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/ALTER-INMEMORY-JOIN-GROUP.html +alter_inmemory_join_group + : ALTER INMEMORY JOIN GROUP (schema_name '.')? jg = id_expression (ADD | REMOVE) '(' ( + schema_name '.' + )? t = id_expression '(' c = id_expression ')' ')' + ; + +create_user + : CREATE USER user_object_name (IF NOT EXISTS)? ( + identified_by + | identified_other_clause + | user_tablespace_clause + | quota_clause + | profile_clause + | password_expire_clause + | user_lock_clause + | user_editions_clause + | container_clause + )+ + ; + +// The standard clauses only permit one user per statement. +// The proxy clause allows multiple users for a proxy designation. +alter_user + : ALTER USER user_object_name ( + alter_identified_by + | identified_other_clause + | user_tablespace_clause + | quota_clause + | profile_clause + | user_default_role_clause + | password_expire_clause + | user_lock_clause + | alter_user_editions_clause + | container_clause + | container_data_clause + )+ + | user_object_name (',' user_object_name)* proxy_clause + ; + +drop_user + : DROP USER user_object_name (IF EXISTS)? CASCADE? + ; + +alter_identified_by + : identified_by (REPLACE id_expression)? + ; + +identified_by + : IDENTIFIED BY id_expression + ; + +identified_other_clause + : IDENTIFIED (EXTERNALLY | GLOBALLY) (AS quoted_string)? + ; + +user_tablespace_clause + : (DEFAULT | TEMPORARY) TABLESPACE id_expression + ; + +quota_clause + : QUOTA (size_clause | UNLIMITED) ON id_expression + ; + +profile_clause + : PROFILE id_expression + ; + +role_clause + : role_name (',' role_name)* + | ALL (EXCEPT role_name (',' role_name)*)* + ; + +user_default_role_clause + : DEFAULT ROLE (NONE | role_clause) + ; + +password_expire_clause + : PASSWORD EXPIRE + ; + +user_lock_clause + : ACCOUNT (LOCK | UNLOCK) + ; + +user_editions_clause + : ENABLE EDITIONS + ; + +alter_user_editions_clause + : user_editions_clause (FOR regular_id (',' regular_id)*)? FORCE? + ; + +proxy_clause + : REVOKE CONNECT THROUGH (ENTERPRISE USERS | user_object_name) + | GRANT CONNECT THROUGH ( + ENTERPRISE USERS + | user_object_name (WITH (NO ROLES | ROLE role_clause))? (AUTHENTICATION REQUIRED)? ( + AUTHENTICATED USING (PASSWORD | CERTIFICATE | DISTINGUISHED NAME) + )? + ) + ; + +container_names + : LEFT_PAREN id_expression (',' id_expression)* RIGHT_PAREN + ; + +set_container_data + : SET CONTAINER_DATA EQUALS_OP (ALL | DEFAULT | container_names) + ; + +add_rem_container_data + : (ADD | REMOVE) CONTAINER_DATA EQUALS_OP container_names + ; + +container_data_clause + : set_container_data + | add_rem_container_data (FOR container_tableview_name)? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/ADMINISTER-KEY-MANAGEMENT.html +administer_key_management + : ADMINISTER KEY MANAGEMENT ( + keystore_management_clauses + | key_management_clauses + | secret_management_clauses + | zero_downtime_software_patching_clauses + ) + ; + +keystore_management_clauses + : create_keystore + | open_keystore + | close_keystore + | backup_keystore + | alter_keystore_password + | merge_into_new_keystore + | merge_into_existing_keystore + | isolate_keystore + | unite_keystore + ; + +create_keystore + : CREATE ( + KEYSTORE ksl = CHAR_STRING + | LOCAL? AUTO_LOGIN KEYSTORE FROM KEYSTORE ksl = CHAR_STRING + ) IDENTIFIED BY keystore_password + ; + +open_keystore + : SET KEYSTORE OPEN force_keystore? identified_by_store container_clause? + ; + +force_keystore + : FORCE KEYSTORE + ; + +close_keystore + : SET KEYSTORE CLOSE identified_by_store? container_clause? + ; + +backup_keystore + : BACKUP KEYSTORE (USING bi = CHAR_STRING)? force_keystore? identified_by_store ( + TO ksl = CHAR_STRING + )? + ; + +alter_keystore_password + : ALTER KEYSTORE PASSWORD force_keystore? IDENTIFIED BY o = keystore_password SET n = keystore_password with_backup_clause? + ; + +merge_into_new_keystore + : MERGE KEYSTORE ksl1 = CHAR_STRING identified_by_password_clause? AND KEYSTORE ksl2 = CHAR_STRING identified_by_password_clause? INTO NEW + KEYSTORE ksl2 = CHAR_STRING identified_by_password_clause + ; + +merge_into_existing_keystore + : MERGE KEYSTORE ksl1 = CHAR_STRING identified_by_password_clause? INTO EXISTING KEYSTORE ksl2 = CHAR_STRING identified_by_password_clause + with_backup_clause? + ; + +isolate_keystore + : FORCE? ISOLATE KEYSTORE IDENTIFIED BY i = keystore_password FROM ROOT KEYSTORE force_keystore? identified_by_store with_backup_clause? + ; + +unite_keystore + : UNITE KEYSTORE IDENTIFIED BY i = keystore_password WITH ROOT KEYSTORE force_keystore? identified_by_store with_backup_clause? + ; + +key_management_clauses + : set_key + | create_key + | use_key + | set_key_tag + | export_keys + | import_keys + | migrate_keys + | reverse_migrate_keys + | move_keys + ; + +set_key + : SET ENCRYPTION? KEY ((mkid ':')? mk)? using_tag_clause? using_algorithm_clause? force_keystore? identified_by_store with_backup_clause? + container_clause? + ; + +create_key + : CREATE ENCRYPTION? KEY ((mkid ':')? mk)? using_tag_clause? using_algorithm_clause? force_keystore? identified_by_store with_backup_clause? + container_clause? + ; + +mkid + : CHAR_STRING + ; + +mk + : CHAR_STRING + ; + +use_key + : USE ENCRYPTION? KEY k = CHAR_STRING using_tag_clause? force_keystore? identified_by_store with_backup_clause? + ; + +set_key_tag + : SET TAG t = CHAR_STRING FOR k = CHAR_STRING force_keystore? identified_by_store with_backup_clause? + ; + +export_keys + : EXPORT ENCRYPTION? KEYS WITH SECRET secret TO filename force_keystore? identified_by_store ( + WITH IDENTIFIER IN (CHAR_STRING (',' CHAR_STRING)* | '(' subquery ')') + )? + ; + +import_keys + : IMPORT ENCRYPTION? KEYS WITH SECRET secret FROM filename force_keystore? identified_by_store with_backup_clause? + ; + +migrate_keys + : SET ENCRYPTION? KEY IDENTIFIED BY hsm = secret force_keystore? MIGRATE USING keystore_password with_backup_clause? + ; + +reverse_migrate_keys + : SET ENCRYPTION? KEY IDENTIFIED BY s = secret force_keystore? REVERSE MIGRATE USING hsm = secret + ; + +move_keys + : MOVE ENCRYPTION? KEYS TO NEW KEYSTORE ksl1 = CHAR_STRING IDENTIFIED BY ksp1 = keystore_password FROM FORCE? KEYSTORE IDENTIFIED BY ksp = + keystore_password (WITH IDENTIFIER IN (CHAR_STRING (',' CHAR_STRING)* | subquery))? with_backup_clause? + ; + +identified_by_store + : IDENTIFIED BY (EXTERNAL STORE | keystore_password) + ; + +using_algorithm_clause + : USING ALGORITHM ea = CHAR_STRING + ; + +using_tag_clause + : USING TAG t = CHAR_STRING + ; + +secret_management_clauses + : add_update_secret + | delete_secret + | add_update_secret_seps + | delete_secret_seps + ; + +add_update_secret + : (ADD | UPDATE) SECRET s = CHAR_STRING FOR CLIENT ci = CHAR_STRING using_tag_clause? force_keystore? identified_by_store? with_backup_clause? + ; + +delete_secret + : DELETE SECRET FOR CLIENT ci = CHAR_STRING force_keystore? identified_by_store with_backup_clause? + ; + +add_update_secret_seps + : (ADD | UPDATE) SECRET s = CHAR_STRING FOR CLIENT ci = CHAR_STRING using_tag_clause? TO LOCAL? AUTO_LOGIN KEYSTORE directory_path + ; + +delete_secret_seps + : DELETE SECRET s = CHAR_STRING SQ FOR CLIENT ci = CHAR_STRING FROM LOCAL? AUTO_LOGIN KEYSTORE directory_path + ; + +zero_downtime_software_patching_clauses + : SWITCHOVER TO? LIBRARY path FOR ALL CONTAINERS //inconsistent documentation + ; + +with_backup_clause + : WITH BACKUP (USING bi = CHAR_STRING)? + ; + +identified_by_password_clause + : IDENTIFIED BY keystore_password + ; + +keystore_password + : DELIMITED_ID + ; + +path + : CHAR_STRING + ; + +secret + : DELIMITED_ID + ; + +// https://docs.oracle.com/cd/E11882_01/server.112/e41084/statements_4005.htm#SQLRF01105 +analyze + : ( + ANALYZE (TABLE tableview_name | INDEX index_name) partition_extention_clause? + | ANALYZE CLUSTER cluster_name + ) (validation_clauses | LIST CHAINED ROWS into_clause1? | DELETE SYSTEM? STATISTICS) + ; + +partition_extention_clause + : PARTITION ( + '(' partition_name ')' + | FOR '(' partition_key_value (',' partition_key_value)* ')' + ) + | SUBPARTITION ( + '(' subpartition_name ')' + | FOR '(' subpartition_key_value (',' subpartition_key_value)* ')' + ) + ; + +validation_clauses + : VALIDATE REF UPDATE (SET DANGLING TO NULL_)? + | VALIDATE STRUCTURE (CASCADE FAST | CASCADE online_or_offline? into_clause? | CASCADE)? online_or_offline? into_clause? + ; + +compute_clauses + : COMPUTE SYSTEM? STATISTICS for_clause? + ; + +for_clause + : FOR ( + TABLE for_clause* + | ALL (INDEXED? COLUMNS (SIZE UNSIGNED_INTEGER)? for_clause* | LOCAL? INDEXES) + | COLUMNS (SIZE UNSIGNED_INTEGER)? (column_name SIZE UNSIGNED_INTEGER)+ for_clause* + ) + ; + +online_or_offline + : OFFLINE + | ONLINE + ; + +into_clause1 + : INTO tableview_name? + ; + +//Making assumption on partition ad subpartition key value clauses +partition_key_value + : literal + | TIMESTAMP quoted_string + ; + +subpartition_key_value + : literal + | TIMESTAMP quoted_string + ; + +//https://docs.oracle.com/cd/E11882_01/server.112/e41084/statements_4006.htm#SQLRF01106 +associate_statistics + : ASSOCIATE STATISTICS WITH (column_association | function_association) storage_table_clause? + ; + +column_association + : COLUMNS tableview_name '.' column_name (',' tableview_name '.' column_name)* using_statistics_type + ; + +function_association + : ( + FUNCTIONS function_name (',' function_name)* + | PACKAGES package_name (',' package_name)* + | TYPES type_name (',' type_name)* + | INDEXES index_name (',' index_name)* + | INDEXTYPES indextype_name (',' indextype_name)* + ) ( + using_statistics_type + | default_cost_clause (',' default_selectivity_clause)? + | default_selectivity_clause (',' default_cost_clause)? + ) + ; + +indextype_name + : id_expression + ; + +using_statistics_type + : USING (statistics_type_name | NULL_) + ; + +statistics_type_name + : regular_id + ; + +default_cost_clause + : DEFAULT COST '(' cpu_cost ',' io_cost ',' network_cost ')' + ; + +cpu_cost + : UNSIGNED_INTEGER + ; + +io_cost + : UNSIGNED_INTEGER + ; + +network_cost + : UNSIGNED_INTEGER + ; + +default_selectivity_clause + : DEFAULT SELECTIVITY default_selectivity + ; + +default_selectivity + : UNSIGNED_INTEGER + ; + +storage_table_clause + : WITH (SYSTEM | USER) MANAGED STORAGE TABLES + ; + +// https://docs.oracle.com/database/121/SQLRF/statements_4008.htm#SQLRF56110 +unified_auditing + : {this.isVersion12()}? AUDIT ( + POLICY policy_name ((BY | EXCEPT) audit_user (',' audit_user)*)? (WHENEVER NOT? SUCCESSFUL)? + | CONTEXT NAMESPACE oracle_namespace ATTRIBUTES attribute_name (',' attribute_name)* ( + BY audit_user (',' audit_user)* + )? + ) + ; + +policy_name + : identifier + ; + +// https://docs.oracle.com/cd/E11882_01/server.112/e41084/statements_4007.htm#SQLRF01107 +// https://docs.oracle.com/database/121/SQLRF/statements_4007.htm#SQLRF01107 + +audit_traditional + : AUDIT ( + audit_operation_clause (auditing_by_clause | IN SESSION CURRENT)? + | audit_schema_object_clause + | NETWORK + | audit_direct_path + ) (BY (SESSION | ACCESS))? (WHENEVER NOT? SUCCESSFUL)? audit_container_clause? + ; + +audit_direct_path + : {this.isVersion12()}? DIRECT_PATH auditing_by_clause + ; + +audit_container_clause + : {this.isVersion12()}? (CONTAINER EQUALS_OP (CURRENT | ALL)) + ; + +audit_operation_clause + : ( + (sql_statement_shortcut | ALL STATEMENTS?) (',' (sql_statement_shortcut | ALL STATEMENTS?))* + | (system_privilege | ALL PRIVILEGES) (',' (system_privilege | ALL PRIVILEGES))* + ) + ; + +auditing_by_clause + : BY audit_user (',' audit_user)* + ; + +audit_user + : regular_id + ; + +audit_schema_object_clause + : (sql_operation (',' sql_operation)* | ALL) auditing_on_clause + ; + +sql_operation + : ALTER + | AUDIT + | COMMENT + | DELETE + | EXECUTE + | FLASHBACK + | GRANT + | INDEX + | INSERT + | LOCK + | READ + | RENAME + | SELECT + | UPDATE + ; + +auditing_on_clause + : ON ( + object_name + | DIRECTORY regular_id + | MINING MODEL model_name + | {this.isVersion12()}? SQL TRANSLATION PROFILE profile_name + | DEFAULT + ) + ; + +model_name + : (id_expression '.')? id_expression + ; + +object_name + : (id_expression '.')? id_expression + ; + +profile_name + : (id_expression '.')? id_expression + ; + +sql_statement_shortcut + : ALTER SYSTEM + | CLUSTER + | CONTEXT + | DATABASE LINK + | DIMENSION + | DIRECTORY + | INDEX + | MATERIALIZED VIEW + | NOT EXISTS + | OUTLINE + | {this.isVersion12()}? PLUGGABLE DATABASE + | PROCEDURE + | PROFILE + | PUBLIC DATABASE LINK + | PUBLIC SYNONYM + | ROLE + | ROLLBACK SEGMENT + | SEQUENCE + | SESSION + | SYNONYM + | SYSTEM AUDIT + | SYSTEM GRANT + | TABLE + | TABLESPACE + | TRIGGER + | TYPE + | USER + | VIEW + | ALTER SEQUENCE + | ALTER TABLE + | COMMENT TABLE + | DELETE TABLE + | EXECUTE PROCEDURE + | GRANT DIRECTORY + | GRANT PROCEDURE + | GRANT SEQUENCE + | GRANT TABLE + | GRANT TYPE + | INSERT TABLE + | LOCK TABLE + | SELECT SEQUENCE + | SELECT TABLE + | UPDATE TABLE + ; + +drop_index + : DROP INDEX index_name (IF EXISTS)? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DISASSOCIATE-STATISTICS.html +disassociate_statistics + : DISASSOCIATE STATISTICS FROM ( + COLUMNS (schema_name '.')? tb = id_expression '.' c = id_expression ( + ',' (schema_name '.')? tb = id_expression '.' c = id_expression + )* + | FUNCTIONS (schema_name '.')? fn = id_expression ( + ',' (schema_name '.')? fn = id_expression + )* + | PACKAGES (schema_name '.')? pkg = id_expression ( + ',' (schema_name '.')? pkg = id_expression + )* + | TYPES (schema_name '.')? t = id_expression (',' (schema_name '.')? t = id_expression)* + | INDEXES (schema_name '.')? ix = id_expression (',' (schema_name '.')? ix = id_expression)* + | INDEXTYPES (schema_name '.')? it = id_expression ( + ',' (schema_name '.')? it = id_expression + )* + ) FORCE? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-INDEXTYPE.html +drop_indextype + : DROP INDEXTYPE (schema_name '.')? it = id_expression FORCE? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-INMEMORY-JOIN-GROUP.html +drop_inmemory_join_group + : DROP INMEMORY JOIN GROUP (schema_name '.')? jg = id_expression + ; + +flashback_table + : FLASHBACK TABLE tableview_name (',' tableview_name)* TO ( + ((SCN | TIMESTAMP) expression | RESTORE POINT restore_point) ((ENABLE | DISABLE) TRIGGERS)? + | BEFORE DROP (RENAME TO tableview_name)? + ) + ; + +restore_point + : identifier ('.' id_expression)* + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/PURGE.html +purge_statement + : PURGE ( + (TABLE | INDEX) id_expression + | TABLESPACE SET? ts = id_expression (USER u = id_expression)? + | RECYCLEBIN + | DBA_RECYCLEBIN + ) + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/NOAUDIT-Traditional-Auditing.html +noaudit_statement + : NOAUDIT ( + audit_operation_clause auditing_by_clause? + | audit_schema_object_clause + | NETWORK + | DIRECT_PATH LOAD auditing_by_clause? + ) (WHENEVER NOT? SUCCESSFUL)? container_clause? + ; + +rename_object + : RENAME object_name TO object_name + ; + +grant_statement + : GRANT (','? (role_name | system_privilege | object_privilege paren_column_list?))+ ( + ON grant_object_name + )? TO (grantee_name | PUBLIC) (',' (grantee_name | PUBLIC))* (WITH (ADMIN | DELEGATE) OPTION)? ( + WITH HIERARCHY OPTION + )? (WITH GRANT OPTION)? container_clause? + ; + +container_clause + : CONTAINER EQUALS_OP (CURRENT | ALL) + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/REVOKE.html +revoke_statement + : REVOKE ( + (revoke_system_privilege | revoke_object_privileges) container_clause? + | revoke_roles_from_programs + ) + ; + +revoke_system_privilege + : (system_privilege | role_name | ALL PRIVILEGES) FROM revokee_clause + ; + +revokee_clause + : (id_expression | PUBLIC) (',' (id_expression | PUBLIC))* + ; + +revoke_object_privileges + : (object_privilege | ALL PRIVILEGES?) (',' (object_privilege | ALL PRIVILEGES?))* on_object_clause FROM revokee_clause ( + CASCADE CONSTRAINTS + | FORCE + )? + ; + +on_object_clause + : ON ( + (schema_name '.')? o = id_expression + | USER id_expression (',' id_expression)* + | DIRECTORY directory_name + | EDITION edition_name + | MINING MODEL (schema_name '.')? mmn = id_expression + | JAVA (SOURCE | RESOURCE) (schema_name '.')? o2 = id_expression + | SQL TRANSLATION PROFILE (schema_name '.')? p = id_expression + ) + ; + +revoke_roles_from_programs + : (role_name (',' role_name)* | ALL) FROM program_unit (',' program_unit)* + ; + +program_unit + : (FUNCTION | PROCEDURE | PACKAGE) (schema_name '.')? id_expression + ; + +create_dimension + : CREATE DIMENSION identifier level_clause+ ( + hierarchy_clause + | attribute_clause + | extended_attribute_clause + )+ + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-DIRECTORY.html +create_directory + : CREATE (OR REPLACE)? DIRECTORY directory_name (SHARING '=' (METADATA | NONE))? AS directory_path + ; + +directory_name + : regular_id + ; + +directory_path + : CHAR_STRING + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-INMEMORY-JOIN-GROUP.html +create_inmemory_join_group + : CREATE INMEMORY JOIN GROUP (schema_name '.')? jg = id_expression '(' (schema_name '.')? t = id_expression '(' c = id_expression ')' ( + ',' (schema_name '.')? t = id_expression '(' c = id_expression ')' + )+ ')' + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-HIERARCHY.html +drop_hierarchy + : DROP HIERARCHY (schema_name '.')? hn = id_expression + ; + +// https://docs.oracle.com/cd/E11882_01/appdev.112/e25519/alter_library.htm#LNPLS99946 +// https://docs.oracle.com/database/121/LNPLS/alter_library.htm#LNPLS99946 +alter_library + : ALTER LIBRARY library_name ( + COMPILE library_debug? compiler_parameters_clause* (REUSE SETTINGS)? + | library_editionable + ) + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-JAVA.html +drop_java + : DROP JAVA (SOURCE | CLASS | RESOURCE) (schema_name '.')? id_expression + ; + +drop_library + : DROP LIBRARY library_name + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-JAVA.html +create_java + : CREATE (OR REPLACE)? (AND (RESOLVE | COMPILE))? NOFORCE? JAVA ( + (SOURCE | RESOURCE) NAMED (schema_name '.')? pn = id_expression + | CLASS (SCHEMA id_expression)? + ) (SHARING '=' (METADATA | NONE))? invoker_rights_clause? ( + RESOLVER '(' ('(' CHAR_STRING ','? (sn = id_expression | '-') ')')+ ')' + )? ( + USING ( + BFILE '(' d = id_expression ',' filename ')' + | (CLOB | BLOB | BFILE) subquery + | CHAR_STRING + ) + | AS CHAR_STRING + ) + ; + +create_library + : CREATE (OR REPLACE)? (EDITIONABLE | NONEDITIONABLE)? LIBRARY plsql_library_source + ; + +plsql_library_source + : library_name (IS | AS) quoted_string (IN directory_name)? (AGENT quoted_string)? ( + CREDENTIAL credential_name + )? + ; + +credential_name + : (id_expression '.')? id_expression + ; + +library_editionable + : {this.isVersion12()}? (EDITIONABLE | NONEDITIONABLE) + ; + +library_debug + : {this.isVersion12()}? DEBUG + ; + +compiler_parameters_clause + : parameter_name EQUALS_OP parameter_value + ; + +parameter_value + : regular_id + | CHAR_STRING + ; + +library_name + : (regular_id '.')? regular_id + ; + +alter_dimension + : ALTER DIMENSION identifier ( + (ADD (level_clause | hierarchy_clause | attribute_clause | extended_attribute_clause))+ + | ( + DROP ( + LEVEL identifier (RESTRICT | CASCADE)? + | HIERARCHY identifier + | ATTRIBUTE identifier ( + LEVEL identifier (COLUMN column_name (',' COLUMN column_name)*)? + )? + ) + )+ + | COMPILE + ) + ; + +level_clause + : LEVEL identifier IS ( + table_name '.' column_name + | '(' table_name '.' column_name (',' table_name '.' column_name)* ')' + ) (SKIP_ WHEN NULL_)? + ; + +hierarchy_clause + : HIERARCHY identifier '(' identifier (CHILD OF identifier)+ dimension_join_clause? ')' + ; + +dimension_join_clause + : (JOIN KEY column_one_or_more_sub_clause REFERENCES identifier)+ + ; + +attribute_clause + : (ATTRIBUTE identifier DETERMINES column_one_or_more_sub_clause)+ + ; + +extended_attribute_clause + : ATTRIBUTE identifier (LEVEL identifier DETERMINES column_one_or_more_sub_clause)+ + ; + +column_one_or_more_sub_clause + : column_name + | '(' column_name (',' column_name)* ')' + ; + +// https://docs.oracle.com/cd/E11882_01/server.112/e41084/statements_4004.htm#SQLRF01104 +// https://docs.oracle.com/database/121/SQLRF/statements_4004.htm#SQLRF01104 +alter_view + : ALTER VIEW tableview_name ( + ADD out_of_line_constraint + | MODIFY CONSTRAINT constraint_name (RELY | NORELY) + | DROP ( + CONSTRAINT constraint_name + | PRIMARY KEY + | UNIQUE '(' column_name (',' column_name)* ')' + ) + | COMPILE + | READ (ONLY | WRITE) + | alter_view_editionable? + ) + ; + +alter_view_editionable + : {this.isVersion12()}? (EDITIONABLE | NONEDITIONABLE) + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-VIEW.html +create_view + : CREATE (OR REPLACE)? (NO? FORCE)? editioning_clause? VIEW (schema_name '.')? v = id_expression ( + IF NOT EXISTS + )? (SHARING '=' (METADATA | EXTENDED? DATA | NONE))? view_options? ( + DEFAULT COLLATION cn = id_expression + )? (BEQUEATH (CURRENT_USER | DEFINER))? AS select_only_statement subquery_restriction_clause? ( + CONTAINER_MAP + | CONTAINERS_DEFAULT + )? + ; + +editioning_clause + : EDITIONING + | EDITIONABLE EDITIONING? + | NONEDITIONABLE + ; + +view_options + : view_alias_constraint + | object_view_clause + | xmltype_view_clause + ; + +view_alias_constraint + : '(' (','? (table_alias inline_constraint* | out_of_line_constraint))+ ')' + ; + +object_view_clause + : OF (schema_name '.')? tn = id_expression ( + WITH OBJECT (IDENTIFIER | ID) (DEFAULT | '(' REGULAR_ID (',' REGULAR_ID)* ')') + | UNDER (schema_name '.')? sv = id_expression + ) ('(' (','? (out_of_line_constraint | REGULAR_ID inline_constraint))+ ')')* + ; + +inline_constraint + : (CONSTRAINT constraint_name)? ( + NOT? NULL_ + | UNIQUE + | PRIMARY KEY + | references_clause + | check_constraint + ) constraint_state? + ; + +inline_ref_constraint + : SCOPE IS tableview_name + | WITH ROWID + | (CONSTRAINT constraint_name)? references_clause constraint_state? + ; + +out_of_line_ref_constraint + : SCOPE FOR '(' ref_col_or_attr = regular_id ')' IS tableview_name + | REF '(' ref_col_or_attr = regular_id ')' WITH ROWID + | (CONSTRAINT constraint_name)? FOREIGN KEY '(' (','? ref_col_or_attr = regular_id)+ ')' references_clause constraint_state? + ; + +out_of_line_constraint + : ( + ((CONSTRAINT | CONSTRAINTS) constraint_name)? ( + UNIQUE '(' column_name (',' column_name)* ')' + | PRIMARY KEY '(' column_name (',' column_name)* ')' + | foreign_key_clause + | CHECK '(' condition ')' + ) + ) constraint_state? + ; + +constraint_state + : ( + NOT? DEFERRABLE + | INITIALLY (IMMEDIATE | DEFERRED) + | (RELY | NORELY) + | (ENABLE | DISABLE) + | (VALIDATE | NOVALIDATE) + | using_index_clause + )+ + ; + +xmltype_view_clause + : OF XMLTYPE xml_schema_spec? WITH OBJECT (IDENTIFIER | ID) ( + DEFAULT + | '(' expression (',' expression)* ')' + ) + ; + +xml_schema_spec + : (XMLSCHEMA xml_schema_url)? ELEMENT (element | xml_schema_url '#' element) ( + STORE ALL VARRAYS AS (LOBS | TABLES) + )? (allow_or_disallow NONSCHEMA)? (allow_or_disallow ANYSCHEMA)? + ; + +xml_schema_url + : DELIMITED_ID + ; + +element + : DELIMITED_ID + ; + +alter_tablespace + : ALTER TABLESPACE tablespace ( + DEFAULT table_compression? storage_clause? + | MINIMUM EXTENT size_clause + | RESIZE size_clause + | COALESCE + | SHRINK SPACE_KEYWORD (KEEP size_clause)? + | RENAME TO new_tablespace_name + | begin_or_end BACKUP + | datafile_tempfile_clauses + | tablespace_logging_clauses + | tablespace_group_clause + | tablespace_state_clauses + | autoextend_clause + | flashback_mode_clause + | tablespace_retention_clause + ) + ; + +datafile_tempfile_clauses + : ADD (datafile_specification | tempfile_specification) + | DROP (DATAFILE | TEMPFILE) (filename | UNSIGNED_INTEGER) (KEEP size_clause)? + | SHRINK TEMPFILE (filename | UNSIGNED_INTEGER) (KEEP size_clause)? + | RENAME DATAFILE filename (',' filename)* TO filename (',' filename)* + | (DATAFILE | TEMPFILE) (online_or_offline) + ; + +tablespace_logging_clauses + : logging_clause + | NO? FORCE LOGGING + ; + +tablespace_group_clause + : TABLESPACE GROUP (tablespace_group_name | CHAR_STRING) + ; + +tablespace_group_name + : regular_id + ; + +tablespace_state_clauses + : ONLINE + | OFFLINE (NORMAL | TEMPORARY | IMMEDIATE)? + | READ (ONLY | WRITE) + | PERMANENT + | TEMPORARY + ; + +flashback_mode_clause + : FLASHBACK (ON | OFF) + ; + +new_tablespace_name + : tablespace + ; + +create_tablespace + : CREATE (BIGFILE | SMALLFILE)? ( + permanent_tablespace_clause + | temporary_tablespace_clause + | undo_tablespace_clause + ) + ; + +permanent_tablespace_clause + : TABLESPACE id_expression (IF NOT EXISTS)? datafile_specification? ( + MINIMUM EXTENT size_clause + | BLOCKSIZE size_clause + | logging_clause + | FORCE LOGGING + | (ONLINE | OFFLINE) + | ENCRYPTION tablespace_encryption_spec + | DEFAULT //TODO table_compression? storage_clause? + | extent_management_clause + | segment_management_clause + | flashback_mode_clause + )* + ; + +tablespace_encryption_spec + : USING encrypt_algorithm = CHAR_STRING + ; + +logging_clause + : LOGGING + | NOLOGGING + | FILESYSTEM_LIKE_LOGGING + ; + +extent_management_clause + : EXTENT MANAGEMENT LOCAL (AUTOALLOCATE | UNIFORM (SIZE size_clause)?)? + ; + +segment_management_clause + : SEGMENT SPACE_KEYWORD MANAGEMENT (AUTO | MANUAL) + ; + +temporary_tablespace_clause + : TEMPORARY TABLESPACE tablespace_name = id_expression (IF NOT EXISTS)? tempfile_specification? tablespace_group_clause? extent_management_clause? + ; + +undo_tablespace_clause + : UNDO TABLESPACE tablespace_name = id_expression (IF NOT EXISTS)? datafile_specification? extent_management_clause? tablespace_retention_clause? + ; + +tablespace_retention_clause + : RETENTION (GUARANTEE | NOGUARANTEE) + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-TABLESPACE-SET.html +create_tablespace_set + : CREATE TABLESPACE SET tss = id_expression (IN SHARDSPACE ss = id_expression)? ( + USING TEMPLATE '(' (DATAFILE file_specification (',' file_specification)*)? permanent_tablespace_attrs+ ')' + )? + ; + +permanent_tablespace_attrs + : MINIMUM EXTENT size_clause + | BLOCKSIZE numeric K_LETTER? + | logging_clause + | FORCE LOGGING + | tablespace_encryption_clause + | default_tablespace_params + | ONLINE + | OFFLINE + | extent_management_clause + | segment_management_clause + | flashback_mode_clause + | lost_write_protection + ; + +tablespace_encryption_clause + : ENCRYPTION (tablespace_encryption_spec? ENCRYPT | DECRYPT) + ; + +default_tablespace_params + : DEFAULT default_table_compression? default_index_compression? inmmemory_clause? ilm_clause? storage_clause? + ; + +default_table_compression + : TABLE (COMPRESS FOR (OLTP | QUERY low_high | ARCHIVE low_high) | NOCOMPRESS) + ; + +low_high + : LOW + | HIGH + ; + +default_index_compression + : INDEX (COMPRESS ADVANCED low_high | NOCOMPRESS) + ; + +inmmemory_clause + : INMEMORY inmemory_attributes? ( + TEXT ( + column_name (',' column_name)* + | column_name USING policy_name (',' column_name USING policy_name)* + ) + )? + | NO INMEMORY + ; + +// asm_filename is just a charater string. Would need to parse the string +// to find diskgroup... +datafile_specification + : DATAFILE (','? datafile_tempfile_spec) + ; + +tempfile_specification + : TEMPFILE (','? datafile_tempfile_spec) + ; + +datafile_tempfile_spec + : CHAR_STRING? (SIZE size_clause)? REUSE? autoextend_clause? + ; + +redo_log_file_spec + : (filename | '(' filename (',' filename)* ')') (SIZE size_clause)? (BLOCKSIZE size_clause)? REUSE? + ; + +autoextend_clause + : AUTOEXTEND (OFF | ON (NEXT size_clause)? maxsize_clause?) + ; + +maxsize_clause + : MAXSIZE (UNLIMITED | size_clause) + ; + +build_clause + : BUILD (IMMEDIATE | DEFERRED) + ; + +parallel_clause + : NOPARALLEL + | PARALLEL parallel_count = UNSIGNED_INTEGER? + ; + +alter_materialized_view + : ALTER MATERIALIZED VIEW tableview_name ( + physical_attributes_clause + | modify_mv_column_clause + | table_compression + | lob_storage_clause (',' lob_storage_clause)* + | modify_lob_storage_clause (',' modify_lob_storage_clause)* + //TODO | alter_table_partitioning + | parallel_clause + | logging_clause + | allocate_extent_clause + | deallocate_unused_clause + | shrink_clause + | (cache_or_nocache) + )? alter_iot_clauses? (USING INDEX physical_attributes_clause)? alter_mv_option1? ( + enable_or_disable QUERY REWRITE + | COMPILE + | CONSIDER FRESH + )? + ; + +alter_mv_option1 + : alter_mv_refresh + //TODO | MODIFY scoped_table_ref_constraint + ; + +alter_mv_refresh + : REFRESH ( + FAST + | COMPLETE + | FORCE + | ON (DEMAND | COMMIT) + | START WITH expression + | NEXT expression + | WITH PRIMARY KEY + | USING DEFAULT? MASTER ROLLBACK SEGMENT rollback_segment? + | USING (ENFORCED | TRUSTED) CONSTRAINTS + )+ + ; + +rollback_segment + : regular_id + ; + +modify_mv_column_clause + : MODIFY '(' column_name (ENCRYPT encryption_spec | DECRYPT)? ')' + ; + +alter_materialized_view_log + : ALTER MATERIALIZED VIEW LOG FORCE? ON tableview_name ( + physical_attributes_clause + | add_mv_log_column_clause + //TODO | alter_table_partitioning + | parallel_clause + | logging_clause + | allocate_extent_clause + | shrink_clause + | move_mv_log_clause + | cache_or_nocache + )? mv_log_augmentation? mv_log_purge_clause? + ; + +add_mv_log_column_clause + : ADD '(' column_name ')' + ; + +move_mv_log_clause + : MOVE segment_attributes_clause parallel_clause? + ; + +mv_log_augmentation + : ADD ( + (OBJECT ID | PRIMARY KEY | ROWID | SEQUENCE) ('(' column_name (',' column_name)* ')')? + | '(' column_name (',' column_name)* ')' + ) new_values_clause? + ; + +// Should bound this to just date/time expr +datetime_expr + : expression + ; + +// Should bound this to just interval expr +interval_expr + : expression + ; + +synchronous_or_asynchronous + : SYNCHRONOUS + | ASYNCHRONOUS + ; + +including_or_excluding + : INCLUDING + | EXCLUDING + ; + +create_materialized_view_log + : CREATE MATERIALIZED VIEW LOG ON tableview_name ( + ( + physical_attributes_clause + | TABLESPACE tablespace_name = id_expression + | logging_clause + | (CACHE | NOCACHE) + )+ + )? parallel_clause? + // table_partitioning_clauses TODO + ( + WITH (','? ( OBJECT ID | PRIMARY KEY | ROWID | SEQUENCE | COMMIT SCN))* ( + '(' ( ','? regular_id)+ ')' new_values_clause? + )? mv_log_purge_clause? + )* + ; + +new_values_clause + : (INCLUDING | EXCLUDING) NEW VALUES + ; + +mv_log_purge_clause + : PURGE ( + IMMEDIATE (SYNCHRONOUS | ASYNCHRONOUS)? + // |START WITH CLAUSES TODO + ) + ; + +create_materialized_zonemap + : CREATE MATERIALIZED ZONEMAP zonemap_name (LEFT_PAREN column_list RIGHT_PAREN)? zonemap_attributes? zonemap_refresh_clause? ( + (ENABLE | DISABLE) PRUNING + )? (create_zonemap_on_table | create_zonemap_as_subquery) + ; + +alter_materialized_zonemap + : ALTER MATERIALIZED ZONEMAP zonemap_name ( + zonemap_attributes + | zonemap_refresh_clause + | (ENABLE | DISABLE) PRUNING + | COMPILE + | REBUILD + | UNUSABLE + ) + ; + +drop_materialized_zonemap + : DROP MATERIALIZED ZONEMAP zonemap_name + ; + +zonemap_refresh_clause + : REFRESH (FAST | COMPILE | FORCE)? ( + ON (DEMAND | COMMIT | LOAD | DATA MOVEMENT | LOAD DATA MOVEMENT) + )? + ; + +zonemap_attributes + : ( + PCTFREE numeric + | PCTUSED numeric + | SCALE numeric + | TABLESPACE tablespace + | (CACHE | NOCACHE) + )+ + ; + +zonemap_name + : identifier ('.' id_expression)? + ; + +operator_name + : identifier ('.' id_expression)? + ; + +operator_function_name + : identifier ('.' id_expression)* + ; + +create_zonemap_on_table + : ON tableview_name LEFT_PAREN column_list RIGHT_PAREN + ; + +create_zonemap_as_subquery + : AS subquery + ; + +alter_operator + : ALTER OPERATOR operator_name (add_binding_clause | drop_binding_clause | COMPILE) + ; + +drop_operator + : DROP OPERATOR operator_name FORCE? + ; + +create_operator + : CREATE (OR REPLACE)? OPERATOR operator_name BINDING binding_clause (COMMA binding_clause)* ( + SHARING '=' (METADATA | NONE) + )? + ; + +binding_clause + : LEFT_PAREN datatype (COMMA datatype)* RIGHT_PAREN RETURN LEFT_PAREN? datatype RIGHT_PAREN? implementation_clause? using_function_clause + ; + +add_binding_clause + : ADD BINDING binding_clause + ; + +implementation_clause + : ANCILLARY TO primary_operator_list + | operator_context_clause + ; + +primary_operator_list + : primary_operator_item (COMMA primary_operator_item)* + ; + +primary_operator_item + : schema_object_name LEFT_PAREN datatype (COMMA datatype)* RIGHT_PAREN + ; + +operator_context_clause + : WITH INDEX CONTEXT COMMA SCAN CONTEXT implementation_type_name (COMPUTE ANCILLARY DATA)? ( + WITH COLUMN CONTEXT + )? + ; + +using_function_clause + : USING operator_function_name + ; + +drop_binding_clause + : DROP BINDING LEFT_PAREN datatype (COMMA datatype)* RIGHT_PAREN FORCE? + ; + +create_materialized_view + : CREATE MATERIALIZED VIEW tableview_name (OF type_name)? ( + '(' (scoped_table_ref_constraint | mv_column_alias) ( + ',' (scoped_table_ref_constraint | mv_column_alias) + )* ')' + )? ( + ON PREBUILT TABLE ( (WITH | WITHOUT) REDUCED PRECISION)? + | physical_properties? (CACHE | NOCACHE)? parallel_clause? build_clause? + ) ( + USING INDEX ((physical_attributes_clause | TABLESPACE mv_tablespace = id_expression)+)* + | USING NO INDEX + )? create_mv_refresh? (FOR UPDATE)? ((DISABLE | ENABLE) QUERY REWRITE)? AS select_only_statement + ; + +scoped_table_ref_constraint + : SCOPE FOR '(' ref_column_or_attribute = identifier ')' IS (schema_name '.')? scope_table_name_or_c_alias = identifier + ; + +mv_column_alias + : (identifier | quoted_string) (ENCRYPT encryption_spec)? + ; + +create_mv_refresh + : ( + NEVER REFRESH + | REFRESH ( + (FAST | COMPLETE | FORCE) + | ON (DEMAND | COMMIT) + | (START WITH | NEXT) //date goes here TODO + | WITH (PRIMARY KEY | ROWID) + | USING ( + DEFAULT (MASTER | LOCAL)? ROLLBACK SEGMENT + | (MASTER | LOCAL)? ROLLBACK SEGMENT rb_segment = REGULAR_ID + ) + | USING (ENFORCED | TRUSTED) CONSTRAINTS + )+ + ) + ; + +drop_materialized_view + : DROP MATERIALIZED VIEW tableview_name (PRESERVE TABLE)? + ; + +create_context + : CREATE (OR REPLACE)? CONTEXT oracle_namespace USING (schema_object_name '.')? package_name ( + INITIALIZED (EXTERNALLY | GLOBALLY) + | ACCESSED GLOBALLY + )? + ; + +oracle_namespace + : id_expression + ; + +//https://docs.oracle.com/cd/E11882_01/server.112/e41084/statements_5001.htm#SQLRF01201 +create_cluster + : CREATE CLUSTER cluster_name '(' column_name datatype SORT? (',' column_name datatype SORT?)* ')' ( + physical_attributes_clause + | SIZE size_clause + | TABLESPACE tablespace + | INDEX + | (SINGLE TABLE)? HASHKEYS UNSIGNED_INTEGER (HASH IS expression)? + )* parallel_clause? (ROWDEPENDENCIES | NOROWDEPENDENCIES)? (CACHE | NOCACHE)? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-PROFILE.html +create_profile + : CREATE MANDATORY? PROFILE p = id_expression LIMIT (resource_parameters | password_parameters)+ container_clause? + ; + +resource_parameters + : ( + SESSIONS_PER_USER + | CPU_PER_SESSION + | CPU_PER_CALL + | CONNECT_TIME + | IDLE_TIME + | LOGICAL_READS_PER_SESSION + | LOGICAL_READS_PER_CALL + | COMPOSITE_LIMIT + ) (UNSIGNED_INTEGER | UNLIMITED | DEFAULT) + | PRIVATE_SGA (size_clause | UNLIMITED | DEFAULT) + ; + +password_parameters + : ( + FAILED_LOGIN_ATTEMPTS + | PASSWORD_LIFE_TIME + | PASSWORD_REUSE_TIME + | PASSWORD_REUSE_MAX + | PASSWORD_LOCK_TIME + | PASSWORD_GRACE_TIME + | INACTIVE_ACCOUNT_TIME + ) (expression | UNLIMITED | DEFAULT) + | PASSWORD_VERIFY_FUNCTION (function_name | NULL_ | DEFAULT) + | PASSWORD_ROLLOVER_TIME (expression | DEFAULT) + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-LOCKDOWN-PROFILE.html +create_lockdown_profile + : CREATE LOCKDOWN PROFILE id_expression (static_base_profile | dynamic_base_profile)? + ; + +static_base_profile + : FROM bp = id_expression + ; + +dynamic_base_profile + : INCLUDING bp = id_expression + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-OUTLINE.html +create_outline + : CREATE (OR REPLACE)? (PUBLIC | PRIVATE)? OUTLINE (o = id_expression)? ( + FROM (PUBLIC | PRIVATE)? so = id_expression + )? (FOR CATEGORY c = id_expression)? (ON statement)? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-RESTORE-POINT.html +create_restore_point + : CREATE CLEAN? RESTORE POINT rp = id_expression (FOR PLUGGABLE DATABASE pdb = id_expression)? ( + AS OF (TIMESTAMP | SCN) expression + )? (PRESERVE | GUARANTEE FLASHBACK DATABASE)? + ; + +create_role + : CREATE ROLE role_name role_identified_clause? container_clause? + ; + +create_table + : CREATE ( + (GLOBAL | PRIVATE) TEMPORARY + | SHARDED + | DUPLICATED + | IMMUTABLE? BLOCKCHAIN + | IMMUTABLE + )? TABLE (schema_name '.')? table_name (IF NOT EXISTS)? ( + SHARING '=' (METADATA | EXTENDED? DATA | NONE) + )? (relational_table | xmltype_table | object_table) memoptimize_read_write_clause? ( + PARENT tableview_name + )? (USAGE QUEUE)? + ; + +xmltype_table + : OF XMLTYPE ('(' object_properties ')')? (XMLTYPE xmltype_storage)? xmlschema_spec? xmltype_virtual_columns? ( + ON COMMIT (DELETE | PRESERVE) ROWS + )? oid_clause? oid_index_clause? physical_properties? table_properties + ; + +xmltype_virtual_columns + : VIRTUAL COLUMNS '(' column_name AS '(' expression ')' (',' column_name AS '(' expression ')')* ')' + ; + +xmltype_column_properties + : XMLTYPE COLUMN? column_name xmltype_storage? xmlschema_spec? + ; + +xmltype_storage + : STORE AS ( + OBJECT RELATIONAL + | (SECUREFILE | BASICFILE)? (CLOB | BINARY XML) ( + lob_segname ('(' (TABLESPACE tablespace_name = id_expression)? lob_parameters storage_clause?')')? + | '(' (TABLESPACE tablespace_name = id_expression)? lob_parameters storage_clause? ')' + )? + ) + | STORE VARRAYS AS (LOBS | TABLES) + ; + +xmlschema_spec + : (XMLSCHEMA DELIMITED_ID)? (ELEMENT DELIMITED_ID)? (allow_or_disallow NONSCHEMA)? ( + allow_or_disallow ANYSCHEMA + )? + ; + +object_table + : OF (schema_name '.')? object_type object_table_substitution? ( + '(' object_properties (',' object_properties)* ')' + )? (ON COMMIT (DELETE | PRESERVE) ROWS)? oid_clause? oid_index_clause? physical_properties? table_properties + ; + +object_type + : regular_id + ; + +oid_index_clause + : OIDINDEX index_name? '(' (physical_attributes_clause | TABLESPACE tablespace)+ ')' + ; + +oid_clause + : OBJECT IDENTIFIER IS (SYSTEM GENERATED | PRIMARY KEY) + ; + +object_properties + : (column_name | attribute_name) (DEFAULT expression)? ( + inline_constraint (',' inline_constraint)* + | inline_ref_constraint + )? + | out_of_line_constraint + | out_of_line_ref_constraint + | supplemental_logging_props + ; + +object_table_substitution + : NOT? SUBSTITUTABLE AT ALL LEVELS + ; + +relational_table + : ('(' relational_property (',' relational_property)* ')')? immutable_table_clauses blockchain_table_clauses? ( + DEFAULT COLLATION collation_name + )? (ON COMMIT (DROP | PRESERVE) DEFINITION)? (ON COMMIT (DELETE | PRESERVE) ROWS)? physical_properties? table_properties + ; + +immutable_table_clauses + : immutable_table_no_drop_clause? immutable_table_no_delete_clause? + ; + +immutable_table_no_drop_clause + : NO DROP (UNTIL numeric DAYS IDLE)? + ; + +immutable_table_no_delete_clause + : NO DELETE (LOCKED? | UNTIL numeric DAYS AFTER INSERT LOCKED?) + ; + +blockchain_table_clauses + : blockchain_drop_table_clause blockchain_row_retention_clause blockchain_hash_and_data_format_clause + ; + +blockchain_drop_table_clause + : NO DROP (UNTIL numeric DAYS IDLE)? + ; + +blockchain_row_retention_clause + : NO DELETE (LOCKED? | UNTIL numeric DAYS AFTER INSERT LOCKED?) + ; + +blockchain_hash_and_data_format_clause + : HASHING USING SHA2_512_Q VERSION V1_Q + ; + +collation_name + : identifier + ; + +table_properties + : column_properties? read_only_clause? indexing_clause? table_partitioning_clauses? attribute_clustering_clause? ( + CACHE + | NOCACHE + )? result_cache_clause? parallel_clause? monitoring_nomonitoring? (ROWDEPENDENCIES | NOROWDEPENDENCIES)? enable_disable_clause* row_movement_clause? + logical_replication_clause? flashback_archive_clause? physical_properties? (ROW ARCHIVAL)? ( + AS select_only_statement + | FOR EXCHANGE WITH TABLE (schema_name '.')? table_name + )? + ; + +read_only_clause + : READ (ONLY | WRITE) + ; + +indexing_clause + : INDEXING (ON | OFF) + ; + +attribute_clustering_clause + : CLUSTERING clustering_join? cluster_clause (yes_no? ON LOAD)? (yes_no? ON DATA MOVEMENT)? zonemap_clause? + ; + +clustering_join + : (schema_name '.')? table_name clustering_join_item (',' clustering_join_item)* + ; + +clustering_join_item + : JOIN (schema_name '.')? table_name ON '(' equijoin_condition ')' + ; + +equijoin_condition + : expression + ; + +cluster_clause + : BY (LINEAR | INTERLEAVED)? ORDER clustering_columns + ; + +clustering_columns + : clustering_column_group + | '(' clustering_column_group (',' clustering_column_group)* ')' + ; + +clustering_column_group + : '(' column_name (',' column_name)* ')' + ; + +yes_no + : YES + | NO + ; + +zonemap_clause + : WITH MATERIALIZED ZONEMAP ('(' zonemap_name ')')? + | WITHOUT MATERIALIZED ZONEMAP + ; + +logical_replication_clause + : DISABLE LOGICAL REPLICATION + | ENABLE LOGICAL REPLICATION ((ALL | ALLOW NOVALIDATE) KEYS)? + ; + +table_name + : identifier + ; + +relational_property + : column_definition + | virtual_column_definition + | period_definition + | out_of_line_constraint + | out_of_line_ref_constraint + | supplemental_logging_props + ; + +table_partitioning_clauses + : range_partitions + | list_partitions + | hash_partitions + | composite_range_partitions + | composite_list_partitions + | composite_hash_partitions + | reference_partitioning + | system_partitioning + ; + +range_partitions + : PARTITION BY RANGE '(' column_name (',' column_name)* ')' ( + INTERVAL '(' expression ')' (STORE IN '(' tablespace (',' tablespace)* ')')? + )? '(' PARTITION partition_name? range_values_clause table_partition_description ( + ',' PARTITION partition_name? range_values_clause table_partition_description + )* ')' + ; + +list_partitions + : PARTITION BY LIST '(' column_name ')' '(' PARTITION partition_name? list_values_clause table_partition_description ( + ',' PARTITION partition_name? list_values_clause table_partition_description + )* ')' + ; + +hash_partitions + : PARTITION BY HASH '(' column_name (',' column_name)* ')' ( + individual_hash_partitions + | hash_partitions_by_quantity + ) + ; + +individual_hash_partitions + : '(' PARTITION partition_name? partitioning_storage_clause? ( + ',' PARTITION partition_name? partitioning_storage_clause? + )* ')' + ; + +hash_partitions_by_quantity + : PARTITIONS hash_partition_quantity (STORE IN '(' tablespace (',' tablespace)* ')')? ( + table_compression + | key_compression + )? (OVERFLOW STORE IN '(' tablespace (',' tablespace)* ')')? + ; + +hash_partition_quantity + : UNSIGNED_INTEGER + ; + +composite_range_partitions + : PARTITION BY RANGE '(' column_name (',' column_name)* ')' ( + INTERVAL '(' expression ')' (STORE IN '(' tablespace (',' tablespace)* ')')? + )? (subpartition_by_range | subpartition_by_list | subpartition_by_hash) '(' range_partition_desc ( + ',' range_partition_desc + )* ')' + ; + +composite_list_partitions + : PARTITION BY LIST '(' column_name ')' ( + subpartition_by_range + | subpartition_by_list + | subpartition_by_hash + ) '(' list_partition_desc (',' list_partition_desc)* ')' + ; + +composite_hash_partitions + : PARTITION BY HASH '(' (',' column_name)+ ')' ( + subpartition_by_range + | subpartition_by_list + | subpartition_by_hash + ) (individual_hash_partitions | hash_partitions_by_quantity) + ; + +reference_partitioning + : PARTITION BY REFERENCE '(' regular_id ')' ( + '(' reference_partition_desc (',' reference_partition_desc)* ')' + )? + ; + +reference_partition_desc + : PARTITION partition_name? table_partition_description + ; + +system_partitioning + : PARTITION BY SYSTEM ( + PARTITIONS UNSIGNED_INTEGER + | reference_partition_desc (',' reference_partition_desc)* + )? + ; + +range_partition_desc + : PARTITION partition_name? range_values_clause? table_partition_description ( + ( + '(' ( + range_subpartition_desc (',' range_subpartition_desc)* + | list_subpartition_desc (',' list_subpartition_desc)* + | individual_hash_subparts (',' individual_hash_subparts)* + ) ')' + | hash_subparts_by_quantity + ) + )? + ; + +list_partition_desc + : PARTITION partition_name? list_values_clause? table_partition_description ( + ( + '(' ( + range_subpartition_desc (',' range_subpartition_desc)* + | list_subpartition_desc (',' list_subpartition_desc)* + | individual_hash_subparts (',' individual_hash_subparts)* + ) ')' + | hash_subparts_by_quantity + ) + )? + ; + +subpartition_template + : SUBPARTITION TEMPLATE ( + ( + '(' ( + range_subpartition_desc (',' range_subpartition_desc)* + | list_subpartition_desc (',' list_subpartition_desc)* + | individual_hash_subparts (',' individual_hash_subparts)* + ) ')' + | hash_subpartition_quantity + ) + ) + ; + +hash_subpartition_quantity + : UNSIGNED_INTEGER + ; + +subpartition_by_range + : SUBPARTITION BY RANGE '(' column_name (',' column_name)* ')' subpartition_template? + ; + +subpartition_by_list + : SUBPARTITION BY LIST '(' column_name ')' subpartition_template? + ; + +subpartition_by_hash + : SUBPARTITION BY HASH '(' column_name (',' column_name)* ')' ( + SUBPARTITIONS UNSIGNED_INTEGER (STORE IN '(' tablespace (',' tablespace)* ')')? + | subpartition_template + )? + ; + +subpartition_name + : partition_name + ; + +range_subpartition_desc + : SUBPARTITION subpartition_name? range_values_clause partitioning_storage_clause? + ; + +list_subpartition_desc + : SUBPARTITION subpartition_name? list_values_clause partitioning_storage_clause? + ; + +individual_hash_subparts + : SUBPARTITION subpartition_name? partitioning_storage_clause? + ; + +hash_subparts_by_quantity + : SUBPARTITIONS UNSIGNED_INTEGER (STORE IN '(' tablespace (',' tablespace)* ')')? + ; + +range_values_clause + : VALUES LESS THAN '(' range_values_list ')' + ; + +range_values_list + : literal (',' literal)* + | TIMESTAMP literal (',' TIMESTAMP literal)* + ; + +list_values_clause + : VALUES '(' (literal (',' literal)* | TIMESTAMP literal (',' TIMESTAMP literal)* | DEFAULT) ')' + ; + +table_partition_description + : deferred_segment_creation? segment_attributes_clause? (table_compression | key_compression)? ( + OVERFLOW segment_attributes_clause? + )? (lob_storage_clause | varray_col_properties | nested_table_col_properties)* + ; + +partitioning_storage_clause + : ( + TABLESPACE tablespace + | OVERFLOW (TABLESPACE tablespace)? + | table_compression + | key_compression + | lob_partitioning_storage + | VARRAY varray_item STORE AS (BASICFILE | SECUREFILE)? LOB lob_segname + )+ + ; + +lob_partitioning_storage + : LOB '(' lob_item ')' STORE AS (BASICFILE | SECUREFILE)? ( + lob_segname ('(' TABLESPACE tablespace ')')? + | '(' TABLESPACE tablespace ')' + ) + ; + +datatype_null_enable + : column_name datatype SORT? (DEFAULT expression)? ( + ENCRYPT (USING CHAR_STRING)? (IDENTIFIED BY REGULAR_ID)? CHAR_STRING? (NO? SALT)? + )? (NOT NULL_)? (ENABLE | DISABLE)? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/size_clause.html +// Technically, this should only allow 'K' | 'M' | 'G' | 'T' | 'P' | 'E' +// but having issues with examples/numbers01.sql line 11 "sysdate -1m" +size_clause + : UNSIGNED_INTEGER (K_LETTER | M_LETTER | G_LETTER | T_LETTER | P_LETTER | E_LETTER)? + ; + +table_compression + : COMPRESS (BASIC | FOR ( OLTP | (QUERY | ARCHIVE) (LOW | HIGH)?))? + | NOCOMPRESS + ; + +// avoid to match an empty string in +inmemory_table_clause + : inmemory_column_clause+ + | (INMEMORY inmemory_attributes? | NO INMEMORY) inmemory_column_clause* + ; + +// avoid to match an empty string in +inmemory_attributes + : inmemory_memcompress inmemory_priority? inmemory_distribute? inmemory_duplicate? + | inmemory_priority inmemory_distribute? inmemory_duplicate? + | inmemory_distribute inmemory_duplicate? + | inmemory_duplicate + ; + +inmemory_memcompress + : MEMCOMPRESS FOR (DML | (QUERY | CAPACITY) (LOW | HIGH)?) + | NO MEMCOMPRESS + ; + +inmemory_priority + : PRIORITY (NONE | LOW | MEDIUM | HIGH | CRITICAL) + ; + +inmemory_distribute + : DISTRIBUTE (AUTO | BY (ROWID RANGE | PARTITION | SUBPARTITION))? ( + FOR SERVICE (DEFAULT | ALL | identifier | NONE) + )? + ; + +inmemory_duplicate + : DUPLICATE ALL? + | NO DUPLICATE + ; + +inmemory_column_clause + : (INMEMORY inmemory_memcompress? | NO INMEMORY) '(' column_list ')' + ; + +physical_attributes_clause + : ( + PCTFREE pctfree = UNSIGNED_INTEGER + | PCTUSED pctused = UNSIGNED_INTEGER + | INITRANS inittrans = UNSIGNED_INTEGER + | MAXTRANS maxtrans = UNSIGNED_INTEGER + | COMPUTE STATISTICS + | storage_clause + | compute_clauses + )+ + ; + +storage_clause + : STORAGE '(' ( + INITIAL initial_size = size_clause + | NEXT next_size = size_clause + | MINEXTENTS minextents = (UNSIGNED_INTEGER | UNLIMITED) + | MAXEXTENTS minextents = (UNSIGNED_INTEGER | UNLIMITED) + | PCTINCREASE pctincrease = UNSIGNED_INTEGER + | FREELISTS freelists = UNSIGNED_INTEGER + | FREELIST GROUPS freelist_groups = UNSIGNED_INTEGER + | OPTIMAL (size_clause | NULL_) + | BUFFER_POOL (KEEP | RECYCLE | DEFAULT) + | FLASH_CACHE (KEEP | NONE | DEFAULT) + | CELL_FLASH_CACHE (KEEP | NONE | DEFAULT) + | ENCRYPT + )+ ')' + ; + +deferred_segment_creation + : SEGMENT CREATION (IMMEDIATE | DEFERRED) + ; + +segment_attributes_clause + : ( + physical_attributes_clause + | TABLESPACE (tablespace_name = id_expression | SET? identifier) + | table_compression + | logging_clause + )+ + ; + +physical_properties + : deferred_segment_creation? segment_attributes_clause table_compression? inmemory_table_clause? ilm_clause? + | deferred_segment_creation? ( + ORGANIZATION ( + HEAP segment_attributes_clause? heap_org_table_clause + | INDEX segment_attributes_clause? index_org_table_clause + | EXTERNAL external_table_clause + ) + | EXTERNAL PARTITION ATTRIBUTES external_table_clause (REJECT LIMIT)? + ) + | CLUSTER cluster_name '(' column_name (',' column_name)* ')' + ; + +ilm_clause + : ILM ( + ADD POLICY ilm_policy_clause + | (DELETE | ENABLE | DISABLE) POLICY ilm_policy_clause + | DELETE_ALL + | ENABLE_ALL + | DISABLE_ALL + ) + ; + +ilm_policy_clause + : ilm_compression_policy + | ilm_tiering_policy + | ilm_inmemory_policy + ; + +ilm_compression_policy + : table_compression segment_group ilm_after_on + | ((ROW | COLUMN) STORE COMPRESS (ADVANCED | FOR QUERY)) ROW AFTER ilm_time_period OF NO MODIFICATION + ; + +ilm_tiering_policy + : TIER TO tablespace ( + segment_group? (ON function_name)? + | READ ONLY segment_group? ilm_after_on + ) + ; + +ilm_after_on + : AFTER ilm_time_period OF (NO (ACCESS | MODIFICATION) | CREATION) + | ON function_name + ; + +segment_group + : SEGMENT + | GROUP + ; + +ilm_inmemory_policy + : (SET INMEMORY inmemory_attributes? | MODIFY INMEMORY inmemory_memcompress | NO INMEMORY) SEGMENT? ilm_after_on + ; + +ilm_time_period + : numeric (DAY | DAYS | MONTH | MONTHS | YEAR | YEARS) + ; + +heap_org_table_clause + : table_compression? inmemory_table_clause? ilm_clause? + ; + +external_table_clause + : '(' (TYPE access_driver_type)? external_table_data_props ')' ( + REJECT LIMIT (numeric | UNLIMITED) + )? inmemory_table_clause? + ; + +access_driver_type + : ORACLE_LOADER + | ORACLE_DATAPUMP + | ORACLE_HDFS + | ORACLE_HIVE + ; + +external_table_data_props + : (DEFAULT DIRECTORY directory_name)? ( + ACCESS PARAMETERS ( + '(' CHAR_STRING ')' + | '(' opaque_format_spec ')' + | USING CLOB select_only_statement + ) + )? (LOCATION '(' directory_name COLON CHAR_STRING (',' directory_name COLON CHAR_STRING)* ')')? + ; + +opaque_format_spec + : //TODO https://docs.oracle.com/en/database/oracle/oracle-database/21/sutil/oracle-external-tables.html + ; + +row_movement_clause + : (ENABLE | DISABLE)? ROW MOVEMENT + ; + +flashback_archive_clause + : FLASHBACK ARCHIVE fa = id_expression? + | NO FLASHBACK ARCHIVE + ; + +log_grp + : UNSIGNED_INTEGER + ; + +supplemental_table_logging + : ADD SUPPLEMENTAL LOG (supplemental_log_grp_clause | supplemental_id_key_clause) ( + ',' SUPPLEMENTAL LOG (supplemental_log_grp_clause | supplemental_id_key_clause) + )* + | DROP SUPPLEMENTAL LOG (supplemental_id_key_clause | GROUP log_grp) ( + ',' SUPPLEMENTAL LOG (supplemental_id_key_clause | GROUP log_grp) + )* + ; + +supplemental_log_grp_clause + : GROUP log_grp '(' regular_id (NO LOG)? (',' regular_id (NO LOG)?)* ')' ALWAYS? + ; + +supplemental_id_key_clause + : DATA '(' (','? ( ALL | PRIMARY KEY | UNIQUE | FOREIGN KEY))+ ')' COLUMNS + ; + +allocate_extent_clause + : ALLOCATE EXTENT ( + '(' ( + SIZE size_clause + | DATAFILE datafile = CHAR_STRING + | INSTANCE inst_num = UNSIGNED_INTEGER + )+ ')' + )? + ; + +deallocate_unused_clause + : DEALLOCATE UNUSED (KEEP size_clause)? + ; + +shrink_clause + : SHRINK SPACE_KEYWORD COMPACT? CASCADE? + ; + +records_per_block_clause + : (MINIMIZE | NOMINIMIZE)? RECORDS_PER_BLOCK + ; + +upgrade_table_clause + : UPGRADE (NOT? INCLUDING DATA) column_properties + ; + +truncate_table + : TRUNCATE TABLE tableview_name PURGE? + ; + +drop_table + : DROP TABLE tableview_name (IF EXISTS)? PURGE? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-TABLESPACE.html +drop_tablespace + : DROP TABLESPACE ts = id_expression (IF EXISTS)? ((DROP | KEEP) QUOTA?)? including_contents_clause? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-TABLESPACE-SET.html +drop_tablespace_set + : DROP TABLESPACE SET tss = id_expression including_contents_clause? + ; + +including_contents_clause + : INCLUDING CONTENTS ((AND | KEEP) DATAFILES)? (CASCADE CONSTRAINTS)? + ; + +drop_view + : DROP VIEW tableview_name (IF EXISTS)? (CASCADE CONSTRAINT)? + ; + +comment_on_column + : COMMENT ON COLUMN column_name IS quoted_string + ; + +enable_or_disable + : ENABLE + | DISABLE + ; + +allow_or_disallow + : ALLOW + | DISALLOW + ; + +// Synonym DDL Clauses + +alter_synonym + : ALTER PUBLIC? SYNONYM (schema_name '.')? synonym_name ( + EDITIONABLE + | NONEDITIONABLE + | COMPILE + ) + ; + +create_synonym + // Synonym's schema cannot be specified for public synonyms + : CREATE (OR REPLACE)? PUBLIC SYNONYM synonym_name FOR (schema_name PERIOD)? schema_object_name ( + AT_SIGN link_name + )? + | CREATE (OR REPLACE)? SYNONYM (schema_name PERIOD)? synonym_name FOR (schema_name PERIOD)? schema_object_name ( + AT_SIGN (schema_name PERIOD)? link_name + )? + ; + +drop_synonym + : DROP PUBLIC? SYNONYM (schema_name '.')? synonym_name FORCE? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-SPFILE.html +create_spfile + : CREATE SPFILE ('=' spfile_name)? FROM (PFILE ('=' pfile_name)? (AS COPY)? | MEMORY) + ; + +spfile_name + : CHAR_STRING + ; + +pfile_name + : CHAR_STRING + ; + +comment_on_table + : COMMENT ON TABLE tableview_name IS quoted_string + ; + +comment_on_materialized + : COMMENT ON MATERIALIZED VIEW tableview_name IS quoted_string + ; + +alter_analytic_view + : ALTER ANALYTIC VIEW (schema_name '.')? av = id_expression ( + RENAME TO id_expression + | COMPILE + | alter_add_cache_clause + | alter_drop_cache_clause + ) + ; + +alter_add_cache_clause + : ADD CACHE MEASURE GROUP '(' (ALL | measure_list)? ')' LEVELS '(' levels_item ( + ',' levels_item + )* ')' + ; + +levels_item + : ((d = id_expression '.')? h = id_expression '.')? l = id_expression + ; + +measure_list + : id_expression (',' id_expression)* + ; + +alter_drop_cache_clause + : DROP CACHE MEASURE GROUP '(' (ALL | measure_list)? ')' LEVELS '(' levels_item ( + ',' levels_item + )* ')' + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/ALTER-ATTRIBUTE-DIMENSION.html +alter_attribute_dimension + : ALTER ATTRIBUTE DIMENSION (schema_name '.')? ad = id_expression ( + RENAME TO nad = id_expression + | COMPILE + ) + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/ALTER-AUDIT-POLICY-Unified-Auditing.html +alter_audit_policy + : ALTER AUDIT POLICY p = id_expression ADD? ( + privilege_audit_clause? action_audit_clause? role_audit_clause? + | (ONLY TOPLEVEL)? + ) DROP? (privilege_audit_clause? action_audit_clause? role_audit_clause? | (ONLY TOPLEVEL)?) ( + CONDITION (DROP | CHAR_STRING EVALUATE PER (STATEMENT | SESSION | INSTANCE)) + )? + ; + +alter_cluster + : ALTER CLUSTER cluster_name ( + physical_attributes_clause + | SIZE size_clause + | allocate_extent_clause + | deallocate_unused_clause + | cache_or_nocache + )+ parallel_clause? + ; + +drop_analytic_view + : DROP ANALYTIC VIEW (schema_name '.')? av = id_expression + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-ATTRIBUTE-DIMENSION.html +drop_attribute_dimension + : DROP ATTRIBUTE DIMENSION (schema_name '.')? ad = id_expression + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-AUDIT-POLICY-Unified-Auditing.html +drop_audit_policy + : DROP AUDIT POLICY p = id_expression + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-FLASHBACK-ARCHIVE.html +drop_flashback_archive + : DROP FLASHBACK ARCHIVE fa = id_expression + ; + +drop_cluster + : DROP CLUSTER cluster_name (INCLUDING TABLES (CASCADE CONSTRAINTS)?)? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-CONTEXT.html +drop_context + : DROP CONTEXT ns = id_expression + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-DIRECTORY.html +drop_directory + : DROP DIRECTORY dn = id_expression + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-DISKGROUP.html +drop_diskgroup + : DROP DISKGROUP dgn = id_expression ((FORCE? INCLUDING | EXCLUDING) CONTENTS)? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-EDITION.html +drop_edition + : DROP EDITION e = id_expression CASCADE? + ; + +truncate_cluster + : TRUNCATE CLUSTER cluster_name ((DROP | REUSE) STORAGE)? + ; + +cache_or_nocache + : CACHE + | NOCACHE + ; + +database_name + : id_expression + ; + +alter_database + : ALTER database_clause ( + startup_clauses + | recovery_clauses + | database_file_clauses + | logfile_clauses + | controlfile_clauses + | standby_database_clauses + | default_settings_clause + | instance_clauses + | security_clause + | prepare_clause + | drop_mirror_clause + | lost_write_protection + | cdb_fleet_clauses + | property_clauses + | replay_upgrade_clauses + ) + ; + +database_clause + : PLUGGABLE? DATABASE database_name? + ; + +startup_clauses + : MOUNT ((STANDBY | CLONE) DATABASE)? + | OPEN (READ WRITE)? resetlogs_or_noresetlogs? upgrade_or_downgrade? + | OPEN READ ONLY + ; + +resetlogs_or_noresetlogs + : RESETLOGS + | NORESETLOGS + ; + +upgrade_or_downgrade + : UPGRADE + | DOWNGRADE + ; + +recovery_clauses + : general_recovery + | managed_standby_recovery + | begin_or_end BACKUP + ; + +begin_or_end + : BEGIN + | END + ; + +general_recovery + : RECOVER AUTOMATIC? (FROM CHAR_STRING)? ( + (full_database_recovery | partial_database_recovery | LOGFILE CHAR_STRING)? ( + (TEST | ALLOW UNSIGNED_INTEGER CORRUPTION | parallel_clause)+ + )? + | CONTINUE DEFAULT? + | CANCEL + ) + ; + +//Need to come back to +full_database_recovery + : STANDBY? DATABASE ( + ( + UNTIL (CANCEL | TIME CHAR_STRING | CHANGE UNSIGNED_INTEGER | CONSISTENT) + | USING BACKUP CONTROLFILE + | SNAPSHOT TIME CHAR_STRING + )+ + )? + ; + +partial_database_recovery + : TABLESPACE tablespace (',' tablespace)* + | DATAFILE CHAR_STRING + | filenumber (',' CHAR_STRING | filenumber)* + | partial_database_recovery_10g + ; + +partial_database_recovery_10g + : {this.isVersion10()}? STANDBY ( + TABLESPACE tablespace (',' tablespace)* + | DATAFILE CHAR_STRING + | filenumber (',' CHAR_STRING | filenumber)* + ) UNTIL (CONSISTENT WITH)? CONTROLFILE + ; + +managed_standby_recovery + : RECOVER ( + MANAGED STANDBY DATABASE ( + ( + USING CURRENT LOGFILE + | DISCONNECT (FROM SESSION)? + | NODELAY + | UNTIL CHANGE UNSIGNED_INTEGER + | UNTIL CONSISTENT + | parallel_clause + )+ + | FINISH + | CANCEL + )? + | TO LOGICAL STANDBY (db_name | KEEP IDENTITY) + ) + ; + +db_name + : regular_id + ; + +database_file_clauses + : RENAME FILE filename (',' filename)* TO filename + | create_datafile_clause + | alter_datafile_clause + | alter_tempfile_clause + | move_datafile_clause + ; + +create_datafile_clause + : CREATE DATAFILE (filename | filenumber) (',' (filename | filenumber))* ( + AS ( + //TODO (','? file_specification)+ | + NEW + ) + )? + ; + +alter_datafile_clause + : DATAFILE (filename | filenumber) (',' (filename | filenumber))* ( + ONLINE + | OFFLINE (FOR DROP)? + | RESIZE size_clause + | autoextend_clause + | END BACKUP + ) + ; + +alter_tempfile_clause + : TEMPFILE (filename | filenumber) (',' (filename | filenumber))* ( + RESIZE size_clause + | autoextend_clause + | DROP (INCLUDING DATAFILES) + | ONLINE + | OFFLINE + ) + ; + +move_datafile_clause + : MOVE DATAFILE (filename | filenumber) (',' (filename | filenumber))* (TO filename)? REUSE? KEEP? + ; + +logfile_clauses + : (ARCHIVELOG MANUAL? | NOARCHIVELOG) + | NO? FORCE LOGGING + | SET STANDBY NOLOGGING FOR (DATA AVAILABILITY | LOAD PERFORMANCE) + | RENAME FILE filename (',' filename)* TO filename + | CLEAR UNARCHIVED? LOGFILE logfile_descriptor (',' logfile_descriptor)* ( + UNRECOVERABLE DATAFILE + )? + | add_logfile_clauses + | drop_logfile_clauses + | switch_logfile_clause + | supplemental_db_logging + ; + +add_logfile_clauses + : ADD STANDBY? LOGFILE ( + (INSTANCE CHAR_STRING | THREAD UNSIGNED_INTEGER)? group_redo_logfile+ + | MEMBER filename REUSE? (',' filename REUSE?)* TO logfile_descriptor ( + ',' logfile_descriptor + )* + ) + ; + +group_redo_logfile + : (GROUP UNSIGNED_INTEGER)? redo_log_file_spec + ; + +drop_logfile_clauses + : DROP STANDBY? LOGFILE ( + logfile_descriptor (',' logfile_descriptor)* + | MEMBER filename (',' filename)* + ) + ; + +switch_logfile_clause + : SWITCH ALL LOGFILES TO BLOCKSIZE UNSIGNED_INTEGER + ; + +supplemental_db_logging + : add_or_drop SUPPLEMENTAL LOG (DATA | supplemental_id_key_clause | supplemental_plsql_clause) + ; + +add_or_drop + : ADD + | DROP + ; + +supplemental_plsql_clause + : DATA FOR PROCEDURAL REPLICATION + ; + +logfile_descriptor + : GROUP UNSIGNED_INTEGER + | '(' filename (',' filename)* ')' + | filename + ; + +controlfile_clauses + : CREATE (LOGICAL | PHYSICAL)? STANDBY CONTROLFILE AS filename REUSE? + | BACKUP CONTROLFILE TO (filename REUSE? | trace_file_clause) + ; + +trace_file_clause + : TRACE (AS filename REUSE?)? (RESETLOGS | NORESETLOGS)? + ; + +standby_database_clauses + : ( + activate_standby_db_clause + | maximize_standby_db_clause + | register_logfile_clause + | commit_switchover_clause + | start_standby_clause + | stop_standby_clause + | convert_database_clause + ) parallel_clause? + ; + +activate_standby_db_clause + : ACTIVATE (PHYSICAL | LOGICAL)? STANDBY DATABASE (FINISH APPLY)? + ; + +maximize_standby_db_clause + : SET STANDBY DATABASE TO MAXIMIZE (PROTECTION | AVAILABILITY | PERFORMANCE) + ; + +register_logfile_clause + : REGISTER (OR REPLACE)? (PHYSICAL | LOGICAL) LOGFILE //TODO (','? file_specification)+ + //TODO (FOR logminer_session_name)? + ; + +commit_switchover_clause + : (PREPARE | COMMIT) TO SWITCHOVER ( + ( + TO ( + ((PHYSICAL | LOGICAL)? PRIMARY | PHYSICAL? STANDBY) ( + (WITH | WITHOUT)? SESSION SHUTDOWN (WAIT | NOWAIT) + )? + | LOGICAL STANDBY + ) + | LOGICAL STANDBY + ) + | CANCEL + )? + ; + +start_standby_clause + : START LOGICAL STANDBY APPLY IMMEDIATE? NODELAY? ( + NEW PRIMARY regular_id + | INITIAL scn_value = UNSIGNED_INTEGER? + | SKIP_ FAILED TRANSACTION + | FINISH + )? + ; + +stop_standby_clause + : (STOP | ABORT) LOGICAL STANDBY APPLY + ; + +convert_database_clause + : CONVERT TO (PHYSICAL | SNAPSHOT) STANDBY + ; + +default_settings_clause + : DEFAULT EDITION EQUALS_OP edition_name + | SET DEFAULT (BIGFILE | SMALLFILE) TABLESPACE + | DEFAULT TABLESPACE tablespace + | DEFAULT TEMPORARY TABLESPACE (tablespace | tablespace_group_name) + | RENAME GLOBAL_NAME TO database ('.' domain)+ + | ENABLE BLOCK CHANGE TRACKING (USING FILE filename REUSE?)? + | DISABLE BLOCK CHANGE TRACKING + | flashback_mode_clause + | set_time_zone_clause + ; + +set_time_zone_clause + : SET TIMEZONE EQUALS_OP CHAR_STRING + ; + +instance_clauses + : enable_or_disable INSTANCE CHAR_STRING + ; + +security_clause + : GUARD (ALL | STANDBY | NONE) + ; + +domain + : id_expression + ; + +database + : id_expression + ; + +edition_name + : regular_id + ; + +filenumber + : UNSIGNED_INTEGER + ; + +filename + : CHAR_STRING + ; + +prepare_clause + : PREPARE MIRROR COPY c = id_expression (WITH (UNPROTECTED | MIRROR | HIGH) REDUNDANCY)? ( + FOR DATABASE id_expression + )? + ; + +drop_mirror_clause + : DROP MIRROR COPY mn = id_expression + ; + +lost_write_protection + : (ENABLE | DISABLE | REMOVE | SUSPEND) LOST WRITE PROTECTION + ; + +cdb_fleet_clauses + : lead_cdb_clause + | lead_cdb_uri_clause + ; + +lead_cdb_clause + : SET LEAD_CDB '=' (TRUE | FALSE) + ; + +lead_cdb_uri_clause + : SET LEAD_CDB_URI '=' CHAR_STRING + ; + +property_clauses + : PROPERTY (SET | REMOVE) DEFAULT_CREDENTIAL '=' qcn = id_expression + ; + +replay_upgrade_clauses + : UPGRADE SYNC (ON | OFF) + ; + +alter_database_link + : ALTER SHARED? PUBLIC? DATABASE LINK local_link_name ( + CONNECT TO user_object_name IDENTIFIED BY password_value link_authentication? + | link_authentication + ) + ; + +password_value + : id_expression + | numeric + | VALUES CHAR_STRING + ; + +link_authentication + : AUTHENTICATED BY user_object_name IDENTIFIED BY password_value + ; + +// added by zrh +create_database + : CREATE DATABASE database_name ( + USER (SYS | SYSTEM) IDENTIFIED BY password_value + | CONTROLFILE REUSE + | (MAXDATAFILES | MAXINSTANCES) UNSIGNED_INTEGER + | NATIONAL? CHARACTER SET char_set_name + | SET DEFAULT (BIGFILE | SMALLFILE) TABLESPACE + | database_logging_clauses + | tablespace_clauses + | set_time_zone_clause + | (BIGFILE | SMALLFILE)? USER_DATA TABLESPACE tablespace_group_name DATAFILE datafile_tempfile_spec ( + ',' datafile_tempfile_spec + )* + | enable_pluggable_database + )+ + ; + +database_logging_clauses + : LOGFILE database_logging_sub_clause (',' database_logging_sub_clause)* + | (MAXLOGFILES | MAXLOGMEMBERS | MAXLOGHISTORY) UNSIGNED_INTEGER + | ARCHIVELOG + | NOARCHIVELOG + | FORCE LOGGING + ; + +database_logging_sub_clause + : (GROUP UNSIGNED_INTEGER)? file_specification + ; + +tablespace_clauses + : EXTENT MANAGEMENT LOCAL + | SYSAUX? DATAFILE file_specification (',' file_specification)* + | default_tablespace + | default_temp_tablespace + | undo_tablespace + ; + +enable_pluggable_database + : ENABLE PLUGGABLE DATABASE ( + SEED file_name_convert? (SYSTEM tablespace_datafile_clauses)? ( + SYSAUX tablespace_datafile_clauses + )? + )? undo_mode_clause? + ; + +file_name_convert + : FILE_NAME_CONVERT EQUALS_OP ( + '(' filename_convert_sub_clause (',' filename_convert_sub_clause)* ')' + | NONE + ) + ; + +filename_convert_sub_clause + : CHAR_STRING (',' CHAR_STRING)? + ; + +tablespace_datafile_clauses + : DATAFILES (SIZE size_clause | autoextend_clause)+ + ; + +undo_mode_clause + : LOCAL UNDO (ON | OFF) + ; + +default_tablespace + : DEFAULT TABLESPACE tablespace (DATAFILE datafile_tempfile_spec)? extent_management_clause? + ; + +default_temp_tablespace + : (BIGFILE | SMALLFILE)? DEFAULT ( + TEMPORARY TABLESPACE + | LOCAL TEMPORARY TABLESPACE FOR (ALL | LEAF) + ) tablespace (TEMPFILE file_specification (',' file_specification)*)? extent_management_clause? + ; + +undo_tablespace + : (BIGFILE | SMALLFILE)? UNDO TABLESPACE tablespace ( + DATAFILE file_specification (',' file_specification)* + )? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/DROP-DATABASE.html +drop_database + : DROP DATABASE (INCLUDING BACKUPS)? NOPROMPT? + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-DATABASE-LINK.html +create_database_link + : CREATE SHARED? PUBLIC? DATABASE LINK link_name ( + CONNECT TO ( + CURRENT_USER + | user_object_name IDENTIFIED BY password_value link_authentication? + ) + | link_authentication + )* (USING CHAR_STRING)? + ; + +drop_database_link + : DROP PUBLIC? DATABASE LINK link_name + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/ALTER-TABLESPACE-SET.html +alter_tablespace_set + : ALTER TABLESPACE SET tss = id_expression alter_tablespace_attrs + ; + +alter_tablespace_attrs + : default_tablespace_params + | MINIMUM EXTENT size_clause + | RESIZE size_clause + | COALESCE + | SHRINK SPACE_KEYWORD (KEEP size_clause)? + | RENAME TO nts = id_expression + | (BEGIN | END) BACKUP + | datafile_tempfile_clauses + | tablespace_logging_clauses + | tablespace_group_clause + | tablespace_state_clauses + | autoextend_clause + | flashback_mode_clause + | tablespace_retention_clause + | alter_tablespace_encryption + | lost_write_protection + ; + +alter_tablespace_encryption + : ENCRYPTION ( + OFFLINE (tablespace_encryption_spec? ENCRYPT | DECRYPT) + | ONLINE (tablespace_encryption_spec? (ENCRYPT | REKEY) | DECRYPT) ts_file_name_convert? + | FINISH (ENCRYPT | REKEY | DECRYPT) ts_file_name_convert? + ) + ; + +ts_file_name_convert + : FILE_NAME_CONVERT '=' '(' CHAR_STRING ',' CHAR_STRING (',' CHAR_STRING ',' CHAR_STRING)* ')' KEEP? + ; + +alter_role + : ALTER ROLE role_name role_identified_clause container_clause? + ; + +role_identified_clause + : NOT IDENTIFIED + | IDENTIFIED ( + BY identifier + | USING identifier ('.' id_expression)? + | EXTERNALLY + | GLOBALLY (AS CHAR_STRING)? + ) + ; + +alter_table + : ALTER TABLE tableview_name memoptimize_read_write_clause* ( + | alter_table_properties + | constraint_clauses + | column_clauses + | alter_table_partitioning + //TODO | alter_external_table + | move_table_clause + ) ((enable_disable_clause | enable_or_disable (TABLE LOCK | ALL TRIGGERS))+)? + ; + +memoptimize_read_write_clause + : NO? MEMOPTIMIZE FOR (READ | WRITE) + ; + +alter_table_properties + : alter_table_properties_1 + | RENAME TO tableview_name + | shrink_clause + | READ ONLY + | READ WRITE + | REKEY CHAR_STRING + ; + +alter_table_partitioning + : add_table_partition + | drop_table_partition + | merge_table_partition + | modify_table_partition + | split_table_partition + | truncate_table_partition + | exchange_table_partition + | coalesce_table_partition + | alter_interval_partition + ; + +add_table_partition + : ADD ( + range_partition_desc + | list_partition_desc + | PARTITION partition_name? (TABLESPACE tablespace)? key_compression? UNUSABLE? + ) + ; + +drop_table_partition + : DROP (partition_extended_names | subpartition_extended_names) ( + update_index_clauses parallel_clause? + )? + ; + +merge_table_partition + : MERGE PARTITION partition_name AND partition_name INTO PARTITION partition_name + ; + +modify_table_partition + : MODIFY ( + PARTITION partition_name ((ADD | DROP) list_values_clause)? (ADD range_subpartition_desc)? ( + REBUILD? UNUSABLE LOCAL INDEXES + )? shrink_clause? + | range_partitions + ) + ; + +split_table_partition + : SPLIT partition_extended_names ( + AT '(' literal (',' literal)* ')' INTO '(' range_partition_desc (',' range_partition_desc)* ')' + | INTO '(' ( + range_partition_desc (',' range_partition_desc)* + | list_partition_desc (',' list_partition_desc)* + ) ')' + ) (update_global_index_clause | update_index_clauses)? + ; + +truncate_table_partition + : TRUNCATE (partition_extended_names | subpartition_extended_names) ( + (DROP ALL? | REUSE)? STORAGE + )? CASCADE? (update_index_clauses parallel_clause?)? + ; + +exchange_table_partition + : EXCHANGE PARTITION partition_name WITH TABLE tableview_name ((INCLUDING | EXCLUDING) INDEXES)? ( + (WITH | WITHOUT) VALIDATION + )? + ; + +coalesce_table_partition + : COALESCE PARTITION parallel_clause? (allow_or_disallow CLUSTERING)? + ; + +alter_interval_partition + : SET INTERVAL '(' (constant | expression)? ')' + ; + +partition_extended_names + : (PARTITION | PARTITIONS) ( + partition_name (',' partition_name)* + | '(' partition_name (',' partition_name)* ')' + | FOR '('? partition_key_value (',' partition_key_value)* ')'? + ) + ; + +subpartition_extended_names + : (SUBPARTITION | SUBPARTITIONS) ( + partition_name (UPDATE INDEXES)? + | '(' partition_name (',' partition_name)* ')' + | FOR '('? subpartition_key_value (',' subpartition_key_value)* ')'? + ) + ; + +alter_table_properties_1 + : ( + physical_attributes_clause + | logging_clause + | table_compression + | inmemory_table_clause + | supplemental_table_logging + | allocate_extent_clause + | deallocate_unused_clause + | (CACHE | NOCACHE) + | RESULT_CACHE '(' MODE (DEFAULT | FORCE) ')' + | upgrade_table_clause + | records_per_block_clause + | parallel_clause + | row_movement_clause + | flashback_archive_clause + )+ alter_iot_clauses? + ; + +alter_iot_clauses + : index_org_table_clause + | alter_overflow_clause + | alter_mapping_table_clause + | COALESCE + ; + +alter_mapping_table_clause + : MAPPING TABLE (allocate_extent_clause | deallocate_unused_clause) + ; + +alter_overflow_clause + : add_overflow_clause + | OVERFLOW ( + segment_attributes_clause + | allocate_extent_clause + | shrink_clause + | deallocate_unused_clause + )+ + ; + +add_overflow_clause + : ADD OVERFLOW segment_attributes_clause? ( + '(' PARTITION segment_attributes_clause? (',' PARTITION segment_attributes_clause?)* ')' + )? + ; + +update_index_clauses + : update_global_index_clause + | update_all_indexes_clause + ; + +update_global_index_clause + : (UPDATE | INVALIDATE) GLOBAL INDEXES + ; + +update_all_indexes_clause + : UPDATE INDEXES ('(' update_all_indexes_index_clause ')')? + ; + +update_all_indexes_index_clause + : index_name '(' (update_index_partition | update_index_subpartition) ')' ( + ',' update_all_indexes_clause + )* + ; + +update_index_partition + : index_partition_description index_subpartition_clause? (',' update_index_partition)* + ; + +update_index_subpartition + : SUBPARTITION subpartition_name? (TABLESPACE tablespace)? (',' update_index_subpartition)* + ; + +enable_disable_clause + : (ENABLE | DISABLE) (VALIDATE | NOVALIDATE)? ( + UNIQUE '(' column_name (',' column_name)* ')' + | PRIMARY KEY + | CONSTRAINT constraint_name + ) using_index_clause? exceptions_clause? CASCADE? ((KEEP | DROP) INDEX)? + ; + +using_index_clause + : USING INDEX (index_name | '(' create_index ')' | index_attributes)? + ; + +index_attributes + : ( + physical_attributes_clause + | logging_clause + | TABLESPACE (tablespace | DEFAULT) + | key_compression + | sort_or_nosort + | REVERSE + | visible_or_invisible + | parallel_clause + )+ + ; + +sort_or_nosort + : SORT + | NOSORT + ; + +exceptions_clause + : EXCEPTIONS INTO tableview_name + ; + +move_table_clause + : MOVE ONLINE? segment_attributes_clause? table_compression? index_org_table_clause? ( + lob_storage_clause + | varray_col_properties + )* parallel_clause? + ; + +index_org_table_clause + : (mapping_table_clause | PCTTHRESHOLD UNSIGNED_INTEGER | key_compression)+ index_org_overflow_clause? + | index_org_overflow_clause // rule move_table_clause contains an optional block with at least one alternative that can match an empty string + ; + +mapping_table_clause + : MAPPING TABLE + | NOMAPPING + ; + +key_compression + : NOCOMPRESS + | COMPRESS UNSIGNED_INTEGER + ; + +index_org_overflow_clause + : (INCLUDING column_name)? OVERFLOW segment_attributes_clause? + ; + +column_clauses + : add_modify_drop_column_clauses + | rename_column_clause + | modify_collection_retrieval + | modify_lob_storage_clause + ; + +modify_collection_retrieval + : MODIFY NESTED TABLE collection_item RETURN AS (LOCATOR | VALUE) + ; + +collection_item + : tableview_name + ; + +rename_column_clause + : RENAME COLUMN old_column_name TO new_column_name + ; + +old_column_name + : column_name + ; + +new_column_name + : column_name + ; + +add_modify_drop_column_clauses + : (constraint_clauses | add_column_clause | modify_column_clauses | drop_column_clause)+ + ; + +drop_column_clause + : SET UNUSED (COLUMN column_name | ('(' column_name (',' column_name)* ')')) ( + CASCADE CONSTRAINTS + | INVALIDATE + )* + | DROP (COLUMN column_name | '(' column_name (',' column_name)* ')') ( + CASCADE CONSTRAINTS + | INVALIDATE + )* (CHECKPOINT UNSIGNED_INTEGER)? + | DROP (UNUSED COLUMNS | COLUMNS CONTINUE) (CHECKPOINT UNSIGNED_INTEGER) + ; + +modify_column_clauses + : MODIFY ( + '(' modify_col_properties (',' modify_col_properties)* ')' + | '(' modify_col_visibility (',' modify_col_visibility)* ')' + | modify_col_properties + | modify_col_visibility + | modify_col_substitutable + ) + ; + +modify_col_properties + : column_name datatype? (DEFAULT expression)? (ENCRYPT encryption_spec | DECRYPT)? inline_constraint* lob_storage_clause? + //TODO alter_xmlschema_clause + ; + +modify_col_visibility + : column_name (VISIBLE | INVISIBLE) + ; + +modify_col_substitutable + : COLUMN column_name NOT? SUBSTITUTABLE AT ALL LEVELS FORCE? + ; + +add_column_clause + : ADD ( + '(' (column_definition | virtual_column_definition) ( + ',' (column_definition | virtual_column_definition) + )* ')' + | ( column_definition | virtual_column_definition) + ) column_properties? + //TODO (','? out_of_line_part_storage ) + ; + +alter_varray_col_properties + : MODIFY VARRAY varray_item '(' modify_lob_parameters ')' + ; + +varray_col_properties + : VARRAY varray_item ( + substitutable_column_clause? varray_storage_clause + | substitutable_column_clause + ) + ; + +varray_storage_clause + : STORE AS (SECUREFILE | BASICFILE)? LOB ( + lob_segname? '(' lob_storage_parameters ')' + | lob_segname + ) + ; + +lob_segname + : regular_id + ; + +lob_item + : regular_id + | quoted_string + | DELIMITED_ID + ; + +lob_storage_parameters + : TABLESPACE tablespace_name = id_expression + | (lob_parameters storage_clause?) + | storage_clause + ; + +lob_storage_clause + : LOB ( + '(' lob_item (',' lob_item)* ')' STORE AS ( + (SECUREFILE | BASICFILE) + | '(' lob_storage_parameters* ')' + )+ + | '(' lob_item ')' STORE AS ( + (SECUREFILE | BASICFILE) + | lob_segname + | '(' lob_storage_parameters* ')' + )+ + ) + ; + +modify_lob_storage_clause + : MODIFY LOB '(' lob_item ')' '(' modify_lob_parameters ')' + ; + +modify_lob_parameters + : ( + storage_clause + | (PCTVERSION | FREEPOOLS) UNSIGNED_INTEGER + | REBUILD FREEPOOLS + | lob_retention_clause + | lob_deduplicate_clause + | lob_compression_clause + | ENCRYPT encryption_spec + | DECRYPT + | CACHE + | (CACHE | NOCACHE | CACHE READS) logging_clause? + | allocate_extent_clause + | shrink_clause + | deallocate_unused_clause + )+ + ; + +lob_parameters + : ( + (ENABLE | DISABLE) STORAGE IN ROW UNSIGNED_INTEGER? + | CHUNK UNSIGNED_INTEGER + | PCTVERSION UNSIGNED_INTEGER + | FREEPOOLS UNSIGNED_INTEGER + | lob_retention_clause + | lob_deduplicate_clause + | lob_compression_clause + | ENCRYPT encryption_spec + | DECRYPT + | (CACHE | NOCACHE | CACHE READS) logging_clause? + )+ + ; + +lob_deduplicate_clause + : DEDUPLICATE + | KEEP_DUPLICATES + ; + +lob_compression_clause + : NOCOMPRESS + | COMPRESS (HIGH | MEDIUM | LOW)? + ; + +lob_retention_clause + : RETENTION (MAX | MIN UNSIGNED_INTEGER | AUTO | NONE)? + ; + +encryption_spec + : (USING CHAR_STRING)? (IDENTIFIED BY REGULAR_ID)? CHAR_STRING? (NO? SALT)? + ; + +tablespace + : id_expression + ; + +varray_item + : (id_expression '.')? (id_expression '.')? id_expression + ; + +column_properties + : ( + object_type_col_properties + | nested_table_col_properties + | (varray_col_properties | lob_storage_clause) ( + '(' lob_partition_storage (',' lob_partition_storage)* ')' + )? //TODO '(' ( ','? lob_partition_storage)+ ')' + | xmltype_column_properties + )+ + ; + +lob_partition_storage + : LOB ( + '(' lob_item (',' lob_item) ')' STORE AS ( + (SECUREFILE | BASICFILE) + | '(' lob_storage_parameters ')' + )+ + | '(' lob_item ')' STORE AS ( + (SECUREFILE | BASICFILE) + | lob_segname + | '(' lob_storage_parameters ')' + )+ + ) + ; + +period_definition + : {this.isVersion12()}? PERIOD FOR column_name ('(' start_time_column ',' end_time_column ')')? + ; + +start_time_column + : column_name + ; + +end_time_column + : column_name + ; + +column_definition + : column_name ((datatype | regular_id) (COLLATE column_collation_name)?)? SORT? ( + VISIBLE + | INVISIBLE + )? (DEFAULT (ON NULL_)? expression | identity_clause)? (ENCRYPT encryption_spec)? ( + inline_constraint+ + | inline_ref_constraint + )? + ; + +column_collation_name + : id_expression + ; + +identity_clause + : GENERATED (ALWAYS | BY DEFAULT (ON NULL_)?)? AS IDENTITY identity_options_parentheses? + ; + +//https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-TABLE.html#GUID-F9CE0CC3-13AE-4744-A43C-EAC7A71AAAB6 +// NOTE to identity options +// according to the SQL Reference, identity_options be nested in parentheses. +// But statements without parentheses can also be executed successfully on a oracle database. +// See this issue for more details: https://github.com/antlr/grammars-v4/issues/3183 +identity_options_parentheses + : identity_options+ + | '(' identity_options+ ')' + ; + +identity_options + : START WITH (numeric | LIMIT VALUE) + | INCREMENT BY numeric + | MAXVALUE numeric + | NOMAXVALUE + | MINVALUE numeric + | NOMINVALUE + | CYCLE + | NOCYCLE + | CACHE numeric + | NOCACHE + | ORDER + | NOORDER + | NOKEEP + | NOSCALE + ; + +virtual_column_definition + : column_name (datatype COLLATE column_collation_name)? (VISIBLE | INVISIBLE)? autogenerated_sequence_definition? VIRTUAL? + evaluation_edition_clause? (UNUSABLE BEFORE (CURRENT EDITION | EDITION edition_name))? ( + UNUSABLE BEGINNING WITH ((CURRENT | NULL_) EDITION | EDITION edition_name) + )? inline_constraint* + ; + +autogenerated_sequence_definition + : GENERATED (ALWAYS | BY DEFAULT (ON NULL_)?)? AS IDENTITY ( + '(' (sequence_start_clause | sequence_spec)* ')' + )? + ; + +evaluation_edition_clause + : EVALUATE USING ((CURRENT | NULL_) EDITION | EDITION edition_name) + ; + +out_of_line_part_storage + : PARTITION partition_name + ; + +nested_table_col_properties + : NESTED TABLE (nested_item | COLUMN_VALUE) substitutable_column_clause? (LOCAL | GLOBAL)? STORE AS tableview_name ( + '(' ('(' object_properties ')' | physical_properties | column_properties)+ ')' + )? (RETURN AS? (LOCATOR | VALUE))? + ; + +nested_item + : regular_id + ; + +substitutable_column_clause + : ELEMENT? IS OF TYPE? '(' type_name ')' + | NOT? SUBSTITUTABLE AT ALL LEVELS + ; + +partition_name + : regular_id + | DELIMITED_ID + ; + +supplemental_logging_props + : SUPPLEMENTAL LOG (supplemental_log_grp_clause | supplemental_id_key_clause) + ; + +column_or_attribute + : regular_id + ; + +object_type_col_properties + : COLUMN column = regular_id substitutable_column_clause + ; + +constraint_clauses + : ADD '(' (out_of_line_constraint (',' out_of_line_constraint)* | out_of_line_ref_constraint) ')' + | ADD (out_of_line_constraint | out_of_line_ref_constraint) + | MODIFY ( + CONSTRAINT constraint_name + | PRIMARY KEY + | UNIQUE '(' column_name (',' column_name)* ')' + ) constraint_state CASCADE? + | RENAME CONSTRAINT old_constraint_name TO new_constraint_name + | drop_constraint_clause+ + ; + +old_constraint_name + : constraint_name + ; + +new_constraint_name + : constraint_name + ; + +drop_constraint_clause + : DROP ( + PRIMARY KEY + | UNIQUE '(' column_name (',' column_name)* ')' + | CONSTRAINT constraint_name + ) CASCADE? ((KEY | DROP) INDEX)? ONLINE? + ; + +add_constraint + : ADD (CONSTRAINT constraint_name)? add_constraint_clause ( + ',' (CONSTRAINT constraint_name)? add_constraint_clause + )+ + ; + +add_constraint_clause + : primary_key_clause + | foreign_key_clause + | unique_key_clause + | check_constraint + ; + +check_constraint + : CHECK '(' condition ')' DISABLE? + ; + +drop_constraint + : DROP CONSTRAINT constraint_name + ; + +enable_constraint + : ENABLE CONSTRAINT constraint_name + ; + +disable_constraint + : DISABLE CONSTRAINT constraint_name + ; + +foreign_key_clause + : FOREIGN KEY paren_column_list references_clause on_delete_clause? + ; + +references_clause + : REFERENCES tableview_name paren_column_list? (ON DELETE (CASCADE | SET NULL_))? + ; + +on_delete_clause + : ON DELETE (CASCADE | SET NULL_) + ; + +unique_key_clause + : UNIQUE paren_column_list using_index_clause? + ; + +primary_key_clause + : PRIMARY KEY paren_column_list using_index_clause? + ; + +// Anonymous PL/SQL code block + +anonymous_block + : (DECLARE seq_of_declare_specs)? BEGIN seq_of_statements (EXCEPTION exception_handler+)? END + ; + +// Common DDL Clauses + +invoker_rights_clause + : AUTHID (CURRENT_USER | DEFINER) + ; + +call_spec + : java_spec + | c_spec + ; + +// Call Spec Specific Clauses + +java_spec + : LANGUAGE JAVA NAME CHAR_STRING + ; + +c_spec + : (LANGUAGE C_LETTER | EXTERNAL) ( + NAME id_expression LIBRARY identifier + | LIBRARY identifier (NAME id_expression)? + ) c_agent_in_clause? (WITH CONTEXT)? c_parameters_clause? + ; + +c_agent_in_clause + : AGENT IN '(' expressions ')' + ; + +c_parameters_clause + : PARAMETERS '(' c_external_parameter (',' c_external_parameter)* ')' + ; + +c_external_parameter + : CONTEXT + | SELF (TDO | c_property)? + | (parameter_name | RETURN) c_property? (BY REFERENCE)? external_datatype = regular_id? + ; + +c_property + : INDICATOR (STRUCT | TDO)? + | LENGTH + | DURATION + | MAXLEN + | CHARSETID + | CHARSETFORM + ; + +parameter + : parameter_name (IN | OUT | INOUT | NOCOPY)* type_spec? default_value_part? + ; + +default_value_part + : (ASSIGN_OP | DEFAULT) expression + ; + +// Elements Declarations + +seq_of_declare_specs + : declare_spec+ + ; + +declare_spec + : pragma_declaration + | exception_declaration + | procedure_spec + | function_spec + | variable_declaration + | subtype_declaration + | cursor_declaration + | type_declaration + | procedure_body + | function_body + | selection_directive + ; + +// incorporates constant_declaration +variable_declaration + : identifier CONSTANT? type_spec (NOT? NULL_)? default_value_part? ';' + ; + +subtype_declaration + : SUBTYPE identifier IS type_spec (RANGE expression '..' expression)? (NOT NULL_)? ';' + ; + +// cursor_declaration incorportates curscursor_body and cursor_spec + +cursor_declaration + : CURSOR identifier ('(' parameter_spec (',' parameter_spec)* ')')? (RETURN type_spec)? ( + IS select_statement + )? ';' + ; + +parameter_spec + : parameter_name (IN? type_spec)? default_value_part? + ; + +exception_declaration + : identifier EXCEPTION ';' + ; + +pragma_declaration + : PRAGMA ( + SERIALLY_REUSABLE + | AUTONOMOUS_TRANSACTION + | EXCEPTION_INIT '(' exception_name ',' numeric_negative ')' + | INLINE '(' id1 = identifier ',' expression ')' + | RESTRICT_REFERENCES '(' (identifier | DEFAULT) (',' identifier)+ ')' + | DEPRECATE '(' identifier ( ',' CHAR_STRING)? ')' + ) ';' + ; + +// Record Declaration Specific Clauses + +// incorporates ref_cursor_type_definition + +record_type_def + : RECORD '(' field_spec (',' field_spec)* ')' + ; + +field_spec + : column_name type_spec? (NOT NULL_)? default_value_part? + ; + +ref_cursor_type_def + : REF CURSOR (RETURN type_spec)? + ; + +type_declaration + : TYPE identifier IS (table_type_def | varray_type_def | record_type_def | ref_cursor_type_def) ';' + ; + +table_type_def + : TABLE OF type_spec (NOT NULL_)? table_indexed_by_part? + ; + +table_indexed_by_part + : (idx1 = INDEXED | idx2 = INDEX) BY type_spec + ; + +//https://docs.oracle.com/en/database/oracle/oracle-database/21/lnpls/collection-variable.html#GUID-89A1863C-65A1-40CF-9392-86E9FDC21BE9 +varray_type_def + : (VARRAY | VARYING? ARRAY) '(' expression ')' OF type_spec (NOT NULL_)? + ; + +// Statements + +seq_of_statements + : (pragma_declaration* statement (';' | EOF) | label_declaration | selection_directive)+ + ; + +label_declaration + : ltp1 = '<' '<' label_name '>' '>' + ; + +statement + : body + | block + | assignment_statement + | continue_statement + | exit_statement + | goto_statement + | if_statement + | loop_statement + | forall_statement + | null_statement + | raise_statement + | return_statement + | case_statement + | sql_statement + | call_statement + | pipe_row_statement + ; + +swallow_to_semi + : ~';'+ + ; + +assignment_statement + : (general_element | bind_variable) ASSIGN_OP expression + ; + +continue_statement + : CONTINUE label_name? (WHEN condition)? + ; + +exit_statement + : EXIT label_name? (WHEN condition)? + ; + +goto_statement + : GOTO label_name + ; + +if_statement + : IF condition THEN seq_of_statements elsif_part* else_part? END IF + ; + +elsif_part + : ELSIF condition THEN seq_of_statements + ; + +else_part + : ELSE seq_of_statements + ; + +loop_statement + : label_declaration? (WHILE condition | FOR cursor_loop_param)? LOOP seq_of_statements END LOOP label_name? + ; + +// Loop Specific Clause + +cursor_loop_param + : index_name IN REVERSE? lower_bound range_separator = '..' upper_bound + | record_name IN (cursor_name ('(' expressions? ')')? | '(' select_statement ')') + ; + +//https://docs.oracle.com/en/database/oracle/oracle-database/21/lnpls/FORALL-statement.html#GUID-C45B8241-F9DF-4C93-8577-C840A25963DB +forall_statement + : FORALL index_name IN bounds_clause (SAVE EXCEPTIONS)? data_manipulation_language_statements + ; + +bounds_clause + : lower_bound '..' upper_bound + | INDICES OF general_element between_bound? + | VALUES OF index_name + ; + +between_bound + : BETWEEN lower_bound AND upper_bound + ; + +lower_bound + : concatenation + ; + +upper_bound + : concatenation + ; + +null_statement + : NULL_ + ; + +raise_statement + : RAISE exception_name? + ; + +return_statement + : RETURN expression? + ; + +call_statement + : CALL? routine_name function_argument? ('.' routine_name function_argument?)* ( + INTO bind_variable + )? + ; + +pipe_row_statement + : PIPE ROW '(' expression ')' + ; + +selection_directive + : DOLLAR_IF condition DOLLAR_THEN selection_directive_body ( + DOLLAR_ELSIF selection_directive_body + )* (DOLLAR_ELSE selection_directive_body)? DOLLAR_END + ; + +error_directive + : DOLLAR_ERROR concatenation DOLLAR_END + ; + +selection_directive_body + : ( + pragma_declaration? statement ';' + | variable_declaration + | error_directive + | function_body + | procedure_body + )+ + ; + +body + : BEGIN seq_of_statements? (EXCEPTION exception_handler+)? END label_name? + ; + +// Body Specific Clause + +exception_handler + : WHEN exception_name (OR exception_name)* THEN seq_of_statements + ; + +trigger_block + : (DECLARE declare_spec*)? body? + ; + +tps_block + : declare_spec* body + ; + +block + : (DECLARE declare_spec*)? body + ; + +// SQL Statements + +sql_statement + : execute_immediate + | data_manipulation_language_statements + | cursor_manipulation_statements + | transaction_control_statements + | collection_method_call + ; + +execute_immediate + : EXECUTE IMMEDIATE expression ( + into_clause using_clause? + | using_clause dynamic_returning_clause? + | dynamic_returning_clause + )? + ; + +// Execute Immediate Specific Clause + +dynamic_returning_clause + : (RETURNING | RETURN) into_clause + ; + +// DML Statements + +data_manipulation_language_statements + : merge_statement + | lock_table_statement + | select_statement + | update_statement + | delete_statement + | insert_statement + | explain_statement + ; + +// Cursor Manipulation Statements + +cursor_manipulation_statements + : close_statement + | open_statement + | fetch_statement + | open_for_statement + ; + +close_statement + : CLOSE cursor_name + ; + +open_statement + : OPEN cursor_name ('(' expressions? ')')? + ; + +fetch_statement + : FETCH cursor_name ( + it1 = INTO variable_or_collection (',' variable_or_collection)* + | BULK COLLECT INTO variable_or_collection (',' variable_or_collection)* ( + LIMIT (numeric | variable_or_collection) + )? + ) + ; + +variable_or_collection + : variable_name + | collection_expression + ; + +open_for_statement + : OPEN variable_name FOR (select_statement | expression) using_clause? + ; + +// Transaction Control SQL Statements + +transaction_control_statements + : set_transaction_command + | set_constraint_command + | commit_statement + | rollback_statement + | savepoint_statement + ; + +set_transaction_command + : SET TRANSACTION ( + READ (ONLY | WRITE) + | ISOLATION LEVEL (SERIALIZABLE | READ COMMITTED) + | USE ROLLBACK SEGMENT rollback_segment_name + )? (NAME quoted_string)? + ; + +set_constraint_command + : SET (CONSTRAINT | CONSTRAINTS) (ALL | constraint_name (',' constraint_name)*) ( + IMMEDIATE + | DEFERRED + ) + ; + +// https://docs.oracle.com/cd/E18283_01/server.112/e17118/statements_4010.htm#SQLRF01110 +// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/COMMIT.html +commit_statement + : COMMIT WORK? write_clause? ( + COMMENT CHAR_STRING write_clause? + | FORCE (CHAR_STRING (',' numeric)? | CORRUPT_XID CHAR_STRING | CORRUPT_XID_ALL) + )? + ; + +write_clause + : WRITE (WAIT | NOWAIT)? (IMMEDIATE | BATCH)? + ; + +rollback_statement + : ROLLBACK WORK? (TO SAVEPOINT? savepoint_name | FORCE quoted_string)? + ; + +savepoint_statement + : SAVEPOINT savepoint_name + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/19/lnpls/collection-method.html#GUID-7AF1A3C4-D04B-4F91-9D7B-C92C75E3A300 +collection_method_call // collection methods invocation that could be used as a statement + : expression '.' ( + (DELETE | EXTEND) ('(' index += expression (',' index += expression)* ')')? + | TRIM ('(' index += expression ')')? + ) + ; + +// Dml + +/* TODO +//SHOULD BE OVERRIDEN! +compilation_unit + : seq_of_statements* EOF + ; + +//SHOULD BE OVERRIDEN! +seq_of_statements + : select_statement + | update_statement + | delete_statement + | insert_statement + | lock_table_statement + | merge_statement + | explain_statement +// | case_statement[true] + ; +*/ + +explain_statement + : EXPLAIN ( + select_statement + | update_statement + | delete_statement + | insert_statement + | merge_statement + ) + ; + +select_only_statement + : with_clause? subquery + ; + +select_statement + : select_only_statement (for_update_clause | order_by_clause | offset_clause | fetch_clause)* + ; + +// Select Specific Clauses +with_clause + : WITH (function_body | procedure_body)* with_factoring_clause (',' with_factoring_clause)* + | WITH (function_body | procedure_body)+ (with_factoring_clause (',' with_factoring_clause)*)? + ; + +with_factoring_clause + : subquery_factoring_clause + | subav_factoring_clause + ; + +subquery_factoring_clause + : query_name paren_column_list? AS '(' subquery order_by_clause? ')' search_clause? cycle_clause? + ; + +search_clause + : SEARCH (DEPTH | BREADTH) FIRST BY column_name ASC? DESC? (NULLS FIRST)? (NULLS LAST)? ( + ',' column_name ASC? DESC? (NULLS FIRST)? (NULLS LAST)? + )* SET column_name + ; + +cycle_clause + : CYCLE column_list SET column_name TO expression DEFAULT expression + ; + +subav_factoring_clause + : subav_name = id_expression ANALYTIC VIEW AS '(' subav_clause ')' + ; + +subav_clause + : USING subav_name = object_name hierarchies_clause? filter_clauses? add_calcs_clause? + ; + +hierarchies_clause + : HIERARCHIES '(' hier_alias += object_name (',' hier_alias += object_name)* ')' + ; + +filter_clauses + : FILTER FACT '(' filter_clause (',' filter_clause)* ')' + ; + +filter_clause + : (MEASURES | hier_alias = object_name) TO condition + ; + +add_calcs_clause + : ADD MEASURES '(' add_calc_meas_clause (',' add_calc_meas_clause)* ')' + ; + +add_calc_meas_clause + : meas_name = id_expression AS '(' expression ')' + ; + +subquery + : subquery_basic_elements subquery_operation_part* + ; + +subquery_basic_elements + : query_block + | '(' subquery ')' + ; + +subquery_operation_part + : (UNION ALL? | INTERSECT | MINUS) subquery_basic_elements + ; + +query_block + : SELECT (DISTINCT | UNIQUE | ALL)? selected_list into_clause? from_clause? where_clause? ( + hierarchical_query_clause + | group_by_clause + )* model_clause? order_by_clause? offset_clause? fetch_clause? + ; + +selected_list + : '*' + | select_list_elements (',' select_list_elements)* + ; + +from_clause + : FROM table_ref_list + ; + +select_list_elements + : tableview_name '.' ASTERISK + | expression column_alias? + ; + +table_ref_list + : table_ref (',' table_ref)* + ; + +// NOTE to PIVOT clause +// according the SQL reference this should not be possible +// according to he reality it is. Here we probably apply pivot/unpivot onto whole join clause +// eventhough it is not enclosed in parenthesis. See pivot examples 09,10,11 + +table_ref + : table_ref_aux join_clause* (pivot_clause | unpivot_clause)? + ; + +table_ref_aux + : table_ref_aux_internal flashback_query_clause* (/*{isTableAlias()}?*/ table_alias)? + ; + +table_ref_aux_internal + : dml_table_expression_clause (pivot_clause | unpivot_clause)? # table_ref_aux_internal_one + | '(' table_ref subquery_operation_part* ')' (pivot_clause | unpivot_clause)? # table_ref_aux_internal_two + | ONLY '(' dml_table_expression_clause ')' # table_ref_aux_internal_thre + ; + +join_clause + : query_partition_clause? (CROSS | NATURAL)? (INNER | outer_join_type)? JOIN table_ref_aux query_partition_clause? ( + join_on_part + | join_using_part + )* + | (CROSS | OUTER) APPLY table_ref_aux + ; + +join_on_part + : ON condition + ; + +join_using_part + : USING paren_column_list + ; + +outer_join_type + : (FULL | LEFT | RIGHT) OUTER? + ; + +query_partition_clause + : PARTITION BY (('(' (subquery | expressions)? ')') | expressions) + ; + +flashback_query_clause + : VERSIONS (PERIOD_KEYWORD FOR column_name BETWEEN | BETWEEN (SCN | TIMESTAMP)) expression AND expression + | AS OF ((SCN | TIMESTAMP | SNAPSHOT) expression | PERIOD_KEYWORD FOR column_name expression) + ; + +pivot_clause + : PIVOT XML? '(' pivot_element (',' pivot_element)* pivot_for_clause pivot_in_clause ')' + ; + +pivot_element + : (numeric_function | aggregate_function_name '(' expression ')') column_alias? + ; + +pivot_for_clause + : FOR (column_name | paren_column_list) + ; + +pivot_in_clause + : IN '(' (subquery | ANY (',' ANY)* | pivot_in_clause_element (',' pivot_in_clause_element)*) ')' + ; + +pivot_in_clause_element + : pivot_in_clause_elements column_alias? + ; + +pivot_in_clause_elements + : expression + | '(' expressions? ')' + ; + +unpivot_clause + : UNPIVOT ((INCLUDE | EXCLUDE) NULLS)? '(' (column_name | paren_column_list) pivot_for_clause unpivot_in_clause ')' + ; + +unpivot_in_clause + : IN '(' unpivot_in_elements (',' unpivot_in_elements)* ')' + ; + +unpivot_in_elements + : (column_name | paren_column_list) (AS (constant | '(' constant (',' constant)* ')'))? + ; + +hierarchical_query_clause + : CONNECT BY NOCYCLE? condition start_part? + | start_part CONNECT BY NOCYCLE? condition + ; + +start_part + : START WITH condition + ; + +group_by_clause + : GROUP BY group_by_elements (',' group_by_elements)* having_clause? + | having_clause (GROUP BY group_by_elements (',' group_by_elements)*)? + ; + +group_by_elements + : grouping_sets_clause + | rollup_cube_clause + | expression + ; + +rollup_cube_clause + : (ROLLUP | CUBE) '(' grouping_sets_elements (',' grouping_sets_elements)* ')' + ; + +grouping_sets_clause + : GROUPING SETS '(' grouping_sets_elements (',' grouping_sets_elements)* ')' + ; + +grouping_sets_elements + : rollup_cube_clause + | '(' expressions? ')' + | expression + ; + +having_clause + : HAVING condition + ; + +model_clause + : MODEL cell_reference_options* return_rows_clause? reference_model* main_model + ; + +cell_reference_options + : (IGNORE | KEEP) NAV + | UNIQUE (DIMENSION | SINGLE REFERENCE) + ; + +return_rows_clause + : RETURN (UPDATED | ALL) ROWS + ; + +reference_model + : REFERENCE reference_model_name ON '(' subquery ')' model_column_clauses cell_reference_options* + ; + +main_model + : (MAIN main_model_name)? model_column_clauses cell_reference_options* model_rules_clause + ; + +model_column_clauses + : model_column_partition_part? DIMENSION BY model_column_list MEASURES model_column_list + ; + +model_column_partition_part + : PARTITION BY model_column_list + ; + +model_column_list + : '(' model_column (',' model_column)* ')' + ; + +model_column + : (expression | query_block) column_alias? + ; + +model_rules_clause + : model_rules_part? '(' (model_rules_element (',' model_rules_element)*)? ')' + ; + +model_rules_part + : RULES (UPDATE | UPSERT ALL?)? ((AUTOMATIC | SEQUENTIAL) ORDER)? model_iterate_clause? + ; + +model_rules_element + : (UPDATE | UPSERT ALL?)? cell_assignment order_by_clause? '=' expression + ; + +cell_assignment + : model_expression + ; + +model_iterate_clause + : ITERATE '(' expression ')' until_part? + ; + +until_part + : UNTIL '(' condition ')' + ; + +order_by_clause + : ORDER SIBLINGS? BY order_by_elements (',' order_by_elements)* + ; + +order_by_elements + : expression (ASC | DESC)? (NULLS (FIRST | LAST))? + ; + +offset_clause + : OFFSET expression (ROW | ROWS) + ; + +fetch_clause + : FETCH (FIRST | NEXT) (expression PERCENT_KEYWORD?)? (ROW | ROWS) (ONLY | WITH TIES) + ; + +for_update_clause + : FOR UPDATE for_update_of_part? for_update_options? + ; + +for_update_of_part + : OF column_list + ; + +for_update_options + : SKIP_ LOCKED + | NOWAIT + | WAIT expression + ; + +update_statement + : UPDATE general_table_ref update_set_clause where_clause? static_returning_clause? error_logging_clause? + ; + +// Update Specific Clauses + +update_set_clause + : SET ( + column_based_update_set_clause (',' column_based_update_set_clause)* + | VALUE '(' identifier ')' '=' expression + ) + ; + +column_based_update_set_clause + : column_name '=' expression + | paren_column_list '=' subquery + ; + +delete_statement + : DELETE FROM? general_table_ref where_clause? static_returning_clause? error_logging_clause? + ; + +insert_statement + : INSERT (single_table_insert | multi_table_insert) + ; + +// Insert Specific Clauses + +single_table_insert + : insert_into_clause (values_clause static_returning_clause? | select_statement) error_logging_clause? + ; + +multi_table_insert + : (ALL multi_table_element+ | conditional_insert_clause) select_statement + ; + +multi_table_element + : insert_into_clause values_clause? error_logging_clause? + ; + +conditional_insert_clause + : (ALL | FIRST)? conditional_insert_when_part+ conditional_insert_else_part? + ; + +conditional_insert_when_part + : WHEN condition THEN multi_table_element+ + ; + +conditional_insert_else_part + : ELSE multi_table_element+ + ; + +insert_into_clause + : INTO general_table_ref paren_column_list? + ; + +values_clause + : VALUES (REGULAR_ID | '(' expressions ')' | collection_expression) + ; + +merge_statement + : MERGE INTO tableview_name table_alias? USING selected_tableview ON '(' condition ')' ( + merge_update_clause merge_insert_clause? + | merge_insert_clause merge_update_clause? + )? error_logging_clause? + ; + +// Merge Specific Clauses + +merge_update_clause + : WHEN MATCHED THEN UPDATE SET merge_element (',' merge_element)* where_clause? merge_update_delete_part? + ; + +merge_element + : column_name '=' expression + ; + +merge_update_delete_part + : DELETE where_clause + ; + +merge_insert_clause + : WHEN NOT MATCHED THEN INSERT paren_column_list? values_clause where_clause? + ; + +selected_tableview + : (tableview_name | '(' select_statement ')') table_alias? + ; + +lock_table_statement + : LOCK TABLE lock_table_element (',' lock_table_element)* IN lock_mode MODE wait_nowait_part? + ; + +wait_nowait_part + : WAIT expression + | NOWAIT + ; + +// Lock Specific Clauses + +lock_table_element + : tableview_name partition_extension_clause? + ; + +lock_mode + : ROW SHARE + | ROW EXCLUSIVE + | SHARE UPDATE? + | SHARE ROW EXCLUSIVE + | EXCLUSIVE + ; + +// Common DDL Clauses + +general_table_ref + : (dml_table_expression_clause | ONLY '(' dml_table_expression_clause ')') table_alias? + ; + +static_returning_clause + : (RETURNING | RETURN) expressions into_clause + ; + +error_logging_clause + : LOG ERRORS error_logging_into_part? expression? error_logging_reject_part? + ; + +error_logging_into_part + : INTO tableview_name + ; + +error_logging_reject_part + : REJECT LIMIT (UNLIMITED | expression) + ; + +dml_table_expression_clause + : table_collection_expression + | '(' select_statement subquery_restriction_clause? ')' + | tableview_name hierarchies_clause? sample_clause? + | json_table_clause (AS identifier)? + | LATERAL '(' subquery subquery_restriction_clause? ')' + ; + +table_collection_expression + : (TABLE | THE) ('(' subquery ')' | '(' expression ')' outer_join_sign?) + ; + +subquery_restriction_clause + : WITH (READ ONLY | CHECK OPTION (CONSTRAINT constraint_name)?) + ; + +sample_clause + : SAMPLE BLOCK? '(' expression (',' expression)? ')' seed_part? + ; + +seed_part + : SEED '(' expression ')' + ; + +// Expression & Condition + +condition + : expression + | JSON_EQUAL '(' expressions ')' + ; + +expressions + : expression (',' expression)* + ; + +expression + : cursor_expression + | logical_expression + ; + +cursor_expression + : CURSOR '(' subquery ')' + ; + +logical_expression + : unary_logical_expression + | logical_expression AND logical_expression + | logical_expression OR logical_expression + ; + +unary_logical_expression + : NOT? multiset_expression unary_logical_operation? + ; + +unary_logical_operation + : IS NOT? logical_operation + ; + +logical_operation + : ( + NULL_ + | NAN + | PRESENT + | INFINITE + | A_LETTER SET + | EMPTY_ + | OF TYPE? '(' ONLY? type_spec (',' type_spec)* ')' + | JSON (FORMAT JSON)? (STRICT | LAX)? ((WITH | WITHOUT) UNIQUE KEYS)? + ) + ; + +multiset_expression + : relational_expression (multiset_type = (MEMBER | SUBMULTISET) OF? concatenation)? + | multiset_expression MULTISET multiset_operator = (EXCEPT | INTERSECT | UNION) ( + ALL + | DISTINCT + )? relational_expression + ; + +relational_expression + : relational_expression relational_operator relational_expression + | compound_expression + ; + +compound_expression + : concatenation ( + NOT? ( + IN in_elements + | BETWEEN between_elements + | like_type = (LIKE | LIKEC | LIKE2 | LIKE4) concatenation (ESCAPE concatenation)? + ) + )? + ; + +relational_operator + : '=' + | (NOT_EQUAL_OP | '<' '>' | '!' '=' | '^' '=') + | ('<' | '>') '='? + ; + +in_elements + : '(' subquery ')' + | '(' concatenation (',' concatenation)* ')' + | constant + | bind_variable + | general_element + ; + +between_elements + : concatenation AND concatenation + ; + +concatenation + : model_expression (AT (LOCAL | TIME ZONE concatenation) | interval_expression)? ( + ON OVERFLOW (TRUNCATE | ERROR) + )? + | concatenation op = DOUBLE_ASTERISK concatenation + | concatenation op = (ASTERISK | SOLIDUS | MOD) concatenation + | concatenation op = (PLUS_SIGN | MINUS_SIGN) concatenation + | concatenation BAR BAR concatenation + | concatenation COLLATE column_collation_name + ; + +interval_expression + : DAY ('(' concatenation ')')? TO SECOND ('(' concatenation ')')? + | YEAR ('(' concatenation ')')? TO MONTH + | concatenation (SECOND | DAY | MONTH | YEAR)? + ; + +model_expression + : unary_expression ('[' model_expression_element ']')? + ; + +model_expression_element + : (ANY | expression) (',' (ANY | expression))* + | single_column_for_loop (',' single_column_for_loop)* + | multi_column_for_loop + ; + +single_column_for_loop + : FOR column_name ( + IN '(' expressions? ')' + | (LIKE expression)? FROM fromExpr = expression TO toExpr = expression action_type = ( + INCREMENT + | DECREMENT + ) action_expr = expression + ) + ; + +multi_column_for_loop + : FOR paren_column_list IN '(' (subquery | '(' expressions? ')') ')' + ; + +unary_expression + : ('-' | '+') unary_expression + | PRIOR unary_expression + | CONNECT_BY_ROOT unary_expression + | /*TODO {input.LT(1).getText().equalsIgnoreCase("new") && !input.LT(2).getText().equals(".")}?*/ NEW unary_expression + | DISTINCT unary_expression + | ALL unary_expression + | /*TODO{(input.LA(1) == CASE || input.LA(2) == CASE)}?*/ case_statement /*[false]*/ + | unary_expression '.' ( + (COUNT | FIRST | LAST | LIMIT) + | (EXISTS | NEXT | PRIOR) '(' index += expression ')' + ) + | quantified_expression + | standard_function + | atom + | implicit_cursor_expression + ; + +// https://docs.oracle.com/en/database/oracle/oracle-database/21/lnpls/plsql-optimization-and-tuning.html#GUID-DAF46F06-EF3F-4B1A-A518-5238B80C69FA +implicit_cursor_expression + : SQL ( + PERCENT_BULK_ROWCOUNT '(' index = expression ')' + | PERCENT_BULK_EXCEPTIONS ('.' COUNT | '(' expression ')' '.' (ERROR_INDEX | ERROR_CODE)) + ) + ; + +collection_expression + : collation_name '(' expression ')' ('.' general_element_part)* + ; + +case_statement /*TODO [boolean isStatementParameter] +TODO scope { + boolean isStatement; +} +@init {$case_statement::isStatement = $isStatementParameter;}*/ + : searched_case_statement + | simple_case_statement + ; + +// CASE + +simple_case_statement + : label_declaration? ck1 = CASE expression simple_case_when_part+ case_else_part? END CASE? label_name? + ; + +simple_case_when_part + : WHEN expression THEN (/*TODO{$case_statement::isStatement}?*/ seq_of_statements | expression) + ; + +searched_case_statement + : label_declaration? ck1 = CASE searched_case_when_part+ case_else_part? END CASE? label_name? + ; + +searched_case_when_part + : WHEN condition THEN (/*TODO{$case_statement::isStatement}?*/ seq_of_statements | expression) + ; + +case_else_part + : ELSE (/*{$case_statement::isStatement}?*/ seq_of_statements | expression) + ; + +atom + : bind_variable + | constant + | inquiry_directive + | general_element outer_join_sign? + | '(' subquery ')' subquery_operation_part* + | '(' expressions ')' + ; + +quantified_expression + : (SOME | EXISTS | ALL | ANY) ( + '(' select_only_statement ')' + | '(' expression (',' expression)* ')' + ) + ; + +string_function + : SUBSTR '(' expression ',' expression (',' expression)? ')' + | TO_CHAR '(' (table_element | standard_function | expression) (',' quoted_string)? ( + ',' quoted_string + )? ')' + | DECODE '(' expressions ')' + | CHR '(' concatenation USING NCHAR_CS ')' + | NVL '(' expression ',' expression ')' + | TRIM '(' ((LEADING | TRAILING | BOTH)? expression? FROM)? concatenation ')' + | TO_DATE '(' (table_element | standard_function | expression) ( + DEFAULT concatenation ON CONVERSION ERROR + )? (',' quoted_string (',' quoted_string)?)? ')' + ; + +standard_function + : string_function + | numeric_function_wrapper + | json_function + | other_function + ; + +//see as https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/JSON_ARRAY.html#GUID-46CDB3AF-5795-455B-85A8-764528CEC43B +json_function + : JSON_ARRAY '(' json_array_element (',' json_array_element)* json_on_null_clause? json_return_clause? STRICT? ')' + | JSON_ARRAYAGG '(' expression (FORMAT JSON)? order_by_clause? json_on_null_clause? json_return_clause? STRICT? ')' + | JSON_OBJECT '(' json_object_content ')' + | JSON_OBJECTAGG '(' KEY? expression VALUE expression ((NULL_ | ABSENT) ON NULL_)? ( + RETURNING (VARCHAR2 ('(' UNSIGNED_INTEGER ( BYTE | CHAR)? ')')? | CLOB | BLOB) + )? STRICT? (WITH UNIQUE KEYS)? ')' + | JSON_QUERY '(' expression (FORMAT JSON)? ',' CHAR_STRING json_query_returning_clause json_query_wrapper_clause? json_query_on_error_clause? + json_query_on_empty_clause? ')' + | JSON_SERIALIZE '(' CHAR_STRING (RETURNING json_query_return_type)? PRETTY? ASCII? TRUNCATE? ( + (NULL_ | ERROR | EMPTY_ (ARRAY | OBJECT)) ON ERROR + )? ')' + | JSON_TRANSFORM '(' expression ',' json_transform_op (',' json_transform_op)* ')' + | JSON_VALUE '(' expression (FORMAT JSON)? ( + ',' CHAR_STRING? json_value_return_clause? ((ERROR | NULL_ | DEFAULT literal)? ON ERROR)? ( + (ERROR | NULL_ | DEFAULT literal)? ON EMPTY_ + )? json_value_on_mismatch_clause? ')' + )? + ; + +json_object_content + : (json_object_entry (',' json_object_entry)* | '*') json_on_null_clause? json_return_clause? STRICT? ( + WITH UNIQUE KEYS + )? + ; + +json_object_entry + : (KEY? expression (VALUE | IS)? expression | expression ':' expression | identifier) ( + FORMAT JSON + )? + ; + +json_table_clause + : JSON_TABLE '(' expression (FORMAT JSON)? (',' CHAR_STRING)? ((ERROR | NULL_) ON ERROR)? ( + (EMPTY_ | NULL_) ON EMPTY_ + )? json_column_clause? ')' + ; + +json_array_element + : (expression | CHAR_STRING | NULL_ | UNSIGNED_INTEGER | json_function) (FORMAT JSON)? + ; + +json_on_null_clause + : (NULL_ | ABSENT) ON NULL_ + ; + +json_return_clause + : RETURNING (VARCHAR2 ('(' UNSIGNED_INTEGER ( BYTE | CHAR)? ')')? | CLOB | BLOB) + ; + +json_transform_op + : REMOVE CHAR_STRING ((IGNORE | ERROR)? ON MISSING)? + | INSERT CHAR_STRING '=' CHAR_STRING ((REPLACE | IGNORE | ERROR) ON EXISTING)? ( + (NULL_ | IGNORE | ERROR | REMOVE)? ON NULL_ + )? + | REPLACE CHAR_STRING '=' CHAR_STRING ((CREATE | IGNORE | ERROR) ON MISSING)? ( + (NULL_ | IGNORE | ERROR)? ON NULL_ + )? + | expression (FORMAT JSON)? + | APPEND CHAR_STRING '=' CHAR_STRING ((CREATE | IGNORE | ERROR) ON MISSING)? ( + (NULL_ | IGNORE | ERROR)? ON NULL_ + )? + | SET CHAR_STRING '=' expression (FORMAT JSON)? ((REPLACE | IGNORE | ERROR) ON EXISTING)? ( + (CREATE | IGNORE | ERROR) ON MISSING + )? ((NULL_ | IGNORE | ERROR)? ON NULL_)? + ; + +json_column_clause + : COLUMNS '(' json_column_definition (',' json_column_definition)* ')' + ; + +json_column_definition + : expression json_value_return_type? (EXISTS? PATH CHAR_STRING | TRUNCATE (PATH CHAR_STRING)?)? json_query_on_error_clause? + json_query_on_empty_clause? + | expression json_query_return_type? TRUNCATE? FORMAT JSON json_query_wrapper_clause? PATH CHAR_STRING + | NESTED PATH? expression ('[' ASTERISK ']')? json_column_clause + | expression FOR ORDINALITY + ; + +json_query_returning_clause + : (RETURNING json_query_return_type)? PRETTY? ASCII? + ; + +json_query_return_type + : VARCHAR2 ('(' UNSIGNED_INTEGER ( BYTE | CHAR)? ')')? + | CLOB + | BLOB + ; + +json_query_wrapper_clause + : (WITHOUT ARRAY? WRAPPER) + | (WITH (UNCONDITIONAL | CONDITIONAL)? ARRAY? WRAPPER) + ; + +json_query_on_error_clause + : (ERROR | NULL_ | EMPTY_ | EMPTY_ ARRAY | EMPTY_ OBJECT)? ON ERROR + ; + +json_query_on_empty_clause + : (ERROR | NULL_ | EMPTY_ | EMPTY_ ARRAY | EMPTY_ OBJECT)? ON EMPTY_ + ; + +json_value_return_clause + : RETURNING json_value_return_type? ASCII? + ; + +json_value_return_type + : VARCHAR2 ('(' UNSIGNED_INTEGER ( BYTE | CHAR)? ')')? TRUNCATE? + | CLOB + | DATE + | NUMBER ('(' INTEGER (',' INTEGER)? ')')? + | TIMESTAMP (WITH TIMEZONE)? + | SDO_GEOMETRY + | expression (USING CASESENSITIVE MAPPING)? + ; + +json_value_on_mismatch_clause + : (IGNORE | ERROR | NULL_) ON MISMATCH ('(' MISSING DATA | EXTRA DATA | TYPE ERROR ')')? + ; + +literal + : CHAR_STRING + | string_function + | numeric + | MAXVALUE + ; + +numeric_function_wrapper + : numeric_function (single_column_for_loop | multi_column_for_loop)? + ; + +numeric_function + : SUM '(' (DISTINCT | ALL)? expression ')' + | COUNT '(' (ASTERISK | ((DISTINCT | UNIQUE | ALL)? concatenation)?) ')' over_clause? + | ROUND '(' expression (',' UNSIGNED_INTEGER)? ')' + | AVG '(' (DISTINCT | ALL)? expression ')' + | MAX '(' (DISTINCT | ALL)? expression ')' + | LEAST '(' expressions ')' + | GREATEST '(' expressions ')' + ; + +listagg_overflow_clause + : ON OVERFLOW (ERROR | TRUNCATE) CHAR_STRING? ((WITH | WITHOUT) COUNT)? + ; + +other_function + : over_clause_keyword function_argument_analytic over_clause? + | /*TODO stantard_function_enabling_using*/ regular_id function_argument_modeling using_clause? + | COUNT '(' (ASTERISK | (DISTINCT | UNIQUE | ALL)? concatenation) ')' over_clause? + | (CAST | XMLCAST) '(' (MULTISET '(' subquery ')' | concatenation) AS type_spec ( + DEFAULT concatenation ON CONVERSION ERROR + )? (',' quoted_string (',' quoted_string)?)? ')' + | COALESCE '(' table_element (',' (numeric | quoted_string))? ')' + | COLLECT '(' (DISTINCT | UNIQUE)? concatenation collect_order_by_part? ')' + | within_or_over_clause_keyword function_argument within_or_over_part+ + | LISTAGG '(' (ALL | DISTINCT | UNIQUE)? argument (',' string_delimiter)? listagg_overflow_clause? ')' ( + WITHIN GROUP '(' order_by_clause ')' + )? over_clause? + | cursor_name (PERCENT_ISOPEN | PERCENT_FOUND | PERCENT_NOTFOUND | PERCENT_ROWCOUNT) + | DECOMPOSE '(' concatenation (CANONICAL | COMPATIBILITY)? ')' + | EXTRACT '(' regular_id FROM concatenation ')' + | (FIRST_VALUE | LAST_VALUE) function_argument_analytic respect_or_ignore_nulls? over_clause + | (LEAD | LAG) function_argument_analytic respect_or_ignore_nulls? over_clause + | standard_prediction_function_keyword '(' expressions cost_matrix_clause? using_clause? ')' + | (TO_BINARY_DOUBLE | TO_BINARY_FLOAT | TO_NUMBER | TO_TIMESTAMP | TO_TIMESTAMP_TZ) '(' concatenation ( + DEFAULT concatenation ON CONVERSION ERROR + )? (',' quoted_string (',' quoted_string)?)? ')' + | (TO_DSINTERVAL | TO_YMINTERVAL) '(' concatenation (DEFAULT concatenation ON CONVERSION ERROR)? ')' + | TRANSLATE '(' expression (USING (CHAR_CS | NCHAR_CS))? (',' expression)* ')' + | TREAT '(' expression AS REF? type_spec ')' ('.' general_element_part)* + | TRIM '(' ((LEADING | TRAILING | BOTH)? quoted_string? FROM)? concatenation ')' + | VALIDATE_CONVERSION '(' concatenation AS type_spec (',' quoted_string (',' quoted_string)?)? ')' + | XMLAGG '(' expression order_by_clause? ')' ('.' general_element_part)* + | (XMLCOLATTVAL | XMLFOREST) '(' xml_multiuse_expression_element ( + ',' xml_multiuse_expression_element + )* ')' ('.' general_element_part)* + | XMLELEMENT '(' (ENTITYESCAPING | NOENTITYESCAPING)? (NAME | EVALNAME)? expression ( + /*TODO{input.LT(2).getText().equalsIgnoreCase("xmlattributes")}?*/ ',' xml_attributes_clause + )? (',' expression column_alias?)* ')' ('.' general_element_part)* + | XMLEXISTS '(' expression xml_passing_clause? ')' + | XMLPARSE '(' (DOCUMENT | CONTENT) concatenation WELLFORMED? ')' ('.' general_element_part)* + | XMLPI '(' (NAME identifier | EVALNAME concatenation) (',' concatenation)? ')' ( + '.' general_element_part + )* + | XMLQUERY '(' concatenation xml_passing_clause? RETURNING CONTENT (NULL_ ON EMPTY_)? ')' ( + '.' general_element_part + )* + | XMLROOT '(' concatenation (',' xmlroot_param_version_part)? ( + ',' xmlroot_param_standalone_part + )? ')' ('.' general_element_part)* + | XMLSERIALIZE '(' (DOCUMENT | CONTENT) concatenation (AS type_spec)? xmlserialize_param_enconding_part? xmlserialize_param_version_part? + xmlserialize_param_ident_part? ((HIDE | SHOW) DEFAULTS)? ')' ('.' general_element_part)? + | TIME CHAR_STRING + | xmltable + ; + +over_clause_keyword + : AVG + | CORR + | LAG_DIFF + | LAG_DIFF_PERCENT + | MAX + | MEDIAN + | MIN + | NTH_VALUE + | NTILE + | RATIO_TO_REPORT + | ROW_NUMBER + | SUM + | VARIANCE + | REGR_ + | STDDEV + | VAR_ + | VAR_POP + | COVAR_ + | WM_CONCAT + ; + +within_or_over_clause_keyword + : CUME_DIST + | DENSE_RANK + | PERCENT_RANK + | PERCENTILE_CONT + | PERCENTILE_DISC + | RANK + ; + +standard_prediction_function_keyword + : PREDICTION + | PREDICTION_BOUNDS + | PREDICTION_COST + | PREDICTION_DETAILS + | PREDICTION_PROBABILITY + | PREDICTION_SET + ; + +over_clause + : OVER '(' ( + query_partition_clause? (order_by_clause windowing_clause?)? + | HIERARCHY th = id_expression OFFSET numeric (ACROSS ANCESTOR AT LEVEL id_expression)? + ) ')' + ; + +windowing_clause + : windowing_type (BETWEEN windowing_elements AND windowing_elements | windowing_elements) + ; + +windowing_type + : ROWS + | RANGE + ; + +windowing_elements + : UNBOUNDED PRECEDING + | CURRENT ROW + | concatenation (PRECEDING | FOLLOWING) + ; + +using_clause + : USING (ASTERISK | using_element (',' using_element)*) + ; + +using_element + : (IN OUT? | OUT)? select_list_elements + ; + +collect_order_by_part + : ORDER BY concatenation (',' concatenation)* + ; + +within_or_over_part + : WITHIN GROUP '(' order_by_clause ')' + | over_clause + ; + +string_delimiter + : CHAR_STRING + | string_function + | string_delimiter BAR BAR string_delimiter + | '(' string_delimiter ')' + | id_expression + ; + +cost_matrix_clause + : COST ( + MODEL AUTO? + | '(' cost_class_name (',' cost_class_name)* ')' VALUES '(' expressions? ')' + ) + ; + +xml_passing_clause + : PASSING (BY VALUE)? expression column_alias? (',' expression column_alias?)* + ; + +xml_attributes_clause + : XMLATTRIBUTES '(' (ENTITYESCAPING | NOENTITYESCAPING)? (SCHEMACHECK | NOSCHEMACHECK)? xml_multiuse_expression_element ( + ',' xml_multiuse_expression_element + )* ')' + ; + +xml_namespaces_clause + : XMLNAMESPACES '(' (concatenation column_alias)? (',' concatenation column_alias)* xml_general_default_part? ')' + ; + +xml_table_column + : xml_column_name (FOR ORDINALITY | type_spec (PATH concatenation)? xml_general_default_part?) + ; + +xml_general_default_part + : DEFAULT concatenation + ; + +xml_multiuse_expression_element + : expression (AS (id_expression | EVALNAME concatenation))? + ; + +xmlroot_param_version_part + : VERSION (NO VALUE | expression) + ; + +xmlroot_param_standalone_part + : STANDALONE (YES | NO VALUE?) + ; + +xmlserialize_param_enconding_part + : ENCODING concatenation + ; + +xmlserialize_param_version_part + : VERSION concatenation + ; + +xmlserialize_param_ident_part + : NO INDENT + | INDENT (SIZE '=' concatenation)? + ; + +// SqlPlus + +sql_plus_command_no_semicolon + : set_command + ; + +sql_plus_command + : EXIT + | PROMPT_MESSAGE + | SHOW (ERR | ERRORS) + | whenever_command + | timing_command + | start_command + ; + +start_command + : START_CMD id_expression PERIOD (SQL | FILE_EXT) + ; + +whenever_command + : WHENEVER (SQLERROR | OSERROR) ( + EXIT (SUCCESS | FAILURE | WARNING | variable_name | numeric) (COMMIT | ROLLBACK)? + | CONTINUE (COMMIT | ROLLBACK | NONE)? + ) + ; + +set_command + : SET regular_id (CHAR_STRING | ON | OFF | /*EXACT_NUM_LIT*/ numeric | regular_id) + ; + +timing_command + : TIMING (START timing_text = id_expression* | SHOW | STOP)? + ; + +// Common + +partition_extension_clause + : (SUBPARTITION | PARTITION) FOR? '(' expressions? ')' + ; + +column_alias + : AS? (identifier | quoted_string) + | AS + ; + +table_alias + : identifier + | quoted_string + ; + +where_clause + : WHERE (CURRENT OF cursor_name | condition | quantitative_where_stmt) + ; + +quantitative_where_stmt + : expression relational_operator (SOME | ALL | ANY) '(' expression (',' expression)* ')' + ; + +into_clause + : (BULK COLLECT)? INTO (general_element | bind_variable) ( + ',' (general_element | bind_variable) + )* + ; + +// Common Named Elements + +xml_column_name + : identifier + | quoted_string + ; + +cost_class_name + : identifier + ; + +attribute_name + : identifier + ; + +savepoint_name + : identifier + ; + +rollback_segment_name + : identifier + ; + +table_var_name + : identifier + ; + +schema_name + : identifier + ; + +routine_name + : identifier ('.' id_expression)* ('@' link_name)? + ; + +package_name + : identifier + ; + +implementation_type_name + : identifier ('.' id_expression)? + ; + +parameter_name + : identifier + ; + +reference_model_name + : identifier + ; + +main_model_name + : identifier + ; + +container_tableview_name + : identifier ('.' id_expression)? + ; + +aggregate_function_name + : identifier ('.' id_expression)* + ; + +query_name + : identifier + ; + +grantee_name + : id_expression identified_by? + ; + +role_name + : id_expression + | CONNECT + ; + +constraint_name + : identifier ('.' id_expression)* ('@' link_name)? + ; + +label_name + : id_expression + ; + +type_name + : id_expression ('.' id_expression)* + ; + +sequence_name + : id_expression ('.' id_expression)* + ; + +exception_name + : identifier ('.' id_expression)* + ; + +function_name + : identifier ('.' id_expression)? + ; + +procedure_name + : identifier ('.' id_expression)? + ; + +trigger_name + : identifier ('.' id_expression)? + ; + +variable_name + : (INTRODUCER char_set_name)? id_expression ('.' id_expression)? + | bind_variable + ; + +index_name + : identifier ('.' id_expression)? + ; + +cursor_name + : general_element + | bind_variable + ; + +record_name + : identifier + | bind_variable + ; + +collection_name + : identifier ('.' id_expression)? + ; + +link_name + : database ('.' domain)* (AT_SIGN connection_qualifier)? + ; + +local_link_name + : identifier + ; + +connection_qualifier + : identifier + ; + +column_name + : identifier ('.' id_expression)* + ; + +tableview_name + : identifier ('.' id_expression)? ( + AT_SIGN link_name + | /*TODO{!(input.LA(2) == BY)}?*/ partition_extension_clause + )? + | xmltable outer_join_sign? + ; + +xmltable + : XMLTABLE '(' (xml_namespaces_clause ',')? concatenation xml_passing_clause? ( + COLUMNS xml_table_column (',' xml_table_column)* + )? ')' ('.' general_element_part)? + ; + +char_set_name + : id_expression ('.' id_expression)* + ; + +synonym_name + : identifier + ; + +// Represents a valid DB object name in DDL commands which are valid for several DB (or schema) objects. +// For instance, create synonym ... for , or rename to . +// Both are valid for sequences, tables, views, etc. +schema_object_name + : id_expression + ; + +dir_object_name + : id_expression + ; + +user_object_name + : id_expression + ; + +grant_object_name + : tableview_name + | USER user_object_name (',' user_object_name)* + | DIRECTORY dir_object_name + | EDITION schema_object_name + | MINING MODEL schema_object_name + | JAVA (SOURCE | RESOURCE) schema_object_name + | SQL TRANSLATION PROFILE schema_object_name + ; + +column_list + : column_name (',' column_name)* + ; + +paren_column_list + : LEFT_PAREN column_list RIGHT_PAREN + ; + +// PL/SQL Specs + +// NOTE: In reality this applies to aggregate functions only +keep_clause + : KEEP '(' DENSE_RANK (FIRST | LAST) (query_partition_clause | order_by_clause) ')' over_clause? + ; + +function_argument + : '(' (argument (',' argument)*)? ')' keep_clause? + ; + +function_argument_analytic + : '(' (argument respect_or_ignore_nulls? (',' argument respect_or_ignore_nulls?)*)? ')' keep_clause? + ; + +function_argument_modeling + : '(' column_name (',' (numeric | NULL_) (',' (numeric | NULL_))?)? USING ( + tableview_name '.' ASTERISK + | ASTERISK + | expression column_alias? (',' expression column_alias?)* + ) ')' keep_clause? + ; + +respect_or_ignore_nulls + : (RESPECT | IGNORE) NULLS + ; + +argument + : (identifier '=' '>')? expression + ; + +type_spec + : datatype + | REF? type_name (PERCENT_ROWTYPE | PERCENT_TYPE)? + ; + +datatype + : native_datatype_element precision_part? (WITH LOCAL? TIME ZONE | CHARACTER SET char_set_name)? + | INTERVAL (YEAR | DAY) ('(' expression ')')? TO (MONTH | SECOND) ('(' expression ')')? + ; + +precision_part + : '(' (numeric | ASTERISK) (',' (numeric | numeric_negative))? (CHAR | BYTE)? ')' + ; + +native_datatype_element + : BINARY_INTEGER + | PLS_INTEGER + | NATURAL + | BINARY_FLOAT + | BINARY_DOUBLE + | NATURALN + | POSITIVE + | POSITIVEN + | SIGNTYPE + | SIMPLE_INTEGER + | NVARCHAR2 + | DEC + | INTEGER + | INT + | NUMERIC + | SMALLINT + | NUMBER + | DECIMAL + | DOUBLE PRECISION? + | FLOAT + | REAL + | NCHAR + | LONG RAW? + | CHAR + | CHARACTER + | VARCHAR2 + | VARCHAR + | STRING + | RAW + | BOOLEAN + | DATE + | ROWID + | UROWID + | YEAR + | MONTH + | DAY + | HOUR + | MINUTE + | SECOND + | TIMEZONE_HOUR + | TIMEZONE_MINUTE + | TIMEZONE_REGION + | TIMEZONE_ABBR + | TIMESTAMP + | TIMESTAMP_UNCONSTRAINED + | TIMESTAMP_TZ_UNCONSTRAINED + | TIMESTAMP_LTZ_UNCONSTRAINED + | YMINTERVAL_UNCONSTRAINED + | DSINTERVAL_UNCONSTRAINED + | BFILE + | BLOB + | CLOB + | NCLOB + | MLSLABEL + | XMLTYPE + | SYSXMLTYPE + ; + +bind_variable + : (BINDVAR | ':' UNSIGNED_INTEGER) + // Pro*C/C++ indicator variables + (INDICATOR? (BINDVAR | ':' UNSIGNED_INTEGER))? ('.' general_element_part)* + ; + +general_element + : general_element_part + | general_element ('.' general_element_part)+ + | '(' general_element ')' + ; + +general_element_part + : (INTRODUCER char_set_name)? id_expression ('@' link_name)? function_argument* + ; + +table_element + : (INTRODUCER char_set_name)? id_expression ('.' id_expression)* + ; + +object_privilege + : ALL PRIVILEGES? + | ALTER + | DEBUG + | DELETE + | EXECUTE + | FLASHBACK ARCHIVE + | INDEX + | INHERIT PRIVILEGES + | INSERT + | KEEP SEQUENCE + | MERGE VIEW + | ON COMMIT REFRESH + | QUERY REWRITE + | READ + | REFERENCES + | SELECT + | TRANSLATE SQL + | UNDER + | UPDATE + | USE + | WRITE + ; + +//Ordered by type rather than alphabetically +system_privilege + : ALL PRIVILEGES + | ADVISOR + | ADMINISTER ANY? SQL TUNING SET + | (ALTER | CREATE | DROP) ANY SQL PROFILE + | ADMINISTER SQL MANAGEMENT OBJECT + | CREATE ANY? CLUSTER + | (ALTER | DROP) ANY CLUSTER + | (CREATE | DROP) ANY CONTEXT + | EXEMPT REDACTION POLICY + | ALTER DATABASE + | (ALTER | CREATE) PUBLIC? DATABASE LINK + | DROP PUBLIC DATABASE LINK + | DEBUG CONNECT SESSION + | DEBUG ANY PROCEDURE + | ANALYZE ANY DICTIONARY + | CREATE ANY? DIMENSION + | (ALTER | DROP) ANY DIMENSION + | (CREATE | DROP) ANY DIRECTORY + | (CREATE | DROP) ANY EDITION + | FLASHBACK (ARCHIVE ADMINISTER | ANY TABLE) + | (ALTER | CREATE | DROP) ANY INDEX + | CREATE ANY? INDEXTYPE + | (ALTER | DROP | EXECUTE) ANY INDEXTYPE + | CREATE (ANY | EXTERNAL)? JOB + | EXECUTE ANY (CLASS | PROGRAM) + | MANAGE SCHEDULER + | ADMINISTER KEY MANAGEMENT + | CREATE ANY? LIBRARY + | (ALTER | DROP | EXECUTE) ANY LIBRARY + | LOGMINING + | CREATE ANY? MATERIALIZED VIEW + | (ALTER | DROP) ANY MATERIALIZED VIEW + | GLOBAL? QUERY REWRITE + | ON COMMIT REFRESH + | CREATE ANY? MINING MODEL + | (ALTER | DROP | SELECT | COMMENT) ANY MINING MODEL + | CREATE ANY? CUBE + | (ALTER | DROP | SELECT | UPDATE) ANY CUBE + | CREATE ANY? MEASURE FOLDER + | (DELETE | DROP | INSERT) ANY MEASURE FOLDER + | CREATE ANY? CUBE DIMENSION + | (ALTER | DELETE | DROP | INSERT | SELECT | UPDATE) ANY CUBE DIMENSION + | CREATE ANY? CUBE BUILD PROCESS + | (DROP | UPDATE) ANY CUBE BUILD PROCESS + | CREATE ANY? OPERATOR + | (ALTER | DROP | EXECUTE) ANY OPERATOR + | (CREATE | ALTER | DROP) ANY OUTLINE + | CREATE PLUGGABLE DATABASE + | SET CONTAINER + | CREATE ANY? PROCEDURE + | (ALTER | DROP | EXECUTE) ANY PROCEDURE + | (CREATE | ALTER | DROP) PROFILE + | CREATE ROLE + | (ALTER | DROP | GRANT) ANY ROLE + | (CREATE | ALTER | DROP) ROLLBACK SEGMENT + | CREATE ANY? SEQUENCE + | (ALTER | DROP | SELECT) ANY SEQUENCE + | (ALTER | CREATE | RESTRICTED) SESSION + | ALTER RESOURCE COST + | CREATE ANY? SQL TRANSLATION PROFILE + | (ALTER | DROP | USE) ANY SQL TRANSLATION PROFILE + | TRANSLATE ANY SQL + | CREATE ANY? SYNONYM + | DROP ANY SYNONYM + | (CREATE | DROP) PUBLIC SYNONYM + | CREATE ANY? TABLE + | (ALTER | BACKUP | COMMENT | DELETE | DROP | INSERT | LOCK | READ | SELECT | UPDATE) ANY TABLE + | (CREATE | ALTER | DROP | MANAGE | UNLIMITED) TABLESPACE + | CREATE ANY? TRIGGER + | (ALTER | DROP) ANY TRIGGER + | ADMINISTER DATABASE TRIGGER + | CREATE ANY? TYPE + | (ALTER | DROP | EXECUTE | UNDER) ANY TYPE + | (CREATE | ALTER | DROP) USER + | CREATE ANY? VIEW + | (DROP | UNDER | MERGE) ANY VIEW + | (ANALYZE | AUDIT) ANY + | BECOME USER + | CHANGE NOTIFICATION + | EXEMPT ACCESS POLICY + | FORCE ANY? TRANSACTION + | GRANT ANY OBJECT? PRIVILEGE + | INHERIT ANY PRIVILEGES + | KEEP DATE TIME + | KEEP SYSGUID + | PURGE DBA_RECYCLEBIN + | RESUMABLE + | SELECT ANY (DICTIONARY | TRANSACTION) + | SYSBACKUP + | SYSDBA + | SYSDG + | SYSKM + | SYSOPER + ; + +// $> + +// $ executeMulti(SimpleSqlStatement statement, Connection connection, + boolean limitRowSize, Integer offset, Integer count, + Integer resultSetId, ExecutionContext executionContext) + throws SQLException { + DMExecutableSql parsed = parseExecutableSql(statement.getSql()); + if (!parsed.isExplain()) { + return super.executeMulti(statement, connection, limitRowSize, offset, count, resultSetId, executionContext); + } + + markStatementAsExplain(statement); + Chat2DBContext.guardStatement(parsed.originalSql()); + long startedAtEpochMs = System.currentTimeMillis(); + long executeStartedNanos = System.nanoTime(); + String plan = explainClient.getExplainInfo(connection, parsed.executableSql()); + long executeDurationNanos = ExecutionTiming.elapsedNanos(executeStartedNanos); + return List.of(buildExplainResponse(parsed, plan, executionContext, startedAtEpochMs, executeDurationNanos)); + } + + @Override + protected List executeMultiStreaming(SimpleSqlStatement statement, Connection connection, + boolean limitRowSize, Integer offset, Integer count, + Integer resultSetId, ISqlExecutionResultConsumer consumer, + ISqlExecutionStatementListener statementListener, + ISqlExecutionCancellation cancellation, SqlTypeEnum sqlType, + String originalSql, int pageNo, int pageSize, + AtomicInteger streamResultSequence, int statementSequence, + ExecutionContext executionContext) + throws SQLException { + DMExecutableSql parsed = parseExecutableSql(statement.getSql()); + if (!parsed.isExplain()) { + return super.executeMultiStreaming(statement, connection, limitRowSize, offset, count, resultSetId, + consumer, statementListener, cancellation, sqlType, originalSql, pageNo, pageSize, + streamResultSequence, statementSequence, executionContext); + } + + markStatementAsExplain(statement); + checkCanceled(cancellation); + Chat2DBContext.guardStatement(parsed.originalSql()); + long startedAtEpochMs = System.currentTimeMillis(); + long executeStartedNanos = System.nanoTime(); + String plan = explainClient.getExplainInfo(connection, parsed.executableSql()); + long executeDurationNanos = ExecutionTiming.elapsedNanos(executeStartedNanos); + checkCanceled(cancellation); + + ExecuteResponse response = buildExplainResponse( + parsed, plan, executionContext, startedAtEpochMs, executeDurationNanos); + response.setPageNo(pageNo); + response.setPageSize(response.getDataList().size()); + response.setHasNextPage(Boolean.FALSE); + response.setFuzzyTotal(Integer.toString(response.getDataList().size())); + publishMaterializedQueryResult(response, consumer, streamResultSequence, statementSequence, pageNo, pageSize); + return List.of(response); + } + + private void markStatementAsExplain(SimpleSqlStatement statement) { + // Outer execute/executeStreaming overwrite response.sqlType from statement.sqlType + // after setExplain() rewrote the SQL text without updating the type. + statement.setSqlType(ai.chat2db.community.domain.api.enums.parser.SqlTypeEnum.EXPLAIN.name()); + } + + private DMExecutableSql parseExecutableSql(String sql) throws SQLException { + try { + return parser.parseExecutableSql(sql); + } catch (IllegalArgumentException e) { + // Keep EXPLAIN off the default JDBC path when the DM grammar cannot classify it. + throw new SQLException(e.getMessage(), e); + } + } + + private ExecuteResponse buildExplainResponse(DMExecutableSql parsed, String plan, + ExecutionContext executionContext, + long startedAtEpochMs, long executeDurationNanos) { + List

headerList = new ArrayList<>(); + headerList.add(Header.builder() + .name("Execution Plan") + .dataType("VARCHAR") + .build()); + + List row = new ArrayList<>(); + row.add(ResultCell.of(plan)); + + List> dataList = new ArrayList<>(); + dataList.add(row); + + return ExecuteResponse.builder() + .success(Boolean.TRUE) + .sql(parsed.originalSql()) + .originalSql(parsed.originalSql()) + .sqlType(ai.chat2db.community.domain.api.enums.parser.SqlTypeEnum.EXPLAIN.name()) + .headerList(headerList) + .dataList(dataList) + .hasNextPage(Boolean.FALSE) + .executionContext(executionContext) + .executionMetrics(ExecutionTiming.complete( + ExecutionTiming.started(startedAtEpochMs), executeDurationNanos, 0L, 1)) + .build(); + } + + private void checkCanceled(ISqlExecutionCancellation cancellation) throws SQLException { + if (cancellation != null && cancellation.isCanceled()) { + throw new SQLException("SQL execution canceled"); + } + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMExplainClient.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMExplainClient.java new file mode 100644 index 0000000000..d78139a3e5 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMExplainClient.java @@ -0,0 +1,76 @@ +package ai.chat2db.plugin.dm; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.SQLException; + +final class DMExplainClient { + + String getExplainInfo(Connection connection, String sql) throws SQLException { + try { + Object dmConnection = resolveDmConnection(connection); + ClassLoader driverClassLoader = classLoaderOf(dmConnection); + Class dmConnectionClass = Class.forName("dm.jdbc.driver.DmdbConnection", true, driverClassLoader); + Method method = dmConnectionClass.getMethod("getExplainInfo", String.class); + return (String) method.invoke(dmConnection, sql); + } catch (InvocationTargetException e) { + Throwable cause = e.getTargetException(); + if (cause instanceof SQLException sqlException) { + throw sqlException; + } + throw new SQLException("DM getExplainInfo failed", cause); + } catch (ReflectiveOperationException e) { + throw new SQLException("DM JDBC driver does not support getExplainInfo", e); + } + } + + private Object resolveDmConnection(Connection connection) throws SQLException, ClassNotFoundException { + Connection unwrapped = safeUnwrapConnection(connection); + ClassLoader driverClassLoader = classLoaderOf(unwrapped); + Class dmConnectionClass = Class.forName("dm.jdbc.driver.DmdbConnection", true, driverClassLoader); + + if (dmConnectionClass.isInstance(connection)) { + return connection; + } + if (dmConnectionClass.isInstance(unwrapped)) { + return unwrapped; + } + + Object fromOuter = safeUnwrapAs(connection, dmConnectionClass); + if (fromOuter != null) { + return fromOuter; + } + Object fromInner = safeUnwrapAs(unwrapped, dmConnectionClass); + if (fromInner != null) { + return fromInner; + } + throw new SQLException("DM JDBC driver does not support getExplainInfo"); + } + + private Connection safeUnwrapConnection(Connection connection) { + try { + Connection unwrapped = connection.unwrap(Connection.class); + return unwrapped == null ? connection : unwrapped; + } catch (SQLException e) { + return connection; + } + } + + private Object safeUnwrapAs(Connection connection, Class dmConnectionClass) { + try { + return connection.unwrap(dmConnectionClass.asSubclass(Connection.class)); + } catch (SQLException e) { + return null; + } + } + + private ClassLoader classLoaderOf(Object target) { + ClassLoader loader = target.getClass().getClassLoader(); + if (loader != null) { + return loader; + } + ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); + return contextLoader == null ? DMExplainClient.class.getClassLoader() : contextLoader; + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java index 7bac616c98..18a73bf648 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java @@ -6,6 +6,7 @@ import ai.chat2db.plugin.dm.enums.type.DMDefaultValueEnum; import ai.chat2db.plugin.dm.enums.type.DMIndexTypeEnum; import ai.chat2db.plugin.dm.value.DMValueProcessor; +import ai.chat2db.spi.ICommandExecutor; import ai.chat2db.spi.IDbMetaData; import ai.chat2db.spi.ISQLIdentifierProcessor; import ai.chat2db.spi.ISqlBuilder; @@ -297,6 +298,11 @@ public ISqlBuilder getSqlBuilder() { return new DMSqlBuilder(); } + @Override + public ICommandExecutor getCommandExecutor() { + return DMCommandExecutor.INSTANCE; + } + @Override public TableMeta getTableMeta(String databaseName, String schemaName, String tableName) { return TableMeta.builder() diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java index 3df748d0e0..3894b9bb19 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java @@ -1,5 +1,7 @@ package ai.chat2db.plugin.dm.builder; +import ai.chat2db.plugin.dm.parser.DMExecutableSql; +import ai.chat2db.plugin.dm.parser.DMSqlParser; import ai.chat2db.spi.constant.SQLConstants; import ai.chat2db.plugin.dm.identifier.DMIdentifierProcessor; @@ -32,6 +34,8 @@ import static ai.chat2db.plugin.dm.constant.DMSqlBuilderConstants.*; public class DMSqlBuilder extends DefaultSqlBuilder { + private final DMSqlParser parser = new DMSqlParser(); + @Override public String quoteIdentifier(String identifier) { return DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(identifier); @@ -292,6 +296,12 @@ public String buildCreateSchema(Schema schema) { return sqlBuilder.toString(); } + @Override + public String buildExplain(String sql) { + DMExecutableSql executableSql = parser.parseExecutableSql(sql); + return executableSql.isExplain() ? sql : SQLConstants.EXPLAIN_SQL_PREFIX + sql; + } + private static String quoteStringLiteral(String value) { return DMIdentifierProcessor.INSTANCE.quoteStringLiteral(value); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMDialect.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMDialect.java new file mode 100644 index 0000000000..db5f7cf57d --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMDialect.java @@ -0,0 +1,83 @@ +package ai.chat2db.plugin.dm.parser; + +import ai.chat2db.spi.parser.dialect.AbstractSQLDialect; +import ai.chat2db.plugin.dm.parser.base.DMLexer; +import ai.chat2db.plugin.dm.parser.rule.manager.DMRuleManager; +import ai.chat2db.spi.IRuleManager; + +import java.util.Set; + +public class DMDialect extends AbstractSQLDialect { + private static final IRuleManager ORACLE_RULE_MANAGER = new DMRuleManager(); + + private static final Set ORACLE_COMMENT_TOKENS = Set.of(DMLexer.SINGLE_LINE_COMMENT, DMLexer.MULTI_LINE_COMMENT); + + private static final Set SQL_START_KEYWORDS = Set.of( + "SELECT", "INSERT", "UPDATE", "DELETE", "ALTER", "DROP", + "ADMINISTER", "ANALYZE", "BEGIN", "COMMIT", "CREATE", + "DECLARE", "EXECUTE", "GRANT", "RENAME", "REVOKE", + "SAVEPOINT", "TRUNCATE", "LOCK", "NOAUDIT", "PURGE", + "EXPLAIN", "FLASHBACK", "ASSOCIATE", "AUDIT", "MERGE", + "ROLLBACK", "DISASSOCIATE", ";", "COMMENT", + "WITH", "EXIT", "PROMPT_MESSAGE", "SHOW", "WHENEVER", + "TIMING", "START_CMD" + ); + + @Override + public Set getCommentTokens() { + return ORACLE_COMMENT_TOKENS; + } + + @Override + public boolean isComment(int tokenType) { + return getCommentTokens().contains(tokenType); + } + + @Override + public Set getSqlStartKeywords() { + return SQL_START_KEYWORDS; + } + + + @Override + public Set getBlockHeaderPrefixes() { + Set blockHeaderPrefixes = super.getBlockHeaderPrefixes(); + blockHeaderPrefixes.add("COMPOUND"); + return blockHeaderPrefixes; + } + + @Override + public Set getIndependentBlockHeaders() { + Set independentBlockHeaders = super.getIndependentBlockHeaders(); + independentBlockHeaders.add("DECLARE"); + return independentBlockHeaders; + } + + @Override + public Set getDependentBlockHeaders() { + Set dependentBlockHeaders = super.getDependentBlockHeaders(); + dependentBlockHeaders.add("PACKAGE"); + return dependentBlockHeaders; + } + + @Override + public Set getInnerBlockPrefix() { + Set innerBlockPrefix = super.getInnerBlockPrefix(); + innerBlockPrefix.add("AS"); + innerBlockPrefix.add("IS"); + return innerBlockPrefix; + } + + @Override + public Set getPackageBodyInnerBlockHeaders() { + Set packageBodyInnerBlockHeaders = super.getPackageBodyInnerBlockHeaders(); + packageBodyInnerBlockHeaders.add("FUNCTION"); + packageBodyInnerBlockHeaders.add("PROCEDURE"); + return packageBodyInnerBlockHeaders; + } + + @Override + public IRuleManager getRuleManager() { + return ORACLE_RULE_MANAGER; + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMExecutableSql.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMExecutableSql.java new file mode 100644 index 0000000000..59f2b3f264 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMExecutableSql.java @@ -0,0 +1,10 @@ +package ai.chat2db.plugin.dm.parser; + +import ai.chat2db.community.domain.api.enums.parser.SqlTypeEnum; + +public record DMExecutableSql(String sqlType, String originalSql, String executableSql) { + + public boolean isExplain() { + return SqlTypeEnum.EXPLAIN.name().equals(sqlType); + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMExecutionVisitor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMExecutionVisitor.java new file mode 100644 index 0000000000..649fec706f --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMExecutionVisitor.java @@ -0,0 +1,49 @@ +package ai.chat2db.plugin.dm.parser; + +import ai.chat2db.community.domain.api.enums.parser.SqlTypeEnum; +import ai.chat2db.plugin.dm.parser.base.DMParser; +import ai.chat2db.plugin.dm.parser.base.DMParserBaseVisitor; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.ParserRuleContext; + +public class DMExecutionVisitor extends DMParserBaseVisitor { + + private final CommonTokenStream tokens; + + private final String originalSql; + + private DMExecutableSql result; + + public DMExecutionVisitor(CommonTokenStream tokens, String originalSql) { + this.tokens = tokens; + this.originalSql = originalSql; + } + + @Override + public DMExecutableSql visitSql_script(DMParser.Sql_scriptContext ctx) { + visitChildren(ctx); + return result == null ? new DMExecutableSql(SqlTypeEnum.OTHER.name(), originalSql, originalSql) : result; + } + + @Override + public DMExecutableSql visitExplain_statement(DMParser.Explain_statementContext ctx) { + ParserRuleContext innerStatement = firstNonNull( + ctx.select_statement(), + ctx.update_statement(), + ctx.delete_statement(), + ctx.insert_statement(), + ctx.merge_statement()); + String executableSql = innerStatement == null ? originalSql : tokens.getText(innerStatement.getSourceInterval()); + result = new DMExecutableSql(SqlTypeEnum.EXPLAIN.name(), originalSql, executableSql); + return result; + } + + private ParserRuleContext firstNonNull(ParserRuleContext... candidates) { + for (ParserRuleContext candidate : candidates) { + if (candidate != null) { + return candidate; + } + } + return null; + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMSqlParser.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMSqlParser.java index 9b66a16999..59b1c02b2c 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMSqlParser.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/DMSqlParser.java @@ -1,6 +1,487 @@ package ai.chat2db.plugin.dm.parser; -import ai.chat2db.plugin.oracle.parser.OracleSqlParser; +import ai.chat2db.spi.DefaultSQLFileSplitter; +import ai.chat2db.spi.constant.SQLConstants; +import ai.chat2db.community.domain.api.enums.parser.FileSizeUnitEnum; +import ai.chat2db.community.domain.api.enums.parser.SqlTypeEnum; +import ai.chat2db.community.domain.api.model.parser.statement.Statement; +import ai.chat2db.community.domain.api.model.parser.statement.StatementContext; +import ai.chat2db.plugin.dm.parser.base.DMLexer; +import ai.chat2db.plugin.dm.parser.base.DMParser; +import ai.chat2db.plugin.dm.parser.base.DMParserBaseVisitor; +import ai.chat2db.plugin.dm.parser.error.strategy.DMErrorStrategy; +import ai.chat2db.plugin.dm.parser.visitor.DMCreateTableVisitor; +import ai.chat2db.plugin.dm.parser.visitor.DMParserVisitor; +import ai.chat2db.plugin.dm.parser.visitor.DMSimpleParserVisitor; +import ai.chat2db.plugin.dm.parser.visitor.DMValidTableVisitor; +import ai.chat2db.spi.parser.AbstractSqlParser; +import ai.chat2db.community.domain.api.service.task.ITaskProgressListener; +import ai.chat2db.spi.IRuleManager; +import ai.chat2db.community.domain.api.service.db.ISqlBatchHandler; +import ai.chat2db.spi.util.TokenUtil; +import org.antlr.v4.runtime.*; +import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.antlr.v4.runtime.tree.ParseTree; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; -public class DMSqlParser extends OracleSqlParser { +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class DMSqlParser extends AbstractSqlParser { + + + public DMSqlParser() { + super(new DMParser(null), new DMDialect()); + } + + @Override + protected Lexer createLexer(CharStream charStream) { + return new DMLexer(charStream); + } + + + @Override + protected ParseTree parserRoot(DMParser parser) { + return parser.sql_script(); + } + + @Override + protected StatementContext createStatementContext(CommonTokenStream tokenStream, SqlTypeEnum sqlTypeEnum) { + StatementContext statementContext = new StatementContext(tokenStream); + statementContext.setRecoverSet(DMErrorStrategy.recoverSet); + DMParserBaseVisitor visitor = createVisitor(statementContext, sqlTypeEnum); + statementContext.setVisitor(visitor); + return statementContext; + } + + + private DMParserBaseVisitor createVisitor(StatementContext statementContext, SqlTypeEnum sqlTypeEnum) { + return switch (sqlTypeEnum) { + case SIMPLE -> new DMSimpleParserVisitor(statementContext); + case CREATE_TABLE -> new DMCreateTableVisitor(statementContext); + case VALID_TABLE -> new DMValidTableVisitor(statementContext); + default -> new DMParserVisitor(statementContext); + }; + } + + /** + * Classifies an executable statement for the DM command executor. + * Ordinary SQL only pays for a cheap lexer scan of the first keyword; + * a full ANTLR parse runs only when that keyword is {@code EXPLAIN}. + * Parse failures for EXPLAIN never silently fall back to JDBC. + */ + public DMExecutableSql parseExecutableSql(String sql) { + if (StringUtils.isBlank(sql)) { + return new DMExecutableSql(SqlTypeEnum.OTHER.name(), sql, sql); + } + DMLexer lexer = new DMLexer(CharStreams.fromString(sql)); + CommonTokenStream tokens = new CommonTokenStream(lexer); + tokens.fill(); + if (!isExplainLeadingKeyword(tokens)) { + return new DMExecutableSql(SqlTypeEnum.OTHER.name(), sql, sql); + } + + DMParser parser = new DMParser(tokens); + parser.removeErrorListeners(); + parser.setErrorHandler(new BailErrorStrategy()); + try { + DMExecutableSql parsed = new DMExecutionVisitor(tokens, sql).visit(parser.sql_script()); + if (parsed == null || !parsed.isExplain()) { + throw new IllegalArgumentException("Failed to parse DM EXPLAIN statement"); + } + return parsed; + } catch (ParseCancellationException e) { + throw new IllegalArgumentException("Failed to parse DM EXPLAIN statement", e); + } + } + + private boolean isExplainLeadingKeyword(CommonTokenStream tokens) { + for (Token token : tokens.getTokens()) { + if (token.getType() == Token.EOF) { + break; + } + if (!TokenUtil.hasValuableText(token) || dialect.isComment(token.getType())) { + continue; + } + return token.getType() == DMLexer.EXPLAIN; + } + return false; + } + + @Override + public List parserSqlScript(String sql) { + Lexer lexer = getLexer(sql); + CommonTokenStream tokenStream = createTokenStream(lexer); + tokenStream.fill(); + List tokens = tokenStream.getTokens(); + + List statements = new ArrayList<>(); + IRuleManager ruleManager = dialect.getRuleManager(); + Token firstToken = null; + Token lastToken = null; + Token firstKeyword = null; + Token lastKeyword = null; + boolean encounteredBlockHeaderPrefix = false; + SqlScriptBlockContext curBlock = null; + for (int i = 0; i < tokens.size(); i++) { + Token token = tokens.get(i); + int tokenType = token.getType(); + if (tokenType == Token.EOF) { + break; + } + String currentTokenText = token.getText().trim(); + if (!TokenUtil.hasValuableText(token)) { + continue; + } else if (dialect.isComment(tokenType)) { + continue; + } else if (tokenType == DMLexer.SOLIDUS && Objects.isNull(firstToken)) { + continue; + } + if (dialect.isStatementDelimiter(currentTokenText) && Objects.isNull(curBlock)) { + if (Objects.isNull(firstToken)) { + continue; + } + Statement statement = getStatement(tokenStream, firstToken, lastToken); + String originalSql = statement.getOriginalSql(); + if (StringUtils.isNotBlank(originalSql)) { + String trimSql = originalSql.trim(); + if (trimSql.endsWith("/")) { + String substring = trimSql.substring(0, trimSql.length() - 1); + if (Objects.nonNull(lastKeyword)) { + if ("END".equalsIgnoreCase(lastKeyword.getText())) { + substring += ";"; + } + } + statement.setSql(substring); + } + } + statements.add(statement); + firstToken = null; + lastToken = null; + firstKeyword = null; + lastKeyword = null; + curBlock = null; + encounteredBlockHeaderPrefix = false; + continue; + } + if (Objects.isNull(firstToken)) { + firstToken = token; + } + if (currentTokenText.length() == 1) { + if (StringUtils.equalsAnyIgnoreCase(currentTokenText, + SQLConstants.OPEN_PARENTHESIS, + SQLConstants.OPEN_CURLY_BRACE, + SQLConstants.OPEN_SQUARE_BRACKET)) { + curBlock = new SqlScriptBlockContext(curBlock); + } + if (StringUtils.equalsAnyIgnoreCase(currentTokenText, + SQLConstants.CLOSE_PARENTHESIS, + SQLConstants.CLOSE_CURLY_BRACE, + SQLConstants.CLOSE_SQUARE_BRACKET)) { + if (Objects.nonNull(curBlock)) { + curBlock = curBlock.parent; + } + } + } + if (dialect.isBlockHeaderPrefix(currentTokenText)) { + encounteredBlockHeaderPrefix = true; + lastToken = token; + firstKeyword = token; + lastKeyword = token; + continue; + } + + if (encounteredBlockHeaderPrefix) { + if (dialect.isDependentBlockHeader(currentTokenText)) { + boolean isBlockHeader = true; + if (Objects.nonNull(ruleManager)) { + isBlockHeader = ruleManager.matchRules(tokens, i); + } + if (isBlockHeader) { + curBlock = new SqlScriptBlockContext(curBlock, true); + lastToken = token; + lastKeyword = token; + encounteredBlockHeaderPrefix = false; + } + continue; + } + } else { + if (dialect.isIndependentBlockHeader(currentTokenText) && Objects.isNull(curBlock)) { + curBlock = new SqlScriptBlockContext(null, true); + if (Objects.isNull(firstKeyword)) { + firstKeyword = token; + } + lastToken = token; + lastKeyword = token; + continue; + } + if (Objects.nonNull(curBlock) + && dialect.isPackageBodyInnerBlockHeader(currentTokenText) + && Objects.nonNull(ruleManager) + && ruleManager.matchRules(tokens, i)) { + curBlock = new SqlScriptBlockContext(curBlock, true); + lastToken = token; + lastKeyword = token; + continue; + + } + } + + if (dialect.isBlockBegin(currentTokenText)) { + if (Objects.nonNull(lastToken) && dialect.isBlockEnd(lastToken.getText())) { + lastToken = token; + continue; + } else { + boolean isBlock = true; + if (Objects.nonNull(ruleManager)) { + isBlock = ruleManager.matchRules(tokens, i); + } + if (isBlock) { + if (Objects.nonNull(curBlock) && curBlock.isHeader) { + curBlock = curBlock.parent; + } + curBlock = new SqlScriptBlockContext(curBlock); + } + } + } else if (dialect.isBlockEnd(currentTokenText)) { + if (Objects.nonNull(curBlock)) { + curBlock = curBlock.parent; + } + } + + + lastToken = token; + if (dialect.isKeyword(currentTokenText)) { + if (Objects.isNull(firstKeyword)) { + firstKeyword = token; + } + lastKeyword = token; + } + + } + + return statements; + } + + @Override + public int parserSqlScript(File file, ITaskProgressListener progressListener, ISqlBatchHandler sqlBatchHandler) { + try (DefaultSQLFileSplitter safeSQLFileSplitter = new DefaultSQLFileSplitter(10, FileSizeUnitEnum.MB, file, StandardCharsets.UTF_8)) { + String content; + Lexer lexer = new DMLexer(null); + CharStream charStream = null; + long bytesRead = 0L; + int statementCount = 0; + List currentTokens = new ArrayList<>(50); + IRuleManager ruleManager = dialect.getRuleManager(); + while (StringUtils.isNotBlank(content = safeSQLFileSplitter.nextContent())) { + if (CollectionUtils.isNotEmpty(currentTokens)) { + String leftTokens = currentTokens.stream().map(Token::getText).collect(Collectors.joining()); + charStream = CharStreams.fromString(leftTokens + content); + currentTokens.clear(); + } else { + charStream = CharStreams.fromString(content); + } + lexer.setInputStream(charStream); + UnbufferedTokenStream tokenStream = createUnbufferedTokenStream(lexer); + boolean encounteredBlockHeaderPrefix = false; + boolean encounteredOpenParenthesis = false; + Token firstToken = null; + Token lastToken = null; + Token firstKeyword = null; + Token lastKeyword = null; + SqlScriptBlockContext curBlock = null; + while (tokenStream.LA(1) != Token.EOF) { + Token token = tokenStream.LT(1); + int tokenType = token.getType(); + String text = token.getText(); + bytesRead += text.getBytes().length; + String currentTokenText = text.trim(); + if (!TokenUtil.hasValuableText(token) || dialect.isComment(tokenType)) { + if (Objects.nonNull(firstToken)) { + currentTokens.add(token); + } + tokenStream.consume(); + continue; + } else if (tokenType == DMLexer.SOLIDUS && Objects.isNull(firstToken)) { + tokenStream.consume(); + continue; + } + if (dialect.isStatementDelimiter(currentTokenText) && Objects.isNull(curBlock)) { + if (Objects.isNull(firstToken)) { + tokenStream.consume(); + continue; + } + Statement statement = getStatement(currentTokens); + String originalSql = statement.getOriginalSql(); + if (StringUtils.isNotBlank(originalSql)) { + String trimSql = originalSql.trim(); + if (trimSql.endsWith("/")) { + String substring = trimSql.substring(0, trimSql.length() - 1); + if (Objects.nonNull(lastKeyword)) { + if ("END".equalsIgnoreCase(lastKeyword.getText())) { + substring += ";"; + } + } + statement.setSql(substring); + } + } + sqlBatchHandler.handle(statement); + statementCount++; + firstToken = null; + lastToken = null; + firstKeyword = null; + lastKeyword = null; + curBlock = null; + encounteredBlockHeaderPrefix = false; + currentTokens.clear(); + tokenStream.consume(); + progressListener.onProgress(bytesRead, statementCount); + continue; + } + if (Objects.isNull(firstToken)) { + firstToken = token; + } + currentTokens.add(token); + if (currentTokenText.length() == 1) { + if (SQLConstants.OPEN_PARENTHESIS.equalsIgnoreCase(currentTokenText)) { + encounteredOpenParenthesis = true; + curBlock = new SqlScriptBlockContext(curBlock); + + } else if (StringUtils.equalsAnyIgnoreCase(currentTokenText, + SQLConstants.OPEN_CURLY_BRACE, + SQLConstants.OPEN_SQUARE_BRACKET)) { + curBlock = new SqlScriptBlockContext(curBlock); + } else if (SQLConstants.CLOSE_PARENTHESIS.equalsIgnoreCase(currentTokenText)) { + encounteredOpenParenthesis = false; + if (Objects.nonNull(curBlock)) { + curBlock = curBlock.parent; + } + } + else if (StringUtils.equalsAnyIgnoreCase(currentTokenText, + SQLConstants.CLOSE_CURLY_BRACE, + SQLConstants.CLOSE_SQUARE_BRACKET)) { + if (Objects.nonNull(curBlock)) { + curBlock = curBlock.parent; + } + } + } + if (dialect.isBlockHeaderPrefix(currentTokenText)) { + encounteredBlockHeaderPrefix = true; + lastToken = token; + firstKeyword = token; + lastKeyword = token; + tokenStream.consume(); + continue; + } + + if (encounteredBlockHeaderPrefix) { + if (dialect.isDependentBlockHeader(currentTokenText)) { + boolean isBlockHeader = true; + if (Objects.nonNull(ruleManager)) { + isBlockHeader = ruleManager.matchRules(tokenStream); + } + if (isBlockHeader) { + curBlock = new SqlScriptBlockContext(curBlock, true); + lastToken = token; + lastKeyword = token; + encounteredBlockHeaderPrefix = false; + } + tokenStream.consume(); + continue; + } + } else { + if (dialect.isIndependentBlockHeader(currentTokenText) && Objects.isNull(curBlock)) { + curBlock = new SqlScriptBlockContext(null, true); + if (Objects.isNull(firstKeyword)) { + firstKeyword = token; + } + lastToken = token; + lastKeyword = token; + tokenStream.consume(); + continue; + } + if (Objects.nonNull(curBlock) + && dialect.isPackageBodyInnerBlockHeader(currentTokenText) + && Objects.nonNull(ruleManager) + && ruleManager.matchRules(tokenStream)) { + if (encounteredOpenParenthesis && DMLexer.PROCEDURE == tokenType) { + if (checkLastValuableToken(currentTokens, DMLexer.LEFT_PAREN)) { + lastToken = token; + lastKeyword = token; + tokenStream.consume(); + continue; + } + } else { + curBlock = new SqlScriptBlockContext(curBlock, true); + lastToken = token; + lastKeyword = token; + tokenStream.consume(); + continue; + } + + } + } + + if (dialect.isBlockBegin(currentTokenText)) { + if (Objects.nonNull(lastToken) && dialect.isBlockEnd(lastToken.getText())) { + lastToken = token; + tokenStream.consume(); + continue; + } else { + boolean isBlock = true; + if (Objects.nonNull(ruleManager)) { + isBlock = ruleManager.matchRules(tokenStream); + } + if (isBlock) { + if (Objects.nonNull(curBlock) && curBlock.isHeader) { + curBlock = curBlock.parent; + } + curBlock = new SqlScriptBlockContext(curBlock); + } + } + } else if (dialect.isBlockEnd(currentTokenText)) { + if (Objects.nonNull(curBlock)) { + curBlock = curBlock.parent; + } + } + + + lastToken = token; + if (dialect.isKeyword(currentTokenText)) { + if (Objects.isNull(firstKeyword)) { + firstKeyword = token; + } + lastKeyword = token; + } + tokenStream.consume(); + } + } + if (CollectionUtils.isNotEmpty(currentTokens)) { + Statement statement = getStatement(currentTokens); + sqlBatchHandler.handle(statement); + statementCount++; + currentTokens.clear(); + progressListener.onProgress(bytesRead, statementCount); + } + sqlBatchHandler.flush(); + return statementCount; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private boolean checkLastValuableToken(List currentTokens, Integer targetTokenType) { + for (int i = currentTokens.size() - 1; i > 0; i--) { + Token token = currentTokens.get(i); + if (TokenUtil.hasValuableText(token)) { + return token.getType() == targetTokenType; + } + } + return false; + } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/base/DMLexerBase.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/base/DMLexerBase.java new file mode 100644 index 0000000000..1f0aed6db7 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/base/DMLexerBase.java @@ -0,0 +1,20 @@ +package ai.chat2db.plugin.dm.parser.base; + +import org.antlr.v4.runtime.*; + +public abstract class DMLexerBase extends Lexer +{ + public DMLexerBase self; + + public DMLexerBase(CharStream input) + { + super(input); + self = this; + } + + protected boolean IsNewlineAtPos(int pos) + { + int la = _input.LA(pos); + return la == -1 || la == '\n'; + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/base/DMParserBase.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/base/DMParserBase.java new file mode 100644 index 0000000000..4eb7b4bd69 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/base/DMParserBase.java @@ -0,0 +1,31 @@ +package ai.chat2db.plugin.dm.parser.base; + +import org.antlr.v4.runtime.*; + +public abstract class DMParserBase extends Parser +{ + private boolean _isVersion12 = true; + private boolean _isVersion10 = true; + public DMParserBase self; + + public DMParserBase(TokenStream input) { + super(input); + self = this; + } + + public boolean isVersion12() { + return _isVersion12; + } + + public void setVersion12(boolean value) { + _isVersion12 = value; + } + + public boolean isVersion10() { + return _isVersion10; + } + + public void setVersion10(boolean value) { + _isVersion10 = value; + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/error/strategy/DMErrorStrategy.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/error/strategy/DMErrorStrategy.java new file mode 100644 index 0000000000..7990007e99 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/error/strategy/DMErrorStrategy.java @@ -0,0 +1,29 @@ +package ai.chat2db.plugin.dm.parser.error.strategy; + +import ai.chat2db.spi.parser.error.strategy.BaseErrorStrategy; +import ai.chat2db.plugin.dm.parser.base.DMParser; +import org.antlr.v4.runtime.misc.IntervalSet; + +public class DMErrorStrategy extends BaseErrorStrategy { + + private static final int[] stmtBeginToken = new int[]{ + DMParser.SELECT, DMParser.INSERT, DMParser.UPDATE, + DMParser.DELETE, DMParser.ALTER, DMParser.DROP, + DMParser.ADMINISTER, DMParser.ANALYZE, DMParser.BEGIN, + DMParser.COMMIT, DMParser.CREATE, DMParser.DECLARE, + DMParser.EXECUTE, DMParser.GRANT, DMParser.RENAME, + DMParser.REVOKE, DMParser.SAVEPOINT, DMParser.TRUNCATE, + DMParser.LOCK, DMParser.NOAUDIT, DMParser.PURGE, + DMParser.EXPLAIN, DMParser.FLASHBACK, DMParser.ASSOCIATE, + DMParser.AUDIT, DMParser.MERGE, DMParser.ROLLBACK, + DMParser.DISASSOCIATE, DMParser.SEMICOLON, + DMParser.COMMENT,DMParser.WITH,DMParser.EXIT, + DMParser.PROMPT_MESSAGE,DMParser.SHOW,DMParser.WHENEVER, + DMParser.TIMING,DMParser.START_CMD, + + }; + + public static final IntervalSet recoverSet = new IntervalSet(stmtBeginToken); + + +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/listener/DMSelectListener.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/listener/DMSelectListener.java new file mode 100644 index 0000000000..bba126c913 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/listener/DMSelectListener.java @@ -0,0 +1,209 @@ +package ai.chat2db.plugin.dm.parser.listener; + +import ai.chat2db.community.domain.api.enums.parser.IdentifierTypeEnum; +import ai.chat2db.community.domain.api.model.parser.info.ColumnInfo; +import ai.chat2db.community.domain.api.model.parser.info.TableInfo; +import ai.chat2db.community.domain.api.model.parser.statement.Statement; +import ai.chat2db.community.domain.api.model.parser.statement.StatementContext; +import ai.chat2db.community.domain.api.model.parser.token.Identifier; +import ai.chat2db.plugin.dm.parser.base.DMParser; +import ai.chat2db.plugin.dm.parser.base.DMParserBaseListener; +import ai.chat2db.spi.util.SqlStringUtil; +import ai.chat2db.spi.util.TokenUtil; +import io.vavr.control.Try; +import lombok.extern.slf4j.Slf4j; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.ParseTreeWalker; +import org.antlr.v4.runtime.tree.TerminalNode; + +import java.util.*; + +@Slf4j +public class DMSelectListener extends DMParserBaseListener { + + + private final StatementContext context; + + private Map tableAliasMap = new HashMap<>(); + + private Map columnAliasMap = new HashMap<>(); + + public DMSelectListener(StatementContext context) { + this.context = context; + } + + public void parserSelectStatement(ParseTree parseTree) { + tableAliasMap = new HashMap<>(); + columnAliasMap = new HashMap<>(); + ParseTreeWalker parseTreeWalker = new ParseTreeWalker(); + parseTreeWalker.walk(this, parseTree); + } + + + @Override + public void exitSelect_statement(DMParser.Select_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + currentStatement.setColumnAliasMap(columnAliasMap); + currentStatement.setTableAliasMap(tableAliasMap); + } + + @Override + public void enterSelected_list(DMParser.Selected_listContext ctx) { + TerminalNode asterisk = ctx.ASTERISK(); + if (Objects.nonNull(asterisk)) { + columnAliasMap.put( + ColumnInfo.builder().column("*").build(), null + ); + } + } + + @Override + public void enterSelect_list_elements(DMParser.Select_list_elementsContext ctx) { + TerminalNode asterisk = ctx.ASTERISK(); + if (asterisk != null) { + String schema = null, table = null; + DMParser.Tableview_nameContext tableviewNameContext = ctx.tableview_name(); + if (tableviewNameContext != null) { + String text = tableviewNameContext.getText(); + String[] split = text.split("\\."); + int length = split.length; + if (length == 2) { + schema = SqlStringUtil.removeQuote(split[0]); + } + table = SqlStringUtil.removeQuote(split[length - 1]); + } + columnAliasMap.put( + ColumnInfo.builder().schema(schema).table(table).column("*").build(), null + ); + return; + } + Try.of(() -> { + DMParser.Column_aliasContext columnAliasContext = ctx.column_alias(); + DMParser.ExpressionContext expression = ctx.expression(); + if (Objects.isNull(expression)) { + return null; + } + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + CommonTokenStream commonTokenStream = context.getCommonTokenStream(); + Token database = null, table = null, column = null, alias = null; + String schemaText = null, tableText = null, columnText = null, aliasText = null; + ColumnInfo columnInfo; + List defaultChannelToken = TokenUtil.getParserRuleTokensOnDefault(commonTokenStream, expression); + int dotCount = defaultChannelToken.stream() + .mapToInt(token -> (int) token.getText().chars().filter(ch -> ch == '.').count()) + .sum(); + int defaultTokenSize = defaultChannelToken.size(); + if (dotCount == 0) { + if (defaultTokenSize == 2) { + alias = defaultChannelToken.get(1); + aliasText = SqlStringUtil.removeQuote(alias.getText()); + } + if (defaultTokenSize > 0) { + column = defaultChannelToken.get(0); + columnText = SqlStringUtil.removeQuote(column.getText()); + } + + } else if (dotCount == 1) { + if (defaultTokenSize - 1 == 3) { + alias = defaultChannelToken.get(3); + aliasText = SqlStringUtil.removeQuote(alias.getText()); + } + if (defaultTokenSize > 2) { + table = defaultChannelToken.get(0); + column = defaultChannelToken.get(2); + tableText = SqlStringUtil.removeQuote(table.getText()); + columnText = SqlStringUtil.removeQuote(column.getText()); + } + } else if (dotCount == 2) { + if (defaultTokenSize - 2 == 4) { + alias = defaultChannelToken.get(5); + aliasText = SqlStringUtil.removeQuote(alias.getText()); + } + if (defaultTokenSize > 4) { + database = defaultChannelToken.get(0); + table = defaultChannelToken.get(2); + column = defaultChannelToken.get(4); + schemaText = SqlStringUtil.removeQuote(database.getText()); + tableText = SqlStringUtil.removeQuote(table.getText()); + columnText = SqlStringUtil.removeQuote(column.getText()); + } + } + if (Objects.isNull(alias) && Objects.nonNull(columnAliasContext)) { + alias = columnAliasContext.stop; + aliasText = SqlStringUtil.removeQuote(columnAliasContext.getText()); + } + + if (Objects.nonNull(column)) { + Identifier identifier = new Identifier(); + identifier.setIdentifierName(columnText); + identifier.setIdentifierType(IdentifierTypeEnum.COLUMN.name()); + identifier.setFirstToken(column); + identifier.setIdentifierSchema(schemaText); + identifier.setIdentifierTable(tableText); + columnInfo = ColumnInfo.builder().schema(schemaText).table(tableText).column(columnText).build(); + if (Objects.nonNull(alias)) { + aliasText = alias.getText(); + } + columnAliasMap.put(columnInfo, aliasText); + } + return null; + + }).onFailure(e -> { + log.error("enter select list error", e); + }); + + } + + + @Override + public void enterTable_ref_aux(DMParser.Table_ref_auxContext ctx) { + Try.of(() -> { + DMParser.Table_ref_aux_internalContext tableRefAuxInternalContext = ctx.table_ref_aux_internal(); + if (Objects.isNull(tableRefAuxInternalContext)) { + return null; + } + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + CommonTokenStream commonTokenStream = context.getCommonTokenStream(); + Token database = null, table, alias = null; + String schemaText = null, tableText, aliasText = null; + TableInfo tableInfo; + DMParser.Table_aliasContext tableAliasContext = ctx.table_alias(); + if (Objects.nonNull(tableAliasContext)) { + alias = tableAliasContext.stop; + aliasText = SqlStringUtil.removeQuote(tableAliasContext.getText()); + } + List defaultChannelTokens = TokenUtil.getParserRuleTokensOnDefault(commonTokenStream, tableRefAuxInternalContext); + int dotCount = defaultChannelTokens.stream() + .mapToInt(token -> (int) token.getText().chars().filter(ch -> ch == '.').count()) + .sum(); + if (dotCount == 1) { + database = defaultChannelTokens.get(0); + schemaText = SqlStringUtil.removeQuote(database.getText()); + } + table = defaultChannelTokens.get(defaultChannelTokens.size() - 1); + tableText = SqlStringUtil.removeQuote(table.getText()); + + if (Objects.nonNull(database)) { + + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), database); + } + tableInfo = TableInfo.builder().schema(schemaText).table(tableText).build(); + if (Objects.nonNull(alias)) { + aliasText = alias.getText(); + } + tableAliasMap.put(tableInfo, aliasText); + return null; + }).onFailure(e -> log.error("enter table ref aux error", e)); + } + + +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/DMRulePredicate.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/DMRulePredicate.java new file mode 100644 index 0000000000..d572d2ef30 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/DMRulePredicate.java @@ -0,0 +1,26 @@ +package ai.chat2db.plugin.dm.parser.rule; + +import ai.chat2db.spi.parser.AbstractRulePredicate; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenStream; + +import java.util.List; + +public abstract class DMRulePredicate extends AbstractRulePredicate { + + + @Override + public boolean matches(List tokens, int currentIndex) { + return true; + } + + @Override + public boolean needUpgradeBlock(List tokens, int currentIndex) { + return false; + } + + @Override + public boolean matches(TokenStream tokenStream) { + return true; + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/manager/DMRuleManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/manager/DMRuleManager.java new file mode 100644 index 0000000000..118d7d6114 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/manager/DMRuleManager.java @@ -0,0 +1,30 @@ +package ai.chat2db.plugin.dm.parser.rule.manager; + +import ai.chat2db.plugin.dm.parser.rule.predicate.*; +import ai.chat2db.spi.parser.AbstractRuleManager; +import ai.chat2db.spi.IRulePredicate; + +import java.util.HashMap; +import java.util.Map; + +public class DMRuleManager extends AbstractRuleManager { + + + private static final Map RULE_PREDICATES = new HashMap<>(); + + static { + RULE_PREDICATES.put("PACKAGE", new DMCreatePackagePredicate(1)); + RULE_PREDICATES.put("IF", new DMIfBlockPredicate(20)); + RULE_PREDICATES.put("FUNCTION",new DMCreateFunctionPredicate(100)); + RULE_PREDICATES.put("PROCEDURE", new DMCreateProcedurePredicate(100)); + RULE_PREDICATES.put(("BEGIN"),new DMBeginBlockPredicate(1)); + } + + + @Override + public Map getRules() { + return RULE_PREDICATES; + } + + +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMBeginBlockPredicate.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMBeginBlockPredicate.java new file mode 100644 index 0000000000..25079d3629 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMBeginBlockPredicate.java @@ -0,0 +1,32 @@ +package ai.chat2db.plugin.dm.parser.rule.predicate; + +import ai.chat2db.plugin.dm.parser.base.DMLexer; +import ai.chat2db.plugin.dm.parser.rule.DMRulePredicate; +import lombok.NoArgsConstructor; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenStream; + +import java.util.List; +import java.util.Set; + +@NoArgsConstructor +public class DMBeginBlockPredicate extends DMRulePredicate { + + + public DMBeginBlockPredicate(int lookAheadTokens) { + this.lookaheadTokens = lookAheadTokens; + } + + private static final Set MATCH_TOKENS = Set.of(DMLexer.BACKUP, DMLexer.RETURN); + + @Override + public boolean matches(List tokens, int currentIndex) { + return getNthValidTokenWithMatch(tokens, currentIndex, 1, Set.of("BACKUP", "RETURN")) == -1; + + } + + @Override + public boolean matches(TokenStream tokenStream) { + return getNthValidTokenWithMatch(tokenStream, 1, MATCH_TOKENS) == -1; + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMCreateFunctionPredicate.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMCreateFunctionPredicate.java new file mode 100644 index 0000000000..7e75870be6 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMCreateFunctionPredicate.java @@ -0,0 +1,92 @@ +package ai.chat2db.plugin.dm.parser.rule.predicate; + +import ai.chat2db.plugin.dm.parser.base.DMLexer; +import ai.chat2db.plugin.dm.parser.rule.DMRulePredicate; +import ai.chat2db.spi.util.TokenUtil; +import lombok.NoArgsConstructor; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenStream; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; + + +@NoArgsConstructor +public class DMCreateFunctionPredicate extends DMRulePredicate { + + + public DMCreateFunctionPredicate(int lookAheadTokens) { + this.lookaheadTokens = lookAheadTokens; + } + + @Override + public boolean matches(List tokens, int currentIndex) { + if (lookaheadTokens == 0) { + lookaheadTokens = tokens.size(); + } + int lookaheadIndex = currentIndex + 1; + int valuableTokenFound = 0; + + boolean encounteredIsOrAs = false; + while (lookaheadIndex < tokens.size() && valuableTokenFound < lookaheadTokens) { + Token token = tokens.get(lookaheadIndex); + if (!TokenUtil.hasValuableText(token)) { + lookaheadIndex++; + continue; + } + lookaheadIndex++; + valuableTokenFound++; + String text = token.getText().trim(); + if (StringUtils.equalsAnyIgnoreCase(text, "IS", "AS")) { + encounteredIsOrAs = true; + } else if ("BEGIN".equalsIgnoreCase(text)) { + return true; + } else if (!encounteredIsOrAs && ";".equalsIgnoreCase(text)) { + return false; + } else if ("LANGUAGE".equalsIgnoreCase(text)) { + return false; + } + + } + return false; + + } + + @Override + public boolean matches(TokenStream tokenStream) { + + int valuableTokenFound = 0; + int offset = 2; + boolean encounteredIsOrAs = false; + + while (valuableTokenFound < lookaheadTokens) { + Token token = tokenStream.LT(offset); + int tokenType = token.getType(); + if (tokenType == Token.EOF) { + return false; + } + + if (!TokenUtil.hasValuableText(token)) { + offset++; + continue; + } + + valuableTokenFound++; + String text = token.getText().trim(); + if (DMLexer.AS == tokenType || DMLexer.IS == tokenType) { + encounteredIsOrAs = true; + } else if (DMLexer.BEGIN == tokenType) { + return true; + } else if (!encounteredIsOrAs && DMLexer.SEMICOLON == tokenType) { + return false; + } else if (DMLexer.LANGUAGE == tokenType) { + return false; + } + + offset++; + } + + return false; + } + +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMCreatePackagePredicate.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMCreatePackagePredicate.java new file mode 100644 index 0000000000..cc9890ae7c --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMCreatePackagePredicate.java @@ -0,0 +1,38 @@ +package ai.chat2db.plugin.dm.parser.rule.predicate; + +import ai.chat2db.plugin.dm.parser.rule.DMRulePredicate; +import ai.chat2db.spi.util.TokenUtil; +import org.antlr.v4.runtime.Token; + +import java.util.List; + +public class DMCreatePackagePredicate extends DMRulePredicate { + + + public DMCreatePackagePredicate(int lookAheadTokens) { + this.lookaheadTokens = lookAheadTokens; + } + + + @Override + public boolean needUpgradeBlock(List tokens, int currentIndex) { + int lookaheadIndex = currentIndex + 1; + int valuableTokenFound = 0; + while (lookaheadIndex < tokens.size() && valuableTokenFound < lookaheadTokens) { + Token token = tokens.get(lookaheadIndex); + if (!TokenUtil.hasValuableText(token)) { + lookaheadIndex++; + continue; + } + valuableTokenFound++; + String text = token.getText().trim(); + if (valuableTokenFound == 1 && "BODY".equalsIgnoreCase(text)) { + return false; + } + lookaheadIndex++; + + } + + return true; + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMCreateProcedurePredicate.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMCreateProcedurePredicate.java new file mode 100644 index 0000000000..04b11d5cbe --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMCreateProcedurePredicate.java @@ -0,0 +1,106 @@ +package ai.chat2db.plugin.dm.parser.rule.predicate; + +import ai.chat2db.plugin.dm.parser.base.DMLexer; +import ai.chat2db.plugin.dm.parser.rule.DMRulePredicate; +import ai.chat2db.spi.util.TokenUtil; +import lombok.NoArgsConstructor; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenStream; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; + +@NoArgsConstructor +public class DMCreateProcedurePredicate extends DMRulePredicate { + + + public DMCreateProcedurePredicate(int lookAheadTokens) { + this.lookaheadTokens = lookAheadTokens; + } + + + @Override + public boolean matches(List tokens, int currentIndex) { + if (lookaheadTokens == 0) { + lookaheadTokens = tokens.size(); + } + int lookaheadIndex = currentIndex + 1; + int valuableTokenFound = 0; + + boolean encounteredIsOrAs = false; + boolean previousIsLeftParen = getNthValidTokenWithMatchBackward(tokens, currentIndex, 1, "(") != -1; + + while (lookaheadIndex < tokens.size() && valuableTokenFound < lookaheadTokens) { + Token token = tokens.get(lookaheadIndex); + if (!TokenUtil.hasValuableText(token)) { + lookaheadIndex++; + continue; + } + lookaheadIndex++; + valuableTokenFound++; + String text = token.getText().trim(); + if (StringUtils.equalsAnyIgnoreCase(text, "IS", "AS")) { + encounteredIsOrAs = true; + } else if ("BEGIN".equalsIgnoreCase(text)) { + return true; + } else if (!encounteredIsOrAs && ";".equalsIgnoreCase(text)) { + return false; + } else if ("LANGUAGE".equalsIgnoreCase(text)) { + return false; + } else if ("EXTERNAL".equalsIgnoreCase(text)) { + return false; + } else if (previousIsLeftParen && ")".equalsIgnoreCase(text)) { + return false; + } + + } + return false; + + } + + + @Override + public boolean matches(TokenStream tokenStream) { + + int offset = 2; + int valuableTokenFound = 0; + + boolean encounteredIsOrAs = false; + + + while (valuableTokenFound < lookaheadTokens) { + Token token = tokenStream.LT(offset); + int tokenType = token.getType(); + if (tokenType == Token.EOF) { + return false; + } + if (!TokenUtil.hasValuableText(token)) { + offset++; + continue; + } + + valuableTokenFound++; + switch (tokenType) { + case DMLexer.IS: + case DMLexer.AS: + encounteredIsOrAs = true; + break; + case DMLexer.BEGIN: + return true; + case DMLexer.SEMICOLON: + if (!encounteredIsOrAs) { + return false; + } + break; + case DMLexer.LANGUAGE: + case DMLexer.EXTERNAL: + return false; + } + + offset++; + } + return false; + } + + +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMIfBlockPredicate.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMIfBlockPredicate.java new file mode 100644 index 0000000000..411fd74bbb --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/rule/predicate/DMIfBlockPredicate.java @@ -0,0 +1,50 @@ +package ai.chat2db.plugin.dm.parser.rule.predicate; + +import ai.chat2db.plugin.dm.parser.base.DMLexer; +import ai.chat2db.plugin.dm.parser.rule.DMRulePredicate; +import ai.chat2db.spi.util.TokenUtil; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenStream; + +import java.util.List; +import java.util.Set; + +public class DMIfBlockPredicate extends DMRulePredicate { + + public DMIfBlockPredicate(int lookAheadTokens) { + this.lookaheadTokens = lookAheadTokens; + } + + private static final Set MATCH_TOKENS = Set.of(DMLexer.NOT, DMLexer.EXISTS); + + @Override + public boolean matches(List tokens, int currentIndex) { + int lookaheadIndex = currentIndex + 1; + int valuableTokenFound = 0; + + while (lookaheadIndex < tokens.size() && valuableTokenFound < lookaheadTokens) { + Token token = tokens.get(lookaheadIndex); + if (!TokenUtil.hasValuableText(token)) { + lookaheadIndex++; + continue; + } + valuableTokenFound++; + String text = token.getText().trim(); + if ("THEN".equalsIgnoreCase(text)) { + return true; + } else if ("NOT".equalsIgnoreCase(text) && valuableTokenFound == 1) { + return false; + } else if ("EXISTS".equalsIgnoreCase(text) && valuableTokenFound == 1) { + return false; + } + lookaheadIndex++; + + } + return false; + } + + @Override + public boolean matches(TokenStream tokenStream) { + return getNthValidTokenWithMatch(tokenStream, 1, MATCH_TOKENS) == -1; + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMCreateTableVisitor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMCreateTableVisitor.java new file mode 100644 index 0000000000..9a8be76d55 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMCreateTableVisitor.java @@ -0,0 +1,13 @@ +package ai.chat2db.plugin.dm.parser.visitor; + +import ai.chat2db.community.domain.api.model.parser.statement.StatementContext; +import ai.chat2db.plugin.dm.parser.base.DMParserBaseVisitor; + +public class DMCreateTableVisitor extends DMParserBaseVisitor { + + private StatementContext context; + + public DMCreateTableVisitor(StatementContext context) { + this.context = context; + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMParserVisitor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMParserVisitor.java new file mode 100644 index 0000000000..1ce02754e1 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMParserVisitor.java @@ -0,0 +1,1075 @@ +package ai.chat2db.plugin.dm.parser.visitor; + +import ai.chat2db.community.domain.api.enums.parser.IdentifierTypeEnum; +import ai.chat2db.community.domain.api.enums.parser.SqlTypeEnum; +import ai.chat2db.community.domain.api.enums.parser.StatementValidTypeEnum; +import ai.chat2db.community.domain.api.model.parser.statement.Statement; +import ai.chat2db.community.domain.api.model.parser.statement.StatementContext; +import ai.chat2db.community.domain.api.model.parser.statement.insert.InsertRowTokenRange; +import ai.chat2db.community.domain.api.model.parser.token.Identifier; +import ai.chat2db.plugin.dm.parser.base.DMParser; +import ai.chat2db.plugin.dm.parser.base.DMParserBaseVisitor; +import ai.chat2db.spi.util.InsertValueTokenUtil; +import ai.chat2db.spi.util.SqlCommentUtil; +import ai.chat2db.spi.util.SqlStringUtil; +import ai.chat2db.spi.util.TokenUtil; +import io.vavr.control.Try; +import lombok.extern.slf4j.Slf4j; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenStream; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.Objects; + +@Slf4j +public class DMParserVisitor extends DMParserBaseVisitor { + + private final StatementContext context; + + public DMParserVisitor(StatementContext context) { + this.context = context; + } + + + @Override + public Void visitSql_script(DMParser.Sql_scriptContext ctx) { + + List unitStatement = ctx.unit_statement(); + TokenStream commonTokenStream = context.getCommonTokenStream(); + for (int i = 0; i < unitStatement.size(); i++) { + DMParser.Unit_statementContext child = unitStatement.get(i); + Statement statement = new Statement(); + int sqlCommentSearchStartIndex = 0; + if (i != 0) { + DMParser.Unit_statementContext unitStatementContext = unitStatement.get(i - 1); + if (Objects.nonNull(unitStatementContext)) { + sqlCommentSearchStartIndex = unitStatementContext.getStop().getTokenIndex(); + + } + } + int sqlCommentSearchEndIndex = child.getStart().getTokenIndex(); + String sqlComment = SqlCommentUtil.searchSqlComment(sqlCommentSearchStartIndex, sqlCommentSearchEndIndex, commonTokenStream); + statement.setComment(sqlComment); + context.setCurrentStatement(statement); + statement.setStatementType(StatementValidTypeEnum.VALID.name()); + String sql = ""; + if (StringUtils.isNotBlank(sqlComment)) { + sql = "--" + " " + sqlComment + "\n"; + } + sql += commonTokenStream.getText(child.getSourceInterval()); + statement.setSql(sql); + statement.setFirstToken(child.getStart()); + statement.setLastToken(child.getStop()); + context.addStatement(statement); + visit(child); + } + return null; + } + + @Override + public Void visitExplain_statement(DMParser.Explain_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.EXPLAIN.name()); + return super.visitExplain_statement(ctx); + } + + @Override + public Void visitAlter_table(DMParser.Alter_tableContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_TABLE.name()); + DMParser.Tableview_nameContext tableviewNameContext = ctx.tableview_name(); + visitTableViewName(tableviewNameContext, IdentifierTypeEnum.TABLE); + return null; + } + + @Override + public Void visitSelect_statement(DMParser.Select_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.SELECT.name()); + return super.visitSelect_statement(ctx); + } + + + @Override + public Void visitCreate_function_body(DMParser.Create_function_bodyContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_FUNCTION.name()); + String sql = currentStatement.getSql(); + if (!sql.endsWith(";")) { + sql = sql + ";"; + currentStatement.setSql(sql); + } + return super.visitCreate_function_body(ctx); + } + + + @Override + public Void visitAnonymous_block(DMParser.Anonymous_blockContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ANONYMOUS_BLOCK.name()); + String sql = currentStatement.getSql(); + if (!sql.endsWith(";")) { + sql = sql + ";"; + currentStatement.setSql(sql); + } + return super.visitAnonymous_block(ctx); + } + + @Override + public Void visitFunction_name(DMParser.Function_nameContext ctx) { + Try.run(() -> { + Token start = ctx.getStart(); + Token stop = ctx.getStop(); + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return; + } + if (start.getTokenIndex() == stop.getTokenIndex()) { + currentStatement.addIdentifier(SqlStringUtil.removeQuote(start.getText()), + IdentifierTypeEnum.FUNCTION.name(), start); + } else { + String schemaText = SqlStringUtil.removeQuote(start.getText()); + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), start); + Identifier identifier = new Identifier(); + String functionText = SqlStringUtil.removeQuote(stop.getText()); + identifier.setIdentifierName(functionText); + identifier.setIdentifierType(IdentifierTypeEnum.FUNCTION.name()); + identifier.setFirstToken(stop); + identifier.setIdentifierSchema(schemaText); + currentStatement.addIdentifier(identifier); + } + }).onFailure(e -> { + log.error("visitFunction_name error", e); + }); + return super.visitFunction_name(ctx); + } + + @Override + public Void visitCreate_procedure_body(DMParser.Create_procedure_bodyContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_PROCEDURE.name()); + String sql = currentStatement.getSql(); + if (!sql.endsWith(";")) { + sql = sql + ";"; + currentStatement.setSql(sql); + } + return super.visitCreate_procedure_body(ctx); + } + + @Override + public Void visitProcedure_name(DMParser.Procedure_nameContext ctx) { + Try.run(() -> { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return; + } + Token start = ctx.getStart(); + Token stop = ctx.getStop(); + if (start.getTokenIndex() == stop.getTokenIndex()) { + currentStatement.addIdentifier(SqlStringUtil.removeQuote(start.getText()), + IdentifierTypeEnum.PROCEDURE.name(), start); + } else { + String schemaText = SqlStringUtil.removeQuote(start.getText()); + + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), start); + String procedureText = SqlStringUtil.removeQuote(stop.getText()); + Identifier identifier = new Identifier(); + identifier.setIdentifierName(procedureText); + identifier.setIdentifierType(IdentifierTypeEnum.PROCEDURE.name()); + identifier.setFirstToken(stop); + identifier.setIdentifierSchema(schemaText); + + currentStatement.addIdentifier(identifier); + } + }).onFailure(e -> { + log.error("visitProcedure_name error", e); + }); + return super.visitProcedure_name(ctx); + } + + @Override + public Void visitCreate_trigger(DMParser.Create_triggerContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_TRIGGER.name()); + String sql = currentStatement.getSql(); + if (!sql.endsWith(";")) { + sql = sql + ";"; + currentStatement.setSql(sql); + } + return super.visitCreate_trigger(ctx); + } + + @Override + public Void visitTrigger_name(DMParser.Trigger_nameContext ctx) { + Try.run(() -> { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return; + } + Token start = ctx.getStart(); + Token stop = ctx.getStop(); + if (start.getTokenIndex() == stop.getTokenIndex()) { + currentStatement.addIdentifier(SqlStringUtil.removeQuote(start.getText()), + IdentifierTypeEnum.TRIGGER.name(), start); + } else { + String schemaText = SqlStringUtil.removeQuote(start.getText()); + + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), start); + String triggerText = SqlStringUtil.removeQuote(stop.getText()); + Identifier identifier = new Identifier(); + identifier.setIdentifierName(triggerText); + identifier.setIdentifierType(IdentifierTypeEnum.TRIGGER.name()); + identifier.setFirstToken(stop); + identifier.setIdentifierSchema(schemaText); + + currentStatement.addIdentifier(identifier); + } + }).onFailure(e -> { + log.error("visitTrigger_name error", e); + }); + return super.visitTrigger_name(ctx); + } + + @Override + public Void visitCreate_index(DMParser.Create_indexContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_INDEX.name()); + DMParser.Table_index_clauseContext tableIndexClauseContext = ctx.table_index_clause(); + if (Objects.nonNull(tableIndexClauseContext)) { + DMParser.Tableview_nameContext tableviewNameContext = tableIndexClauseContext.tableview_name(); + visitTableViewName(tableviewNameContext, IdentifierTypeEnum.TABLE); + } + return super.visitCreate_index(ctx); + } + + @Override + public Void visitIndex_name(DMParser.Index_nameContext ctx) { + Try.run(() -> { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return; + } + Token start = ctx.getStart(); + Token stop = ctx.getStop(); + if (start.getTokenIndex() == stop.getTokenIndex()) { + currentStatement.addIdentifier(SqlStringUtil.removeQuote(start.getText()), + IdentifierTypeEnum.INDEX.name(), start); + } else { + String tableText = SqlStringUtil.removeQuote(start.getText()); + + currentStatement.addIdentifier(tableText, + IdentifierTypeEnum.TABLE.name(), start); + String indexText = SqlStringUtil.removeQuote(stop.getText()); + Identifier identifier = new Identifier(); + identifier.setIdentifierName(indexText); + identifier.setIdentifierType(IdentifierTypeEnum.INDEX.name()); + identifier.setFirstToken(stop); + identifier.setIdentifierTable(tableText); + + currentStatement.addIdentifier(identifier); + } + }).onFailure(e -> { + log.error("visitIndex_name error", e); + }); + return super.visitIndex_name(ctx); + } + + private void visitTableViewName(DMParser.Tableview_nameContext ctx, IdentifierTypeEnum identifierTypeEnum) { + Try.run(() -> { + if (Objects.nonNull(ctx)) { + Token start = ctx.getStart(); + Token stop = ctx.getStop(); + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return; + } + if (start.getTokenIndex() == stop.getTokenIndex()) { + currentStatement.addIdentifier(SqlStringUtil.removeQuote(start.getText()), + identifierTypeEnum.name(), start); + } else { + String schemaText = SqlStringUtil.removeQuote(start.getText()); + + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), start); + String tableViewText = SqlStringUtil.removeQuote(stop.getText()); + Identifier identifier = new Identifier(); + identifier.setIdentifierName(tableViewText); + identifier.setIdentifierType(identifierTypeEnum.name()); + identifier.setFirstToken(stop); + identifier.setIdentifierSchema(schemaText); + + currentStatement.addIdentifier(identifier); + } + } + }).onFailure(e -> log.error("visitTableViewName error", e)); + + } + + + @Override + public Void visitCreate_user(DMParser.Create_userContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_USER.name()); + DMParser.User_object_nameContext userObjectNameContext = ctx.user_object_name(); + if (Objects.nonNull(userObjectNameContext)) { + Token start = userObjectNameContext.getStart(); + currentStatement.addIdentifier(SqlStringUtil.removeQuote(start.getText()), + IdentifierTypeEnum.USER.name(), start); + } + return super.visitCreate_user(ctx); + } + + + @Override + public Void visitCreate_view(DMParser.Create_viewContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_VIEW.name()); + Try.run(() -> { + DMParser.Schema_nameContext schemaNameContext = ctx.schema_name(); + String schemaText = null; + if (Objects.nonNull(schemaNameContext)) { + Token start = schemaNameContext.getStart(); + schemaText = SqlStringUtil.removeQuote(start.getText()); + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), start); + } + DMParser.Id_expressionContext idExpressionContext = ctx.id_expression(0); + if (Objects.nonNull(idExpressionContext)) { + Token start = idExpressionContext.getStart(); + Identifier identifier = new Identifier(); + identifier.setIdentifierName(SqlStringUtil.removeQuote(start.getText())); + identifier.setIdentifierType(IdentifierTypeEnum.VIEW.name()); + identifier.setFirstToken(start); + identifier.setIdentifierSchema(schemaText); + currentStatement.addIdentifier(identifier); + } + }).onFailure(e -> { + log.error("visitCreate_view error", e); + }); + + return super.visitCreate_view(ctx); + } + + + @Override + public Void visitCreate_role(DMParser.Create_roleContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_ROLE.name()); + DMParser.Role_nameContext roleNameContext = ctx.role_name(); + if (Objects.nonNull(roleNameContext)) { + Token start = roleNameContext.getStart(); + currentStatement.addIdentifier(SqlStringUtil.removeQuote(start.getText()), + IdentifierTypeEnum.ROLE.name(), start); + } + return super.visitCreate_role(ctx); + } + + @Override + public Void visitCreate_table(DMParser.Create_tableContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_TABLE.name()); + return super.visitCreate_table(ctx); + } + + @Override + public Void visitTable_name(DMParser.Table_nameContext ctx) { + Try.run(() -> { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return; + } + Token start = ctx.getStart(); + Token stop = ctx.getStop(); + if (start.getTokenIndex() == stop.getTokenIndex()) { + currentStatement.addIdentifier(SqlStringUtil.removeQuote(start.getText()), + IdentifierTypeEnum.TABLE.name(), start); + } else { + String schemaText = SqlStringUtil.removeQuote(start.getText()); + + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), start); + String tableText = SqlStringUtil.removeQuote(stop.getText()); + Identifier identifier = new Identifier(); + identifier.setIdentifierName(tableText); + identifier.setIdentifierType(IdentifierTypeEnum.TABLE.name()); + identifier.setFirstToken(stop); + identifier.setIdentifierSchema(schemaText); + + currentStatement.addIdentifier(identifier); + } + }).onFailure(e -> { + log.error("visitTable_name error", e); + }); + + return super.visitTable_name(ctx); + } + + @Override + public Void visitSchema_name(DMParser.Schema_nameContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + Token start = ctx.getStart(); + currentStatement.addIdentifier(SqlStringUtil.removeQuote(start.getText()), + IdentifierTypeEnum.SCHEMA.name(), start); + return super.visitSchema_name(ctx); + } + + @Override + public Void visitCreate_database(DMParser.Create_databaseContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_DATABASE.name()); + return super.visitCreate_database(ctx); + } + + @Override + public Void visitDatabase_name(DMParser.Database_nameContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + Token start = ctx.getStart(); + currentStatement.addIdentifier(SqlStringUtil.removeQuote(start.getText()), + IdentifierTypeEnum.SCHEMA.name(), start); + return super.visitDatabase_name(ctx); + } + + @Override + public Void visitDrop_function(DMParser.Drop_functionContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_FUNCTION.name()); + return super.visitDrop_function(ctx); + } + + @Override + public Void visitDrop_procedure(DMParser.Drop_procedureContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_PROCEDURE.name()); + return super.visitDrop_procedure(ctx); + } + + @Override + public Void visitDrop_role(DMParser.Drop_roleContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_ROLE.name()); + DMParser.Role_nameContext roleNameContext = ctx.role_name(); + if (Objects.nonNull(roleNameContext)) { + Token start = roleNameContext.getStart(); + currentStatement.addIdentifier(SqlStringUtil.removeQuote(start.getText()), + IdentifierTypeEnum.ROLE.name(), start); + } + return super.visitDrop_role(ctx); + } + + @Override + public Void visitDrop_trigger(DMParser.Drop_triggerContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_TRIGGER.name()); + return super.visitDrop_trigger(ctx); + } + + + @Override + public Void visitDrop_user(DMParser.Drop_userContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_USER.name()); + DMParser.User_object_nameContext userObjectNameContext = ctx.user_object_name(); + if (Objects.nonNull(userObjectNameContext)) { + Token start = userObjectNameContext.getStart(); + currentStatement.addIdentifier(SqlStringUtil.removeQuote(start.getText()), + IdentifierTypeEnum.USER.name(), start); + } + return super.visitDrop_user(ctx); + } + + @Override + public Void visitDrop_index(DMParser.Drop_indexContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_INDEX.name()); + return super.visitDrop_index(ctx); + } + + + @Override + public Void visitDrop_table(DMParser.Drop_tableContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_TABLE.name()); + DMParser.Tableview_nameContext tableviewNameContext = ctx.tableview_name(); + visitTableViewName(tableviewNameContext, IdentifierTypeEnum.TABLE); + return super.visitDrop_table(ctx); + } + + + @Override + public Void visitDrop_view(DMParser.Drop_viewContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_VIEW.name()); + DMParser.Tableview_nameContext tableviewNameContext = ctx.tableview_name(); + visitTableViewName(tableviewNameContext, IdentifierTypeEnum.VIEW); + return super.visitDrop_view(ctx); + } + + + @Override + public Void visitAlter_session(DMParser.Alter_sessionContext ctx) { + + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + DMParser.Alter_session_set_clauseContext alterSessionSetClauseContext = ctx.alter_session_set_clause(); + if (Objects.isNull(alterSessionSetClauseContext) + || alterSessionSetClauseContext.parameter_name().isEmpty() + || !"CURRENT_SCHEMA".equalsIgnoreCase( + alterSessionSetClauseContext.parameter_name(0).getText())) { + return super.visitAlter_session(ctx); + } + currentStatement.setType(SqlTypeEnum.SET_SCHEMA.name()); + Try.run(() -> { + DMParser.Parameter_valueContext parameterValueContext = alterSessionSetClauseContext.parameter_value(0); + if (Objects.nonNull(parameterValueContext)) { + String schemaText = SqlStringUtil.removeQuote(parameterValueContext.getText()); + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), parameterValueContext.getStart()); + } + }).onFailure(e -> { + log.error("alter session error", e); + }); + return super.visitAlter_session(ctx); + } + + @Override + public Void visitDml_table_expression_clause(DMParser.Dml_table_expression_clauseContext ctx) { + DMParser.Tableview_nameContext tableviewNameContext = ctx.tableview_name(); + visitTableViewName(tableviewNameContext, IdentifierTypeEnum.TABLE); + return super.visitDml_table_expression_clause(ctx); + } + + @Override + public Void visitUpdate_statement(DMParser.Update_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.UPDATE.name()); + return super.visitUpdate_statement(ctx); + } + + @Override + public Void visitInsert_statement(DMParser.Insert_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.INSERT.name()); + visitInsertValueMappings(context, ctx); + return super.visitInsert_statement(ctx); + } + + static void visitInsertValueMappings(StatementContext context, DMParser.Insert_statementContext ctx) { + Try.run(() -> { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement) || Objects.isNull(ctx)) { + return; + } + DMParser.Single_table_insertContext singleTableInsertContext = ctx.single_table_insert(); + if (Objects.isNull(singleTableInsertContext)) { + return; + } + DMParser.Insert_into_clauseContext insertIntoClauseContext = singleTableInsertContext.insert_into_clause(); + DMParser.Values_clauseContext valuesClauseContext = singleTableInsertContext.values_clause(); + if (Objects.isNull(insertIntoClauseContext)) { + return; + } + DMParser.Paren_column_listContext columnListContext = insertIntoClauseContext.paren_column_list(); + DMParser.ExpressionsContext expressionsContext = Objects.nonNull(valuesClauseContext) + ? valuesClauseContext.expressions() : null; + if (Objects.isNull(columnListContext) || Objects.isNull(columnListContext.column_list())) { + return; + } + List columnContexts = columnListContext.column_list().column_name(); + if (CollectionUtils.isEmpty(columnContexts)) { + return; + } + List rowTokenRanges = getInsertRowTokenRanges(context, ctx, valuesClauseContext); + if (rowTokenRanges.isEmpty()) { + return; + } + Token rowFirstToken = rowTokenRanges.get(0).getFirstToken(); + Token rowLastToken = rowTokenRanges.get(0).getLastToken(); + List valueContexts = Objects.nonNull(expressionsContext) + ? expressionsContext.expression() : List.of(); + + int columnSize = Math.min(columnContexts.size(), valueContexts.size()); + for (int columnIndex = 0; columnIndex < columnSize; columnIndex++) { + DMParser.Column_nameContext columnContext = columnContexts.get(columnIndex); + DMParser.ExpressionContext expressionContext = valueContexts.get(columnIndex); + if (Objects.isNull(columnContext) || Objects.isNull(expressionContext)) { + continue; + } + currentStatement.addInsertValueMapping(columnContext.getStart(), columnContext.getStop(), + expressionContext.getStart(), expressionContext.getStop(), rowFirstToken, rowLastToken, + 0, columnIndex); + } + for (int columnIndex = columnSize; columnIndex < columnContexts.size(); columnIndex++) { + DMParser.Column_nameContext columnContext = columnContexts.get(columnIndex); + if (Objects.isNull(columnContext)) { + continue; + } + currentStatement.addUnmappedInsertColumn(columnContext.getStart(), columnContext.getStop(), + rowFirstToken, rowLastToken, 0, columnIndex); + } + for (int valueIndex = columnSize; valueIndex < valueContexts.size(); valueIndex++) { + DMParser.ExpressionContext expressionContext = valueContexts.get(valueIndex); + if (Objects.isNull(expressionContext)) { + continue; + } + currentStatement.addUnmappedInsertValue(expressionContext.getStart(), expressionContext.getStop(), + rowFirstToken, rowLastToken, 0, valueIndex); + } + }).onFailure(e -> log.error("oracle visitInsertValueMappings error", e)); + } + + private static List getInsertRowTokenRanges(StatementContext context, + DMParser.Insert_statementContext ctx, + DMParser.Values_clauseContext valuesClauseContext) { + List rowTokenRanges = List.of(); + if (Objects.nonNull(valuesClauseContext)) { + Token rowFirstToken = Objects.nonNull(valuesClauseContext.LEFT_PAREN()) + ? valuesClauseContext.LEFT_PAREN().getSymbol() : valuesClauseContext.getStart(); + Token rowLastToken = Objects.nonNull(valuesClauseContext.RIGHT_PAREN()) + ? valuesClauseContext.RIGHT_PAREN().getSymbol() : valuesClauseContext.getStop(); + rowTokenRanges = InsertValueTokenUtil.singleRowTokenRange(rowFirstToken, rowLastToken); + } + if (!rowTokenRanges.isEmpty()) { + return rowTokenRanges; + } + return InsertValueTokenUtil.searchValuesRowTokenRanges(context, ctx.getStart(), ctx.getStop()); + } + + @Override + public Void visitDelete_statement(DMParser.Delete_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DELETE.name()); + return super.visitDelete_statement(ctx); + } + + @Override + public Void visitColumn_name(DMParser.Column_nameContext ctx) { + Try.of(() -> { + DMParser.IdentifierContext identifier = ctx.identifier(); + if (Objects.isNull(identifier)) { + return null; + } + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + List idExpressionContexts = ctx.id_expression(); + if (CollectionUtils.isEmpty(idExpressionContexts)) { + currentStatement.addIdentifier(SqlStringUtil.removeQuote(identifier.getText()), + IdentifierTypeEnum.COLUMN.name(), identifier.getStart()); + } else { + String schemaText = null, tableText = null, columnText = null; + Token databaseToken = null, tableToken = null, columnToken = null; + if (idExpressionContexts.size() == 2) { + databaseToken = identifier.getStart(); + tableToken = idExpressionContexts.get(0).getStop(); + columnToken = idExpressionContexts.get(1).getStop(); + schemaText = SqlStringUtil.removeQuote(SqlStringUtil.removeQuote(identifier.getText())); + tableText = SqlStringUtil.removeQuote(SqlStringUtil.removeQuote(idExpressionContexts.get(0).getText())); + columnText = SqlStringUtil.removeQuote(SqlStringUtil.removeQuote(idExpressionContexts.get(1).getText())); + } else if (idExpressionContexts.size() == 1) { + tableToken = identifier.getStop(); + columnToken = idExpressionContexts.get(0).getStop(); + tableText = SqlStringUtil.removeQuote(SqlStringUtil.removeQuote(identifier.getText())); + columnText = SqlStringUtil.removeQuote(SqlStringUtil.removeQuote(idExpressionContexts.get(0).getText())); + } + + if (Objects.nonNull(databaseToken)) { + + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), databaseToken); + } + if (Objects.nonNull(tableToken)) { + Identifier identifier1 = new Identifier(); + identifier1.setIdentifierName(tableText); + identifier1.setIdentifierType(IdentifierTypeEnum.TABLE.name()); + identifier1.setFirstToken(tableToken); + identifier1.setIdentifierDatabase(schemaText); + + currentStatement.addIdentifier(identifier1); + } + if (Objects.nonNull(columnToken)) { + Identifier identifier1 = new Identifier(); + identifier1.setIdentifierName(columnText); + identifier1.setIdentifierType(IdentifierTypeEnum.COLUMN.name()); + identifier1.setFirstToken(columnToken); + identifier1.setIdentifierDatabase(schemaText); + identifier1.setIdentifierTable(tableText); + currentStatement.addIdentifier(identifier1); + } + } + return null; + }).onFailure(e -> { + log.error("visitColumn_name error", e); + }); + + return null; + } + + + @Override + public Void visitSelect_list_elements(DMParser.Select_list_elementsContext ctx) { + Try.of(() -> { + DMParser.Column_aliasContext columnAliasContext = ctx.column_alias(); + DMParser.ExpressionContext expression = ctx.expression(); + if (Objects.isNull(expression)) { + return null; + } + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + CommonTokenStream commonTokenStream = context.getCommonTokenStream(); + Token database = null, table = null, column = null, alias = null; + String schemaText = null, tableText = null, columnText = null, aliasText = null; + List defaultChannelToken = TokenUtil.getParserRuleTokensOnDefault(commonTokenStream, expression); + int dotCount = defaultChannelToken.stream() + .mapToInt(token -> (int) token.getText().chars().filter(ch -> ch == '.').count()) + .sum(); + int defaultTokenSize = defaultChannelToken.size(); + if (dotCount == 0) { + if (defaultTokenSize == 2) { + alias = defaultChannelToken.get(1); + aliasText = SqlStringUtil.removeQuote(alias.getText()); + } + if (defaultTokenSize > 0) { + column = defaultChannelToken.get(0); + columnText = SqlStringUtil.removeQuote(column.getText()); + } + + } else if (dotCount == 1) { + if (defaultTokenSize - 1 == 3) { + alias = defaultChannelToken.get(3); + aliasText = SqlStringUtil.removeQuote(alias.getText()); + } + if (defaultTokenSize > 2) { + table = defaultChannelToken.get(0); + column = defaultChannelToken.get(2); + tableText = SqlStringUtil.removeQuote(table.getText()); + columnText = SqlStringUtil.removeQuote(column.getText()); + } + } else if (dotCount == 2) { + if (defaultTokenSize - 2 == 4) { + alias = defaultChannelToken.get(5); + aliasText = SqlStringUtil.removeQuote(alias.getText()); + } + if (defaultTokenSize > 4) { + database = defaultChannelToken.get(0); + table = defaultChannelToken.get(2); + column = defaultChannelToken.get(4); + schemaText = SqlStringUtil.removeQuote(database.getText()); + tableText = SqlStringUtil.removeQuote(table.getText()); + columnText = SqlStringUtil.removeQuote(column.getText()); + } + } + if (Objects.isNull(alias) && Objects.nonNull(columnAliasContext)) { + alias = columnAliasContext.stop; + aliasText = SqlStringUtil.removeQuote(columnAliasContext.getText()); + } + if (Objects.nonNull(database)) { + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), database); + } + if (Objects.nonNull(table)) { + Identifier identifier = new Identifier(); + identifier.setIdentifierName(tableText); + identifier.setIdentifierType(IdentifierTypeEnum.TABLE.name()); + identifier.setFirstToken(table); + identifier.setIdentifierSchema(schemaText); + + currentStatement.addIdentifier(identifier); + } + if (Objects.nonNull(column)) { + Identifier identifier = new Identifier(); + identifier.setIdentifierName(columnText); + identifier.setIdentifierType(IdentifierTypeEnum.COLUMN.name()); + identifier.setFirstToken(column); + identifier.setIdentifierSchema(schemaText); + identifier.setIdentifierTable(tableText); + if (Objects.nonNull(alias)) { + identifier.setIdentifierAlias(aliasText); + identifier.setLastToken(alias); + } + currentStatement.addIdentifier(identifier); + } + return null; + + }).onFailure(e -> { + log.error("visit select list error", e); + }); + + return null; + } + + + @Override + public Void visitTable_ref_aux(DMParser.Table_ref_auxContext ctx) { + if (ctx.getStart().getText().equalsIgnoreCase("(")) { + return super.visitTable_ref_aux(ctx); + } + Try.of(() -> { + DMParser.Table_ref_aux_internalContext tableRefAuxInternalContext = ctx.table_ref_aux_internal(); + if (Objects.isNull(tableRefAuxInternalContext)) { + return null; + } + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + CommonTokenStream commonTokenStream = context.getCommonTokenStream(); + Token database = null, table, alias = null; + String schemaText = null, tableText, aliasText = null; + DMParser.Table_aliasContext tableAliasContext = ctx.table_alias(); + if (Objects.nonNull(tableAliasContext)) { + alias = tableAliasContext.stop; + aliasText = SqlStringUtil.removeQuote(tableAliasContext.getText()); + } + List defaultChannelTokens = TokenUtil.getParserRuleTokensOnDefault(commonTokenStream, tableRefAuxInternalContext); + int dotCount = defaultChannelTokens.stream() + .mapToInt(token -> (int) token.getText().chars().filter(ch -> ch == '.').count()) + .sum(); + if (dotCount == 1) { + database = defaultChannelTokens.get(0); + schemaText = SqlStringUtil.removeQuote(database.getText()); + } + table = defaultChannelTokens.get(defaultChannelTokens.size() - 1); + tableText = SqlStringUtil.removeQuote(table.getText()); + + if (Objects.nonNull(database)) { + + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), database); + } + Identifier identifier = new Identifier(); + identifier.setIdentifierName(tableText); + identifier.setIdentifierType(IdentifierTypeEnum.TABLE.name()); + identifier.setFirstToken(table); + identifier.setIdentifierSchema(schemaText); + if (Objects.nonNull(alias)) { + identifier.setIdentifierAlias(aliasText); + identifier.setLastToken(alias); + } + currentStatement.addIdentifier(identifier); + return null; + }).onFailure(e -> log.error("visit table ref aux error", e)); + return null; + } + + @Override + public Void visitJoin_on_part(DMParser.Join_on_partContext ctx) { + Try.of(() -> { + DMParser.ConditionContext condition = ctx.condition(); + if (Objects.isNull(condition)) { + return null; + } + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + CommonTokenStream commonTokenStream = context.getCommonTokenStream(); + List defaultChannelTokens = TokenUtil.getParserRuleTokensOnDefault(commonTokenStream, condition); + List> tokenLists = TokenUtil.splitTokensBySymbolText(defaultChannelTokens, "="); + for (List tokenList : tokenLists) { + int size = tokenList.size(); + if (size == 5) { + Token database = tokenList.get(0); + Token table = tokenList.get(2); + Token column = tokenList.get(4); + String schemaText = SqlStringUtil.removeQuote(database.getText()); + String tableText = SqlStringUtil.removeQuote(table.getText()); + String columnText = SqlStringUtil.removeQuote(column.getText()); + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), database); + Identifier identifier = new Identifier(); + identifier.setIdentifierName(tableText); + identifier.setIdentifierType(IdentifierTypeEnum.TABLE.name()); + identifier.setFirstToken(table); + identifier.setIdentifierSchema(schemaText); + + currentStatement.addIdentifier(identifier); + Identifier identifier1 = new Identifier(); + identifier1.setIdentifierName(columnText); + identifier1.setIdentifierType(IdentifierTypeEnum.COLUMN.name()); + identifier1.setFirstToken(column); + identifier1.setIdentifierDatabase(schemaText); + identifier1.setIdentifierTable(tableText); + currentStatement.addIdentifier(identifier1); + } else if (size == 3) { + Token table = tokenList.get(0); + Token column = tokenList.get(2); + String tableText = SqlStringUtil.removeQuote(table.getText()); + String columnText = SqlStringUtil.removeQuote(column.getText()); + Identifier identifier = new Identifier(); + identifier.setIdentifierName(tableText); + identifier.setIdentifierType(IdentifierTypeEnum.TABLE.name()); + identifier.setFirstToken(table); + + currentStatement.addIdentifier(identifier); + Identifier identifier1 = new Identifier(); + identifier1.setIdentifierName(columnText); + identifier1.setIdentifierType(IdentifierTypeEnum.COLUMN.name()); + identifier1.setFirstToken(column); + identifier1.setIdentifierTable(tableText); + currentStatement.addIdentifier(identifier1); + } + } + + return null; + }).onFailure(e -> { + log.error("visit join on part error", e); + }); + + return null; + } + + + @Override + public Void visitColumn_definition(DMParser.Column_definitionContext ctx) { + Try.of(() -> { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + DMParser.Column_nameContext columnNameContext = ctx.column_name(); + if (Objects.isNull(columnNameContext)) { + return null; + } + String columnName = context.getText(columnNameContext.getSourceInterval()); + if (StringUtils.isBlank(columnName)) { + return null; + } + String[] split = columnName.split("\\."); + columnName = split[split.length - 1]; + DMParser.DatatypeContext datatype = ctx.datatype(); + if (Objects.isNull(datatype)) { + return null; + } + String datatypeText = datatype.getText(); + Identifier identifier = new Identifier(); + identifier.setIdentifierName(SqlStringUtil.removeQuote(columnName)); + identifier.setIdentifierType(IdentifierTypeEnum.COLUMN.name()); + identifier.setIdentifierDataType(datatypeText); + + currentStatement.addIdentifier(identifier); + return null; + }).onFailure(e -> log.error("visit column definition error", e)); + + return null; + } + + @Override + public Void visitCall_statement(DMParser.Call_statementContext ctx) { + Try.of(() -> { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + DMParser.Routine_nameContext routineNameContext = ctx.routine_name(0); + if (Objects.isNull(routineNameContext)) { + return null; + } + Token start = routineNameContext.getStart(); + Token stop = routineNameContext.getStop(); + if (start.getTokenIndex() != stop.getTokenIndex()) { + Identifier identifier = new Identifier(); + identifier.setIdentifierName(SqlStringUtil.removeQuote(start.getText())); + identifier.setIdentifierType(IdentifierTypeEnum.SCHEMA.name()); + identifier.setFirstToken(start); + + currentStatement.addIdentifier(identifier); + } + Identifier identifier = new Identifier(); + identifier.setIdentifierName(SqlStringUtil.removeQuote(stop.getText())); + identifier.setIdentifierType(IdentifierTypeEnum.PROCEDURE.name()); + identifier.setFirstToken(stop); + currentStatement.addIdentifier(identifier); + return null; + }).onFailure(e -> log.error("visit call statement error", e)); + return null; + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMSimpleParserVisitor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMSimpleParserVisitor.java new file mode 100644 index 0000000000..b62a39ef05 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMSimpleParserVisitor.java @@ -0,0 +1,464 @@ +package ai.chat2db.plugin.dm.parser.visitor; + +import ai.chat2db.community.domain.api.enums.parser.IdentifierTypeEnum; +import ai.chat2db.community.domain.api.enums.parser.SqlTypeEnum; +import ai.chat2db.community.domain.api.enums.parser.StatementValidTypeEnum; +import ai.chat2db.community.domain.api.model.parser.statement.Statement; +import ai.chat2db.community.domain.api.model.parser.statement.StatementContext; +import ai.chat2db.community.domain.api.model.parser.token.Identifier; +import ai.chat2db.plugin.dm.parser.base.DMParser; +import ai.chat2db.plugin.dm.parser.base.DMParserBaseVisitor; +import ai.chat2db.spi.util.SqlCommentUtil; +import ai.chat2db.spi.util.SqlStringUtil; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenStream; + +import java.util.List; +import java.util.Objects; + + +public class DMSimpleParserVisitor extends DMParserBaseVisitor { + + private final StatementContext context; + + public DMSimpleParserVisitor(StatementContext context) { + this.context = context; + } + + @Override + public Void visitSql_script(DMParser.Sql_scriptContext ctx) { + + List unitStatement = ctx.unit_statement(); + TokenStream commonTokenStream = context.getCommonTokenStream(); + for (int i = 0; i < unitStatement.size(); i++) { + DMParser.Unit_statementContext child = unitStatement.get(i); + Statement statement = new Statement(); + int sqlCommentSearchStartIndex = 0; + if (i != 0) { + DMParser.Unit_statementContext unitStatementContext = unitStatement.get(i - 1); + if (Objects.nonNull(unitStatementContext)) { + sqlCommentSearchStartIndex = unitStatementContext.getStop().getTokenIndex(); + + } + } + int sqlCommentSearchEndIndex = child.getStart().getTokenIndex(); + String sqlComment = SqlCommentUtil.searchSqlComment(sqlCommentSearchStartIndex, sqlCommentSearchEndIndex, commonTokenStream); + statement.setComment(sqlComment); + context.setCurrentStatement(statement); + statement.setStatementType(StatementValidTypeEnum.VALID.name()); + statement.setSql(commonTokenStream.getText(child.getSourceInterval())); + statement.setFirstToken(child.getStart()); + statement.setLastToken(child.getStop()); + context.addStatement(statement); + visit(child); + } + return null; + } + + @Override + public Void visitSelect_statement(DMParser.Select_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.SELECT.name()); + return null; + } + + @Override + public Void visitExplain_statement(DMParser.Explain_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.EXPLAIN.name()); + return super.visitExplain_statement(ctx); + } + + @Override + public Void visitAnonymous_block(DMParser.Anonymous_blockContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ANONYMOUS_BLOCK.name()); + String sql = currentStatement.getSql(); + if (!sql.endsWith(";")) { + sql = sql + ";"; + currentStatement.setSql(sql); + } + return super.visitAnonymous_block(ctx); + } + + @Override + public Void visitCreate_function_body(DMParser.Create_function_bodyContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_FUNCTION.name()); + String sql = currentStatement.getSql(); + if (!sql.endsWith(";")) { + sql = sql + ";"; + currentStatement.setSql(sql); + } + return super.visitCreate_function_body(ctx); + } + + @Override + public Void visitCreate_procedure_body(DMParser.Create_procedure_bodyContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_PROCEDURE.name()); + String sql = currentStatement.getSql(); + if (!sql.endsWith(";")) { + sql = sql + ";"; + currentStatement.setSql(sql); + } + return super.visitCreate_procedure_body(ctx); + } + + @Override + public Void visitCreate_trigger(DMParser.Create_triggerContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_TRIGGER.name()); + String sql = currentStatement.getSql(); + if (!sql.endsWith(";")) { + sql = sql + ";"; + currentStatement.setSql(sql); + } + return super.visitCreate_trigger(ctx); + } + + @Override + public Void visitCreate_index(DMParser.Create_indexContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_INDEX.name()); + return null; + } + + @Override + public Void visitCreate_user(DMParser.Create_userContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_USER.name()); + currentStatement.addIdentifier(new Identifier()); + return null; + } + + + @Override + public Void visitCreate_view(DMParser.Create_viewContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_VIEW.name()); + DMParser.Schema_nameContext schemaNameContext = ctx.schema_name(); + String schemaText; + if (Objects.nonNull(schemaNameContext)) { + Token start = schemaNameContext.getStart(); + schemaText = SqlStringUtil.removeQuote(start.getText()); + Identifier identifier = new Identifier(); + identifier.setIdentifierSchema(schemaText); + identifier.setIdentifierType(IdentifierTypeEnum.SCHEMA.name()); + + currentStatement.addIdentifier(identifier); + } else { + Identifier identifier = new Identifier(); + identifier.setIdentifierType(IdentifierTypeEnum.SCHEMA.name()); + currentStatement.addIdentifier(identifier); + } + return null; + } + + @Override + public Void visitCreate_role(DMParser.Create_roleContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_ROLE.name()); + currentStatement.addIdentifier(new Identifier()); + return null; + } + + @Override + public Void visitCreate_table(DMParser.Create_tableContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_TABLE.name()); + DMParser.Schema_nameContext schemaNameContext = ctx.schema_name(); + if (Objects.nonNull(schemaNameContext)) { + Token start = schemaNameContext.getStart(); + String schemaText = SqlStringUtil.removeQuote(start.getText()); + Identifier identifier = new Identifier(); + identifier.setIdentifierSchema(schemaText); + identifier.setIdentifierType(IdentifierTypeEnum.SCHEMA.name()); + + currentStatement.addIdentifier(identifier); + } else { + currentStatement.addIdentifier(new Identifier()); + } + return super.visitCreate_table(ctx); + } + + @Override + public Void visitCreate_database(DMParser.Create_databaseContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_DATABASE.name()); + currentStatement.addIdentifier(new Identifier()); + return null; + } + + + @Override + public Void visitDrop_function(DMParser.Drop_functionContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_FUNCTION.name()); + return super.visitDrop_function(ctx); + } + + @Override + public Void visitDrop_procedure(DMParser.Drop_procedureContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_PROCEDURE.name()); + return super.visitDrop_procedure(ctx); + } + + @Override + public Void visitDrop_role(DMParser.Drop_roleContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_ROLE.name()); + return null; + } + + @Override + public Void visitDrop_trigger(DMParser.Drop_triggerContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_TRIGGER.name()); + return super.visitDrop_trigger(ctx); + } + + + @Override + public Void visitDrop_user(DMParser.Drop_userContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_USER.name()); + currentStatement.addIdentifier(new Identifier()); + + return null; + } + + @Override + public Void visitDrop_index(DMParser.Drop_indexContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_INDEX.name()); + return null; + } + + @Override + public Void visitDrop_table(DMParser.Drop_tableContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_TABLE.name()); + DMParser.Tableview_nameContext tableviewNameContext = ctx.tableview_name(); + visitTableViewName(tableviewNameContext); + return null; + } + + + @Override + public Void visitDrop_view(DMParser.Drop_viewContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_VIEW.name()); + DMParser.Tableview_nameContext tableviewNameContext = ctx.tableview_name(); + visitTableViewName(tableviewNameContext); + return super.visitDrop_view(ctx); + } + + @Override + public Void visitAlter_session(DMParser.Alter_sessionContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + DMParser.Alter_session_set_clauseContext alterSessionSetClauseContext = ctx.alter_session_set_clause(); + if (Objects.isNull(alterSessionSetClauseContext) + || alterSessionSetClauseContext.parameter_name().isEmpty() + || !"CURRENT_SCHEMA".equalsIgnoreCase( + alterSessionSetClauseContext.parameter_name(0).getText())) { + return super.visitAlter_session(ctx); + } + currentStatement.setType(SqlTypeEnum.SET_SCHEMA.name()); + DMParser.Parameter_valueContext parameterValueContext = alterSessionSetClauseContext.parameter_value(0); + if (Objects.nonNull(parameterValueContext)) { + Identifier identifier = new Identifier(); + String schemaText = SqlStringUtil.removeQuote(parameterValueContext.getText()); + identifier.setIdentifierSchema(schemaText); + identifier.setIdentifierType(IdentifierTypeEnum.SCHEMA.name()); + currentStatement.addIdentifier(identifier); + } + return null; + } + + @Override + public Void visitUpdate_statement(DMParser.Update_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.UPDATE.name()); + return null; + } + + @Override + public Void visitInsert_statement(DMParser.Insert_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.INSERT.name()); + DMParserVisitor.visitInsertValueMappings(context, ctx); + return null; + } + + @Override + public Void visitDelete_statement(DMParser.Delete_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DELETE.name()); + return null; + } + + private void visitTableViewName(DMParser.Tableview_nameContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return; + } + if (Objects.nonNull(ctx)) { + Token start = ctx.getStart(); + Token stop = ctx.getStop(); + if (start.getTokenIndex() != stop.getTokenIndex()) { + String schemaText = SqlStringUtil.removeQuote(start.getText()); + Identifier identifier = new Identifier(); + identifier.setIdentifierType(IdentifierTypeEnum.SCHEMA.name()); + identifier.setIdentifierSchema(schemaText); + currentStatement.addIdentifier(identifier); + } else { + Identifier identifier = new Identifier(); + identifier.setIdentifierType(IdentifierTypeEnum.SCHEMA.name()); + currentStatement.addIdentifier(identifier); + } + + } + } + + @Override + public Void visitFunction_name(DMParser.Function_nameContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + Token start = ctx.getStart(); + Token stop = ctx.getStop(); + if (start.getTokenIndex() != stop.getTokenIndex()) { + String schemaText = SqlStringUtil.removeQuote(start.getText()); + Identifier identifier = new Identifier(); + identifier.setIdentifierType(IdentifierTypeEnum.SCHEMA.name()); + identifier.setIdentifierSchema(schemaText); + currentStatement.addIdentifier(identifier); + } else { + Identifier identifier = new Identifier(); + identifier.setIdentifierType(IdentifierTypeEnum.SCHEMA.name()); + currentStatement.addIdentifier(identifier); + } + return null; + } + + @Override + public Void visitProcedure_name(DMParser.Procedure_nameContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + Token start = ctx.getStart(); + Token stop = ctx.getStop(); + if (start.getTokenIndex() != stop.getTokenIndex()) { + String schemaText = SqlStringUtil.removeQuote(start.getText()); + Identifier identifier = new Identifier(); + identifier.setIdentifierType(IdentifierTypeEnum.SCHEMA.name()); + identifier.setIdentifierSchema(schemaText); + currentStatement.addIdentifier(identifier); + } else { + Identifier identifier = new Identifier(); + identifier.setIdentifierType(IdentifierTypeEnum.SCHEMA.name()); + currentStatement.addIdentifier(identifier); + } + return null; + } + + @Override + public Void visitTrigger_name(DMParser.Trigger_nameContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + Token start = ctx.getStart(); + Token stop = ctx.getStop(); + if (start.getTokenIndex() != stop.getTokenIndex()) { + String schemaText = SqlStringUtil.removeQuote(start.getText()); + Identifier identifier = new Identifier(); + identifier.setIdentifierType(IdentifierTypeEnum.SCHEMA.name()); + identifier.setIdentifierSchema(schemaText); + currentStatement.addIdentifier(identifier); + } else { + Identifier identifier = new Identifier(); + identifier.setIdentifierType(IdentifierTypeEnum.SCHEMA.name()); + currentStatement.addIdentifier(identifier); + } + return null; + } + + +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMValidTableVisitor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMValidTableVisitor.java new file mode 100644 index 0000000000..364f558c31 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMValidTableVisitor.java @@ -0,0 +1,1207 @@ +package ai.chat2db.plugin.dm.parser.visitor; + +import ai.chat2db.community.domain.api.enums.parser.IdentifierTypeEnum; +import ai.chat2db.community.domain.api.enums.parser.SqlTypeEnum; +import ai.chat2db.community.domain.api.enums.parser.StatementValidTypeEnum; +import ai.chat2db.community.domain.api.model.parser.statement.Statement; +import ai.chat2db.community.domain.api.model.parser.statement.StatementContext; +import ai.chat2db.community.domain.api.model.parser.token.Identifier; +import ai.chat2db.plugin.dm.parser.base.DMParser; +import ai.chat2db.plugin.dm.parser.base.DMParserBaseVisitor; +import ai.chat2db.plugin.dm.parser.listener.DMSelectListener; +import ai.chat2db.spi.util.SqlStringUtil; +import ai.chat2db.spi.util.TokenUtil; +import io.vavr.control.Try; +import lombok.extern.slf4j.Slf4j; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenStream; + +import java.util.List; +import java.util.Objects; + +@Slf4j +public class DMValidTableVisitor extends DMParserBaseVisitor { + + private final StatementContext context; + + private final DMSelectListener oracleSelectListener; + + public DMValidTableVisitor(StatementContext context) { + this.context = context; + oracleSelectListener = new DMSelectListener(context); + } + + + @Override + public Void visitSql_script(DMParser.Sql_scriptContext ctx) { + + List unitStatement = ctx.unit_statement(); + TokenStream commonTokenStream = context.getCommonTokenStream(); + for (DMParser.Unit_statementContext child : unitStatement) { + Statement statement = new Statement(); + context.setCurrentStatement(statement); + statement.setStatementType(StatementValidTypeEnum.VALID.name()); + statement.setSql(commonTokenStream.getText(child.getSourceInterval())); + statement.setFirstToken(child.getStart()); + statement.setLastToken(child.getStop()); + context.addStatement(statement); + visit(child); + } + return null; + } + + + @Override + public Void visitTruncate_table(DMParser.Truncate_tableContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.TRUNCATE_TABLE.name()); + return null; + } + + @Override + public Void visitDml_table_expression_clause(DMParser.Dml_table_expression_clauseContext ctx) { + DMParser.Tableview_nameContext tableviewNameContext = ctx.tableview_name(); + visitTableViewName(tableviewNameContext, IdentifierTypeEnum.TABLE); + return super.visitDml_table_expression_clause(ctx); + } + @Override + public Void visitDrop_table(DMParser.Drop_tableContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_TABLE.name()); + DMParser.Tableview_nameContext tableviewNameContext = ctx.tableview_name(); + visitTableViewName(tableviewNameContext, IdentifierTypeEnum.TABLE); + return super.visitDrop_table(ctx); + } + + @Override + public Void visitCreate_table(DMParser.Create_tableContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_TABLE.name()); + return null; + } + + @Override + public Void visitTable_name(DMParser.Table_nameContext ctx) { + Try.run(() -> { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return; + } + Token start = ctx.getStart(); + Token stop = ctx.getStop(); + if (start.getTokenIndex() == stop.getTokenIndex()) { + currentStatement.addIdentifier(SqlStringUtil.removeQuote(start.getText()), + IdentifierTypeEnum.TABLE.name(), start); + } else { + String schemaText = SqlStringUtil.removeQuote(start.getText()); + + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), start); + String tableText = SqlStringUtil.removeQuote(stop.getText()); + Identifier identifier = new Identifier(); + identifier.setIdentifierName(tableText); + identifier.setIdentifierType(IdentifierTypeEnum.TABLE.name()); + identifier.setFirstToken(stop); + identifier.setIdentifierSchema(schemaText); + + currentStatement.addIdentifier(identifier); + } + }).onFailure(e -> { + log.error("visitTable_name error", e); + }); + + return super.visitTable_name(ctx); + } + + @Override + public Void visitTable_ref_aux(DMParser.Table_ref_auxContext ctx) { + Try.of(() -> { + DMParser.Table_ref_aux_internalContext tableRefAuxInternalContext = ctx.table_ref_aux_internal(); + if (Objects.isNull(tableRefAuxInternalContext)) { + return null; + } + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + CommonTokenStream commonTokenStream = context.getCommonTokenStream(); + Token database = null, table, alias = null; + String schemaText = null, tableText, aliasText = null; + DMParser.Table_aliasContext tableAliasContext = ctx.table_alias(); + if (Objects.nonNull(tableAliasContext)) { + alias = tableAliasContext.stop; + aliasText = SqlStringUtil.removeQuote(tableAliasContext.getText()); + } + List defaultChannelTokens = TokenUtil.getParserRuleTokensOnDefault(commonTokenStream, tableRefAuxInternalContext); + int dotCount = defaultChannelTokens.stream() + .mapToInt(token -> (int) token.getText().chars().filter(ch -> ch == '.').count()) + .sum(); + if (dotCount == 1) { + database = defaultChannelTokens.get(0); + schemaText = SqlStringUtil.removeQuote(database.getText()); + } + table = defaultChannelTokens.get(defaultChannelTokens.size() - 1); + tableText = SqlStringUtil.removeQuote(table.getText()); + + if (Objects.nonNull(database)) { + + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), database); + } + Identifier identifier = new Identifier(); + identifier.setIdentifierName(tableText); + identifier.setIdentifierType(IdentifierTypeEnum.TABLE.name()); + identifier.setFirstToken(table); + identifier.setIdentifierSchema(schemaText); + if (Objects.nonNull(alias)) { + identifier.setIdentifierAlias(aliasText); + identifier.setLastToken(alias); + } + currentStatement.addIdentifier(identifier); + return null; + }).onFailure(e -> log.error("visit table ref aux error", e)); + + return null; + } + + @Override + public Void visitJoin_on_part(DMParser.Join_on_partContext ctx) { + Try.of(() -> { + DMParser.ConditionContext condition = ctx.condition(); + if (Objects.isNull(condition)) { + return null; + } + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + CommonTokenStream commonTokenStream = context.getCommonTokenStream(); + List defaultChannelTokens = TokenUtil.getParserRuleTokensOnDefault(commonTokenStream, condition); + List> tokenLists = TokenUtil.splitTokensBySymbolText(defaultChannelTokens, "="); + for (List tokenList : tokenLists) { + int size = tokenList.size(); + if (size == 5) { + Token database = tokenList.get(0); + Token table = tokenList.get(2); + Token column = tokenList.get(4); + String schemaText = SqlStringUtil.removeQuote(database.getText()); + String tableText = SqlStringUtil.removeQuote(table.getText()); + String columnText = SqlStringUtil.removeQuote(column.getText()); + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), database); + Identifier identifier = new Identifier(); + identifier.setIdentifierName(tableText); + identifier.setIdentifierType(IdentifierTypeEnum.TABLE.name()); + identifier.setFirstToken(table); + identifier.setIdentifierSchema(schemaText); + + currentStatement.addIdentifier(identifier); + Identifier identifier1 = new Identifier(); + identifier1.setIdentifierName(columnText); + identifier1.setIdentifierType(IdentifierTypeEnum.COLUMN.name()); + identifier1.setFirstToken(column); + identifier1.setIdentifierDatabase(schemaText); + identifier1.setIdentifierTable(tableText); + currentStatement.addIdentifier(identifier1); + } else if (size == 3) { + Token table = tokenList.get(0); + Token column = tokenList.get(2); + String tableText = SqlStringUtil.removeQuote(table.getText()); + String columnText = SqlStringUtil.removeQuote(column.getText()); + Identifier identifier = new Identifier(); + identifier.setIdentifierName(tableText); + identifier.setIdentifierType(IdentifierTypeEnum.TABLE.name()); + identifier.setFirstToken(table); + + currentStatement.addIdentifier(identifier); + Identifier identifier1 = new Identifier(); + identifier1.setIdentifierName(columnText); + identifier1.setIdentifierType(IdentifierTypeEnum.COLUMN.name()); + identifier1.setFirstToken(column); + identifier1.setIdentifierTable(tableText); + currentStatement.addIdentifier(identifier1); + } + } + + return null; + }).onFailure(e -> { + log.error("visit join on part error", e); + }); + + return null; + } + + + @Override + public Void visitAlter_view(DMParser.Alter_viewContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_VIEW.name()); + return super.visitAlter_view(ctx); + } + + @Override + public Void visitAlter_analytic_view(DMParser.Alter_analytic_viewContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_ANALYTIC_VIEW.name()); + return super.visitAlter_analytic_view(ctx); + } + + @Override + public Void visitAlter_attribute_dimension(DMParser.Alter_attribute_dimensionContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_ATTRIBUTE_DIMENSION.name()); + return super.visitAlter_attribute_dimension(ctx); + } + + + @Override + public Void visitAlter_audit_policy(DMParser.Alter_audit_policyContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_AUDIT_POLICY.name()); + return super.visitAlter_audit_policy(ctx); + } + + @Override + public Void visitAlter_cluster(DMParser.Alter_clusterContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_CLUSTER.name()); + return super.visitAlter_cluster(ctx); + } + + + @Override + public Void visitAlter_database(DMParser.Alter_databaseContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_DATABASE.name()); + return super.visitAlter_database(ctx); + } + + + @Override + public Void visitAlter_database_link(DMParser.Alter_database_linkContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_DATABASE_LINK.name()); + return super.visitAlter_database_link(ctx); + } + + @Override + public Void visitAlter_dimension(DMParser.Alter_dimensionContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_DIMENSION.name()); + return super.visitAlter_dimension(ctx); + } + + + @Override + public Void visitAlter_diskgroup(DMParser.Alter_diskgroupContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_DISKGROUP.name()); + return super.visitAlter_diskgroup(ctx); + } + + @Override + public Void visitAlter_flashback_archive(DMParser.Alter_flashback_archiveContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_FLASHBACK_ARCHIVE.name()); + return super.visitAlter_flashback_archive(ctx); + } + + @Override + public Void visitAlter_function(DMParser.Alter_functionContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_FUNCTION.name()); + return super.visitAlter_function(ctx); + } + + @Override + public Void visitAlter_hierarchy(DMParser.Alter_hierarchyContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_HIERARCHY.name()); + return super.visitAlter_hierarchy(ctx); + } + + @Override + public Void visitAlter_index(DMParser.Alter_indexContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_INDEX.name()); + return super.visitAlter_index(ctx); + } + + + @Override + public Void visitAlter_inmemory_join_group(DMParser.Alter_inmemory_join_groupContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_INMEMORY_JOIN_GROUP.name()); + return super.visitAlter_inmemory_join_group(ctx); + } + + @Override + public Void visitAlter_java(DMParser.Alter_javaContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_JAVA.name()); + return super.visitAlter_java(ctx); + } + + @Override + public Void visitAlter_library(DMParser.Alter_libraryContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_LIBRARY.name()); + return super.visitAlter_library(ctx); + } + + @Override + public Void visitAlter_lockdown_profile(DMParser.Alter_lockdown_profileContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_LOCKDOWN_PROFILE.name()); + return super.visitAlter_lockdown_profile(ctx); + } + + @Override + public Void visitAlter_materialized_view(DMParser.Alter_materialized_viewContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_MATERIALIZED_VIEW.name()); + return super.visitAlter_materialized_view(ctx); + } + + @Override + public Void visitAlter_materialized_view_log(DMParser.Alter_materialized_view_logContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_MATERIALIZED_VIEW_LOG.name()); + return super.visitAlter_materialized_view_log(ctx); + } + + @Override + public Void visitAlter_materialized_zonemap(DMParser.Alter_materialized_zonemapContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_MATERIALIZED_ZONEMAP.name()); + return super.visitAlter_materialized_zonemap(ctx); + } + + @Override + public Void visitAlter_operator(DMParser.Alter_operatorContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_OPERATOR.name()); + return super.visitAlter_operator(ctx); + } + + @Override + public Void visitAlter_outline(DMParser.Alter_outlineContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_OUTLINE.name()); + return super.visitAlter_outline(ctx); + } + + @Override + public Void visitAlter_package(DMParser.Alter_packageContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_PACKAGE.name()); + return super.visitAlter_package(ctx); + } + + + @Override + public Void visitAlter_pmem_filestore(DMParser.Alter_pmem_filestoreContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_PMEM_FILESTORE.name()); + return super.visitAlter_pmem_filestore(ctx); + } + + @Override + public Void visitAlter_procedure(DMParser.Alter_procedureContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_PROCEDURE.name()); + return super.visitAlter_procedure(ctx); + } + + + @Override + public Void visitAlter_resource_cost(DMParser.Alter_resource_costContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_RESOURCE_COST.name()); + return super.visitAlter_resource_cost(ctx); + } + + @Override + public Void visitAlter_role(DMParser.Alter_roleContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_ROLE.name()); + return super.visitAlter_role(ctx); + } + + @Override + public Void visitAlter_rollback_segment(DMParser.Alter_rollback_segmentContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_ROLLBACK_SEGMENT.name()); + return super.visitAlter_rollback_segment(ctx); + } + + @Override + public Void visitAlter_sequence(DMParser.Alter_sequenceContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_SEQUENCE.name()); + return super.visitAlter_sequence(ctx); + } + + @Override + public Void visitAlter_session(DMParser.Alter_sessionContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_SESSION.name()); + return super.visitAlter_session(ctx); + } + + @Override + public Void visitAlter_synonym(DMParser.Alter_synonymContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_SYNONYM.name()); + return super.visitAlter_synonym(ctx); + } + + @Override + public Void visitAlter_table(DMParser.Alter_tableContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_TABLE.name()); + DMParser.Tableview_nameContext tableviewNameContext = ctx.tableview_name(); + visitTableViewName(tableviewNameContext, IdentifierTypeEnum.TABLE); + return null; + } + private void visitTableViewName(DMParser.Tableview_nameContext ctx, IdentifierTypeEnum identifierTypeEnum) { + Try.run(() -> { + if (Objects.nonNull(ctx)) { + Token start = ctx.getStart(); + Token stop = ctx.getStop(); + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return; + } + if (start.getTokenIndex() == stop.getTokenIndex()) { + currentStatement.addIdentifier(SqlStringUtil.removeQuote(start.getText()), + identifierTypeEnum.name(), start); + } else { + String schemaText = SqlStringUtil.removeQuote(start.getText()); + + currentStatement.addIdentifier(schemaText, + IdentifierTypeEnum.SCHEMA.name(), start); + String tableViewText = SqlStringUtil.removeQuote(stop.getText()); + Identifier identifier = new Identifier(); + identifier.setIdentifierName(tableViewText); + identifier.setIdentifierType(identifierTypeEnum.name()); + identifier.setFirstToken(stop); + identifier.setIdentifierSchema(schemaText); + + currentStatement.addIdentifier(identifier); + } + } + }).onFailure(e -> log.error("visitTableViewName error", e)); + + } + + + @Override + public Void visitAlter_tablespace(DMParser.Alter_tablespaceContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_TABLESPACE.name()); + return super.visitAlter_tablespace(ctx); + } + + @Override + public Void visitAlter_tablespace_set(DMParser.Alter_tablespace_setContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_TABLESPACE_SET.name()); + return super.visitAlter_tablespace_set(ctx); + } + + @Override + public Void visitAlter_trigger(DMParser.Alter_triggerContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_TRIGGER.name()); + return super.visitAlter_trigger(ctx); + } + + @Override + public Void visitAlter_type(DMParser.Alter_typeContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_TYPE.name()); + return super.visitAlter_type(ctx); + } + + @Override + public Void visitAlter_user(DMParser.Alter_userContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.ALTER_USER.name()); + return super.visitAlter_user(ctx); + } + + + @Override + public Void visitCall_statement(DMParser.Call_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CALL_PROC.name()); + return super.visitCall_statement(ctx); + } + + @Override + public Void visitCreate_analytic_view(DMParser.Create_analytic_viewContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_ANALYTIC_VIEW.name()); + return super.visitCreate_analytic_view(ctx); + } + + @Override + public Void visitCreate_attribute_dimension(DMParser.Create_attribute_dimensionContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_ATTRIBUTE_DIMENSION.name()); + return super.visitCreate_attribute_dimension(ctx); + } + + @Override + public Void visitCreate_audit_policy(DMParser.Create_audit_policyContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_AUDIT_POLICY.name()); + return super.visitCreate_audit_policy(ctx); + } + + @Override + public Void visitCreate_cluster(DMParser.Create_clusterContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_CLUSTER.name()); + return super.visitCreate_cluster(ctx); + } + + @Override + public Void visitCreate_context(DMParser.Create_contextContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_CONTEXT.name()); + return super.visitCreate_context(ctx); + } + + @Override + public Void visitCreate_controlfile(DMParser.Create_controlfileContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_CONTROLFILE.name()); + return super.visitCreate_controlfile(ctx); + } + + @Override + public Void visitCreate_diskgroup(DMParser.Create_diskgroupContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_DISKGROUP.name()); + return super.visitCreate_diskgroup(ctx); + } + + @Override + public Void visitCreate_edition(DMParser.Create_editionContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_EDITION.name()); + return super.visitCreate_edition(ctx); + } + + @Override + public Void visitCreate_flashback_archive(DMParser.Create_flashback_archiveContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_FLASHBACK_ARCHIVE.name()); + return super.visitCreate_flashback_archive(ctx); + } + + @Override + public Void visitCreate_hierarchy(DMParser.Create_hierarchyContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_HIERARCHY.name()); + return super.visitCreate_hierarchy(ctx); + } + + @Override + public Void visitCreate_inmemory_join_group(DMParser.Create_inmemory_join_groupContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_INMEMORY_JOIN_GROUP.name()); + return super.visitCreate_inmemory_join_group(ctx); + } + + @Override + public Void visitCreate_java(DMParser.Create_javaContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_JAVA.name()); + return super.visitCreate_java(ctx); + } + + @Override + public Void visitCreate_library(DMParser.Create_libraryContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_LIBRARY.name()); + return super.visitCreate_library(ctx); + } + + @Override + public Void visitCreate_lockdown_profile(DMParser.Create_lockdown_profileContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_LOCKDOWN_PROFILE.name()); + return super.visitCreate_lockdown_profile(ctx); + } + + @Override + public Void visitCreate_operator(DMParser.Create_operatorContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_OPERATOR.name()); + return super.visitCreate_operator(ctx); + } + + @Override + public Void visitCreate_outline(DMParser.Create_outlineContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_OUTLINE.name()); + return super.visitCreate_outline(ctx); + } + + @Override + public Void visitCreate_restore_point(DMParser.Create_restore_pointContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_RESTORE_POINT.name()); + return super.visitCreate_restore_point(ctx); + } + + @Override + public Void visitCreate_spfile(DMParser.Create_spfileContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.CREATE_SPFILE.name()); + return super.visitCreate_spfile(ctx); + } + + @Override + public Void visitDrop_analytic_view(DMParser.Drop_analytic_viewContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_ANALYTIC_VIEW.name()); + return super.visitDrop_analytic_view(ctx); + } + + @Override + public Void visitDrop_attribute_dimension(DMParser.Drop_attribute_dimensionContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_ATTRIBUTE_DIMENSION.name()); + return super.visitDrop_attribute_dimension(ctx); + } + + @Override + public Void visitDrop_audit_policy(DMParser.Drop_audit_policyContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_AUDIT_POLICY.name()); + return super.visitDrop_audit_policy(ctx); + } + + @Override + public Void visitDrop_cluster(DMParser.Drop_clusterContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_CLUSTER.name()); + return super.visitDrop_cluster(ctx); + } + + @Override + public Void visitDrop_context(DMParser.Drop_contextContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_CONTEXT.name()); + return super.visitDrop_context(ctx); + } + + @Override + public Void visitDrop_directory(DMParser.Drop_directoryContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_DIRECTORY.name()); + return super.visitDrop_directory(ctx); + } + + @Override + public Void visitDrop_diskgroup(DMParser.Drop_diskgroupContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_DISKGROUP.name()); + return super.visitDrop_diskgroup(ctx); + } + + @Override + public Void visitDrop_edition(DMParser.Drop_editionContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_EDITION.name()); + return super.visitDrop_edition(ctx); + } + + @Override + public Void visitDrop_flashback_archive(DMParser.Drop_flashback_archiveContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_FLASHBACK_ARCHIVE.name()); + return super.visitDrop_flashback_archive(ctx); + } + + @Override + public Void visitDrop_hierarchy(DMParser.Drop_hierarchyContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_HIERARCHY.name()); + return super.visitDrop_hierarchy(ctx); + } + + @Override + public Void visitDrop_inmemory_join_group(DMParser.Drop_inmemory_join_groupContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_INMEMORY_JOIN_GROUP.name()); + return super.visitDrop_inmemory_join_group(ctx); + } + + @Override + public Void visitDrop_java(DMParser.Drop_javaContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_JAVA.name()); + return super.visitDrop_java(ctx); + } + + @Override + public Void visitDrop_library(DMParser.Drop_libraryContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_LIBRARY.name()); + return super.visitDrop_library(ctx); + } + + @Override + public Void visitDrop_lockdown_profile(DMParser.Drop_lockdown_profileContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_LOCKDOWN_PROFILE.name()); + return super.visitDrop_lockdown_profile(ctx); + } + + @Override + public Void visitDrop_operator(DMParser.Drop_operatorContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_OPERATOR.name()); + return super.visitDrop_operator(ctx); + } + + @Override + public Void visitDrop_outline(DMParser.Drop_outlineContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_OUTLINE.name()); + return super.visitDrop_outline(ctx); + } + + @Override + public Void visitDrop_restore_point(DMParser.Drop_restore_pointContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DROP_RESTORE_POINT.name()); + return super.visitDrop_restore_point(ctx); + } + + @Override + public Void visitComment_on_column(DMParser.Comment_on_columnContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.COMMENT_ON_COLUMN.name()); + return super.visitComment_on_column(ctx); + } + + @Override + public Void visitComment_on_table(DMParser.Comment_on_tableContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.COMMENT_ON_TABLE.name()); + return super.visitComment_on_table(ctx); + } + + @Override + public Void visitComment_on_materialized(DMParser.Comment_on_materializedContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.COMMENT_ON_MATERIALIZED.name()); + return super.visitComment_on_materialized(ctx); + } + + @Override + public Void visitDisassociate_statistics(DMParser.Disassociate_statisticsContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DISASSOCIATE_STATISTICS.name()); + return super.visitDisassociate_statistics(ctx); + } + + @Override + public Void visitFlashback_table(DMParser.Flashback_tableContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.FLASHBACK_TABLE.name()); + return super.visitFlashback_table(ctx); + } + + @Override + public Void visitGrant_statement(DMParser.Grant_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.GRANT.name()); + return super.visitGrant_statement(ctx); + } + + @Override + public Void visitNoaudit_statement(DMParser.Noaudit_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.NOAUDIT.name()); + return super.visitNoaudit_statement(ctx); + } + + @Override + public Void visitPurge_statement(DMParser.Purge_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.PURGE.name()); + return super.visitPurge_statement(ctx); + } + + @Override + public Void visitRename_object(DMParser.Rename_objectContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.RENAME.name()); + return super.visitRename_object(ctx); + } + + @Override + public Void visitRevoke_statement(DMParser.Revoke_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.REVOKE.name()); + return super.visitRevoke_statement(ctx); + } + + @Override + public Void visitTruncate_cluster(DMParser.Truncate_clusterContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.TRUNCATE_CLUSTER.name()); + return super.visitTruncate_cluster(ctx); + } + + @Override + public Void visitUnified_auditing(DMParser.Unified_auditingContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.UNIFIED_AUDIT.name()); + return super.visitUnified_auditing(ctx); + } + + @Override + public Void visitSelect_statement(DMParser.Select_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.SELECT.name()); + oracleSelectListener.parserSelectStatement(ctx); + return null; + } + + @Override + public Void visitInsert_statement(DMParser.Insert_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.INSERT.name()); + return super.visitInsert_statement(ctx); + } + + @Override + public Void visitDelete_statement(DMParser.Delete_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.DELETE.name()); + return super.visitDelete_statement(ctx); + } + + @Override + public Void visitUpdate_statement(DMParser.Update_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.UPDATE.name()); + return super.visitUpdate_statement(ctx); + } + + @Override + public Void visitMerge_statement(DMParser.Merge_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.MERGE.name()); + return super.visitMerge_statement(ctx); + } + + @Override + public Void visitLock_table_statement(DMParser.Lock_table_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.LOCK_TABLES.name()); + return super.visitLock_table_statement(ctx); + } + + + @Override + public Void visitExplain_statement(DMParser.Explain_statementContext ctx) { + Statement currentStatement = context.getCurrentStatement(); + if (Objects.isNull(currentStatement)) { + return null; + } + currentStatement.setType(SqlTypeEnum.EXPLAIN.name()); + return super.visitExplain_statement(ctx); + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMCommandExecutorTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMCommandExecutorTest.java new file mode 100644 index 0000000000..723bf42ca9 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMCommandExecutorTest.java @@ -0,0 +1,638 @@ +package ai.chat2db.plugin.dm; + +import ai.chat2db.community.domain.api.model.result.ExecuteResponse; +import ai.chat2db.community.domain.api.model.result.ExecutionContext; +import ai.chat2db.community.domain.api.model.result.ResultCell; +import ai.chat2db.community.domain.api.config.DriverConfig; +import ai.chat2db.community.domain.api.enums.plugin.SqlTypeEnum; +import ai.chat2db.community.domain.api.model.sql.SqlExecuteRequest; +import ai.chat2db.community.domain.api.model.sql.SimpleSqlStatement; +import ai.chat2db.community.tools.util.I18nUtils; +import ai.chat2db.community.domain.api.service.db.ISqlExecutionCancellation; +import ai.chat2db.community.domain.api.service.db.ISqlExecutionResultConsumer; +import ai.chat2db.community.domain.api.service.db.ISqlExecutionStatementListener; +import ai.chat2db.spi.IPlugin; +import ai.chat2db.spi.model.datasource.ConnectInfo; +import ai.chat2db.spi.sql.Chat2DBContext; +import ai.chat2db.spi.sql.ConnectionPool; +import ai.chat2db.plugin.dm.builder.DMSqlBuilder; +import ai.chat2db.plugin.dm.parser.DMExecutableSql; +import ai.chat2db.plugin.dm.parser.DMSqlParser; +import dm.jdbc.driver.DmdbConnection; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.List; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class DMCommandExecutorTest { + + private static final long DATA_SOURCE_ID = 2762L; + + private IPlugin previousPlugin; + + private boolean contextBound; + + @BeforeAll + static void setUpI18n() throws Exception { + Field field = I18nUtils.class.getDeclaredField("messageSourceStatic"); + field.setAccessible(true); + field.set(null, new MessageSource() { + @Override + public String getMessage(String code, Object[] args, String defaultMessage, Locale locale) { + return defaultMessage == null ? code : defaultMessage; + } + + @Override + public String getMessage(String code, Object[] args, Locale locale) { + return code; + } + + @Override + public String getMessage(MessageSourceResolvable resolvable, Locale locale) { + String[] codes = resolvable.getCodes(); + return codes == null || codes.length == 0 ? resolvable.getDefaultMessage() : codes[0]; + } + }); + } + + @AfterEach + void tearDownContext() { + if (!contextBound) { + return; + } + Chat2DBContext.removeContext(); + ConnectionPool.removeConnection(DATA_SOURCE_ID); + if (previousPlugin == null) { + Chat2DBContext.PLUGIN_MAP.remove("DM"); + } else { + Chat2DBContext.PLUGIN_MAP.put("DM", previousPlugin); + } + contextBound = false; + previousPlugin = null; + } + + @Test + void allDmSqlShouldUseDmExecutor() { + assertSame(DMCommandExecutor.INSTANCE, new DMMetaData().getCommandExecutor()); + } + + @Test + void parserShouldRecognizeExplicitExplainAndReturnInnerSql() { + DMExecutableSql result = new DMSqlParser().parseExecutableSql("EXPLAIN SELECT * FROM SYSOBJECTS"); + + assertTrue(result.isExplain()); + assertEquals("SELECT * FROM SYSOBJECTS", result.executableSql()); + } + + @Test + void builderShouldNotAddExplainTwice() { + assertEquals("EXPLAIN SELECT * FROM SYSOBJECTS", + new DMSqlBuilder().buildExplain("EXPLAIN SELECT * FROM SYSOBJECTS")); + } + + @Test + void ordinarySqlShouldDelegateToDefaultExecutionInsideDmExecutor() throws Exception { + try (Connection connection = DriverManager.getConnection("jdbc:h2:mem:dm_executor_select")) { + putContext(connection); + List results = executeMulti("SELECT 1 AS ID", connection); + + assertEquals(1, results.size()); + assertEquals("1", results.get(0).getDataList().get(0).get(0).getValue()); + } + } + + @Test + void explainShouldCallGetExplainInfoInsteadOfPreparedStatementExecute() throws Exception { + Connection connection = dmExplainConnection(); + + List results = executeMulti("EXPLAIN SELECT * FROM SYSOBJECTS", connection); + + assertEquals(1, results.size()); + assertEquals("plan for: SELECT * FROM SYSOBJECTS", results.get(0).getDataList().get(0).get(0).getValue()); + } + + @Test + void explainButtonShouldBuildExplainOnceAndCallGetExplainInfo() throws Exception { + Connection connection = dmExplainConnection(); + putContext(connection); + + SqlExecuteRequest request = request("SELECT * FROM SYSOBJECTS"); + request.setExplain(true); + + List results = DMCommandExecutor.INSTANCE.execute(request); + + assertEquals(1, results.size()); + assertEquals(ai.chat2db.community.domain.api.enums.parser.SqlTypeEnum.EXPLAIN.name(), + results.get(0).getSqlType()); + // non-streaming path will add row-number, so plan is in the 2nd column. + assertEquals("plan for: SELECT * FROM SYSOBJECTS", results.get(0).getDataList().get(0).get(1).getValue()); + assertNotNull(results.get(0).getExecutionMetrics()); + assertNotNull(results.get(0).getExecutionMetrics().getTotalDurationMs()); + assertEquals(1, results.get(0).getExecutionMetrics().getFetchedRowCount()); + } + + @Test + void explainButtonStreamingShouldKeepSqlTypeExplain() throws Exception { + Connection connection = dmExplainConnection(); + putContext(connection); + + SqlExecuteRequest request = request("SELECT * FROM SYSOBJECTS"); + request.setExplain(true); + CapturingResultConsumer consumer = new CapturingResultConsumer(); + + DMCommandExecutor.INSTANCE.executeStreaming(request, consumer, null, () -> false); + + assertEquals(1, consumer.resultStartedCount); + assertEquals(0, consumer.resultStartedRowCount); + assertEquals(1, consumer.rowsEventCount); + assertEquals(1, consumer.receivedRows.size()); + assertEquals("plan for: SELECT * FROM SYSOBJECTS", consumer.receivedRows.get(0).get(1).getValue()); + assertEquals(1, consumer.resultFinished.size()); + assertEquals(ai.chat2db.community.domain.api.enums.parser.SqlTypeEnum.EXPLAIN.name(), + consumer.resultFinished.get(0).getSqlType()); + assertNotNull(consumer.resultFinished.get(0).getExecutionMetrics()); + assertEquals(1, consumer.resultFinished.get(0).getExecutionMetrics().getFetchedRowCount()); + } + + @Test + void shouldStreamExplainPlanWithRowNumber() throws Exception { + Connection connection = dmExplainConnection(); + putContext(connection); + + SqlExecuteRequest request = request("EXPLAIN SELECT * FROM SYSOBJECTS"); + CapturingResultConsumer consumer = new CapturingResultConsumer(); + + DMCommandExecutor.INSTANCE.executeStreaming(request, consumer, null, () -> false); + + // Outer executeStreaming owns the single resultFinished; publishMaterializedQueryResult must not duplicate it. + assertEquals(1, consumer.resultFinished.size()); + assertEquals(1, consumer.resultStartedCount); + assertEquals(0, consumer.resultStartedRowCount); + assertEquals(1, consumer.rowsEventCount); + assertEquals(1, consumer.receivedRows.size()); + assertEquals("plan for: SELECT * FROM SYSOBJECTS", consumer.receivedRows.get(0).get(1).getValue()); + + ExecuteResponse response = consumer.resultFinished.get(0); + assertEquals(ai.chat2db.community.domain.api.enums.parser.SqlTypeEnum.EXPLAIN.name(), response.getSqlType()); + assertTrue(response.getHeaderList().size() >= 2); + assertEquals("Execution Plan", response.getHeaderList().get(response.getHeaderList().size() - 1).getName()); + assertEquals("plan for: SELECT * FROM SYSOBJECTS", response.getDataList().get(0).get(1).getValue()); + assertNotNull(response.getExecutionMetrics()); + assertNotNull(response.getExecutionMetrics().getTotalDurationMs()); + assertEquals(1, response.getExecutionMetrics().getFetchedRowCount()); + } + + @Test + void ordinarySqlShouldSkipFullAntlrParseAtExecutorEntry() { + DMExecutableSql parsed = new DMSqlParser().parseExecutableSql("SELECT 1 AS ID FROM DUAL"); + + assertTrue(!parsed.isExplain()); + assertEquals("SELECT 1 AS ID FROM DUAL", parsed.executableSql()); + } + + @Test + void invalidExplainShouldNotFallBackToJdbcPath() { + assertThrows(IllegalArgumentException.class, + () -> new DMSqlParser().parseExecutableSql("EXPLAIN PLAN FOR SELECT 1 FROM DUAL")); + } + + @Test + void explainShouldWorkThroughStatementGuardAndConnectionOnlyWrapper() throws Exception { + List guardedSql = new ArrayList<>(); + Connection dmdb = dmExplainConnection(); + Connection poolWrapped = connectionOnlyWrapper(dmdb); + putContext(poolWrapped); + + try (Chat2DBContext.StatementGuardScope ignored = Chat2DBContext.bindStatementGuard(guardedSql::add)) { + SqlExecuteRequest request = request("EXPLAIN SELECT * FROM SYSOBJECTS"); + List results = DMCommandExecutor.INSTANCE.execute(request); + + assertEquals(1, results.size()); + assertEquals("plan for: SELECT * FROM SYSOBJECTS", results.get(0).getDataList().get(0).get(1).getValue()); + assertTrue(guardedSql.contains("EXPLAIN SELECT * FROM SYSOBJECTS")); + } + } + + @Test + void shouldHandleMultiStatementsWithExplainAndNormalSelect() throws Exception { + try (Connection h2Connection = DriverManager.getConnection("jdbc:h2:mem:dm_multi_statement")) { + Connection hybrid = dmHybridConnection(h2Connection); + putContext(hybrid); + + SqlExecuteRequest request = request("EXPLAIN SELECT * FROM SYSOBJECTS; SELECT 1 AS ID"); + request.setExplain(false); + request.setSingle(false); + + List results = DMCommandExecutor.INSTANCE.execute(request); + + // Should return at least 2 execute results: one EXPLAIN plan and one SELECT result. + assertTrue(results.size() >= 2); + + String explainPlan = results.stream() + .flatMap(r -> r.getDataList().stream()) + .filter(row -> row.size() >= 2 && "plan for: SELECT * FROM SYSOBJECTS".equals(row.get(1).getValue())) + .map(row -> row.get(1).getValue()) + .findFirst() + .orElse(null); + assertEquals("plan for: SELECT * FROM SYSOBJECTS", explainPlan); + + String selectValue = results.stream() + .flatMap(r -> r.getDataList().stream()) + .filter(row -> row.size() >= 2 && "1".equals(row.get(1).getValue())) + .map(row -> row.get(1).getValue()) + .findFirst() + .orElse(null); + assertEquals("1", selectValue); + } + } + + @Test + void streamingCancellationShouldStopAfterSecondCancellationCheck() throws Exception { + Connection connection = dmExplainConnection(); + + CapturingResultConsumer consumer = new CapturingResultConsumer(); + AtomicInteger streamResultSequence = new AtomicInteger(0); + ISqlExecutionCancellation cancellation = () -> streamResultSequence.incrementAndGet() >= 2; + + SimpleSqlStatement statement = new SimpleSqlStatement("EXPLAIN SELECT * FROM SYSOBJECTS"); + + var method = DMCommandExecutor.class.getDeclaredMethod("executeMultiStreaming", + SimpleSqlStatement.class, + Connection.class, + boolean.class, + Integer.class, + Integer.class, + Integer.class, + ISqlExecutionResultConsumer.class, + ISqlExecutionStatementListener.class, + ISqlExecutionCancellation.class, + SqlTypeEnum.class, + String.class, + int.class, + int.class, + AtomicInteger.class, + int.class, + ExecutionContext.class); + method.setAccessible(true); + + InvocationTargetException exception = assertThrows(InvocationTargetException.class, + () -> method.invoke(DMCommandExecutor.INSTANCE, + statement, + connection, + true, + 0, + 10, + null, + consumer, + null, + cancellation, + null, + statement.getSql(), + 1, + 10, + streamResultSequence, + 1, + null)); + assertTrue(exception.getCause() instanceof SQLException); + assertEquals(0, consumer.resultFinished.size()); + assertEquals(0, consumer.rowsEventCount); + } + + @Test + void explainShouldResolveDmdbConnectionFromIndependentDriverClassLoader() throws Exception { + try (URLClassLoader driverClassLoader = newDriverIsolatingClassLoader()) { + Class isolatedDmConnection = Class.forName( + "dm.jdbc.driver.DmdbConnection", true, driverClassLoader); + assertNotSame(DmdbConnection.class, isolatedDmConnection, + "driver ClassLoader must define its own DmdbConnection type"); + + Connection connection = (Connection) Proxy.newProxyInstance( + driverClassLoader, + new Class[]{Connection.class, isolatedDmConnection}, + (proxy, method, args) -> switch (method.getName()) { + case "unwrap" -> unwrap(proxy, (Class) args[0]); + case "isWrapperFor" -> ((Class) args[0]).isInstance(proxy); + case "getExplainInfo" -> "isolated-plan:" + args[0]; + case "isClosed" -> false; + default -> defaultValue(method.getReturnType()); + }); + + assertNotSame(getClass().getClassLoader(), connection.getClass().getClassLoader()); + putContext(connection); + + List results = DMCommandExecutor.INSTANCE.execute( + request("EXPLAIN SELECT * FROM SYSOBJECTS")); + + assertEquals(1, results.size()); + assertEquals("isolated-plan:SELECT * FROM SYSOBJECTS", + results.get(0).getDataList().get(0).get(1).getValue()); + } + } + + @Test + void explainDriverSqlExceptionShouldSurfaceOnNonStreamingPath() throws Exception { + Connection connection = dmExplainConnectionThrowing( + new SQLException("DM-EXPLAIN-ERR-2762", "HY000", 2762)); + putContext(connection); + + List results = DMCommandExecutor.INSTANCE.execute( + request("EXPLAIN SELECT * FROM SYSOBJECTS")); + + assertEquals(1, results.size()); + assertEquals(Boolean.FALSE, results.get(0).getSuccess()); + assertTrue(results.get(0).getMessage().contains("DM-EXPLAIN-ERR-2762")); + } + + @Test + void explainDriverSqlExceptionShouldFinishStreamingPath() throws Exception { + Connection connection = dmExplainConnectionThrowing( + new SQLException("DM-EXPLAIN-ERR-STREAM", "HY000", 2763)); + putContext(connection); + + CapturingResultConsumer consumer = new CapturingResultConsumer(); + DMCommandExecutor.INSTANCE.executeStreaming( + request("EXPLAIN SELECT * FROM SYSOBJECTS"), consumer, null, () -> false); + + assertEquals(0, consumer.resultStartedCount); + assertEquals(0, consumer.rowsEventCount); + assertEquals(1, consumer.statementFinishedCount); + assertEquals(1, consumer.resultFinished.size()); + ExecuteResponse finished = consumer.resultFinished.get(0); + assertEquals(Boolean.FALSE, finished.getSuccess()); + assertTrue(finished.getMessage().contains("DM-EXPLAIN-ERR-STREAM")); + } + + @SuppressWarnings("unchecked") + private List executeMulti(String sql, Connection connection) throws Exception { + Method method = DMCommandExecutor.class.getDeclaredMethod("executeMulti", SimpleSqlStatement.class, + Connection.class, boolean.class, Integer.class, Integer.class, Integer.class, + ai.chat2db.community.domain.api.model.result.ExecutionContext.class); + method.setAccessible(true); + return (List) method.invoke(DMCommandExecutor.INSTANCE, + new SimpleSqlStatement(sql), connection, true, 0, 10, null, null); + } + + private Connection dmExplainConnection() { + return dmExplainConnection(null); + } + + private Connection dmExplainConnectionThrowing(SQLException failure) { + return dmExplainConnection(failure); + } + + private Connection dmExplainConnection(SQLException failure) { + return (Connection) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[]{Connection.class, DmdbConnection.class}, + (proxy, method, args) -> { + if ("getExplainInfo".equals(method.getName())) { + if (failure != null) { + throw failure; + } + return "plan for: " + args[0]; + } + return switch (method.getName()) { + case "unwrap" -> unwrap(proxy, (Class) args[0]); + case "isWrapperFor" -> ((Class) args[0]).isInstance(proxy); + case "isClosed" -> false; + default -> defaultValue(method.getReturnType()); + }; + }); + } + + /** + * Child-first loader that defines {@code dm.jdbc.driver.*} from its own URLs, + * mimicking Chat2DB's independent JDBC driver ClassLoader. + */ + private URLClassLoader newDriverIsolatingClassLoader() throws Exception { + URL classResource = getClass().getClassLoader().getResource("dm/jdbc/driver/DmdbConnection.class"); + assertNotNull(classResource, "test stub DmdbConnection.class must be on the test classpath"); + + Path root = Files.createTempDirectory("dm-driver-isolation"); + Path packageDir = root.resolve("dm/jdbc/driver"); + Files.createDirectories(packageDir); + try (var in = classResource.openStream()) { + Files.copy(in, packageDir.resolve("DmdbConnection.class")); + } + + return new URLClassLoader(new URL[]{root.toUri().toURL()}, getClass().getClassLoader()) { + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("dm.jdbc.driver.")) { + Class loaded = findLoadedClass(name); + if (loaded == null) { + loaded = findClass(name); + } + if (resolve) { + resolveClass(loaded); + } + return loaded; + } + return super.loadClass(name, resolve); + } + }; + } + + private Connection dmHybridConnection(Connection delegate) { + return (Connection) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[]{Connection.class, DmdbConnection.class}, + (proxy, method, args) -> { + String name = method.getName(); + if ("getExplainInfo".equals(name)) { + return "plan for: " + args[0]; + } + if ("unwrap".equals(name)) { + Class target = (Class) args[0]; + if (target == Connection.class || target.isInstance(proxy) || target == DmdbConnection.class) { + return proxy; + } + return delegate.unwrap(target); + } + if ("isWrapperFor".equals(name)) { + Class target = (Class) args[0]; + return target == Connection.class || target == DmdbConnection.class + || target.isInstance(proxy) || delegate.isWrapperFor(target); + } + if ("isClosed".equals(name)) { + return delegate.isClosed(); + } + if (method.getDeclaringClass() == Object.class) { + return switch (name) { + case "equals" -> proxy == args[0]; + case "hashCode" -> System.identityHashCode(proxy); + case "toString" -> "DMHybridConnection"; + default -> throw new IllegalStateException("Unexpected Object method: " + name); + }; + } + return method.invoke(delegate, args); + }); + } + + private Connection connectionOnlyWrapper(Connection delegate) { + return (Connection) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[]{Connection.class}, + (proxy, method, args) -> { + String name = method.getName(); + if ("unwrap".equals(name)) { + Class target = (Class) args[0]; + if (target == Connection.class) { + return proxy; + } + if (target.isInstance(delegate) || target == DmdbConnection.class) { + return delegate; + } + return delegate.unwrap(target); + } + if ("isWrapperFor".equals(name)) { + Class target = (Class) args[0]; + return target == Connection.class + || target.isInstance(delegate) + || target == DmdbConnection.class + || delegate.isWrapperFor(target); + } + if (method.getDeclaringClass() == Object.class) { + return switch (name) { + case "equals" -> proxy == args[0]; + case "hashCode" -> System.identityHashCode(proxy); + case "toString" -> "ConnectionOnlyWrapper"; + default -> throw new IllegalStateException("Unexpected Object method: " + name); + }; + } + return method.invoke(delegate, args); + }); + } + + private void putContext(Connection connection) { + previousPlugin = Chat2DBContext.PLUGIN_MAP.put("DM", new DMPlugin()); + contextBound = true; + ConnectInfo connectInfo = new ConnectInfo(); + connectInfo.setDataSourceId(DATA_SOURCE_ID); + connectInfo.setConsoleId(1L); + connectInfo.setDbType("DM"); + connectInfo.setDatabaseName(""); + connectInfo.setSchemaName("SYSDBA"); + connectInfo.setUrl("jdbc:test:dm"); + connectInfo.setConnection(connection); + DriverConfig driverConfig = new DriverConfig(); + driverConfig.setDbType("DM"); + connectInfo.setDriverConfig(driverConfig); + Chat2DBContext.putContext(connectInfo); + } + + private SqlExecuteRequest request(String sql) { + SqlExecuteRequest request = new SqlExecuteRequest(); + request.setScript(sql); + request.setConsoleId(1L); + request.setDataSourceId(DATA_SOURCE_ID); + request.setDatabaseName(""); + request.setSchemaName("SYSDBA"); + request.setPageNo(1); + request.setPageSize(10); + request.setErrorContinue(Boolean.TRUE); + return request; + } + + private Object unwrap(Object proxy, Class targetClass) throws SQLException { + if (targetClass.isInstance(proxy) || targetClass == Connection.class) { + return proxy; + } + throw new SQLException("Unsupported unwrap target: " + targetClass.getName()); + } + + private Object defaultValue(Class returnType) { + if (!returnType.isPrimitive()) { + return null; + } + if (returnType == boolean.class) { + return false; + } + if (returnType == int.class) { + return 0; + } + if (returnType == long.class) { + return 0L; + } + if (returnType == double.class) { + return 0D; + } + if (returnType == float.class) { + return 0F; + } + if (returnType == short.class) { + return (short) 0; + } + if (returnType == byte.class) { + return (byte) 0; + } + if (returnType == char.class) { + return '\0'; + } + return null; + } + + private static final class CapturingResultConsumer implements ISqlExecutionResultConsumer { + + private int resultStartedCount; + /** Row count carried on resultStarted; should stay 0 for materialized explain streaming. */ + private int resultStartedRowCount; + private int rowsEventCount; + private int statementFinishedCount; + private final List> receivedRows = new ArrayList<>(); + private final List resultFinished = new ArrayList<>(); + + @Override + public void statementStarted(String sql, String originalSql, String comment) { + } + + @Override + public void resultStarted(ExecuteResponse result) { + resultStartedCount++; + resultStartedRowCount = result.getDataList() == null ? 0 : result.getDataList().size(); + } + + @Override + public void rows(ExecuteResponse result, List> rows) { + rowsEventCount++; + receivedRows.addAll(rows); + } + + @Override + public void resultFinished(ExecuteResponse result) { + resultFinished.add(result); + } + + @Override + public void updateCount(ExecuteResponse result) { + } + + @Override + public void statementFinished(String sql, long duration) { + statementFinishedCount++; + } + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/dm/jdbc/driver/DmdbConnection.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/dm/jdbc/driver/DmdbConnection.java new file mode 100644 index 0000000000..13c008c300 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/dm/jdbc/driver/DmdbConnection.java @@ -0,0 +1,9 @@ +package dm.jdbc.driver; + +import java.sql.Connection; +import java.sql.SQLException; + +public interface DmdbConnection extends Connection { + + String getExplainInfo(String sql) throws SQLException; +} diff --git a/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/DefaultSQLExecutor.java b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/DefaultSQLExecutor.java index 346659bd59..54ab64f821 100644 --- a/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/DefaultSQLExecutor.java +++ b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/DefaultSQLExecutor.java @@ -1316,6 +1316,31 @@ private ExecuteResponse streamQueryExecuteResponse(Statement stmt, boolean limit return executeResult; } + /** + * Publishes an already-materialized query result on the streaming path. + * Sends an empty {@code dataList} with {@code resultStarted}, then the real + * rows via {@code rows}, matching {@link #streamQueryExecuteResponse}. + * The caller ({@link #executeStreaming}) remains responsible for a single + * {@code resultFinished}. + */ + protected void publishMaterializedQueryResult(ExecuteResponse response, + ISqlExecutionResultConsumer consumer, + AtomicInteger streamResultSequence, + int statementSequence, + int pageNo, + int pageSize) { + response.setStatementSequence(statementSequence); + setStreamResultId(response, streamResultSequence.incrementAndGet()); + addRowNumber(response, pageNo, pageSize); + List> numberedRows = response.getDataList() == null + ? new ArrayList<>() + : new ArrayList<>(response.getDataList()); + response.setDataList(new ArrayList<>()); + consumer.resultStarted(response); + consumer.rows(response, numberedRows); + response.setDataList(numberedRows); + } + private void setStreamResultId(ExecuteResponse executeResult, int streamResultId) { Map extra = executeResult.getExtra(); if (extra == null) { diff --git a/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/sql/Chat2DBContext.java b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/sql/Chat2DBContext.java index 4f0e028538..5dbeee541c 100644 --- a/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/sql/Chat2DBContext.java +++ b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/sql/Chat2DBContext.java @@ -139,6 +139,13 @@ public static StatementGuardScope bindStatementGuard(Consumer statementG return new StatementGuardScope(previous); } + public static void guardStatement(String sql) { + Consumer statementGuard = STATEMENT_GUARD_THREAD_LOCAL.get(); + if (statementGuard != null) { + statementGuard.accept(sql); + } + } + public static String getDbVersion() { ConnectInfo connectInfo = getConnectInfo(); From 44de376960f786fdee68ca810a325c4b1f60e2b6 Mon Sep 17 00:00:00 2001 From: hhz <1839462565@qq.com> Date: Fri, 28 Aug 2026 16:52:24 +0800 Subject: [PATCH 15/49] test(dm): cover EXPLAIN statement classification --- .../dm/parser/visitor/DMParserVisitor.java | 2 + .../parser/visitor/DMSimpleParserVisitor.java | 2 + .../parser/visitor/DMValidTableVisitor.java | 2 + .../ai/chat2db/plugin/dm/DMSqlParserTest.java | 52 +++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMSqlParserTest.java diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMParserVisitor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMParserVisitor.java index 1ce02754e1..96a87e3c5e 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMParserVisitor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMParserVisitor.java @@ -75,6 +75,8 @@ public Void visitExplain_statement(DMParser.Explain_statementContext ctx) { if (Objects.isNull(currentStatement)) { return null; } + // Statement#setType is first-write-wins. Set the outer type before visiting the + // inner DML so EXPLAIN remains the statement type while identifiers are collected. currentStatement.setType(SqlTypeEnum.EXPLAIN.name()); return super.visitExplain_statement(ctx); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMSimpleParserVisitor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMSimpleParserVisitor.java index b62a39ef05..6d94a7a66f 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMSimpleParserVisitor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMSimpleParserVisitor.java @@ -71,6 +71,8 @@ public Void visitExplain_statement(DMParser.Explain_statementContext ctx) { if (Objects.isNull(currentStatement)) { return null; } + // Statement#setType is first-write-wins. Set the outer type before visiting the + // inner DML so EXPLAIN remains the statement type. currentStatement.setType(SqlTypeEnum.EXPLAIN.name()); return super.visitExplain_statement(ctx); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMValidTableVisitor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMValidTableVisitor.java index 364f558c31..78bda28044 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMValidTableVisitor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMValidTableVisitor.java @@ -1201,6 +1201,8 @@ public Void visitExplain_statement(DMParser.Explain_statementContext ctx) { if (Objects.isNull(currentStatement)) { return null; } + // Statement#setType is first-write-wins. Set the outer type before visiting the + // inner DML so EXPLAIN remains the statement type while table metadata is collected. currentStatement.setType(SqlTypeEnum.EXPLAIN.name()); return super.visitExplain_statement(ctx); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMSqlParserTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMSqlParserTest.java new file mode 100644 index 0000000000..98fd157248 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMSqlParserTest.java @@ -0,0 +1,52 @@ +package ai.chat2db.plugin.dm; + +import ai.chat2db.community.domain.api.enums.parser.SqlTypeEnum; +import ai.chat2db.community.domain.api.model.parser.result.SqlParserResponse; +import ai.chat2db.community.domain.api.model.parser.statement.Statement; +import ai.chat2db.plugin.dm.parser.DMSqlParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DMSqlParserTest { + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("explainStatements") + void parserStatementsShouldPreserveExplainAsOuterStatementType(String sql) { + assertExplainType(new DMSqlParser().parserStatements(sql), sql); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("explainStatements") + void simpleParserStatementsShouldPreserveExplainAsOuterStatementType(String sql) { + assertExplainType(new DMSqlParser().simpleParserStatements(sql), sql); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("explainStatements") + void validTableStatementsShouldPreserveExplainAsOuterStatementType(String sql) { + assertExplainType(new DMSqlParser().validTableStatements(sql), sql); + } + + private static Stream explainStatements() { + return Stream.of( + "EXPLAIN SELECT * FROM SYSOBJECTS", + "EXPLAIN UPDATE SYSOBJECTS SET NAME = 'UPDATED' WHERE ID = 1", + "EXPLAIN DELETE FROM SYSOBJECTS WHERE ID = 1", + "EXPLAIN INSERT INTO SYSOBJECTS (ID, NAME) VALUES (1, 'CREATED')", + "EXPLAIN MERGE INTO TARGET_TABLE T USING SOURCE_TABLE S ON (T.ID = S.ID) " + + "WHEN MATCHED THEN UPDATE SET T.NAME = S.NAME" + ); + } + + private void assertExplainType(SqlParserResponse response, String sql) { + List statements = response.getStatements(); + + assertEquals(1, statements.size(), sql); + assertEquals(SqlTypeEnum.EXPLAIN.name(), statements.get(0).getType(), sql); + } +} From 6a8950c4042f1d38b5691ca5d172685f299d9f02 Mon Sep 17 00:00:00 2001 From: hhz <1839462565@qq.com> Date: Fri, 28 Aug 2026 17:04:10 +0800 Subject: [PATCH 16/49] fix(dm): preserve EXPLAIN parser statement type --- .../ai/chat2db/plugin/dm/parser/visitor/DMParserVisitor.java | 4 +--- .../plugin/dm/parser/visitor/DMSimpleParserVisitor.java | 4 +--- .../chat2db/plugin/dm/parser/visitor/DMValidTableVisitor.java | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMParserVisitor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMParserVisitor.java index 96a87e3c5e..f58273c14c 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMParserVisitor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMParserVisitor.java @@ -75,10 +75,8 @@ public Void visitExplain_statement(DMParser.Explain_statementContext ctx) { if (Objects.isNull(currentStatement)) { return null; } - // Statement#setType is first-write-wins. Set the outer type before visiting the - // inner DML so EXPLAIN remains the statement type while identifiers are collected. currentStatement.setType(SqlTypeEnum.EXPLAIN.name()); - return super.visitExplain_statement(ctx); + return null; } @Override diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMSimpleParserVisitor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMSimpleParserVisitor.java index 6d94a7a66f..b8fa570f9c 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMSimpleParserVisitor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMSimpleParserVisitor.java @@ -71,10 +71,8 @@ public Void visitExplain_statement(DMParser.Explain_statementContext ctx) { if (Objects.isNull(currentStatement)) { return null; } - // Statement#setType is first-write-wins. Set the outer type before visiting the - // inner DML so EXPLAIN remains the statement type. currentStatement.setType(SqlTypeEnum.EXPLAIN.name()); - return super.visitExplain_statement(ctx); + return null; } @Override diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMValidTableVisitor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMValidTableVisitor.java index 78bda28044..5179a28333 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMValidTableVisitor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/parser/visitor/DMValidTableVisitor.java @@ -1201,9 +1201,7 @@ public Void visitExplain_statement(DMParser.Explain_statementContext ctx) { if (Objects.isNull(currentStatement)) { return null; } - // Statement#setType is first-write-wins. Set the outer type before visiting the - // inner DML so EXPLAIN remains the statement type while table metadata is collected. currentStatement.setType(SqlTypeEnum.EXPLAIN.name()); - return super.visitExplain_statement(ctx); + return null; } } From 904ad5763c63c8b17e91c00306f9c3a6eff8856f Mon Sep 17 00:00:00 2001 From: zgq Date: Mon, 31 Aug 2026 18:11:22 +0800 Subject: [PATCH 17/49] feat(ai): add chat history navigation and renaming --- chat2db-community-client/package.json | 2 +- .../scripts/i18n-source-hashes.json | 4 +- .../MessageNavigationRail/index.tsx | 73 +++++++++ .../components/MessageNavigationRail/style.ts | 106 +++++++++++++ .../src/blocks/AI/index.tsx | 143 ++++++++++++++++-- .../src/blocks/AI/messageNavigation.test.ts | 31 ++++ .../src/blocks/AI/messageNavigation.ts | 56 +++++++ .../src/blocks/AI/style.ts | 79 +++++++++- .../components/InlineRenameInput/index.tsx | 88 +++++++++++ .../src/i18n/en-US/stream.ts | 3 + .../src/i18n/es-ES/stream.ts | 3 + .../src/i18n/ja-JP/stream.ts | 3 + .../src/i18n/ko-KR/stream.ts | 3 + .../src/i18n/zh-CN/stream.ts | 3 + .../src/pages/main/CommunityMainPage.tsx | 18 +++ .../main/components/StreamSidebar/index.tsx | 38 ++++- .../main/components/StreamSidebar/style.ts | 7 +- .../src/service/aiStream.ts | 4 + .../api/service/ai/IAiChatHistoryService.java | 9 ++ .../impl/ai/AiChatHistoryServiceImpl.java | 16 ++ .../impl/ai/AiChatHistoryServiceImplTest.java | 29 ++++ .../web/api/controller/AiChatController.java | 14 ++ .../ai/AiChatSessionRenameRequest.java | 16 ++ .../ai/AiChatStreamAdapterScenarioTest.java | 4 + 24 files changed, 729 insertions(+), 23 deletions(-) create mode 100644 chat2db-community-client/src/blocks/AI/components/MessageNavigationRail/index.tsx create mode 100644 chat2db-community-client/src/blocks/AI/components/MessageNavigationRail/style.ts create mode 100644 chat2db-community-client/src/blocks/AI/messageNavigation.test.ts create mode 100644 chat2db-community-client/src/blocks/AI/messageNavigation.ts create mode 100644 chat2db-community-client/src/components/InlineRenameInput/index.tsx create mode 100644 chat2db-community-server/chat2db-community-web/src/main/java/ai/chat2db/community/web/api/model/request/ai/AiChatSessionRenameRequest.java diff --git a/chat2db-community-client/package.json b/chat2db-community-client/package.json index 80b981cba3..8b4f6a0a61 100644 --- a/chat2db-community-client/package.json +++ b/chat2db-community-client/package.json @@ -25,7 +25,7 @@ "lint:style": "stylelint \"src/**/*.{css,less}\" --max-warnings=0", "test:i18n": "node ./scripts/validate-i18n.cjs", "test:community-boundary": "node ./scripts/verify-community-boundary.cjs", - "test:retired-ai": "tsx src/blocks/AI/retiredAiSurface.test.ts && tsx src/blocks/AI/components/AIChatInput/mentionSelection.test.ts && tsx src/blocks/AI/knowledgeSelection.test.ts", + "test:retired-ai": "tsx src/blocks/AI/retiredAiSurface.test.ts && tsx src/blocks/AI/components/AIChatInput/mentionSelection.test.ts && tsx src/blocks/AI/knowledgeSelection.test.ts && tsx src/blocks/AI/messageNavigation.test.ts", "test:hot-update": "tsx src/store/global/slices/hotUpdate/action.test.ts", "test:result-pagination": "tsx src/blocks/SearchResult/components/ResultSet/pagination.test.ts", "test:tree-node-path": "tsx src/blocks/NewTree/utils/treeNodePath.test.ts", diff --git a/chat2db-community-client/scripts/i18n-source-hashes.json b/chat2db-community-client/scripts/i18n-source-hashes.json index 648acb4cf0..d8044e9e41 100644 --- a/chat2db-community-client/scripts/i18n-source-hashes.json +++ b/chat2db-community-client/scripts/i18n-source-hashes.json @@ -20,7 +20,7 @@ "setting.ts": "14aea4aebdbe5cff2ae9ba4bf955aa42a95ac23d1950ea69291b22c29330d7cf", "spaceSetting.ts": "defdf3138e5e1782f0282760bcef4ce1b8550cc80b03052fea468d97165cb980", "sqlEditor.ts": "5c41e5f24d5ba78909795bcc681cabb8460cca075233b665dc4940afe82ce0c9", - "stream.ts": "8418dd25b0fd54b2ef2b15a2573edf0a4876efb4e69936ff82a157ea5cf42be4", + "stream.ts": "2e2fca0b1ba1ecd5a1501b767be8857cf0b72d800d69a4acf9cd62a61d5ceaf5", "workspace.ts": "ff3ba00ee05c5e04c6ccaef4083a83e4f01b5307eff17b9afc6480525a9ef141" }, "ko-KR": { @@ -41,7 +41,7 @@ "setting.ts": "14aea4aebdbe5cff2ae9ba4bf955aa42a95ac23d1950ea69291b22c29330d7cf", "spaceSetting.ts": "defdf3138e5e1782f0282760bcef4ce1b8550cc80b03052fea468d97165cb980", "sqlEditor.ts": "5c41e5f24d5ba78909795bcc681cabb8460cca075233b665dc4940afe82ce0c9", - "stream.ts": "8418dd25b0fd54b2ef2b15a2573edf0a4876efb4e69936ff82a157ea5cf42be4", + "stream.ts": "2e2fca0b1ba1ecd5a1501b767be8857cf0b72d800d69a4acf9cd62a61d5ceaf5", "workspace.ts": "ff3ba00ee05c5e04c6ccaef4083a83e4f01b5307eff17b9afc6480525a9ef141" } } diff --git a/chat2db-community-client/src/blocks/AI/components/MessageNavigationRail/index.tsx b/chat2db-community-client/src/blocks/AI/components/MessageNavigationRail/index.tsx new file mode 100644 index 0000000000..41159174ae --- /dev/null +++ b/chat2db-community-client/src/blocks/AI/components/MessageNavigationRail/index.tsx @@ -0,0 +1,73 @@ +import { memo, useState, type CSSProperties } from 'react'; +import { cx } from 'antd-style'; + +import i18n from '@/i18n'; +import { + getMessageNavigationRailWidth, + type UserMessageNavigationItem, +} from '../../messageNavigation'; +import { useStyles } from './style'; + +interface MessageNavigationRailProps { + items: UserMessageNavigationItem[]; + onNavigate: (messageId: string) => void; +} + +const MessageNavigationRail = ({ items, onNavigate }: MessageNavigationRailProps) => { + const { styles } = useStyles(); + const [selectedIndex, setSelectedIndex] = useState(-1); + const [railScrollTop, setRailScrollTop] = useState(0); + const activeIndex = selectedIndex < items.length ? selectedIndex : -1; + const selectedItem = activeIndex >= 0 ? items[activeIndex] : null; + + if (!items.length) { + return null; + } + + return ( +
setSelectedIndex(-1)} + onBlur={(event) => { + if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { + setSelectedIndex(-1); + } + }} + > +
setRailScrollTop(event.currentTarget.scrollTop)} + > + {items.map((item, itemIndex) => ( + + ))} +
+ {selectedItem ? ( +
+
{selectedItem.label}
+ {selectedItem.assistantPreview ? ( +
{selectedItem.assistantPreview}
+ ) : null} +
+ ) : null} +
+ ); +}; + +export default memo(MessageNavigationRail); diff --git a/chat2db-community-client/src/blocks/AI/components/MessageNavigationRail/style.ts b/chat2db-community-client/src/blocks/AI/components/MessageNavigationRail/style.ts new file mode 100644 index 0000000000..594007d051 --- /dev/null +++ b/chat2db-community-client/src/blocks/AI/components/MessageNavigationRail/style.ts @@ -0,0 +1,106 @@ +import { createStyles } from 'antd-style'; + +export const useStyles = createStyles(({ css, token }) => ({ + rail: css` + position: absolute; + z-index: 6; + top: 50%; + left: max(8px, calc(50% - 448px)); + transform: translateY(-50%); + width: 36px; + padding: 6px 0; + opacity: 0.72; + transition: opacity 0.15s ease; + + &:hover, + &:focus-within { + opacity: 1; + } + `, + list: css` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + width: 36px; + max-height: min(240px, 60vh); + overflow-y: auto; + overflow-x: hidden; + `, + item: css` + display: flex; + align-items: center; + width: 34px; + height: 12px; + padding: 0; + border: 0; + background: transparent; + cursor: pointer; + + > span { + display: block; + width: var(--message-navigation-rail-width, 8px); + height: 2px; + border-radius: 1px; + background: ${token.colorTextQuaternary}; + opacity: 0.55; + transition: background-color 0.1s ease, opacity 0.1s ease; + } + + &:hover > span, + &:focus-visible > span { + width: 22px; + background: ${token.colorTextSecondary}; + opacity: 1; + } + + `, + itemActive: css` + > span, + &:hover > span, + &:focus-visible > span { + width: 30px; + height: 3px; + background: ${token.colorText}; + opacity: 1; + } + `, + preview: css` + position: absolute; + left: 44px; + transform: translateY(-50%); + z-index: 1; + width: min(320px, calc(100vw - 96px)); + height: 108px; + min-width: 0; + padding: 10px 12px; + border: 1px solid ${token.colorBorderSecondary}; + border-radius: 8px; + background: ${token.colorBgElevated}; + box-shadow: ${token.boxShadowSecondary}; + pointer-events: none; + box-sizing: border-box; + `, + previewQuestion: css` + display: -webkit-box; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + color: ${token.colorText}; + font-size: 13px; + font-weight: 600; + line-height: 20px; + `, + previewAnswer: css` + display: -webkit-box; + overflow: hidden; + margin-top: 6px; + padding-top: 6px; + border-top: 1px solid ${token.colorBorderSecondary}; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + color: ${token.colorTextSecondary}; + font-size: 12px; + line-height: 18px; + `, +})); diff --git a/chat2db-community-client/src/blocks/AI/index.tsx b/chat2db-community-client/src/blocks/AI/index.tsx index efa6f8ad13..89c6caf687 100644 --- a/chat2db-community-client/src/blocks/AI/index.tsx +++ b/chat2db-community-client/src/blocks/AI/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Dropdown, Flex, Modal } from 'antd'; import feedback from '@/utils/feedback'; import { @@ -52,6 +52,10 @@ import { toKnowledgeSelectionReferences } from './knowledgeSelection'; import { buildWorkspaceObjectTabTitle } from '@/utils/workspaceObjectTabTitle'; import type { IConnectionEnv } from '@/typings'; import { resolveAIDataSourceContext } from './dataSourceContext'; +import { buildUserMessageNavigationItems } from './messageNavigation'; +import { Pencil } from 'lucide-react'; +import MessageNavigationRail from './components/MessageNavigationRail'; +import InlineRenameInput from '@/components/InlineRenameInput'; /** detects unclosed text in flowing text ```chart block, return chart and whether there are any unfinished diagrams */ function splitIncompleteChartBlock(text: string): { textBeforeChart: string; hasIncompleteChart: boolean } { @@ -534,6 +538,7 @@ export default function AI({ variant = 'page', onTableClick, onPinSql, onSession questionType?: QuestionType; } | null>(null); const [currentRoundUserMessageId, setCurrentRoundUserMessageId] = useState(null); + const [highlightedUserMessageId, setHighlightedUserMessageId] = useState(null); const [messageListContentHeight, setMessageListContentHeight] = useState(0); // Session management. @@ -541,6 +546,7 @@ export default function AI({ variant = 'page', onTableClick, onPinSql, onSession const [currentSessionTitle, setCurrentSessionTitle] = useState(''); const [openSettings, setOpenSettings] = useState(false); const [sessionLoading, setSessionLoading] = useState(false); + const [panelRenamingSessionId, setPanelRenamingSessionId] = useState(null); const isEmptyState = !messages.length && !streamingText && !streamTraceEntries.length; const streamingRef = useRef(''); @@ -570,6 +576,12 @@ export default function AI({ variant = 'page', onTableClick, onPinSql, onSession const topAlignmentTimerRef = useRef(null); const pendingInitialBottomSyncRef = useRef(false); const initialBottomSyncTimerRef = useRef(null); + const messageHighlightTimerRef = useRef(null); + + const userMessageNavigationItems = useMemo( + () => buildUserMessageNavigationItems(messages, i18n('stream.messageNavigation.userMessage')), + [messages, language], + ); const lockScrollTracking = useCallback((behavior: ScrollBehavior = 'auto') => { suppressScrollTrackingRef.current = true; @@ -753,6 +765,22 @@ export default function AI({ variant = 'page', onTableClick, onPinSql, onSession [scrollMessageToTop], ); + const handleNavigateToUserMessage = useCallback( + (messageId: string) => { + setAutoFollow(false); + scrollMessageToTop(messageId); + setHighlightedUserMessageId(messageId); + if (messageHighlightTimerRef.current !== null) { + window.clearTimeout(messageHighlightTimerRef.current); + } + messageHighlightTimerRef.current = window.setTimeout(() => { + setHighlightedUserMessageId(null); + messageHighlightTimerRef.current = null; + }, 1600); + }, + [scrollMessageToTop, setAutoFollow], + ); + const flushPendingBuffer = useCallback(() => { return; }, []); @@ -945,6 +973,10 @@ export default function AI({ variant = 'page', onTableClick, onPinSql, onSession window.clearTimeout(initialBottomSyncTimerRef.current); initialBottomSyncTimerRef.current = null; } + if (messageHighlightTimerRef.current !== null) { + window.clearTimeout(messageHighlightTimerRef.current); + messageHighlightTimerRef.current = null; + } }; }, []); @@ -1236,6 +1268,11 @@ export default function AI({ variant = 'page', onTableClick, onPinSql, onSession currentSessionTitleRef.current = ''; setCurrentRoundUserMessageId(null); currentRoundUserMessageIdRef.current = null; + setHighlightedUserMessageId(null); + if (messageHighlightTimerRef.current !== null) { + window.clearTimeout(messageHighlightTimerRef.current); + messageHighlightTimerRef.current = null; + } currentRoundBlockRef.current = null; newSessionIdRef.current = null; inProgressSessionRef.current = null; @@ -1245,6 +1282,30 @@ export default function AI({ variant = 'page', onTableClick, onPinSql, onSession onSessionChange?.(); }, [isPanel, clearChatIdFromPath, onSessionChange, stop]); + const startPanelHistoryRename = useCallback((session: IChatSession) => { + setPanelRenamingSessionId(session.id); + }, []); + + const renamePanelHistorySession = useCallback(async (sessionId: string, title: string) => { + try { + await aiStreamService.renameChatSession({ id: sessionId, title }); + setSessionList((prev) => prev.map((item) => (item.id === sessionId ? { ...item, title } : item))); + if (currentSessionIdRef.current === sessionId) { + setCurrentSessionTitle(title); + currentSessionTitleRef.current = title; + } + window.dispatchEvent( + new CustomEvent('stream:sessionRenamed', { + detail: { sessionId, title }, + }), + ); + feedback.success(i18n('common.message.modifySuccessfully')); + } catch (error) { + feedback.error(i18n('stream.sidebar.renameFailed')); + throw error; + } + }, []); + const handleDeleteHistorySession = useCallback( async (sessionId: string) => { try { @@ -1334,6 +1395,11 @@ export default function AI({ variant = 'page', onTableClick, onPinSql, onSession currentSessionTitleRef.current = title || ''; setCurrentRoundUserMessageId(null); currentRoundUserMessageIdRef.current = null; + setHighlightedUserMessageId(null); + if (messageHighlightTimerRef.current !== null) { + window.clearTimeout(messageHighlightTimerRef.current); + messageHighlightTimerRef.current = null; + } currentRoundBlockRef.current = null; const inProgressSession = inProgressSessionRef.current; @@ -1457,6 +1523,23 @@ export default function AI({ variant = 'page', onTableClick, onPinSql, onSession }; }, [isPanel, handleLoadSessionById]); + useEffect(() => { + const handleSessionRenamed = (event: Event) => { + const { sessionId, title } = (event as CustomEvent<{ sessionId: string; title: string }>).detail || {}; + if (!sessionId || !title) { + return; + } + setSessionList((prev) => prev.map((session) => (session.id === sessionId ? { ...session, title } : session))); + if (currentSessionIdRef.current === sessionId) { + setCurrentSessionTitle(title); + currentSessionTitleRef.current = title; + } + }; + + window.addEventListener('stream:sessionRenamed', handleSessionRenamed); + return () => window.removeEventListener('stream:sessionRenamed', handleSessionRenamed); + }, []); + // Send a message. const handleSend = useCallback( @@ -1891,7 +1974,13 @@ export default function AI({ variant = 'page', onTableClick, onPinSql, onSession style={isLastRound && messageListContentHeight > 0 ? { minHeight: messageListContentHeight } : undefined} > {round.user && ( -
setMessageElement(round.user!.id, node)}> +
setMessageElement(round.user!.id, node)} + >
{round.user.attachments?.length ? (
@@ -2006,12 +2095,42 @@ export default function AI({ variant = 'page', onTableClick, onPinSql, onSession
handleLoadSessionById(item.id, item.title)} + onClick={() => { + if (panelRenamingSessionId !== item.id) { + handleLoadSessionById(item.id, item.title); + } + }} > - {item.title || i18n('stream.session.title')} + {panelRenamingSessionId === item.id ? ( + setPanelRenamingSessionId(null)} + onSubmit={(title) => renamePanelHistorySession(item.id, title)} + /> + ) : ( + {item.title || i18n('stream.session.title')} + )} + + + + + + + ), + }, ]; + const lockMetadataMessage = data.find((item) => item.lockMetadataState === 'UNAVAILABLE')?.lockMetadataMessage; return (
@@ -73,17 +238,22 @@ const ActiveTransactionsContent = ({ {i18n('common.button.refresh')}
+ {!error && lockMetadataMessage && ( +
+ {i18n('workspace.ops.lockMetadataDegraded')} {lockMetadataMessage} +
+ )} {error ? (
{error}
) : (
`${record.threadId}-${record.trxId ?? 'ro'}`} + rowKey={getActiveTransactionRowKey} columns={columns} dataSource={data} loading={loading} pagination={false} - scroll={{ x: 1200 }} + scroll={{ x: 1900 }} /> )} diff --git a/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx b/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx index b5c438ba74..cd38603164 100644 --- a/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx +++ b/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx @@ -55,6 +55,7 @@ import accountAdminService, { AccountActionType, formatAccountExecuteMessage } f import CreateAccountContent, { CreateAccountValues } from '../components/CreateAccountContent'; import DeleteDatabaseSchemaConfirmContent from '../components/DeleteDatabaseSchemaConfirmContent'; import ActiveTransactionsContent from '../components/ActiveTransactionsContent'; +import { buildSessionInspectionSql } from '../components/ActiveTransactionsContent/activeTransactionUtils'; import { buildWorkspaceObjectTabTitle } from '@/utils/workspaceObjectTabTitle'; import { allowsResourceOperations } from '@/client-extension/resourceOperationCapabilities'; import type { ResourceOperation, ResourceOperationCapabilities } from '@/client-extension/types'; @@ -410,13 +411,27 @@ export const useCreateRightClickMenu = () => { text: i18n('workspace.ops.activeTransactions'), icon: 'icon-file-text', handle: () => { + const handleOpenSession = (threadId: number) => { + createConsole({ + name: i18n('workspace.ops.sessionThreadTitle', threadId), + ddl: buildSessionInspectionSql(threadId), + dataSourceId: dataSourceId!, + dataSourceName: dataSourceName!, + environmentId, + environment, + databaseType: databaseType!, + databaseName, + schemaName, + }); + }; staticModal.confirm({ title: i18n('workspace.ops.activeTransactions'), content: ( ), footer: null, diff --git a/chat2db-community-client/src/i18n/en-US/workspace.ts b/chat2db-community-client/src/i18n/en-US/workspace.ts index 0837438ad6..5471094a5a 100644 --- a/chat2db-community-client/src/i18n/en-US/workspace.ts +++ b/chat2db-community-client/src/i18n/en-US/workspace.ts @@ -312,6 +312,19 @@ export default { 'workspace.ops.permissionRequired': 'Insufficient permissions.', 'workspace.ops.processPrivilegeHint': 'PROCESS privilege is required for full transaction visibility.', 'workspace.ops.activeTransactionCount': '{1} active transaction(s)', + 'workspace.ops.waitedLock': 'Waited Lock', + 'workspace.ops.blocker': 'Blocker', + 'workspace.ops.sessionAction': 'Session', + 'workspace.ops.ownerSession': 'Owner', + 'workspace.ops.blockerSession': 'Blocker', + 'workspace.ops.openSession': 'Open the transaction owner session', + 'workspace.ops.openBlockingSession': 'Open the blocking session', + 'workspace.ops.sessionUnavailable': 'Session unavailable', + 'workspace.ops.queryUnavailable': 'SQL unavailable', + 'workspace.ops.emptyValue': 'Empty', + 'workspace.ops.lockMetadataUnavailable': 'Lock metadata unavailable', + 'workspace.ops.lockMetadataDegraded': 'Lock-wait details are degraded:', + 'workspace.ops.sessionThreadTitle': 'MySQL session {1}', 'workspace.routine.button.invoke': 'Invoke routine', 'workspace.routine.button.apply': 'Apply changes', 'workspace.routine.button.refresh': 'Reload DDL', diff --git a/chat2db-community-client/src/i18n/es-ES/workspace.ts b/chat2db-community-client/src/i18n/es-ES/workspace.ts index 7428e15864..083c532397 100644 --- a/chat2db-community-client/src/i18n/es-ES/workspace.ts +++ b/chat2db-community-client/src/i18n/es-ES/workspace.ts @@ -314,6 +314,19 @@ export default { 'workspace.ops.permissionRequired': 'Permisos insuficientes.', 'workspace.ops.processPrivilegeHint': 'Se requiere el privilegio PROCESS para ver las transacciones.', 'workspace.ops.activeTransactionCount': '{1} transacción(es) activa(s)', + 'workspace.ops.waitedLock': 'Bloqueo esperado', + 'workspace.ops.blocker': 'Bloqueador', + 'workspace.ops.sessionAction': 'Sesión', + 'workspace.ops.ownerSession': 'Propietaria', + 'workspace.ops.blockerSession': 'Bloqueadora', + 'workspace.ops.openSession': 'Abrir la sesión propietaria de la transacción', + 'workspace.ops.openBlockingSession': 'Abrir la sesión bloqueadora', + 'workspace.ops.sessionUnavailable': 'Sesión no disponible', + 'workspace.ops.queryUnavailable': 'SQL no disponible', + 'workspace.ops.emptyValue': 'Vacío', + 'workspace.ops.lockMetadataUnavailable': 'Metadatos de bloqueo no disponibles', + 'workspace.ops.lockMetadataDegraded': 'Los detalles de espera de bloqueo están degradados:', + 'workspace.ops.sessionThreadTitle': 'Sesión MySQL {1}', 'workspace.routine.button.invoke': 'Invocar rutina', 'workspace.routine.button.apply': 'Aplicar cambios', 'workspace.routine.button.refresh': 'Volver a cargar DDL', diff --git a/chat2db-community-client/src/i18n/ja-JP/workspace.ts b/chat2db-community-client/src/i18n/ja-JP/workspace.ts index 943019bebc..5b17fd81b0 100644 --- a/chat2db-community-client/src/i18n/ja-JP/workspace.ts +++ b/chat2db-community-client/src/i18n/ja-JP/workspace.ts @@ -312,6 +312,19 @@ export default { 'workspace.ops.permissionRequired': '権限が不足しています。', 'workspace.ops.processPrivilegeHint': 'トランザクションの完全な表示には PROCESS 権限が必要です。', 'workspace.ops.activeTransactionCount': 'アクティブなトランザクション: {1} 件', + 'workspace.ops.waitedLock': '待機中のロック', + 'workspace.ops.blocker': 'ブロッカー', + 'workspace.ops.sessionAction': 'セッション', + 'workspace.ops.ownerSession': '所有者', + 'workspace.ops.blockerSession': 'ブロッカー', + 'workspace.ops.openSession': 'トランザクション所有者のセッションを開く', + 'workspace.ops.openBlockingSession': 'ブロックしているセッションを開く', + 'workspace.ops.sessionUnavailable': 'セッションは利用できません', + 'workspace.ops.queryUnavailable': 'SQL は利用できません', + 'workspace.ops.emptyValue': '空', + 'workspace.ops.lockMetadataUnavailable': 'ロックメタデータは利用できません', + 'workspace.ops.lockMetadataDegraded': 'ロック待機の詳細は制限されています:', + 'workspace.ops.sessionThreadTitle': 'MySQL セッション {1}', 'workspace.routine.button.invoke': 'ルーチンを呼び出す', 'workspace.routine.button.apply': '変更を適用', 'workspace.routine.button.refresh': 'DDL を再読み込み', diff --git a/chat2db-community-client/src/i18n/ko-KR/workspace.ts b/chat2db-community-client/src/i18n/ko-KR/workspace.ts index 8263be2c91..e82f59243d 100644 --- a/chat2db-community-client/src/i18n/ko-KR/workspace.ts +++ b/chat2db-community-client/src/i18n/ko-KR/workspace.ts @@ -310,6 +310,19 @@ export default { 'workspace.ops.permissionRequired': '권한이 부족합니다.', 'workspace.ops.processPrivilegeHint': '트랜잭션 전체 보기에는 PROCESS 권한이 필요합니다.', 'workspace.ops.activeTransactionCount': '활성 트랜잭션 {1}개', + 'workspace.ops.waitedLock': '대기 중인 잠금', + 'workspace.ops.blocker': '차단 세션', + 'workspace.ops.sessionAction': '세션', + 'workspace.ops.ownerSession': '소유자', + 'workspace.ops.blockerSession': '차단자', + 'workspace.ops.openSession': '트랜잭션 소유자 세션 열기', + 'workspace.ops.openBlockingSession': '차단 중인 세션 열기', + 'workspace.ops.sessionUnavailable': '세션을 사용할 수 없음', + 'workspace.ops.queryUnavailable': 'SQL을 사용할 수 없음', + 'workspace.ops.emptyValue': '비어 있음', + 'workspace.ops.lockMetadataUnavailable': '잠금 메타데이터를 사용할 수 없음', + 'workspace.ops.lockMetadataDegraded': '잠금 대기 세부 정보가 제한되었습니다:', + 'workspace.ops.sessionThreadTitle': 'MySQL 세션 {1}', 'workspace.routine.button.invoke': '루틴 호출', 'workspace.routine.button.apply': '변경 사항 적용', 'workspace.routine.button.refresh': 'DDL 다시 불러오기', diff --git a/chat2db-community-client/src/i18n/zh-CN/workspace.ts b/chat2db-community-client/src/i18n/zh-CN/workspace.ts index 5263b35a97..6a45d9edcc 100644 --- a/chat2db-community-client/src/i18n/zh-CN/workspace.ts +++ b/chat2db-community-client/src/i18n/zh-CN/workspace.ts @@ -303,6 +303,19 @@ export default { 'workspace.ops.permissionRequired': '权限不足。', 'workspace.ops.processPrivilegeHint': '完整事务可见性需要 PROCESS 权限。', 'workspace.ops.activeTransactionCount': '{1} 个活动事务', + 'workspace.ops.waitedLock': '等待的锁', + 'workspace.ops.blocker': '阻塞方', + 'workspace.ops.sessionAction': '会话', + 'workspace.ops.ownerSession': '事务会话', + 'workspace.ops.blockerSession': '阻塞会话', + 'workspace.ops.openSession': '打开事务所属会话', + 'workspace.ops.openBlockingSession': '打开阻塞会话', + 'workspace.ops.sessionUnavailable': '会话不可用', + 'workspace.ops.queryUnavailable': 'SQL 不可用', + 'workspace.ops.emptyValue': '空值', + 'workspace.ops.lockMetadataUnavailable': '锁元数据不可用', + 'workspace.ops.lockMetadataDegraded': '锁等待详情已降级:', + 'workspace.ops.sessionThreadTitle': 'MySQL 会话 {1}', 'workspace.routine.button.invoke': '调用例程', 'workspace.routine.button.apply': '提交变更', 'workspace.routine.button.refresh': '重新加载 DDL', diff --git a/chat2db-community-client/src/service/sql.ts b/chat2db-community-client/src/service/sql.ts index 1ee5c04d64..b33da5bd13 100644 --- a/chat2db-community-client/src/service/sql.ts +++ b/chat2db-community-client/src/service/sql.ts @@ -445,6 +445,37 @@ export interface IActiveTransactionItem { host: string | null; db: string | null; query: string | null; + queryState?: 'VISIBLE' | 'UNAVAILABLE'; + sessionAvailable?: boolean; + sessionState?: 'LIVE' | 'DISAPPEARED_OR_HIDDEN'; + canOpenSession?: boolean; + waitingLockId?: string | null; + blockingLockId?: string | null; + blockingTrxId?: string | null; + waitingPerformanceSchemaThreadId?: number | null; + blockingPerformanceSchemaThreadId?: number | null; + blockingThreadId?: number | null; + blockingSessionAvailable?: boolean; + canOpenBlockingSession?: boolean; + blockingUser?: string | null; + blockingHost?: string | null; + blockingDb?: string | null; + waitingObject?: string | null; + waitingIndex?: string | null; + waitingLockType?: string | null; + waitingLockMode?: string | null; + waitingLockStatus?: string | null; + waitingLockData?: string | null; + blockingObject?: string | null; + blockingIndex?: string | null; + blockingLockType?: string | null; + blockingLockMode?: string | null; + blockingLockStatus?: string | null; + blockingLockData?: string | null; + lockWaitAvailable?: boolean; + lockMetadataState?: 'AVAILABLE' | 'UNAVAILABLE'; + lockMetadataSource?: 'MYSQL_80_PERFORMANCE_SCHEMA' | 'MYSQL_57_INFORMATION_SCHEMA' | null; + lockMetadataMessage?: string | null; } export interface IActiveTransactionRequest { diff --git a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java index c4d5557f8f..55e4cac23a 100644 --- a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java +++ b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Service; import java.sql.Connection; +import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -17,25 +18,115 @@ public class DbActiveTransactionServiceImpl implements IDbActiveTransactionService { /** - * InnoDB transaction metadata joined with the owning processlist row. Works on MySQL - * 5.7 and 8.0; {@code trx_query} is null without the PROCESS privilege (or for - * read-only transactions on 8.0, where the query text is intentionally not captured). + * InnoDB transaction metadata joined with the owning processlist row. The + * transaction source is always {@code information_schema.innodb_trx}; each refresh is + * a new live snapshot and does not retain rows for transactions that have committed + * or rolled back. */ private static final String SQL_ACTIVE_TRANSACTIONS = "SELECT t.trx_id, t.trx_state, t.trx_started, " + "TIMESTAMPDIFF(SECOND, t.trx_started, NOW()) AS trx_age_seconds, " + "t.trx_isolation_level, t.trx_rows_locked, t.trx_rows_modified, " + "t.trx_lock_structs, t.trx_mysql_thread_id, " - + "p.USER, p.HOST, p.DB, t.trx_query " + + "p.ID AS process_id, p.USER AS process_user, p.HOST AS process_host, p.DB AS process_db, " + + "t.trx_query, " + + "%s " + "FROM information_schema.innodb_trx t " + "LEFT JOIN information_schema.processlist p ON t.trx_mysql_thread_id = p.ID " + + "%s " + "ORDER BY t.trx_started"; + private static final String LOCK_COLUMNS_80 = + "w.requesting_lock_id, w.blocking_lock_id, w.blocking_trx_id, " + + "w.requesting_ps_thread_id, w.blocking_ps_thread_id, " + + "w.waiting_object_schema, w.waiting_object_name, w.waiting_index_name, " + + "w.waiting_lock_type, w.waiting_lock_mode, w.waiting_lock_status, w.waiting_lock_data, " + + "w.blocking_object_schema, w.blocking_object_name, w.blocking_index_name, " + + "w.blocking_lock_type, w.blocking_lock_mode, w.blocking_lock_status, w.blocking_lock_data, " + + "bt.trx_mysql_thread_id AS blocking_thread_id, bp.ID AS blocking_process_id, " + + "bp.USER AS blocking_user, bp.HOST AS blocking_host, bp.DB AS blocking_db"; + + private static final String LOCK_JOIN_80 = + "LEFT JOIN (" + + "SELECT dlw.REQUESTING_ENGINE_TRANSACTION_ID AS requesting_trx_id, " + + "dlw.REQUESTING_ENGINE_LOCK_ID AS requesting_lock_id, " + + "dlw.BLOCKING_ENGINE_LOCK_ID AS blocking_lock_id, " + + "dlw.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx_id, " + + "dlw.REQUESTING_THREAD_ID AS requesting_ps_thread_id, " + + "dlw.BLOCKING_THREAD_ID AS blocking_ps_thread_id, " + + "rl.OBJECT_SCHEMA AS waiting_object_schema, rl.OBJECT_NAME AS waiting_object_name, " + + "rl.INDEX_NAME AS waiting_index_name, rl.LOCK_TYPE AS waiting_lock_type, " + + "rl.LOCK_MODE AS waiting_lock_mode, rl.LOCK_STATUS AS waiting_lock_status, " + + "rl.LOCK_DATA AS waiting_lock_data, " + + "bl.OBJECT_SCHEMA AS blocking_object_schema, bl.OBJECT_NAME AS blocking_object_name, " + + "bl.INDEX_NAME AS blocking_index_name, bl.LOCK_TYPE AS blocking_lock_type, " + + "bl.LOCK_MODE AS blocking_lock_mode, bl.LOCK_STATUS AS blocking_lock_status, " + + "bl.LOCK_DATA AS blocking_lock_data " + + "FROM performance_schema.data_lock_waits dlw " + + "LEFT JOIN performance_schema.data_locks rl " + + "ON dlw.REQUESTING_ENGINE_LOCK_ID = rl.ENGINE_LOCK_ID " + + "AND dlw.ENGINE = rl.ENGINE " + + "LEFT JOIN performance_schema.data_locks bl " + + "ON dlw.BLOCKING_ENGINE_LOCK_ID = bl.ENGINE_LOCK_ID " + + "AND dlw.ENGINE = bl.ENGINE" + + ") w ON w.requesting_trx_id = t.trx_id " + + "LEFT JOIN information_schema.innodb_trx bt ON bt.trx_id = w.blocking_trx_id " + + "LEFT JOIN information_schema.processlist bp ON bt.trx_mysql_thread_id = bp.ID"; + + private static final String LOCK_COLUMNS_57 = + "w.requesting_lock_id, w.blocking_lock_id, w.blocking_trx_id, " + + "NULL AS requesting_ps_thread_id, NULL AS blocking_ps_thread_id, " + + "NULL AS waiting_object_schema, w.waiting_object_name, w.waiting_index_name, " + + "w.waiting_lock_type, w.waiting_lock_mode, NULL AS waiting_lock_status, w.waiting_lock_data, " + + "NULL AS blocking_object_schema, w.blocking_object_name, w.blocking_index_name, " + + "w.blocking_lock_type, w.blocking_lock_mode, NULL AS blocking_lock_status, w.blocking_lock_data, " + + "bt.trx_mysql_thread_id AS blocking_thread_id, bp.ID AS blocking_process_id, " + + "bp.USER AS blocking_user, bp.HOST AS blocking_host, bp.DB AS blocking_db"; + + private static final String LOCK_JOIN_57 = + "LEFT JOIN (" + + "SELECT lw.requesting_trx_id, lw.requested_lock_id AS requesting_lock_id, " + + "lw.blocking_lock_id, lw.blocking_trx_id, " + + "rl.lock_table AS waiting_object_name, rl.lock_index AS waiting_index_name, " + + "rl.lock_type AS waiting_lock_type, rl.lock_mode AS waiting_lock_mode, " + + "rl.lock_data AS waiting_lock_data, " + + "bl.lock_table AS blocking_object_name, bl.lock_index AS blocking_index_name, " + + "bl.lock_type AS blocking_lock_type, bl.lock_mode AS blocking_lock_mode, " + + "bl.lock_data AS blocking_lock_data " + + "FROM information_schema.innodb_lock_waits lw " + + "LEFT JOIN information_schema.innodb_locks rl ON lw.requested_lock_id = rl.lock_id " + + "LEFT JOIN information_schema.innodb_locks bl ON lw.blocking_lock_id = bl.lock_id" + + ") w ON w.requesting_trx_id = t.trx_id " + + "LEFT JOIN information_schema.innodb_trx bt ON bt.trx_id = w.blocking_trx_id " + + "LEFT JOIN information_schema.processlist bp ON bt.trx_mysql_thread_id = bp.ID"; + + private static final String LOCK_COLUMNS_UNAVAILABLE = + "NULL AS requesting_lock_id, NULL AS blocking_lock_id, NULL AS blocking_trx_id, " + + "NULL AS requesting_ps_thread_id, NULL AS blocking_ps_thread_id, " + + "NULL AS waiting_object_schema, NULL AS waiting_object_name, NULL AS waiting_index_name, " + + "NULL AS waiting_lock_type, NULL AS waiting_lock_mode, NULL AS waiting_lock_status, " + + "NULL AS waiting_lock_data, " + + "NULL AS blocking_object_schema, NULL AS blocking_object_name, NULL AS blocking_index_name, " + + "NULL AS blocking_lock_type, NULL AS blocking_lock_mode, NULL AS blocking_lock_status, " + + "NULL AS blocking_lock_data, " + + "NULL AS blocking_thread_id, NULL AS blocking_process_id, " + + "NULL AS blocking_user, NULL AS blocking_host, NULL AS blocking_db"; + + private static final String SQL_PROBE_MYSQL80_LOCKS = + "SELECT 1 FROM performance_schema.data_lock_waits WHERE 1 = 0"; + + private static final String SQL_PROBE_MYSQL57_LOCKS = + "SELECT 1 FROM information_schema.innodb_lock_waits WHERE 1 = 0"; + + private static final String LOCK_METADATA_AVAILABLE = "AVAILABLE"; + private static final String LOCK_METADATA_UNAVAILABLE = "UNAVAILABLE"; + @Override public List> activeTransactions() { Connection connection = Chat2DBContext.getConnection(); + LockMetadataQuery lockMetadataQuery = resolveLockMetadataQuery(connection); try { - return DefaultSQLExecutor.getInstance().execute(connection, SQL_ACTIVE_TRANSACTIONS, resultSet -> { + return DefaultSQLExecutor.getInstance().execute(connection, lockMetadataQuery.sql(), resultSet -> { List> transactions = new ArrayList<>(); while (resultSet.next()) { Map row = new LinkedHashMap<>(); @@ -47,16 +138,31 @@ public List> activeTransactions() { row.put("rowsLocked", resultSet.getLong("trx_rows_locked")); row.put("rowsModified", resultSet.getLong("trx_rows_modified")); row.put("lockStructs", resultSet.getLong("trx_lock_structs")); - row.put("threadId", resultSet.getLong("trx_mysql_thread_id")); - row.put("user", resultSet.getString("USER")); - row.put("host", resultSet.getString("HOST")); - row.put("db", resultSet.getString("DB")); - row.put("query", resultSet.getString("trx_query")); + Long threadId = nullableLong(resultSet, "trx_mysql_thread_id"); + row.put("threadId", threadId); + row.put("user", resultSet.getString("process_user")); + row.put("host", resultSet.getString("process_host")); + row.put("db", resultSet.getString("process_db")); + String query = resultSet.getString("trx_query"); + row.put("query", query); + Long processId = nullableLong(resultSet, "process_id"); + row.put("sessionAvailable", processId != null); + row.put("sessionState", processId == null ? "DISAPPEARED_OR_HIDDEN" : "LIVE"); + row.put("canOpenSession", processId != null && threadId != null); + row.put("queryState", query == null ? "UNAVAILABLE" : "VISIBLE"); + row.put("lockMetadataState", lockMetadataQuery.available() + ? LOCK_METADATA_AVAILABLE : LOCK_METADATA_UNAVAILABLE); + row.put("lockMetadataSource", lockMetadataQuery.source()); + row.put("lockMetadataMessage", lockMetadataQuery.message()); + putLockWait(row, resultSet); transactions.add(row); } return transactions; }); } catch (RuntimeException exception) { + if (lockMetadataQuery.available() && hasMissingOrDeniedMetadataError(exception)) { + return activeTransactionsWithoutLockMetadata(connection, metadataErrorMessage(exception)); + } if (hasProcessPrivilegeError(exception)) { throw new BusinessException("mysql.activeTransaction.processPrivilegeRequired", null, exception); } @@ -64,6 +170,126 @@ public List> activeTransactions() { } } + private List> activeTransactionsWithoutLockMetadata(Connection connection, String message) { + LockMetadataQuery fallback = unavailableLockMetadataQuery(message); + return DefaultSQLExecutor.getInstance().execute(connection, fallback.sql(), resultSet -> { + List> transactions = new ArrayList<>(); + while (resultSet.next()) { + Map row = new LinkedHashMap<>(); + row.put("trxId", resultSet.getString("trx_id")); + row.put("state", resultSet.getString("trx_state")); + row.put("startedAt", resultSet.getTimestamp("trx_started")); + row.put("ageSeconds", resultSet.getLong("trx_age_seconds")); + row.put("isolationLevel", resultSet.getString("trx_isolation_level")); + row.put("rowsLocked", resultSet.getLong("trx_rows_locked")); + row.put("rowsModified", resultSet.getLong("trx_rows_modified")); + row.put("lockStructs", resultSet.getLong("trx_lock_structs")); + Long threadId = nullableLong(resultSet, "trx_mysql_thread_id"); + row.put("threadId", threadId); + row.put("user", resultSet.getString("process_user")); + row.put("host", resultSet.getString("process_host")); + row.put("db", resultSet.getString("process_db")); + String query = resultSet.getString("trx_query"); + Long processId = nullableLong(resultSet, "process_id"); + row.put("query", query); + row.put("sessionAvailable", processId != null); + row.put("sessionState", processId == null ? "DISAPPEARED_OR_HIDDEN" : "LIVE"); + row.put("canOpenSession", processId != null && threadId != null); + row.put("queryState", query == null ? "UNAVAILABLE" : "VISIBLE"); + row.put("lockMetadataState", LOCK_METADATA_UNAVAILABLE); + row.put("lockMetadataSource", null); + row.put("lockMetadataMessage", fallback.message()); + putLockWait(row, resultSet); + transactions.add(row); + } + return transactions; + }); + } + + private static void putLockWait(Map row, ResultSet resultSet) throws SQLException { + Long blockingThreadId = nullableLong(resultSet, "blocking_thread_id"); + Long blockingProcessId = nullableLong(resultSet, "blocking_process_id"); + String waitingLockId = resultSet.getString("requesting_lock_id"); + String blockingLockId = resultSet.getString("blocking_lock_id"); + row.put("waitingLockId", waitingLockId); + row.put("blockingLockId", blockingLockId); + row.put("blockingTrxId", resultSet.getString("blocking_trx_id")); + row.put("waitingPerformanceSchemaThreadId", nullableLong(resultSet, "requesting_ps_thread_id")); + row.put("blockingPerformanceSchemaThreadId", nullableLong(resultSet, "blocking_ps_thread_id")); + row.put("blockingThreadId", blockingThreadId); + row.put("blockingSessionAvailable", blockingProcessId != null); + row.put("canOpenBlockingSession", blockingProcessId != null && blockingThreadId != null); + row.put("blockingUser", resultSet.getString("blocking_user")); + row.put("blockingHost", resultSet.getString("blocking_host")); + row.put("blockingDb", resultSet.getString("blocking_db")); + row.put("waitingObject", lockObject(resultSet, "waiting_object_schema", "waiting_object_name")); + row.put("waitingIndex", resultSet.getString("waiting_index_name")); + row.put("waitingLockType", resultSet.getString("waiting_lock_type")); + row.put("waitingLockMode", resultSet.getString("waiting_lock_mode")); + row.put("waitingLockStatus", resultSet.getString("waiting_lock_status")); + row.put("waitingLockData", resultSet.getString("waiting_lock_data")); + row.put("blockingObject", lockObject(resultSet, "blocking_object_schema", "blocking_object_name")); + row.put("blockingIndex", resultSet.getString("blocking_index_name")); + row.put("blockingLockType", resultSet.getString("blocking_lock_type")); + row.put("blockingLockMode", resultSet.getString("blocking_lock_mode")); + row.put("blockingLockStatus", resultSet.getString("blocking_lock_status")); + row.put("blockingLockData", resultSet.getString("blocking_lock_data")); + row.put("lockWaitAvailable", waitingLockId != null || blockingLockId != null); + } + + private static String lockObject(ResultSet resultSet, String schemaColumn, String objectColumn) throws SQLException { + String schema = resultSet.getString(schemaColumn); + String object = resultSet.getString(objectColumn); + if (schema == null || schema.isBlank()) { + return object; + } + if (object == null || object.isBlank()) { + return schema; + } + return schema + "." + object; + } + + private static Long nullableLong(ResultSet resultSet, String column) throws SQLException { + long value = resultSet.getLong(column); + return resultSet.wasNull() ? null : value; + } + + private static LockMetadataQuery resolveLockMetadataQuery(Connection connection) { + try { + executeProbe(connection, SQL_PROBE_MYSQL80_LOCKS); + return new LockMetadataQuery(activeTransactionSql(LOCK_COLUMNS_80, LOCK_JOIN_80), + true, "MYSQL_80_PERFORMANCE_SCHEMA", null); + } catch (RuntimeException exception) { + if (!hasMissingOrDeniedMetadataError(exception)) { + throw exception; + } + } + + try { + executeProbe(connection, SQL_PROBE_MYSQL57_LOCKS); + return new LockMetadataQuery(activeTransactionSql(LOCK_COLUMNS_57, LOCK_JOIN_57), + true, "MYSQL_57_INFORMATION_SCHEMA", null); + } catch (RuntimeException exception) { + if (!hasMissingOrDeniedMetadataError(exception)) { + throw exception; + } + return unavailableLockMetadataQuery(metadataErrorMessage(exception)); + } + } + + private static void executeProbe(Connection connection, String sql) { + DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> null); + } + + static String activeTransactionSql(String lockColumns, String lockJoin) { + return String.format(SQL_ACTIVE_TRANSACTIONS, lockColumns, lockJoin); + } + + static LockMetadataQuery unavailableLockMetadataQuery(String message) { + return new LockMetadataQuery(activeTransactionSql(LOCK_COLUMNS_UNAVAILABLE, ""), + false, null, message); + } + static boolean hasProcessPrivilegeError(Throwable throwable) { Throwable current = throwable; while (current != null) { @@ -77,4 +303,41 @@ static boolean hasProcessPrivilegeError(Throwable throwable) { } return false; } + + static boolean hasMissingOrDeniedMetadataError(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof SQLException sqlException) { + int errorCode = sqlException.getErrorCode(); + String message = sqlException.getMessage(); + String normalized = message == null ? "" : message.toUpperCase(); + if (errorCode == 1142 || errorCode == 1146 || errorCode == 1227 + || normalized.contains("DATA_LOCK") + || normalized.contains("INNODB_LOCK") + || normalized.contains("PERFORMANCE_SCHEMA") + || normalized.contains("COMMAND DENIED") + || normalized.contains("DOESN'T EXIST") + || normalized.contains("DOES NOT EXIST") + || normalized.contains("PROCESS")) { + return true; + } + } + current = current.getCause(); + } + return false; + } + + static String metadataErrorMessage(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof SQLException) { + return "Lock wait metadata is unavailable or access was denied."; + } + current = current.getCause(); + } + return "Lock wait metadata is not available for this MySQL version or account."; + } + + record LockMetadataQuery(String sql, boolean available, String source, String message) { + } } diff --git a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/test/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImplTest.java b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/test/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImplTest.java index 4233165f95..2dd490466d 100644 --- a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/test/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImplTest.java +++ b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/test/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImplTest.java @@ -19,4 +19,46 @@ void recognizesProcessPrivilegeFailuresThroughWrappedCauses() { assertFalse(DbActiveTransactionServiceImpl.hasProcessPrivilegeError( new SQLException("table not found", "42S02", 1146))); } + + @Test + void buildsLiveTransactionQueryWithMySql80LockWaitMetadata() { + String sql = DbActiveTransactionServiceImpl.activeTransactionSql("w.requesting_lock_id", "JOIN wait_table w"); + + assertTrue(sql.contains("FROM information_schema.innodb_trx t")); + assertTrue(sql.contains("LEFT JOIN information_schema.processlist p ON t.trx_mysql_thread_id = p.ID")); + assertTrue(sql.contains("w.requesting_lock_id")); + assertTrue(sql.contains("JOIN wait_table w")); + assertTrue(sql.contains("ORDER BY t.trx_started")); + } + + @Test + void unavailableLockMetadataQueryKeepsLiveTransactionSource() { + DbActiveTransactionServiceImpl.LockMetadataQuery query = + DbActiveTransactionServiceImpl.unavailableLockMetadataQuery("missing metadata"); + + assertFalse(query.available()); + assertTrue(query.sql().contains("FROM information_schema.innodb_trx t")); + assertTrue(query.sql().contains("NULL AS blocking_trx_id")); + assertTrue(query.sql().contains("LEFT JOIN information_schema.processlist p")); + assertTrue(query.sql().contains("ORDER BY t.trx_started")); + } + + @Test + void recognizesMissingOrDeniedLockMetadataFailures() { + assertTrue(DbActiveTransactionServiceImpl.hasMissingOrDeniedMetadataError( + new SQLException("Table 'performance_schema.data_lock_waits' doesn't exist", "42S02", 1146))); + assertTrue(DbActiveTransactionServiceImpl.hasMissingOrDeniedMetadataError( + new SQLException("SELECT command denied to user for table 'data_locks'", "42000", 1142))); + assertFalse(DbActiveTransactionServiceImpl.hasMissingOrDeniedMetadataError( + new SQLException("Syntax error", "42000", 1064))); + } + + @Test + void lockMetadataDiagnosticDoesNotExposeJdbcDetails() { + String message = DbActiveTransactionServiceImpl.metadataErrorMessage( + new SQLException("SELECT denied for private_schema.data_locks", "42000", 1142)); + + assertTrue(message.contains("unavailable or access was denied")); + assertFalse(message.contains("private_schema")); + } } diff --git a/script/test-fixtures/mysql/MYSQL-OPS-002/README.md b/script/test-fixtures/mysql/MYSQL-OPS-002/README.md index 4cb3b32f30..729cf9fefe 100644 --- a/script/test-fixtures/mysql/MYSQL-OPS-002/README.md +++ b/script/test-fixtures/mysql/MYSQL-OPS-002/README.md @@ -16,7 +16,19 @@ 3. Open a second connection and run `START TRANSACTION; UPDATE ops002_accounts SET balance = balance - 100 WHERE id = 1;` — leave it open. 4. Refresh the view — verify the transaction appears with state RUNNING, a growing age, isolation level REPEATABLE READ, thread ID, user, host, database, and the UPDATE SQL text. 5. Open a third connection and run a second open transaction — verify both are listed, ordered by start time. -6. On the same connection run a lock wait (`UPDATE ops002_accounts ... WHERE id = 1` while the first transaction holds the row lock) — verify the waiting transaction's state is LOCK WAIT. -7. Commit the first transaction — refresh — verify the row disappears. -8. Connect as `ops002_user`, open a transaction, and refresh the view — verify the current user's transaction appears, and other users' transactions show NULL/empty SQL text (explicit unavailable state). -9. With no open transactions, refresh — verify the empty state is shown normally. +6. On the third connection run `START TRANSACTION; UPDATE ops002_accounts SET balance = balance + 10 WHERE id = 1;` while the first transaction holds the row lock — verify the waiting transaction's state is LOCK WAIT. +7. On MySQL 8.0, capture fixture evidence with: + ```sql + SELECT REQUESTING_ENGINE_TRANSACTION_ID, REQUESTING_ENGINE_LOCK_ID, + BLOCKING_ENGINE_TRANSACTION_ID, BLOCKING_ENGINE_LOCK_ID + FROM performance_schema.data_lock_waits; + ``` +8. On MySQL 5.7, capture fixture evidence with: + ```sql + SELECT requesting_trx_id, requested_lock_id, blocking_trx_id, blocking_lock_id + FROM information_schema.innodb_lock_waits; + ``` +9. Verify the Active Transactions view shows waited-lock and blocker fields for the waiting row, and that Owner/Blocker session buttons open a datasource-bound console filtered by the exact `information_schema.PROCESSLIST.ID`. +10. Commit or roll back the first transaction — refresh — verify committed or rolled-back transactions disappear instead of remaining as historical rows. +11. Connect as `ops002_user`, open a transaction, and refresh the view — verify the current user's transaction appears, hidden SQL is rendered as an explicit unavailable state, and lock-wait metadata degrades explicitly when the account or server cannot expose it. +12. With no open transactions, refresh — verify the empty state is shown normally. From f9537192de31c40fb964f04ecfa8964dac9aff52 Mon Sep 17 00:00:00 2001 From: liuhy Date: Tue, 1 Sep 2026 05:12:59 -0700 Subject: [PATCH 33/49] fix(mysql): harden active transaction inspection Signed-off-by: liuhy --- chat2db-community-client/package.json | 3 +- .../activeTransactionUtils.test.ts | 24 +++++- .../activeTransactionUtils.ts | 25 +++++++ .../ActiveTransactionsContent/index.tsx | 21 +++++- .../NewTree/hooks/useCreateRightClickMenu.tsx | 7 +- .../src/blocks/NewTree/menuConfig.tsx | 2 +- .../blocks/NewTree/treeMenuGrouping.test.ts | 10 +++ .../db/DbActiveTransactionServiceImpl.java | 75 +++++++++++-------- .../DbActiveTransactionServiceImplTest.java | 26 +++++++ .../mysql/MYSQL-OPS-002/README.md | 6 +- .../mysql/MYSQL-OPS-002/grants.sql | 3 +- .../mysql/MYSQL-OPS-002/init.sql | 5 +- 12 files changed, 164 insertions(+), 43 deletions(-) diff --git a/chat2db-community-client/package.json b/chat2db-community-client/package.json index 8b4f6a0a61..4706d1c859 100644 --- a/chat2db-community-client/package.json +++ b/chat2db-community-client/package.json @@ -12,7 +12,7 @@ "build": "yarn run build:web:community", "build:prod": "yarn run build:web:community", "build:web": "umi build", - "prebuild:web:community": "yarn test:community-boundary && yarn test:retired-ai && yarn test:tree-title-highlight && yarn test:tree-search-lifecycle && yarn test:driver-upload && yarn test:tree-loading && yarn test:tree-node-lookup && yarn test:tree-node-path && yarn test:data-source-authorization && yarn test:data-source-mutation-refresh && yarn test:ai-model-config && yarn test:ai-model-select && yarn test:export-connections && yarn test:main-page-navigation && yarn test:console-tab-name && yarn test:workspace-tab-scroll && yarn test:workspace-split-lifecycle && yarn test:sql-completion-context && yarn test:file-manager-label && yarn test:local-file-encoding && yarn test:editor-close && yarn test:result-set-ui && yarn test:result-pagination && yarn test:result-table-layout && yarn test:result-status && yarn test:data-source-identity && yarn test:data-source-execution-snapshot && yarn test:data-source-watermark && yarn test:hot-update && yarn test:task-center && yarn test:application-exit && yarn test:monaco-lifecycle && yarn test:result-set-editor", + "prebuild:web:community": "yarn test:community-boundary && yarn test:retired-ai && yarn test:tree-title-highlight && yarn test:tree-search-lifecycle && yarn test:driver-upload && yarn test:tree-loading && yarn test:tree-node-lookup && yarn test:tree-node-path && yarn test:data-source-authorization && yarn test:active-transactions && yarn test:data-source-mutation-refresh && yarn test:ai-model-config && yarn test:ai-model-select && yarn test:export-connections && yarn test:main-page-navigation && yarn test:console-tab-name && yarn test:workspace-tab-scroll && yarn test:workspace-split-lifecycle && yarn test:sql-completion-context && yarn test:file-manager-label && yarn test:local-file-encoding && yarn test:editor-close && yarn test:result-set-ui && yarn test:result-pagination && yarn test:result-table-layout && yarn test:result-status && yarn test:data-source-identity && yarn test:data-source-execution-snapshot && yarn test:data-source-watermark && yarn test:hot-update && yarn test:task-center && yarn test:application-exit && yarn test:monaco-lifecycle && yarn test:result-set-editor", "postbuild:web:community": "node ./scripts/verify-production-bundles.cjs", "build:web:2java": "yarn run build:web:community && rm -rf ../chat2db-community-server/chat2db-community-start/src/main/resources/thymeleaf/* && cp -r dist/index.html ../chat2db-community-server/chat2db-community-start/src/main/resources/thymeleaf/", "build:web:community": "cross-env UMI_ENV=community cross-env APP_NAME=chat2db-community cross-env APP_VERSION=${npm_config_app_version} cross-env PRINT_LOGS=${npm_config_print_logs} cross-env APP_PORT=${npm_config_app_port} umi build", @@ -27,6 +27,7 @@ "test:community-boundary": "node ./scripts/verify-community-boundary.cjs", "test:retired-ai": "tsx src/blocks/AI/retiredAiSurface.test.ts && tsx src/blocks/AI/components/AIChatInput/mentionSelection.test.ts && tsx src/blocks/AI/knowledgeSelection.test.ts && tsx src/blocks/AI/messageNavigation.test.ts", "test:hot-update": "tsx src/store/global/slices/hotUpdate/action.test.ts", + "test:active-transactions": "tsx src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.test.ts && tsx src/blocks/NewTree/treeMenuGrouping.test.ts", "test:result-pagination": "tsx src/blocks/SearchResult/components/ResultSet/pagination.test.ts", "test:tree-node-path": "tsx src/blocks/NewTree/utils/treeNodePath.test.ts", "test:ai-model-config": "tsx src/blocks/AI/components/AIModelConfigModal/modelConfigDefaults.test.ts", diff --git a/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.test.ts b/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.test.ts index 96df8b8c87..0cde2833a7 100644 --- a/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.test.ts +++ b/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.test.ts @@ -1,11 +1,15 @@ import assert from 'node:assert/strict'; import { + beginActiveTransactionRefresh, buildSessionInspectionSql, canOpenTransactionSession, + canShowActiveTransactionsMenu, getActiveTransactionRowKey, getLiveTransactionAge, getTransactionSessionThreadId, + invalidateActiveTransactionRefresh, + isLatestActiveTransactionRefresh, } from './activeTransactionUtils'; import type { IActiveTransactionItem } from '@/service/sql'; @@ -40,11 +44,29 @@ const waitingTransaction: IActiveTransactionItem = { lockMetadataSource: 'MYSQL_80_PERFORMANCE_SCHEMA', }; -assert.equal(getActiveTransactionRowKey(waitingTransaction), '421337:421337:7:3:2:45'); +assert.equal(getActiveTransactionRowKey(waitingTransaction), '421337:421337:7:3:2:421336:7:3:2:421336:45'); +assert.notEqual( + getActiveTransactionRowKey(waitingTransaction), + getActiveTransactionRowKey({ + ...waitingTransaction, + blockingLockId: '421338:7:3:2', + blockingTrxId: '421338', + }), +); assert.equal(canOpenTransactionSession(waitingTransaction, 'owner'), true); assert.equal(canOpenTransactionSession(waitingTransaction, 'blocker'), true); assert.equal(getTransactionSessionThreadId(waitingTransaction, 'owner'), 45); assert.equal(getTransactionSessionThreadId(waitingTransaction, 'blocker'), 44); +assert.equal(canShowActiveTransactionsMenu('MYSQL', true), true); +assert.equal(canShowActiveTransactionsMenu('MYSQL', false), false); +assert.equal(canShowActiveTransactionsMenu('ORACLE', true), false); +const refreshGenerationRef = { current: 0 }; +const olderRefresh = beginActiveTransactionRefresh(refreshGenerationRef); +const newerRefresh = beginActiveTransactionRefresh(refreshGenerationRef); +assert.equal(isLatestActiveTransactionRefresh(refreshGenerationRef, olderRefresh), false); +assert.equal(isLatestActiveTransactionRefresh(refreshGenerationRef, newerRefresh), true); +invalidateActiveTransactionRefresh(refreshGenerationRef); +assert.equal(isLatestActiveTransactionRefresh(refreshGenerationRef, newerRefresh), false); assert.equal(getLiveTransactionAge(12, 1_000, 4_900), 15); assert.equal(getLiveTransactionAge(12, 5_000, 4_000), 12); assert.equal(getLiveTransactionAge(null, 1_000, 4_900), null); diff --git a/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.ts b/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.ts index c4cd502460..8120fa02df 100644 --- a/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.ts +++ b/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.ts @@ -1,13 +1,38 @@ import type { IActiveTransactionItem } from '@/service/sql'; +import { DatabaseTypeCode } from '@/constants/common'; +import { + beginLatestRequest, + invalidateLatestRequest, + isLatestRequest, + RequestGenerationRef, +} from '@/utils/latestRequest'; export function getActiveTransactionRowKey(record: IActiveTransactionItem): string { return [ record.trxId || 'no-trx', record.waitingLockId || 'no-wait', + record.blockingLockId || 'no-blocking-lock', + record.blockingTrxId || 'no-blocking-trx', record.threadId == null ? 'no-thread' : String(record.threadId), ].join(':'); } +export function canShowActiveTransactionsMenu(databaseType: string | undefined, hasPermission: boolean): boolean { + return databaseType === DatabaseTypeCode.MYSQL && hasPermission; +} + +export function beginActiveTransactionRefresh(generationRef: RequestGenerationRef): number { + return beginLatestRequest(generationRef); +} + +export function invalidateActiveTransactionRefresh(generationRef: RequestGenerationRef): void { + invalidateLatestRequest(generationRef); +} + +export function isLatestActiveTransactionRefresh(generationRef: RequestGenerationRef, generation: number): boolean { + return isLatestRequest(generationRef, generation); +} + export function getLiveTransactionAge( ageSeconds: number | null, snapshotAtMs: number, diff --git a/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/index.tsx b/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/index.tsx index 500002157a..92e18ef1d0 100644 --- a/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/index.tsx +++ b/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/index.tsx @@ -1,13 +1,17 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { Button, Space, Table, Tag, Tooltip, Typography } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import i18n from '@/i18n'; import sqlService, { IActiveTransactionItem } from '@/service/sql'; +import { RequestGenerationRef } from '@/utils/latestRequest'; import { + beginActiveTransactionRefresh, canOpenTransactionSession, getActiveTransactionRowKey, getLiveTransactionAge, getTransactionSessionThreadId, + invalidateActiveTransactionRefresh, + isLatestActiveTransactionRefresh, } from './activeTransactionUtils'; /** @@ -33,27 +37,40 @@ const ActiveTransactionsContent = ({ const [error, setError] = useState(null); const [snapshotAtMs, setSnapshotAtMs] = useState(() => Date.now()); const [nowMs, setNowMs] = useState(() => Date.now()); + const refreshGenerationRef = useRef({ current: 0 }); const load = useCallback(() => { + const generation = beginActiveTransactionRefresh(refreshGenerationRef.current); setLoading(true); setError(null); sqlService .getActiveTransactionList({ dataSourceId, databaseName, schemaName }) .then((list) => { + if (!isLatestActiveTransactionRefresh(refreshGenerationRef.current, generation)) { + return; + } const loadedAt = Date.now(); setData(list || []); setSnapshotAtMs(loadedAt); setNowMs(loadedAt); }) .catch((e) => { + if (!isLatestActiveTransactionRefresh(refreshGenerationRef.current, generation)) { + return; + } setData([]); setError(e?.message || i18n('common.text.failure')); }) - .finally(() => setLoading(false)); + .finally(() => { + if (isLatestActiveTransactionRefresh(refreshGenerationRef.current, generation)) { + setLoading(false); + } + }); }, [dataSourceId, databaseName, schemaName]); useEffect(() => { load(); + return () => invalidateActiveTransactionRefresh(refreshGenerationRef.current); }, [load]); useEffect(() => { diff --git a/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx b/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx index cd38603164..82c3ffd89e 100644 --- a/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx +++ b/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx @@ -55,7 +55,10 @@ import accountAdminService, { AccountActionType, formatAccountExecuteMessage } f import CreateAccountContent, { CreateAccountValues } from '../components/CreateAccountContent'; import DeleteDatabaseSchemaConfirmContent from '../components/DeleteDatabaseSchemaConfirmContent'; import ActiveTransactionsContent from '../components/ActiveTransactionsContent'; -import { buildSessionInspectionSql } from '../components/ActiveTransactionsContent/activeTransactionUtils'; +import { + buildSessionInspectionSql, + canShowActiveTransactionsMenu, +} from '../components/ActiveTransactionsContent/activeTransactionUtils'; import { buildWorkspaceObjectTabTitle } from '@/utils/workspaceObjectTabTitle'; import { allowsResourceOperations } from '@/client-extension/resourceOperationCapabilities'; import type { ResourceOperation, ResourceOperationCapabilities } from '@/client-extension/types'; @@ -438,6 +441,8 @@ export const useCreateRightClickMenu = () => { width: 1100, }); }, + discard: !canShowActiveTransactionsMenu(databaseType, hasPermission), + requiredOperations: ['SELECT'], }, [OperationColumn.CreateAccount]: { diff --git a/chat2db-community-client/src/blocks/NewTree/menuConfig.tsx b/chat2db-community-client/src/blocks/NewTree/menuConfig.tsx index d5e5e153a8..ec6aa09fcf 100644 --- a/chat2db-community-client/src/blocks/NewTree/menuConfig.tsx +++ b/chat2db-community-client/src/blocks/NewTree/menuConfig.tsx @@ -100,7 +100,6 @@ export const dropMenuConfig = { OperationColumn.Divider, // Management OperationColumn.MoveToGroup, - OperationColumn.ActiveTransactions, OperationColumn.Refresh, OperationColumn.Divider, OperationColumn.RemoveDataSource, @@ -162,6 +161,7 @@ export const dropMenuConfig = { OperationColumn.Divider, // Management OperationColumn.MoveToGroup, + OperationColumn.ActiveTransactions, OperationColumn.Refresh, OperationColumn.Divider, OperationColumn.RemoveDataSource, diff --git a/chat2db-community-client/src/blocks/NewTree/treeMenuGrouping.test.ts b/chat2db-community-client/src/blocks/NewTree/treeMenuGrouping.test.ts index 6c2bc91a64..1c48dcd2d6 100644 --- a/chat2db-community-client/src/blocks/NewTree/treeMenuGrouping.test.ts +++ b/chat2db-community-client/src/blocks/NewTree/treeMenuGrouping.test.ts @@ -1,4 +1,5 @@ import assert from 'node:assert/strict'; +import { DatabaseTypeCode } from '@/constants/common'; import { OperationColumn, TreeNodeType } from '@/constants/tree'; import { dropMenuConfig } from './menuConfig'; @@ -56,4 +57,13 @@ assert.deepEqual(splitGroups(dropMenuConfig.DEFAULT[TreeNodeType.SAVE_CONSOLE]), [OperationColumn.RemoveConsole], ]); +assert.equal( + dropMenuConfig[DatabaseTypeCode.ORACLE][TreeNodeType.DATA_SOURCE].includes(OperationColumn.ActiveTransactions), + false, +); +assert.equal( + dropMenuConfig.DEFAULT[TreeNodeType.DATA_SOURCE].includes(OperationColumn.ActiveTransactions), + true, +); + console.log('Tree menu grouping tests passed'); diff --git a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java index 55e4cac23a..befb3b2000 100644 --- a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java +++ b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java @@ -172,38 +172,42 @@ public List> activeTransactions() { private List> activeTransactionsWithoutLockMetadata(Connection connection, String message) { LockMetadataQuery fallback = unavailableLockMetadataQuery(message); - return DefaultSQLExecutor.getInstance().execute(connection, fallback.sql(), resultSet -> { - List> transactions = new ArrayList<>(); - while (resultSet.next()) { - Map row = new LinkedHashMap<>(); - row.put("trxId", resultSet.getString("trx_id")); - row.put("state", resultSet.getString("trx_state")); - row.put("startedAt", resultSet.getTimestamp("trx_started")); - row.put("ageSeconds", resultSet.getLong("trx_age_seconds")); - row.put("isolationLevel", resultSet.getString("trx_isolation_level")); - row.put("rowsLocked", resultSet.getLong("trx_rows_locked")); - row.put("rowsModified", resultSet.getLong("trx_rows_modified")); - row.put("lockStructs", resultSet.getLong("trx_lock_structs")); - Long threadId = nullableLong(resultSet, "trx_mysql_thread_id"); - row.put("threadId", threadId); - row.put("user", resultSet.getString("process_user")); - row.put("host", resultSet.getString("process_host")); - row.put("db", resultSet.getString("process_db")); - String query = resultSet.getString("trx_query"); - Long processId = nullableLong(resultSet, "process_id"); - row.put("query", query); - row.put("sessionAvailable", processId != null); - row.put("sessionState", processId == null ? "DISAPPEARED_OR_HIDDEN" : "LIVE"); - row.put("canOpenSession", processId != null && threadId != null); - row.put("queryState", query == null ? "UNAVAILABLE" : "VISIBLE"); - row.put("lockMetadataState", LOCK_METADATA_UNAVAILABLE); - row.put("lockMetadataSource", null); - row.put("lockMetadataMessage", fallback.message()); - putLockWait(row, resultSet); - transactions.add(row); - } - return transactions; - }); + try { + return DefaultSQLExecutor.getInstance().execute(connection, fallback.sql(), resultSet -> { + List> transactions = new ArrayList<>(); + while (resultSet.next()) { + Map row = new LinkedHashMap<>(); + row.put("trxId", resultSet.getString("trx_id")); + row.put("state", resultSet.getString("trx_state")); + row.put("startedAt", resultSet.getTimestamp("trx_started")); + row.put("ageSeconds", resultSet.getLong("trx_age_seconds")); + row.put("isolationLevel", resultSet.getString("trx_isolation_level")); + row.put("rowsLocked", resultSet.getLong("trx_rows_locked")); + row.put("rowsModified", resultSet.getLong("trx_rows_modified")); + row.put("lockStructs", resultSet.getLong("trx_lock_structs")); + Long threadId = nullableLong(resultSet, "trx_mysql_thread_id"); + row.put("threadId", threadId); + row.put("user", resultSet.getString("process_user")); + row.put("host", resultSet.getString("process_host")); + row.put("db", resultSet.getString("process_db")); + String query = resultSet.getString("trx_query"); + Long processId = nullableLong(resultSet, "process_id"); + row.put("query", query); + row.put("sessionAvailable", processId != null); + row.put("sessionState", processId == null ? "DISAPPEARED_OR_HIDDEN" : "LIVE"); + row.put("canOpenSession", processId != null && threadId != null); + row.put("queryState", query == null ? "UNAVAILABLE" : "VISIBLE"); + row.put("lockMetadataState", LOCK_METADATA_UNAVAILABLE); + row.put("lockMetadataSource", null); + row.put("lockMetadataMessage", fallback.message()); + putLockWait(row, resultSet); + transactions.add(row); + } + return transactions; + }); + } catch (RuntimeException exception) { + throw sanitizeActiveTransactionQueryException(exception); + } } private static void putLockWait(Map row, ResultSet resultSet) throws SQLException { @@ -338,6 +342,13 @@ static String metadataErrorMessage(Throwable throwable) { return "Lock wait metadata is not available for this MySQL version or account."; } + static RuntimeException sanitizeActiveTransactionQueryException(RuntimeException exception) { + if (hasProcessPrivilegeError(exception)) { + return new BusinessException("mysql.activeTransaction.processPrivilegeRequired", null, exception); + } + return exception; + } + record LockMetadataQuery(String sql, boolean available, String source, String message) { } } diff --git a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/test/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImplTest.java b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/test/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImplTest.java index 2dd490466d..e81d444007 100644 --- a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/test/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImplTest.java +++ b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/test/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImplTest.java @@ -1,11 +1,15 @@ package ai.chat2db.community.domain.core.impl.db; +import ai.chat2db.community.tools.exception.BusinessException; import org.junit.jupiter.api.Test; import java.sql.SQLException; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; class DbActiveTransactionServiceImplTest { @@ -61,4 +65,26 @@ void lockMetadataDiagnosticDoesNotExposeJdbcDetails() { assertTrue(message.contains("unavailable or access was denied")); assertFalse(message.contains("private_schema")); } + + @Test + void fallbackProcessPrivilegeFailureUsesSanitizedBusinessCode() { + RuntimeException executionFailure = new IllegalStateException("execution failed", + new SQLException("Access denied; you need (at least one of) the PROCESS privilege", "42000", 1227)); + + BusinessException exception = assertThrows(BusinessException.class, () -> { + throw DbActiveTransactionServiceImpl.sanitizeActiveTransactionQueryException(executionFailure); + }); + + assertEquals("mysql.activeTransaction.processPrivilegeRequired", exception.getCode()); + assertSame(executionFailure, exception.getCause()); + } + + @Test + void nonProcessFallbackFailureRemainsOriginalRuntimeException() { + RuntimeException executionFailure = new IllegalStateException("syntax error", + new SQLException("Syntax error", "42000", 1064)); + + assertSame(executionFailure, + DbActiveTransactionServiceImpl.sanitizeActiveTransactionQueryException(executionFailure)); + } } diff --git a/script/test-fixtures/mysql/MYSQL-OPS-002/README.md b/script/test-fixtures/mysql/MYSQL-OPS-002/README.md index 729cf9fefe..65c8fe424b 100644 --- a/script/test-fixtures/mysql/MYSQL-OPS-002/README.md +++ b/script/test-fixtures/mysql/MYSQL-OPS-002/README.md @@ -4,7 +4,9 @@ - `init.sql` creates: - `ops002_admin` — administrator with PROCESS (full transaction visibility) - - `ops002_user` — limited account (no PROCESS; sees NULL SQL text for others) + - `ops002_user` — limited account (no PROCESS; can inspect its own visible + InnoDB transactions, but cross-user transaction/process details can be hidden + or rejected by MySQL privilege checks) - `ops002_test` database with `ops002_accounts` and sample data - `grants.sql` grants PROCESS to the admin account only - `cleanup.sql` drops test objects and users @@ -30,5 +32,5 @@ ``` 9. Verify the Active Transactions view shows waited-lock and blocker fields for the waiting row, and that Owner/Blocker session buttons open a datasource-bound console filtered by the exact `information_schema.PROCESSLIST.ID`. 10. Commit or roll back the first transaction — refresh — verify committed or rolled-back transactions disappear instead of remaining as historical rows. -11. Connect as `ops002_user`, open a transaction, and refresh the view — verify the current user's transaction appears, hidden SQL is rendered as an explicit unavailable state, and lock-wait metadata degrades explicitly when the account or server cannot expose it. +11. Connect as `ops002_user`, open a transaction, and refresh the view — verify the current user's visible transaction can appear, hidden SQL is rendered as an explicit unavailable state when MySQL returns NULL, lock-wait metadata degrades explicitly when the account or server cannot expose it, and a PROCESS denial from the transaction/process query is reported as a permission-required state rather than a generic runtime error. 12. With no open transactions, refresh — verify the empty state is shown normally. diff --git a/script/test-fixtures/mysql/MYSQL-OPS-002/grants.sql b/script/test-fixtures/mysql/MYSQL-OPS-002/grants.sql index 0183fd6ccc..5d58737ae4 100644 --- a/script/test-fixtures/mysql/MYSQL-OPS-002/grants.sql +++ b/script/test-fixtures/mysql/MYSQL-OPS-002/grants.sql @@ -1,5 +1,6 @@ -- MYSQL-OPS-002: Grants for test user -- ops002_admin has PROCESS for full transaction visibility (all users + SQL text). GRANT PROCESS ON *.* TO 'ops002_admin'@'%'; --- ops002_user intentionally has NO PROCESS: transactions appear with NULL query text. +-- ops002_user intentionally has NO PROCESS: cross-user transaction/process details can +-- be hidden, returned with NULL SQL text, or rejected by MySQL privilege checks. FLUSH PRIVILEGES; diff --git a/script/test-fixtures/mysql/MYSQL-OPS-002/init.sql b/script/test-fixtures/mysql/MYSQL-OPS-002/init.sql index 547fff78dd..0d00a03d33 100644 --- a/script/test-fixtures/mysql/MYSQL-OPS-002/init.sql +++ b/script/test-fixtures/mysql/MYSQL-OPS-002/init.sql @@ -1,7 +1,8 @@ -- MYSQL-OPS-002: Active transaction inspection -- Test fixture: users, a test table, and a stored helper that opens transactions. --- The admin account can see all transactions (PROCESS); the limited account can only --- see its own transactions and NULL SQL text. +-- The admin account can see all transactions (PROCESS). The limited account has no +-- PROCESS grant: MySQL may hide cross-user transaction/process details, return NULL SQL +-- text, or reject active-transaction inspection depending on the server/version path. CREATE DATABASE IF NOT EXISTS `ops002_test`; USE `ops002_test`; From 42dade47da1adaa399b763c36796131a48410ccd Mon Sep 17 00:00:00 2001 From: zgq Date: Wed, 2 Sep 2026 12:08:11 +0800 Subject: [PATCH 34/49] fix(i18n): localize dynamic datasource forms --- .../src/components/ConnectionEdit/index.tsx | 10 +++ .../src/utils/dynamicDatabaseRegistry.test.ts | 5 ++ .../src/utils/dynamicDatabaseRegistry.ts | 75 ++++++++++++++++++- 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/chat2db-community-client/src/components/ConnectionEdit/index.tsx b/chat2db-community-client/src/components/ConnectionEdit/index.tsx index a3b9c75035..0deceb97bc 100644 --- a/chat2db-community-client/src/components/ConnectionEdit/index.tsx +++ b/chat2db-community-client/src/components/ConnectionEdit/index.tsx @@ -35,6 +35,14 @@ const OSCAR_JDBC_URL_PREFIX = 'jdbc:oscar://'; const OSCAR_DRIVER_CLASS = 'com.oscar.Driver'; const connectionFormTranslations: Partial>> = { + [LangType.ZH_CN]: { + 'User&Password': '用户名和密码', + NONE: '无', + }, + [LangType.JA_JP]: { + 'User&Password': 'ユーザー名とパスワード', + NONE: 'なし', + }, [LangType.ES_ES]: { 'USE SSH': 'Usar SSH', 'SSH Hostname': 'Host SSH', @@ -67,6 +75,7 @@ const connectionFormTranslations: Partial ({ assert.equal(byName('port').defaultValue, '8812'); assert.equal(byName('host').defaultValue, 'localhost'); assert.equal(byName('database').defaultValue, 'qdb'); + assert.equal(byName('alias').labelName['zh-CN'], '名称'); + assert.equal(byName('host').labelName['zh-CN'], '主机'); + assert.equal(byName('authenticationType').labelName['zh-CN'], '身份验证'); + assert.equal(byName('database').labelName['zh-CN'], '数据库'); + assert.equal(byName('url').labelName['zh-CN'], 'URL'); assert.equal(config.baseInfo.template, 'jdbc:postgresql://{host}:{port}/{database}'); const match = 'jdbc:postgresql://myhost:5555/mydb'.match(config.baseInfo.pattern); assert.equal(match?.[1], 'myhost'); diff --git a/chat2db-community-client/src/utils/dynamicDatabaseRegistry.ts b/chat2db-community-client/src/utils/dynamicDatabaseRegistry.ts index a43735f1be..991e0212da 100644 --- a/chat2db-community-client/src/utils/dynamicDatabaseRegistry.ts +++ b/chat2db-community-client/src/utils/dynamicDatabaseRegistry.ts @@ -1,5 +1,9 @@ import { AuthenticationType, InputType } from '@/components/ConnectionEdit/config/enum'; -import type { IConnectionConfig, IFormItem } from '@/components/ConnectionEdit/config/types'; +import type { + IConnectionConfig, + IFormItem, + ILocalizedConnectionText, +} from '@/components/ConnectionEdit/config/types'; import type { ISupportedDatabaseSummary } from '@/service/supportedDatabase'; import type { IDatabase } from '@/typings'; @@ -15,8 +19,73 @@ import type { IDatabase } from '@/typings'; /** Fallback glyph from the shared colourful sprite; has a -dark variant. */ export const DYNAMIC_DATABASE_ICON = 'icon-colourful-table'; -const localized = (text: string) => - ({ 'en-US': text, 'zh-CN': text, 'ja-JP': text, 'es-ES': text, 'ko-KR': text }) as any; +const CONNECTION_FORM_LABELS: Record = { + Name: { + 'en-US': 'Name', + 'zh-CN': '名称', + 'ja-JP': '名前', + 'es-ES': 'Nombre', + 'ko-KR': '이름', + }, + Host: { + 'en-US': 'Host', + 'zh-CN': '主机', + 'ja-JP': 'ホスト', + 'es-ES': 'Host', + 'ko-KR': '호스트', + }, + Authentication: { + 'en-US': 'Authentication', + 'zh-CN': '身份验证', + 'ja-JP': '認証', + 'es-ES': 'Autenticación', + 'ko-KR': '인증', + }, + User: { + 'en-US': 'User', + 'zh-CN': '用户名', + 'ja-JP': 'ユーザー名', + 'es-ES': 'Usuario', + 'ko-KR': '사용자', + }, + Password: { + 'en-US': 'Password', + 'zh-CN': '密码', + 'ja-JP': 'パスワード', + 'es-ES': 'Contraseña', + 'ko-KR': '비밀번호', + }, + Database: { + 'en-US': 'Database', + 'zh-CN': '数据库', + 'ja-JP': 'データベース', + 'es-ES': 'Base de datos', + 'ko-KR': '데이터베이스', + }, + URL: { + 'en-US': 'URL', + 'zh-CN': 'URL', + 'ja-JP': 'URL', + 'es-ES': 'URL', + 'ko-KR': 'URL', + }, + File: { + 'en-US': 'File', + 'zh-CN': '文件', + 'ja-JP': 'ファイル', + 'es-ES': 'Archivo', + 'ko-KR': '파일', + }, +}; + +const localized = (text: string): ILocalizedConnectionText => + CONNECTION_FORM_LABELS[text] || { + 'en-US': text, + 'zh-CN': text, + 'ja-JP': text, + 'es-ES': text, + 'ko-KR': text, + }; export interface SharedFormItems { envItem: IFormItem; From 02868f08f75f9124fd0e159575f57e827b2be1ab Mon Sep 17 00:00:00 2001 From: zgq Date: Wed, 2 Sep 2026 12:11:25 +0800 Subject: [PATCH 35/49] fix(mysql): centralize index visibility values and guard unknown versions --- .../scripts/i18n-source-hashes.json | 4 +- .../DatabaseTableEditor/IndexList/index.tsx | 17 ++++-- .../src/constants/editTable.ts | 7 +++ .../src/i18n/en-US/editTable.ts | 2 + .../src/i18n/es-ES/editTable.ts | 2 + .../src/i18n/ja-JP/editTable.ts | 2 + .../src/i18n/ko-KR/editTable.ts | 2 + .../src/i18n/zh-CN/editTable.ts | 2 + .../chat2db/plugin/mysql/MysqlMetaData.java | 2 +- .../plugin/mysql/MysqlVersionSupport.java | 2 +- .../constant/MysqlMetaDataConstants.java | 1 + .../mysql/enums/type/MysqlIndexTypeEnum.java | 2 +- .../plugin/mysql/MysqlVersionSupportTest.java | 11 ++++ .../mysql/builder/MysqlSqlBuilderTest.java | 56 +++++++++++++------ 14 files changed, 84 insertions(+), 28 deletions(-) diff --git a/chat2db-community-client/scripts/i18n-source-hashes.json b/chat2db-community-client/scripts/i18n-source-hashes.json index fa763fb491..7996ff73af 100644 --- a/chat2db-community-client/scripts/i18n-source-hashes.json +++ b/chat2db-community-client/scripts/i18n-source-hashes.json @@ -8,7 +8,7 @@ "common.ts": "24aa2228702754bc019f1e21dd7be9f6d1c6ff02ed9bd9227ab319f331248b11", "connection.ts": "d70578a3444978b60ea41c670e8b180849eaab297e33325db56c891e1058e899", "dashboard.ts": "b99f5e6b81271573a8db1a585ac879724441b7163050899c3abaf9498217bd21", - "editTable.ts": "0764f431d90e66b5815dc1397ac0f9830c8a517561d1580e5c01f42cbd90fcb6", + "editTable.ts": "7794c664295f9e5d0f83edb2ff4779a272385effa0ec6daeec751c3263571de6", "editTableData.ts": "b6f40d2e1f612b716c0dbc2ade925069a039585a28ada997e004c33ce2fd0901", "feedback.ts": "080fbb0b2e6578d10499d47e0f23b7161feffb769c8a574048c5a737c9624eea", "login.ts": "df4c7c4b2020ef3e9a70277f364d078fc6a10ad81be31d3b648933f14c423f43", @@ -29,7 +29,7 @@ "common.ts": "24aa2228702754bc019f1e21dd7be9f6d1c6ff02ed9bd9227ab319f331248b11", "connection.ts": "d70578a3444978b60ea41c670e8b180849eaab297e33325db56c891e1058e899", "dashboard.ts": "b99f5e6b81271573a8db1a585ac879724441b7163050899c3abaf9498217bd21", - "editTable.ts": "0764f431d90e66b5815dc1397ac0f9830c8a517561d1580e5c01f42cbd90fcb6", + "editTable.ts": "7794c664295f9e5d0f83edb2ff4779a272385effa0ec6daeec751c3263571de6", "editTableData.ts": "b6f40d2e1f612b716c0dbc2ade925069a039585a28ada997e004c33ce2fd0901", "feedback.ts": "080fbb0b2e6578d10499d47e0f23b7161feffb769c8a574048c5a737c9624eea", "login.ts": "df4c7c4b2020ef3e9a70277f364d078fc6a10ad81be31d3b648933f14c423f43", diff --git a/chat2db-community-client/src/blocks/DatabaseTableEditor/IndexList/index.tsx b/chat2db-community-client/src/blocks/DatabaseTableEditor/IndexList/index.tsx index 439383debe..01d63edc54 100644 --- a/chat2db-community-client/src/blocks/DatabaseTableEditor/IndexList/index.tsx +++ b/chat2db-community-client/src/blocks/DatabaseTableEditor/IndexList/index.tsx @@ -18,7 +18,7 @@ import { Table, Input, Form, Select, Modal } from 'antd'; import { v4 as uuidv4 } from 'uuid'; import IncludeCol, { IIncludeColRef } from '../IncludeCol'; import { IIndexItem, IIndexIncludeColumnItem } from '@/typings'; -import { EditColumnOperationType } from '@/constants'; +import { EditColumnOperationType, MYSQL_INDEX_VISIBILITY, MYSQL_PRIMARY_INDEX_TYPE } from '@/constants'; import { shouldHideOracleIndexColumn, shouldShowMysqlIndexMethod, shouldShowMysqlIndexVisible } from '@/utils/databaseJudgments'; import Iconfont from '@/components/Iconfont'; import { Context } from '../index'; @@ -354,14 +354,21 @@ const IndexList = forwardRef((props: IProps, ref: ForwardedRef) = dataIndex: 'visible', width: '100px', render: (text: boolean | null | undefined, record: IIndexItem) => { - const isPrimaryKey = record.type === 'Primary'; + const isPrimaryKey = record.type === MYSQL_PRIMARY_INDEX_TYPE; const editable = isEditing(record) && !isPrimaryKey; - const value = record.visible === false ? 'INVISIBLE' : 'VISIBLE'; + const value = + record.visible === MYSQL_INDEX_VISIBILITY.INVISIBLE + ? i18n('editTable.option.indexInvisible') + : i18n('editTable.option.indexVisible'); return editable ? ( ) : ( diff --git a/chat2db-community-client/src/constants/editTable.ts b/chat2db-community-client/src/constants/editTable.ts index 042cf7c683..7c5821183c 100644 --- a/chat2db-community-client/src/constants/editTable.ts +++ b/chat2db-community-client/src/constants/editTable.ts @@ -14,3 +14,10 @@ export enum NullableType { // Can be null Null = 1, } + +export const MYSQL_PRIMARY_INDEX_TYPE = 'Primary' as const; + +export const MYSQL_INDEX_VISIBILITY = { + VISIBLE: true, + INVISIBLE: false, +} as const; diff --git a/chat2db-community-client/src/i18n/en-US/editTable.ts b/chat2db-community-client/src/i18n/en-US/editTable.ts index 268fdbea63..e10007fa1d 100644 --- a/chat2db-community-client/src/i18n/en-US/editTable.ts +++ b/chat2db-community-client/src/i18n/en-US/editTable.ts @@ -12,6 +12,8 @@ export default { 'editTable.label.indexType': 'Type', 'editTable.label.indexMethod': 'Index method', 'editTable.label.indexVisible': 'Visible', + 'editTable.option.indexVisible': 'VISIBLE', + 'editTable.option.indexInvisible': 'INVISIBLE', 'editTable.label.includeColumn': 'Include column', 'editTable.button.createTable': 'Create Table', 'editTable.button.importTable': 'Export Table', diff --git a/chat2db-community-client/src/i18n/es-ES/editTable.ts b/chat2db-community-client/src/i18n/es-ES/editTable.ts index 162190f2fb..bbc407ad31 100644 --- a/chat2db-community-client/src/i18n/es-ES/editTable.ts +++ b/chat2db-community-client/src/i18n/es-ES/editTable.ts @@ -12,6 +12,8 @@ export default { 'editTable.label.indexType': 'Tipo', 'editTable.label.indexMethod': 'Método de índice', 'editTable.label.indexVisible': 'Visible', + 'editTable.option.indexVisible': 'VISIBLE', + 'editTable.option.indexInvisible': 'INVISIBLE', 'editTable.label.includeColumn': 'Columna incluida', 'editTable.button.createTable': 'Crear tabla', 'editTable.button.importTable': 'Exportar tabla', diff --git a/chat2db-community-client/src/i18n/ja-JP/editTable.ts b/chat2db-community-client/src/i18n/ja-JP/editTable.ts index 979bd3745f..6079be80ad 100644 --- a/chat2db-community-client/src/i18n/ja-JP/editTable.ts +++ b/chat2db-community-client/src/i18n/ja-JP/editTable.ts @@ -12,6 +12,8 @@ export default { 'editTable.label.indexType': 'タイプ', 'editTable.label.indexMethod': 'インデックス方法', 'editTable.label.indexVisible': '表示', + 'editTable.option.indexVisible': '表示', + 'editTable.option.indexInvisible': '非表示', 'editTable.label.includeColumn': '列を含む', 'editTable.button.createTable': 'テーブルを作成', 'editTable.button.importTable': 'テーブルをエクスポート', diff --git a/chat2db-community-client/src/i18n/ko-KR/editTable.ts b/chat2db-community-client/src/i18n/ko-KR/editTable.ts index 1d15ee8487..c7e2a31be6 100644 --- a/chat2db-community-client/src/i18n/ko-KR/editTable.ts +++ b/chat2db-community-client/src/i18n/ko-KR/editTable.ts @@ -12,6 +12,8 @@ export default { 'editTable.label.indexType': '유형', 'editTable.label.indexMethod': '인덱스 방식', 'editTable.label.indexVisible': '표시', + 'editTable.option.indexVisible': '표시', + 'editTable.option.indexInvisible': '숨김', 'editTable.label.includeColumn': '포함 열', 'editTable.button.createTable': '테이블 생성', 'editTable.button.importTable': '테이블 내보내기', diff --git a/chat2db-community-client/src/i18n/zh-CN/editTable.ts b/chat2db-community-client/src/i18n/zh-CN/editTable.ts index 4c70a77e4a..5dc416da30 100644 --- a/chat2db-community-client/src/i18n/zh-CN/editTable.ts +++ b/chat2db-community-client/src/i18n/zh-CN/editTable.ts @@ -12,6 +12,8 @@ export default { 'editTable.label.indexType': '索引类型', 'editTable.label.indexMethod': '索引方法', 'editTable.label.indexVisible': '可见', + 'editTable.option.indexVisible': '可见', + 'editTable.option.indexInvisible': '不可见', 'editTable.label.includeColumn': '包含列', 'editTable.button.createTable': '新建表', 'editTable.button.importTable': '导出表', diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java index 8df916d8a0..ea2d11577d 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java @@ -345,7 +345,7 @@ public List indexes(Connection connection, String databaseName, Stri index.setComment(resultSet.getString(FIELD_INDEX_COMMENT_FALLBACK)); } try { - index.setVisible("YES".equalsIgnoreCase(resultSet.getString(FIELD_IS_VISIBLE))); + index.setVisible(INDEX_VISIBLE_VALUE.equalsIgnoreCase(resultSet.getString(FIELD_IS_VISIBLE))); } catch (SQLException e) { index.setVisible(Boolean.TRUE); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlVersionSupport.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlVersionSupport.java index b0713345a8..26a2159329 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlVersionSupport.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlVersionSupport.java @@ -21,7 +21,7 @@ public static boolean supportsInvisibleIndexes(String dbVersion) { public static boolean currentVersionDisallowsInvisibleIndexes() { String dbVersion = getCurrentDbVersion(); - return StringUtils.isNotBlank(dbVersion) && !supportsInvisibleIndexes(dbVersion); + return !supportsInvisibleIndexes(dbVersion); } public static String getCurrentDbVersion() { diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlMetaDataConstants.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlMetaDataConstants.java index c59b09de81..72a395236a 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlMetaDataConstants.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlMetaDataConstants.java @@ -72,6 +72,7 @@ public final class MysqlMetaDataConstants { public static final String FIELD_INDEX_COMMENT_FALLBACK = "Comment"; public static final String FIELD_INDEX_TYPE = "Index_type"; public static final String FIELD_IS_VISIBLE = "Visible"; + public static final String INDEX_VISIBLE_VALUE = "YES"; public static final String FIELD_IS_NULLABLE = "IS_NULLABLE"; public static final String FIELD_KEY_NAME = "Key_name"; public static final String FIELD_NAME = "Name"; diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java index 00ff97b760..9979e140c4 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java @@ -156,7 +156,7 @@ public String buildAlterIndexVisibility(TableIndex tableIndex) { String visibility = Boolean.FALSE.equals(tableIndex.getVisible()) ? SQL_INVISIBLE : SQL_VISIBLE; return StringUtils.join(SQL_ALTER_INDEX, MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getName()), - " ", visibility); + SQLConstants.SPACE, visibility); } private String buildDropIndex(TableIndex tableIndex) { diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlVersionSupportTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlVersionSupportTest.java index b14056b282..5addfe4653 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlVersionSupportTest.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlVersionSupportTest.java @@ -1,5 +1,6 @@ package ai.chat2db.plugin.mysql; +import ai.chat2db.spi.sql.Chat2DBContext; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -7,6 +8,11 @@ class MysqlVersionSupportTest { + @org.junit.jupiter.api.AfterEach + void clearContext() { + Chat2DBContext.removeContext(); + } + @Test void shouldGateInvisibleIndexesAtMysqlEight() { assertFalse(MysqlVersionSupport.supportsInvisibleIndexes("5.7.44-log")); @@ -15,4 +21,9 @@ void shouldGateInvisibleIndexesAtMysqlEight() { assertFalse(MysqlVersionSupport.supportsInvisibleIndexes("10.6.0-MariaDB")); assertFalse(MysqlVersionSupport.supportsInvisibleIndexes(null)); } + + @Test + void shouldDisallowInvisibleIndexesWhenVersionIsUnknown() { + assertTrue(MysqlVersionSupport.currentVersionDisallowsInvisibleIndexes()); + } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilderTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilderTest.java index 58143b3998..decbf0f615 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilderTest.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilderTest.java @@ -216,17 +216,35 @@ void shouldBuildAlterSqlWithIndexMethodWhenAddingMysqlIndexes() { @Test void shouldRebuildIndexWhenVisibilityAndMethodChangeTogether() { - MysqlSqlBuilder builder = new MysqlSqlBuilder(); - Table oldTable = mysqlTableWithIndexes(List.of(mysqlIndex("idx_content", "BTREE", true, - List.of(mysqlIndexColumn("content", "ASC", null))))); - Table newTable = mysqlTableWithIndexes(List.of(mysqlModifiedIndex("idx_content", "HASH", false, - List.of(mysqlIndexColumn("content", "ASC", null))))); + withMysqlVersion("8.0.36", () -> { + MysqlSqlBuilder builder = new MysqlSqlBuilder(); + Table oldTable = mysqlTableWithIndexes(List.of(mysqlIndex("idx_content", "BTREE", true, + List.of(mysqlIndexColumn("content", "ASC", null))))); + Table newTable = mysqlTableWithIndexes(List.of(mysqlModifiedIndex("idx_content", "HASH", false, + List.of(mysqlIndexColumn("content", "ASC", null))))); - String sql = builder.ddl().table().buildAlterTable(oldTable, newTable); + String sql = builder.ddl().table().buildAlterTable(oldTable, newTable); + + assertEquals("ALTER TABLE `test_db`.`sample_table`\n" + + "\tDROP INDEX `idx_content`,\n" + + "ADD INDEX `idx_content` (`content` ASC) USING HASH INVISIBLE;", sql); + }); + } + + @Test + void shouldUseLightweightAlterForVisibilityOnlyChange() { + withMysqlVersion("8.0.36", () -> { + MysqlSqlBuilder builder = new MysqlSqlBuilder(); + Table oldTable = mysqlTableWithIndexes(List.of(mysqlIndex("idx_content", "BTREE", true, + List.of(mysqlIndexColumn("content", "ASC", 16L))))); + Table newTable = mysqlTableWithIndexes(List.of(mysqlModifiedIndex("idx_content", "BTREE", false, + List.of(mysqlIndexColumn("content", "ASC", 16L))))); - assertEquals("ALTER TABLE `test_db`.`sample_table`\n" - + "\tDROP INDEX `idx_content`,\n" - + "ADD INDEX `idx_content` (`content` ASC) USING HASH INVISIBLE;", sql); + String sql = builder.ddl().table().buildAlterTable(oldTable, newTable); + + assertEquals("ALTER TABLE `test_db`.`sample_table`\n" + + "\tALTER INDEX `idx_content` INVISIBLE;", sql); + }); } @Test @@ -266,17 +284,19 @@ void shouldRejectInvisibleIndexVisibilityChangeOnMysql57() { @Test void shouldRebuildIndexWhenVisibilityAndPrefixLengthChangeTogether() { - MysqlSqlBuilder builder = new MysqlSqlBuilder(); - Table oldTable = mysqlTableWithIndexes(List.of(mysqlIndex("idx_content", "BTREE", true, - List.of(mysqlIndexColumn("content", "ASC", 16L))))); - Table newTable = mysqlTableWithIndexes(List.of(mysqlModifiedIndex("idx_content", "BTREE", false, - List.of(mysqlIndexColumn("content", "ASC", 32L))))); + withMysqlVersion("8.0.36", () -> { + MysqlSqlBuilder builder = new MysqlSqlBuilder(); + Table oldTable = mysqlTableWithIndexes(List.of(mysqlIndex("idx_content", "BTREE", true, + List.of(mysqlIndexColumn("content", "ASC", 16L))))); + Table newTable = mysqlTableWithIndexes(List.of(mysqlModifiedIndex("idx_content", "BTREE", false, + List.of(mysqlIndexColumn("content", "ASC", 32L))))); - String sql = builder.ddl().table().buildAlterTable(oldTable, newTable); + String sql = builder.ddl().table().buildAlterTable(oldTable, newTable); - assertEquals("ALTER TABLE `test_db`.`sample_table`\n" - + "\tDROP INDEX `idx_content`,\n" - + "ADD INDEX `idx_content` (`content`(32) ASC) USING BTREE INVISIBLE;", sql); + assertEquals("ALTER TABLE `test_db`.`sample_table`\n" + + "\tDROP INDEX `idx_content`,\n" + + "ADD INDEX `idx_content` (`content`(32) ASC) USING BTREE INVISIBLE;", sql); + }); } @Test From ccb0f05bfe413f28ed610573ae77e6b3949c6ff0 Mon Sep 17 00:00:00 2001 From: zgq Date: Wed, 2 Sep 2026 12:41:35 +0800 Subject: [PATCH 36/49] fix(terminal): make system shell fallback reliable --- .../jcef/terminal/TerminalSessionManager.java | 115 ++++------ .../terminal/TerminalSessionManagerTest.java | 211 ++++++++---------- 2 files changed, 135 insertions(+), 191 deletions(-) diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/java/ai/chat2db/community/jcef/terminal/TerminalSessionManager.java b/chat2db-community-server/chat2db-community-jcef/src/main/java/ai/chat2db/community/jcef/terminal/TerminalSessionManager.java index d71185fffa..a17fdd7b2c 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/java/ai/chat2db/community/jcef/terminal/TerminalSessionManager.java +++ b/chat2db-community-server/chat2db-community-jcef/src/main/java/ai/chat2db/community/jcef/terminal/TerminalSessionManager.java @@ -43,9 +43,6 @@ public final class TerminalSessionManager { private static final TerminalEventPublisher JCEF_EVENT_PUBLISHER = TerminalSessionManager::publishToJcef; private static volatile TerminalEventPublisher eventPublisher = JCEF_EVENT_PUBLISHER; - static final String DEFAULT_SHELL_PROPERTY = "chat2db.community.terminal.default-shell"; - private static volatile TerminalProcessFactory processFactory = TerminalSessionManager::defaultProcessFactory; - private TerminalSessionManager() { } @@ -54,22 +51,29 @@ public static Map create(Path directory, int columns, int rows) } public static Map create(Path directory, int columns, int rows, String shellId) throws IOException { - return create(directory, columns, rows, resolveShellCandidates(shellId)); + return create( + directory, + columns, + rows, + resolveShellCandidates(shellId), + TerminalSessionManager::defaultProcessFactory + ); } - static Map create(Path directory, int columns, int rows, List candidates) throws IOException { + static Map create(Path directory, int columns, int rows, List candidates, + TerminalProcessFactory processFactory) throws IOException { + Path cwd = resolveTerminalDirectory(directory); IOException lastFailure = null; for (ShellCommand shell : candidates) { try { - return create(directory, columns, rows, shell); + return createSession(cwd, columns, rows, shell, processFactory); } catch (IOException failure) { // Some corporate security suites (e.g. QiAnXin) deny process creation for // powershell.exe even though the binary is present in PATH. Treat a spawn-time // failure as "this shell cannot be invoked" and try the next candidate so the // terminal still opens via, for example, cmd.exe. lastFailure = failure; - log.warn("Unable to start terminal shell [{}], trying the next available shell", - shell.command(), failure); + log.warn("Unable to start terminal shell [{}]", shell.displayName(), failure); } } throw lastFailure != null @@ -89,11 +93,25 @@ static Path resolveUserHomeDirectory(String configuredUserHome) { } private static Map create(Path directory, int columns, int rows, ShellCommand shell) throws IOException { + return createSession( + resolveTerminalDirectory(directory), + columns, + rows, + shell, + TerminalSessionManager::defaultProcessFactory + ); + } + + private static Path resolveTerminalDirectory(Path directory) throws IOException { Path cwd = directory.toRealPath(); if (!Files.isDirectory(cwd)) { throw new IllegalArgumentException("Terminal working directory is not available"); } + return cwd; + } + private static Map createSession(Path cwd, int columns, int rows, ShellCommand shell, + TerminalProcessFactory processFactory) throws IOException { Map environment = new HashMap<>(System.getenv()); environment.put("TERM", "xterm-256color"); environment.put("COLORTERM", "truecolor"); @@ -103,8 +121,13 @@ private static Map create(Path directory, int columns, int rows, applyShellColorEnvironment(environment, shell); } - PtyProcess process = startProcess(shell.command().toArray(String[]::new), cwd.toString(), environment, - Math.max(columns, 20), Math.max(rows, 5)); + PtyProcess process = processFactory.create( + shell.command().toArray(String[]::new), + cwd.toString(), + environment, + Math.max(columns, 20), + Math.max(rows, 5) + ); String sessionId = UUID.randomUUID().toString(); TerminalSession session = new TerminalSession(sessionId, cwd, shell, process); @@ -119,11 +142,6 @@ private static Map create(Path directory, int columns, int rows, return result; } - private static PtyProcess startProcess(String[] command, String directory, Map environment, - int columns, int rows) throws IOException { - return processFactory.create(command, directory, environment, columns, rows); - } - static PtyProcess defaultProcessFactory(String[] command, String directory, Map environment, int columns, int rows) throws IOException { return new PtyProcessBuilder(command) @@ -304,65 +322,26 @@ private static List resolveSystemShellCandidates(String os) { } static List resolveSystemShellCandidates(String os, CommandResolver resolver) { - List candidates = new ArrayList<>(); - Set seenCommands = new HashSet<>(); - String configuredShell = configuredDefaultShell(); - if (configuredShell != null) { - ShellCommand leading = resolveConfiguredShell(configuredShell); - if (leading != null) { - candidates.add(leading); - seenCommands.add(leading.command().get(0)); - } - } if (isWindows(os)) { + List candidates = new ArrayList<>(); String pwsh = resolver.find(os, "pwsh.exe", "pwsh"); if (pwsh != null) { - addCandidate(candidates, seenCommands, powerShell("system", "PowerShell 7", pwsh)); + candidates.add(powerShell("system", "PowerShell 7", pwsh)); } String powerShell = resolver.find(os, "powershell.exe"); if (powerShell != null) { - addCandidate(candidates, seenCommands, powerShell("system", "Windows PowerShell", powerShell)); + candidates.add(powerShell("system", "Windows PowerShell", powerShell)); } // Keep cmd.exe as the last resort so a blocked PowerShell still yields a usable shell. String cmd = resolver.find(os, resolveCommandPrompt()); - addCandidate(candidates, seenCommands, commandPrompt("system", cmd == null ? resolveCommandPrompt() : cmd)); + candidates.add(commandPrompt("system", cmd == null ? resolveCommandPrompt() : cmd)); return candidates; } - ShellCommand systemShell = resolveNonWindowsSystemShell(os); - if (seenCommands.add(systemShell.command().get(0))) { - candidates.add(systemShell); - } - return candidates; - } - - static ShellCommand commandPrompt(String id, String command) { - // /K keeps cmd.exe resident after launch, matching pwsh's -NoExit behaviour. - return new ShellCommand(id, List.of(command, "/K"), "Command Prompt", ShellFamily.CMD); - } - - static String configuredDefaultShell() { - String configured = System.getProperty(DEFAULT_SHELL_PROPERTY); - if (configured == null || configured.isBlank()) { - return null; - } - String normalized = configured.trim().toLowerCase(Locale.ROOT); - // "system" means auto resolution; treating it as a configured default would loop back here. - return "system".equals(normalized) ? null : normalized; + return List.of(resolveNonWindowsSystemShell(os)); } - private static ShellCommand resolveConfiguredShell(String shellId) { - try { - return resolveShell(shellId); - } catch (IllegalArgumentException exception) { - log.warn("Ignoring configured default terminal shell [{}]: {}", shellId, exception.getMessage()); - return null; - } - } - - private static void addCandidate(List candidates, Set seenCommands, ShellCommand shell) { - if (seenCommands.add(shell.command().get(0))) { - candidates.add(shell); - } + private static ShellCommand commandPrompt(String id, String command) { + return new ShellCommand(id, List.of(command), "Command Prompt", ShellFamily.CMD); } private static ShellCommand resolveNonWindowsSystemShell(String os) { @@ -374,7 +353,7 @@ private static ShellCommand resolveNonWindowsSystemShell(String os) { return unixShell("system", fallbackName, requireCommand(os, fallbackName, fallbackName)); } - static void applyShellColorEnvironment(Map environment, ShellCommand shell) { + private static void applyShellColorEnvironment(Map environment, ShellCommand shell) { switch (shell.family()) { case BASH -> environment.put("PS1", "\\[\\033[34m\\]\\u@\\h\\[\\033[0m\\]:" + "\\[\\033[32m\\]\\w\\[\\033[0m\\]\\$ "); @@ -386,7 +365,7 @@ static void applyShellColorEnvironment(Map environment, ShellCom } } - static ShellCommand unixShell(String id, String displayName, String command) { + private static ShellCommand unixShell(String id, String displayName, String command) { String executableName = Path.of(command).getFileName().toString().toLowerCase(Locale.ROOT); ShellFamily family = executableName.contains("zsh") ? ShellFamily.ZSH @@ -394,7 +373,7 @@ static ShellCommand unixShell(String id, String displayName, String command) { return new ShellCommand(id, List.of(command, "-l"), displayName, family); } - static ShellCommand powerShell(String id, String displayName, String command) { + private static ShellCommand powerShell(String id, String displayName, String command) { String colorSetup = "if (Get-Command Set-PSReadLineOption -ErrorAction SilentlyContinue) { " + "Set-PSReadLineOption -Colors @{ " + "Command = 'Blue'; Parameter = 'Cyan'; String = 'Green'; " @@ -483,14 +462,6 @@ static void resetEventPublisherForTests() { eventPublisher = JCEF_EVENT_PUBLISHER; } - static void setProcessFactoryForTests(TerminalProcessFactory factory) { - processFactory = factory == null ? TerminalSessionManager::defaultProcessFactory : factory; - } - - static void resetProcessFactoryForTests() { - processFactory = TerminalSessionManager::defaultProcessFactory; - } - @FunctionalInterface interface CommandResolver { String find(String os, String... candidates); diff --git a/chat2db-community-server/chat2db-community-jcef/src/test/java/ai/chat2db/community/jcef/terminal/TerminalSessionManagerTest.java b/chat2db-community-server/chat2db-community-jcef/src/test/java/ai/chat2db/community/jcef/terminal/TerminalSessionManagerTest.java index f888481c51..35e34258b1 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/test/java/ai/chat2db/community/jcef/terminal/TerminalSessionManagerTest.java +++ b/chat2db-community-server/chat2db-community-jcef/src/test/java/ai/chat2db/community/jcef/terminal/TerminalSessionManagerTest.java @@ -3,6 +3,9 @@ import ai.chat2db.community.jcef.enums.ActionTypeEnum; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; import java.io.IOException; @@ -18,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -29,8 +31,6 @@ class TerminalSessionManagerTest { @AfterEach void resetAfterEach() { TerminalSessionManager.resetEventPublisherForTests(); - TerminalSessionManager.resetProcessFactoryForTests(); - System.clearProperty(TerminalSessionManager.DEFAULT_SHELL_PROPERTY); } @Test @@ -51,10 +51,8 @@ void createsResizableInteractiveSessionInRequestedDirectory() throws Exception { } @Test + @DisabledOnOs(OS.WINDOWS) void reportsRunningChildProcessAsBusy() throws Exception { - if (System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win")) { - return; - } Map session = TerminalSessionManager.create(directory, 80, 24); String sessionId = (String) session.get("sessionId"); try { @@ -141,10 +139,8 @@ void enablesCompatibleColorHints() { } @Test + @DisabledOnOs(OS.WINDOWS) void batchesLargeOutputAndWaitsForAcknowledgement() throws Exception { - if (System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win")) { - return; - } List batches = new CopyOnWriteArrayList<>(); List sequences = new CopyOnWriteArrayList<>(); TerminalSessionManager.setEventPublisherForTests((sessionId, actionType, message) -> { @@ -181,73 +177,8 @@ void batchesLargeOutputAndWaitsForAcknowledgement() throws Exception { } } - @Test - void powershellShellsConfigureNoLogoNoExitAndCommandArgs() { - TerminalSessionManager.ShellCommand pwsh = - TerminalSessionManager.powerShell("pwsh", "PowerShell 7", "pwsh.exe"); - assertEquals(TerminalSessionManager.ShellFamily.POWERSHELL, pwsh.family()); - assertEquals("pwsh.exe", pwsh.command().get(0)); - assertTrue(pwsh.command().contains("-NoLogo")); - assertTrue(pwsh.command().contains("-NoExit")); - assertTrue(pwsh.command().contains("-Command")); - assertTrue(pwsh.command().stream().anyMatch(arg -> arg.contains("Set-PSReadLineOption"))); - - TerminalSessionManager.ShellCommand windowsPowerShell = - TerminalSessionManager.powerShell("powershell", "Windows PowerShell", "powershell.exe"); - assertEquals(TerminalSessionManager.ShellFamily.POWERSHELL, windowsPowerShell.family()); - assertTrue(windowsPowerShell.command().contains("-NoExit")); - assertTrue(windowsPowerShell.command().contains("-Command")); - } - - @Test - void cmdUsesExplicitStayAliveArgument() { - TerminalSessionManager.ShellCommand cmd = TerminalSessionManager.commandPrompt("cmd", "cmd.exe"); - assertEquals(TerminalSessionManager.ShellFamily.CMD, cmd.family()); - assertEquals(List.of("cmd.exe", "/K"), cmd.command()); - assertFalse(cmd.command().contains("-NoLogo")); - assertFalse(cmd.command().contains("-NoExit")); - assertFalse(cmd.command().contains("-Command")); - } - - @Test - void bashSetsColoredPs1Prompt() { - Map environment = new HashMap<>(); - TerminalSessionManager.applyShellColorEnvironment(environment, - TerminalSessionManager.unixShell("system", "Bash", "bash")); - assertTrue(environment.containsKey("PS1")); - assertFalse(environment.containsKey("PROMPT")); - } - - @Test - void cmdSetsColoredPromptWhilePowershellUsesCommandColors() { - Map cmdEnvironment = new HashMap<>(); - TerminalSessionManager.applyShellColorEnvironment(cmdEnvironment, - new TerminalSessionManager.ShellCommand( - "cmd", List.of("cmd.exe"), "Command Prompt", TerminalSessionManager.ShellFamily.CMD)); - assertEquals("$E[34m$P$E[0m$G$S", cmdEnvironment.get("PROMPT")); - - Map powershellEnvironment = new HashMap<>(); - TerminalSessionManager.applyShellColorEnvironment(powershellEnvironment, - TerminalSessionManager.powerShell("system", "PowerShell 7", "pwsh.exe")); - assertFalse(powershellEnvironment.containsKey("PS1")); - assertFalse(powershellEnvironment.containsKey("PROMPT")); - } - - @Test - void configuredDefaultShellNormalizesProperty() { - System.clearProperty(TerminalSessionManager.DEFAULT_SHELL_PROPERTY); - assertNull(TerminalSessionManager.configuredDefaultShell()); - - System.setProperty(TerminalSessionManager.DEFAULT_SHELL_PROPERTY, " system "); - assertNull(TerminalSessionManager.configuredDefaultShell()); - - System.setProperty(TerminalSessionManager.DEFAULT_SHELL_PROPERTY, " PWSH "); - assertEquals("pwsh", TerminalSessionManager.configuredDefaultShell()); - } - @Test void windowsSystemShellsFallBackPwshThenPowerShellThenCmd() { - System.clearProperty(TerminalSessionManager.DEFAULT_SHELL_PROPERTY); List candidates = TerminalSessionManager.resolveSystemShellCandidates("windows 11", (os, names) -> { for (String name : names) { @@ -267,69 +198,86 @@ void windowsSystemShellsFallBackPwshThenPowerShellThenCmd() { List.of("PowerShell 7", "Windows PowerShell", "Command Prompt"), candidates.stream().map(TerminalSessionManager.ShellCommand::displayName) .collect(java.util.stream.Collectors.toList())); + assertTrue(candidates.stream().allMatch(candidate -> "system".equals(candidate.id()))); assertEquals(TerminalSessionManager.ShellFamily.CMD, candidates.get(candidates.size() - 1).family()); } @Test - void zshSetsColoredPrompt() { - Map environment = new HashMap<>(); - TerminalSessionManager.applyShellColorEnvironment(environment, - TerminalSessionManager.unixShell("system", "Zsh", "zsh")); - assertTrue(environment.containsKey("PROMPT")); - assertFalse(environment.containsKey("PS1")); + void windowsSystemShellsKeepCmdWhenPowershellIsUnavailable() { + List candidates = + TerminalSessionManager.resolveSystemShellCandidates("windows 11", (os, names) -> null); + + assertEquals(1, candidates.size()); + assertEquals("system", candidates.get(0).id()); + assertEquals("Command Prompt", candidates.get(0).displayName()); + assertEquals(TerminalSessionManager.ShellFamily.CMD, candidates.get(0).family()); } @Test + @EnabledOnOs(OS.WINDOWS) void windowsSystemShellFallsBackToCmdWhenPowershellSpawnBlocked() throws Exception { - if (!System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win")) { - return; - } - System.clearProperty(TerminalSessionManager.DEFAULT_SHELL_PROPERTY); AtomicInteger attempts = new AtomicInteger(); - TerminalSessionManager.setProcessFactoryForTests((command, dir, env, columns, rows) -> { - attempts.incrementAndGet(); - if (!command[0].toLowerCase(Locale.ROOT).contains("cmd")) { - throw new IOException("blocked"); - } - return TerminalSessionManager.defaultProcessFactory(command, dir, env, columns, rows); - }); - Path cwd = Path.of(System.getProperty("java.io.tmpdir")); + TerminalSessionManager.ShellCommand blocked = new TerminalSessionManager.ShellCommand( + "system", + List.of("powershell.exe"), + "Windows PowerShell", + TerminalSessionManager.ShellFamily.POWERSHELL + ); + TerminalSessionManager.ShellCommand fallback = new TerminalSessionManager.ShellCommand( + "system", + List.of(System.getenv().getOrDefault("ComSpec", "cmd.exe")), + "Command Prompt", + TerminalSessionManager.ShellFamily.CMD + ); + Map session = TerminalSessionManager.create( + directory, + 80, + 24, + List.of(blocked, fallback), + (command, dir, env, columns, rows) -> { + attempts.incrementAndGet(); + if (!command[0].toLowerCase(Locale.ROOT).contains("cmd")) { + throw new IOException("blocked"); + } + return TerminalSessionManager.defaultProcessFactory(command, dir, env, columns, rows); + } + ); + String sessionId = (String) session.get("sessionId"); try { - Map session = TerminalSessionManager.create(cwd, 80, 24, "system"); - String sessionId = (String) session.get("sessionId"); - try { - assertNotNull(sessionId); - assertTrue(attempts.get() >= 2); - assertTrue((Boolean) TerminalSessionManager.status(sessionId).get("alive")); - } finally { - TerminalSessionManager.kill(sessionId); - } + assertNotNull(sessionId); + assertEquals(2, attempts.get()); + assertEquals("Command Prompt", session.get("shell")); + assertEquals("system", session.get("shellId")); + assertTrue((Boolean) TerminalSessionManager.status(sessionId).get("alive")); } finally { - TerminalSessionManager.resetProcessFactoryForTests(); + TerminalSessionManager.kill(sessionId); } } @Test void fallsBackToNextShellWhenSpawnFails() throws Exception { - if (System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win")) { - return; - } AtomicInteger attempts = new AtomicInteger(); - TerminalSessionManager.setProcessFactoryForTests((command, dir, env, columns, rows) -> { - if (attempts.getAndIncrement() == 0) { - throw new IOException("powershell blocked by security software"); - } - return TerminalSessionManager.defaultProcessFactory(command, dir, env, columns, rows); - }); TerminalSessionManager.ShellCommand blocked = new TerminalSessionManager.ShellCommand( "blocked", List.of("powershell.exe"), "Windows PowerShell", TerminalSessionManager.ShellFamily.POWERSHELL); - TerminalSessionManager.ShellCommand fallback = TerminalSessionManager.unixShell("bash", "Bash", "bash"); - Map session = TerminalSessionManager.create(directory, 80, 24, List.of(blocked, fallback)); + TerminalSessionManager.ShellCommand fallback = + TerminalSessionManager.resolveShellCandidates("system").get(0); + Map session = TerminalSessionManager.create( + directory, + 80, + 24, + List.of(blocked, fallback), + (command, dir, env, columns, rows) -> { + if (attempts.getAndIncrement() == 0) { + throw new IOException("powershell blocked by security software"); + } + return TerminalSessionManager.defaultProcessFactory(command, dir, env, columns, rows); + } + ); String sessionId = (String) session.get("sessionId"); try { assertNotNull(sessionId); - assertEquals("bash", session.get("shellId")); + assertEquals("system", session.get("shellId")); assertEquals(2, attempts.get()); assertTrue((Boolean) TerminalSessionManager.status(sessionId).get("alive")); } finally { @@ -340,17 +288,42 @@ void fallsBackToNextShellWhenSpawnFails() throws Exception { @Test void throwsWhenNoCandidateCanStart() { AtomicInteger attempts = new AtomicInteger(); - TerminalSessionManager.setProcessFactoryForTests((command, dir, env, columns, rows) -> { - attempts.incrementAndGet(); - throw new IOException("blocked"); - }); TerminalSessionManager.ShellCommand blocked = new TerminalSessionManager.ShellCommand( "blocked", List.of("powershell.exe"), "Windows PowerShell", TerminalSessionManager.ShellFamily.POWERSHELL); TerminalSessionManager.ShellCommand cmd = new TerminalSessionManager.ShellCommand( "cmd", List.of("cmd.exe"), "Command Prompt", TerminalSessionManager.ShellFamily.CMD); IOException exception = assertThrows(IOException.class, - () -> TerminalSessionManager.create(directory, 80, 24, List.of(blocked, cmd))); + () -> TerminalSessionManager.create( + directory, + 80, + 24, + List.of(blocked, cmd), + (command, dir, env, columns, rows) -> { + attempts.incrementAndGet(); + throw new IOException("blocked"); + } + )); assertEquals("blocked", exception.getMessage()); assertEquals(2, attempts.get()); } + + @Test + void doesNotRetryShellsWhenWorkingDirectoryIsUnavailable() { + AtomicInteger attempts = new AtomicInteger(); + TerminalSessionManager.ShellCommand shell = new TerminalSessionManager.ShellCommand( + "shell", List.of("shell"), "Shell", TerminalSessionManager.ShellFamily.OTHER); + + assertThrows(IOException.class, + () -> TerminalSessionManager.create( + directory.resolve("missing"), + 80, + 24, + List.of(shell, shell), + (command, dir, env, columns, rows) -> { + attempts.incrementAndGet(); + throw new IOException("should not start"); + } + )); + assertEquals(0, attempts.get()); + } } From 11527fe721b66120c423edf96fe315c0a158ab7d Mon Sep 17 00:00:00 2001 From: zgq Date: Wed, 2 Sep 2026 12:48:58 +0800 Subject: [PATCH 37/49] fix(workspace): keep local files out of localStorage --- .../WorkspaceTabs/FilePreviewTab.tsx | 33 ++++++++++ .../components/WorkspaceTabs/index.tsx | 37 ++++++++--- .../src/store/workspace/store.ts | 11 +++- .../utils/workspaceTabPersistence.test.ts | 65 ++++++++++++++++++- .../utils/workspaceTabPersistence.ts | 35 +++++++++- 5 files changed, 165 insertions(+), 16 deletions(-) diff --git a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/FilePreviewTab.tsx b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/FilePreviewTab.tsx index ba299006ec..52bd1a59c2 100644 --- a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/FilePreviewTab.tsx +++ b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/FilePreviewTab.tsx @@ -268,6 +268,39 @@ const FilePreviewTab = memo(({ file, workspaceTabId }: FilePreviewTabProps) => { markdownContentRef.current = markdownContent; markdownPersistedContentRef.current = markdownPersistedContent; + useEffect(() => { + if (!isMarkdown || file.ddl !== undefined || !file.filePath) { + return; + } + void useWorkspaceStore + .getState() + .readFile(file.filePath, file.fileExtension, { + rootToken: file.fileRootToken, + relativePath: file.fileRelativePath, + charset: file.fileCharset, + workspaceTabId, + }) + .catch((error) => console.error('Failed to restore local file content', error)); + }, [ + file.ddl, + file.fileCharset, + file.fileExtension, + file.filePath, + file.fileRelativePath, + file.fileRootToken, + isMarkdown, + workspaceTabId, + ]); + + useEffect(() => { + if (!isMarkdown || file.ddl === undefined) { + return; + } + setMarkdownContent(file.ddl); + setMarkdownPreviewContent(file.ddl); + setMarkdownPersistedContent(file.ddl); + }, [file.ddl, file.filePath, isMarkdown]); + const markdownSaveKeybinding = useMemo(() => { const shortcutConfig = getEffectiveShortcutConfigMap(shortcutOverrides as ShortcutOverrides); return shortcutBindingToMonacoKeybinding(shortcutConfig[ShortcutAction.SqlSave].binding, monaco) || undefined; diff --git a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx index ec0bf7ae80..55243ac2e6 100644 --- a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx +++ b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx @@ -271,6 +271,17 @@ function rebuildSqlExecuteTabData(item: IWorkspaceTab) { return uniqueData; } + if ( + item.type === WorkspaceTabType.LocalSQLFile && + uniqueData.ddl === undefined && + uniqueData.filePath + ) { + return { + ...uniqueData, + loadSQL: () => jcefApi.readFile(uniqueData.filePath!, uniqueData.fileCharset).then((file) => file.content), + }; + } + const { dataSourceId, databaseName, schemaName } = uniqueData; if (item.type === WorkspaceTabType.VIEW) { @@ -887,6 +898,7 @@ const WorkspaceTabs = memo(() => { tabs: IWorkspaceTab[], layout: IWorkspaceTabSplitLayout | null | undefined = useWorkspaceStore.getState().workspaceTabSplitLayout, nextActiveConsoleId: string | number | null | undefined = useWorkspaceStore.getState().activeConsoleId, + nextRecentlyClosedWorkspaceTabs?: IWorkspaceTab[], ) => { const orderedTabs = orderPinnedWorkspaceTabsFirst(tabs); const orderedLayout = orderSplitLayoutPaneIdsByPinned(layout || null, orderedTabs); @@ -902,6 +914,9 @@ const WorkspaceTabs = memo(() => { if (!areWorkspaceTabSplitLayoutsEqual(currentState.workspaceTabSplitLayout, normalizedLayout)) { nextState.workspaceTabSplitLayout = normalizedLayout; } + if (nextRecentlyClosedWorkspaceTabs !== undefined) { + nextState.recentlyClosedWorkspaceTabs = nextRecentlyClosedWorkspaceTabs; + } useWorkspaceStore.setState(nextState); }; @@ -1018,25 +1033,22 @@ const WorkspaceTabs = memo(() => { }); }; - const rememberClosedWorkspaceTabs = (tabs: IWorkspaceTab[]) => { + const createRecentlyClosedWorkspaceTabs = (tabs: IWorkspaceTab[]) => { + const currentRecentlyClosed = useWorkspaceStore.getState().recentlyClosedWorkspaceTabs || []; if (!tabs.length) { - return; + return currentRecentlyClosed; } const currentEditorList = useWorkspaceStore.getState().editorList || {}; const snapshots = tabs .map((tab) => createPersistableWorkspaceTabSnapshot(tab, currentEditorList[tab.id]?.getValue?.())) .filter(Boolean) as IWorkspaceTab[]; if (!snapshots.length) { - return; + return currentRecentlyClosed; } - const currentRecentlyClosed = useWorkspaceStore.getState().recentlyClosedWorkspaceTabs || []; - const nextRecentlyClosedWorkspaceTabs = [...snapshots, ...currentRecentlyClosed].slice( + return [...snapshots, ...currentRecentlyClosed].slice( 0, RECENTLY_CLOSED_WORKSPACE_TAB_LIMIT, ); - useWorkspaceStore.setState({ - recentlyClosedWorkspaceTabs: nextRecentlyClosedWorkspaceTabs, - }); }; const closeWorkspaceTabs = (tabs: IWorkspaceTab[]) => { @@ -1053,8 +1065,13 @@ const WorkspaceTabs = memo(() => { layout: workspaceTabSplitLayout, orderedNextWorkspaceTabList, }); - rememberClosedWorkspaceTabs(closableTabs); - setWorkspaceTabsState(orderedNextWorkspaceTabList, workspaceTabSplitLayout, nextActiveConsoleId); + const nextRecentlyClosedWorkspaceTabs = createRecentlyClosedWorkspaceTabs(closableTabs); + setWorkspaceTabsState( + orderedNextWorkspaceTabList, + workspaceTabSplitLayout, + nextActiveConsoleId, + nextRecentlyClosedWorkspaceTabs, + ); if (closeTabIds.has(activeConsoleId as any)) { setActiveConsoleId(nextActiveConsoleId); diff --git a/chat2db-community-client/src/store/workspace/store.ts b/chat2db-community-client/src/store/workspace/store.ts index afb7f668ec..40087a0dd9 100644 --- a/chat2db-community-client/src/store/workspace/store.ts +++ b/chat2db-community-client/src/store/workspace/store.ts @@ -1,5 +1,5 @@ import { clientRuntime } from '@client-runtime'; -import { PersistOptions, devtools, persist } from 'zustand/middleware'; +import { createJSONStorage, PersistOptions, devtools, persist } from 'zustand/middleware'; import { shallow } from 'zustand/shallow'; import { createWithEqualityFn } from 'zustand/traditional'; import { StateCreator } from 'zustand/vanilla'; @@ -13,6 +13,7 @@ import { getHydratedWorkspaceLayout, getPersistableWorkspaceLayout, getPersistableWorkspaceTabList, + createSafeWorkspaceStorage, } from './utils/workspaceTabPersistence'; type WorkspaceAction = CommonAction & ConfigAction & ConsoleAction & ModalAction; @@ -39,6 +40,7 @@ type GlobalPersist = Pick< // local-storage Options const persistOptions: PersistOptions = { name: clientRuntime.workspaceStoreName, + storage: createJSONStorage(() => createSafeWorkspaceStorage(localStorage)), partialize: (state) => { const workspaceTabList = getPersistableWorkspaceTabList(state.workspaceTabList); return { @@ -55,10 +57,17 @@ const persistOptions: PersistOptions = { }, merge: (persistedState, currentState) => { const storedState = (persistedState || {}) as Partial; + const workspaceTabList = getPersistableWorkspaceTabList(storedState.workspaceTabList); return { ...currentState, ...storedState, layout: getHydratedWorkspaceLayout(currentState.layout, storedState.layout), + workspaceTabList, + activeConsoleId: getPersistableActiveConsoleId({ + activeConsoleId: storedState.activeConsoleId, + workspaceTabList, + }), + recentlyClosedWorkspaceTabs: getPersistableWorkspaceTabList(storedState.recentlyClosedWorkspaceTabs) || [], }; }, }; diff --git a/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.test.ts b/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.test.ts index 231d1df57a..92269d801b 100644 --- a/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.test.ts +++ b/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.test.ts @@ -6,6 +6,7 @@ import { getPersistableActiveConsoleId, getPersistableWorkspaceLayout, getPersistableWorkspaceTabList, + createSafeWorkspaceStorage, } from './workspaceTabPersistence'; const tabs: IWorkspaceTab[] = [ @@ -51,8 +52,43 @@ assert.deepEqual( ); assert.deepEqual( getPersistableWorkspaceTabList(tabs)?.[0].uniqueData, - tabs[0].uniqueData, - 'local file charset and BOM metadata must survive workspace persistence', + { + filePath: '/tmp/README.md', + fileExtension: 'md', + fileCharset: 'UTF-8', + fileBom: true, + }, + 'local file metadata must survive workspace persistence without its content', +); +assert.equal(tabs[0].uniqueData?.ddl, '# Hello', 'persistence filtering must not modify live editor state'); + +const largeLocalFileContent = 'x'.repeat(Math.ceil(2.8 * 1024 * 1024)); +const largeLocalFileTabs: IWorkspaceTab[] = ['sql', 'md', 'txt', 'json'].map((extension) => ({ + id: `large-${extension}`, + type: WorkspaceTabType.LocalSQLFile, + title: `large.${extension}`, + uniqueData: { + filePath: `/tmp/large.${extension}`, + fileExtension: extension, + fileCharset: 'UTF-8', + ddl: largeLocalFileContent, + }, +})); +const persistedLargeLocalFiles = getPersistableWorkspaceTabList(largeLocalFileTabs); +assert.deepEqual( + persistedLargeLocalFiles?.map((tab) => tab.uniqueData?.ddl), + [undefined, undefined, undefined, undefined], + 'all local text file types must omit their content from workspace localStorage', +); +assert.equal( + JSON.stringify(persistedLargeLocalFiles).length < 4096, + true, + 'large local files must persist only a small metadata payload', +); +assert.equal( + largeLocalFileTabs.every((tab) => tab.uniqueData?.ddl === largeLocalFileContent), + true, + 'persistence filtering must preserve every live local editor value', ); const consoleTabs: IWorkspaceTab[] = Array.from({ length: 101 }, (_, index) => ({ @@ -104,6 +140,31 @@ assert.deepEqual( 'shared non-circular values must remain available in each persisted tab', ); +const storageWriteErrors: unknown[] = []; +let failStorageWrites = true; +let storedWorkspaceValue: string | undefined; +const safeStorage = createSafeWorkspaceStorage( + { + getItem: () => null, + setItem: (_name, value) => { + if (failStorageWrites) { + throw new DOMException('Quota exceeded', 'QuotaExceededError'); + } + storedWorkspaceValue = value; + }, + removeItem: () => undefined, + }, + (error) => storageWriteErrors.push(error), +); +assert.doesNotThrow( + () => safeStorage.setItem('Chat2DB_Workspace_Store', '{}'), + 'workspace persistence failures must not interrupt UI state updates', +); +assert.equal(storageWriteErrors.length, 1, 'workspace persistence failures must still be reported'); +failStorageWrites = false; +safeStorage.setItem('Chat2DB_Workspace_Store', '{"state":{}}'); +assert.equal(storedWorkspaceValue, '{"state":{}}', 'workspace persistence must recover after a failed write'); + const circularPanelState: Record = {}; circularPanelState.self = circularPanelState; const persistableLayout = getPersistableWorkspaceLayout({ diff --git a/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.ts b/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.ts index e1e253cfe0..3520bd0d88 100644 --- a/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.ts +++ b/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.ts @@ -1,5 +1,6 @@ import { WorkspaceTabType } from '@/constants/workspace'; import { IWorkspaceTab } from '@/typings/workspace'; +import type { StateStorage } from 'zustand/middleware'; import { initConfigState, type ConfigState } from '../slices/config/initialState'; /** @@ -15,6 +16,17 @@ function capPersistedTabs(tabs: IWorkspaceTab[]): IWorkspaceTab[] { return tabs.length > MAX_PERSISTED_TABS ? tabs.slice(-MAX_PERSISTED_TABS) : tabs; } +function stripLocalFileContent(tab: IWorkspaceTab): IWorkspaceTab { + if (tab.type !== WorkspaceTabType.LocalSQLFile || !tab.uniqueData) { + return tab; + } + const { ddl: _ddl, ...uniqueData } = tab.uniqueData; + return { + ...tab, + uniqueData, + }; +} + function createPersistenceReplacer() { const ancestors: unknown[] = []; @@ -76,9 +88,9 @@ export function getPersistableWorkspaceTabList(workspaceTabList?: IWorkspaceTab[ return workspaceTabList || null; } - const persistableTabs = workspaceTabList.filter( - (tab) => tab.type !== WorkspaceTabType.Terminal && !tab.uniqueData?.filePreviewMimeType, - ); + const persistableTabs = workspaceTabList + .filter((tab) => tab.type !== WorkspaceTabType.Terminal && !tab.uniqueData?.filePreviewMimeType) + .map(stripLocalFileContent); const cappedTabs = capPersistedTabs(persistableTabs); try { @@ -92,6 +104,23 @@ export function getPersistableWorkspaceTabList(workspaceTabList?: IWorkspaceTab[ } } +export function createSafeWorkspaceStorage( + storage: StateStorage, + onWriteError: (error: unknown) => void = (error) => console.error('Failed to persist workspace state', error), +): StateStorage { + return { + getItem: (name) => storage.getItem(name), + setItem: (name, value) => { + try { + return storage.setItem(name, value); + } catch (error) { + onWriteError(error); + } + }, + removeItem: (name) => storage.removeItem(name), + }; +} + export function getPersistableActiveConsoleId(params: { activeConsoleId?: string | number | null; workspaceTabList?: IWorkspaceTab[] | null; From 88f4e8af2a3fb0dc2d42a696fb7546204d0896a2 Mon Sep 17 00:00:00 2001 From: zgq Date: Wed, 2 Sep 2026 13:04:52 +0800 Subject: [PATCH 38/49] fix(workspace): migrate legacy local file state --- .../components/WorkspaceTabs/index.tsx | 18 ++++++--- .../src/store/workspace/store.ts | 12 ++---- .../utils/localFileWorkspaceTab.test.ts | 39 ++++++++++++++++++- .../workspace/utils/localFileWorkspaceTab.ts | 18 +++++++++ .../utils/workspaceTabPersistence.test.ts | 19 +++++++++ .../utils/workspaceTabPersistence.ts | 19 +++++++++ 6 files changed, 110 insertions(+), 15 deletions(-) diff --git a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx index 55243ac2e6..9903ec3440 100644 --- a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx +++ b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx @@ -48,6 +48,7 @@ import TerminalTab from './TerminalTab'; import { useWorkspaceStore } from '@/store/workspace'; import { useGlobalStore } from '@/store/global'; import { getPersistableActiveConsoleId } from '@/store/workspace/utils/workspaceTabPersistence'; +import { getRestoredLocalFileReadRequest } from '@/store/workspace/utils/localFileWorkspaceTab'; import { isWorkspaceResultInspectorCode } from '@/store/workspace/utils/resultInspector'; import { isConsoleTabNameCustomized } from '@/store/workspace/utils/consoleTabName'; import { useTreeStore } from '@/store/tree'; @@ -271,14 +272,19 @@ function rebuildSqlExecuteTabData(item: IWorkspaceTab) { return uniqueData; } - if ( - item.type === WorkspaceTabType.LocalSQLFile && - uniqueData.ddl === undefined && - uniqueData.filePath - ) { + const localFileReadRequest = getRestoredLocalFileReadRequest(item); + if (localFileReadRequest) { return { ...uniqueData, - loadSQL: () => jcefApi.readFile(uniqueData.filePath!, uniqueData.fileCharset).then((file) => file.content), + loadSQL: () => + useWorkspaceStore + .getState() + .readFile( + localFileReadRequest.filePath, + localFileReadRequest.fileExtension, + localFileReadRequest.context, + ) + .then((file) => file?.content || ''), }; } diff --git a/chat2db-community-client/src/store/workspace/store.ts b/chat2db-community-client/src/store/workspace/store.ts index 40087a0dd9..34939ccb87 100644 --- a/chat2db-community-client/src/store/workspace/store.ts +++ b/chat2db-community-client/src/store/workspace/store.ts @@ -14,6 +14,7 @@ import { getPersistableWorkspaceLayout, getPersistableWorkspaceTabList, createSafeWorkspaceStorage, + sanitizePersistedWorkspaceTabsState, } from './utils/workspaceTabPersistence'; type WorkspaceAction = CommonAction & ConfigAction & ConsoleAction & ModalAction; @@ -40,6 +41,7 @@ type GlobalPersist = Pick< // local-storage Options const persistOptions: PersistOptions = { name: clientRuntime.workspaceStoreName, + version: 1, storage: createJSONStorage(() => createSafeWorkspaceStorage(localStorage)), partialize: (state) => { const workspaceTabList = getPersistableWorkspaceTabList(state.workspaceTabList); @@ -55,19 +57,13 @@ const persistOptions: PersistOptions = { recentlyClosedWorkspaceTabs: getPersistableWorkspaceTabList(state.recentlyClosedWorkspaceTabs) || [], }; }, + migrate: (persistedState) => sanitizePersistedWorkspaceTabsState((persistedState || {}) as GlobalPersist), merge: (persistedState, currentState) => { - const storedState = (persistedState || {}) as Partial; - const workspaceTabList = getPersistableWorkspaceTabList(storedState.workspaceTabList); + const storedState = sanitizePersistedWorkspaceTabsState((persistedState || {}) as Partial); return { ...currentState, ...storedState, layout: getHydratedWorkspaceLayout(currentState.layout, storedState.layout), - workspaceTabList, - activeConsoleId: getPersistableActiveConsoleId({ - activeConsoleId: storedState.activeConsoleId, - workspaceTabList, - }), - recentlyClosedWorkspaceTabs: getPersistableWorkspaceTabList(storedState.recentlyClosedWorkspaceTabs) || [], }; }, }; diff --git a/chat2db-community-client/src/store/workspace/utils/localFileWorkspaceTab.test.ts b/chat2db-community-client/src/store/workspace/utils/localFileWorkspaceTab.test.ts index df1e886b02..87d9157dd7 100644 --- a/chat2db-community-client/src/store/workspace/utils/localFileWorkspaceTab.test.ts +++ b/chat2db-community-client/src/store/workspace/utils/localFileWorkspaceTab.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { WorkspaceTabType } from '@/constants/workspace'; import type { IBoundInfo, IWorkspaceTab } from '@/typings/workspace'; -import { refreshLocalFileWorkspaceTab } from './localFileWorkspaceTab'; +import { getRestoredLocalFileReadRequest, refreshLocalFileWorkspaceTab } from './localFileWorkspaceTab'; const originalUniqueData = Object.freeze({ filePath: '/tmp/report.pdf', @@ -65,4 +65,41 @@ assert.equal( 'a closed target tab must not fall back to another tab with the same path', ); +const restoredReadRequest = getRestoredLocalFileReadRequest({ + id: 'restored-sql', + type: WorkspaceTabType.LocalSQLFile, + title: 'restored.sql', + uniqueData: { + filePath: '/tmp/root/restored.sql', + fileExtension: 'sql', + fileRootToken: 'root-token', + fileRelativePath: 'nested/restored.sql', + fileCharset: 'GB18030', + }, +}); +assert.deepEqual( + restoredReadRequest, + { + filePath: '/tmp/root/restored.sql', + fileExtension: 'sql', + context: { + rootToken: 'root-token', + relativePath: 'nested/restored.sql', + charset: 'GB18030', + workspaceTabId: 'restored-sql', + }, + }, + 'restored local files must preserve directory access and encoding context', +); +assert.equal( + getRestoredLocalFileReadRequest({ + id: 'live-sql', + type: WorkspaceTabType.LocalSQLFile, + title: 'live.sql', + uniqueData: { filePath: '/tmp/live.sql', ddl: 'select 1' }, + }), + undefined, + 'live local files with content must not be reloaded from disk', +); + console.log('local file workspace tab tests passed'); diff --git a/chat2db-community-client/src/store/workspace/utils/localFileWorkspaceTab.ts b/chat2db-community-client/src/store/workspace/utils/localFileWorkspaceTab.ts index b4fb94f833..77823333e5 100644 --- a/chat2db-community-client/src/store/workspace/utils/localFileWorkspaceTab.ts +++ b/chat2db-community-client/src/store/workspace/utils/localFileWorkspaceTab.ts @@ -1,4 +1,22 @@ import type { IBoundInfo, IWorkspaceTab } from '@/typings/workspace'; +import { WorkspaceTabType } from '@/constants/workspace'; + +export function getRestoredLocalFileReadRequest(tab: IWorkspaceTab) { + const file = tab.uniqueData; + if (tab.type !== WorkspaceTabType.LocalSQLFile || !file?.filePath || file.ddl !== undefined) { + return undefined; + } + return { + filePath: file.filePath, + fileExtension: file.fileExtension, + context: { + rootToken: file.fileRootToken, + relativePath: file.fileRelativePath, + charset: file.fileCharset, + workspaceTabId: tab.id, + }, + }; +} export function refreshLocalFileWorkspaceTab( workspaceTabList: IWorkspaceTab[] | null | undefined, diff --git a/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.test.ts b/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.test.ts index 92269d801b..578d2b4a6e 100644 --- a/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.test.ts +++ b/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.test.ts @@ -7,6 +7,7 @@ import { getPersistableWorkspaceLayout, getPersistableWorkspaceTabList, createSafeWorkspaceStorage, + sanitizePersistedWorkspaceTabsState, } from './workspaceTabPersistence'; const tabs: IWorkspaceTab[] = [ @@ -165,6 +166,24 @@ failStorageWrites = false; safeStorage.setItem('Chat2DB_Workspace_Store', '{"state":{}}'); assert.equal(storedWorkspaceValue, '{"state":{}}', 'workspace persistence must recover after a failed write'); +const sanitizedLegacyState = sanitizePersistedWorkspaceTabsState({ + workspaceTabList: largeLocalFileTabs, + activeConsoleId: 'large-sql', + recentlyClosedWorkspaceTabs: largeLocalFileTabs, + currentConnectionDetails: { dataSourceId: 42 }, +}); +assert.equal( + JSON.stringify(sanitizedLegacyState).includes(largeLocalFileContent), + false, + 'legacy workspace migration must remove local file content from open and recently closed tabs', +); +assert.equal(sanitizedLegacyState.activeConsoleId, 'large-sql', 'legacy workspace migration must retain the active tab'); +assert.deepEqual( + sanitizedLegacyState.currentConnectionDetails, + { dataSourceId: 42 }, + 'legacy workspace migration must preserve unrelated persisted state', +); + const circularPanelState: Record = {}; circularPanelState.self = circularPanelState; const persistableLayout = getPersistableWorkspaceLayout({ diff --git a/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.ts b/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.ts index 3520bd0d88..3da4915630 100644 --- a/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.ts +++ b/chat2db-community-client/src/store/workspace/utils/workspaceTabPersistence.ts @@ -134,3 +134,22 @@ export function getPersistableActiveConsoleId(params: { } return workspaceTabList[0].id; } + +export interface PersistedWorkspaceTabsState { + workspaceTabList?: IWorkspaceTab[] | null; + activeConsoleId?: string | number | null; + recentlyClosedWorkspaceTabs?: IWorkspaceTab[] | null; +} + +export function sanitizePersistedWorkspaceTabsState(state: T) { + const workspaceTabList = getPersistableWorkspaceTabList(state.workspaceTabList); + return { + ...state, + workspaceTabList, + activeConsoleId: getPersistableActiveConsoleId({ + activeConsoleId: state.activeConsoleId, + workspaceTabList, + }), + recentlyClosedWorkspaceTabs: getPersistableWorkspaceTabList(state.recentlyClosedWorkspaceTabs) || [], + }; +} From 4e894c08eda5f714b97e4c7c6e1f7ead5f7c0eea Mon Sep 17 00:00:00 2001 From: zgq Date: Wed, 2 Sep 2026 12:42:51 +0800 Subject: [PATCH 39/49] perf(mysql): avoid full parse for ordinary completion scripts --- .../MysqlSqlCompletionStatementLocator.java | 62 +++++- .../MysqlSqlCompletionPerformanceTest.java | 201 ++++++++++++++++++ ...ysqlSqlCompletionStatementLocatorTest.java | 92 ++++++++ 3 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/completion/MysqlSqlCompletionPerformanceTest.java diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/completion/locate/MysqlSqlCompletionStatementLocator.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/completion/locate/MysqlSqlCompletionStatementLocator.java index 1976a0a701..1f4378c797 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/completion/locate/MysqlSqlCompletionStatementLocator.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/completion/locate/MysqlSqlCompletionStatementLocator.java @@ -113,6 +113,11 @@ public SqlCompletionStatementWindow locate(SqlCompletionInputCleanResponse input SqlCompletionStatementWindowTypeEnum.EMPTY_STATEMENT.name()); } + List tokens = MysqlSqlCompletionTokenUtil.tokens(parseSql); + if (canUseTokenWindow(tokens) && !hasUnsafeLexerErrors(sourceSql)) { + return tokenFallbackWindow(sourceSql, parseSql, cursor, tokens, List.of()); + } + ParserWindowResult rawResult = parseWindow(parseSql, cursor, StatementWindowAuthorityEnum.RAW_PARSE); if (rawResult.proof().isPresent() && rawResult.syntaxErrors() == 0) { return toWindow(sourceSql, parseSql, rawResult.proof().get(), cursor); @@ -146,9 +151,16 @@ private Optional probeProof(String parseSql, int cursor, b } private SqlCompletionStatementWindow tokenFallbackWindow(String sourceSql, String parseSql, int cursor) { - CommonTokenStream tokenStream = MysqlSqlCompletionTokenUtil.tokenStream(parseSql); - List tokens = tokenStream.getTokens(); + List tokens = MysqlSqlCompletionTokenUtil.tokens(parseSql); List routineWindows = findRoutineWindows(tokens, parseSql); + return tokenFallbackWindow(sourceSql, parseSql, cursor, tokens, routineWindows); + } + + private SqlCompletionStatementWindow tokenFallbackWindow(String sourceSql, + String parseSql, + int cursor, + List tokens, + List routineWindows) { RoutineWindow routineWindow = containingRoutineWindow(routineWindows, cursor); if (Objects.nonNull(routineWindow)) { return new SqlCompletionStatementWindow( @@ -185,6 +197,52 @@ private SqlCompletionStatementWindow tokenFallbackWindow(String sourceSql, Strin type.name()); } + private boolean canUseTokenWindow(List tokens) { + boolean hasStatementSeparator = false; + int parenthesisDepth = 0; + int topLevelStatementStarts = 0; + boolean balancedParentheses = true; + for (Token token : tokens) { + if (!MysqlSqlCompletionTokenUtil.isDefaultToken(token)) { + continue; + } + int tokenType = token.getType(); + if (tokenType == MySqlLexer.SEMI) { + hasStatementSeparator = true; + continue; + } + if (tokenType == MySqlLexer.CREATE || tokenType == MySqlLexer.DELIMITER_STATEMENT) { + return false; + } + if (tokenType == MySqlLexer.LR_BRACKET) { + parenthesisDepth++; + continue; + } + if (tokenType == MySqlLexer.RR_BRACKET) { + if (parenthesisDepth == 0) { + balancedParentheses = false; + } else { + parenthesisDepth--; + } + continue; + } + if (parenthesisDepth == 0 && isSqlStatementStartToken(tokenType)) { + topLevelStatementStarts++; + } + } + return balancedParentheses && parenthesisDepth == 0 + && (hasStatementSeparator || topLevelStatementStarts == 1); + } + + private boolean hasUnsafeLexerErrors(String sourceSql) { + for (Token token : MysqlSqlCompletionTokenUtil.tokens(sourceSql)) { + if (token.getChannel() == MySqlLexer.ERRORCHANNEL && !"?".equals(token.getText())) { + return true; + } + } + return false; + } + private ParserWindowResult parseWindow(String sql, int cursor, StatementWindowAuthorityEnum authority) { CommonTokenStream tokenStream = MysqlSqlCompletionTokenUtil.tokenStream(sql); ParsedRoot parsedRoot = parseRoot(tokenStream); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/completion/MysqlSqlCompletionPerformanceTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/completion/MysqlSqlCompletionPerformanceTest.java new file mode 100644 index 0000000000..2b2e19c606 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/completion/MysqlSqlCompletionPerformanceTest.java @@ -0,0 +1,201 @@ +package ai.chat2db.plugin.mysql.completion; + +import ai.chat2db.community.domain.api.enums.completion.SqlCompletionCandidateTypeEnum; +import ai.chat2db.community.domain.api.model.completion.SqlCompletionCandidate; +import ai.chat2db.community.domain.api.model.completion.request.DbSqlCompletionMetadataRequest; +import ai.chat2db.community.domain.api.model.completion.request.DbSqlCompletionRequest; +import ai.chat2db.community.domain.api.model.completion.result.SqlCompletionMetadataResponse; +import ai.chat2db.community.domain.api.model.completion.result.SqlCompletionResponse; +import ai.chat2db.community.domain.api.service.db.ISqlCompletionMetadataProvider; +import ai.chat2db.spi.parser.completion.SqlCompletionDialectComponents; +import ai.chat2db.spi.parser.completion.SqlCompletionPipelineState; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + + +class MysqlSqlCompletionPerformanceTest { + + private static final int WARMUP_RUNS = 1; + private static final int MEASURED_RUNS = 2; + private static final long MAX_SINGLE_COMPLETION_NANOS = TimeUnit.SECONDS.toNanos(5); + + private final SqlCompletionDialectComponents components = new MysqlSqlCompletionDialect().components(); + + @Test + void largeSqlCompletionStaysWithinPerformanceBudget() { + List cases = List.of( + new PerformanceCase("single_statement_200_lines_166_ids", singleStatement(200, 166), false), + new PerformanceCase("multi_statement_200_history_166_ids", multiStatement(200, 166), true), + new PerformanceCase("multi_statement_200_history_500_ids", multiStatement(200, 500), true), + new PerformanceCase("multi_statement_200_history_1000_ids", multiStatement(200, 1000), true)); + + for (PerformanceCase performanceCase : cases) { + CountingMetadataProvider metadataProvider = new CountingMetadataProvider(); + DbSqlCompletionRequest request = DbSqlCompletionRequest.of( + performanceCase.sql(), performanceCase.sql().length(), "MYSQL", 0, metadataProvider); + for (int index = 0; index < WARMUP_RUNS; index++) { + measure(request); + } + metadataProvider.reset(); + + PipelineMeasurement lastMeasurement = null; + for (int index = 0; index < MEASURED_RUNS; index++) { + lastMeasurement = measure(request); + Assertions.assertTrue(lastMeasurement.totalNanos() < MAX_SINGLE_COMPLETION_NANOS, + performanceCase.name()); + } + + Assertions.assertNotNull(lastMeasurement); + Assertions.assertTrue(lastMeasurement.totalNanos() > 0, performanceCase.name()); + Assertions.assertFalse(lastMeasurement.stageNanos().isEmpty(), performanceCase.name()); + SqlCompletionResponse result = lastMeasurement.state().result(); + Assertions.assertEquals("SUCCESS", result.getStatus(), performanceCase.name()); + Assertions.assertFalse(result.getCandidates().isEmpty(), performanceCase.name()); + Assertions.assertTrue(lastMeasurement.state().window().sourceSql().length() <= performanceCase.sql().length(), + performanceCase.name()); + if (performanceCase.hasHistory()) { + Assertions.assertTrue(lastMeasurement.state().window().sourceStartOffset() > 0, + performanceCase.name()); + } + Assertions.assertTrue(metadataProvider.totalCalls() > 0, performanceCase.name()); + } + } + + private PipelineMeasurement measure(DbSqlCompletionRequest request) { + Map stageNanos = new LinkedHashMap<>(); + long totalStartedAtNanos = System.nanoTime(); + SqlCompletionPipelineState state = SqlCompletionPipelineState.start(request); + + long stageStartedAtNanos = System.nanoTime(); + state = state.withInput(components.inputCleaner().clean(state)); + stageNanos.put("input", elapsed(stageStartedAtNanos)); + + stageStartedAtNanos = System.nanoTime(); + state = state.withWindow(components.statementLocator().locate(state)); + stageNanos.put("statementWindow", elapsed(stageStartedAtNanos)); + + stageStartedAtNanos = System.nanoTime(); + state = state.withCursorContext(components.cursorAnalyzer().analyze(state)); + stageNanos.put("cursor", elapsed(stageStartedAtNanos)); + if (state.cursorContext() == null) { + return new PipelineMeasurement(state.withResult(SqlCompletionResponse.empty()), stageNanos, + elapsed(totalStartedAtNanos)); + } + + stageStartedAtNanos = System.nanoTime(); + state = state.withDummySql(components.dummyBuilder().build(state)); + stageNanos.put("dummySql", elapsed(stageStartedAtNanos)); + + stageStartedAtNanos = System.nanoTime(); + state = state.withLocalContext(components.localContextCollector().collect(state)); + stageNanos.put("localContext", elapsed(stageStartedAtNanos)); + + stageStartedAtNanos = System.nanoTime(); + state = state.withC3Result(components.c3Collector().collect(state)); + stageNanos.put("c3", elapsed(stageStartedAtNanos)); + + stageStartedAtNanos = System.nanoTime(); + state = state.withRuleEvidence(components.ruleEvidenceCollector().collect(state)); + stageNanos.put("ruleEvidence", elapsed(stageStartedAtNanos)); + + stageStartedAtNanos = System.nanoTime(); + state = state.withSlots(components.slotClassifier().classify(state)); + stageNanos.put("slots", elapsed(stageStartedAtNanos)); + + stageStartedAtNanos = System.nanoTime(); + state = state.withIntents(components.intentResolver().resolve(state)); + stageNanos.put("intents", elapsed(stageStartedAtNanos)); + + stageStartedAtNanos = System.nanoTime(); + state = state.withCandidatePlan(components.candidatePlanner().plan(state)); + stageNanos.put("candidatePlan", elapsed(stageStartedAtNanos)); + + stageStartedAtNanos = System.nanoTime(); + state = state.withResult(components.presentationProcessor().process(state)); + stageNanos.put("presentation", elapsed(stageStartedAtNanos)); + return new PipelineMeasurement(state, stageNanos, elapsed(totalStartedAtNanos)); + } + + private static long elapsed(long startedAtNanos) { + return Math.max(0L, System.nanoTime() - startedAtNanos); + } + + private static String singleStatement(int targetLineCount, int idCount) { + StringBuilder sql = currentStatementPrefix(idCount); + int lineCount = 2 + idCount / 10; + while (lineCount < targetLineCount - 1) { + sql.append("-- filler\n"); + lineCount++; + } + return sql.append("and na").toString(); + } + + private static String multiStatement(int historyStatementCount, int idCount) { + StringBuilder sql = new StringBuilder(); + for (int index = 0; index < historyStatementCount; index++) { + sql.append("select * from archive_").append(index).append(";\n"); + } + return sql.append(currentStatementPrefix(idCount)).append("and na").toString(); + } + + private static StringBuilder currentStatementPrefix(int idCount) { + StringBuilder sql = new StringBuilder("select * from orders where id in (\n"); + for (int index = 0; index < idCount; index++) { + if (index > 0) { + sql.append(','); + } + sql.append(index); + if (index % 10 == 9) { + sql.append('\n'); + } + } + return sql.append(")\n"); + } + + private record PerformanceCase(String name, String sql, boolean hasHistory) { + } + + private record PipelineMeasurement(SqlCompletionPipelineState state, + Map stageNanos, + long totalNanos) { + } + + private static final class CountingMetadataProvider implements ISqlCompletionMetadataProvider { + + private final Map counts = new LinkedHashMap<>(); + + @Override + public SqlCompletionMetadataResponse list(DbSqlCompletionMetadataRequest request) { + counts.merge(request.type(), 1, Integer::sum); + SqlCompletionCandidateTypeEnum type = SqlCompletionCandidateTypeEnum.from(request.type()); + if (type == SqlCompletionCandidateTypeEnum.COLUMN) { + return SqlCompletionMetadataResponse.of(List.of(column("name"), column("status"))); + } + if (type == SqlCompletionCandidateTypeEnum.DATABASE) { + return SqlCompletionMetadataResponse.of(List.of( + SqlCompletionCandidate.of(SqlCompletionCandidateTypeEnum.DATABASE, "app"))); + } + return SqlCompletionMetadataResponse.of(new ArrayList<>()); + } + + private static SqlCompletionCandidate column(String name) { + SqlCompletionCandidate candidate = SqlCompletionCandidate.of(SqlCompletionCandidateTypeEnum.COLUMN, name); + candidate.setColumnName(name); + candidate.setTableName("orders"); + return candidate; + } + + private int totalCalls() { + return counts.values().stream().mapToInt(Integer::intValue).sum(); + } + + private void reset() { + counts.clear(); + } + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/completion/locate/MysqlSqlCompletionStatementLocatorTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/completion/locate/MysqlSqlCompletionStatementLocatorTest.java index f115840e5c..837556512f 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/completion/locate/MysqlSqlCompletionStatementLocatorTest.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/completion/locate/MysqlSqlCompletionStatementLocatorTest.java @@ -179,6 +179,98 @@ void ignoresSemicolonInsideLineCommentBeforeCursor() { assertCurrentWindow(window, sql, 0, sql.length()); } + @Test + void keepsNestedSelectInsideSingleStatementOnTheFastPath() { + String sql = "select * from orders where id in (select id from users where users.id = 1) and na"; + + SqlCompletionStatementWindow window = locate(sql); + + assertCurrentWindow(window, sql, 0, sql.length()); + } + + @Test + void keepsUnionQueryTogetherWhenTopLevelSelectsRepeat() { + String sql = "select id from orders where na union select id from archived_orders where na"; + + SqlCompletionStatementWindow window = locate(sql); + + assertCurrentWindow(window, sql, 0, sql.length()); + } + + @Test + void keepsInsertSelectTogetherWhenTopLevelStatementKeywordsRepeat() { + String sql = "insert into orders(id) select id from users where na"; + + SqlCompletionStatementWindow window = locate(sql); + + assertCurrentWindow(window, sql, 0, sql.length()); + } + + @Test + void preservesCurrentWindowWhenLargeHistoryCursorIsBeforeSeparator() { + StringBuilder sqlBuilder = new StringBuilder(); + for (int index = 0; index < 200; index++) { + sqlBuilder.append("select * from archive_").append(index).append(";\n"); + } + int currentStart = sqlBuilder.length(); + sqlBuilder.append("select * from orders where na; select * from next_table"); + String sql = sqlBuilder.toString(); + int cursor = currentStart + "select * from orders where na".length(); + + SqlCompletionStatementWindow window = locate(sql, cursor); + + assertCurrentWindow(window, "select * from orders where na", currentStart, cursor); + } + + @Test + void fallsBackToParserForUnclosedParentheses() { + String sql = "select coalesce(o.sta from app.orders o"; + + SqlCompletionStatementWindow window = locate(sql); + + assertCurrentWindow(window, sql, 0, sql.length()); + } + + @Test + void fallsBackToParserForUnclosedStringLiteral() { + String sql = "select * from orders where name = 'unterminated; select * from users where na"; + + SqlCompletionStatementWindow window = locate(sql); + + assertCurrentWindow(window, "select * from users where na", sql.lastIndexOf("select * from users"), + sql.length()); + } + + @Test + void fallsBackToParserForUnclosedBlockComment() { + String sql = "select * from orders /* unterminated; select * from users where na"; + + SqlCompletionStatementWindow window = locate(sql); + + assertCurrentWindow(window, "select * from users where na", sql.lastIndexOf("select * from users"), + sql.length()); + } + + @Test + void keepsQuestionMarkParametersEligibleForTheFastPath() { + String sql = "select * from orders where id = ?; select * from users where na"; + + SqlCompletionStatementWindow window = locate(sql); + + assertCurrentWindow(window, "select * from users where na", sql.lastIndexOf("select * from users"), + sql.length()); + } + + @Test + void doesNotUseFastPathForUnmatchedClosingParenthesis() { + String sql = "select * from orders where id = 1); select * from users where na"; + + SqlCompletionStatementWindow window = locate(sql); + + assertCurrentWindow(window, "select * from users where na", sql.lastIndexOf("select * from users"), + sql.length()); + } + @Test void keepsLargeScriptCurrentWindowBoundedByStatementSeparator() { StringBuilder sqlBuilder = new StringBuilder(); From e439f86779e46d005a611a55ede7c9e2d123569c Mon Sep 17 00:00:00 2001 From: liuhy Date: Tue, 1 Sep 2026 22:51:46 -0700 Subject: [PATCH 40/49] fix(mysql): format active transaction start time --- .../activeTransactionUtils.test.ts | 6 ++++++ .../ActiveTransactionsContent/activeTransactionUtils.ts | 9 +++++++++ .../components/ActiveTransactionsContent/index.tsx | 2 ++ chat2db-community-client/src/service/sql.ts | 2 +- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.test.ts b/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.test.ts index 0cde2833a7..6effbf5801 100644 --- a/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.test.ts +++ b/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.test.ts @@ -5,6 +5,7 @@ import { buildSessionInspectionSql, canOpenTransactionSession, canShowActiveTransactionsMenu, + formatActiveTransactionStartedAt, getActiveTransactionRowKey, getLiveTransactionAge, getTransactionSessionThreadId, @@ -70,6 +71,11 @@ assert.equal(isLatestActiveTransactionRefresh(refreshGenerationRef, newerRefresh assert.equal(getLiveTransactionAge(12, 1_000, 4_900), 15); assert.equal(getLiveTransactionAge(12, 5_000, 4_000), 12); assert.equal(getLiveTransactionAge(null, 1_000, 4_900), null); +assert.equal(formatActiveTransactionStartedAt('2026-08-31 12:00:00'), '2026-08-31 12:00:00'); +assert.match(formatActiveTransactionStartedAt(1_788_352_626_000), /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); +assert.notEqual(formatActiveTransactionStartedAt(1_788_352_626_000), '1788352626000'); +assert.equal(formatActiveTransactionStartedAt(null), '-'); +assert.equal(formatActiveTransactionStartedAt('not-a-date'), '-'); assert.equal( buildSessionInspectionSql(45), [ diff --git a/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.ts b/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.ts index 8120fa02df..33bca196f6 100644 --- a/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.ts +++ b/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.ts @@ -1,5 +1,6 @@ import type { IActiveTransactionItem } from '@/service/sql'; import { DatabaseTypeCode } from '@/constants/common'; +import dayjs from 'dayjs'; import { beginLatestRequest, invalidateLatestRequest, @@ -44,6 +45,14 @@ export function getLiveTransactionAge( return ageSeconds + Math.max(0, Math.floor((nowMs - snapshotAtMs) / 1000)); } +export function formatActiveTransactionStartedAt(value: number | string | null): string { + if (value == null || value === '') { + return '-'; + } + const timestamp = dayjs(value); + return timestamp.isValid() ? timestamp.format('YYYY-MM-DD HH:mm:ss') : '-'; +} + export function canOpenTransactionSession(record: IActiveTransactionItem, target: 'owner' | 'blocker'): boolean { if (target === 'blocker') { return Boolean(record.canOpenBlockingSession && record.blockingThreadId != null); diff --git a/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/index.tsx b/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/index.tsx index 92e18ef1d0..78a9a85278 100644 --- a/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/index.tsx +++ b/chat2db-community-client/src/blocks/NewTree/components/ActiveTransactionsContent/index.tsx @@ -7,6 +7,7 @@ import { RequestGenerationRef } from '@/utils/latestRequest'; import { beginActiveTransactionRefresh, canOpenTransactionSession, + formatActiveTransactionStartedAt, getActiveTransactionRowKey, getLiveTransactionAge, getTransactionSessionThreadId, @@ -108,6 +109,7 @@ const ActiveTransactionsContent = ({ title: i18n('workspace.ops.transactionStarted'), dataIndex: 'startedAt', width: 170, + render: formatActiveTransactionStartedAt, sorter: (a, b) => String(a.startedAt || '').localeCompare(String(b.startedAt || '')), }, { diff --git a/chat2db-community-client/src/service/sql.ts b/chat2db-community-client/src/service/sql.ts index b33da5bd13..8e5eea65ef 100644 --- a/chat2db-community-client/src/service/sql.ts +++ b/chat2db-community-client/src/service/sql.ts @@ -434,7 +434,7 @@ const copyTable = createRequest('/api/rdb/table/copy', { export interface IActiveTransactionItem { trxId: string | null; state: string | null; - startedAt: string | null; + startedAt: number | string | null; ageSeconds: number | null; isolationLevel: string | null; rowsLocked: number | null; From b2c27e65785088b2decaf6200bd8a9783128b62f Mon Sep 17 00:00:00 2001 From: zgq Date: Wed, 2 Sep 2026 16:01:36 +0800 Subject: [PATCH 41/49] refactor(client): centralize database capability checks --- .../DatabaseTableEditor/ColumnList/index.tsx | 34 +++- .../NewTree/hooks/useCreateRightClickMenu.tsx | 45 +++-- .../src/blocks/NewTree/treeConfig.tsx | 13 +- .../src/components/CreateDatabase/index.tsx | 14 +- .../core/completionProviderManager.ts | 14 +- .../core/sqlCompletionModelMode.test.ts | 43 +++-- .../SQLEditor/core/sqlCompletionModelMode.ts | 10 - .../SQLEditor/editor/SQLEditor/index.tsx | 19 +- .../components/OperationLine/index.tsx | 10 +- .../src/utils/databaseJudgments.test.ts | 172 +++++++----------- .../src/utils/databaseJudgments.ts | 79 -------- 11 files changed, 192 insertions(+), 261 deletions(-) diff --git a/chat2db-community-client/src/blocks/DatabaseTableEditor/ColumnList/index.tsx b/chat2db-community-client/src/blocks/DatabaseTableEditor/ColumnList/index.tsx index d8694e8580..e5ba515909 100644 --- a/chat2db-community-client/src/blocks/DatabaseTableEditor/ColumnList/index.tsx +++ b/chat2db-community-client/src/blocks/DatabaseTableEditor/ColumnList/index.tsx @@ -20,7 +20,7 @@ import { Context } from '../index'; import { IColumnItemNew, IColumnTypes } from '@/typings'; import i18n from '@/i18n'; import { DatabaseCapability, EditColumnOperationType, MYSQL_VISIBILITY, NullableType } from '@/constants'; -import { isDatabaseCapabilitySupported, isSqliteExistingColumnReadonly } from '@/utils/databaseJudgments'; +import { isDatabaseCapabilitySupported } from '@/utils/databaseJudgments'; import CustomSelect from '@/components/CustomSelect'; import Iconfont from '@/components/Iconfont'; import { useStyles } from './style'; @@ -270,7 +270,13 @@ const ColumnList = forwardRef((props: IProps, ref: ForwardedRef)
{ - if (isSqliteExistingColumnReadonly(databaseType, record.editStatus)) { + if ( + record.editStatus !== EditColumnOperationType.Add && + !isDatabaseCapabilitySupported( + databaseType, + DatabaseCapability.TABLE_EDITOR_EXISTING_COLUMN_EDIT, + ) + ) { return null; } handelNullable(record); @@ -279,7 +285,11 @@ const ColumnList = forwardRef((props: IProps, ref: ForwardedRef) disabled={ editingConfig?.supportNullable === false || !!record.primaryKey || - isSqliteExistingColumnReadonly(databaseType, record.editStatus) + (record.editStatus !== EditColumnOperationType.Add && + !isDatabaseCapabilitySupported( + databaseType, + DatabaseCapability.TABLE_EDITOR_EXISTING_COLUMN_EDIT, + )) } />
@@ -295,7 +305,13 @@ const ColumnList = forwardRef((props: IProps, ref: ForwardedRef)
{ - if (isSqliteExistingColumnReadonly(databaseType, record.editStatus)) { + if ( + record.editStatus !== EditColumnOperationType.Add && + !isDatabaseCapabilitySupported( + databaseType, + DatabaseCapability.TABLE_EDITOR_EXISTING_COLUMN_EDIT, + ) + ) { return null; } handelPrimaryKey(record); @@ -325,7 +341,10 @@ const ColumnList = forwardRef((props: IProps, ref: ForwardedRef) width: columnsWidth[7], render: (text: string, record: IColumnItemNew) => { // sqlLite does not support deleting fields. New fields can be deleted. - if (isSqliteExistingColumnReadonly(databaseType, record.editStatus)) { + if ( + record.editStatus !== EditColumnOperationType.Add && + !isDatabaseCapabilitySupported(databaseType, DatabaseCapability.TABLE_EDITOR_EXISTING_COLUMN_EDIT) + ) { return null; } return ( @@ -666,7 +685,10 @@ const ColumnList = forwardRef((props: IProps, ref: ForwardedRef) return { onClick: () => { // sqlLite does not support modifying fields. New fields can be modified. - if (isSqliteExistingColumnReadonly(databaseType, record.editStatus)) { + if ( + record.editStatus !== EditColumnOperationType.Add && + !isDatabaseCapabilitySupported(databaseType, DatabaseCapability.TABLE_EDITOR_EXISTING_COLUMN_EDIT) + ) { return; } if (editingData?.key !== record.key) { diff --git a/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx b/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx index 74cd945dab..0e5b98a4ec 100644 --- a/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx +++ b/chat2db-community-client/src/blocks/NewTree/hooks/useCreateRightClickMenu.tsx @@ -4,7 +4,14 @@ import { SquarePen } from 'lucide-react'; import { type ReactNode, useRef } from 'react'; import { v4 as uuid } from 'uuid'; -import { ConsoleOpenedStatus, OperationColumn, TreeNodeType, WorkspaceTabType, databaseTypeList } from '@/constants'; +import { + ConsoleOpenedStatus, + DatabaseCapability, + OperationColumn, + TreeNodeType, + WorkspaceTabType, + databaseTypeList, +} from '@/constants'; import { ImportExportType } from '@/constants/importExport'; import { ShortcutAction } from '@/constants/shortcut'; import { TreeNodeData } from '@/typings'; @@ -34,15 +41,7 @@ import { viewDDL } from '../functions/viewDDL'; // ----- utils ----- import { compatibleDataBaseName, getDatabaseSupport } from '@/utils/database'; -import { - canDeleteDatabase, - canDeleteSchema, - canExportData, - canExportSqlFile, - canGenerateJavaClass, - canImportData, - canRunSqlFile, -} from '@/utils/databaseJudgments'; +import { isDatabaseCapabilitySupported } from '@/utils/databaseJudgments'; import { dropMenuConfig } from '../menuConfig'; import { handleExportSqlFile } from '@/blocks/ImportAndExport/functions/exportSqlFile'; @@ -744,7 +743,7 @@ export const useCreateRightClickMenu = () => { treeNodeType !== TreeNodeType.DATABASE || !hasPermission || !supportDatabase || - !canDeleteDatabase(databaseType), + !isDatabaseCapabilitySupported(databaseType, DatabaseCapability.DATABASE_DELETE), requiredOperations: ['DROP'], }, @@ -753,7 +752,10 @@ export const useCreateRightClickMenu = () => { icon: 'icon-trash', handle: openDeleteSchemaModal, discard: - treeNodeType !== TreeNodeType.SCHEMA || !hasPermission || !supportSchema || !canDeleteSchema(databaseType), + treeNodeType !== TreeNodeType.SCHEMA || + !hasPermission || + !supportSchema || + !isDatabaseCapabilitySupported(databaseType, DatabaseCapability.SCHEMA_DELETE), requiredOperations: ['DROP'], }, @@ -1011,7 +1013,10 @@ export const useCreateRightClickMenu = () => { schemaName, }); }, - discard: !canImportExport || !canRunSqlFile(databaseType) || !hasPermission, + discard: + !canImportExport || + !isDatabaseCapabilitySupported(databaseType, DatabaseCapability.IMPORT_EXPORT) || + !hasPermission, }, [OperationColumn.CopyMcpConfig]: { @@ -1088,7 +1093,7 @@ export const useCreateRightClickMenu = () => { discard: (treeNodeType === TreeNodeType.DATABASE && supportSchema) || !canImportExport || - !canExportSqlFile(databaseType), + !isDatabaseCapabilitySupported(databaseType, DatabaseCapability.IMPORT_EXPORT), }, // Export data. @@ -1106,7 +1111,9 @@ export const useCreateRightClickMenu = () => { }); }, discard: - (treeNodeType === TreeNodeType.DATABASE && supportSchema) || !canImportExport || !canExportData(databaseType), + (treeNodeType === TreeNodeType.DATABASE && supportSchema) || + !canImportExport || + !isDatabaseCapabilitySupported(databaseType, DatabaseCapability.IMPORT_EXPORT), }, // Import data. @@ -1124,7 +1131,9 @@ export const useCreateRightClickMenu = () => { }); }, discard: - (treeNodeType === TreeNodeType.DATABASE && supportSchema) || !canImportExport || !canImportData(databaseType), + (treeNodeType === TreeNodeType.DATABASE && supportSchema) || + !canImportExport || + !isDatabaseCapabilitySupported(databaseType, DatabaseCapability.IMPORT_EXPORT), requiredOperations: ['INSERT'], }, @@ -1140,7 +1149,9 @@ export const useCreateRightClickMenu = () => { tableName: tableName!, }); }, - discard: !canImportExport || !canGenerateJavaClass(databaseType), + discard: + !canImportExport || + !isDatabaseCapabilitySupported(databaseType, DatabaseCapability.JAVA_CLASS_GENERATION), }, // Truncate the table. diff --git a/chat2db-community-client/src/blocks/NewTree/treeConfig.tsx b/chat2db-community-client/src/blocks/NewTree/treeConfig.tsx index d772c9efb1..58d3c7ebd1 100644 --- a/chat2db-community-client/src/blocks/NewTree/treeConfig.tsx +++ b/chat2db-community-client/src/blocks/NewTree/treeConfig.tsx @@ -1,4 +1,4 @@ -import { ConsoleStatus, OperationColumn, TreeNodeType, WorkspaceTabType } from '@/constants'; +import { ConsoleStatus, DatabaseCapability, OperationColumn, TreeNodeType, WorkspaceTabType } from '@/constants'; import i18n from '@/i18n'; import accountAdminService from '@/service/accountAdmin'; import connectionService from '@/service/connection'; @@ -7,7 +7,7 @@ import mysqlServer from '@/service/sql'; import { useTreeStore } from '@/store/tree'; import { IConnectionDetails, TreeNodeData } from '@/typings'; import { getDatabaseSupport } from '@/utils/database'; -import { canUseAccountManage, isMongodbTreeDataSource, isRedisTreeDataSource } from '@/utils/databaseJudgments'; +import { isDatabaseCapabilitySupported } from '@/utils/databaseJudgments'; import { v4 as uuid } from 'uuid'; import { createSavedConsoleTreeNodeKey } from '@/store/tree/backgroundRefresh'; @@ -255,7 +255,10 @@ export const treeConfig: { [key in TreeNodeType]: ITreeConfigItem } = { return new Promise((r, j) => { const { dataSourceId, databaseType } = extraParams; const { supportDatabase, supportSchema } = getDatabaseSupport(databaseType); - const accountNode: TreeNodeData | null = canUseAccountManage(databaseType) + const accountNode: TreeNodeData | null = isDatabaseCapabilitySupported( + databaseType, + DatabaseCapability.ACCOUNT_MANAGEMENT, + ) ? { key: treeConfig[TreeNodeType.DATABASE_ACCOUNTS].createTreeNodeKey!({ dataSourceId }), originalTitle: i18n('workspace.databaseAccount.title'), @@ -603,7 +606,7 @@ export const treeConfig: { [key in TreeNodeType]: ITreeConfigItem } = { }, createSaveConsolesNode(nodeExtraParams), ]; - if (isRedisTreeDataSource(databaseType)) { + if (isDatabaseCapabilitySupported(databaseType, DatabaseCapability.REDIS_TREE)) { finalData = redisData; } r(finalData); @@ -705,7 +708,7 @@ export const treeConfig: { [key in TreeNodeType]: ITreeConfigItem } = { ]; let finalData = data; - if (isMongodbTreeDataSource(databaseType)) { + if (isDatabaseCapabilitySupported(databaseType, DatabaseCapability.MONGODB_TREE)) { finalData = mongodbData; } r(finalData); diff --git a/chat2db-community-client/src/components/CreateDatabase/index.tsx b/chat2db-community-client/src/components/CreateDatabase/index.tsx index d832eb5600..3f29dba91c 100644 --- a/chat2db-community-client/src/components/CreateDatabase/index.tsx +++ b/chat2db-community-client/src/components/CreateDatabase/index.tsx @@ -5,9 +5,9 @@ import { v4 as uuid } from 'uuid'; import sqlService from '@/service/sql'; import i18n from '@/i18n'; import { debounce } from 'lodash'; -import { DatabaseTypeCode } from '@/constants'; +import { DatabaseCapability, DatabaseTypeCode } from '@/constants'; import { useWorkspaceStore } from '@/store/workspace'; -import { canSetCreateDatabaseCharset, canSetCreateDatabaseCollation } from '@/utils/databaseJudgments'; +import { isDatabaseCapabilitySupported } from '@/utils/databaseJudgments'; import type { ICharset, ICollation } from '@/typings'; import { buildCharsetOptions, buildCollationOptions } from './options'; import { useStyles } from './style'; @@ -62,8 +62,14 @@ const CreateDatabase = () => { const [previewReady, setPreviewReady] = useState(false); const screens = Grid.useBreakpoint(); - const supportsCharset = canSetCreateDatabaseCharset(relyOnParams?.databaseType); - const supportsCollation = canSetCreateDatabaseCollation(relyOnParams?.databaseType); + const supportsCharset = isDatabaseCapabilitySupported( + relyOnParams?.databaseType, + DatabaseCapability.DATABASE_CREATE_CHARSET, + ); + const supportsCollation = isDatabaseCapabilitySupported( + relyOnParams?.databaseType, + DatabaseCapability.DATABASE_CREATE_COLLATION, + ); const charsetOptions = useMemo(() => buildCharsetOptions(charsets), [charsets]); const collationOptions = useMemo( diff --git a/chat2db-community-client/src/components/SQLEditor/core/completionProviderManager.ts b/chat2db-community-client/src/components/SQLEditor/core/completionProviderManager.ts index 5812fd7c75..b3fd38a147 100644 --- a/chat2db-community-client/src/components/SQLEditor/core/completionProviderManager.ts +++ b/chat2db-community-client/src/components/SQLEditor/core/completionProviderManager.ts @@ -1,12 +1,9 @@ import * as monaco from 'monaco-editor'; import keywordObj, { IFunctionParameter, IKeyword } from '../helper/keywords'; -import { DatabaseTypeCode } from '@/constants'; -import { quoteSqlCompletionIdentifier } from '@/utils/databaseJudgments'; +import { DatabaseCapability, DatabaseTypeCode } from '@/constants'; +import { isDatabaseCapabilitySupported, quoteSqlCompletionIdentifier } from '@/utils/databaseJudgments'; import SQLParserService from '@/service/sqlParser'; -import { - isBackendCompletionDatabaseType, - isBackendCompletionModel, -} from './sqlCompletionModelMode'; +import { isBackendCompletionModel } from './sqlCompletionModelMode'; import { getBackendCompletionItemEffectiveFilterText, getBackendCompletionItemInsertText, @@ -201,7 +198,10 @@ class CompletionProviderManager { const { dataSourceId, databaseType, databaseName, schemaName } = dbInfo; const completionContextId = getSqlCompletionContextId(dbInfo); - const backendCompletionMode = isBackendCompletionDatabaseType(databaseType); + const backendCompletionMode = isDatabaseCapabilitySupported( + databaseType, + DatabaseCapability.BACKEND_COMPLETION, + ); if (databaseType && !backendCompletionMode) { this.registerBuiltInKeywordsProvider(); this.registerBuiltInFunctionsProvider(); diff --git a/chat2db-community-client/src/components/SQLEditor/core/sqlCompletionModelMode.test.ts b/chat2db-community-client/src/components/SQLEditor/core/sqlCompletionModelMode.test.ts index f0b6ced41b..a91bf941bc 100644 --- a/chat2db-community-client/src/components/SQLEditor/core/sqlCompletionModelMode.test.ts +++ b/chat2db-community-client/src/components/SQLEditor/core/sqlCompletionModelMode.test.ts @@ -1,49 +1,58 @@ import assert from 'node:assert/strict'; -import { DatabaseTypeCode } from '../../../constants/common'; -import { - isBackendCompletionDatabaseType, - isBackendEditorHintsDatabaseType, - isBackendCompletionModel, - setBackendCompletionModel, -} from './sqlCompletionModelMode'; +import { DatabaseTypeCode } from '@/constants/common'; +import { DatabaseCapability } from '@/constants/databaseCapabilities'; +import { isDatabaseCapabilitySupported } from '@/utils/databaseJudgments'; +import { isBackendCompletionModel, setBackendCompletionModel } from './sqlCompletionModelMode'; const model = {} as any; -assert.equal(isBackendCompletionDatabaseType(DatabaseTypeCode.MYSQL), true, 'MySQL uses backend completion mode'); assert.equal( - isBackendCompletionDatabaseType(DatabaseTypeCode.POSTGRESQL), + isDatabaseCapabilitySupported(DatabaseTypeCode.MYSQL, DatabaseCapability.BACKEND_COMPLETION), + true, + 'MySQL uses backend completion mode', +); +assert.equal( + isDatabaseCapabilitySupported(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.BACKEND_COMPLETION), false, 'non-configured databases keep legacy completion mode', ); assert.equal( - isBackendCompletionDatabaseType(DatabaseTypeCode.GAUSSDB), + isDatabaseCapabilitySupported(DatabaseTypeCode.GAUSSDB, DatabaseCapability.BACKEND_COMPLETION), false, 'GaussDB keeps legacy completion mode while using backend editor hints', ); -assert.equal(isBackendCompletionDatabaseType(undefined), false, 'missing database type keeps legacy completion mode'); -assert.equal(isBackendEditorHintsDatabaseType(DatabaseTypeCode.MYSQL), true, 'MySQL supports backend editor hints'); assert.equal( - isBackendEditorHintsDatabaseType(DatabaseTypeCode.POSTGRESQL), + isDatabaseCapabilitySupported(undefined, DatabaseCapability.BACKEND_COMPLETION), + false, + 'missing database type keeps legacy completion mode', +); +assert.equal( + isDatabaseCapabilitySupported(DatabaseTypeCode.MYSQL, DatabaseCapability.BACKEND_EDITOR_HINTS), + true, + 'MySQL supports backend editor hints', +); +assert.equal( + isDatabaseCapabilitySupported(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.BACKEND_EDITOR_HINTS), true, 'PostgreSQL supports backend editor hints without switching completion mode', ); assert.equal( - isBackendEditorHintsDatabaseType(DatabaseTypeCode.GAUSSDB), + isDatabaseCapabilitySupported(DatabaseTypeCode.GAUSSDB, DatabaseCapability.BACKEND_EDITOR_HINTS), true, 'GaussDB supports PostgreSQL-compatible backend editor hints', ); assert.equal( - isBackendEditorHintsDatabaseType(DatabaseTypeCode.SQLSERVER), + isDatabaseCapabilitySupported(DatabaseTypeCode.SQLSERVER, DatabaseCapability.BACKEND_EDITOR_HINTS), false, 'unvalidated SQL dialects do not request INSERT editor hints', ); assert.equal( - isBackendEditorHintsDatabaseType(DatabaseTypeCode.MONGODB), + isDatabaseCapabilitySupported(DatabaseTypeCode.MONGODB, DatabaseCapability.BACKEND_EDITOR_HINTS), false, 'non-relational databases do not request SQL INSERT editor hints', ); assert.equal( - isBackendEditorHintsDatabaseType(DatabaseTypeCode.GBASE8S), + isDatabaseCapabilitySupported(DatabaseTypeCode.GBASE8S, DatabaseCapability.BACKEND_EDITOR_HINTS), false, 'databases without a SQL syntax plugin do not advertise backend editor hints', ); diff --git a/chat2db-community-client/src/components/SQLEditor/core/sqlCompletionModelMode.ts b/chat2db-community-client/src/components/SQLEditor/core/sqlCompletionModelMode.ts index ab5f806980..2a16562c7e 100644 --- a/chat2db-community-client/src/components/SQLEditor/core/sqlCompletionModelMode.ts +++ b/chat2db-community-client/src/components/SQLEditor/core/sqlCompletionModelMode.ts @@ -1,19 +1,9 @@ import * as monaco from 'monaco-editor'; -import type { DatabaseTypeCode } from '../../../constants/common'; -import { canUseBackendCompletion, canUseBackendEditorHints } from '@/utils/databaseJudgments'; // Frontend routing capability: these databases consume backend completion results // directly, so local snippets/providers should stay out of the completion list. const backendCompletionModels = new WeakSet(); -export function isBackendCompletionDatabaseType(databaseType: DatabaseTypeCode | null | undefined): boolean { - return canUseBackendCompletion(databaseType); -} - -export function isBackendEditorHintsDatabaseType(databaseType: DatabaseTypeCode | null | undefined): boolean { - return canUseBackendEditorHints(databaseType); -} - export function setBackendCompletionModel( model: monaco.editor.ITextModel | null | undefined, enabled: boolean, diff --git a/chat2db-community-client/src/components/SQLEditor/editor/SQLEditor/index.tsx b/chat2db-community-client/src/components/SQLEditor/editor/SQLEditor/index.tsx index 339ede0d75..ab654c0d28 100644 --- a/chat2db-community-client/src/components/SQLEditor/editor/SQLEditor/index.tsx +++ b/chat2db-community-client/src/components/SQLEditor/editor/SQLEditor/index.tsx @@ -5,11 +5,9 @@ import { useGlobalStore } from '@/store/global'; import { debounce } from 'lodash'; import MonacoEditor, { MonacoEditorRef } from '../MonacoEditor'; import CompletionProviderManager from '../../core/completionProviderManager'; -import { - isBackendCompletionDatabaseType, - isBackendEditorHintsDatabaseType, - setBackendCompletionModel, -} from '../../core/sqlCompletionModelMode'; +import { setBackendCompletionModel } from '../../core/sqlCompletionModelMode'; +import { DatabaseCapability } from '@/constants'; +import { isDatabaseCapabilitySupported } from '@/utils/databaseJudgments'; import { SqlCompletionHintScope, createSqlCompletionHintStore, @@ -245,7 +243,10 @@ const SQLEditor = forwardRef( if (!model) { return; } - const backendCompletionMode = isBackendCompletionDatabaseType(dbInfo.databaseType); + const backendCompletionMode = isDatabaseCapabilitySupported( + dbInfo.databaseType, + DatabaseCapability.BACKEND_COMPLETION, + ); setBackendCompletionModel(model, backendCompletionMode); completionProvider.current?.bindModelDBInfo(model, dbInfo); if (!backendCompletionMode) { @@ -702,7 +703,11 @@ const SQLEditor = forwardRef( positionSnapshot?: monaco.Position | null, scope?: SqlCompletionHintScope | null, ) => { - if (!isBackendEditorHintsDatabaseType(dbInfo.databaseType) || !completionProvider.current || !editor) { + if ( + !isDatabaseCapabilitySupported(dbInfo.databaseType, DatabaseCapability.BACKEND_EDITOR_HINTS) || + !completionProvider.current || + !editor + ) { return; } const model = editor.getModel(); diff --git a/chat2db-community-client/src/pages/main/workspace/components/OperationLine/index.tsx b/chat2db-community-client/src/pages/main/workspace/components/OperationLine/index.tsx index 5d57689c26..1ae4464699 100644 --- a/chat2db-community-client/src/pages/main/workspace/components/OperationLine/index.tsx +++ b/chat2db-community-client/src/pages/main/workspace/components/OperationLine/index.tsx @@ -10,7 +10,8 @@ import Iconfont from '@/components/Iconfont'; // ----- store ----- import { useWorkspaceStore } from '@/store/workspace'; -import { canCreateDatabase, canCreateSchema } from '@/utils/databaseJudgments'; +import { DatabaseCapability } from '@/constants'; +import { isDatabaseCapabilitySupported } from '@/utils/databaseJudgments'; interface IProps { searchValue: string; @@ -45,10 +46,13 @@ const OperationLine = (props: IProps) => { const showCreate = useMemo(() => { if (currentConnectionDetails?.supportDatabase) { - return canCreateDatabase(currentConnectionDetails!.type!); + return isDatabaseCapabilitySupported( + currentConnectionDetails.type, + DatabaseCapability.DATABASE_CREATE, + ); } if (currentConnectionDetails?.supportSchema) { - return canCreateSchema(currentConnectionDetails!.type!); + return isDatabaseCapabilitySupported(currentConnectionDetails.type, DatabaseCapability.SCHEMA_CREATE); } }, [currentConnectionDetails]); diff --git a/chat2db-community-client/src/utils/databaseJudgments.test.ts b/chat2db-community-client/src/utils/databaseJudgments.test.ts index 486609510e..d748effb7b 100644 --- a/chat2db-community-client/src/utils/databaseJudgments.test.ts +++ b/chat2db-community-client/src/utils/databaseJudgments.test.ts @@ -1,33 +1,27 @@ import assert from 'node:assert/strict'; import { DatabaseTypeCode } from '@/constants/common'; import { DatabaseCapability, IdentifierQuoteMode } from '@/constants/databaseCapabilities'; -import { EditColumnOperationType } from '@/constants/editTable'; import { - canCreateDatabase, - canCreateSchema, - canSetCreateDatabaseCharset, - canSetCreateDatabaseCollation, - canDeleteDatabase, - canDeleteSchema, - canExportData, - canExportSqlFile, - canGenerateJavaClass, - canImportData, - canRunSqlFile, - canUseAccountManage, - canUseBackendCompletion, - canUseRoutineOperation, getDatabaseSupport, getOpenTableIdentifierQuoteMode, getSqlCompletionIdentifierQuoteMode, - isMongodbTreeDataSource, - isRedisTreeDataSource, - isSqliteExistingColumnReadonly, isDatabaseCapabilitySupported, quoteOpenTableIdentifier, quoteSqlCompletionIdentifier, } from './databaseJudgments'; +const assertCapability = ( + databaseType: DatabaseTypeCode | string | null | undefined, + capability: DatabaseCapability, + expected: boolean, +) => { + assert.equal( + isDatabaseCapabilitySupported(databaseType, capability), + expected, + `${String(databaseType)} ${capability}`, + ); +}; + assert.deepEqual(getDatabaseSupport(DatabaseTypeCode.MYSQL), { supportDatabase: true, supportSchema: false, @@ -49,30 +43,30 @@ assert.deepEqual(getDatabaseSupport('oscar_db'), { supportSchema: true, }); -assert.equal(canUseRoutineOperation(DatabaseTypeCode.MYSQL), true); -assert.equal(canUseRoutineOperation('mysql'), true); -assert.equal(canUseRoutineOperation(DatabaseTypeCode.POSTGRESQL), false); -assert.equal(canUseAccountManage(DatabaseTypeCode.MYSQL), true); -assert.equal(canUseAccountManage(DatabaseTypeCode.ORACLE), false); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.ROUTINE_OPERATION, true); +assertCapability('mysql', DatabaseCapability.ROUTINE_OPERATION, true); +assertCapability(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.ROUTINE_OPERATION, false); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.ACCOUNT_MANAGEMENT, true); +assertCapability(DatabaseTypeCode.ORACLE, DatabaseCapability.ACCOUNT_MANAGEMENT, false); -assert.equal(canDeleteDatabase(DatabaseTypeCode.MYSQL), true); -assert.equal(canDeleteDatabase(DatabaseTypeCode.POSTGRESQL), true); -assert.equal(canDeleteDatabase(DatabaseTypeCode.ORACLE), false); -assert.equal(canDeleteSchema(DatabaseTypeCode.POSTGRESQL), true); -assert.equal(canDeleteSchema(DatabaseTypeCode.MYSQL), false); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.DATABASE_DELETE, true); +assertCapability(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.DATABASE_DELETE, true); +assertCapability(DatabaseTypeCode.ORACLE, DatabaseCapability.DATABASE_DELETE, false); +assertCapability(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.SCHEMA_DELETE, true); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.SCHEMA_DELETE, false); -assert.equal(canCreateDatabase(DatabaseTypeCode.H2), false); -assert.equal(canCreateDatabase(DatabaseTypeCode.MYSQL), true); -assert.equal(canSetCreateDatabaseCharset(DatabaseTypeCode.MYSQL), true); -assert.equal(canSetCreateDatabaseCharset(DatabaseTypeCode.POSTGRESQL), false); -assert.equal(canSetCreateDatabaseCharset(DatabaseTypeCode.SQLITE), false); -assert.equal(canSetCreateDatabaseCollation(DatabaseTypeCode.MYSQL), true); -assert.equal(canSetCreateDatabaseCollation(DatabaseTypeCode.POSTGRESQL), false); -assert.equal(canSetCreateDatabaseCollation(DatabaseTypeCode.SQLITE), false); -assert.equal(canCreateSchema(DatabaseTypeCode.ORACLE), false); -assert.equal(canCreateSchema(DatabaseTypeCode.OSCAR), false); -assert.equal(canCreateSchema(DatabaseTypeCode.POSTGRESQL), true); -assert.equal(canCreateSchema('oscar_db'), true); +assertCapability(DatabaseTypeCode.H2, DatabaseCapability.DATABASE_CREATE, false); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.DATABASE_CREATE, true); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.DATABASE_CREATE_CHARSET, true); +assertCapability(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.DATABASE_CREATE_CHARSET, false); +assertCapability(DatabaseTypeCode.SQLITE, DatabaseCapability.DATABASE_CREATE_CHARSET, false); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.DATABASE_CREATE_COLLATION, true); +assertCapability(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.DATABASE_CREATE_COLLATION, false); +assertCapability(DatabaseTypeCode.SQLITE, DatabaseCapability.DATABASE_CREATE_COLLATION, false); +assertCapability(DatabaseTypeCode.ORACLE, DatabaseCapability.SCHEMA_CREATE, false); +assertCapability(DatabaseTypeCode.OSCAR, DatabaseCapability.SCHEMA_CREATE, false); +assertCapability(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.SCHEMA_CREATE, true); +assertCapability('oscar_db', DatabaseCapability.SCHEMA_CREATE, true); for (const databaseType of [ DatabaseTypeCode.REDIS, @@ -84,22 +78,41 @@ for (const databaseType of [ DatabaseTypeCode.KINGBASE, DatabaseTypeCode.HIVE, ]) { - assert.equal(canRunSqlFile(databaseType), false, `${databaseType} cannot run SQL files`); - assert.equal(canExportSqlFile(databaseType), false, `${databaseType} cannot export SQL files`); - assert.equal(canExportData(databaseType), false, `${databaseType} cannot export data`); - assert.equal(canImportData(databaseType), false, `${databaseType} cannot import data`); + assertCapability(databaseType, DatabaseCapability.IMPORT_EXPORT, false); } -assert.equal(canRunSqlFile(DatabaseTypeCode.MYSQL), true); -assert.equal(canExportSqlFile(DatabaseTypeCode.MYSQL), true); -assert.equal(canExportData(DatabaseTypeCode.MYSQL), true); -assert.equal(canImportData(DatabaseTypeCode.MYSQL), true); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.IMPORT_EXPORT, true); + +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.JAVA_CLASS_GENERATION, true); +assertCapability(DatabaseTypeCode.ORACLE, DatabaseCapability.JAVA_CLASS_GENERATION, true); +assertCapability(DatabaseTypeCode.REDIS, DatabaseCapability.JAVA_CLASS_GENERATION, false); + +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.BACKEND_COMPLETION, true); +assertCapability(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.BACKEND_COMPLETION, false); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.BACKEND_EDITOR_HINTS, true); +assertCapability(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.BACKEND_EDITOR_HINTS, true); +assertCapability(DatabaseTypeCode.GAUSSDB, DatabaseCapability.BACKEND_EDITOR_HINTS, true); +assertCapability(DatabaseTypeCode.SQLSERVER, DatabaseCapability.BACKEND_EDITOR_HINTS, false); -assert.equal(canGenerateJavaClass(DatabaseTypeCode.MYSQL), true); -assert.equal(canGenerateJavaClass(DatabaseTypeCode.ORACLE), true); -assert.equal(canGenerateJavaClass(DatabaseTypeCode.REDIS), false); +assertCapability(DatabaseTypeCode.REDIS, DatabaseCapability.REDIS_TREE, true); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.REDIS_TREE, false); +assertCapability(DatabaseTypeCode.MONGODB, DatabaseCapability.MONGODB_TREE, true); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.MONGODB_TREE, false); -assert.equal(canUseBackendCompletion(DatabaseTypeCode.MYSQL), true); -assert.equal(canUseBackendCompletion(DatabaseTypeCode.POSTGRESQL), false); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.TABLE_EDITOR_BASE_INFO, true); +assertCapability(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.TABLE_EDITOR_BASE_INFO, false); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.TABLE_EDITOR_INDEX_METHOD, true); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.TABLE_EDITOR_COLUMN_VISIBILITY, true); +assertCapability(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.TABLE_EDITOR_COLUMN_VISIBILITY, false); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.TABLE_EDITOR_INDEX_VISIBILITY, true); +assertCapability(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.TABLE_EDITOR_INDEX_VISIBILITY, false); +assertCapability(DatabaseTypeCode.ORACLE, DatabaseCapability.TABLE_EDITOR_INDEX_COLUMN, false); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.TABLE_EDITOR_INDEX_COLUMN, true); +assertCapability(DatabaseTypeCode.SQLITE, DatabaseCapability.TABLE_EDITOR_INCLUDE_COLLATION, true); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.TABLE_EDITOR_INCLUDE_COLLATION, false); +assertCapability(DatabaseTypeCode.SQLITE, DatabaseCapability.TABLE_EDITOR_EXISTING_COLUMN_EDIT, false); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.TABLE_EDITOR_EXISTING_COLUMN_EDIT, true); +assertCapability(DatabaseTypeCode.SQLSERVER, DatabaseCapability.TABLE_EDITOR_SPARSE_COLUMN, true); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.TABLE_EDITOR_SPARSE_COLUMN, false); assert.equal(getOpenTableIdentifierQuoteMode(DatabaseTypeCode.POSTGRESQL), IdentifierQuoteMode.DOUBLE_QUOTE); assert.equal(getOpenTableIdentifierQuoteMode(DatabaseTypeCode.SQLSERVER), IdentifierQuoteMode.SQUARE_BRACKET); @@ -115,57 +128,4 @@ assert.equal(getSqlCompletionIdentifierQuoteMode(DatabaseTypeCode.OCEANBASE), Id assert.equal(quoteSqlCompletionIdentifier('User Table', DatabaseTypeCode.OCEANBASE_ORACLE), '"User Table"'); assert.equal(quoteSqlCompletionIdentifier('User Table', DatabaseTypeCode.OCEANBASE), '`User Table`'); -assert.equal(isRedisTreeDataSource(DatabaseTypeCode.REDIS), true); -assert.equal(isRedisTreeDataSource(DatabaseTypeCode.MYSQL), false); -assert.equal(isMongodbTreeDataSource(DatabaseTypeCode.MONGODB), true); -assert.equal(isMongodbTreeDataSource(DatabaseTypeCode.MYSQL), false); - -assert.equal( - isDatabaseCapabilitySupported(DatabaseTypeCode.MYSQL, DatabaseCapability.TABLE_EDITOR_BASE_INFO), - true, -); -assert.equal( - isDatabaseCapabilitySupported(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.TABLE_EDITOR_BASE_INFO), - false, -); -assert.equal( - isDatabaseCapabilitySupported(DatabaseTypeCode.MYSQL, DatabaseCapability.TABLE_EDITOR_INDEX_METHOD), - true, -); -assert.equal( - isDatabaseCapabilitySupported(DatabaseTypeCode.MYSQL, DatabaseCapability.TABLE_EDITOR_COLUMN_VISIBILITY), - true, -); -assert.equal( - isDatabaseCapabilitySupported(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.TABLE_EDITOR_COLUMN_VISIBILITY), - false, -); -assert.equal( - isDatabaseCapabilitySupported(DatabaseTypeCode.MYSQL, DatabaseCapability.TABLE_EDITOR_INDEX_VISIBILITY), - true, -); -assert.equal( - isDatabaseCapabilitySupported(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.TABLE_EDITOR_INDEX_VISIBILITY), - false, -); -assert.equal( - isDatabaseCapabilitySupported(DatabaseTypeCode.ORACLE, DatabaseCapability.TABLE_EDITOR_INDEX_COLUMN), - false, -); -assert.equal( - isDatabaseCapabilitySupported(DatabaseTypeCode.SQLITE, DatabaseCapability.TABLE_EDITOR_INCLUDE_COLLATION), - true, -); -assert.equal(isSqliteExistingColumnReadonly(DatabaseTypeCode.SQLITE, EditColumnOperationType.Modify), true); -assert.equal(isSqliteExistingColumnReadonly(DatabaseTypeCode.SQLITE, EditColumnOperationType.Add), false); -assert.equal(isSqliteExistingColumnReadonly(DatabaseTypeCode.MYSQL, EditColumnOperationType.Modify), false); -assert.equal( - isDatabaseCapabilitySupported(DatabaseTypeCode.SQLSERVER, DatabaseCapability.TABLE_EDITOR_SPARSE_COLUMN), - true, -); -assert.equal( - isDatabaseCapabilitySupported(DatabaseTypeCode.MYSQL, DatabaseCapability.TABLE_EDITOR_SPARSE_COLUMN), - false, -); - console.log('databaseJudgments.test.ts: all assertions passed'); diff --git a/chat2db-community-client/src/utils/databaseJudgments.ts b/chat2db-community-client/src/utils/databaseJudgments.ts index 9cb2918047..842d9aa5fe 100644 --- a/chat2db-community-client/src/utils/databaseJudgments.ts +++ b/chat2db-community-client/src/utils/databaseJudgments.ts @@ -1,7 +1,6 @@ import { DatabaseTypeCode } from '@/constants/common'; import { DatabaseCapability, IdentifierQuoteMode } from '@/constants/databaseCapabilities'; import { getDatabaseInfo, normalizeDatabaseType } from '@/constants/database'; -import { EditColumnOperationType } from '@/constants/editTable'; type DatabaseTypeInput = DatabaseTypeCode | string | null | undefined; @@ -208,66 +207,6 @@ export const getDatabaseSupport = (databaseType?: DatabaseTypeInput) => { }; }; -export const canUseRoutineOperation = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.ROUTINE_OPERATION); -}; - -export const canUseAccountManage = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.ACCOUNT_MANAGEMENT); -}; - -export const canDeleteDatabase = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.DATABASE_DELETE); -}; - -export const canDeleteSchema = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.SCHEMA_DELETE); -}; - -export const canCreateDatabase = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.DATABASE_CREATE); -}; - -export const canSetCreateDatabaseCharset = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.DATABASE_CREATE_CHARSET); -}; - -export const canSetCreateDatabaseCollation = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.DATABASE_CREATE_COLLATION); -}; - -export const canCreateSchema = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.SCHEMA_CREATE); -}; - -export const canRunSqlFile = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.IMPORT_EXPORT); -}; - -export const canExportSqlFile = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.IMPORT_EXPORT); -}; - -export const canExportData = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.IMPORT_EXPORT); -}; - -export const canImportData = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.IMPORT_EXPORT); -}; - -export const canGenerateJavaClass = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.JAVA_CLASS_GENERATION); -}; - -export const canUseBackendCompletion = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.BACKEND_COMPLETION); -}; - -export const canUseBackendEditorHints = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.BACKEND_EDITOR_HINTS); -}; - export const getOpenTableIdentifierQuoteMode = (databaseType?: DatabaseTypeInput): IdentifierQuoteMode => { return getIdentifierQuoteModeFromConfig(openTableIdentifierQuoteJudgments, databaseType); }; @@ -296,21 +235,3 @@ export const quoteOpenTableIdentifier = (name: string, databaseType?: DatabaseTy export const quoteSqlCompletionIdentifier = (name: string, databaseType?: DatabaseTypeInput): string => { return quoteIdentifierByMode(name, getSqlCompletionIdentifierQuoteMode(databaseType)); }; - -export const isRedisTreeDataSource = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.REDIS_TREE); -}; - -export const isMongodbTreeDataSource = (databaseType?: DatabaseTypeInput): boolean => { - return isDatabaseCapabilitySupported(databaseType, DatabaseCapability.MONGODB_TREE); -}; - -export const isSqliteExistingColumnReadonly = ( - databaseType?: DatabaseTypeInput, - editStatus?: EditColumnOperationType | null, -): boolean => { - return ( - !isDatabaseCapabilitySupported(databaseType, DatabaseCapability.TABLE_EDITOR_EXISTING_COLUMN_EDIT) && - editStatus !== EditColumnOperationType.Add - ); -}; From 3802df76735fb8a30db72e7294e88a417e425ffe Mon Sep 17 00:00:00 2001 From: zgq Date: Wed, 2 Sep 2026 16:18:37 +0800 Subject: [PATCH 42/49] fix(client): keep MySQL visibility keywords literal --- .../scripts/i18n-source-hashes.json | 4 ++-- .../DatabaseTableEditor/ColumnList/index.tsx | 14 +++++++------- .../blocks/DatabaseTableEditor/IndexList/index.tsx | 14 +++++++------- .../src/constants/editTable.ts | 4 ++-- .../src/i18n/en-US/editTable.ts | 2 -- .../src/i18n/es-ES/editTable.ts | 2 -- .../src/i18n/ja-JP/editTable.ts | 2 -- .../src/i18n/ko-KR/editTable.ts | 2 -- .../src/i18n/zh-CN/editTable.ts | 2 -- 9 files changed, 18 insertions(+), 28 deletions(-) diff --git a/chat2db-community-client/scripts/i18n-source-hashes.json b/chat2db-community-client/scripts/i18n-source-hashes.json index 6d66ca9bc9..9ed22b806c 100644 --- a/chat2db-community-client/scripts/i18n-source-hashes.json +++ b/chat2db-community-client/scripts/i18n-source-hashes.json @@ -8,7 +8,7 @@ "common.ts": "24aa2228702754bc019f1e21dd7be9f6d1c6ff02ed9bd9227ab319f331248b11", "connection.ts": "d70578a3444978b60ea41c670e8b180849eaab297e33325db56c891e1058e899", "dashboard.ts": "b99f5e6b81271573a8db1a585ac879724441b7163050899c3abaf9498217bd21", - "editTable.ts": "4f42d733da897877c821bcdb3bf91febf9d80ffb26c1188d3190605a92b2a171", + "editTable.ts": "6d1233d574dbd2e038ed112bc8677b12f61a0fc831f56f77d5ce024a96a319f6", "editTableData.ts": "b6f40d2e1f612b716c0dbc2ade925069a039585a28ada997e004c33ce2fd0901", "feedback.ts": "080fbb0b2e6578d10499d47e0f23b7161feffb769c8a574048c5a737c9624eea", "login.ts": "df4c7c4b2020ef3e9a70277f364d078fc6a10ad81be31d3b648933f14c423f43", @@ -29,7 +29,7 @@ "common.ts": "24aa2228702754bc019f1e21dd7be9f6d1c6ff02ed9bd9227ab319f331248b11", "connection.ts": "d70578a3444978b60ea41c670e8b180849eaab297e33325db56c891e1058e899", "dashboard.ts": "b99f5e6b81271573a8db1a585ac879724441b7163050899c3abaf9498217bd21", - "editTable.ts": "4f42d733da897877c821bcdb3bf91febf9d80ffb26c1188d3190605a92b2a171", + "editTable.ts": "6d1233d574dbd2e038ed112bc8677b12f61a0fc831f56f77d5ce024a96a319f6", "editTableData.ts": "b6f40d2e1f612b716c0dbc2ade925069a039585a28ada997e004c33ce2fd0901", "feedback.ts": "080fbb0b2e6578d10499d47e0f23b7161feffb769c8a574048c5a737c9624eea", "login.ts": "df4c7c4b2020ef3e9a70277f364d078fc6a10ad81be31d3b648933f14c423f43", diff --git a/chat2db-community-client/src/blocks/DatabaseTableEditor/ColumnList/index.tsx b/chat2db-community-client/src/blocks/DatabaseTableEditor/ColumnList/index.tsx index e5ba515909..46a5236e76 100644 --- a/chat2db-community-client/src/blocks/DatabaseTableEditor/ColumnList/index.tsx +++ b/chat2db-community-client/src/blocks/DatabaseTableEditor/ColumnList/index.tsx @@ -370,17 +370,17 @@ const ColumnList = forwardRef((props: IProps, ref: ForwardedRef) render: (text: boolean | null | undefined, record: IColumnItemNew) => { const editable = isEditing(record); const value = - record.visible === MYSQL_VISIBILITY.INVISIBLE - ? i18n('editTable.option.invisible') - : i18n('editTable.option.visible'); + record.visible === MYSQL_VISIBILITY.INVISIBLE.value + ? MYSQL_VISIBILITY.INVISIBLE.label + : MYSQL_VISIBILITY.VISIBLE.label; return editable ? ( diff --git a/chat2db-community-client/src/blocks/DatabaseTableEditor/IndexList/index.tsx b/chat2db-community-client/src/blocks/DatabaseTableEditor/IndexList/index.tsx index 92e8aeb690..7eed2ba95e 100644 --- a/chat2db-community-client/src/blocks/DatabaseTableEditor/IndexList/index.tsx +++ b/chat2db-community-client/src/blocks/DatabaseTableEditor/IndexList/index.tsx @@ -362,17 +362,17 @@ const IndexList = forwardRef((props: IProps, ref: ForwardedRef) = const isPrimaryKey = record.type === MYSQL_PRIMARY_INDEX_TYPE; const editable = isEditing(record) && !isPrimaryKey; const value = - record.visible === MYSQL_VISIBILITY.INVISIBLE - ? i18n('editTable.option.invisible') - : i18n('editTable.option.visible'); + record.visible === MYSQL_VISIBILITY.INVISIBLE.value + ? MYSQL_VISIBILITY.INVISIBLE.label + : MYSQL_VISIBILITY.VISIBLE.label; return editable ? ( diff --git a/chat2db-community-client/src/constants/editTable.ts b/chat2db-community-client/src/constants/editTable.ts index c068f9a438..6739d50b07 100644 --- a/chat2db-community-client/src/constants/editTable.ts +++ b/chat2db-community-client/src/constants/editTable.ts @@ -18,6 +18,6 @@ export enum NullableType { export const MYSQL_PRIMARY_INDEX_TYPE = 'Primary' as const; export const MYSQL_VISIBILITY = { - VISIBLE: true, - INVISIBLE: false, + VISIBLE: { label: 'VISIBLE', value: true }, + INVISIBLE: { label: 'INVISIBLE', value: false }, } as const; diff --git a/chat2db-community-client/src/i18n/en-US/editTable.ts b/chat2db-community-client/src/i18n/en-US/editTable.ts index 98805523fb..13e2a9c3b9 100644 --- a/chat2db-community-client/src/i18n/en-US/editTable.ts +++ b/chat2db-community-client/src/i18n/en-US/editTable.ts @@ -12,8 +12,6 @@ export default { 'editTable.label.indexType': 'Type', 'editTable.label.indexMethod': 'Index method', 'editTable.label.indexVisible': 'Visible', - 'editTable.option.visible': 'VISIBLE', - 'editTable.option.invisible': 'INVISIBLE', 'editTable.label.includeColumn': 'Include column', 'editTable.button.createTable': 'Create Table', 'editTable.button.importTable': 'Export Table', diff --git a/chat2db-community-client/src/i18n/es-ES/editTable.ts b/chat2db-community-client/src/i18n/es-ES/editTable.ts index 436bf3b366..284e4689b2 100644 --- a/chat2db-community-client/src/i18n/es-ES/editTable.ts +++ b/chat2db-community-client/src/i18n/es-ES/editTable.ts @@ -12,8 +12,6 @@ export default { 'editTable.label.indexType': 'Tipo', 'editTable.label.indexMethod': 'Método de índice', 'editTable.label.indexVisible': 'Visible', - 'editTable.option.visible': 'VISIBLE', - 'editTable.option.invisible': 'INVISIBLE', 'editTable.label.includeColumn': 'Columna incluida', 'editTable.button.createTable': 'Crear tabla', 'editTable.button.importTable': 'Exportar tabla', diff --git a/chat2db-community-client/src/i18n/ja-JP/editTable.ts b/chat2db-community-client/src/i18n/ja-JP/editTable.ts index 9d4097ff2c..4f48ebb14c 100644 --- a/chat2db-community-client/src/i18n/ja-JP/editTable.ts +++ b/chat2db-community-client/src/i18n/ja-JP/editTable.ts @@ -12,8 +12,6 @@ export default { 'editTable.label.indexType': 'タイプ', 'editTable.label.indexMethod': 'インデックス方法', 'editTable.label.indexVisible': '表示', - 'editTable.option.visible': '表示', - 'editTable.option.invisible': '非表示', 'editTable.label.includeColumn': '列を含む', 'editTable.button.createTable': 'テーブルを作成', 'editTable.button.importTable': 'テーブルをエクスポート', diff --git a/chat2db-community-client/src/i18n/ko-KR/editTable.ts b/chat2db-community-client/src/i18n/ko-KR/editTable.ts index 52a75bdaa1..8af32d81c2 100644 --- a/chat2db-community-client/src/i18n/ko-KR/editTable.ts +++ b/chat2db-community-client/src/i18n/ko-KR/editTable.ts @@ -12,8 +12,6 @@ export default { 'editTable.label.indexType': '유형', 'editTable.label.indexMethod': '인덱스 방식', 'editTable.label.indexVisible': '표시', - 'editTable.option.visible': '표시', - 'editTable.option.invisible': '숨김', 'editTable.label.includeColumn': '포함 열', 'editTable.button.createTable': '테이블 생성', 'editTable.button.importTable': '테이블 내보내기', diff --git a/chat2db-community-client/src/i18n/zh-CN/editTable.ts b/chat2db-community-client/src/i18n/zh-CN/editTable.ts index 2d878a6ba8..fe67946f51 100644 --- a/chat2db-community-client/src/i18n/zh-CN/editTable.ts +++ b/chat2db-community-client/src/i18n/zh-CN/editTable.ts @@ -12,8 +12,6 @@ export default { 'editTable.label.indexType': '索引类型', 'editTable.label.indexMethod': '索引方法', 'editTable.label.indexVisible': '可见', - 'editTable.option.visible': '可见', - 'editTable.option.invisible': '不可见', 'editTable.label.includeColumn': '包含列', 'editTable.button.createTable': '新建表', 'editTable.button.importTable': '导出表', From 961c6e51455bb2e30485613555d84d0163c8ca27 Mon Sep 17 00:00:00 2001 From: zgq Date: Wed, 2 Sep 2026 19:08:07 +0800 Subject: [PATCH 43/49] fix(result): stabilize sorting and edit actions --- chat2db-community-client/package.json | 4 +- .../components/ResultSet/index.tsx | 69 ++++++++++----- .../components/ResultSet/resultEditActions.ts | 40 +++++++++ .../components/ResultSet/resultSetUi.test.ts | 86 +++++++++++++++++++ .../ResultSet/viewTablePagingFlow.test.ts | 18 ++-- .../components/ResultSetTable/index.tsx | 36 ++++++++ .../components/ResultSetToolbar/index.tsx | 18 ++-- .../components/ResultSetToolbar/style.ts | 7 ++ .../components/ScreeningResult/index.tsx | 8 +- .../SingleFileMonacoEditor/index.tsx | 24 ++---- .../SingleFileMonacoEditor/refAdapter.test.ts | 26 ++++++ .../SingleFileMonacoEditor/refAdapter.ts | 18 ++++ .../src/hooks/viewTablePagingModel.ts | 6 +- .../workspace/components/SQLExecute/index.tsx | 13 --- .../src/utils/shortcutDispatch.test.ts | 14 +++ .../request/db/SelectResultUpdateRequest.java | 2 - .../db/DmlExecutionRequestValidationTest.java | 11 +++ 17 files changed, 325 insertions(+), 75 deletions(-) create mode 100644 chat2db-community-client/src/blocks/SearchResult/components/ResultSet/resultEditActions.ts create mode 100644 chat2db-community-client/src/components/SingleFileMonacoEditor/refAdapter.test.ts create mode 100644 chat2db-community-client/src/components/SingleFileMonacoEditor/refAdapter.ts diff --git a/chat2db-community-client/package.json b/chat2db-community-client/package.json index f99bc378c0..d6c4085444 100644 --- a/chat2db-community-client/package.json +++ b/chat2db-community-client/package.json @@ -12,7 +12,7 @@ "build": "yarn run build:web:community", "build:prod": "yarn run build:web:community", "build:web": "umi build", - "prebuild:web:community": "yarn test:community-boundary && yarn test:database-capabilities && yarn test:table-editor && yarn test:retired-ai && yarn test:tree-title-highlight && yarn test:tree-search-lifecycle && yarn test:driver-upload && yarn test:tree-loading && yarn test:tree-node-lookup && yarn test:tree-node-path && yarn test:data-source-authorization && yarn test:data-source-mutation-refresh && yarn test:ai-model-config && yarn test:ai-model-select && yarn test:export-connections && yarn test:main-page-navigation && yarn test:shortcut && yarn test:console-tab-name && yarn test:workspace-tab-scroll && yarn test:workspace-split-lifecycle && yarn test:sql-completion-context && yarn test:file-manager-label && yarn test:local-file-encoding && yarn test:local-file-save && yarn test:local-file-tree-path && yarn test:saved-console-dirty && yarn test:editor-close && yarn test:result-set-ui && yarn test:result-pagination && yarn test:result-table-layout && yarn test:result-status && yarn test:data-source-identity && yarn test:data-source-execution-snapshot && yarn test:data-source-watermark && yarn test:hot-update && yarn test:task-center && yarn test:application-exit && yarn test:monaco-lifecycle && yarn test:result-set-editor", + "prebuild:web:community": "yarn test:community-boundary && yarn test:database-capabilities && yarn test:table-editor && yarn test:retired-ai && yarn test:tree-title-highlight && yarn test:tree-search-lifecycle && yarn test:driver-upload && yarn test:tree-loading && yarn test:tree-node-lookup && yarn test:tree-node-path && yarn test:data-source-authorization && yarn test:data-source-mutation-refresh && yarn test:ai-model-config && yarn test:ai-model-select && yarn test:export-connections && yarn test:main-page-navigation && yarn test:shortcut && yarn test:console-tab-name && yarn test:workspace-tab-scroll && yarn test:workspace-split-lifecycle && yarn test:sql-completion-context && yarn test:file-manager-label && yarn test:local-file-encoding && yarn test:local-file-save && yarn test:local-file-tree-path && yarn test:saved-console-dirty && yarn test:editor-close && yarn test:result-set-ui && yarn test:result-pagination && yarn test:sql-execution-stream && yarn test:result-table-layout && yarn test:result-status && yarn test:data-source-identity && yarn test:data-source-execution-snapshot && yarn test:data-source-watermark && yarn test:hot-update && yarn test:task-center && yarn test:application-exit && yarn test:monaco-lifecycle && yarn test:result-set-editor", "postbuild:web:community": "node ./scripts/verify-production-bundles.cjs", "build:web:2java": "yarn run build:web:community && rm -rf ../chat2db-community-server/chat2db-community-start/src/main/resources/thymeleaf/* && cp -r dist/index.html ../chat2db-community-server/chat2db-community-start/src/main/resources/thymeleaf/", "build:web:community": "cross-env UMI_ENV=community cross-env APP_NAME=chat2db-community cross-env APP_VERSION=${npm_config_app_version} cross-env PRINT_LOGS=${npm_config_print_logs} cross-env APP_PORT=${npm_config_app_port} umi build", @@ -78,7 +78,7 @@ "test:saved-console-dirty": "tsx src/utils/savedConsoleDirty.test.ts", "test:local-file-encoding": "tsx src/utils/localFileEncoding.test.ts && yarn test:local-file-tab-refresh && tsx src/store/workspace/utils/workspaceTabPersistence.test.ts", "test:main-page-navigation": "tsx src/utils/mainPageNavigation.test.ts && tsx src/pages/main/navigationItems.test.ts && tsx src/client-extension/extension.test.ts", - "test:monaco-lifecycle": "tsx src/utils/monaco.test.ts", + "test:monaco-lifecycle": "tsx src/utils/monaco.test.ts && tsx src/components/SingleFileMonacoEditor/refAdapter.test.ts", "test:mcp-lifecycle": "tsx src/blocks/Setting/McpSetting/mcpLifecycle.test.ts", "test:sql-completion-context": "tsx src/components/SQLEditor/core/sqlCompletionContext.test.ts && tsx src/components/SQLEditor/core/sqlCompletionRequestError.test.ts", "test:sql-in-clipboard": "tsx src/utils/sqlInClipboard.test.ts && tsx src/components/SQLEditor/helper/sqlInsertValueDefaults.test.ts", diff --git a/chat2db-community-client/src/blocks/SearchResult/components/ResultSet/index.tsx b/chat2db-community-client/src/blocks/SearchResult/components/ResultSet/index.tsx index 68d2505951..a6cc11bc9f 100644 --- a/chat2db-community-client/src/blocks/SearchResult/components/ResultSet/index.tsx +++ b/chat2db-community-client/src/blocks/SearchResult/components/ResultSet/index.tsx @@ -29,6 +29,7 @@ import { ITableInstance } from '@/blocks/CanvasTable/typings'; import { ShortcutAction, ShortcutOverrides, + ShortcutScope, getEffectiveShortcutConfigMap, isShortcutEventMatch, } from '@/constants/shortcut'; @@ -63,6 +64,7 @@ import { import { resolveResultInspectorActiveCell } from '../ResultSetTable/selectionState'; import { areResultCellValuesEquivalent } from './inspectorState'; import SqlExecutionLoading from '@/components/SqlExecutionLoading'; +import { hasPendingResultEdit, resolvePendingResultEditOperations } from './resultEditActions'; interface IProps { resultData: IManageResultData; @@ -89,6 +91,7 @@ export default memo( const screenResultRef = useRef(null); const resultSetTableRef = useRef(null); const [hasOperationRecord, setHasOperationRecord] = useState(false); + const [hasActiveEditorChange, setHasActiveEditorChange] = useState(false); const sqlPreviewExecuteRef = useRef(null); const sidebarViewDataRef = useRef(null); const modalViewDataRef = useRef(null); @@ -121,6 +124,7 @@ export default memo( const currentWorkspaceExtend = useWorkspaceStore((state) => state.currentWorkspaceExtend); const inspectorExtendCode = useMemo(() => getWorkspaceResultInspectorCode(searchAreaId), [searchAreaId]); const inspectorSidebarOpen = currentWorkspaceExtend === inspectorExtendCode; + const hasPendingChanges = hasPendingResultEdit(hasOperationRecord, hasActiveEditorChange); const inspectorOpen = inspectorSidebarOpen || inspectorModalOpen; const shortcutOverrides = useGlobalStore((s) => s.shortcutOverrides); const shortcutConfig = useMemo( @@ -145,6 +149,7 @@ export default memo( useEffect(() => { setResultData(props.resultData); + setHasActiveEditorChange(false); }, [props.resultData]); useEffect(() => { @@ -294,18 +299,29 @@ export default memo( const completeActiveEditor = useCallback(async () => { await Promise.resolve(resultSetTableRef.current?.tableInstance?.completeEditCell?.()); await new Promise((resolve) => setTimeout(resolve, 0)); + setHasActiveEditorChange(false); }, []); - const handleUpdateSubmit = useCallback(() => { - completeActiveEditor().then(() => { - const operations = resultSetTableRef.current?.operationRecordUtils?.getOperationChangeDetail(); - sqlPreviewExecuteRef.current?.handleExecuteSql({ - operations: transformOperations(operations, resultData.headerList), - resultData, - callback: setSubmitLoading, - }); + const getPendingEditOperations = useCallback( + () => + resolvePendingResultEditOperations({ + completeActiveEditor, + getOperations: () => resultSetTableRef.current?.operationRecordUtils?.getOperationChangeDetail(), + }), + [completeActiveEditor], + ); + + const handleUpdateSubmit = useCallback(async () => { + const operations = await getPendingEditOperations(); + if (!operations.length) { + return; + } + sqlPreviewExecuteRef.current?.handleExecuteSql({ + operations: transformOperations(operations, resultData.headerList), + resultData, + callback: setSubmitLoading, }); - }, [completeActiveEditor, resultData]); + }, [getPendingEditOperations, resultData]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -334,7 +350,7 @@ export default memo( } if (isShortcutEventMatch(e, shortcutConfig[ShortcutAction.ResultSubmit].binding)) { e.preventDefault(); - if (hasOperationRecord) { + if (hasPendingChanges || resultSetTableRef.current?.tableInstance?.editorManager?.editingEditor) { handleUpdateSubmit(); } } @@ -348,7 +364,7 @@ export default memo( return () => { resultSetContent?.removeEventListener('keydown', handleKeyDown); }; - }, [hasOperationRecord, handleSearch, handleUpdateSubmit, shortcutConfig]); + }, [handleSearch, handleUpdateSubmit, hasPendingChanges, shortcutConfig]); // SQL execution successful const handleExecuteSuccess = useCallback(() => { @@ -384,21 +400,23 @@ export default memo( resultSetTableRef.current?.operationRecordUtils?.handleDeleteRow(); }, []); - const handleRevocation = useCallback(() => { + const handleRevocation = useCallback(async () => { + await completeActiveEditor(); resultSetTableRef.current?.operationRecordUtils?.handleRevocation(); - }, []); + }, [completeActiveEditor]); const handleOperationChange = useCallback((_hasOperationRecord) => { setHasOperationRecord(_hasOperationRecord); }, []); - const handleViewSQl = () => { - completeActiveEditor().then(() => { - const operations = resultSetTableRef.current?.operationRecordUtils?.getOperationChangeDetail(); - sqlPreviewExecuteRef.current?.handleViewSQL({ - operations: transformOperations(operations, resultData.headerList), - resultData, - }); + const handleViewSQl = async () => { + const operations = await getPendingEditOperations(); + if (!operations.length) { + return; + } + sqlPreviewExecuteRef.current?.handleViewSQL({ + operations: transformOperations(operations, resultData.headerList), + resultData, }); }; @@ -814,14 +832,20 @@ export default memo( return ( <> -
+
{(executing || submitLoading) && ( )} <> ( resultData={resultData} setOrderByText={setOrderByText} onOperationChange={handleOperationChange} + onActiveEditChange={setHasActiveEditorChange} onTableOperationUtils={onTableOperationUtils} onFilterCountChange={setActiveFilterCount} onSelectionChange={handleSelectionChange} diff --git a/chat2db-community-client/src/blocks/SearchResult/components/ResultSet/resultEditActions.ts b/chat2db-community-client/src/blocks/SearchResult/components/ResultSet/resultEditActions.ts new file mode 100644 index 0000000000..9772d7c73b --- /dev/null +++ b/chat2db-community-client/src/blocks/SearchResult/components/ResultSet/resultEditActions.ts @@ -0,0 +1,40 @@ +import isEqual from 'lodash/isEqual'; + +export async function resolvePendingResultEditOperations(params: { + completeActiveEditor: () => Promise; + getOperations: () => T[] | undefined; +}) { + await params.completeActiveEditor(); + return params.getOperations() || []; +} + +interface ResultEditorTable { + editorManager?: { + editingEditor?: { + getValue?: () => unknown; + }; + editCell?: { + col: number; + row: number; + }; + }; + getCellOriginValue: (col: number, row: number) => unknown; +} + +export function hasActiveResultEditorChange(tableInstance: ResultEditorTable | null | undefined) { + const editorManager = tableInstance?.editorManager; + const editor = editorManager?.editingEditor; + const editCell = editorManager?.editCell; + if (!tableInstance || !editor?.getValue || !editCell) { + return false; + } + const currentValue = editor.getValue(); + if (currentValue === undefined) { + return false; + } + return !isEqual(currentValue, tableInstance.getCellOriginValue(editCell.col, editCell.row)); +} + +export function hasPendingResultEdit(hasOperationRecord: boolean, hasActiveEditorChange: boolean) { + return hasOperationRecord || hasActiveEditorChange; +} diff --git a/chat2db-community-client/src/blocks/SearchResult/components/ResultSet/resultSetUi.test.ts b/chat2db-community-client/src/blocks/SearchResult/components/ResultSet/resultSetUi.test.ts index b079f166c4..bb3bab9d1f 100644 --- a/chat2db-community-client/src/blocks/SearchResult/components/ResultSet/resultSetUi.test.ts +++ b/chat2db-community-client/src/blocks/SearchResult/components/ResultSet/resultSetUi.test.ts @@ -34,6 +34,11 @@ import { joinContextMenuGroups, } from '../ResultSetTable/event/onContextmenuCell/menuGroups'; import { matchResultSearchValue } from '../FESearch/searchMatcher'; +import { + hasActiveResultEditorChange, + hasPendingResultEdit, + resolvePendingResultEditOperations, +} from './resultEditActions'; const exportBarSource = readFileSync('src/blocks/SearchResult/components/ExportBar/index.tsx', 'utf8'); const tabsSource = readFileSync('src/components/Tabs/index.tsx', 'utf8'); @@ -43,6 +48,14 @@ const resultToolbarStyleSource = readFileSync( 'src/blocks/SearchResult/components/ResultSetToolbar/style.ts', 'utf8', ); +const resultToolbarSource = readFileSync( + 'src/blocks/SearchResult/components/ResultSetToolbar/index.tsx', + 'utf8', +); +const resultSetTableComponentSource = readFileSync( + 'src/blocks/SearchResult/components/ResultSetTable/index.tsx', + 'utf8', +); const columnVisibilityModalSource = readFileSync( 'src/blocks/SearchResult/components/ResultSetTable/ColumnVisibilityModal.tsx', 'utf8', @@ -59,6 +72,10 @@ const resultSetSource = readFileSync( 'src/blocks/SearchResult/components/ResultSet/index.tsx', 'utf8', ); +const sqlExecuteSource = readFileSync( + 'src/pages/main/workspace/components/SQLExecute/index.tsx', + 'utf8', +); const headerTooltipSource = readFileSync( 'src/blocks/SearchResult/components/ResultSetTable/hooks/useHeaderTooltip.tsx', 'utf8', @@ -85,6 +102,75 @@ test('query result export sends the concrete table as the task target', () => { assert.match(exportBarSource, /tableNames: tableName \? \[tableName\] : undefined,/); }); +test('result edit actions commit the active editor before reading pending operations', async () => { + const calls: string[] = []; + let operations: Array<{ type: string }> = []; + const result = await resolvePendingResultEditOperations({ + completeActiveEditor: async () => { + calls.push('complete'); + await Promise.resolve(); + operations = [{ type: 'UPDATE' }]; + }, + getOperations: () => { + calls.push('read'); + return operations; + }, + }); + + assert.deepEqual(calls, ['complete', 'read']); + assert.deepEqual(result, [{ type: 'UPDATE' }]); + assert.deepEqual( + await resolvePendingResultEditOperations({ + completeActiveEditor: async () => undefined, + getOperations: () => undefined, + }), + [], + 'an unchanged edit cannot produce an empty preview or submit request', + ); + assert.equal(hasPendingResultEdit(false, false), false); + assert.equal(hasPendingResultEdit(true, false), true); + assert.equal(hasPendingResultEdit(false, true), true); + const editorState = { + editorManager: { + editingEditor: { getValue: () => 'original' }, + editCell: { col: 2, row: 1 }, + }, + getCellOriginValue: () => 'original', + }; + assert.equal(hasActiveResultEditorChange(editorState), false); + editorState.editorManager.editingEditor.getValue = () => 'changed'; + assert.equal(hasActiveResultEditorChange(editorState), true); + editorState.editorManager.editingEditor.getValue = () => 'original'; + assert.equal(hasActiveResultEditorChange(editorState), false); + assert.match(resultToolbarSource, /disabled=\{!hasPendingChanges\}/); + assert.doesNotMatch(resultToolbarSource, /isActive=\{hasPendingChanges\}/); + assert.doesNotMatch(resultToolbarSource, /type="primary"/); + assert.equal( + resultToolbarSource.match(/hasPendingChanges && styles\.pendingAction/g)?.length, + 3, + 'revert, preview, and submit should highlight only their icons when changes are pending', + ); + assert.match(resultSetTableComponentSource, /tableElement\.addEventListener\('input', syncActiveEditState, true\)/); + assert.doesNotMatch(resultSetTableComponentSource, /document\.addEventListener\('(input|change)'/); + assert.match( + resultSetSource, + /const handleRevocation = useCallback\(async \(\) => \{[\s\S]*?await completeActiveEditor\(\);[\s\S]*?operationRecordUtils\?\.handleRevocation\(\)/, + ); + assert.match( + resultSetSource, + /const operations = await getPendingEditOperations\(\);[\s\S]*?if \(!operations\.length\) \{[\s\S]*?return;/, + ); + assert.match(resultSetSource, /data-shortcut-scope=\{ShortcutScope\.ResultSet\}/); +}); + +test('result table sorting and paging refresh the current result instead of creating a console result', () => { + assert.doesNotMatch(sqlExecuteSource, /onResultPagingChange=\{handleResultPagingChange\}/); + assert.match( + resultSetSource, + /const requestSequence = \+\+executeRequestSequenceRef\.current;[\s\S]*?executeSQL\(executeSqlParams\)[\s\S]*?setResultData\(/, + ); +}); + test('manage-columns title exposes an aligned localized help tooltip', () => { assert.match(columnVisibilityModalSource, /
)} diff --git a/chat2db-community-client/src/blocks/NewTree/components/TitleRender/index.tsx b/chat2db-community-client/src/blocks/NewTree/components/TitleRender/index.tsx index 6ebf90e491..7e7e06813e 100644 --- a/chat2db-community-client/src/blocks/NewTree/components/TitleRender/index.tsx +++ b/chat2db-community-client/src/blocks/NewTree/components/TitleRender/index.tsx @@ -7,7 +7,7 @@ import { setFocusedContent, getFocusedContent } from '@/store/common/copyFocused import { switchIcon, treeConfig } from '../../treeConfig'; import LoadingGracile from '@/components/Loading/LoadingGracile'; import { type ThemeAppearance } from 'antd-style'; -import { ChevronRight, User, Users } from 'lucide-react'; +import { Activity, ChevronRight, SquareActivity, User, Users } from 'lucide-react'; import { ContextMenuRef } from '@/components/ContextMenu'; import Filtration from '../Filtration'; import { splitSearchHighlight } from './highlightSearchText'; @@ -160,6 +160,14 @@ const TitleRender = (props: IProps) => { return ; } + if (nodeData.treeNodeType === TreeNodeType.MONITOR) { + return ; + } + + if (nodeData.treeNodeType === TreeNodeType.ACTIVE_TRANSACTIONS) { + return ; + } + if (isExpanded && switchIcon[nodeData.treeNodeType]!.unfoldIcon) { return ( void; @@ -129,6 +125,7 @@ export const canBeDoubleClicked = [ TreeNodeType.TRIGGER, TreeNodeType.ALL_DATA, TreeNodeType.DATABASE_ACCOUNT, + TreeNodeType.ACTIVE_TRANSACTIONS, TreeNodeType.SAVE_CONSOLE, ]; @@ -412,35 +409,20 @@ export const useCreateRightClickMenu = () => { [OperationColumn.ActiveTransactions]: { text: i18n('workspace.ops.activeTransactions'), icon: 'icon-file-text', + doubleClickTrigger: true, handle: () => { - const handleOpenSession = (threadId: number) => { - createConsole({ - name: i18n('workspace.ops.sessionThreadTitle', threadId), - ddl: buildSessionInspectionSql(threadId), - dataSourceId: dataSourceId!, - dataSourceName: dataSourceName!, - environmentId, - environment, - databaseType: databaseType!, - databaseName, - schemaName, - }); - }; - staticModal.confirm({ + addWorkspaceTab({ + id: createActiveTransactionsWorkspaceTabId(dataSourceId), + type: WorkspaceTabType.ActiveTransactions, title: i18n('workspace.ops.activeTransactions'), - content: ( - - ), - footer: null, - width: 1100, + uniqueData: { + ...extraParams, + }, }); }, - discard: !canShowActiveTransactionsMenu(databaseType, hasPermission), + discard: + !hasPermission || + !isDatabaseCapabilitySupported(databaseType, DatabaseCapability.ACTIVE_TRANSACTION_INSPECTION), requiredOperations: ['SELECT'], }, diff --git a/chat2db-community-client/src/blocks/NewTree/menuConfig.tsx b/chat2db-community-client/src/blocks/NewTree/menuConfig.tsx index ec6aa09fcf..f013ece2b2 100644 --- a/chat2db-community-client/src/blocks/NewTree/menuConfig.tsx +++ b/chat2db-community-client/src/blocks/NewTree/menuConfig.tsx @@ -161,11 +161,12 @@ export const dropMenuConfig = { OperationColumn.Divider, // Management OperationColumn.MoveToGroup, - OperationColumn.ActiveTransactions, OperationColumn.Refresh, OperationColumn.Divider, OperationColumn.RemoveDataSource, ], + [TreeNodeType.MONITOR]: [], + [TreeNodeType.ACTIVE_TRANSACTIONS]: [OperationColumn.ActiveTransactions], [TreeNodeType.DATABASE_ACCOUNTS]: [OperationColumn.CreateAccount, OperationColumn.Refresh], [TreeNodeType.DATABASE_ACCOUNT]: [OperationColumn.OpenAccountPrivileges], [TreeNodeType.SCHEMAS]: [], diff --git a/chat2db-community-client/src/blocks/NewTree/monitorTree.test.ts b/chat2db-community-client/src/blocks/NewTree/monitorTree.test.ts new file mode 100644 index 0000000000..921158ffac --- /dev/null +++ b/chat2db-community-client/src/blocks/NewTree/monitorTree.test.ts @@ -0,0 +1,42 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { DatabaseTypeCode } from '@/constants/common'; +import { DatabaseCapability } from '@/constants/databaseCapabilities'; +import { TreeNodeType } from '@/constants/tree'; +import { isDatabaseCapabilitySupported } from '@/utils/databaseJudgments'; +import { + createActiveTransactionsTreeNodeKey, + createActiveTransactionsWorkspaceTabId, + createMonitorTreeNodeKey, + MONITOR_TREE_ITEMS, +} from './monitorTree'; + +assert.deepEqual(MONITOR_TREE_ITEMS, [ + { + capability: DatabaseCapability.ACTIVE_TRANSACTION_INSPECTION, + treeNodeType: TreeNodeType.ACTIVE_TRANSACTIONS, + titleKey: 'workspace.ops.activeTransactions', + }, +]); +assert.equal( + isDatabaseCapabilitySupported(DatabaseTypeCode.MYSQL, DatabaseCapability.ACTIVE_TRANSACTION_INSPECTION), + true, +); +assert.equal( + isDatabaseCapabilitySupported(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.ACTIVE_TRANSACTION_INSPECTION), + false, +); +assert.equal(createMonitorTreeNodeKey(42), 'dataSource_42-monitor'); +assert.equal(createActiveTransactionsTreeNodeKey(42), 'dataSource_42-monitor-activeTransactions'); +assert.equal(createActiveTransactionsWorkspaceTabId(42), 'activeTransactions-42'); + +const monitorTreeSource = readFileSync('src/blocks/NewTree/monitorTree.ts', 'utf8'); +const treeConfigSource = readFileSync('src/blocks/NewTree/treeConfig.tsx', 'utf8'); +const workspaceTabsSource = readFileSync('src/pages/main/workspace/components/WorkspaceTabs/index.tsx', 'utf8'); +assert.doesNotMatch(monitorTreeSource, /isDatabaseCapabilitySupported|getMonitorTreeNodeTypes/); +assert.match(treeConfigSource, /isDatabaseCapabilitySupported/); +assert.match(treeConfigSource, /DatabaseCapability\.ACTIVE_TRANSACTION_INSPECTION|capability/); +assert.match(workspaceTabsSource, /isDatabaseCapabilitySupported/); +assert.match(workspaceTabsSource, /DatabaseCapability\.ACTIVE_TRANSACTION_INSPECTION/); + +console.log('Monitor tree tests passed'); diff --git a/chat2db-community-client/src/blocks/NewTree/monitorTree.ts b/chat2db-community-client/src/blocks/NewTree/monitorTree.ts new file mode 100644 index 0000000000..e34c55f740 --- /dev/null +++ b/chat2db-community-client/src/blocks/NewTree/monitorTree.ts @@ -0,0 +1,18 @@ +import { DatabaseCapability } from '@/constants/databaseCapabilities'; +import { TreeNodeType } from '@/constants/tree'; + +export const MONITOR_TREE_ITEMS = [ + { + capability: DatabaseCapability.ACTIVE_TRANSACTION_INSPECTION, + treeNodeType: TreeNodeType.ACTIVE_TRANSACTIONS, + titleKey: 'workspace.ops.activeTransactions', + }, +] as const; + +export const createMonitorTreeNodeKey = (dataSourceId?: number) => `dataSource_${dataSourceId}-monitor`; + +export const createActiveTransactionsTreeNodeKey = (dataSourceId?: number) => + `${createMonitorTreeNodeKey(dataSourceId)}-activeTransactions`; + +export const createActiveTransactionsWorkspaceTabId = (dataSourceId?: number) => + `activeTransactions-${dataSourceId}`; diff --git a/chat2db-community-client/src/blocks/NewTree/treeConfig.tsx b/chat2db-community-client/src/blocks/NewTree/treeConfig.tsx index 58d3c7ebd1..4be4f42168 100644 --- a/chat2db-community-client/src/blocks/NewTree/treeConfig.tsx +++ b/chat2db-community-client/src/blocks/NewTree/treeConfig.tsx @@ -10,6 +10,11 @@ import { getDatabaseSupport } from '@/utils/database'; import { isDatabaseCapabilitySupported } from '@/utils/databaseJudgments'; import { v4 as uuid } from 'uuid'; import { createSavedConsoleTreeNodeKey } from '@/store/tree/backgroundRefresh'; +import { + createActiveTransactionsTreeNodeKey, + createMonitorTreeNodeKey, + MONITOR_TREE_ITEMS, +} from './monitorTree'; const fileIcon = 'icon-colourful-folder-close'; const unfoldFileIcon = 'icon-colourful-folder-open'; @@ -179,6 +184,17 @@ function createSaveConsolesNode(extraParams: any): TreeNodeData { }; } +function createMonitorNode(extraParams: any): TreeNodeData { + return { + key: createMonitorTreeNodeKey(extraParams.dataSourceId), + originalTitle: i18n('workspace.ops.monitor'), + title: null, + treeNodeType: TreeNodeType.MONITOR, + isLeaf: false, + extraParams, + }; +} + export const treeConfig: { [key in TreeNodeType]: ITreeConfigItem } = { [TreeNodeType.GROUPS]: { getChildren: () => { @@ -268,6 +284,20 @@ export const treeConfig: { [key in TreeNodeType]: ITreeConfigItem } = { extraParams, } : null; + const monitorNode = MONITOR_TREE_ITEMS.some(({ capability }) => + isDatabaseCapabilitySupported(databaseType, capability), + ) + ? createMonitorNode(extraParams) + : null; + const appendDataSourceNodes = (data: TreeNodeData[]) => { + if (monitorNode) { + data.push(monitorNode); + } + if (accountNode) { + data.push(accountNode); + } + return data; + }; if (supportDatabase === false && supportSchema === false) { // No database or schema level at all (Firebird, IoTDB, ...): the // connection itself is the namespace, so show the object folders @@ -317,10 +347,7 @@ export const treeConfig: { [key in TreeNodeType]: ITreeConfigItem } = { }, createSaveConsolesNode(nodeExtraParams), ]; - if (accountNode) { - data.push(accountNode); - } - r(data); + r(appendDataSourceNodes(data)); return; } if (supportDatabase === false) { @@ -345,10 +372,7 @@ export const treeConfig: { [key in TreeNodeType]: ITreeConfigItem } = { }, }; }); - if (accountNode) { - data.push(accountNode); - } - r(data); + r(appendDataSourceNodes(data)); }) .catch(() => { j(); @@ -375,10 +399,7 @@ export const treeConfig: { [key in TreeNodeType]: ITreeConfigItem } = { }, }; }); - if (accountNode) { - data.push(accountNode); - } - r(data); + r(appendDataSourceNodes(data)); }) .catch(() => { j(); @@ -392,6 +413,29 @@ export const treeConfig: { [key in TreeNodeType]: ITreeConfigItem } = { }, }, + [TreeNodeType.MONITOR]: { + getChildren: (extraParams: any) => { + return Promise.resolve( + MONITOR_TREE_ITEMS.filter(({ capability }) => + isDatabaseCapabilitySupported(extraParams.databaseType, capability), + ).map(({ treeNodeType, titleKey }) => ({ + key: treeConfig[treeNodeType].createTreeNodeKey!(extraParams), + originalTitle: i18n(titleKey), + title: null, + treeNodeType, + isLeaf: true, + extraParams, + })), + ); + }, + createTreeNodeKey: (params) => createMonitorTreeNodeKey(params.dataSourceId), + }, + + [TreeNodeType.ACTIVE_TRANSACTIONS]: { + getChildren: () => Promise.resolve([]), + createTreeNodeKey: (params) => createActiveTransactionsTreeNodeKey(params.dataSourceId), + }, + [TreeNodeType.DATABASE_ACCOUNTS]: { getChildren: (extraParams: any) => { return accountAdminService.list({ dataSourceId: extraParams.dataSourceId }).then((accounts) => { diff --git a/chat2db-community-client/src/blocks/NewTree/treeMenuGrouping.test.ts b/chat2db-community-client/src/blocks/NewTree/treeMenuGrouping.test.ts index 1c48dcd2d6..e3809ca490 100644 --- a/chat2db-community-client/src/blocks/NewTree/treeMenuGrouping.test.ts +++ b/chat2db-community-client/src/blocks/NewTree/treeMenuGrouping.test.ts @@ -63,7 +63,8 @@ assert.equal( ); assert.equal( dropMenuConfig.DEFAULT[TreeNodeType.DATA_SOURCE].includes(OperationColumn.ActiveTransactions), - true, + false, ); +assert.deepEqual(dropMenuConfig.DEFAULT[TreeNodeType.ACTIVE_TRANSACTIONS], [OperationColumn.ActiveTransactions]); console.log('Tree menu grouping tests passed'); diff --git a/chat2db-community-client/src/constants/activeTransaction.ts b/chat2db-community-client/src/constants/activeTransaction.ts new file mode 100644 index 0000000000..6b3379292a --- /dev/null +++ b/chat2db-community-client/src/constants/activeTransaction.ts @@ -0,0 +1,27 @@ +export enum ActiveTransactionQueryState { + VISIBLE = 'VISIBLE', + UNAVAILABLE = 'UNAVAILABLE', +} + +export enum ActiveTransactionSessionState { + LIVE = 'LIVE', + DISAPPEARED_OR_HIDDEN = 'DISAPPEARED_OR_HIDDEN', +} + +export enum ActiveTransactionLockMetadataState { + AVAILABLE = 'AVAILABLE', + UNAVAILABLE = 'UNAVAILABLE', +} + +export enum ActiveTransactionLockMetadataSource { + MYSQL_80_PERFORMANCE_SCHEMA = 'MYSQL_80_PERFORMANCE_SCHEMA', + MYSQL_57_INFORMATION_SCHEMA = 'MYSQL_57_INFORMATION_SCHEMA', +} + +export const ACTIVE_TRANSACTION_PROCESS_PRIVILEGE_ERROR_CODE = + 'mysql.activeTransaction.processPrivilegeRequired'; + +export const MYSQL_ACTIVE_TRANSACTION_STATE = { + RUNNING: 'RUNNING', + LOCK_WAIT: 'LOCK WAIT', +} as const; diff --git a/chat2db-community-client/src/constants/databaseCapabilities.ts b/chat2db-community-client/src/constants/databaseCapabilities.ts index 3eb60deab9..686c4a7eaf 100644 --- a/chat2db-community-client/src/constants/databaseCapabilities.ts +++ b/chat2db-community-client/src/constants/databaseCapabilities.ts @@ -1,6 +1,7 @@ export enum DatabaseCapability { ROUTINE_OPERATION = 'ROUTINE_OPERATION', ACCOUNT_MANAGEMENT = 'ACCOUNT_MANAGEMENT', + ACTIVE_TRANSACTION_INSPECTION = 'ACTIVE_TRANSACTION_INSPECTION', DATABASE_DELETE = 'DATABASE_DELETE', SCHEMA_DELETE = 'SCHEMA_DELETE', DATABASE_CREATE = 'DATABASE_CREATE', diff --git a/chat2db-community-client/src/constants/tree.ts b/chat2db-community-client/src/constants/tree.ts index c4f0de2ac0..e069d16a18 100644 --- a/chat2db-community-client/src/constants/tree.ts +++ b/chat2db-community-client/src/constants/tree.ts @@ -5,6 +5,8 @@ export enum TreeNodeType { ALL_DATA = 'allData', // All data dedicated to redis DATA_SOURCES = 'dataSources', DATA_SOURCE = 'dataSource', + MONITOR = 'monitor', + ACTIVE_TRANSACTIONS = 'activeTransactions', DATABASE_ACCOUNTS = 'databaseAccounts', DATABASE_ACCOUNT = 'databaseAccount', DATABASE = 'database', diff --git a/chat2db-community-client/src/constants/workspace.ts b/chat2db-community-client/src/constants/workspace.ts index 17f539cf5a..3e55641804 100644 --- a/chat2db-community-client/src/constants/workspace.ts +++ b/chat2db-community-client/src/constants/workspace.ts @@ -33,6 +33,7 @@ export enum WorkspaceTabType { ViewERModal = 'viewERModal', RedisAllData = 'redisAllData', AccountPrivileges = 'accountPrivileges', + ActiveTransactions = 'activeTransactions', ContentDiff = 'contentDiff', Terminal = 'terminal', } @@ -88,6 +89,9 @@ export const workspaceTabConfig: { [WorkspaceTabType.AccountPrivileges]: { icon: 'icon-users', }, + [WorkspaceTabType.ActiveTransactions]: { + icon: 'icon-file-text', + }, [WorkspaceTabType.ContentDiff]: { icon: 'icon-switch-horizontal', }, diff --git a/chat2db-community-client/src/i18n/en-US/workspace.ts b/chat2db-community-client/src/i18n/en-US/workspace.ts index 5471094a5a..855da9eee4 100644 --- a/chat2db-community-client/src/i18n/en-US/workspace.ts +++ b/chat2db-community-client/src/i18n/en-US/workspace.ts @@ -296,6 +296,7 @@ export default { 'workspace.databaseAccount.lockUnsupported': 'Account lock is not supported by this MySQL version or current account privileges', 'workspace.ops.activeTransactions': 'Active Transactions', + 'workspace.ops.monitor': 'Monitor', 'workspace.ops.transactionId': 'Transaction ID', 'workspace.ops.transactionState': 'State', 'workspace.ops.transactionStarted': 'Started', @@ -304,7 +305,7 @@ export default { 'workspace.ops.transactionIsolation': 'Isolation', 'workspace.ops.rowsLocked': 'Rows Locked', 'workspace.ops.rowsModified': 'Rows Modified', - 'workspace.ops.threadId': 'Thread ID', + 'workspace.ops.threadId': 'MySQL Connection ID', 'workspace.ops.user': 'User', 'workspace.ops.host': 'Host', 'workspace.ops.database': 'Database', @@ -314,17 +315,14 @@ export default { 'workspace.ops.activeTransactionCount': '{1} active transaction(s)', 'workspace.ops.waitedLock': 'Waited Lock', 'workspace.ops.blocker': 'Blocker', - 'workspace.ops.sessionAction': 'Session', - 'workspace.ops.ownerSession': 'Owner', - 'workspace.ops.blockerSession': 'Blocker', - 'workspace.ops.openSession': 'Open the transaction owner session', - 'workspace.ops.openBlockingSession': 'Open the blocking session', + 'workspace.ops.openSession': 'Inspect the current connection in a new console', + 'workspace.ops.openBlockingSession': 'Inspect the blocking connection in a new console', 'workspace.ops.sessionUnavailable': 'Session unavailable', 'workspace.ops.queryUnavailable': 'SQL unavailable', 'workspace.ops.emptyValue': 'Empty', 'workspace.ops.lockMetadataUnavailable': 'Lock metadata unavailable', 'workspace.ops.lockMetadataDegraded': 'Lock-wait details are degraded:', - 'workspace.ops.sessionThreadTitle': 'MySQL session {1}', + 'workspace.ops.sessionThreadTitle': 'MySQL connection {1}', 'workspace.routine.button.invoke': 'Invoke routine', 'workspace.routine.button.apply': 'Apply changes', 'workspace.routine.button.refresh': 'Reload DDL', diff --git a/chat2db-community-client/src/i18n/es-ES/workspace.ts b/chat2db-community-client/src/i18n/es-ES/workspace.ts index 083c532397..28f3e26871 100644 --- a/chat2db-community-client/src/i18n/es-ES/workspace.ts +++ b/chat2db-community-client/src/i18n/es-ES/workspace.ts @@ -298,6 +298,7 @@ export default { 'workspace.databaseAccount.lockUnsupported': 'Esta versión de MySQL o los privilegios de la cuenta actual no admiten el bloqueo de cuentas', 'workspace.ops.activeTransactions': 'Transacciones activas', + 'workspace.ops.monitor': 'Monitorización', 'workspace.ops.transactionId': 'ID de transacción', 'workspace.ops.transactionState': 'Estado', 'workspace.ops.transactionStarted': 'Inicio', @@ -306,7 +307,7 @@ export default { 'workspace.ops.transactionIsolation': 'Aislamiento', 'workspace.ops.rowsLocked': 'Filas bloqueadas', 'workspace.ops.rowsModified': 'Filas modificadas', - 'workspace.ops.threadId': 'ID de hilo', + 'workspace.ops.threadId': 'ID de conexión MySQL', 'workspace.ops.user': 'Usuario', 'workspace.ops.host': 'Host', 'workspace.ops.database': 'Base de datos', @@ -316,17 +317,14 @@ export default { 'workspace.ops.activeTransactionCount': '{1} transacción(es) activa(s)', 'workspace.ops.waitedLock': 'Bloqueo esperado', 'workspace.ops.blocker': 'Bloqueador', - 'workspace.ops.sessionAction': 'Sesión', - 'workspace.ops.ownerSession': 'Propietaria', - 'workspace.ops.blockerSession': 'Bloqueadora', - 'workspace.ops.openSession': 'Abrir la sesión propietaria de la transacción', - 'workspace.ops.openBlockingSession': 'Abrir la sesión bloqueadora', + 'workspace.ops.openSession': 'Inspeccionar la conexión actual en una consola nueva', + 'workspace.ops.openBlockingSession': 'Inspeccionar la conexión bloqueadora en una consola nueva', 'workspace.ops.sessionUnavailable': 'Sesión no disponible', 'workspace.ops.queryUnavailable': 'SQL no disponible', 'workspace.ops.emptyValue': 'Vacío', 'workspace.ops.lockMetadataUnavailable': 'Metadatos de bloqueo no disponibles', 'workspace.ops.lockMetadataDegraded': 'Los detalles de espera de bloqueo están degradados:', - 'workspace.ops.sessionThreadTitle': 'Sesión MySQL {1}', + 'workspace.ops.sessionThreadTitle': 'Conexión MySQL {1}', 'workspace.routine.button.invoke': 'Invocar rutina', 'workspace.routine.button.apply': 'Aplicar cambios', 'workspace.routine.button.refresh': 'Volver a cargar DDL', diff --git a/chat2db-community-client/src/i18n/ja-JP/workspace.ts b/chat2db-community-client/src/i18n/ja-JP/workspace.ts index 5b17fd81b0..9c3c2d121b 100644 --- a/chat2db-community-client/src/i18n/ja-JP/workspace.ts +++ b/chat2db-community-client/src/i18n/ja-JP/workspace.ts @@ -296,6 +296,7 @@ export default { 'workspace.databaseAccount.lockUnsupported': 'この MySQL バージョンまたは現在の権限ではアカウントロックを利用できません', 'workspace.ops.activeTransactions': 'アクティブなトランザクション', + 'workspace.ops.monitor': '監視', 'workspace.ops.transactionId': 'トランザクション ID', 'workspace.ops.transactionState': '状態', 'workspace.ops.transactionStarted': '開始時刻', @@ -304,7 +305,7 @@ export default { 'workspace.ops.transactionIsolation': '分離レベル', 'workspace.ops.rowsLocked': 'ロック行数', 'workspace.ops.rowsModified': '変更行数', - 'workspace.ops.threadId': 'スレッド ID', + 'workspace.ops.threadId': 'MySQL 接続 ID', 'workspace.ops.user': 'ユーザー', 'workspace.ops.host': 'ホスト', 'workspace.ops.database': 'データベース', @@ -314,17 +315,14 @@ export default { 'workspace.ops.activeTransactionCount': 'アクティブなトランザクション: {1} 件', 'workspace.ops.waitedLock': '待機中のロック', 'workspace.ops.blocker': 'ブロッカー', - 'workspace.ops.sessionAction': 'セッション', - 'workspace.ops.ownerSession': '所有者', - 'workspace.ops.blockerSession': 'ブロッカー', - 'workspace.ops.openSession': 'トランザクション所有者のセッションを開く', - 'workspace.ops.openBlockingSession': 'ブロックしているセッションを開く', + 'workspace.ops.openSession': '新しい Console で現在の接続を確認', + 'workspace.ops.openBlockingSession': '新しい Console でブロック元の接続を確認', 'workspace.ops.sessionUnavailable': 'セッションは利用できません', 'workspace.ops.queryUnavailable': 'SQL は利用できません', 'workspace.ops.emptyValue': '空', 'workspace.ops.lockMetadataUnavailable': 'ロックメタデータは利用できません', 'workspace.ops.lockMetadataDegraded': 'ロック待機の詳細は制限されています:', - 'workspace.ops.sessionThreadTitle': 'MySQL セッション {1}', + 'workspace.ops.sessionThreadTitle': 'MySQL 接続 {1}', 'workspace.routine.button.invoke': 'ルーチンを呼び出す', 'workspace.routine.button.apply': '変更を適用', 'workspace.routine.button.refresh': 'DDL を再読み込み', diff --git a/chat2db-community-client/src/i18n/ko-KR/workspace.ts b/chat2db-community-client/src/i18n/ko-KR/workspace.ts index e82f59243d..c7b886ddb4 100644 --- a/chat2db-community-client/src/i18n/ko-KR/workspace.ts +++ b/chat2db-community-client/src/i18n/ko-KR/workspace.ts @@ -294,6 +294,7 @@ export default { 'workspace.databaseAccount.revokePrivilege': '권한 회수', 'workspace.databaseAccount.lockUnsupported': '이 MySQL 버전 또는 현재 계정 권한에서는 계정 잠금을 지원하지 않습니다', 'workspace.ops.activeTransactions': '활성 트랜잭션', + 'workspace.ops.monitor': '모니터링', 'workspace.ops.transactionId': '트랜잭션 ID', 'workspace.ops.transactionState': '상태', 'workspace.ops.transactionStarted': '시작 시간', @@ -302,7 +303,7 @@ export default { 'workspace.ops.transactionIsolation': '격리 수준', 'workspace.ops.rowsLocked': '잠긴 행', 'workspace.ops.rowsModified': '수정된 행', - 'workspace.ops.threadId': '스레드 ID', + 'workspace.ops.threadId': 'MySQL 연결 ID', 'workspace.ops.user': '사용자', 'workspace.ops.host': '호스트', 'workspace.ops.database': '데이터베이스', @@ -312,17 +313,14 @@ export default { 'workspace.ops.activeTransactionCount': '활성 트랜잭션 {1}개', 'workspace.ops.waitedLock': '대기 중인 잠금', 'workspace.ops.blocker': '차단 세션', - 'workspace.ops.sessionAction': '세션', - 'workspace.ops.ownerSession': '소유자', - 'workspace.ops.blockerSession': '차단자', - 'workspace.ops.openSession': '트랜잭션 소유자 세션 열기', - 'workspace.ops.openBlockingSession': '차단 중인 세션 열기', + 'workspace.ops.openSession': '새 Console에서 현재 연결 검사', + 'workspace.ops.openBlockingSession': '새 Console에서 차단 연결 검사', 'workspace.ops.sessionUnavailable': '세션을 사용할 수 없음', 'workspace.ops.queryUnavailable': 'SQL을 사용할 수 없음', 'workspace.ops.emptyValue': '비어 있음', 'workspace.ops.lockMetadataUnavailable': '잠금 메타데이터를 사용할 수 없음', 'workspace.ops.lockMetadataDegraded': '잠금 대기 세부 정보가 제한되었습니다:', - 'workspace.ops.sessionThreadTitle': 'MySQL 세션 {1}', + 'workspace.ops.sessionThreadTitle': 'MySQL 연결 {1}', 'workspace.routine.button.invoke': '루틴 호출', 'workspace.routine.button.apply': '변경 사항 적용', 'workspace.routine.button.refresh': 'DDL 다시 불러오기', diff --git a/chat2db-community-client/src/i18n/zh-CN/workspace.ts b/chat2db-community-client/src/i18n/zh-CN/workspace.ts index 6a45d9edcc..53de96d9bd 100644 --- a/chat2db-community-client/src/i18n/zh-CN/workspace.ts +++ b/chat2db-community-client/src/i18n/zh-CN/workspace.ts @@ -287,6 +287,7 @@ export default { 'workspace.databaseAccount.revokePrivilege': '撤销权限', 'workspace.databaseAccount.lockUnsupported': '当前 MySQL 版本或账号权限不支持账号锁定', 'workspace.ops.activeTransactions': '活动事务', + 'workspace.ops.monitor': '监控', 'workspace.ops.transactionId': '事务 ID', 'workspace.ops.transactionState': '状态', 'workspace.ops.transactionStarted': '开始时间', @@ -295,7 +296,7 @@ export default { 'workspace.ops.transactionIsolation': '隔离级别', 'workspace.ops.rowsLocked': '锁定行数', 'workspace.ops.rowsModified': '修改行数', - 'workspace.ops.threadId': '线程 ID', + 'workspace.ops.threadId': 'MySQL 连接 ID', 'workspace.ops.user': '用户', 'workspace.ops.host': '主机', 'workspace.ops.database': '数据库', @@ -305,17 +306,14 @@ export default { 'workspace.ops.activeTransactionCount': '{1} 个活动事务', 'workspace.ops.waitedLock': '等待的锁', 'workspace.ops.blocker': '阻塞方', - 'workspace.ops.sessionAction': '会话', - 'workspace.ops.ownerSession': '事务会话', - 'workspace.ops.blockerSession': '阻塞会话', - 'workspace.ops.openSession': '打开事务所属会话', - 'workspace.ops.openBlockingSession': '打开阻塞会话', + 'workspace.ops.openSession': '在新 Console 中检查当前连接', + 'workspace.ops.openBlockingSession': '在新 Console 中检查阻塞连接', 'workspace.ops.sessionUnavailable': '会话不可用', 'workspace.ops.queryUnavailable': 'SQL 不可用', 'workspace.ops.emptyValue': '空值', 'workspace.ops.lockMetadataUnavailable': '锁元数据不可用', 'workspace.ops.lockMetadataDegraded': '锁等待详情已降级:', - 'workspace.ops.sessionThreadTitle': 'MySQL 会话 {1}', + 'workspace.ops.sessionThreadTitle': 'MySQL 连接 {1}', 'workspace.routine.button.invoke': '调用例程', 'workspace.routine.button.apply': '提交变更', 'workspace.routine.button.refresh': '重新加载 DDL', diff --git a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx index 9903ec3440..bc63a3b57e 100644 --- a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx +++ b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx @@ -19,7 +19,7 @@ import { } from '@dnd-kit/core'; // ----- constants ----- -import { ConsoleOpenedStatus, WorkspaceTabType, workspaceTabConfig } from '@/constants'; +import { ConsoleOpenedStatus, DatabaseCapability, WorkspaceTabType, workspaceTabConfig } from '@/constants'; import { DEFAULT_TERMINAL_SETTINGS } from '@/constants/terminal'; import { IWorkspaceTab, @@ -40,6 +40,7 @@ import RedisAllData from '@/blocks/RedisAllData'; import Iconfont from '@/components/Iconfont'; import { useZoerStore } from '@/store/zoer'; import AccountPrivilegePanel from '../AccountPrivilegePanel'; +import ActiveTransactionsContent from '@/blocks/NewTree/components/ActiveTransactionsContent'; import ContentDiffTab from './ContentDiffTab'; import FilePreviewTab from './FilePreviewTab'; import TerminalTab from './TerminalTab'; @@ -60,6 +61,7 @@ import jcefApi from '@/jcef'; import { copyToClipboard, getTemporaryId, isTemporaryId } from '@/utils'; import { resolveDataSourceIdentityColor } from '@/utils/dataSourceIdentity'; +import { isDatabaseCapabilitySupported } from '@/utils/databaseJudgments'; import { useIndexDBStore } from '@/store/indexDB'; import { getDatabaseSupport } from '@/utils/database'; @@ -88,6 +90,7 @@ import { } from './terminalTabPlacement'; import { getNextActiveWorkspaceTabIdAfterClose } from './workspaceTabSelection'; import { + appendWorkspaceTabToPane, areWorkspaceTabSplitLayoutsEqual, collectWorkspaceTabPaneIds, createWorkspaceTabSplitNode, @@ -1116,26 +1119,18 @@ const WorkspaceTabs = memo(() => { } }; + const appendNewConsoleToPane = (consoleId: string | number, targetPaneId?: WorkspaceTabPaneId) => { + const currentLayout = useWorkspaceStore.getState().workspaceTabSplitLayout; + if (!currentLayout) { + return; + } + const activePaneId = targetPaneId || currentLayout.activePane || MAIN_WORKSPACE_TAB_PANE; + updateWorkspaceTabSplitLayout(appendWorkspaceTabToPane(currentLayout, consoleId, activePaneId)); + }; + const createNewConsole = (targetPaneId?: WorkspaceTabPaneId) => { - const appendNewConsoleToActivePane = (consoleId: string | number) => { - const currentLayout = useWorkspaceStore.getState().workspaceTabSplitLayout; - if (!currentLayout) { - return; - } - const activePaneId = targetPaneId || currentLayout.activePane || MAIN_WORKSPACE_TAB_PANE; - updateWorkspaceTabSplitLayout({ - ...currentLayout, - activePane: activePaneId, - paneTabIds: { - ...currentLayout.paneTabIds, - [activePaneId]: [...(currentLayout.paneTabIds[activePaneId] || []), consoleId], - }, - activeTabIds: { - ...currentLayout.activeTabIds, - [activePaneId]: consoleId, - }, - }); - }; + const appendNewConsoleToActivePane = (consoleId: string | number) => + appendNewConsoleToPane(consoleId, targetPaneId); if (zoerBoundInfo) { const param: any = zoerBoundInfo; @@ -1384,18 +1379,7 @@ const WorkspaceTabs = memo(() => { } const activePaneId = workspaceTabSplitLayout?.activePane || MAIN_WORKSPACE_TAB_PANE; const nextLayout = workspaceTabSplitLayout - ? { - ...workspaceTabSplitLayout, - activePane: activePaneId, - paneTabIds: { - ...workspaceTabSplitLayout.paneTabIds, - [activePaneId]: [...(workspaceTabSplitLayout.paneTabIds[activePaneId] || []), nextTab.id], - }, - activeTabIds: { - ...workspaceTabSplitLayout.activeTabIds, - [activePaneId]: nextTab.id, - }, - } + ? appendWorkspaceTabToPane(workspaceTabSplitLayout, nextTab.id, activePaneId) : workspaceTabSplitLayout; setWorkspaceTabsState([...(workspaceTabList || []), nextTab], nextLayout, nextTab.id); setActiveConsoleId(nextTab.id); @@ -1853,6 +1837,41 @@ const WorkspaceTabs = memo(() => { return ; }; + const renderActiveTransactions = (item: IWorkspaceTab) => { + const { uniqueData } = item; + if ( + !uniqueData?.dataSourceId || + !isDatabaseCapabilitySupported( + uniqueData.databaseType, + DatabaseCapability.ACTIVE_TRANSACTION_INSPECTION, + ) + ) { + return; + } + return ( +
+ { + createConsole({ + name: i18n('workspace.ops.sessionThreadTitle', connectionId), + ddl: sql, + dataSourceId: uniqueData.dataSourceId!, + dataSourceName: uniqueData.dataSourceName!, + environmentId: uniqueData.environmentId, + environment: uniqueData.environment, + databaseType: uniqueData.databaseType!, + databaseName: uniqueData.databaseName, + schemaName: uniqueData.schemaName, + }).then((consoleId) => appendNewConsoleToPane(consoleId)); + }} + /> +
+ ); + }; + const renderContentDiff = (item: IWorkspaceTab) => { const { uniqueData } = item; return ( @@ -1901,6 +1920,8 @@ const WorkspaceTabs = memo(() => { return renderRedisAllData(item); case WorkspaceTabType.AccountPrivileges: return renderAccountPrivileges(item); + case WorkspaceTabType.ActiveTransactions: + return renderActiveTransactions(item); case WorkspaceTabType.ContentDiff: return renderContentDiff(item); case WorkspaceTabType.Terminal: diff --git a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/workspaceTabLayout.test.ts b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/workspaceTabLayout.test.ts new file mode 100644 index 0000000000..7abe5d262d --- /dev/null +++ b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/workspaceTabLayout.test.ts @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict'; +import type { IWorkspaceTabSplitLayout } from '@/typings'; +import { appendWorkspaceTabToPane } from './workspaceTabLayout'; + +const layout: IWorkspaceTabSplitLayout = { + direction: 'horizontal', + activePane: 'split', + paneTabIds: { + main: ['main-tab'], + split: ['monitor-tab'], + }, + activeTabIds: { + main: 'main-tab', + split: 'monitor-tab', + }, +}; + +const appended = appendWorkspaceTabToPane(layout, 596); +assert.deepEqual(appended.paneTabIds.split, ['monitor-tab', 596]); +assert.equal(appended.activeTabIds.split, 596); +assert.equal(appended.activePane, 'split'); +assert.deepEqual(appended.paneTabIds.main, ['main-tab']); +assert.deepEqual(appendWorkspaceTabToPane(appended, 596).paneTabIds.split, ['monitor-tab', 596]); + +console.log('Workspace tab layout tests passed'); diff --git a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/workspaceTabLayout.ts b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/workspaceTabLayout.ts index c45c4d0364..3e5f0c33a6 100644 --- a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/workspaceTabLayout.ts +++ b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/workspaceTabLayout.ts @@ -7,6 +7,26 @@ import type { let splitNodeSequence = 0; +export function appendWorkspaceTabToPane( + layout: IWorkspaceTabSplitLayout, + tabId: string | number, + paneId: WorkspaceTabPaneId = layout.activePane, +): IWorkspaceTabSplitLayout { + const paneTabIds = layout.paneTabIds[paneId] || []; + return { + ...layout, + activePane: paneId, + paneTabIds: { + ...layout.paneTabIds, + [paneId]: paneTabIds.includes(tabId) ? paneTabIds : [...paneTabIds, tabId], + }, + activeTabIds: { + ...layout.activeTabIds, + [paneId]: tabId, + }, + }; +} + function createSplitNodeId() { splitNodeSequence += 1; return `split_${Date.now()}_${splitNodeSequence}`; diff --git a/chat2db-community-client/src/service/sql.ts b/chat2db-community-client/src/service/sql.ts index 8e5eea65ef..c13a539214 100644 --- a/chat2db-community-client/src/service/sql.ts +++ b/chat2db-community-client/src/service/sql.ts @@ -15,6 +15,12 @@ import { } from '@/typings'; import { DatabaseTypeCode } from '@/constants'; import { ExportSizeEnum, ExportTypeEnum } from '@/typings/resultTable'; +import type { + ActiveTransactionLockMetadataSource, + ActiveTransactionLockMetadataState, + ActiveTransactionQueryState, + ActiveTransactionSessionState, +} from '@/constants/activeTransaction'; import type { IDdlExecuteRequest, ISqlEditorExecuteRequest, @@ -445,10 +451,11 @@ export interface IActiveTransactionItem { host: string | null; db: string | null; query: string | null; - queryState?: 'VISIBLE' | 'UNAVAILABLE'; + queryState?: ActiveTransactionQueryState; sessionAvailable?: boolean; - sessionState?: 'LIVE' | 'DISAPPEARED_OR_HIDDEN'; + sessionState?: ActiveTransactionSessionState; canOpenSession?: boolean; + connectionInspectionSql: string | null; waitingLockId?: string | null; blockingLockId?: string | null; blockingTrxId?: string | null; @@ -457,6 +464,7 @@ export interface IActiveTransactionItem { blockingThreadId?: number | null; blockingSessionAvailable?: boolean; canOpenBlockingSession?: boolean; + blockingConnectionInspectionSql: string | null; blockingUser?: string | null; blockingHost?: string | null; blockingDb?: string | null; @@ -473,9 +481,8 @@ export interface IActiveTransactionItem { blockingLockStatus?: string | null; blockingLockData?: string | null; lockWaitAvailable?: boolean; - lockMetadataState?: 'AVAILABLE' | 'UNAVAILABLE'; - lockMetadataSource?: 'MYSQL_80_PERFORMANCE_SCHEMA' | 'MYSQL_57_INFORMATION_SCHEMA' | null; - lockMetadataMessage?: string | null; + lockMetadataState?: ActiveTransactionLockMetadataState; + lockMetadataSource?: ActiveTransactionLockMetadataSource | null; } export interface IActiveTransactionRequest { @@ -486,7 +493,7 @@ export interface IActiveTransactionRequest { const getActiveTransactionList = createRequest( '/api/rdb/active_transaction/list', - { method: 'get' }, + { method: 'get', errorLevel: false }, ); const checkIsSelectSQL = createRequest<{ sql: string; dbType: DatabaseTypeCode }, boolean>('/api/sql/valid_select'); diff --git a/chat2db-community-client/src/utils/databaseJudgments.test.ts b/chat2db-community-client/src/utils/databaseJudgments.test.ts index d748effb7b..ac6c8d1166 100644 --- a/chat2db-community-client/src/utils/databaseJudgments.test.ts +++ b/chat2db-community-client/src/utils/databaseJudgments.test.ts @@ -48,6 +48,8 @@ assertCapability('mysql', DatabaseCapability.ROUTINE_OPERATION, true); assertCapability(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.ROUTINE_OPERATION, false); assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.ACCOUNT_MANAGEMENT, true); assertCapability(DatabaseTypeCode.ORACLE, DatabaseCapability.ACCOUNT_MANAGEMENT, false); +assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.ACTIVE_TRANSACTION_INSPECTION, true); +assertCapability(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.ACTIVE_TRANSACTION_INSPECTION, false); assertCapability(DatabaseTypeCode.MYSQL, DatabaseCapability.DATABASE_DELETE, true); assertCapability(DatabaseTypeCode.POSTGRESQL, DatabaseCapability.DATABASE_DELETE, true); diff --git a/chat2db-community-client/src/utils/databaseJudgments.ts b/chat2db-community-client/src/utils/databaseJudgments.ts index 842d9aa5fe..1b55d282ac 100644 --- a/chat2db-community-client/src/utils/databaseJudgments.ts +++ b/chat2db-community-client/src/utils/databaseJudgments.ts @@ -26,6 +26,9 @@ const databaseJudgments: Record implementedBy: [DatabaseTypeCode.MYSQL], normalizeDatabaseType: true, }, + [DatabaseCapability.ACTIVE_TRANSACTION_INSPECTION]: { + implementedBy: [DatabaseTypeCode.MYSQL], + }, [DatabaseCapability.DATABASE_DELETE]: { implementedBy: [DatabaseTypeCode.MYSQL, DatabaseTypeCode.POSTGRESQL], }, diff --git a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/model/transaction/ActiveTransaction.java b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/model/transaction/ActiveTransaction.java new file mode 100644 index 0000000000..0caf3d4dc4 --- /dev/null +++ b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/model/transaction/ActiveTransaction.java @@ -0,0 +1,75 @@ +package ai.chat2db.community.domain.api.model.transaction; + +import lombok.Data; + +import java.sql.Timestamp; + +@Data +public class ActiveTransaction { + + public enum QueryState { + VISIBLE, + UNAVAILABLE + } + + public enum SessionState { + LIVE, + DISAPPEARED_OR_HIDDEN + } + + public enum LockMetadataState { + AVAILABLE, + UNAVAILABLE + } + + public enum LockMetadataSource { + MYSQL_80_PERFORMANCE_SCHEMA, + MYSQL_57_INFORMATION_SCHEMA + } + + private String trxId; + private String state; + private Timestamp startedAt; + private Long ageSeconds; + private String isolationLevel; + private Long rowsLocked; + private Long rowsModified; + private Long lockStructs; + private Long threadId; + private String user; + private String host; + private String db; + private String query; + private QueryState queryState; + private Boolean sessionAvailable; + private SessionState sessionState; + private Boolean canOpenSession; + private String connectionInspectionSql; + private String waitingLockId; + private String blockingLockId; + private String blockingTrxId; + private Long waitingPerformanceSchemaThreadId; + private Long blockingPerformanceSchemaThreadId; + private Long blockingThreadId; + private Boolean blockingSessionAvailable; + private Boolean canOpenBlockingSession; + private String blockingConnectionInspectionSql; + private String blockingUser; + private String blockingHost; + private String blockingDb; + private String waitingObject; + private String waitingIndex; + private String waitingLockType; + private String waitingLockMode; + private String waitingLockStatus; + private String waitingLockData; + private String blockingObject; + private String blockingIndex; + private String blockingLockType; + private String blockingLockMode; + private String blockingLockStatus; + private String blockingLockData; + private Boolean lockWaitAvailable; + private LockMetadataState lockMetadataState; + private LockMetadataSource lockMetadataSource; +} diff --git a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/service/db/IDbActiveTransactionService.java b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/service/db/IDbActiveTransactionService.java index 5b4e91aed4..8e9507612f 100644 --- a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/service/db/IDbActiveTransactionService.java +++ b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/service/db/IDbActiveTransactionService.java @@ -1,7 +1,8 @@ package ai.chat2db.community.domain.api.service.db; +import ai.chat2db.community.domain.api.model.transaction.ActiveTransaction; + import java.util.List; -import java.util.Map; /** * Inspects active InnoDB transactions (MYSQL-OPS-002). Read-only; requires @@ -13,7 +14,7 @@ public interface IDbActiveTransactionService { * Lists active InnoDB transactions with their state, age, isolation level, lock * counters, owning thread, user, host, database, and current SQL (when visible). * - * @return a list of transaction maps, empty when no transaction is active. + * @return active transactions, empty when no transaction is active. */ - List> activeTransactions(); + List activeTransactions(); } diff --git a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java index befb3b2000..160044ccdb 100644 --- a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java +++ b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImpl.java @@ -1,354 +1,23 @@ package ai.chat2db.community.domain.core.impl.db; +import ai.chat2db.community.domain.api.model.transaction.ActiveTransaction; import ai.chat2db.community.domain.api.service.db.IDbActiveTransactionService; import ai.chat2db.community.tools.exception.BusinessException; -import ai.chat2db.spi.DefaultSQLExecutor; +import ai.chat2db.spi.IActiveTransactionManager; import ai.chat2db.spi.sql.Chat2DBContext; import org.springframework.stereotype.Service; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; @Service public class DbActiveTransactionServiceImpl implements IDbActiveTransactionService { - /** - * InnoDB transaction metadata joined with the owning processlist row. The - * transaction source is always {@code information_schema.innodb_trx}; each refresh is - * a new live snapshot and does not retain rows for transactions that have committed - * or rolled back. - */ - private static final String SQL_ACTIVE_TRANSACTIONS = - "SELECT t.trx_id, t.trx_state, t.trx_started, " - + "TIMESTAMPDIFF(SECOND, t.trx_started, NOW()) AS trx_age_seconds, " - + "t.trx_isolation_level, t.trx_rows_locked, t.trx_rows_modified, " - + "t.trx_lock_structs, t.trx_mysql_thread_id, " - + "p.ID AS process_id, p.USER AS process_user, p.HOST AS process_host, p.DB AS process_db, " - + "t.trx_query, " - + "%s " - + "FROM information_schema.innodb_trx t " - + "LEFT JOIN information_schema.processlist p ON t.trx_mysql_thread_id = p.ID " - + "%s " - + "ORDER BY t.trx_started"; - - private static final String LOCK_COLUMNS_80 = - "w.requesting_lock_id, w.blocking_lock_id, w.blocking_trx_id, " - + "w.requesting_ps_thread_id, w.blocking_ps_thread_id, " - + "w.waiting_object_schema, w.waiting_object_name, w.waiting_index_name, " - + "w.waiting_lock_type, w.waiting_lock_mode, w.waiting_lock_status, w.waiting_lock_data, " - + "w.blocking_object_schema, w.blocking_object_name, w.blocking_index_name, " - + "w.blocking_lock_type, w.blocking_lock_mode, w.blocking_lock_status, w.blocking_lock_data, " - + "bt.trx_mysql_thread_id AS blocking_thread_id, bp.ID AS blocking_process_id, " - + "bp.USER AS blocking_user, bp.HOST AS blocking_host, bp.DB AS blocking_db"; - - private static final String LOCK_JOIN_80 = - "LEFT JOIN (" - + "SELECT dlw.REQUESTING_ENGINE_TRANSACTION_ID AS requesting_trx_id, " - + "dlw.REQUESTING_ENGINE_LOCK_ID AS requesting_lock_id, " - + "dlw.BLOCKING_ENGINE_LOCK_ID AS blocking_lock_id, " - + "dlw.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx_id, " - + "dlw.REQUESTING_THREAD_ID AS requesting_ps_thread_id, " - + "dlw.BLOCKING_THREAD_ID AS blocking_ps_thread_id, " - + "rl.OBJECT_SCHEMA AS waiting_object_schema, rl.OBJECT_NAME AS waiting_object_name, " - + "rl.INDEX_NAME AS waiting_index_name, rl.LOCK_TYPE AS waiting_lock_type, " - + "rl.LOCK_MODE AS waiting_lock_mode, rl.LOCK_STATUS AS waiting_lock_status, " - + "rl.LOCK_DATA AS waiting_lock_data, " - + "bl.OBJECT_SCHEMA AS blocking_object_schema, bl.OBJECT_NAME AS blocking_object_name, " - + "bl.INDEX_NAME AS blocking_index_name, bl.LOCK_TYPE AS blocking_lock_type, " - + "bl.LOCK_MODE AS blocking_lock_mode, bl.LOCK_STATUS AS blocking_lock_status, " - + "bl.LOCK_DATA AS blocking_lock_data " - + "FROM performance_schema.data_lock_waits dlw " - + "LEFT JOIN performance_schema.data_locks rl " - + "ON dlw.REQUESTING_ENGINE_LOCK_ID = rl.ENGINE_LOCK_ID " - + "AND dlw.ENGINE = rl.ENGINE " - + "LEFT JOIN performance_schema.data_locks bl " - + "ON dlw.BLOCKING_ENGINE_LOCK_ID = bl.ENGINE_LOCK_ID " - + "AND dlw.ENGINE = bl.ENGINE" - + ") w ON w.requesting_trx_id = t.trx_id " - + "LEFT JOIN information_schema.innodb_trx bt ON bt.trx_id = w.blocking_trx_id " - + "LEFT JOIN information_schema.processlist bp ON bt.trx_mysql_thread_id = bp.ID"; - - private static final String LOCK_COLUMNS_57 = - "w.requesting_lock_id, w.blocking_lock_id, w.blocking_trx_id, " - + "NULL AS requesting_ps_thread_id, NULL AS blocking_ps_thread_id, " - + "NULL AS waiting_object_schema, w.waiting_object_name, w.waiting_index_name, " - + "w.waiting_lock_type, w.waiting_lock_mode, NULL AS waiting_lock_status, w.waiting_lock_data, " - + "NULL AS blocking_object_schema, w.blocking_object_name, w.blocking_index_name, " - + "w.blocking_lock_type, w.blocking_lock_mode, NULL AS blocking_lock_status, w.blocking_lock_data, " - + "bt.trx_mysql_thread_id AS blocking_thread_id, bp.ID AS blocking_process_id, " - + "bp.USER AS blocking_user, bp.HOST AS blocking_host, bp.DB AS blocking_db"; - - private static final String LOCK_JOIN_57 = - "LEFT JOIN (" - + "SELECT lw.requesting_trx_id, lw.requested_lock_id AS requesting_lock_id, " - + "lw.blocking_lock_id, lw.blocking_trx_id, " - + "rl.lock_table AS waiting_object_name, rl.lock_index AS waiting_index_name, " - + "rl.lock_type AS waiting_lock_type, rl.lock_mode AS waiting_lock_mode, " - + "rl.lock_data AS waiting_lock_data, " - + "bl.lock_table AS blocking_object_name, bl.lock_index AS blocking_index_name, " - + "bl.lock_type AS blocking_lock_type, bl.lock_mode AS blocking_lock_mode, " - + "bl.lock_data AS blocking_lock_data " - + "FROM information_schema.innodb_lock_waits lw " - + "LEFT JOIN information_schema.innodb_locks rl ON lw.requested_lock_id = rl.lock_id " - + "LEFT JOIN information_schema.innodb_locks bl ON lw.blocking_lock_id = bl.lock_id" - + ") w ON w.requesting_trx_id = t.trx_id " - + "LEFT JOIN information_schema.innodb_trx bt ON bt.trx_id = w.blocking_trx_id " - + "LEFT JOIN information_schema.processlist bp ON bt.trx_mysql_thread_id = bp.ID"; - - private static final String LOCK_COLUMNS_UNAVAILABLE = - "NULL AS requesting_lock_id, NULL AS blocking_lock_id, NULL AS blocking_trx_id, " - + "NULL AS requesting_ps_thread_id, NULL AS blocking_ps_thread_id, " - + "NULL AS waiting_object_schema, NULL AS waiting_object_name, NULL AS waiting_index_name, " - + "NULL AS waiting_lock_type, NULL AS waiting_lock_mode, NULL AS waiting_lock_status, " - + "NULL AS waiting_lock_data, " - + "NULL AS blocking_object_schema, NULL AS blocking_object_name, NULL AS blocking_index_name, " - + "NULL AS blocking_lock_type, NULL AS blocking_lock_mode, NULL AS blocking_lock_status, " - + "NULL AS blocking_lock_data, " - + "NULL AS blocking_thread_id, NULL AS blocking_process_id, " - + "NULL AS blocking_user, NULL AS blocking_host, NULL AS blocking_db"; - - private static final String SQL_PROBE_MYSQL80_LOCKS = - "SELECT 1 FROM performance_schema.data_lock_waits WHERE 1 = 0"; - - private static final String SQL_PROBE_MYSQL57_LOCKS = - "SELECT 1 FROM information_schema.innodb_lock_waits WHERE 1 = 0"; - - private static final String LOCK_METADATA_AVAILABLE = "AVAILABLE"; - private static final String LOCK_METADATA_UNAVAILABLE = "UNAVAILABLE"; - @Override - public List> activeTransactions() { - Connection connection = Chat2DBContext.getConnection(); - LockMetadataQuery lockMetadataQuery = resolveLockMetadataQuery(connection); - try { - return DefaultSQLExecutor.getInstance().execute(connection, lockMetadataQuery.sql(), resultSet -> { - List> transactions = new ArrayList<>(); - while (resultSet.next()) { - Map row = new LinkedHashMap<>(); - row.put("trxId", resultSet.getString("trx_id")); - row.put("state", resultSet.getString("trx_state")); - row.put("startedAt", resultSet.getTimestamp("trx_started")); - row.put("ageSeconds", resultSet.getLong("trx_age_seconds")); - row.put("isolationLevel", resultSet.getString("trx_isolation_level")); - row.put("rowsLocked", resultSet.getLong("trx_rows_locked")); - row.put("rowsModified", resultSet.getLong("trx_rows_modified")); - row.put("lockStructs", resultSet.getLong("trx_lock_structs")); - Long threadId = nullableLong(resultSet, "trx_mysql_thread_id"); - row.put("threadId", threadId); - row.put("user", resultSet.getString("process_user")); - row.put("host", resultSet.getString("process_host")); - row.put("db", resultSet.getString("process_db")); - String query = resultSet.getString("trx_query"); - row.put("query", query); - Long processId = nullableLong(resultSet, "process_id"); - row.put("sessionAvailable", processId != null); - row.put("sessionState", processId == null ? "DISAPPEARED_OR_HIDDEN" : "LIVE"); - row.put("canOpenSession", processId != null && threadId != null); - row.put("queryState", query == null ? "UNAVAILABLE" : "VISIBLE"); - row.put("lockMetadataState", lockMetadataQuery.available() - ? LOCK_METADATA_AVAILABLE : LOCK_METADATA_UNAVAILABLE); - row.put("lockMetadataSource", lockMetadataQuery.source()); - row.put("lockMetadataMessage", lockMetadataQuery.message()); - putLockWait(row, resultSet); - transactions.add(row); - } - return transactions; - }); - } catch (RuntimeException exception) { - if (lockMetadataQuery.available() && hasMissingOrDeniedMetadataError(exception)) { - return activeTransactionsWithoutLockMetadata(connection, metadataErrorMessage(exception)); - } - if (hasProcessPrivilegeError(exception)) { - throw new BusinessException("mysql.activeTransaction.processPrivilegeRequired", null, exception); - } - throw exception; - } - } - - private List> activeTransactionsWithoutLockMetadata(Connection connection, String message) { - LockMetadataQuery fallback = unavailableLockMetadataQuery(message); - try { - return DefaultSQLExecutor.getInstance().execute(connection, fallback.sql(), resultSet -> { - List> transactions = new ArrayList<>(); - while (resultSet.next()) { - Map row = new LinkedHashMap<>(); - row.put("trxId", resultSet.getString("trx_id")); - row.put("state", resultSet.getString("trx_state")); - row.put("startedAt", resultSet.getTimestamp("trx_started")); - row.put("ageSeconds", resultSet.getLong("trx_age_seconds")); - row.put("isolationLevel", resultSet.getString("trx_isolation_level")); - row.put("rowsLocked", resultSet.getLong("trx_rows_locked")); - row.put("rowsModified", resultSet.getLong("trx_rows_modified")); - row.put("lockStructs", resultSet.getLong("trx_lock_structs")); - Long threadId = nullableLong(resultSet, "trx_mysql_thread_id"); - row.put("threadId", threadId); - row.put("user", resultSet.getString("process_user")); - row.put("host", resultSet.getString("process_host")); - row.put("db", resultSet.getString("process_db")); - String query = resultSet.getString("trx_query"); - Long processId = nullableLong(resultSet, "process_id"); - row.put("query", query); - row.put("sessionAvailable", processId != null); - row.put("sessionState", processId == null ? "DISAPPEARED_OR_HIDDEN" : "LIVE"); - row.put("canOpenSession", processId != null && threadId != null); - row.put("queryState", query == null ? "UNAVAILABLE" : "VISIBLE"); - row.put("lockMetadataState", LOCK_METADATA_UNAVAILABLE); - row.put("lockMetadataSource", null); - row.put("lockMetadataMessage", fallback.message()); - putLockWait(row, resultSet); - transactions.add(row); - } - return transactions; - }); - } catch (RuntimeException exception) { - throw sanitizeActiveTransactionQueryException(exception); - } - } - - private static void putLockWait(Map row, ResultSet resultSet) throws SQLException { - Long blockingThreadId = nullableLong(resultSet, "blocking_thread_id"); - Long blockingProcessId = nullableLong(resultSet, "blocking_process_id"); - String waitingLockId = resultSet.getString("requesting_lock_id"); - String blockingLockId = resultSet.getString("blocking_lock_id"); - row.put("waitingLockId", waitingLockId); - row.put("blockingLockId", blockingLockId); - row.put("blockingTrxId", resultSet.getString("blocking_trx_id")); - row.put("waitingPerformanceSchemaThreadId", nullableLong(resultSet, "requesting_ps_thread_id")); - row.put("blockingPerformanceSchemaThreadId", nullableLong(resultSet, "blocking_ps_thread_id")); - row.put("blockingThreadId", blockingThreadId); - row.put("blockingSessionAvailable", blockingProcessId != null); - row.put("canOpenBlockingSession", blockingProcessId != null && blockingThreadId != null); - row.put("blockingUser", resultSet.getString("blocking_user")); - row.put("blockingHost", resultSet.getString("blocking_host")); - row.put("blockingDb", resultSet.getString("blocking_db")); - row.put("waitingObject", lockObject(resultSet, "waiting_object_schema", "waiting_object_name")); - row.put("waitingIndex", resultSet.getString("waiting_index_name")); - row.put("waitingLockType", resultSet.getString("waiting_lock_type")); - row.put("waitingLockMode", resultSet.getString("waiting_lock_mode")); - row.put("waitingLockStatus", resultSet.getString("waiting_lock_status")); - row.put("waitingLockData", resultSet.getString("waiting_lock_data")); - row.put("blockingObject", lockObject(resultSet, "blocking_object_schema", "blocking_object_name")); - row.put("blockingIndex", resultSet.getString("blocking_index_name")); - row.put("blockingLockType", resultSet.getString("blocking_lock_type")); - row.put("blockingLockMode", resultSet.getString("blocking_lock_mode")); - row.put("blockingLockStatus", resultSet.getString("blocking_lock_status")); - row.put("blockingLockData", resultSet.getString("blocking_lock_data")); - row.put("lockWaitAvailable", waitingLockId != null || blockingLockId != null); - } - - private static String lockObject(ResultSet resultSet, String schemaColumn, String objectColumn) throws SQLException { - String schema = resultSet.getString(schemaColumn); - String object = resultSet.getString(objectColumn); - if (schema == null || schema.isBlank()) { - return object; - } - if (object == null || object.isBlank()) { - return schema; - } - return schema + "." + object; - } - - private static Long nullableLong(ResultSet resultSet, String column) throws SQLException { - long value = resultSet.getLong(column); - return resultSet.wasNull() ? null : value; - } - - private static LockMetadataQuery resolveLockMetadataQuery(Connection connection) { - try { - executeProbe(connection, SQL_PROBE_MYSQL80_LOCKS); - return new LockMetadataQuery(activeTransactionSql(LOCK_COLUMNS_80, LOCK_JOIN_80), - true, "MYSQL_80_PERFORMANCE_SCHEMA", null); - } catch (RuntimeException exception) { - if (!hasMissingOrDeniedMetadataError(exception)) { - throw exception; - } + public List activeTransactions() { + IActiveTransactionManager manager = Chat2DBContext.getActiveTransactionManager(); + if (manager == null) { + throw new BusinessException("activeTransaction.inspection.unsupported"); } - - try { - executeProbe(connection, SQL_PROBE_MYSQL57_LOCKS); - return new LockMetadataQuery(activeTransactionSql(LOCK_COLUMNS_57, LOCK_JOIN_57), - true, "MYSQL_57_INFORMATION_SCHEMA", null); - } catch (RuntimeException exception) { - if (!hasMissingOrDeniedMetadataError(exception)) { - throw exception; - } - return unavailableLockMetadataQuery(metadataErrorMessage(exception)); - } - } - - private static void executeProbe(Connection connection, String sql) { - DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> null); - } - - static String activeTransactionSql(String lockColumns, String lockJoin) { - return String.format(SQL_ACTIVE_TRANSACTIONS, lockColumns, lockJoin); - } - - static LockMetadataQuery unavailableLockMetadataQuery(String message) { - return new LockMetadataQuery(activeTransactionSql(LOCK_COLUMNS_UNAVAILABLE, ""), - false, null, message); - } - - static boolean hasProcessPrivilegeError(Throwable throwable) { - Throwable current = throwable; - while (current != null) { - if (current instanceof SQLException sqlException - && (sqlException.getErrorCode() == 1227 - || sqlException.getMessage() != null - && sqlException.getMessage().toUpperCase().contains("PROCESS"))) { - return true; - } - current = current.getCause(); - } - return false; - } - - static boolean hasMissingOrDeniedMetadataError(Throwable throwable) { - Throwable current = throwable; - while (current != null) { - if (current instanceof SQLException sqlException) { - int errorCode = sqlException.getErrorCode(); - String message = sqlException.getMessage(); - String normalized = message == null ? "" : message.toUpperCase(); - if (errorCode == 1142 || errorCode == 1146 || errorCode == 1227 - || normalized.contains("DATA_LOCK") - || normalized.contains("INNODB_LOCK") - || normalized.contains("PERFORMANCE_SCHEMA") - || normalized.contains("COMMAND DENIED") - || normalized.contains("DOESN'T EXIST") - || normalized.contains("DOES NOT EXIST") - || normalized.contains("PROCESS")) { - return true; - } - } - current = current.getCause(); - } - return false; - } - - static String metadataErrorMessage(Throwable throwable) { - Throwable current = throwable; - while (current != null) { - if (current instanceof SQLException) { - return "Lock wait metadata is unavailable or access was denied."; - } - current = current.getCause(); - } - return "Lock wait metadata is not available for this MySQL version or account."; - } - - static RuntimeException sanitizeActiveTransactionQueryException(RuntimeException exception) { - if (hasProcessPrivilegeError(exception)) { - return new BusinessException("mysql.activeTransaction.processPrivilegeRequired", null, exception); - } - return exception; - } - - record LockMetadataQuery(String sql, boolean available, String source, String message) { + return manager.activeTransactions(Chat2DBContext.getConnection()); } } diff --git a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/test/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImplTest.java b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/test/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImplTest.java index e81d444007..ac69650a6f 100644 --- a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/test/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImplTest.java +++ b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/test/java/ai/chat2db/community/domain/core/impl/db/DbActiveTransactionServiceImplTest.java @@ -1,90 +1,98 @@ package ai.chat2db.community.domain.core.impl.db; +import ai.chat2db.community.domain.api.config.DBConfig; +import ai.chat2db.community.domain.api.model.transaction.ActiveTransaction; import ai.chat2db.community.tools.exception.BusinessException; +import ai.chat2db.spi.IActiveTransactionManager; +import ai.chat2db.spi.IPlugin; +import ai.chat2db.spi.model.datasource.ConnectInfo; +import ai.chat2db.spi.sql.Chat2DBContext; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import java.sql.SQLException; +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class DbActiveTransactionServiceImplTest { - @Test - void recognizesProcessPrivilegeFailuresThroughWrappedCauses() { - SQLException denied = new SQLException( - "Access denied; you need (at least one of) the PROCESS privilege", "42000", 1227); + private static final String DB_TYPE = "ACTIVE_TRANSACTION_TEST"; - assertTrue(DbActiveTransactionServiceImpl.hasProcessPrivilegeError( - new IllegalStateException("execution failed", denied))); - assertFalse(DbActiveTransactionServiceImpl.hasProcessPrivilegeError( - new SQLException("table not found", "42S02", 1146))); + @AfterEach + void tearDown() { + Chat2DBContext.removeContext(); + Chat2DBContext.PLUGIN_MAP.remove(DB_TYPE); } @Test - void buildsLiveTransactionQueryWithMySql80LockWaitMetadata() { - String sql = DbActiveTransactionServiceImpl.activeTransactionSql("w.requesting_lock_id", "JOIN wait_table w"); - - assertTrue(sql.contains("FROM information_schema.innodb_trx t")); - assertTrue(sql.contains("LEFT JOIN information_schema.processlist p ON t.trx_mysql_thread_id = p.ID")); - assertTrue(sql.contains("w.requesting_lock_id")); - assertTrue(sql.contains("JOIN wait_table w")); - assertTrue(sql.contains("ORDER BY t.trx_started")); - } + void delegatesInspectionToCurrentDatabasePlugin() { + AtomicBoolean called = new AtomicBoolean(); + ActiveTransaction expected = new ActiveTransaction(); + expected.setTrxId("421337"); + bindContext(new IActiveTransactionManager() { + @Override + public List activeTransactions(Connection connection) { + called.set(true); + assertNotNull(connection); + return List.of(expected); + } + }); - @Test - void unavailableLockMetadataQueryKeepsLiveTransactionSource() { - DbActiveTransactionServiceImpl.LockMetadataQuery query = - DbActiveTransactionServiceImpl.unavailableLockMetadataQuery("missing metadata"); + List transactions = new DbActiveTransactionServiceImpl().activeTransactions(); - assertFalse(query.available()); - assertTrue(query.sql().contains("FROM information_schema.innodb_trx t")); - assertTrue(query.sql().contains("NULL AS blocking_trx_id")); - assertTrue(query.sql().contains("LEFT JOIN information_schema.processlist p")); - assertTrue(query.sql().contains("ORDER BY t.trx_started")); + assertTrue(called.get()); + assertEquals(List.of(expected), transactions); } @Test - void recognizesMissingOrDeniedLockMetadataFailures() { - assertTrue(DbActiveTransactionServiceImpl.hasMissingOrDeniedMetadataError( - new SQLException("Table 'performance_schema.data_lock_waits' doesn't exist", "42S02", 1146))); - assertTrue(DbActiveTransactionServiceImpl.hasMissingOrDeniedMetadataError( - new SQLException("SELECT command denied to user for table 'data_locks'", "42000", 1142))); - assertFalse(DbActiveTransactionServiceImpl.hasMissingOrDeniedMetadataError( - new SQLException("Syntax error", "42000", 1064))); - } + void rejectsPluginWithoutActiveTransactionCapability() { + bindContext(null); - @Test - void lockMetadataDiagnosticDoesNotExposeJdbcDetails() { - String message = DbActiveTransactionServiceImpl.metadataErrorMessage( - new SQLException("SELECT denied for private_schema.data_locks", "42000", 1142)); + BusinessException exception = assertThrows(BusinessException.class, + () -> new DbActiveTransactionServiceImpl().activeTransactions()); - assertTrue(message.contains("unavailable or access was denied")); - assertFalse(message.contains("private_schema")); + assertEquals("activeTransaction.inspection.unsupported", exception.getCode()); } - @Test - void fallbackProcessPrivilegeFailureUsesSanitizedBusinessCode() { - RuntimeException executionFailure = new IllegalStateException("execution failed", - new SQLException("Access denied; you need (at least one of) the PROCESS privilege", "42000", 1227)); - - BusinessException exception = assertThrows(BusinessException.class, () -> { - throw DbActiveTransactionServiceImpl.sanitizeActiveTransactionQueryException(executionFailure); + private static void bindContext(IActiveTransactionManager manager) { + Chat2DBContext.PLUGIN_MAP.put(DB_TYPE, new IPlugin() { + @Override + public DBConfig getDBConfig() { + DBConfig config = new DBConfig(); + config.setDbType(DB_TYPE); + return config; + } + + @Override + public IActiveTransactionManager getActiveTransactionManager() { + return manager; + } }); - - assertEquals("mysql.activeTransaction.processPrivilegeRequired", exception.getCode()); - assertSame(executionFailure, exception.getCause()); - } - - @Test - void nonProcessFallbackFailureRemainsOriginalRuntimeException() { - RuntimeException executionFailure = new IllegalStateException("syntax error", - new SQLException("Syntax error", "42000", 1064)); - - assertSame(executionFailure, - DbActiveTransactionServiceImpl.sanitizeActiveTransactionQueryException(executionFailure)); + Connection connection = (Connection) Proxy.newProxyInstance( + DbActiveTransactionServiceImplTest.class.getClassLoader(), + new Class[]{Connection.class}, + (proxy, method, args) -> { + if ("isClosed".equals(method.getName())) { + return false; + } + if ("close".equals(method.getName())) { + return null; + } + if (method.getReturnType() == boolean.class) { + return false; + } + return null; + }); + ConnectInfo connectInfo = new ConnectInfo(); + connectInfo.setDataSourceId(42L); + connectInfo.setDbType(DB_TYPE); + connectInfo.setConnection(connection); + Chat2DBContext.putContext(connectInfo); } } diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages.properties b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages.properties index f5d038af90..a5a9ce982e 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages.properties +++ b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages.properties @@ -17,3 +17,5 @@ menu.help.viewChangelog=View Changelog fileChooser.select.dir.title=Please select a directory fileChooser.select.file.title=Please select a save location +mysql.activeTransaction.processPrivilegeRequired=PROCESS privilege is required to inspect active InnoDB transactions +activeTransaction.inspection.unsupported=The current datasource does not support active transaction inspection diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_en.properties b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_en.properties index f5d038af90..a5a9ce982e 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_en.properties +++ b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_en.properties @@ -17,3 +17,5 @@ menu.help.viewChangelog=View Changelog fileChooser.select.dir.title=Please select a directory fileChooser.select.file.title=Please select a save location +mysql.activeTransaction.processPrivilegeRequired=PROCESS privilege is required to inspect active InnoDB transactions +activeTransaction.inspection.unsupported=The current datasource does not support active transaction inspection diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_en_US.properties b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_en_US.properties index f5d038af90..a5a9ce982e 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_en_US.properties +++ b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_en_US.properties @@ -17,3 +17,5 @@ menu.help.viewChangelog=View Changelog fileChooser.select.dir.title=Please select a directory fileChooser.select.file.title=Please select a save location +mysql.activeTransaction.processPrivilegeRequired=PROCESS privilege is required to inspect active InnoDB transactions +activeTransaction.inspection.unsupported=The current datasource does not support active transaction inspection diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_es.properties b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_es.properties index 78e9b5df84..bfd095d4c1 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_es.properties +++ b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_es.properties @@ -17,3 +17,5 @@ menu.help.viewChangelog=Ver registro de cambios fileChooser.select.dir.title=Seleccione un directorio fileChooser.select.file.title=Seleccione una ubicación para guardar +mysql.activeTransaction.processPrivilegeRequired=Se requiere el privilegio PROCESS para inspeccionar transacciones InnoDB activas +activeTransaction.inspection.unsupported=La fuente de datos actual no admite la inspección de transacciones activas diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_es_ES.properties b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_es_ES.properties index 78e9b5df84..bfd095d4c1 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_es_ES.properties +++ b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_es_ES.properties @@ -17,3 +17,5 @@ menu.help.viewChangelog=Ver registro de cambios fileChooser.select.dir.title=Seleccione un directorio fileChooser.select.file.title=Seleccione una ubicación para guardar +mysql.activeTransaction.processPrivilegeRequired=Se requiere el privilegio PROCESS para inspeccionar transacciones InnoDB activas +activeTransaction.inspection.unsupported=La fuente de datos actual no admite la inspección de transacciones activas diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ja.properties b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ja.properties index 438ba8b068..3f8aa3ec75 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ja.properties +++ b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ja.properties @@ -17,3 +17,5 @@ menu.help.viewChangelog=変更履歴を表示 fileChooser.select.dir.title=ディレクトリを選択してください fileChooser.select.file.title=保存先を選択してください +mysql.activeTransaction.processPrivilegeRequired=アクティブな InnoDB トランザクションの確認には PROCESS 権限が必要です +activeTransaction.inspection.unsupported=現在のデータソースはアクティブトランザクションの確認に対応していません diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ja_JP.properties b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ja_JP.properties index 438ba8b068..3f8aa3ec75 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ja_JP.properties +++ b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ja_JP.properties @@ -17,3 +17,5 @@ menu.help.viewChangelog=変更履歴を表示 fileChooser.select.dir.title=ディレクトリを選択してください fileChooser.select.file.title=保存先を選択してください +mysql.activeTransaction.processPrivilegeRequired=アクティブな InnoDB トランザクションの確認には PROCESS 権限が必要です +activeTransaction.inspection.unsupported=現在のデータソースはアクティブトランザクションの確認に対応していません diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ko.properties b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ko.properties index 649b421d70..c3f91b7274 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ko.properties +++ b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ko.properties @@ -17,3 +17,5 @@ menu.help.viewChangelog=변경 로그 보기 fileChooser.select.dir.title=디렉터리를 선택해 주세요 fileChooser.select.file.title=저장 위치를 선택해 주세요 +mysql.activeTransaction.processPrivilegeRequired=활성 InnoDB 트랜잭션을 확인하려면 PROCESS 권한이 필요합니다 +activeTransaction.inspection.unsupported=현재 데이터 소스는 활성 트랜잭션 검사를 지원하지 않습니다 diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ko_KR.properties b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ko_KR.properties index 649b421d70..c3f91b7274 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ko_KR.properties +++ b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_ko_KR.properties @@ -17,3 +17,5 @@ menu.help.viewChangelog=변경 로그 보기 fileChooser.select.dir.title=디렉터리를 선택해 주세요 fileChooser.select.file.title=저장 위치를 선택해 주세요 +mysql.activeTransaction.processPrivilegeRequired=활성 InnoDB 트랜잭션을 확인하려면 PROCESS 권한이 필요합니다 +activeTransaction.inspection.unsupported=현재 데이터 소스는 활성 트랜잭션 검사를 지원하지 않습니다 diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh.properties b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh.properties index b83391d90a..33bda36398 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh.properties +++ b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh.properties @@ -17,3 +17,5 @@ menu.help.viewChangelog=查看更改日志 fileChooser.select.dir.title=请选择一个目录 fileChooser.select.file.title=请选择保存位置 +mysql.activeTransaction.processPrivilegeRequired=查看活动 InnoDB 事务需要 PROCESS 权限 +activeTransaction.inspection.unsupported=当前数据源不支持活动事务检查 diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh_CN.properties b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh_CN.properties index b83391d90a..33bda36398 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh_CN.properties +++ b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh_CN.properties @@ -17,3 +17,5 @@ menu.help.viewChangelog=查看更改日志 fileChooser.select.dir.title=请选择一个目录 fileChooser.select.file.title=请选择保存位置 +mysql.activeTransaction.processPrivilegeRequired=查看活动 InnoDB 事务需要 PROCESS 权限 +activeTransaction.inspection.unsupported=当前数据源不支持活动事务检查 diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh_Hans.properties b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh_Hans.properties index b83391d90a..33bda36398 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh_Hans.properties +++ b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh_Hans.properties @@ -17,3 +17,5 @@ menu.help.viewChangelog=查看更改日志 fileChooser.select.dir.title=请选择一个目录 fileChooser.select.file.title=请选择保存位置 +mysql.activeTransaction.processPrivilegeRequired=查看活动 InnoDB 事务需要 PROCESS 权限 +activeTransaction.inspection.unsupported=当前数据源不支持活动事务检查 diff --git a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh_Hans_CN.properties b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh_Hans_CN.properties index b83391d90a..33bda36398 100644 --- a/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh_Hans_CN.properties +++ b/chat2db-community-server/chat2db-community-jcef/src/main/resources/i18n/messages_zh_Hans_CN.properties @@ -17,3 +17,5 @@ menu.help.viewChangelog=查看更改日志 fileChooser.select.dir.title=请选择一个目录 fileChooser.select.file.title=请选择保存位置 +mysql.activeTransaction.processPrivilegeRequired=查看活动 InnoDB 事务需要 PROCESS 权限 +activeTransaction.inspection.unsupported=当前数据源不支持活动事务检查 diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlActiveTransactionManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlActiveTransactionManager.java new file mode 100644 index 0000000000..8d15b0a787 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlActiveTransactionManager.java @@ -0,0 +1,331 @@ +package ai.chat2db.plugin.mysql; + +import ai.chat2db.community.domain.api.model.transaction.ActiveTransaction; +import ai.chat2db.community.domain.api.model.transaction.ActiveTransaction.LockMetadataSource; +import ai.chat2db.community.domain.api.model.transaction.ActiveTransaction.LockMetadataState; +import ai.chat2db.community.domain.api.model.transaction.ActiveTransaction.QueryState; +import ai.chat2db.community.domain.api.model.transaction.ActiveTransaction.SessionState; +import ai.chat2db.community.tools.exception.BusinessException; +import ai.chat2db.spi.DefaultSQLExecutor; +import ai.chat2db.spi.IActiveTransactionManager; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class MysqlActiveTransactionManager implements IActiveTransactionManager { + + /** + * InnoDB transaction metadata joined with the owning processlist row. The + * transaction source is always {@code information_schema.innodb_trx}; each refresh is + * a new live snapshot and does not retain rows for transactions that have committed + * or rolled back. + */ + private static final String SQL_ACTIVE_TRANSACTIONS = + "SELECT t.trx_id, t.trx_state, t.trx_started, " + + "TIMESTAMPDIFF(SECOND, t.trx_started, NOW()) AS trx_age_seconds, " + + "t.trx_isolation_level, t.trx_rows_locked, t.trx_rows_modified, " + + "t.trx_lock_structs, t.trx_mysql_thread_id, " + + "p.ID AS process_id, p.USER AS process_user, p.HOST AS process_host, p.DB AS process_db, " + + "t.trx_query, " + + "%s " + + "FROM information_schema.innodb_trx t " + + "LEFT JOIN information_schema.processlist p ON t.trx_mysql_thread_id = p.ID " + + "%s " + + "ORDER BY t.trx_started"; + + private static final String LOCK_COLUMNS_80 = + "w.requesting_lock_id, w.blocking_lock_id, w.blocking_trx_id, " + + "w.requesting_ps_thread_id, w.blocking_ps_thread_id, " + + "w.waiting_object_schema, w.waiting_object_name, w.waiting_index_name, " + + "w.waiting_lock_type, w.waiting_lock_mode, w.waiting_lock_status, w.waiting_lock_data, " + + "w.blocking_object_schema, w.blocking_object_name, w.blocking_index_name, " + + "w.blocking_lock_type, w.blocking_lock_mode, w.blocking_lock_status, w.blocking_lock_data, " + + "bt.trx_mysql_thread_id AS blocking_thread_id, bp.ID AS blocking_process_id, " + + "bp.USER AS blocking_user, bp.HOST AS blocking_host, bp.DB AS blocking_db"; + + private static final String LOCK_JOIN_80 = + "LEFT JOIN (" + + "SELECT dlw.REQUESTING_ENGINE_TRANSACTION_ID AS requesting_trx_id, " + + "dlw.REQUESTING_ENGINE_LOCK_ID AS requesting_lock_id, " + + "dlw.BLOCKING_ENGINE_LOCK_ID AS blocking_lock_id, " + + "dlw.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx_id, " + + "dlw.REQUESTING_THREAD_ID AS requesting_ps_thread_id, " + + "dlw.BLOCKING_THREAD_ID AS blocking_ps_thread_id, " + + "rl.OBJECT_SCHEMA AS waiting_object_schema, rl.OBJECT_NAME AS waiting_object_name, " + + "rl.INDEX_NAME AS waiting_index_name, rl.LOCK_TYPE AS waiting_lock_type, " + + "rl.LOCK_MODE AS waiting_lock_mode, rl.LOCK_STATUS AS waiting_lock_status, " + + "rl.LOCK_DATA AS waiting_lock_data, " + + "bl.OBJECT_SCHEMA AS blocking_object_schema, bl.OBJECT_NAME AS blocking_object_name, " + + "bl.INDEX_NAME AS blocking_index_name, bl.LOCK_TYPE AS blocking_lock_type, " + + "bl.LOCK_MODE AS blocking_lock_mode, bl.LOCK_STATUS AS blocking_lock_status, " + + "bl.LOCK_DATA AS blocking_lock_data " + + "FROM performance_schema.data_lock_waits dlw " + + "LEFT JOIN performance_schema.data_locks rl " + + "ON dlw.REQUESTING_ENGINE_LOCK_ID = rl.ENGINE_LOCK_ID " + + "AND dlw.ENGINE = rl.ENGINE " + + "LEFT JOIN performance_schema.data_locks bl " + + "ON dlw.BLOCKING_ENGINE_LOCK_ID = bl.ENGINE_LOCK_ID " + + "AND dlw.ENGINE = bl.ENGINE" + + ") w ON w.requesting_trx_id = t.trx_id " + + "LEFT JOIN information_schema.innodb_trx bt ON bt.trx_id = w.blocking_trx_id " + + "LEFT JOIN information_schema.processlist bp ON bt.trx_mysql_thread_id = bp.ID"; + + private static final String LOCK_COLUMNS_57 = + "w.requesting_lock_id, w.blocking_lock_id, w.blocking_trx_id, " + + "NULL AS requesting_ps_thread_id, NULL AS blocking_ps_thread_id, " + + "NULL AS waiting_object_schema, w.waiting_object_name, w.waiting_index_name, " + + "w.waiting_lock_type, w.waiting_lock_mode, NULL AS waiting_lock_status, w.waiting_lock_data, " + + "NULL AS blocking_object_schema, w.blocking_object_name, w.blocking_index_name, " + + "w.blocking_lock_type, w.blocking_lock_mode, NULL AS blocking_lock_status, w.blocking_lock_data, " + + "bt.trx_mysql_thread_id AS blocking_thread_id, bp.ID AS blocking_process_id, " + + "bp.USER AS blocking_user, bp.HOST AS blocking_host, bp.DB AS blocking_db"; + + private static final String LOCK_JOIN_57 = + "LEFT JOIN (" + + "SELECT lw.requesting_trx_id, lw.requested_lock_id AS requesting_lock_id, " + + "lw.blocking_lock_id, lw.blocking_trx_id, " + + "rl.lock_table AS waiting_object_name, rl.lock_index AS waiting_index_name, " + + "rl.lock_type AS waiting_lock_type, rl.lock_mode AS waiting_lock_mode, " + + "rl.lock_data AS waiting_lock_data, " + + "bl.lock_table AS blocking_object_name, bl.lock_index AS blocking_index_name, " + + "bl.lock_type AS blocking_lock_type, bl.lock_mode AS blocking_lock_mode, " + + "bl.lock_data AS blocking_lock_data " + + "FROM information_schema.innodb_lock_waits lw " + + "LEFT JOIN information_schema.innodb_locks rl ON lw.requested_lock_id = rl.lock_id " + + "LEFT JOIN information_schema.innodb_locks bl ON lw.blocking_lock_id = bl.lock_id" + + ") w ON w.requesting_trx_id = t.trx_id " + + "LEFT JOIN information_schema.innodb_trx bt ON bt.trx_id = w.blocking_trx_id " + + "LEFT JOIN information_schema.processlist bp ON bt.trx_mysql_thread_id = bp.ID"; + + private static final String LOCK_COLUMNS_UNAVAILABLE = + "NULL AS requesting_lock_id, NULL AS blocking_lock_id, NULL AS blocking_trx_id, " + + "NULL AS requesting_ps_thread_id, NULL AS blocking_ps_thread_id, " + + "NULL AS waiting_object_schema, NULL AS waiting_object_name, NULL AS waiting_index_name, " + + "NULL AS waiting_lock_type, NULL AS waiting_lock_mode, NULL AS waiting_lock_status, " + + "NULL AS waiting_lock_data, " + + "NULL AS blocking_object_schema, NULL AS blocking_object_name, NULL AS blocking_index_name, " + + "NULL AS blocking_lock_type, NULL AS blocking_lock_mode, NULL AS blocking_lock_status, " + + "NULL AS blocking_lock_data, " + + "NULL AS blocking_thread_id, NULL AS blocking_process_id, " + + "NULL AS blocking_user, NULL AS blocking_host, NULL AS blocking_db"; + + private static final String SQL_PROBE_MYSQL80_LOCKS = + "SELECT 1 FROM performance_schema.data_lock_waits WHERE 1 = 0"; + + private static final String SQL_PROBE_MYSQL57_LOCKS = + "SELECT 1 FROM information_schema.innodb_lock_waits WHERE 1 = 0"; + + private static final String SQL_CONNECTION_INSPECTION = + "SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO\n" + + "FROM information_schema.PROCESSLIST\n" + + "WHERE ID = %d;"; + + static final String PROCESS_PRIVILEGE_ERROR_CODE = "mysql.activeTransaction.processPrivilegeRequired"; + + @Override + public List activeTransactions(Connection connection) { + LockMetadataQuery lockMetadataQuery = resolveLockMetadataQuery(connection); + try { + return executeActiveTransactions(connection, lockMetadataQuery); + } catch (RuntimeException exception) { + if (lockMetadataQuery.available() && hasMissingOrDeniedMetadataError(exception)) { + return activeTransactionsWithoutLockMetadata(connection); + } + throw sanitizeActiveTransactionQueryException(exception); + } + } + + private List activeTransactionsWithoutLockMetadata(Connection connection) { + try { + return executeActiveTransactions(connection, unavailableLockMetadataQuery()); + } catch (RuntimeException exception) { + throw sanitizeActiveTransactionQueryException(exception); + } + } + + private static List executeActiveTransactions( + Connection connection, + LockMetadataQuery lockMetadataQuery + ) { + return DefaultSQLExecutor.getInstance().execute(connection, lockMetadataQuery.sql(), resultSet -> { + List transactions = new ArrayList<>(); + while (resultSet.next()) { + transactions.add(readTransaction(resultSet, lockMetadataQuery)); + } + return transactions; + }); + } + + static ActiveTransaction readTransaction(ResultSet resultSet, LockMetadataQuery lockMetadataQuery) + throws SQLException { + ActiveTransaction transaction = new ActiveTransaction(); + transaction.setTrxId(resultSet.getString("trx_id")); + transaction.setState(resultSet.getString("trx_state")); + transaction.setStartedAt(resultSet.getTimestamp("trx_started")); + transaction.setAgeSeconds(nullableLong(resultSet, "trx_age_seconds")); + transaction.setIsolationLevel(resultSet.getString("trx_isolation_level")); + transaction.setRowsLocked(nullableLong(resultSet, "trx_rows_locked")); + transaction.setRowsModified(nullableLong(resultSet, "trx_rows_modified")); + transaction.setLockStructs(nullableLong(resultSet, "trx_lock_structs")); + Long threadId = nullableLong(resultSet, "trx_mysql_thread_id"); + transaction.setThreadId(threadId); + transaction.setUser(resultSet.getString("process_user")); + transaction.setHost(resultSet.getString("process_host")); + transaction.setDb(resultSet.getString("process_db")); + String query = resultSet.getString("trx_query"); + transaction.setQuery(query); + Long processId = nullableLong(resultSet, "process_id"); + transaction.setSessionAvailable(processId != null); + transaction.setSessionState(processId == null ? SessionState.DISAPPEARED_OR_HIDDEN : SessionState.LIVE); + boolean canOpenSession = processId != null && threadId != null; + transaction.setCanOpenSession(canOpenSession); + transaction.setConnectionInspectionSql(canOpenSession ? connectionInspectionSql(threadId) : null); + transaction.setQueryState(query == null ? QueryState.UNAVAILABLE : QueryState.VISIBLE); + transaction.setLockMetadataState(lockMetadataQuery.available() + ? LockMetadataState.AVAILABLE : LockMetadataState.UNAVAILABLE); + transaction.setLockMetadataSource(lockMetadataQuery.source()); + putLockWait(transaction, resultSet); + return transaction; + } + + private static void putLockWait(ActiveTransaction transaction, ResultSet resultSet) throws SQLException { + Long blockingThreadId = nullableLong(resultSet, "blocking_thread_id"); + Long blockingProcessId = nullableLong(resultSet, "blocking_process_id"); + String waitingLockId = resultSet.getString("requesting_lock_id"); + String blockingLockId = resultSet.getString("blocking_lock_id"); + transaction.setWaitingLockId(waitingLockId); + transaction.setBlockingLockId(blockingLockId); + transaction.setBlockingTrxId(resultSet.getString("blocking_trx_id")); + transaction.setWaitingPerformanceSchemaThreadId(nullableLong(resultSet, "requesting_ps_thread_id")); + transaction.setBlockingPerformanceSchemaThreadId(nullableLong(resultSet, "blocking_ps_thread_id")); + transaction.setBlockingThreadId(blockingThreadId); + transaction.setBlockingSessionAvailable(blockingProcessId != null); + boolean canOpenBlockingSession = blockingProcessId != null && blockingThreadId != null; + transaction.setCanOpenBlockingSession(canOpenBlockingSession); + transaction.setBlockingConnectionInspectionSql( + canOpenBlockingSession ? connectionInspectionSql(blockingThreadId) : null); + transaction.setBlockingUser(resultSet.getString("blocking_user")); + transaction.setBlockingHost(resultSet.getString("blocking_host")); + transaction.setBlockingDb(resultSet.getString("blocking_db")); + transaction.setWaitingObject(lockObject(resultSet, "waiting_object_schema", "waiting_object_name")); + transaction.setWaitingIndex(resultSet.getString("waiting_index_name")); + transaction.setWaitingLockType(resultSet.getString("waiting_lock_type")); + transaction.setWaitingLockMode(resultSet.getString("waiting_lock_mode")); + transaction.setWaitingLockStatus(resultSet.getString("waiting_lock_status")); + transaction.setWaitingLockData(resultSet.getString("waiting_lock_data")); + transaction.setBlockingObject(lockObject(resultSet, "blocking_object_schema", "blocking_object_name")); + transaction.setBlockingIndex(resultSet.getString("blocking_index_name")); + transaction.setBlockingLockType(resultSet.getString("blocking_lock_type")); + transaction.setBlockingLockMode(resultSet.getString("blocking_lock_mode")); + transaction.setBlockingLockStatus(resultSet.getString("blocking_lock_status")); + transaction.setBlockingLockData(resultSet.getString("blocking_lock_data")); + transaction.setLockWaitAvailable(waitingLockId != null || blockingLockId != null); + } + + private static String lockObject(ResultSet resultSet, String schemaColumn, String objectColumn) throws SQLException { + String schema = resultSet.getString(schemaColumn); + String object = resultSet.getString(objectColumn); + if (schema == null || schema.isBlank()) { + return object; + } + if (object == null || object.isBlank()) { + return schema; + } + return schema + "." + object; + } + + private static Long nullableLong(ResultSet resultSet, String column) throws SQLException { + long value = resultSet.getLong(column); + return resultSet.wasNull() ? null : value; + } + + static String connectionInspectionSql(Long connectionId) { + return String.format(SQL_CONNECTION_INSPECTION, connectionId); + } + + private static LockMetadataQuery resolveLockMetadataQuery(Connection connection) { + try { + executeProbe(connection, SQL_PROBE_MYSQL80_LOCKS); + return new LockMetadataQuery(activeTransactionSql(LOCK_COLUMNS_80, LOCK_JOIN_80), + true, LockMetadataSource.MYSQL_80_PERFORMANCE_SCHEMA); + } catch (RuntimeException exception) { + if (!hasMissingOrDeniedMetadataError(exception)) { + throw exception; + } + } + + try { + executeProbe(connection, SQL_PROBE_MYSQL57_LOCKS); + return new LockMetadataQuery(activeTransactionSql(LOCK_COLUMNS_57, LOCK_JOIN_57), + true, LockMetadataSource.MYSQL_57_INFORMATION_SCHEMA); + } catch (RuntimeException exception) { + if (!hasMissingOrDeniedMetadataError(exception)) { + throw exception; + } + return unavailableLockMetadataQuery(); + } + } + + private static void executeProbe(Connection connection, String sql) { + DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> null); + } + + static String activeTransactionSql(String lockColumns, String lockJoin) { + return String.format(SQL_ACTIVE_TRANSACTIONS, lockColumns, lockJoin); + } + + static LockMetadataQuery unavailableLockMetadataQuery() { + return new LockMetadataQuery(activeTransactionSql(LOCK_COLUMNS_UNAVAILABLE, ""), + false, null); + } + + static boolean hasProcessPrivilegeError(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof SQLException sqlException + && (sqlException.getErrorCode() == 1227 + || sqlException.getMessage() != null + && sqlException.getMessage().toUpperCase().contains("PROCESS"))) { + return true; + } + current = current.getCause(); + } + return false; + } + + static boolean hasMissingOrDeniedMetadataError(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof SQLException sqlException) { + int errorCode = sqlException.getErrorCode(); + String message = sqlException.getMessage(); + String normalized = message == null ? "" : message.toUpperCase(); + if (errorCode == 1142 || errorCode == 1146 || errorCode == 1227 + || normalized.contains("DATA_LOCK") + || normalized.contains("INNODB_LOCK") + || normalized.contains("PERFORMANCE_SCHEMA") + || normalized.contains("COMMAND DENIED") + || normalized.contains("DOESN'T EXIST") + || normalized.contains("DOES NOT EXIST") + || normalized.contains("PROCESS")) { + return true; + } + } + current = current.getCause(); + } + return false; + } + + static RuntimeException sanitizeActiveTransactionQueryException(RuntimeException exception) { + if (hasProcessPrivilegeError(exception)) { + return new BusinessException(PROCESS_PRIVILEGE_ERROR_CODE, null, exception); + } + return exception; + } + + record LockMetadataQuery(String sql, boolean available, LockMetadataSource source) { + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlPlugin.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlPlugin.java index ed966c07b6..782c407062 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlPlugin.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlPlugin.java @@ -4,6 +4,7 @@ import ai.chat2db.community.domain.api.model.metadata.TableColumn; import ai.chat2db.community.domain.api.model.result.ResultSetEditorMetadata; import ai.chat2db.spi.IAccountManager; +import ai.chat2db.spi.IActiveTransactionManager; import ai.chat2db.spi.IDbManager; import ai.chat2db.spi.IDbMetaData; import ai.chat2db.spi.IPlugin; @@ -44,6 +45,11 @@ public IRoutineManager getRoutineManager() { return new MysqlRoutineManager(); } + @Override + public IActiveTransactionManager getActiveTransactionManager() { + return new MysqlActiveTransactionManager(); + } + private static final class NativeMysqlMetaData extends MysqlMetaData { @Override diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlActiveTransactionManagerTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlActiveTransactionManagerTest.java new file mode 100644 index 0000000000..cffabd53b5 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlActiveTransactionManagerTest.java @@ -0,0 +1,181 @@ +package ai.chat2db.plugin.mysql; + +import ai.chat2db.community.domain.api.model.transaction.ActiveTransaction; +import ai.chat2db.community.domain.api.model.transaction.ActiveTransaction.LockMetadataSource; +import ai.chat2db.community.domain.api.model.transaction.ActiveTransaction.LockMetadataState; +import ai.chat2db.community.domain.api.model.transaction.ActiveTransaction.QueryState; +import ai.chat2db.community.tools.exception.BusinessException; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Proxy; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class MysqlActiveTransactionManagerTest { + + @Test + void recognizesProcessPrivilegeFailuresThroughWrappedCauses() { + SQLException denied = new SQLException( + "Access denied; you need (at least one of) the PROCESS privilege", "42000", 1227); + + assertTrue(MysqlActiveTransactionManager.hasProcessPrivilegeError( + new IllegalStateException("execution failed", denied))); + assertFalse(MysqlActiveTransactionManager.hasProcessPrivilegeError( + new SQLException("table not found", "42S02", 1146))); + } + + @Test + void buildsLiveTransactionQueryWithMySql80LockWaitMetadata() { + String sql = MysqlActiveTransactionManager.activeTransactionSql("w.requesting_lock_id", "JOIN wait_table w"); + + assertTrue(sql.contains("FROM information_schema.innodb_trx t")); + assertTrue(sql.contains("LEFT JOIN information_schema.processlist p ON t.trx_mysql_thread_id = p.ID")); + assertTrue(sql.contains("w.requesting_lock_id")); + assertTrue(sql.contains("JOIN wait_table w")); + assertTrue(sql.contains("ORDER BY t.trx_started")); + } + + @Test + void unavailableLockMetadataQueryKeepsLiveTransactionSource() { + MysqlActiveTransactionManager.LockMetadataQuery query = + MysqlActiveTransactionManager.unavailableLockMetadataQuery(); + + assertFalse(query.available()); + assertTrue(query.sql().contains("FROM information_schema.innodb_trx t")); + assertTrue(query.sql().contains("NULL AS blocking_trx_id")); + assertTrue(query.sql().contains("LEFT JOIN information_schema.processlist p")); + assertTrue(query.sql().contains("ORDER BY t.trx_started")); + } + + @Test + void recognizesMissingOrDeniedLockMetadataFailures() { + assertTrue(MysqlActiveTransactionManager.hasMissingOrDeniedMetadataError( + new SQLException("Table 'performance_schema.data_lock_waits' doesn't exist", "42S02", 1146))); + assertTrue(MysqlActiveTransactionManager.hasMissingOrDeniedMetadataError( + new SQLException("SELECT command denied to user for table 'data_locks'", "42000", 1142))); + assertFalse(MysqlActiveTransactionManager.hasMissingOrDeniedMetadataError( + new SQLException("Syntax error", "42000", 1064))); + } + + @Test + void mapsResultSetToTypedTransactionResponse() throws SQLException { + ResultSet resultSet = resultSet(Map.ofEntries( + Map.entry("trx_id", "421337"), + Map.entry("trx_state", "LOCK WAIT"), + Map.entry("trx_started", Timestamp.valueOf("2026-08-31 12:00:00")), + Map.entry("trx_age_seconds", 12L), + Map.entry("trx_isolation_level", "REPEATABLE READ"), + Map.entry("trx_rows_locked", 1L), + Map.entry("trx_rows_modified", 0L), + Map.entry("trx_lock_structs", 2L), + Map.entry("trx_mysql_thread_id", 45L), + Map.entry("process_id", 45L), + Map.entry("process_user", "ops002_user"), + Map.entry("process_host", "127.0.0.1:50000"), + Map.entry("process_db", "ops002_test"), + Map.entry("requesting_lock_id", "421337:7:3:2"), + Map.entry("blocking_lock_id", "421336:7:3:2"), + Map.entry("blocking_trx_id", "421336"), + Map.entry("blocking_thread_id", 44L), + Map.entry("blocking_process_id", 44L) + )); + + ActiveTransaction transaction = MysqlActiveTransactionManager.readTransaction( + resultSet, + new MysqlActiveTransactionManager.LockMetadataQuery( + "SELECT ...", true, LockMetadataSource.MYSQL_80_PERFORMANCE_SCHEMA) + ); + + assertEquals("421337", transaction.getTrxId()); + assertEquals(45L, transaction.getThreadId()); + assertEquals(QueryState.UNAVAILABLE, transaction.getQueryState()); + assertEquals(LockMetadataState.AVAILABLE, transaction.getLockMetadataState()); + assertEquals(LockMetadataSource.MYSQL_80_PERFORMANCE_SCHEMA, transaction.getLockMetadataSource()); + assertTrue(transaction.getCanOpenSession()); + assertEquals("SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO\n" + + "FROM information_schema.PROCESSLIST\nWHERE ID = 45;", + transaction.getConnectionInspectionSql()); + assertTrue(transaction.getLockWaitAvailable()); + assertEquals(44L, transaction.getBlockingThreadId()); + assertEquals("SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO\n" + + "FROM information_schema.PROCESSLIST\nWHERE ID = 44;", + transaction.getBlockingConnectionInspectionSql()); + assertNull(transaction.getQuery()); + } + + @Test + void unavailableConnectionsDoNotExposeInspectionSql() throws SQLException { + ResultSet resultSet = resultSet(Map.ofEntries( + Map.entry("trx_id", "421338"), + Map.entry("trx_state", "RUNNING"), + Map.entry("trx_started", Timestamp.valueOf("2026-08-31 12:00:00")), + Map.entry("trx_mysql_thread_id", 46L) + )); + + ActiveTransaction transaction = MysqlActiveTransactionManager.readTransaction( + resultSet, + MysqlActiveTransactionManager.unavailableLockMetadataQuery() + ); + + assertFalse(transaction.getCanOpenSession()); + assertNull(transaction.getConnectionInspectionSql()); + assertFalse(transaction.getCanOpenBlockingSession()); + assertNull(transaction.getBlockingConnectionInspectionSql()); + } + + @Test + void fallbackProcessPrivilegeFailureUsesSanitizedBusinessCode() { + RuntimeException executionFailure = new IllegalStateException("execution failed", + new SQLException("Access denied; you need (at least one of) the PROCESS privilege", "42000", 1227)); + + BusinessException exception = assertThrows(BusinessException.class, () -> { + throw MysqlActiveTransactionManager.sanitizeActiveTransactionQueryException(executionFailure); + }); + + assertEquals(MysqlActiveTransactionManager.PROCESS_PRIVILEGE_ERROR_CODE, exception.getCode()); + assertSame(executionFailure, exception.getCause()); + } + + @Test + void nonProcessFallbackFailureRemainsOriginalRuntimeException() { + RuntimeException executionFailure = new IllegalStateException("syntax error", + new SQLException("Syntax error", "42000", 1064)); + + assertSame(executionFailure, + MysqlActiveTransactionManager.sanitizeActiveTransactionQueryException(executionFailure)); + } + + private static ResultSet resultSet(Map values) { + AtomicBoolean wasNull = new AtomicBoolean(); + return (ResultSet) Proxy.newProxyInstance( + MysqlActiveTransactionManagerTest.class.getClassLoader(), + new Class[]{ResultSet.class}, + (proxy, method, arguments) -> { + if ("wasNull".equals(method.getName())) { + return wasNull.get(); + } + if (arguments != null && arguments.length == 1 && arguments[0] instanceof String column) { + Object value = values.get(column); + wasNull.set(value == null); + return switch (method.getName()) { + case "getString" -> value == null ? null : value.toString(); + case "getTimestamp" -> value; + case "getLong" -> value == null ? 0L : ((Number) value).longValue(); + default -> throw new UnsupportedOperationException(method.getName()); + }; + } + throw new UnsupportedOperationException(method.getName()); + } + ); + } +} diff --git a/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/IActiveTransactionManager.java b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/IActiveTransactionManager.java new file mode 100644 index 0000000000..af2b26e1cd --- /dev/null +++ b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/IActiveTransactionManager.java @@ -0,0 +1,14 @@ +package ai.chat2db.spi; + +import ai.chat2db.community.domain.api.model.transaction.ActiveTransaction; + +import java.sql.Connection; +import java.util.List; + +/** + * Provides dialect-specific active transaction inspection. + */ +public interface IActiveTransactionManager { + + List activeTransactions(Connection connection); +} diff --git a/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/IPlugin.java b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/IPlugin.java index 934e6cae65..9b7090fc68 100644 --- a/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/IPlugin.java +++ b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/IPlugin.java @@ -108,6 +108,15 @@ default IRoutineManager getRoutineManager() { return null; } + /** + * Returns the optional active transaction manager for this plugin. + * + * @return active transaction manager when supported; otherwise {@code null}. + */ + default IActiveTransactionManager getActiveTransactionManager() { + return null; + } + /** * Returns all database configurations supported by this plugin instance. * diff --git a/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/sql/Chat2DBContext.java b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/sql/Chat2DBContext.java index 5dbeee541c..75db407f87 100644 --- a/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/sql/Chat2DBContext.java +++ b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/sql/Chat2DBContext.java @@ -1,6 +1,7 @@ package ai.chat2db.spi.sql; import ai.chat2db.spi.IAccountManager; +import ai.chat2db.spi.IActiveTransactionManager; import ai.chat2db.spi.IDbManager; import ai.chat2db.spi.IDbMetaData; import ai.chat2db.spi.IPlugin; @@ -124,6 +125,15 @@ public static IRoutineManager getRoutineManager() { return plugin == null ? null : plugin.getRoutineManager(); } + public static IActiveTransactionManager getActiveTransactionManager() { + ConnectInfo connectInfo = getConnectInfo(); + if (connectInfo == null || StringUtils.isBlank(connectInfo.getDbType())) { + return null; + } + IPlugin plugin = PLUGIN_MAP.get(connectInfo.getDbType()); + return plugin == null ? null : plugin.getActiveTransactionManager(); + } + public static Connection getConnection() { Connection connection = ConnectionPool.getConnection(getConnectInfo()); return StatementGuardConnection.wrap(connection, STATEMENT_GUARD_THREAD_LOCAL.get()); diff --git a/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages.properties b/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages.properties index 8cd19647c0..57b355e41d 100644 --- a/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages.properties +++ b/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages.properties @@ -163,6 +163,8 @@ mysql.account.scopeRequired=Privilege scope is required mysql.account.scopeUnsupported=Unsupported privilege scope mysql.account.tableRequired=Table name is required mysql.account.userRequired=User is required +mysql.activeTransaction.processPrivilegeRequired=PROCESS privilege is required to inspect active InnoDB transactions +activeTransaction.inspection.unsupported=The current datasource does not support active transaction inspection api.privateKeyNotFound=Private key information does not exist api.decryptPasswordError=Password decryption failed datasource.identityColor.invalid=Identity color must use the #RRGGBB format diff --git a/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_en_US.properties b/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_en_US.properties index 91d9c86c18..d94da83977 100644 --- a/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_en_US.properties +++ b/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_en_US.properties @@ -180,6 +180,8 @@ mysql.account.scopeRequired=Privilege scope is required mysql.account.scopeUnsupported=Unsupported privilege scope mysql.account.tableRequired=Table name is required mysql.account.userRequired=User is required +mysql.activeTransaction.processPrivilegeRequired=PROCESS privilege is required to inspect active InnoDB transactions +activeTransaction.inspection.unsupported=The current datasource does not support active transaction inspection api.privateKeyNotFound=Private key information does not exist api.decryptPasswordError=Password decryption failed datasource.identityColor.invalid=Identity color must use the #RRGGBB format diff --git a/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_es_ES.properties b/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_es_ES.properties index d5f5368031..04bfb289ea 100644 --- a/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_es_ES.properties +++ b/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_es_ES.properties @@ -180,6 +180,8 @@ mysql.account.scopeRequired=El ámbito del privilegio es obligatorio mysql.account.scopeUnsupported=Ámbito del privilegio no compatible mysql.account.tableRequired=El nombre de la tabla es obligatorio mysql.account.userRequired=El usuario es obligatorio +mysql.activeTransaction.processPrivilegeRequired=Se requiere el privilegio PROCESS para inspeccionar transacciones InnoDB activas +activeTransaction.inspection.unsupported=La fuente de datos actual no admite la inspección de transacciones activas api.privateKeyNotFound=La información de la clave privada no existe api.decryptPasswordError=No se pudo descifrar la contraseña datasource.identityColor.invalid=El color de identidad debe usar el formato #RRGGBB diff --git a/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_ja_JP.properties b/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_ja_JP.properties index 81c60f8289..8bc82bf8a4 100644 --- a/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_ja_JP.properties +++ b/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_ja_JP.properties @@ -60,6 +60,8 @@ sqlExecution.streamingUnsupported=現在のデータソース実行器はスト networkProxy.unexpectedResponseCode=予期しないレスポンスコードです: {0} networkProxy.hostRequired=プロキシホストが必要です networkProxy.portRange=プロキシポートは 1 から 65535 の範囲で指定してください +mysql.activeTransaction.processPrivilegeRequired=アクティブな InnoDB トランザクションの確認には PROCESS 権限が必要です +activeTransaction.inspection.unsupported=現在のデータソースはアクティブトランザクションの確認に対応していません api.privateKeyNotFound=秘密鍵情報が存在しません api.decryptPasswordError=パスワードの復号に失敗しました task.delete.activeForbidden=保留中または実行中のタスクは削除できません diff --git a/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_ko_KR.properties b/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_ko_KR.properties index 63283522dc..c5bc0c8bb5 100644 --- a/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_ko_KR.properties +++ b/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_ko_KR.properties @@ -180,6 +180,8 @@ mysql.account.scopeRequired=권한 범위가 필요합니다 mysql.account.scopeUnsupported=지원되지 않는 권한 범위입니다 mysql.account.tableRequired=테이블 이름이 필요합니다 mysql.account.userRequired=사용자가 필요합니다 +mysql.activeTransaction.processPrivilegeRequired=활성 InnoDB 트랜잭션을 확인하려면 PROCESS 권한이 필요합니다 +activeTransaction.inspection.unsupported=현재 데이터 소스는 활성 트랜잭션 검사를 지원하지 않습니다 api.privateKeyNotFound=개인 키 정보가 존재하지 않습니다 api.decryptPasswordError=비밀번호를 복호화하지 못했습니다 datasource.identityColor.invalid=식별 색상은 #RRGGBB 형식을 사용해야 합니다 diff --git a/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_zh_CN.properties b/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_zh_CN.properties index 79a84ee782..e9eb925c30 100644 --- a/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_zh_CN.properties +++ b/chat2db-community-server/chat2db-community-start/src/main/resources/i18n/messages_zh_CN.properties @@ -175,6 +175,8 @@ mysql.account.scopeRequired=权限范围不能为空 mysql.account.scopeUnsupported=不支持的权限范围 mysql.account.tableRequired=表名称不能为空 mysql.account.userRequired=用户不能为空 +mysql.activeTransaction.processPrivilegeRequired=查看活动 InnoDB 事务需要 PROCESS 权限 +activeTransaction.inspection.unsupported=当前数据源不支持活动事务检查 api.privateKeyNotFound=私钥密钥信息不存在 api.decryptPasswordError=解密密码错误 datasource.identityColor.invalid=身份颜色必须使用 #RRGGBB 格式 diff --git a/chat2db-community-server/chat2db-community-web/src/main/java/ai/chat2db/community/web/api/controller/DbActiveTransactionController.java b/chat2db-community-server/chat2db-community-web/src/main/java/ai/chat2db/community/web/api/controller/DbActiveTransactionController.java index 3b583a91f2..f2f3c6ac63 100644 --- a/chat2db-community-server/chat2db-community-web/src/main/java/ai/chat2db/community/web/api/controller/DbActiveTransactionController.java +++ b/chat2db-community-server/chat2db-community-web/src/main/java/ai/chat2db/community/web/api/controller/DbActiveTransactionController.java @@ -1,5 +1,6 @@ package ai.chat2db.community.web.api.controller; +import ai.chat2db.community.domain.api.model.transaction.ActiveTransaction; import ai.chat2db.community.domain.api.service.db.IDbActiveTransactionService; import ai.chat2db.community.tools.wrapper.result.DataResult; import ai.chat2db.community.web.api.aspect.connection.ConnectionInfoAspect; @@ -11,7 +12,6 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; -import java.util.Map; /** * Exposes active InnoDB transaction inspection (MYSQL-OPS-002). @@ -29,10 +29,10 @@ public class DbActiveTransactionController { *

* Endpoint: {@code GET /api/rdb/active_transaction/list}. * - * @return data result containing a list of transaction maps. + * @return active transactions. */ @GetMapping("/list") - public DataResult>> list(@Valid DataSourceBaseRequest request) { + public DataResult> list(@Valid DataSourceBaseRequest request) { return DataResult.of(activeTransactionService.activeTransactions()); } } diff --git a/script/test-fixtures/mysql/MYSQL-OPS-002/README.md b/script/test-fixtures/mysql/MYSQL-OPS-002/README.md index 65c8fe424b..88b29a24e3 100644 --- a/script/test-fixtures/mysql/MYSQL-OPS-002/README.md +++ b/script/test-fixtures/mysql/MYSQL-OPS-002/README.md @@ -3,7 +3,8 @@ ## Fixture - `init.sql` creates: - - `ops002_admin` — administrator with PROCESS (full transaction visibility) + - `ops002_admin` — administrator with PROCESS plus read access to MySQL 8.0 + lock-wait metadata (full transaction and blocking-chain visibility) - `ops002_user` — limited account (no PROCESS; can inspect its own visible InnoDB transactions, but cross-user transaction/process details can be hidden or rejected by MySQL privilege checks) @@ -32,5 +33,5 @@ ``` 9. Verify the Active Transactions view shows waited-lock and blocker fields for the waiting row, and that Owner/Blocker session buttons open a datasource-bound console filtered by the exact `information_schema.PROCESSLIST.ID`. 10. Commit or roll back the first transaction — refresh — verify committed or rolled-back transactions disappear instead of remaining as historical rows. -11. Connect as `ops002_user`, open a transaction, and refresh the view — verify the current user's visible transaction can appear, hidden SQL is rendered as an explicit unavailable state when MySQL returns NULL, lock-wait metadata degrades explicitly when the account or server cannot expose it, and a PROCESS denial from the transaction/process query is reported as a permission-required state rather than a generic runtime error. +11. Connect as `ops002_user`, open a transaction, and refresh the view — verify hidden SQL is rendered as an explicit unavailable state when MySQL returns NULL, lock-wait metadata degrades explicitly when the account or server cannot expose it, and a PROCESS denial from the transaction/process query is reported as a permission-required state rather than a generic runtime error. 12. With no open transactions, refresh — verify the empty state is shown normally. diff --git a/script/test-fixtures/mysql/MYSQL-OPS-002/grants.sql b/script/test-fixtures/mysql/MYSQL-OPS-002/grants.sql index 5d58737ae4..ce14777313 100644 --- a/script/test-fixtures/mysql/MYSQL-OPS-002/grants.sql +++ b/script/test-fixtures/mysql/MYSQL-OPS-002/grants.sql @@ -1,6 +1,8 @@ -- MYSQL-OPS-002: Grants for test user -- ops002_admin has PROCESS for full transaction visibility (all users + SQL text). GRANT PROCESS ON *.* TO 'ops002_admin'@'%'; +GRANT SELECT ON `performance_schema`.`data_lock_waits` TO 'ops002_admin'@'%'; +GRANT SELECT ON `performance_schema`.`data_locks` TO 'ops002_admin'@'%'; -- ops002_user intentionally has NO PROCESS: cross-user transaction/process details can -- be hidden, returned with NULL SQL text, or rejected by MySQL privilege checks. FLUSH PRIVILEGES; From 223f436656a44f7ca226f265613f905415f3af07 Mon Sep 17 00:00:00 2001 From: zgq Date: Thu, 3 Sep 2026 12:43:00 +0800 Subject: [PATCH 45/49] docs(mysql): align active transaction test steps --- script/test-fixtures/mysql/MYSQL-OPS-002/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/test-fixtures/mysql/MYSQL-OPS-002/README.md b/script/test-fixtures/mysql/MYSQL-OPS-002/README.md index 88b29a24e3..b68ce78804 100644 --- a/script/test-fixtures/mysql/MYSQL-OPS-002/README.md +++ b/script/test-fixtures/mysql/MYSQL-OPS-002/README.md @@ -15,7 +15,7 @@ ## Verification 1. Connect as `ops002_admin`. -2. Open the Active Transactions view (right-click the datasource node). +2. Expand the datasource's Monitor node, then double-click Active Transactions. 3. Open a second connection and run `START TRANSACTION; UPDATE ops002_accounts SET balance = balance - 100 WHERE id = 1;` — leave it open. 4. Refresh the view — verify the transaction appears with state RUNNING, a growing age, isolation level REPEATABLE READ, thread ID, user, host, database, and the UPDATE SQL text. 5. Open a third connection and run a second open transaction — verify both are listed, ordered by start time. @@ -31,7 +31,7 @@ SELECT requesting_trx_id, requested_lock_id, blocking_trx_id, blocking_lock_id FROM information_schema.innodb_lock_waits; ``` -9. Verify the Active Transactions view shows waited-lock and blocker fields for the waiting row, and that Owner/Blocker session buttons open a datasource-bound console filtered by the exact `information_schema.PROCESSLIST.ID`. +9. Verify the Active Transactions view shows waited-lock and blocker fields for the waiting row, and that the owner and blocker connection ID links open datasource-bound consoles filtered by the exact `information_schema.PROCESSLIST.ID`. 10. Commit or roll back the first transaction — refresh — verify committed or rolled-back transactions disappear instead of remaining as historical rows. 11. Connect as `ops002_user`, open a transaction, and refresh the view — verify hidden SQL is rendered as an explicit unavailable state when MySQL returns NULL, lock-wait metadata degrades explicitly when the account or server cannot expose it, and a PROCESS denial from the transaction/process query is reported as a permission-required state rather than a generic runtime error. 12. With no open transactions, refresh — verify the empty state is shown normally. From b8aa245a68d73d1e1e6c5538cd26911017a6efe6 Mon Sep 17 00:00:00 2001 From: zgq Date: Thu, 3 Sep 2026 13:01:13 +0800 Subject: [PATCH 46/49] fix(workspace): keep panel resizing responsive --- .../editor/SQLEditorWithOperation/style.ts | 13 ++++++++++ .../WorkspaceExtendBody/index.tsx | 6 ++--- .../components/WorkspaceRight/index.tsx | 6 +++-- .../components/WorkspaceTabs/index.tsx | 21 +++++++++++++--- .../src/pages/main/workspace/index.tsx | 25 ++++++------------- 5 files changed, 44 insertions(+), 27 deletions(-) diff --git a/chat2db-community-client/src/components/SQLEditor/editor/SQLEditorWithOperation/style.ts b/chat2db-community-client/src/components/SQLEditor/editor/SQLEditorWithOperation/style.ts index 1c40655bd8..a8ac89bba4 100644 --- a/chat2db-community-client/src/components/SQLEditor/editor/SQLEditorWithOperation/style.ts +++ b/chat2db-community-client/src/components/SQLEditor/editor/SQLEditorWithOperation/style.ts @@ -25,6 +25,19 @@ export const useStyles = createStyles(({ css, token }) => { width: 100%; height: 0px; border-top: 1px solid ${colorBorderLayout}; + + > .monaco-editor { + width: 100% !important; + } + + > .monaco-editor > .overflow-guard { + width: 100% !important; + } + + > .monaco-editor .editor-scrollable { + right: 0 !important; + width: auto !important; + } `, watermarkLayer: css` position: absolute; diff --git a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/WorkspaceExtendBody/index.tsx b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/WorkspaceExtendBody/index.tsx index 72e16c348d..1fd049fc23 100644 --- a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/WorkspaceExtendBody/index.tsx +++ b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/WorkspaceExtendBody/index.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { extendConfig, isWorkspaceRecordCode } from '../config'; import { useWorkspaceStore } from '@/store/workspace'; import { useStyles } from './style'; @@ -10,7 +10,7 @@ import { } from '@/store/workspace/utils/resultInspector'; import WorkspaceRecordSwitcher from '../WorkspaceRecordSwitcher'; -export default () => { +export default memo(() => { const { styles } = useStyles(); const currentWorkspaceExtend = useWorkspaceStore((state) => state.currentWorkspaceExtend); @@ -43,4 +43,4 @@ export default () => { ) : ( false ); -}; +}); diff --git a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceRight/index.tsx b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceRight/index.tsx index 4439900349..d6f420ed1c 100644 --- a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceRight/index.tsx +++ b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceRight/index.tsx @@ -66,8 +66,10 @@ const WorkspaceRight = memo(() => { maxSize={maxPanelSize} primary="second" allowResize={panelRight} - onChange={(newSize) => { - setPanelRightWidth(newSize); + onDragFinished={(newSize) => { + if (Number.isFinite(newSize) && newSize !== panelRightWidth) { + setPanelRightWidth(newSize); + } }} >

diff --git a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx index 9903ec3440..da15d53895 100644 --- a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx +++ b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceTabs/index.tsx @@ -107,6 +107,7 @@ const SplitPaneAny = SplitPane as any; const MAIN_WORKSPACE_TAB_PANE: WorkspaceTabPaneId = 'main'; const SPLIT_WORKSPACE_TAB_PANE: WorkspaceTabPaneId = 'split'; const WORKSPACE_TAB_PANE_DROPPABLE_PREFIX = 'workspace-tab-pane:'; +const WORKSPACE_TAB_HEADER_HEIGHT = 36; const WORKSPACE_TAB_WIDTH = 200; const WORKSPACE_TAB_HORIZONTAL_RESIZE_CLASS = 'WorkspaceTabHorizontalResizing'; const WORKSPACE_TAB_VERTICAL_RESIZE_CLASS = 'WorkspaceTabVerticalResizing'; @@ -841,6 +842,9 @@ const WorkspaceTabs = memo(() => { // The split box does not exist in an empty workspace. Re-run when the first // tab mounts even if the normalized split layout remains null. useLayoutEffect(() => { + if (!workspaceTabSplitLayout) { + return; + } const container = splitTabBoxRef.current; if (!container) { return; @@ -2087,7 +2091,7 @@ const WorkspaceTabs = memo(() => { }} > onPaneTabChange(paneId, key)} @@ -2172,16 +2176,25 @@ const WorkspaceTabs = memo(() => { const paneId = activeTabPaneIds.get(item.key); const bounds = paneId ? paneContentBounds[paneId] : undefined; const isActive = paneId !== undefined; + const fillsSinglePane = isActive && !workspaceTabSplitLayout; + const isVisible = fillsSinglePane || !!bounds; if (item.destroyOnHide && !isActive) { return null; } return (
{ setPanelLeftWidth: state.setPanelLeftWidth, }; }); - const [size, setSize] = React.useState(panelLeftWidth); - - useEffect(() => { - if (size !== panelLeftWidth) { - setSize(panelLeftWidth); - } - }, [panelLeftWidth]); - return (
{ - if (newSize < 100) { - setSize(0); - setPanelLeftWidth(0); - return; + onDragFinished={(newSize) => { + const nextWidth = newSize < 100 ? 0 : newSize; + if (Number.isFinite(nextWidth) && nextWidth !== panelLeftWidth) { + setPanelLeftWidth(nextWidth); } - setSize(newSize); - setPanelLeftWidth(newSize); }} - size={size} + size={panelLeftWidth} minSize={0} primary="first" > From 3ca2412b12138058cce5987e66cd1c4cf3b5e728 Mon Sep 17 00:00:00 2001 From: zgq Date: Thu, 3 Sep 2026 13:01:59 +0800 Subject: [PATCH 47/49] fix(tabs): restore theme accent fallback --- chat2db-community-client/package.json | 2 +- .../SQLEditor/editor/SQLEditorWithOperation/index.tsx | 4 ++-- chat2db-community-client/src/components/Tabs/index.tsx | 5 ++--- .../src/components/Tabs/tabAccentColor.test.ts | 9 +++++++++ .../src/components/Tabs/tabAccentColor.ts | 9 +++++++++ 5 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 chat2db-community-client/src/components/Tabs/tabAccentColor.test.ts create mode 100644 chat2db-community-client/src/components/Tabs/tabAccentColor.ts diff --git a/chat2db-community-client/package.json b/chat2db-community-client/package.json index d6c4085444..a58f988c2b 100644 --- a/chat2db-community-client/package.json +++ b/chat2db-community-client/package.json @@ -83,7 +83,7 @@ "test:sql-completion-context": "tsx src/components/SQLEditor/core/sqlCompletionContext.test.ts && tsx src/components/SQLEditor/core/sqlCompletionRequestError.test.ts", "test:sql-in-clipboard": "tsx src/utils/sqlInClipboard.test.ts && tsx src/components/SQLEditor/helper/sqlInsertValueDefaults.test.ts", "test:console-tab-name": "tsx src/store/workspace/utils/consoleTabName.test.ts && tsx src/utils/workspaceObjectTabTitle.test.ts", - "test:workspace-tab-scroll": "tsx src/store/workspace/utils/workspaceTabScrollRequest.test.ts && tsx src/components/Tabs/activeTabScroll.test.ts && tsx src/components/Tabs/wheelScroll.test.ts", + "test:workspace-tab-scroll": "tsx src/store/workspace/utils/workspaceTabScrollRequest.test.ts && tsx src/components/Tabs/activeTabScroll.test.ts && tsx src/components/Tabs/wheelScroll.test.ts && tsx src/components/Tabs/tabAccentColor.test.ts", "test:workspace-split-lifecycle": "tsx src/pages/main/workspace/components/WorkspaceTabs/workspaceTabContentLayout.test.ts", "test:tree-title-highlight": "tsx src/blocks/NewTree/components/TitleRender/highlightSearchText.test.ts", "test:tree-search-lifecycle": "tsx src/pages/main/workspace/components/WorkspaceTreeSearch/lifecycle.test.ts && tsx src/pages/main/workspace/components/WorkspaceTreeSearch/refresh.test.ts", diff --git a/chat2db-community-client/src/components/SQLEditor/editor/SQLEditorWithOperation/index.tsx b/chat2db-community-client/src/components/SQLEditor/editor/SQLEditorWithOperation/index.tsx index df5ee10721..5f9f473d67 100644 --- a/chat2db-community-client/src/components/SQLEditor/editor/SQLEditorWithOperation/index.tsx +++ b/chat2db-community-client/src/components/SQLEditor/editor/SQLEditorWithOperation/index.tsx @@ -142,7 +142,7 @@ const SQLEditorWithOperation = forwardRef(contextMenuDefaultConfig); const [contextTableIdentifier, setContextTableIdentifier] = useState(null); @@ -1164,7 +1164,7 @@ const SQLEditorWithOperation = forwardRef((props) => { const tabStyle = { ...t.styles, - ...(t.accentColor !== undefined - ? ({ '--chat2db-tab-accent-color': t.accentColor || 'transparent' } as React.CSSProperties) - : {}), + ...getTabAccentStyle(t.accentColor), }; const tabNode = enableReorder ? ( Date: Thu, 3 Sep 2026 13:08:42 +0800 Subject: [PATCH 48/49] refactor(mysql): centralize active transaction constants --- .../mysql/MysqlActiveTransactionManager.java | 217 +++++------------- .../MysqlActiveTransactionConstants.java | 171 ++++++++++++++ .../MysqlActiveTransactionManagerTest.java | 80 ++++--- 3 files changed, 279 insertions(+), 189 deletions(-) create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlActiveTransactionConstants.java diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlActiveTransactionManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlActiveTransactionManager.java index 8d15b0a787..ecac3229f8 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlActiveTransactionManager.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlActiveTransactionManager.java @@ -14,116 +14,11 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.Locale; -public class MysqlActiveTransactionManager implements IActiveTransactionManager { - - /** - * InnoDB transaction metadata joined with the owning processlist row. The - * transaction source is always {@code information_schema.innodb_trx}; each refresh is - * a new live snapshot and does not retain rows for transactions that have committed - * or rolled back. - */ - private static final String SQL_ACTIVE_TRANSACTIONS = - "SELECT t.trx_id, t.trx_state, t.trx_started, " - + "TIMESTAMPDIFF(SECOND, t.trx_started, NOW()) AS trx_age_seconds, " - + "t.trx_isolation_level, t.trx_rows_locked, t.trx_rows_modified, " - + "t.trx_lock_structs, t.trx_mysql_thread_id, " - + "p.ID AS process_id, p.USER AS process_user, p.HOST AS process_host, p.DB AS process_db, " - + "t.trx_query, " - + "%s " - + "FROM information_schema.innodb_trx t " - + "LEFT JOIN information_schema.processlist p ON t.trx_mysql_thread_id = p.ID " - + "%s " - + "ORDER BY t.trx_started"; - - private static final String LOCK_COLUMNS_80 = - "w.requesting_lock_id, w.blocking_lock_id, w.blocking_trx_id, " - + "w.requesting_ps_thread_id, w.blocking_ps_thread_id, " - + "w.waiting_object_schema, w.waiting_object_name, w.waiting_index_name, " - + "w.waiting_lock_type, w.waiting_lock_mode, w.waiting_lock_status, w.waiting_lock_data, " - + "w.blocking_object_schema, w.blocking_object_name, w.blocking_index_name, " - + "w.blocking_lock_type, w.blocking_lock_mode, w.blocking_lock_status, w.blocking_lock_data, " - + "bt.trx_mysql_thread_id AS blocking_thread_id, bp.ID AS blocking_process_id, " - + "bp.USER AS blocking_user, bp.HOST AS blocking_host, bp.DB AS blocking_db"; - - private static final String LOCK_JOIN_80 = - "LEFT JOIN (" - + "SELECT dlw.REQUESTING_ENGINE_TRANSACTION_ID AS requesting_trx_id, " - + "dlw.REQUESTING_ENGINE_LOCK_ID AS requesting_lock_id, " - + "dlw.BLOCKING_ENGINE_LOCK_ID AS blocking_lock_id, " - + "dlw.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx_id, " - + "dlw.REQUESTING_THREAD_ID AS requesting_ps_thread_id, " - + "dlw.BLOCKING_THREAD_ID AS blocking_ps_thread_id, " - + "rl.OBJECT_SCHEMA AS waiting_object_schema, rl.OBJECT_NAME AS waiting_object_name, " - + "rl.INDEX_NAME AS waiting_index_name, rl.LOCK_TYPE AS waiting_lock_type, " - + "rl.LOCK_MODE AS waiting_lock_mode, rl.LOCK_STATUS AS waiting_lock_status, " - + "rl.LOCK_DATA AS waiting_lock_data, " - + "bl.OBJECT_SCHEMA AS blocking_object_schema, bl.OBJECT_NAME AS blocking_object_name, " - + "bl.INDEX_NAME AS blocking_index_name, bl.LOCK_TYPE AS blocking_lock_type, " - + "bl.LOCK_MODE AS blocking_lock_mode, bl.LOCK_STATUS AS blocking_lock_status, " - + "bl.LOCK_DATA AS blocking_lock_data " - + "FROM performance_schema.data_lock_waits dlw " - + "LEFT JOIN performance_schema.data_locks rl " - + "ON dlw.REQUESTING_ENGINE_LOCK_ID = rl.ENGINE_LOCK_ID " - + "AND dlw.ENGINE = rl.ENGINE " - + "LEFT JOIN performance_schema.data_locks bl " - + "ON dlw.BLOCKING_ENGINE_LOCK_ID = bl.ENGINE_LOCK_ID " - + "AND dlw.ENGINE = bl.ENGINE" - + ") w ON w.requesting_trx_id = t.trx_id " - + "LEFT JOIN information_schema.innodb_trx bt ON bt.trx_id = w.blocking_trx_id " - + "LEFT JOIN information_schema.processlist bp ON bt.trx_mysql_thread_id = bp.ID"; - - private static final String LOCK_COLUMNS_57 = - "w.requesting_lock_id, w.blocking_lock_id, w.blocking_trx_id, " - + "NULL AS requesting_ps_thread_id, NULL AS blocking_ps_thread_id, " - + "NULL AS waiting_object_schema, w.waiting_object_name, w.waiting_index_name, " - + "w.waiting_lock_type, w.waiting_lock_mode, NULL AS waiting_lock_status, w.waiting_lock_data, " - + "NULL AS blocking_object_schema, w.blocking_object_name, w.blocking_index_name, " - + "w.blocking_lock_type, w.blocking_lock_mode, NULL AS blocking_lock_status, w.blocking_lock_data, " - + "bt.trx_mysql_thread_id AS blocking_thread_id, bp.ID AS blocking_process_id, " - + "bp.USER AS blocking_user, bp.HOST AS blocking_host, bp.DB AS blocking_db"; - - private static final String LOCK_JOIN_57 = - "LEFT JOIN (" - + "SELECT lw.requesting_trx_id, lw.requested_lock_id AS requesting_lock_id, " - + "lw.blocking_lock_id, lw.blocking_trx_id, " - + "rl.lock_table AS waiting_object_name, rl.lock_index AS waiting_index_name, " - + "rl.lock_type AS waiting_lock_type, rl.lock_mode AS waiting_lock_mode, " - + "rl.lock_data AS waiting_lock_data, " - + "bl.lock_table AS blocking_object_name, bl.lock_index AS blocking_index_name, " - + "bl.lock_type AS blocking_lock_type, bl.lock_mode AS blocking_lock_mode, " - + "bl.lock_data AS blocking_lock_data " - + "FROM information_schema.innodb_lock_waits lw " - + "LEFT JOIN information_schema.innodb_locks rl ON lw.requested_lock_id = rl.lock_id " - + "LEFT JOIN information_schema.innodb_locks bl ON lw.blocking_lock_id = bl.lock_id" - + ") w ON w.requesting_trx_id = t.trx_id " - + "LEFT JOIN information_schema.innodb_trx bt ON bt.trx_id = w.blocking_trx_id " - + "LEFT JOIN information_schema.processlist bp ON bt.trx_mysql_thread_id = bp.ID"; - - private static final String LOCK_COLUMNS_UNAVAILABLE = - "NULL AS requesting_lock_id, NULL AS blocking_lock_id, NULL AS blocking_trx_id, " - + "NULL AS requesting_ps_thread_id, NULL AS blocking_ps_thread_id, " - + "NULL AS waiting_object_schema, NULL AS waiting_object_name, NULL AS waiting_index_name, " - + "NULL AS waiting_lock_type, NULL AS waiting_lock_mode, NULL AS waiting_lock_status, " - + "NULL AS waiting_lock_data, " - + "NULL AS blocking_object_schema, NULL AS blocking_object_name, NULL AS blocking_index_name, " - + "NULL AS blocking_lock_type, NULL AS blocking_lock_mode, NULL AS blocking_lock_status, " - + "NULL AS blocking_lock_data, " - + "NULL AS blocking_thread_id, NULL AS blocking_process_id, " - + "NULL AS blocking_user, NULL AS blocking_host, NULL AS blocking_db"; +import static ai.chat2db.plugin.mysql.constant.MysqlActiveTransactionConstants.*; - private static final String SQL_PROBE_MYSQL80_LOCKS = - "SELECT 1 FROM performance_schema.data_lock_waits WHERE 1 = 0"; - - private static final String SQL_PROBE_MYSQL57_LOCKS = - "SELECT 1 FROM information_schema.innodb_lock_waits WHERE 1 = 0"; - - private static final String SQL_CONNECTION_INSPECTION = - "SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO\n" - + "FROM information_schema.PROCESSLIST\n" - + "WHERE ID = %d;"; - - static final String PROCESS_PRIVILEGE_ERROR_CODE = "mysql.activeTransaction.processPrivilegeRequired"; +public class MysqlActiveTransactionManager implements IActiveTransactionManager { @Override public List activeTransactions(Connection connection) { @@ -162,22 +57,22 @@ private static List executeActiveTransactions( static ActiveTransaction readTransaction(ResultSet resultSet, LockMetadataQuery lockMetadataQuery) throws SQLException { ActiveTransaction transaction = new ActiveTransaction(); - transaction.setTrxId(resultSet.getString("trx_id")); - transaction.setState(resultSet.getString("trx_state")); - transaction.setStartedAt(resultSet.getTimestamp("trx_started")); - transaction.setAgeSeconds(nullableLong(resultSet, "trx_age_seconds")); - transaction.setIsolationLevel(resultSet.getString("trx_isolation_level")); - transaction.setRowsLocked(nullableLong(resultSet, "trx_rows_locked")); - transaction.setRowsModified(nullableLong(resultSet, "trx_rows_modified")); - transaction.setLockStructs(nullableLong(resultSet, "trx_lock_structs")); - Long threadId = nullableLong(resultSet, "trx_mysql_thread_id"); + transaction.setTrxId(resultSet.getString(RESULT_TRX_ID)); + transaction.setState(resultSet.getString(RESULT_TRX_STATE)); + transaction.setStartedAt(resultSet.getTimestamp(RESULT_TRX_STARTED)); + transaction.setAgeSeconds(nullableLong(resultSet, RESULT_TRX_AGE_SECONDS)); + transaction.setIsolationLevel(resultSet.getString(RESULT_TRX_ISOLATION_LEVEL)); + transaction.setRowsLocked(nullableLong(resultSet, RESULT_TRX_ROWS_LOCKED)); + transaction.setRowsModified(nullableLong(resultSet, RESULT_TRX_ROWS_MODIFIED)); + transaction.setLockStructs(nullableLong(resultSet, RESULT_TRX_LOCK_STRUCTS)); + Long threadId = nullableLong(resultSet, RESULT_TRX_MYSQL_THREAD_ID); transaction.setThreadId(threadId); - transaction.setUser(resultSet.getString("process_user")); - transaction.setHost(resultSet.getString("process_host")); - transaction.setDb(resultSet.getString("process_db")); - String query = resultSet.getString("trx_query"); + transaction.setUser(resultSet.getString(RESULT_PROCESS_USER)); + transaction.setHost(resultSet.getString(RESULT_PROCESS_HOST)); + transaction.setDb(resultSet.getString(RESULT_PROCESS_DB)); + String query = resultSet.getString(RESULT_TRX_QUERY); transaction.setQuery(query); - Long processId = nullableLong(resultSet, "process_id"); + Long processId = nullableLong(resultSet, RESULT_PROCESS_ID); transaction.setSessionAvailable(processId != null); transaction.setSessionState(processId == null ? SessionState.DISAPPEARED_OR_HIDDEN : SessionState.LIVE); boolean canOpenSession = processId != null && threadId != null; @@ -192,36 +87,40 @@ static ActiveTransaction readTransaction(ResultSet resultSet, LockMetadataQuery } private static void putLockWait(ActiveTransaction transaction, ResultSet resultSet) throws SQLException { - Long blockingThreadId = nullableLong(resultSet, "blocking_thread_id"); - Long blockingProcessId = nullableLong(resultSet, "blocking_process_id"); - String waitingLockId = resultSet.getString("requesting_lock_id"); - String blockingLockId = resultSet.getString("blocking_lock_id"); + Long blockingThreadId = nullableLong(resultSet, RESULT_BLOCKING_THREAD_ID); + Long blockingProcessId = nullableLong(resultSet, RESULT_BLOCKING_PROCESS_ID); + String waitingLockId = resultSet.getString(RESULT_REQUESTING_LOCK_ID); + String blockingLockId = resultSet.getString(RESULT_BLOCKING_LOCK_ID); transaction.setWaitingLockId(waitingLockId); transaction.setBlockingLockId(blockingLockId); - transaction.setBlockingTrxId(resultSet.getString("blocking_trx_id")); - transaction.setWaitingPerformanceSchemaThreadId(nullableLong(resultSet, "requesting_ps_thread_id")); - transaction.setBlockingPerformanceSchemaThreadId(nullableLong(resultSet, "blocking_ps_thread_id")); + transaction.setBlockingTrxId(resultSet.getString(RESULT_BLOCKING_TRX_ID)); + transaction.setWaitingPerformanceSchemaThreadId(nullableLong(resultSet, RESULT_REQUESTING_PS_THREAD_ID)); + transaction.setBlockingPerformanceSchemaThreadId(nullableLong(resultSet, RESULT_BLOCKING_PS_THREAD_ID)); transaction.setBlockingThreadId(blockingThreadId); transaction.setBlockingSessionAvailable(blockingProcessId != null); boolean canOpenBlockingSession = blockingProcessId != null && blockingThreadId != null; transaction.setCanOpenBlockingSession(canOpenBlockingSession); transaction.setBlockingConnectionInspectionSql( canOpenBlockingSession ? connectionInspectionSql(blockingThreadId) : null); - transaction.setBlockingUser(resultSet.getString("blocking_user")); - transaction.setBlockingHost(resultSet.getString("blocking_host")); - transaction.setBlockingDb(resultSet.getString("blocking_db")); - transaction.setWaitingObject(lockObject(resultSet, "waiting_object_schema", "waiting_object_name")); - transaction.setWaitingIndex(resultSet.getString("waiting_index_name")); - transaction.setWaitingLockType(resultSet.getString("waiting_lock_type")); - transaction.setWaitingLockMode(resultSet.getString("waiting_lock_mode")); - transaction.setWaitingLockStatus(resultSet.getString("waiting_lock_status")); - transaction.setWaitingLockData(resultSet.getString("waiting_lock_data")); - transaction.setBlockingObject(lockObject(resultSet, "blocking_object_schema", "blocking_object_name")); - transaction.setBlockingIndex(resultSet.getString("blocking_index_name")); - transaction.setBlockingLockType(resultSet.getString("blocking_lock_type")); - transaction.setBlockingLockMode(resultSet.getString("blocking_lock_mode")); - transaction.setBlockingLockStatus(resultSet.getString("blocking_lock_status")); - transaction.setBlockingLockData(resultSet.getString("blocking_lock_data")); + transaction.setBlockingUser(resultSet.getString(RESULT_BLOCKING_USER)); + transaction.setBlockingHost(resultSet.getString(RESULT_BLOCKING_HOST)); + transaction.setBlockingDb(resultSet.getString(RESULT_BLOCKING_DB)); + transaction.setWaitingObject(lockObject(resultSet, RESULT_WAITING_OBJECT_SCHEMA, RESULT_WAITING_OBJECT_NAME)); + transaction.setWaitingIndex(resultSet.getString(RESULT_WAITING_INDEX_NAME)); + transaction.setWaitingLockType(resultSet.getString(RESULT_WAITING_LOCK_TYPE)); + transaction.setWaitingLockMode(resultSet.getString(RESULT_WAITING_LOCK_MODE)); + transaction.setWaitingLockStatus(resultSet.getString(RESULT_WAITING_LOCK_STATUS)); + transaction.setWaitingLockData(resultSet.getString(RESULT_WAITING_LOCK_DATA)); + transaction.setBlockingObject(lockObject( + resultSet, + RESULT_BLOCKING_OBJECT_SCHEMA, + RESULT_BLOCKING_OBJECT_NAME + )); + transaction.setBlockingIndex(resultSet.getString(RESULT_BLOCKING_INDEX_NAME)); + transaction.setBlockingLockType(resultSet.getString(RESULT_BLOCKING_LOCK_TYPE)); + transaction.setBlockingLockMode(resultSet.getString(RESULT_BLOCKING_LOCK_MODE)); + transaction.setBlockingLockStatus(resultSet.getString(RESULT_BLOCKING_LOCK_STATUS)); + transaction.setBlockingLockData(resultSet.getString(RESULT_BLOCKING_LOCK_DATA)); transaction.setLockWaitAvailable(waitingLockId != null || blockingLockId != null); } @@ -234,7 +133,7 @@ private static String lockObject(ResultSet resultSet, String schemaColumn, Strin if (object == null || object.isBlank()) { return schema; } - return schema + "." + object; + return schema + QUALIFIED_NAME_SEPARATOR + object; } private static Long nullableLong(ResultSet resultSet, String column) throws SQLException { @@ -278,7 +177,7 @@ static String activeTransactionSql(String lockColumns, String lockJoin) { } static LockMetadataQuery unavailableLockMetadataQuery() { - return new LockMetadataQuery(activeTransactionSql(LOCK_COLUMNS_UNAVAILABLE, ""), + return new LockMetadataQuery(activeTransactionSql(LOCK_COLUMNS_UNAVAILABLE, LOCK_JOIN_UNAVAILABLE), false, null); } @@ -286,9 +185,8 @@ static boolean hasProcessPrivilegeError(Throwable throwable) { Throwable current = throwable; while (current != null) { if (current instanceof SQLException sqlException - && (sqlException.getErrorCode() == 1227 - || sqlException.getMessage() != null - && sqlException.getMessage().toUpperCase().contains("PROCESS"))) { + && (sqlException.getErrorCode() == MYSQL_ERROR_SPECIFIC_ACCESS_DENIED + || normalizedMessage(sqlException).contains(PROCESS_PRIVILEGE_MESSAGE_MARKER))) { return true; } current = current.getCause(); @@ -300,17 +198,9 @@ static boolean hasMissingOrDeniedMetadataError(Throwable throwable) { Throwable current = throwable; while (current != null) { if (current instanceof SQLException sqlException) { - int errorCode = sqlException.getErrorCode(); - String message = sqlException.getMessage(); - String normalized = message == null ? "" : message.toUpperCase(); - if (errorCode == 1142 || errorCode == 1146 || errorCode == 1227 - || normalized.contains("DATA_LOCK") - || normalized.contains("INNODB_LOCK") - || normalized.contains("PERFORMANCE_SCHEMA") - || normalized.contains("COMMAND DENIED") - || normalized.contains("DOESN'T EXIST") - || normalized.contains("DOES NOT EXIST") - || normalized.contains("PROCESS")) { + String normalized = normalizedMessage(sqlException); + if (LOCK_METADATA_UNAVAILABLE_ERROR_CODES.contains(sqlException.getErrorCode()) + || LOCK_METADATA_UNAVAILABLE_MESSAGE_MARKERS.stream().anyMatch(normalized::contains)) { return true; } } @@ -321,11 +211,16 @@ static boolean hasMissingOrDeniedMetadataError(Throwable throwable) { static RuntimeException sanitizeActiveTransactionQueryException(RuntimeException exception) { if (hasProcessPrivilegeError(exception)) { - return new BusinessException(PROCESS_PRIVILEGE_ERROR_CODE, null, exception); + return new BusinessException(ERROR_CODE_PROCESS_PRIVILEGE_REQUIRED, null, exception); } return exception; } + private static String normalizedMessage(SQLException exception) { + String message = exception.getMessage(); + return message == null ? EMPTY_ERROR_MESSAGE : message.toUpperCase(Locale.ROOT); + } + record LockMetadataQuery(String sql, boolean available, LockMetadataSource source) { } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlActiveTransactionConstants.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlActiveTransactionConstants.java new file mode 100644 index 0000000000..faf882070e --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlActiveTransactionConstants.java @@ -0,0 +1,171 @@ +package ai.chat2db.plugin.mysql.constant; + +import java.util.List; +import java.util.Set; + +public final class MysqlActiveTransactionConstants { + + public static final String RESULT_TRX_ID = "trx_id"; + public static final String RESULT_TRX_STATE = "trx_state"; + public static final String RESULT_TRX_STARTED = "trx_started"; + public static final String RESULT_TRX_AGE_SECONDS = "trx_age_seconds"; + public static final String RESULT_TRX_ISOLATION_LEVEL = "trx_isolation_level"; + public static final String RESULT_TRX_ROWS_LOCKED = "trx_rows_locked"; + public static final String RESULT_TRX_ROWS_MODIFIED = "trx_rows_modified"; + public static final String RESULT_TRX_LOCK_STRUCTS = "trx_lock_structs"; + public static final String RESULT_TRX_MYSQL_THREAD_ID = "trx_mysql_thread_id"; + public static final String RESULT_TRX_QUERY = "trx_query"; + public static final String RESULT_PROCESS_ID = "process_id"; + public static final String RESULT_PROCESS_USER = "process_user"; + public static final String RESULT_PROCESS_HOST = "process_host"; + public static final String RESULT_PROCESS_DB = "process_db"; + public static final String RESULT_REQUESTING_LOCK_ID = "requesting_lock_id"; + public static final String RESULT_BLOCKING_LOCK_ID = "blocking_lock_id"; + public static final String RESULT_BLOCKING_TRX_ID = "blocking_trx_id"; + public static final String RESULT_REQUESTING_PS_THREAD_ID = "requesting_ps_thread_id"; + public static final String RESULT_BLOCKING_PS_THREAD_ID = "blocking_ps_thread_id"; + public static final String RESULT_BLOCKING_THREAD_ID = "blocking_thread_id"; + public static final String RESULT_BLOCKING_PROCESS_ID = "blocking_process_id"; + public static final String RESULT_BLOCKING_USER = "blocking_user"; + public static final String RESULT_BLOCKING_HOST = "blocking_host"; + public static final String RESULT_BLOCKING_DB = "blocking_db"; + public static final String RESULT_WAITING_OBJECT_SCHEMA = "waiting_object_schema"; + public static final String RESULT_WAITING_OBJECT_NAME = "waiting_object_name"; + public static final String RESULT_WAITING_INDEX_NAME = "waiting_index_name"; + public static final String RESULT_WAITING_LOCK_TYPE = "waiting_lock_type"; + public static final String RESULT_WAITING_LOCK_MODE = "waiting_lock_mode"; + public static final String RESULT_WAITING_LOCK_STATUS = "waiting_lock_status"; + public static final String RESULT_WAITING_LOCK_DATA = "waiting_lock_data"; + public static final String RESULT_BLOCKING_OBJECT_SCHEMA = "blocking_object_schema"; + public static final String RESULT_BLOCKING_OBJECT_NAME = "blocking_object_name"; + public static final String RESULT_BLOCKING_INDEX_NAME = "blocking_index_name"; + public static final String RESULT_BLOCKING_LOCK_TYPE = "blocking_lock_type"; + public static final String RESULT_BLOCKING_LOCK_MODE = "blocking_lock_mode"; + public static final String RESULT_BLOCKING_LOCK_STATUS = "blocking_lock_status"; + public static final String RESULT_BLOCKING_LOCK_DATA = "blocking_lock_data"; + + public static final String SQL_ACTIVE_TRANSACTIONS = + "SELECT t.trx_id, t.trx_state, t.trx_started, " + + "TIMESTAMPDIFF(SECOND, t.trx_started, NOW()) AS trx_age_seconds, " + + "t.trx_isolation_level, t.trx_rows_locked, t.trx_rows_modified, " + + "t.trx_lock_structs, t.trx_mysql_thread_id, " + + "p.ID AS process_id, p.USER AS process_user, p.HOST AS process_host, p.DB AS process_db, " + + "t.trx_query, " + + "%s " + + "FROM information_schema.innodb_trx t " + + "LEFT JOIN information_schema.processlist p ON t.trx_mysql_thread_id = p.ID " + + "%s " + + "ORDER BY t.trx_started"; + + public static final String LOCK_COLUMNS_80 = + "w.requesting_lock_id, w.blocking_lock_id, w.blocking_trx_id, " + + "w.requesting_ps_thread_id, w.blocking_ps_thread_id, " + + "w.waiting_object_schema, w.waiting_object_name, w.waiting_index_name, " + + "w.waiting_lock_type, w.waiting_lock_mode, w.waiting_lock_status, w.waiting_lock_data, " + + "w.blocking_object_schema, w.blocking_object_name, w.blocking_index_name, " + + "w.blocking_lock_type, w.blocking_lock_mode, w.blocking_lock_status, w.blocking_lock_data, " + + "bt.trx_mysql_thread_id AS blocking_thread_id, bp.ID AS blocking_process_id, " + + "bp.USER AS blocking_user, bp.HOST AS blocking_host, bp.DB AS blocking_db"; + + public static final String LOCK_JOIN_80 = + "LEFT JOIN (" + + "SELECT dlw.REQUESTING_ENGINE_TRANSACTION_ID AS requesting_trx_id, " + + "dlw.REQUESTING_ENGINE_LOCK_ID AS requesting_lock_id, " + + "dlw.BLOCKING_ENGINE_LOCK_ID AS blocking_lock_id, " + + "dlw.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx_id, " + + "dlw.REQUESTING_THREAD_ID AS requesting_ps_thread_id, " + + "dlw.BLOCKING_THREAD_ID AS blocking_ps_thread_id, " + + "rl.OBJECT_SCHEMA AS waiting_object_schema, rl.OBJECT_NAME AS waiting_object_name, " + + "rl.INDEX_NAME AS waiting_index_name, rl.LOCK_TYPE AS waiting_lock_type, " + + "rl.LOCK_MODE AS waiting_lock_mode, rl.LOCK_STATUS AS waiting_lock_status, " + + "rl.LOCK_DATA AS waiting_lock_data, " + + "bl.OBJECT_SCHEMA AS blocking_object_schema, bl.OBJECT_NAME AS blocking_object_name, " + + "bl.INDEX_NAME AS blocking_index_name, bl.LOCK_TYPE AS blocking_lock_type, " + + "bl.LOCK_MODE AS blocking_lock_mode, bl.LOCK_STATUS AS blocking_lock_status, " + + "bl.LOCK_DATA AS blocking_lock_data " + + "FROM performance_schema.data_lock_waits dlw " + + "LEFT JOIN performance_schema.data_locks rl " + + "ON dlw.REQUESTING_ENGINE_LOCK_ID = rl.ENGINE_LOCK_ID " + + "AND dlw.ENGINE = rl.ENGINE " + + "LEFT JOIN performance_schema.data_locks bl " + + "ON dlw.BLOCKING_ENGINE_LOCK_ID = bl.ENGINE_LOCK_ID " + + "AND dlw.ENGINE = bl.ENGINE" + + ") w ON w.requesting_trx_id = t.trx_id " + + "LEFT JOIN information_schema.innodb_trx bt ON bt.trx_id = w.blocking_trx_id " + + "LEFT JOIN information_schema.processlist bp ON bt.trx_mysql_thread_id = bp.ID"; + + public static final String LOCK_COLUMNS_57 = + "w.requesting_lock_id, w.blocking_lock_id, w.blocking_trx_id, " + + "NULL AS requesting_ps_thread_id, NULL AS blocking_ps_thread_id, " + + "NULL AS waiting_object_schema, w.waiting_object_name, w.waiting_index_name, " + + "w.waiting_lock_type, w.waiting_lock_mode, NULL AS waiting_lock_status, w.waiting_lock_data, " + + "NULL AS blocking_object_schema, w.blocking_object_name, w.blocking_index_name, " + + "w.blocking_lock_type, w.blocking_lock_mode, NULL AS blocking_lock_status, w.blocking_lock_data, " + + "bt.trx_mysql_thread_id AS blocking_thread_id, bp.ID AS blocking_process_id, " + + "bp.USER AS blocking_user, bp.HOST AS blocking_host, bp.DB AS blocking_db"; + + public static final String LOCK_JOIN_57 = + "LEFT JOIN (" + + "SELECT lw.requesting_trx_id, lw.requested_lock_id AS requesting_lock_id, " + + "lw.blocking_lock_id, lw.blocking_trx_id, " + + "rl.lock_table AS waiting_object_name, rl.lock_index AS waiting_index_name, " + + "rl.lock_type AS waiting_lock_type, rl.lock_mode AS waiting_lock_mode, " + + "rl.lock_data AS waiting_lock_data, " + + "bl.lock_table AS blocking_object_name, bl.lock_index AS blocking_index_name, " + + "bl.lock_type AS blocking_lock_type, bl.lock_mode AS blocking_lock_mode, " + + "bl.lock_data AS blocking_lock_data " + + "FROM information_schema.innodb_lock_waits lw " + + "LEFT JOIN information_schema.innodb_locks rl ON lw.requested_lock_id = rl.lock_id " + + "LEFT JOIN information_schema.innodb_locks bl ON lw.blocking_lock_id = bl.lock_id" + + ") w ON w.requesting_trx_id = t.trx_id " + + "LEFT JOIN information_schema.innodb_trx bt ON bt.trx_id = w.blocking_trx_id " + + "LEFT JOIN information_schema.processlist bp ON bt.trx_mysql_thread_id = bp.ID"; + + public static final String LOCK_COLUMNS_UNAVAILABLE = + "NULL AS requesting_lock_id, NULL AS blocking_lock_id, NULL AS blocking_trx_id, " + + "NULL AS requesting_ps_thread_id, NULL AS blocking_ps_thread_id, " + + "NULL AS waiting_object_schema, NULL AS waiting_object_name, NULL AS waiting_index_name, " + + "NULL AS waiting_lock_type, NULL AS waiting_lock_mode, NULL AS waiting_lock_status, " + + "NULL AS waiting_lock_data, " + + "NULL AS blocking_object_schema, NULL AS blocking_object_name, NULL AS blocking_index_name, " + + "NULL AS blocking_lock_type, NULL AS blocking_lock_mode, NULL AS blocking_lock_status, " + + "NULL AS blocking_lock_data, " + + "NULL AS blocking_thread_id, NULL AS blocking_process_id, " + + "NULL AS blocking_user, NULL AS blocking_host, NULL AS blocking_db"; + + public static final String SQL_PROBE_MYSQL80_LOCKS = + "SELECT 1 FROM performance_schema.data_lock_waits WHERE 1 = 0"; + public static final String SQL_PROBE_MYSQL57_LOCKS = + "SELECT 1 FROM information_schema.innodb_lock_waits WHERE 1 = 0"; + public static final String SQL_CONNECTION_INSPECTION = + "SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO\n" + + "FROM information_schema.PROCESSLIST\n" + + "WHERE ID = %d;"; + + public static final String ERROR_CODE_PROCESS_PRIVILEGE_REQUIRED = + "mysql.activeTransaction.processPrivilegeRequired"; + public static final int MYSQL_ERROR_TABLE_ACCESS_DENIED = 1142; + public static final int MYSQL_ERROR_TABLE_NOT_FOUND = 1146; + public static final int MYSQL_ERROR_SPECIFIC_ACCESS_DENIED = 1227; + public static final Set LOCK_METADATA_UNAVAILABLE_ERROR_CODES = Set.of( + MYSQL_ERROR_TABLE_ACCESS_DENIED, + MYSQL_ERROR_TABLE_NOT_FOUND, + MYSQL_ERROR_SPECIFIC_ACCESS_DENIED + ); + public static final String PROCESS_PRIVILEGE_MESSAGE_MARKER = "PROCESS"; + public static final List LOCK_METADATA_UNAVAILABLE_MESSAGE_MARKERS = List.of( + "DATA_LOCK", + "INNODB_LOCK", + "PERFORMANCE_SCHEMA", + "COMMAND DENIED", + "DOESN'T EXIST", + "DOES NOT EXIST", + PROCESS_PRIVILEGE_MESSAGE_MARKER + ); + public static final String LOCK_JOIN_UNAVAILABLE = ""; + public static final String EMPTY_ERROR_MESSAGE = ""; + public static final String QUALIFIED_NAME_SEPARATOR = "."; + + private MysqlActiveTransactionConstants() { + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlActiveTransactionManagerTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlActiveTransactionManagerTest.java index cffabd53b5..b82ed53678 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlActiveTransactionManagerTest.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlActiveTransactionManagerTest.java @@ -7,13 +7,17 @@ import ai.chat2db.community.tools.exception.BusinessException; import org.junit.jupiter.api.Test; +import java.io.IOException; import java.lang.reflect.Proxy; +import java.nio.file.Files; +import java.nio.file.Path; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import static ai.chat2db.plugin.mysql.constant.MysqlActiveTransactionConstants.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; @@ -26,12 +30,14 @@ class MysqlActiveTransactionManagerTest { @Test void recognizesProcessPrivilegeFailuresThroughWrappedCauses() { SQLException denied = new SQLException( - "Access denied; you need (at least one of) the PROCESS privilege", "42000", 1227); + "Access denied; you need (at least one of) the PROCESS privilege", + "42000", + MYSQL_ERROR_SPECIFIC_ACCESS_DENIED); assertTrue(MysqlActiveTransactionManager.hasProcessPrivilegeError( new IllegalStateException("execution failed", denied))); assertFalse(MysqlActiveTransactionManager.hasProcessPrivilegeError( - new SQLException("table not found", "42S02", 1146))); + new SQLException("table not found", "42S02", MYSQL_ERROR_TABLE_NOT_FOUND))); } @Test @@ -60,9 +66,11 @@ void unavailableLockMetadataQueryKeepsLiveTransactionSource() { @Test void recognizesMissingOrDeniedLockMetadataFailures() { assertTrue(MysqlActiveTransactionManager.hasMissingOrDeniedMetadataError( - new SQLException("Table 'performance_schema.data_lock_waits' doesn't exist", "42S02", 1146))); + new SQLException("Table 'performance_schema.data_lock_waits' doesn't exist", + "42S02", MYSQL_ERROR_TABLE_NOT_FOUND))); assertTrue(MysqlActiveTransactionManager.hasMissingOrDeniedMetadataError( - new SQLException("SELECT command denied to user for table 'data_locks'", "42000", 1142))); + new SQLException("SELECT command denied to user for table 'data_locks'", + "42000", MYSQL_ERROR_TABLE_ACCESS_DENIED))); assertFalse(MysqlActiveTransactionManager.hasMissingOrDeniedMetadataError( new SQLException("Syntax error", "42000", 1064))); } @@ -70,24 +78,24 @@ void recognizesMissingOrDeniedLockMetadataFailures() { @Test void mapsResultSetToTypedTransactionResponse() throws SQLException { ResultSet resultSet = resultSet(Map.ofEntries( - Map.entry("trx_id", "421337"), - Map.entry("trx_state", "LOCK WAIT"), - Map.entry("trx_started", Timestamp.valueOf("2026-08-31 12:00:00")), - Map.entry("trx_age_seconds", 12L), - Map.entry("trx_isolation_level", "REPEATABLE READ"), - Map.entry("trx_rows_locked", 1L), - Map.entry("trx_rows_modified", 0L), - Map.entry("trx_lock_structs", 2L), - Map.entry("trx_mysql_thread_id", 45L), - Map.entry("process_id", 45L), - Map.entry("process_user", "ops002_user"), - Map.entry("process_host", "127.0.0.1:50000"), - Map.entry("process_db", "ops002_test"), - Map.entry("requesting_lock_id", "421337:7:3:2"), - Map.entry("blocking_lock_id", "421336:7:3:2"), - Map.entry("blocking_trx_id", "421336"), - Map.entry("blocking_thread_id", 44L), - Map.entry("blocking_process_id", 44L) + Map.entry(RESULT_TRX_ID, "421337"), + Map.entry(RESULT_TRX_STATE, "LOCK WAIT"), + Map.entry(RESULT_TRX_STARTED, Timestamp.valueOf("2026-08-31 12:00:00")), + Map.entry(RESULT_TRX_AGE_SECONDS, 12L), + Map.entry(RESULT_TRX_ISOLATION_LEVEL, "REPEATABLE READ"), + Map.entry(RESULT_TRX_ROWS_LOCKED, 1L), + Map.entry(RESULT_TRX_ROWS_MODIFIED, 0L), + Map.entry(RESULT_TRX_LOCK_STRUCTS, 2L), + Map.entry(RESULT_TRX_MYSQL_THREAD_ID, 45L), + Map.entry(RESULT_PROCESS_ID, 45L), + Map.entry(RESULT_PROCESS_USER, "ops002_user"), + Map.entry(RESULT_PROCESS_HOST, "127.0.0.1:50000"), + Map.entry(RESULT_PROCESS_DB, "ops002_test"), + Map.entry(RESULT_REQUESTING_LOCK_ID, "421337:7:3:2"), + Map.entry(RESULT_BLOCKING_LOCK_ID, "421336:7:3:2"), + Map.entry(RESULT_BLOCKING_TRX_ID, "421336"), + Map.entry(RESULT_BLOCKING_THREAD_ID, 44L), + Map.entry(RESULT_BLOCKING_PROCESS_ID, 44L) )); ActiveTransaction transaction = MysqlActiveTransactionManager.readTransaction( @@ -116,10 +124,10 @@ void mapsResultSetToTypedTransactionResponse() throws SQLException { @Test void unavailableConnectionsDoNotExposeInspectionSql() throws SQLException { ResultSet resultSet = resultSet(Map.ofEntries( - Map.entry("trx_id", "421338"), - Map.entry("trx_state", "RUNNING"), - Map.entry("trx_started", Timestamp.valueOf("2026-08-31 12:00:00")), - Map.entry("trx_mysql_thread_id", 46L) + Map.entry(RESULT_TRX_ID, "421338"), + Map.entry(RESULT_TRX_STATE, "RUNNING"), + Map.entry(RESULT_TRX_STARTED, Timestamp.valueOf("2026-08-31 12:00:00")), + Map.entry(RESULT_TRX_MYSQL_THREAD_ID, 46L) )); ActiveTransaction transaction = MysqlActiveTransactionManager.readTransaction( @@ -136,16 +144,32 @@ void unavailableConnectionsDoNotExposeInspectionSql() throws SQLException { @Test void fallbackProcessPrivilegeFailureUsesSanitizedBusinessCode() { RuntimeException executionFailure = new IllegalStateException("execution failed", - new SQLException("Access denied; you need (at least one of) the PROCESS privilege", "42000", 1227)); + new SQLException("Access denied; you need (at least one of) the PROCESS privilege", + "42000", MYSQL_ERROR_SPECIFIC_ACCESS_DENIED)); BusinessException exception = assertThrows(BusinessException.class, () -> { throw MysqlActiveTransactionManager.sanitizeActiveTransactionQueryException(executionFailure); }); - assertEquals(MysqlActiveTransactionManager.PROCESS_PRIVILEGE_ERROR_CODE, exception.getCode()); + assertEquals(ERROR_CODE_PROCESS_PRIVILEGE_REQUIRED, exception.getCode()); assertSame(executionFailure, exception.getCause()); } + @Test + void managerDoesNotInlineSqlOrResultColumnNames() throws IOException { + Path sourcePath = Path.of( + System.getProperty("basedir"), + "src/main/java/ai/chat2db/plugin/mysql/MysqlActiveTransactionManager.java" + ); + String source = Files.readString(sourcePath); + + assertFalse(source.contains("resultSet.getString(\"")); + assertFalse(source.contains("resultSet.getTimestamp(\"")); + assertFalse(source.contains("nullableLong(resultSet, \"")); + assertFalse(source.contains("information_schema")); + assertFalse(source.contains("performance_schema")); + } + @Test void nonProcessFallbackFailureRemainsOriginalRuntimeException() { RuntimeException executionFailure = new IllegalStateException("syntax error", From 20e91222a810dee8af7be623fab707c2262acc09 Mon Sep 17 00:00:00 2001 From: HandSonic <8078023+handsonic@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:32:02 +0800 Subject: [PATCH 49/49] fix(notification): ignore stale refreshes --- .../src/components/NotificationNav/index.tsx | 9 ++- .../notificationRefreshCoordinator.test.ts | 81 +++++++++++++++++++ .../notificationRefreshCoordinator.ts | 27 +++++++ .../src/utils/mainPageNavigation.test.ts | 1 + 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 chat2db-community-client/src/components/NotificationNav/notificationRefreshCoordinator.test.ts create mode 100644 chat2db-community-client/src/components/NotificationNav/notificationRefreshCoordinator.ts diff --git a/chat2db-community-client/src/components/NotificationNav/index.tsx b/chat2db-community-client/src/components/NotificationNav/index.tsx index 3d9a12ac29..4e1cd32e54 100644 --- a/chat2db-community-client/src/components/NotificationNav/index.tsx +++ b/chat2db-community-client/src/components/NotificationNav/index.tsx @@ -9,6 +9,7 @@ import { openWebPage } from '@/utils/url'; import { Empty, EmptyImage, IconButton, IconfontSvg, Modal } from '@chat2db/ui'; import { RefreshCcw } from 'lucide-react'; import { useStyles } from './styles'; +import { NotificationRefreshCoordinator } from './notificationRefreshCoordinator'; interface NotificationButtonProps { /** Drawer mode is controlled by the parent and does not render an icon button. */ @@ -28,6 +29,7 @@ const NotificationButton = ({ drawerMode, open: externalOpen, onClose }: Notific const [popNoSData, setNoSPopData] = useState(null); const [modalSizeWidth, setModalSizeWidth] = useState(); const mountedRef = useRef(true); + const refreshCoordinatorRef = useRef(new NotificationRefreshCoordinator()); useEffect(() => { mountedRef.current = true; @@ -36,6 +38,7 @@ const NotificationButton = ({ drawerMode, open: externalOpen, onClose }: Notific queryPopNotification(); return () => { mountedRef.current = false; + refreshCoordinatorRef.current.invalidateAll(); }; }, []); @@ -58,15 +61,17 @@ const NotificationButton = ({ drawerMode, open: externalOpen, onClose }: Notific }, [popSOpen]); const queryUnreadCount = () => { + const owner = refreshCoordinatorRef.current.begin('unread'); notificationService.queryUnreadCount().then((res) => { - if (!mountedRef.current) return; + if (!mountedRef.current || !refreshCoordinatorRef.current.isCurrent(owner)) return; setHasUnread(res > 0); }); }; const queryNotificationList = () => { + const owner = refreshCoordinatorRef.current.begin('list'); notificationService.queryNotificationList({ pageNo: 1, pageSize: 50 }).then((res) => { - if (!mountedRef.current) return; + if (!mountedRef.current || !refreshCoordinatorRef.current.isCurrent(owner)) return; setList(res.data); }); }; diff --git a/chat2db-community-client/src/components/NotificationNav/notificationRefreshCoordinator.test.ts b/chat2db-community-client/src/components/NotificationNav/notificationRefreshCoordinator.test.ts new file mode 100644 index 0000000000..cb25bd3849 --- /dev/null +++ b/chat2db-community-client/src/components/NotificationNav/notificationRefreshCoordinator.test.ts @@ -0,0 +1,81 @@ +import assert from 'node:assert/strict'; +import { + NotificationRefreshCoordinator, + type NotificationRefreshChannel, +} from './notificationRefreshCoordinator'; + +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((next) => { + resolve = next; + }); + return { promise, resolve }; +} + +async function commitWhenCurrent( + coordinator: NotificationRefreshCoordinator, + channel: NotificationRefreshChannel, + response: Promise, + commits: string[], +) { + const owner = coordinator.begin(channel); + const value = await response; + if (coordinator.isCurrent(owner)) { + commits.push(value); + } +} + +async function run() { + const coordinator = new NotificationRefreshCoordinator(); + const listCommits: string[] = []; + const unreadCommits: string[] = []; + const mountList = deferred(); + const mountUnread = deferred(); + const refreshList = deferred(); + const refreshUnread = deferred(); + + const mountListRequest = commitWhenCurrent(coordinator, 'list', mountList.promise, listCommits); + const mountUnreadRequest = commitWhenCurrent(coordinator, 'unread', mountUnread.promise, unreadCommits); + const refreshListRequest = commitWhenCurrent(coordinator, 'list', refreshList.promise, listCommits); + const refreshUnreadRequest = commitWhenCurrent(coordinator, 'unread', refreshUnread.promise, unreadCommits); + + refreshList.resolve('refresh-list'); + refreshUnread.resolve('refresh-unread'); + await Promise.all([refreshListRequest, refreshUnreadRequest]); + mountList.resolve('mount-list'); + mountUnread.resolve('mount-unread'); + await Promise.all([mountListRequest, mountUnreadRequest]); + + const activeListOwner = coordinator.begin('list'); + coordinator.begin('unread'); + const listSurvivesUnreadRefresh = coordinator.isCurrent(activeListOwner); + + const pendingAtUnmount = coordinator.begin('list'); + coordinator.invalidateAll(); + const unmountedOwnerIsCurrent = coordinator.isCurrent(pendingAtUnmount); + const remountedOwner = coordinator.begin('list'); + + assert.deepEqual( + { + listCommits, + unreadCommits, + listSurvivesUnreadRefresh, + unmountedOwnerIsCurrent, + remountedOwnerIsCurrent: coordinator.isCurrent(remountedOwner), + }, + { + listCommits: ['refresh-list'], + unreadCommits: ['refresh-unread'], + listSurvivesUnreadRefresh: true, + unmountedOwnerIsCurrent: false, + remountedOwnerIsCurrent: true, + }, + ); + + console.log('Notification refresh coordinator tests passed.'); +} + +run().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/chat2db-community-client/src/components/NotificationNav/notificationRefreshCoordinator.ts b/chat2db-community-client/src/components/NotificationNav/notificationRefreshCoordinator.ts new file mode 100644 index 0000000000..af414cedd5 --- /dev/null +++ b/chat2db-community-client/src/components/NotificationNav/notificationRefreshCoordinator.ts @@ -0,0 +1,27 @@ +export type NotificationRefreshChannel = 'list' | 'unread'; + +export interface NotificationRefreshOwner { + channel: NotificationRefreshChannel; + generation: number; +} + +export class NotificationRefreshCoordinator { + private generations: Record = { + list: 0, + unread: 0, + }; + + begin(channel: NotificationRefreshChannel): NotificationRefreshOwner { + this.generations[channel] += 1; + return { channel, generation: this.generations[channel] }; + } + + isCurrent(owner: NotificationRefreshOwner): boolean { + return this.generations[owner.channel] === owner.generation; + } + + invalidateAll(): void { + this.generations.list += 1; + this.generations.unread += 1; + } +} diff --git a/chat2db-community-client/src/utils/mainPageNavigation.test.ts b/chat2db-community-client/src/utils/mainPageNavigation.test.ts index 86c2f6e7c2..7fa1f8306a 100644 --- a/chat2db-community-client/src/utils/mainPageNavigation.test.ts +++ b/chat2db-community-client/src/utils/mainPageNavigation.test.ts @@ -1,4 +1,5 @@ import assert from 'node:assert/strict'; +import '../components/NotificationNav/notificationRefreshCoordinator.test'; import { createMainRootRoute, DEFAULT_MAIN_PAGE_ACTIVE_TAB,