Skip to content

Commit 91a1ac6

Browse files
authored
feat(schema, data-modeling): use iterable cursor in schema analysis COMPASS-9150 COMPASS-9315 (#6894)
1 parent 240623d commit 91a1ac6

File tree

13 files changed

+368
-94
lines changed

13 files changed

+368
-94
lines changed

package-lock.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-data-modeling/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"compass-preferences-model": "^2.36.0",
6767
"hadron-app-registry": "^9.4.8",
6868
"lodash": "^4.17.21",
69+
"mongodb": "^6.14.1",
6970
"mongodb-ns": "^2.4.2",
7071
"mongodb-schema": "^12.5.2",
7172
"react": "^17.0.2",

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

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { isAction } from './util';
33
import type { DataModelingThunkAction } from './reducer';
44
import { analyzeDocuments } from 'mongodb-schema';
55
import { getCurrentDiagramFromState } from './diagram';
6+
import type { Document } from 'bson';
7+
import type { AggregationCursor } from 'mongodb';
68

79
export type AnalysisProcessState = {
810
currentAnalysisOptions:
@@ -155,32 +157,32 @@ export function startAnalysis(
155157
try {
156158
const dataService =
157159
services.connections.getDataServiceForConnection(connectionId);
158-
const samples = await Promise.all(
160+
const schema = await Promise.all(
159161
namespaces.map(async (ns) => {
160-
// TODO
161-
const sample = await dataService.sample(
162+
const sample: AggregationCursor<Document> = dataService.sampleCursor(
162163
ns,
163164
{ size: 100 },
164-
undefined,
165165
{
166-
abortSignal: cancelController.signal,
166+
signal: cancelController.signal,
167+
promoteValues: false,
168+
},
169+
{
170+
fallbackReadPreference: 'secondaryPreferred',
167171
}
168172
);
173+
174+
const accessor = await analyzeDocuments(sample, {
175+
signal: cancelController.signal,
176+
});
177+
178+
// TODO(COMPASS-9314): Update how we show analysis progress.
169179
dispatch({
170180
type: AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED,
171181
namespace: ns,
172182
});
173-
return { ns, sample };
174-
})
175-
);
176-
const schema = await Promise.all(
177-
samples.map(async ({ ns, sample }) => {
178-
const schema = await analyzeDocuments(sample, {
183+
184+
const schema = await accessor.getMongoDBJsonSchema({
179185
signal: cancelController.signal,
180-
}).then((accessor) => {
181-
return accessor.getMongoDBJsonSchema({
182-
signal: cancelController.signal,
183-
});
184186
});
185187
dispatch({
186188
type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED,

packages/compass-schema-validation/src/index.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { createLoggerLocator } from '@mongodb-js/compass-logging/provider';
1313
import { telemetryLocator } from '@mongodb-js/compass-telemetry/provider';
1414
import { SchemaValidationTabTitle } from './plugin-title';
1515
import { workspacesServiceLocator } from '@mongodb-js/compass-workspaces/provider';
16+
import type { RequiredDataServiceProps } from './modules';
1617

1718
const CompassSchemaValidationHadronPlugin = registerHadronPlugin(
1819
{
@@ -23,13 +24,8 @@ const CompassSchemaValidationHadronPlugin = registerHadronPlugin(
2324
activate: onActivated,
2425
},
2526
{
26-
dataService: dataServiceLocator as DataServiceLocator<
27-
| 'aggregate'
28-
| 'collectionInfo'
29-
| 'updateCollection'
30-
| 'sample'
31-
| 'isCancelError'
32-
>,
27+
dataService:
28+
dataServiceLocator as DataServiceLocator<RequiredDataServiceProps>,
3329
connectionInfoRef: connectionInfoRefLocator,
3430
instance: mongoDBInstanceLocator,
3531
preferences: preferencesLocator,

packages/compass-schema-validation/src/modules/index.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,13 @@ export type RootAction =
6767
| EditModeAction
6868
| ResetAction;
6969

70-
export type DataService = Pick<
71-
OriginalDataService,
70+
export type RequiredDataServiceProps =
7271
| 'aggregate'
7372
| 'collectionInfo'
7473
| 'updateCollection'
75-
| 'sample'
76-
| 'isCancelError'
77-
>;
74+
| 'sampleCursor'
75+
| 'isCancelError';
76+
export type DataService = Pick<OriginalDataService, RequiredDataServiceProps>;
7877

7978
export type SchemaValidationExtraArgs = {
8079
dataService: DataService;

packages/compass-schema-validation/src/stores/store.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ const fakeDataService = {
4242
/* never resolves */
4343
}),
4444
isCancelError: () => false,
45-
sample: () => [{ prop1: 'abc' }],
45+
sampleCursor: () =>
46+
({
47+
async *[Symbol.asyncIterator]() {
48+
await new Promise((resolve) => setTimeout(resolve, 0));
49+
yield* [{ prop1: 'abc' }];
50+
},
51+
} as any),
4652
} as any;
4753

4854
const fakeWorkspaces = {

packages/compass-schema/src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import CompassSchema from './components/compass-schema';
99
import { registerHadronPlugin } from 'hadron-app-registry';
1010
import { activateSchemaPlugin } from './stores/store';
11+
import type { RequiredDataServiceProps } from './stores/store';
1112
import { createLoggerLocator } from '@mongodb-js/compass-logging/provider';
1213
import { telemetryLocator } from '@mongodb-js/compass-telemetry/provider';
1314
import { preferencesLocator } from 'compass-preferences-model/provider';
@@ -31,9 +32,8 @@ const CompassSchemaHadronPlugin = registerHadronPlugin(
3132
activate: activateSchemaPlugin,
3233
},
3334
{
34-
dataService: dataServiceLocator as DataServiceLocator<
35-
'sample' | 'isCancelError'
36-
>,
35+
dataService:
36+
dataServiceLocator as DataServiceLocator<RequiredDataServiceProps>,
3737
logger: createLoggerLocator('COMPASS-SCHEMA-UI'),
3838
track: telemetryLocator,
3939
preferences: preferencesLocator,

packages/compass-schema/src/modules/schema-analysis.spec.ts

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import mongoDBSchemaAnalyzeSchema from 'mongodb-schema';
55
import type { Schema } from 'mongodb-schema';
66
import { createNoopLogger } from '@mongodb-js/compass-logging/provider';
77
import { isInternalFieldPath } from 'hadron-document';
8-
9-
import { analyzeSchema, calculateSchemaMetadata } from './schema-analysis';
108
import {
119
createSandboxFromDefaultPreferences,
1210
type PreferencesAccess,
1311
} from 'compass-preferences-model';
1412

13+
import { analyzeSchema, calculateSchemaMetadata } from './schema-analysis';
14+
1515
const testDocs = [
1616
{
1717
someFields: {
@@ -81,13 +81,18 @@ describe('schema-analysis', function () {
8181

8282
describe('#getResult', function () {
8383
it('returns the schema', async function () {
84+
const docs = [
85+
{ x: 1 },
86+
{ y: 2, __safeContent__: [bson.Binary.createFromBase64('aaaa')] },
87+
];
8488
const dataService = {
85-
sample: () =>
86-
Promise.resolve([
87-
{ x: 1 },
88-
{ y: 2, __safeContent__: [bson.Binary.createFromBase64('aaaa')] },
89-
]),
90-
isCancelError: () => false,
89+
sampleCursor: () =>
90+
({
91+
async *[Symbol.asyncIterator]() {
92+
await new Promise((resolve) => setTimeout(resolve, 0));
93+
yield* docs;
94+
},
95+
} as any),
9196
};
9297
const abortController = new AbortController();
9398
const abortSignal = abortController.signal;
@@ -179,10 +184,17 @@ describe('schema-analysis', function () {
179184

180185
it('adds promoteValues: false so the analyzer can report more accurate types', async function () {
181186
const dataService = {
182-
sample: () => Promise.resolve([]),
183-
isCancelError: () => false,
187+
sampleCursor: () =>
188+
({
189+
async *[Symbol.asyncIterator]() {
190+
await new Promise((resolve) => setTimeout(resolve, 0));
191+
yield {
192+
a: 123,
193+
};
194+
},
195+
} as any),
184196
};
185-
const sampleSpy = sinon.spy(dataService, 'sample');
197+
const sampleSpy = sinon.spy(dataService, 'sampleCursor');
186198
const abortController = new AbortController();
187199
const abortSignal = abortController.signal;
188200

@@ -199,19 +211,31 @@ describe('schema-analysis', function () {
199211
expect(sampleSpy).to.have.been.calledWith(
200212
'db.coll',
201213
{},
202-
{ promoteValues: false }
214+
{ signal: abortSignal, promoteValues: false },
215+
{ fallbackReadPreference: 'secondaryPreferred' }
203216
);
204217
});
205218

206219
it('returns undefined if is cancelled', async function () {
207-
const dataService = {
208-
sample: () => Promise.reject(new Error('test error')),
209-
isCancelError: () => true,
210-
};
211-
212220
const abortController = new AbortController();
213221
const abortSignal = abortController.signal;
214222

223+
const dataService = {
224+
sampleCursor: () =>
225+
({
226+
async *[Symbol.asyncIterator]() {
227+
await new Promise((resolve) => setTimeout(resolve, 0));
228+
yield {
229+
a: 123,
230+
};
231+
abortController.abort();
232+
yield {
233+
a: 345,
234+
};
235+
},
236+
} as any),
237+
};
238+
215239
const result = await analyzeSchema(
216240
dataService,
217241
abortSignal,
@@ -228,13 +252,19 @@ describe('schema-analysis', function () {
228252
it('throws if sample throws', async function () {
229253
const error: Error & {
230254
code?: any;
231-
} = new Error('should have been thrown');
255+
} = new Error('pineapple');
232256
error.name = 'MongoError';
233257
error.code = new bson.Int32(1000);
234258

235259
const dataService = {
236-
sample: () => Promise.reject(error),
237-
isCancelError: () => false,
260+
sampleCursor: () =>
261+
({
262+
async *[Symbol.asyncIterator]() {
263+
await new Promise((resolve) => setTimeout(resolve, 0));
264+
yield {};
265+
throw error;
266+
},
267+
} as any),
238268
};
239269

240270
const abortController = new AbortController();
@@ -251,7 +281,7 @@ describe('schema-analysis', function () {
251281
preferences
252282
);
253283
} catch (err: any) {
254-
expect(err.message).to.equal('should have been thrown');
284+
expect(err.message).to.equal('pineapple');
255285
expect(err.code).to.equal(1000);
256286
return;
257287
}

packages/compass-schema/src/modules/schema-analysis.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@ export const analyzeSchema = async (
5050
ns,
5151
});
5252

53-
const docs = await dataService.sample(
53+
const sampleCursor = dataService.sampleCursor(
5454
ns,
5555
query,
5656
{
5757
...aggregateOptions,
5858
promoteValues: false,
59+
signal: abortSignal,
5960
},
6061
{
61-
abortSignal,
6262
fallbackReadPreference: 'secondaryPreferred',
6363
}
6464
);
@@ -72,7 +72,10 @@ export const analyzeSchema = async (
7272
: {
7373
signal: abortSignal,
7474
};
75-
const schemaAccessor = await analyzeDocuments(docs, schemaParseOptions);
75+
const schemaAccessor = await analyzeDocuments(
76+
sampleCursor,
77+
schemaParseOptions
78+
);
7679
log.info(mongoLogId(1001000090), 'Schema', 'Schema analysis completed', {
7780
ns,
7881
});
@@ -81,8 +84,14 @@ export const analyzeSchema = async (
8184
log.error(mongoLogId(1001000091), 'Schema', 'Schema analysis failed', {
8285
ns,
8386
error: err.message,
87+
aborted: abortSignal.aborted,
88+
...(abortSignal.aborted
89+
? { abortReason: abortSignal.reason?.message ?? abortSignal.reason }
90+
: {}),
8491
});
85-
if (dataService.isCancelError(err)) {
92+
93+
if (abortSignal.aborted) {
94+
// The operation was aborted, so we don't throw an error.
8695
debug('caught background operation terminated error', err);
8796
return;
8897
}

0 commit comments

Comments
 (0)