1
1
import type { Reducer , AnyAction , Action } from 'redux' ;
2
+ import {
3
+ analyzeDocuments ,
4
+ SchemaParseOptions ,
5
+ type Schema ,
6
+ } from 'mongodb-schema' ;
7
+
2
8
import type { CollectionMetadata } from 'mongodb-collection-model' ;
3
9
import type { ThunkAction } from 'redux-thunk' ;
4
10
import type AppRegistry from '@mongodb-js/compass-app-registry' ;
5
11
import type { workspacesServiceLocator } from '@mongodb-js/compass-workspaces/provider' ;
6
12
import type { CollectionSubtab } from '@mongodb-js/compass-workspaces' ;
7
13
import type { DataService } from '@mongodb-js/compass-connections/provider' ;
8
14
import type { experimentationServiceLocator } from '@mongodb-js/compass-telemetry/provider' ;
15
+ import { calculateSchemaMetadata } from '@mongodb-js/compass-schema' ;
16
+ import type { Logger } from '@mongodb-js/compass-logging/provider' ;
17
+ import { type PreferencesAccess } from 'compass-preferences-model/provider' ;
18
+ import { isInternalFieldPath } from 'hadron-document' ;
19
+ import { mongoLogId } from '@mongodb-js/compass-logging' ;
20
+ import toNS from 'mongodb-ns' ;
21
+
22
+ const DEFAULT_SAMPLE_SIZE = 100 ;
9
23
10
24
function isAction < A extends AnyAction > (
11
25
action : AnyAction ,
@@ -22,32 +36,79 @@ type CollectionThunkAction<R, A extends AnyAction = AnyAction> = ThunkAction<
22
36
dataService : DataService ;
23
37
workspaces : ReturnType < typeof workspacesServiceLocator > ;
24
38
experimentationServices : ReturnType < typeof experimentationServiceLocator > ;
39
+ logger : Logger ;
40
+ preferences : PreferencesAccess ;
41
+ analysisAbortControllerRef : { current ?: AbortController } ;
25
42
} ,
26
43
A
27
44
> ;
28
45
46
+ export enum SchemaAnalysisStatus {
47
+ INITIAL = 'initial' ,
48
+ ANALYZING = 'analyzing' ,
49
+ COMPLETED = 'completed' ,
50
+ ERROR = 'error' ,
51
+ }
52
+
53
+ type SchemaAnalysis = {
54
+ status : SchemaAnalysisStatus ;
55
+ schema : Schema | null ;
56
+ sampleDocument : Document | null ;
57
+ schemaMetadata : {
58
+ maxNestingDepth : number ;
59
+ validationRules : Document ;
60
+ } | null ;
61
+ error : string | null ;
62
+ } ;
63
+
29
64
export type CollectionState = {
30
65
workspaceTabId : string ;
31
66
namespace : string ;
32
67
metadata : CollectionMetadata | null ;
33
68
editViewName ?: string ;
69
+ schemaAnalysis : SchemaAnalysis ;
34
70
} ;
35
71
36
- enum CollectionActions {
72
+ export enum CollectionActions {
37
73
CollectionMetadataFetched = 'compass-collection/CollectionMetadataFetched' ,
74
+ SchemaAnalysisStarted = 'compass-collection/SchemaAnalysisStarted' ,
75
+ SchemaAnalysisFinished = 'compass-collection/SchemaAnalysisFinished' ,
76
+ SchemaAnalysisFailed = 'compass-collection/SchemaAnalysisFailed' ,
38
77
}
39
78
40
79
interface CollectionMetadataFetchedAction {
41
80
type : CollectionActions . CollectionMetadataFetched ;
42
81
metadata : CollectionMetadata ;
43
82
}
44
83
84
+ interface SchemaAnalysisStartedAction {
85
+ type : CollectionActions . SchemaAnalysisStarted ;
86
+ analysisStartTime : number ;
87
+ }
88
+
89
+ interface SchemaAnalysisFinishedAction {
90
+ type : CollectionActions . SchemaAnalysisFinished ;
91
+ schemaAnalysis : SchemaAnalysis ;
92
+ }
93
+
94
+ interface SchemaAnalysisFailedAction {
95
+ type : CollectionActions . SchemaAnalysisFailed ;
96
+ error : Error ;
97
+ }
98
+
45
99
const reducer : Reducer < CollectionState , Action > = (
46
100
state = {
47
101
// TODO(COMPASS-7782): use hook to get the workspace tab id instead
48
102
workspaceTabId : '' ,
49
103
namespace : '' ,
50
104
metadata : null ,
105
+ schemaAnalysis : {
106
+ status : SchemaAnalysisStatus . INITIAL ,
107
+ schema : null ,
108
+ sampleDocument : null ,
109
+ schemaMetadata : null ,
110
+ error : null ,
111
+ } ,
51
112
} ,
52
113
action
53
114
) => {
@@ -62,6 +123,53 @@ const reducer: Reducer<CollectionState, Action> = (
62
123
metadata : action . metadata ,
63
124
} ;
64
125
}
126
+
127
+ if (
128
+ isAction < SchemaAnalysisStartedAction > (
129
+ action ,
130
+ CollectionActions . SchemaAnalysisStarted
131
+ )
132
+ ) {
133
+ return {
134
+ ...state ,
135
+ schemaAnalysis : {
136
+ status : SchemaAnalysisStatus . ANALYZING ,
137
+ schema : null ,
138
+ sampleDocument : null ,
139
+ schemaMetadata : null ,
140
+ error : null ,
141
+ } ,
142
+ } ;
143
+ }
144
+
145
+ if (
146
+ isAction < SchemaAnalysisFinishedAction > (
147
+ action ,
148
+ CollectionActions . SchemaAnalysisFinished
149
+ )
150
+ ) {
151
+ return {
152
+ ...state ,
153
+ schemaAnalysis : action . schemaAnalysis ,
154
+ } ;
155
+ }
156
+
157
+ if (
158
+ isAction < SchemaAnalysisFailedAction > (
159
+ action ,
160
+ CollectionActions . SchemaAnalysisFailed
161
+ )
162
+ ) {
163
+ return {
164
+ ...state ,
165
+ schemaAnalysis : {
166
+ ...state . schemaAnalysis ,
167
+ status : SchemaAnalysisStatus . ERROR ,
168
+ error : action . error . message ,
169
+ } ,
170
+ } ;
171
+ }
172
+
65
173
return state ;
66
174
} ;
67
175
@@ -82,6 +190,115 @@ export const selectTab = (
82
190
} ;
83
191
} ;
84
192
193
+ export const analyzeCollectionSchema = ( ) : CollectionThunkAction < void > => {
194
+ return async (
195
+ dispatch ,
196
+ getState ,
197
+ { analysisAbortControllerRef, dataService, preferences, logger }
198
+ ) => {
199
+ const { schemaAnalysis, namespace } = getState ( ) ;
200
+ const analysisStatus = schemaAnalysis . status ;
201
+ if ( analysisStatus === SchemaAnalysisStatus . ANALYZING ) {
202
+ logger . debug (
203
+ 'Schema analysis is already in progress, skipping new analysis.'
204
+ ) ;
205
+ return ;
206
+ }
207
+
208
+ analysisAbortControllerRef . current = new AbortController ( ) ;
209
+ const abortSignal = analysisAbortControllerRef . current . signal ;
210
+
211
+ const analysisStartTime = Date . now ( ) ;
212
+
213
+ try {
214
+ logger . debug ( 'Schema analysis started.' ) ;
215
+
216
+ dispatch ( {
217
+ type : CollectionActions . SchemaAnalysisStarted ,
218
+ analysisStartTime,
219
+ } ) ;
220
+
221
+ // Sample documents
222
+ const samplingOptions = { size : DEFAULT_SAMPLE_SIZE } ;
223
+ const driverOptions = {
224
+ maxTimeMS : preferences . getPreferences ( ) . maxTimeMS ,
225
+ signal : abortSignal ,
226
+ } ;
227
+ const sampleCursor = dataService . sampleCursor (
228
+ namespace ,
229
+ samplingOptions ,
230
+ driverOptions ,
231
+ {
232
+ fallbackReadPreference : 'secondaryPreferred' ,
233
+ }
234
+ ) ;
235
+ const sampleDocuments = await sampleCursor . toArray ( ) ;
236
+
237
+ // Analyze sampled documents
238
+ const schemaParseOptions : SchemaParseOptions = {
239
+ signal : abortSignal ,
240
+ } ;
241
+ const schemaAccessor = await analyzeDocuments (
242
+ sampleDocuments ,
243
+ schemaParseOptions
244
+ ) ;
245
+ if ( abortSignal ?. aborted ) {
246
+ throw new Error ( abortSignal ?. reason || new Error ( 'Operation aborted' ) ) ;
247
+ }
248
+
249
+ let schema : Schema | null = null ;
250
+ if ( schemaAccessor ) {
251
+ schema = await schemaAccessor . getInternalSchema ( ) ;
252
+ // Filter out internal fields from the schema
253
+ schema . fields = schema . fields . filter (
254
+ ( { path } ) => ! isInternalFieldPath ( path [ 0 ] )
255
+ ) ;
256
+ // TODO: Transform schema to structure that will be used by the LLM.
257
+ }
258
+
259
+ let schemaMetadata = null ;
260
+ if ( schema !== null ) {
261
+ const { schema_depth } = await calculateSchemaMetadata ( schema ) ;
262
+ const { database, collection } = toNS ( namespace ) ;
263
+ const collInfo = await dataService . collectionInfo ( database , collection ) ;
264
+ schemaMetadata = {
265
+ maxNestingDepth : schema_depth ,
266
+ validationRules : collInfo ?. validation ?. validator || null ,
267
+ } ;
268
+ }
269
+ dispatch ( {
270
+ type : CollectionActions . SchemaAnalysisFinished ,
271
+ schemaAnalysis : {
272
+ status : SchemaAnalysisStatus . COMPLETED ,
273
+ schema,
274
+ sampleDocument : sampleDocuments [ 0 ] ?? null ,
275
+ schemaMetadata,
276
+ } ,
277
+ } ) ;
278
+ } catch ( err : any ) {
279
+ logger . log . error (
280
+ mongoLogId ( 1_001_000_363 ) ,
281
+ 'Collection' ,
282
+ 'Schema analysis failed' ,
283
+ {
284
+ namespace,
285
+ error : err . message ,
286
+ aborted : abortSignal . aborted ,
287
+ ...( abortSignal . aborted
288
+ ? { abortReason : abortSignal . reason ?. message ?? abortSignal . reason }
289
+ : { } ) ,
290
+ }
291
+ ) ;
292
+ dispatch ( {
293
+ type : CollectionActions . SchemaAnalysisFailed ,
294
+ error : err as Error ,
295
+ } ) ;
296
+ } finally {
297
+ analysisAbortControllerRef . current = undefined ;
298
+ }
299
+ } ;
300
+ } ;
301
+
85
302
export type CollectionTabPluginMetadata = CollectionMetadata & {
86
303
/**
87
304
* Initial query for the query bar
0 commit comments