diff --git a/packages/web/src/query/SqlEditor.svelte b/packages/web/src/query/SqlEditor.svelte
index e94f5b5aa..aaefede79 100644
--- a/packages/web/src/query/SqlEditor.svelte
+++ b/packages/web/src/query/SqlEditor.svelte
@@ -9,11 +9,20 @@
-
+
diff --git a/packages/web/src/query/analyseQuerySources.ts b/packages/web/src/query/analyseQuerySources.ts
new file mode 100644
index 000000000..d9e255540
--- /dev/null
+++ b/packages/web/src/query/analyseQuerySources.ts
@@ -0,0 +1,38 @@
+export default function analyseQuerySources(sql, sourceNames) {
+ const upperSourceNames = sourceNames.map(x => x.toUpperCase());
+ const tokens = sql.split(/\s+/);
+ const res = [];
+ for (let i = 0; i < tokens.length; i += 1) {
+ const lastWordMatch = tokens[i].match(/([^.]+)$/);
+ if (lastWordMatch) {
+ const word = lastWordMatch[1];
+ const wordUpper = word.toUpperCase();
+ if (upperSourceNames.includes(wordUpper)) {
+ const preWord = tokens[i - 1];
+ if (preWord && /^((join)|(from)|(update)|(delete)|(insert))$/i.test(preWord)) {
+ let postWord = tokens[i + 1];
+ if (postWord && /^as$/i.test(postWord)) {
+ postWord = tokens[i + 2];
+ }
+ if (!postWord) {
+ res.push({
+ name: word,
+ });
+ } else if (
+ /^((where)|(inner)|(left)|(right)|(on)|(join))$/i.test(postWord) ||
+ !/^[a-zA-Z][a-zA-Z0-9]*$/i.test(postWord)
+ ) {
+ res.push({
+ name: word,
+ });
+ } else
+ res.push({
+ name: word,
+ alias: postWord,
+ });
+ }
+ }
+ }
+ }
+ return res;
+}
diff --git a/packages/web/src/query/codeCompletion.ts b/packages/web/src/query/codeCompletion.ts
new file mode 100644
index 000000000..e36c191bc
--- /dev/null
+++ b/packages/web/src/query/codeCompletion.ts
@@ -0,0 +1,122 @@
+import { addCompleter, setCompleters } from 'ace-builds/src-noconflict/ext-language_tools';
+import { getDatabaseInfo } from '../utility/metadataLoaders';
+import analyseQuerySources from './analyseQuerySources';
+
+const COMMON_KEYWORDS = [
+ 'select',
+ 'where',
+ 'update',
+ 'delete',
+ 'group',
+ 'order',
+ 'from',
+ 'by',
+ 'create',
+ 'table',
+ 'drop',
+ 'alter',
+ 'view',
+ 'execute',
+ 'procedure',
+ 'distinct',
+ 'go',
+];
+
+export function mountCodeCompletion({ conid, database, editor }) {
+ setCompleters([]);
+ addCompleter({
+ getCompletions: async function (editor, session, pos, prefix, callback) {
+ const cursor = session.selection.cursor;
+ const line = session.getLine(cursor.row).slice(0, cursor.column);
+ const dbinfo = await getDatabaseInfo({ conid, database });
+
+ let list = COMMON_KEYWORDS.map(word => ({
+ name: word,
+ value: word,
+ caption: word,
+ meta: 'keyword',
+ score: 800,
+ }));
+
+ if (dbinfo) {
+ const colMatch = line.match(/([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)?$/);
+ if (colMatch) {
+ const table = colMatch[1];
+ const sources = analyseQuerySources(editor.getValue(), [
+ ...dbinfo.tables.map(x => x.pureName),
+ ...dbinfo.views.map(x => x.pureName),
+ ]);
+ const source = sources.find(x => (x.alias || x.name) == table);
+ console.log('sources', sources);
+ console.log('table', table, source);
+ if (source) {
+ const table = dbinfo.tables.find(x => x.pureName == source.name);
+ if (table) {
+ list = [
+ ...table.columns.map(x => ({
+ name: x.columnName,
+ value: x.columnName,
+ caption: x.columnName,
+ meta: 'column',
+ score: 1000,
+ })),
+ ];
+ }
+ }
+ } else {
+ list = [
+ ...list,
+ ...dbinfo.tables.map(x => ({
+ name: x.pureName,
+ value: x.pureName,
+ caption: x.pureName,
+ meta: 'table',
+ score: 1000,
+ })),
+ ...dbinfo.views.map(x => ({
+ name: x.pureName,
+ value: x.pureName,
+ caption: x.pureName,
+ meta: 'view',
+ score: 1000,
+ })),
+ ];
+ }
+ }
+
+ // if (/(join)|(from)|(update)|(delete)|(insert)\s*([a-zA-Z0-9_]*)?$/i.test(line)) {
+ // if (dbinfo) {
+ // }
+ // }
+
+ callback(null, list);
+ },
+ });
+
+ const doLiveAutocomplete = function (e) {
+ const editor = e.editor;
+ var hasCompleter = editor.completer && editor.completer.activated;
+ const session = editor.session;
+ const cursor = session.selection.cursor;
+ const line = session.getLine(cursor.row).slice(0, cursor.column);
+
+ // We don't want to autocomplete with no prefix
+ if (e.command.name === 'backspace') {
+ // do not hide after backspace
+ } else if (e.command.name === 'insertstring') {
+ if ((!hasCompleter && /^[a-zA-Z]/.test(e.args)) || e.args == '.') {
+ editor.execCommand('startAutocomplete');
+ }
+
+ if (e.args == ' ' && /((from)|(join))\s*$/i.test(line)) {
+ editor.execCommand('startAutocomplete');
+ }
+ }
+ };
+
+ editor.commands.on('afterExec', doLiveAutocomplete);
+
+ return () => {
+ editor.commands.removeListener('afterExec', doLiveAutocomplete);
+ };
+}
diff --git a/packages/web/src/tabs/QueryTab.svelte b/packages/web/src/tabs/QueryTab.svelte
index 605804452..eaf0e69ef 100644
--- a/packages/web/src/tabs/QueryTab.svelte
+++ b/packages/web/src/tabs/QueryTab.svelte
@@ -204,6 +204,8 @@
setEditorData(e.detail)}