fix(generate): honor explicit unique value generation

Refs: #5958
This commit is contained in:
t8y2 committed 2026-08-12 19:05:41 +00:00
1 parent a740e2e9ce
commit bd83ee189b
11 files changed
+263 -27

No files matched your search

@@ -4,7 +4,7 @@ import { useI18n } from "vue-i18n";
import { useConnectionStore } from "@/stores/connectionStore";
import * as api from "@/lib/backend/api";
import type { GenerateResult, TableGenerateConfig } from "@/lib/dataGrid/dataGenerate";
import { displayGeneratedValue, findGeneratorKey, formatGeneratedValue, generateTableData, defaultGeneratorParams, supportsGeneratedMultiRowValues } from "@/lib/dataGrid/dataGenerate";
import { displayGeneratedValue, findGeneratorKey, formatGeneratedValue, generateTableData, defaultGeneratorParams, supportsGeneratedMultiRowValues, UniqueValueGenerationError } from "@/lib/dataGrid/dataGenerate";
import { qualifiedTableName, quoteTableIdentifier } from "@/lib/table/tableSelectSql";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
@@ -61,6 +61,7 @@ interface GeneratedTableResult extends GenerateResult {
schema: string;
}
const generatedResults = ref<GeneratedTableResult[]>([]);
const generationError = ref("");
const connectionName = computed(() => (props.prefillConnectionId ? store.getConfig(props.prefillConnectionId)?.name : ""));
@@ -396,25 +397,31 @@ async function fetchMaxValues(cfg: TableGenerateConfig): Promise<Record<string,
async function doGenerate() {
if (!Object.values(checkedTables).some(Boolean)) return;
generationError.value = "";
const results: GeneratedTableResult[] = [];
const order = tableOrder.value.length > 0 ? tableOrder.value : Object.keys(configs);
for (const key of order) {
if (!checkedTables[key]) continue;
const cfg = configs[key];
let columns = cfg.columns.filter((col) => checkedColumns[colKey(cfg.schema, cfg.tableName, col.columnName)] !== false);
if (columns.length === 0) continue;
const aiStarts = await fetchMaxValues(cfg);
if (Object.keys(aiStarts).length > 0) {
columns = columns.map((col) => {
const start = aiStarts[col.columnName];
if (start !== undefined) {
return { ...col, generatorKey: "sequence", generatorParams: { startValue: start, increment: 1 } };
}
return col;
});
try {
const order = tableOrder.value.length > 0 ? tableOrder.value : Object.keys(configs);
for (const key of order) {
if (!checkedTables[key]) continue;
const cfg = configs[key];
let columns = cfg.columns.filter((col) => checkedColumns[colKey(cfg.schema, cfg.tableName, col.columnName)] !== false);
if (columns.length === 0) continue;
const aiStarts = await fetchMaxValues(cfg);
if (Object.keys(aiStarts).length > 0) {
columns = columns.map((col) => {
const start = aiStarts[col.columnName];
if (start !== undefined) {
return { ...col, generatorKey: "sequence", generatorParams: { startValue: start, increment: 1 } };
}
return col;
});
}
const result = generateTableData({ ...cfg, columns }, dbType.value);
results.push({ tableName: cfg.tableName, schema: cfg.schema, ...result });
}
const result = generateTableData({ ...cfg, columns }, dbType.value);
results.push({ tableName: cfg.tableName, schema: cfg.schema, ...result });
} catch (error) {
generationError.value = generationErrorMessage(error);
return;
}
generatedResults.value = results;
previewTableIndex.value = 0;
@@ -438,8 +445,20 @@ async function regenerate() {
return col;
});
}
const result = generateTableData({ ...cfg, columns }, dbType.value);
generatedResults.value[previewTableIndex.value] = { tableName: cfg.tableName, schema: cfg.schema, ...result };
generationError.value = "";
try {
const result = generateTableData({ ...cfg, columns }, dbType.value);
generatedResults.value[previewTableIndex.value] = { tableName: cfg.tableName, schema: cfg.schema, ...result };
} catch (error) {
generationError.value = generationErrorMessage(error);
}
}
function generationErrorMessage(error: unknown): string {
if (error instanceof UniqueValueGenerationError) {
return t("dataGenerate.uniqueExhausted", { table: error.tableName, column: error.columnName, attempts: error.attempts });
}
return error instanceof Error ? error.message : String(error);
}
function copyAllSql() {
@@ -498,7 +517,7 @@ function allSqlStatements(): string[] {
}
async function startInsert() {
if (executing.value) return;
if (executing.value || generationError.value) return;
const cid = props.prefillConnectionId;
const db = props.prefillDatabase;
if (!cid || !db) return;
@@ -807,6 +826,8 @@ async function onFileSelected(event: Event) {
<span class="ml-1 font-medium">{{ connectionName || props.prefillDatabase }}</span>
</div>
<div v-if="generationError" class="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive" role="alert">{{ generationError }}</div>
<template v-if="currentStep === 'config'">
<div class="flex gap-4 min-h-[400px]">
<!-- left: schema tree -->
@@ -1024,7 +1045,7 @@ async function onFileSelected(event: Event) {
</template>
<template v-else-if="currentStep === 'preview'">
<Button variant="outline" size="sm" class="h-7 text-xs" @click="currentStep = 'config'">{{ t("dataGenerate.prevStep") }}</Button>
<Button variant="default" size="sm" class="h-7 text-xs" :disabled="executing" @click="startInsert">
<Button variant="default" size="sm" class="h-7 text-xs" :disabled="executing || !!generationError" @click="startInsert">
<Loader2 v-if="executing" class="mr-1 h-3 w-3 animate-spin" />
{{ t("dataGenerate.startInsert") }}
</Button>
+1
View File
@@ -4819,6 +4819,7 @@ export default {
includeDefault: "Include DEFAULT",
includeNull: "Include NULL",
unique: "Set unique",
uniqueExhausted: "Could not generate a distinct value for {table}.{column} within {attempts} attempts. Increase the available value range or generate fewer rows.",
disableLinks: "Disable data links between columns",
imageMode: "Mode",
generate: "Generate",
+1
View File
@@ -4600,6 +4600,7 @@ export default withEnglishFallback({
includeDefault: "Include DEFAULT",
includeNull: "Include NULL",
unique: "Set unique",
uniqueExhausted: "No se pudo generar un valor distinto para {table}.{column} en {attempts} intentos. Amplía el rango de valores o genera menos filas.",
disableLinks: "Disable data links between columns",
imageMode: "Mode",
generate: "Generate",
+1
View File
@@ -4598,6 +4598,7 @@ export default withEnglishFallback({
includeDefault: "Include DEFAULT",
includeNull: "Include NULL",
unique: "Set unique",
uniqueExhausted: "Impossibile generare un valore distinto per {table}.{column} in {attempts} tentativi. Amplia l'intervallo dei valori o genera meno righe.",
disableLinks: "Disable data links between columns",
imageMode: "Mode",
generate: "Generate",
+1
View File
@@ -4637,6 +4637,7 @@ export default withEnglishFallback({
includeDefault: "DEFAULTを含める",
includeNull: "NULLを含める",
unique: "一意に設定",
uniqueExhausted: "{attempts} 回の試行で {table}.{column} の重複しない値を生成できませんでした。値の範囲を広げるか、生成行数を減らしてください。",
disableLinks: "列間のデータリンクを無効化",
imageMode: "モード",
generate: "生成",
+1
View File
@@ -4318,6 +4318,7 @@ export default withEnglishFallback({
includeDefault: "DEFAULT 포함",
includeNull: "NULL 포함",
unique: "고유 설정",
uniqueExhausted: "{attempts}번 시도했지만 {table}.{column}의 중복되지 않는 값을 생성하지 못했습니다. 값 범위를 늘리거나 생성 행 수를 줄이세요.",
disableLinks: "컬럼 간 데이터 연결 비활성화",
imageMode: "모드",
generate: "생성",
+1
View File
@@ -4600,6 +4600,7 @@ export default withEnglishFallback({
includeDefault: "Include DEFAULT",
includeNull: "Include NULL",
unique: "Set unique",
uniqueExhausted: "Não foi possível gerar um valor distinto para {table}.{column} em {attempts} tentativas. Aumente o intervalo de valores ou gere menos linhas.",
disableLinks: "Disable data links between columns",
imageMode: "Mode",
generate: "Generate",
+1
View File
@@ -4742,6 +4742,7 @@ export default withEnglishFallback({
includeDefault: "包含默认值",
includeNull: "包含 NULL",
unique: "设置唯一",
uniqueExhausted: "无法在 {attempts} 次尝试内为表 {table} 的字段 {column} 生成不重复值,请增大可选值范围或减少生成行数。",
disableLinks: "禁用字段之间的数据链接",
imageMode: "模式",
generate: "生成",
+1
View File
@@ -3945,6 +3945,7 @@ export default withEnglishFallback({
includeDefault: "包含預設值",
includeNull: "包含 NULL",
unique: "設定唯一",
uniqueExhausted: "無法在 {attempts} 次嘗試內為資料表 {table} 的欄位 {column} 產生不重複值,請增加可選值範圍或減少產生列數。",
disableLinks: "禁用欄位之間的資料連結",
imageMode: "模式",
generate: "生成",
+44 -2
View File
@@ -1774,6 +1774,29 @@ export interface GenerateResult {
statements: string[];
}
const MAX_UNIQUE_GENERATION_ATTEMPTS = 100;
export class UniqueValueGenerationError extends Error {
constructor(
public readonly tableName: string,
public readonly columnName: string,
public readonly attempts: number,
) {
super(`Unable to generate a unique value for column "${columnName}" in table "${tableName}" after ${attempts} attempts.`);
this.name = "UniqueValueGenerationError";
}
}
function generatedValueIdentity(value: unknown): string {
if (isGeneratedSqlExpression(value)) return `sql:${value.sql}`;
if (value === null) return "null";
if (value === undefined) return "undefined";
if (typeof value === "number") return `number:${value}`;
if (typeof value === "boolean") return `boolean:${value}`;
if (typeof value === "string") return `string:${value}`;
return `${typeof value}:${JSON.stringify(value)}`;
}
export function supportsGeneratedMultiRowValues(databaseType?: DatabaseType): boolean {
return databaseType !== "oracle" && databaseType !== "oceanbase-oracle" && databaseType !== "iris";
}
@@ -1810,15 +1833,34 @@ export function generateTableData(config: TableGenerateConfig, databaseType?: Da
const shouldAddTbname = isTdengineStable && !hasTbnameColumn;
const tdengineChildTableName = shouldAddTbname ? generateTdengineChildTableName() : null;
const tagValues = new Map<string, unknown>();
const uniqueValues = config.columns.map((column) => (column.generatorParams?.unique ? new Set<string>() : null));
const colNames = shouldAddTbname ? ["tbname", ...config.columns.map((c) => c.columnName)] : config.columns.map((c) => c.columnName);
const rows: unknown[][] = [];
for (let i = 0; i < config.rowCount; i++) {
const row = config.columns.map((col) => {
const row = config.columns.map((col, columnIndex) => {
if (isTdengineStable && col.isTag && tagValues.has(col.columnName)) {
return tagValues.get(col.columnName);
}
const value = generateValue(col.columnName, col.dataType, col.generatorKey, i, col.generatorParams, col.isAutoIncrement ? null : col.columnDefault);
let value: unknown;
if (col.generatorParams?.unique) {
const seen = uniqueValues[columnIndex]!;
let found = false;
for (let attempt = 0; attempt < MAX_UNIQUE_GENERATION_ATTEMPTS; attempt++) {
value = generateValue(col.columnName, col.dataType, col.generatorKey, i, col.generatorParams, col.isAutoIncrement ? null : col.columnDefault);
const identity = generatedValueIdentity(value);
if (!seen.has(identity)) {
seen.add(identity);
found = true;
break;
}
}
if (!found) {
throw new UniqueValueGenerationError(config.tableName, col.columnName, MAX_UNIQUE_GENERATION_ATTEMPTS);
}
} else {
value = generateValue(col.columnName, col.dataType, col.generatorKey, i, col.generatorParams, col.isAutoIncrement ? null : col.columnDefault);
}
if (isTdengineStable && col.isTag) {
tagValues.set(col.columnName, value);
}