Skip to content

Commit 9b16160

Browse files
committed
feat(data-modeling): automatically infer relationships from indexes and object ids
1 parent 9b7ff5b commit 9b16160

File tree

4 files changed

+310
-31
lines changed

4 files changed

+310
-31
lines changed

packages/compass-data-modeling/src/store/analysis-process.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { isAction } from './util';
33
import type { DataModelingThunkAction } from './reducer';
44
import { analyzeDocuments, type MongoDBJSONSchema } from 'mongodb-schema';
55
import { getCurrentDiagramFromState } from './diagram';
6-
import type { Document } from 'bson';
7-
import type { AggregationCursor } from 'mongodb';
6+
import { UUID } from 'bson';
87
import type { Relationship } from '../services/data-model-storage';
98
import { applyLayout } from '@mongodb-js/diagramming';
109
import { collectionToDiagramNode } from '../utils/nodes-and-edges';
10+
import { inferLocalToForeignRelationshipsForCollection } from './relationships';
1111

1212
export type AnalysisProcessState = {
1313
currentAnalysisOptions:
@@ -161,18 +161,18 @@ export function startAnalysis(
161161
options,
162162
});
163163
try {
164+
let relations: Relationship[] = [];
164165
const dataService =
165166
services.connections.getDataServiceForConnection(connectionId);
167+
166168
const collections = await Promise.all(
167169
namespaces.map(async (ns) => {
168-
const sample: AggregationCursor<Document> = dataService.sampleCursor(
170+
const sample = await dataService.sample(
169171
ns,
170172
{ size: 100 },
173+
{ promoteValues: false },
171174
{
172-
signal: cancelController.signal,
173-
promoteValues: false,
174-
},
175-
{
175+
abortSignal: cancelController.signal,
176176
fallbackReadPreference: 'secondaryPreferred',
177177
}
178178
);
@@ -194,12 +194,38 @@ export function startAnalysis(
194194
type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED,
195195
namespace: ns,
196196
});
197-
return { ns, schema };
197+
return { ns, schema, sample };
198198
})
199199
);
200200

201201
if (options.automaticallyInferRelations) {
202-
// TODO
202+
relations = (
203+
await Promise.all(
204+
collections.map(
205+
({
206+
ns,
207+
schema,
208+
sample,
209+
}): Promise<Relationship['relationship'][]> => {
210+
return inferLocalToForeignRelationshipsForCollection(
211+
ns,
212+
schema,
213+
sample,
214+
collections,
215+
dataService
216+
);
217+
}
218+
)
219+
)
220+
).flatMap((relationships) => {
221+
return relationships.map((relationship) => {
222+
return {
223+
id: new UUID().toHexString(),
224+
relationship,
225+
isInferred: true,
226+
};
227+
});
228+
});
203229
}
204230

205231
if (cancelController.signal.aborted) {
@@ -229,7 +255,7 @@ export function startAnalysis(
229255
const position = node ? node.position : { x: 0, y: 0 };
230256
return { ...coll, position };
231257
}),
232-
relations: [],
258+
relations,
233259
});
234260

235261
services.track('Data Modeling Diagram Created', {

packages/compass-data-modeling/src/store/diagram.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,9 @@ describe('Data Modeling store', function () {
314314
jsonSchema: {
315315
bsonType: 'object',
316316
properties: {
317-
field1: { bsonType: 'string' },
317+
field1: {
318+
anyOf: [{ bsonType: 'string' }, { bsonType: 'int' }],
319+
},
318320
field2: { bsonType: 'int' },
319321
field3: { bsonType: 'int' },
320322
},

packages/compass-data-modeling/src/store/diagram.ts

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -714,32 +714,52 @@ export const selectCurrentModelFromState = (state: DataModelingState) => {
714714
return selectCurrentModel(selectCurrentDiagramFromState(state).edits);
715715
};
716716

717-
function extractFields(
718-
parentSchema: MongoDBJSONSchema,
719-
parentKey?: string[],
720-
fields: string[][] = []
717+
/**
718+
* A very simplistic depth-first traversing function that only handles a subset
719+
* of real JSON schema keywords that is applicable to our MongoDB JSON schema
720+
* format
721+
*/
722+
export function traverseMongoDBJSONSchema(
723+
schema: MongoDBJSONSchema,
724+
visitor: (schema: MongoDBJSONSchema, path: string[]) => void,
725+
path: string[] = []
721726
) {
722-
if ('anyOf' in parentSchema && parentSchema.anyOf) {
723-
for (const schema of parentSchema.anyOf) {
724-
extractFields(schema, parentKey, fields);
727+
if (schema.anyOf) {
728+
for (const s of schema.anyOf) {
729+
traverseMongoDBJSONSchema(s, visitor, path);
725730
}
731+
return;
726732
}
727-
if ('items' in parentSchema && parentSchema.items) {
728-
const items = Array.isArray(parentSchema.items)
729-
? parentSchema.items
730-
: [parentSchema.items];
731-
for (const schema of items) {
732-
extractFields(schema, parentKey, fields);
733+
734+
visitor(schema, path);
735+
736+
if (schema.items) {
737+
for (const s of Array.isArray(schema.items)
738+
? schema.items
739+
: [schema.items]) {
740+
traverseMongoDBJSONSchema(s, visitor, path);
733741
}
734-
}
735-
if ('properties' in parentSchema && parentSchema.properties) {
736-
for (const [key, value] of Object.entries(parentSchema.properties)) {
737-
const fullKey = parentKey ? [...parentKey, key] : [key];
738-
fields.push(fullKey);
739-
extractFields(value, fullKey, fields);
742+
} else if (schema.properties) {
743+
for (const [key, s] of Object.entries(schema.properties)) {
744+
traverseMongoDBJSONSchema(s, visitor, [...path, key]);
740745
}
741746
}
742-
return fields;
747+
}
748+
749+
function extractFields(schema: MongoDBJSONSchema) {
750+
// Keeping fields stringified in a set to make sure that we don't include
751+
// duplicates
752+
const fields = new Set<string>();
753+
traverseMongoDBJSONSchema(schema, (_schema, path) => {
754+
// Skip the root path
755+
if (path.length === 0) {
756+
return;
757+
}
758+
fields.add(path.join('__FIELD_PATH_SEPARATOR__'));
759+
});
760+
return Array.from(fields.values(), (pathStr) => {
761+
return pathStr.split('__FIELD_PATH_SEPARATOR__');
762+
});
743763
}
744764

745765
function getFieldsForCurrentModel(

0 commit comments

Comments
 (0)