From 8fd2bb71bb3675cf93561bc6a9616e229cfe75f4 Mon Sep 17 00:00:00 2001 From: el-makarova Date: Tue, 12 Mar 2024 11:46:08 +0300 Subject: [PATCH] feat: add YQL autocomplete --- package-lock.json | 43 + package.json | 1 + .../Tenant/ObjectGeneral/ObjectGeneral.tsx | 8 +- src/containers/Tenant/Tenant.tsx | 10 +- src/utils/monaco.ts | 17 + src/utils/yqlSuggestions/constants.ts | 770 ++++++++++++++++++ .../yqlSuggestions/generateSuggestions.ts | 309 +++++++ src/utils/yqlSuggestions/yqlSuggestions.ts | 135 +++ 8 files changed, 1289 insertions(+), 4 deletions(-) create mode 100644 src/utils/yqlSuggestions/constants.ts create mode 100644 src/utils/yqlSuggestions/generateSuggestions.ts create mode 100644 src/utils/yqlSuggestions/yqlSuggestions.ts diff --git a/package-lock.json b/package-lock.json index e23346e451..f45a2c2175 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@gravity-ui/paranoid": "^1.4.0", "@gravity-ui/react-data-table": "^1.2.0", "@gravity-ui/uikit": "^5.30.1", + "@gravity-ui/websql-autocomplete": "^8.0.2", "@reduxjs/toolkit": "^2.2.1", "axios": "^1.6.7", "bem-cn-lite": "^4.1.0", @@ -3516,6 +3517,23 @@ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@gravity-ui/uikit/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@gravity-ui/websql-autocomplete": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@gravity-ui/websql-autocomplete/-/websql-autocomplete-8.0.2.tgz", + "integrity": "sha512-Oj0xQU5rSucPxtru0WwUXtnBHq3E6fQJ+Ge11J5qWoFWfALKfJU8xhvfrTWCxwZIC79iqSyhBOHbPvN3kpaX1w==", + "dependencies": { + "antlr4-c3": "~3.3.5", + "antlr4ng": "^2.0.10" + }, + "engines": { + "node": ">=16.0" + } + }, "node_modules/@gravity-ui/yagr": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@gravity-ui/yagr/-/yagr-4.2.3.tgz", @@ -6420,6 +6438,31 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/antlr4-c3": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/antlr4-c3/-/antlr4-c3-3.3.7.tgz", + "integrity": "sha512-F3ndE38wwA6z6AjUbL3heSdEGl4TxulGDPf9xB0/IY4dbRHWBh6XNaqFwur8vHKQk9FS5yNABHeg2wqlqIYO0w==", + "dependencies": { + "antlr4ng": "2.0.11" + } + }, + "node_modules/antlr4ng": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/antlr4ng/-/antlr4ng-2.0.11.tgz", + "integrity": "sha512-9jM91VVtHSqHkAHQsXHaoaiewFETMvUTI1/tXvwTiFw4f7zke3IGlwEyoKN9NS0FqIwDKFvUNW2e1cKPniTkVQ==", + "peerDependencies": { + "antlr4ng-cli": "1.0.7" + } + }, + "node_modules/antlr4ng-cli": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/antlr4ng-cli/-/antlr4ng-cli-1.0.7.tgz", + "integrity": "sha512-qN2FsDBmLvsQcA5CWTrPz8I8gNXeS1fgXBBhI78VyxBSBV/EJgqy8ks6IDTC9jyugpl40csCQ4sL5K4i2YZ/2w==", + "peer": true, + "bin": { + "antlr4ng": "index.js" + } + }, "node_modules/anymatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", diff --git a/package.json b/package.json index 31d1f42e5a..710fd4214d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@gravity-ui/paranoid": "^1.4.0", "@gravity-ui/react-data-table": "^1.2.0", "@gravity-ui/uikit": "^5.30.1", + "@gravity-ui/websql-autocomplete": "^8.0.2", "@reduxjs/toolkit": "^2.2.1", "axios": "^1.6.7", "bem-cn-lite": "^4.1.0", diff --git a/src/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx b/src/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx index b5c67efbbb..b64e6dc349 100644 --- a/src/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx +++ b/src/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx @@ -19,6 +19,7 @@ const b = cn('object-general'); interface ObjectGeneralProps { type: EPathType; + tenantName: string; additionalTenantProps?: AdditionalTenantsProps; additionalNodesProps?: AdditionalNodesProps; } @@ -30,13 +31,13 @@ function ObjectGeneral(props: ObjectGeneralProps) { const [initialPage] = useSetting(TENANT_INITIAL_PAGE_KEY); const queryParams = parseQuery(location); - const {name: tenantName, tenantPage = initialPage} = queryParams; + const {tenantPage = initialPage} = queryParams; const renderTabContent = () => { - const {type, additionalTenantProps, additionalNodesProps} = props; + const {type, additionalTenantProps, additionalNodesProps, tenantName} = props; switch (tenantPage) { case TENANT_PAGES_IDS.query: { - return ; + return ; } default: { return ( @@ -51,6 +52,7 @@ function ObjectGeneral(props: ObjectGeneralProps) { }; const renderContent = () => { + const {tenantName} = props; if (!tenantName) { return null; } diff --git a/src/containers/Tenant/Tenant.tsx b/src/containers/Tenant/Tenant.tsx index 5c9c339fad..4c6f4bdf8d 100644 --- a/src/containers/Tenant/Tenant.tsx +++ b/src/containers/Tenant/Tenant.tsx @@ -1,4 +1,4 @@ -import {useEffect, useReducer} from 'react'; +import {useEffect, useReducer, useRef} from 'react'; import cn from 'bem-cn-lite'; import {useLocation} from 'react-router'; import qs from 'qs'; @@ -11,6 +11,7 @@ import {DEFAULT_IS_TENANT_SUMMARY_COLLAPSED, DEFAULT_SIZE_TENANT_KEY} from '../. import {useTypedSelector, useTypedDispatch} from '../../utils/hooks'; import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; import {disableAutorefresh, getSchema} from '../../store/reducers/schema/schema'; +import {registerYQLCompletionItemProvider} from '../../utils/monaco'; import SplitPane from '../../components/SplitPane'; import {AccessDenied} from '../../components/Errors/403'; @@ -49,6 +50,7 @@ function Tenant(props: TenantProps) { undefined, getTenantSummaryState, ); + const previousTenant = useRef(); const {currentSchemaPath, currentSchema: currentItem = {}} = useTypedSelector( (state) => state.schema, @@ -77,6 +79,11 @@ function Tenant(props: TenantProps) { const {name} = queryParams; const tenantName = name as string; + if (tenantName && typeof tenantName === 'string' && previousTenant.current !== tenantName) { + registerYQLCompletionItemProvider(tenantName); + previousTenant.current = tenantName; + } + useEffect(() => { dispatch(getSchema({path: tenantName})); }, [tenantName, dispatch]); @@ -140,6 +147,7 @@ function Tenant(props: TenantProps) { type={preloadedPathType || currentPathType} additionalTenantProps={props.additionalTenantProps} additionalNodesProps={props.additionalNodesProps} + tenantName={tenantName} /> )} diff --git a/src/utils/monaco.ts b/src/utils/monaco.ts index 395b7ed22b..8eaf6721c4 100644 --- a/src/utils/monaco.ts +++ b/src/utils/monaco.ts @@ -1,4 +1,5 @@ import * as monaco from 'monaco-editor'; +import {createProvideSuggestionsFunction} from './yqlSuggestions/yqlSuggestions'; export const LANGUAGE_S_EXPRESSION_ID = 's-expression'; @@ -64,6 +65,22 @@ function registerSExpressionLanguage() { }); } +let completionProvider: monaco.IDisposable | undefined; + +function disableCodeSuggestions(): void { + if (completionProvider) { + completionProvider.dispose(); + } +} + +export function registerYQLCompletionItemProvider(database: string) { + disableCodeSuggestions(); + completionProvider = monaco.languages.registerCompletionItemProvider('sql', { + triggerCharacters: [' ', '\n', '', ',', '.', '`', '('], + provideCompletionItems: createProvideSuggestionsFunction(database), + }); +} + export function registerLanguages() { registerSExpressionLanguage(); } diff --git a/src/utils/yqlSuggestions/constants.ts b/src/utils/yqlSuggestions/constants.ts new file mode 100644 index 0000000000..2a874a4bf0 --- /dev/null +++ b/src/utils/yqlSuggestions/constants.ts @@ -0,0 +1,770 @@ +export const SimpleTypes = [ + 'String', + 'Bool', + 'Int32', + 'Uint32', + 'Int64', + 'Uint64', + 'Float', + 'Double', + 'Void', + 'Yson', + 'Utf8', + 'Unit', + 'Json', + 'Date', + 'Datetime', + 'Timestamp', + 'Interval', + 'Null', + 'Int8', + 'Uint8', + 'Int16', + 'Uint16', + 'TzDate', + 'TzDatetime', + 'TzTimestamp', + 'Uuid', + 'EmptyList', + 'EmptyDict', + 'JsonDocument', + 'DyNumber', +]; + +export const SimpleFunctions = [ + 'CAST', + 'COALESCE', + 'LENGTH', + 'LEN', + 'SUBSTRING', + 'FIND', + 'RFIND', + 'StartsWith', + 'EndsWith', + 'IF', + 'NANVL', + 'Random', + 'RandomNumber', + 'RandomUuid', + 'CurrentUtcDate', + 'CurrentUtcDatetime', + 'CurrentUtcTimestamp', + 'CurrentTzDate', + 'CurrentTzDatetime', + 'CurrentTzTimestamp', + 'AddTimezone', + 'RemoveTimezone', + 'MAX_OF', + 'MIN_OF', + 'GREATEST', + 'LEAST', + 'AsTuple', + 'AsStruct', + 'AsList', + 'AsDict', + 'AsSet', + 'AsListStrict', + 'AsDictStrict', + 'AsSetStrict', + 'Variant', + 'AsVariant', + 'Enum', + 'AsEnum', + 'AsTagged', + 'Untag', + 'TableRow', + 'JoinTableRow', + 'Ensure', + 'EnsureType', + 'EnsureConvertibleTo', + 'ToBytes', + 'FromBytes', + 'ByteAt', + 'TestBit', + 'ClearBit', + 'SetBit', + 'FlipBit', + 'Abs', + 'Just', + 'Unwrap', + 'Nothing', + 'Callable', + 'Pickle', + 'StablePickle', + 'Unpickle', + 'StaticMap', + 'StaticZip', + 'AggregationFactory', + 'AggregateTransformInput', + 'AggregateTransformOutput', + 'AggregateFlatten', + 'ListCreate', + 'AsListStrict', + 'ListLength', + 'ListHasItems', + 'ListCollect', + 'ListSort', + 'ListSortAsc', + 'ListSortDesc', + 'ListExtend', + 'ListExtendStrict', + 'ListUnionAll', + 'ListZip', + 'ListZipAll', + 'ListEnumerate', + 'ListReverse', + 'ListSkip', + 'ListTake', + 'ListIndexOf', + 'ListMap', + 'ListFilter', + 'ListFlatMap', + 'ListNotNull', + 'ListFlatten', + 'ListUniq', + 'ListAny', + 'ListAll', + 'ListHas', + 'ListHead', + 'ListLast', + 'ListMin', + 'ListMax', + 'ListSum', + 'ListAvg', + 'ListFold', + 'ListFold1', + 'ListFoldMap', + 'ListFold1Map', + 'ListFromRange', + 'ListReplicate', + 'ListConcat', + 'ListExtract', + 'ListTakeWhile', + 'ListSkipWhile', + 'ListAggregate', + 'ToDict', + 'ToMultiDict', + 'ToSet', + 'DictCreate', + 'SetCreate', + 'DictLength', + 'DictHasItems', + 'DictItems', + 'DictKeys', + 'DictPayloads', + 'DictLookup', + 'DictContains', + 'DictAggregate', + 'SetIsDisjoint', + 'SetIntersection', + 'SetIncludes', + 'SetUnion', + 'SetDifference', + 'SetSymmetricDifference', + 'TryMember', + 'ExpandStruct', + 'AddMember', + 'RemoveMember', + 'ForceRemoveMember', + 'ChooseMembers', + 'RemoveMembers', + 'ForceRemoveMembers', + 'CombineMembers', + 'FlattenMembers', + 'StructMembers', + 'RenameMembers', + 'ForceRenameMembers', + 'GatherMembers', + 'SpreadMembers', + 'ForceSpreadMembers', + 'FormatType', + 'ParseType', + 'TypeOf', + 'InstanceOf', + 'DataType', + 'OptionalType', + 'ListType', + 'StreamType', + 'DictType', + 'TupleType', + 'StructType', + 'VariantType', + 'ResourceType', + 'CallableType', + 'GenericType', + 'UnitType', + 'VoidType', + 'OptionalItemType', + 'ListItemType', + 'StreamItemType', + 'DictKeyType', + 'DictPayloadType', + 'TupleElementType', + 'StructMemberType', + 'CallableResultType', + 'CallableArgumentType', + 'VariantUnderlyingType', + 'JSON_EXISTS', + 'JSON_VALUE', + 'JSON_QUERY', +]; + +export const AggregateFunctions = [ + 'COUNT', + 'MIN', + 'MAX', + 'SUM', + 'AVG', + 'COUNT_IF', + 'SUM_IF', + 'AVG_IF', + 'SOME', + 'CountDistinctEstimate', + 'HyperLogLog', + 'AGGREGATE_LIST', + 'AGGREGATE_LIST_DISTINCT', + 'AGG_LIST', + 'AGG_LIST_DISTINCT', + 'MAX_BY', + 'MIN_BY', + 'AGGREGATE_BY', + 'MULTI_AGGREGATE_BY', + 'TOP', + 'BOTTOM', + 'TOP_BY', + 'BOTTOM_BY', + 'TOPFREQ', + 'MODE', + 'STDDEV', + 'VARIANCE', + 'CORRELATION', + 'COVARIANCE', + 'PERCENTILE', + 'MEDIAN', + 'HISTOGRAM', + 'LogarithmicHistogram', + 'LogHistogram', + 'LinearHistogram', + 'BOOL_AND', + 'BOOL_OR', + 'BOOL_XOR', + 'BIT_AND', + 'BIT_OR', + 'BIT_XOR', + 'SessionStart', +]; + +const RawUdfs = { + DateTime: [ + 'EndOfMonth', + 'Format', + 'FromMicroseconds', + 'FromMilliseconds', + 'FromSeconds', + 'GetDayOfMonth', + 'GetDayOfWeek', + 'GetDayOfWeekName', + 'GetDayOfYear', + 'GetHour', + 'GetMicrosecondOfSecond', + 'GetMillisecondOfSecond', + 'GetMinute', + 'GetMonth', + 'GetMonthName', + 'GetSecond', + 'GetTimezoneId', + 'GetTimezoneName', + 'GetWeekOfYear', + 'GetWeekOfYearIso8601', + 'GetYear', + 'IntervalFromDays', + 'IntervalFromHours', + 'IntervalFromMicroseconds', + 'IntervalFromMilliseconds', + 'IntervalFromMinutes', + 'IntervalFromSeconds', + 'MakeDate', + 'MakeDatetime', + 'MakeTimestamp', + 'MakeTzDate', + 'MakeTzDatetime', + 'MakeTzTimestamp', + 'Parse', + 'ParseHttp', + 'ParseIso8601', + 'ParseRfc822', + 'ParseX509', + 'ShiftMonths', + 'ShiftQuarters', + 'ShiftYears', + 'Split', + 'StartOf', + 'StartOfDay', + 'StartOfMonth', + 'StartOfQuarter', + 'StartOfWeek', + 'StartOfYear', + 'TimeOfDay', + 'ToDays', + 'ToHours', + 'ToMicroseconds', + 'ToMilliseconds', + 'ToMinutes', + 'ToSeconds', + 'Update', + ], + + Dsv: ['Parse', 'ReadRecord', 'Serialize'], + + String: [ + 'AsciiToLower', + 'AsciiToTitle', + 'AsciiToUpper', + 'Base32Decode', + 'Base32Encode', + 'Base32StrictDecode', + 'Base64Decode', + 'Base64Encode', + 'Base64EncodeUrl', + 'Base64StrictDecode', + 'Bin', + 'BinText', + 'CgiEscape', + 'CgiUnescape', + 'Collapse', + 'CollapseText', + 'Contains', + 'DecodeHtml', + 'EncodeHtml', + 'EndsWith', + 'EndsWithIgnoreCase', + 'EscapeC', + 'FromByteList', + 'HasPrefix', + 'HasPrefixIgnoreCase', + 'HasSuffix', + 'HasSuffixIgnoreCase', + 'Hex', + 'HexDecode', + 'HexEncode', + 'HexText', + 'HumanReadableBytes', + 'HumanReadableDuration', + 'HumanReadableQuantity', + 'IsAscii', + 'IsAsciiAlnum', + 'IsAsciiAlpha', + 'IsAsciiDigit', + 'IsAsciiHex', + 'IsAsciiLower', + 'IsAsciiSpace', + 'IsAsciiUpper', + 'JoinFromList', + 'LeftPad', + 'LevensteinDistance', + 'Prec', + 'RemoveAll', + 'RemoveFirst', + 'RemoveLast', + 'ReplaceAll', + 'ReplaceFirst', + 'ReplaceLast', + 'RightPad', + 'SBin', + 'SHex', + 'SplitToList', + 'StartsWith', + 'StartsWithIgnoreCase', + 'Strip', + 'ToByteList', + 'UnescapeC', + ], + + Compress: ['BZip2', 'Brotli', 'Gzip', 'Lzma', 'Snappy', 'Zlib', 'Zstd'], + + TryDecompress: ['BZip2', 'Brotli', 'Gzip', 'Lzma', 'Snappy', 'Xz', 'Zlib', 'Zstd'], + + Unicode: [ + 'Find', + 'Fold', + 'FromCodePointList', + 'GetLength', + 'IsAlnum', + 'IsAlpha', + 'IsAscii', + 'IsDigit', + 'IsHex', + 'IsLower', + 'IsSpace', + 'IsUnicodeSet', + 'IsUpper', + 'IsUtf', + 'JoinFromList', + 'LevensteinDistance', + 'Normalize', + 'NormalizeNFC', + 'NormalizeNFD', + 'NormalizeNFKC', + 'NormalizeNFKD', + 'RFind', + 'RemoveAll', + 'RemoveFirst', + 'RemoveLast', + 'ReplaceAll', + 'ReplaceFirst', + 'ReplaceLast', + 'Reverse', + 'SplitToList', + 'Strip', + 'Substring', + 'ToCodePointList', + 'ToLower', + 'ToTitle', + 'ToUint64', + 'ToUpper', + 'Translit', + 'TryToUint64', + ], + + Url: [ + 'BuildQueryString', + 'CanBePunycodeHostName', + 'CutQueryStringAndFragment', + 'CutScheme', + 'CutWWW', + 'CutWWW2', + 'Decode', + 'Encode', + 'ForceHostNameToPunycode', + 'ForcePunycodeToHostName', + 'GetCGIParam', + 'GetDomain', + 'GetDomainLevel', + 'GetFragment', + 'GetHost', + 'GetHostPort', + 'GetOwner', + 'GetPath', + 'GetPort', + 'GetScheme', + 'GetSchemeHost', + 'GetSchemeHostPort', + 'GetSignificantDomain', + 'GetTLD', + 'GetTail', + 'HostNameToPunycode', + 'IsAllowedByRobotsTxt', + 'IsKnownTLD', + 'IsWellKnownTLD', + 'Normalize', + 'NormalizeWithDefaultHttpScheme', + 'Parse', + 'PunycodeToHostName', + 'QueryStringToDict', + 'QueryStringToList', + ], + + Yson: [ + 'Attributes', + 'Contains', + 'ConvertTo', + 'ConvertToBool', + 'ConvertToBoolDict', + 'ConvertToBoolList', + 'ConvertToDict', + 'ConvertToDouble', + 'ConvertToDoubleDict', + 'ConvertToDoubleList', + 'ConvertToInt64', + 'ConvertToInt64Dict', + 'ConvertToInt64List', + 'ConvertToList', + 'ConvertToString', + 'ConvertToStringDict', + 'ConvertToStringList', + 'ConvertToUint64', + 'ConvertToUint64Dict', + 'ConvertToUint64List', + 'Equals', + 'From', + 'GetHash', + 'GetLength', + 'IsBool', + 'IsDict', + 'IsDouble', + 'IsEntity', + 'IsInt64', + 'IsList', + 'IsString', + 'IsUint64', + 'Lookup', + 'LookupBool', + 'LookupDict', + 'LookupDouble', + 'LookupInt64', + 'LookupList', + 'LookupString', + 'LookupUint64', + 'Options', + 'Parse', + 'ParseJson', + 'ParseJsonDecodeUtf8', + 'Serialize', + 'SerializeJson', + 'SerializePretty', + 'SerializeText', + 'WithAttributes', + 'YPath', + 'YPathBool', + 'YPathDict', + 'YPathDouble', + 'YPathInt64', + 'YPathList', + 'YPathString', + 'YPathUint64', + ], + + HyperLogLog: ['AddValue', 'Create', 'Deserialize', 'GetResult', 'Merge', 'Serialize'], + Hyperscan: [ + 'BacktrackingGrep', + 'BacktrackingMatch', + 'Capture', + 'Grep', + 'Match', + 'MultiGrep', + 'MultiMatch', + 'Replace', + ], + Ip: [ + 'ConvertToIPv6', + 'FromString', + 'GetSubnet', + 'GetSubnetByMask', + 'IsEmbeddedIPv4', + 'IsIPv4', + 'IsIPv6', + 'SubnetFromString', + 'SubnetMatch', + 'SubnetToString', + 'ToFixedIPv6String', + 'ToString', + ], + Json: [ + 'BoolAsJsonNode', + 'CompilePath', + 'DoubleAsJsonNode', + 'JsonAsJsonNode', + 'JsonDocumentSqlExists', + 'JsonDocumentSqlQuery', + 'JsonDocumentSqlQueryConditionalWrap', + 'JsonDocumentSqlQueryWrap', + 'JsonDocumentSqlTryExists', + 'JsonDocumentSqlValueBool', + 'JsonDocumentSqlValueConvertToUtf8', + 'JsonDocumentSqlValueInt64', + 'JsonDocumentSqlValueNumber', + 'JsonDocumentSqlValueUtf8', + 'Parse', + 'Serialize', + 'SerializeToJsonDocument', + 'SqlExists', + 'SqlQuery', + 'SqlQueryConditionalWrap', + 'SqlQueryWrap', + 'SqlTryExists', + 'SqlValueBool', + 'SqlValueConvertToUtf8', + 'SqlValueInt64', + 'SqlValueNumber', + 'SqlValueUtf8', + 'Utf8AsJsonNode', + ], + Math: [ + 'Abs', + 'Acos', + 'Asin', + 'Asinh', + 'Atan', + 'Atan2', + 'Cbrt', + 'Ceil', + 'Cos', + 'Cosh', + 'E', + 'Eps', + 'Erf', + 'ErfInv', + 'ErfcInv', + 'Exp', + 'Exp2', + 'Fabs', + 'Floor', + 'Fmod', + 'FuzzyEquals', + 'Hypot', + 'IsFinite', + 'IsInf', + 'IsNaN', + 'Ldexp', + 'Lgamma', + 'Log', + 'Log10', + 'Log2', + 'Mod', + 'NearbyInt', + 'Pi', + 'Pow', + 'Rem', + 'Remainder', + 'Rint', + 'Round', + 'RoundDownward', + 'RoundToNearest', + 'RoundTowardZero', + 'RoundUpward', + 'Sigmoid', + 'Sin', + 'Sinh', + 'Sqrt', + 'Tan', + 'Tanh', + 'Tgamma', + 'Trunc', + ], + + Pire: ['Capture', 'Grep', 'Match', 'MultiGrep', 'MultiMatch', 'Replace'], + + Re2: [ + 'Capture', + 'Count', + 'Escape', + 'FindAndConsume', + 'Grep', + 'Match', + 'Options', + 'PatternFromLike', + 'Replace', + ], + + Re2posix: [ + 'Capture', + 'Count', + 'Escape', + 'FindAndConsume', + 'Grep', + 'Match', + 'Options', + 'PatternFromLike', + 'Replace', + ], + + Protobuf: ['Parse', 'Serialize', 'TryParse'], + + Digest: [ + 'Argon2', + 'Blake2B', + 'CityHash', + 'CityHash128', + 'Crc32c', + 'Crc64', + 'FarmHashFingerprint', + 'FarmHashFingerprint128', + 'FarmHashFingerprint2', + 'FarmHashFingerprint32', + 'FarmHashFingerprint64', + 'Fnv32', + 'Fnv64', + 'HighwayHash', + 'IntHash64', + 'Md5HalfMix', + 'Md5Hex', + 'Md5Raw', + 'MurMurHash', + 'MurMurHash2A', + 'MurMurHash2A32', + 'MurMurHash32', + 'NumericHash', + 'Sha1', + 'Sha256', + 'SipHash', + 'SuperFastHash', + 'XXH3', + 'XXH3_128', + ], + + Decompress: ['BZip2', 'Brotli', 'Gzip', 'Lzma', 'Snappy', 'Xz', 'Zlib', 'Zstd'], + + Histogram: [ + 'CalcLowerBound', + 'CalcLowerBoundSafe', + 'CalcUpperBound', + 'CalcUpperBoundSafe', + 'GetSumAboveBound', + 'GetSumBelowBound', + 'GetSumInRange', + 'Normalize', + 'Print', + 'ToCumulativeDistributionFunction', + ], +}; + +export const Udfs = Object.entries(RawUdfs).reduce((acc, [udfModule, functions]) => { + const moduleFunctions = functions.map((f) => `${udfModule}::${f}`); + return acc.concat(moduleFunctions); +}, [] as string[]); + +export const WindowFunctions = [ + 'ROW_NUMBER', + 'LAG', + 'LEAD', + 'FIRST_VALUE', + 'LAST_VALUE', + 'RANK', + 'DENSE_RANK', + 'SessionState', +]; + +export const TableFunction = [ + 'CONCAT', + 'EACH', + 'RANGE', + 'LIKE', + 'REGEXP', + 'CONCAT_STRICT', + 'RANGE_STRICT', + 'FILTER', + 'FOLDER', + 'WalkFolders', +]; + +export const Pragmas = [ + 'TablePathPrefix', + 'AnsiInForEmptyOrNullableItemsCollections', + 'File', + 'Folder', + 'Library', + 'Warning', + 'package', + 'override_library', + 'AutoCommit', + 'DqEngine', + 'SimpleColumns', + 'DisableSimpleColumns', + 'CoalesceJoinKeysOnQualifiedAll', + 'StrictJoinKeyTypes', + 'AnsiRankForNullableKeys', + 'AnsiCurrentRow', + 'AnsiOrderByLimitInUnionAll', + 'OrderedColumns', + 'DisableOrderedColumns', + 'PositionalUnionAll', + 'RegexUseRe2', + 'ClassicDivision', + 'AllowDotInAlias', + 'WarnUnnamedColumns', + 'GroupByLimit', + 'GroupByCubeLimit', + 'config.flags', + 'kikimr.IsolationLevel', + 'Kikimr.ScanQuery', +]; diff --git a/src/utils/yqlSuggestions/generateSuggestions.ts b/src/utils/yqlSuggestions/generateSuggestions.ts new file mode 100644 index 0000000000..ea18952c89 --- /dev/null +++ b/src/utils/yqlSuggestions/generateSuggestions.ts @@ -0,0 +1,309 @@ +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import { + ColumnAliasSuggestion, + KeywordSuggestion, + YqlAutocompleteResult, +} from '@gravity-ui/websql-autocomplete'; + +import { + AggregateFunctions, + Pragmas, + SimpleFunctions, + SimpleTypes, + TableFunction, + Udfs, + WindowFunctions, +} from './constants'; + +const re = /[\s'"-/@]/; + +function wrapStringToBackticks(value: string) { + let result = value; + if (value.match(re)) { + result = `\`${value}\``; + } + return result; +} + +function removeBackticks(value: string) { + let normalizedValue = value; + if (value.startsWith('`')) { + normalizedValue = value.slice(1, -1); + } + return normalizedValue; +} + +type SuggestionType = keyof Omit; + +const SuggestionsWeight: Record = { + suggestTemplates: 0, + suggestPragmas: 1, + suggestEntity: 2, + suggestColumns: 3, + suggestColumnAliases: 4, + suggestKeywords: 5, + suggestAggregateFunctions: 6, + suggestTableFunctions: 7, + suggestWindowFunctions: 8, + suggestFunctions: 9, + suggestUdfs: 10, + suggestSimpleTypes: 11, +}; + +const KEEP_CACHE_MILLIS = 5 * 60 * 1000; + +function getColumnsWithCache() { + const cache = new Map(); + return async (path: string) => { + const normalizedPath = removeBackticks(path); + const existed = cache.get(path); + if (existed) { + return existed; + } + const columns = []; + const data = await window.api.getDescribe({path: normalizedPath}); + if (data?.Status === 'StatusSuccess') { + const desc = data.PathDescription; + if (desc?.Table?.Columns) { + for (const c of desc.Table.Columns) { + if (c.Name) { + columns.push(c.Name); + } + } + } + if (desc?.ColumnTableDescription?.Schema?.Columns) { + for (const c of desc.ColumnTableDescription.Schema.Columns) { + if (c.Name) { + columns.push(c.Name); + } + } + } + if (desc?.ExternalTableDescription?.Columns) { + for (const c of desc.ExternalTableDescription.Columns) { + if (c.Name) { + columns.push(c.Name); + } + } + } + } + + cache.set(path, columns); + setTimeout(() => { + cache.delete(path); + }, KEEP_CACHE_MILLIS); + return columns; + }; +} + +const getColumns = getColumnsWithCache(); + +function getSuggestionIndex(suggestionType: SuggestionType) { + return SuggestionsWeight[suggestionType]; +} + +async function getSimpleFunctions() { + return SimpleFunctions; +} +async function getWindowFunctions() { + return WindowFunctions; +} +async function getTableFunctions() { + return TableFunction; +} +async function getAggregateFunctions() { + return AggregateFunctions; +} +async function getPragmas() { + return Pragmas; +} +async function getUdfs() { + return Udfs; +} +async function getSimpleTypes() { + return SimpleTypes; +} + +export async function generateColumnsSuggestion( + rangeToInsertSuggestion: monaco.IRange, + suggestColumns: YqlAutocompleteResult['suggestColumns'] | undefined, + database: string, +): Promise { + if (!suggestColumns?.tables) { + return []; + } + const suggestions: monaco.languages.CompletionItem[] = []; + const multi = suggestColumns.tables.length > 1; + for (const entity of suggestColumns.tables ?? []) { + let normalizedEntityName = removeBackticks(entity.name); + // if it's relative entity path + if (!normalizedEntityName.startsWith('/')) { + normalizedEntityName = `${database}/${normalizedEntityName}`; + } + const fields = await getColumns(normalizedEntityName); + fields.forEach((columnName: string) => { + const normalizedName = wrapStringToBackticks(columnName); + let columnNameSuggestion = normalizedName; + if (entity.alias) { + columnNameSuggestion = `${entity.alias}.${normalizedName}`; + } else if (multi) { + // no need to wrap entity.name to backticks, because it's already with them if needed + columnNameSuggestion = `${entity.name}.${normalizedName}`; + } + suggestions.push({ + label: columnNameSuggestion, + insertText: columnNameSuggestion, + kind: monaco.languages.CompletionItemKind.Field, + detail: 'Column', + range: rangeToInsertSuggestion, + sortText: suggestionIndexToWeight(getSuggestionIndex('suggestColumns')), + }); + }); + } + return suggestions; +} + +export function generateColumnAliasesSuggestion( + rangeToInsertSuggestion: monaco.IRange, + suggestColumnAliases?: ColumnAliasSuggestion[], +) { + if (!suggestColumnAliases) { + return []; + } + return suggestColumnAliases?.map((columnAliasSuggestion) => ({ + label: columnAliasSuggestion.name, + insertText: columnAliasSuggestion.name, + kind: monaco.languages.CompletionItemKind.Field, + detail: 'Column alias', + range: rangeToInsertSuggestion, + sortText: suggestionIndexToWeight(getSuggestionIndex('suggestColumnAliases')), + })); +} +export function generateKeywordsSuggestion( + rangeToInsertSuggestion: monaco.IRange, + suggestKeywords?: KeywordSuggestion[], +) { + if (!suggestKeywords) { + return []; + } + return suggestKeywords?.map((keywordSuggestion) => ({ + label: keywordSuggestion.value, + insertText: keywordSuggestion.value, + kind: monaco.languages.CompletionItemKind.Keyword, + detail: 'Keyword', + range: rangeToInsertSuggestion, + sortText: suggestionIndexToWeight(getSuggestionIndex('suggestKeywords')), + })); +} + +export async function generateEntitiesSuggestion( + _rangeToInsertSuggestion: monaco.IRange, +): Promise { + return []; +} +export async function generateSimpleFunctionsSuggestion( + rangeToInsertSuggestion: monaco.IRange, +): Promise { + const functions = await getSimpleFunctions(); + return functions.map((el) => ({ + label: el, + insertText: el, + kind: monaco.languages.CompletionItemKind.Function, + detail: 'Function', + range: rangeToInsertSuggestion, + sortText: suggestionIndexToWeight(getSuggestionIndex('suggestFunctions')), + })); +} +export async function generateSimpleTypesSuggestion( + rangeToInsertSuggestion: monaco.IRange, +): Promise { + const simpleTypes = await getSimpleTypes(); + return simpleTypes.map((el) => ({ + label: el, + insertText: el, + kind: monaco.languages.CompletionItemKind.TypeParameter, + detail: 'Type', + range: rangeToInsertSuggestion, + sortText: suggestionIndexToWeight(getSuggestionIndex('suggestSimpleTypes')), + })); +} +export async function generateUdfSuggestion( + rangeToInsertSuggestion: monaco.IRange, +): Promise { + const udfs = await getUdfs(); + return udfs.map((el) => ({ + label: el, + insertText: el, + kind: monaco.languages.CompletionItemKind.Function, + detail: 'UDF', + range: rangeToInsertSuggestion, + sortText: suggestionIndexToWeight(getSuggestionIndex('suggestUdfs')), + })); +} +export async function generateWindowFunctionsSuggestion( + rangeToInsertSuggestion: monaco.IRange, +): Promise { + const windowFunctions = await getWindowFunctions(); + return windowFunctions.map((el) => ({ + label: el, + insertText: el, + kind: monaco.languages.CompletionItemKind.Function, + detail: 'Window function', + range: rangeToInsertSuggestion, + sortText: suggestionIndexToWeight(getSuggestionIndex('suggestWindowFunctions')), + })); +} +export async function generateTableFunctionsSuggestion( + rangeToInsertSuggestion: monaco.IRange, +): Promise { + const tableFunctions = await getTableFunctions(); + return tableFunctions.map((el) => ({ + label: el, + insertText: el, + kind: monaco.languages.CompletionItemKind.Function, + detail: 'Table function', + range: rangeToInsertSuggestion, + sortText: suggestionIndexToWeight(getSuggestionIndex('suggestTableFunctions')), + })); +} +export async function generateAggregateFunctionsSuggestion( + rangeToInsertSuggestion: monaco.IRange, +): Promise { + const aggreagteFunctions = await getAggregateFunctions(); + return aggreagteFunctions.map((el) => ({ + label: el, + insertText: el, + kind: monaco.languages.CompletionItemKind.Function, + detail: 'Aggregate function', + range: rangeToInsertSuggestion, + sortText: suggestionIndexToWeight(getSuggestionIndex('suggestAggregateFunctions')), + })); +} +export async function generatePragmasSuggestion( + rangeToInsertSuggestion: monaco.IRange, +): Promise { + const pragmas = await getPragmas(); + return pragmas.map((el) => ({ + label: el, + insertText: el, + kind: monaco.languages.CompletionItemKind.Module, + detail: 'Pragma', + range: rangeToInsertSuggestion, + sortText: suggestionIndexToWeight(getSuggestionIndex('suggestPragmas')), + })); +} + +const alphabet = 'abcdefghijklmnopqrstuvwxyz'; + +function suggestionIndexToWeight(index: number): string { + const characterInsideAlphabet = alphabet[index]; + if (characterInsideAlphabet) { + return characterInsideAlphabet; + } + + const duplicateTimes = Math.floor(index / alphabet.length); + const remains = index % alphabet.length; + + const lastCharacter = alphabet.slice(-1); + + return lastCharacter.repeat(duplicateTimes) + alphabet[remains]; +} diff --git a/src/utils/yqlSuggestions/yqlSuggestions.ts b/src/utils/yqlSuggestions/yqlSuggestions.ts new file mode 100644 index 0000000000..d4a8cc5b7f --- /dev/null +++ b/src/utils/yqlSuggestions/yqlSuggestions.ts @@ -0,0 +1,135 @@ +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import {CursorPosition, parseYqlQuery} from '@gravity-ui/websql-autocomplete'; + +import { + generateAggregateFunctionsSuggestion, + generateColumnAliasesSuggestion, + generateColumnsSuggestion, + generateEntitiesSuggestion, + generateKeywordsSuggestion, + generatePragmasSuggestion, + generateSimpleFunctionsSuggestion, + generateSimpleTypesSuggestion, + generateTableFunctionsSuggestion, + generateUdfSuggestion, + generateWindowFunctionsSuggestion, +} from './generateSuggestions'; + +export function createProvideSuggestionsFunction(database: string) { + return async ( + model: monaco.editor.ITextModel, + monacoCursorPosition: monaco.Position, + _context: monaco.languages.CompletionContext, + _token: monaco.CancellationToken, + ) => { + const cursorPosition: CursorPosition = { + line: monacoCursorPosition.lineNumber, + column: monacoCursorPosition.column, + }; + const rangeToInsertSuggestion = getRangeToInsertSuggestion(model, monacoCursorPosition); + + const suggestions = await getSuggestions( + model, + cursorPosition, + rangeToInsertSuggestion, + database, + ); + + return {suggestions}; + }; +} + +async function getSuggestions( + model: monaco.editor.ITextModel, + cursorPosition: CursorPosition, + rangeToInsertSuggestion: monaco.IRange, + database: string, +): Promise { + const parseResult = parseYqlQuery(model.getValue(), cursorPosition); + let entitiesSuggestions: monaco.languages.CompletionItem[] = []; + let functionsSuggestions: monaco.languages.CompletionItem[] = []; + let aggregateFunctionsSuggestions: monaco.languages.CompletionItem[] = []; + let windowFunctionsSuggestions: monaco.languages.CompletionItem[] = []; + let tableFunctionsSuggestions: monaco.languages.CompletionItem[] = []; + let udfsSuggestions: monaco.languages.CompletionItem[] = []; + let simpleTypesSuggestions: monaco.languages.CompletionItem[] = []; + let pragmasSuggestions: monaco.languages.CompletionItem[] = []; + + if (parseResult.suggestEntity) { + entitiesSuggestions = await generateEntitiesSuggestion(rangeToInsertSuggestion); + } + if (parseResult.suggestFunctions) { + functionsSuggestions = await generateSimpleFunctionsSuggestion(rangeToInsertSuggestion); + } + if (parseResult.suggestAggregateFunctions) { + aggregateFunctionsSuggestions = await generateAggregateFunctionsSuggestion( + rangeToInsertSuggestion, + ); + } + if (parseResult.suggestWindowFunctions) { + windowFunctionsSuggestions = await generateWindowFunctionsSuggestion( + rangeToInsertSuggestion, + ); + } + if (parseResult.suggestTableFunctions) { + tableFunctionsSuggestions = await generateTableFunctionsSuggestion(rangeToInsertSuggestion); + } + if (parseResult.suggestSimpleTypes) { + simpleTypesSuggestions = await generateSimpleTypesSuggestion(rangeToInsertSuggestion); + } + if (parseResult.suggestUdfs) { + udfsSuggestions = await generateUdfSuggestion(rangeToInsertSuggestion); + } + if (parseResult.suggestPragmas) { + pragmasSuggestions = await generatePragmasSuggestion(rangeToInsertSuggestion); + } + + const columnAliasSuggestion = await generateColumnAliasesSuggestion( + rangeToInsertSuggestion, + parseResult.suggestColumnAliases, + ); + + const columnsSuggestions = await generateColumnsSuggestion( + rangeToInsertSuggestion, + parseResult.suggestColumns, + database, + ); + + const keywordsSuggestions = generateKeywordsSuggestion( + rangeToInsertSuggestion, + parseResult.suggestKeywords, + ); + + const suggestions: monaco.languages.CompletionItem[] = [ + ...entitiesSuggestions, + ...functionsSuggestions, + ...windowFunctionsSuggestions, + ...tableFunctionsSuggestions, + ...udfsSuggestions, + ...simpleTypesSuggestions, + ...pragmasSuggestions, + ...columnAliasSuggestion, + ...columnsSuggestions, + ...keywordsSuggestions, + ...aggregateFunctionsSuggestions, + ]; + + return suggestions; +} + +function getRangeToInsertSuggestion( + model: monaco.editor.ITextModel, + cursorPosition: monaco.Position, +): monaco.IRange { + const {startColumn: lastWordStartColumn, endColumn: lastWordEndColumn} = + model.getWordUntilPosition(cursorPosition); + // https://github.com/microsoft/monaco-editor/discussions/3639#discussioncomment-5190373 if user already typed "$" sign, it should not be duplicated + const dollarBeforeLastWordStart = + model.getLineContent(cursorPosition.lineNumber)[lastWordStartColumn - 2] === '$' ? 1 : 0; + return { + startColumn: lastWordStartColumn - dollarBeforeLastWordStart, + startLineNumber: cursorPosition.lineNumber, + endColumn: lastWordEndColumn, + endLineNumber: cursorPosition.lineNumber, + }; +}