11import type { Reducer , AnyAction , Action } from 'redux' ;
2+ import {
3+ analyzeDocuments ,
4+ SchemaParseOptions ,
5+ type Schema ,
6+ } from 'mongodb-schema' ;
7+
28import type { CollectionMetadata } from 'mongodb-collection-model' ;
39import type { ThunkAction } from 'redux-thunk' ;
410import type AppRegistry from '@mongodb-js/compass-app-registry' ;
511import type { workspacesServiceLocator } from '@mongodb-js/compass-workspaces/provider' ;
612import type { CollectionSubtab } from '@mongodb-js/compass-workspaces' ;
713import type { DataService } from '@mongodb-js/compass-connections/provider' ;
814import 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 ;
923
1024function isAction < A extends AnyAction > (
1125 action : AnyAction ,
@@ -22,32 +36,79 @@ type CollectionThunkAction<R, A extends AnyAction = AnyAction> = ThunkAction<
2236 dataService : DataService ;
2337 workspaces : ReturnType < typeof workspacesServiceLocator > ;
2438 experimentationServices : ReturnType < typeof experimentationServiceLocator > ;
39+ logger : Logger ;
40+ preferences : PreferencesAccess ;
41+ analysisAbortControllerRef : { current ?: AbortController } ;
2542 } ,
2643 A
2744> ;
2845
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+
2964export type CollectionState = {
3065 workspaceTabId : string ;
3166 namespace : string ;
3267 metadata : CollectionMetadata | null ;
3368 editViewName ?: string ;
69+ schemaAnalysis : SchemaAnalysis ;
3470} ;
3571
36- enum CollectionActions {
72+ export enum CollectionActions {
3773 CollectionMetadataFetched = 'compass-collection/CollectionMetadataFetched' ,
74+ SchemaAnalysisStarted = 'compass-collection/SchemaAnalysisStarted' ,
75+ SchemaAnalysisFinished = 'compass-collection/SchemaAnalysisFinished' ,
76+ SchemaAnalysisFailed = 'compass-collection/SchemaAnalysisFailed' ,
3877}
3978
4079interface CollectionMetadataFetchedAction {
4180 type : CollectionActions . CollectionMetadataFetched ;
4281 metadata : CollectionMetadata ;
4382}
4483
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+
4599const reducer : Reducer < CollectionState , Action > = (
46100 state = {
47101 // TODO(COMPASS-7782): use hook to get the workspace tab id instead
48102 workspaceTabId : '' ,
49103 namespace : '' ,
50104 metadata : null ,
105+ schemaAnalysis : {
106+ status : SchemaAnalysisStatus . INITIAL ,
107+ schema : null ,
108+ sampleDocument : null ,
109+ schemaMetadata : null ,
110+ error : null ,
111+ } ,
51112 } ,
52113 action
53114) => {
@@ -62,6 +123,53 @@ const reducer: Reducer<CollectionState, Action> = (
62123 metadata : action . metadata ,
63124 } ;
64125 }
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+
65173 return state ;
66174} ;
67175
@@ -82,6 +190,115 @@ export const selectTab = (
82190 } ;
83191} ;
84192
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+
85302export type CollectionTabPluginMetadata = CollectionMetadata & {
86303 /**
87304 * Initial query for the query bar
0 commit comments