Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
87b1b54
feat(data-modeling): implement automatic relationship inference algor…
gribnoysup Sep 3, 2025
909a424
Merge remote-tracking branch 'origin/main' into COMPASS-9776
gribnoysup Sep 5, 2025
83bbfff
chore(data-service, data-modeling): add proper support for indexes fu…
gribnoysup Sep 5, 2025
b34c3d8
Merge branch 'main' into COMPASS-9776
gribnoysup Sep 9, 2025
1510814
chore(data-modeling): add method description; add unit tests
gribnoysup Sep 9, 2025
17fef4a
chore(data-modeling): fix type in test
gribnoysup Sep 9, 2025
022bd0e
chore(data-modeling): filter out nullish values from the sample; fix …
gribnoysup Sep 9, 2025
508b96e
chore(data-modeling): more comments
gribnoysup Sep 9, 2025
768447d
Merge branch 'main' into COMPASS-9776
gribnoysup Sep 9, 2025
e40bd32
chore(data-modeling): adjust wording
gribnoysup Sep 10, 2025
d66be99
Merge remote-tracking branch 'origin/main' into COMPASS-9776
gribnoysup Sep 10, 2025
d6286a0
chore(data-modeling): do not allow multiple analysis to run at the sa…
gribnoysup Sep 10, 2025
1578994
fix(data-service): make sure that _getOptionsWithFallbackReadPreferen…
gribnoysup Sep 10, 2025
811d84e
chore(data-modeling): convert traverse to a generator function
gribnoysup Sep 10, 2025
2e953fa
chore(data-modeling): better comment
gribnoysup Sep 10, 2025
3da4089
chore(data-service): improve _getOptionsWithFallbackReadPreference types
gribnoysup Sep 11, 2025
c2c98ff
chore(data-modeling): do not filter out id fields with matching types…
gribnoysup Sep 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ type NewDiagramFormProps = {
collections: string[];
selectedCollections: string[];
error: Error | null;
analysisInProgress: boolean;

onCancel: () => void;
onNameChange: (name: string) => void;
Expand All @@ -224,6 +225,7 @@ const NewDiagramForm: React.FunctionComponent<NewDiagramFormProps> = ({
collections,
selectedCollections,
error,
analysisInProgress,
onCancel,
onNameChange,
onNameConfirm,
Expand Down Expand Up @@ -297,7 +299,9 @@ const NewDiagramForm: React.FunctionComponent<NewDiagramFormProps> = ({
onConfirmAction: onCollectionsSelectionConfirm,
confirmActionLabel: 'Generate',
isConfirmDisabled:
!selectedCollections || selectedCollections.length === 0,
!selectedCollections ||
selectedCollections.length === 0 ||
analysisInProgress,
onCancelAction: onDatabaseSelectCancel,
cancelLabel: 'Back',
footerText: (
Expand All @@ -312,19 +316,20 @@ const NewDiagramForm: React.FunctionComponent<NewDiagramFormProps> = ({
}
}, [
currentStep,
onNameConfirm,
diagramName,
onCancel,
onCollectionsSelectionConfirm,
onConnectionConfirmSelection,
onConnectionSelectCancel,
onDatabaseConfirmSelection,
onDatabaseSelectCancel,
onNameConfirm,
onNameConfirmCancel,
selectedCollections,
selectedConnectionId,
onNameConfirmCancel,
onDatabaseConfirmSelection,
selectedDatabase,
collections,
onConnectionSelectCancel,
collections.length,
onCollectionsSelectionConfirm,
selectedCollections,
analysisInProgress,
onDatabaseSelectCancel,
]);

const formContent = useMemo(() => {
Expand Down Expand Up @@ -509,6 +514,8 @@ export default connect(
collections: databaseCollections ?? [],
selectedCollections: selectedCollections ?? [],
error,
analysisInProgress:
state.analysisProgress.analysisProcessStatus === 'in-progress',
};
},
{
Expand Down
129 changes: 101 additions & 28 deletions packages/compass-data-modeling/src/store/analysis-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { isAction } from './util';
import type { DataModelingThunkAction } from './reducer';
import { analyzeDocuments, type MongoDBJSONSchema } from 'mongodb-schema';
import { getCurrentDiagramFromState } from './diagram';
import type { Document } from 'bson';
import type { AggregationCursor } from 'mongodb';
import { UUID } from 'bson';
import type { Relationship } from '../services/data-model-storage';
import { applyLayout } from '@mongodb-js/diagramming';
import { collectionToBaseNodeForLayout } from '../utils/nodes-and-edges';
import { inferForeignToLocalRelationshipsForCollection } from './relationships';
import { mongoLogId } from '@mongodb-js/compass-logging/provider';

export type AnalysisProcessState = {
currentAnalysisOptions:
Expand All @@ -18,9 +19,10 @@ export type AnalysisProcessState = {
collections: string[];
} & AnalysisOptions)
| null;
analysisProcessStatus: 'idle' | 'in-progress';
samplesFetched: number;
schemasAnalyzed: number;
relationsInferred: boolean;
relationsInferred: number;
};

export enum AnalysisProcessActionTypes {
Expand Down Expand Up @@ -58,6 +60,8 @@ export type NamespaceSchemaAnalyzedAction = {

export type NamespacesRelationsInferredAction = {
type: AnalysisProcessActionTypes.NAMESPACES_RELATIONS_INFERRED;
namespace: string;
count: number;
};

export type AnalysisFinishedAction = {
Expand Down Expand Up @@ -92,9 +96,10 @@ export type AnalysisProgressActions =

const INITIAL_STATE = {
currentAnalysisOptions: null,
analysisProcessStatus: 'idle' as const,
samplesFetched: 0,
schemasAnalyzed: 0,
relationsInferred: false,
relationsInferred: 0,
};

export const analysisProcessReducer: Reducer<AnalysisProcessState> = (
Expand All @@ -106,6 +111,7 @@ export const analysisProcessReducer: Reducer<AnalysisProcessState> = (
) {
return {
...INITIAL_STATE,
analysisProcessStatus: 'in-progress',
currentAnalysisOptions: {
name: action.name,
connectionId: action.connectionId,
Expand All @@ -127,6 +133,16 @@ export const analysisProcessReducer: Reducer<AnalysisProcessState> = (
schemasAnalyzed: state.schemasAnalyzed + 1,
};
}
if (
isAction(action, AnalysisProcessActionTypes.ANALYSIS_CANCELED) ||
isAction(action, AnalysisProcessActionTypes.ANALYSIS_FAILED) ||
isAction(action, AnalysisProcessActionTypes.ANALYSIS_FINISHED)
) {
return {
...state,
analysisProcessStatus: 'idle',
};
}
return state;
};

Expand All @@ -146,11 +162,26 @@ export function startAnalysis(
| AnalysisCanceledAction
| AnalysisFailedAction
> {
return async (dispatch, getState, services) => {
return async (
dispatch,
getState,
{
connections,
cancelAnalysisControllerRef,
logger,
track,
dataModelStorage,
preferences,
}
) => {
// Analysis is in progress, don't start a new one unless user canceled it
if (cancelAnalysisControllerRef.current) {
return;
}
const namespaces = collections.map((collName) => {
return `${database}.${collName}`;
});
const cancelController = (services.cancelAnalysisControllerRef.current =
const cancelController = (cancelAnalysisControllerRef.current =
new AbortController());
dispatch({
type: AnalysisProcessActionTypes.ANALYZING_COLLECTIONS_START,
Expand All @@ -161,18 +192,17 @@ export function startAnalysis(
options,
});
try {
const dataService =
services.connections.getDataServiceForConnection(connectionId);
let relations: Relationship[] = [];
const dataService = connections.getDataServiceForConnection(connectionId);

const collections = await Promise.all(
namespaces.map(async (ns) => {
const sample: AggregationCursor<Document> = dataService.sampleCursor(
const sample = await dataService.sample(
ns,
{ size: 100 },
{ promoteValues: false },
{
signal: cancelController.signal,
promoteValues: false,
},
{
abortSignal: cancelController.signal,
fallbackReadPreference: 'secondaryPreferred',
}
);
Expand All @@ -194,26 +224,71 @@ export function startAnalysis(
type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED,
namespace: ns,
});
return { ns, schema };
return { ns, schema, sample };
})
);

if (options.automaticallyInferRelations) {
// TODO
if (
preferences.getPreferences().enableAutomaticRelationshipInference &&
options.automaticallyInferRelations
) {
relations = (
await Promise.all(
collections.map(
async ({
ns,
schema,
sample,
}): Promise<Relationship['relationship'][]> => {
const relationships =
await inferForeignToLocalRelationshipsForCollection(
ns,
schema,
sample,
collections,
dataService,
cancelController.signal,
(err) => {
logger.log.warn(
mongoLogId(1_001_000_371),
'DataModeling',
'Failed to identify relationship for collection',
{ ns, error: err.message }
);
}
);
dispatch({
type: AnalysisProcessActionTypes.NAMESPACES_RELATIONS_INFERRED,
namespace: ns,
count: relationships.length,
});
return relationships;
}
)
)
).flatMap((relationships) => {
return relationships.map((relationship) => {
return {
id: new UUID().toHexString(),
relationship,
isInferred: true,
};
});
});
}

if (cancelController.signal.aborted) {
throw cancelController.signal.reason;
}

const positioned = await applyLayout(
collections.map((coll) =>
collectionToBaseNodeForLayout({
collections.map((coll) => {
return collectionToBaseNodeForLayout({
ns: coll.ns,
jsonSchema: coll.schema,
displayPosition: [0, 0],
})
),
});
}),
[],
'LEFT_RIGHT'
);
Expand All @@ -229,22 +304,20 @@ export function startAnalysis(
const position = node ? node.position : { x: 0, y: 0 };
return { ...coll, position };
}),
relations: [],
relations,
});

services.track('Data Modeling Diagram Created', {
track('Data Modeling Diagram Created', {
num_collections: collections.length,
});

void services.dataModelStorage.save(
getCurrentDiagramFromState(getState())
);
void dataModelStorage.save(getCurrentDiagramFromState(getState()));
} catch (err) {
if (cancelController.signal.aborted) {
dispatch({ type: AnalysisProcessActionTypes.ANALYSIS_CANCELED });
} else {
services.logger.log.error(
services.logger.mongoLogId(1_001_000_350),
logger.log.error(
mongoLogId(1_001_000_350),
'DataModeling',
'Failed to analyze schema',
{ err }
Expand All @@ -255,7 +328,7 @@ export function startAnalysis(
});
}
} finally {
services.cancelAnalysisControllerRef.current = null;
cancelAnalysisControllerRef.current = null;
}
};
}
Expand Down
Loading
Loading