diff --git a/global.d.ts b/global.d.ts index ceca16a3..372da004 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1,4 +1,7 @@ +// https://www.ibm.com/docs/en/i/7.4?topic=views-syscolumns2 interface TableColumn { + TABLE_SCHEMA: string, + TABLE_NAME: string, COLUMN_NAME: string, SYSTEM_COLUMN_NAME: string, CONSTRAINT_NAME?: string, @@ -13,7 +16,9 @@ interface TableColumn { IS_IDENTITY: "YES" | "NO", } +// https://www.ibm.com/docs/en/i/7.4?topic=views-sysparms interface SQLParm { + SPECIFIC_SCHEMA: string, SPECIFIC_NAME: string, PARAMETER_NAME: string, PARAMETER_MODE: "IN" | "OUT" | "INOUT", @@ -25,6 +30,7 @@ interface SQLParm { DEFAULT?: string, LONG_COMMENT?: string, ORDINAL_POSITION: number, + ROW_TYPE: "P" | "R", } interface BasicSQLObject { diff --git a/package-lock.json b/package-lock.json index 2b65baab..5af274c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,17 @@ { "name": "vscode-db2i", - "version": "1.3.1", + "version": "1.4.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode-db2i", - "version": "1.3.1", + "version": "1.4.1", "dependencies": { "@ibm/mapepire-js": "^0.3.0", "chart.js": "^4.4.2", "csv": "^6.1.3", "json-to-markdown-table": "^1.0.0", - "lru-cache": "^6.0.0", "node-fetch": "^3.3.1", "showdown": "^2.1.0", "sql-formatter": "^14.0.0" @@ -3228,6 +3227,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -5700,7 +5700,8 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/yocto-queue": { "version": "1.0.0", @@ -8090,6 +8091,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "requires": { "yallist": "^4.0.0" } @@ -9690,7 +9692,8 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "yocto-queue": { "version": "1.0.0", diff --git a/package.json b/package.json index 5848de9d..6264a365 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "chart.js": "^4.4.2", "csv": "^6.1.3", "json-to-markdown-table": "^1.0.0", - "lru-cache": "^6.0.0", "node-fetch": "^3.3.1", "showdown": "^2.1.0", "sql-formatter": "^14.0.0" diff --git a/src/database/callable.ts b/src/database/callable.ts index 464a8bbe..60c17bf3 100644 --- a/src/database/callable.ts +++ b/src/database/callable.ts @@ -1,11 +1,13 @@ import vscode from "vscode" import { JobManager } from "../config"; -import { QueryOptions } from "../connection/types"; +import { QueryOptions } from "@ibm/mapepire-js/dist/src/types"; const {instance} = vscode.extensions.getExtension(`halcyontechltd.code-for-ibmi`).exports; export type CallableType = "PROCEDURE"|"FUNCTION"; export interface CallableRoutine { + schema: string; + name: string; specificNames: string[]; type: string; } @@ -13,6 +15,7 @@ export interface CallableRoutine { export interface CallableSignature { specificName: string; parms: SQLParm[]; + returns: SQLParm[]; } export default class Callable { @@ -25,6 +28,8 @@ export default class Callable { ); let routine: CallableRoutine = { + schema, + name, specificNames: [], type: forType } @@ -41,7 +46,7 @@ export default class Callable { const results = await JobManager.runSQL( [ `SELECT * FROM QSYS2.SYSPARMS`, - `WHERE SPECIFIC_SCHEMA = ? AND ROW_TYPE = 'P' AND SPECIFIC_NAME in (${specificNames.map(n => `?`).join(`, `)})`, + `WHERE SPECIFIC_SCHEMA = ? AND ROW_TYPE in ('P', 'R') AND SPECIFIC_NAME in (${specificNames.map(n => `?`).join(`, `)})`, `ORDER BY ORDINAL_POSITION` ].join(` `), { @@ -56,49 +61,11 @@ export default class Callable { const groupedResults: CallableSignature[] = uniqueSpecificNames.map(name => { return { specificName: name, - parms: results.filter(row => row.SPECIFIC_NAME === name) + parms: results.filter(row => row.SPECIFIC_NAME === name && row.ROW_TYPE === `P`), + returns: results.filter(row => row.SPECIFIC_NAME === name && row.ROW_TYPE === `R`) } }); return groupedResults; } - - /** - * @param schema Not user input - * @param specificName Not user input - * @returns - */ - static getParms(schema: string, specificName: string, resolveName: boolean = false): Promise { - const rowType = `P`; // Parameter - return Callable.getFromSysParms(schema, specificName, rowType, resolveName); - } - - static getResultColumns(schema: string, specificName: string, resolveName: boolean = false) { - const rowType = `R`; // Row - return Callable.getFromSysParms(schema, specificName, rowType, resolveName); - } - - static getFromSysParms(schema: string, name: string, rowType: "P"|"R", resolveName: boolean = false): Promise { - let parameters = [schema, rowType]; - - let specificNameClause = undefined; - - if (resolveName) { - specificNameClause = `SPECIFIC_NAME = (select specific_name from qsys2.sysroutines where specific_schema = ? and routine_name = ?)`; - parameters.push(schema, name); - } else { - specificNameClause = `SPECIFIC_NAME = ?`; - parameters.push(name); - } - - const options : QueryOptions = { parameters }; - return JobManager.runSQL( - [ - `SELECT * FROM QSYS2.SYSPARMS`, - `WHERE SPECIFIC_SCHEMA = ? AND ROW_TYPE = ? AND ${specificNameClause}`, - `ORDER BY ORDINAL_POSITION` - ].join(` `), - options - ); - } } \ No newline at end of file diff --git a/src/database/schemas.ts b/src/database/schemas.ts index 6e97aa48..6da419d4 100644 --- a/src/database/schemas.ts +++ b/src/database/schemas.ts @@ -4,7 +4,7 @@ import { getInstance } from "../base"; import { JobManager } from "../config"; export type SQLType = "schemas" | "tables" | "views" | "aliases" | "constraints" | "functions" | "variables" | "indexes" | "procedures" | "sequences" | "packages" | "triggers" | "types" | "logicals"; -type PageData = { filter?: string, offset?: number, limit?: number }; +export type PageData = { filter?: string, offset?: number, limit?: number }; const typeMap = { 'tables': [`T`, `P`, `M`], diff --git a/src/database/table.ts b/src/database/table.ts index c18b4899..e7928519 100644 --- a/src/database/table.ts +++ b/src/database/table.ts @@ -13,6 +13,8 @@ export default class Table { static async getItems(schema: string, name: string): Promise { const sql = [ `SELECT `, + ` column.TABLE_SCHEMA,`, + ` column.TABLE_NAME,`, ` column.COLUMN_NAME,`, ` key.CONSTRAINT_NAME,`, ` column.DATA_TYPE, `, diff --git a/src/extension.ts b/src/extension.ts index fcd14bed..1830ccda 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,7 +10,7 @@ import { getInstance, loadBase } from "./base"; import { JobManager, onConnectOrServerInstall, initConfig } from "./config"; import { queryHistory } from "./views/queryHistoryView"; import { ExampleBrowser } from "./views/examples/exampleBrowser"; -import { languageInit } from "./language"; +import { languageInit } from "./language/providers"; import { initialiseTestSuite } from "./testing"; import { JobManagerView } from "./views/jobManager/jobManagerView"; import { ServerComponent } from "./connection/serverComponent"; @@ -21,6 +21,7 @@ import { SelfTreeDecorationProvider, selfCodesResultsView } from "./views/jobMan import Configuration from "./configuration"; import { JDBCOptions } from "@ibm/mapepire-js/dist/src/types"; import { Db2iUriHandler, getStatementUri } from "./uriHandler"; +import { DbCache } from "./language/providers/logic/cache"; export interface Db2i { sqlJobManager: SQLJobManager, @@ -88,6 +89,7 @@ export function activate(context: vscode.ExtensionContext): Db2i { const instance = getInstance(); instance.subscribe(context, `connected`, `db2i-connected`, () => { + DbCache.resetCache(); selfCodesView.setRefreshEnabled(false); selfCodesView.setJobOnly(false); // Refresh the examples when we have it, so we only display certain examples diff --git a/src/language/index.ts b/src/language/index.ts deleted file mode 100644 index 89200b0a..00000000 --- a/src/language/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { completionProvider } from "./providers/completionProvider"; -import { formatProvider } from "./providers/formatProvider"; -import { signatureProvider } from "./providers/parameterProvider"; - -export function languageInit() { - let functionality = []; - - functionality.push( - completionProvider, - formatProvider, - signatureProvider - ); - - return functionality; -} diff --git a/src/language/providers/completionItemCache.ts b/src/language/providers/completionItemCache.ts deleted file mode 100644 index 48b69d86..00000000 --- a/src/language/providers/completionItemCache.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CompletionItem } from "vscode"; -import LRU from "lru-cache"; -import { QualifiedObject } from "../sql/types"; - -export let changedCache: Set = new Set(); - -export interface CompletionItemCacheObj { - cacheType: "columns" | "all"; - cacheList: CompletionItem[]; -} - -export default class CompletionItemCache extends LRU { - constructor() { - super({ - max: 50, - }); - } -} - -export function toKey(context: string, sqlObj: QualifiedObject|string) { - return `${context}-` + (typeof sqlObj === `string` ? sqlObj : `${sqlObj.schema}.${sqlObj.name}`).toUpperCase(); -} \ No newline at end of file diff --git a/src/language/providers/completionProvider.ts b/src/language/providers/completionProvider.ts index a3887f06..ae605a83 100644 --- a/src/language/providers/completionProvider.ts +++ b/src/language/providers/completionProvider.ts @@ -3,20 +3,17 @@ import { JobManager } from "../../config"; import { default as Database, SQLType, - default as Schemas, } from "../../database/schemas"; import Statement from "../../database/statement"; -import Table from "../../database/table"; import Document from "../sql/document"; import * as LanguageStatement from "../sql/statement"; -import { CTEReference, CallableReference, ClauseType, ObjectRef, StatementType } from "../sql/types"; -import CompletionItemCache, { changedCache, toKey } from "./completionItemCache"; -import Callable, { CallableType } from "../../database/callable"; -import { ServerComponent } from "../../connection/serverComponent"; -import { prepareParamType, createCompletionItem, getParmAttributes, completionItemCache } from "./completion"; -import { isCallableType, getCallableParameters } from "./callable"; -import { variable } from "sql-formatter/lib/src/lexer/regexFactory"; -import { localAssistIsEnabled, remoteAssistIsEnabled } from "./available"; +import { CTEReference, ClauseType, ObjectRef, StatementType } from "../sql/types"; +import { CallableType } from "../../database/callable"; +import { prepareParamType, createCompletionItem, getParmAttributes } from "./logic/completion"; +import { isCallableType, getCallableParameters } from "./logic/callable"; +import { localAssistIsEnabled, remoteAssistIsEnabled } from "./logic/available"; +import { DbCache } from "./logic/cache"; +import { getSqlDocument } from "./logic/parse"; export interface CompletionType { order: string; @@ -91,59 +88,52 @@ async function getObjectColumns( isUDTF = false ): Promise { - const cacheKey = toKey(`columns`, schema + name) - const tableUpdate: boolean = changedCache.delete(cacheKey); - const isCached = completionItemCache.has(cacheKey); + let completionItems: CompletionItem[]; - if (!isCached || tableUpdate) { schema = Statement.noQuotes(Statement.delimName(schema, true)); name = Statement.noQuotes(Statement.delimName(name, true)); - - let completionItems: CompletionItem[] = []; - if (isUDTF) { - const resultSet = await Callable.getResultColumns(schema, name, true); - - if (!resultSet?.length ? true : false) { - completionItemCache.set(cacheKey, []); - return []; - } - - completionItems = resultSet.map((i) => - createCompletionItem( - Statement.prettyName(i.PARAMETER_NAME), - CompletionItemKind.Field, - getParmAttributes(i), - `Schema: ${schema}\nObject: ${name}\n`, - `a@objectcolumn` - ) - ); + if (isUDTF) { + const resultSet = await DbCache.getCachedSignatures(schema, name); + + if (!resultSet?.length ? true : false) { + return []; + } - } else { - const columns = await Table.getItems(schema, name); + const chosenSig = resultSet[resultSet.length-1]; + + completionItems = chosenSig.returns.map((i) => + createCompletionItem( + Statement.prettyName(i.PARAMETER_NAME), + CompletionItemKind.Field, + getParmAttributes(i), + `Schema: ${schema}\nObject: ${name}\n`, + `a@objectcolumn` + ) + ); - if (!columns?.length ? true : false) { - completionItemCache.set(cacheKey, []); - return []; - } + } else { + const columns = await DbCache.getObjectColumns(schema, name); - completionItems = columns.map((i) => - createCompletionItem( - Statement.prettyName(i.COLUMN_NAME), - CompletionItemKind.Field, - getColumnAttributes(i), - `Schema: ${schema}\nTable: ${name}\n`, - `a@objectcolumn` - ) - ); + if (!columns?.length ? true : false) { + return []; } - - const allCols = getAllColumns(name, schema, completionItems); - completionItems.push(allCols); - completionItemCache.set(cacheKey, completionItems); + + completionItems = columns.map((i) => + createCompletionItem( + Statement.prettyName(i.COLUMN_NAME), + CompletionItemKind.Field, + getColumnAttributes(i), + `Schema: ${schema}\nTable: ${name}\n`, + `a@objectcolumn` + ) + ); } + + const allCols = getAllColumns(name, schema, completionItems); + completionItems.push(allCols); - return completionItemCache.get(cacheKey); + return completionItems; } /** @@ -154,36 +144,33 @@ async function getObjectCompletions( sqlTypes: { [index: string]: CompletionType } ): Promise { forSchema = Statement.noQuotes(Statement.delimName(forSchema, true)); - const schemaUpdate: boolean = changedCache.delete(forSchema); - if (!completionItemCache.has(forSchema) || schemaUpdate) { - const promises = Object.entries(sqlTypes).map(async ([_, value]) => { - const data = await Database.getObjects(forSchema, [value.type]); - return data.map((table) => - createCompletionItem( - Statement.prettyName(table.name), - value.icon, - value.label, - `Schema: ${table.schema}`, - value.order - ) - ); - }); + + const promises = Object.entries(sqlTypes).map(async ([_, value]) => { + const data = await DbCache.getObjects(forSchema, [value.type]); + return data.map((table) => + createCompletionItem( + Statement.prettyName(table.name), + value.icon, + value.label, + `Schema: ${table.schema}`, + value.order + ) + ); + }); - const results = await Promise.allSettled(promises); - const list = results - .filter((result) => result.status == "fulfilled") - .map((result) => (result as PromiseFulfilledResult).value) - .flat(); + const results = await Promise.allSettled(promises); + const list = results + .filter((result) => result.status == "fulfilled") + .map((result) => (result as PromiseFulfilledResult).value) + .flat(); - completionItemCache.set(forSchema, list); - } - return completionItemCache.get(forSchema); + return list; } async function getCompletionItemsForSchema( schema: string ): Promise { - const data = (await Database.getObjects(schema, ["procedures"])); + const data = await DbCache.getObjects(schema, ["procedures"]); return data .filter((v, i, a) => a.findIndex(t => (t.name === v.name)) === i) //Hide overloads here @@ -313,11 +300,7 @@ async function getCompletionItemsForTriggerDot( } async function getCachedSchemas() { - if (completionItemCache.has(`SCHEMAS-FOR-SYSTEM`)) { - return completionItemCache.get(`SCHEMAS-FOR-SYSTEM`); - } - - const allSchemas: BasicSQLObject[] = await Schemas.getObjects( + const allSchemas: BasicSQLObject[] = await DbCache.getObjects( undefined, [`schemas`] ); @@ -330,7 +313,6 @@ async function getCachedSchemas() { ) ); - completionItemCache.set(`SCHEMAS-FOR-SYSTEM`, completionItems); return completionItems; } @@ -617,7 +599,7 @@ export const completionProvider = languages.registerCompletionItemProvider( const content = document.getText(); const offset = document.offsetAt(position); - const sqlDoc = new Document(content); + const sqlDoc = getSqlDocument(document); const currentStatement = sqlDoc.getStatementByOffset(offset); const allItems: CompletionItem[] = []; diff --git a/src/language/providers/hoverProvider.ts b/src/language/providers/hoverProvider.ts new file mode 100644 index 00000000..8fd2b107 --- /dev/null +++ b/src/language/providers/hoverProvider.ts @@ -0,0 +1,197 @@ +import { Hover, languages, MarkdownString, workspace } from "vscode"; +import { getSqlDocument } from "./logic/parse"; +import { DbCache, LookupResult, RoutineDetail } from "./logic/cache"; +import { JobManager } from "../../config"; +import Statement from "../../database/statement"; +import { getParmAttributes, prepareParamType } from "./logic/completion"; +import { StatementType } from "../sql/types"; +import { remoteAssistIsEnabled } from "./logic/available"; +import { getPositionData } from "./logic/callable"; +import { CallableSignature } from "../../database/callable"; + +// ================================= +// We need to open provider to exist so symbols can be cached for hover support when opening files +// ================================= + +export const openProvider = workspace.onDidOpenTextDocument(async (document) => { + if (document.languageId === `sql`) { + if (remoteAssistIsEnabled()) { + const sqlDoc = getSqlDocument(document); + const defaultSchema = getDefaultSchema(); + + for (const statement of sqlDoc.statements) { + const refs = statement.getObjectReferences(); + if (refs.length) { + if (statement.type === StatementType.Call) { + const first = refs[0]; + if (first.object.name) { + const name = Statement.noQuotes(Statement.delimName(first.object.name, true)); + const schema = Statement.noQuotes(Statement.delimName(first.object.schema || defaultSchema, true)); + const result = await DbCache.getRoutine(schema, name, `PROCEDURE`); + if (result) { + await DbCache.getSignaturesFor(schema, name, result.specificNames); + } + } + + } else { + for (const ref of refs) { + if (ref.object.name) { + const name = Statement.noQuotes(Statement.delimName(ref.object.name, true)); + const schema = Statement.noQuotes(Statement.delimName(ref.object.schema || defaultSchema, true)); + if (ref.isUDTF) { + const result = await DbCache.getRoutine(schema, name, `FUNCTION`); + if (result) { + await DbCache.getSignaturesFor(schema, name, result.specificNames); + } + } else { + await DbCache.getObjectColumns(schema, name); + } + } + } + } + } + } + } + + } +}); + +export const hoverProvider = languages.registerHoverProvider({ language: `sql` }, { + async provideHover(document, position, token) { + if (!remoteAssistIsEnabled()) return; + + const defaultSchema = getDefaultSchema(); + const sqlDoc = getSqlDocument(document); + const offset = document.offsetAt(position); + + const tokAt = sqlDoc.getTokenByOffset(offset); + const statementAt = sqlDoc.getStatementByOffset(offset); + + const md = new MarkdownString(); + + if (statementAt) { + const refs = statementAt.getObjectReferences(); + const possibleNames = refs.map(ref => ref.object.name).filter(name => name); + + for (const ref of refs) { + const atRef = offset >= ref.tokens[0].range.start && offset <= ref.tokens[ref.tokens.length - 1].range.end; + + if (atRef) { + const schema = ref.object.schema || defaultSchema; + const result = await lookupSymbol(ref.object.name, schema, possibleNames); + + + if (result) { + if ('routine' in result) { + const routineOffset = ref.tokens[ref.tokens.length-1].range.end+1; + const callableRef = statementAt.getCallableDetail(routineOffset, false); + if (callableRef) { + const { currentCount } = getPositionData(callableRef, routineOffset); + const signatures = await DbCache.getCachedSignatures(callableRef.parentRef.object.schema, callableRef.parentRef.object.name); + const possibleSignatures = signatures.filter((s) => s.parms.length >= currentCount).sort((a, b) => a.parms.length - b.parms.length); + const signature = possibleSignatures.find((signature) => currentCount <= signature.parms.length); + if (signature) { + addRoutineMd(md, signature, result); + } + } + } else { + addSymbol(md, result); + } + } + + if (systemSchemas.includes(schema.toUpperCase())) { + addSearch(md, ref.object.name, result !== undefined); + } + } + } + + // If no symbol found, check if we can find a symbol by name + if (md.value.length === 0 && tokAt && tokAt.type === `word` && tokAt.value) { + const result = await lookupSymbol(tokAt.value, undefined, possibleNames); + if (result) { + addSymbol(md, result); + } + } + } + + return md.value ? new Hover(md) : undefined; + } +}); + +const systemSchemas = [`QSYS`, `QSYS2`, `SYSTOOLS`]; + +function addRoutineMd(base: MarkdownString, signature: CallableSignature, result: RoutineDetail) { + const returns = signature.returns.length > 0 ? `: ${signature.returns.length} column${signature.returns.length === 1 ? `` : `s`}` : ''; + + let codeLines: string[] = [`${Statement.prettyName(result.routine.name)}(`]; + + for (let i = 0; i < signature.parms.length; i++) { + const parm = signature.parms[i]; + let parmString = ` ${Statement.prettyName(parm.PARAMETER_NAME || `parm${i + 1}`)} => ${prepareParamType(parm)}`; + + if (i < signature.parms.length - 1) { + parmString += `,`; + } + + codeLines.push(parmString); + } + + codeLines.push(`)${returns}`); + + base.appendCodeblock(codeLines.join(`\n`), `sql`); + + let parmDetail = [``, `---`]; + + for (let i = 0; i < signature.parms.length; i++) { + const parm = signature.parms[i]; + const escapedAsterisk = parm.LONG_COMMENT ? parm.LONG_COMMENT.replace(/\*/g, `\\*`) : ``; + parmDetail.push(``, `*@param* \`${Statement.prettyName(parm.PARAMETER_NAME || `parm${i}`)}\` ${escapedAsterisk}`); + } + + parmDetail.push(``); + + base.appendMarkdown(parmDetail.join(`\n`)); +} + +function addSearch(base: MarkdownString, value: string, withGap = true) { + if (withGap) { + base.appendMarkdown([``, `---`, ``].join(`\n`)); + } + + base.appendMarkdown([ + `[Search on IBM Documentation](https://www.ibm.com/docs/en/search/${encodeURI(value)})` + ].join(`\n\n`)); +} + +function addList(base: MarkdownString, items: string[]) { + if (items.length) { + base.appendMarkdown(`\n` + items.map(item => `- ${item}`).join(`\n`) + `\n`); + } +} + +function addSymbol(base: MarkdownString, symbol: LookupResult) { + base.isTrusted = true; + if ('PARAMETER_NAME' in symbol) { + base.appendCodeblock(prepareParamType(symbol) + `\n`, `sql`); + } + else if ('COLUMN_NAME' in symbol) { + base.appendCodeblock(prepareParamType(symbol) + `\n`, `sql`); + } + else if ('name' in symbol) { + addList(base, [ + `**Description:** ${symbol.text}`, + ]); + } +} + +function lookupSymbol(name: string, schema: string | undefined, possibleNames: string[]) { + name = Statement.noQuotes(Statement.delimName(name, true)); + schema = schema ? Statement.noQuotes(Statement.delimName(schema, true)) : undefined + + return DbCache.lookupSymbol(name, schema, possibleNames); +} + +const getDefaultSchema = (): string => { + const currentJob = JobManager.getSelection(); + return currentJob && currentJob.job.options.libraries[0] ? currentJob.job.options.libraries[0] : `QGPL`; +} \ No newline at end of file diff --git a/src/language/providers/index.ts b/src/language/providers/index.ts new file mode 100644 index 00000000..07a54872 --- /dev/null +++ b/src/language/providers/index.ts @@ -0,0 +1,18 @@ +import { completionProvider } from "./completionProvider"; +import { formatProvider } from "./formatProvider"; +import { hoverProvider, openProvider } from "./hoverProvider"; +import { signatureProvider } from "./parameterProvider"; + +export function languageInit() { + let functionality = []; + + functionality.push( + completionProvider, + formatProvider, + signatureProvider, + hoverProvider, + openProvider + ); + + return functionality; +} diff --git a/src/language/providers/available.ts b/src/language/providers/logic/available.ts similarity index 69% rename from src/language/providers/available.ts rename to src/language/providers/logic/available.ts index ff056916..020d91a6 100644 --- a/src/language/providers/available.ts +++ b/src/language/providers/logic/available.ts @@ -1,7 +1,7 @@ import { env } from "process"; -import { ServerComponent } from "../../connection/serverComponent"; -import { JobManager } from "../../config"; +import { ServerComponent } from "../../../connection/serverComponent"; +import { JobManager } from "../../../config"; export function localAssistIsEnabled() { return (env.DB2I_DISABLE_CA !== `true`); diff --git a/src/language/providers/logic/cache.ts b/src/language/providers/logic/cache.ts new file mode 100644 index 00000000..2417747b --- /dev/null +++ b/src/language/providers/logic/cache.ts @@ -0,0 +1,166 @@ +import Callable, { CallableRoutine, CallableSignature, CallableType } from "../../../database/callable"; +import Schemas, { PageData, SQLType } from "../../../database/schemas"; +import Table from "../../../database/table"; + +export interface RoutineDetail { + routine: CallableRoutine; + signatures: CallableSignature[]; +} + +export type LookupResult = RoutineDetail | SQLParm | BasicSQLObject | TableColumn; + +export class DbCache { + private static schemaObjects: Map = new Map(); + private static objectColumns: Map = new Map(); + private static routines: Map = new Map(); + private static routineSignatures: Map = new Map(); + + private static toReset: string[] = []; + + static async resetCache() { + this.objectColumns.clear(); + this.schemaObjects.clear(); + this.toReset = []; + } + + static resetObject(name: string) { + this.toReset.push(name.toLowerCase()); + } + + private static shouldReset(name?: string) { + if (name) { + const inx = this.toReset.indexOf(name.toLowerCase()); + + if (inx > -1) { + this.toReset.splice(inx, 1); + return true; + } + } + + return false; + } + + static async lookupSymbol(name: string, schema: string|undefined, objectFilter: string[]): Promise { + const included = (lookupName: string) => { + if (objectFilter) { + return objectFilter.includes(lookupName.toLowerCase()); + } + return true; + } + + if (schema) { + // Looking routine + const routine = this.getCachedRoutine(schema, name, `FUNCTION`) || this.getCachedRoutine(schema, name, `PROCEDURE`); + if (routine) { + const signatures = this.getCachedSignatures(schema, name); + return { routine, signatures } as RoutineDetail; + } + + objectFilter = objectFilter.map(o => o.toLowerCase()); + + // Search objects + for (const currentSchema of this.schemaObjects.values()) { + const chosenObject = currentSchema.find(sqlObject => included(sqlObject.name) && sqlObject.name === name && sqlObject.schema === schema); + if (chosenObject) { + return chosenObject; + } + } + + // Finally, let's do a last lookup + const lookupRoutine = await this.getRoutine(schema, name, `FUNCTION`) || await this.getRoutine(schema, name, `PROCEDURE`); + if (lookupRoutine) { + const signatures = await this.getSignaturesFor(schema, name, lookupRoutine.specificNames); + return { routine: lookupRoutine, signatures } as RoutineDetail; + } + } + + // Lookup by column + + // First object columns + for (const currentObject of this.objectColumns.values()) { + const chosenColumn = currentObject.find(column => included(column.TABLE_NAME) && column.COLUMN_NAME.toLowerCase() === name.toLowerCase()); + if (chosenColumn) { + return chosenColumn; + } + } + + // Then by routine result columns + for (const currentRoutineSig of this.routineSignatures.values()) { + for (const signature of currentRoutineSig) { + const chosenColumn = signature.returns.find(column => column.PARAMETER_NAME.toLowerCase() === name.toLowerCase()); + if (chosenColumn) { + return chosenColumn; + } + } + } + } + + static async getObjectColumns(schema: string, name: string) { + const key = getKey(`columns`, schema, name); + + if (!this.objectColumns.has(key) || this.shouldReset(name)) { + const result = await Table.getItems(schema, name); + if (result) { + this.objectColumns.set(key, result); + } + } + + return this.objectColumns.get(key) || []; + } + + static async getObjects(schema: string, types: SQLType[], details?: PageData) { + const key = getKey(`objects`, schema, types.join(`&`)); + + if (!this.schemaObjects.has(key) || this.shouldReset(schema)) { + const result = await Schemas.getObjects(schema, types, details); + if (result) { + this.schemaObjects.set(key, result); + } + } + + return this.schemaObjects.get(key) || []; + } + + static async getRoutine(schema: string, name: string, type: CallableType) { + const key = getKey(type, schema, name); + + if (!this.routines.has(key) || this.shouldReset(name)) { + const result = await Callable.getType(schema, name, type); + if (result) { + this.routines.set(key, result); + } else { + this.routines.set(key, false); + return false; + } + } + + return this.routines.get(key) || undefined; + } + + static getCachedRoutine(schema: string, name: string, type: CallableType) { + const key = getKey(type, schema, name); + return this.routines.get(key) || undefined + } + + static async getSignaturesFor(schema: string, name: string, specificNames: string[]) { + const key = getKey(`signatures`, schema, name); + + if (!this.routineSignatures.has(key) || this.shouldReset(name)) { + const result = await Callable.getSignaturesFor(schema, specificNames); + if (result) { + this.routineSignatures.set(key, result); + } + } + + return this.routineSignatures.get(key) || []; + } + + static getCachedSignatures(schema: string, name: string) { + const key = getKey(`signatures`, schema, name); + return this.routineSignatures.get(key) || []; + } +} + +function getKey(type: string, schema: string, name: string = `all`) { + return `${type}.${schema}.${name}`.toLowerCase(); +} \ No newline at end of file diff --git a/src/language/providers/callable.ts b/src/language/providers/logic/callable.ts similarity index 81% rename from src/language/providers/callable.ts rename to src/language/providers/logic/callable.ts index 1a8b078a..d6f0ca8e 100644 --- a/src/language/providers/callable.ts +++ b/src/language/providers/logic/callable.ts @@ -1,11 +1,9 @@ import { CompletionItem, CompletionItemKind, SnippetString } from "vscode"; -import Callable, { CallableSignature, CallableType } from "../../database/callable"; -import { ObjectRef, QualifiedObject, CallableReference } from "../sql/types"; -import Statement from "../../database/statement"; -import { completionItemCache, createCompletionItem, getParmAttributes } from "./completion"; -import { toKey } from "./completionItemCache"; - -const SIGNATURE_CONTEXT_KEY = `sigs`; +import { CallableSignature, CallableType } from "../../../database/callable"; +import { ObjectRef, CallableReference } from "../../sql/types"; +import Statement from "../../../database/statement"; +import { createCompletionItem, getParmAttributes } from "./completion"; +import { DbCache } from "./cache"; /** * Checks if the ref exists as a procedure or function. Then, @@ -16,21 +14,13 @@ export async function isCallableType(ref: ObjectRef, type: CallableType) { ref.object.schema = Statement.noQuotes(Statement.delimName(ref.object.schema, true)); ref.object.name = Statement.noQuotes(Statement.delimName(ref.object.name, true)); - const cacheKey = toKey(SIGNATURE_CONTEXT_KEY, ref.object); - - if (completionItemCache.has(cacheKey)) { - return true; - } - - const callableRoutine = await Callable.getType(ref.object.schema, ref.object.name, type); + const callableRoutine = await DbCache.getRoutine(ref.object.schema, ref.object.name, type); if (callableRoutine) { - const parms = await Callable.getSignaturesFor(ref.object.schema, callableRoutine.specificNames); - completionItemCache.set(cacheKey, parms); + await DbCache.getSignaturesFor(ref.object.schema, ref.object.name, callableRoutine.specificNames); return true; } else { // Not callable, let's just cache it as empty to stop spamming the db - completionItemCache.set(cacheKey, []); } } @@ -42,7 +32,7 @@ export async function isCallableType(ref: ObjectRef, type: CallableType) { * that are stored in the cache for a specific procedure */ export function getCallableParameters(ref: CallableReference, offset: number): CompletionItem[] { - const signatures = getCachedSignatures(ref); + const signatures = DbCache.getCachedSignatures(ref.parentRef.object.schema, ref.parentRef.object.name) if (signatures) { const { firstNamedParameter, currentCount } = getPositionData(ref, offset); @@ -149,11 +139,4 @@ export function getPositionData(ref: CallableReference, offset: number) { currentCount: paramCommas.length + 1, firstNamedParameter }; -} - -export function getCachedSignatures(ref: CallableReference): CallableSignature[] | undefined { - const key = toKey(SIGNATURE_CONTEXT_KEY, ref.parentRef.object); - if (completionItemCache.has(key)) { - return completionItemCache.get(key); - } } \ No newline at end of file diff --git a/src/language/providers/completion.ts b/src/language/providers/logic/completion.ts similarity index 67% rename from src/language/providers/completion.ts rename to src/language/providers/logic/completion.ts index 6b377f41..e2f1674d 100644 --- a/src/language/providers/completion.ts +++ b/src/language/providers/logic/completion.ts @@ -1,7 +1,4 @@ import { CompletionItemKind, CompletionItem } from "vscode"; -import CompletionItemCache from "./completionItemCache"; - -export const completionItemCache = new CompletionItemCache(); export function createCompletionItem( name: string, @@ -27,13 +24,21 @@ export function getParmAttributes(parm: SQLParm): string { } export function prepareParamType(param: TableColumn | SQLParm): string { + let baseType = param.DATA_TYPE.toLowerCase(); + if (param.CHARACTER_MAXIMUM_LENGTH) { - return `${param.DATA_TYPE}(${param.CHARACTER_MAXIMUM_LENGTH})`; + baseType += `(${param.CHARACTER_MAXIMUM_LENGTH})`; } if (param.NUMERIC_PRECISION !== null && param.NUMERIC_SCALE !== null) { - return `${param.DATA_TYPE}(${param.NUMERIC_PRECISION}, ${param.NUMERIC_SCALE})`; + baseType += `(${param.NUMERIC_PRECISION}, ${param.NUMERIC_SCALE})`; } - return `${param.DATA_TYPE}`; + const usefulNull = 'COLUMN_NAME' in param || ('ROW_TYPE' in param && param.ROW_TYPE === 'R'); + + if (usefulNull && [`Y`, `YES`].includes(param.IS_NULLABLE)) { + baseType += ` nullable`; + }; + + return baseType; } diff --git a/src/language/providers/logic/parse.ts b/src/language/providers/logic/parse.ts new file mode 100644 index 00000000..d95a70cd --- /dev/null +++ b/src/language/providers/logic/parse.ts @@ -0,0 +1,22 @@ +import { TextDocument } from "vscode"; +import Document from "../../sql/document"; + +let cached: Map = new Map(); + +export function getSqlDocument(document: TextDocument): Document { + const uri = document.uri.toString(); + const likelyNew = document.uri.scheme === `untitled` && document.version === 1; + + if (cached.has(uri) && !likelyNew) { + const { ast, version } = cached.get(uri)!; + + if (version === document.version) { + return ast; + } + } + + const newAsp = new Document(document.getText()); + cached.set(uri, { ast: newAsp, version: document.version }); + + return newAsp; +} \ No newline at end of file diff --git a/src/language/providers/parameterProvider.ts b/src/language/providers/parameterProvider.ts index 235fc0fe..fbe2368c 100644 --- a/src/language/providers/parameterProvider.ts +++ b/src/language/providers/parameterProvider.ts @@ -1,11 +1,13 @@ import { MarkdownString, ParameterInformation, Position, Range, SignatureHelp, SignatureInformation, TextEdit, languages } from "vscode"; import Statement from "../../database/statement"; import Document from "../sql/document"; -import { getCachedSignatures, getCallableParameters, getPositionData, isCallableType } from "./callable"; -import { getParmAttributes, prepareParamType } from "./completion"; +import { getCallableParameters, getPositionData, isCallableType } from "./logic/callable"; +import { getParmAttributes, prepareParamType } from "./logic/completion"; import { CallableType } from "../../database/callable"; import { StatementType } from "../sql/types"; -import { remoteAssistIsEnabled } from "./available"; +import { remoteAssistIsEnabled } from "./logic/available"; +import { DbCache } from "./logic/cache"; +import { getSqlDocument } from "./logic/parse"; export const signatureProvider = languages.registerSignatureHelpProvider({ language: `sql` }, { async provideSignatureHelp(document, position, token, context) { @@ -14,7 +16,7 @@ export const signatureProvider = languages.registerSignatureHelpProvider({ langu if (remoteAssistIsEnabled()) { - const sqlDoc = new Document(content); + const sqlDoc = getSqlDocument(document); const currentStatement = sqlDoc.getStatementByOffset(offset); if (currentStatement) { @@ -24,7 +26,7 @@ export const signatureProvider = languages.registerSignatureHelpProvider({ langu if (callableRef) { const isValid = await isCallableType(callableRef.parentRef, routineType); if (isValid) { - let signatures = getCachedSignatures(callableRef); + let signatures = DbCache.getCachedSignatures(callableRef.parentRef.object.schema, callableRef.parentRef.object.name); if (signatures) { const help = new SignatureHelp(); diff --git a/src/language/sql/document.ts b/src/language/sql/document.ts index 2a51a348..2bbf41df 100644 --- a/src/language/sql/document.ts +++ b/src/language/sql/document.ts @@ -50,7 +50,14 @@ export default class Document { case `keyword`: switch (tokens[i].value?.toUpperCase()) { + case `LOOP`: + // This handles the case that 'END LOOP' is supported. + if (currentStatementType === StatementType.End) { + break; + } case `BEGIN`: + case `DO`: + case `THEN`: // We include BEGIN in the current statement // then the next statement beings const statementTokens = tokens.slice(statementStart, i+1); diff --git a/src/language/sql/statement.ts b/src/language/sql/statement.ts index 55944910..e40a84a1 100644 --- a/src/language/sql/statement.ts +++ b/src/language/sql/statement.ts @@ -26,9 +26,6 @@ export default class Statement { } switch (this.type) { - case StatementType.With: - this.tokens = SQLTokeniser.createBlocks(this.tokens); - break; case StatementType.Create: // No scalar transformation here.. break; @@ -213,23 +210,25 @@ export default class Statement { getCTEReferences(): CTEReference[] { if (this.type !== StatementType.With) return []; + + const withBlocks = SQLTokeniser.createBlocks(this.tokens.slice(0)); let cteList: CTEReference[] = []; - for (let i = 0; i < this.tokens.length; i++) { - if (tokenIs(this.tokens[i], `word`)) { - let cteName = this.tokens[i].value!; + for (let i = 0; i < withBlocks.length; i++) { + if (tokenIs(withBlocks[i], `word`) || tokenIs(withBlocks[i], `function`)) { + let cteName = withBlocks[i].value!; let parameters: string[] = []; let statementBlockI = i+1; - if (tokenIs(this.tokens[i+1], `block`) && tokenIs(this.tokens[i+2], `keyword`, `AS`)) { - parameters = this.tokens[i+1].block!.filter(blockToken => blockToken.type === `word`).map(blockToken => blockToken.value!) + if (tokenIs(withBlocks[i+1], `block`) && tokenIs(withBlocks[i+2], `keyword`, `AS`)) { + parameters = withBlocks[i+1].block!.filter(blockToken => blockToken.type === `word`).map(blockToken => blockToken.value!) statementBlockI = i+3; - } else if (tokenIs(this.tokens[i+1], `keyword`, `AS`)) { + } else if (tokenIs(withBlocks[i+1], `keyword`, `AS`)) { statementBlockI = i+2; } - const statementBlock = this.tokens[statementBlockI]; + const statementBlock = withBlocks[statementBlockI]; if (tokenIs(statementBlock, `block`)) { cteList.push({ name: cteName, @@ -241,7 +240,7 @@ export default class Statement { i = statementBlockI; } - if (tokenIs(this.tokens[i], `statementType`, `SELECT`)) { + if (tokenIs(withBlocks[i], `statementType`, `SELECT`)) { break; } } @@ -331,6 +330,9 @@ export default class Statement { if (sqlObj) { doAdd(sqlObj); i += sqlObj.tokens.length; + if (sqlObj.isUDTF || sqlObj.fromLateral) { + i += 3; //For the brackets + } } } } @@ -513,17 +515,15 @@ export default class Statement { let endIndex = i; - let isUDTF = false; - - if (tokenIs(nextToken, `function`, `TABLE`)) { - isUDTF = true; - } + const isUDTF = tokenIs(nextToken, `function`, `TABLE`); + const isLateral = tokenIs(nextToken, `function`, `LATERAL`); if (isUDTF) { sqlObj = this.getRefAtToken(i+2); if (sqlObj) { sqlObj.isUDTF = true; const blockTokens = this.getBlockAt(sqlObj.tokens[0].range.end); + sqlObj.tokens = blockTokens; nextIndex = i + 2 + blockTokens.length; nextToken = this.tokens[nextIndex]; @@ -532,7 +532,15 @@ export default class Statement { nextIndex = -1; nextToken = undefined; } - + } else if (isLateral) { + const blockTokens = this.getBlockAt(nextToken.range.end+1); + const newStatement = new Statement(blockTokens, {start: nextToken.range.start, end: blockTokens[blockTokens.length-1].range.end}); + [sqlObj] = newStatement.getObjectReferences(); + + sqlObj.fromLateral = true; + nextIndex = i + 2 + blockTokens.length; + nextToken = this.tokens[nextIndex]; + } else { if (nextToken && NameTypes.includes(nextToken.type)) { nextIndex = i; diff --git a/src/language/sql/tests/statements.test.ts b/src/language/sql/tests/statements.test.ts index a3d7ed4a..f06008aa 100644 --- a/src/language/sql/tests/statements.test.ts +++ b/src/language/sql/tests/statements.test.ts @@ -943,6 +943,147 @@ describe(`Object references`, () => { expect(refs[0].object.name).toBe(`apiVersion`); expect(refs[0].createType).toBe(`varchar(10) ccsid 1208 default '2023-07-07'`); }); + + test(`SELECT, WITH & LATERAL`, () => { + const lines = [ + `with qsysobjs (lib, obj, type) as (`, + ` select object_library, object_name, object_type`, + ` from table (qsys2.object_ownership(current_user))`, + ` where path_name is null`, + `)`, + `select lib concat '/' concat obj concat ' (' concat type concat ')' as label,`, + ` objsize as "Size"`, + ` from qsysobjs q, lateral (`, + ` select objcreated, last_used_timestamp, objsize`, + ` from table (qsys2.object_statistics(lib, type, obj))`, + ` ) z`, + `where objsize is not null`, + `order by OBJSIZE DESC`, + `limit 10;`, + ].join(`\n`); + + const document = new Document(lines); + + expect(document.statements.length).toBe(1); + + const statement = document.statements[0]; + + expect(statement.type).toBe(StatementType.With); + + const refs = statement.getObjectReferences(); + expect(refs.length).toBe(3); + + expect(refs[0].object.name).toBe(`object_ownership`); + expect(refs[0].object.schema).toBe(`qsys2`); + + expect(refs[1].object.name).toBe(`qsysobjs`); + expect(refs[1].object.schema).toBeUndefined(); + expect(refs[1].alias).toBe(`q`); + + expect(refs[2].object.name).toBe(`object_statistics`); + expect(refs[2].object.schema).toBe(`qsys2`); + expect(refs[2].alias).toBe(`z`); + }); + + test(`Multiple UDTFs`, () => { + const lines = [ + `SELECT b.objlongschema, b.objname, b.objtype, b.objattribute, b.objcreated, b.objsize, b.objtext, b.days_used_count, b.last_used_timestamp,b.* FROM `, + ` TABLE (QSYS2.OBJECT_STATISTICS('*ALLUSRAVL ', '*LIB') ) as a, `, + ` TABLE (QSYS2.OBJECT_STATISTICS(a.objname, 'ALL') ) AS b`, + `WHERE b.OBJOWNER = 'user-name'`, + `ORDER BY b.OBJSIZE DESC`, + `FETCH FIRST 100 ROWS ONLY;`, + ].join(`\n`); + + const document = new Document(lines); + + expect(document.statements.length).toBe(1); + + const statement = document.statements[0]; + + expect(statement.type).toBe(StatementType.Select); + + const refs = statement.getObjectReferences(); + + expect(refs.length).toBe(2); + expect(refs[0].object.name).toBe(`OBJECT_STATISTICS`); + expect(refs[0].object.schema).toBe(`QSYS2`); + expect(refs[0].alias).toBe(`a`); + + expect(refs[1].object.name).toBe(`OBJECT_STATISTICS`); + expect(refs[1].object.schema).toBe(`QSYS2`); + expect(refs[1].alias).toBe(`b`); + }); + + test('LOOP statements', () => { + const lines = [ + `CREATE OR REPLACE PROCEDURE KRAKEN917.Wait_For_Kraken(kraken_job_name varchar(10), delay_time bigint default 30)`, + `BEGIN`, + ` DECLARE v_sql_stmt CLOB(1M) CCSID 37;`, + ``, + ` DECLARE number_of_active_jobs INT;`, + ``, + ` CALL systools.lprintf('Waiting for job to finish...');`, + ``, + ` fetch_loop: LOOP`, + ` SET v_sql_stmt ='values(SELECT COUNT(*) FROM TABLE (qsys2.active_job_info(subsystem_list_filter => ''QBATCH'')) WHERE JOB_NAME_SHORT LIKE ''' CONCAT kraken_job_name CONCAT ''') into ?';`, + ``, + ` PREPARE values_st FROM v_sql_stmt;`, + ``, + ` EXECUTE values_st USING number_of_active_jobs;`, + ``, + ` IF number_of_active_jobs = 0 THEN`, + ` CALL SYSTOOLS.LPRINTF(kraken_job_name CONCAT ' JOB DONE');`, + ``, + ` LEAVE fetch_loop;`, + ``, + ` END IF;`, + ``, + ` CALL qsys2.qcmdexc('DLYJOB ' CONCAT delay_time);`, + ``, + ` END LOOP fetch_loop;`, + ``, + `END;`, + ].join(`\n`); + + const document = new Document(lines); + + const groups = document.getStatementGroups(); + expect(groups.length).toBe(1); + + const group = groups[0]; + + // console.log(group.statements.map((s, so) => `${so} ` + s.type.padEnd(10) + ` ` + s.tokens.map(t => t.value).join(' '))); + + expect(group.statements.length).toBe(16); + expect(group.statements.map(s => s.type)).toEqual([ + 'Create', 'Declare', + 'Declare', 'Call', + 'Unknown', 'Unknown', + 'Unknown', 'Unknown', + 'Unknown', 'Call', + 'Unknown', 'End', + 'Call', 'End', + 'Unknown', 'End' + ]); + + let refs; + + const firstCall = group.statements[3]; + refs = firstCall.getObjectReferences(); + expect(refs.length).toBe(1); + expect(refs[0].object.name).toBe(`lprintf`); + + const secondCall = group.statements[9]; + refs = secondCall.getObjectReferences(); + expect(refs.length).toBe(1); + expect(refs[0].object.name).toBe(`LPRINTF`); + + const thirdCall = group.statements[12]; + refs = thirdCall.getObjectReferences(); + expect(refs.length).toBe(1); + expect(refs[0].object.name).toBe(`qcmdexc`); + }) }); describe(`Offset reference tests`, () => { @@ -1138,8 +1279,27 @@ describe(`PL body tests`, () => { const refs = statement.getObjectReferences(); const ctes = statement.getCTEReferences(); - expect(refs.length).toBe(1); - expect(refs[0].object.name).toBe(`Temp02`); + expect(refs.length).toBe(7); + expect(refs[0].object.name).toBe(`shipments`); + expect(refs[0].alias).toBe(`s`); + + expect(refs[1].object.name).toBe(`BillingDate`); + expect(refs[1].alias).toBeUndefined(); + + expect(refs[2].object.name).toBe(`Temp01`); + expect(refs[2].alias).toBe(`t1`); + + expect(refs[3].object.name).toBe(`Temp01`); + expect(refs[3].alias).toBe(`t1`); + + expect(refs[4].object.name).toBe(`Temp02`); + expect(refs[4].alias).toBe(`t2`); + + expect(refs[5].object.name).toBe(`customers`); + expect(refs[5].alias).toBe(`c`); + + expect(refs[6].object.name).toBe(`Temp02`); + expect(refs[6].alias).toBeUndefined(); expect(ctes.length).toBe(3); expect(ctes[0].name).toBe(`Temp01`); @@ -1174,9 +1334,11 @@ describe(`PL body tests`, () => { expect(statement.type).toBe(StatementType.With); const objs = statement.getObjectReferences(); - expect(objs.length).toBe(1); - expect(objs[0].object.schema).toBe(undefined); - expect(objs[0].object.name).toBe(`cteme`); + expect(objs.length).toBe(2); + expect(objs[0].object.schema).toBe(`qsys2`); + expect(objs[0].object.name).toBe(`sysixadv`); + expect(objs[1].object.schema).toBe(undefined); + expect(objs[1].object.name).toBe(`cteme`); const ctes = statement.getCTEReferences(); expect(ctes.length).toBe(1); diff --git a/src/language/sql/tokens.ts b/src/language/sql/tokens.ts index 19837829..e462c064 100644 --- a/src/language/sql/tokens.ts +++ b/src/language/sql/tokens.ts @@ -74,7 +74,7 @@ export default class SQLTokeniser { { name: `KEYWORD`, match: [{ type: `word`, match: (value: string) => { - return [`AS`, `FOR`, `OR`, `REPLACE`, `BEGIN`, `END`, `CURSOR`, `DEFAULT`, `HANDLER`, `REFERENCES`, `ON`, `UNIQUE`, `SPECIFIC`, `EXTERNAL`].includes(value.toUpperCase()); + return [`AS`, `FOR`, `OR`, `REPLACE`, `BEGIN`, `DO`, `THEN`, `LOOP`, `END`, `CURSOR`, `DEFAULT`, `HANDLER`, `REFERENCES`, `ON`, `UNIQUE`, `SPECIFIC`, `EXTERNAL`].includes(value.toUpperCase()); } }], becomes: `keyword`, }, diff --git a/src/language/sql/types.ts b/src/language/sql/types.ts index 75c69e20..b79f1479 100644 --- a/src/language/sql/types.ts +++ b/src/language/sql/types.ts @@ -84,6 +84,7 @@ export interface ObjectRef { alias?: string; isUDTF?: boolean; + fromLateral?: boolean; /** only used within create statements */ createType?: string; diff --git a/src/uriHandler.ts b/src/uriHandler.ts index f01dfb04..39ec126b 100644 --- a/src/uriHandler.ts +++ b/src/uriHandler.ts @@ -2,7 +2,7 @@ import { commands, env, Selection, Uri, UriHandler, window, workspace } from "vs import querystring from "querystring"; import Document from "./language/sql/document"; import { ServerComponent } from "./connection/serverComponent"; -import { remoteAssistIsEnabled } from "./language/providers/available"; +import { remoteAssistIsEnabled } from "./language/providers/logic/available"; export class Db2iUriHandler implements UriHandler { handleUri(uri: Uri) { diff --git a/src/views/results/index.ts b/src/views/results/index.ts index 9125e84d..b643be06 100644 --- a/src/views/results/index.ts +++ b/src/views/results/index.ts @@ -5,7 +5,6 @@ import * as csv from "csv/sync"; import { JobManager } from "../../config"; import Document from "../../language/sql/document"; -import { changedCache } from "../../language/providers/completionItemCache"; import { ParsedEmbeddedStatement, StatementGroup, StatementType } from "../../language/sql/types"; import Statement from "../../language/sql/statement"; import { ExplainTree } from "./explain/nodes"; @@ -16,6 +15,7 @@ import { ResultSetPanelProvider } from "./resultSetPanelProvider"; import { generateSqlForAdvisedIndexes } from "./explain/advice"; import { updateStatusBar } from "../jobManager/statusBar"; import { ExplainType } from "@ibm/mapepire-js/dist/src/types"; +import { DbCache } from "../../language/providers/logic/cache"; export type StatementQualifier = "statement" | "explain" | "onlyexplain" | "json" | "csv" | "cl" | "sql"; @@ -202,7 +202,10 @@ async function runHandler(options?: StatementInfo) { statement.type === StatementType.Create && ref.createType.toUpperCase() === `schema` ? ref.object.schema || `` : ref.object.schema + ref.object.name; - changedCache.add((databaseObj || ``).toUpperCase()); + + if (databaseObj) { + DbCache.resetObject(databaseObj); + } } if (statementDetail.content.trim().length > 0) {