From b16614bc1c6fd10dacb7971573efec845b334edb Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Wed, 3 Jun 2026 03:26:57 +0000 Subject: [PATCH] refactor: remove built-in hive driver (#2935) --- sqle/docs/docs.go | 3 - sqle/docs/swagger.json | 3 - sqle/docs/swagger.yaml | 2 - sqle/driver/hive/diff_table.go | 784 ----------------- sqle/driver/hive/hive.go | 1020 ---------------------- sqle/driver/hive/hive_compare_test.go | 1136 ------------------------- sqle/driver/hive/hive_test.go | 857 ------------------- sqle/driver/hive/logo.go | 5 - sqle/server/sqled.go | 1 - 9 files changed, 3811 deletions(-) delete mode 100644 sqle/driver/hive/diff_table.go delete mode 100644 sqle/driver/hive/hive.go delete mode 100644 sqle/driver/hive/hive_compare_test.go delete mode 100644 sqle/driver/hive/hive_test.go delete mode 100644 sqle/driver/hive/logo.go diff --git a/sqle/docs/docs.go b/sqle/docs/docs.go index b06ca4bca..9e990882d 100644 --- a/sqle/docs/docs.go +++ b/sqle/docs/docs.go @@ -16345,9 +16345,6 @@ var doc = `{ "db_type": { "type": "string" }, - "default_port": { - "type": "integer" - }, "params": { "type": "array", "items": { diff --git a/sqle/docs/swagger.json b/sqle/docs/swagger.json index cbe47c4f2..bc7dc526a 100644 --- a/sqle/docs/swagger.json +++ b/sqle/docs/swagger.json @@ -16329,9 +16329,6 @@ "db_type": { "type": "string" }, - "default_port": { - "type": "integer" - }, "params": { "type": "array", "items": { diff --git a/sqle/docs/swagger.yaml b/sqle/docs/swagger.yaml index 98e524b8e..039a69bb2 100644 --- a/sqle/docs/swagger.yaml +++ b/sqle/docs/swagger.yaml @@ -1809,8 +1809,6 @@ definitions: properties: db_type: type: string - default_port: - type: integer params: items: $ref: '#/definitions/v1.InstanceAdditionalParamResV1' diff --git a/sqle/driver/hive/diff_table.go b/sqle/driver/hive/diff_table.go deleted file mode 100644 index 6e8610cd9..000000000 --- a/sqle/driver/hive/diff_table.go +++ /dev/null @@ -1,784 +0,0 @@ -// Package hive provides the built-in Hive driver for sqle-ee. This file -// implements diffTableDDL, the helper that compares two Hive CREATE TABLE -// statements and decides whether the difference can be applied with ALTER -// statements or must fall back to DROP + CREATE. -// -// The implementation follows docs/spec/design.md §3.4 (variant SQL generation -// matrix, D3 decision) and the Hive ALTER capability matrix in -// docs/spec/exploration.md §4. It addresses compat-RISK-6: when a difference -// cannot be safely expressed with ALTER (partition key changes, storage -// format changes, SerDe changes, EXTERNAL flag changes, incompatible column -// type changes, column deletion), the caller must emit DROP+CREATE with a -// data-loss WARNING. -// -// Per project convention (dev.md "非必要少用正则表达式"), this parser is -// section-based and operates on trimmed/uppercased keywords rather than -// regular expressions. It is robust to whitespace and case differences. -package hive - -import ( - "fmt" - "sort" - "strings" -) - -// runtimeTBLPropertyKeys lists TBLPROPERTIES keys that Hive maintains -// internally and that change frequently as data is written. These must be -// filtered out before comparing TBLPROPERTIES sets so that the diff matrix -// does not produce false-positive DROP+CREATE results (compat-RISK-6). -// -// Source: design.md §3.4 + exploration.md §4. -var runtimeTBLPropertyKeys = map[string]struct{}{ - "transient_lastDdlTime": {}, - "numFiles": {}, - "numRows": {}, - "rawDataSize": {}, - "totalSize": {}, - "COLUMN_STATS_ACCURATE": {}, -} - -// hiveTableSchema captures the structural elements of a Hive table that -// matter for diff decisions. The struct deliberately ignores presentation -// concerns (quoting, whitespace, comment line position) so that two DDLs -// that are semantically identical compare equal. -type hiveTableSchema struct { - // columns is the ordered list of column definitions. Order matters - // because Hive ALTER TABLE ADD COLUMNS appends at the end. - columns []hiveColumn - // partitionedBy is the ordered list of partition columns. A non-empty - // difference always forces DROP+CREATE because Hive cannot change - // partition keys via ALTER (design §3.4). - partitionedBy []hiveColumn - // storedAs is the file format (TEXTFILE, ORC, PARQUET, AVRO, ...). An - // empty string means the DDL did not declare STORED AS explicitly. - storedAs string - // rowFormat captures ROW FORMAT clauses including SERDE class and SERDE - // properties. A difference forces DROP+CREATE. - rowFormat string - // tblProperties contains TBLPROPERTIES with runtime keys filtered out. - tblProperties map[string]string - // external is true when the CREATE TABLE statement is CREATE EXTERNAL - // TABLE. Toggling EXTERNAL is allowed via ALTER per design §3.4 but - // only via TBLPROPERTIES SET; the parser captures it here so the diff - // can detect the toggle. - external bool - // location is the LOCATION clause value. A difference is treated as an - // ALTER-able change (ALTER TABLE ... SET LOCATION). - location string - // tableName is the unqualified table name extracted from the DDL. - // Used to compose ALTER statements. - tableName string - // comment captures the COMMENT 'xxx' table-level comment (separate from - // TBLPROPERTIES). Hive supports changing the table comment via - // ALTER TABLE ... SET TBLPROPERTIES('comment'='new'), so this is an - // ALTER-able difference. - comment string -} - -// hiveColumn is one column definition: name + type + optional COMMENT. -type hiveColumn struct { - name string - colType string - comment string -} - -// diffTableDDL compares two Hive CREATE TABLE statements (base = source, -// target = destination) and returns the variant SQL strategy: -// -// - alterStmts: a sequence of ALTER TABLE statements that, applied in -// order on the target side, will reconcile its structure with the base. -// Empty when fallbackDropCreate is true. -// - fallbackDropCreate: true when at least one structural difference falls -// into the "B. DROP+CREATE" bucket per design.md §3.4 (partition key, -// storage format, ROW FORMAT/SerDe, EXTERNAL toggle, incompatible -// column type change, column deletion, or combinations thereof). -// - err: non-nil only when the DDL strings could not be parsed. -// -// When base and target are semantically identical, returns (nil, false, nil). -func diffTableDDL(base, target string) (alterStmts []string, fallbackDropCreate bool, err error) { - baseSchema, err := parseHiveTableDDL(base) - if err != nil { - return nil, false, fmt.Errorf("parse base DDL: %v", err) - } - targetSchema, err := parseHiveTableDDL(target) - if err != nil { - return nil, false, fmt.Errorf("parse target DDL: %v", err) - } - - // Prefer the base table name when emitting ALTER statements; if base is - // missing the name (unlikely but defensive) fall back to target. - tableName := baseSchema.tableName - if tableName == "" { - tableName = targetSchema.tableName - } - - // === DROP+CREATE triggers (design §3.4) === - - // 1. Partition key changes (any add / remove / type-change / reorder). - if !columnsEqual(baseSchema.partitionedBy, targetSchema.partitionedBy) { - return nil, true, nil - } - // 2. STORED AS changes. - if !equalFold(baseSchema.storedAs, targetSchema.storedAs) { - return nil, true, nil - } - // 3. ROW FORMAT / SerDe changes. - if !equalFold(baseSchema.rowFormat, targetSchema.rowFormat) { - return nil, true, nil - } - // 4. EXTERNAL toggle. Per design §3.4 EXTERNAL toggling itself is - // "ALTER able" via TBLPROPERTIES('EXTERNAL'='TRUE'); we treat the - // keyword-level toggle in CREATE TABLE as an indicator and emit the - // corresponding ALTER SET TBLPROPERTIES statement rather than - // DROP+CREATE. - // 5. Incompatible column type change OR column deletion. - colAlters, colTypeIncompat, colDeleted := diffColumns( - baseSchema.columns, targetSchema.columns, tableName) - if colTypeIncompat || colDeleted { - return nil, true, nil - } - - // === ALTER path: collect statements === - - // Columns first (ADD, CHANGE for rename/widen/comment). - alterStmts = append(alterStmts, colAlters...) - - // EXTERNAL toggle via TBLPROPERTIES. - if baseSchema.external != targetSchema.external { - val := "FALSE" - if baseSchema.external { - val = "TRUE" - } - alterStmts = append(alterStmts, fmt.Sprintf( - "ALTER TABLE %s SET TBLPROPERTIES ('EXTERNAL'='%s');", tableName, val)) - } - - // Table comment change. - if baseSchema.comment != targetSchema.comment { - alterStmts = append(alterStmts, fmt.Sprintf( - "ALTER TABLE %s SET TBLPROPERTIES ('comment'='%s');", - tableName, escapeProperty(baseSchema.comment))) - } - - // LOCATION change (ALTER-able per design §3.4). - if !equalFold(baseSchema.location, targetSchema.location) { - if baseSchema.location != "" { - alterStmts = append(alterStmts, fmt.Sprintf( - "ALTER TABLE %s SET LOCATION '%s';", tableName, baseSchema.location)) - } - } - - // TBLPROPERTIES (business-level keys; runtime keys already filtered). - tblAlters := diffTBLProperties(baseSchema.tblProperties, targetSchema.tblProperties, tableName) - alterStmts = append(alterStmts, tblAlters...) - - return alterStmts, false, nil -} - -// diffColumns walks base/target column lists and returns ALTER statements to -// reconcile them. It also reports whether any difference is "incompatible" -// (forcing DROP+CREATE). -// -// Rules (design §3.4 row "TABLE / 改列类型"): -// - same column count, same names, only type widening or comment change -// -> ALTER CHANGE COLUMN per affected column (compatible). -// - rename (same position, different name) -> ALTER CHANGE COLUMN. -// - base has new columns at the end that target lacks -> ALTER ADD COLUMNS. -// - target has columns that base lacks (target column deletion from -// base perspective) -> column deletion, forces DROP+CREATE. -// - column type change that is not a widening direction -> incompatible. -// -// "Widening" is conservatively limited to the pairs explicitly listed in -// design §3.4 ("int → bigint" etc.). Everything else is incompatible. -func diffColumns(base, target []hiveColumn, tableName string) (alters []string, incompatible, deleted bool) { - // Detect deletion first: any target column whose name is not in base - // (case-insensitive) signals a column removal from base → target. - baseNames := make(map[string]int) - for i, c := range base { - baseNames[strings.ToLower(c.name)] = i - } - targetNames := make(map[string]int) - for i, c := range target { - targetNames[strings.ToLower(c.name)] = i - } - for _, c := range target { - if _, ok := baseNames[strings.ToLower(c.name)]; !ok { - deleted = true - return - } - } - - // For each column in base, decide ADD vs CHANGE vs no-op. - for _, bc := range base { - idx, ok := targetNames[strings.ToLower(bc.name)] - if !ok { - // New column in base → ADD COLUMNS. - col := fmt.Sprintf("%s %s", bc.name, bc.colType) - if bc.comment != "" { - col += fmt.Sprintf(" COMMENT '%s'", escapeProperty(bc.comment)) - } - alters = append(alters, fmt.Sprintf( - "ALTER TABLE %s ADD COLUMNS (%s);", tableName, col)) - continue - } - tc := target[idx] - - // Same name -> compare type + comment. - if !typesEqual(bc.colType, tc.colType) { - if !isCompatibleTypeChange(tc.colType, bc.colType) { - incompatible = true - return - } - // Compatible: emit CHANGE COLUMN. - alters = append(alters, formatChangeColumn(tableName, bc)) - continue - } - if bc.comment != tc.comment { - // Only comment differs. - alters = append(alters, formatChangeColumn(tableName, bc)) - } - } - return alters, incompatible, deleted -} - -// formatChangeColumn produces `ALTER TABLE t CHANGE COLUMN c c type COMMENT 'x';` -func formatChangeColumn(tableName string, c hiveColumn) string { - s := fmt.Sprintf("ALTER TABLE %s CHANGE COLUMN %s %s %s", - tableName, c.name, c.name, c.colType) - if c.comment != "" { - s += fmt.Sprintf(" COMMENT '%s'", escapeProperty(c.comment)) - } - return s + ";" -} - -// isCompatibleTypeChange reports whether changing a column from `from` to -// `to` is safe (no data loss). We support the widening pairs documented in -// design §3.4: int → bigint, smallint → int / bigint, tinyint → smallint / -// int / bigint, float → double, char/varchar → string (Hive treats string as -// the widest text). Everything else is treated as incompatible. -func isCompatibleTypeChange(from, to string) bool { - f := normalizeType(from) - t := normalizeType(to) - if f == t { - return true - } - widening := map[string]map[string]bool{ - "tinyint": {"smallint": true, "int": true, "bigint": true}, - "smallint": {"int": true, "bigint": true}, - "int": {"bigint": true}, - "float": {"double": true}, - "char": {"string": true, "varchar": true}, - "varchar": {"string": true}, - } - if dest, ok := widening[f]; ok { - return dest[t] - } - return false -} - -// typesEqual normalizes whitespace and case before comparing. -func typesEqual(a, b string) bool { - return normalizeType(a) == normalizeType(b) -} - -// normalizeType collapses whitespace, lowercases, and strips the size -// specifier from char/varchar so "varchar(20)" compares equal to "varchar". -// This matches Hive's permissive widening semantics: only the base type -// matters for the widening matrix. -func normalizeType(t string) string { - t = strings.ToLower(strings.TrimSpace(t)) - if i := strings.IndexByte(t, '('); i >= 0 { - t = t[:i] - } - return strings.TrimSpace(t) -} - -// columnsEqual reports whether two column lists are identical in name, -// type, and order (comment is ignored; partition keys carry no comment). -func columnsEqual(a, b []hiveColumn) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if !strings.EqualFold(a[i].name, b[i].name) { - return false - } - if !typesEqual(a[i].colType, b[i].colType) { - return false - } - } - return true -} - -// diffTBLProperties emits ALTER TABLE ... SET TBLPROPERTIES statements for -// keys whose values differ. Both inputs already have runtime keys filtered. -// The output is deterministic (keys sorted lexicographically). -func diffTBLProperties(base, target map[string]string, tableName string) []string { - var changed []string - keys := make(map[string]struct{}) - for k := range base { - keys[k] = struct{}{} - } - for k := range target { - keys[k] = struct{}{} - } - sortedKeys := make([]string, 0, len(keys)) - for k := range keys { - sortedKeys = append(sortedKeys, k) - } - sort.Strings(sortedKeys) - - var setParts []string - for _, k := range sortedKeys { - if base[k] != target[k] { - // Even "delete from base" → emit empty-string set (Hive does not - // support UNSET via SET; UNSET TBLPROPERTIES exists separately, - // but for our diff we treat removal as setting empty). - setParts = append(setParts, fmt.Sprintf("'%s'='%s'", - escapeProperty(k), escapeProperty(base[k]))) - } - } - if len(setParts) > 0 { - changed = append(changed, fmt.Sprintf( - "ALTER TABLE %s SET TBLPROPERTIES (%s);", - tableName, strings.Join(setParts, ", "))) - } - return changed -} - -// escapeProperty replaces single quotes inside property values with the -// Hive-safe doubled form ''. -func escapeProperty(s string) string { - return strings.ReplaceAll(s, "'", "''") -} - -// equalFold is shorthand for case-insensitive equality. -func equalFold(a, b string) bool { - return strings.EqualFold(strings.TrimSpace(a), strings.TrimSpace(b)) -} - -// parseHiveTableDDL parses a Hive CREATE TABLE statement into a -// hiveTableSchema. The parser is intentionally simple and section-based: -// it walks the DDL token by token (keyword-driven) and collects each -// section's body into the corresponding hiveTableSchema field. It does NOT -// attempt to validate the SQL — invalid DDL still produces a partial schema. -// -// Why no regex (per dev.md "非必要少用正则表达式"): -// - Hive DDL nesting (parentheses inside TBLPROPERTIES values, quoted -// SerDe property strings) is difficult to express precisely with -// regex without backtracking pitfalls. -// - A section-driven loop is easier to extend (new section types added -// in later Hive versions). -func parseHiveTableDDL(ddl string) (*hiveTableSchema, error) { - if strings.TrimSpace(ddl) == "" { - return nil, fmt.Errorf("empty DDL") - } - s := &hiveTableSchema{ - tblProperties: map[string]string{}, - } - - // Tokenize lightly: keep the original DDL but identify section - // boundaries by uppercase keywords. We scan through the DDL once. - upper := strings.ToUpper(ddl) - - // EXTERNAL flag and table name come from the CREATE TABLE prefix. - cidx := strings.Index(upper, "CREATE") - if cidx < 0 { - return nil, fmt.Errorf("missing CREATE keyword") - } - // Check for EXTERNAL between CREATE and TABLE. - tidx := strings.Index(upper[cidx:], "TABLE") - if tidx < 0 { - return nil, fmt.Errorf("missing TABLE keyword") - } - prefix := upper[cidx : cidx+tidx] - if strings.Contains(prefix, "EXTERNAL") { - s.external = true - } - // Table name: first token after TABLE (skipping IF NOT EXISTS). - afterTable := strings.TrimSpace(ddl[cidx+tidx+len("TABLE"):]) - afterTableUpper := strings.ToUpper(afterTable) - if strings.HasPrefix(afterTableUpper, "IF NOT EXISTS") { - afterTable = strings.TrimSpace(afterTable[len("IF NOT EXISTS"):]) - } - // Take everything up to the next whitespace or '(' as the name. - nameEnd := strings.IndexAny(afterTable, " \t\n(") - if nameEnd < 0 { - nameEnd = len(afterTable) - } - s.tableName = stripQualifier(strings.Trim(afterTable[:nameEnd], "`")) - - // Body parsing: find the column-list parenthesized block first. - openIdx := strings.IndexByte(afterTable, '(') - if openIdx < 0 { - // No columns; this is an unusual but valid DDL (e.g. CTAS). - return s, nil - } - closeIdx := matchParen(afterTable, openIdx) - if closeIdx < 0 { - return nil, fmt.Errorf("unmatched column list parenthesis") - } - columnsBody := afterTable[openIdx+1 : closeIdx] - s.columns = parseColumnList(columnsBody) - - // Remaining = everything after the column list. - rest := strings.TrimSpace(afterTable[closeIdx+1:]) - parseSections(rest, s) - return s, nil -} - -// stripQualifier turns "db.t" into "t" (Hive supports schema-qualified -// names in SHOW CREATE output). -func stripQualifier(s string) string { - if i := strings.LastIndex(s, "."); i >= 0 { - return s[i+1:] - } - return s -} - -// matchParen returns the index of the matching closing parenthesis for the -// '(' at openIdx, respecting single-quoted strings. Returns -1 on mismatch. -func matchParen(s string, openIdx int) int { - if openIdx < 0 || openIdx >= len(s) || s[openIdx] != '(' { - return -1 - } - depth := 0 - inStr := false - for i := openIdx; i < len(s); i++ { - c := s[i] - if c == '\'' && (i == 0 || s[i-1] != '\\') { - inStr = !inStr - continue - } - if inStr { - continue - } - switch c { - case '(': - depth++ - case ')': - depth-- - if depth == 0 { - return i - } - } - } - return -1 -} - -// parseColumnList parses the comma-separated column list inside the -// parenthesized body. Each entry is "name type [COMMENT 'x']". Commas -// inside parentheses (decimal(10,2), struct) are honored. -func parseColumnList(body string) []hiveColumn { - var cols []hiveColumn - for _, raw := range splitTopLevelCommas(body) { - entry := strings.TrimSpace(raw) - if entry == "" { - continue - } - col := parseColumnEntry(entry) - if col.name != "" { - cols = append(cols, col) - } - } - return cols -} - -// splitTopLevelCommas splits a string by commas that are NOT inside -// parentheses or single-quoted strings. -func splitTopLevelCommas(s string) []string { - var parts []string - var buf strings.Builder - depth := 0 - inStr := false - for i := 0; i < len(s); i++ { - c := s[i] - if c == '\'' && (i == 0 || s[i-1] != '\\') { - inStr = !inStr - } - if !inStr { - switch c { - case '(', '<': - depth++ - case ')', '>': - depth-- - case ',': - if depth == 0 { - parts = append(parts, buf.String()) - buf.Reset() - continue - } - } - } - buf.WriteByte(c) - } - if buf.Len() > 0 { - parts = append(parts, buf.String()) - } - return parts -} - -// parseColumnEntry parses a single column definition: name type [COMMENT 'x']. -func parseColumnEntry(entry string) hiveColumn { - col := hiveColumn{} - entry = strings.TrimSpace(strings.Trim(entry, "\n\r\t ")) - // Name: first whitespace-delimited token, may be backtick-quoted. - nameEnd := indexAnyOutsideQuotes(entry, " \t\n") - if nameEnd < 0 { - col.name = strings.Trim(entry, "`") - return col - } - col.name = strings.Trim(entry[:nameEnd], "`") - rest := strings.TrimSpace(entry[nameEnd:]) - // COMMENT may appear after the type. Find " COMMENT " (case-insensitive) - // outside of quotes. - upperRest := strings.ToUpper(rest) - cidx := strings.Index(upperRest, " COMMENT ") - if cidx >= 0 { - col.colType = strings.TrimSpace(rest[:cidx]) - commentPart := strings.TrimSpace(rest[cidx+len(" COMMENT "):]) - col.comment = stripQuotes(commentPart) - } else { - col.colType = strings.TrimSpace(rest) - } - return col -} - -// indexAnyOutsideQuotes returns the index of the first byte in `chars` -// that occurs outside of a single-quoted string. -func indexAnyOutsideQuotes(s, chars string) int { - inStr := false - for i := 0; i < len(s); i++ { - c := s[i] - if c == '\'' { - inStr = !inStr - continue - } - if inStr { - continue - } - if strings.IndexByte(chars, c) >= 0 { - return i - } - } - return -1 -} - -// stripQuotes removes surrounding single quotes (Hive comment literals). -func stripQuotes(s string) string { - s = strings.TrimSpace(s) - if len(s) >= 2 && s[0] == '\'' && s[len(s)-1] == '\'' { - return s[1 : len(s)-1] - } - return s -} - -// parseSections walks the post-column-list body and fills the schema with -// COMMENT, PARTITIONED BY, ROW FORMAT, STORED AS, LOCATION, TBLPROPERTIES. -// Sections are keyword-driven; their order in Hive DDL is fixed but the -// parser does not rely on the order. -func parseSections(rest string, s *hiveTableSchema) { - cursor := 0 - for cursor < len(rest) { - // Skip whitespace. - for cursor < len(rest) && isWhitespace(rest[cursor]) { - cursor++ - } - if cursor >= len(rest) { - break - } - - remaining := rest[cursor:] - upper := strings.ToUpper(remaining) - - switch { - case strings.HasPrefix(upper, "COMMENT "): - cursor += len("COMMENT ") - val, consumed := consumeQuotedString(rest[cursor:]) - s.comment = val - cursor += consumed - case strings.HasPrefix(upper, "PARTITIONED BY"): - cursor += len("PARTITIONED BY") - cursor = skipWhitespace(rest, cursor) - if cursor < len(rest) && rest[cursor] == '(' { - closeIdx := matchParen(rest, cursor) - if closeIdx > cursor { - s.partitionedBy = parseColumnList(rest[cursor+1 : closeIdx]) - cursor = closeIdx + 1 - } else { - cursor++ - } - } - case strings.HasPrefix(upper, "ROW FORMAT"): - // Capture everything up to the next top-level section keyword. - cursor += len("ROW FORMAT") - next := findNextSectionKeyword(rest, cursor) - s.rowFormat = strings.TrimSpace(rest[cursor:next]) - cursor = next - case strings.HasPrefix(upper, "STORED AS"): - cursor += len("STORED AS") - next := findNextSectionKeyword(rest, cursor) - s.storedAs = strings.TrimSpace(rest[cursor:next]) - cursor = next - case strings.HasPrefix(upper, "STORED BY"): - cursor += len("STORED BY") - next := findNextSectionKeyword(rest, cursor) - // Treat STORED BY like storedAs for diff purposes. - s.storedAs = "STORED BY " + strings.TrimSpace(rest[cursor:next]) - cursor = next - case strings.HasPrefix(upper, "LOCATION"): - cursor += len("LOCATION") - cursor = skipWhitespace(rest, cursor) - val, consumed := consumeQuotedString(rest[cursor:]) - s.location = val - cursor += consumed - case strings.HasPrefix(upper, "TBLPROPERTIES"): - cursor += len("TBLPROPERTIES") - cursor = skipWhitespace(rest, cursor) - if cursor < len(rest) && rest[cursor] == '(' { - closeIdx := matchParen(rest, cursor) - if closeIdx > cursor { - parseTBLProperties(rest[cursor+1:closeIdx], s) - cursor = closeIdx + 1 - } else { - cursor++ - } - } - default: - // Unknown keyword: skip one rune so we make progress. - cursor++ - } - } -} - -// findNextSectionKeyword returns the index in `s` (>= start) of the next -// top-level Hive table-DDL section keyword (COMMENT, PARTITIONED BY, etc.), -// or len(s) if none is found. Keywords occurring inside parentheses are -// ignored. This allows ROW FORMAT bodies to span multiple lines safely. -func findNextSectionKeyword(s string, start int) int { - keywords := []string{ - "COMMENT ", "PARTITIONED BY", "CLUSTERED BY", "SKEWED BY", - "ROW FORMAT", "STORED AS", "STORED BY", "LOCATION", "TBLPROPERTIES", - } - depth := 0 - inStr := false - for i := start; i < len(s); i++ { - c := s[i] - if c == '\'' && (i == 0 || s[i-1] != '\\') { - inStr = !inStr - continue - } - if inStr { - continue - } - switch c { - case '(': - depth++ - case ')': - depth-- - } - if depth != 0 { - continue - } - // Match keyword at the start of a word (preceded by whitespace). - if i == start || isWhitespace(s[i-1]) { - upperFrom := strings.ToUpper(s[i:]) - for _, kw := range keywords { - if strings.HasPrefix(upperFrom, kw) { - return i - } - } - } - } - return len(s) -} - -// parseTBLProperties parses the body of a TBLPROPERTIES (...) section and -// fills s.tblProperties, filtering out runtime keys (compat-RISK-6). -// Body format: 'key1'='value1', 'key2'='value2', ... -func parseTBLProperties(body string, s *hiveTableSchema) { - for _, raw := range splitTopLevelCommas(body) { - entry := strings.TrimSpace(raw) - if entry == "" { - continue - } - // Find '=' that is OUTSIDE quotes. - eq := indexEqualsOutsideQuotes(entry) - if eq < 0 { - continue - } - k := stripQuotes(strings.TrimSpace(entry[:eq])) - v := stripQuotes(strings.TrimSpace(entry[eq+1:])) - if _, runtime := runtimeTBLPropertyKeys[k]; runtime { - continue - } - // Hive also surfaces EXTERNAL via TBLPROPERTIES; treat that as the - // external flag, not a property. - if strings.EqualFold(k, "EXTERNAL") { - if strings.EqualFold(v, "TRUE") { - s.external = true - } - continue - } - // COMMENT in TBLPROPERTIES maps to the table comment field; prefer - // the inline COMMENT clause if it was already set. - if strings.EqualFold(k, "comment") { - if s.comment == "" { - s.comment = v - } - continue - } - s.tblProperties[k] = v - } -} - -// indexEqualsOutsideQuotes returns the index of the first '=' outside of -// single-quoted strings. -func indexEqualsOutsideQuotes(s string) int { - inStr := false - for i := 0; i < len(s); i++ { - c := s[i] - if c == '\'' && (i == 0 || s[i-1] != '\\') { - inStr = !inStr - continue - } - if !inStr && c == '=' { - return i - } - } - return -1 -} - -// consumeQuotedString reads a single-quoted string starting at the current -// position (after optional leading whitespace) and returns the unquoted -// value plus the number of input bytes consumed. -func consumeQuotedString(s string) (string, int) { - i := 0 - for i < len(s) && isWhitespace(s[i]) { - i++ - } - if i >= len(s) || s[i] != '\'' { - return "", i - } - start := i + 1 - for j := start; j < len(s); j++ { - if s[j] == '\'' && (j == start || s[j-1] != '\\') { - return s[start:j], j + 1 - } - } - return s[start:], len(s) -} - -// skipWhitespace returns the index of the next non-whitespace byte at or -// after `i`. -func skipWhitespace(s string, i int) int { - for i < len(s) && isWhitespace(s[i]) { - i++ - } - return i -} - -// isWhitespace reports whether c is one of the ASCII whitespace bytes. -func isWhitespace(c byte) bool { - return c == ' ' || c == '\t' || c == '\n' || c == '\r' -} diff --git a/sqle/driver/hive/hive.go b/sqle/driver/hive/hive.go deleted file mode 100644 index 3c72e4c95..000000000 --- a/sqle/driver/hive/hive.go +++ /dev/null @@ -1,1020 +0,0 @@ -package hive - -import ( - "context" - databaseDriver "database/sql/driver" - "fmt" - "strconv" - "strings" - - "github.com/actiontech/dms/pkg/dms-common/i18nPkg" - sqleDriver "github.com/actiontech/sqle/sqle/driver" - driverV2 "github.com/actiontech/sqle/sqle/driver/v2" - "github.com/actiontech/sqle/sqle/pkg/params" - "github.com/beltran/gohive" - "github.com/sirupsen/logrus" -) - -func init() { - sqleDriver.BuiltInPluginProcessors[driverV2.DriverTypeHive] = &PluginProcessor{} -} - -// PluginProcessor implements driver.PluginProcessor for Hive. -type PluginProcessor struct{} - -// hiveQueryRunner abstracts the minimum Hive cursor operations needed by -// GetDatabaseObjectDDL / GetDatabaseDiffModifySQL. It allows unit tests to -// substitute a fake without requiring a real gohive connection or network. -// -// runSingleStringQuery executes a query that returns rows of a single STRING -// column (e.g. SHOW DATABASES, SHOW CREATE TABLE) and returns each row as a -// string. The implementation is responsible for opening / closing the cursor. -type hiveQueryRunner interface { - runSingleStringQuery(ctx context.Context, query string) ([]string, error) -} - -// HiveDriverImpl implements driver.Plugin for Hive. -type HiveDriverImpl struct { - log *logrus.Entry - dsn *driverV2.DSN - conn *gohive.Connection - // runner is the query executor used by ObjectDDL / DiffModifySQL paths. - // In production it is set to gohiveQueryRunner wrapping h.conn; in unit - // tests it can be replaced with a fake to avoid network dependency. - runner hiveQueryRunner - // compareRunnerFactory builds a query runner for the calibrated - // (compared) DSN side of GetDatabaseDiffModifySQL. In production it - // opens a real gohive connection. Unit tests inject a fake to avoid - // requiring a Hive server. - compareRunnerFactory func(dsn *driverV2.DSN) (hiveQueryRunner, func(), error) - // execRunnerFactory builds a hiveExecRunner used by Exec / ExecBatch. - // In production it is nil and a fresh gohiveExecRunner backed by - // h.conn is constructed on demand. Unit tests inject a fake so the - // Exec path can be exercised without a live HiveServer2. - execRunnerFactory func(h *HiveDriverImpl) hiveExecRunner -} - -func (p *PluginProcessor) GetDriverMetas() (*driverV2.DriverMetas, error) { - return &driverV2.DriverMetas{ - PluginName: driverV2.DriverTypeHive, - DatabaseDefaultPort: 10000, - Logo: logo, - DatabaseAdditionalParams: additionalParams(), - Rules: []*driverV2.Rule{}, - EnabledOptionalModule: []driverV2.OptionalModule{ - driverV2.OptionalGetDatabaseObjectDDL, - driverV2.OptionalGetDatabaseDiffModifySQL, - }, - }, nil -} - -func (p *PluginProcessor) Open(l *logrus.Entry, cfg *driverV2.Config) (sqleDriver.Plugin, error) { - impl := &HiveDriverImpl{ - log: l, - } - if cfg.DSN != nil { - impl.dsn = cfg.DSN - conn, err := newHiveConnection(cfg.DSN) - if err != nil { - return nil, fmt.Errorf("failed to connect to Hive: %v", err) - } - impl.conn = conn - impl.runner = &gohiveQueryRunner{conn: conn} - } - // Default compareRunnerFactory opens a fresh gohive connection to the - // calibratedDSN. Unit tests override this with a fake. - impl.compareRunnerFactory = defaultCompareRunnerFactory - return impl, nil -} - -// defaultCompareRunnerFactory opens a real gohive connection to the -// calibrated DSN and returns a runner + close hook. -func defaultCompareRunnerFactory(dsn *driverV2.DSN) (hiveQueryRunner, func(), error) { - conn, err := newHiveConnection(dsn) - if err != nil { - return nil, func() {}, fmt.Errorf("connect to compared Hive: %v", err) - } - closer := func() { _ = conn.Close() } - return &gohiveQueryRunner{conn: conn}, closer, nil -} - -// gohiveQueryRunner is the production hiveQueryRunner backed by *gohive.Connection. -type gohiveQueryRunner struct { - conn *gohive.Connection -} - -// hs2NoResultRowErrMarkers are substring markers that identify a HiveServer2 -// "ROW-ERR" returned from FetchResults on a statement that produces no result -// columns (e.g. `USE `, `SET ...`, DDL). Such an error is **non-fatal** in -// HS2's protocol: the same connection can still execute the next statement. -// -// See compat-RISK-10 (sqle-ee/docs/dev/compat_risks.md) and the reference -// implementation in sqle-ee/cmd/hivetool/main.go (which tolerates ROW-ERR -// and breaks out of the fetch loop). -var hs2NoResultRowErrMarkers = []string{ - // HiveServer2's generic "no result columns" message. - "Server-side error; please check HS2 logs.", - // Some HS2 builds wrap the same condition with an explicit status. - "StatusCode:ERROR_STATUS", -} - -// isHS2NoResultRowErr reports whether err is the non-fatal ROW-ERR -// HiveServer2 returns from FetchResults for a no-result-column statement. -// It is a pure string match against the error's Error() output so we can -// unit-test the classifier without needing a real gohive cursor. -// -// The function is conservative: only errors that match ALL of the canonical -// markers are treated as tolerable. Real Hive runtime errors (syntax errors, -// missing table, etc.) carry a different status / message and will NOT be -// matched. -func isHS2NoResultRowErr(err error) bool { - if err == nil { - return false - } - msg := err.Error() - for _, m := range hs2NoResultRowErrMarkers { - if !strings.Contains(msg, m) { - return false - } - } - return true -} - -// hiveCursor abstracts the minimum *gohive.Cursor surface used by the -// fetch loop. It exists solely so unit tests can substitute a fake; the -// production code path uses *gohive.Cursor directly. -type hiveCursor interface { - HasMore(ctx context.Context) bool - FetchOne(ctx context.Context, dests ...interface{}) - Err() error -} - -// gohiveCursorAdapter adapts *gohive.Cursor (whose error is a public field -// rather than a method) to the hiveCursor interface for use in fetchAllRows. -type gohiveCursorAdapter struct { - c *gohive.Cursor -} - -func (a gohiveCursorAdapter) HasMore(ctx context.Context) bool { return a.c.HasMore(ctx) } -func (a gohiveCursorAdapter) FetchOne(ctx context.Context, dests ...interface{}) { a.c.FetchOne(ctx, dests...) } -func (a gohiveCursorAdapter) Err() error { return a.c.Err } - -// fetchAllRows pulls a single STRING column from cur and returns each row. -// HS2 ROW-ERR (non-fatal) is tolerated: when encountered, the loop breaks -// and the rows captured so far are returned with a nil error. Real fetch -// errors (any error that is not isHS2NoResultRowErr) are propagated. -// -// This is the shared core of gohiveQueryRunner.runSingleStringQuery; it is -// also used by unit tests via a fake hiveCursor so that the ROW-ERR -// tolerance contract can be exercised without a live HiveServer2 instance. -func fetchAllRows(ctx context.Context, cur hiveCursor) ([]string, error) { - var rows []string - for cur.HasMore(ctx) { - if e := cur.Err(); e != nil { - if isHS2NoResultRowErr(e) { - // HS2 ROW-ERR on a no-result-column statement; treat as EOF. - break - } - return nil, fmt.Errorf("failed to fetch row: %v", e) - } - var val string - cur.FetchOne(ctx, &val) - if e := cur.Err(); e != nil { - if isHS2NoResultRowErr(e) { - // Same ROW-ERR can surface during FetchOne; tolerate and stop. - break - } - return nil, fmt.Errorf("failed to scan row: %v", e) - } - rows = append(rows, val) - } - return rows, nil -} - -func (g *gohiveQueryRunner) runSingleStringQuery(ctx context.Context, query string) ([]string, error) { - if g.conn == nil { - return nil, fmt.Errorf("hive connection is not initialized") - } - cursor := g.conn.Cursor() - defer cursor.Close() - - cursor.Exec(ctx, query) - if cursor.Err != nil { - return nil, fmt.Errorf("failed to execute %q: %v", query, cursor.Err) - } - - return fetchAllRows(ctx, gohiveCursorAdapter{c: cursor}) -} - -func (p *PluginProcessor) Stop() error { - return nil -} - -func additionalParams() params.Params { - return params.Params{ - { - Key: "auth", - Value: "NONE", - Desc: "authentication mode", - Type: params.ParamTypeString, - Enums: []params.EnumsValue{ - {Value: "NONE", Desc: "No authentication (SASL)"}, - {Value: "NOSASL", Desc: "No authentication"}, - {Value: "LDAP", Desc: "LDAP authentication"}, - {Value: "KERBEROS", Desc: "Kerberos authentication"}, - }, - }, - { - Key: "transport_mode", - Value: "binary", - Desc: "transport mode (binary or http)", - Type: params.ParamTypeString, - Enums: []params.EnumsValue{ - {Value: "binary", Desc: "Binary transport (default)"}, - {Value: "http", Desc: "HTTP transport"}, - }, - }, - { - Key: "service", - Desc: "Kerberos service name (optional, used when auth=KERBEROS)", - Type: params.ParamTypeString, - }, - } -} - -// newHiveConnection creates a gohive connection from DSN parameters. -// It reads host, port, user, password, database from DSN and auth/transport_mode -// from AdditionalParams. This follows the same approach as DMS-EE's NewHiveConn. -func newHiveConnection(dsn *driverV2.DSN) (*gohive.Connection, error) { - port, err := strconv.Atoi(dsn.Port) - if err != nil { - return nil, fmt.Errorf("invalid port %q: %v", dsn.Port, err) - } - - conf := gohive.NewConnectConfiguration() - conf.Username = dsn.User - conf.Password = dsn.Password - if dsn.DatabaseName != "" { - conf.Database = dsn.DatabaseName - } - - auth := "NONE" - if dsn.AdditionalParams != nil { - if authParam := dsn.AdditionalParams.GetParam("auth"); authParam != nil { - if v := authParam.String(); v != "" { - auth = v - } - } - if transportParam := dsn.AdditionalParams.GetParam("transport_mode"); transportParam != nil { - if v := transportParam.String(); v != "" { - conf.TransportMode = v - } - } - if serviceParam := dsn.AdditionalParams.GetParam("service"); serviceParam != nil { - if v := serviceParam.String(); v != "" { - conf.Service = v - } - } - } - - conn, err := gohive.Connect(dsn.Host, port, auth, conf) - if err != nil { - return nil, fmt.Errorf("gohive connect failed: %v", err) - } - return conn, nil -} - -// Ping tests the connectivity to the Hive server by executing SELECT 1. -func (h *HiveDriverImpl) Ping(ctx context.Context) error { - if h.conn == nil { - return fmt.Errorf("hive connection is not initialized") - } - cursor := h.conn.Cursor() - cursor.Exec(ctx, "SELECT 1") - defer cursor.Close() - if cursor.Err != nil { - return fmt.Errorf("hive ping failed: %v", cursor.Err) - } - return nil -} - -// Parse parses sqlText into Node array. It uses keyword prefix matching -// to classify SQL statements as DQL/DML/DDL. -func (h *HiveDriverImpl) Parse(ctx context.Context, sqlText string) ([]driverV2.Node, error) { - sqls := splitSQL(sqlText) - nodes := make([]driverV2.Node, 0, len(sqls)) - for _, sql := range sqls { - sqlType := classifySQL(sql) - nodes = append(nodes, driverV2.Node{ - Text: sql, - Type: sqlType, - Fingerprint: sql, - }) - } - return nodes, nil -} - -// Audit performs SQL audit. Currently returns empty results (no audit rules) -// as per design requirement TC-02. -func (h *HiveDriverImpl) Audit(ctx context.Context, sqls []string) ([]*driverV2.AuditResults, error) { - results := make([]*driverV2.AuditResults, len(sqls)) - for i := range sqls { - results[i] = &driverV2.AuditResults{} - } - return results, nil -} - -func (h *HiveDriverImpl) Close(ctx context.Context) { - if h.conn != nil { - h.conn.Close() - } -} - -// hiveExecResult is the driver.Result implementation returned by Hive's -// Exec / ExecBatch. Hive does NOT report LastInsertId or RowsAffected for -// most statements (HiveServer2 cursor.Exec is fire-and-forget for DDL and -// returns affected rows only for a small subset of DMLs through a separate -// channel that gohive does not surface). The contract follows -// database/sql/driver.Result by returning a defensive error rather than -// fabricating a zero — callers that genuinely need these values can detect -// the unsupported-by-driver case from the error message. -type hiveExecResult struct{} - -func (hiveExecResult) LastInsertId() (int64, error) { - return 0, fmt.Errorf("hive plugin does not support LastInsertId") -} - -func (hiveExecResult) RowsAffected() (int64, error) { - return 0, fmt.Errorf("hive plugin does not support RowsAffected") -} - -// hiveExecRunner abstracts the minimum cursor surface needed by Exec / -// ExecBatch. It allows unit tests to substitute a fake cursor (so the -// execution contract can be exercised without a live HiveServer2) and -// keeps the driver layer decoupled from gohive's concrete cursor type. -type hiveExecRunner interface { - exec(ctx context.Context, query string) error -} - -// gohiveExecRunner is the production hiveExecRunner backed by *gohive.Connection. -// It opens a fresh cursor per statement (matching gohive's recommended usage), -// invokes cursor.Exec, and inspects cursor.Err. The HS2 ROW-ERR on no-result -// statements (see compat-RISK-10) is tolerated using the same classifier as -// runSingleStringQuery so DDL/DML batches don't fail spuriously. -type gohiveExecRunner struct { - conn *gohive.Connection -} - -func (g *gohiveExecRunner) exec(ctx context.Context, query string) error { - if g.conn == nil { - return fmt.Errorf("hive connection is not initialized") - } - cursor := g.conn.Cursor() - defer cursor.Close() - - cursor.Exec(ctx, query) - if cursor.Err != nil { - if isHS2NoResultRowErr(cursor.Err) { - // HS2 returns a ROW-ERR for DDL / SET / USE statements that - // produce no result columns. cursor.Exec has already submitted - // the statement; treat as success (compat-RISK-10). - return nil - } - return fmt.Errorf("failed to execute %q: %v", query, cursor.Err) - } - return nil -} - -// stripSQLTerminator removes a single trailing semicolon (and surrounding -// whitespace) from a Hive statement. HiveServer2 rejects DDL/DML that -// contains a trailing ';' because the JDBC protocol assumes one statement -// per execute call. The structure-compare modify-SQL output emits -// "USE ; DROP TABLE x; CREATE TABLE x ...;" which is split on ';' -// upstream — but defensive trimming here makes the driver tolerant of -// either form. -func stripSQLTerminator(query string) string { - q := strings.TrimSpace(query) - for strings.HasSuffix(q, ";") { - q = strings.TrimSpace(strings.TrimSuffix(q, ";")) - } - return q -} - -// Exec submits a single Hive statement to HiveServer2 and waits for it to -// complete. Empty / whitespace-only / comment-only statements are skipped -// (no-op success) so that the modify-SQL splitter upstream — which can -// produce empty trailers after stripping `;` — does not cause spurious -// errors. -func (h *HiveDriverImpl) Exec(ctx context.Context, query string) (databaseDriver.Result, error) { - trimmed := stripSQLTerminator(query) - if trimmed == "" || isAllCommentLines(trimmed) { - // Nothing meaningful to execute; succeed silently. This matches - // MySQL driver behaviour for empty statements piped through - // ExecBatch and avoids HS2 syntax-error noise. - return hiveExecResult{}, nil - } - - // h.execRunnerFactory is the unit-test injection point. Production - // path (factory == nil) requires a live connection. - if h.execRunnerFactory == nil && h.conn == nil { - return nil, fmt.Errorf("hive connection is not initialized") - } - - runner := h.execRunner() - if err := runner.exec(ctx, trimmed); err != nil { - return nil, err - } - return hiveExecResult{}, nil -} - -// ExecBatch executes a sequence of statements via Exec. If any statement -// fails the batch stops and returns the partial results so the caller can -// see how far the batch progressed (matches MySQL driver's contract in -// sqle/driver/mysql/mysql.go::ExecBatch). -func (h *HiveDriverImpl) ExecBatch(ctx context.Context, sqls ...string) ([]databaseDriver.Result, error) { - results := make([]databaseDriver.Result, 0, len(sqls)) - for _, sql := range sqls { - result, err := h.Exec(ctx, sql) - results = append(results, result) - if err != nil { - return results, fmt.Errorf("exec sql failed: \n%s \n%v", sql, err) - } - } - return results, nil -} - -// execRunner returns the execRunner used by Exec. h.execRunnerFactory is -// the test-injection point; if unset, a fresh gohiveExecRunner backed by -// h.conn is constructed on demand (production path). -func (h *HiveDriverImpl) execRunner() hiveExecRunner { - if h.execRunnerFactory != nil { - return h.execRunnerFactory(h) - } - return &gohiveExecRunner{conn: h.conn} -} - -// isAllCommentLines reports whether every non-empty line in s is a Hive -// SQL comment (-- ...). Comment-only statements occur when modify-SQL -// output is split on ';' and a trailing block like -// -// "-- WARNING: data loss risk\n" -// -// is left behind. HiveServer2 rejects "comment-only" statements with a -// generic parser error, so the driver normalises them to a no-op. -func isAllCommentLines(s string) bool { - if s == "" { - return true - } - for _, line := range strings.Split(s, "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - if !strings.HasPrefix(line, "--") { - return false - } - } - return true -} - -func (h *HiveDriverImpl) Tx(ctx context.Context, queries ...string) (*driverV2.TxResponse, error) { - return nil, fmt.Errorf("hive plugin does not support Tx") -} - -func (h *HiveDriverImpl) Query(ctx context.Context, sql string, conf *driverV2.QueryConf) (*driverV2.QueryResult, error) { - return nil, fmt.Errorf("hive plugin does not support Query") -} - -func (h *HiveDriverImpl) Explain(ctx context.Context, conf *driverV2.ExplainConf) (*driverV2.ExplainResult, error) { - return nil, fmt.Errorf("hive plugin does not support Explain") -} - -func (h *HiveDriverImpl) ExplainJSONFormat(ctx context.Context, conf *driverV2.ExplainConf) (*driverV2.ExplainJSONResult, error) { - return nil, fmt.Errorf("hive plugin does not support ExplainJSONFormat") -} - -func (h *HiveDriverImpl) GenRollbackSQL(ctx context.Context, sql string) (string, i18nPkg.I18nStr, error) { - return "", nil, nil -} - -func (h *HiveDriverImpl) KillProcess(ctx context.Context) error { - return fmt.Errorf("hive plugin does not support KillProcess") -} - -func (h *HiveDriverImpl) Schemas(ctx context.Context) ([]string, error) { - if h.conn == nil { - return nil, fmt.Errorf("hive connection is not initialized") - } - cursor := h.conn.Cursor() - defer cursor.Close() - - cursor.Exec(ctx, "SHOW DATABASES") - if cursor.Err != nil { - return nil, fmt.Errorf("failed to execute SHOW DATABASES: %v", cursor.Err) - } - - // SHOW DATABASES returns a single STRING column. Reuse fetchAllRows so - // the ROW-ERR tolerance contract (compat-RISK-10) stays consistent with - // runSingleStringQuery's behaviour. - return fetchAllRows(ctx, gohiveCursorAdapter{c: cursor}) -} - -func (h *HiveDriverImpl) GetTableMetaBySQL(ctx context.Context, conf *sqleDriver.GetTableMetaBySQLConf) (*sqleDriver.GetTableMetaBySQLResult, error) { - return nil, fmt.Errorf("hive plugin does not support GetTableMetaBySQL") -} - -func (h *HiveDriverImpl) EstimateSQLAffectRows(ctx context.Context, sql string) (*driverV2.EstimatedAffectRows, error) { - return nil, fmt.Errorf("hive plugin does not support EstimateSQLAffectRows") -} - -// hiveFunctionUnsupportedMsg is the Chinese error message returned when a -// FUNCTION object is requested. The driver does not implement FUNCTION DDL -// in this batch; it is planned for the second batch (design §3.2.1 / §3.5). -const hiveFunctionUnsupportedMsg = "Hive FUNCTION 暂未支持(计划第二批落地)" - -// defaultHiveSchema is the schema used when an object info has an empty -// SchemaName (design §3.2.1 "USE ; schema 为空走默认 default"). -const defaultHiveSchema = "default" - -// listAllSchemaObjects enumerates every TABLE and VIEW in the current -// (already-USEd) Hive schema. It mirrors MySQL driver's "default to full -// schema" behaviour for callers (controllers / server) that pass an -// objInfo with an empty DatabaseObjects slice — the contract is that the -// driver fills in the discovery itself. -// -// Discovery strategy: `SHOW VIEWS` first to learn which names are views, -// then `SHOW TABLES` for the union of TABLE+VIEW, and finally subtract -// the view names from the table list. The runner is expected to be -// pointing at the desired schema already (caller did USE ). -// -// Note: SHOW VIEWS is not available before Hive 2.2; if it fails (any -// error, including ROW-ERR via fetchAllRows convention) we fall back to -// "all names are TABLE", which is a tolerable degradation for the -// structure-comparison use case — both sides degrade identically and -// SHOW CREATE TABLE works for VIEWs in Hive anyway. -func listAllSchemaObjects(ctx context.Context, runner hiveQueryRunner) ([]*driverV2.DatabaseObject, error) { - tableNames, err := runner.runSingleStringQuery(ctx, "SHOW TABLES") - if err != nil { - return nil, fmt.Errorf("show tables: %v", err) - } - viewSet := make(map[string]struct{}) - if viewNames, verr := runner.runSingleStringQuery(ctx, "SHOW VIEWS"); verr == nil { - for _, v := range viewNames { - viewSet[v] = struct{}{} - } - } - out := make([]*driverV2.DatabaseObject, 0, len(tableNames)) - for _, name := range tableNames { - if name == "" { - continue - } - objType := driverV2.ObjectType_TABLE - if _, isView := viewSet[name]; isView { - objType = driverV2.ObjectType_VIEW - } - out = append(out, &driverV2.DatabaseObject{ - ObjectName: name, - ObjectType: objType, - }) - } - return out, nil -} - -// GetDatabaseObjectDDL fetches the CREATE statement for each requested -// (schema, object) pair. It implements the contract described in -// docs/spec/design.md §3.2.1: -// -// - For each schema, first `USE ` (or "default" if empty). -// - When the caller passes an empty DatabaseObjects slice, the driver -// auto-discovers every TABLE/VIEW in the schema via -// listAllSchemaObjects, mirroring the MySQL driver's behaviour. -// - TABLE -> SHOW CREATE TABLE -// - VIEW -> SHOW CREATE TABLE (Hive views reuse this command) -// - FUNCTION -> skip the object entirely and emit a WARN log; do not -// append a placeholder DDL, do not return a Go error. -// FUNCTION support is planned for the second batch -// (compat-RISK-9; aligned with PROCEDURE/TRIGGER/EVENT). -// - PROCEDURE / TRIGGER / EVENT -> short-circuit: skip the object entirely -// and emit a WARN log; do not panic, do not return an error -// (Hive does not support these object types — compat-RISK-4). -// -// Behavior on error: requesting an unsupported object type (FUNCTION / -// PROCEDURE / TRIGGER / EVENT) never aborts the whole batch — the driver -// skips that object and continues so other TABLE/VIEW results survive. -// Real connection errors against TABLE/VIEW are returned as a normal -// Go error. -func (h *HiveDriverImpl) GetDatabaseObjectDDL(ctx context.Context, objInfos []*driverV2.DatabaseSchemaInfo) ([]*driverV2.DatabaseSchemaObjectResult, error) { - if h.runner == nil { - return nil, fmt.Errorf("hive connection is not initialized") - } - - results := make([]*driverV2.DatabaseSchemaObjectResult, 0, len(objInfos)) - for _, objInfo := range objInfos { - schemaName := objInfo.SchemaName - if schemaName == "" { - schemaName = defaultHiveSchema - } - - // USE first so subsequent unqualified object references - // resolve to this database (design §3.2.1). - if _, err := h.runner.runSingleStringQuery(ctx, fmt.Sprintf("USE %s", schemaName)); err != nil { - return nil, fmt.Errorf("use schema %q failed: %v", schemaName, err) - } - - // Default discovery: when the caller did not specify which objects - // to inspect, enumerate every TABLE/VIEW in the current schema. - // This mirrors mysql/mysql_ee.go::GetDatabaseObjectDDL line 380-389 - // and is what server/compare/database_compare_ee.go ExecDatabaseCompare - // relies on (it never populates DatabaseObjects itself). - if len(objInfo.DatabaseObjects) == 0 { - discovered, derr := listAllSchemaObjects(ctx, h.runner) - if derr != nil { - return nil, fmt.Errorf("enumerate schema %q failed: %v", schemaName, derr) - } - objInfo.DatabaseObjects = discovered - } - - dbDDLs := make([]*driverV2.DatabaseObjectDDL, 0, len(objInfo.DatabaseObjects)) - for _, obj := range objInfo.DatabaseObjects { - switch obj.ObjectType { - case driverV2.ObjectType_TABLE, driverV2.ObjectType_VIEW: - // Hive views reuse SHOW CREATE TABLE; the rows returned by - // HiveServer2 are joined with newline to form the DDL. - rows, err := h.runner.runSingleStringQuery(ctx, - fmt.Sprintf("SHOW CREATE TABLE %s", obj.ObjectName)) - if err != nil { - return nil, fmt.Errorf("show create %s.%s failed: %v", - schemaName, obj.ObjectName, err) - } - dbDDLs = append(dbDDLs, &driverV2.DatabaseObjectDDL{ - DatabaseObject: &driverV2.DatabaseObject{ - ObjectName: obj.ObjectName, - ObjectType: obj.ObjectType, - }, - ObjectDDL: strings.Join(rows, "\n"), - }) - case driverV2.ObjectType_FUNCTION: - // FUNCTION is planned for the second batch (compat-RISK-9). - // Aligned with the GetDatabaseDiffModifySQL behaviour and the - // PROCEDURE/TRIGGER/EVENT short-circuit: skip the FUNCTION - // objInfo, do NOT append a placeholder DDL, do NOT return a - // Go error. The upstream pipeline surfaces the unsupported - // state via the WARN log + an empty result entry when every - // requested object is FUNCTION (compat-RISK-9 verified, TC-HIVE-015 / 016). - if h.log != nil { - h.log.WithField("object", obj.ObjectName). - WithField("objectType", "FUNCTION"). - Warnf("hive driver: %s; skipped", hiveFunctionUnsupportedMsg) - } - continue - case driverV2.ObjectType_PROCEDURE, - driverV2.ObjectType_TRIGGER, - driverV2.ObjectType_EVENT: - // Hive does not physically support these object types - // (compat-RISK-4). Short-circuit: skip the object so upstream - // can continue processing the rest of the batch; do NOT - // panic, do NOT return an error. - if h.log != nil { - h.log.WithField("object", obj.ObjectName). - WithField("objectType", obj.ObjectType). - Warn("hive driver: object type not supported, skipped") - } - continue - default: - // Unknown object type: warn and skip rather than fail; future - // versions may add new ObjectType constants. - if h.log != nil { - h.log.WithField("object", obj.ObjectName). - WithField("objectType", obj.ObjectType). - Warn("hive driver: unknown object type, skipped") - } - continue - } - } - - results = append(results, &driverV2.DatabaseSchemaObjectResult{ - SchemaName: schemaName, - DatabaseObjectDDLs: dbDDLs, - }) - } - return results, nil -} - -// WARNING comments emitted at the head of TABLE / VIEW DROP+CREATE SQL -// segments. Both Chinese and English lines are required by design §3.5 and -// are consumed by dms-ui-ee ModifiedSqlDrawer for top-banner detection. -const ( - hiveWarningTableDropCreate = "-- WARNING: data loss risk; table will be dropped and recreated.\n" + - "-- 警告: 数据将丢失;表将被删除并重建。\n" - hiveWarningViewDropCreate = "-- WARNING: view will be recreated; downstream queries depending on this view may be affected.\n" + - "-- 警告: 视图将被重建;依赖该视图的下游查询可能受影响。\n" -) - -// GetDatabaseDiffModifySQL generates the variant SQL needed to reconcile -// the calibrated (compared) side of a Hive instance so its objects match -// the base side (this driver's connection). The full strategy matrix is -// in docs/spec/design.md §3.4 and §3.2.2. -// -// Per-object behavior: -// -// - TABLE only in base, not in compared -> CREATE TABLE ... (no WARNING) -// - TABLE only in compared, not in base -> DROP TABLE IF EXISTS (no WARNING) -// - TABLE on both sides, ALTER-able diff -> ALTER TABLE sequence (no WARNING) -// - TABLE on both sides, fallback diff -> DROP+CREATE (WARNING) -// - VIEW (any diff, any direction) -> DROP+CREATE (WARNING) -// - FUNCTION -> short-circuit, skip silently -// with a WARN log (compat-RISK-9; aligned with PROCEDURE/TRIGGER/EVENT) -// - PROCEDURE / TRIGGER / EVENT -> short-circuit, skip silently -// with a WARN log (compat-RISK-4) -// -// Each non-empty SchemaName produces a result entry with `USE ;` -// prefixed to the SQL block. -func (h *HiveDriverImpl) GetDatabaseDiffModifySQL(ctx context.Context, calibratedDSN *driverV2.DSN, objInfos []*driverV2.DatabasCompareSchemaInfo) ([]*driverV2.DatabaseDiffModifySQLResult, error) { - if h.runner == nil { - return nil, fmt.Errorf("hive base connection is not initialized") - } - if h.compareRunnerFactory == nil { - return nil, fmt.Errorf("hive compareRunnerFactory is not initialized") - } - // Open compare-side runner once for the whole call; close at the end. - compareRunner, closeCompare, err := h.compareRunnerFactory(calibratedDSN) - if err != nil { - return nil, err - } - defer closeCompare() - - results := make([]*driverV2.DatabaseDiffModifySQLResult, 0, len(objInfos)) - for _, objInfo := range objInfos { - baseSchemaName := objInfo.BaseSchemaName - if baseSchemaName == "" { - baseSchemaName = defaultHiveSchema - } - comparedSchemaName := objInfo.ComparedSchemaName - if comparedSchemaName == "" { - comparedSchemaName = defaultHiveSchema - } - - // USE on base side first so subsequent SHOW CREATE TABLE resolves. - if _, err := h.runner.runSingleStringQuery(ctx, - fmt.Sprintf("USE %s", baseSchemaName)); err != nil { - return nil, fmt.Errorf("use base schema %q: %v", baseSchemaName, err) - } - if _, err := compareRunner.runSingleStringQuery(ctx, - fmt.Sprintf("USE %s", comparedSchemaName)); err != nil { - return nil, fmt.Errorf("use compared schema %q: %v", comparedSchemaName, err) - } - - // Default discovery (mirrors GetDatabaseObjectDDL): when the caller - // passes an empty DatabaseObjects slice, take the union of objects - // in both schemas so DROP / CREATE diffs for "only-in-base" and - // "only-in-compared" tables surface. - objs := objInfo.DatabaseObjects - if len(objs) == 0 { - baseObjs, berr := listAllSchemaObjects(ctx, h.runner) - if berr != nil { - return nil, fmt.Errorf("enumerate base schema %q: %v", baseSchemaName, berr) - } - comparedObjs, cerr := listAllSchemaObjects(ctx, compareRunner) - if cerr != nil { - return nil, fmt.Errorf("enumerate compared schema %q: %v", comparedSchemaName, cerr) - } - seen := make(map[string]struct{}) - unionObjs := make([]*driverV2.DatabaseObject, 0, len(baseObjs)+len(comparedObjs)) - for _, o := range append(baseObjs, comparedObjs...) { - key := o.ObjectType + "/" + o.ObjectName - if _, ok := seen[key]; ok { - continue - } - seen[key] = struct{}{} - unionObjs = append(unionObjs, o) - } - objs = unionObjs - // USE again on the base side after the enumeration round-trip - // so the SHOW CREATE TABLE calls below resolve in the right db. - if _, err := h.runner.runSingleStringQuery(ctx, - fmt.Sprintf("USE %s", baseSchemaName)); err != nil { - return nil, fmt.Errorf("re-use base schema %q: %v", baseSchemaName, err) - } - if _, err := compareRunner.runSingleStringQuery(ctx, - fmt.Sprintf("USE %s", comparedSchemaName)); err != nil { - return nil, fmt.Errorf("re-use compared schema %q: %v", comparedSchemaName, err) - } - } - - sqls := make([]string, 0) - sqls = append(sqls, fmt.Sprintf("USE %s;", comparedSchemaName)) - - for _, obj := range objs { - switch obj.ObjectType { - case driverV2.ObjectType_TABLE: - stmts, terr := diffTableObject(ctx, h.runner, compareRunner, obj.ObjectName) - if terr != nil { - return nil, terr - } - sqls = append(sqls, stmts...) - case driverV2.ObjectType_VIEW: - stmts, verr := diffViewObject(ctx, h.runner, compareRunner, obj.ObjectName) - if verr != nil { - return nil, verr - } - sqls = append(sqls, stmts...) - case driverV2.ObjectType_FUNCTION: - // Compat-RISK-9: FUNCTION is planned for the second batch. - // Skip the FUNCTION object so the rest of the batch (TABLE / - // VIEW main paths) continues to produce ALTER / DROP+CREATE - // SQL. Aligned with PROCEDURE/TRIGGER/EVENT short-circuit and - // design §3.2.2 line 239 ("跳过 objInfo, results 不含该项"). - // See TC-HIVE-016 mixed-batch fix. - if h.log != nil { - h.log.WithField("object", obj.ObjectName). - WithField("objectType", "FUNCTION"). - Warnf("hive driver: %s; skipped", hiveFunctionUnsupportedMsg) - } - continue - case driverV2.ObjectType_PROCEDURE, - driverV2.ObjectType_TRIGGER, - driverV2.ObjectType_EVENT: - // Compat-RISK-4: short-circuit physically unsupported types. - if h.log != nil { - h.log.WithField("object", obj.ObjectName). - WithField("objectType", obj.ObjectType). - Warn("hive driver: object type not supported, skipped") - } - continue - default: - if h.log != nil { - h.log.WithField("object", obj.ObjectName). - WithField("objectType", obj.ObjectType). - Warn("hive driver: unknown object type, skipped") - } - continue - } - } - - results = append(results, &driverV2.DatabaseDiffModifySQLResult{ - SchemaName: comparedSchemaName, - ModifySQLs: sqls, - }) - } - return results, nil -} - -// fetchTableDDL runs SHOW CREATE TABLE for the given object and returns the -// concatenated DDL string. An empty string with exists=false indicates the -// table does not exist on that side (including query errors such as Hive's -// SemanticException for missing objects), matching the MySQL impl pattern. -func fetchTableDDL(ctx context.Context, runner hiveQueryRunner, objectName string) (string, bool, error) { - rows, err := runner.runSingleStringQuery(ctx, - fmt.Sprintf("SHOW CREATE TABLE %s", objectName)) - if err == nil && len(rows) > 0 { - return strings.Join(rows, "\n"), true, nil - } - return "", false, nil -} - -// diffTableObject generates the variant SQL for a single TABLE object. -// It captures DDL from both sides and dispatches to diffTableDDL for the -// detailed ALTER-vs-DROP+CREATE matrix decision. -func diffTableObject(ctx context.Context, baseRunner, compareRunner hiveQueryRunner, objectName string) ([]string, error) { - baseDDL, baseExists, err := fetchTableDDL(ctx, baseRunner, objectName) - if err != nil { - return nil, err - } - compareDDL, compareExists, err := fetchTableDDL(ctx, compareRunner, objectName) - if err != nil { - return nil, err - } - - switch { - case baseExists && !compareExists: - // Only on base side -> create on compared side (no WARNING). - return []string{ensureSemicolon(baseDDL)}, nil - case !baseExists && compareExists: - // Only on compared side -> drop from compared (no WARNING). - return []string{fmt.Sprintf("DROP TABLE IF EXISTS %s;", objectName)}, nil - case !baseExists && !compareExists: - // Neither side has it; nothing to do. - return nil, nil - } - - // Both sides exist: decide ALTER vs DROP+CREATE. - alters, fallback, err := diffTableDDL(baseDDL, compareDDL) - if err != nil { - return nil, fmt.Errorf("diffTableDDL %q: %v", objectName, err) - } - if fallback { - // DROP+CREATE with WARNING header (compat-RISK-6). - return []string{ - hiveWarningTableDropCreate + - fmt.Sprintf("DROP TABLE IF EXISTS %s;\n", objectName) + - ensureSemicolon(baseDDL), - }, nil - } - if len(alters) == 0 { - // No structural difference detected. - return nil, nil - } - return alters, nil -} - -// diffViewObject generates the variant SQL for a single VIEW object. -// Per design §3.4 / D3 decision: any view difference produces a unified -// DROP+CREATE with the view-recreated WARNING header. -func diffViewObject(ctx context.Context, baseRunner, compareRunner hiveQueryRunner, objectName string) ([]string, error) { - // Hive views use SHOW CREATE TABLE. - baseDDL, baseExists, err := fetchTableDDL(ctx, baseRunner, objectName) - if err != nil { - return nil, err - } - compareDDL, compareExists, err := fetchTableDDL(ctx, compareRunner, objectName) - if err != nil { - return nil, err - } - - switch { - case baseExists && !compareExists: - return []string{ensureSemicolon(baseDDL)}, nil - case !baseExists && compareExists: - return []string{fmt.Sprintf("DROP VIEW IF EXISTS %s;", objectName)}, nil - case !baseExists && !compareExists: - return nil, nil - } - - if normalizeWhitespace(baseDDL) == normalizeWhitespace(compareDDL) { - return nil, nil - } - // Any difference triggers DROP+CREATE with the VIEW-specific WARNING - // (compat-RISK-6, design §3.4 unified rule). - return []string{ - hiveWarningViewDropCreate + - fmt.Sprintf("DROP VIEW IF EXISTS %s;\n", objectName) + - ensureSemicolon(baseDDL), - }, nil -} - -// ensureSemicolon appends a trailing semicolon if the DDL string does not -// already end with one (ignoring trailing whitespace). -func ensureSemicolon(s string) string { - trimmed := strings.TrimRight(s, " \t\n\r") - if strings.HasSuffix(trimmed, ";") { - return trimmed + "\n" - } - return trimmed + ";\n" -} - -// normalizeWhitespace collapses runs of whitespace to a single space so -// that view DDL comparisons are not sensitive to formatting differences -// (HiveServer2 sometimes emits extra newlines). -func normalizeWhitespace(s string) string { - return strings.Join(strings.Fields(s), " ") -} - -func (h *HiveDriverImpl) Backup(ctx context.Context, backupStrategy string, sql string, backupMaxRows uint64) ([]string, string, error) { - return nil, "", fmt.Errorf("hive plugin does not support Backup") -} - -func (h *HiveDriverImpl) RecommendBackupStrategy(ctx context.Context, sql string) (*sqleDriver.RecommendBackupStrategyRes, error) { - return nil, fmt.Errorf("hive plugin does not support RecommendBackupStrategy") -} - -func (h *HiveDriverImpl) GetSelectivityOfSQLColumns(ctx context.Context, sql string) (map[string]map[string]float32, error) { - return nil, fmt.Errorf("hive plugin does not support GetSelectivityOfSQLColumns") -} - -// splitSQL splits SQL text by semicolons and filters out empty statements. -func splitSQL(sqlText string) []string { - parts := strings.Split(sqlText, ";") - result := make([]string, 0, len(parts)) - for _, p := range parts { - trimmed := strings.TrimSpace(p) - if trimmed != "" { - result = append(result, trimmed) - } - } - return result -} - -// classifySQL classifies a SQL statement by its keyword prefix. -// Returns driverV2.SQLTypeDQL, driverV2.SQLTypeDML, or driverV2.SQLTypeDDL. -func classifySQL(sql string) string { - upper := strings.ToUpper(strings.TrimSpace(sql)) - - switch { - case strings.HasPrefix(upper, "SELECT"), - strings.HasPrefix(upper, "WITH"), - strings.HasPrefix(upper, "SHOW"), - strings.HasPrefix(upper, "DESCRIBE"), - strings.HasPrefix(upper, "DESC"), - strings.HasPrefix(upper, "EXPLAIN"): - return driverV2.SQLTypeDQL - case strings.HasPrefix(upper, "INSERT"), - strings.HasPrefix(upper, "UPDATE"), - strings.HasPrefix(upper, "DELETE"), - strings.HasPrefix(upper, "MERGE"), - strings.HasPrefix(upper, "LOAD"), - strings.HasPrefix(upper, "EXPORT"): - return driverV2.SQLTypeDML - default: - return driverV2.SQLTypeDDL - } -} diff --git a/sqle/driver/hive/hive_compare_test.go b/sqle/driver/hive/hive_compare_test.go deleted file mode 100644 index eca620264..000000000 --- a/sqle/driver/hive/hive_compare_test.go +++ /dev/null @@ -1,1136 +0,0 @@ -// Tests for the Hive driver's compare-capability surface introduced by -// Issue #2872. The 16 `Test_*` functions defined here cover every row of -// the design.md §3.4 variant SQL matrix and §5.2.4 unit-test table, plus -// the metas declaration required by compat-RISK-1. -// -// Design choices: -// - Each test uses the map-case style required by dev.md (key = scenario, -// value = inputs + expectations); table iteration is deterministic via -// sorted keys when output order matters. -// - No network or database is touched: the Hive cursor is replaced with -// fakeQueryRunner that satisfies the hiveQueryRunner interface. -// - WARNING text assertions use strings.Contains rather than full-string -// equality so cosmetic tweaks (whitespace, trailing newline) do not -// break tests. -package hive - -import ( - "context" - "sort" - "strings" - "testing" - - driverV2 "github.com/actiontech/sqle/sqle/driver/v2" - "github.com/sirupsen/logrus" -) - -// fakeQueryRunner implements hiveQueryRunner using a preloaded map of -// query string -> rows. The match is performed by exact equality first, -// then by prefix (so callers can register a "SHOW CREATE TABLE tbl_order" -// response without having to anticipate USE-statement prefixes). -// -// When a query is registered with errOnQuery (non-empty), runSingleStringQuery -// returns that as an error to simulate Hive failures. -type fakeQueryRunner struct { - rows map[string][]string - errOnQuery map[string]string - // log captures every query the code under test issued, in order. - log []string -} - -func newFakeRunner() *fakeQueryRunner { - return &fakeQueryRunner{ - rows: map[string][]string{}, - errOnQuery: map[string]string{}, - } -} - -func (f *fakeQueryRunner) on(query string, rows ...string) *fakeQueryRunner { - f.rows[query] = rows - return f -} - -func (f *fakeQueryRunner) fail(query, errMsg string) *fakeQueryRunner { - f.errOnQuery[query] = errMsg - return f -} - -func (f *fakeQueryRunner) runSingleStringQuery(_ context.Context, q string) ([]string, error) { - f.log = append(f.log, q) - if msg, ok := f.errOnQuery[q]; ok { - return nil, fakeError(msg) - } - if rows, ok := f.rows[q]; ok { - return rows, nil - } - // Default: pretend the table/view does not exist by returning an error - // (matches how SHOW CREATE TABLE behaves for missing objects). - return nil, fakeError("missing object for query: " + q) -} - -type fakeError string - -func (e fakeError) Error() string { return string(e) } - -// newDriverWithRunners builds a HiveDriverImpl wired up with provided -// fake runners for base and compared sides. The compareRunnerFactory -// returns the supplied compareRunner regardless of DSN. -func newDriverWithRunners(base, compared hiveQueryRunner) *HiveDriverImpl { - return &HiveDriverImpl{ - log: logrus.NewEntry(logrus.New()), - runner: base, - compareRunnerFactory: func(_ *driverV2.DSN) (hiveQueryRunner, func(), error) { - return compared, func() {}, nil - }, - } -} - -// --------------------------------------------------------------------- // -// 1. Test_Metas_EnabledOptional -// --------------------------------------------------------------------- // - -// Test_Metas_EnabledOptional verifies compat-RISK-1: the metas exposed by -// the Hive driver must declare BOTH OptionalGetDatabaseObjectDDL and -// OptionalGetDatabaseDiffModifySQL so the structure-compare capability -// check whitelist accepts Hive. -func Test_Metas_EnabledOptional(t *testing.T) { - cases := map[string]struct { - want driverV2.OptionalModule - }{ - "OptionalGetDatabaseObjectDDL": {want: driverV2.OptionalGetDatabaseObjectDDL}, - "OptionalGetDatabaseDiffModifySQL": {want: driverV2.OptionalGetDatabaseDiffModifySQL}, - } - p := &PluginProcessor{} - metas, err := p.GetDriverMetas() - if err != nil { - t.Fatalf("GetDriverMetas: %v", err) - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - if !metas.IsOptionalModuleEnabled(tc.want) { - t.Errorf("metas missing %v; got modules=%v", tc.want, metas.EnabledOptionalModule) - } - }) - } -} - -// --------------------------------------------------------------------- // -// 2. Test_GetDatabaseObjectDDL_TableHappy -// --------------------------------------------------------------------- // - -// Test_GetDatabaseObjectDDL_TableHappy verifies that TABLE objects flow -// through SHOW CREATE TABLE and the rows are concatenated as the DDL. -func Test_GetDatabaseObjectDDL_TableHappy(t *testing.T) { - cases := map[string]struct { - schema string - object string - rows []string - want string - }{ - "single row": { - schema: "sqle_compare_test", - object: "tbl_order", - rows: []string{"CREATE TABLE tbl_order (id BIGINT) STORED AS ORC;"}, - want: "CREATE TABLE tbl_order (id BIGINT) STORED AS ORC;", - }, - "multi row joined by newline": { - schema: "default", - object: "users", - rows: []string{"CREATE TABLE users(", " id INT", ") STORED AS TEXTFILE;"}, - want: "CREATE TABLE users(\n id INT\n) STORED AS TEXTFILE;", - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - runner := newFakeRunner(). - on("USE "+tc.schema, "OK"). - on("SHOW CREATE TABLE "+tc.object, tc.rows...) - h := &HiveDriverImpl{ - log: logrus.NewEntry(logrus.New()), - runner: runner, - } - results, err := h.GetDatabaseObjectDDL(context.Background(), - []*driverV2.DatabaseSchemaInfo{{ - SchemaName: tc.schema, - DatabaseObjects: []*driverV2.DatabaseObject{ - {ObjectName: tc.object, ObjectType: driverV2.ObjectType_TABLE}, - }, - }}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(results) != 1 || len(results[0].DatabaseObjectDDLs) != 1 { - t.Fatalf("unexpected results shape: %+v", results) - } - got := results[0].DatabaseObjectDDLs[0].ObjectDDL - if got != tc.want { - t.Errorf("DDL mismatch:\n got=%q\nwant=%q", got, tc.want) - } - }) - } -} - -// --------------------------------------------------------------------- // -// 3. Test_GetDatabaseObjectDDL_ViewHappy -// --------------------------------------------------------------------- // - -// Test_GetDatabaseObjectDDL_ViewHappy verifies that VIEW objects ALSO go -// through SHOW CREATE TABLE (Hive reuses the same command for views). -func Test_GetDatabaseObjectDDL_ViewHappy(t *testing.T) { - cases := map[string]struct { - object string - rows []string - }{ - "basic view": { - object: "v_order_active", - rows: []string{ - "CREATE VIEW v_order_active AS SELECT id, name FROM tbl_order WHERE 1=1;", - }, - }, - } - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - runner := newFakeRunner(). - on("USE default", "OK"). - on("SHOW CREATE TABLE "+tc.object, tc.rows...) - h := &HiveDriverImpl{log: logrus.NewEntry(logrus.New()), runner: runner} - results, err := h.GetDatabaseObjectDDL(context.Background(), - []*driverV2.DatabaseSchemaInfo{{ - SchemaName: "default", - DatabaseObjects: []*driverV2.DatabaseObject{ - {ObjectName: tc.object, ObjectType: driverV2.ObjectType_VIEW}, - }, - }}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(results[0].DatabaseObjectDDLs) != 1 { - t.Fatalf("expected 1 DDL, got %d", len(results[0].DatabaseObjectDDLs)) - } - // The driver must dispatch the same SHOW CREATE TABLE query for - // VIEW; verify against the call log. - found := false - for _, q := range runner.log { - if q == "SHOW CREATE TABLE "+tc.object { - found = true - break - } - } - if !found { - t.Errorf("expected SHOW CREATE TABLE call for VIEW; got log=%v", runner.log) - } - }) - } -} - -// --------------------------------------------------------------------- // -// 4. Test_GetDatabaseObjectDDL_FunctionRejected -// --------------------------------------------------------------------- // - -// Test_GetDatabaseObjectDDL_FunctionRejected verifies compat-RISK-9 after -// the FIX-002 driver alignment (TC-HIVE-015): FUNCTION objects are silently -// skipped (no Go error, no placeholder DDL entry), matching the behaviour -// of the PROCEDURE/TRIGGER/EVENT short-circuit. The Chinese error message -// is emitted via the WARN log only — never as a returned Go error. -func Test_GetDatabaseObjectDDL_FunctionRejected(t *testing.T) { - cases := map[string]struct { - object string - }{ - "function planned for batch 2": { - object: "my_udf", - }, - } - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - runner := newFakeRunner().on("USE default", "OK") - h := &HiveDriverImpl{log: logrus.NewEntry(logrus.New()), runner: runner} - results, err := h.GetDatabaseObjectDDL(context.Background(), - []*driverV2.DatabaseSchemaInfo{{ - SchemaName: "default", - DatabaseObjects: []*driverV2.DatabaseObject{ - {ObjectName: tc.object, ObjectType: driverV2.ObjectType_FUNCTION}, - }, - }}) - if err != nil { - t.Fatalf("expected nil error for FUNCTION skip, got %v", err) - } - if len(results) != 1 { - t.Fatalf("expected 1 schema result entry, got %d", len(results)) - } - if got := len(results[0].DatabaseObjectDDLs); got != 0 { - t.Errorf("expected 0 DDLs (FUNCTION skipped) but got %d entries: %#v", - got, results[0].DatabaseObjectDDLs) - } - }) - } -} - -// --------------------------------------------------------------------- // -// 5. Test_GetDatabaseObjectDDL_UnsupportedTypeShortCircuit -// --------------------------------------------------------------------- // - -// Test_GetDatabaseObjectDDL_UnsupportedTypeShortCircuit verifies -// compat-RISK-4: PROCEDURE / TRIGGER / EVENT are silently skipped (no -// error, no panic). One case per object type. -func Test_GetDatabaseObjectDDL_UnsupportedTypeShortCircuit(t *testing.T) { - cases := map[string]string{ - "PROCEDURE": driverV2.ObjectType_PROCEDURE, - "TRIGGER": driverV2.ObjectType_TRIGGER, - "EVENT": driverV2.ObjectType_EVENT, - } - for name, objType := range cases { - t.Run(name, func(t *testing.T) { - runner := newFakeRunner().on("USE default", "OK") - h := &HiveDriverImpl{log: logrus.NewEntry(logrus.New()), runner: runner} - results, err := h.GetDatabaseObjectDDL(context.Background(), - []*driverV2.DatabaseSchemaInfo{{ - SchemaName: "default", - DatabaseObjects: []*driverV2.DatabaseObject{ - {ObjectName: "x_obj", ObjectType: objType}, - }, - }}) - if err != nil { - t.Fatalf("expected nil error for %s short-circuit, got %v", objType, err) - } - if len(results) != 1 { - t.Fatalf("expected 1 result entry, got %d", len(results)) - } - if got := len(results[0].DatabaseObjectDDLs); got != 0 { - t.Errorf("expected 0 DDLs (skipped) for %s, got %d", objType, got) - } - }) - } -} - -// --------------------------------------------------------------------- // -// 6. Test_GetDatabaseObjectDDL_EmptyObjectList -// --------------------------------------------------------------------- // - -// Test_GetDatabaseObjectDDL_EmptyObjectList verifies the **new contract** -// for empty DatabaseObjects (compat-RISK-10 secondary fix, Task-TEST-FIX-001): -// -// - When the caller passes an empty DatabaseObjects slice, the driver -// now auto-enumerates every TABLE/VIEW in the schema via SHOW TABLES -// / SHOW VIEWS (mirroring MySQL driver mysql_ee.go::GetDatabaseObjectDDL -// line 380-389). -// - This is required because server/compare/database_compare_ee.go -// ExecDatabaseCompare never populates DatabaseObjects itself — it only -// forwards SchemaName, expecting the driver to discover the rest. -// -// The previous contract ("empty list = zero DDLs returned") was incompatible -// with the controller's actual call shape and would always yield "same". -func Test_GetDatabaseObjectDDL_EmptyObjectList(t *testing.T) { - cases := map[string]struct { - schema string - showTables []string - showViews []string - showCreates map[string]string // object name -> DDL row - expectCount int - }{ - "empty objects in default schema": { - schema: "default", - showTables: []string{"t1", "v1"}, - showViews: []string{"v1"}, - showCreates: map[string]string{ - "t1": "CREATE TABLE `t1` (`id` int)", - "v1": "CREATE VIEW `v1` AS SELECT 1", - }, - expectCount: 2, - }, - "empty objects in named schema": { - schema: "sqle_compare_test", - showTables: []string{"t_diff_only"}, - showViews: []string{}, - showCreates: map[string]string{ - "t_diff_only": "CREATE TABLE `t_diff_only` (`a` string)", - }, - expectCount: 1, - }, - "empty schema name falls back default": { - schema: "", - showTables: []string{}, - showViews: []string{}, - expectCount: 0, - }, - } - for name, tc := range cases { - tc := tc - t.Run(name, func(t *testing.T) { - effective := tc.schema - if effective == "" { - effective = "default" - } - runner := newFakeRunner(). - on("USE "+effective, "OK") - // Auto-discovery scripts: SHOW TABLES, SHOW VIEWS, then SHOW - // CREATE TABLE for every discovered object. fakeRunner.on uses - // a variadic rows... param so a slice expands via "...". - runner.on("SHOW TABLES", tc.showTables...) - runner.on("SHOW VIEWS", tc.showViews...) - for obj, ddl := range tc.showCreates { - runner.on("SHOW CREATE TABLE "+obj, ddl) - } - h := &HiveDriverImpl{log: logrus.NewEntry(logrus.New()), runner: runner} - results, err := h.GetDatabaseObjectDDL(context.Background(), - []*driverV2.DatabaseSchemaInfo{{ - SchemaName: tc.schema, - DatabaseObjects: nil, - }}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(results) != 1 { - t.Fatalf("expected 1 schema result, got %d", len(results)) - } - if got := len(results[0].DatabaseObjectDDLs); got != tc.expectCount { - t.Errorf("expected %d DDLs from auto-discovery, got %d", tc.expectCount, got) - } - }) - } -} - -// --------------------------------------------------------------------- // -// 7. Test_GetDatabaseDiffModifySQL_OnlyBaseHasTable -// --------------------------------------------------------------------- // - -// Test_GetDatabaseDiffModifySQL_OnlyBaseHasTable: design §3.4 row -// "TABLE / 只在基准侧" -> CREATE TABLE on compared side; no WARNING. -func Test_GetDatabaseDiffModifySQL_OnlyBaseHasTable(t *testing.T) { - cases := map[string]struct { - object string - ddl string - }{ - "new table only in base": { - object: "tbl_order", - ddl: "CREATE TABLE tbl_order (id BIGINT, name STRING) STORED AS ORC", - }, - } - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - base := newFakeRunner(). - on("USE base_schema", "OK"). - on("SHOW CREATE TABLE "+tc.object, tc.ddl) - compared := newFakeRunner(). - on("USE compared_schema", "OK") - // SHOW CREATE TABLE on compared returns default error → not found - h := newDriverWithRunners(base, compared) - results, err := h.GetDatabaseDiffModifySQL(context.Background(), - &driverV2.DSN{}, - []*driverV2.DatabasCompareSchemaInfo{{ - BaseSchemaName: "base_schema", - ComparedSchemaName: "compared_schema", - DatabaseObjects: []*driverV2.DatabaseObject{ - {ObjectName: tc.object, ObjectType: driverV2.ObjectType_TABLE}, - }, - }}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(results) != 1 { - t.Fatalf("expected 1 result, got %d", len(results)) - } - full := strings.Join(results[0].ModifySQLs, "\n") - if !strings.Contains(full, "USE compared_schema;") { - t.Errorf("expected USE compared_schema; header; got=%q", full) - } - if !strings.Contains(full, "CREATE TABLE "+tc.object) { - t.Errorf("expected CREATE TABLE %s; got=%q", tc.object, full) - } - if strings.Contains(full, "WARNING") { - t.Errorf("expected NO WARNING for only-in-base case; got=%q", full) - } - }) - } -} - -// --------------------------------------------------------------------- // -// 8. Test_GetDatabaseDiffModifySQL_OnlyTargetHasTable -// --------------------------------------------------------------------- // - -// Test_GetDatabaseDiffModifySQL_OnlyTargetHasTable: design §3.4 row -// "TABLE / 只在比对侧" -> DROP TABLE IF EXISTS on compared side; no WARNING. -func Test_GetDatabaseDiffModifySQL_OnlyTargetHasTable(t *testing.T) { - cases := map[string]struct { - object string - ddl string - }{ - "orphan table only in compared": { - object: "tbl_stale", - ddl: "CREATE TABLE tbl_stale (id INT)", - }, - } - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - base := newFakeRunner(). - on("USE base_schema", "OK") - // SHOW CREATE TABLE on base returns default "not found" - compared := newFakeRunner(). - on("USE compared_schema", "OK"). - on("SHOW CREATE TABLE "+tc.object, tc.ddl) - h := newDriverWithRunners(base, compared) - results, err := h.GetDatabaseDiffModifySQL(context.Background(), - &driverV2.DSN{}, - []*driverV2.DatabasCompareSchemaInfo{{ - BaseSchemaName: "base_schema", - ComparedSchemaName: "compared_schema", - DatabaseObjects: []*driverV2.DatabaseObject{ - {ObjectName: tc.object, ObjectType: driverV2.ObjectType_TABLE}, - }, - }}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - full := strings.Join(results[0].ModifySQLs, "\n") - if !strings.Contains(full, "DROP TABLE IF EXISTS "+tc.object) { - t.Errorf("expected DROP TABLE IF EXISTS %s; got=%q", tc.object, full) - } - if strings.Contains(full, "WARNING") { - t.Errorf("expected NO WARNING for only-in-compared case; got=%q", full) - } - }) - } -} - -// --------------------------------------------------------------------- // -// 9. Test_GetDatabaseDiffModifySQL_TableAddColumn -// --------------------------------------------------------------------- // - -// Test_GetDatabaseDiffModifySQL_TableAddColumn: design §3.4 row -// "TABLE / 加列 (ADD COLUMN)" -> ALTER ADD COLUMNS; no WARNING. -func Test_GetDatabaseDiffModifySQL_TableAddColumn(t *testing.T) { - cases := map[string]struct { - object string - baseDDL string - compDDL string - // substrings the produced SQL block must contain - wantContains []string - // substrings the produced SQL block must NOT contain - wantNotContains []string - }{ - "add a single column at end": { - object: "tbl_order", - baseDDL: "CREATE TABLE tbl_order (id BIGINT, name STRING, age INT) STORED AS ORC", - compDDL: "CREATE TABLE tbl_order (id BIGINT, name STRING) STORED AS ORC", - wantContains: []string{ - "ALTER TABLE tbl_order ADD COLUMNS (age INT)", - }, - wantNotContains: []string{"WARNING", "DROP TABLE"}, - }, - } - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - base := newFakeRunner(). - on("USE base_schema", "OK"). - on("SHOW CREATE TABLE "+tc.object, tc.baseDDL) - compared := newFakeRunner(). - on("USE compared_schema", "OK"). - on("SHOW CREATE TABLE "+tc.object, tc.compDDL) - h := newDriverWithRunners(base, compared) - results, err := h.GetDatabaseDiffModifySQL(context.Background(), - &driverV2.DSN{}, - []*driverV2.DatabasCompareSchemaInfo{{ - BaseSchemaName: "base_schema", - ComparedSchemaName: "compared_schema", - DatabaseObjects: []*driverV2.DatabaseObject{ - {ObjectName: tc.object, ObjectType: driverV2.ObjectType_TABLE}, - }, - }}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - full := strings.Join(results[0].ModifySQLs, "\n") - for _, want := range tc.wantContains { - if !strings.Contains(full, want) { - t.Errorf("expected SQL to contain %q; got=%q", want, full) - } - } - for _, notWant := range tc.wantNotContains { - if strings.Contains(full, notWant) { - t.Errorf("expected SQL NOT to contain %q; got=%q", notWant, full) - } - } - }) - } -} - -// --------------------------------------------------------------------- // -// 10. Test_GetDatabaseDiffModifySQL_TableChangeColumnType_Compat -// --------------------------------------------------------------------- // - -// Test_GetDatabaseDiffModifySQL_TableChangeColumnType_Compat: design §3.4 -// row "TABLE / 改列类型(兼容 widen)" -> ALTER CHANGE COLUMN; no WARNING. -func Test_GetDatabaseDiffModifySQL_TableChangeColumnType_Compat(t *testing.T) { - cases := map[string]struct { - baseDDL string - compDDL string - }{ - "int to bigint widening": { - baseDDL: "CREATE TABLE t (id BIGINT, age INT) STORED AS ORC", - compDDL: "CREATE TABLE t (id INT, age INT) STORED AS ORC", - }, - } - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - base := newFakeRunner(). - on("USE base_schema", "OK"). - on("SHOW CREATE TABLE t", tc.baseDDL) - compared := newFakeRunner(). - on("USE compared_schema", "OK"). - on("SHOW CREATE TABLE t", tc.compDDL) - h := newDriverWithRunners(base, compared) - results, err := h.GetDatabaseDiffModifySQL(context.Background(), - &driverV2.DSN{}, - []*driverV2.DatabasCompareSchemaInfo{{ - BaseSchemaName: "base_schema", - ComparedSchemaName: "compared_schema", - DatabaseObjects: []*driverV2.DatabaseObject{ - {ObjectName: "t", ObjectType: driverV2.ObjectType_TABLE}, - }, - }}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - full := strings.Join(results[0].ModifySQLs, "\n") - if !strings.Contains(full, "ALTER TABLE t CHANGE COLUMN id id BIGINT") { - t.Errorf("expected CHANGE COLUMN id id BIGINT; got=%q", full) - } - if strings.Contains(full, "WARNING") { - t.Errorf("compatible widening should not emit WARNING; got=%q", full) - } - if strings.Contains(full, "DROP TABLE") { - t.Errorf("compatible widening must not fall back to DROP+CREATE; got=%q", full) - } - }) - } -} - -// --------------------------------------------------------------------- // -// 11. Test_GetDatabaseDiffModifySQL_TableChangeColumnType_Incompat -// --------------------------------------------------------------------- // - -// Test_GetDatabaseDiffModifySQL_TableChangeColumnType_Incompat: design §3.4 -// row "TABLE / 改列类型(不兼容)" -> DROP+CREATE; WARNING (data loss). -func Test_GetDatabaseDiffModifySQL_TableChangeColumnType_Incompat(t *testing.T) { - cases := map[string]struct { - baseDDL string - compDDL string - }{ - "bigint to string is incompatible": { - baseDDL: "CREATE TABLE t (id STRING) STORED AS ORC", - compDDL: "CREATE TABLE t (id BIGINT) STORED AS ORC", - }, - "int to timestamp is incompatible": { - baseDDL: "CREATE TABLE t (ts TIMESTAMP) STORED AS ORC", - compDDL: "CREATE TABLE t (ts INT) STORED AS ORC", - }, - } - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - base := newFakeRunner(). - on("USE base_schema", "OK"). - on("SHOW CREATE TABLE t", tc.baseDDL) - compared := newFakeRunner(). - on("USE compared_schema", "OK"). - on("SHOW CREATE TABLE t", tc.compDDL) - h := newDriverWithRunners(base, compared) - results, err := h.GetDatabaseDiffModifySQL(context.Background(), - &driverV2.DSN{}, - []*driverV2.DatabasCompareSchemaInfo{{ - BaseSchemaName: "base_schema", - ComparedSchemaName: "compared_schema", - DatabaseObjects: []*driverV2.DatabaseObject{ - {ObjectName: "t", ObjectType: driverV2.ObjectType_TABLE}, - }, - }}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - full := strings.Join(results[0].ModifySQLs, "\n") - if !strings.Contains(full, "DROP TABLE IF EXISTS t") { - t.Errorf("expected DROP TABLE IF EXISTS t; got=%q", full) - } - if !strings.Contains(full, "CREATE TABLE t") { - t.Errorf("expected CREATE TABLE t; got=%q", full) - } - if !strings.Contains(full, "-- WARNING:") { - t.Errorf("expected WARNING marker for data loss case; got=%q", full) - } - if !strings.Contains(full, "数据将丢失") { - t.Errorf("expected Chinese data loss warning; got=%q", full) - } - }) - } -} - -// --------------------------------------------------------------------- // -// 12. Test_GetDatabaseDiffModifySQL_TableChangeStoredAs -// --------------------------------------------------------------------- // - -// Test_GetDatabaseDiffModifySQL_TableChangeStoredAs: design §3.4 row -// "TABLE / 改存储格式 (STORED AS)" -> DROP+CREATE; WARNING. -func Test_GetDatabaseDiffModifySQL_TableChangeStoredAs(t *testing.T) { - cases := map[string]struct { - baseDDL string - compDDL string - }{ - "ORC to PARQUET storage change": { - baseDDL: "CREATE TABLE t (id BIGINT) STORED AS PARQUET", - compDDL: "CREATE TABLE t (id BIGINT) STORED AS ORC", - }, - "TEXTFILE to ORC storage change": { - baseDDL: "CREATE TABLE t (id BIGINT) STORED AS ORC", - compDDL: "CREATE TABLE t (id BIGINT) STORED AS TEXTFILE", - }, - } - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - base := newFakeRunner(). - on("USE base_schema", "OK"). - on("SHOW CREATE TABLE t", tc.baseDDL) - compared := newFakeRunner(). - on("USE compared_schema", "OK"). - on("SHOW CREATE TABLE t", tc.compDDL) - h := newDriverWithRunners(base, compared) - results, err := h.GetDatabaseDiffModifySQL(context.Background(), - &driverV2.DSN{}, - []*driverV2.DatabasCompareSchemaInfo{{ - BaseSchemaName: "base_schema", - ComparedSchemaName: "compared_schema", - DatabaseObjects: []*driverV2.DatabaseObject{ - {ObjectName: "t", ObjectType: driverV2.ObjectType_TABLE}, - }, - }}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - full := strings.Join(results[0].ModifySQLs, "\n") - if !strings.Contains(full, "DROP TABLE IF EXISTS t") { - t.Errorf("expected DROP TABLE IF EXISTS t; got=%q", full) - } - if !strings.Contains(full, "-- WARNING:") { - t.Errorf("expected WARNING marker; got=%q", full) - } - }) - } -} - -// --------------------------------------------------------------------- // -// 13. Test_GetDatabaseDiffModifySQL_ViewDiff_AlwaysDropCreate -// --------------------------------------------------------------------- // - -// Test_GetDatabaseDiffModifySQL_ViewDiff_AlwaysDropCreate: design §3.4 D3 -// row "VIEW / 任意差异" -> 统一 DROP+CREATE; WARNING (view recreated). -func Test_GetDatabaseDiffModifySQL_ViewDiff_AlwaysDropCreate(t *testing.T) { - cases := map[string]struct { - baseDDL string - compDDL string - }{ - "select clause differs": { - baseDDL: "CREATE VIEW v AS SELECT id, name FROM t WHERE 1=1", - compDDL: "CREATE VIEW v AS SELECT id FROM t", - }, - "trivial whitespace-only diff should be ignored": { - baseDDL: "CREATE VIEW v AS SELECT id FROM t", - compDDL: "CREATE VIEW v AS SELECT id FROM t", - }, - } - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - base := newFakeRunner(). - on("USE base_schema", "OK"). - on("SHOW CREATE TABLE v", tc.baseDDL) - compared := newFakeRunner(). - on("USE compared_schema", "OK"). - on("SHOW CREATE TABLE v", tc.compDDL) - h := newDriverWithRunners(base, compared) - results, err := h.GetDatabaseDiffModifySQL(context.Background(), - &driverV2.DSN{}, - []*driverV2.DatabasCompareSchemaInfo{{ - BaseSchemaName: "base_schema", - ComparedSchemaName: "compared_schema", - DatabaseObjects: []*driverV2.DatabaseObject{ - {ObjectName: "v", ObjectType: driverV2.ObjectType_VIEW}, - }, - }}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - full := strings.Join(results[0].ModifySQLs, "\n") - // Whitespace-only diffs must NOT emit DROP+CREATE. - if name == "trivial whitespace-only diff should be ignored" { - if strings.Contains(full, "DROP VIEW") { - t.Errorf("trivial whitespace diff must not emit DROP VIEW; got=%q", full) - } - return - } - if !strings.Contains(full, "DROP VIEW IF EXISTS v") { - t.Errorf("expected DROP VIEW IF EXISTS v; got=%q", full) - } - if !strings.Contains(full, "-- WARNING:") { - t.Errorf("expected view-recreated WARNING marker; got=%q", full) - } - if !strings.Contains(full, "视图将被重建") { - t.Errorf("expected Chinese view-recreated message; got=%q", full) - } - }) - } -} - -// --------------------------------------------------------------------- // -// 14. Test_GetDatabaseDiffModifySQL_FunctionRejected -// --------------------------------------------------------------------- // - -// Test_GetDatabaseDiffModifySQL_FunctionRejected: design §3.2.2 line 239 -// row "FUNCTION / 第二批". After FIX-002 (TC-HIVE-015 / 016): the driver -// silently skips the FUNCTION object — no Go error, no FUNCTION entry in -// the SQL block. Only the leading `USE compared_schema;` header remains; -// upstream layers can detect "all objects unsupported" by the empty body -// (compat-RISK-9 verified; aligned with PROCEDURE/TRIGGER/EVENT -// short-circuit at line 824). -func Test_GetDatabaseDiffModifySQL_FunctionRejected(t *testing.T) { - cases := map[string]struct { - object string - }{ - "function returns no error and skips silently": { - object: "my_udf", - }, - } - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - base := newFakeRunner().on("USE base_schema", "OK") - compared := newFakeRunner().on("USE compared_schema", "OK") - h := newDriverWithRunners(base, compared) - results, err := h.GetDatabaseDiffModifySQL(context.Background(), - &driverV2.DSN{}, - []*driverV2.DatabasCompareSchemaInfo{{ - BaseSchemaName: "base_schema", - ComparedSchemaName: "compared_schema", - DatabaseObjects: []*driverV2.DatabaseObject{ - {ObjectName: tc.object, ObjectType: driverV2.ObjectType_FUNCTION}, - }, - }}) - if err != nil { - t.Fatalf("expected nil error for FUNCTION skip, got %v", err) - } - if len(results) != 1 { - t.Fatalf("expected 1 schema result, got %d", len(results)) - } - full := strings.Join(results[0].ModifySQLs, "\n") - // Only the USE header should be present; no FUNCTION-related output. - if !strings.Contains(full, "USE compared_schema;") { - t.Errorf("expected USE compared_schema; header in block; got=%q", full) - } - if strings.Contains(full, tc.object) { - t.Errorf("expected FUNCTION object %q to be skipped from results; got block=%q", - tc.object, full) - } - if strings.Contains(full, "DROP") || - strings.Contains(full, "CREATE") || - strings.Contains(full, "ALTER") { - t.Errorf("expected FUNCTION to produce no DDL; got block=%q", full) - } - }) - } -} - -// --------------------------------------------------------------------- // -// 14b. Test_GetDatabaseDiffModifySQL_MixedFunctionAndTable -// --------------------------------------------------------------------- // - -// Test_GetDatabaseDiffModifySQL_MixedFunctionAndTable: TC-HIVE-016 (compat- -// RISK-9). Verifies that a mixed batch containing both a TABLE and a -// FUNCTION lets the TABLE main path produce its ALTER SQL while the -// FUNCTION is silently skipped. Before FIX-002 the driver returned a hard -// error at the FUNCTION branch, dropping the TABLE result entirely; this -// test pins the fixed behaviour. -func Test_GetDatabaseDiffModifySQL_MixedFunctionAndTable(t *testing.T) { - cases := map[string]struct { - baseDDL string - compDDL string - }{ - "int to bigint widening alongside FUNCTION": { - baseDDL: "CREATE TABLE t_alter_widen (amt BIGINT) STORED AS ORC", - compDDL: "CREATE TABLE t_alter_widen (amt INT) STORED AS ORC", - }, - } - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - base := newFakeRunner(). - on("USE base_schema", "OK"). - on("SHOW CREATE TABLE t_alter_widen", tc.baseDDL) - compared := newFakeRunner(). - on("USE compared_schema", "OK"). - on("SHOW CREATE TABLE t_alter_widen", tc.compDDL) - h := newDriverWithRunners(base, compared) - results, err := h.GetDatabaseDiffModifySQL(context.Background(), - &driverV2.DSN{}, - []*driverV2.DatabasCompareSchemaInfo{{ - BaseSchemaName: "base_schema", - ComparedSchemaName: "compared_schema", - DatabaseObjects: []*driverV2.DatabaseObject{ - {ObjectName: "t_alter_widen", ObjectType: driverV2.ObjectType_TABLE}, - {ObjectName: "fake_fn", ObjectType: driverV2.ObjectType_FUNCTION}, - }, - }}) - if err != nil { - t.Fatalf("expected nil error for mixed batch, got %v", err) - } - if len(results) != 1 { - t.Fatalf("expected 1 schema result, got %d", len(results)) - } - full := strings.Join(results[0].ModifySQLs, "\n") - // TABLE main path: ALTER produced. - if !strings.Contains(full, "ALTER TABLE t_alter_widen CHANGE COLUMN amt amt BIGINT") { - t.Errorf("expected TABLE ALTER CHANGE COLUMN amt amt BIGINT; got=%q", full) - } - // FUNCTION must be skipped — no fake_fn anywhere in the block. - if strings.Contains(full, "fake_fn") { - t.Errorf("expected FUNCTION fake_fn to be skipped; got block=%q", full) - } - // No WARNING / DROP fallback for a compatible widen. - if strings.Contains(full, "WARNING") { - t.Errorf("compatible widening should not emit WARNING; got=%q", full) - } - if strings.Contains(full, "DROP TABLE") { - t.Errorf("compatible widening must not fall back to DROP+CREATE; got=%q", full) - } - // Header preserved. - if !strings.Contains(full, "USE compared_schema;") { - t.Errorf("expected USE compared_schema; header in block; got=%q", full) - } - }) - } -} - -// --------------------------------------------------------------------- // -// 14c. Test_GetDatabaseObjectDDL_MixedFunctionAndTable -// --------------------------------------------------------------------- // - -// Test_GetDatabaseObjectDDL_MixedFunctionAndTable: TC-HIVE-016 sister -// coverage for the SHOW CREATE TABLE path. A mixed batch (TABLE + -// FUNCTION) returns the TABLE DDL and silently skips the FUNCTION — -// the result entry contains exactly one DatabaseObjectDDL for the TABLE. -func Test_GetDatabaseObjectDDL_MixedFunctionAndTable(t *testing.T) { - runner := newFakeRunner(). - on("USE default", "OK"). - on("SHOW CREATE TABLE tbl_order", - "CREATE TABLE tbl_order (id BIGINT) STORED AS ORC;") - h := &HiveDriverImpl{log: logrus.NewEntry(logrus.New()), runner: runner} - results, err := h.GetDatabaseObjectDDL(context.Background(), - []*driverV2.DatabaseSchemaInfo{{ - SchemaName: "default", - DatabaseObjects: []*driverV2.DatabaseObject{ - {ObjectName: "tbl_order", ObjectType: driverV2.ObjectType_TABLE}, - {ObjectName: "fake_fn", ObjectType: driverV2.ObjectType_FUNCTION}, - }, - }}) - if err != nil { - t.Fatalf("expected nil error for mixed batch, got %v", err) - } - if len(results) != 1 { - t.Fatalf("expected 1 schema result, got %d", len(results)) - } - ddls := results[0].DatabaseObjectDDLs - if len(ddls) != 1 { - t.Fatalf("expected exactly 1 DDL (TABLE only), got %d: %#v", len(ddls), ddls) - } - if ddls[0].DatabaseObject == nil || - ddls[0].DatabaseObject.ObjectType != driverV2.ObjectType_TABLE || - ddls[0].DatabaseObject.ObjectName != "tbl_order" { - t.Errorf("expected TABLE tbl_order entry; got %#v", ddls[0]) - } - if !strings.Contains(ddls[0].ObjectDDL, "CREATE TABLE tbl_order") { - t.Errorf("expected SHOW CREATE TABLE output for tbl_order; got=%q", ddls[0].ObjectDDL) - } -} - -// --------------------------------------------------------------------- // -// 15. Test_GetDatabaseDiffModifySQL_UnsupportedTypeFiltered -// --------------------------------------------------------------------- // - -// Test_GetDatabaseDiffModifySQL_UnsupportedTypeFiltered: design §3.2.2 -// row PROCEDURE/TRIGGER/EVENT -> driver skips them; SQL block only -// contains the USE prefix (compat-RISK-4). -func Test_GetDatabaseDiffModifySQL_UnsupportedTypeFiltered(t *testing.T) { - cases := map[string]string{ - "PROCEDURE": driverV2.ObjectType_PROCEDURE, - "TRIGGER": driverV2.ObjectType_TRIGGER, - "EVENT": driverV2.ObjectType_EVENT, - } - for name, objType := range cases { - t.Run(name, func(t *testing.T) { - base := newFakeRunner().on("USE base_schema", "OK") - compared := newFakeRunner().on("USE compared_schema", "OK") - h := newDriverWithRunners(base, compared) - results, err := h.GetDatabaseDiffModifySQL(context.Background(), - &driverV2.DSN{}, - []*driverV2.DatabasCompareSchemaInfo{{ - BaseSchemaName: "base_schema", - ComparedSchemaName: "compared_schema", - DatabaseObjects: []*driverV2.DatabaseObject{ - {ObjectName: "x_obj", ObjectType: objType}, - }, - }}) - if err != nil { - t.Fatalf("expected nil error for short-circuited type %s; got %v", objType, err) - } - if len(results) != 1 { - t.Fatalf("expected 1 schema result, got %d", len(results)) - } - full := strings.Join(results[0].ModifySQLs, "\n") - // Only the USE header should be present; no DROP/CREATE/ALTER. - if strings.Contains(full, "DROP") || - strings.Contains(full, "CREATE") || - strings.Contains(full, "ALTER") { - t.Errorf("expected %s to be filtered; got block=%q", objType, full) - } - if !strings.Contains(full, "USE compared_schema;") { - t.Errorf("expected USE compared_schema; header; got=%q", full) - } - }) - } -} - -// --------------------------------------------------------------------- // -// 16. Test_DiffTableDDL_Matrix — design §3.4 row-by-row coverage -// --------------------------------------------------------------------- // - -// Test_DiffTableDDL_Matrix walks every row of the design §3.4 ALTER vs -// DROP+CREATE decision matrix. Each case keys on a label that matches a -// matrix row; the body lists base/compared DDLs and the expected outcome -// (ALTER substring(s) or fallback=true with a WARNING expectation). -// -// Coverage by matrix row: -// - 加列 -> case "add column" -// - 删列 -> case "drop column" -// - 改列名 -> case "rename column" -// - 改列类型 (兼容 widen) -> case "widen int to bigint" -// - 改列类型 (不兼容) -> case "incompat type" -// - 改列注释 -> case "change column comment" -// - 改表注释 -> case "change table comment" -// - 改 TBLPROPERTIES (业务字段) -> case "alter tblproperties" -// - 改分区键定义 -> case "change partition key" -// - 改存储格式 (STORED AS) -> case "change stored as" -// - 改 ROW FORMAT / SerDe -> case "change row format" -// - 改 EXTERNAL/MANAGED -> case "toggle external" -// - 改 LOCATION -> case "change location" -// - 多类差异组合 -> case "combined incompatible" -// - 运行时 TBLPROPERTIES 不触发 -> case "runtime properties ignored" -func Test_DiffTableDDL_Matrix(t *testing.T) { - cases := map[string]struct { - base string - target string - wantAlt []string // substrings that must appear in alterStmts joined - wantFB bool // expected fallbackDropCreate - }{ - "add column": { - base: "CREATE TABLE t (id BIGINT, name STRING, age INT) STORED AS ORC", - target: "CREATE TABLE t (id BIGINT, name STRING) STORED AS ORC", - wantAlt: []string{"ALTER TABLE t ADD COLUMNS (age INT)"}, - }, - "drop column": { - base: "CREATE TABLE t (id BIGINT) STORED AS ORC", - target: "CREATE TABLE t (id BIGINT, name STRING) STORED AS ORC", - wantFB: true, - }, - "rename column": { - base: "CREATE TABLE t (id BIGINT, full_name STRING) STORED AS ORC", - target: "CREATE TABLE t (id BIGINT, name STRING) STORED AS ORC", - wantFB: true, // rename detected as "name removed + full_name added" → deletion path - }, - "widen int to bigint": { - base: "CREATE TABLE t (id BIGINT) STORED AS ORC", - target: "CREATE TABLE t (id INT) STORED AS ORC", - wantAlt: []string{"ALTER TABLE t CHANGE COLUMN id id BIGINT"}, - }, - "incompat type": { - base: "CREATE TABLE t (ts TIMESTAMP) STORED AS ORC", - target: "CREATE TABLE t (ts INT) STORED AS ORC", - wantFB: true, - }, - "change column comment": { - base: "CREATE TABLE t (id BIGINT COMMENT 'new note') STORED AS ORC", - target: "CREATE TABLE t (id BIGINT COMMENT 'old note') STORED AS ORC", - wantAlt: []string{"ALTER TABLE t CHANGE COLUMN id id BIGINT COMMENT 'new note'"}, - }, - "change table comment": { - base: "CREATE TABLE t (id BIGINT) COMMENT 'updated' STORED AS ORC", - target: "CREATE TABLE t (id BIGINT) COMMENT 'old' STORED AS ORC", - wantAlt: []string{"SET TBLPROPERTIES ('comment'='updated')"}, - }, - "alter tblproperties": { - base: "CREATE TABLE t (id BIGINT) STORED AS ORC TBLPROPERTIES ('biz.owner'='alice')", - target: "CREATE TABLE t (id BIGINT) STORED AS ORC TBLPROPERTIES ('biz.owner'='bob')", - wantAlt: []string{"SET TBLPROPERTIES ('biz.owner'='alice')"}, - }, - "change partition key": { - base: "CREATE TABLE t (id BIGINT) PARTITIONED BY (dt STRING) STORED AS ORC", - target: "CREATE TABLE t (id BIGINT) PARTITIONED BY (dt STRING, region STRING) STORED AS ORC", - wantFB: true, - }, - "change stored as": { - base: "CREATE TABLE t (id BIGINT) STORED AS PARQUET", - target: "CREATE TABLE t (id BIGINT) STORED AS ORC", - wantFB: true, - }, - "change row format": { - base: "CREATE TABLE t (id BIGINT) ROW FORMAT SERDE 'org.apache.hadoop.hive.ql.io.orc.OrcSerde' STORED AS ORC", - target: "CREATE TABLE t (id BIGINT) ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' STORED AS ORC", - wantFB: true, - }, - "toggle external": { - base: "CREATE EXTERNAL TABLE t (id BIGINT) STORED AS ORC", - target: "CREATE TABLE t (id BIGINT) STORED AS ORC", - wantAlt: []string{"SET TBLPROPERTIES ('EXTERNAL'='TRUE')"}, - }, - "change location": { - base: "CREATE TABLE t (id BIGINT) STORED AS ORC LOCATION 'hdfs://new/path'", - target: "CREATE TABLE t (id BIGINT) STORED AS ORC LOCATION 'hdfs://old/path'", - wantAlt: []string{"SET LOCATION 'hdfs://new/path'"}, - }, - "combined incompatible": { - base: "CREATE TABLE t (id BIGINT, age INT) PARTITIONED BY (dt STRING) STORED AS ORC", - target: "CREATE TABLE t (id BIGINT) PARTITIONED BY (dt STRING) STORED AS PARQUET", - wantFB: true, // STORED AS change alone is enough; combined with extra column makes it stronger - }, - "runtime properties ignored": { - base: "CREATE TABLE t (id BIGINT) STORED AS ORC TBLPROPERTIES ('transient_lastDdlTime'='100')", - target: "CREATE TABLE t (id BIGINT) STORED AS ORC TBLPROPERTIES ('transient_lastDdlTime'='200', 'numFiles'='5')", - // No ALTER expected: runtime keys are filtered, so the schemas - // compare equal. - }, - } - - // Iterate in sorted-key order so failures are deterministic. - keys := make([]string, 0, len(cases)) - for k := range cases { - keys = append(keys, k) - } - sort.Strings(keys) - - for _, name := range keys { - tc := cases[name] - t.Run(name, func(t *testing.T) { - alters, fallback, err := diffTableDDL(tc.base, tc.target) - if err != nil { - t.Fatalf("diffTableDDL error: %v", err) - } - if fallback != tc.wantFB { - t.Errorf("fallback mismatch: got=%v want=%v\nbase=%q\ntarget=%q\nalters=%v", - fallback, tc.wantFB, tc.base, tc.target, alters) - return - } - joined := strings.Join(alters, "\n") - for _, want := range tc.wantAlt { - if !strings.Contains(joined, want) { - t.Errorf("expected ALTER substring %q in:\n%s", want, joined) - } - } - if tc.wantFB && len(alters) != 0 { - t.Errorf("fallback case should have empty alterStmts, got %v", alters) - } - }) - } -} diff --git a/sqle/driver/hive/hive_test.go b/sqle/driver/hive/hive_test.go deleted file mode 100644 index 962d5d9ab..000000000 --- a/sqle/driver/hive/hive_test.go +++ /dev/null @@ -1,857 +0,0 @@ -package hive - -import ( - "context" - "fmt" - "strings" - "testing" - - sqleDriver "github.com/actiontech/sqle/sqle/driver" - driverV2 "github.com/actiontech/sqle/sqle/driver/v2" - "github.com/sirupsen/logrus" -) - -func TestHivePluginRegistered(t *testing.T) { - processor, ok := sqleDriver.BuiltInPluginProcessors[driverV2.DriverTypeHive] - if !ok { - t.Fatalf("expected BuiltInPluginProcessors to contain key %q", driverV2.DriverTypeHive) - } - if processor == nil { - t.Fatal("expected BuiltInPluginProcessors[Hive] to be non-nil") - } -} - -func TestGetDriverMetas(t *testing.T) { - p := &PluginProcessor{} - metas, err := p.GetDriverMetas() - if err != nil { - t.Fatalf("GetDriverMetas() returned error: %v", err) - } - - // Verify PluginName - if metas.PluginName != "Hive" { - t.Errorf("expected PluginName=%q, got %q", "Hive", metas.PluginName) - } - - // Verify DefaultPort - if metas.DatabaseDefaultPort != 10000 { - t.Errorf("expected DatabaseDefaultPort=10000, got %d", metas.DatabaseDefaultPort) - } - - // Verify Rules is empty - if len(metas.Rules) != 0 { - t.Errorf("expected empty Rules, got %d rules", len(metas.Rules)) - } - - // Verify EnabledOptionalModule declares structure-compare capabilities (compat-RISK-1). - // The set must contain OptionalGetDatabaseObjectDDL and OptionalGetDatabaseDiffModifySQL - // for the controller/server capability check whitelist to accept Hive. - expectedModules := map[driverV2.OptionalModule]bool{ - driverV2.OptionalGetDatabaseObjectDDL: false, - driverV2.OptionalGetDatabaseDiffModifySQL: false, - } - for _, m := range metas.EnabledOptionalModule { - if _, ok := expectedModules[m]; ok { - expectedModules[m] = true - } - } - for m, seen := range expectedModules { - if !seen { - t.Errorf("expected EnabledOptionalModule to contain %v", m) - } - } - - // Verify additionalParams: auth - authParam := metas.DatabaseAdditionalParams.GetParam("auth") - if authParam == nil { - t.Fatal("expected additionalParams to contain 'auth' param") - } - if authParam.Value != "NONE" { - t.Errorf("expected auth default value=%q, got %q", "NONE", authParam.Value) - } - expectedAuthEnums := []string{"NONE", "NOSASL", "LDAP", "KERBEROS"} - if len(authParam.Enums) != len(expectedAuthEnums) { - t.Fatalf("expected %d auth enums, got %d", len(expectedAuthEnums), len(authParam.Enums)) - } - for i, expected := range expectedAuthEnums { - if authParam.Enums[i].Value != expected { - t.Errorf("auth enum[%d]: expected %q, got %q", i, expected, authParam.Enums[i].Value) - } - } - - // Verify additionalParams: transport_mode - transportParam := metas.DatabaseAdditionalParams.GetParam("transport_mode") - if transportParam == nil { - t.Fatal("expected additionalParams to contain 'transport_mode' param") - } - if transportParam.Value != "binary" { - t.Errorf("expected transport_mode default value=%q, got %q", "binary", transportParam.Value) - } - expectedTransportEnums := []string{"binary", "http"} - if len(transportParam.Enums) != len(expectedTransportEnums) { - t.Fatalf("expected %d transport_mode enums, got %d", len(expectedTransportEnums), len(transportParam.Enums)) - } - for i, expected := range expectedTransportEnums { - if transportParam.Enums[i].Value != expected { - t.Errorf("transport_mode enum[%d]: expected %q, got %q", i, expected, transportParam.Enums[i].Value) - } - } - - // Verify additionalParams: service - serviceParam := metas.DatabaseAdditionalParams.GetParam("service") - if serviceParam == nil { - t.Fatal("expected additionalParams to contain 'service' param") - } - if serviceParam.Value != "" { - t.Errorf("expected service default value=%q, got %q", "", serviceParam.Value) - } -} - -func TestClassifySQL(t *testing.T) { - cases := map[string]struct { - input string - expected string - }{ - // DQL cases - "SELECT uppercase": {input: "SELECT * FROM t", expected: driverV2.SQLTypeDQL}, - "select lowercase": {input: "select id from t", expected: driverV2.SQLTypeDQL}, - "WITH CTE": {input: "WITH cte AS (SELECT 1) SELECT * FROM cte", expected: driverV2.SQLTypeDQL}, - "SHOW TABLES": {input: "SHOW TABLES", expected: driverV2.SQLTypeDQL}, - "DESCRIBE table": {input: "DESCRIBE my_table", expected: driverV2.SQLTypeDQL}, - "DESC table": {input: "DESC my_table", expected: driverV2.SQLTypeDQL}, - "EXPLAIN query": {input: "EXPLAIN SELECT 1", expected: driverV2.SQLTypeDQL}, - "leading whitespace SELECT": {input: " SELECT 1", expected: driverV2.SQLTypeDQL}, - - // DML cases - "INSERT": {input: "INSERT INTO t VALUES (1)", expected: driverV2.SQLTypeDML}, - "UPDATE": {input: "UPDATE t SET a=1", expected: driverV2.SQLTypeDML}, - "DELETE": {input: "DELETE FROM t WHERE id=1", expected: driverV2.SQLTypeDML}, - "MERGE": {input: "MERGE INTO t USING s ON t.id=s.id", expected: driverV2.SQLTypeDML}, - "LOAD": {input: "LOAD DATA INPATH '/path' INTO TABLE t", expected: driverV2.SQLTypeDML}, - "EXPORT": {input: "EXPORT TABLE t TO '/path'", expected: driverV2.SQLTypeDML}, - - // DDL cases (default) - "CREATE TABLE": {input: "CREATE TABLE t (id INT)", expected: driverV2.SQLTypeDDL}, - "ALTER TABLE": {input: "ALTER TABLE t ADD COLUMNS (col STRING)", expected: driverV2.SQLTypeDDL}, - "DROP TABLE": {input: "DROP TABLE t", expected: driverV2.SQLTypeDDL}, - "GRANT": {input: "GRANT SELECT ON t TO user", expected: driverV2.SQLTypeDDL}, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - got := classifySQL(tc.input) - if got != tc.expected { - t.Errorf("classifySQL(%q) = %q, want %q", tc.input, got, tc.expected) - } - }) - } -} - -func TestSplitSQL(t *testing.T) { - cases := map[string]struct { - input string - expected []string - }{ - "single SQL": { - input: "SELECT 1", - expected: []string{"SELECT 1"}, - }, - "multiple SQLs": { - input: "SELECT 1; SELECT 2; SELECT 3", - expected: []string{"SELECT 1", "SELECT 2", "SELECT 3"}, - }, - "trailing semicolon": { - input: "SELECT 1;", - expected: []string{"SELECT 1"}, - }, - "empty input": { - input: "", - expected: []string{}, - }, - "whitespace only": { - input: " ; ; ", - expected: []string{}, - }, - "mixed with whitespace": { - input: " SELECT 1 ; INSERT INTO t VALUES(1) ; ", - expected: []string{"SELECT 1", "INSERT INTO t VALUES(1)"}, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - got := splitSQL(tc.input) - if len(got) != len(tc.expected) { - t.Fatalf("splitSQL(%q): got %d results, want %d", tc.input, len(got), len(tc.expected)) - } - for i, s := range got { - if s != tc.expected[i] { - t.Errorf("splitSQL(%q)[%d] = %q, want %q", tc.input, i, s, tc.expected[i]) - } - } - }) - } -} - -func TestAuditReturnsEmptyResults(t *testing.T) { - p := &PluginProcessor{} - plugin, err := p.Open(logrus.NewEntry(logrus.New()), &driverV2.Config{}) - if err != nil { - t.Fatalf("Open() returned error: %v", err) - } - impl := plugin.(*HiveDriverImpl) - - sqls := []string{"SELECT 1", "INSERT INTO t VALUES(1)", "CREATE TABLE t(id INT)"} - results, err := impl.Audit(context.Background(), sqls) - if err != nil { - t.Fatalf("Audit() returned error: %v", err) - } - if len(results) != len(sqls) { - t.Fatalf("Audit() returned %d results, want %d", len(results), len(sqls)) - } - for i, r := range results { - if r == nil { - t.Errorf("Audit() result[%d] is nil", i) - } - } -} - -func TestParse(t *testing.T) { - p := &PluginProcessor{} - plugin, err := p.Open(logrus.NewEntry(logrus.New()), &driverV2.Config{}) - if err != nil { - t.Fatalf("Open() returned error: %v", err) - } - impl := plugin.(*HiveDriverImpl) - - cases := map[string]struct { - input string - expectedCount int - expectedTypes []string - }{ - "single DQL": { - input: "SELECT 1", - expectedCount: 1, - expectedTypes: []string{driverV2.SQLTypeDQL}, - }, - "multiple mixed": { - input: "SELECT 1; INSERT INTO t VALUES(1); CREATE TABLE t(id INT)", - expectedCount: 3, - expectedTypes: []string{driverV2.SQLTypeDQL, driverV2.SQLTypeDML, driverV2.SQLTypeDDL}, - }, - "empty input": { - input: "", - expectedCount: 0, - expectedTypes: []string{}, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - nodes, err := impl.Parse(context.Background(), tc.input) - if err != nil { - t.Fatalf("Parse(%q) returned error: %v", tc.input, err) - } - if len(nodes) != tc.expectedCount { - t.Fatalf("Parse(%q) returned %d nodes, want %d", tc.input, len(nodes), tc.expectedCount) - } - for i, node := range nodes { - if node.Type != tc.expectedTypes[i] { - t.Errorf("Parse(%q) node[%d].Type = %q, want %q", tc.input, i, node.Type, tc.expectedTypes[i]) - } - } - }) - } -} - -func TestPingWithNilDSN(t *testing.T) { - p := &PluginProcessor{} - plugin, err := p.Open(logrus.NewEntry(logrus.New()), &driverV2.Config{}) - if err != nil { - t.Fatalf("Open() returned error: %v", err) - } - impl := plugin.(*HiveDriverImpl) - - err = impl.Ping(context.Background()) - if err == nil { - t.Error("expected Ping() to return error when dsn is nil") - } -} - -func TestOpenWithNilDSN(t *testing.T) { - p := &PluginProcessor{} - plugin, err := p.Open(logrus.NewEntry(logrus.New()), &driverV2.Config{DSN: nil}) - if err != nil { - t.Fatalf("Open() with nil DSN should succeed in offline mode, got error: %v", err) - } - if plugin == nil { - t.Fatal("Open() returned nil plugin") - } -} - -func TestPingWithNilConn(t *testing.T) { - // When Open is called with nil DSN (offline audit mode), conn is nil. - // Ping should return an error indicating uninitialized connection. - impl := &HiveDriverImpl{ - log: logrus.NewEntry(logrus.New()), - } - err := impl.Ping(context.Background()) - if err == nil { - t.Error("expected Ping() to return error when conn is nil") - } - if !strings.Contains(err.Error(), "not initialized") { - t.Errorf("expected error to mention 'not initialized', got: %v", err) - } -} - -func TestCloseWithNilConn(t *testing.T) { - // Close should not panic when conn is nil (offline audit mode). - impl := &HiveDriverImpl{ - log: logrus.NewEntry(logrus.New()), - } - // Should not panic - impl.Close(context.Background()) -} - -// fakeHiveCursor lets tests drive fetchAllRows through fully-scripted -// HasMore / FetchOne / Err sequences. It mimics how HiveServer2 surfaces a -// non-fatal ROW-ERR after a no-result-column statement (USE, SET, DDL). -// -// Each step in `steps` represents one HasMore tick. Setting HasMore=false -// signals end of stream; setting Err to a non-nil value triggers the -// tolerance branch in fetchAllRows. If FetchValue is non-empty, FetchOne -// will assign it to the destination string before the next HasMore tick. -type fakeHiveCursor struct { - steps []fakeCursorStep - idx int - err error - fetchVal string -} - -type fakeCursorStep struct { - HasMore bool - FetchValue string - ErrBeforeNext error // err returned by Err() *after* this step's HasMore -} - -func (f *fakeHiveCursor) HasMore(ctx context.Context) bool { - if f.idx >= len(f.steps) { - return false - } - step := f.steps[f.idx] - f.fetchVal = step.FetchValue - // Error surfaces immediately on entering the next iteration so the - // fetchAllRows branch can pick it up before FetchOne. - f.err = step.ErrBeforeNext - return step.HasMore -} - -func (f *fakeHiveCursor) FetchOne(ctx context.Context, dests ...interface{}) { - if f.idx < len(f.steps) && len(dests) > 0 { - if p, ok := dests[0].(*string); ok { - *p = f.fetchVal - } - } - f.idx++ -} - -func (f *fakeHiveCursor) Err() error { return f.err } - -func Test_IsHS2NoResultRowErr(t *testing.T) { - cases := []struct { - name string - err error - want bool - }{ - { - name: "nil", - err: nil, - want: false, - }, - { - name: "tolerable_HS2_row_err", - // Real-world payload from HiveServer2 FetchResults on a USE statement. - err: fmt.Errorf("TStatus({StatusCode:ERROR_STATUS InfoMessages:[Server-side error; please check HS2 logs.] SqlState: ErrorCode: ErrorMessage:})"), - want: true, - }, - { - name: "syntax_error_not_tolerable", - err: fmt.Errorf("FAILED: ParseException line 1:0 cannot recognize input near 'SELEKT'"), - want: false, - }, - { - name: "missing_marker_status_only", - err: fmt.Errorf("StatusCode:ERROR_STATUS but message differs"), - want: false, - }, - { - name: "missing_status_marker", - err: fmt.Errorf("Server-side error; please check HS2 logs."), - want: false, - }, - } - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - got := isHS2NoResultRowErr(tc.err) - if got != tc.want { - t.Errorf("isHS2NoResultRowErr(%v) = %v, want %v", tc.err, got, tc.want) - } - }) - } -} - -// Test_FetchAllRows_RowErrTolerant exercises the HS2 ROW-ERR tolerance -// contract for compat-RISK-10. The three scenarios verify: -// -// 1. USE-like statement returns ROW-ERR with zero rows -> fetchAllRows -// yields (nil, nil) (no hard error; caller can keep executing). -// 2. SHOW TABLES yields two rows then a trailing ROW-ERR -> rows are -// returned and the ROW-ERR is treated as EOF (compat-RISK-10 must -// not drop already-fetched rows). -// 3. A real Hive runtime error (syntax error) is propagated -> caller -// receives the wrapped failure as before the fix. -func Test_FetchAllRows_RowErrTolerant(t *testing.T) { - hs2RowErr := fmt.Errorf("TStatus({StatusCode:ERROR_STATUS InfoMessages:[Server-side error; please check HS2 logs.]})") - syntaxErr := fmt.Errorf("FAILED: ParseException syntax error") - - cases := map[string]struct { - steps []fakeCursorStep - wantRows []string - wantErr bool - errMatch string - }{ - "USE_statement_row_err_tolerated": { - // First HasMore tick surfaces ROW-ERR with no row -> loop breaks. - steps: []fakeCursorStep{ - {HasMore: true, ErrBeforeNext: hs2RowErr}, - }, - wantRows: nil, - wantErr: false, - }, - "SHOW_TABLES_rows_then_trailing_row_err": { - // Two real rows arrive; the third HasMore tick is the HS2 - // terminator with ROW-ERR. Tolerated -> existing rows preserved. - steps: []fakeCursorStep{ - {HasMore: true, FetchValue: "t_base_only"}, - {HasMore: true, FetchValue: "t_diff_only"}, - {HasMore: true, ErrBeforeNext: hs2RowErr}, - }, - wantRows: []string{"t_base_only", "t_diff_only"}, - wantErr: false, - }, - "real_syntax_error_propagates": { - // A non-ROW-ERR is a genuine failure and must surface. - steps: []fakeCursorStep{ - {HasMore: true, ErrBeforeNext: syntaxErr}, - }, - wantRows: nil, - wantErr: true, - errMatch: "ParseException", - }, - } - - for name, tc := range cases { - tc := tc - t.Run(name, func(t *testing.T) { - cur := &fakeHiveCursor{steps: tc.steps} - rows, err := fetchAllRows(context.Background(), cur) - if tc.wantErr { - if err == nil { - t.Fatalf("expected error, got nil (rows=%v)", rows) - } - if tc.errMatch != "" && !strings.Contains(err.Error(), tc.errMatch) { - t.Errorf("err = %v, want substring %q", err, tc.errMatch) - } - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(rows) != len(tc.wantRows) { - t.Fatalf("rows len = %d, want %d (rows=%v)", len(rows), len(tc.wantRows), rows) - } - for i, r := range rows { - if r != tc.wantRows[i] { - t.Errorf("rows[%d] = %q, want %q", i, r, tc.wantRows[i]) - } - } - }) - } -} - -// Test_RunSingleStringQuery_NilConn verifies the early-return guard -// continues to work after the refactor. -func Test_RunSingleStringQuery_NilConn(t *testing.T) { - g := &gohiveQueryRunner{conn: nil} - _, err := g.runSingleStringQuery(context.Background(), "USE default") - if err == nil { - t.Fatal("expected error when conn is nil") - } - if !strings.Contains(err.Error(), "not initialized") { - t.Errorf("err = %v, want substring %q", err, "not initialized") - } -} - -// scriptedRunner is a hiveQueryRunner whose runSingleStringQuery returns -// pre-baked rows / errors keyed by exact query text. Used to verify the -// listAllSchemaObjects helper and the "default discovery" behaviour of -// GetDatabaseObjectDDL without needing a live HiveServer2. -type scriptedRunner struct { - scripts map[string]scriptedReply - calls []string -} - -type scriptedReply struct { - rows []string - err error -} - -func (s *scriptedRunner) runSingleStringQuery(ctx context.Context, query string) ([]string, error) { - s.calls = append(s.calls, query) - r, ok := s.scripts[query] - if !ok { - return nil, fmt.Errorf("scriptedRunner: no script for %q", query) - } - return r.rows, r.err -} - -func Test_ListAllSchemaObjects(t *testing.T) { - t.Run("tables_and_views_classified", func(t *testing.T) { - runner := &scriptedRunner{scripts: map[string]scriptedReply{ - "SHOW TABLES": {rows: []string{"t_base_only", "v_user_summary", "t_alter_widen"}}, - "SHOW VIEWS": {rows: []string{"v_user_summary"}}, - }} - objs, err := listAllSchemaObjects(context.Background(), runner) - if err != nil { - t.Fatalf("unexpected err: %v", err) - } - if len(objs) != 3 { - t.Fatalf("len=%d want 3 (%v)", len(objs), objs) - } - got := map[string]string{} - for _, o := range objs { - got[o.ObjectName] = o.ObjectType - } - if got["v_user_summary"] != driverV2.ObjectType_VIEW { - t.Errorf("v_user_summary type = %q want VIEW", got["v_user_summary"]) - } - if got["t_base_only"] != driverV2.ObjectType_TABLE { - t.Errorf("t_base_only type = %q want TABLE", got["t_base_only"]) - } - if got["t_alter_widen"] != driverV2.ObjectType_TABLE { - t.Errorf("t_alter_widen type = %q want TABLE", got["t_alter_widen"]) - } - }) - t.Run("show_views_failure_degrades_to_all_table", func(t *testing.T) { - // Older Hive (< 2.2) does not support SHOW VIEWS; we must not blow up. - runner := &scriptedRunner{scripts: map[string]scriptedReply{ - "SHOW TABLES": {rows: []string{"t1", "v1"}}, - "SHOW VIEWS": {err: fmt.Errorf("unsupported on old HS2")}, - }} - objs, err := listAllSchemaObjects(context.Background(), runner) - if err != nil { - t.Fatalf("expected tolerant fallback, got err: %v", err) - } - if len(objs) != 2 { - t.Fatalf("len=%d want 2", len(objs)) - } - for _, o := range objs { - if o.ObjectType != driverV2.ObjectType_TABLE { - t.Errorf("%s degraded type = %q want TABLE", o.ObjectName, o.ObjectType) - } - } - }) - t.Run("show_tables_error_propagates", func(t *testing.T) { - runner := &scriptedRunner{scripts: map[string]scriptedReply{ - "SHOW TABLES": {err: fmt.Errorf("real fetch error")}, - }} - _, err := listAllSchemaObjects(context.Background(), runner) - if err == nil { - t.Fatal("expected err to propagate") - } - if !strings.Contains(err.Error(), "show tables") { - t.Errorf("err = %v want substring %q", err, "show tables") - } - }) - t.Run("empty_names_skipped", func(t *testing.T) { - runner := &scriptedRunner{scripts: map[string]scriptedReply{ - "SHOW TABLES": {rows: []string{"", "t1", ""}}, - "SHOW VIEWS": {rows: []string{}}, - }} - objs, err := listAllSchemaObjects(context.Background(), runner) - if err != nil { - t.Fatalf("unexpected err: %v", err) - } - if len(objs) != 1 || objs[0].ObjectName != "t1" { - t.Errorf("expected single t1, got %v", objs) - } - }) -} - -func Test_GetDatabaseObjectDDL_DefaultDiscovery(t *testing.T) { - // When caller passes objInfo.DatabaseObjects = nil, the driver must - // auto-enumerate via listAllSchemaObjects before producing DDLs. - runner := &scriptedRunner{scripts: map[string]scriptedReply{ - "USE default": {rows: nil}, - "SHOW TABLES": {rows: []string{"t_base_only", "v_user_summary"}}, - "SHOW VIEWS": {rows: []string{"v_user_summary"}}, - "SHOW CREATE TABLE t_base_only": {rows: []string{"CREATE TABLE `t_base_only` (`id` int)"}}, - "SHOW CREATE TABLE v_user_summary": {rows: []string{"CREATE VIEW `v_user_summary` AS SELECT 1"}}, - }} - impl := &HiveDriverImpl{runner: runner} - - res, err := impl.GetDatabaseObjectDDL(context.Background(), []*driverV2.DatabaseSchemaInfo{ - {SchemaName: "default"}, // DatabaseObjects intentionally nil - }) - if err != nil { - t.Fatalf("unexpected err: %v", err) - } - if len(res) != 1 { - t.Fatalf("len(res)=%d want 1", len(res)) - } - if res[0].SchemaName != "default" { - t.Errorf("schema = %q want default", res[0].SchemaName) - } - if len(res[0].DatabaseObjectDDLs) != 2 { - t.Fatalf("ddls len = %d want 2 (auto-discovery): %v", len(res[0].DatabaseObjectDDLs), res[0].DatabaseObjectDDLs) - } - gotTypes := map[string]string{} - for _, d := range res[0].DatabaseObjectDDLs { - gotTypes[d.DatabaseObject.ObjectName] = d.DatabaseObject.ObjectType - } - if gotTypes["t_base_only"] != driverV2.ObjectType_TABLE { - t.Errorf("t_base_only type = %q want TABLE", gotTypes["t_base_only"]) - } - if gotTypes["v_user_summary"] != driverV2.ObjectType_VIEW { - t.Errorf("v_user_summary type = %q want VIEW", gotTypes["v_user_summary"]) - } -} - -// fakeExecRunner records every Exec invocation and lets tests script the -// per-query error response. It is the unit-test injection point for the -// Exec / ExecBatch contract on HiveDriverImpl. -type fakeExecRunner struct { - calls []string - errs map[string]error - defaultErr error -} - -func (f *fakeExecRunner) exec(ctx context.Context, query string) error { - f.calls = append(f.calls, query) - if e, ok := f.errs[query]; ok { - return e - } - return f.defaultErr -} - -func Test_StripSQLTerminator(t *testing.T) { - cases := map[string]struct { - in string - want string - }{ - "plain": {in: "SELECT 1", want: "SELECT 1"}, - "trailing semicolon": {in: "SELECT 1;", want: "SELECT 1"}, - "trailing whitespace_and_semicolons": {in: " SELECT 1 ;;\n ", want: "SELECT 1"}, - "only_semicolons": {in: ";;;", want: ""}, - "empty": {in: "", want: ""}, - "whitespace_only": {in: " \n ", want: ""}, - } - for name, tc := range cases { - tc := tc - t.Run(name, func(t *testing.T) { - got := stripSQLTerminator(tc.in) - if got != tc.want { - t.Errorf("stripSQLTerminator(%q) = %q want %q", tc.in, got, tc.want) - } - }) - } -} - -func Test_IsAllCommentLines(t *testing.T) { - cases := map[string]struct { - in string - want bool - }{ - "empty": {in: "", want: true}, - "single_comment": {in: "-- hello", want: true}, - "multiline_comments": {in: "-- WARNING: data loss risk\n-- second comment", want: true}, - "blank_lines_and_comment": {in: "\n -- only comment\n \n", want: true}, - "mixed": {in: "-- WARNING\nDROP TABLE t", want: false}, - "sql_only": {in: "DROP TABLE t", want: false}, - } - for name, tc := range cases { - tc := tc - t.Run(name, func(t *testing.T) { - got := isAllCommentLines(tc.in) - if got != tc.want { - t.Errorf("isAllCommentLines(%q) = %v want %v", tc.in, got, tc.want) - } - }) - } -} - -// Test_Exec_SingleStatement covers the happy path: a DDL is forwarded to -// the runner without alteration; the trailing semicolon (if present) is -// stripped before submission; the returned hiveExecResult exposes the -// "not supported" contract for LastInsertId / RowsAffected. -func Test_Exec_SingleStatement(t *testing.T) { - runner := &fakeExecRunner{} - impl := &HiveDriverImpl{ - execRunnerFactory: func(_ *HiveDriverImpl) hiveExecRunner { return runner }, - } - res, err := impl.Exec(context.Background(), "DROP TABLE IF EXISTS sqle_compare_test.t_diff_only;") - if err != nil { - t.Fatalf("Exec returned err: %v", err) - } - if res == nil { - t.Fatal("Exec returned nil result") - } - if _, err := res.LastInsertId(); err == nil { - t.Error("expected LastInsertId to return an error (hive not supported)") - } - if _, err := res.RowsAffected(); err == nil { - t.Error("expected RowsAffected to return an error (hive not supported)") - } - if len(runner.calls) != 1 { - t.Fatalf("expected 1 runner.calls, got %d (%v)", len(runner.calls), runner.calls) - } - if runner.calls[0] != "DROP TABLE IF EXISTS sqle_compare_test.t_diff_only" { - t.Errorf("expected trailing-; stripped, got %q", runner.calls[0]) - } -} - -func Test_Exec_EmptyAndCommentStatementsAreNoOp(t *testing.T) { - runner := &fakeExecRunner{defaultErr: fmt.Errorf("runner must not be called")} - impl := &HiveDriverImpl{ - execRunnerFactory: func(_ *HiveDriverImpl) hiveExecRunner { return runner }, - } - cases := []string{ - "", - " ", - ";", - ";;\n;", - "-- WARNING: data loss risk", - "-- 警告: 数据将丢失\n-- second comment", - } - for _, q := range cases { - q := q - t.Run(fmt.Sprintf("noop_%q", q), func(t *testing.T) { - res, err := impl.Exec(context.Background(), q) - if err != nil { - t.Fatalf("Exec(%q) returned err: %v", q, err) - } - if res == nil { - t.Fatalf("Exec(%q) returned nil result", q) - } - }) - } - if len(runner.calls) != 0 { - t.Errorf("expected runner to receive zero calls for no-op queries, got %v", runner.calls) - } -} - -func Test_Exec_PropagatesRunnerError(t *testing.T) { - runner := &fakeExecRunner{ - errs: map[string]error{ - "CREATE TABLE x(a INT)": fmt.Errorf("FAILED: SemanticException [Error 10001]: Table x already exists"), - }, - } - impl := &HiveDriverImpl{ - execRunnerFactory: func(_ *HiveDriverImpl) hiveExecRunner { return runner }, - } - _, err := impl.Exec(context.Background(), "CREATE TABLE x(a INT)") - if err == nil { - t.Fatal("expected Exec to return runner err") - } - if !strings.Contains(err.Error(), "Table x already exists") { - t.Errorf("expected runner err to be wrapped, got: %v", err) - } -} - -func Test_Exec_NilConnAndNoFactoryFails(t *testing.T) { - impl := &HiveDriverImpl{} - _, err := impl.Exec(context.Background(), "DROP TABLE t") - if err == nil { - t.Fatal("expected Exec to fail when both conn and factory are nil") - } - if !strings.Contains(err.Error(), "not initialized") { - t.Errorf("expected 'not initialized', got: %v", err) - } -} - -// Test_ExecBatch_AllSucceed verifies the batch contract: every statement is -// forwarded in order, results are returned 1:1, and the per-statement -// trailing-; is stripped consistently with Exec. -func Test_ExecBatch_AllSucceed(t *testing.T) { - runner := &fakeExecRunner{} - impl := &HiveDriverImpl{ - execRunnerFactory: func(_ *HiveDriverImpl) hiveExecRunner { return runner }, - } - sqls := []string{ - "USE sqle_compare_test;", - "ALTER TABLE t_alter_widen CHANGE COLUMN amt amt BIGINT;", - "-- WARNING: data loss risk", // comment-only -> no-op - "DROP TABLE IF EXISTS t_diff_only;", - } - results, err := impl.ExecBatch(context.Background(), sqls...) - if err != nil { - t.Fatalf("ExecBatch returned err: %v", err) - } - if len(results) != len(sqls) { - t.Fatalf("expected %d results, got %d", len(sqls), len(results)) - } - for i, r := range results { - if r == nil { - t.Errorf("results[%d] is nil", i) - } - } - // Comment-only statement is filtered before reaching runner. - wantCalls := []string{ - "USE sqle_compare_test", - "ALTER TABLE t_alter_widen CHANGE COLUMN amt amt BIGINT", - "DROP TABLE IF EXISTS t_diff_only", - } - if len(runner.calls) != len(wantCalls) { - t.Fatalf("runner.calls = %v want %v", runner.calls, wantCalls) - } - for i, want := range wantCalls { - if runner.calls[i] != want { - t.Errorf("runner.calls[%d] = %q want %q", i, runner.calls[i], want) - } - } -} - -// Test_ExecBatch_StopsOnFirstError mirrors MySQL driver behaviour: on the -// first error the batch returns the partial result set plus a wrapped err, -// without executing any subsequent statement. -func Test_ExecBatch_StopsOnFirstError(t *testing.T) { - runner := &fakeExecRunner{ - errs: map[string]error{ - "DROP TABLE IF EXISTS t_diff_only": fmt.Errorf("permission denied"), - }, - } - impl := &HiveDriverImpl{ - execRunnerFactory: func(_ *HiveDriverImpl) hiveExecRunner { return runner }, - } - sqls := []string{ - "USE sqle_compare_test", - "DROP TABLE IF EXISTS t_diff_only", // fails here - "CREATE TABLE never_runs (id INT)", - } - results, err := impl.ExecBatch(context.Background(), sqls...) - if err == nil { - t.Fatal("expected ExecBatch to return err on first failure") - } - if !strings.Contains(err.Error(), "permission denied") { - t.Errorf("expected err to wrap runner err, got: %v", err) - } - // Two results: one for USE (nil result is hiveExecResult{}), one for failed DROP (also returned). - if len(results) != 2 { - t.Fatalf("expected 2 partial results, got %d", len(results)) - } - // The CREATE TABLE statement must NOT be invoked after the failure. - if len(runner.calls) != 2 { - t.Errorf("runner.calls = %v, expected 2 (USE + DROP), CREATE must be skipped", runner.calls) - } -} diff --git a/sqle/driver/hive/logo.go b/sqle/driver/hive/logo.go deleted file mode 100644 index b58edf243..000000000 --- a/sqle/driver/hive/logo.go +++ /dev/null @@ -1,5 +0,0 @@ -package hive - -// logo stores the Hive icon PNG binary data. -// It can be nil initially; the frontend will use a default placeholder icon. -var logo []byte diff --git a/sqle/server/sqled.go b/sqle/server/sqled.go index a5c258002..47346badc 100644 --- a/sqle/server/sqled.go +++ b/sqle/server/sqled.go @@ -14,7 +14,6 @@ import ( "github.com/actiontech/sqle/sqle/driver" "github.com/actiontech/sqle/sqle/utils" - _ "github.com/actiontech/sqle/sqle/driver/hive" _ "github.com/actiontech/sqle/sqle/driver/mysql" driverV2 "github.com/actiontech/sqle/sqle/driver/v2" "github.com/actiontech/sqle/sqle/errors"