import ClusterSchema , { IColumnDef } from '@/models/cassandra/ClusterSchema' ; import QueryTokenizer from './CassQueryTokenizer' ; const uppercaseRegex = new RegExp ( /[A-Z]+/ ) ; const quotedRegex = new RegExp ( /^"(.*)"$/ ) ; interface ISuggestion { name : string ; value : string ; score : number ; meta : any ; icon : string ; className : string ; } export default class QueryCompleter { private schema : ClusterSchema | undefined ; constructor ( readonly queryTokenizer , clusterSchema ) { this . schema = clusterSchema ; } public updateSchema ( schema ) { this . schema = schema ; } public getCompleter ( ) { const self = this ; const queryTokenizer = this . queryTokenizer ; return { getCompletions ( editor , _session , pos , _prefix , callback ) { const { row , column } = pos ; const allPriorTokens = queryTokenizer . getTokensUpToRow ( row ) ; const keyspaceName = queryTokenizer . getKeyspace ( ) ; const tableName = queryTokenizer . getTable ( ) ; let currentToken = editor . session . getTokenAt ( row , column ) ; if ( currentToken && ( currentToken . type === 'text' || currentToken . type === 'empty' ) ) { currentToken = self . queryTokenizer . getPreviousNonTextToken ( allPriorTokens , currentToken ) ; } let suggestions = new Array < ISuggestion > ( ) ; if ( currentToken ) { switch ( currentToken . type ) { case 'statement.terminator' : suggestions = [ ] ; break ; case 'paren.lparen' : { const previousToken = self . queryTokenizer . getPreviousNonTextToken ( allPriorTokens , currentToken ) ; if ( previousToken && previousToken . value . toLowerCase ( ) === 'values' ) { suggestions = [ ] ; } else { suggestions = self . getColumnSuggestions ( keyspaceName , tableName ) ; } break ; } case 'punctuation.comma' : case 'punctuation.separator' : suggestions = self . getColumnSuggestions ( keyspaceName , tableName ) ; break ; case 'identifier' : { const previousToken = self . queryTokenizer . getPreviousNonTextToken ( allPriorTokens , currentToken ) ; if ( previousToken && previousToken . type === 'keyword' && keyspaceName && tableName ) { suggestions = self . getSuggestionsForKeyword ( keyspaceName , tableName , allPriorTokens , previousToken ) ; } else { suggestions = self . getKeyspaceSuggestions ( ) ; } break ; } case 'entity.name.table' : { if ( keyspaceName && tableName && self . schema && self . schema . hasTable ( keyspaceName , tableName ) ) { suggestions = self . getCurrentActionOptionSuggestions ( ) ; } else { suggestions = self . getTableSuggestions ( keyspaceName ) ; } break ; } case 'entity.name.keyspace' : suggestions = self . getKeyspaceSuggestions ( ) ; break ; case 'statement.operator.equals' : break ; case 'punctuation.operator' : case 'where.statement.column_name' : case 'select.statement.column' : suggestions = self . getTableSuggestions ( keyspaceName ) ; break ; case 'where.statement.column_value' : suggestions = self . getCurrentActionOptionSuggestions ( ) ; break ; case 'keyword' : suggestions = self . getSuggestionsForKeyword ( keyspaceName , tableName , allPriorTokens , currentToken ) ; break ; default : } } else { const containsActionToken = self . queryTokenizer . containsActionToken ( ) ; if ( ! containsActionToken && keyspaceName ) { suggestions = self . getActionSuggestions ( [ 'SELECT' , 'INSERT' , 'UPDATE' , ] ) ; } } callback ( null , suggestions ) ; } , } ; } param name returns private private quoteName ( name ) { if ( quotedRegex . test ( name ) ) { return name ; } if ( uppercaseRegex . test ( name ) ) { return `"${ name }"` ; } return name ; } private getKeyspaceSuggestions ( ) { if ( ! this . schema ) { return [ ] ; } const keyspaceNames = this . schema . getKeyspaceNames ( ) . map ( this . quoteName ) ; return this . getSuggestions ( keyspaceNames , 'keyspace' ) ; } private getColumnSuggestions ( keyspaceName , tableName ) { if ( ! this . schema ) { return [ ] ; } const columns = this . schema . getColumns ( keyspaceName , tableName ) ; const columnMap = columns . reduce ( ( prev , curr ) => prev . set ( this . quoteName ( curr . name ) , curr ) , new Map < string , IColumnDef > ( ) ) ; const columnNames = Array . from ( columnMap . keys ( ) ) ; return this . getSuggestions ( columnNames , ( columnName ) => { const column = columnMap . get ( columnName ) as any ; if ( column . isPartitionKey ) { return 'partition key' ; } else if ( column . isClusteringColumn ) { return 'clustering column' ; } return 'column' ; } ) ; } private getTableSuggestions ( keyspaceName ) { if ( ! this . schema ) { return [ ] ; } const tableNames = this . schema . getTableNames ( keyspaceName ) . map ( this . quoteName ) ; return this . getSuggestions ( tableNames , 'table' ) ; } private getActionSuggestions ( actions ) { return this . getSuggestions ( actions , 'action' ) ; } returns { Array . < Object > } private getCurrentActionOptionSuggestions ( ) { const actionToken = this . queryTokenizer . getActionToken ( ) ; if ( ! actionToken ) { return [ ] ; } const actionOptions = this . queryTokenizer . getOptionsForKeyword ( actionToken . value ) ; return this . getSuggestions ( actionOptions , 'option' ) ; } param items param { String | Function } meta param options private getSuggestions ( items , meta , options = { } as any ) { return items . map ( ( item , index ) => { const metaValue = typeof meta === 'string' ? meta : meta ( item ) ; const className = `${ metaValue . replace ( /\s+/ , '_' ) }Icon` ; return { name : `${ item }` , value : `${ item }` , score : options . score || items . length - index , meta : metaValue , icon : 'property' , className , } ; } ) ; } private getSuggestionsForKeyword ( keyspaceName , tableName , previousTokens , token ) { if ( ! token ) { return [ ] ; } let suggestions = new Array < ISuggestion > ( ) ; switch ( token . value . toLowerCase ( ) ) { case 'update' : case 'from' : case 'into' : suggestions = this . getKeyspaceSuggestions ( ) ; break ; case 'set' : case 'where' : case 'and' : suggestions = this . getColumnSuggestions ( keyspaceName , tableName ) ; break ; case 'select' : { suggestions = new Array < ISuggestion > ( ) . concat ( this . getColumnSuggestions ( keyspaceName , tableName ) , this . getSuggestions ( [ '*' ] , 'column' ) ) ; suggestions . forEach ( ( item , index ) => ( item . score = suggestions . length - index ) ) ; break ; } default : { const previousToken = this . queryTokenizer . getPreviousToken ( previousTokens , token ) ; if ( previousToken && previousToken . type === 'keyword' ) { suggestions = this . getSuggestionsForKeyword ( keyspaceName , tableName , previousTokens , previousToken ) ; } break ; } } return suggestions ; } }