mirror of
https://github.com/teableio/teable.git
synced 2026-02-04 20:46:48 +08:00
feat(v2): explain computed update reasons
This commit is contained in:
parent
848efcd238
commit
d970fa66d4
@ -32,6 +32,8 @@ interface ExplainResultPanelProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
type ComputedUpdateReason = NonNullable<IExplainResultDto['sqlExplains'][number]['computedReason']>;
|
||||
|
||||
function ComplexityScoreCard({ level, score }: { level: string; score: number }) {
|
||||
const config: Record<string, { bg: string; border: string; text: string; label: string }> = {
|
||||
trivial: {
|
||||
@ -149,6 +151,100 @@ function ExplainOutputBlock({
|
||||
);
|
||||
}
|
||||
|
||||
function ComputedReasonBlock({ reason }: { reason: ComputedUpdateReason }) {
|
||||
return (
|
||||
<div className="mt-4 rounded-md border bg-muted/40 p-3 text-xs space-y-3">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<GitBranch className="h-4 w-4" />
|
||||
<span className="font-medium">Computed Update Reason</span>
|
||||
<Badge variant="outline" className="text-[10px] h-4 px-1 uppercase">
|
||||
{reason.changeType}
|
||||
</Badge>
|
||||
</div>
|
||||
{reason.notes.length > 0 && (
|
||||
<div className="text-muted-foreground">{reason.notes.join(' ')}</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-[11px] font-medium text-muted-foreground mb-1">Triggered By</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{reason.seedFields.length > 0 ? (
|
||||
reason.seedFields.map((seed) => {
|
||||
const Icon = getFieldTypeIcon(seed.fieldType);
|
||||
return (
|
||||
<div
|
||||
key={seed.fieldId}
|
||||
className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-background border text-[11px]"
|
||||
title={`${seed.tableName} · ${seed.fieldType}`}
|
||||
>
|
||||
<Icon className="h-3 w-3 text-muted-foreground shrink-0" />
|
||||
<span className="font-medium">{seed.fieldName}</span>
|
||||
<span className="text-muted-foreground">({seed.fieldType})</span>
|
||||
<Badge variant="secondary" className="text-[10px] h-4 px-1">
|
||||
{seed.impact === 'link_relation' ? 'link' : 'value'}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<span className="text-muted-foreground">No seed fields</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[11px] font-medium text-muted-foreground mb-1">Updates</div>
|
||||
<div className="space-y-2">
|
||||
{reason.targetFields.length > 0 ? (
|
||||
reason.targetFields.map((target) => {
|
||||
const Icon = getFieldTypeIcon(target.fieldType);
|
||||
return (
|
||||
<div key={target.fieldId} className="rounded-md border bg-background px-2 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="font-medium">{target.fieldName}</span>
|
||||
<span className="text-muted-foreground">({target.fieldType})</span>
|
||||
</div>
|
||||
<div className="mt-1 space-y-1 text-[11px]">
|
||||
{target.dependencies.length > 0 ? (
|
||||
target.dependencies.map((dep, index) => (
|
||||
<div
|
||||
key={`${dep.fromFieldId}-${index}`}
|
||||
className="flex flex-wrap items-center gap-1.5 text-muted-foreground"
|
||||
>
|
||||
<span className="font-medium text-foreground">
|
||||
{dep.fromTableName}.{dep.fromFieldName}
|
||||
</span>
|
||||
<span>({dep.fromFieldType})</span>
|
||||
<Badge variant="outline" className="text-[10px] h-4 px-1">
|
||||
{dep.kind}
|
||||
</Badge>
|
||||
{dep.semantic && (
|
||||
<Badge variant="outline" className="text-[10px] h-4 px-1">
|
||||
{dep.semantic}
|
||||
</Badge>
|
||||
)}
|
||||
{dep.isSeed && (
|
||||
<Badge variant="secondary" className="text-[10px] h-4 px-1">
|
||||
seed
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">No direct dependencies</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<span className="text-muted-foreground">No computed targets</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
icon: Icon,
|
||||
label,
|
||||
@ -584,6 +680,9 @@ export function ExplainResultPanel({ result, className }: ExplainResultPanelProp
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{sqlInfo.computedReason && (
|
||||
<ComputedReasonBlock reason={sqlInfo.computedReason} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -45,6 +45,7 @@ import { DEFAULT_EXPLAIN_OPTIONS } from '../types';
|
||||
import { v2CommandExplainTokens } from '../di/tokens';
|
||||
import { SqlExplainRunner } from '../utils/SqlExplainRunner';
|
||||
import { ComplexityCalculator } from '../utils/ComplexityCalculator';
|
||||
import { buildComputedUpdateReason } from '../utils/ComputedUpdateReasonBuilder';
|
||||
|
||||
/**
|
||||
* Analyzer for CreateRecordCommand.
|
||||
@ -266,6 +267,14 @@ export class CreateRecordAnalyzer implements ICommandAnalyzer<CreateRecordComman
|
||||
for (const step of batch.steps) {
|
||||
batchFieldIds.push(...step.fieldIds);
|
||||
}
|
||||
const computedReason = buildComputedUpdateReason({
|
||||
plan,
|
||||
graphData,
|
||||
tableById,
|
||||
changedFieldIds,
|
||||
targetFieldIds: batchFieldIds,
|
||||
changeType: plan.changeType,
|
||||
});
|
||||
|
||||
// Get batch table name
|
||||
const batchTableNameResult = batchTable.dbTableName();
|
||||
@ -290,6 +299,7 @@ export class CreateRecordAnalyzer implements ICommandAnalyzer<CreateRecordComman
|
||||
parameters: [],
|
||||
explainAnalyze: null,
|
||||
explainOnly: null,
|
||||
computedReason,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@ -302,6 +312,7 @@ export class CreateRecordAnalyzer implements ICommandAnalyzer<CreateRecordComman
|
||||
parameters: [],
|
||||
explainAnalyze: null,
|
||||
explainOnly: null,
|
||||
computedReason,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@ -322,6 +333,7 @@ export class CreateRecordAnalyzer implements ICommandAnalyzer<CreateRecordComman
|
||||
parameters: [],
|
||||
explainAnalyze: null,
|
||||
explainOnly: null,
|
||||
computedReason,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@ -372,6 +384,7 @@ export class CreateRecordAnalyzer implements ICommandAnalyzer<CreateRecordComman
|
||||
parameters: compiled.parameters as unknown[],
|
||||
explainAnalyze,
|
||||
explainOnly,
|
||||
computedReason,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,6 +43,7 @@ import { DEFAULT_EXPLAIN_OPTIONS } from '../types';
|
||||
import { v2CommandExplainTokens } from '../di/tokens';
|
||||
import { SqlExplainRunner } from '../utils/SqlExplainRunner';
|
||||
import { ComplexityCalculator } from '../utils/ComplexityCalculator';
|
||||
import { buildComputedUpdateReason } from '../utils/ComputedUpdateReasonBuilder';
|
||||
|
||||
/**
|
||||
* Analyzer for DeleteRecordsCommand.
|
||||
@ -112,6 +113,7 @@ export class DeleteRecordsAnalyzer implements ICommandAnalyzer<DeleteRecordsComm
|
||||
let computedImpact: ComputedImpactInfo;
|
||||
let plan: ComputedUpdatePlan | null = null;
|
||||
let tableById: Map<string, Table> | null = null;
|
||||
let graphData: FieldDependencyGraphData | null = null;
|
||||
|
||||
if (linkFieldIds.length > 0) {
|
||||
plan = yield* await analyzer.planner.plan({
|
||||
@ -122,7 +124,7 @@ export class DeleteRecordsAnalyzer implements ICommandAnalyzer<DeleteRecordsComm
|
||||
});
|
||||
planningMs = Date.now() - graphStartTime;
|
||||
|
||||
const graphData = yield* await analyzer.dependencyGraph.load(table.baseId());
|
||||
graphData = yield* await analyzer.dependencyGraph.load(table.baseId());
|
||||
dependencyGraphMs = Date.now() - graphStartTime;
|
||||
|
||||
// Load tables for name resolution
|
||||
@ -136,7 +138,7 @@ export class DeleteRecordsAnalyzer implements ICommandAnalyzer<DeleteRecordsComm
|
||||
mergedOptions
|
||||
);
|
||||
} else {
|
||||
const graphData = yield* await analyzer.dependencyGraph.load(table.baseId());
|
||||
graphData = yield* await analyzer.dependencyGraph.load(table.baseId());
|
||||
dependencyGraphMs = Date.now() - graphStartTime;
|
||||
planningMs = dependencyGraphMs;
|
||||
|
||||
@ -192,7 +194,7 @@ export class DeleteRecordsAnalyzer implements ICommandAnalyzer<DeleteRecordsComm
|
||||
});
|
||||
|
||||
// Generate SQL for computed field updates on linked tables
|
||||
if (plan && plan.sameTableBatches.length > 0 && tableById) {
|
||||
if (plan && plan.sameTableBatches.length > 0 && tableById && graphData) {
|
||||
for (let i = 0; i < plan.sameTableBatches.length; i++) {
|
||||
const batch = plan.sameTableBatches[i];
|
||||
const batchTable = tableById.get(batch.tableId.toString());
|
||||
@ -206,6 +208,14 @@ export class DeleteRecordsAnalyzer implements ICommandAnalyzer<DeleteRecordsComm
|
||||
for (const step of batch.steps) {
|
||||
batchFieldIds.push(...step.fieldIds);
|
||||
}
|
||||
const computedReason = buildComputedUpdateReason({
|
||||
plan,
|
||||
graphData,
|
||||
tableById,
|
||||
changedFieldIds: linkFieldIds,
|
||||
targetFieldIds: batchFieldIds,
|
||||
changeType: plan.changeType,
|
||||
});
|
||||
|
||||
// Get batch table name
|
||||
const batchTableNameResult = batchTable.dbTableName();
|
||||
@ -230,6 +240,7 @@ export class DeleteRecordsAnalyzer implements ICommandAnalyzer<DeleteRecordsComm
|
||||
parameters: [],
|
||||
explainAnalyze: null,
|
||||
explainOnly: null,
|
||||
computedReason,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@ -242,6 +253,7 @@ export class DeleteRecordsAnalyzer implements ICommandAnalyzer<DeleteRecordsComm
|
||||
parameters: [],
|
||||
explainAnalyze: null,
|
||||
explainOnly: null,
|
||||
computedReason,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@ -262,6 +274,7 @@ export class DeleteRecordsAnalyzer implements ICommandAnalyzer<DeleteRecordsComm
|
||||
parameters: [],
|
||||
explainAnalyze: null,
|
||||
explainOnly: null,
|
||||
computedReason,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@ -312,6 +325,7 @@ export class DeleteRecordsAnalyzer implements ICommandAnalyzer<DeleteRecordsComm
|
||||
parameters: compiled.parameters as unknown[],
|
||||
explainAnalyze,
|
||||
explainOnly,
|
||||
computedReason,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,6 +44,7 @@ import { DEFAULT_EXPLAIN_OPTIONS } from '../types';
|
||||
import { v2CommandExplainTokens } from '../di/tokens';
|
||||
import { SqlExplainRunner } from '../utils/SqlExplainRunner';
|
||||
import { ComplexityCalculator } from '../utils/ComplexityCalculator';
|
||||
import { buildComputedUpdateReason } from '../utils/ComputedUpdateReasonBuilder';
|
||||
|
||||
/**
|
||||
* Analyzer for UpdateRecordCommand.
|
||||
@ -275,6 +276,14 @@ export class UpdateRecordAnalyzer implements ICommandAnalyzer<UpdateRecordComman
|
||||
for (const step of batch.steps) {
|
||||
batchFieldIds.push(...step.fieldIds);
|
||||
}
|
||||
const computedReason = buildComputedUpdateReason({
|
||||
plan,
|
||||
graphData,
|
||||
tableById,
|
||||
changedFieldIds,
|
||||
targetFieldIds: batchFieldIds,
|
||||
changeType: plan.changeType,
|
||||
});
|
||||
|
||||
// Get batch table name
|
||||
const batchTableNameResult = batchTable.dbTableName();
|
||||
@ -300,6 +309,7 @@ export class UpdateRecordAnalyzer implements ICommandAnalyzer<UpdateRecordComman
|
||||
parameters: [],
|
||||
explainAnalyze: null,
|
||||
explainOnly: null,
|
||||
computedReason,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@ -312,6 +322,7 @@ export class UpdateRecordAnalyzer implements ICommandAnalyzer<UpdateRecordComman
|
||||
parameters: [],
|
||||
explainAnalyze: null,
|
||||
explainOnly: null,
|
||||
computedReason,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@ -332,6 +343,7 @@ export class UpdateRecordAnalyzer implements ICommandAnalyzer<UpdateRecordComman
|
||||
parameters: [],
|
||||
explainAnalyze: null,
|
||||
explainOnly: null,
|
||||
computedReason,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@ -382,6 +394,7 @@ export class UpdateRecordAnalyzer implements ICommandAnalyzer<UpdateRecordComman
|
||||
parameters: compiled.parameters as unknown[],
|
||||
explainAnalyze,
|
||||
explainOnly,
|
||||
computedReason,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,6 +110,55 @@ export type ExplainAnalyzeOutput = {
|
||||
readonly estimatedRows?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Seed field that triggered computed updates.
|
||||
*/
|
||||
export type ComputedUpdateSeedField = {
|
||||
readonly fieldId: string;
|
||||
readonly fieldName: string;
|
||||
readonly fieldType: string;
|
||||
readonly tableId: string;
|
||||
readonly tableName: string;
|
||||
readonly impact: 'value' | 'link_relation';
|
||||
};
|
||||
|
||||
/**
|
||||
* Direct dependency of a computed field.
|
||||
*/
|
||||
export type ComputedUpdateDependency = {
|
||||
readonly fromFieldId: string;
|
||||
readonly fromFieldName: string;
|
||||
readonly fromFieldType: string;
|
||||
readonly fromTableId: string;
|
||||
readonly fromTableName: string;
|
||||
readonly kind: 'same_record' | 'cross_record';
|
||||
readonly semantic?: string;
|
||||
readonly linkFieldId?: string;
|
||||
readonly isSeed: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Computed field updated in a batch, including dependencies.
|
||||
*/
|
||||
export type ComputedUpdateTargetField = {
|
||||
readonly fieldId: string;
|
||||
readonly fieldName: string;
|
||||
readonly fieldType: string;
|
||||
readonly tableId: string;
|
||||
readonly tableName: string;
|
||||
readonly dependencies: ReadonlyArray<ComputedUpdateDependency>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reason details for computed update batches.
|
||||
*/
|
||||
export type ComputedUpdateReason = {
|
||||
readonly changeType: 'insert' | 'update' | 'delete';
|
||||
readonly seedFields: ReadonlyArray<ComputedUpdateSeedField>;
|
||||
readonly targetFields: ReadonlyArray<ComputedUpdateTargetField>;
|
||||
readonly notes: ReadonlyArray<string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* SQL explain information for a single step.
|
||||
*/
|
||||
@ -119,6 +168,7 @@ export type SqlExplainInfo = {
|
||||
readonly parameters: ReadonlyArray<unknown>;
|
||||
readonly explainAnalyze: ExplainAnalyzeOutput | null;
|
||||
readonly explainOnly: ExplainOutput | null;
|
||||
readonly computedReason?: ComputedUpdateReason;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -0,0 +1,158 @@
|
||||
import type {
|
||||
ComputedUpdatePlan,
|
||||
FieldDependencyGraphData,
|
||||
} from '@teable/v2-adapter-table-repository-postgres';
|
||||
import type { FieldId, Table, TableId } from '@teable/v2-core';
|
||||
|
||||
import type {
|
||||
ComputedUpdateReason,
|
||||
ComputedUpdateSeedField,
|
||||
ComputedUpdateTargetField,
|
||||
ComputedUpdateDependency,
|
||||
} from '../types';
|
||||
|
||||
type BuildComputedUpdateReasonParams = {
|
||||
plan: ComputedUpdatePlan;
|
||||
graphData: FieldDependencyGraphData;
|
||||
tableById: Map<string, Table>;
|
||||
changedFieldIds: ReadonlyArray<FieldId>;
|
||||
targetFieldIds: ReadonlyArray<FieldId>;
|
||||
changeType: ComputedUpdatePlan['changeType'];
|
||||
};
|
||||
|
||||
export const buildComputedUpdateReason = (
|
||||
params: BuildComputedUpdateReasonParams
|
||||
): ComputedUpdateReason => {
|
||||
const { plan, graphData, tableById, changedFieldIds, targetFieldIds, changeType } = params;
|
||||
const { fieldsById, edges } = graphData;
|
||||
|
||||
const resolveTableName = (tableId: TableId): string => {
|
||||
const table = tableById.get(tableId.toString());
|
||||
return table ? table.name().toString() : tableId.toString();
|
||||
};
|
||||
|
||||
const resolveFieldName = (tableId: TableId, fieldId: FieldId): string => {
|
||||
const table = tableById.get(tableId.toString());
|
||||
if (table) {
|
||||
const fieldResult = table.getField((f) => f.id().equals(fieldId));
|
||||
if (fieldResult.isOk()) {
|
||||
return fieldResult.value.name().toString();
|
||||
}
|
||||
}
|
||||
return fieldId.toString();
|
||||
};
|
||||
|
||||
const resolveFieldType = (fieldId: FieldId): string =>
|
||||
fieldsById.get(fieldId.toString())?.type ?? 'unknown';
|
||||
|
||||
const seedFieldIdSet = new Set(changedFieldIds.map((id) => id.toString()));
|
||||
const targetFieldIdSet = new Set(targetFieldIds.map((id) => id.toString()));
|
||||
|
||||
const edgesByTarget = new Map<string, Array<(typeof edges)[number]>>();
|
||||
for (const edge of edges) {
|
||||
const targetId = edge.toFieldId.toString();
|
||||
if (!targetFieldIdSet.has(targetId)) {
|
||||
continue;
|
||||
}
|
||||
const existing = edgesByTarget.get(targetId) ?? [];
|
||||
existing.push(edge);
|
||||
edgesByTarget.set(targetId, existing);
|
||||
}
|
||||
|
||||
const pickPreferredEdges = (
|
||||
targetEdges: ReadonlyArray<(typeof edges)[number]>
|
||||
): ReadonlyArray<(typeof edges)[number]> => {
|
||||
const preferredSemanticGroups = new Set<string>();
|
||||
for (const edge of targetEdges) {
|
||||
if (edge.semantic && edge.semantic !== 'formula_ref') {
|
||||
preferredSemanticGroups.add(`${edge.fromFieldId.toString()}|${edge.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = targetEdges.filter((edge) => {
|
||||
if (edge.semantic !== 'formula_ref') return true;
|
||||
const key = `${edge.fromFieldId.toString()}|${edge.kind}`;
|
||||
return !preferredSemanticGroups.has(key);
|
||||
});
|
||||
|
||||
const deduped = new Map<string, (typeof edges)[number]>();
|
||||
for (const edge of filtered) {
|
||||
const linkKey = edge.linkFieldId?.toString() ?? '';
|
||||
const key = `${edge.fromFieldId.toString()}|${edge.kind}|${linkKey}`;
|
||||
if (!deduped.has(key)) {
|
||||
deduped.set(key, edge);
|
||||
}
|
||||
}
|
||||
|
||||
return [...deduped.values()];
|
||||
};
|
||||
|
||||
const targetFields: ComputedUpdateTargetField[] = targetFieldIds.map((fieldId) => {
|
||||
const meta = fieldsById.get(fieldId.toString());
|
||||
const tableId = meta?.tableId ?? plan.seedTableId;
|
||||
const dependencies: ComputedUpdateDependency[] = pickPreferredEdges(
|
||||
edgesByTarget.get(fieldId.toString()) ?? []
|
||||
).map((edge) => ({
|
||||
fromFieldId: edge.fromFieldId.toString(),
|
||||
fromFieldName: resolveFieldName(edge.fromTableId, edge.fromFieldId),
|
||||
fromFieldType: resolveFieldType(edge.fromFieldId),
|
||||
fromTableId: edge.fromTableId.toString(),
|
||||
fromTableName: resolveTableName(edge.fromTableId),
|
||||
kind: edge.kind,
|
||||
semantic: edge.semantic,
|
||||
linkFieldId: edge.linkFieldId?.toString(),
|
||||
isSeed: seedFieldIdSet.has(edge.fromFieldId.toString()),
|
||||
}));
|
||||
|
||||
return {
|
||||
fieldId: fieldId.toString(),
|
||||
fieldName: resolveFieldName(tableId, fieldId),
|
||||
fieldType: meta?.type ?? 'unknown',
|
||||
tableId: tableId.toString(),
|
||||
tableName: resolveTableName(tableId),
|
||||
dependencies,
|
||||
};
|
||||
});
|
||||
|
||||
const seededDependencyIds = new Set<string>();
|
||||
for (const target of targetFields) {
|
||||
for (const dep of target.dependencies) {
|
||||
if (dep.isSeed) {
|
||||
seededDependencyIds.add(dep.fromFieldId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const seedFields: ComputedUpdateSeedField[] = changedFieldIds
|
||||
.filter((fieldId) => seededDependencyIds.has(fieldId.toString()))
|
||||
.map((fieldId) => {
|
||||
const meta = fieldsById.get(fieldId.toString());
|
||||
const tableId = meta?.tableId ?? plan.seedTableId;
|
||||
return {
|
||||
fieldId: fieldId.toString(),
|
||||
fieldName: resolveFieldName(tableId, fieldId),
|
||||
fieldType: meta?.type ?? 'unknown',
|
||||
tableId: tableId.toString(),
|
||||
tableName: resolveTableName(tableId),
|
||||
impact: meta?.type === 'link' ? 'link_relation' : 'value',
|
||||
};
|
||||
});
|
||||
|
||||
const notes: string[] = [];
|
||||
if (changeType === 'insert') {
|
||||
notes.push('Insert computes stored computed fields in the seed table for initial values.');
|
||||
}
|
||||
if (changeType === 'delete') {
|
||||
notes.push('Delete recomputes computed fields impacted by link relation changes.');
|
||||
}
|
||||
if (seedFields.length === 0 && changeType === 'update') {
|
||||
notes.push('No changed fields directly matched dependency edges for this batch.');
|
||||
}
|
||||
|
||||
return {
|
||||
changeType,
|
||||
seedFields,
|
||||
targetFields,
|
||||
notes,
|
||||
};
|
||||
};
|
||||
@ -54,12 +54,60 @@ const explainAnalyzeOutputSchema = z.object({
|
||||
estimatedRows: z.number().optional(),
|
||||
});
|
||||
|
||||
const computedUpdateSeedFieldSchema = z.object({
|
||||
fieldId: z.string(),
|
||||
fieldName: z.string(),
|
||||
fieldType: z.string(),
|
||||
tableId: z.string(),
|
||||
tableName: z.string(),
|
||||
impact: z.enum(['value', 'link_relation']),
|
||||
});
|
||||
|
||||
const computedUpdateDependencySchema = z.object({
|
||||
fromFieldId: z.string(),
|
||||
fromFieldName: z.string(),
|
||||
fromFieldType: z.string(),
|
||||
fromTableId: z.string(),
|
||||
fromTableName: z.string(),
|
||||
kind: z.enum(['same_record', 'cross_record']),
|
||||
semantic: z
|
||||
.enum([
|
||||
'formula_ref',
|
||||
'lookup_source',
|
||||
'lookup_link',
|
||||
'link_title',
|
||||
'rollup_source',
|
||||
'conditional_rollup_source',
|
||||
'conditional_lookup_source',
|
||||
])
|
||||
.optional(),
|
||||
linkFieldId: z.string().optional(),
|
||||
isSeed: z.boolean(),
|
||||
});
|
||||
|
||||
const computedUpdateTargetFieldSchema = z.object({
|
||||
fieldId: z.string(),
|
||||
fieldName: z.string(),
|
||||
fieldType: z.string(),
|
||||
tableId: z.string(),
|
||||
tableName: z.string(),
|
||||
dependencies: z.array(computedUpdateDependencySchema),
|
||||
});
|
||||
|
||||
const computedUpdateReasonSchema = z.object({
|
||||
changeType: z.enum(['insert', 'update', 'delete']),
|
||||
seedFields: z.array(computedUpdateSeedFieldSchema),
|
||||
targetFields: z.array(computedUpdateTargetFieldSchema),
|
||||
notes: z.array(z.string()),
|
||||
});
|
||||
|
||||
const sqlExplainInfoSchema = z.object({
|
||||
stepDescription: z.string(),
|
||||
sql: z.string(),
|
||||
parameters: z.array(z.unknown()),
|
||||
explainAnalyze: explainAnalyzeOutputSchema.nullable(),
|
||||
explainOnly: explainOutputSchema.nullable(),
|
||||
computedReason: computedUpdateReasonSchema.optional(),
|
||||
});
|
||||
|
||||
const dependencyEdgeInfoSchema = z.object({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user