mirror of
https://github.com/t8y2/dbx.git
synced 2026-11-01 00:25:38 +00:00
Add TDengine support and editable grid handling
This commit is contained in:
@@ -201,7 +201,7 @@ const driverProfiles: Record<
|
||||
kylin: { type: "kylin", port: 7070, user: "ADMIN", label: "Apache Kylin", icon: "kylin" },
|
||||
sundb: { type: "sundb", port: 22000, user: "root", label: "SunDB", icon: "sundb" },
|
||||
jdbc: { type: "jdbc", port: 0, user: "", label: "JDBC", icon: "jdbc" },
|
||||
tdengine: { type: "mysql", port: 6030, user: "root", label: "TDengine", icon: "tdengine" },
|
||||
tdengine: { type: "tdengine", port: 6041, user: "root", label: "TDengine", icon: "tdengine" },
|
||||
custom_mysql: {
|
||||
type: "mysql",
|
||||
port: 3306,
|
||||
@@ -267,14 +267,15 @@ watch(
|
||||
if (config) {
|
||||
const profile = profileForConfig(config);
|
||||
editingId.value = config.id;
|
||||
const profileConfig = driverProfiles[profile];
|
||||
form.value = {
|
||||
name: config.name,
|
||||
db_type: config.db_type,
|
||||
db_type: profileConfig?.type || config.db_type,
|
||||
driver_profile: profile,
|
||||
driver_label: config.driver_label || driverProfiles[profile]?.label || config.db_type,
|
||||
url_params: config.url_params || "",
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
port: profile === "tdengine" && (config.port === 0 || config.port === 6030) ? 6041 : config.port,
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
database: config.database,
|
||||
|
||||
@@ -77,6 +77,7 @@ import {
|
||||
canEditExistingTableRows,
|
||||
hiveTablePropertiesIndicateTransactional,
|
||||
isHiddenGridColumn,
|
||||
isTdengineExistingRowReadonlyColumn,
|
||||
usesSyntheticRowIdKey,
|
||||
} from "@/lib/tableEditing";
|
||||
import { formatGridSqlLiteral } from "@/lib/dataGridSql";
|
||||
@@ -1153,7 +1154,12 @@ function canEditRowItem(item: RowItem | undefined): boolean {
|
||||
}
|
||||
|
||||
function canEditCellItem(item: RowItem | undefined, columnIndex: number): boolean {
|
||||
return canEditRowItem(item) && canEditColumn(columnIndex);
|
||||
if (!canEditRowItem(item) || !canEditColumn(columnIndex)) return false;
|
||||
if (!item?.isNew) {
|
||||
const column = props.result.columns[columnIndex] ?? "";
|
||||
if (isTdengineExistingRowReadonlyColumn(props.databaseType, column, props.tableMeta?.columns ?? [])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function canDeleteRowItem(item: RowItem | undefined): boolean {
|
||||
|
||||
@@ -74,13 +74,14 @@ async function openTableTarget(target: NavigationTarget) {
|
||||
columns,
|
||||
primaryKeys,
|
||||
});
|
||||
if (useRowId) {
|
||||
if (useRowId || config.db_type === "tdengine") {
|
||||
const newSql = buildTableSelectSql({
|
||||
databaseType: config.db_type,
|
||||
schema: target.schema,
|
||||
tableName: target.tableName,
|
||||
whereInput: target.whereInput,
|
||||
primaryKeys,
|
||||
columns: columns.map((column) => column.name),
|
||||
includeRowId: true,
|
||||
});
|
||||
queryStore.updateSql(tabId, newSql);
|
||||
|
||||
@@ -63,6 +63,9 @@ export function connectionUrlPlaceholder(dbType: DatabaseType): string {
|
||||
case "dameng":
|
||||
return "dm://user:password@host:port";
|
||||
|
||||
case "tdengine":
|
||||
return "tdengine://user:password@host:6041/database";
|
||||
|
||||
case "jdbc":
|
||||
return "jdbc:mysql://host:3306/database";
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ const SCHEME_PROFILES: Record<string, ConnectionProfile> = {
|
||||
dameng: { type: "dameng", profile: "dm", label: "DM (Dameng)", defaultPort: 5236 },
|
||||
gaussdb: { type: "gaussdb", profile: "gaussdb", label: "GaussDB", defaultPort: 5432 },
|
||||
opengauss: { type: "gaussdb", profile: "opengauss", label: "openGauss", defaultPort: 5432 },
|
||||
tdengine: { type: "tdengine", profile: "tdengine", label: "TDengine", defaultPort: 6041 },
|
||||
"taos-ws": { type: "tdengine", profile: "tdengine", label: "TDengine", defaultPort: 6041 },
|
||||
};
|
||||
|
||||
const HTTP_SELECTED_PROFILES: Record<string, ConnectionProfile> = {
|
||||
|
||||
+101
-2
@@ -1,5 +1,5 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import { DBX_NEO4J_ELEMENT_ID_COLUMN, DBX_ROWID_COLUMN } from "./tableEditing.ts";
|
||||
import { DBX_NEO4J_ELEMENT_ID_COLUMN, DBX_ROWID_COLUMN, DBX_TDENGINE_TBNAME_COLUMN } from "./tableEditing.ts";
|
||||
import { qualifiedTableName, quoteTableIdentifier } from "./tableSelectSql.ts";
|
||||
|
||||
export type GridCellValue = string | number | boolean | null;
|
||||
@@ -8,6 +8,7 @@ export interface DataGridTableMeta {
|
||||
schema?: string;
|
||||
tableName: string;
|
||||
primaryKeys: string[];
|
||||
columns?: DataGridColumnInfo[];
|
||||
}
|
||||
|
||||
export interface DataGridColumnInfo {
|
||||
@@ -108,6 +109,7 @@ function validateInsertedPrimaryKeys(options: DataGridSaveValidationOptions): st
|
||||
|
||||
export function buildDataGridSaveStatements(options: DataGridSaveStatementOptions): string[] {
|
||||
if (options.databaseType === "neo4j") return buildNeo4jDataGridSaveStatements(options);
|
||||
if (options.databaseType === "tdengine") return buildTdengineDataGridSaveStatements(options);
|
||||
const saveColumns = effectiveColumns(options.sourceColumns, options.columns);
|
||||
|
||||
const table = qualifiedTableName({
|
||||
@@ -155,6 +157,81 @@ export function buildDataGridSaveStatements(options: DataGridSaveStatementOption
|
||||
return statements;
|
||||
}
|
||||
|
||||
function buildTdengineDataGridSaveStatements(options: DataGridSaveStatementOptions): string[] {
|
||||
const saveColumns = effectiveColumns(options.sourceColumns, options.columns);
|
||||
const statements: string[] = [];
|
||||
const canOverwriteExistingRows = saveColumns.some((column) => column?.toLowerCase() === DBX_TDENGINE_TBNAME_COLUMN);
|
||||
|
||||
if (canOverwriteExistingRows) {
|
||||
for (const [rowIndex, changes] of options.dirtyRows) {
|
||||
const row = options.rows[rowIndex];
|
||||
if (!row) continue;
|
||||
const afterRow = [...row];
|
||||
for (const [columnIndex, value] of changes) {
|
||||
afterRow[columnIndex] = value;
|
||||
}
|
||||
const statement = buildTdengineInsertStatement(options, saveColumns, afterRow);
|
||||
if (statement) statements.push(statement);
|
||||
}
|
||||
}
|
||||
|
||||
for (const row of options.newRows) {
|
||||
const statement = buildTdengineInsertStatement(options, saveColumns, row);
|
||||
if (statement) statements.push(statement);
|
||||
}
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
function buildTdengineInsertStatement(
|
||||
options: DataGridSaveStatementOptions,
|
||||
saveColumns: Array<string | undefined>,
|
||||
row: GridCellValue[],
|
||||
): string | undefined {
|
||||
const tbname = tdengineTbnameValue(saveColumns, row);
|
||||
const table = qualifiedTableName({
|
||||
databaseType: "tdengine",
|
||||
schema: options.tableMeta.schema,
|
||||
tableName: options.tableMeta.tableName,
|
||||
});
|
||||
const tagColumns = tdengineTagColumns(options.tableMeta.columns);
|
||||
const insertPairs = saveColumns
|
||||
.map((column, index) => ({ column, value: row[index] }))
|
||||
.filter((pair): pair is { column: string; value: GridCellValue } => !!pair.column)
|
||||
.filter((pair) => pair.value !== null && pair.value !== undefined)
|
||||
.filter((pair) => tdengineCanInsertColumn(pair.column, options.tableMeta.tableName, tbname, tagColumns));
|
||||
if (!insertPairs.length) return undefined;
|
||||
const columns = insertPairs.map((pair) => quoteIdent("tdengine", pair.column)).join(", ");
|
||||
const values = insertPairs.map((pair) => formatGridSqlLiteral(pair.value, "tdengine")).join(", ");
|
||||
return `INSERT INTO ${table} (${columns}) VALUES (${values});`;
|
||||
}
|
||||
|
||||
function tdengineTbnameValue(saveColumns: Array<string | undefined>, row: GridCellValue[]): string | undefined {
|
||||
const index = saveColumns.findIndex((column) => column?.toLowerCase() === DBX_TDENGINE_TBNAME_COLUMN);
|
||||
if (index < 0) return undefined;
|
||||
const value = row[index];
|
||||
return value === null || value === undefined ? undefined : String(value);
|
||||
}
|
||||
|
||||
function tdengineCanInsertColumn(
|
||||
column: string,
|
||||
tableName: string,
|
||||
tbname: string | undefined,
|
||||
tagColumns: Set<string>,
|
||||
): boolean {
|
||||
const normalized = column.toLowerCase();
|
||||
const targetIsChildTable = !tbname || tbname === tableName;
|
||||
if (!targetIsChildTable) return true;
|
||||
if (normalized === DBX_TDENGINE_TBNAME_COLUMN) return false;
|
||||
return !tagColumns.has(normalized);
|
||||
}
|
||||
|
||||
function tdengineTagColumns(columns: DataGridColumnInfo[] | undefined): Set<string> {
|
||||
return new Set(
|
||||
(columns ?? []).filter((column) => /\btag\b/i.test(column.extra ?? "")).map((column) => column.name.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
export function buildDataGridRollbackStatements(options: DataGridSaveStatementOptions): string[] {
|
||||
if (options.databaseType === "neo4j") return buildNeo4jDataGridRollbackStatements(options);
|
||||
const saveColumns = effectiveColumns(options.sourceColumns, options.columns);
|
||||
@@ -253,10 +330,32 @@ export function formatGridSqlLiteral(value: GridCellValue, databaseType?: Databa
|
||||
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
||||
const text = String(value);
|
||||
if (text === "") return databaseType === "sqlserver" ? "N''" : "''";
|
||||
const escaped = `'${text.replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
|
||||
const literalText = databaseType === "tdengine" ? formatTdengineTimestampLiteralText(text) : text;
|
||||
const escaped = `'${literalText.replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
|
||||
return databaseType === "sqlserver" ? `N${escaped}` : escaped;
|
||||
}
|
||||
|
||||
function formatTdengineTimestampLiteralText(text: string): string {
|
||||
const match = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})(\.\d{1,9})?$/.exec(text);
|
||||
if (!match) return text;
|
||||
return `${match[1]}T${match[2]}${normalizeFractionalSeconds(match[3])}${localTimezoneOffsetSuffix(text)}`;
|
||||
}
|
||||
|
||||
function normalizeFractionalSeconds(fraction: string | undefined): string {
|
||||
if (!fraction) return ".000";
|
||||
return fraction.length >= 4 ? fraction.slice(0, 4) : fraction.padEnd(4, "0");
|
||||
}
|
||||
|
||||
function localTimezoneOffsetSuffix(text: string): string {
|
||||
const date = new Date(text.replace(" ", "T"));
|
||||
const offsetMinutes = Number.isNaN(date.getTime()) ? new Date().getTimezoneOffset() : date.getTimezoneOffset();
|
||||
const sign = offsetMinutes <= 0 ? "+" : "-";
|
||||
const abs = Math.abs(offsetMinutes);
|
||||
const hours = String(Math.floor(abs / 60)).padStart(2, "0");
|
||||
const minutes = String(abs % 60).padStart(2, "0");
|
||||
return `${sign}${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
function buildPrimaryKeyWhere(
|
||||
databaseType: DatabaseType | undefined,
|
||||
primaryKeys: string[],
|
||||
|
||||
@@ -15,6 +15,7 @@ export const SCHEMA_AWARE_TYPES = new Set<DatabaseType>([
|
||||
"snowflake",
|
||||
"trino",
|
||||
"db2",
|
||||
"tdengine",
|
||||
]);
|
||||
|
||||
export const SQL_FILE_UNSUPPORTED_TYPES = new Set<DatabaseType>(["redis", "mongodb", "elasticsearch"]);
|
||||
@@ -62,6 +63,7 @@ export const DATABASE_SEARCH_SUPPORTED_TYPES = new Set<DatabaseType>([
|
||||
"bigquery",
|
||||
"kylin",
|
||||
"sundb",
|
||||
"tdengine",
|
||||
]);
|
||||
|
||||
export const TABLE_IMPORT_SUPPORTED_TYPES = new Set<DatabaseType>([
|
||||
@@ -124,6 +126,7 @@ export const TREE_SCHEMA_TYPES = new Set<DatabaseType>([
|
||||
"jdbc",
|
||||
"trino",
|
||||
"h2",
|
||||
"tdengine",
|
||||
]);
|
||||
|
||||
export const PG_LIKE_STRUCTURE_TYPES = new Set<DatabaseType>(["postgres", "redshift", "gaussdb"]);
|
||||
@@ -147,6 +150,7 @@ export const AGENT_DRIVER_TYPES = new Set<DatabaseType>([
|
||||
"kylin",
|
||||
"sundb",
|
||||
"gaussdb",
|
||||
"tdengine",
|
||||
]);
|
||||
|
||||
export const TRANSFER_SQL_TYPES = new Set<DatabaseType>([
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface TableDataCapability {
|
||||
updateRequiresPrimaryKey: boolean;
|
||||
deleteRequiresPrimaryKey: boolean;
|
||||
requiresTransactionalTableForExistingRows: boolean;
|
||||
existingRowsReadonly?: boolean;
|
||||
transaction: boolean;
|
||||
readonly?: boolean;
|
||||
}
|
||||
@@ -77,6 +78,15 @@ const DATABASE_CAPABILITY_OVERRIDES: Partial<Record<DatabaseType, Partial<Databa
|
||||
readonly: true,
|
||||
},
|
||||
},
|
||||
tdengine: {
|
||||
tableData: {
|
||||
insert: true,
|
||||
updateRequiresPrimaryKey: true,
|
||||
deleteRequiresPrimaryKey: true,
|
||||
requiresTransactionalTableForExistingRows: false,
|
||||
transaction: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getDatabaseCapability(dbType?: DatabaseType): DatabaseCapability {
|
||||
|
||||
@@ -3,9 +3,11 @@ import { getDatabaseCapability } from "./databaseCapabilities";
|
||||
|
||||
export const DBX_ROWID_COLUMN = "__DBX_ROWID";
|
||||
export const DBX_NEO4J_ELEMENT_ID_COLUMN = "__DBX_ELEMENT_ID";
|
||||
export const DBX_TDENGINE_TBNAME_COLUMN = "tbname";
|
||||
|
||||
export function editablePrimaryKeys(databaseType: DatabaseType | undefined, columns: ColumnInfo[]): string[] {
|
||||
const primaryKeys = columns.filter((column) => column.is_primary_key).map((column) => column.name);
|
||||
if (databaseType === "tdengine" && primaryKeys.length > 0) return [DBX_TDENGINE_TBNAME_COLUMN, ...primaryKeys];
|
||||
const syntheticKey = getDatabaseCapability(databaseType).syntheticKey;
|
||||
if (syntheticKey === "oracle-rowid" && primaryKeys.length === 0) return [DBX_ROWID_COLUMN];
|
||||
if (syntheticKey === "neo4j-element-id" && primaryKeys.length === 0) return [DBX_NEO4J_ELEMENT_ID_COLUMN];
|
||||
@@ -30,7 +32,10 @@ export function canEditExistingTableRows(
|
||||
): boolean {
|
||||
const tableData = getDatabaseCapability(databaseType).tableData;
|
||||
if (tableData.readonly) return false;
|
||||
if (tableData.existingRowsReadonly) return false;
|
||||
if (tableData.requiresTransactionalTableForExistingRows && hiveTableTransactional !== true) return false;
|
||||
if (databaseType === "tdengine" && !primaryKeys?.some((key) => key.toLowerCase() === DBX_TDENGINE_TBNAME_COLUMN))
|
||||
return false;
|
||||
if (tableData.updateRequiresPrimaryKey && primaryKeys && primaryKeys.length === 0) return false;
|
||||
return true;
|
||||
}
|
||||
@@ -63,3 +68,14 @@ export function isHiddenGridColumn(
|
||||
if (databaseType === "neo4j" && column === DBX_NEO4J_ELEMENT_ID_COLUMN) return true;
|
||||
return usesSyntheticRowIdKey(databaseType, primaryKeys) && column.toUpperCase() === DBX_ROWID_COLUMN;
|
||||
}
|
||||
|
||||
export function isTdengineExistingRowReadonlyColumn(
|
||||
databaseType: DatabaseType | undefined,
|
||||
column: string,
|
||||
columns: ColumnInfo[],
|
||||
): boolean {
|
||||
if (databaseType !== "tdengine") return false;
|
||||
if (column.toLowerCase() === DBX_TDENGINE_TBNAME_COLUMN) return true;
|
||||
const columnInfo = columns.find((info) => info.name.toLowerCase() === column.toLowerCase());
|
||||
return !!columnInfo?.is_primary_key || /\btag\b/i.test(columnInfo?.extra ?? "");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DatabaseType } from "../types/database.ts";
|
||||
import { isSchemaAware, usesFetchFirst } from "./databaseCapabilities.ts";
|
||||
import { DBX_NEO4J_ELEMENT_ID_COLUMN, DBX_ROWID_COLUMN } from "./tableEditing.ts";
|
||||
import { DBX_NEO4J_ELEMENT_ID_COLUMN, DBX_ROWID_COLUMN, DBX_TDENGINE_TBNAME_COLUMN } from "./tableEditing.ts";
|
||||
|
||||
export interface BuildTableSelectSqlOptions {
|
||||
databaseType?: DatabaseType;
|
||||
@@ -17,7 +17,8 @@ export interface BuildTableSelectSqlOptions {
|
||||
}
|
||||
|
||||
export function quoteTableIdentifier(databaseType: DatabaseType | undefined, name: string): string {
|
||||
if (databaseType === "mysql" || databaseType === "hive") return `\`${name.replace(/`/g, "``")}\``;
|
||||
if (databaseType === "mysql" || databaseType === "hive" || databaseType === "tdengine")
|
||||
return `\`${name.replace(/`/g, "``")}\``;
|
||||
if (databaseType === "informix" && /^[A-Za-z_][A-Za-z0-9_$]*$/.test(name)) return name;
|
||||
if (databaseType === "neo4j") return quoteCypherIdentifier(name);
|
||||
if (databaseType === "sqlserver") return `[${name.replace(/\]/g, "]]")}]`;
|
||||
@@ -34,6 +35,7 @@ function isOracleRowId(databaseType: DatabaseType | undefined, name: string): bo
|
||||
|
||||
function quoteOrderIdentifier(databaseType: DatabaseType | undefined, name: string, tableAlias?: string): string {
|
||||
if (isOracleRowId(databaseType, name)) return tableAlias ? `${tableAlias}.ROWID` : "ROWID";
|
||||
if (isTdengineTbname(databaseType, name)) return DBX_TDENGINE_TBNAME_COLUMN;
|
||||
return quoteTableIdentifier(databaseType, name);
|
||||
}
|
||||
|
||||
@@ -90,7 +92,20 @@ export function buildTableSelectSql(options: BuildTableSelectSqlOptions): string
|
||||
}
|
||||
|
||||
function buildSelectColumns(databaseType: DatabaseType | undefined, columns?: string[]): string {
|
||||
if (databaseType !== "hive" || !columns?.length) return "*";
|
||||
if (!columns?.length) return "*";
|
||||
if (databaseType === "tdengine") {
|
||||
const tdengineColumns = columns.some((column) => column.toLowerCase() === DBX_TDENGINE_TBNAME_COLUMN)
|
||||
? columns
|
||||
: [DBX_TDENGINE_TBNAME_COLUMN, ...columns];
|
||||
return tdengineColumns
|
||||
.map((column) => {
|
||||
if (isTdengineTbname(databaseType, column)) return DBX_TDENGINE_TBNAME_COLUMN;
|
||||
const ident = quoteTableIdentifier(databaseType, column);
|
||||
return `${ident} AS ${ident}`;
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
if (databaseType !== "hive") return "*";
|
||||
return columns
|
||||
.map((column) => {
|
||||
const ident = quoteTableIdentifier(databaseType, column);
|
||||
@@ -99,6 +114,10 @@ function buildSelectColumns(databaseType: DatabaseType | undefined, columns?: st
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function isTdengineTbname(databaseType: DatabaseType | undefined, name: string): boolean {
|
||||
return databaseType === "tdengine" && name.toLowerCase() === DBX_TDENGINE_TBNAME_COLUMN;
|
||||
}
|
||||
|
||||
function buildNeo4jTableSelectSql(options: BuildTableSelectSqlOptions, limit: number): string {
|
||||
const label = quoteCypherIdentifier(options.tableName);
|
||||
const predicate = normalizeWhereInput(options.whereInput);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "@/lib/sqlAnalysis";
|
||||
import { restoreOpenTabsState, serializeOpenTabs } from "@/lib/openTabsPersistence";
|
||||
import { mongoDocumentsToQueryResult, parseMongoFindCommand } from "@/lib/mongoShellCommand";
|
||||
import { editablePrimaryKeys } from "@/lib/tableEditing";
|
||||
import * as api from "@/lib/api";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
@@ -364,7 +365,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
|
||||
try {
|
||||
const columns = await api.getColumns(tab.connectionId, tab.database, schema, analysis.tableName);
|
||||
const primaryKeys = columns.filter((c) => c.is_primary_key).map((c) => c.name);
|
||||
const primaryKeys = editablePrimaryKeys(dbType as DatabaseType, columns);
|
||||
|
||||
tab.tableMeta = {
|
||||
schema: schema || undefined,
|
||||
|
||||
@@ -29,6 +29,7 @@ export type DatabaseType =
|
||||
| "bigquery"
|
||||
| "kylin"
|
||||
| "sundb"
|
||||
| "tdengine"
|
||||
| "jdbc";
|
||||
|
||||
export interface ConnectionConfig {
|
||||
|
||||
Reference in New Issue
Block a user