This is an automated email from the ASF dual-hosted git repository.

qiuxiafan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git


The following commit(s) were added to refs/heads/main by this push:
     new 9c1e29c7 feat(ui): implement the prompt and highlight for BydbQL  
(#840)
9c1e29c7 is described below

commit 9c1e29c7c52e13abf48e05e8e6de072de361a07a
Author: Fine0830 <[email protected]>
AuthorDate: Mon Nov 10 11:24:51 2025 +0800

    feat(ui): implement the prompt and highlight for BydbQL  (#840)
---
 ui/src/components/BydbQL/Index.vue          | 298 ++++++++++++++-
 ui/src/components/CodeMirror/bydbql-hint.js | 557 ++++++++++++++++++++++++++++
 ui/src/components/CodeMirror/bydbql-mode.js | 139 +++++++
 ui/src/components/CodeMirror/index.vue      | 123 +++++-
 4 files changed, 1083 insertions(+), 34 deletions(-)

diff --git a/ui/src/components/BydbQL/Index.vue 
b/ui/src/components/BydbQL/Index.vue
index 69d4dc0b..09f1588f 100644
--- a/ui/src/components/BydbQL/Index.vue
+++ b/ui/src/components/BydbQL/Index.vue
@@ -17,15 +17,23 @@
   ~ under the License.
 -->
 <script setup>
-  import { ref, computed, onMounted, nextTick } from 'vue';
+  import { ref, computed, onMounted } from 'vue';
   import { ElMessage } from 'element-plus';
-  import { executeBydbQLQuery } from '@/api/index';
+  import {
+    executeBydbQLQuery,
+    getGroupList,
+    getAllTypesOfResourceList,
+    getTopNAggregationList,
+    getindexRuleList,
+    getResourceOfAllType,
+  } from '@/api/index';
   import CodeMirror from '@/components/CodeMirror/index.vue';
   import TopNTable from '@/components/common/TopNTable.vue';
   import MeasureAndStreamTable from 
'@/components/common/MeasureAndStreamTable.vue';
   import PropertyTable from '@/components/common/PropertyTable.vue';
   import TraceTable from '@/components/common/TraceTable.vue';
-  import { CatalogToGroupType } from '@/components/common/data';
+  import { CatalogToGroupType, GroupTypeToCatalog, SupportedIndexRuleTypes } 
from '@/components/common/data';
+  import { updateSchemasAndGroups } from 
'@/components/CodeMirror/bydbql-hint.js';
 
   // Default query text with example queries as comments
   const queryText = ref(`-- Example queries:
@@ -39,6 +47,7 @@ SELECT * FROM STREAM log in sw_recordsLog TIME > '-30m'`);
   const loading = ref(false);
   const error = ref(null);
   const executionTime = ref(0);
+  const codeMirrorInstance = ref(null);
 
   const hasResult = computed(() => queryResult.value !== null);
   const resultType = computed(() => {
@@ -55,7 +64,6 @@ SELECT * FROM STREAM log in sw_recordsLog TIME > '-30m'`);
     if (!queryResult.value) return [];
 
     try {
-      // Handle TopN results differently
       if (queryResult.value.topnResult?.lists) {
         const topnLists = queryResult.value.topnResult.lists;
         const rows = topnLists
@@ -273,19 +281,273 @@ SELECT * FROM STREAM log in sw_recordsLog TIME > 
'-30m'`);
     error.value = null;
     executionTime.value = 0;
   }
-  // Setup keyboard shortcuts for CodeMirror
-  onMounted(() => {
-    nextTick(() => {
-      // Find the CodeMirror instance and add keyboard shortcuts
-      const codeMirrorElement = document.querySelector('.query-input 
.CodeMirror');
-      if (codeMirrorElement && codeMirrorElement.CodeMirror) {
-        const cm = codeMirrorElement.CodeMirror;
-        cm.setOption('extraKeys', {
-          'Ctrl-Enter': executeQuery,
-          'Cmd-Enter': executeQuery,
-        });
+
+  function onCodeMirrorReady(cm) {
+    codeMirrorInstance.value = cm;
+    const currentExtraKeys = cm.getOption('extraKeys') || {};
+    const mergedExtraKeys = {
+      ...currentExtraKeys,
+      'Ctrl-Enter': executeQuery,
+      'Cmd-Enter': executeQuery,
+    };
+    cm.setOption('extraKeys', mergedExtraKeys);
+  }
+
+  // Fetch groups and schemas for autocomplete
+  async function fetchSchemaData() {
+    try {
+      const groupResponse = await getGroupList();
+      if (groupResponse.error) {
+        console.error('Failed to fetch groups:', groupResponse.error);
+        return;
       }
-    });
+
+      // Only include groups with valid catalog types (property, stream, 
trace, measure, topn)
+      const groups = (groupResponse.group || [])
+        .filter((g) => CatalogToGroupType[g.catalog])
+        .map((g) => g.metadata.name);
+      const schemaSets = {
+        stream: new Set(),
+        measure: new Set(),
+        trace: new Set(),
+        property: new Set(),
+        topn: new Set(),
+      };
+      const schemaGroupMap = {
+        stream: {},
+        measure: {},
+        trace: {},
+        property: {},
+        topn: {},
+      };
+      const schemaDetailSets = {
+        stream: {},
+        measure: {},
+        trace: {},
+        property: {},
+        topn: {},
+      };
+      const indexRuleSets = {};
+      const indexRuleGroupMap = {};
+      const indexRuleNameLookup = {};
+
+      const groupsData = groupResponse.group || [];
+
+      const collectTagNames = (schemaLike) => {
+        const tags = new Set();
+        if (!schemaLike) {
+          return tags;
+        }
+        const addTagName = (tagName) => {
+          if (typeof tagName === 'string' && tagName.trim().length > 0) {
+            tags.add(tagName);
+          }
+        };
+
+        const tagFamilies = schemaLike.tagFamilies || schemaLike.tag_families 
|| [];
+        tagFamilies.forEach((family) => {
+          (family?.tags || []).forEach((tag) => {
+            addTagName(tag?.name || tag?.key || tag?.metadata?.name);
+          });
+        });
+
+        const explicitTags = schemaLike.tags || schemaLike.tagNames || [];
+        explicitTags.forEach((tag) => {
+          if (typeof tag === 'string') {
+            addTagName(tag);
+          } else {
+            addTagName(tag?.name || tag?.key || tag?.metadata?.name);
+          }
+        });
+
+        const entityTagNames = schemaLike.entity?.tagNames || 
schemaLike.entity?.tag_names || [];
+        entityTagNames.forEach(addTagName);
+
+        const shardingKeyTagNames = schemaLike.shardingKey?.tagNames || 
schemaLike.shardingKey?.tag_names || [];
+        shardingKeyTagNames.forEach(addTagName);
+
+        addTagName(schemaLike.traceIdTagName || schemaLike.trace_id_tag_name);
+        addTagName(schemaLike.spanIdTagName || schemaLike.span_id_tag_name);
+        addTagName(schemaLike.timestampTagName || 
schemaLike.timestamp_tag_name);
+
+        return tags;
+      };
+
+      const emitUpdate = () => {
+        const schemas = 
Object.fromEntries(Object.entries(schemaSets).map(([key, value]) => [key, 
[...value]]));
+        const schemaToGroups = Object.fromEntries(
+          Object.entries(schemaGroupMap).map(([type, map]) => [
+            type,
+            Object.fromEntries(
+              Object.entries(map).map(([schema, groupSet]) => [
+                schema,
+                [...groupSet].sort((a, b) => a.localeCompare(b)),
+              ]),
+            ),
+          ]),
+        );
+        const schemaDetails = Object.fromEntries(
+          Object.entries(schemaDetailSets).map(([type, schemaMap]) => [
+            type,
+            Object.fromEntries(
+              Object.entries(schemaMap).map(([schemaName, detailSets]) => [
+                schemaName,
+                {
+                  tags: [...detailSets.tags].sort((a, b) => 
a.localeCompare(b)),
+                },
+              ]),
+            ),
+          ]),
+        );
+
+        const indexRuleSchemas = Object.fromEntries(
+          Object.entries(indexRuleSets).map(([type, set]) => [type, 
[...set].sort((a, b) => a.localeCompare(b))]),
+        );
+        const indexRuleGroups = Object.fromEntries(
+          Object.entries(indexRuleGroupMap).map(([type, ruleMap]) => [
+            type,
+            Object.fromEntries(
+              Object.entries(ruleMap).map(([rule, groupSet]) => [
+                rule,
+                [...groupSet].sort((a, b) => a.localeCompare(b)),
+              ]),
+            ),
+          ]),
+        );
+
+        updateSchemasAndGroups(groups, schemas, schemaToGroups, {
+          indexRuleSchemas,
+          indexRuleGroups,
+          indexRuleNameLookup,
+          schemaDetails,
+        });
+      };
+
+      const processGroup = async (group) => {
+        const groupName = group.metadata.name;
+        const catalog = group.catalog;
+        const type = CatalogToGroupType[catalog];
+
+        if (!type) {
+          return;
+        }
+
+        try {
+          const schemaResponse = await getAllTypesOfResourceList(type, 
groupName);
+          if (!schemaResponse.error) {
+            const schemaList = schemaResponse[type === 
CatalogToGroupType.CATALOG_PROPERTY ? 'properties' : type] || [];
+            for (const schema of schemaList) {
+              const name = schema?.metadata?.name;
+              if (!name) {
+                continue;
+              }
+              const lowerName = name.toLowerCase();
+              schemaSets[type].add(name);
+              if (!schemaGroupMap[type][lowerName]) {
+                schemaGroupMap[type][lowerName] = new Set();
+              }
+              schemaGroupMap[type][lowerName].add(groupName);
+
+              if (!schemaDetailSets[type][lowerName]) {
+                schemaDetailSets[type][lowerName] = {
+                  tags: new Set(),
+                };
+              }
+
+              const detailEntry = schemaDetailSets[type][lowerName];
+              collectTagNames(schema).forEach((tag) => 
detailEntry.tags.add(tag));
+
+              if (
+                (type === CatalogToGroupType.CATALOG_PROPERTY || type === 
CatalogToGroupType.CATALOG_TRACE) &&
+                detailEntry.tags.size === 0
+              ) {
+                try {
+                  const detailResponse = await getResourceOfAllType(type, 
groupName, name);
+                  const detailSchema =
+                    (detailResponse && typeof detailResponse === 'object' ? 
detailResponse?.[type] : null) || null;
+                  collectTagNames(detailSchema).forEach((tag) => 
detailEntry.tags.add(tag));
+                } catch (err) {
+                  console.error(`Failed to fetch ${type} schema detail for 
${name} in group ${groupName}:`, err);
+                }
+              }
+            }
+          }
+        } catch (e) {
+          console.error(`Failed to fetch ${type} schemas for group 
${groupName}:`, e);
+        }
+        const normalizedType = typeof type === 'string' ? type.toLowerCase() : 
type;
+        if (SupportedIndexRuleTypes.includes(normalizedType)) {
+          if (!indexRuleSets[normalizedType]) {
+            indexRuleSets[normalizedType] = new Set();
+          }
+          if (!indexRuleGroupMap[normalizedType]) {
+            indexRuleGroupMap[normalizedType] = {};
+          }
+          if (!indexRuleNameLookup[normalizedType]) {
+            indexRuleNameLookup[normalizedType] = {};
+          }
+
+          try {
+            const indexRuleResponse = await getindexRuleList(groupName);
+            if (!indexRuleResponse.error) {
+              (indexRuleResponse.indexRule || [])
+                .map((s) => s.metadata?.name)
+                .filter((s) => Boolean(s) && !s.noSort)
+                .forEach((name) => {
+                  const lowerName = name.toLowerCase();
+                  indexRuleSets[normalizedType].add(name);
+                  indexRuleNameLookup[normalizedType][lowerName] = name;
+                  if (!indexRuleGroupMap[normalizedType][lowerName]) {
+                    indexRuleGroupMap[normalizedType][lowerName] = new Set();
+                  }
+                  indexRuleGroupMap[normalizedType][lowerName].add(groupName);
+                });
+            }
+          } catch (e) {
+            console.error(`Failed to fetch index rule schemas for group 
${groupName}:`, e);
+          }
+        }
+
+        if (catalog === GroupTypeToCatalog.measure) {
+          try {
+            const topnResponse = await getTopNAggregationList(groupName);
+            if (!topnResponse.error) {
+              (topnResponse.topNAggregation || [])
+                .map((s) => s.metadata?.name)
+                .filter(Boolean)
+                .forEach((name) => {
+                  const lowerName = name.toLowerCase();
+                  schemaSets.topn.add(name);
+                  if (!schemaGroupMap.topn[lowerName]) {
+                    schemaGroupMap.topn[lowerName] = new Set();
+                  }
+                  schemaGroupMap.topn[lowerName].add(groupName);
+                });
+            }
+          } catch (e) {
+            console.error(`Failed to fetch topn schemas for group 
${groupName}:`, e);
+          }
+        }
+
+        emitUpdate();
+      };
+
+      emitUpdate();
+
+      groupsData.forEach((group) => {
+        processGroup(group).catch((e) => {
+          const groupName = group?.metadata?.name || 'unknown';
+          console.error(`Failed to process group ${groupName}:`, e);
+        });
+      });
+    } catch (e) {
+      console.error('Failed to fetch schema data:', e);
+    }
+  }
+
+  // Setup on mount
+  onMounted(() => {
+    fetchSchemaData();
   });
 </script>
 
@@ -304,12 +566,14 @@ SELECT * FROM STREAM log in sw_recordsLog TIME > '-30m'`);
       <div class="query-input-container">
         <CodeMirror
           v-model="queryText"
-          :mode="'sql'"
+          :mode="'bydbql'"
           :lint="false"
           :readonly="false"
           :style-active-line="true"
           :auto-refresh="true"
+          :enable-hint="true"
           class="query-input"
+          @ready="onCodeMirrorReady"
         />
       </div>
     </el-card>
diff --git a/ui/src/components/CodeMirror/bydbql-hint.js 
b/ui/src/components/CodeMirror/bydbql-hint.js
new file mode 100644
index 00000000..a33e8d7a
--- /dev/null
+++ b/ui/src/components/CodeMirror/bydbql-hint.js
@@ -0,0 +1,557 @@
+/*
+ * Licensed to Apache Software Foundation (ASF) under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Apache Software Foundation (ASF) licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import CodeMirror from 'codemirror';
+import { SupportedIndexRuleTypes } from '../common/data';
+
+// BydbQL keywords
+const BYDBQL_KEYWORDS = [
+  'SELECT',
+  'FROM',
+  'WHERE',
+  'ORDER BY',
+  'LIMIT',
+  'OFFSET',
+  'AND',
+  'OR',
+  'NOT',
+  'IN',
+  'LIKE',
+  'BETWEEN',
+  'IS',
+  'NULL',
+  'TRUE',
+  'FALSE',
+  'AS',
+  'DISTINCT',
+  'GROUP BY',
+  'HAVING',
+  'TIME',
+  'ASC',
+  'DESC',
+  'SHOW',
+  'TOP',
+  'UNION',
+  'INTERSECT',
+  'EXCEPT',
+  'AGGREGATE BY',
+  'WITH QUERY_TRACE',
+  'ON',
+  'STAGES',
+  'GROUP',
+  'BY',
+  'MATCH',
+  'SUM',
+  'MEAN',
+  'AVG',
+  'COUNT',
+  'MAX',
+  'MIN',
+  'TAG',
+];
+
+// BydbQL entity types
+const ENTITY_TYPES = ['STREAM', 'MEASURE', 'TRACE', 'PROPERTY', 'TOPN'];
+let schemasAndGroups = {
+  groups: [],
+  schemas: {},
+  schemaToGroups: {},
+  indexRulesByType: {},
+  indexRulesByGroup: {},
+  schemaDetails: {},
+  typeProjections: {},
+  globalProjections: { tags: [] },
+};
+
+export function updateSchemasAndGroups(groups, schemas, schemaToGroups, 
indexRuleData = {}) {
+  schemasAndGroups.groups = groups || [];
+  schemasAndGroups.schemas = schemas || {};
+  schemasAndGroups.schemaToGroups = schemaToGroups || {};
+
+  const { indexRuleSchemas = {}, indexRuleGroups = {}, indexRuleNameLookup = 
{}, schemaDetails = {} } = indexRuleData;
+
+  const indexRulesByType = {};
+  for (const [type, rules] of Object.entries(indexRuleSchemas)) {
+    const normalizedType = typeof type === 'string' ? type.toLowerCase() : 
type;
+    const uniqueRules = Array.from(new Set((rules || []).filter((rule) => 
typeof rule === 'string')));
+    indexRulesByType[normalizedType] = uniqueRules.sort((a, b) => 
a.localeCompare(b));
+  }
+
+  const indexRulesByGroup = {};
+  for (const [type, ruleMap] of Object.entries(indexRuleGroups)) {
+    const normalizedType = typeof type === 'string' ? type.toLowerCase() : 
type;
+    const typeNameLookup = indexRuleNameLookup?.[type] || 
indexRuleNameLookup?.[normalizedType] || {};
+    const groupRuleSets = {};
+
+    for (const [ruleKey, groupList] of Object.entries(ruleMap || {})) {
+      const displayName = typeNameLookup[ruleKey] || ruleKey;
+      (groupList || []).forEach((group) => {
+        const normalizedGroup = typeof group === 'string' ? 
group.toLowerCase() : group;
+        if (!normalizedGroup) {
+          return;
+        }
+        if (!groupRuleSets[normalizedGroup]) {
+          groupRuleSets[normalizedGroup] = new Set();
+        }
+        groupRuleSets[normalizedGroup].add(displayName);
+      });
+    }
+
+    indexRulesByGroup[normalizedType] = Object.fromEntries(
+      Object.entries(groupRuleSets).map(([groupKey, ruleSet]) => [
+        groupKey,
+        [...ruleSet].sort((a, b) => a.localeCompare(b)),
+      ]),
+    );
+  }
+
+  schemasAndGroups.indexRulesByType = indexRulesByType;
+  schemasAndGroups.indexRulesByGroup = indexRulesByGroup;
+
+  const normalizedSchemaDetails = {};
+  const typeProjections = {};
+  const globalTagSet = new Set();
+
+  for (const [typeKey, schemaMap] of Object.entries(schemaDetails || {})) {
+    const normalizedType = typeof typeKey === 'string' ? typeKey.toLowerCase() 
: typeKey;
+    const normalizedSchemaMap = {};
+    const typeTagSet = new Set();
+
+    for (const [schemaName, detail] of Object.entries(schemaMap || {})) {
+      const normalizedSchemaName = typeof schemaName === 'string' ? 
schemaName.toLowerCase() : schemaName;
+      const tagList = Array.from(
+        new Set((detail?.tags || []).filter((tag) => typeof tag === 'string' 
&& tag.trim().length > 0)),
+      ).sort((a, b) => a.localeCompare(b));
+      normalizedSchemaMap[normalizedSchemaName] = {
+        tags: tagList,
+      };
+
+      tagList.forEach((tag) => {
+        typeTagSet.add(tag);
+        globalTagSet.add(tag);
+      });
+    }
+
+    normalizedSchemaDetails[normalizedType] = normalizedSchemaMap;
+    typeProjections[normalizedType] = {
+      tags: Array.from(typeTagSet).sort((a, b) => a.localeCompare(b)),
+    };
+  }
+
+  schemasAndGroups.schemaDetails = normalizedSchemaDetails;
+  schemasAndGroups.typeProjections = typeProjections;
+  schemasAndGroups.globalProjections = {
+    tags: Array.from(globalTagSet).sort((a, b) => a.localeCompare(b)),
+  };
+}
+
+function getWordAt(cm, pos) {
+  const line = cm.getLine(pos.line);
+
+  let wordStart = pos.ch;
+  let wordEnd = pos.ch;
+  // Find word boundaries
+  while (wordStart > 0 && /\w/.test(line.charAt(wordStart - 1))) {
+    wordStart--;
+  }
+  while (wordEnd < line.length && /\w/.test(line.charAt(wordEnd))) {
+    wordEnd++;
+  }
+
+  return {
+    word: line.slice(wordStart, wordEnd),
+    start: wordStart,
+    end: wordEnd,
+  };
+}
+
+function extractFromClauseContext(text) {
+  const fromClauseRegex =
+    
/\bFROM\s+(STREAM|MEASURE|TRACE|PROPERTY|TOPN)\s+(\w+)(?:\s+IN\s+([\s\S]*?)(?=\bWHERE\b|\bORDER\b|\bGROUP\b|\bTIME\b|\bLIMIT\b|\bOFFSET\b|\bWITH\b|\bHAVING\b|\bON\b|$))?/gi;
+  let match;
+  let lastContext = null;
+
+  while ((match = fromClauseRegex.exec(text)) !== null) {
+    const entityType = match[1]?.toLowerCase();
+    const schemaName = match[2];
+    const rawGroups = match[3] || '';
+
+    const groups = rawGroups
+      .replace(/[()]/g, ' ')
+      .split(',')
+      .map((group) => group.trim())
+      .filter(Boolean);
+
+    lastContext = {
+      entityType,
+      schemaName,
+      groups,
+    };
+  }
+
+  return lastContext;
+}
+
+function findLastKeywordMatch(text, keyword) {
+  if (!text) {
+    return null;
+  }
+  const regex = new RegExp(`\\b${keyword}\\b`, 'gi');
+  let match;
+  let lastMatch = null;
+  while ((match = regex.exec(text)) !== null) {
+    lastMatch = match;
+  }
+  return lastMatch;
+}
+
+function detectSelectClauseContext(cm, textBeforeCursor) {
+  const selectMatch = findLastKeywordMatch(textBeforeCursor, 'SELECT');
+  if (!selectMatch) {
+    return null;
+  }
+
+  const betweenSelectAndCursor = textBeforeCursor.slice(selectMatch.index + 
selectMatch[0].length);
+  if (/\bFROM\b/i.test(betweenSelectAndCursor)) {
+    return null;
+  }
+
+  const fullText = cm.getValue();
+  const textFromSelect = fullText.slice(selectMatch.index);
+  const fromClauseRegex =
+    
/\bFROM\s+(STREAM|MEASURE|TRACE|PROPERTY|TOPN)\s+(\w+)(?:\s+IN\s+([\s\S]*?)(?=\bWHERE\b|\bORDER\b|\bGROUP\b|\bTIME\b|\bLIMIT\b|\bOFFSET\b|\bWITH\b|\bHAVING\b|\bON\b|$))?/i;
+  const fromMatch = fromClauseRegex.exec(textFromSelect);
+
+  if (!fromMatch) {
+    return { type: 'select_projection' };
+  }
+
+  const entityType = fromMatch[1]?.toLowerCase() || null;
+  const schemaName = fromMatch[2] || null;
+  const rawGroups = fromMatch[3] || '';
+  const groups = rawGroups
+    .replace(/[()]/g, ' ')
+    .split(',')
+    .map((group) => group.trim())
+    .filter(Boolean);
+
+  return {
+    type: 'select_projection',
+    entityType,
+    schemaName,
+    groups,
+  };
+}
+
+function getQueryContext(cm, cursor) {
+  const textBeforeCursor = cm.getRange({ line: 0, ch: 0 }, cursor);
+
+  const selectClauseContext = detectSelectClauseContext(cm, textBeforeCursor);
+  if (selectClauseContext) {
+    return selectClauseContext;
+  }
+
+  if (!textBeforeCursor.trim()) {
+    return { type: 'empty_line' };
+  }
+
+  const lastNewlineIndex = textBeforeCursor.lastIndexOf('\n');
+  if (lastNewlineIndex !== -1) {
+    const lastLine = textBeforeCursor.slice(lastNewlineIndex + 1);
+    if (!lastLine.trim()) {
+      return { type: 'empty_line' };
+    }
+  }
+
+  const orderByRegex = /\bORDER\s+BY\b[\s\w.,-]*$/i;
+  const orderByMatch = orderByRegex.exec(textBeforeCursor);
+  if (orderByMatch) {
+    const textBeforeOrderBy = textBeforeCursor.slice(0, orderByMatch.index);
+    const fromContext = extractFromClauseContext(textBeforeOrderBy);
+    if (fromContext && 
SupportedIndexRuleTypes.includes(fromContext.entityType)) {
+      return {
+        type: 'entity_order_by',
+        entityType: fromContext.entityType,
+        groupNames: fromContext.groups || [],
+      };
+    }
+    return { type: 'order_by' };
+  }
+
+  // Check if we're typing after 'in' (group name context)
+  const inMatch = 
textBeforeCursor.match(/\bFROM\s+(STREAM|MEASURE|TRACE|PROPERTY|TOPN)\s+(\w+)\s+in\s+(\w*)$/i);
+  if (inMatch) {
+    return { type: 'group', entityType: inMatch[1].toLowerCase(), schemaName: 
inMatch[2] };
+  }
+
+  // Check if we're typing after schema name (expecting 'in')
+  const schemaMatch = 
textBeforeCursor.match(/\bFROM\s+(STREAM|MEASURE|TRACE|PROPERTY|TOPN)\s+(\w+)\s+(\w*)$/i);
+  if (schemaMatch) {
+    return { type: 'in_keyword' };
+  }
+
+  // Check if we're typing after entity type (schema name context)
+  const entityMatch = 
textBeforeCursor.match(/\bFROM\s+(STREAM|MEASURE|TRACE|PROPERTY|TOPN)\s+(\w*)$/i);
+  if (entityMatch) {
+    return { type: 'schema', entityType: entityMatch[1].toLowerCase() };
+  }
+
+  // Check if we're typing after FROM (entity type context)
+  if (/\bFROM\s+(\w*)$/i.test(textBeforeCursor)) {
+    return { type: 'entity_type' };
+  }
+
+  // Default: suggest keywords
+  return { type: 'keyword' };
+}
+
+// Generate hint list based on context
+function generateHints(context, word) {
+  const hints = [];
+  const lowerWord = word ? word.toLowerCase() : '';
+
+  switch (context.type) {
+    case 'empty_line':
+      for (const keyword of BYDBQL_KEYWORDS) {
+        if (!lowerWord || keyword.toLowerCase().startsWith(lowerWord)) {
+          hints.push({
+            text: keyword,
+            displayText: keyword,
+            className: 'bydbql-hint-keyword',
+          });
+        }
+      }
+      break;
+
+    case 'select_projection': {
+      const seen = new Set();
+      let projectionMatches = 0;
+      const pushHint = (text, displayText, className, track = false) => {
+        if (!text) {
+          return;
+        }
+        const normalized = text.toLowerCase();
+        if (lowerWord && !normalized.startsWith(lowerWord)) {
+          return;
+        }
+        if (seen.has(normalized)) {
+          return;
+        }
+        seen.add(normalized);
+        hints.push({
+          text,
+          displayText: displayText || text,
+          className,
+        });
+        if (track) {
+          projectionMatches += 1;
+        }
+      };
+
+      pushHint('*', '*', 'bydbql-hint-keyword');
+
+      const normalizedType = context.entityType ? 
context.entityType.toLowerCase() : null;
+      const normalizedSchema = context.schemaName ? 
context.schemaName.toLowerCase() : null;
+      const schemaMeta =
+        normalizedType && normalizedSchema
+          ? 
schemasAndGroups.schemaDetails?.[normalizedType]?.[normalizedSchema]
+          : null;
+      const typeMeta = normalizedType ? 
schemasAndGroups.typeProjections?.[normalizedType] : null;
+      const globalMeta = schemasAndGroups.globalProjections || { tags: [] };
+
+      const tagCandidates = schemaMeta?.tags?.length
+        ? schemaMeta.tags
+        : typeMeta?.tags?.length
+          ? typeMeta.tags
+          : globalMeta.tags;
+      tagCandidates.forEach((tag) => pushHint(tag, `${tag}`, 
'bydbql-hint-tag', true));
+
+      if (projectionMatches === 0 && hints.length === 0) {
+        for (const keyword of BYDBQL_KEYWORDS) {
+          if (!lowerWord || keyword.toLowerCase().startsWith(lowerWord)) {
+            hints.push({
+              text: keyword,
+              displayText: keyword,
+              className: 'bydbql-hint-keyword',
+            });
+          }
+        }
+      }
+
+      break;
+    }
+
+    case 'entity_type':
+      // Suggest entity types (STREAM, MEASURE, etc.)
+      for (const type of ENTITY_TYPES) {
+        if (!lowerWord || type.toLowerCase().startsWith(lowerWord)) {
+          hints.push({
+            text: type,
+            displayText: type,
+            className: 'bydbql-hint-entity-type',
+          });
+        }
+      }
+      break;
+
+    case 'schema':
+      // Suggest schema names for the given entity type
+      const schemas = schemasAndGroups.schemas[context.entityType] || [];
+      for (const schema of schemas) {
+        if (!lowerWord || schema.toLowerCase().startsWith(lowerWord)) {
+          hints.push({
+            text: schema,
+            displayText: schema,
+            className: 'bydbql-hint-schema',
+          });
+        }
+      }
+      break;
+
+    case 'in_keyword':
+      // Suggest 'in' keyword
+      if (!lowerWord || 'in'.startsWith(lowerWord)) {
+        hints.push({
+          text: 'in',
+          displayText: 'in',
+          className: 'bydbql-hint-keyword',
+        });
+      }
+      break;
+
+    case 'group': {
+      const schemaKey = context.schemaName ? context.schemaName.toLowerCase() 
: '';
+      const schemaGroupsMap = 
schemasAndGroups.schemaToGroups[context.entityType] || {};
+      const relatedGroups = schemaKey ? schemaGroupsMap[schemaKey] || [] : [];
+      const targetGroups = relatedGroups.length > 0 ? relatedGroups : 
schemasAndGroups.groups;
+
+      for (const group of targetGroups) {
+        if (!lowerWord || group.toLowerCase().startsWith(lowerWord)) {
+          hints.push({
+            text: group,
+            displayText: group,
+            className: 'bydbql-hint-group',
+          });
+        }
+      }
+      break;
+    }
+
+    case 'entity_order_by': {
+      const entityType = context.entityType || '';
+      const normalizedGroups = (context.groupNames || []).map((group) => 
group.toLowerCase());
+      const indexRulesByGroup = 
schemasAndGroups.indexRulesByGroup?.[entityType] || {};
+      const aggregatedRuleSet = new Set();
+
+      for (const group of normalizedGroups) {
+        const rules = indexRulesByGroup[group] || [];
+        for (const rule of rules) {
+          aggregatedRuleSet.add(rule);
+        }
+      }
+
+      const fallbackRules = schemasAndGroups.indexRulesByType?.[entityType] || 
[];
+      const candidates =
+        aggregatedRuleSet.size > 0 ? [...aggregatedRuleSet].sort((a, b) => 
a.localeCompare(b)) : fallbackRules.slice();
+
+      for (const rule of candidates) {
+        if (!lowerWord || rule.toLowerCase().startsWith(lowerWord)) {
+          hints.push({
+            text: rule,
+            displayText: rule,
+            className: 'bydbql-hint-index-rule',
+          });
+        }
+      }
+
+      if (!lowerWord || 'ASC'.toLowerCase().startsWith(lowerWord)) {
+        hints.push({
+          text: 'ASC',
+          displayText: 'ASC',
+          className: 'bydbql-hint-keyword',
+        });
+      }
+      if (!lowerWord || 'DESC'.toLowerCase().startsWith(lowerWord)) {
+        hints.push({
+          text: 'DESC',
+          displayText: 'DESC',
+          className: 'bydbql-hint-keyword',
+        });
+      }
+      break;
+    }
+
+    case 'order_by': {
+      const orderKeywords = ['ASC', 'DESC'];
+      for (const keyword of orderKeywords) {
+        if (!lowerWord || keyword.toLowerCase().startsWith(lowerWord)) {
+          hints.push({
+            text: keyword,
+            displayText: keyword,
+            className: 'bydbql-hint-keyword',
+          });
+        }
+      }
+      break;
+    }
+
+    case 'keyword':
+    default:
+      for (const keyword of BYDBQL_KEYWORDS) {
+        if (!lowerWord || keyword.toLowerCase().startsWith(lowerWord)) {
+          hints.push({
+            text: keyword,
+            displayText: keyword,
+            className: 'bydbql-hint-keyword',
+          });
+        }
+      }
+
+      for (const type of ENTITY_TYPES) {
+        if (!lowerWord || type.toLowerCase().startsWith(lowerWord)) {
+          hints.push({
+            text: type,
+            displayText: type,
+            className: 'bydbql-hint-entity-type',
+          });
+        }
+      }
+      break;
+  }
+
+  return hints;
+}
+
+// Main hint function
+CodeMirror.registerHelper('hint', 'bydbql', function (cm) {
+  const cursor = cm.getCursor();
+  const { word, start, end } = getWordAt(cm, cursor);
+  const context = getQueryContext(cm, cursor);
+  const hints = generateHints(context, word);
+
+  if (hints.length === 0) {
+    return null;
+  }
+
+  return {
+    list: hints,
+    from: CodeMirror.Pos(cursor.line, start),
+    to: CodeMirror.Pos(cursor.line, end),
+  };
+});
diff --git a/ui/src/components/CodeMirror/bydbql-mode.js 
b/ui/src/components/CodeMirror/bydbql-mode.js
new file mode 100644
index 00000000..eb125d3d
--- /dev/null
+++ b/ui/src/components/CodeMirror/bydbql-mode.js
@@ -0,0 +1,139 @@
+/*
+ * Licensed to Apache Software Foundation (ASF) under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Apache Software Foundation (ASF) licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import CodeMirror from 'codemirror';
+
+// Define BydbQL mode extending SQL
+CodeMirror.defineMode('bydbql', function (config) {
+  const sqlMode = CodeMirror.getMode(config, 'text/x-sql');
+  if (!sqlMode || typeof sqlMode.token !== 'function') {
+    throw new Error(
+      "CodeMirror SQL mode ('text/x-sql') must be loaded before defining the 
BydbQL mode. Please ensure the SQL mode is imported first.",
+    );
+  }
+
+  const entityTypes = {
+    STREAM: true,
+    MEASURE: true,
+    TRACE: true,
+    PROPERTY: true,
+    TOPN: true,
+  };
+
+  // BydbQL-specific keywords
+  const bydbqlKeywords = {
+    SELECT: true,
+    FROM: true,
+    WHERE: true,
+    ORDER: true,
+    BY: true,
+    ASC: true,
+    DESC: true,
+    LIMIT: true,
+    OFFSET: true,
+    AND: true,
+    OR: true,
+    NOT: true,
+    IN: true,
+    LIKE: true,
+    BETWEEN: true,
+    IS: true,
+    NULL: true,
+    TRUE: true,
+    FALSE: true,
+    AS: true,
+    DISTINCT: true,
+    ALL: true,
+    ANY: true,
+    SOME: true,
+    EXISTS: true,
+    CASE: true,
+    WHEN: true,
+    THEN: true,
+    ELSE: true,
+    END: true,
+    UNION: true,
+    INTERSECT: true,
+    EXCEPT: true,
+    GROUP: true,
+    HAVING: true,
+    TIME: true,
+    SHOW: true,
+    TOP: true,
+    AGGREGATE: true,
+    MATCH: true,
+    SUM: true,
+    MEAN: true,
+    AVG: true,
+    COUNT: true,
+    MAX: true,
+    MIN: true,
+    TAG: true,
+  };
+
+  const isWord = (value) => /^[A-Za-z_]\w*$/.test(value);
+
+  return {
+    startState: function () {
+      return {
+        sqlState: CodeMirror.startState(sqlMode),
+      };
+    },
+
+    copyState: function (state) {
+      return {
+        sqlState: CodeMirror.copyState(sqlMode, state.sqlState),
+      };
+    },
+
+    token: function (stream, state) {
+      const style = sqlMode.token(stream, state.sqlState);
+      if (style === 'comment' || style === 'string') {
+        return style;
+      }
+
+      const current = stream.current();
+      if (!isWord(current)) {
+        return style;
+      }
+
+      const upperWord = current.toUpperCase();
+      if (entityTypes[upperWord]) {
+        return 'entity-type';
+      }
+      if (bydbqlKeywords[upperWord]) {
+        return 'keyword';
+      }
+
+      return 'variable-2';
+    },
+
+    indent: function (state, textAfter) {
+      return sqlMode.indent ? sqlMode.indent(state.sqlState, textAfter) : 
CodeMirror.Pass;
+    },
+
+    electricChars: sqlMode.electricChars,
+    blockCommentStart: '/*',
+    blockCommentEnd: '*/',
+    lineComment: '--',
+  };
+});
+
+// Set MIME type for BydbQL
+CodeMirror.defineMIME('text/x-bydbql', 'bydbql');
diff --git a/ui/src/components/CodeMirror/index.vue 
b/ui/src/components/CodeMirror/index.vue
index 48403d9a..f06d0229 100644
--- a/ui/src/components/CodeMirror/index.vue
+++ b/ui/src/components/CodeMirror/index.vue
@@ -24,7 +24,7 @@
 </template>
 
 <script>
-  import { onMounted, ref, watch } from 'vue';
+  import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
   import CodeMirror from 'codemirror';
   import 'codemirror/lib/codemirror.css';
   import 'codemirror/mode/yaml/yaml.js';
@@ -32,6 +32,8 @@
   import 'codemirror/mode/css/css.js';
   import 'codemirror/addon/lint/yaml-lint.js';
   import 'codemirror/theme/dracula.css';
+  import './bydbql-mode.js';
+  import './bydbql-hint.js';
   import jsYaml from 'js-yaml';
   window.jsyaml = jsYaml;
   export default {
@@ -65,61 +67,150 @@
         type: Boolean,
         default: true,
       },
+      enableHint: {
+        type: Boolean,
+        default: false,
+      },
+      extraKeys: {
+        type: Object,
+        default: () => ({}),
+      },
     },
-    emits: ['update:modelValue'],
+    emits: ['update:modelValue', 'ready'],
     setup(props, { emit }) {
       const textarea = ref(null);
       const code = ref(props.modelValue);
       let coder;
+      let autocompleteTimeout;
+      let hintAddonLoaded = false;
+      const AUTOCOMPLETE_DELAY = 200;
+
       watch(
         () => props.modelValue,
         (val) => {
-          coder?.setValue(val);
+          const currentValue = coder?.getValue();
+          if (val !== currentValue) {
+            coder?.setValue(val);
+          }
         },
       );
+
+      // Get mode based on prop
+      const getModeString = () => {
+        if (props.mode === 'bydbql') {
+          return 'text/x-bydbql';
+        }
+        if (props.mode === 'yaml') {
+          return 'text/x-yaml';
+        }
+        if (props.mode === 'css') {
+          return 'text/css';
+        }
+        return 'text/x-yaml';
+      };
+
       const options = {
-        mode: 'text/x-yaml',
+        mode: getModeString(),
         tabSize: 2,
         theme: props.theme,
         lineNumbers: true,
         line: true,
         readOnly: props.readonly,
         lint: props.lint,
-        gutters: ['CodeMirror-lint-markers'],
+        gutters: props.lint ? ['CodeMirror-lint-markers'] : [],
         styleActiveLine: props.styleActiveLine,
         autoRefresh: props.autoRefresh,
         height: '500px',
+        extraKeys: props.extraKeys,
       };
+
       const initialize = async () => {
         try {
-          /*  let theme = `codemirror/theme/${props.theme}.css`
-        await import(theme) */
           if (props.lint) {
             await import('codemirror/addon/lint/lint.js');
             await import('codemirror/addon/lint/lint.css');
           }
-          /* if (props.mode) {
-          await import(`codemirror/mode/${props.mode}/${props.mode}.js`)
-        } */
           if (props.autoRefresh) {
             await import('codemirror/addon/display/autorefresh');
           }
           if (props.styleActiveLine) {
             await import('codemirror/addon/selection/active-line');
           }
-        } catch (e) {}
+          if (props.enableHint) {
+            await import('codemirror/addon/hint/show-hint.js');
+            await import('codemirror/addon/hint/show-hint.css');
+            hintAddonLoaded = true;
+          }
+        } catch (e) {
+          console.error('Error loading CodeMirror addons:', e);
+        }
+
         coder = CodeMirror.fromTextArea(textarea.value, options);
+
         coder.on('blur', (coder) => {
           const newValue = coder.getValue();
           emit('update:modelValue', newValue);
         });
+
+        // Enable automatic autocomplete when typing (on keyup events)
+        if (props.enableHint && hintAddonLoaded) {
+          coder.on('keyup', (cm, event) => {
+            // Don't show hints for special keys
+            const excludedKeys = [
+              8, // Backspace
+              9, // Tab
+              13, // Enter
+              16,
+              17,
+              18, // Shift, Ctrl, Alt
+              20, // Caps Lock
+              27, // Escape
+              33,
+              34,
+              35,
+              36,
+              37,
+              38,
+              39,
+              40, // Page/Arrow keys
+            ];
+
+            if (excludedKeys.includes(event.keyCode)) {
+              return;
+            }
+
+            if (autocompleteTimeout) {
+              clearTimeout(autocompleteTimeout);
+            }
+
+            autocompleteTimeout = setTimeout(() => {
+              if (!cm.state.completionActive) {
+                CodeMirror.commands.autocomplete(cm, CodeMirror.hint.bydbql, { 
completeSingle: false });
+              }
+            }, AUTOCOMPLETE_DELAY);
+          });
+        } else if (props.enableHint && !hintAddonLoaded) {
+          console.warn('CodeMirror hint addon failed to load; autocomplete is 
disabled.');
+        }
+
+        // Emit ready event with coder instance
+        emit('ready', coder);
       };
+
       onMounted(() => {
         initialize();
       });
+
+      onBeforeUnmount(() => {
+        if (autocompleteTimeout) {
+          clearTimeout(autocompleteTimeout);
+        }
+      });
+
       const checkYaml = async (val) => {
         jsYaml.load(val);
       };
+
       return {
         code,
         options,
@@ -139,13 +230,11 @@
       height: 100%;
       width: 100%;
       .CodeMirror-code {
-        line-height: 19px;
+        line-height: 20px;
       }
     }
-  }
-</style>
-<style>
-  .CodeMirror-lint-tooltip {
-    z-index: 10000 !important;
+    :deep(.cm-entity-type) {
+      color: #bd93f9;
+    }
   }
 </style>


Reply via email to