From 3b16851db4856b01c973478a7a3c1876914f3304 Mon Sep 17 00:00:00 2001 From: joe-re Date: Sat, 19 Feb 2022 19:31:05 +0900 Subject: [PATCH 1/9] move getFromNodeByPos function to utils --- packages/server/src/complete.ts | 26 +++----------------------- packages/server/src/complete/utils.ts | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/server/src/complete.ts b/packages/server/src/complete.ts index 28f13bfa..7c1f5a05 100644 --- a/packages/server/src/complete.ts +++ b/packages/server/src/complete.ts @@ -32,6 +32,7 @@ import { createTablesFromFromNodes, findColumnAtPosition, getAllNestedFromNodes, + getFromNodeByPos, } from './complete/utils' import { Identifier } from './complete/Identifier' @@ -110,7 +111,7 @@ class Completer { const fromNodes = getAllNestedFromNodes( parsedFromClause?.from?.tables || [] ) - const fromNodeOnCursor = this.getFromNodeByPos(fromNodes) + const fromNodeOnCursor = getFromNodeByPos(fromNodes, this.pos) if ( fromNodeOnCursor && fromNodeOnCursor.type === 'incomplete_subquery' @@ -172,27 +173,6 @@ class Completer { this.candidates.push(item) } - /** - * Finds the most deeply nested FROM node that have a range encompasing the position. - * In cases such as SELECT * FROM T1 JOIN (SELECT * FROM (SELECT * FROM T2 )) - * We will get a list of nodes like this - * SELECT * FROM T1 - * (SELECT * FROM - * (SELECT * FROM T2)) - * The idea is to reverse the list so that the most nested queries come first. Then - * apply a filter to keep only the FROM nodes which encompass the position and take - * the first one from that resulting list. - * @param fromNodes - * @param pos - * @returns - */ - getFromNodeByPos(fromNodes: FromTableNode[]) { - return fromNodes - .reverse() - .filter((tableNode) => isPosInLocation(tableNode.location, this.pos)) - .shift() - } - addCandidatesForTables(tables: Table[]) { createCandidatesForTables(tables, this.lastToken).forEach((item) => { this.addCandidate(item) @@ -399,7 +379,7 @@ class Completer { addJoinCondidates(ast: SelectStatement) { // from clause: complete 'ON' keyword on 'INNER JOIN' if (ast.type === 'select' && Array.isArray(ast.from?.tables)) { - const fromTable = this.getFromNodeByPos(ast.from?.tables || []) + const fromTable = getFromNodeByPos(ast.from?.tables || [], this.pos) if (fromTable && fromTable.type === 'table') { this.addCandidatesForTables(this.schema.tables) this.addCandidateIfStartsWithLastToken( diff --git a/packages/server/src/complete/utils.ts b/packages/server/src/complete/utils.ts index ded7664e..e48004c5 100644 --- a/packages/server/src/complete/utils.ts +++ b/packages/server/src/complete/utils.ts @@ -331,3 +331,24 @@ export function getAllNestedFromNodes( return result }) } + +/** + * Finds the most deeply nested FROM node that have a range encompasing the position. + * In cases such as SELECT * FROM T1 JOIN (SELECT * FROM (SELECT * FROM T2 )) + * We will get a list of nodes like this + * SELECT * FROM T1 + * (SELECT * FROM + * (SELECT * FROM T2)) + * The idea is to reverse the list so that the most nested queries come first. Then + * apply a filter to keep only the FROM nodes which encompass the position and take + * the first one from that resulting list. + * @param fromNodes + * @param pos + * @returns + */ +export function getFromNodeByPos(fromNodes: FromTableNode[], pos: Pos) { + return fromNodes + .reverse() + .filter((tableNode) => isPosInLocation(tableNode.location, pos)) + .shift() +} From b0c08858c04e034103e2f36d931b5f1c90bd8009 Mon Sep 17 00:00:00 2001 From: joe-re Date: Sun, 20 Feb 2022 11:43:14 +0900 Subject: [PATCH 2/9] extract create candidates logics --- packages/server/src/complete.ts | 247 +++++------------- packages/server/src/complete/Identifier.ts | 2 +- .../candidates/createAliasCandidates.ts | 13 + .../createBasicKeywordCandidates.ts | 16 ++ .../candidates/createColumnCandidates.ts | 54 ++++ .../candidates/createFunctionCandidates.ts | 22 ++ .../candidates/createJoinCandidates.ts | 30 +++ .../candidates/createJoinTableCndidates.ts | 73 ++++++ ...teKeywordCandidatesFromExpectedLiterals.ts | 45 ++++ .../createSelectAllColumnsCandidates.ts | 39 +++ .../candidates/createTableCandidates.ts | 28 ++ packages/server/src/complete/utils.ts | 59 +---- 12 files changed, 396 insertions(+), 232 deletions(-) create mode 100644 packages/server/src/complete/candidates/createAliasCandidates.ts create mode 100644 packages/server/src/complete/candidates/createBasicKeywordCandidates.ts create mode 100644 packages/server/src/complete/candidates/createColumnCandidates.ts create mode 100644 packages/server/src/complete/candidates/createFunctionCandidates.ts create mode 100644 packages/server/src/complete/candidates/createJoinCandidates.ts create mode 100644 packages/server/src/complete/candidates/createJoinTableCndidates.ts create mode 100644 packages/server/src/complete/candidates/createKeywordCandidatesFromExpectedLiterals.ts create mode 100644 packages/server/src/complete/candidates/createSelectAllColumnsCandidates.ts create mode 100644 packages/server/src/complete/candidates/createTableCandidates.ts diff --git a/packages/server/src/complete.ts b/packages/server/src/complete.ts index 7c1f5a05..213d8c40 100644 --- a/packages/server/src/complete.ts +++ b/packages/server/src/complete.ts @@ -14,43 +14,33 @@ import log4js from 'log4js' import { CompletionItem } from 'vscode-languageserver-types' import { Schema, Table } from './database_libs/AbstractClient' import { - makeTableAlias, getRidOfAfterPosString, getLastToken, - makeTableName, isPosInLocation, toCompletionItemForKeyword, - toCompletionItemForAlias, - toCompletionItemForFunction, ICONS, - createCandidatesForColumnsOfAnyTable, - createCandidatesForTables, - createCandidatesForExpectedLiterals, - getAliasFromFromTableNode, - isTableMatch, - makeColumnName, createTablesFromFromNodes, findColumnAtPosition, getAllNestedFromNodes, - getFromNodeByPos, + getNearestFromTableFromPos, } from './complete/utils' -import { Identifier } from './complete/Identifier' +import { createBasicKeywordCandidates } from './complete/candidates/createBasicKeywordCandidates' +import { createTableCandidates } from './complete/candidates/createTableCandidates' +import { createJoinCondidates } from './complete/candidates/createJoinCandidates' +import { + createCandidatesForColumnsOfAnyTable, + createCandidatesForScopedColumns, +} from './complete/candidates/createColumnCandidates' +import { createAliasCandidates } from './complete/candidates/createAliasCandidates' +import { createSelectAllColumnsCandidates } from './complete/candidates/createSelectAllColumnsCandidates' +import { createFunctionCandidates } from './complete/candidates/createFunctionCandidates' +import { createKeywordCandidatesFromExpectedLiterals } from './complete/candidates/createKeywordCandidatesFromExpectedLiterals' +import { createJoinTablesCandidates } from './complete/candidates/createJoinTableCndidates' type Pos = { line: number; column: number } const logger = log4js.getLogger() -const CLAUSES: string[] = [ - 'SELECT', - 'WHERE', - 'ORDER BY', - 'GROUP BY', - 'LIMIT', - '--', - '/*', - '(', -] - function getFromNodesFromClause(sql: string): FromClauseParserResult | null { try { return parseFromClause(sql) @@ -111,7 +101,7 @@ class Completer { const fromNodes = getAllNestedFromNodes( parsedFromClause?.from?.tables || [] ) - const fromNodeOnCursor = getFromNodeByPos(fromNodes, this.pos) + const fromNodeOnCursor = getNearestFromTableFromPos(fromNodes, this.pos) if ( fromNodeOnCursor && fromNodeOnCursor.type === 'incomplete_subquery' @@ -142,18 +132,22 @@ class Completer { } addCandidatesForBasicKeyword() { - CLAUSES.map((v) => toCompletionItemForKeyword(v)).forEach((v) => - this.addCandidateIfStartsWithLastToken(v) - ) + createBasicKeywordCandidates().forEach((v) => { + this.addCandidate(v, this.lastToken) + }) } addCandidatesForExpectedLiterals(expected: ExpectedLiteralNode[]) { - createCandidatesForExpectedLiterals(expected).forEach((v) => { - this.addCandidateIfStartsWithLastToken(v) + createKeywordCandidatesFromExpectedLiterals(expected).forEach((v) => { + this.addCandidate(v, this.lastToken) }) } - addCandidate(item: CompletionItem) { + addCandidate(item: CompletionItem, token?: string) { + // when token is passed and it doesn't matche to completion candidates, it doesn't add the item to candidates + if (token && !item.label.startsWith(token)) { + return + } // JupyterLab requires the dot or space character preceeding the key pressed // If the dot or space character are not added to the label then searching // in the list of suggestion does not work. @@ -174,7 +168,7 @@ class Completer { } addCandidatesForTables(tables: Table[]) { - createCandidatesForTables(tables, this.lastToken).forEach((item) => { + createTableCandidates(tables, this.lastToken).forEach((item) => { this.addCandidate(item) }) } @@ -187,12 +181,6 @@ class Completer { ) } - addCandidateIfStartsWithLastToken(item: CompletionItem) { - if (item.label.startsWith(this.lastToken)) { - this.addCandidate(item) - } - } - addCandidatesForIncompleteSubquery( incompleteSubquery: IncompleteSubqueryNode ) { @@ -264,47 +252,14 @@ class Completer { expected: ExpectedLiteralNode[], fromNodes: FromTableNode[] ) { - let joinType = '' - if ('INNER'.startsWith(this.lastToken)) joinType = 'INNER' - if ('LEFT'.startsWith(this.lastToken)) joinType = 'LEFT' - if ('RIGH'.startsWith(this.lastToken)) joinType = 'RIGHT' - - if (joinType && expected.map((v) => v.text).find((v) => v === 'JOIN')) { - if (fromNodes && fromNodes.length > 0) { - const fromNode = fromNodes[0] - const fromAlias = getAliasFromFromTableNode(fromNode) - const fromTable = this.schema.tables.find((table) => - isTableMatch(fromNode, table) - ) - - this.schema.tables - .filter((table) => table != fromTable) - .forEach((table) => { - table.columns - .filter((column) => - fromTable?.columns - .map((col) => col.columnName) - .includes(column.columnName) - ) - .map((column) => { - return { - tableName: makeTableName(table), - alias: makeTableAlias(table.tableName), - columnName: column.columnName, - } - }) - .map((match) => { - const label = `${joinType} JOIN ${match.tableName} AS ${match.alias} ON ${match.alias}.${match.columnName} = ${fromAlias}.${match.columnName}` - return { - label: label, - detail: 'utility', - kind: ICONS.UTILITY, - } - }) - .forEach((item) => this.addCandidate(item)) - }) - } - } + createJoinTablesCandidates( + this.schema.tables, + expected, + fromNodes, + this.lastToken + ).forEach((v) => { + this.addCandidate(v) + }) } addCandidatesForParsedDeleteStatement(ast: DeleteStatement) { @@ -321,20 +276,14 @@ class Completer { } } - completeSelectStatement(ast: SelectStatement) { - if (Array.isArray(ast.columns)) { - this.addCandidateIfStartsWithLastToken(toCompletionItemForKeyword('FROM')) - this.addCandidateIfStartsWithLastToken(toCompletionItemForKeyword('AS')) - } - } - addCandidatesForParsedSelectQuery(ast: SelectStatement) { this.addCandidatesForBasicKeyword() - this.completeSelectStatement(ast) + if (Array.isArray(ast.columns)) { + this.addCandidate(toCompletionItemForKeyword('FROM'), this.lastToken) + this.addCandidate(toCompletionItemForKeyword('AS'), this.lastToken) + } if (!ast.distinct) { - this.addCandidateIfStartsWithLastToken( - toCompletionItemForKeyword('DISTINCT') - ) + this.addCandidate(toCompletionItemForKeyword('DISTINCT'), this.lastToken) } const columnRef = findColumnAtPosition(ast, this.pos) if (!columnRef) { @@ -377,117 +326,51 @@ class Completer { } addJoinCondidates(ast: SelectStatement) { - // from clause: complete 'ON' keyword on 'INNER JOIN' - if (ast.type === 'select' && Array.isArray(ast.from?.tables)) { - const fromTable = getFromNodeByPos(ast.from?.tables || [], this.pos) - if (fromTable && fromTable.type === 'table') { - this.addCandidatesForTables(this.schema.tables) - this.addCandidateIfStartsWithLastToken( - toCompletionItemForKeyword('INNER JOIN') - ) - this.addCandidateIfStartsWithLastToken( - toCompletionItemForKeyword('LEFT JOIN') - ) - if (fromTable.join && !fromTable.on) { - this.addCandidateIfStartsWithLastToken( - toCompletionItemForKeyword('ON') - ) - } - } - } + createJoinCondidates( + ast, + this.schema.tables, + this.pos, + this.lastToken + ).forEach((v) => { + // TODO: need to organize + this.addCandidate(v, v.kind === ICONS.TABLE ? '' : this.lastToken) + }) } addCandidatesForFunctions() { console.time('addCandidatesForFunctions') - if (!this.lastToken) { - // Nothing was typed, return all lowercase functions - this.schema.functions - .map((func) => toCompletionItemForFunction(func)) - .forEach((item) => this.addCandidate(item)) - } else { - // If user typed the start of the function - const lower = this.lastToken.toLowerCase() - const isTypedUpper = this.lastToken != lower - this.schema.functions - // Search using lowercase prefix - .filter((v) => v.name.startsWith(lower)) - // If typed string is in upper case, then return upper case suggestions - .map((v) => { - if (isTypedUpper) v.name = v.name.toUpperCase() - return v - }) - .map((v) => toCompletionItemForFunction(v)) - .forEach((item) => this.addCandidate(item)) - } + createFunctionCandidates(this.schema.functions, this.lastToken).forEach( + (v) => { + this.addCandidate(v) + } + ) console.timeEnd('addCandidatesForFunctions') } addCandidatesForSelectStar(fromNodes: FromTableNode[], tables: Table[]) { console.time('addCandidatesForSelectStar') - tables - .flatMap((table) => { - return fromNodes - .filter((fromNode) => isTableMatch(fromNode, table)) - .map(getAliasFromFromTableNode) - .filter( - () => - this.lastToken.toUpperCase() === 'SELECT' || // complete SELECT keyword - this.lastToken === '' - ) // complete at space after SELECT - .map((alias) => { - const columnNames = table.columns - .map((col) => makeColumnName(alias, col.columnName)) - .join(',\n') - const label = `Select all columns from ${alias}` - let prefix = '' - if (this.lastToken) { - prefix = this.lastToken + '\n' - } - - return { - label: label, - insertText: prefix + columnNames, - filterText: prefix + label, - detail: 'utility', - kind: ICONS.UTILITY, - } - }) - }) - .forEach((item) => this.addCandidate(item)) + createSelectAllColumnsCandidates(fromNodes, tables, this.lastToken).forEach( + (v) => { + this.addCandidate(v) + } + ) console.timeEnd('addCandidatesForSelectStar') } addCandidatesForScopedColumns(fromNodes: FromTableNode[], tables: Table[]) { console.time('addCandidatesForScopedColumns') - tables - .flatMap((table) => { - return fromNodes - .filter((fromNode) => isTableMatch(fromNode, table)) - .map(getAliasFromFromTableNode) - .filter((alias) => this.lastToken.startsWith(alias + '.')) - .flatMap((alias) => - table.columns.map((col) => { - return new Identifier( - this.lastToken, - makeColumnName(alias, col.columnName), - col.description, - ICONS.COLUMN - ) - }) - ) - }) - .filter((item) => item.matchesLastToken()) - .map((item) => item.toCompletionItem()) - .forEach((item) => this.addCandidate(item)) + createCandidatesForScopedColumns(fromNodes, tables, this.lastToken).forEach( + (v) => { + this.addCandidate(v) + } + ) console.timeEnd('addCandidatesForScopedColumns') } addCandidatesForAliases(fromNodes: FromTableNode[]) { - fromNodes - .map((fromNode) => fromNode.as) - .filter((aliasName) => aliasName && aliasName.startsWith(this.lastToken)) - .map((aliasName) => toCompletionItemForAlias(aliasName || '')) - .forEach((item) => this.addCandidate(item)) + createAliasCandidates(fromNodes, this.lastToken).forEach((v) => { + this.addCandidate(v) + }) } } diff --git a/packages/server/src/complete/Identifier.ts b/packages/server/src/complete/Identifier.ts index 5318071a..950eb55b 100644 --- a/packages/server/src/complete/Identifier.ts +++ b/packages/server/src/complete/Identifier.ts @@ -40,7 +40,7 @@ export class Identifier { toCompletionItem(): CompletionItem { const idx = this.lastToken.lastIndexOf('.') - const label = this.identifier.substr(idx + 1) + const label = this.identifier.substring(idx + 1) let kindName: string let tableAlias = '' if (this.kind === ICONS.TABLE) { diff --git a/packages/server/src/complete/candidates/createAliasCandidates.ts b/packages/server/src/complete/candidates/createAliasCandidates.ts new file mode 100644 index 00000000..e70aba5e --- /dev/null +++ b/packages/server/src/complete/candidates/createAliasCandidates.ts @@ -0,0 +1,13 @@ +import { CompletionItem } from 'vscode-languageserver-types' +import { FromTableNode } from '@joe-re/sql-parser' +import { toCompletionItemForAlias } from '../utils' + +export function createAliasCandidates( + fromNodes: FromTableNode[], + token: string +): CompletionItem[] { + return fromNodes + .map((fromNode) => fromNode.as) + .filter((aliasName) => aliasName && aliasName.startsWith(token)) + .map((aliasName) => toCompletionItemForAlias(aliasName || '')) +} diff --git a/packages/server/src/complete/candidates/createBasicKeywordCandidates.ts b/packages/server/src/complete/candidates/createBasicKeywordCandidates.ts new file mode 100644 index 00000000..fecbb82f --- /dev/null +++ b/packages/server/src/complete/candidates/createBasicKeywordCandidates.ts @@ -0,0 +1,16 @@ +import { toCompletionItemForKeyword } from '../utils' + +const CLAUSES: string[] = [ + 'SELECT', + 'WHERE', + 'ORDER BY', + 'GROUP BY', + 'LIMIT', + '--', + '/*', + '(', +] + +export function createBasicKeywordCandidates() { + return CLAUSES.map((v) => toCompletionItemForKeyword(v)) +} diff --git a/packages/server/src/complete/candidates/createColumnCandidates.ts b/packages/server/src/complete/candidates/createColumnCandidates.ts new file mode 100644 index 00000000..98b4e4f1 --- /dev/null +++ b/packages/server/src/complete/candidates/createColumnCandidates.ts @@ -0,0 +1,54 @@ +import { CompletionItem } from 'vscode-languageserver-types' +import { FromTableNode } from '@joe-re/sql-parser' +import { Table } from '../../database_libs/AbstractClient' +import { + getAliasFromFromTableNode, + ICONS, + isTableMatch, + makeColumnName, +} from '../utils' +import { Identifier } from '../Identifier' + +export function createCandidatesForColumnsOfAnyTable( + tables: Table[], + lastToken: string +): CompletionItem[] { + return tables + .flatMap((table) => table.columns) + .map((column) => { + return new Identifier( + lastToken, + column.columnName, + column.description, + ICONS.TABLE + ) + }) + .filter((item) => item.matchesLastToken()) + .map((item) => item.toCompletionItem()) +} + +export function createCandidatesForScopedColumns( + fromNodes: FromTableNode[], + tables: Table[], + lastToken: string +): CompletionItem[] { + return tables + .flatMap((table) => { + return fromNodes + .filter((fromNode) => isTableMatch(fromNode, table)) + .map(getAliasFromFromTableNode) + .filter((alias) => lastToken.startsWith(alias + '.')) + .flatMap((alias) => + table.columns.map((col) => { + return new Identifier( + lastToken, + makeColumnName(alias, col.columnName), + col.description, + ICONS.COLUMN + ) + }) + ) + }) + .filter((item) => item.matchesLastToken()) + .map((item) => item.toCompletionItem()) +} diff --git a/packages/server/src/complete/candidates/createFunctionCandidates.ts b/packages/server/src/complete/candidates/createFunctionCandidates.ts new file mode 100644 index 00000000..4d0e1210 --- /dev/null +++ b/packages/server/src/complete/candidates/createFunctionCandidates.ts @@ -0,0 +1,22 @@ +import { toCompletionItemForFunction } from '../utils' +import { DbFunction } from '../../database_libs/AbstractClient' + +export function createFunctionCandidates( + functions: DbFunction[], + token?: string +) { + const lowerToken = token?.toLocaleLowerCase() ?? '' + const isTypedUpper = token !== lowerToken + const targets = functions.filter((v) => v.name.startsWith(lowerToken)) + return ( + targets + // Search using lowercase prefix + .filter((v) => v.name.startsWith(lowerToken)) + // If typed string is in upper case, then return upper case suggestions + .map((v) => { + if (isTypedUpper) v.name = v.name.toUpperCase() + return v + }) + .map((v) => toCompletionItemForFunction(v)) + ) +} diff --git a/packages/server/src/complete/candidates/createJoinCandidates.ts b/packages/server/src/complete/candidates/createJoinCandidates.ts new file mode 100644 index 00000000..7378c48b --- /dev/null +++ b/packages/server/src/complete/candidates/createJoinCandidates.ts @@ -0,0 +1,30 @@ +import { CompletionItem } from 'vscode-languageserver-types' +import { SelectStatement } from '@joe-re/sql-parser' +import { + getNearestFromTableFromPos, + toCompletionItemForKeyword, +} from '../utils' +import { Table } from '../../database_libs/AbstractClient' +import { createTableCandidates } from './createTableCandidates' + +type Pos = { line: number; column: number } + +export function createJoinCondidates( + ast: SelectStatement, + tables: Table[], + pos: Pos, + token: string +): CompletionItem[] { + if (!Array.isArray(ast.from?.tables)) { + return [] + } + const result: CompletionItem[] = [] + const fromTable = getNearestFromTableFromPos(ast.from?.tables || [], pos) + if (fromTable && fromTable.type === 'table') { + result.push(...createTableCandidates(tables, token)) + result.push(toCompletionItemForKeyword('INNER JOIN')) + result.push(toCompletionItemForKeyword('LEFT JOIN')) + result.push(toCompletionItemForKeyword('ON')) + } + return result +} diff --git a/packages/server/src/complete/candidates/createJoinTableCndidates.ts b/packages/server/src/complete/candidates/createJoinTableCndidates.ts new file mode 100644 index 00000000..e2c714b1 --- /dev/null +++ b/packages/server/src/complete/candidates/createJoinTableCndidates.ts @@ -0,0 +1,73 @@ +import { FromTableNode, ExpectedLiteralNode } from '@joe-re/sql-parser' +import { CompletionItem } from 'vscode-languageserver-types' +import { + getAliasFromFromTableNode, + ICONS, + isTableMatch, + makeTableAlias, + makeTableName, +} from '../utils' +import { Table } from '../../database_libs/AbstractClient' + +export function createJoinTablesCandidates( + tables: Table[], + expected: ExpectedLiteralNode[], + fromNodes: FromTableNode[], + token?: string +): CompletionItem[] { + if (!(fromNodes && fromNodes.length > 0)) { + return [] + } + + let joinType = '' + if ('INNER'.startsWith(token || '')) { + joinType = 'INNER' + } else if ('LEFT'.startsWith(token || '')) { + joinType = 'LEFT' + } else if ('RIGH'.startsWith(token || '')) { + joinType = 'RIGHT' + } + + if (!joinType) { + return [] + } + + const isExpectedJoinKeyWord = !!expected + .map((v) => v.text) + .find((v) => v === 'JOIN') + + if (!isExpectedJoinKeyWord) { + return [] + } + + const fromNode = fromNodes[0] + const fromAlias = getAliasFromFromTableNode(fromNode) + const fromTable = tables.find((table) => isTableMatch(fromNode, table)) + + return tables + .filter((table) => table !== fromTable) + .reduce((c, p) => { + const newItems = p.columns + .filter((column) => + fromTable?.columns + .map((col) => col.columnName) + .includes(column.columnName) + ) + .map((column) => { + return { + tableName: makeTableName(p), + alias: makeTableAlias(p.tableName), + columnName: column.columnName, + } + }) + .map((match) => { + const label = `${joinType} JOIN ${match.tableName} AS ${match.alias} ON ${match.alias}.${match.columnName} = ${fromAlias}.${match.columnName}` + return { + label: label, + detail: 'utility', + kind: ICONS.UTILITY, + } + }) + return c.concat(newItems) + }, [] as CompletionItem[]) +} diff --git a/packages/server/src/complete/candidates/createKeywordCandidatesFromExpectedLiterals.ts b/packages/server/src/complete/candidates/createKeywordCandidatesFromExpectedLiterals.ts new file mode 100644 index 00000000..982c7990 --- /dev/null +++ b/packages/server/src/complete/candidates/createKeywordCandidatesFromExpectedLiterals.ts @@ -0,0 +1,45 @@ +import { ExpectedLiteralNode } from '@joe-re/sql-parser' +import { CompletionItem } from 'vscode-languageserver-types' +import { toCompletionItemForKeyword } from '../utils' + +const UNDESIRED_LITERAL = [ + '+', + '-', + '*', + '$', + ':', + 'COUNT', + 'AVG', + 'SUM', + 'MIN', + 'MAX', + '`', + '"', + "'", +] + +export function createKeywordCandidatesFromExpectedLiterals( + nodes: ExpectedLiteralNode[] +): CompletionItem[] { + const literals = nodes.map((v) => v.text) + const uniqueLiterals = [...new Set(literals)] + return uniqueLiterals + .filter((v) => !UNDESIRED_LITERAL.includes(v)) + .map((v) => { + switch (v) { + case 'ORDER': + return 'ORDER BY' + case 'GROUP': + return 'GROUP BY' + case 'LEFT': + return 'LEFT JOIN' + case 'RIGHT': + return 'RIGHT JOIN' + case 'INNER': + return 'INNER JOIN' + default: + return v + } + }) + .map((v) => toCompletionItemForKeyword(v)) +} diff --git a/packages/server/src/complete/candidates/createSelectAllColumnsCandidates.ts b/packages/server/src/complete/candidates/createSelectAllColumnsCandidates.ts new file mode 100644 index 00000000..3a16a565 --- /dev/null +++ b/packages/server/src/complete/candidates/createSelectAllColumnsCandidates.ts @@ -0,0 +1,39 @@ +import { FromTableNode } from '@joe-re/sql-parser' +import { Table } from '../../database_libs/AbstractClient' +import { ICONS, makeColumnName } from '../utils' +import { isTableMatch, getAliasFromFromTableNode } from '../utils' + +export function createSelectAllColumnsCandidates( + fromNodes: FromTableNode[], + tables: Table[], + lastToken: string +) { + return tables.flatMap((table) => { + return fromNodes + .filter((fromNode) => isTableMatch(fromNode, table)) + .map(getAliasFromFromTableNode) + .filter( + () => + lastToken.toUpperCase() === 'SELECT' || // complete SELECT keyword + lastToken === '' + ) // complete at space after SELECT + .map((alias) => { + const columnNames = table.columns + .map((col) => makeColumnName(alias, col.columnName)) + .join(',\n') + const label = `Select all columns from ${alias}` + let prefix = '' + if (lastToken) { + prefix = lastToken + '\n' + } + + return { + label: label, + insertText: prefix + columnNames, + filterText: prefix + label, + detail: 'utility', + kind: ICONS.UTILITY, + } + }) + }) +} diff --git a/packages/server/src/complete/candidates/createTableCandidates.ts b/packages/server/src/complete/candidates/createTableCandidates.ts new file mode 100644 index 00000000..152dbdc0 --- /dev/null +++ b/packages/server/src/complete/candidates/createTableCandidates.ts @@ -0,0 +1,28 @@ +import { Table } from '../../database_libs/AbstractClient' +import { ICONS } from '../utils' +import { Identifier } from '../Identifier' + +/** + * Given a table returns all possible ways to refer to it. + * That is by table name only, using the database scope, + * using the catalog and database scopes. + * @param table + * @returns + */ +function allTableNameCombinations(table: Table): string[] { + const names = [table.tableName] + if (table.database) names.push(table.database + '.' + table.tableName) + if (table.catalog) + names.push(table.catalog + '.' + table.database + '.' + table.tableName) + return names +} + +export function createTableCandidates(tables: Table[], lastToken: string) { + return tables + .flatMap((table) => allTableNameCombinations(table)) + .map((aTableNameVariant) => { + return new Identifier(lastToken, aTableNameVariant, '', ICONS.TABLE) + }) + .filter((item) => item.matchesLastToken()) + .map((item) => item.toCompletionItem()) +} diff --git a/packages/server/src/complete/utils.ts b/packages/server/src/complete/utils.ts index e48004c5..bf9901c0 100644 --- a/packages/server/src/complete/utils.ts +++ b/packages/server/src/complete/utils.ts @@ -8,7 +8,6 @@ import { import { CompletionItem, CompletionItemKind } from 'vscode-languageserver-types' import log4js from 'log4js' import { Table, DbFunction } from '../database_libs/AbstractClient' -import { Identifier } from './Identifier' const logger = log4js.getLogger() @@ -119,49 +118,6 @@ export function toCompletionItemForKeyword(name: string): CompletionItem { return item } -export function createCandidatesForColumnsOfAnyTable( - tables: Table[], - lastToken: string -): CompletionItem[] { - return tables - .flatMap((table) => table.columns) - .map((column) => { - return new Identifier( - lastToken, - column.columnName, - column.description, - ICONS.TABLE - ) - }) - .filter((item) => item.matchesLastToken()) - .map((item) => item.toCompletionItem()) -} - -export function createCandidatesForTables(tables: Table[], lastToken: string) { - return tables - .flatMap((table) => allTableNameCombinations(table)) - .map((aTableNameVariant) => { - return new Identifier(lastToken, aTableNameVariant, '', ICONS.TABLE) - }) - .filter((item) => item.matchesLastToken()) - .map((item) => item.toCompletionItem()) -} - -/** - * Given a table returns all possible ways to refer to it. - * That is by table name only, using the database scope, - * using the catalog and database scopes. - * @param table - * @returns - */ -export function allTableNameCombinations(table: Table): string[] { - const names = [table.tableName] - if (table.database) names.push(table.database + '.' + table.tableName) - if (table.catalog) - names.push(table.catalog + '.' + table.database + '.' + table.tableName) - return names -} - // Check if parser expects us to terminate a single quote value or double quoted column name // SELECT TABLE1.COLUMN1 FROM TABLE1 WHERE TABLE1.COLUMN1 = "hoge. // We don't offer the ', the ", the ` as suggestions @@ -346,9 +302,14 @@ export function getAllNestedFromNodes( * @param pos * @returns */ -export function getFromNodeByPos(fromNodes: FromTableNode[], pos: Pos) { - return fromNodes - .reverse() - .filter((tableNode) => isPosInLocation(tableNode.location, pos)) - .shift() +export function getNearestFromTableFromPos( + fromNodes: FromTableNode[], + pos: Pos +): FromTableNode | null { + return ( + fromNodes + .reverse() + .filter((tableNode) => isPosInLocation(tableNode.location, pos)) + .shift() ?? null + ) } From caef0ecf364ff1c449f32662f114f60400e75b5f Mon Sep 17 00:00:00 2001 From: joe-re Date: Sun, 20 Feb 2022 12:00:39 +0900 Subject: [PATCH 3/9] refactor supressing keyword completion logic --- packages/server/src/complete.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/server/src/complete.ts b/packages/server/src/complete.ts index 213d8c40..e4e04540 100644 --- a/packages/server/src/complete.ts +++ b/packages/server/src/complete.ts @@ -133,19 +133,23 @@ class Completer { addCandidatesForBasicKeyword() { createBasicKeywordCandidates().forEach((v) => { - this.addCandidate(v, this.lastToken) + this.addCandidate(v) }) } addCandidatesForExpectedLiterals(expected: ExpectedLiteralNode[]) { createKeywordCandidatesFromExpectedLiterals(expected).forEach((v) => { - this.addCandidate(v, this.lastToken) + this.addCandidate(v) }) } - addCandidate(item: CompletionItem, token?: string) { - // when token is passed and it doesn't matche to completion candidates, it doesn't add the item to candidates - if (token && !item.label.startsWith(token)) { + addCandidate(item: CompletionItem) { + // A keyword completion can be occured anyplace and need to suppress them. + if ( + item.kind && + item.kind === ICONS.KEYWORD && + !item.label.startsWith(this.lastToken) + ) { return } // JupyterLab requires the dot or space character preceeding the key pressed @@ -279,11 +283,11 @@ class Completer { addCandidatesForParsedSelectQuery(ast: SelectStatement) { this.addCandidatesForBasicKeyword() if (Array.isArray(ast.columns)) { - this.addCandidate(toCompletionItemForKeyword('FROM'), this.lastToken) - this.addCandidate(toCompletionItemForKeyword('AS'), this.lastToken) + this.addCandidate(toCompletionItemForKeyword('FROM')) + this.addCandidate(toCompletionItemForKeyword('AS')) } if (!ast.distinct) { - this.addCandidate(toCompletionItemForKeyword('DISTINCT'), this.lastToken) + this.addCandidate(toCompletionItemForKeyword('DISTINCT')) } const columnRef = findColumnAtPosition(ast, this.pos) if (!columnRef) { @@ -332,8 +336,7 @@ class Completer { this.pos, this.lastToken ).forEach((v) => { - // TODO: need to organize - this.addCandidate(v, v.kind === ICONS.TABLE ? '' : this.lastToken) + this.addCandidate(v) }) } From f07daa821732230362b91c588a5cb7fe97e93741 Mon Sep 17 00:00:00 2001 From: joe-re Date: Sun, 20 Feb 2022 12:05:58 +0900 Subject: [PATCH 4/9] delete unusef function --- ...teKeywordCandidatesFromExpectedLiterals.ts | 3 + packages/server/src/complete/utils.ts | 71 ++++--------------- 2 files changed, 18 insertions(+), 56 deletions(-) diff --git a/packages/server/src/complete/candidates/createKeywordCandidatesFromExpectedLiterals.ts b/packages/server/src/complete/candidates/createKeywordCandidatesFromExpectedLiterals.ts index 982c7990..b41544a2 100644 --- a/packages/server/src/complete/candidates/createKeywordCandidatesFromExpectedLiterals.ts +++ b/packages/server/src/complete/candidates/createKeywordCandidatesFromExpectedLiterals.ts @@ -2,6 +2,9 @@ import { ExpectedLiteralNode } from '@joe-re/sql-parser' import { CompletionItem } from 'vscode-languageserver-types' import { toCompletionItemForKeyword } from '../utils' +// Check if parser expects us to terminate a single quote value or double quoted column name +// SELECT TABLE1.COLUMN1 FROM TABLE1 WHERE TABLE1.COLUMN1 = "hoge. +// We don't offer the ', the ", the ` as suggestions const UNDESIRED_LITERAL = [ '+', '-', diff --git a/packages/server/src/complete/utils.ts b/packages/server/src/complete/utils.ts index bf9901c0..52a76f6d 100644 --- a/packages/server/src/complete/utils.ts +++ b/packages/server/src/complete/utils.ts @@ -1,6 +1,5 @@ import { ColumnRefNode, - ExpectedLiteralNode, FromTableNode, NodeRange, SelectStatement, @@ -22,22 +21,6 @@ export const ICONS = { UTILITY: CompletionItemKind.Event, } -const UNDESIRED_LITERAL = [ - '+', - '-', - '*', - '$', - ':', - 'COUNT', - 'AVG', - 'SUM', - 'MIN', - 'MAX', - '`', - '"', - "'", -] - function isNotEmpty(value: T | null | undefined): value is T { return value === null || value === undefined ? false : true } @@ -118,35 +101,6 @@ export function toCompletionItemForKeyword(name: string): CompletionItem { return item } -// Check if parser expects us to terminate a single quote value or double quoted column name -// SELECT TABLE1.COLUMN1 FROM TABLE1 WHERE TABLE1.COLUMN1 = "hoge. -// We don't offer the ', the ", the ` as suggestions -export function createCandidatesForExpectedLiterals( - nodes: ExpectedLiteralNode[] -): CompletionItem[] { - const literals = nodes.map((v) => v.text) - const uniqueLiterals = [...new Set(literals)] - return uniqueLiterals - .filter((v) => !UNDESIRED_LITERAL.includes(v)) - .map((v) => { - switch (v) { - case 'ORDER': - return 'ORDER BY' - case 'GROUP': - return 'GROUP BY' - case 'LEFT': - return 'LEFT JOIN' - case 'RIGHT': - return 'RIGHT JOIN' - case 'INNER': - return 'INNER JOIN' - default: - return v - } - }) - .map((v) => toCompletionItemForKeyword(v)) -} - export function getAliasFromFromTableNode(node: FromTableNode): string { if (node.as) { return node.as @@ -190,16 +144,21 @@ export function isTableMatch(fromNode: FromTableNode, table: Table): boolean { return true } -export function getColumnRefByPos(columns: ColumnRefNode[], pos: Pos) { - return columns.find( - (v) => - // guard against ColumnRefNode that don't have a location, - // for example sql functions that are not known to the parser - v.location && - v.location.start.line === pos.line + 1 && - v.location.start.column <= pos.column && - v.location.end.line === pos.line + 1 && - v.location.end.column >= pos.column +export function getColumnRefByPos( + columns: ColumnRefNode[], + pos: Pos +): ColumnRefNode | null { + return ( + columns.find( + (v) => + // guard against ColumnRefNode that don't have a location, + // for example sql functions that are not known to the parser + v.location && + v.location.start.line === pos.line + 1 && + v.location.start.column <= pos.column && + v.location.end.line === pos.line + 1 && + v.location.end.column >= pos.column + ) ?? null ) } From cf1ffe355ef3aefb500c42afcd26696b15c032cb Mon Sep 17 00:00:00 2001 From: joe-re Date: Sun, 20 Feb 2022 12:15:34 +0900 Subject: [PATCH 5/9] extract converting completion item logics --- packages/server/src/complete.ts | 6 ++- .../src/complete/CompletionItemUtils.ts | 39 ++++++++++++++++++ .../candidates/createAliasCandidates.ts | 2 +- .../createBasicKeywordCandidates.ts | 2 +- .../candidates/createColumnCandidates.ts | 2 +- .../candidates/createFunctionCandidates.ts | 2 +- .../candidates/createJoinCandidates.ts | 6 +-- .../candidates/createJoinTableCndidates.ts | 2 +- ...teKeywordCandidatesFromExpectedLiterals.ts | 2 +- .../createSelectAllColumnsCandidates.ts | 3 +- .../candidates/createTableCandidates.ts | 2 +- packages/server/src/complete/utils.ts | 40 +------------------ packages/server/test/complete.test.ts | 14 +++---- 13 files changed, 60 insertions(+), 62 deletions(-) create mode 100644 packages/server/src/complete/CompletionItemUtils.ts diff --git a/packages/server/src/complete.ts b/packages/server/src/complete.ts index e4e04540..f4ccf1ca 100644 --- a/packages/server/src/complete.ts +++ b/packages/server/src/complete.ts @@ -17,8 +17,6 @@ import { getRidOfAfterPosString, getLastToken, isPosInLocation, - toCompletionItemForKeyword, - ICONS, createTablesFromFromNodes, findColumnAtPosition, getAllNestedFromNodes, @@ -36,6 +34,10 @@ import { createSelectAllColumnsCandidates } from './complete/candidates/createSe import { createFunctionCandidates } from './complete/candidates/createFunctionCandidates' import { createKeywordCandidatesFromExpectedLiterals } from './complete/candidates/createKeywordCandidatesFromExpectedLiterals' import { createJoinTablesCandidates } from './complete/candidates/createJoinTableCndidates' +import { + ICONS, + toCompletionItemForKeyword, +} from './complete/CompletionItemUtils' type Pos = { line: number; column: number } diff --git a/packages/server/src/complete/CompletionItemUtils.ts b/packages/server/src/complete/CompletionItemUtils.ts new file mode 100644 index 00000000..98c4c273 --- /dev/null +++ b/packages/server/src/complete/CompletionItemUtils.ts @@ -0,0 +1,39 @@ +import { CompletionItem, CompletionItemKind } from 'vscode-languageserver-types' +import { DbFunction } from '../database_libs/AbstractClient' + +export const ICONS = { + KEYWORD: CompletionItemKind.Text, + COLUMN: CompletionItemKind.Interface, + TABLE: CompletionItemKind.Field, + FUNCTION: CompletionItemKind.Property, + ALIAS: CompletionItemKind.Variable, + UTILITY: CompletionItemKind.Event, +} + +export function toCompletionItemForFunction(f: DbFunction): CompletionItem { + const item: CompletionItem = { + label: f.name, + detail: 'function', + kind: ICONS.FUNCTION, + documentation: f.description, + } + return item +} + +export function toCompletionItemForAlias(alias: string): CompletionItem { + const item: CompletionItem = { + label: alias, + detail: 'alias', + kind: ICONS.ALIAS, + } + return item +} + +export function toCompletionItemForKeyword(name: string): CompletionItem { + const item: CompletionItem = { + label: name, + kind: ICONS.KEYWORD, + detail: 'keyword', + } + return item +} diff --git a/packages/server/src/complete/candidates/createAliasCandidates.ts b/packages/server/src/complete/candidates/createAliasCandidates.ts index e70aba5e..9f1d9895 100644 --- a/packages/server/src/complete/candidates/createAliasCandidates.ts +++ b/packages/server/src/complete/candidates/createAliasCandidates.ts @@ -1,6 +1,6 @@ import { CompletionItem } from 'vscode-languageserver-types' import { FromTableNode } from '@joe-re/sql-parser' -import { toCompletionItemForAlias } from '../utils' +import { toCompletionItemForAlias } from '../CompletionItemUtils' export function createAliasCandidates( fromNodes: FromTableNode[], diff --git a/packages/server/src/complete/candidates/createBasicKeywordCandidates.ts b/packages/server/src/complete/candidates/createBasicKeywordCandidates.ts index fecbb82f..839226ca 100644 --- a/packages/server/src/complete/candidates/createBasicKeywordCandidates.ts +++ b/packages/server/src/complete/candidates/createBasicKeywordCandidates.ts @@ -1,4 +1,4 @@ -import { toCompletionItemForKeyword } from '../utils' +import { toCompletionItemForKeyword } from '../CompletionItemUtils' const CLAUSES: string[] = [ 'SELECT', diff --git a/packages/server/src/complete/candidates/createColumnCandidates.ts b/packages/server/src/complete/candidates/createColumnCandidates.ts index 98b4e4f1..46c159fd 100644 --- a/packages/server/src/complete/candidates/createColumnCandidates.ts +++ b/packages/server/src/complete/candidates/createColumnCandidates.ts @@ -3,10 +3,10 @@ import { FromTableNode } from '@joe-re/sql-parser' import { Table } from '../../database_libs/AbstractClient' import { getAliasFromFromTableNode, - ICONS, isTableMatch, makeColumnName, } from '../utils' +import { ICONS } from '../CompletionItemUtils' import { Identifier } from '../Identifier' export function createCandidatesForColumnsOfAnyTable( diff --git a/packages/server/src/complete/candidates/createFunctionCandidates.ts b/packages/server/src/complete/candidates/createFunctionCandidates.ts index 4d0e1210..ebb277ad 100644 --- a/packages/server/src/complete/candidates/createFunctionCandidates.ts +++ b/packages/server/src/complete/candidates/createFunctionCandidates.ts @@ -1,5 +1,5 @@ -import { toCompletionItemForFunction } from '../utils' import { DbFunction } from '../../database_libs/AbstractClient' +import { toCompletionItemForFunction } from '../CompletionItemUtils' export function createFunctionCandidates( functions: DbFunction[], diff --git a/packages/server/src/complete/candidates/createJoinCandidates.ts b/packages/server/src/complete/candidates/createJoinCandidates.ts index 7378c48b..6eec7405 100644 --- a/packages/server/src/complete/candidates/createJoinCandidates.ts +++ b/packages/server/src/complete/candidates/createJoinCandidates.ts @@ -1,10 +1,8 @@ import { CompletionItem } from 'vscode-languageserver-types' import { SelectStatement } from '@joe-re/sql-parser' -import { - getNearestFromTableFromPos, - toCompletionItemForKeyword, -} from '../utils' +import { getNearestFromTableFromPos } from '../utils' import { Table } from '../../database_libs/AbstractClient' +import { toCompletionItemForKeyword } from '../CompletionItemUtils' import { createTableCandidates } from './createTableCandidates' type Pos = { line: number; column: number } diff --git a/packages/server/src/complete/candidates/createJoinTableCndidates.ts b/packages/server/src/complete/candidates/createJoinTableCndidates.ts index e2c714b1..2209cc12 100644 --- a/packages/server/src/complete/candidates/createJoinTableCndidates.ts +++ b/packages/server/src/complete/candidates/createJoinTableCndidates.ts @@ -2,12 +2,12 @@ import { FromTableNode, ExpectedLiteralNode } from '@joe-re/sql-parser' import { CompletionItem } from 'vscode-languageserver-types' import { getAliasFromFromTableNode, - ICONS, isTableMatch, makeTableAlias, makeTableName, } from '../utils' import { Table } from '../../database_libs/AbstractClient' +import { ICONS } from '../CompletionItemUtils' export function createJoinTablesCandidates( tables: Table[], diff --git a/packages/server/src/complete/candidates/createKeywordCandidatesFromExpectedLiterals.ts b/packages/server/src/complete/candidates/createKeywordCandidatesFromExpectedLiterals.ts index b41544a2..6abc4e09 100644 --- a/packages/server/src/complete/candidates/createKeywordCandidatesFromExpectedLiterals.ts +++ b/packages/server/src/complete/candidates/createKeywordCandidatesFromExpectedLiterals.ts @@ -1,6 +1,6 @@ import { ExpectedLiteralNode } from '@joe-re/sql-parser' import { CompletionItem } from 'vscode-languageserver-types' -import { toCompletionItemForKeyword } from '../utils' +import { toCompletionItemForKeyword } from '../CompletionItemUtils' // Check if parser expects us to terminate a single quote value or double quoted column name // SELECT TABLE1.COLUMN1 FROM TABLE1 WHERE TABLE1.COLUMN1 = "hoge. diff --git a/packages/server/src/complete/candidates/createSelectAllColumnsCandidates.ts b/packages/server/src/complete/candidates/createSelectAllColumnsCandidates.ts index 3a16a565..a02bf5c3 100644 --- a/packages/server/src/complete/candidates/createSelectAllColumnsCandidates.ts +++ b/packages/server/src/complete/candidates/createSelectAllColumnsCandidates.ts @@ -1,7 +1,8 @@ import { FromTableNode } from '@joe-re/sql-parser' import { Table } from '../../database_libs/AbstractClient' -import { ICONS, makeColumnName } from '../utils' +import { makeColumnName } from '../utils' import { isTableMatch, getAliasFromFromTableNode } from '../utils' +import { ICONS } from '../CompletionItemUtils' export function createSelectAllColumnsCandidates( fromNodes: FromTableNode[], diff --git a/packages/server/src/complete/candidates/createTableCandidates.ts b/packages/server/src/complete/candidates/createTableCandidates.ts index 152dbdc0..12e5535a 100644 --- a/packages/server/src/complete/candidates/createTableCandidates.ts +++ b/packages/server/src/complete/candidates/createTableCandidates.ts @@ -1,6 +1,6 @@ import { Table } from '../../database_libs/AbstractClient' -import { ICONS } from '../utils' import { Identifier } from '../Identifier' +import { ICONS } from '../CompletionItemUtils' /** * Given a table returns all possible ways to refer to it. diff --git a/packages/server/src/complete/utils.ts b/packages/server/src/complete/utils.ts index 52a76f6d..9fce6c16 100644 --- a/packages/server/src/complete/utils.ts +++ b/packages/server/src/complete/utils.ts @@ -4,23 +4,13 @@ import { NodeRange, SelectStatement, } from '@joe-re/sql-parser' -import { CompletionItem, CompletionItemKind } from 'vscode-languageserver-types' import log4js from 'log4js' -import { Table, DbFunction } from '../database_libs/AbstractClient' +import { Table } from '../database_libs/AbstractClient' const logger = log4js.getLogger() type Pos = { line: number; column: number } -export const ICONS = { - KEYWORD: CompletionItemKind.Text, - COLUMN: CompletionItemKind.Interface, - TABLE: CompletionItemKind.Field, - FUNCTION: CompletionItemKind.Property, - ALIAS: CompletionItemKind.Variable, - UTILITY: CompletionItemKind.Event, -} - function isNotEmpty(value: T | null | undefined): value is T { return value === null || value === undefined ? false : true } @@ -73,34 +63,6 @@ export function isPosInLocation(location: NodeRange, pos: Pos) { ) } -export function toCompletionItemForFunction(f: DbFunction): CompletionItem { - const item: CompletionItem = { - label: f.name, - detail: 'function', - kind: ICONS.FUNCTION, - documentation: f.description, - } - return item -} - -export function toCompletionItemForAlias(alias: string): CompletionItem { - const item: CompletionItem = { - label: alias, - detail: 'alias', - kind: ICONS.ALIAS, - } - return item -} - -export function toCompletionItemForKeyword(name: string): CompletionItem { - const item: CompletionItem = { - label: name, - kind: ICONS.KEYWORD, - detail: 'keyword', - } - return item -} - export function getAliasFromFromTableNode(node: FromTableNode): string { if (node.as) { return node.as diff --git a/packages/server/test/complete.test.ts b/packages/server/test/complete.test.ts index 4a5dd065..8f903f1d 100644 --- a/packages/server/test/complete.test.ts +++ b/packages/server/test/complete.test.ts @@ -1,5 +1,6 @@ import { complete } from '../src/complete' import { Identifier, utils } from '../src/complete/index' +import { ICONS } from '../src/complete/CompletionItemUtils' describe('keyword completion', () => { test("complete 'SELECT' keyword", () => { @@ -1010,17 +1011,12 @@ describe('DELETE statement', () => { describe('toCompletionItemForIdentifier', () => { test('complete comlumn name', () => { - const item = new Identifier('col', 'column1', '', utils.ICONS.COLUMN) + const item = new Identifier('col', 'column1', '', ICONS.COLUMN) const completion = item.toCompletionItem() expect(completion.label).toEqual('column1') }) test('complete aliased comlumn name', () => { - const item = new Identifier( - 'ali.col', - 'ali.column1', - '', - utils.ICONS.COLUMN - ) + const item = new Identifier('ali.col', 'ali.column1', '', ICONS.COLUMN) const completion = item.toCompletionItem() expect(completion.label).toEqual('column1') }) @@ -1029,7 +1025,7 @@ describe('toCompletionItemForIdentifier', () => { 'ali.column1.sub', 'ali.column1.subcolumn2', '', - utils.ICONS.COLUMN + ICONS.COLUMN ) const completion = item.toCompletionItem() expect(completion.label).toEqual('subcolumn2') @@ -1039,7 +1035,7 @@ describe('toCompletionItemForIdentifier', () => { 'ali.colu', 'ali.column1.subcolumn2', '', - utils.ICONS.COLUMN + ICONS.COLUMN ) const completion = item.toCompletionItem() expect(completion.label).toEqual('column1.subcolumn2') From 5dfeff9e89a9482a002f5cc350a3e55da441d46c Mon Sep 17 00:00:00 2001 From: joe-re Date: Sun, 20 Feb 2022 19:06:50 +0900 Subject: [PATCH 6/9] move complete function directory --- .../server/src/{ => complete}/complete.ts | 27 +++++++++---------- packages/server/src/complete/index.ts | 3 ++- 2 files changed, 14 insertions(+), 16 deletions(-) rename packages/server/src/{ => complete}/complete.ts (92%) diff --git a/packages/server/src/complete.ts b/packages/server/src/complete/complete.ts similarity index 92% rename from packages/server/src/complete.ts rename to packages/server/src/complete/complete.ts index f4ccf1ca..db7558c5 100644 --- a/packages/server/src/complete.ts +++ b/packages/server/src/complete/complete.ts @@ -12,7 +12,7 @@ import { } from '@joe-re/sql-parser' import log4js from 'log4js' import { CompletionItem } from 'vscode-languageserver-types' -import { Schema, Table } from './database_libs/AbstractClient' +import { Schema, Table } from '../database_libs/AbstractClient' import { getRidOfAfterPosString, getLastToken, @@ -21,23 +21,20 @@ import { findColumnAtPosition, getAllNestedFromNodes, getNearestFromTableFromPos, -} from './complete/utils' -import { createBasicKeywordCandidates } from './complete/candidates/createBasicKeywordCandidates' -import { createTableCandidates } from './complete/candidates/createTableCandidates' -import { createJoinCondidates } from './complete/candidates/createJoinCandidates' +} from './utils' +import { createBasicKeywordCandidates } from './candidates/createBasicKeywordCandidates' +import { createTableCandidates } from './candidates/createTableCandidates' +import { createJoinCondidates } from './candidates/createJoinCandidates' import { createCandidatesForColumnsOfAnyTable, createCandidatesForScopedColumns, -} from './complete/candidates/createColumnCandidates' -import { createAliasCandidates } from './complete/candidates/createAliasCandidates' -import { createSelectAllColumnsCandidates } from './complete/candidates/createSelectAllColumnsCandidates' -import { createFunctionCandidates } from './complete/candidates/createFunctionCandidates' -import { createKeywordCandidatesFromExpectedLiterals } from './complete/candidates/createKeywordCandidatesFromExpectedLiterals' -import { createJoinTablesCandidates } from './complete/candidates/createJoinTableCndidates' -import { - ICONS, - toCompletionItemForKeyword, -} from './complete/CompletionItemUtils' +} from './candidates/createColumnCandidates' +import { createAliasCandidates } from './candidates/createAliasCandidates' +import { createSelectAllColumnsCandidates } from './candidates/createSelectAllColumnsCandidates' +import { createFunctionCandidates } from './candidates/createFunctionCandidates' +import { createKeywordCandidatesFromExpectedLiterals } from './candidates/createKeywordCandidatesFromExpectedLiterals' +import { createJoinTablesCandidates } from './candidates/createJoinTableCndidates' +import { ICONS, toCompletionItemForKeyword } from './CompletionItemUtils' type Pos = { line: number; column: number } diff --git a/packages/server/src/complete/index.ts b/packages/server/src/complete/index.ts index 81bc39ee..6491bb33 100644 --- a/packages/server/src/complete/index.ts +++ b/packages/server/src/complete/index.ts @@ -1,4 +1,5 @@ import { Identifier } from './Identifier' import * as utils from './utils' +import { complete } from './complete' -export { Identifier, utils } +export { Identifier, utils, complete } From c3874b1ea6c2eb47e1253e47733b1b157c9b0f92 Mon Sep 17 00:00:00 2001 From: joe-re Date: Sun, 20 Feb 2022 19:15:23 +0900 Subject: [PATCH 7/9] extract AST manipulation functions from utils --- .../src/complete/{utils.ts => AstUtils.ts} | 135 ++++++------------ packages/server/src/complete/Identifier.ts | 2 +- packages/server/src/complete/StringUtils.ts | 57 ++++++++ .../candidates/createColumnCandidates.ts | 7 +- .../candidates/createJoinCandidates.ts | 2 +- .../candidates/createJoinTableCndidates.ts | 4 +- .../createSelectAllColumnsCandidates.ts | 5 +- packages/server/src/complete/complete.ts | 5 +- packages/server/src/complete/index.ts | 3 +- packages/server/test/complete.test.ts | 37 ++--- 10 files changed, 131 insertions(+), 126 deletions(-) rename packages/server/src/complete/{utils.ts => AstUtils.ts} (78%) create mode 100644 packages/server/src/complete/StringUtils.ts diff --git a/packages/server/src/complete/utils.ts b/packages/server/src/complete/AstUtils.ts similarity index 78% rename from packages/server/src/complete/utils.ts rename to packages/server/src/complete/AstUtils.ts index 9fce6c16..9cd48a96 100644 --- a/packages/server/src/complete/utils.ts +++ b/packages/server/src/complete/AstUtils.ts @@ -1,8 +1,8 @@ import { ColumnRefNode, FromTableNode, - NodeRange, SelectStatement, + NodeRange, } from '@joe-re/sql-parser' import log4js from 'log4js' import { Table } from '../database_libs/AbstractClient' @@ -15,97 +15,6 @@ function isNotEmpty(value: T | null | undefined): value is T { return value === null || value === undefined ? false : true } -export function makeTableAlias(tableName: string): string { - if (tableName.length > 3) { - return tableName.substring(0, 3) - } - return tableName -} - -export function getRidOfAfterPosString(sql: string, pos: Pos) { - return sql - .split('\n') - .filter((_v, idx) => pos.line >= idx) - .map((v, idx) => (idx === pos.line ? v.slice(0, pos.column) : v)) - .join('\n') -} - -// Gets the last token from the given string considering that tokens can contain dots. -export function getLastToken(sql: string): string { - const match = sql.match(/^(?:.|\s)*[^A-z0-9\\.:'](.*?)$/) - if (match) { - let prevToken = '' - let currentToken = match[1] - while (currentToken != prevToken) { - prevToken = currentToken - currentToken = prevToken.replace(/\[.*?\]/, '') - } - return currentToken - } - return sql -} - -export function makeTableName(table: Table): string { - if (table.catalog) { - return table.catalog + '.' + table.database + '.' + table.tableName - } else if (table.database) { - return table.database + '.' + table.tableName - } - return table.tableName -} - -export function isPosInLocation(location: NodeRange, pos: Pos) { - return ( - location.start.line === pos.line + 1 && - location.start.column <= pos.column && - location.end.line === pos.line + 1 && - location.end.column >= pos.column - ) -} - -export function getAliasFromFromTableNode(node: FromTableNode): string { - if (node.as) { - return node.as - } - if (node.type === 'table') { - return node.table - } - return '' -} - -/** - * Test if the given table matches the fromNode. - * @param fromNode - * @param table - * @returns - */ -export function isTableMatch(fromNode: FromTableNode, table: Table): boolean { - switch (fromNode.type) { - case 'subquery': { - if (fromNode.as && fromNode.as !== table.tableName) { - return false - } - break - } - case 'table': { - if (fromNode.table && fromNode.table !== table.tableName) { - return false - } - if (fromNode.db && fromNode.db !== table.database) { - return false - } - if (fromNode.catalog && fromNode.catalog !== table.catalog) { - return false - } - break - } - default: { - return false - } - } - return true -} - export function getColumnRefByPos( columns: ColumnRefNode[], pos: Pos @@ -124,8 +33,13 @@ export function getColumnRefByPos( ) } -export function makeColumnName(alias: string, columnName: string) { - return alias ? alias + '.' + columnName : columnName +export function isPosInLocation(location: NodeRange, pos: Pos) { + return ( + location.start.line === pos.line + 1 && + location.start.column <= pos.column && + location.end.line === pos.line + 1 && + location.end.column >= pos.column + ) } export function createTablesFromFromNodes(fromNodes: FromTableNode[]): Table[] { @@ -234,3 +148,36 @@ export function getNearestFromTableFromPos( .shift() ?? null ) } + +/** + * Test if the given table matches the fromNode. + * @param fromNode + * @param table + * @returns + */ +export function isTableMatch(fromNode: FromTableNode, table: Table): boolean { + switch (fromNode.type) { + case 'subquery': { + if (fromNode.as && fromNode.as !== table.tableName) { + return false + } + break + } + case 'table': { + if (fromNode.table && fromNode.table !== table.tableName) { + return false + } + if (fromNode.db && fromNode.db !== table.database) { + return false + } + if (fromNode.catalog && fromNode.catalog !== table.catalog) { + return false + } + break + } + default: { + return false + } + } + return true +} diff --git a/packages/server/src/complete/Identifier.ts b/packages/server/src/complete/Identifier.ts index 950eb55b..8ba356f1 100644 --- a/packages/server/src/complete/Identifier.ts +++ b/packages/server/src/complete/Identifier.ts @@ -1,5 +1,5 @@ import { CompletionItem, CompletionItemKind } from 'vscode-languageserver-types' -import { makeTableAlias } from './utils' +import { makeTableAlias } from './StringUtils' export const ICONS = { KEYWORD: CompletionItemKind.Text, diff --git a/packages/server/src/complete/StringUtils.ts b/packages/server/src/complete/StringUtils.ts new file mode 100644 index 00000000..97c62c6a --- /dev/null +++ b/packages/server/src/complete/StringUtils.ts @@ -0,0 +1,57 @@ +import { FromTableNode } from '@joe-re/sql-parser' +import { Table } from '../database_libs/AbstractClient' + +type Pos = { line: number; column: number } + +export function makeTableAlias(tableName: string): string { + if (tableName.length > 3) { + return tableName.substring(0, 3) + } + return tableName +} + +export function getRidOfAfterPosString(sql: string, pos: Pos): string { + return sql + .split('\n') + .filter((_v, idx) => pos.line >= idx) + .map((v, idx) => (idx === pos.line ? v.slice(0, pos.column) : v)) + .join('\n') +} + +// Gets the last token from the given string considering that tokens can contain dots. +export function getLastToken(sql: string): string { + const match = sql.match(/^(?:.|\s)*[^A-z0-9\\.:'](.*?)$/) + if (match) { + let prevToken = '' + let currentToken = match[1] + while (currentToken != prevToken) { + prevToken = currentToken + currentToken = prevToken.replace(/\[.*?\]/, '') + } + return currentToken + } + return sql +} + +export function makeTableName(table: Table): string { + if (table.catalog) { + return table.catalog + '.' + table.database + '.' + table.tableName + } else if (table.database) { + return table.database + '.' + table.tableName + } + return table.tableName +} + +export function getAliasFromFromTableNode(node: FromTableNode): string { + if (node.as) { + return node.as + } + if (node.type === 'table') { + return node.table + } + return '' +} + +export function makeColumnName(alias: string, columnName: string) { + return alias ? alias + '.' + columnName : columnName +} diff --git a/packages/server/src/complete/candidates/createColumnCandidates.ts b/packages/server/src/complete/candidates/createColumnCandidates.ts index 46c159fd..7b87e4b9 100644 --- a/packages/server/src/complete/candidates/createColumnCandidates.ts +++ b/packages/server/src/complete/candidates/createColumnCandidates.ts @@ -1,11 +1,8 @@ import { CompletionItem } from 'vscode-languageserver-types' import { FromTableNode } from '@joe-re/sql-parser' import { Table } from '../../database_libs/AbstractClient' -import { - getAliasFromFromTableNode, - isTableMatch, - makeColumnName, -} from '../utils' +import { getAliasFromFromTableNode, makeColumnName } from '../StringUtils' +import { isTableMatch } from '../AstUtils' import { ICONS } from '../CompletionItemUtils' import { Identifier } from '../Identifier' diff --git a/packages/server/src/complete/candidates/createJoinCandidates.ts b/packages/server/src/complete/candidates/createJoinCandidates.ts index 6eec7405..9157ed02 100644 --- a/packages/server/src/complete/candidates/createJoinCandidates.ts +++ b/packages/server/src/complete/candidates/createJoinCandidates.ts @@ -1,6 +1,6 @@ import { CompletionItem } from 'vscode-languageserver-types' import { SelectStatement } from '@joe-re/sql-parser' -import { getNearestFromTableFromPos } from '../utils' +import { getNearestFromTableFromPos } from '../AstUtils' import { Table } from '../../database_libs/AbstractClient' import { toCompletionItemForKeyword } from '../CompletionItemUtils' import { createTableCandidates } from './createTableCandidates' diff --git a/packages/server/src/complete/candidates/createJoinTableCndidates.ts b/packages/server/src/complete/candidates/createJoinTableCndidates.ts index 2209cc12..a34cf34e 100644 --- a/packages/server/src/complete/candidates/createJoinTableCndidates.ts +++ b/packages/server/src/complete/candidates/createJoinTableCndidates.ts @@ -2,10 +2,10 @@ import { FromTableNode, ExpectedLiteralNode } from '@joe-re/sql-parser' import { CompletionItem } from 'vscode-languageserver-types' import { getAliasFromFromTableNode, - isTableMatch, makeTableAlias, makeTableName, -} from '../utils' +} from '../StringUtils' +import { isTableMatch } from '../AstUtils' import { Table } from '../../database_libs/AbstractClient' import { ICONS } from '../CompletionItemUtils' diff --git a/packages/server/src/complete/candidates/createSelectAllColumnsCandidates.ts b/packages/server/src/complete/candidates/createSelectAllColumnsCandidates.ts index a02bf5c3..f30989b5 100644 --- a/packages/server/src/complete/candidates/createSelectAllColumnsCandidates.ts +++ b/packages/server/src/complete/candidates/createSelectAllColumnsCandidates.ts @@ -1,7 +1,8 @@ import { FromTableNode } from '@joe-re/sql-parser' import { Table } from '../../database_libs/AbstractClient' -import { makeColumnName } from '../utils' -import { isTableMatch, getAliasFromFromTableNode } from '../utils' +import { makeColumnName } from '../StringUtils' +import { getAliasFromFromTableNode } from '../StringUtils' +import { isTableMatch } from '../AstUtils' import { ICONS } from '../CompletionItemUtils' export function createSelectAllColumnsCandidates( diff --git a/packages/server/src/complete/complete.ts b/packages/server/src/complete/complete.ts index db7558c5..27971490 100644 --- a/packages/server/src/complete/complete.ts +++ b/packages/server/src/complete/complete.ts @@ -13,15 +13,14 @@ import { import log4js from 'log4js' import { CompletionItem } from 'vscode-languageserver-types' import { Schema, Table } from '../database_libs/AbstractClient' +import { getRidOfAfterPosString, getLastToken } from './StringUtils' import { - getRidOfAfterPosString, - getLastToken, isPosInLocation, createTablesFromFromNodes, findColumnAtPosition, getAllNestedFromNodes, getNearestFromTableFromPos, -} from './utils' +} from './AstUtils' import { createBasicKeywordCandidates } from './candidates/createBasicKeywordCandidates' import { createTableCandidates } from './candidates/createTableCandidates' import { createJoinCondidates } from './candidates/createJoinCandidates' diff --git a/packages/server/src/complete/index.ts b/packages/server/src/complete/index.ts index 6491bb33..73bd392f 100644 --- a/packages/server/src/complete/index.ts +++ b/packages/server/src/complete/index.ts @@ -1,5 +1,4 @@ import { Identifier } from './Identifier' -import * as utils from './utils' import { complete } from './complete' -export { Identifier, utils, complete } +export { Identifier, complete } diff --git a/packages/server/test/complete.test.ts b/packages/server/test/complete.test.ts index 8f903f1d..1c3f9ac4 100644 --- a/packages/server/test/complete.test.ts +++ b/packages/server/test/complete.test.ts @@ -1,5 +1,6 @@ import { complete } from '../src/complete' -import { Identifier, utils } from '../src/complete/index' +import { Identifier } from '../src/complete/index' +import * as StringUtils from '../src/complete/StringUtils' import { ICONS } from '../src/complete/CompletionItemUtils' describe('keyword completion', () => { @@ -665,25 +666,29 @@ describe('Nested ColumnName completion', () => { }) test('getLastToken', () => { - expect(utils.getLastToken('SELECT abc')).toEqual('abc') - expect(utils.getLastToken('SELECT abc.def')).toEqual('abc.def') - expect(utils.getLastToken('SELECT abc[0]')).toEqual('abc') - expect(utils.getLastToken('SELECT abc[0].')).toEqual('abc.') - expect(utils.getLastToken('SELECT abc[0].d')).toEqual('abc.d') - expect(utils.getLastToken('SELECT abc[0].def[0]')).toEqual('abc.def') - expect(utils.getLastToken('SELECT abc[0].def[0].')).toEqual('abc.def.') - expect(utils.getLastToken('SELECT abc[0].def[0].g')).toEqual('abc.def.g') - - expect(utils.getLastToken("SELECT abc['key']")).toEqual('abc') - expect(utils.getLastToken("SELECT abc['key.name'].")).toEqual('abc.') - expect(utils.getLastToken("SELECT abc['key'].d")).toEqual('abc.d') - expect(utils.getLastToken("SELECT abc['key'].def['key']")).toEqual( + expect(StringUtils.getLastToken('SELECT abc')).toEqual('abc') + expect(StringUtils.getLastToken('SELECT abc.def')).toEqual('abc.def') + expect(StringUtils.getLastToken('SELECT abc[0]')).toEqual('abc') + expect(StringUtils.getLastToken('SELECT abc[0].')).toEqual('abc.') + expect(StringUtils.getLastToken('SELECT abc[0].d')).toEqual('abc.d') + expect(StringUtils.getLastToken('SELECT abc[0].def[0]')).toEqual('abc.def') + expect(StringUtils.getLastToken('SELECT abc[0].def[0].')).toEqual( + 'abc.def.' + ) + expect(StringUtils.getLastToken('SELECT abc[0].def[0].g')).toEqual( + 'abc.def.g' + ) + + expect(StringUtils.getLastToken("SELECT abc['key']")).toEqual('abc') + expect(StringUtils.getLastToken("SELECT abc['key.name'].")).toEqual('abc.') + expect(StringUtils.getLastToken("SELECT abc['key'].d")).toEqual('abc.d') + expect(StringUtils.getLastToken("SELECT abc['key'].def['key']")).toEqual( 'abc.def' ) - expect(utils.getLastToken("SELECT abc['key'].def['key'].")).toEqual( + expect(StringUtils.getLastToken("SELECT abc['key'].def['key'].")).toEqual( 'abc.def.' ) - expect(utils.getLastToken("SELECT abc['key'].def[0].g")).toEqual( + expect(StringUtils.getLastToken("SELECT abc['key'].def[0].g")).toEqual( 'abc.def.g' ) }) From 6380655294b9d968fe1869622af64bc8c21a01df Mon Sep 17 00:00:00 2001 From: joe-re Date: Mon, 21 Feb 2022 07:54:30 +0900 Subject: [PATCH 8/9] refactor test directory --- packages/server/test/complete.test.ts | 64 ------------------- .../server/test/complete/Identifier.test.ts | 37 +++++++++++ .../server/test/complete/StringUtils.test.ts | 30 +++++++++ 3 files changed, 67 insertions(+), 64 deletions(-) create mode 100644 packages/server/test/complete/Identifier.test.ts create mode 100644 packages/server/test/complete/StringUtils.test.ts diff --git a/packages/server/test/complete.test.ts b/packages/server/test/complete.test.ts index 1c3f9ac4..e51868fb 100644 --- a/packages/server/test/complete.test.ts +++ b/packages/server/test/complete.test.ts @@ -1,7 +1,4 @@ import { complete } from '../src/complete' -import { Identifier } from '../src/complete/index' -import * as StringUtils from '../src/complete/StringUtils' -import { ICONS } from '../src/complete/CompletionItemUtils' describe('keyword completion', () => { test("complete 'SELECT' keyword", () => { @@ -665,34 +662,6 @@ describe('Nested ColumnName completion', () => { expect(result.candidates[1].label).toEqual('`with spaces`.`sub space`') }) - test('getLastToken', () => { - expect(StringUtils.getLastToken('SELECT abc')).toEqual('abc') - expect(StringUtils.getLastToken('SELECT abc.def')).toEqual('abc.def') - expect(StringUtils.getLastToken('SELECT abc[0]')).toEqual('abc') - expect(StringUtils.getLastToken('SELECT abc[0].')).toEqual('abc.') - expect(StringUtils.getLastToken('SELECT abc[0].d')).toEqual('abc.d') - expect(StringUtils.getLastToken('SELECT abc[0].def[0]')).toEqual('abc.def') - expect(StringUtils.getLastToken('SELECT abc[0].def[0].')).toEqual( - 'abc.def.' - ) - expect(StringUtils.getLastToken('SELECT abc[0].def[0].g')).toEqual( - 'abc.def.g' - ) - - expect(StringUtils.getLastToken("SELECT abc['key']")).toEqual('abc') - expect(StringUtils.getLastToken("SELECT abc['key.name'].")).toEqual('abc.') - expect(StringUtils.getLastToken("SELECT abc['key'].d")).toEqual('abc.d') - expect(StringUtils.getLastToken("SELECT abc['key'].def['key']")).toEqual( - 'abc.def' - ) - expect(StringUtils.getLastToken("SELECT abc['key'].def['key'].")).toEqual( - 'abc.def.' - ) - expect(StringUtils.getLastToken("SELECT abc['key'].def[0].g")).toEqual( - 'abc.def.g' - ) - }) - test('with array subscripted', () => { const result = complete( 'SELECT t.abc[0]. FROM TABLE1 as t', @@ -1013,36 +982,3 @@ describe('DELETE statement', () => { expect(result.candidates[1].label).toEqual('COLUMN2') }) }) - -describe('toCompletionItemForIdentifier', () => { - test('complete comlumn name', () => { - const item = new Identifier('col', 'column1', '', ICONS.COLUMN) - const completion = item.toCompletionItem() - expect(completion.label).toEqual('column1') - }) - test('complete aliased comlumn name', () => { - const item = new Identifier('ali.col', 'ali.column1', '', ICONS.COLUMN) - const completion = item.toCompletionItem() - expect(completion.label).toEqual('column1') - }) - test('complete aliased nested comlumn last part name', () => { - const item = new Identifier( - 'ali.column1.sub', - 'ali.column1.subcolumn2', - '', - ICONS.COLUMN - ) - const completion = item.toCompletionItem() - expect(completion.label).toEqual('subcolumn2') - }) - test('complete aliased nested comlumn first part name', () => { - const item = new Identifier( - 'ali.colu', - 'ali.column1.subcolumn2', - '', - ICONS.COLUMN - ) - const completion = item.toCompletionItem() - expect(completion.label).toEqual('column1.subcolumn2') - }) -}) diff --git a/packages/server/test/complete/Identifier.test.ts b/packages/server/test/complete/Identifier.test.ts new file mode 100644 index 00000000..a9388a74 --- /dev/null +++ b/packages/server/test/complete/Identifier.test.ts @@ -0,0 +1,37 @@ +import { ICONS } from '../../src/complete/CompletionItemUtils' +import { Identifier } from '../../src/complete/index' + +describe('Identifier', () => { + describe('toCompletionItem', () => { + test('complete comlumn name', () => { + const item = new Identifier('col', 'column1', '', ICONS.COLUMN) + const completion = item.toCompletionItem() + expect(completion.label).toEqual('column1') + }) + test('complete aliased comlumn name', () => { + const item = new Identifier('ali.col', 'ali.column1', '', ICONS.COLUMN) + const completion = item.toCompletionItem() + expect(completion.label).toEqual('column1') + }) + test('complete aliased nested comlumn last part name', () => { + const item = new Identifier( + 'ali.column1.sub', + 'ali.column1.subcolumn2', + '', + ICONS.COLUMN + ) + const completion = item.toCompletionItem() + expect(completion.label).toEqual('subcolumn2') + }) + test('complete aliased nested comlumn first part name', () => { + const item = new Identifier( + 'ali.colu', + 'ali.column1.subcolumn2', + '', + ICONS.COLUMN + ) + const completion = item.toCompletionItem() + expect(completion.label).toEqual('column1.subcolumn2') + }) + }) +}) diff --git a/packages/server/test/complete/StringUtils.test.ts b/packages/server/test/complete/StringUtils.test.ts new file mode 100644 index 00000000..30d3b7b0 --- /dev/null +++ b/packages/server/test/complete/StringUtils.test.ts @@ -0,0 +1,30 @@ +import * as StringUtils from '../../src/complete/StringUtils' +describe('StringUtils', () => { + test('getLastToken', () => { + expect(StringUtils.getLastToken('SELECT abc')).toEqual('abc') + expect(StringUtils.getLastToken('SELECT abc.def')).toEqual('abc.def') + expect(StringUtils.getLastToken('SELECT abc[0]')).toEqual('abc') + expect(StringUtils.getLastToken('SELECT abc[0].')).toEqual('abc.') + expect(StringUtils.getLastToken('SELECT abc[0].d')).toEqual('abc.d') + expect(StringUtils.getLastToken('SELECT abc[0].def[0]')).toEqual('abc.def') + expect(StringUtils.getLastToken('SELECT abc[0].def[0].')).toEqual( + 'abc.def.' + ) + expect(StringUtils.getLastToken('SELECT abc[0].def[0].g')).toEqual( + 'abc.def.g' + ) + + expect(StringUtils.getLastToken("SELECT abc['key']")).toEqual('abc') + expect(StringUtils.getLastToken("SELECT abc['key.name'].")).toEqual('abc.') + expect(StringUtils.getLastToken("SELECT abc['key'].d")).toEqual('abc.d') + expect(StringUtils.getLastToken("SELECT abc['key'].def['key']")).toEqual( + 'abc.def' + ) + expect(StringUtils.getLastToken("SELECT abc['key'].def['key'].")).toEqual( + 'abc.def.' + ) + expect(StringUtils.getLastToken("SELECT abc['key'].def[0].g")).toEqual( + 'abc.def.g' + ) + }) +}) From dc64250f1fd4e272889748a4435a000e66def07f Mon Sep 17 00:00:00 2001 From: joe-re Date: Mon, 21 Feb 2022 08:00:38 +0900 Subject: [PATCH 9/9] unify Pos definition --- packages/server/src/complete/AstUtils.ts | 3 +-- packages/server/src/complete/StringUtils.ts | 3 +-- .../server/src/complete/candidates/createJoinCandidates.ts | 3 +-- packages/server/src/complete/complete.ts | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/server/src/complete/AstUtils.ts b/packages/server/src/complete/AstUtils.ts index 9cd48a96..fd9afc05 100644 --- a/packages/server/src/complete/AstUtils.ts +++ b/packages/server/src/complete/AstUtils.ts @@ -6,11 +6,10 @@ import { } from '@joe-re/sql-parser' import log4js from 'log4js' import { Table } from '../database_libs/AbstractClient' +import { Pos } from './complete' const logger = log4js.getLogger() -type Pos = { line: number; column: number } - function isNotEmpty(value: T | null | undefined): value is T { return value === null || value === undefined ? false : true } diff --git a/packages/server/src/complete/StringUtils.ts b/packages/server/src/complete/StringUtils.ts index 97c62c6a..92143eb4 100644 --- a/packages/server/src/complete/StringUtils.ts +++ b/packages/server/src/complete/StringUtils.ts @@ -1,7 +1,6 @@ import { FromTableNode } from '@joe-re/sql-parser' import { Table } from '../database_libs/AbstractClient' - -type Pos = { line: number; column: number } +import { Pos } from './complete' export function makeTableAlias(tableName: string): string { if (tableName.length > 3) { diff --git a/packages/server/src/complete/candidates/createJoinCandidates.ts b/packages/server/src/complete/candidates/createJoinCandidates.ts index 9157ed02..4d0f0eaf 100644 --- a/packages/server/src/complete/candidates/createJoinCandidates.ts +++ b/packages/server/src/complete/candidates/createJoinCandidates.ts @@ -3,10 +3,9 @@ import { SelectStatement } from '@joe-re/sql-parser' import { getNearestFromTableFromPos } from '../AstUtils' import { Table } from '../../database_libs/AbstractClient' import { toCompletionItemForKeyword } from '../CompletionItemUtils' +import { Pos } from '../complete' import { createTableCandidates } from './createTableCandidates' -type Pos = { line: number; column: number } - export function createJoinCondidates( ast: SelectStatement, tables: Table[], diff --git a/packages/server/src/complete/complete.ts b/packages/server/src/complete/complete.ts index 27971490..e819c408 100644 --- a/packages/server/src/complete/complete.ts +++ b/packages/server/src/complete/complete.ts @@ -35,7 +35,7 @@ import { createKeywordCandidatesFromExpectedLiterals } from './candidates/create import { createJoinTablesCandidates } from './candidates/createJoinTableCndidates' import { ICONS, toCompletionItemForKeyword } from './CompletionItemUtils' -type Pos = { line: number; column: number } +export type Pos = { line: number; column: number } const logger = log4js.getLogger()