From e930396f66653a6b460c0d51af3d41ed34d5082e Mon Sep 17 00:00:00 2001 From: zhangyunfei Date: Thu, 17 Jul 2025 06:50:46 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20add=20support=20for=20=E5=8D=97?= =?UTF-8?q?=E5=A4=A7=E9=80=9A=E7=94=A8(GBASE)=20GBase8s=20database?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dialects/gbase8s.go | 1089 +++++++++++++++++++++++++++++++++++++++++++ engine.go | 16 + schemas/type.go | 1 + sync.go | 1 + 4 files changed, 1107 insertions(+) create mode 100644 dialects/gbase8s.go diff --git a/dialects/gbase8s.go b/dialects/gbase8s.go new file mode 100644 index 00000000..72cf71aa --- /dev/null +++ b/dialects/gbase8s.go @@ -0,0 +1,1089 @@ +// Copyright 2021 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package dialects + +import ( + "context" + "database/sql" + "errors" + "fmt" + "regexp" + "strconv" + "strings" + + "xorm.io/xorm/core" + "xorm.io/xorm/schemas" +) + +func init() { + RegisterDriver("gbase8s", &gbase8sDriver{}) + RegisterDialect(schemas.GBASE8S, func() Dialect { + return &gbase8s{} + }) +} + +var ( + gbase8sReservedWords = map[string]bool{ + "ACCESS": true, + "ACCOUNT": true, + "ACTIVATE": true, + "ADD": true, + "ADMIN": true, + "ADVISE": true, + "AFTER": true, + "ALL": true, + "ALL_ROWS": true, + "ALLOCATE": true, + "ALTER": true, + "ANALYZE": true, + "AND": true, + "ANY": true, + "ARCHIVE": true, + "ARCHIVELOG": true, + "ARRAY": true, + "AS": true, + "ASC": true, + "AT": true, + "AUDIT": true, + "AUTHENTICATED": true, + "AUTHORIZATION": true, + "AUTOEXTEND": true, + "AUTOMATIC": true, + "BACKUP": true, + "BECOME": true, + "BEFORE": true, + "BEGIN": true, + "BETWEEN": true, + "BFILE": true, + "BITMAP": true, + "BLOB": true, + "BLOCK": true, + "BODY": true, + "BY": true, + "CACHE": true, + "CACHE_INSTANCES": true, + "CANCEL": true, + "CASCADE": true, + "CAST": true, + "CFILE": true, + "CHAINED": true, + "CHANGE": true, + "CHAR": true, + "CHAR_CS": true, + "CHARACTER": true, + "CHECK": true, + "CHECKPOINT": true, + "CHOOSE": true, + "CHUNK": true, + "CLEAR": true, + "CLOB": true, + "CLONE": true, + "CLOSE": true, + "CLOSE_CACHED_OPEN_CURSORS": true, + "CLUSTER": true, + "COALESCE": true, + "COLUMN": true, + "COLUMNS": true, + "COMMENT": true, + "COMMIT": true, + "COMMITTED": true, + "COMPATIBILITY": true, + "COMPILE": true, + "COMPLETE": true, + "COMPOSITE_LIMIT": true, + "COMPRESS": true, + "COMPUTE": true, + "CONNECT": true, + "CONNECT_TIME": true, + "CONSTRAINT": true, + "CONSTRAINTS": true, + "CONTENTS": true, + "CONTINUE": true, + "CONTROLFILE": true, + "CONVERT": true, + "COST": true, + "CPU_PER_CALL": true, + "CPU_PER_SESSION": true, + "CREATE": true, + "CURRENT": true, + "CURRENT_SCHEMA": true, + "CURREN_USER": true, + "CURSOR": true, + "CYCLE": true, + "DANGLING": true, + "DATABASE": true, + "DATAFILE": true, + "DATAFILES": true, + "DATAOBJNO": true, + "DATE": true, + "DBA": true, + "DBHIGH": true, + "DBLOW": true, + "DBMAC": true, + "DEALLOCATE": true, + "DEBUG": true, + "DEC": true, + "DECIMAL": true, + "DECLARE": true, + "DEFAULT": true, + "DEFERRABLE": true, + "DEFERRED": true, + "DEGREE": true, + "DELETE": true, + "DEREF": true, + "DESC": true, + "DIRECTORY": true, + "DISABLE": true, + "DISCONNECT": true, + "DISMOUNT": true, + "DISTINCT": true, + "DISTRIBUTED": true, + "DML": true, + "DOUBLE": true, + "DROP": true, + "DUMP": true, + "EACH": true, + "ELSE": true, + "ENABLE": true, + "END": true, + "ENFORCE": true, + "ENTRY": true, + "ESCAPE": true, + "EXCEPT": true, + "EXCEPTIONS": true, + "EXCHANGE": true, + "EXCLUDING": true, + "EXCLUSIVE": true, + "EXECUTE": true, + "EXISTS": true, + "EXPIRE": true, + "EXPLAIN": true, + "EXTENT": true, + "EXTENTS": true, + "EXTERNALLY": true, + "FAILED_LOGIN_ATTEMPTS": true, + "FALSE": true, + "FAST": true, + "FILE": true, + "FIRST_ROWS": true, + "FLAGGER": true, + "FLOAT": true, + "FLOB": true, + "FLUSH": true, + "FOR": true, + "FORCE": true, + "FOREIGN": true, + "FREELIST": true, + "FREELISTS": true, + "FROM": true, + "FULL": true, + "FUNCTION": true, + "GLOBAL": true, + "GLOBALLY": true, + "GLOBAL_NAME": true, + "GRANT": true, + "GROUP": true, + "GROUPS": true, + "HASH": true, + "HASHKEYS": true, + "HAVING": true, + "HEADER": true, + "HEAP": true, + "IDENTIFIED": true, + "IDGENERATORS": true, + "IDLE_TIME": true, + "IF": true, + "IMMEDIATE": true, + "IN": true, + "INCLUDING": true, + "INCREMENT": true, + "INDEX": true, + "INDEXED": true, + "INDEXES": true, + "INDICATOR": true, + "IND_PARTITION": true, + "INITIAL": true, + "INITIALLY": true, + "INITRANS": true, + "INSERT": true, + "INSTANCE": true, + "INSTANCES": true, + "INSTEAD": true, + "INT": true, + "INTEGER": true, + "INTERMEDIATE": true, + "INTERSECT": true, + "INTO": true, + "IS": true, + "ISOLATION": true, + "ISOLATION_LEVEL": true, + "KEEP": true, + "KEY": true, + "KILL": true, + "LABEL": true, + "LAYER": true, + "LESS": true, + "LEVEL": true, + "LIBRARY": true, + "LIKE": true, + "LIMIT": true, + "LINK": true, + "LIST": true, + "LOB": true, + "LOCAL": true, + "LOCK": true, + "LOCKED": true, + "LOG": true, + "LOGFILE": true, + "LOGGING": true, + "LOGICAL_READS_PER_CALL": true, + "LOGICAL_READS_PER_SESSION": true, + "LONG": true, + "MANAGE": true, + "MASTER": true, + "MAX": true, + "MAXARCHLOGS": true, + "MAXDATAFILES": true, + "MAXEXTENTS": true, + "MAXINSTANCES": true, + "MAXLOGFILES": true, + "MAXLOGHISTORY": true, + "MAXLOGMEMBERS": true, + "MAXSIZE": true, + "MAXTRANS": true, + "MAXVALUE": true, + "MIN": true, + "MEMBER": true, + "MINIMUM": true, + "MINEXTENTS": true, + "MINUS": true, + "MINVALUE": true, + "MLSLABEL": true, + "MLS_LABEL_FORMAT": true, + "MODE": true, + "MODIFY": true, + "MOUNT": true, + "MOVE": true, + "MTS_DISPATCHERS": true, + "MULTISET": true, + "NATIONAL": true, + "NCHAR": true, + "NCHAR_CS": true, + "NCLOB": true, + "NEEDED": true, + "NESTED": true, + "NETWORK": true, + "NEW": true, + "NEXT": true, + "NOARCHIVELOG": true, + "NOAUDIT": true, + "NOCACHE": true, + "NOCOMPRESS": true, + "NOCYCLE": true, + "NOFORCE": true, + "NOLOGGING": true, + "NOMAXVALUE": true, + "NOMINVALUE": true, + "NONE": true, + "NOORDER": true, + "NOOVERRIDE": true, + "NOPARALLEL": true, + "NOREVERSE": true, + "NORMAL": true, + "NOSORT": true, + "NOT": true, + "NOTHING": true, + "NOWAIT": true, + "NULL": true, + "NUMBER": true, + "NUMERIC": true, + "NVARCHAR2": true, + "OBJECT": true, + "OBJNO": true, + "OBJNO_REUSE": true, + "OF": true, + "OFF": true, + "OFFLINE": true, + "OID": true, + "OIDINDEX": true, + "OLD": true, + "ON": true, + "ONLINE": true, + "ONLY": true, + "OPCODE": true, + "OPEN": true, + "OPTIMAL": true, + "OPTIMIZER_GOAL": true, + "OPTION": true, + "OR": true, + "ORDER": true, + "ORGANIZATION": true, + "OSLABEL": true, + "OVERFLOW": true, + "OWN": true, + "PACKAGE": true, + "PARALLEL": true, + "PARTITION": true, + "PASSWORD": true, + "PASSWORD_GRACE_TIME": true, + "PASSWORD_LIFE_TIME": true, + "PASSWORD_LOCK_TIME": true, + "PASSWORD_REUSE_MAX": true, + "PASSWORD_REUSE_TIME": true, + "PASSWORD_VERIFY_FUNCTION": true, + "PCTFREE": true, + "PCTINCREASE": true, + "PCTTHRESHOLD": true, + "PCTUSED": true, + "PCTVERSION": true, + "PERCENT": true, + "PERMANENT": true, + "PLAN": true, + "PLSQL_DEBUG": true, + "POST_TRANSACTION": true, + "PRECISION": true, + "PRESERVE": true, + "PRIMARY": true, + "PRIOR": true, + "PRIVATE": true, + "PRIVATE_SGA": true, + "PRIVILEGE": true, + "PRIVILEGES": true, + "PROCEDURE": true, + "PROFILE": true, + "PUBLIC": true, + "PURGE": true, + "QUEUE": true, + "QUOTA": true, + "RANGE": true, + "RAW": true, + "RBA": true, + "READ": true, + "READUP": true, + "REAL": true, + "REBUILD": true, + "RECOVER": true, + "RECOVERABLE": true, + "RECOVERY": true, + "REF": true, + "REFERENCES": true, + "REFERENCING": true, + "REFRESH": true, + "RENAME": true, + "REPLACE": true, + "RESET": true, + "RESETLOGS": true, + "RESIZE": true, + "RESOURCE": true, + "RESTRICTED": true, + "RETURN": true, + "RETURNING": true, + "REUSE": true, + "REVERSE": true, + "REVOKE": true, + "ROLE": true, + "ROLES": true, + "ROLLBACK": true, + "ROW": true, + "ROWID": true, + "ROWNUM": true, + "ROWS": true, + "RULE": true, + "SAMPLE": true, + "SAVEPOINT": true, + "SB4": true, + "SCAN_INSTANCES": true, + "SCHEMA": true, + "SCN": true, + "SCOPE": true, + "SD_ALL": true, + "SD_INHIBIT": true, + "SD_SHOW": true, + "SEGMENT": true, + "SEG_BLOCK": true, + "SEG_FILE": true, + "SELECT": true, + "SEQUENCE": true, + "SERIALIZABLE": true, + "SESSION": true, + "SESSION_CACHED_CURSORS": true, + "SESSIONS_PER_USER": true, + "SET": true, + "SHARE": true, + "SHARED": true, + "SHARED_POOL": true, + "SHRINK": true, + "SIZE": true, + "SKIP": true, + "SKIP_UNUSABLE_INDEXES": true, + "SMALLINT": true, + "SNAPSHOT": true, + "SOME": true, + "SORT": true, + "SPECIFICATION": true, + "SPLIT": true, + "SQL_TRACE": true, + "STANDBY": true, + "START": true, + "STATEMENT_ID": true, + "STATISTICS": true, + "STOP": true, + "STORAGE": true, + "STORE": true, + "STRUCTURE": true, + "SUCCESSFUL": true, + "SWITCH": true, + "SYS_OP_ENFORCE_NOT_NULL$": true, + "SYS_OP_NTCIMG$": true, + "SYNONYM": true, + "SYSDATE": true, + "SYSDBA": true, + "SYSOPER": true, + "SYSTEM": true, + "TABLE": true, + "TABLES": true, + "TABLESPACE": true, + "TABLESPACE_NO": true, + "TABNO": true, + "TEMPORARY": true, + "THAN": true, + "THE": true, + "THEN": true, + "THREAD": true, + "TIMESTAMP": true, + "TIME": true, + "TO": true, + "TOPLEVEL": true, + "TRACE": true, + "TRACING": true, + "TRANSACTION": true, + "TRANSITIONAL": true, + "TRIGGER": true, + "TRIGGERS": true, + "TRUE": true, + "TRUNCATE": true, + "TX": true, + "TYPE": true, + "UB2": true, + "UBA": true, + "UID": true, + "UNARCHIVED": true, + "UNDO": true, + "UNION": true, + "UNIQUE": true, + "UNLIMITED": true, + "UNLOCK": true, + "UNRECOVERABLE": true, + "UNTIL": true, + "UNUSABLE": true, + "UNUSED": true, + "UPDATABLE": true, + "UPDATE": true, + "USAGE": true, + "USE": true, + "USER": true, + "USING": true, + "VALIDATE": true, + "VALIDATION": true, + "VALUE": true, + "VALUES": true, + "VARCHAR": true, + "VARCHAR2": true, + "VARYING": true, + "VIEW": true, + "WHEN": true, + "WHENEVER": true, + "WHERE": true, + "WITH": true, + "WITHOUT": true, + "WORK": true, + "WRITE": true, + "WRITEDOWN": true, + "WRITEUP": true, + "XID": true, + "YEAR": true, + "ZONE": true, + } + + gbase8sQuoter = schemas.Quoter{ + Prefix: '"', + Suffix: '"', + IsReserved: schemas.AlwaysReserve, + } +) + +type gbase8s struct { + Base +} + +func (db *gbase8s) Init(uri *URI) error { + db.quoter = gbase8sQuoter + return db.Base.Init(db, uri) +} + +func (db *gbase8s) Version(ctx context.Context, queryer core.Queryer) (*schemas.Version, error) { + rows, err := queryer.QueryContext(ctx, "SELECT dbinfo('version', 'full') FROM systables WHERE tabid = 1") + if err != nil { + return nil, err + } + defer rows.Close() + + var version string + if !rows.Next() { + if rows.Err() != nil { + return nil, rows.Err() + } + return nil, errors.New("unknown version") + } + if err := rows.Scan(&version); err != nil { + return nil, err + } + // Parse GBase 8s version string Example: "GBase Server Version 12.10.FC4G1TL" + fields := strings.Fields(version) + if len(fields) >= 4 && fields[0] == "GBase" && fields[1] == "Server" { + version = fields[3] + } + return &schemas.Version{ + Number: version, + Edition: "GBase 8s", + }, nil +} + +func (db *gbase8s) Features() *DialectFeatures { + return &DialectFeatures{ + AutoincrMode: IncrAutoincrMode, + } +} + +func (db *gbase8s) SQLType(c *schemas.Column) string { + var res string + switch t := c.SQLType.Name; t { + case schemas.Serial: + c.IsAutoIncrement = true + res = "SERIAL" + case schemas.BigSerial: + c.IsAutoIncrement = true + res = "BIGSERIAL" + case schemas.TinyInt, schemas.UnsignedTinyInt, schemas.SmallInt, schemas.Bit, schemas.UnsignedBit: + res = "SMALLINT" + case schemas.UnsignedSmallInt, schemas.MediumInt, schemas.UnsignedMediumInt, schemas.Int, schemas.Integer: + if c.IsAutoIncrement { + return "SERIAL" + } + return "INT" + case schemas.UnsignedInt, schemas.BigInt, schemas.UnsignedBigInt: + if c.IsAutoIncrement { + return "BIGSERIAL" + } + return "BIGINT" + case schemas.Real: + res = " SMALLFLOAT" // "REAL" + case schemas.UnsignedFloat, schemas.Double, schemas.Float: + res = "FLOAT" // "DOUBLE PRECISION" + case schemas.Decimal, schemas.Numeric, schemas.Number: + res = "NUMERIC" // DECIMAL + case schemas.Money, schemas.SmallMoney: + res = "Money" + case schemas.Bool, schemas.Boolean: + switch c.Default { + case "true": + c.Default = "1" + case "false": + c.Default = "0" + } + res = "NUMERIC(1,0)" + case schemas.Char, schemas.NChar, schemas.Uuid: + res = "CHAR" + case schemas.Varchar, schemas.NVarchar, schemas.VARCHAR2, schemas.NVarchar: + res = "VARCHAR" + case schemas.Enum, schemas.Set: + res = "VARCHAR(255)" + case schemas.TinyText, schemas.MediumText, schemas.Text, schemas.LongText, schemas.XML, schemas.Array: + res = "TEXT" + case schemas.Clob: + res = "CLOB" + case schemas.TinyBlob, schemas.MediumBlob, schemas.LongBlob, schemas.Blob, schemas.VarBinary, schemas.Bytea: + res = "BLOB" + case schemas.Binary: + res = "BYTE" + case schemas.Json, schemas.Jsonb: + res = "JSON" + case schemas.Date, schemas.Year: + res = "DATE" + case schemas.DateTime, schemas.SmallDateTime, schemas.TimeStamp, schemas.TimeStampz, schemas.Time: + if c.Length >= 1 && c.Length <= 5 { + return fmt.Sprintf("DATETIME YEAR TO FRACTION(%d)", c.Length) + } else { + return "DATETIME YEAR TO FRACTION(5)" + } + default: + res = t + } + hasLen1 := c.Length > 0 + hasLen2 := c.Length2 > 0 + if res == "BIGINT" || res == "INT" || res == "SMALLINT" { + // GBase 8s INT doesn't need length specification + } else if hasLen2 { + res += "(" + strconv.FormatInt(c.Length, 10) + "," + strconv.FormatInt(c.Length2, 10) + ")" + } else if hasLen1 { + res += "(" + strconv.FormatInt(c.Length, 10) + ")" + } + return res +} + +func (db *gbase8s) ColumnTypeKind(t string) int { + t = strings.Replace(t, "SQLT_", "", 1) + switch strings.ToUpper(t) { + case "DATETIME", "DATE", "TIMESTAMP": + return schemas.TIME_TYPE + case "CHAR", "VARCHAR", "NCHAR", "NVARCHAR", "TEXT", "VARCHAR2", "NVARCHAR2", "LONG", "CLOB", "NCLOB": + return schemas.TEXT_TYPE + case "BLOB", "BYTE": + return schemas.BLOB_TYPE + case "BIGINT", "INTEGER", "SMALLINT", "INT", "FLOAT", "REAL", "DOUBLE", "DECIMAL", "NUMERIC", "MONEY", "SMALLFLOAT", "NUMBER": + return schemas.NUMERIC_TYPE + case "SERIAL", "BIGSERIAL": + return schemas.NUMERIC_TYPE + case "BOOLEAN": + return schemas.BOOL_TYPE + default: + return schemas.UNKNOW_TYPE + } +} + +func (db *gbase8s) IsReserved(name string) bool { + _, ok := gbase8sReservedWords[strings.ToUpper(name)] + return ok +} + +func (db *gbase8s) SetQuotePolicy(quotePolicy QuotePolicy) { + switch quotePolicy { + case QuotePolicyNone: + q := gbase8sQuoter + q.IsReserved = schemas.AlwaysNoReserve + db.quoter = q + case QuotePolicyReserved: + q := gbase8sQuoter + q.IsReserved = db.IsReserved + db.quoter = q + case QuotePolicyAlways: + fallthrough + default: + db.quoter = gbase8sQuoter + } +} + +func (db *gbase8s) AutoIncrStr() string { + return "" // "SERIAL" +} + +func (db *gbase8s) GetIndexes(queryer core.Queryer, ctx context.Context, tableName string) (map[string]*schemas.Index, error) { + s := `select col.colname AS column_name, idx.idxtype AS uniqueness, idx.idxname AS index_name FROM sysindexes idx + JOIN systables tab ON idx.tabid = tab.tabid + JOIN ( + SELECT tabid, idxname, part1 AS colno, 1 AS seq FROM sysindexes WHERE part1 > 0 + UNION ALL + SELECT tabid, idxname, part2, 2 FROM sysindexes WHERE part2 > 0 + UNION ALL + SELECT tabid, idxname, part3, 3 FROM sysindexes WHERE part3 > 0 + UNION ALL + SELECT tabid, idxname, part4, 4 FROM sysindexes WHERE part4 > 0 + ) AS part ON part.idxname = idx.idxname AND part.tabid = idx.tabid + JOIN syscolumns col ON col.tabid = idx.tabid AND col.colno = part.colno + WHERE tab.tabname =:1 ORDER BY idx.idxname, part.seq` + s = compressStr(s) + rows, err := queryer.QueryContext(ctx, s, tableName) + if err != nil { + return nil, err + } + defer rows.Close() + + proCols, err := db.primaryKeys(queryer, ctx, tableName) + if err != nil { + return nil, err + } + + indexes := make(map[string]*schemas.Index) + for rows.Next() { + var indexType int + var indexName, colName, uniqueness string + err = rows.Scan(&colName, &uniqueness, &indexName) + if err != nil { + return nil, err + } + indexName = strings.Trim(indexName, `" `) + if inSlice(colName, proCols) { + continue + } + var isRegular bool + if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) { + indexName = indexName[5+len(tableName):] + isRegular = true + } + if uniqueness == "U" { + indexType = schemas.UniqueType + } else { + indexType = schemas.IndexType + } + + var index *schemas.Index + var ok bool + if index, ok = indexes[indexName]; !ok { + index = new(schemas.Index) + index.Type = indexType + index.Name = indexName + index.IsRegular = isRegular + indexes[indexName] = index + } + index.AddColumn(colName) + } + if rows.Err() != nil { + return nil, rows.Err() + } + return indexes, nil +} + +func (db *gbase8s) IndexCheckSQL(tableName, idxName string) (string, []interface{}) { + args := []interface{}{tableName, idxName} + sql := `SELECT idx.idxname FROM sysindexes idx + JOIN systables tab ON idx.tabid = tab.tabid + JOIN syscolumns col ON col.tabid = tab.tabid + WHERE tab.tabname = :1 and idx.idxname = :2 AND col.colno IN ( + idx.part1, idx.part2, idx.part3, idx.part4, idx.part5, idx.part6, idx.part7, idx.part8, + idx.part9, idx.part10, idx.part11, idx.part12, idx.part13, idx.part14, idx.part15, idx.part16 + );` + sql = compressStr(sql) + return sql, args +} + +func (db *gbase8s) DropIndexSQL(tableName string, index *schemas.Index) string { + var name string + if index.IsRegular { + name = index.XName(tableName) + } else { + name = index.Name + } + return fmt.Sprintf("DROP INDEX %s", db.quoter.Quote(name)) +} + +func (db *gbase8s) GetTables(queryer core.Queryer, ctx context.Context) ([]*schemas.Table, error) { + s := "SELECT tabname FROM systables WHERE tabid >= 100 AND tabtype = 'T'" + rows, err := queryer.QueryContext(ctx, s) + if err != nil { + return nil, err + } + defer rows.Close() + + tables := make([]*schemas.Table, 0) + for rows.Next() { + table := schemas.NewEmptyTable() + err = rows.Scan(&table.Name) + if err != nil { + return nil, err + } + tables = append(tables, table) + } + if rows.Err() != nil { + return nil, rows.Err() + } + return tables, nil +} + +func (db *gbase8s) IsTableExist(queryer core.Queryer, ctx context.Context, tableName string) (bool, error) { + return db.HasRecords(queryer, ctx, `SELECT tabname FROM systables WHERE tabname = :1`, tableName) +} + +func (db *gbase8s) CreateTableSQL(ctx context.Context, queryer core.Queryer, table *schemas.Table, tableName string) (string, bool, error) { + sql := "CREATE TABLE " + if tableName == "" { + tableName = table.Name + } + quoter := db.Quoter() + sql += quoter.Quote(tableName) + " (" + pkList := table.PrimaryKeys + for _, colName := range table.ColumnsSeq() { + col := table.GetColumn(colName) + s, _ := ColumnString(db, col, false, false) + sql += s + if len(col.Comment) > 0 { + sql += fmt.Sprintf(" COMMENT '%s'", col.Comment) + } + sql = strings.TrimSpace(sql) + sql += ", " + } + if len(pkList) > 0 { + sql += "PRIMARY KEY ( " + sql += quoter.Join(pkList, ",") + sql += " ), " + } + sql = sql[:len(sql)-2] + ")" + return sql, false, nil +} + +func (db *gbase8s) DropTableSQL(tableName string) (string, bool) { + return fmt.Sprintf("DROP TABLE %s", db.quoter.Quote(tableName)), false +} + +func (db *gbase8s) GetColumns(queryer core.Queryer, ctx context.Context, tableName string) ([]string, map[string]*schemas.Column, error) { + s := `SELECT c.colname, c.coltypename, c.collength, MOD(c.collength, 256) AS scale, (c.collength - MOD(c.collength,256)) / 256 AS precision, + df.default, cm.comments, cs.constrtype FROM syscolumnsext c + left join systables t on c.tabid = t.tabid + left join syscolcomms cm on c.colno = cm.colno and c.tabid = cm.tabid + left join sysdefaultsexpr df on c.colno = df.colno and c.tabid = df.tabid and df.type = 'T' + left join syscoldepend nl on c.colno = nl.colno and c.tabid = nl.tabid + left join sysconstraints cs on nl.constrid = cs.constrid and c.tabid = cs.tabid + where t.tabname = :1` + s = compressStr(s) + rows, err := queryer.QueryContext(ctx, s, tableName) + if err != nil { + return nil, nil, err + } + defer rows.Close() + + proCols, err := db.primaryKeys(queryer, ctx, tableName) + if err != nil { + return nil, nil, err + } + + cols := make(map[string]*schemas.Column) + colSeq := make([]string, 0) + for rows.Next() { + col := new(schemas.Column) + col.Indexes = make(map[string]int) + var colName, colDefault, nullable, dataType, comment *string + var dataLen, precision, scale int64 + err = rows.Scan(&colName, &dataType, &dataLen, &scale, &precision, &colDefault, &comment, &nullable) + if err != nil { + return nil, nil, err + } + + col.Name = strings.Trim(*colName, `" `) + if colDefault != nil { + col.Default = *colDefault + col.DefaultIsEmpty = false + } else { + col.DefaultIsEmpty = true + } + if inSlice(col.Name, proCols) { + col.IsPrimaryKey = true + } + + if nullable != nil && *nullable == "N" { + col.Nullable = true + } else { + col.Nullable = false + } + if comment != nil { + col.Comment = *comment + } + + var ignore bool + var dt string + var len1, len2 int64 + dts := strings.Split(*dataType, "(") + dt = dts[0] + if len(dts) > 1 { + lens := strings.Split(dts[1][:len(dts[1])-1], ",") + if len(lens) > 1 { + len1, _ = strconv.ParseInt(lens[0], 10, 64) + len2, _ = strconv.ParseInt(lens[1], 10, 64) + } else { + len1, _ = strconv.ParseInt(lens[0], 10, 64) + } + } + if scale != 0 && precision != 0 { + col.Length = precision + col.Length2 = scale + } else { + col.Length = scale + } + if strings.Contains(dt, "SERIAL") { + col.IsAutoIncrement = true + } + + switch dt { + case "SERIAL8": + col.SQLType = schemas.SQLType{Name: schemas.BigSerial, DefaultLength: len1, DefaultLength2: len2} + case "VARCHAR2": + col.SQLType = schemas.SQLType{Name: schemas.Varchar, DefaultLength: len1, DefaultLength2: len2} + case "NVARCHAR2": + col.SQLType = schemas.SQLType{Name: schemas.NVarchar, DefaultLength: len1, DefaultLength2: len2} + case "TIMESTAMP WITH TIME ZONE": + col.SQLType = schemas.SQLType{Name: schemas.TimeStampz, DefaultLength: 0, DefaultLength2: 0} + case "DATETIME YEAR TO SECOND": + col.SQLType = schemas.SQLType{Name: schemas.TimeStamp, DefaultLength: 0, DefaultLength2: 0} + case "LONG", "LONG RAW": + col.SQLType = schemas.SQLType{Name: schemas.Text, DefaultLength: 0, DefaultLength2: 0} + case "RAW": + col.SQLType = schemas.SQLType{Name: schemas.Binary, DefaultLength: 0, DefaultLength2: 0} + case "ROWID": + col.SQLType = schemas.SQLType{Name: schemas.Varchar, DefaultLength: 18, DefaultLength2: 0} + case "SMALLFLOAT": + col.SQLType = schemas.SQLType{Name: schemas.Real, DefaultLength: 0, DefaultLength2: 0} + case "BYTE": + col.SQLType = schemas.SQLType{Name: schemas.Binary, DefaultLength: len1, DefaultLength2: len2} + case "AQ$_SUBSCRIBERS": + ignore = true + default: + if strings.Contains(dt, "DATETIME") && strings.Contains(dt, "FRACTION") { + col.SQLType = schemas.SQLType{Name: schemas.TimeStamp, DefaultLength: 0, DefaultLength2: 0} + } else { + col.SQLType = schemas.SQLType{Name: strings.ToUpper(dt), DefaultLength: len1, DefaultLength2: len2} + } + } + if ignore { + continue + } + if _, ok := schemas.SqlTypes[col.SQLType.Name]; !ok { + return nil, nil, fmt.Errorf("Unknown colType %v %v", *dataType, col.SQLType) + } + + cols[col.Name] = col + colSeq = append(colSeq, col.Name) + } + if rows.Err() != nil { + return nil, nil, rows.Err() + } + return colSeq, cols, nil +} + +func (db *gbase8s) ModifyColumnSQL(tableName string, col *schemas.Column) string { + s, _ := ColumnString(db.dialect, col, false, true) + modifyColumnSQL := fmt.Sprintf("ALTER TABLE %s MODIFY %s;", db.quoter.Quote(tableName), s) + if col.Comment != "" { + modifyColumnSQL += fmt.Sprintf("COMMENT ON COLUMN %s.%s IS '%s'", db.quoter.Quote(tableName), db.quoter.Quote(col.Name), col.Comment) + } + return modifyColumnSQL +} + +func (db *gbase8s) IsColumnExist(queryer core.Queryer, ctx context.Context, tableName, colName string) (bool, error) { + args := []interface{}{tableName, colName} + query := "SELECT colname FROM syscolumnsext c, systables t WHERE c.tabid = t.tabid and tabname = :1 AND colname = :2" + return db.HasRecords(queryer, ctx, query, args...) +} + +func (db *gbase8s) Filters() []Filter { + return []Filter{} +} + +type gbase8sDriver struct { + baseDriver +} + +func (g *gbase8sDriver) Features() *DriverFeatures { + return &DriverFeatures{ + SupportReturnInsertedID: true, + } +} + +func (g *gbase8sDriver) GenScanResult(colType string) (interface{}, error) { + colType = strings.Replace(colType, "SQLT_", "", 1) + switch colType { + case "CHAR", "NCHAR", "VARCHAR", "VARCHAR2", "NVARCHAR2", "AFC": + var s sql.NullString + return &s, nil + case "TEXT", "CLOB", "NCLOB", "CLOB2", "JSON", "XMLTYPE": + var s sql.NullString + return &s, nil + case "BYTE", "BLOB": + var r sql.RawBytes + return &r, nil + case "SERIAL", "SERIAL8", "BIGSERIAL", "BIGINT", "INT8": + var s sql.NullInt64 + return &s, nil + case "INTEGER", "INT", "SMALLINT": + var s sql.NullInt32 + return &s, nil + case "FLOAT", "SMALLFLOAT", "REAL": + var s sql.NullFloat64 + return &s, nil + case "DECIMAL", "NUMERIC", "MONEY", "NUMBER", "BDOUBLE": + var s sql.NullFloat64 + return &s, nil + case "DATE", "DATETIME", "TIMESTAMP", "DATETIME YEAR TO SECOND", "DATETIME YEAR TO FRACTION": + var s sql.NullTime + return &s, nil + case "INTERVAL": + var s sql.NullString + return &s, nil + case "BOOLEAN": + var s sql.NullBool + return &s, nil + default: + // 检查是否是 DATETIME YEAR TO FRACTION 的变体 + if strings.Contains(colType, "DATETIME") && strings.Contains(colType, "FRACTION") { + var s sql.NullTime + return &s, nil + } + var r sql.RawBytes + return &r, nil + } +} + +// dataSourceName=user/password@ipv4:port/dbname +// gbase8s://user:password@ip:port/dbname?param2=1¶m2=2 +func (o *gbase8sDriver) Parse(driverName, dataSourceName string) (*URI, error) { + db := &URI{DBType: schemas.GBASE8S} + dsnPattern := regexp.MustCompile( + `^(?P.*):(?P.*)@` + // user:password@ + `(?P.*)` + // ip:port + `\/(?P.*)`) // dbname + matches := dsnPattern.FindStringSubmatch(dataSourceName) + names := dsnPattern.SubexpNames() + for i, match := range matches { + if names[i] == "dbname" { + db.DBName = match + } + } + if db.DBName == "" && len(matches) != 0 { + return nil, errors.New("dbname is empty") + } + return db, nil +} + +func (db *gbase8s) primaryKeys(queryer core.Queryer, ctx context.Context, tableName string) ([]string, error) { + s := `SELECT col.colname AS column_name FROM systables t + JOIN sysconstraints con ON con.tabid = t.tabid + JOIN sysindexes idx ON con.idxname = idx.idxname + JOIN syscolumns col ON col.tabid = t.tabid AND col.colno IN ( + idx.part1, idx.part2, idx.part3, idx.part4, idx.part5, idx.part6, idx.part7, idx.part8, + idx.part9, idx.part10, idx.part11, idx.part12, idx.part13, idx.part14, idx.part15, idx.part16 + ) + WHERE t.tabname = :1 AND con.constrtype = 'P'` + s = compressStr(s) // Compress whitespace in the query string + rows, err := queryer.QueryContext(ctx, s, tableName) + if err != nil { + return nil, err + } + defer rows.Close() + priCols := make([]string, 0) + for rows.Next() { + var priCol string + err = rows.Scan(&priCol) + if err != nil { + return nil, err + } + priCols = append(priCols, priCol) + } + if rows.Err() != nil { + return nil, rows.Err() + } + return priCols, nil +} + +func inSlice(target string, list []string) bool { + for _, item := range list { + if item == target { + return true + } + } + return false +} + +// 用正则将所有连续的空白字符替换成一个空格 +func compressStr(s string) string { + re := regexp.MustCompile(`\s+`) + return strings.TrimSpace(re.ReplaceAllString(s, " ")) +} diff --git a/engine.go b/engine.go index bb7d2325..9a379f2d 100644 --- a/engine.go +++ b/engine.go @@ -798,6 +798,22 @@ func (engine *Engine) dumpTables(ctx context.Context, tables []*schemas.Table, w return err } } + } else if sess.engine.dialect.URI().DBType == schemas.GBASE8S { + stp.Name = strings.Replace(stp.Name, "SQLT_", "", 1) + if stp.IsTime() && len(s.String) == 20 { // "2025-06-10T07:55:31Z" + t, err := time.Parse(time.RFC3339, s.String) + if err != nil { + return fmt.Errorf("failed to parse time %s: %v", s.String, err) + } + r := t.Format("2006-01-02 15:04:05") + if _, err = io.WriteString(w, "'"+r+"'"); err != nil { + return err + } + } else { + if _, err = io.WriteString(w, "'"+strings.ReplaceAll(s.String, "'", "''")+"'"); err != nil { + return err + } + } } else { if _, err = io.WriteString(w, "'"+strings.ReplaceAll(s.String, "'", "''")+"'"); err != nil { return err diff --git a/schemas/type.go b/schemas/type.go index 3dbcee7e..aa0fce27 100644 --- a/schemas/type.go +++ b/schemas/type.go @@ -23,6 +23,7 @@ const ( MSSQL DBType = "mssql" ORACLE DBType = "oracle" DAMENG DBType = "dameng" + GBASE8S DBType = "gbase8s" ) // SQLType represents SQL types diff --git a/sync.go b/sync.go index f1b5e59f..5247b83d 100644 --- a/sync.go +++ b/sync.go @@ -196,6 +196,7 @@ func (session *Session) SyncWithOptions(opts SyncOptions, beans ...interface{}) } } else if col.Comment != oriCol.Comment { if engine.dialect.URI().DBType == schemas.POSTGRES || + engine.dialect.URI().DBType == schemas.GBASE8S || engine.dialect.URI().DBType == schemas.MYSQL { _, err = session.exec(engine.dialect.ModifyColumnSQL(tbNameWithSchema, col)) }