feat(xugu): expand table editor data types
This commit is contained in:
1 parent
884744df4c
commit
82da9cb6dc
7 files changed
+465
-38
No files matched your search
@@ -0,0 +1,144 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestLiveXuguSelectableDataTypes verifies the table-editor choices through the
|
||||||
|
// same database/sql + go-xugu-driver path used by the agent. It is intentionally
|
||||||
|
// opt-in because CI does not provide a XuguDB service.
|
||||||
|
func TestLiveXuguSelectableDataTypes(t *testing.T) {
|
||||||
|
if os.Getenv("XUGU_LIVE_TEST") != "1" {
|
||||||
|
t.Skip("set XUGU_LIVE_TEST=1 with XUGU_LIVE_* connection settings to run the XuguDB integration test")
|
||||||
|
}
|
||||||
|
params := connectParams{
|
||||||
|
Host: os.Getenv("XUGU_LIVE_HOST"),
|
||||||
|
Port: defaultXuguPort,
|
||||||
|
Database: os.Getenv("XUGU_LIVE_DATABASE"),
|
||||||
|
Username: os.Getenv("XUGU_LIVE_USERNAME"),
|
||||||
|
Password: os.Getenv("XUGU_LIVE_PASSWORD"),
|
||||||
|
}
|
||||||
|
if port, err := strconv.Atoi(os.Getenv("XUGU_LIVE_PORT")); err == nil && port > 0 {
|
||||||
|
params.Port = port
|
||||||
|
}
|
||||||
|
if params.Host == "" || params.Database == "" || params.Username == "" || params.Password == "" {
|
||||||
|
t.Skip("XUGU_LIVE_HOST, XUGU_LIVE_DATABASE, XUGU_LIVE_USERNAME, and XUGU_LIVE_PASSWORD are required")
|
||||||
|
}
|
||||||
|
db, err := openDB(params)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("live XuguDB is unavailable: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
assertLiveXuguDataTypeCatalog(t, ctx, db)
|
||||||
|
|
||||||
|
type dataTypeCase struct {
|
||||||
|
name string
|
||||||
|
definition string
|
||||||
|
valueSQL string
|
||||||
|
}
|
||||||
|
tests := []dataTypeCase{
|
||||||
|
{name: "tinyint", definition: "TINYINT", valueSQL: "7"},
|
||||||
|
{name: "double", definition: "DOUBLE", valueSQL: "1.25"},
|
||||||
|
{name: "datetime", definition: "DATETIME", valueSQL: "'2025-06-20 15:16:25'"},
|
||||||
|
{name: "datetime_tz", definition: "DATETIME WITH TIME ZONE", valueSQL: "'2025-06-21 17:16:25+08:00'"},
|
||||||
|
{name: "time_tz", definition: "TIME(3) WITH TIME ZONE", valueSQL: "'17:30:29.123+08:00'"},
|
||||||
|
{name: "timestamp_tz", definition: "TIMESTAMP(6) WITH TIME ZONE", valueSQL: "'2025-06-21 17:16:25.123456+08:00'"},
|
||||||
|
{name: "interval_year", definition: "INTERVAL YEAR", valueSQL: "2025"},
|
||||||
|
{name: "interval_year_month", definition: "INTERVAL YEAR TO MONTH", valueSQL: "'2025-11'"},
|
||||||
|
{name: "interval_month", definition: "INTERVAL MONTH", valueSQL: "1000"},
|
||||||
|
{name: "interval_day", definition: "INTERVAL DAY", valueSQL: "1996"},
|
||||||
|
{name: "interval_day_hour", definition: "INTERVAL DAY TO HOUR", valueSQL: "'66666 23'"},
|
||||||
|
{name: "interval_hour", definition: "INTERVAL HOUR", valueSQL: "5200"},
|
||||||
|
{name: "interval_day_minute", definition: "INTERVAL DAY TO MINUTE", valueSQL: "'9999 23:56'"},
|
||||||
|
{name: "interval_hour_minute", definition: "INTERVAL HOUR TO MINUTE", valueSQL: "'888:26'"},
|
||||||
|
{name: "interval_minute", definition: "INTERVAL MINUTE", valueSQL: "12345"},
|
||||||
|
{name: "interval_day_second", definition: "INTERVAL DAY TO SECOND", valueSQL: "'2222 12:56:24.233'"},
|
||||||
|
{name: "interval_hour_second", definition: "INTERVAL HOUR TO SECOND", valueSQL: "'9999999:59:59.999999'"},
|
||||||
|
{name: "interval_minute_second", definition: "INTERVAL MINUTE TO SECOND", valueSQL: "'9999999:59.999999'"},
|
||||||
|
{name: "interval_second", definition: "INTERVAL SECOND", valueSQL: "1999.9898"},
|
||||||
|
{name: "guid", definition: "GUID", valueSQL: "SYS_GUID()"},
|
||||||
|
{name: "rowid", definition: "ROWID", valueSQL: "NULL"},
|
||||||
|
{name: "json", definition: "JSON", valueSQL: `' {"driver":"ok"}'`},
|
||||||
|
{name: "bit", definition: "BIT", valueSQL: "B'1'"},
|
||||||
|
{name: "varbit", definition: "VARBIT", valueSQL: "B'1010'"},
|
||||||
|
{name: "integer_array", definition: "INTEGER[]", valueSQL: "ARRAY[1, 2, 3]"},
|
||||||
|
{name: "double_array", definition: "DOUBLE[]", valueSQL: "ARRAY[1.25, 2.5]"},
|
||||||
|
{name: "char_array", definition: "CHAR[]", valueSQL: "ARRAY['a', 'b']"},
|
||||||
|
{name: "clob_array", definition: "CLOB[]", valueSQL: "ARRAY['first', 'second']"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
tableName := fmt.Sprintf("DBX_DATA_TYPE_%02d", index)
|
||||||
|
qualifiedTable := quoteIdentifier(params.Username) + "." + quoteIdentifier(tableName)
|
||||||
|
_, _ = db.ExecContext(ctx, "DROP TABLE "+qualifiedTable)
|
||||||
|
defer func() { _, _ = db.ExecContext(context.Background(), "DROP TABLE "+qualifiedTable) }()
|
||||||
|
|
||||||
|
if _, err := db.ExecContext(ctx, "CREATE TABLE "+qualifiedTable+" (\"TYPE_VALUE\" "+test.definition+")"); err != nil {
|
||||||
|
t.Fatalf("create %s column through go-xugu-driver: %v", test.definition, err)
|
||||||
|
}
|
||||||
|
if _, err := db.ExecContext(ctx, "INSERT INTO "+qualifiedTable+" VALUES ("+test.valueSQL+")"); err != nil {
|
||||||
|
t.Fatalf("insert %s value through go-xugu-driver: %v", test.definition, err)
|
||||||
|
}
|
||||||
|
var value any
|
||||||
|
if err := db.QueryRowContext(ctx, "SELECT \"TYPE_VALUE\" FROM "+qualifiedTable).Scan(&value); err != nil {
|
||||||
|
t.Fatalf("read %s value through go-xugu-driver: %v", test.definition, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertLiveXuguDataTypeCatalog(t *testing.T, ctx context.Context, db queryer) {
|
||||||
|
t.Helper()
|
||||||
|
rows, err := db.QueryContext(ctx, "SHOW DATA_TYPES")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SHOW DATA_TYPES through go-xugu-driver: %v", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
columns, err := rows.Columns()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read SHOW DATA_TYPES columns: %v", err)
|
||||||
|
}
|
||||||
|
domainNameIndex := -1
|
||||||
|
for index, column := range columns {
|
||||||
|
if strings.EqualFold(column, "DOMAIN_NAME") {
|
||||||
|
domainNameIndex = index
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if domainNameIndex < 0 {
|
||||||
|
t.Fatalf("SHOW DATA_TYPES does not expose DOMAIN_NAME: %v", columns)
|
||||||
|
}
|
||||||
|
found := make(map[string]bool)
|
||||||
|
for rows.Next() {
|
||||||
|
values, err := scanRow(rows, len(columns))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("scan SHOW DATA_TYPES: %v", err)
|
||||||
|
}
|
||||||
|
if name, ok := values[domainNameIndex].(string); ok {
|
||||||
|
found[strings.ToUpper(strings.TrimSpace(name))] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
t.Fatalf("iterate SHOW DATA_TYPES: %v", err)
|
||||||
|
}
|
||||||
|
for _, required := range []string{"TINYINT", "DOUBLE", "DATETIME", "DATETIME WITH TIME ZONE", "TIME WITH TIME ZONE", "TIMESTAMP WITH TIME ZONE", "INTERVAL DAY TO SECOND", "GUID", "ROWID", "JSON", "BIT", "VARBIT"} {
|
||||||
|
if !found[required] {
|
||||||
|
t.Fatalf("SHOW DATA_TYPES is missing %q; returned %v", required, found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type queryer interface {
|
||||||
|
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
|
||||||
|
}
|
||||||
+68
-22
@@ -183,33 +183,61 @@ WHERE s.DB_ID = CURRENT_DB_ID
|
|||||||
ORDER BY p.SUBPARTI_NO`
|
ORDER BY p.SUBPARTI_NO`
|
||||||
|
|
||||||
var xuguDataTypes = []string{
|
var xuguDataTypes = []string{
|
||||||
"BOOLEAN",
|
|
||||||
"INTEGER",
|
|
||||||
"SMALLINT",
|
|
||||||
"BIGINT",
|
"BIGINT",
|
||||||
"FLOAT",
|
|
||||||
"NUMERIC",
|
|
||||||
"CHAR",
|
|
||||||
"VARCHAR",
|
|
||||||
"CLOB",
|
|
||||||
"DATE",
|
|
||||||
"TIME",
|
|
||||||
"TIMESTAMP",
|
|
||||||
"BINARY",
|
"BINARY",
|
||||||
"VARBINARY",
|
"BIT",
|
||||||
"BLOB",
|
"BLOB",
|
||||||
"XML",
|
|
||||||
"BOOL",
|
"BOOL",
|
||||||
"INT",
|
"BOOLEAN",
|
||||||
"SHORT",
|
"CHAR",
|
||||||
"LONGINT",
|
"CHAR[]",
|
||||||
"LONG",
|
"CLOB",
|
||||||
"REAL",
|
"CLOB[]",
|
||||||
|
"DATE",
|
||||||
|
"DATETIME",
|
||||||
|
"DATETIME WITH TIME ZONE",
|
||||||
"DECIMAL",
|
"DECIMAL",
|
||||||
"TEXT",
|
"DOUBLE",
|
||||||
|
"DOUBLE[]",
|
||||||
|
"FLOAT",
|
||||||
|
"GUID",
|
||||||
|
"INT",
|
||||||
|
"INTEGER",
|
||||||
|
"INTEGER[]",
|
||||||
|
"INTERVAL DAY",
|
||||||
|
"INTERVAL DAY TO HOUR",
|
||||||
|
"INTERVAL DAY TO MINUTE",
|
||||||
|
"INTERVAL DAY TO SECOND",
|
||||||
|
"INTERVAL HOUR",
|
||||||
|
"INTERVAL HOUR TO MINUTE",
|
||||||
|
"INTERVAL HOUR TO SECOND",
|
||||||
|
"INTERVAL MINUTE",
|
||||||
|
"INTERVAL MINUTE TO SECOND",
|
||||||
|
"INTERVAL MONTH",
|
||||||
|
"INTERVAL SECOND",
|
||||||
|
"INTERVAL YEAR",
|
||||||
|
"INTERVAL YEAR TO MONTH",
|
||||||
|
"JSON",
|
||||||
|
"LONG",
|
||||||
|
"LONGINT",
|
||||||
"NCHAR",
|
"NCHAR",
|
||||||
|
"NUMERIC",
|
||||||
"NVARCHAR",
|
"NVARCHAR",
|
||||||
"NVARCHAR2",
|
"NVARCHAR2",
|
||||||
|
"REAL",
|
||||||
|
"ROWID",
|
||||||
|
"SHORT",
|
||||||
|
"SMALLINT",
|
||||||
|
"TEXT",
|
||||||
|
"TIME",
|
||||||
|
"TIME WITH TIME ZONE",
|
||||||
|
"TIMESTAMP",
|
||||||
|
"TIMESTAMP WITH TIME ZONE",
|
||||||
|
"TINYINT",
|
||||||
|
"VARBINARY",
|
||||||
|
"VARBIT",
|
||||||
|
"VARCHAR",
|
||||||
|
"XML",
|
||||||
}
|
}
|
||||||
|
|
||||||
type request struct {
|
type request struct {
|
||||||
@@ -4682,17 +4710,31 @@ func columnTypeDDL(column columnInfo) string {
|
|||||||
if column.CharacterMaximumLength != nil {
|
if column.CharacterMaximumLength != nil {
|
||||||
return fmt.Sprintf("%s(%d)", dataType, *column.CharacterMaximumLength)
|
return fmt.Sprintf("%s(%d)", dataType, *column.CharacterMaximumLength)
|
||||||
}
|
}
|
||||||
if column.NumericPrecision != nil && column.NumericScale != nil {
|
if column.NumericPrecision != nil {
|
||||||
return fmt.Sprintf("%s(%d,%d)", dataType, *column.NumericPrecision, *column.NumericScale)
|
if column.NumericScale != nil {
|
||||||
|
return fmt.Sprintf("%s(%d,%d)", dataType, *column.NumericPrecision, *column.NumericScale)
|
||||||
|
}
|
||||||
|
return xuguSingleParameterTypeDDL(dataType, *column.NumericPrecision)
|
||||||
}
|
}
|
||||||
return dataType
|
return dataType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func xuguSingleParameterTypeDDL(dataType string, parameter int) string {
|
||||||
|
switch dataType {
|
||||||
|
case "TIME WITH TIME ZONE":
|
||||||
|
return fmt.Sprintf("TIME(%d) WITH TIME ZONE", parameter)
|
||||||
|
case "TIMESTAMP WITH TIME ZONE":
|
||||||
|
return fmt.Sprintf("TIMESTAMP(%d) WITH TIME ZONE", parameter)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%s(%d)", dataType, parameter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func decodeXuguScale(dataType string, scale *int) (*int, *int, *int) {
|
func decodeXuguScale(dataType string, scale *int) (*int, *int, *int) {
|
||||||
if scale == nil || *scale < 0 {
|
if scale == nil || *scale < 0 {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
upper := strings.ToUpper(dataType)
|
upper := strings.Join(strings.Fields(strings.ToUpper(dataType)), " ")
|
||||||
if strings.Contains(upper, "CHAR") || strings.Contains(upper, "BINARY") {
|
if strings.Contains(upper, "CHAR") || strings.Contains(upper, "BINARY") {
|
||||||
length := *scale
|
length := *scale
|
||||||
return nil, nil, &length
|
return nil, nil, &length
|
||||||
@@ -4702,6 +4744,10 @@ func decodeXuguScale(dataType string, scale *int) (*int, *int, *int) {
|
|||||||
numericScale := *scale % 65536
|
numericScale := *scale % 65536
|
||||||
return &precision, &numericScale, nil
|
return &precision, &numericScale, nil
|
||||||
}
|
}
|
||||||
|
if upper == "BIT" || upper == "VARBIT" || upper == "TIME" || upper == "TIME WITH TIME ZONE" || upper == "TIMESTAMP" || upper == "TIMESTAMP WITH TIME ZONE" {
|
||||||
|
precision := *scale
|
||||||
|
return &precision, nil, nil
|
||||||
|
}
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -361,11 +361,21 @@ func TestListDataTypesReturnsXuguTypes(t *testing.T) {
|
|||||||
if err := json.Unmarshal(data, &result); err != nil {
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
for _, want := range []string{"INTEGER", "VARCHAR", "NUMERIC", "INT"} {
|
for _, want := range []string{
|
||||||
|
"INTEGER", "VARCHAR", "NUMERIC", "INT",
|
||||||
|
"TINYINT", "DOUBLE", "DATETIME", "DATETIME WITH TIME ZONE", "TIME WITH TIME ZONE", "TIMESTAMP WITH TIME ZONE",
|
||||||
|
"INTERVAL YEAR", "INTERVAL DAY TO SECOND", "GUID", "ROWID", "JSON", "BIT", "VARBIT",
|
||||||
|
"INTEGER[]", "DOUBLE[]", "CHAR[]", "CLOB[]",
|
||||||
|
} {
|
||||||
if !contains(result, want) {
|
if !contains(result, want) {
|
||||||
t.Fatalf("expected data type %q in %v", want, result)
|
t.Fatalf("expected data type %q in %v", want, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, pseudoType := range []string{"NULL", `"NULL"`, "ARRAY", "ROWVERSION", "POINT", "LSEG", "LINE", "BOX", "PATH", "POLYGON", "CIRCLE"} {
|
||||||
|
if contains(result, pseudoType) {
|
||||||
|
t.Fatalf("pseudo/internal type %q must not be offered as a regular column type: %v", pseudoType, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmptyResultSlicesMarshalAsArrays(t *testing.T) {
|
func TestEmptyResultSlicesMarshalAsArrays(t *testing.T) {
|
||||||
@@ -1802,6 +1812,44 @@ func TestDecodeXuguScale(t *testing.T) {
|
|||||||
if precision != nil || scale != nil || length == nil || *length != 128 {
|
if precision != nil || scale != nil || length == nil || *length != 128 {
|
||||||
t.Fatalf("unexpected char scale decode: precision=%v scale=%v length=%v", precision, scale, length)
|
t.Fatalf("unexpected char scale decode: precision=%v scale=%v length=%v", precision, scale, length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
dataType string
|
||||||
|
value int
|
||||||
|
}{
|
||||||
|
{dataType: "BIT", value: 8},
|
||||||
|
{dataType: "VARBIT", value: 64},
|
||||||
|
{dataType: "TIME", value: 3},
|
||||||
|
{dataType: "TIME WITH TIME ZONE", value: 3},
|
||||||
|
{dataType: "TIMESTAMP", value: 6},
|
||||||
|
{dataType: "TIMESTAMP WITH TIME ZONE", value: 6},
|
||||||
|
} {
|
||||||
|
precision, scale, length = decodeXuguScale(test.dataType, &test.value)
|
||||||
|
if precision == nil || *precision != test.value || scale != nil || length != nil {
|
||||||
|
t.Fatalf("unexpected %s scale decode: precision=%v scale=%v length=%v", test.dataType, precision, scale, length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColumnTypeDDLPreservesXuguSingleParameters(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
dataType string
|
||||||
|
precision int
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{dataType: "BIT", precision: 8, want: "BIT(8)"},
|
||||||
|
{dataType: "VARBIT", precision: 64, want: "VARBIT(64)"},
|
||||||
|
{dataType: "TIME", precision: 3, want: "TIME(3)"},
|
||||||
|
{dataType: "TIME WITH TIME ZONE", precision: 3, want: "TIME(3) WITH TIME ZONE"},
|
||||||
|
{dataType: "TIMESTAMP", precision: 6, want: "TIMESTAMP(6)"},
|
||||||
|
{dataType: "TIMESTAMP WITH TIME ZONE", precision: 6, want: "TIMESTAMP(6) WITH TIME ZONE"},
|
||||||
|
} {
|
||||||
|
column := columnInfo{DataType: test.dataType, NumericPrecision: &test.precision}
|
||||||
|
if got := columnTypeDDL(column); got != test.want {
|
||||||
|
t.Fatalf("columnTypeDDL(%s, %d) = %q, want %q", test.dataType, test.precision, got, test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizeXuguColumnTypeUsesVaryingFlag(t *testing.T) {
|
func TestNormalizeXuguColumnTypeUsesVaryingFlag(t *testing.T) {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import {
|
|||||||
createForeignKeyDrafts,
|
createForeignKeyDrafts,
|
||||||
createIndexDrafts,
|
createIndexDrafts,
|
||||||
createTriggerDrafts,
|
createTriggerDrafts,
|
||||||
|
dataTypeBaseInputValue,
|
||||||
dataTypeLengthInputValue,
|
dataTypeLengthInputValue,
|
||||||
dataTypeLengthUnitValue,
|
dataTypeLengthUnitValue,
|
||||||
defaultNewColumnDataType,
|
defaultNewColumnDataType,
|
||||||
@@ -79,7 +80,6 @@ import {
|
|||||||
resolveInsertColumnIndex,
|
resolveInsertColumnIndex,
|
||||||
restoreCharacterLengthUnitsAfterSave,
|
restoreCharacterLengthUnitsAfterSave,
|
||||||
sameStructureIndexType,
|
sameStructureIndexType,
|
||||||
splitDataType,
|
|
||||||
tableStructureIdentifierComparisonKey,
|
tableStructureIdentifierComparisonKey,
|
||||||
toColumnNames,
|
toColumnNames,
|
||||||
} from "@/lib/table/tableStructureEditorState";
|
} from "@/lib/table/tableStructureEditorState";
|
||||||
@@ -891,13 +891,13 @@ const targetLabel = computed(() => buildStructureTargetLabel(connection.value?.n
|
|||||||
|
|
||||||
function isManticoreTextColumn(column: EditableStructureColumn): boolean {
|
function isManticoreTextColumn(column: EditableStructureColumn): boolean {
|
||||||
if (databaseType.value !== "manticoresearch") return false;
|
if (databaseType.value !== "manticoresearch") return false;
|
||||||
const baseType = splitDataType(column.dataType).baseType.trim().toLowerCase();
|
const baseType = dataTypeBaseInputValue(databaseType.value, column.dataType).trim().toLowerCase();
|
||||||
return baseType === "text" || baseType === "string";
|
return baseType === "text" || baseType === "string";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isManticoreJsonColumn(column: EditableStructureColumn): boolean {
|
function isManticoreJsonColumn(column: EditableStructureColumn): boolean {
|
||||||
if (databaseType.value !== "manticoresearch") return false;
|
if (databaseType.value !== "manticoresearch") return false;
|
||||||
return splitDataType(column.dataType).baseType.trim().toLowerCase() === "json";
|
return dataTypeBaseInputValue(databaseType.value, column.dataType).trim().toLowerCase() === "json";
|
||||||
}
|
}
|
||||||
|
|
||||||
let sqlPreviewRequestId = 0;
|
let sqlPreviewRequestId = 0;
|
||||||
@@ -2061,14 +2061,14 @@ function removeMysqlEnumValue(column: EditableStructureColumn, index: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateColumnDataTypeLength(column: EditableStructureColumn, value: string | number) {
|
function updateColumnDataTypeLength(column: EditableStructureColumn, value: string | number) {
|
||||||
const baseType = splitDataType(column.dataType).baseType;
|
const baseType = dataTypeBaseInputValue(databaseType.value, column.dataType);
|
||||||
column.dataType = combineDataTypeForDatabaseWithLengthUnit(databaseType.value, baseType, String(value), dataTypeLengthUnitValue(databaseType.value, column.dataType));
|
column.dataType = combineDataTypeForDatabaseWithLengthUnit(databaseType.value, baseType, String(value), dataTypeLengthUnitValue(databaseType.value, column.dataType));
|
||||||
syncSqlServerIdentityForDataType(column);
|
syncSqlServerIdentityForDataType(column);
|
||||||
syncDamengIdentityForDataType(column);
|
syncDamengIdentityForDataType(column);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateColumnDataTypeLengthUnit(column: EditableStructureColumn, value: unknown) {
|
function updateColumnDataTypeLengthUnit(column: EditableStructureColumn, value: unknown) {
|
||||||
const baseType = splitDataType(column.dataType).baseType;
|
const baseType = dataTypeBaseInputValue(databaseType.value, column.dataType);
|
||||||
const unit = value === "__default" ? "" : String(value ?? "");
|
const unit = value === "__default" ? "" : String(value ?? "");
|
||||||
column.dataType = combineDataTypeForDatabaseWithLengthUnit(databaseType.value, baseType, dataTypeLengthInputValue(databaseType.value, column.dataType), unit);
|
column.dataType = combineDataTypeForDatabaseWithLengthUnit(databaseType.value, baseType, dataTypeLengthInputValue(databaseType.value, column.dataType), unit);
|
||||||
syncSqlServerIdentityForDataType(column);
|
syncSqlServerIdentityForDataType(column);
|
||||||
@@ -2377,7 +2377,7 @@ function isColumnLengthDisabled(column: EditableStructureColumn): boolean {
|
|||||||
if (isColumnTypeDisabled(column)) {
|
if (isColumnTypeDisabled(column)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const baseType = splitDataType(column.dataType).baseType.trim().toLowerCase();
|
const baseType = dataTypeBaseInputValue(databaseType.value, column.dataType).trim().toLowerCase();
|
||||||
return isDataTypeLengthDisabled(databaseType.value, baseType);
|
return isDataTypeLengthDisabled(databaseType.value, baseType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3320,7 +3320,7 @@ watch([activeTab, ddlLoading], ([tab, loading]) => {
|
|||||||
<td :class="structureCellClass">
|
<td :class="structureCellClass">
|
||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
v-if="!isColumnTypeDisabled(column)"
|
v-if="!isColumnTypeDisabled(column)"
|
||||||
:model-value="splitDataType(column.dataType).baseType"
|
:model-value="dataTypeBaseInputValue(databaseType, column.dataType)"
|
||||||
:options="dataTypeOptions"
|
:options="dataTypeOptions"
|
||||||
:placeholder="t('structureEditor.typePlaceholder')"
|
:placeholder="t('structureEditor.typePlaceholder')"
|
||||||
:search-placeholder="t('structureEditor.typePlaceholder')"
|
:search-placeholder="t('structureEditor.typePlaceholder')"
|
||||||
@@ -3332,7 +3332,7 @@ watch([activeTab, ddlLoading], ([tab, loading]) => {
|
|||||||
:trigger-class="[structureMonoControlClass, 'w-full']"
|
:trigger-class="[structureMonoControlClass, 'w-full']"
|
||||||
@update:model-value="(v: string) => updateColumnDataType(column, v)"
|
@update:model-value="(v: string) => updateColumnDataType(column, v)"
|
||||||
/>
|
/>
|
||||||
<Input v-else :model-value="gaussdbMDataTypeDisplayName(splitDataType(column.dataType).baseType)" :class="[structureMonoControlClass, 'w-full']" disabled />
|
<Input v-else :model-value="gaussdbMDataTypeDisplayName(dataTypeBaseInputValue(databaseType, column.dataType))" :class="[structureMonoControlClass, 'w-full']" disabled />
|
||||||
</td>
|
</td>
|
||||||
<td v-if="columnEditorControls.length" :class="structureCellClass">
|
<td v-if="columnEditorControls.length" :class="structureCellClass">
|
||||||
<Popover v-if="isMysqlEnumDataType(databaseType, column.dataType)">
|
<Popover v-if="isMysqlEnumDataType(databaseType, column.dataType)">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
createCopiedColumnDrafts,
|
createCopiedColumnDrafts,
|
||||||
createColumnDrafts,
|
createColumnDrafts,
|
||||||
createTriggerDrafts,
|
createTriggerDrafts,
|
||||||
|
dataTypeBaseInputValue,
|
||||||
dataTypeLengthInputValue,
|
dataTypeLengthInputValue,
|
||||||
dataTypeLengthUnitValue,
|
dataTypeLengthUnitValue,
|
||||||
DATA_TYPE_OPTIONS,
|
DATA_TYPE_OPTIONS,
|
||||||
@@ -410,6 +411,55 @@ describe("tableStructureEditorState", () => {
|
|||||||
expect(defaultNewColumnDataType("clickhouse")).toBe("String");
|
expect(defaultNewColumnDataType("clickhouse")).toBe("String");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("offers the Xugu types that can be used by the table editor", () => {
|
||||||
|
for (const dataType of ["TINYINT", "DOUBLE", "DATETIME", "DATETIME WITH TIME ZONE", "TIME WITH TIME ZONE", "TIMESTAMP WITH TIME ZONE", "INTERVAL YEAR", "INTERVAL DAY TO SECOND", "GUID", "ROWID", "JSON", "BIT", "VARBIT", "INTEGER[]", "DOUBLE[]", "CHAR[]", "CLOB[]"]) {
|
||||||
|
expect(DATA_TYPE_OPTIONS.xugu).toContain(dataType);
|
||||||
|
}
|
||||||
|
for (const pseudoType of ["NULL", '"NULL"', "ARRAY", "ROWVERSION", "POINT", "LSEG", "LINE", "BOX", "PATH", "POLYGON", "CIRCLE"]) {
|
||||||
|
expect(DATA_TYPE_OPTIONS.xugu).not.toContain(pseudoType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps Xugu type parameters within syntax the generic editor can emit", () => {
|
||||||
|
for (const fixedType of ["GUID", "ROWID", "JSON", "XML", "BLOB", "CLOB", "INTEGER[]", "INTERVAL DAY TO SECOND", "DATETIME WITH TIME ZONE"]) {
|
||||||
|
expect(isDataTypeLengthDisabled("xugu", fixedType)).toBe(true);
|
||||||
|
expect(combineDataTypeForDatabase("xugu", fixedType, "12")).toBe(fixedType);
|
||||||
|
}
|
||||||
|
for (const parameterizedType of ["VARCHAR", "BINARY", "BIT", "VARBIT", "NUMERIC", "TIME", "TIME WITH TIME ZONE", "TIMESTAMP", "TIMESTAMP WITH TIME ZONE"]) {
|
||||||
|
expect(isDataTypeLengthDisabled("xugu", parameterizedType)).toBe(false);
|
||||||
|
}
|
||||||
|
expect(combineDataTypeForDatabase("xugu", "TIME", "3")).toBe("TIME(3)");
|
||||||
|
expect(combineDataTypeForDatabase("xugu", "TIME", "4")).toBe("TIME");
|
||||||
|
expect(combineDataTypeForDatabase("xugu", "TIMESTAMP", "6")).toBe("TIMESTAMP(6)");
|
||||||
|
expect(combineDataTypeForDatabase("xugu", "TIMESTAMP", "7")).toBe("TIMESTAMP");
|
||||||
|
expect(combineDataTypeForDatabase("xugu", "TIME WITH TIME ZONE", "3")).toBe("TIME(3) WITH TIME ZONE");
|
||||||
|
expect(combineDataTypeForDatabase("xugu", "TIMESTAMP WITH TIME ZONE", "6")).toBe("TIMESTAMP(6) WITH TIME ZONE");
|
||||||
|
expect(combineDataTypeForDatabase("xugu", "TIMESTAMP WITH TIME ZONE", "7")).toBe("TIMESTAMP WITH TIME ZONE");
|
||||||
|
expect(dataTypeBaseInputValue("xugu", "TIMESTAMP(6) WITH TIME ZONE")).toBe("TIMESTAMP WITH TIME ZONE");
|
||||||
|
expect(dataTypeLengthInputValue("xugu", "TIMESTAMP(6) WITH TIME ZONE")).toBe("6");
|
||||||
|
|
||||||
|
// Xugu-only constraints must not change other database profiles.
|
||||||
|
expect(isDataTypeLengthDisabled("mysql", "json")).toBe(false);
|
||||||
|
expect(isDataTypeLengthDisabled("oracle", "clob")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("round-trips Xugu single-parameter metadata through the editor", () => {
|
||||||
|
const columns = createColumnDrafts(
|
||||||
|
[
|
||||||
|
{ name: "flags", data_type: "BIT", is_nullable: true, column_default: null, is_primary_key: false, extra: null, numeric_precision: 8 },
|
||||||
|
{ name: "bits", data_type: "VARBIT", is_nullable: true, column_default: null, is_primary_key: false, extra: null, numeric_precision: 64 },
|
||||||
|
{ name: "local_time", data_type: "TIME", is_nullable: true, column_default: null, is_primary_key: false, extra: null, numeric_precision: 3 },
|
||||||
|
{ name: "created_at", data_type: "TIMESTAMP", is_nullable: true, column_default: null, is_primary_key: false, extra: null, numeric_precision: 6 },
|
||||||
|
{ name: "created_at_tz", data_type: "TIMESTAMP WITH TIME ZONE", is_nullable: true, column_default: null, is_primary_key: false, extra: null, numeric_precision: 6 },
|
||||||
|
{ name: "local_time_tz", data_type: "TIME WITH TIME ZONE", is_nullable: true, column_default: null, is_primary_key: false, extra: null, numeric_precision: 3 },
|
||||||
|
],
|
||||||
|
"xugu",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(columns.map((column) => column.dataType)).toEqual(["BIT(8)", "VARBIT(64)", "TIME(3)", "TIMESTAMP(6)", "TIMESTAMP(6) WITH TIME ZONE", "TIME(3) WITH TIME ZONE"]);
|
||||||
|
expect(columns.map((column) => column.original?.data_type)).toEqual(columns.map((column) => column.dataType));
|
||||||
|
});
|
||||||
|
|
||||||
it("requires a SQLite rebuild only for a retained existing column type change", () => {
|
it("requires a SQLite rebuild only for a retained existing column type change", () => {
|
||||||
const [column] = createColumnDrafts(
|
const [column] = createColumnDrafts(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -371,7 +371,63 @@ export const DATA_TYPE_OPTIONS: Record<string, string[]> = {
|
|||||||
"interval day to second",
|
"interval day to second",
|
||||||
],
|
],
|
||||||
questdb: ["boolean", "ipv4", "byte", "short", "char", "int", "float", "symbol", "varchar", "string", "long", "date", "timestamp", "timestamp_ns", "double", "uuid", "binary", "long256", "geohash", "array", "interval", "decimal"],
|
questdb: ["boolean", "ipv4", "byte", "short", "char", "int", "float", "symbol", "varchar", "string", "long", "date", "timestamp", "timestamp_ns", "double", "uuid", "binary", "long256", "geohash", "array", "interval", "decimal"],
|
||||||
xugu: ["BOOLEAN", "INTEGER", "SMALLINT", "BIGINT", "FLOAT", "NUMERIC", "CHAR", "VARCHAR", "CLOB", "DATE", "TIME", "TIMESTAMP", "BINARY", "VARBINARY", "BLOB", "XML", "BOOL", "INT", "SHORT", "LONGINT", "LONG", "REAL", "DECIMAL", "TEXT", "NCHAR", "NVARCHAR", "NVARCHAR2"],
|
xugu: [
|
||||||
|
"BIGINT",
|
||||||
|
"BINARY",
|
||||||
|
"BIT",
|
||||||
|
"BLOB",
|
||||||
|
"BOOL",
|
||||||
|
"BOOLEAN",
|
||||||
|
"CHAR",
|
||||||
|
"CHAR[]",
|
||||||
|
"CLOB",
|
||||||
|
"CLOB[]",
|
||||||
|
"DATE",
|
||||||
|
"DATETIME",
|
||||||
|
"DATETIME WITH TIME ZONE",
|
||||||
|
"DECIMAL",
|
||||||
|
"DOUBLE",
|
||||||
|
"DOUBLE[]",
|
||||||
|
"FLOAT",
|
||||||
|
"GUID",
|
||||||
|
"INT",
|
||||||
|
"INTEGER",
|
||||||
|
"INTEGER[]",
|
||||||
|
"INTERVAL DAY",
|
||||||
|
"INTERVAL DAY TO HOUR",
|
||||||
|
"INTERVAL DAY TO MINUTE",
|
||||||
|
"INTERVAL DAY TO SECOND",
|
||||||
|
"INTERVAL HOUR",
|
||||||
|
"INTERVAL HOUR TO MINUTE",
|
||||||
|
"INTERVAL HOUR TO SECOND",
|
||||||
|
"INTERVAL MINUTE",
|
||||||
|
"INTERVAL MINUTE TO SECOND",
|
||||||
|
"INTERVAL MONTH",
|
||||||
|
"INTERVAL SECOND",
|
||||||
|
"INTERVAL YEAR",
|
||||||
|
"INTERVAL YEAR TO MONTH",
|
||||||
|
"JSON",
|
||||||
|
"LONG",
|
||||||
|
"LONGINT",
|
||||||
|
"NCHAR",
|
||||||
|
"NUMERIC",
|
||||||
|
"NVARCHAR",
|
||||||
|
"NVARCHAR2",
|
||||||
|
"REAL",
|
||||||
|
"ROWID",
|
||||||
|
"SHORT",
|
||||||
|
"SMALLINT",
|
||||||
|
"TEXT",
|
||||||
|
"TIME",
|
||||||
|
"TIME WITH TIME ZONE",
|
||||||
|
"TIMESTAMP",
|
||||||
|
"TIMESTAMP WITH TIME ZONE",
|
||||||
|
"TINYINT",
|
||||||
|
"VARBINARY",
|
||||||
|
"VARBIT",
|
||||||
|
"VARCHAR",
|
||||||
|
"XML",
|
||||||
|
],
|
||||||
duckdb: ["BOOLEAN", "TINYINT", "SMALLINT", "INTEGER", "BIGINT", "HUGEINT", "UTINYINT", "USMALLINT", "UINTEGER", "UBIGINT", "FLOAT", "DOUBLE", "DECIMAL", "VARCHAR", "TEXT", "BLOB", "DATE", "TIME", "TIMESTAMP", "TIMESTAMPTZ", "INTERVAL", "UUID", "JSON"],
|
duckdb: ["BOOLEAN", "TINYINT", "SMALLINT", "INTEGER", "BIGINT", "HUGEINT", "UTINYINT", "USMALLINT", "UINTEGER", "UBIGINT", "FLOAT", "DOUBLE", "DECIMAL", "VARCHAR", "TEXT", "BLOB", "DATE", "TIME", "TIMESTAMP", "TIMESTAMPTZ", "INTERVAL", "UUID", "JSON"],
|
||||||
h2: ["BOOLEAN", "TINYINT", "SMALLINT", "INTEGER", "BIGINT", "IDENTITY", "DECIMAL", "NUMERIC", "REAL", "DOUBLE", "FLOAT", "CHAR", "CHARACTER", "VARCHAR", "VARCHAR_IGNORECASE", "CLOB", "BINARY", "VARBINARY", "BLOB", "DATE", "TIME", "TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "UUID", "ARRAY", "JSON"],
|
h2: ["BOOLEAN", "TINYINT", "SMALLINT", "INTEGER", "BIGINT", "IDENTITY", "DECIMAL", "NUMERIC", "REAL", "DOUBLE", "FLOAT", "CHAR", "CHARACTER", "VARCHAR", "VARCHAR_IGNORECASE", "CLOB", "BINARY", "VARBINARY", "BLOB", "DATE", "TIME", "TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "UUID", "ARRAY", "JSON"],
|
||||||
};
|
};
|
||||||
@@ -559,6 +615,8 @@ export const POSTGRES_TYPE_LENGTH_DISABLES: string[] = [
|
|||||||
|
|
||||||
export const ORACLE_LIKE_TYPE_LENGTH_DISABLES: string[] = ["binary_double", "binary_float", "bigint", "boolean", "bool", "byte", "date", "double", "double precision", "float", "integer", "int", "long", "long raw", "nclob", "real", "smallint", "text", "tinyint"];
|
export const ORACLE_LIKE_TYPE_LENGTH_DISABLES: string[] = ["binary_double", "binary_float", "bigint", "boolean", "bool", "byte", "date", "double", "double precision", "float", "integer", "int", "long", "long raw", "nclob", "real", "smallint", "text", "tinyint"];
|
||||||
|
|
||||||
|
const XUGU_TYPE_LENGTH_DISABLES = new Set([...ORACLE_LIKE_TYPE_LENGTH_DISABLES, "blob", "clob", "datetime", "datetime with time zone", "guid", "json", "longint", "rowid", "short", "xml"]);
|
||||||
|
|
||||||
export const SQLSERVER_TYPE_LENGTH_DISABLES: string[] = ["bigint", "bit", "date", "datetime", "image", "int", "integer", "money", "ntext", "real", "smalldatetime", "smallint", "smallmoney", "sql_variant", "text", "timestamp", "tinyint", "uniqueidentifier", "xml"];
|
export const SQLSERVER_TYPE_LENGTH_DISABLES: string[] = ["bigint", "bit", "date", "datetime", "image", "int", "integer", "money", "ntext", "real", "smalldatetime", "smallint", "smallmoney", "sql_variant", "text", "timestamp", "tinyint", "uniqueidentifier", "xml"];
|
||||||
|
|
||||||
export function parseExtraToColumnExtra(extra: string | null | undefined, databaseType?: DatabaseType): ColumnExtra {
|
export function parseExtraToColumnExtra(extra: string | null | undefined, databaseType?: DatabaseType): ColumnExtra {
|
||||||
@@ -753,9 +811,10 @@ function columnDefaultForEditor(column: ColumnInfo, databaseType?: DatabaseType)
|
|||||||
|
|
||||||
const CHARACTER_LENGTH_METADATA_TYPES = new Set(["binary", "char", "character", "character varying", "nchar", "nvarchar", "nvarchar2", "varbinary", "varchar", "varchar2"]);
|
const CHARACTER_LENGTH_METADATA_TYPES = new Set(["binary", "char", "character", "character varying", "nchar", "nvarchar", "nvarchar2", "varbinary", "varchar", "varchar2"]);
|
||||||
const NUMERIC_PRECISION_METADATA_TYPES = new Set(["decimal", "number", "numeric"]);
|
const NUMERIC_PRECISION_METADATA_TYPES = new Set(["decimal", "number", "numeric"]);
|
||||||
|
const XUGU_SINGLE_PRECISION_METADATA_TYPES = new Set(["bit", "time", "time with time zone", "timestamp", "timestamp with time zone", "varbit"]);
|
||||||
|
|
||||||
function columnDataTypeForEditor(column: ColumnInfo, databaseType?: DatabaseType): string {
|
function columnDataTypeForEditor(column: ColumnInfo, databaseType?: DatabaseType): string {
|
||||||
const parsed = splitDataType(column.data_type);
|
const parsed = splitDataTypeForDatabase(databaseType, column.data_type);
|
||||||
if (parsed.params) return column.data_type;
|
if (parsed.params) return column.data_type;
|
||||||
|
|
||||||
const baseType = parsed.baseType.trim().replace(/\s+/g, " ");
|
const baseType = parsed.baseType.trim().replace(/\s+/g, " ");
|
||||||
@@ -763,6 +822,11 @@ function columnDataTypeForEditor(column: ColumnInfo, databaseType?: DatabaseType
|
|||||||
if (CHARACTER_LENGTH_METADATA_TYPES.has(normalized) && Number.isInteger(column.character_maximum_length) && Number(column.character_maximum_length) > 0) {
|
if (CHARACTER_LENGTH_METADATA_TYPES.has(normalized) && Number.isInteger(column.character_maximum_length) && Number(column.character_maximum_length) > 0) {
|
||||||
return combineDataTypeForDatabase(databaseType, baseType, String(column.character_maximum_length));
|
return combineDataTypeForDatabase(databaseType, baseType, String(column.character_maximum_length));
|
||||||
}
|
}
|
||||||
|
if (databaseType === "xugu" && XUGU_SINGLE_PRECISION_METADATA_TYPES.has(normalized) && Number.isInteger(column.numeric_precision)) {
|
||||||
|
const precision = Number(column.numeric_precision);
|
||||||
|
const minimum = normalized === "bit" || normalized === "varbit" ? 1 : 0;
|
||||||
|
if (precision >= minimum) return combineDataTypeForDatabase(databaseType, baseType, String(precision));
|
||||||
|
}
|
||||||
if (NUMERIC_PRECISION_METADATA_TYPES.has(normalized) && Number.isInteger(column.numeric_precision) && Number(column.numeric_precision) > 0) {
|
if (NUMERIC_PRECISION_METADATA_TYPES.has(normalized) && Number.isInteger(column.numeric_precision) && Number(column.numeric_precision) > 0) {
|
||||||
const scale = Number.isInteger(column.numeric_scale) && Number(column.numeric_scale) >= 0 ? `,${column.numeric_scale}` : "";
|
const scale = Number.isInteger(column.numeric_scale) && Number(column.numeric_scale) >= 0 ? `,${column.numeric_scale}` : "";
|
||||||
return combineDataTypeForDatabase(databaseType, baseType, `${column.numeric_precision}${scale}`);
|
return combineDataTypeForDatabase(databaseType, baseType, `${column.numeric_precision}${scale}`);
|
||||||
@@ -1047,6 +1111,23 @@ export function splitDataType(raw: string): { baseType: string; params: string }
|
|||||||
return { baseType, params };
|
return { baseType, params };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function splitDataTypeForDatabase(dbType: DatabaseType | undefined, raw: string): { baseType: string; params: string } {
|
||||||
|
if (dbType === "xugu") {
|
||||||
|
const match = raw.trim().match(/^(TIME|TIMESTAMP)\s*\(([^()]*)\)\s+WITH\s+TIME\s+ZONE$/i);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
baseType: `${match[1]} WITH TIME ZONE`,
|
||||||
|
params: match[2]!.trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return splitDataType(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dataTypeBaseInputValue(dbType: DatabaseType | undefined, rawDataType: string): string {
|
||||||
|
return splitDataTypeForDatabase(dbType, rawDataType).baseType;
|
||||||
|
}
|
||||||
|
|
||||||
export type DataTypeLengthUnit = "BYTE" | "CHAR";
|
export type DataTypeLengthUnit = "BYTE" | "CHAR";
|
||||||
|
|
||||||
const CHARACTER_LENGTH_UNIT_TYPES = new Set(["char", "varchar", "varchar2"]);
|
const CHARACTER_LENGTH_UNIT_TYPES = new Set(["char", "varchar", "varchar2"]);
|
||||||
@@ -1062,7 +1143,7 @@ export function getDataTypeLengthUnitOptions(dbType: DatabaseType | undefined, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
function splitDataTypeLengthParams(dbType: DatabaseType | undefined, rawDataType: string): { length: string; unit: DataTypeLengthUnit | "" } {
|
function splitDataTypeLengthParams(dbType: DatabaseType | undefined, rawDataType: string): { length: string; unit: DataTypeLengthUnit | "" } {
|
||||||
const { params } = splitDataType(rawDataType);
|
const { params } = splitDataTypeForDatabase(dbType, rawDataType);
|
||||||
if (!params || getDataTypeLengthUnitOptions(dbType, rawDataType).length === 0) {
|
if (!params || getDataTypeLengthUnitOptions(dbType, rawDataType).length === 0) {
|
||||||
return { length: params, unit: "" };
|
return { length: params, unit: "" };
|
||||||
}
|
}
|
||||||
@@ -1158,19 +1239,28 @@ export function combineDataTypeForDatabase(dbType: DatabaseType | undefined, bas
|
|||||||
const normalizedParams = normalizeDataTypeParams(dbType, baseType, params);
|
const normalizedParams = normalizeDataTypeParams(dbType, baseType, params);
|
||||||
const mysqlType = combineMysqlNumericAttributeType(dbType, baseType, normalizedParams);
|
const mysqlType = combineMysqlNumericAttributeType(dbType, baseType, normalizedParams);
|
||||||
if (mysqlType) return mysqlType;
|
if (mysqlType) return mysqlType;
|
||||||
|
const xuguTemporalType = combineXuguTemporalType(baseType, normalizedParams, dbType);
|
||||||
|
if (xuguTemporalType) return xuguTemporalType;
|
||||||
return combineDataType(baseType, normalizedParams);
|
return combineDataType(baseType, normalizedParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dataTypeLengthInputValue(dbType: DatabaseType | undefined, rawDataType: string): string {
|
export function dataTypeLengthInputValue(dbType: DatabaseType | undefined, rawDataType: string): string {
|
||||||
const parsed = splitDataType(rawDataType);
|
const parsed = splitDataTypeForDatabase(dbType, rawDataType);
|
||||||
return isDataTypeLengthDisabled(dbType, parsed.baseType) ? "" : splitDataTypeLengthParams(dbType, rawDataType).length;
|
return isDataTypeLengthDisabled(dbType, parsed.baseType) ? "" : splitDataTypeLengthParams(dbType, rawDataType).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function combineXuguTemporalType(baseType: string, params: string, dbType: DatabaseType | undefined): string | null {
|
||||||
|
if (dbType !== "xugu") return null;
|
||||||
|
const match = baseType.trim().match(/^(TIME|TIMESTAMP)\s+WITH\s+TIME\s+ZONE$/i);
|
||||||
|
if (!match) return null;
|
||||||
|
return params ? `${match[1]}(${params}) WITH TIME ZONE` : baseType.trim();
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeDataTypeParams(dbType: DatabaseType | undefined, baseType: string, params: string): string {
|
export function normalizeDataTypeParams(dbType: DatabaseType | undefined, baseType: string, params: string): string {
|
||||||
const p = params.trim();
|
const p = params.trim();
|
||||||
if (!p) return "";
|
if (!p) return "";
|
||||||
if (!isTemporalPrecisionType(dbType, baseType)) return p;
|
if (!isTemporalPrecisionType(dbType, baseType)) return p;
|
||||||
return isValidTemporalPrecision(dbType, p) ? p : "";
|
return isValidTemporalPrecision(dbType, baseType, p) ? p : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTemporalPrecisionType(dbType: DatabaseType | undefined, baseType: string): boolean {
|
function isTemporalPrecisionType(dbType: DatabaseType | undefined, baseType: string): boolean {
|
||||||
@@ -1200,6 +1290,8 @@ function isTemporalPrecisionType(dbType: DatabaseType | undefined, baseType: str
|
|||||||
return ["timestamp", "timestamp with time zone", "timestamp with local time zone"].includes(normalized);
|
return ["timestamp", "timestamp with time zone", "timestamp with local time zone"].includes(normalized);
|
||||||
case "questdb":
|
case "questdb":
|
||||||
return ["timestamp"].includes(normalized);
|
return ["timestamp"].includes(normalized);
|
||||||
|
case "xugu":
|
||||||
|
return ["time", "time with time zone", "timestamp", "timestamp with time zone"].includes(normalized);
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1224,10 +1316,11 @@ function isOracleLikeStructureType(dbType: DatabaseType | undefined): boolean {
|
|||||||
return dbType === "oracle" || dbType === "dameng" || dbType === "oceanbase-oracle" || dbType === "iris" || dbType === "yashandb" || dbType === "xugu";
|
return dbType === "oracle" || dbType === "dameng" || dbType === "oceanbase-oracle" || dbType === "iris" || dbType === "yashandb" || dbType === "xugu";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidTemporalPrecision(dbType: DatabaseType | undefined, params: string): boolean {
|
function isValidTemporalPrecision(dbType: DatabaseType | undefined, baseType: string, params: string): boolean {
|
||||||
if (!/^\d+$/.test(params)) return false;
|
if (!/^\d+$/.test(params)) return false;
|
||||||
const value = Number(params);
|
const value = Number(params);
|
||||||
const max = dbType === "oracle" || dbType === "dameng" || dbType === "oceanbase-oracle" ? 9 : 6;
|
const normalizedBaseType = baseType.trim().replace(/\s+/g, " ").toLowerCase();
|
||||||
|
const max = dbType === "xugu" && ["time", "time with time zone"].includes(normalizedBaseType) ? 3 : dbType === "oracle" || dbType === "dameng" || dbType === "oceanbase-oracle" ? 9 : 6;
|
||||||
return Number.isInteger(value) && value >= 0 && value <= max && String(value) === params;
|
return Number.isInteger(value) && value >= 0 && value <= max && String(value) === params;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1303,6 +1396,10 @@ export function isDataTypeLengthDisabled(_dbType: DatabaseType | undefined, base
|
|||||||
return key !== "bit" && key !== "float_vector";
|
return key !== "bit" && key !== "float_vector";
|
||||||
} else if (_dbType === "postgres" || _dbType === "gaussdb" || _dbType === "kwdb" || _dbType === "opengauss" || _dbType === "highgo" || _dbType === "uxdb" || _dbType === "vastbase" || _dbType === "kingbase") {
|
} else if (_dbType === "postgres" || _dbType === "gaussdb" || _dbType === "kwdb" || _dbType === "opengauss" || _dbType === "highgo" || _dbType === "uxdb" || _dbType === "vastbase" || _dbType === "kingbase") {
|
||||||
return key.endsWith("[]") || POSTGRES_TYPE_LENGTH_DISABLES.includes(key);
|
return key.endsWith("[]") || POSTGRES_TYPE_LENGTH_DISABLES.includes(key);
|
||||||
|
} else if (_dbType === "xugu") {
|
||||||
|
// Xugu array suffixes and interval qualifiers require grammar-aware
|
||||||
|
// placement; a generic TYPE(length) editor would emit invalid DDL for them.
|
||||||
|
return key.endsWith("[]") || key.startsWith("interval ") || XUGU_TYPE_LENGTH_DISABLES.has(key);
|
||||||
} else if (isOracleLikeStructureType(_dbType)) {
|
} else if (isOracleLikeStructureType(_dbType)) {
|
||||||
// Dameng/Oracle integer aliases have fixed precision; MySQL-style display widths generate invalid DDL.
|
// Dameng/Oracle integer aliases have fixed precision; MySQL-style display widths generate invalid DDL.
|
||||||
return ORACLE_LIKE_TYPE_LENGTH_DISABLES.includes(key);
|
return ORACLE_LIKE_TYPE_LENGTH_DISABLES.includes(key);
|
||||||
|
|||||||
@@ -512,6 +512,48 @@ fn builds_postgres_type_change_that_drops_default() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builds_xugu_timezone_temporal_precision_in_final_ddl() {
|
||||||
|
let mut local_time = column("local_time");
|
||||||
|
local_time.data_type = "TIME(3) WITH TIME ZONE".to_string();
|
||||||
|
let mut created_at = column("created_at");
|
||||||
|
created_at.data_type = "TIMESTAMP(6) WITH TIME ZONE".to_string();
|
||||||
|
let created = build_create_table_sql(structure_change_options(
|
||||||
|
DatabaseType::Xugu,
|
||||||
|
Some("public"),
|
||||||
|
"events",
|
||||||
|
vec![local_time, created_at],
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
created.statements,
|
||||||
|
vec![
|
||||||
|
r#"CREATE TABLE "public"."events" (
|
||||||
|
"local_time" TIME(3) WITH TIME ZONE,
|
||||||
|
"created_at" TIMESTAMP(6) WITH TIME ZONE
|
||||||
|
);"#
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut altered_at = column("created_at");
|
||||||
|
altered_at.data_type = "TIMESTAMP(6) WITH TIME ZONE".to_string();
|
||||||
|
altered_at.original = Some(ColumnInfo {
|
||||||
|
name: "created_at".to_string(),
|
||||||
|
data_type: "TIMESTAMP".to_string(),
|
||||||
|
is_nullable: true,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let altered = build_single_column_alter_sql(SingleColumnAlterSqlOptions {
|
||||||
|
database_type: Some(DatabaseType::Xugu),
|
||||||
|
schema: Some("public".to_string()),
|
||||||
|
table_name: "events".to_string(),
|
||||||
|
column: altered_at,
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
altered.statements,
|
||||||
|
vec![r#"ALTER TABLE "public"."events" ALTER COLUMN "created_at" TIMESTAMP(6) WITH TIME ZONE;"#]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builds_postgres_array_and_domain_type_casts_without_affecting_xugu() {
|
fn builds_postgres_array_and_domain_type_casts_without_affecting_xugu() {
|
||||||
let mut tags = column("tags");
|
let mut tags = column("tags");
|
||||||
|
|||||||
Reference in new issue
Block a user