diff --git a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts index b3804dad2..1d69593f6 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts @@ -314,6 +314,48 @@ describe("statementRangeAtCursor", () => { expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]); }); + it("keeps MySQL CREATE TABLE options with table comments", () => { + const sql = `CREATE TABLE test_1 ( + id bigint NOT NULL AUTO_INCREMENT COMMENT '主键id', + deleted tinyint NOT NULL DEFAULT 0 COMMENT '删除标志(0:有效 1:无效)', + locked tinyint NOT NULL DEFAULT 0 COMMENT '是否锁定(0.否,1.是)', + version int NOT NULL DEFAULT 0 COMMENT '版本号', + creatorId bigint DEFAULT NULL COMMENT '创建人ID', + createBy varchar(100) DEFAULT NULL COMMENT '创建人名称', + createdTime timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updaterId bigint DEFAULT NULL COMMENT '修改人ID', + updatedBy varchar(100) DEFAULT NULL COMMENT '修改人名称', + updatedTime timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', + PRIMARY KEY (id) +) +ENGINE = INNODB, +CHARACTER SET utf8mb4, +COLLATE utf8mb4_general_ci, +COMMENT = '测试';`; + + expect(statementRangeAtCursor(sql, indexOf(sql, "CREATE"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1)); + expect(statementRangeAtCursor(sql, indexOf(sql, "COMMENT ="), "mysql")?.sql.trim()).toBe(sql.slice(0, -1)); + expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]); + expect(statementRangeAtCursor(sql, indexOf(sql, "COMMENT ="))?.sql.trim()).toBe(sql.slice(0, -1)); + expect(rangeSqlTexts(executableStatementRanges(sql))).toEqual([sql.slice(0, -1)]); + }); + + it("keeps MySQL CREATE TABLE comments without equals as table options", () => { + const sql = "CREATE TABLE test_2 (\n id bigint NOT NULL\n)\nCOMMENT '测试';"; + + expect(statementRangeAtCursor(sql, indexOf(sql, "COMMENT"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1)); + expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]); + expect(statementRangeAtCursor(sql, indexOf(sql, "COMMENT"))?.sql.trim()).toBe(sql.slice(0, -1)); + expect(rangeSqlTexts(executableStatementRanges(sql))).toEqual([sql.slice(0, -1)]); + }); + + it("does not merge standard COMMENT ON statements into preceding CREATE TABLE statements", () => { + const sql = "CREATE TABLE users (id int)\nCOMMENT ON TABLE users IS 'Users';"; + + expect(rangeSqlTexts(executableStatementRanges(sql, "postgres"))).toEqual(["CREATE TABLE users (id int)", "COMMENT ON TABLE users IS 'Users'"]); + expect(rangeSqlTexts(executableStatementRanges(sql))).toEqual(["CREATE TABLE users (id int)", "COMMENT ON TABLE users IS 'Users'"]); + }); + it("keeps MySQL ALTER TABLE drop column clauses with the statement", () => { const sql = "ALTER TABLE t\n DROP COLUMN a,\n DROP COLUMN b;"; diff --git a/apps/desktop/src/lib/sql/sqlStatementRanges.ts b/apps/desktop/src/lib/sql/sqlStatementRanges.ts index 0419e4dd4..e4c8377cc 100644 --- a/apps/desktop/src/lib/sql/sqlStatementRanges.ts +++ b/apps/desktop/src/lib/sql/sqlStatementRanges.ts @@ -106,6 +106,7 @@ const SET_OPERATION_KEYWORDS = new Set(["UNION", "INTERSECT", "EXCEPT", "MINUS"] const SET_OPERATION_MODIFIER_KEYWORDS = new Set(["ALL", "DISTINCT"]); const ORACLE_LIKE_PL_SQL_DATABASES: ReadonlySet = new Set(["oracle", "dameng", "gaussdb", "yashandb", "oscar", "oceanbase-oracle"]); const MYSQL_ROUTINE_BLOCK_DATABASES: ReadonlySet = new Set(["mysql", "doris", "starrocks", "manticoresearch", "goldendb"]); +const MYSQL_CREATE_TABLE_OPTION_DATABASES: ReadonlySet = new Set(["mysql", "doris", "starrocks", "manticoresearch", "goldendb", "gbase"]); const MYSQL_ROUTINE_OBJECT_TYPES = new Set(["PROCEDURE", "FUNCTION", "TRIGGER", "EVENT"]); const MYSQL_NON_ROUTINE_CREATE_TYPES = new Set(["DATABASE", "INDEX", "LOGFILE", "ROLE", "SCHEMA", "SERVER", "SPATIAL", "TABLE", "TEMPORARY", "UNIQUE", "USER", "VIEW"]); const MYSQL_CONTROL_BLOCK_SUFFIXES = new Set(["IF", "LOOP", "CASE", "REPEAT", "WHILE"]); @@ -503,6 +504,10 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d continue; } + if (currentBodyKeyword === "CREATE" && isMysqlCreateTableOptionContinuation(sql, statement.from, lineStart.from, lineStart.keyword, databaseType)) { + continue; + } + if (currentBodyKeyword === "INSERT" && INSERT_BODY_KEYWORDS.has(lineStart.keyword)) { continue; } @@ -730,6 +735,20 @@ function isSetOperationQueryContinuation(sql: string, from: number, to: number, return false; } +function isMysqlCreateTableOptionContinuation(sql: string, statementFrom: number, lineStartFrom: number, keyword: string, databaseType?: DatabaseType): boolean { + if (databaseType && !MYSQL_CREATE_TABLE_OPTION_DATABASES.has(databaseType)) return false; + if (keyword !== "COMMENT") return false; + if (!startsWithMysqlCreateTable(sql, statementFrom)) return false; + + const next = nextNonWhitespaceChar(sql, lineStartFrom + keyword.length); + return next === "=" || next === "'" || next === '"'; +} + +function startsWithMysqlCreateTable(sql: string, statementFrom: number): boolean { + const text = sql.slice(statementFrom, statementFrom + 256); + return /^CREATE\s+(?:TEMPORARY\s+)?TABLE\b/i.test(text); +} + function topLevelWordsBefore(sql: string, from: number, to: number, limit: number): string[] { const words: string[] = []; let state: QuoteState | "lineComment" | "blockComment" = "none";