├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── release.yaml │ └── webpack_ci.yaml ├── .gitignore ├── .prompts ├── self-analyst.prompt └── self-report.prompt ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── .vscodeignore ├── LICENSE ├── README.md ├── media ├── context.png ├── logo.png └── main.png ├── package-lock.json ├── package.json ├── snippets ├── aggregates.code-snippets ├── contributes.json ├── create.code-snippets ├── http.code-snippets ├── scalars.code-snippets └── variables.code-snippets ├── src ├── IBMiDetail.ts ├── Storage.ts ├── aiProviders │ ├── README.md │ ├── context.ts │ ├── continue │ │ ├── continueContextProvider.ts │ │ └── listTablesContextProvider.ts │ ├── copilot │ │ ├── contributes.json │ │ ├── index.ts │ │ └── sqlTool.ts │ ├── prompt.ts │ └── prompts.ts ├── base.ts ├── config.ts ├── configuration.ts ├── connection │ ├── SCVersion.ts │ ├── manager.ts │ ├── serverComponent.ts │ ├── sqlJob.ts │ ├── syntaxChecker │ │ ├── checker.ts │ │ └── index.ts │ └── types.ts ├── contributes.json ├── database │ ├── callable.ts │ ├── schemas.ts │ ├── serviceInfo.ts │ ├── statement.ts │ ├── table.ts │ └── view.ts ├── dsc.ts ├── extension.ts ├── language │ ├── index.ts │ ├── json.ts │ ├── providers │ │ ├── completionProvider.ts │ │ ├── contributes.json │ │ ├── formatProvider.ts │ │ ├── hoverProvider.ts │ │ ├── index.ts │ │ ├── logic │ │ │ ├── available.ts │ │ │ ├── cache.ts │ │ │ ├── callable.ts │ │ │ ├── completion.ts │ │ │ └── parse.ts │ │ ├── parameterProvider.ts │ │ ├── peekProvider.ts │ │ ├── problemProvider.ts │ │ └── statusProvider.ts │ └── sql │ │ ├── document.ts │ │ ├── statement.ts │ │ ├── tests │ │ ├── blocks.test.ts │ │ ├── statements.test.ts │ │ └── tokens.test.ts │ │ ├── tokens.ts │ │ └── types.ts ├── notebooks │ ├── Controller.ts │ ├── IBMiSerializer.ts │ ├── contributes.json │ ├── jupyter.ts │ └── logic │ │ ├── chart.ts │ │ ├── chartJs.ts │ │ ├── export.ts │ │ ├── openAsNotebook.ts │ │ └── statement.ts ├── testing │ ├── database.ts │ ├── databasePerformance.ts │ ├── index.ts │ ├── jobs.ts │ ├── manager.ts │ ├── selfCodes.ts │ └── testCasesTree.ts ├── types.ts ├── uriHandler.ts └── views │ ├── examples │ ├── contributes.json │ ├── exampleBrowser.ts │ └── index.ts │ ├── html.ts │ ├── jobManager │ ├── ConfigManager.ts │ ├── contributes.json │ ├── editJob │ │ ├── formatTab.ts │ │ ├── index.ts │ │ ├── otherTab.ts │ │ ├── perfTab.ts │ │ ├── sortTab.ts │ │ └── systemTab.ts │ ├── jobLog.ts │ ├── jobManagerView.ts │ ├── selfCodes │ │ ├── contributes.json │ │ ├── nodes.ts │ │ ├── selfCodesBrowser.ts │ │ ├── selfCodesResultsView.ts │ │ └── selfCodesTest.ts │ └── statusBar.ts │ ├── queryHistoryView │ ├── contributes.json │ └── index.ts │ ├── results │ ├── codegen.test.ts │ ├── codegen.ts │ ├── contributes.json │ ├── explain │ │ ├── advice.ts │ │ ├── contributes.json │ │ ├── doveNodeView.ts │ │ ├── doveResultsView.ts │ │ ├── doveTreeDecorationProvider.ts │ │ └── nodes.ts │ ├── html.ts │ ├── index.ts │ └── resultSetPanelProvider.ts │ ├── schemaBrowser │ ├── contributes.json │ ├── copyUI.ts │ ├── index.ts │ └── statements.ts │ └── types │ ├── ColumnTreeItem.ts │ ├── ParmTreeItem.ts │ ├── function.ts │ ├── index.ts │ ├── procedure.ts │ ├── table.ts │ └── view.ts ├── tsconfig.json └── webpack.config.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ⚠️⚠️ **How to collection debug information** ⚠️⚠️ 11 | 12 | After connecting to a system, use the 'Db2 for i Server Component' Output tab to see installation/debug information flow it. By default, it will only show installation information and errors if any. If you need to collect information for a specific job: 13 | 14 | 1. right click on any job (in the SQL Job Manager) and select the option to enabing tracing 15 | 2. recreate the issue with tracing enabled 16 | 3. right click on the same job again and get the tracing data 17 | 4. copy and paste that data into your new issue 18 | 19 | **Describe the bug** 20 | 21 | A clear and concise description of what the bug is. 22 | 23 | **To Reproduce** 24 | 25 | Steps to reproduce the behavior: 26 | 1. Go to '...' 27 | 2. Click on '....' 28 | 3. Scroll down to '....' 29 | 4. See error 30 | 31 | **Expected behavior** 32 | 33 | A clear and concise description of what you expected to happen. 34 | 35 | **Screenshots** 36 | 37 | If applicable, add screenshots to help explain your problem. 38 | 39 | **Environment** 40 | 41 | - OS: [e.g. Windows/Max] 42 | - Extension Version [e.g. 0.3.0] 43 | 44 | **Additional context** 45 | 46 | Add any other context about the problem here. 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Build and publish a release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | release: 9 | name: Release and publish 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/setup-node@v2 13 | with: 14 | node-version: '20' 15 | - uses: actions/checkout@v2 16 | 17 | - run: npm ci 18 | 19 | - run: npm install -g vsce ovsx 20 | 21 | - name: Publish to Marketplace 22 | run: vsce publish -p $PUBLISHER_TOKEN 23 | env: 24 | PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }} 25 | 26 | - name: Publish to Open VSX 27 | run: npx ovsx publish -p ${{ secrets.OPENVSX_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/webpack_ci.yaml: -------------------------------------------------------------------------------- 1 | name: NodeJS with Webpack 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [20.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Type check 25 | run: npm run typings 26 | 27 | - name: Build and package 28 | run: | 29 | npx webpack 30 | npm install -g vsce 31 | vsce package 32 | 33 | - name: get .vsix name 34 | run: | 35 | echo "vsix_name=$(echo *.vsix)" >> $GITHUB_ENV 36 | cat $GITHUB_ENV 37 | 38 | - name: Upload dist artifacts 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: test_vsix 42 | path: ${{env.vsix_name}} 43 | 44 | - name: Find Comment 45 | uses: peter-evans/find-comment@v1 46 | id: fc 47 | with: 48 | issue-number: ${{ github.event.pull_request.number }} 49 | comment-author: 'github-actions[bot]' 50 | body-includes: new build is available 51 | 52 | - name: Post comment 53 | continue-on-error: true 54 | if: steps.fc.outputs.comment-id == '' 55 | uses: actions/github-script@v5 56 | with: 57 | script: | 58 | github.rest.issues.createComment({ 59 | issue_number: context.issue.number, 60 | owner: context.repo.owner, 61 | repo: context.repo.repo, 62 | body: '👋 A new build is available for this PR based on ${{ github.event.pull_request.head.sha }}.\n * [Download here.](https://github.com/codefori/vscode-db2i/actions/runs/${{ github.run_id }})\n* [Read more about how to test](https://github.com/codefori/vscode-db2i/blob/master/.github/pr_testing_template.md)' 63 | }) 64 | 65 | - name: Update comment 66 | continue-on-error: true 67 | if: steps.fc.outputs.comment-id != '' 68 | uses: peter-evans/create-or-update-comment@v1 69 | with: 70 | edit-mode: replace 71 | comment-id: ${{ steps.fc.outputs.comment-id }} 72 | body: | 73 | 👋 A new build is available for this PR based on ${{ github.event.pull_request.head.sha }}. 74 | 75 | * [Download here.](https://github.com/codefori/vscode-db2i/actions/runs/${{ github.run_id }}) 76 | * [Read more about how to test](https://github.com/codefori/vscode-db2i/blob/master/.github/pr_testing_template.md) 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode-test/ 3 | *.vsix 4 | .DS_Store 5 | dist -------------------------------------------------------------------------------- /.prompts/self-analyst.prompt: -------------------------------------------------------------------------------- 1 | temperature: 0.3 2 | description: Analyze and identify patterns in SELF errors on IBM i 3 | --- 4 | 5 | You are a highly experienced SQL error logging specialist with deep expertise in the SQL Error Logging Facility (SELF) on IBM i. Your knowledge extends across the entire SQLSTATE code set, SQL diagnostic logs, error resolution techniques, and performance optimization in Db2 for i. Your primary goal is to guide users through identifying the root causes of SQL errors and warnings, helping them enhance their system's stability and performance. 6 | 7 | When responding to user queries: 8 | - Offer detailed, structured explanations of error causes and potential resolutions. 9 | - Provide examples of similar issues and proven best practices. 10 | - Use your expertise to suggest preventive measures for future error mitigation. 11 | - Highlight any trends or patterns in the errors and suggest long-term improvements to error-handling strategies and system configuration. 12 | - Include references to specific IBM i documentation where applicable. 13 | 14 | Keep in mind the need for both short-term fixes and strategies for long-term stability and performance. 15 | 16 | 17 | {{{ db2i "*SELF"}}} 18 | {{{ input }}} 19 | 20 | As a database administrator, perform a thorough analysis of the SELF errors logged on the IBM i system. Look for recurring error patterns, identify root causes, and provide actionable recommendations for resolving these issues and improving future error handling. 21 | -------------------------------------------------------------------------------- /.prompts/self-report.prompt: -------------------------------------------------------------------------------- 1 | temperature: 0.3 2 | description: Generate a report on SELF error 3 | --- 4 | 5 | You are an expert in the SQL Error Logging Facility (SELF) on IBM i, a specialized tool designed for capturing, logging, and analyzing SQL errors and warnings in Db2 for i environments. Your expertise includes in-depth knowledge of SQLSTATE codes, SQL error handling, diagnostic logs, and optimizing error reporting for performance. Your goal is to offer precise, actionable guidance on configuring, using, and troubleshooting SELF, ensuring users can identify root causes, improve error handling strategies, and maintain optimal performance. 6 | 7 | When responding to user queries: 8 | - Provide clear, step-by-step explanations and direct references to relevant documentation. 9 | - Identify potential edge cases, focusing on how to prevent, detect, and resolve errors efficiently. 10 | - Address both short-term fixes and long-term best practices for robust error logging and system reliability. 11 | 12 | 13 | {{{ input }}} 14 | 15 | Please analyze the provided SELF error using the following guidelines: 16 | 17 | 1. **Error Analysis**: 18 | - Examine the error message, SQLSTATE code, and relevant diagnostic information. 19 | - Identify the root cause and potential contributing factors. 20 | 21 | 2. **SQL Context**: 22 | - Provide the SQL statement or context that likely triggered the error. 23 | - Explain any relevant patterns or operations in the SQL code that could lead to similar issues. 24 | 25 | 3. **Resolution Guidance**: 26 | - Offer specific recommendations for resolving the error. 27 | - Suggest ways to prevent similar errors in future queries or operations. 28 | 29 | 4. **Diagnostic Insights**: 30 | - Analyze the diagnostic logs, explaining their significance and how they relate to the identified issue. 31 | 32 | 5. **Performance Considerations**: 33 | - Discuss any performance implications related to the error and error-handling process. 34 | - Provide tips for optimizing SELF configurations to minimize performance impact during error logging. -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "amodio.tsl-problem-matcher" 7 | ] 8 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}", 19 | "sourceMaps": true 20 | }, 21 | { 22 | "name": "Launch Tests", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "args": [ 26 | "--extensionDevelopmentPath=${workspaceFolder}" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/dist/**/*.js" 30 | ], 31 | "preLaunchTask": "${defaultBuildTask}", 32 | "sourceMaps": true, 33 | "env": { 34 | "db2_testing": "true" 35 | } 36 | }, 37 | { 38 | "name": "Launch Tests (Specific)", 39 | "type": "extensionHost", 40 | "request": "launch", 41 | "args": [ 42 | "--extensionDevelopmentPath=${workspaceFolder}" 43 | ], 44 | "outFiles": [ 45 | "${workspaceFolder}/dist/**/*.js" 46 | ], 47 | "preLaunchTask": "${defaultBuildTask}", 48 | "sourceMaps": true, 49 | "env": { 50 | "db2_testing": "true", 51 | "db2_specific": "true" 52 | } 53 | }, 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "webpack-dev", 9 | "problemMatcher": "$ts-webpack-watch", 10 | "isBackground": false, 11 | 12 | "presentation": { 13 | "reveal": "never" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true, 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/jsconfig.json 8 | **/*.map 9 | **/.eslintrc.json 10 | media/main.png 11 | node_modules 12 | src 13 | .github 14 | src/dsc.ts -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Halcyon Tech Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-db2i 2 | 3 | 4 | 5 | [GitHub star this repo 🌟](https://github.com/codefori/vscode-db2i) 6 | [View our documentation 📘](https://codefori.github.io/docs/extensions/db2i/) 7 | 8 | Db2 for IBM i tools provides SQL functionality to VS Code. **Currently in preview**. 9 | 10 | - Statement executor and result set view 11 | - Schemas view 12 | - Query history 13 | - SQL Job Manager, with JDBC options editor and configuration manager 14 | 15 | --- 16 | 17 | ![](./media/main.png) 18 | 19 | ### Server Component 20 | 21 | As of 0.3.0, the Db2 for i extension requires a server component. The component provides improved performance and makes it easy for us to add better features. This extension will manage the server component installation when you connect to a system with Code for IBM i and will ask the user to confirm any installation or update. [The server component is also open-source](https://github.com/ThePrez/CodeForIBMiServer). 22 | 23 | ## Building from source 24 | 25 | 1. This project requires VS Code and Node.js. 26 | 2. fork & clone repo 27 | 3. `npm i` 28 | 4. `npm run dsc` (to fetch the Server Component) 29 | 5. 'Run Extension' from vscode debug. 30 | -------------------------------------------------------------------------------- /media/context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-db2i/23d96df5611982f4b955200d118abb38afd4308e/media/context.png -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-db2i/23d96df5611982f4b955200d118abb38afd4308e/media/logo.png -------------------------------------------------------------------------------- /media/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-db2i/23d96df5611982f4b955200d118abb38afd4308e/media/main.png -------------------------------------------------------------------------------- /snippets/contributes.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributes": { 3 | "snippets": [ 4 | { 5 | "language": "sql", 6 | "path": "snippets/http.code-snippets" 7 | }, 8 | { 9 | "language": "sql", 10 | "path": "snippets/create.code-snippets" 11 | }, 12 | { 13 | "language": "sql", 14 | "path": "snippets/scalars.code-snippets" 15 | }, 16 | { 17 | "language": "sql", 18 | "path": "snippets/variables.code-snippets" 19 | }, 20 | { 21 | "language": "sql", 22 | "path": "snippets/aggregates.code-snippets" 23 | } 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /snippets/create.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "create procedure": { 3 | "prefix": "create procedure", 4 | "body": [ 5 | "create or replace procedure ${1:procedure_name}($2)", 6 | " program type sub modifies sql data", 7 | " set option usrprf = *user, dynusrprf = *user, commit = *none", 8 | "begin", 9 | " $0", 10 | "end;" 11 | ], 12 | "description": "Simple procedure" 13 | }, 14 | "create function": { 15 | "prefix": "create function", 16 | "body": [ 17 | "create or replace function ${1:function_name}($2)", 18 | " returns $3", 19 | "begin", 20 | " $0", 21 | " --return value;", 22 | "end;" 23 | ], 24 | "description": "Simple function" 25 | }, 26 | "declare": { 27 | "prefix": "declare", 28 | "body": [ 29 | "declare ${1:name} ${2|SMALLINT,INTEGER,INT,BIGINT,DECIMAL,DEC,NUMERIC,FLOAT,REAL,DOUBLE,DECFLOAT,CHAR,CHARACTER,VARCHAR,CLOB,GRAPHIC,VARGRAPHIC,NCHAR,VARYING,NVARCHAR,NCLOB,BLOB,DATE,TIME,TIMESTAMP,XML|}${3} default ${4}${0};", 30 | ], 31 | "description": "Declare variable" 32 | } 33 | } -------------------------------------------------------------------------------- /snippets/http.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "template: http_get_verbose": { 3 | "prefix": "http_get_verbose", 4 | "body": [ 5 | "select RESPONSE_MESSAGE, RESPONSE_HTTP_HEADER", 6 | "into :response_message, :response_header", 7 | "-- Options: https://www.ibm.com/docs/en/i/7.5?topic=functions-http-get-http-get-blob#rbafzscahttpget__HTTP_options", 8 | "from table(HTTP_GET_VERBOSE('${1:url}'${2:, options})) x;", 9 | "$0" 10 | ], 11 | "description": "Simple HTTP GET request with verbose output" 12 | }, 13 | "template: http_post_verbose": { 14 | "prefix": "http_post_verbose", 15 | "body": [ 16 | "select RESPONSE_MESSAGE, RESPONSE_HTTP_HEADER", 17 | "into :response_message, :response_header", 18 | "from table(HTTP_POST_VERBOSE(", 19 | " '${1:url}',", 20 | " '${2:body}',", 21 | " -- Options: https://www.ibm.com/docs/en/i/7.5?topic=functions-http-get-http-get-blob#rbafzscahttpget__HTTP_options", 22 | " json_object('headers': json_object('Authorization': 'Bearer BEARER', 'Content-Type': 'application/json', 'Accept': 'application/json')) ", 23 | ")) x;", 24 | "$0" 25 | ], 26 | "description": "Simple HTTP POST request with verbose output" 27 | }, 28 | "template: parse RESPONSE_HTTP_HEADER": { 29 | "prefix": "response_http_header", 30 | "body": [ 31 | "select HTTP_STATUS_CODE, Content_Type, Content_Length, Set_Cookie", 32 | "into :http_status_code, :content_type, :content_length, :set_cookie", 33 | "from json_table(", 34 | " liama.header,", 35 | " 'lax $' columns (", 36 | " HTTP_STATUS_CODE float(8) path '$.HTTP_STATUS_CODE',", 37 | " Content_Type varchar(1000) ccsid 1208 path '$.Content-Type',", 38 | " Content_Length float(8) path '$.Content-Length',", 39 | " Set_Cookie varchar(1000) ccsid 1208 path '$.Set-Cookie'", 40 | " )", 41 | ")", 42 | ], 43 | "description": "Parse HTTP response header" 44 | } 45 | } -------------------------------------------------------------------------------- /snippets/variables.code-snippets: -------------------------------------------------------------------------------- 1 | // https://www.ibm.com/docs/en/i/7.4?topic=statement-special-registers-in-sql-statements 2 | // https://www.ibm.com/docs/en/i/7.3?topic=reference-built-in-global-variables 3 | { 4 | "job_name": { 5 | "prefix": "job_name", 6 | "body": "job_name", 7 | "description": "This global variable contains the name of the current job." 8 | }, 9 | "process_id": { 10 | "prefix": "process_id", 11 | "body": "process_id", 12 | "description": "This global variable contains the process ID of the current job." 13 | }, 14 | "server_mode_job_name": { 15 | "prefix": "server_mode_job_name", 16 | "body": "server_mode_job_name", 17 | "description": "This global variable contains the name of the job that established the SQL server mode connection." 18 | }, 19 | "current_client_userid": { 20 | "prefix": "current_client_userid", 21 | "body": "current_client_userid", 22 | "description": "The client user ID for the client connection." 23 | }, 24 | "current_date": { 25 | "prefix": "current_date", 26 | "body": "current_date", 27 | "description": "The current date." 28 | }, 29 | "current_path": { 30 | "prefix": "current_path", 31 | "body": "current_path", 32 | "description": "The SQL path used to resolve unqualified data type names, procedure names, and function names in dynamically prepared SQL statements." 33 | }, 34 | "current_server": { 35 | "prefix": "current_server", 36 | "body": "current_server", 37 | "description": "The name of the relational database currently being used." 38 | }, 39 | "current_time": { 40 | "prefix": "current_time", 41 | "body": "current_time", 42 | "description": "The current time." 43 | }, 44 | "current_timestamp": { 45 | "prefix": "current_timestamp", 46 | "body": "current_timestamp", 47 | "description": "The current date and time in timestamp format." 48 | }, 49 | "current_timezone": { 50 | "prefix": "current_timezone", 51 | "body": "current_timezone", 52 | "description": "A duration of time that links local time to Universal Time Coordinated (UTC) using the formula: `local time - CURRENT TIMEZONE = UTC`. It is taken from the system value QUTCOFFSET." 53 | }, 54 | "current_user": { 55 | "prefix": "current_user", 56 | "body": "current_user", 57 | "description": "The primary authorization identifier of the job." 58 | }, 59 | "session_user": { 60 | "prefix": "session_user", 61 | "body": "session_user", 62 | "description": "The runtime authorization identifier (user profile) of the job." 63 | }, 64 | "user": { 65 | "prefix": "user", 66 | "body": "user", 67 | "description": "The runtime authorization identifier (user profile) of the job." 68 | }, 69 | "system_user": { 70 | "prefix": "system_user", 71 | "body": "system_user", 72 | "description": "The authorization identifier (user profile) of the user connected to the database." 73 | }, 74 | // "current path": { 75 | // "prefix": "current path", 76 | // "body": "current path", 77 | // "description": "The SQL path used to resolve unqualified data type names, procedure names, and function names in dynamically prepared SQL statements." 78 | // }, 79 | // "current function path": { 80 | // "prefix": "current function path", 81 | // "body": "current function path", 82 | // "description": "The SQL path used to resolve unqualified data type names, procedure names, and function names in dynamically prepared SQL statements." 83 | // }, 84 | // "current schema": { 85 | // "prefix": "current schema", 86 | // "body": "current schema", 87 | // "description": "The schema name used to qualify unqualified database object references where applicable in dynamically prepared SQL statements." 88 | // }, 89 | // "current server": { 90 | // "prefix": "current server", 91 | // "body": "current server", 92 | // "description": "The name of the relational database currently being used." 93 | // }, 94 | // "current temporal system_time": { 95 | // "prefix": "current temporal system_time", 96 | // "body": "current temporal system_time", 97 | // "description": "The timestamp to use when querying a system_period temporal table." 98 | // }, 99 | // "current time": { 100 | // "prefix": "current time", 101 | // "body": "current time", 102 | // "description": "The current time." 103 | // }, 104 | // "current timestamp": { 105 | // "prefix": "current timestamp", 106 | // "body": "current timestamp", 107 | // "description": "The current date and time in timestamp format." 108 | // }, 109 | // "current timezone": { 110 | // "prefix": "current timezone", 111 | // "body": "current timezone", 112 | // "description": "A duration of time that links local time to Universal Time Coordinated (UTC) using the formula: `local time - CURRENT TIMEZONE = UTC`. It is taken from the system value QUTCOFFSET." 113 | // }, 114 | // "current user": { 115 | // "prefix": "current user", 116 | // "body": "current user", 117 | // "description": "The primary authorization identifier of the job." 118 | // }, 119 | 120 | } -------------------------------------------------------------------------------- /src/IBMiDetail.ts: -------------------------------------------------------------------------------- 1 | import { commands } from "vscode"; 2 | import { getInstance } from "./base"; 3 | import { ServerComponent } from "./connection/serverComponent"; 4 | 5 | export type Db2FeatureIds = `SELF`; 6 | 7 | const featureRequirements: { [id in Db2FeatureIds]: { [osVersion: number]: number } } = { 8 | 'SELF': { 9 | 7.4: 26, 10 | 7.5: 5 11 | } 12 | }; 13 | 14 | export class IBMiDetail { 15 | private version: number = 0; 16 | private db2Level: number = 0; 17 | private features: { [id in Db2FeatureIds]: boolean } = { 18 | 'SELF': false 19 | }; 20 | 21 | setFeatureSupport(featureId: Db2FeatureIds, supported: boolean) { 22 | this.features[featureId] = supported; 23 | commands.executeCommand(`setContext`, `vscode-db2i:${featureId}Supported`, supported); 24 | } 25 | 26 | getVersion() { 27 | return this.version; 28 | } 29 | 30 | getDb2Level() { 31 | return this.db2Level; 32 | } 33 | 34 | async fetchSystemInfo() { 35 | // Disable all features 36 | const features = Object.keys(featureRequirements) as Db2FeatureIds[]; 37 | for (const featureId of features) { 38 | this.setFeatureSupport(featureId, false); 39 | } 40 | 41 | const instance = getInstance(); 42 | const content = instance.getContent(); 43 | 44 | let levelCheckFailed = false; 45 | 46 | const versionResults = await content.runSQL(`select OS_VERSION concat '.' concat OS_RELEASE as VERSION from sysibmadm.env_sys_info`); 47 | this.version = Number(versionResults[0].VERSION); 48 | 49 | try { 50 | const db2LevelResults = await content.runSQL([ 51 | `select max(ptf_group_level) as HIGHEST_DB2_PTF_GROUP_LEVEL`, 52 | `from qsys2.group_ptf_info`, 53 | `where PTF_GROUP_DESCRIPTION like 'DB2 FOR IBM I%' and`, 54 | `ptf_group_status = 'INSTALLED';` 55 | ].join(` `)); 56 | 57 | this.db2Level = Number(db2LevelResults[0].HIGHEST_DB2_PTF_GROUP_LEVEL); 58 | } catch (e) { 59 | ServerComponent.writeOutput(`Failed to get Db2 level. User does not have enough authority: ${e.message}`); 60 | levelCheckFailed = true; 61 | } 62 | 63 | for (const featureId of features) { 64 | const requiredLevelForFeature = featureRequirements[featureId][String(this.version)]; 65 | const supported = requiredLevelForFeature && this.db2Level >= requiredLevelForFeature; 66 | this.setFeatureSupport(featureId, supported); 67 | } 68 | 69 | if (levelCheckFailed) { 70 | const selfSupported = await this.validateSelfInstallation(); 71 | this.setFeatureSupport('SELF', selfSupported); 72 | } 73 | } 74 | 75 | getFeatures() { 76 | return this.features; 77 | } 78 | 79 | private async validateSelfInstallation() { 80 | const instance = getInstance(); 81 | const content = instance.getContent(); 82 | 83 | try { 84 | await content.runSQL(`values SYSIBMADM.SELFCODES`); 85 | 86 | // This means we have the SELF feature 87 | return true; 88 | } catch (e) { 89 | // If we can't run this, then we don't have the SELF feature 90 | return false; 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/Storage.ts: -------------------------------------------------------------------------------- 1 | import vscode from 'vscode'; 2 | 3 | const QUERIES_KEY = `queries`; 4 | const SERVERCOMPONENT_KEY = `serverVersion` 5 | 6 | export interface QueryHistoryItem { 7 | query: string; 8 | unix: number; 9 | starred?: boolean; 10 | } 11 | 12 | export type QueryList = QueryHistoryItem[]; 13 | 14 | abstract class Storage { 15 | protected readonly globalState; 16 | 17 | constructor(context: vscode.ExtensionContext) { 18 | this.globalState = context.globalState; 19 | } 20 | 21 | protected get(key: string): T | undefined { 22 | return this.globalState.get(this.getStorageKey(key)) as T | undefined; 23 | } 24 | 25 | protected async set(key: string, value: any) { 26 | await this.globalState.update(this.getStorageKey(key), value); 27 | } 28 | 29 | protected abstract getStorageKey(key: string): string; 30 | } 31 | 32 | export class ConnectionStorage extends Storage { 33 | private connectionName: string = ""; 34 | constructor(context: vscode.ExtensionContext) { 35 | super(context); 36 | } 37 | 38 | get ready(): boolean { 39 | if (this.connectionName) { 40 | return true; 41 | } 42 | else { 43 | return false; 44 | } 45 | } 46 | 47 | setConnectionName(connectionName: string) { 48 | this.connectionName = connectionName; 49 | } 50 | 51 | protected getStorageKey(key: string): string { 52 | return `${this.connectionName}.${key}`; 53 | } 54 | 55 | getServerComponentName(): string|undefined { 56 | return this.get(SERVERCOMPONENT_KEY); 57 | } 58 | 59 | setServerComponentName(name: string) { 60 | return this.set(SERVERCOMPONENT_KEY, name); 61 | } 62 | 63 | getPastQueries() { 64 | return this.get(QUERIES_KEY) || []; 65 | } 66 | 67 | async setPastQueries(sourceList: QueryList) { 68 | await this.set(QUERIES_KEY, sourceList); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/aiProviders/README.md: -------------------------------------------------------------------------------- 1 | # Db2 for i Context Provider Developer Guide 2 | 3 | This document provides a guide for prompt engineering for Db2 for i. More specifically, how we construct prompts for both Continue and Copilot AI extensions. 4 | 5 | ## Key Concepts 6 | 7 | A key component of the Db2 for i Code Assistant is the ability to provide contextually relevant information about a users Db2i connection and schema. This is done through something called a context provider. 8 | 9 | A context Provider does the following: 10 | - Gathers relevant database information such as table information, column information, based on the user's prompt 11 | - Creates additional "context items" or messages with the information gathered 12 | - Builds a prompt with the context items that is then sent to the AI extension (Continue or Copilot) 13 | 14 | Example for the following user input: 15 | ``` 16 | @Db2i how can I find the number of employees per department? 17 | ``` 18 | 19 | The context provider would gather information about the user's connection and schema, and then build a prompt with the following context items: 20 | - current schema: `SAMPLE` 21 | - table reference: `EMPLOYEE` and `DEPARTMENT` 22 | - column reference: `EMPLOYEE.EMPNO`, `EMPLOYEE.LASTNAME`, `DEPARTMENT.DEPTNO`, `DEPARTMENT.DEPTNAME`, etc 23 | 24 | ## Continue and Copilot Parity 25 | 26 | Both Continue and Copilot AI extensions have APIs for building prompts with context items. The APIs are similar but have some differences. It is important that the actual context items are identical in both extensions. 27 | - In Copilot, we register a `ChatParticipant`: `@Db2i` that is responsible for building the prompt with context items 28 | - In Continue, we register a `ContextProvider`: `@Db2i` that is responsible for building the prompt with context items 29 | 30 | **Note**: We refer to the `ChatParticipant` and `ContextProvider` as `@Db2i` in this document and we use "Context Provider" to refer to both. 31 | 32 | ## Prompt formatting 33 | 34 | The prompt format is very simple: 35 | 36 | ```plaintext 37 | ## Database Schema Definition 38 | 39 | SCHEMA: [schema_name] 40 | TABLES: [table_name_1], [table_name_2], ... 41 | 42 | ## Referenced Tables 43 | TABLE: [table_name_1] 44 | - Columns: 45 | * [column_name]: [data type] [constraints/notes] 46 | * [column_name]: [data type] [constraints/notes] 47 | - Primary Key: [column(s)] 48 | - Foreign Keys: [if any, specify references] 49 | 50 | TABLE: [table_name_2] 51 | - Columns: 52 | * [column_name]: [data type] [constraints/notes] 53 | * [column_name]: [data type] [constraints/notes] 54 | - Primary Key: [column(s)] 55 | - Foreign Keys: [if any, specify references] 56 | 57 | -- (Repeat for all relevant tables) 58 | 59 | ## Database Guidelines 60 | 61 | - [Guideline 1] 62 | - [Guideline 2] 63 | - [Guideline 3] 64 | - ... 65 | 66 | ## Example 67 | 68 | input: 69 | "Generate a SQL query to join the 'orders' and 'customers' tables to list all customers with their respective orders, including only orders with a total amount greater than 100." 70 | 71 | ### Expected Output Format 72 | 73 | - Provide the complete SQL query. 74 | - Include any assumptions as inline comments if needed. 75 | - Format the query clearly and consistently. 76 | 77 | [Insert the user's specific request here] 78 | 79 | ``` 80 | 81 | There are 4 main sections in the prompt: 82 | - **Database Schema Definition**: This section provides the schema information for the user's connection. It includes the current schema and a list of tables in the schema. 83 | - **Referenced Tables**: This section provides detailed information about the tables referenced in the user's query. It includes the columns, primary key, and foreign keys of each table. 84 | - **Database Guidelines**: This section provides general guidelines for writing SQL queries based on the schema context. It includes style conventions, performance optimization tips, validation guidelines, and additional guidelines. 85 | - **Example**: This section provides an example of the user's specific request and the expected output format. 86 | 87 | The users input is inserted at the end of the prompt. This format allows a top down view of the database by providing the schema information first, followed by the reference tables, guidelines, and finally the user's specific request. 88 | 89 | ## Context Provider Implementation 90 | 91 | In both Copilot and Continue, the same prompt format is followed. Here is an example of what that looks like in Continue: 92 | 93 | ![alt text](../../media/context.png) 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/aiProviders/continue/listTablesContextProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContextItem, 3 | ContextProviderDescription, 4 | ContextProviderExtras, 5 | ContextSubmenuItem, 6 | IContextProvider, 7 | LoadSubmenuItemsArgs, 8 | } from "@continuedev/core"; 9 | import * as vscode from "vscode"; 10 | import Schemas from "../../database/schemas"; 11 | import Table from "../../database/table"; 12 | import { 13 | buildSchemaDefinition} from "../context"; 14 | import Configuration from "../../configuration"; 15 | import { getContextItems } from "../prompt"; 16 | import { TableColumn, BasicSQLObject } from "../../types"; 17 | 18 | const listDb2Table: ContextProviderDescription = { 19 | title: "list Db2i Tables", 20 | displayTitle: `Db2i-{tables}`, 21 | description: "Add Db2i Table info to Context", 22 | type: "submenu" 23 | }; 24 | 25 | interface SchemaContextProvider { 26 | schema: string; 27 | provider: IContextProvider, 28 | } 29 | 30 | let providers: SchemaContextProvider[] = [] 31 | 32 | class ListDb2iTables implements IContextProvider { 33 | constructor(private schema: string) { 34 | this.schema = schema; 35 | } 36 | 37 | get description(): ContextProviderDescription { 38 | return { 39 | title: `Db2i-${this.schema}`, 40 | displayTitle: `Db2i-${this.schema}`, 41 | description: "Add Db2i Table info to Context", 42 | type: "submenu" 43 | }; 44 | } 45 | 46 | setCurrentSchema(schema: string) { 47 | this.schema = schema; 48 | } 49 | 50 | getCurrentSchema() { 51 | return this.schema; 52 | } 53 | 54 | async getColumnInfoForAllTables(schema: string) { 55 | const items: TableColumn[] = await Table.getItems(schema); 56 | 57 | return items.map((column) => ({ 58 | table_name: column.TABLE_NAME, 59 | schema: column.TABLE_SCHEMA, 60 | column_name: column.COLUMN_NAME, 61 | column_data_type: column.DATA_TYPE, 62 | })); 63 | } 64 | 65 | async getContextItems( 66 | query: string, 67 | extras: ContextProviderExtras 68 | ): Promise { 69 | let contextItems: ContextItem[] = []; 70 | if (query.toUpperCase() === this.schema.toUpperCase()) { 71 | 72 | const useSchemaDef: boolean = Configuration.get(`ai.useSchemaDefinition`); 73 | if (useSchemaDef) { 74 | const schemaSemantic = await buildSchemaDefinition(this.schema); 75 | if (schemaSemantic) { 76 | contextItems.push({ 77 | name: `SCHEMA Definition`, 78 | description: `${this.schema} definition`, 79 | content: JSON.stringify(schemaSemantic), 80 | }); 81 | } 82 | } 83 | 84 | } else { 85 | const tablesRefs = await getContextItems(query); 86 | for (const tableData of tablesRefs.context) { 87 | contextItems.push(tableData); 88 | } 89 | } 90 | return contextItems; 91 | } 92 | 93 | async loadSubmenuItems( 94 | args: LoadSubmenuItemsArgs 95 | ): Promise { 96 | const tables: BasicSQLObject[] = await Schemas.getObjects(this.schema, [ 97 | `tables`, 98 | ]); 99 | 100 | const schemaSubmenuItem: ContextSubmenuItem = { 101 | id: this.schema, 102 | title: this.schema, 103 | description: `All table info in schema: ${this.schema}`, 104 | }; 105 | 106 | const tableSubmenuItems: ContextSubmenuItem[] = tables.map((table) => ({ 107 | id: table.name, 108 | title: table.name, 109 | description: `${table.schema}-${table.name}`, 110 | })); 111 | 112 | return [schemaSubmenuItem, ...tableSubmenuItems]; 113 | } 114 | } 115 | 116 | export async function registerDb2iTablesProvider(schema?: string) { 117 | if (!schema) { 118 | return; 119 | } 120 | const continueID = `Continue.continue`; 121 | const continueEx = vscode.extensions.getExtension(continueID); 122 | if (continueEx) { 123 | if (!continueEx.isActive) { 124 | await continueEx.activate(); 125 | } 126 | 127 | const existingProvider: SchemaContextProvider = providers.find(p => p.schema === schema); 128 | if (existingProvider !== undefined) { 129 | return; 130 | } else { 131 | const continueAPI = continueEx?.exports; 132 | let provider = new ListDb2iTables(schema); 133 | continueAPI?.registerCustomContextProvider(provider); 134 | providers.push({provider: provider, schema: schema}); 135 | } 136 | } 137 | } 138 | 139 | -------------------------------------------------------------------------------- /src/aiProviders/copilot/contributes.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributes": { 3 | "chatParticipants": [ 4 | { 5 | "id": "vscode-db2i.chat", 6 | "name": "db2i", 7 | "fullName": "Db2 for i", 8 | "description": "Chat with the Db2 for i AI assistant", 9 | "isSticky": true, 10 | "commands": [ 11 | { 12 | "name": "build", 13 | "description": "Build an SQL statement" 14 | } 15 | ] 16 | } 17 | ], 18 | "languageModelTools": [ 19 | { 20 | "name": "vscode-db2i-chat-sqlRunnerTool", 21 | "tags": [ 22 | "sql" 23 | ], 24 | "canBeReferencedInPrompt": true, 25 | "toolReferenceName": "result", 26 | "displayName": "Run SQL statement", 27 | "icon": "$(play)", 28 | "modelDescription": "Run an SQL statement and return the result", 29 | "inputSchema": { 30 | "type": "object", 31 | "properties": { 32 | "statement": { 33 | "type": "string", 34 | "description": "The statement to execute" 35 | } 36 | }, 37 | "required": [ 38 | "statement" 39 | ] 40 | } 41 | } 42 | ] 43 | } 44 | } -------------------------------------------------------------------------------- /src/aiProviders/copilot/sqlTool.ts: -------------------------------------------------------------------------------- 1 | import { CancellationToken, ExtensionContext, LanguageModelTextPart, LanguageModelTool, LanguageModelToolInvocationOptions, LanguageModelToolInvocationPrepareOptions, LanguageModelToolResult, lm, MarkdownString } from "vscode"; 2 | import { JobManager } from "../../config"; 3 | 4 | interface IRunInTerminalParameters { 5 | statement: string; 6 | } 7 | 8 | export const RUN_SQL_TOOL_ID = 'vscode-db2i-chat-sqlRunnerTool'; 9 | 10 | export function registerSqlRunTool(context: ExtensionContext) { 11 | context.subscriptions.push(lm.registerTool(RUN_SQL_TOOL_ID, new RunSqlTool())); 12 | } 13 | 14 | class RunSqlTool 15 | implements LanguageModelTool { 16 | async invoke( 17 | options: LanguageModelToolInvocationOptions, 18 | _token: CancellationToken 19 | ) { 20 | const params = options.input as IRunInTerminalParameters; 21 | 22 | let trimmed = params.statement.trim(); 23 | 24 | if (trimmed.endsWith(`;`)) { 25 | trimmed = trimmed.slice(0, -1); 26 | } 27 | 28 | const result = await JobManager.runSQL(trimmed); 29 | 30 | return new LanguageModelToolResult([new LanguageModelTextPart(JSON.stringify(result))]); 31 | } 32 | 33 | async prepareInvocation( 34 | options: LanguageModelToolInvocationPrepareOptions, 35 | _token: CancellationToken 36 | ) { 37 | const confirmationMessages = { 38 | title: 'Run SQL statement', 39 | message: new MarkdownString( 40 | `Run this statement in your job?` + 41 | `\n\n\`\`\`sql\n${options.input.statement}\n\`\`\`\n` 42 | ), 43 | }; 44 | 45 | return { 46 | invocationMessage: `Running statement`, 47 | confirmationMessages, 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/aiProviders/prompt.ts: -------------------------------------------------------------------------------- 1 | import { JobManager } from "../config"; 2 | import Configuration from "../configuration"; 3 | import { JobInfo } from "../connection/manager"; 4 | import Schemas from "../database/schemas"; 5 | import Statement from "../database/statement"; 6 | import { buildSchemaDefinition, canTalkToDb, getContentItemsForRefs, getSqlContextItems } from "./context"; 7 | import { DB2_SYSTEM_PROMPT } from "./prompts"; 8 | 9 | export interface PromptOptions { 10 | progress?: (text: string) => void; 11 | withDb2Prompt?: boolean; 12 | } 13 | 14 | export interface Db2ContextItems { 15 | name: string; 16 | description: string; 17 | content: string; 18 | } 19 | 20 | export interface BuildResult { 21 | context: Db2ContextItems[]; 22 | followUps: string[]; 23 | } 24 | 25 | export async function getContextItems(input: string, options: PromptOptions = {}): Promise { 26 | const currentJob: JobInfo = JobManager.getSelection(); 27 | 28 | let contextItems: Db2ContextItems[] = []; 29 | let followUps = []; 30 | 31 | const progress = (message: string) => { 32 | if (options.progress) { 33 | options.progress(message); 34 | } 35 | }; 36 | 37 | if (currentJob) { 38 | const currentSchema = currentJob?.job.options.libraries[0] || "QGPL"; 39 | const useSchemaDef: boolean = Configuration.get(`ai.useSchemaDefinition`); 40 | 41 | // TODO: self? 42 | 43 | progress(`Finding objects to work with...`); 44 | 45 | // First, let's take the user input and see if contains any references to SQL objects. 46 | // This returns a list of references to SQL objects, such as tables, views, schemas, etc, 47 | // and the context items that are related to those references. 48 | const userInput = await getSqlContextItems(input); 49 | 50 | contextItems.push(...userInput.items); 51 | 52 | // If the user referenced 2 or more tables, let's add a follow up 53 | if (userInput.refs.filter(r => r.sqlType === `TABLE`).length >= 2) { 54 | const randomIndexA = Math.floor(Math.random() * userInput.refs.length); 55 | const randomIndexB = Math.floor(Math.random() * userInput.refs.length); 56 | const tableA = userInput.refs[randomIndexA].name; 57 | const tableB = userInput.refs[randomIndexB].name; 58 | 59 | if (tableA !== tableB) { 60 | followUps.push(`How can I join ${tableA} and ${tableB}?`); 61 | } 62 | } 63 | 64 | // If the user only requests one reference, then let's do something 65 | if (userInput.refs.length === 1) { 66 | const ref = userInput.refs[0]; 67 | const prettyNameRef = Statement.prettyName(ref.name); 68 | 69 | if (ref.sqlType === `SCHEMA`) { 70 | // If the only reference is a schema, let's just add follow ups 71 | followUps.push( 72 | `What are some objects in that schema?`, 73 | `What is the difference between a schema and a library?`, 74 | ); 75 | 76 | } else { 77 | // If the user referenced a table, view, or other object, let's fetch related objects 78 | progress(`Finding objects related to ${prettyNameRef}...`); 79 | 80 | try { 81 | const relatedObjects = await Schemas.getRelatedObjects(ref); 82 | const contentItems = await getContentItemsForRefs(relatedObjects); 83 | 84 | contextItems.push(...contentItems); 85 | 86 | // Then also add some follow ups 87 | if (relatedObjects.length === 1) { 88 | followUps.push(`How is ${prettyNameRef} related to ${Statement.prettyName(relatedObjects[0].name)}?`); 89 | } else if (ref.sqlType === `TABLE`) { 90 | followUps.push(`What are some objects related to that table?`); 91 | } 92 | } catch (e) { 93 | followUps.push(`What objects are in ${Statement.prettyName(ref.schema)}?`); 94 | console.log(e); 95 | } 96 | 97 | } 98 | 99 | } else if (userInput.refs.length > 1) { 100 | // If there are multiple references, let's just add a follow up 101 | const randomRef = userInput.refs[Math.floor(Math.random() * userInput.refs.length)]; 102 | const prettyNameRef = Statement.prettyName(randomRef.name); 103 | 104 | followUps.push(`What are some objects related to ${prettyNameRef}?`); 105 | 106 | } else if (useSchemaDef) { 107 | // If the user didn't reference any objects, but we are using schema definitions, let's just add the schema definition 108 | progress(`Getting info for schema ${currentSchema}...`); 109 | const schemaSemantic = await buildSchemaDefinition(currentSchema); 110 | if (schemaSemantic) { 111 | contextItems.push({ 112 | name: `SCHEMA Definition`, 113 | description: `${currentSchema} definition`, 114 | content: JSON.stringify(schemaSemantic) 115 | }); 116 | } 117 | } 118 | 119 | if (options.withDb2Prompt) { 120 | contextItems.push({ 121 | name: `system prompt`, 122 | content: DB2_SYSTEM_PROMPT, 123 | description: `system prompt`, 124 | }); 125 | } 126 | } 127 | 128 | return { 129 | context: contextItems, 130 | followUps 131 | }; 132 | } -------------------------------------------------------------------------------- /src/aiProviders/prompts.ts: -------------------------------------------------------------------------------- 1 | export const DB2_SYSTEM_PROMPT = `# Db2 for i Guidelines 2 | 3 | You are an expert in IBM i, specializing in Db2 for i. Assist developers in writing and debugging SQL queries using only the provided table and column metadata. 4 | 5 | ## 1. Metadata Usage 6 | - **Use Only Provided Metadata:** 7 | Generate queries using only the provided metadata. Do not assume or generate any table names, column names, or SQL references not given. 8 | - **Missing Metadata:** 9 | If metadata is missing, inform the user and request the necessary details. 10 | 11 | ## 2. SQL Style & Conventions 12 | - **Formatting:** 13 | Use uppercase for SQL keywords with clear, consistent indentation. 14 | - **Table Aliases:** 15 | Use aliases when joining multiple tables. 16 | - **Join Types:** 17 | Use the correct join type (e.g., INNER JOIN, LEFT JOIN) based on the context. 18 | 19 | ## 3. Performance & Optimization 20 | - **Join Conditions:** 21 | Use explicit join conditions. 22 | - **Selective Columns:** 23 | Select only necessary columns unless a wildcard is explicitly requested. 24 | - **Schema Constraints:** 25 | Respect provided indexes, constraints, and other schema details. 26 | 27 | ## 4. Validation 28 | - **SQL Validity:** 29 | Ensure that all SQL is valid for Db2 for i. 30 | - **Assumptions:** 31 | If metadata is ambiguous, include comments in the SQL explaining your assumptions. 32 | 33 | ## 5. Additional Guidelines 34 | - **Naming Conventions:** 35 | Follow the naming conventions provided in the metadata. 36 | - **Query Simplicity:** 37 | Avoid unnecessary subqueries unless they improve clarity or performance. 38 | - **Clarity & Maintainability:** 39 | Prioritize clarity and maintainability in your queries. 40 | 41 | ## 6. Ambiguity Handling & Follow-Up 42 | - **Clarify Ambiguity:** 43 | Ask clarifying questions if the user's request is ambiguous or lacks details. 44 | 45 | ## Example 46 | 47 | input: 48 | "Generate a SQL query to join the 'orders' and 'customers' tables to list all customers with their respective orders, including only orders with a total amount greater than 100." 49 | 50 | ### Expected Output Format 51 | 52 | - Provide the complete SQL query. 53 | - Include any assumptions as inline comments if needed. 54 | - Format the query clearly and consistently.`; 55 | 56 | export const DB2_SELF_PROMPT = [`Db2 for i self code errors\n`, 57 | `Summarize the SELF code errors provided. The SQL Error Logging Facility (SELF) provides a mechanism that can be used to understand when SQL statements are encountering specific SQL errors or warnings. SELF is built into Db2 for i and can be enabled in specific jobs or system wide. Provide additional details about the errors and how to fix them.\n`, 58 | `Errors:\n`]; -------------------------------------------------------------------------------- /src/base.ts: -------------------------------------------------------------------------------- 1 | import { CodeForIBMi } from "@halcyontech/vscode-ibmi-types"; 2 | import Instance from "@halcyontech/vscode-ibmi-types/Instance"; 3 | import { Extension, ExtensionContext, extensions } from "vscode"; 4 | import { SQLStatementChecker } from "./connection/syntaxChecker"; 5 | 6 | let baseExtension: Extension|undefined; 7 | 8 | export function loadBase(context: ExtensionContext): CodeForIBMi|undefined { 9 | if (!baseExtension) { 10 | baseExtension = (extensions ? extensions.getExtension(`halcyontechltd.code-for-ibmi`) : undefined); 11 | 12 | if (baseExtension) { 13 | baseExtension.activate().then(() => { 14 | baseExtension.exports.componentRegistry.registerComponent(context, new SQLStatementChecker()); 15 | }); 16 | } 17 | } 18 | 19 | return (baseExtension && baseExtension.isActive && baseExtension.exports ? baseExtension.exports : undefined); 20 | } 21 | 22 | export function getBase(): CodeForIBMi|undefined { 23 | return (baseExtension && baseExtension.isActive && baseExtension.exports ? baseExtension.exports : undefined); 24 | } 25 | 26 | export function getInstance(): Instance|undefined { 27 | return (baseExtension && baseExtension.isActive && baseExtension.exports ? baseExtension.exports.instance : undefined); 28 | } -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, commands, window } from "vscode"; 2 | import { ConnectionStorage } from "./Storage"; 3 | import { getInstance } from "./base"; 4 | import { SQLJobManager } from "./connection/manager"; 5 | import { ServerComponent } from "./connection/serverComponent"; 6 | import { JobManagerView } from "./views/jobManager/jobManagerView"; 7 | import Configuration from "./configuration"; 8 | import { ConfigManager } from "./views/jobManager/ConfigManager"; 9 | import { Examples, ServiceInfoLabel } from "./views/examples"; 10 | import { updateStatusBar } from "./views/jobManager/statusBar"; 11 | import { IBMiDetail } from "./IBMiDetail"; 12 | 13 | export let Config: ConnectionStorage; 14 | export let osDetail: IBMiDetail; 15 | export let JobManager: SQLJobManager = new SQLJobManager(); 16 | 17 | export async function onConnectOrServerInstall(): Promise { 18 | const instance = getInstance(); 19 | 20 | Config.setConnectionName(instance.getConnection().currentConnectionName); 21 | 22 | osDetail = new IBMiDetail(); 23 | 24 | await osDetail.fetchSystemInfo(); 25 | 26 | await ServerComponent.initialise().then(installed => { 27 | if (installed) { 28 | JobManagerView.setVisible(true); 29 | } 30 | }); 31 | 32 | await ServerComponent.checkForUpdate(); 33 | 34 | updateStatusBar(); 35 | 36 | if (ServerComponent.isInstalled()) { 37 | JobManagerView.setVisible(true); 38 | 39 | let newJob = Configuration.get(`alwaysStartSQLJob`) || `ask`; 40 | if (typeof newJob !== `string`) newJob = `ask`; //For legacy settings where it used to be a boolean 41 | 42 | switch (newJob) { 43 | case `ask`: 44 | return await askAboutNewJob(true); 45 | 46 | case `new`: 47 | await commands.executeCommand(`vscode-db2i.jobManager.newJob`); 48 | return true; 49 | 50 | case `never`: 51 | break; 52 | 53 | default: 54 | if (ConfigManager.getConfig(newJob)) { 55 | await commands.executeCommand(`vscode-db2i.jobManager.startJobFromConfig`, newJob); 56 | } else { 57 | await commands.executeCommand(`vscode-db2i.jobManager.newJob`); 58 | return true; 59 | } 60 | break; 61 | } 62 | } 63 | return false; 64 | } 65 | 66 | export function initConfig(context: ExtensionContext) { 67 | Config = new ConnectionStorage(context); 68 | 69 | getInstance().subscribe(context, `disconnected`, `db2i-disconnect`, async () => { 70 | JobManagerView.setVisible(false); 71 | JobManager.endAll(); 72 | updateStatusBar(); 73 | 74 | // Remove old service examples 75 | delete Examples[ServiceInfoLabel]; 76 | 77 | // Close out the Visual Explain panels 78 | commands.executeCommand('vscode-db2i.dove.close'); 79 | }); 80 | } 81 | 82 | export async function askAboutNewJob(startup?: boolean): Promise { 83 | const instance = getInstance(); 84 | const connection = instance.getConnection(); 85 | 86 | if (connection) { 87 | 88 | // Wait for the job manager to finish creating jobs if one is not selected 89 | while (JobManager.getSelection() === undefined && JobManager.isCreatingJob()) { 90 | await new Promise(resolve => setTimeout(resolve, 100)); 91 | } 92 | 93 | // If a job is created or already selected, don't ask 94 | if (JobManager.getSelection()) { 95 | return true; 96 | } 97 | 98 | const options = startup ? [`Yes`, `Always`, `No`, `Never`] : [`Yes`, `No`]; 99 | 100 | const chosen = await window.showInformationMessage(`Would you like to start an SQL Job?`, ...options); 101 | switch (chosen) { 102 | case `Yes`: 103 | case `Always`: 104 | if (chosen === `Always`) { 105 | await Configuration.set(`alwaysStartSQLJob`, `new`); 106 | } 107 | 108 | await commands.executeCommand(`vscode-db2i.jobManager.newJob`); 109 | return true; 110 | 111 | case `Never`: 112 | await Configuration.set(`alwaysStartSQLJob`, `never`); 113 | break; 114 | } 115 | } 116 | 117 | return false; 118 | } -------------------------------------------------------------------------------- /src/configuration.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as vscode from "vscode"; 3 | 4 | const getConfiguration = (): vscode.WorkspaceConfiguration => { 5 | return vscode.workspace.getConfiguration(`vscode-db2i`); 6 | } 7 | 8 | export default class Configuration { 9 | /** 10 | * Returns variable not specific to a host (e.g. a global config) 11 | */ 12 | static get(prop: string) { 13 | return getConfiguration().get(prop); 14 | } 15 | 16 | static set(prop: string, newValue: any) { 17 | return getConfiguration().update(prop, newValue, true); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/connection/SCVersion.ts: -------------------------------------------------------------------------------- 1 | 2 | export const VERSION = `2.3.3`; 3 | export const SERVER_VERSION_TAG = `v${VERSION}`; 4 | export const SERVER_VERSION_FILE = `mapepire-server-${VERSION}.jar`; 5 | -------------------------------------------------------------------------------- /src/connection/syntaxChecker/checker.ts: -------------------------------------------------------------------------------- 1 | export const VALIDATOR_NAME = `VALIDATE_STATEMENT`; 2 | export const WRAPPER_NAME = `CHECKSTMTWRAPPED`; 3 | export const VALID_STATEMENT_LENGTH = 32740; 4 | export const MAX_STATEMENT_COUNT = 200; 5 | 6 | export function getValidatorSource(schema: string, version: number) { 7 | return /*sql*/` 8 | create or replace procedure ${schema}.${WRAPPER_NAME} ( 9 | IN statementText char(${VALID_STATEMENT_LENGTH}) FOR SBCS DATA, 10 | IN statementLength int, 11 | IN recordsProvided int, 12 | IN statementLanguage char(10), 13 | IN options char(24), 14 | OUT statementInfo char(1000), 15 | IN statementInfoLength int, 16 | OUT recordsProcessed int, 17 | OUT errorCode char(1000) 18 | ) 19 | LANGUAGE RPGLE 20 | NOT DETERMINISTIC 21 | MODIFIES SQL DATA 22 | EXTERNAL NAME QSYS/QSQCHKS 23 | PARAMETER STYLE GENERAL; 24 | 25 | comment on procedure ${schema}/${WRAPPER_NAME} is '${version} - QSQCHKS Wrapper'; 26 | 27 | create or replace function ${schema}.${VALIDATOR_NAME}(statementText char(${VALID_STATEMENT_LENGTH}) FOR SBCS DATA) --todo: support 1208 parms 28 | returns table ( 29 | messageFileName char(10), 30 | messageFileLibrary char(10), 31 | numberOfStatementsBack int, 32 | curStmtLength int, 33 | errorFirstRecordNumber int, 34 | errorFirstColumnNumber int, 35 | errorLastRecordNumber int, 36 | errorLastColumnNumber int, 37 | errorSyntaxRecordNumber int, 38 | errorSyntaxColumnNumber int, 39 | errorSQLMessageID char(7), 40 | errorSQLSTATE char(5), 41 | errorReplacementText char(1000), 42 | messageText char(132) 43 | ) 44 | modifies sql data 45 | begin 46 | -- Variables required for parameters 47 | declare stmtLength int default 0; 48 | declare recordsProvided int default 1; 49 | declare statementLanguage char(10) default '*NONE'; 50 | declare options char(24) default '000000000000000000000000'; 51 | declare statementInfo char(1000) for bit data default ''; 52 | declare statementInfoLength int default 1000; 53 | declare recordsProcessed int default 0; 54 | declare errorCode char(1000) default ''; 55 | -- 56 | 57 | -- Variables required for parsing the error list 58 | declare messageFileName char(10); 59 | declare messageFileLibrary char(10); 60 | declare numberOfStatementsBack int default 0; 61 | declare currentStatementIndex int default 0; 62 | 63 | -- Variables for each error 64 | declare errorOffset int default 25; 65 | declare curStmtLength int default 0; 66 | declare errorFirstRecordNumber int default 0; 67 | declare errorFirstColumnNumber int default 0; 68 | declare errorLastRecordNumber int default 0; 69 | declare errorLastColumnNumber int default 0; 70 | declare errorSyntaxRecordNumber int default 0; 71 | declare errorSyntaxColumnNumber int default 0; 72 | declare errorSQLMessageID char(7) default ''; 73 | declare errorSQLSTATE char(5) default ''; 74 | declare errorRepLen int default 0; 75 | declare errorReplacementText char(1000) default ''; 76 | declare messageText char(132) default ''; 77 | 78 | set stmtLength = length(rtrim(statementText)); 79 | set options = x'00000001' concat x'00000001' concat x'0000000A' concat '*NONE '; --No naming convention 80 | -- set options = x'00000001' concat x'00000008' concat x'00000004' concat x'000004B0'; -- ccsid 81 | 82 | call ${schema}.${WRAPPER_NAME}( statementText, stmtLength, recordsProvided, statementLanguage, options, statementInfo, statementInfoLength, recordsProcessed, errorCode); 83 | 84 | -- set ${schema}.outlog = statementInfo; 85 | -- set ${schema}.outlog = substr(statementInfo, 21, 4); 86 | 87 | -- Parse the output 88 | set messageFileName = rtrim(substr(statementInfo, 1, 10)); 89 | set messageFileLibrary = rtrim(substr(statementInfo, 11, 10)); 90 | set numberOfStatementsBack = interpret(substr(statementInfo, 21, 4) as int); 91 | set errorOffset = 25; 92 | 93 | while currentStatementIndex < numberOfStatementsBack do 94 | set curStmtLength = interpret(substr(statementInfo, errorOffset, 4) as int); 95 | set errorFirstRecordNumber = interpret(substr(statementInfo, errorOffset + 4, 4) as int); 96 | set errorFirstColumnNumber = interpret(substr(statementInfo, errorOffset + 8, 4) as int); 97 | set errorLastRecordNumber = interpret(substr(statementInfo, errorOffset + 12, 4) as int); 98 | set errorLastColumnNumber = interpret(substr(statementInfo, errorOffset + 16, 4) as int); 99 | set errorSyntaxRecordNumber = interpret(substr(statementInfo, errorOffset + 20, 4) as int); 100 | set errorSyntaxColumnNumber = interpret(substr(statementInfo, errorOffset + 24, 4) as int); 101 | set errorSQLMessageID = rtrim(substr(statementInfo, errorOffset + 28, 7)); 102 | set errorSQLSTATE = rtrim(substr(statementInfo, errorOffset + 35, 5)); 103 | set errorRepLen = interpret(substr(statementInfo, errorOffset + 40, 4) as int); 104 | set errorReplacementText = rtrim(substr(statementInfo, errorOffset + 44, errorRepLen)); 105 | 106 | set errorOffset = errorOffset + 44 + errorRepLen; 107 | 108 | set currentStatementIndex = currentStatementIndex + 1; 109 | 110 | select message_text 111 | into messageText 112 | from table(qsys2.message_file_data(messageFileLibrary, messageFileName)) 113 | where message_id = errorSQLMessageID; 114 | 115 | pipe ( 116 | messageFileName, 117 | messageFileLibrary, 118 | numberOfStatementsBack, 119 | curStmtLength, 120 | errorFirstRecordNumber, 121 | errorFirstColumnNumber, 122 | errorLastRecordNumber, 123 | errorLastColumnNumber, 124 | errorSyntaxRecordNumber, 125 | errorSyntaxColumnNumber, 126 | errorSQLMessageID, 127 | errorSQLSTATE, 128 | errorReplacementText, 129 | messageText 130 | ); 131 | end while; 132 | 133 | return; 134 | end; 135 | 136 | comment on function ${schema}/${VALIDATOR_NAME} is '${version} - SQL Syntax Checker'; 137 | 138 | --select * 139 | --from table(${schema}.validate_statement('select from sample.employee order by a')) x; 140 | -- 141 | --select * from table(qsys2.message_file_data('QSYS', 'QSQLMSG')); 142 | -- 143 | --values hex(substr(${schema}.outlog, 21, 4)); 144 | --values interpret(hex(substr(${schema}.outlog, 21, 4)) as int); 145 | --values hex(1) concat hex(1) concat hex(10) concat '*NONE '; 146 | --values length(x'000004B8'); 147 | --values hex(1200); 148 | `; 149 | } -------------------------------------------------------------------------------- /src/connection/types.ts: -------------------------------------------------------------------------------- 1 | 2 | // Redefined from mapepire-js 3 | export enum JobStatus { 4 | NOT_STARTED = "notStarted", 5 | CONNECTING = "connecting", 6 | READY = "ready", 7 | BUSY = "busy", 8 | ENDED = "ended" 9 | } 10 | 11 | export enum TransactionEndType { 12 | COMMIT = "COMMIT", 13 | ROLLBACK = "ROLLBACK" 14 | } 15 | 16 | export enum ExplainType { 17 | RUN = "run", 18 | DO_NOT_RUN = "doNotRun" 19 | } 20 | // End 21 | 22 | export interface JobLogEntry { 23 | MESSAGE_ID: string; 24 | SEVERITY: string; 25 | MESSAGE_TIMESTAMP: string; 26 | FROM_LIBRARY: string; 27 | FROM_PROGRAM: string; 28 | MESSAGE_TYPE: string; 29 | MESSAGE_TEXT: string; 30 | MESSAGE_SECOND_LEVEL_TEXT: string 31 | } 32 | 33 | export type Rows = {[column: string]: string|number|boolean}[]; -------------------------------------------------------------------------------- /src/contributes.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributes": { 3 | "configuration": [ 4 | { 5 | "id": "vscode-db2i", 6 | "title": "General", 7 | "order": 0, 8 | "properties": { 9 | "vscode-db2i.pageSize": { 10 | "type": "number", 11 | "description": "Page size for Schema browser", 12 | "default": 500 13 | }, 14 | "vscode-db2i.alwaysStartSQLJob": { 15 | "type": "string", 16 | "description": "Name of configuration to use when auto starting a job. 'new' for brand new default job, 'ask' to be asked, 'never' to never start, or a name of a stored configuration", 17 | "default": "ask" 18 | }, 19 | "vscode-db2i.jobConfigs": { 20 | "type": "object", 21 | "description": "Saved configs for", 22 | "default": {}, 23 | "additionalProperties": true 24 | } 25 | } 26 | }, 27 | { 28 | "id": "vscode-db2i.sqlFormat", 29 | "title": "SQL Formatting", 30 | "properties": { 31 | "vscode-db2i.sqlFormat.identifierCase": { 32 | "type": "string", 33 | "description": "SQL identifiers", 34 | "default": "preserve", 35 | "enum": [ 36 | "lower", 37 | "upper", 38 | "preserve" 39 | ], 40 | "enumDescriptions": [ 41 | "Format SQL identifiers in lowercase", 42 | "Format SQL identifiers in uppercase", 43 | "Preserve the current formatting of SQL identifiers" 44 | ] 45 | }, 46 | "vscode-db2i.sqlFormat.keywordCase": { 47 | "type": "string", 48 | "description": "SQL keywords", 49 | "default": "lower", 50 | "enum": [ 51 | "lower", 52 | "upper" 53 | ], 54 | "enumDescriptions": [ 55 | "Format reserved SQL keywords in lowercase", 56 | "Format reserved SQL keywords in uppercase" 57 | ] 58 | } 59 | } 60 | }, 61 | { 62 | "id": "vscode-db2i.ai", 63 | "title": "AI integrations", 64 | "properties": { 65 | "vscode-db2i.ai.useSchemaDefinition": { 66 | "type": "boolean", 67 | "description": "Provide Schema definition as additional context in Continue and Copilot", 68 | "default": true 69 | } 70 | } 71 | } 72 | ], 73 | "viewsContainers": { 74 | "activitybar": [ 75 | { 76 | "id": "db2-explorer", 77 | "title": "Db2 for i", 78 | "icon": "$(database)" 79 | } 80 | ], 81 | "panel": [ 82 | { 83 | "id": "ibmi-panel", 84 | "title": "IBM i", 85 | "icon": "$(search)" 86 | } 87 | ] 88 | }, 89 | "views": { 90 | "db2-explorer": [ 91 | { 92 | "id": "testingView-db2i", 93 | "name": "Test cases", 94 | "when": "code-for-ibmi:connected && vscode-db2i:testing" 95 | } 96 | ] 97 | }, 98 | "commands": [ 99 | { 100 | "command": "vscode-db2i.json.pasteGenerator", 101 | "title": "Paste JSON as SQL", 102 | "category": "Db2 for i" 103 | }, 104 | { 105 | "command": "vscode-db2i.json.pasteParser", 106 | "title": "Generate JSON SQL parser", 107 | "category": "Db2 for i" 108 | }, 109 | { 110 | "command": "vscode-db2i.getStatementUri", 111 | "title": "Copy sharable statement URI", 112 | "category": "Db2 for i" 113 | } 114 | ], 115 | "menus": { 116 | "commandPalette": [ 117 | { 118 | "command": "vscode-db2i.getStatementUri", 119 | "when": "editorLangId == sql" 120 | } 121 | ], 122 | "editor/context": [ 123 | { 124 | "command": "vscode-db2i.getStatementUri", 125 | "group": "sql@0", 126 | "when": "editorLangId == sql" 127 | }, 128 | { 129 | "command": "vscode-db2i.json.pasteGenerator", 130 | "group": "sql@1", 131 | "when": "editorLangId == sql" 132 | }, 133 | { 134 | "command": "vscode-db2i.json.pasteParser", 135 | "group": "sql@2", 136 | "when": "editorLangId == sql" 137 | } 138 | ] 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /src/database/callable.ts: -------------------------------------------------------------------------------- 1 | 2 | import { JobManager } from "../config"; 3 | import { SQLParm } from "../types"; 4 | 5 | export type CallableType = "PROCEDURE"|"FUNCTION"; 6 | export interface CallableRoutine { 7 | schema: string; 8 | name: string; 9 | specificNames: string[]; 10 | type: string; 11 | } 12 | 13 | export interface CallableSignature { 14 | specificName: string; 15 | parms: SQLParm[]; 16 | returns: SQLParm[]; 17 | } 18 | 19 | export default class Callable { 20 | static async getType(schema: string, name: string, forType: CallableType): Promise { 21 | const statement = `select routine_type, specific_name from qsys2.sysroutines where ROUTINE_SCHEMA = ? and ROUTINE_NAME = ? and routine_type = ?`; 22 | 23 | const result = await JobManager.runSQL<{SPECIFIC_NAME: string, ROUTINE_TYPE: CallableType}>( 24 | statement, 25 | {parameters: [schema, name, forType]} 26 | ); 27 | 28 | let routine: CallableRoutine = { 29 | schema, 30 | name, 31 | specificNames: [], 32 | type: forType 33 | } 34 | 35 | if (result.length > 0) { 36 | routine.specificNames = result.map(row => row.SPECIFIC_NAME); 37 | return routine; 38 | } 39 | 40 | return; 41 | } 42 | 43 | static async getSignaturesFor(schema: string, specificNames: string[]): Promise { 44 | const results = await JobManager.runSQL( 45 | [ 46 | `SELECT * FROM QSYS2.SYSPARMS`, 47 | `WHERE SPECIFIC_SCHEMA = ? AND ROW_TYPE in ('P', 'R') AND SPECIFIC_NAME in (${specificNames.map(() => `?`).join(`, `)})`, 48 | `ORDER BY ORDINAL_POSITION` 49 | ].join(` `), 50 | { 51 | parameters: [schema, ...specificNames] 52 | } 53 | ); 54 | 55 | // find unique specific names 56 | const uniqueSpecificNames = Array.from(new Set(results.map(row => row.SPECIFIC_NAME))); 57 | 58 | // group results by specific name 59 | const groupedResults: CallableSignature[] = uniqueSpecificNames.map(name => { 60 | return { 61 | specificName: name, 62 | parms: results.filter(row => row.SPECIFIC_NAME === name && row.ROW_TYPE === `P`), 63 | returns: results.filter(row => row.SPECIFIC_NAME === name && row.ROW_TYPE === `R`) 64 | } 65 | }); 66 | 67 | return groupedResults; 68 | } 69 | } -------------------------------------------------------------------------------- /src/database/serviceInfo.ts: -------------------------------------------------------------------------------- 1 | import { JobManager } from "../config"; 2 | import { SQLExample } from "../views/examples"; 3 | import Statement from "./statement"; 4 | 5 | export async function getServiceInfo(): Promise { 6 | // The reason we check for a selection is because we don't want it to prompt the user to start one here 7 | if (JobManager.getSelection()) { 8 | const resultSet = await JobManager.runSQL<{ SERVICE_NAME: string, EXAMPLE: string }>(`select SERVICE_NAME, EXAMPLE from qsys2.services_info`); 9 | 10 | return resultSet.map(r => ({ 11 | name: Statement.prettyName(r.SERVICE_NAME), 12 | content: [r.EXAMPLE], 13 | })) 14 | } else { 15 | return [{ name: "Please start an SQL job to load the examples", content: [""] }]; 16 | } 17 | } -------------------------------------------------------------------------------- /src/database/statement.ts: -------------------------------------------------------------------------------- 1 | 2 | import { getInstance } from "../base"; 3 | import Configuration from "../configuration"; 4 | import { JobManager } from "../config"; 5 | 6 | import {format, FormatOptionsWithLanguage, IdentifierCase, KeywordCase} from "sql-formatter" 7 | 8 | export default class Statement { 9 | static format(sql: string, options: FormatOptionsWithLanguage = {}) { 10 | const identifierCase: IdentifierCase = (Configuration.get(`sqlFormat.identifierCase`) || `preserve`); 11 | const keywordCase: KeywordCase = (Configuration.get(`sqlFormat.keywordCase`) || `lower`); 12 | return format(sql, { 13 | ...options, 14 | language: `db2i`, // Defaults to "sql" (see the above list of supported dialects) 15 | linesBetweenQueries: 2, // Defaults to 1 16 | identifierCase: identifierCase, 17 | keywordCase: keywordCase, 18 | }); 19 | } 20 | 21 | static validQsysName(name: string) { 22 | const instance = getInstance(); 23 | const connection = instance ? instance.getConnection() : undefined; 24 | 25 | if (instance && connection) { 26 | // We know the encoding specific variants 27 | const variant_chars_local = connection.variantChars.local; 28 | const validQsysName = new RegExp(`^[A-Z0-9${variant_chars_local}][A-Z0-9_${variant_chars_local}.]{0,9}$`); 29 | return validQsysName.test(name); 30 | } else { 31 | // Fall back with standard variants 32 | return name.match(`[^A-Z0-9_@#$]`); 33 | } 34 | } 35 | 36 | /** 37 | * 38 | * @param name Value which should be normalised 39 | * @param fromUser If the value is true, then we likely need to normalise. Items from the database are usually normalised already 40 | * @returns 41 | */ 42 | static delimName(name: string, fromUser = false) { 43 | if (fromUser) { // The name was input by the user 44 | // If already delimited, return it as-is 45 | if (name.startsWith(`"`) && name.endsWith(`"`)) return name; 46 | // If the value contains a space or decimal it needs to be delimited 47 | if (name.includes(` `) || name.includes(`.`)) return `"${name}"`; 48 | // Otherwise, fold to uppercase. The user should have explicitly delimited if that was their intention. 49 | return name.toUpperCase(); 50 | } else { // The name came from a catalog file query 51 | // If the name contains characters other than the valid variants, uppercase, digits, or underscores, it must be delimited 52 | if (Statement.validQsysName(name)) return name; 53 | else { 54 | if (name.includes(` `) || name.includes(`.`) || name !== name.toUpperCase()) { 55 | return `"${name}"`; 56 | } else { 57 | return name; 58 | } 59 | } 60 | } 61 | } 62 | 63 | /** 64 | * Converts a catalog name to a pretty name for UI purposes 65 | * @param name Catalog name 66 | */ 67 | static prettyName(name: string) { 68 | // If the name contains characters other than the valid variants, uppercase, digits, or underscores, it must be delimited 69 | if (Statement.validQsysName(name)) return name.toLowerCase(); 70 | else { 71 | // Delimited name 72 | if (name.includes(` `) || name.includes(`.`) || name !== name.toUpperCase()) { 73 | return `"${name}"`; 74 | } else { 75 | return name.toLowerCase(); 76 | } 77 | } 78 | } 79 | 80 | static noQuotes(name: string) { 81 | if (name.startsWith(`"`) && name.endsWith(`"`)) return name.substring(1, name.length-1); 82 | return name; 83 | } 84 | 85 | static escapeString(value: string) { 86 | value = value.replace(/[\0\n\r\b\t'\x1a]/g, function (s) { 87 | switch (s) { 88 | case `\0`: 89 | return `\\0`; 90 | case `\n`: 91 | return `\\n`; 92 | case `\r`: 93 | return ``; 94 | case `\b`: 95 | return `\\b`; 96 | case `\t`: 97 | return `\\t`; 98 | case `\x1a`: 99 | return `\\Z`; 100 | case `'`: 101 | return `''`; 102 | default: 103 | return `\\` + s; 104 | } 105 | }); 106 | 107 | return value; 108 | } 109 | } -------------------------------------------------------------------------------- /src/database/table.ts: -------------------------------------------------------------------------------- 1 | 2 | import { JobManager } from "../config"; 3 | import { getInstance } from "../base"; 4 | import { TableColumn, CPYFOptions } from "../types"; 5 | 6 | export default class Table { 7 | /** 8 | * @param {string} schema Not user input 9 | * @param {string} table Not user input 10 | * @returns {Promise} 11 | */ 12 | static async getItems(schema: string, table?: string): Promise { 13 | const params = table ? [schema, table] : [schema]; 14 | const sql = [ 15 | `SELECT `, 16 | ` column.TABLE_SCHEMA,`, 17 | ` column.TABLE_NAME,`, 18 | ` column.SYSTEM_COLUMN_NAME,`, 19 | ` column.COLUMN_NAME,`, 20 | ` key.CONSTRAINT_NAME,`, 21 | ` column.DATA_TYPE, `, 22 | ` column.CHARACTER_MAXIMUM_LENGTH,`, 23 | ` column.NUMERIC_SCALE, `, 24 | ` column.NUMERIC_PRECISION,`, 25 | ` column.IS_NULLABLE, `, 26 | ` column.HAS_DEFAULT, `, 27 | ` column.COLUMN_DEFAULT, `, 28 | ` column.COLUMN_TEXT, `, 29 | ` column.IS_IDENTITY`, 30 | `FROM QSYS2.SYSCOLUMNS2 as column`, 31 | `LEFT JOIN QSYS2.syskeycst as key`, 32 | ` on `, 33 | ` column.table_schema = key.table_schema and`, 34 | ` column.table_name = key.table_name and`, 35 | ` column.column_name = key.column_name`, 36 | `WHERE column.TABLE_SCHEMA = ?`, 37 | ...[ 38 | table ? `AND column.TABLE_NAME = ?` : ``, 39 | ], 40 | `ORDER BY column.ORDINAL_POSITION`, 41 | ].join(` `); 42 | 43 | return JobManager.runSQL(sql, {parameters: params}); 44 | } 45 | 46 | /** 47 | * This is to be used instead of getItems when the table is in session/QTEMP 48 | */ 49 | static async getSessionItems(name: string): Promise { 50 | const sql = [ 51 | `SELECT `, 52 | ` column.TABLE_SCHEMA,`, 53 | ` column.TABLE_NAME,`, 54 | ` column.COLUMN_NAME,`, 55 | ` '' as CONSTRAINT_NAME,`, 56 | ` column.DATA_TYPE, `, 57 | ` column.CHARACTER_MAXIMUM_LENGTH,`, 58 | ` column.NUMERIC_SCALE, `, 59 | ` column.NUMERIC_PRECISION,`, 60 | ` column.IS_NULLABLE, `, 61 | ` column.HAS_DEFAULT, `, 62 | ` column.COLUMN_DEFAULT, `, 63 | ` column.COLUMN_TEXT, `, 64 | ` column.IS_IDENTITY`, 65 | `FROM QSYS2.SYSCOLUMNS2_SESSION as column`, 66 | `WHERE column.TABLE_NAME = ?`, 67 | `ORDER BY column.ORDINAL_POSITION`, 68 | ].join(` `); 69 | 70 | return JobManager.runSQL(sql, {parameters: [name]}); 71 | } 72 | 73 | static async isPartitioned(schema: string, name: string): Promise { 74 | const sql = `select table_name, partitioned_table from qsys2.sysfiles where ((table_schema = ? and table_name = ?) or (system_table_schema = ? and system_table_name = ?)) and partitioned_table is not null and partitioned_table = 'YES'`; 75 | const parameters = [schema, name, schema, name]; 76 | 77 | const result = await JobManager.runSQL(sql, {parameters}); 78 | return result.length > 0; 79 | } 80 | 81 | static async clearFile(library: string, objectName: string): Promise { 82 | const command = `CLRPFM ${library}/${objectName}`; 83 | 84 | const commandResult = await getInstance().getConnection().runCommand({ 85 | command: command, 86 | environment: `ile` 87 | }); 88 | 89 | if (commandResult.code !== 0) { 90 | throw new Error(commandResult.stderr); 91 | } 92 | } 93 | 94 | static async copyFile(library: string, objectName: string, options: CPYFOptions): Promise { 95 | const command = [ 96 | `CPYF FROMFILE(${library}/${objectName}) TOFILE(${options.toLib}/${options.toFile})`, 97 | `FROMMBR(${options.fromMbr}) TOMBR(${options.toMbr}) MBROPT(${options.mbrOpt})`, 98 | `CRTFILE(${options.crtFile}) OUTFMT(${options.outFmt})` 99 | ].join(` `); 100 | 101 | const commandResult = await getInstance().getConnection().runCommand({ 102 | command: command, 103 | environment: `ile` 104 | }); 105 | 106 | if (commandResult.code !== 0) { 107 | throw new Error(commandResult.stderr); 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /src/database/view.ts: -------------------------------------------------------------------------------- 1 | 2 | import { JobManager } from "../config"; 3 | import { TableColumn } from "../types"; 4 | 5 | export default class View { 6 | static getColumns(schema: string, name: string): Promise { 7 | return JobManager.runSQL([ 8 | `SELECT * FROM QSYS2.SYSCOLUMNS`, 9 | `WHERE TABLE_SCHEMA = '${schema}' AND TABLE_NAME = '${name}'`, 10 | `ORDER BY ORDINAL_POSITION` 11 | ].join(` `)); 12 | } 13 | } -------------------------------------------------------------------------------- /src/dsc.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | import { existsSync, mkdirSync } from "fs"; 4 | import { writeFile } from "fs/promises"; 5 | import fetch from "node-fetch"; 6 | import { Octokit } from "@octokit/rest"; 7 | import { SERVER_VERSION_FILE, SERVER_VERSION_TAG } from "./connection/SCVersion"; 8 | 9 | async function work() { 10 | const octokit = new Octokit(); 11 | 12 | const owner = `Mapepire-IBMi`; 13 | const repo = `mapepire-server`; 14 | 15 | try { 16 | const result = await octokit.request(`GET /repos/{owner}/{repo}/releases/tags/${SERVER_VERSION_TAG}`, { 17 | owner, 18 | repo, 19 | headers: { 20 | 'X-GitHub-Api-Version': '2022-11-28' 21 | } 22 | }); 23 | 24 | const newAsset = result.data.assets.find(asset => asset.name.endsWith(`.jar`)); 25 | 26 | if (newAsset) { 27 | console.log(`Asset found: ${newAsset.name}`); 28 | 29 | const url = newAsset.browser_download_url; 30 | const distDirectory = path.join(`.`, `dist`); 31 | if (!existsSync(distDirectory)) { 32 | mkdirSync(distDirectory); 33 | } 34 | 35 | const serverFile = path.join(distDirectory, SERVER_VERSION_FILE); 36 | await downloadFile(url, serverFile); 37 | 38 | console.log(`Asset downloaded: ${serverFile}`); 39 | 40 | } else { 41 | console.log(`Release found but no asset found.`); 42 | } 43 | 44 | 45 | } catch (e) { 46 | console.log(e); 47 | } 48 | } 49 | 50 | function downloadFile(url, outputPath) { 51 | return fetch(url) 52 | .then(x => x.arrayBuffer()) 53 | .then(x => writeFile(outputPath, Buffer.from(x))); 54 | } 55 | 56 | work(); -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import * as vscode from "vscode"; 4 | import schemaBrowser from "./views/schemaBrowser"; 5 | 6 | import * as JSONServices from "./language/json"; 7 | import * as resultsProvider from "./views/results"; 8 | 9 | import { JDBCOptions } from "@ibm/mapepire-js/dist/src/types"; 10 | import { getInstance, loadBase } from "./base"; 11 | import { JobManager, initConfig, onConnectOrServerInstall } from "./config"; 12 | import Configuration from "./configuration"; 13 | import { SQLJobManager } from "./connection/manager"; 14 | import { ServerComponent } from "./connection/serverComponent"; 15 | import { OldSQLJob } from "./connection/sqlJob"; 16 | import { languageInit } from "./language/providers"; 17 | import { DbCache } from "./language/providers/logic/cache"; 18 | import { notebookInit } from "./notebooks/IBMiSerializer"; 19 | import { initialiseTestSuite } from "./testing"; 20 | import { Db2iUriHandler, getStatementUri } from "./uriHandler"; 21 | import { ExampleBrowser } from "./views/examples/exampleBrowser"; 22 | import { JobManagerView } from "./views/jobManager/jobManagerView"; 23 | import { SelfTreeDecorationProvider, selfCodesResultsView } from "./views/jobManager/selfCodes/selfCodesResultsView"; 24 | import { registerContinueProvider } from "./aiProviders/continue/continueContextProvider"; 25 | import { queryHistory } from "./views/queryHistoryView"; 26 | import { registerCopilotProvider } from "./aiProviders/copilot"; 27 | import { registerDb2iTablesProvider } from "./aiProviders/continue/listTablesContextProvider"; 28 | import { setCheckerAvailableContext } from "./language/providers/problemProvider"; 29 | 30 | export interface Db2i { 31 | sqlJobManager: SQLJobManager, 32 | sqlJob: (options?: JDBCOptions) => OldSQLJob 33 | } 34 | 35 | // this method is called when your extension is activated 36 | // your extension is activated the very first time the command is executed 37 | 38 | export function activate(context: vscode.ExtensionContext): Db2i { 39 | 40 | // Use the console to output diagnostic information (console.log) and errors (console.error) 41 | // This line of code will only be executed once when your extension is activated 42 | console.log(`Congratulations, your extension "vscode-db2i" is now active!`); 43 | 44 | loadBase(context); 45 | 46 | const exampleBrowser = new ExampleBrowser(context); 47 | const selfCodesView = new selfCodesResultsView(context); 48 | 49 | context.subscriptions.push( 50 | ...languageInit(), 51 | ...notebookInit(), 52 | ServerComponent.initOutputChannel(), 53 | vscode.window.registerTreeDataProvider( 54 | `jobManager`, 55 | new JobManagerView(context) 56 | ), 57 | vscode.window.registerTreeDataProvider( 58 | `schemaBrowser`, 59 | new schemaBrowser(context) 60 | ), 61 | vscode.window.registerTreeDataProvider( 62 | `queryHistory`, 63 | new queryHistory(context) 64 | ), 65 | vscode.window.registerTreeDataProvider( 66 | `exampleBrowser`, 67 | exampleBrowser 68 | ), 69 | vscode.window.registerTreeDataProvider( 70 | 'vscode-db2i.self.nodes', 71 | selfCodesView 72 | ), 73 | vscode.window.registerFileDecorationProvider( 74 | new SelfTreeDecorationProvider() 75 | ), 76 | vscode.window.registerUriHandler(new Db2iUriHandler()), 77 | getStatementUri 78 | ); 79 | 80 | JSONServices.initialise(context); 81 | resultsProvider.initialise(context); 82 | 83 | initConfig(context); 84 | 85 | console.log(`Developer environment: ${process.env.DEV}`); 86 | const devMode = process.env.DEV !== undefined; 87 | let runTests: Function | undefined; 88 | if (devMode) { 89 | // Run tests if not in production build 90 | runTests = initialiseTestSuite(context); 91 | } 92 | 93 | const instance = getInstance(); 94 | 95 | instance.subscribe(context, `connected`, `db2i-connected`, () => { 96 | DbCache.resetCache(); 97 | selfCodesView.setRefreshEnabled(false); 98 | selfCodesView.setJobOnly(false); 99 | setCheckerAvailableContext(); 100 | // Refresh the examples when we have it, so we only display certain examples 101 | onConnectOrServerInstall().then(() => { 102 | exampleBrowser.refresh(); 103 | selfCodesView.setRefreshEnabled(Configuration.get(`jobSelfViewAutoRefresh`) || false); 104 | // register list tables 105 | const currentJob = JobManager.getSelection(); 106 | const currentSchema = currentJob?.job.options.libraries[0]; 107 | registerDb2iTablesProvider(currentSchema); 108 | if (devMode && runTests) { 109 | runTests(); 110 | } 111 | }); 112 | }); 113 | 114 | 115 | // register copilot provider 116 | registerCopilotProvider(context); 117 | // register continue provider 118 | registerContinueProvider(); 119 | 120 | 121 | 122 | instance.subscribe(context, `disconnected`, `db2i-disconnected`, () => ServerComponent.reset()); 123 | 124 | return { sqlJobManager: JobManager, sqlJob: (options?: JDBCOptions) => new OldSQLJob(options) }; 125 | } 126 | 127 | // this method is called when your extension is deactivated 128 | export function deactivate() { } -------------------------------------------------------------------------------- /src/language/index.ts: -------------------------------------------------------------------------------- 1 | import { completionProvider } from "./providers/completionProvider"; 2 | import { formatProvider } from "./providers/formatProvider"; 3 | import { signatureProvider } from "./providers/parameterProvider"; 4 | import { problemProvider } from "./providers/problemProvider"; 5 | 6 | export function languageInit() { 7 | let functionality = []; 8 | 9 | functionality.push( 10 | completionProvider, 11 | formatProvider, 12 | signatureProvider, 13 | problemProvider 14 | ); 15 | 16 | return functionality; 17 | } 18 | -------------------------------------------------------------------------------- /src/language/providers/contributes.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributes": { 3 | "configuration": [ 4 | { 5 | "id": "vscode-db2i.syntax", 6 | "title": "SQL Syntax Options", 7 | "properties": { 8 | "vscode-db2i.syntax.checkOnOpen": { 9 | "type": "boolean", 10 | "description": "If enabled, will check the syntax of the SQL file when it is opened", 11 | "default": false 12 | }, 13 | "vscode-db2i.syntax.checkOnEdit": { 14 | "type": "boolean", 15 | "description": "Whether the syntax checker should run automatically when the document is edited", 16 | "default": true 17 | }, 18 | "vscode-db2i.syntax.checkInterval": { 19 | "type": "number", 20 | "description": "Time between editing (ms) and sending a request to syntax check on the server", 21 | "default": 1500 22 | }, 23 | "vscode-db2i.syntax.showWarnings": { 24 | "type": "boolean", 25 | "description": "Whether SQL syntax warnings should show in the editor", 26 | "default": false 27 | }, 28 | "vscode-db2i.syntax.useSystemNames": { 29 | "type": "boolean", 30 | "description": "Whether to use system names for columns in the content assist", 31 | "default": false 32 | } 33 | } 34 | } 35 | ], 36 | "commands": [ 37 | { 38 | "command": "vscode-db2i.syntax.checkDocument", 39 | "title": "Check SQL syntax", 40 | "category": "Db2 for IBM i", 41 | "enablement": "code-for-ibmi:connected == true && vscode-db2i:jobManager.hasJob && vscode-db2i:statementCanCancel != true && vscode-db2i.syntax.checkerAvailable == true && vscode-db2i.syntax.checkerRunning != true", 42 | "icon": "$(check-all)" 43 | } 44 | ], 45 | "menus": { 46 | "editor/title": [ 47 | { 48 | "command": "vscode-db2i.syntax.checkDocument", 49 | "when": "editorLangId == sql && code-for-ibmi:connected == true", 50 | "group": "navigation" 51 | } 52 | ] 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/language/providers/formatProvider.ts: -------------------------------------------------------------------------------- 1 | import { Position, Range, TextEdit, languages } from "vscode"; 2 | import Statement from "../../database/statement"; 3 | 4 | export const formatProvider = languages.registerDocumentFormattingEditProvider({language: `sql`}, { 5 | async provideDocumentFormattingEdits(document, options, token) { 6 | const formatted = Statement.format( 7 | document.getText(), 8 | { 9 | useTabs: !options.insertSpaces, 10 | tabWidth: options.tabSize, 11 | } 12 | ); 13 | 14 | return [new TextEdit( 15 | new Range(0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER), 16 | formatted 17 | )]; 18 | } 19 | }) -------------------------------------------------------------------------------- /src/language/providers/index.ts: -------------------------------------------------------------------------------- 1 | import { completionProvider } from "./completionProvider"; 2 | import { formatProvider } from "./formatProvider"; 3 | import { hoverProvider, openProvider } from "./hoverProvider"; 4 | import { signatureProvider } from "./parameterProvider"; 5 | import { peekProvider } from "./peekProvider"; 6 | import { checkDocumentDefintion, problemProvider } from "./problemProvider"; 7 | import { Db2StatusProvider } from "./statusProvider"; 8 | 9 | export const sqlLanguageStatus = new Db2StatusProvider(); 10 | 11 | export function languageInit() { 12 | let functionality = []; 13 | 14 | functionality.push( 15 | completionProvider, 16 | formatProvider, 17 | signatureProvider, 18 | hoverProvider, 19 | openProvider, 20 | // peekProvider, 21 | ...problemProvider, 22 | checkDocumentDefintion, 23 | sqlLanguageStatus 24 | ); 25 | 26 | return functionality; 27 | } 28 | -------------------------------------------------------------------------------- /src/language/providers/logic/available.ts: -------------------------------------------------------------------------------- 1 | 2 | import { env } from "process"; 3 | import { ServerComponent } from "../../../connection/serverComponent"; 4 | import { JobManager } from "../../../config"; 5 | import { JobInfo } from "../../../connection/manager"; 6 | import Configuration from "../../../configuration"; 7 | 8 | export function useSystemNames() { 9 | return Configuration.get(`syntax.useSystemNames`) || false; 10 | } 11 | 12 | export function localAssistIsEnabled() { 13 | return (env.DB2I_DISABLE_CA !== `true`); 14 | } 15 | 16 | export function remoteAssistIsEnabled(needsToBeReady?: boolean): JobInfo|undefined { 17 | if (!localAssistIsEnabled()) return; 18 | if (!ServerComponent.isInstalled()) return; 19 | 20 | const selection = JobManager.getSelection(); 21 | if (!selection) return; 22 | if (selection.job.getStatus() !== `ready` && needsToBeReady) return; 23 | 24 | return selection; 25 | } -------------------------------------------------------------------------------- /src/language/providers/logic/cache.ts: -------------------------------------------------------------------------------- 1 | import Callable, { CallableRoutine, CallableSignature, CallableType } from "../../../database/callable"; 2 | import Schemas, { PageData, SQLType } from "../../../database/schemas"; 3 | import Table from "../../../database/table"; 4 | import { SQLParm, BasicSQLObject, TableColumn } from "../../../types"; 5 | 6 | export interface RoutineDetail { 7 | routine: CallableRoutine; 8 | signatures: CallableSignature[]; 9 | } 10 | 11 | export type LookupResult = RoutineDetail | SQLParm | BasicSQLObject | TableColumn; 12 | 13 | export class DbCache { 14 | private static schemaObjects: Map = new Map(); 15 | private static objectColumns: Map = new Map(); 16 | private static routines: Map = new Map(); 17 | private static routineSignatures: Map = new Map(); 18 | 19 | private static toReset: string[] = []; 20 | 21 | static async resetCache() { 22 | this.objectColumns.clear(); 23 | this.schemaObjects.clear(); 24 | this.toReset = []; 25 | } 26 | 27 | static resetObject(name: string) { 28 | this.toReset.push(name.toLowerCase()); 29 | } 30 | 31 | private static shouldReset(name?: string) { 32 | if (name) { 33 | const inx = this.toReset.indexOf(name.toLowerCase()); 34 | 35 | if (inx > -1) { 36 | this.toReset.splice(inx, 1); 37 | return true; 38 | } 39 | } 40 | 41 | return false; 42 | } 43 | 44 | static async lookupSymbol(name: string, schema: string|undefined, objectFilter: string[]): Promise { 45 | const included = (lookupName: string) => { 46 | if (objectFilter) { 47 | return objectFilter.includes(lookupName.toLowerCase()); 48 | } 49 | return true; 50 | } 51 | 52 | if (schema) { 53 | // Looking routine 54 | const routine = this.getCachedRoutine(schema, name, `FUNCTION`) || this.getCachedRoutine(schema, name, `PROCEDURE`); 55 | if (routine) { 56 | const signatures = this.getCachedSignatures(schema, name); 57 | return { routine, signatures } as RoutineDetail; 58 | } 59 | 60 | objectFilter = objectFilter.map(o => o.toLowerCase()); 61 | 62 | // Search objects 63 | for (const currentSchema of this.schemaObjects.values()) { 64 | const chosenObject = currentSchema.find(sqlObject => included(sqlObject.name) && sqlObject.name === name && sqlObject.schema === schema); 65 | if (chosenObject) { 66 | return chosenObject; 67 | } 68 | } 69 | 70 | // Finally, let's do a last lookup 71 | const lookupRoutine = await this.getRoutine(schema, name, `FUNCTION`) || await this.getRoutine(schema, name, `PROCEDURE`); 72 | if (lookupRoutine) { 73 | const signatures = await this.getSignaturesFor(schema, name, lookupRoutine.specificNames); 74 | return { routine: lookupRoutine, signatures } as RoutineDetail; 75 | } 76 | } 77 | 78 | // Lookup by column 79 | 80 | // First object columns 81 | for (const currentObject of this.objectColumns.values()) { 82 | const chosenColumn = currentObject.find(column => included(column.TABLE_NAME) && column.COLUMN_NAME.toLowerCase() === name.toLowerCase()); 83 | if (chosenColumn) { 84 | return chosenColumn; 85 | } 86 | } 87 | 88 | // Then by routine result columns 89 | for (const currentRoutineSig of this.routineSignatures.values()) { 90 | for (const signature of currentRoutineSig) { 91 | const chosenColumn = signature.returns.find(column => column.PARAMETER_NAME.toLowerCase() === name.toLowerCase()); 92 | if (chosenColumn) { 93 | return chosenColumn; 94 | } 95 | } 96 | } 97 | } 98 | 99 | static async getObjectColumns(schema: string, name: string) { 100 | const key = getKey(`columns`, schema, name); 101 | 102 | if (!this.objectColumns.has(key) || this.shouldReset(name)) { 103 | const result = await Table.getItems(schema, name); 104 | if (result) { 105 | this.objectColumns.set(key, result); 106 | } 107 | } 108 | 109 | return this.objectColumns.get(key) || []; 110 | } 111 | 112 | static async getObjects(schema: string, types: SQLType[], details?: PageData) { 113 | const key = getKey(`objects`, schema, types.join(`&`)); 114 | 115 | if (!this.schemaObjects.has(key) || this.shouldReset(schema)) { 116 | const result = await Schemas.getObjects(schema, types, details); 117 | if (result) { 118 | this.schemaObjects.set(key, result); 119 | } 120 | } 121 | 122 | return this.schemaObjects.get(key) || []; 123 | } 124 | 125 | static async getRoutine(schema: string, name: string, type: CallableType) { 126 | const key = getKey(type, schema, name); 127 | 128 | if (!this.routines.has(key) || this.shouldReset(name)) { 129 | const result = await Callable.getType(schema, name, type); 130 | if (result) { 131 | this.routines.set(key, result); 132 | } else { 133 | this.routines.set(key, false); 134 | return false; 135 | } 136 | } 137 | 138 | return this.routines.get(key) || undefined; 139 | } 140 | 141 | static getCachedRoutine(schema: string, name: string, type: CallableType) { 142 | const key = getKey(type, schema, name); 143 | return this.routines.get(key) || undefined 144 | } 145 | 146 | static async getSignaturesFor(schema: string, name: string, specificNames: string[]) { 147 | const key = getKey(`signatures`, schema, name); 148 | 149 | if (!this.routineSignatures.has(key) || this.shouldReset(name)) { 150 | const result = await Callable.getSignaturesFor(schema, specificNames); 151 | if (result) { 152 | this.routineSignatures.set(key, result); 153 | } 154 | } 155 | 156 | return this.routineSignatures.get(key) || []; 157 | } 158 | 159 | static getCachedSignatures(schema: string, name: string) { 160 | const key = getKey(`signatures`, schema, name); 161 | return this.routineSignatures.get(key) || []; 162 | } 163 | } 164 | 165 | function getKey(type: string, schema: string, name: string = `all`) { 166 | return `${type}.${schema}.${name}`.toLowerCase(); 167 | } -------------------------------------------------------------------------------- /src/language/providers/logic/callable.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, CompletionItemKind, SnippetString } from "vscode"; 2 | import { CallableSignature, CallableType } from "../../../database/callable"; 3 | import { ObjectRef, CallableReference } from "../../sql/types"; 4 | import Statement from "../../../database/statement"; 5 | import { createCompletionItem, getParmAttributes } from "./completion"; 6 | import { DbCache } from "./cache"; 7 | import { SQLParm } from "../../../types"; 8 | import { getPositionData } from "../../sql/document"; 9 | 10 | /** 11 | * Checks if the ref exists as a procedure or function. Then, 12 | * stores the parameters in the completionItemCache 13 | */ 14 | export async function isCallableType(ref: ObjectRef, type: CallableType) { 15 | if (ref.object.schema && ref.object.name && ref.object.name.toUpperCase() !== `TABLE`) { 16 | ref.object.schema = Statement.noQuotes(Statement.delimName(ref.object.schema, true)); 17 | ref.object.name = Statement.noQuotes(Statement.delimName(ref.object.name, true)); 18 | 19 | const callableRoutine = await DbCache.getRoutine(ref.object.schema, ref.object.name, type); 20 | 21 | if (callableRoutine) { 22 | await DbCache.getSignaturesFor(ref.object.schema, ref.object.name, callableRoutine.specificNames); 23 | return true; 24 | } else { 25 | // Not callable, let's just cache it as empty to stop spamming the db 26 | } 27 | } 28 | 29 | return false; 30 | } 31 | 32 | /** 33 | * Gets completion items for named paramaters 34 | * that are stored in the cache for a specific procedure 35 | */ 36 | export function getCallableParameters(ref: CallableReference, offset: number): CompletionItem[] { 37 | const signatures = DbCache.getCachedSignatures(ref.parentRef.object.schema, ref.parentRef.object.name) 38 | if (signatures) { 39 | const { firstNamedParameter, currentCount } = getPositionData(ref, offset); 40 | 41 | const allParms = signatures.reduce((acc, val) => acc.concat(val.parms), []); 42 | const usedParms = ref.tokens.filter((token) => allParms.some((parm) => parm.PARAMETER_NAME.toUpperCase() === Statement.noQuotes(token.value?.toUpperCase()))).map(token => Statement.noQuotes(token.value.toUpperCase())); 43 | 44 | let validParms: SQLParm[] = []; 45 | 46 | // Only show signatures that match the current list of arguments 47 | for (const signature of signatures) { 48 | if (usedParms.length === 0 || signature.parms.some(parm => usedParms.some(usedParm => usedParm === parm.PARAMETER_NAME.toUpperCase()))) { 49 | if (currentCount <= signature.parms.length) { 50 | validParms.push(...signature.parms); 51 | } 52 | } 53 | } 54 | 55 | // find signature with the most parameters 56 | // const parms: SQLParm[] = signatures.reduce((acc, val) => acc.length > val.parms.length ? acc : val.parms, []); 57 | 58 | // Find any already referenced parameters in this list 59 | 60 | //call ifs_write(a, b, ifs => '') 61 | 62 | // Get a list of the available parameters 63 | const availableParms = validParms.filter((parm, i) => 64 | (!usedParms.some((usedParm) => usedParm === parm.PARAMETER_NAME.toUpperCase())) && // Hide parameters that have already been named 65 | parm.ORDINAL_POSITION >= ((firstNamedParameter + 1) || currentCount) // Hide parameters that are before the first named parameter 66 | ); 67 | 68 | return availableParms.map((parm) => { 69 | const item = createCompletionItem( 70 | Statement.prettyName(parm.PARAMETER_NAME), 71 | parm.DEFAULT ? CompletionItemKind.Variable : CompletionItemKind.Constant, 72 | getParmAttributes(parm), 73 | parm.LONG_COMMENT, 74 | `@` + String(parm.ORDINAL_POSITION) 75 | ); 76 | 77 | switch (parm.PARAMETER_MODE) { 78 | case `IN`: 79 | case `INOUT`: 80 | 81 | let defaultSnippetValue = `\${0}`; 82 | 83 | if (parm.DEFAULT) { 84 | defaultSnippetValue = `\${0:${parm.DEFAULT}}`; 85 | } 86 | 87 | if (parm.LONG_COMMENT) { 88 | // This logic can take a LONG_COMMENT such as this: 89 | // A, B, C - some comment 90 | // 0=no, 1=yes - some comment 91 | // and turn it into a snippet of options for a parameter 92 | 93 | const splitIndex = parm.LONG_COMMENT.indexOf(`-`); 94 | if (splitIndex !== -1) { 95 | const possibleItems = parm.LONG_COMMENT.substring(0, splitIndex).trim(); 96 | if (possibleItems.includes(`,`)) { 97 | const literalValues = possibleItems 98 | .split(`,`) 99 | .map((item) => item.includes(`=`) ? item.split(`=`)[0].trim() : item.trim()) 100 | .map((item) => parm.CHARACTER_MAXIMUM_LENGTH !== null ? `${item.trim()}` : item.trim()) 101 | 102 | // If there are no spaces in the literal values, then it's likely to be a good candidate for a snippet 103 | if (literalValues.some((item) => item.includes(` `)) === false) { 104 | defaultSnippetValue = `'\${1|` + literalValues.join(`,`) + `|}'\${0}`; 105 | } 106 | } 107 | } 108 | } 109 | 110 | item.insertText = new SnippetString(item.label + ` => ${defaultSnippetValue}`); 111 | break; 112 | case `OUT`: 113 | item.insertText = item.label + ` => ?`; 114 | break; 115 | } 116 | 117 | return item; 118 | }); 119 | } 120 | return []; 121 | } -------------------------------------------------------------------------------- /src/language/providers/logic/completion.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItemKind, CompletionItem } from "vscode"; 2 | import { SQLParm, TableColumn } from "../../../types"; 3 | 4 | export function createCompletionItem( 5 | name: string, 6 | kind: CompletionItemKind, 7 | detail?: string, 8 | documentation?: string, 9 | sortText?: string 10 | ): CompletionItem { 11 | const item = new CompletionItem(name, kind); 12 | item.detail = detail; 13 | item.documentation = documentation; 14 | item.sortText = sortText; 15 | return item; 16 | } 17 | 18 | export function getParmAttributes(parm: SQLParm): string { 19 | const lines: string[] = [ 20 | `Type: ${prepareParamType(parm)}`, 21 | `Default: ${parm.DEFAULT || `-`}`, 22 | `Pass: ${parm.PARAMETER_MODE}`, 23 | ]; 24 | return lines.join(`\n `); 25 | } 26 | 27 | export function prepareParamType(param: TableColumn | SQLParm): string { 28 | let baseType = param.DATA_TYPE.toLowerCase(); 29 | 30 | if (param.CHARACTER_MAXIMUM_LENGTH) { 31 | baseType += `(${param.CHARACTER_MAXIMUM_LENGTH})`; 32 | } 33 | 34 | if (param.NUMERIC_PRECISION !== null && param.NUMERIC_SCALE !== null) { 35 | baseType += `(${param.NUMERIC_PRECISION}, ${param.NUMERIC_SCALE})`; 36 | } 37 | 38 | const usefulNull = 'COLUMN_NAME' in param || ('ROW_TYPE' in param && param.ROW_TYPE === 'R'); 39 | 40 | if (usefulNull && [`Y`, `YES`].includes(param.IS_NULLABLE)) { 41 | baseType += ` nullable`; 42 | }; 43 | 44 | return baseType; 45 | } 46 | -------------------------------------------------------------------------------- /src/language/providers/logic/parse.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument } from "vscode"; 2 | import Document from "../../sql/document"; 3 | import { VALID_STATEMENT_LENGTH } from "../../../connection/syntaxChecker/checker"; 4 | 5 | let cached: Map = new Map(); 6 | 7 | export function getSqlDocument(document: TextDocument): Document|undefined { 8 | if (!isSafeDocument(document)) return undefined; 9 | 10 | const uri = document.uri.toString(); 11 | const likelyNew = document.uri.scheme === `untitled` && document.version === 1; 12 | 13 | if (cached.has(uri) && !likelyNew) { 14 | const { ast, version } = cached.get(uri)!; 15 | 16 | if (version === document.version) { 17 | return ast; 18 | } 19 | } 20 | 21 | const newAsp = new Document(document.getText()); 22 | cached.set(uri, { ast: newAsp, version: document.version }); 23 | 24 | return newAsp; 25 | } 26 | 27 | export function isSafeDocument(doc: TextDocument): boolean { 28 | return doc.languageId === `sql` && doc.lineCount < VALID_STATEMENT_LENGTH; 29 | } -------------------------------------------------------------------------------- /src/language/providers/parameterProvider.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownString, ParameterInformation, Position, Range, SignatureHelp, SignatureInformation, TextEdit, languages } from "vscode"; 2 | import Statement from "../../database/statement"; 3 | import Document, { getPositionData } from "../sql/document"; 4 | import { isCallableType } from "./logic/callable"; 5 | import { prepareParamType } from "./logic/completion"; 6 | import { CallableType } from "../../database/callable"; 7 | import { StatementType } from "../sql/types"; 8 | import { remoteAssistIsEnabled } from "./logic/available"; 9 | import { DbCache } from "./logic/cache"; 10 | import { getSqlDocument } from "./logic/parse"; 11 | 12 | export const signatureProvider = languages.registerSignatureHelpProvider({ language: `sql` }, { 13 | async provideSignatureHelp(document, position, token, context) { 14 | const offset = document.offsetAt(position); 15 | 16 | if (remoteAssistIsEnabled()) { 17 | 18 | const sqlDoc = getSqlDocument(document); 19 | 20 | if (!sqlDoc) return; 21 | 22 | const currentStatement = sqlDoc.getStatementByOffset(offset); 23 | 24 | if (currentStatement) { 25 | const routineType: CallableType = currentStatement. type === StatementType.Call ? `PROCEDURE` : `FUNCTION`; 26 | const callableRef = currentStatement.getCallableDetail(offset, true); 27 | // TODO: check the function actually exists before returning 28 | if (callableRef) { 29 | const isValid = await isCallableType(callableRef.parentRef, routineType); 30 | if (isValid) { 31 | let signatures = DbCache.getCachedSignatures(callableRef.parentRef.object.schema, callableRef.parentRef.object.name); 32 | if (signatures) { 33 | const help = new SignatureHelp(); 34 | 35 | const { firstNamedParameter, currentParm, currentCount } = getPositionData(callableRef, offset); 36 | 37 | if (firstNamedParameter === 0) { 38 | return; 39 | } 40 | 41 | help.activeParameter = currentParm; 42 | help.signatures = []; 43 | 44 | // Remove any signatures that have more parameters than the current count and sort them 45 | signatures = signatures.filter((s) => s.parms.length >= currentCount).sort((a, b) => a.parms.length - b.parms.length); 46 | help.activeSignature = signatures.findIndex((signature) => currentCount <= signature.parms.length); 47 | 48 | for (const signature of signatures) { 49 | const parms = signature.parms; 50 | 51 | const validParms = parms.filter((parm, i) => parm.DEFAULT === null && parm.PARAMETER_MODE !== `OUT` && (firstNamedParameter === undefined || i < firstNamedParameter)); 52 | 53 | const signatureInfo = new SignatureInformation( 54 | (callableRef.parentRef.object.schema ? Statement.prettyName(callableRef.parentRef.object.schema) + `.` : ``) + Statement.prettyName(callableRef.parentRef.object.name) + 55 | `(` + validParms.map((parm, i) => Statement.prettyName(parm.PARAMETER_NAME || `PARM${i}`)).join(`, `) + (parms.length > validParms.length ? `, ...` : ``) + `)`); 56 | 57 | signatureInfo.parameters = validParms.map((parm, i) => { 58 | const mdString = new MarkdownString( 59 | [ 60 | `\`${parm.PARAMETER_MODE} ${prepareParamType(parm).toLowerCase()}\``, 61 | parm.LONG_COMMENT 62 | ].join(`\n\n`), 63 | ) 64 | return new ParameterInformation( 65 | Statement.prettyName(parm.PARAMETER_NAME || `PARM${i}`), 66 | mdString 67 | ); 68 | }); 69 | 70 | help.signatures.push(signatureInfo); 71 | } 72 | 73 | return help; 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | return; 81 | } 82 | }, `,`, `(`); -------------------------------------------------------------------------------- /src/language/providers/peekProvider.ts: -------------------------------------------------------------------------------- 1 | import { languages, workspace } from "vscode"; 2 | import { getSqlDocument } from "./logic/parse"; 3 | import { JobManager } from "../../config"; 4 | import Statement from "../../database/statement"; 5 | import { remoteAssistIsEnabled } from "./logic/available"; 6 | import Schemas, { } from "../../database/schemas"; 7 | 8 | 9 | export const peekProvider = languages.registerDefinitionProvider({ language: `sql` }, { 10 | async provideDefinition(document, position, token) { 11 | if (!remoteAssistIsEnabled()) return; 12 | 13 | const currentJob = JobManager.getSelection(); 14 | 15 | if (!currentJob) return; 16 | 17 | const defaultSchema = await currentJob.job.getCurrentSchema(); 18 | const naming = currentJob.job.getNaming(); 19 | 20 | const sqlDoc = getSqlDocument(document); 21 | const offset = document.offsetAt(position); 22 | 23 | const tokAt = sqlDoc.getTokenByOffset(offset); 24 | const statementAt = sqlDoc.getStatementByOffset(offset); 25 | 26 | if (statementAt) { 27 | const refs = statementAt.getObjectReferences(); 28 | 29 | const ref = refs.find(ref => ref.tokens[0].range.start && offset <= ref.tokens[ref.tokens.length - 1].range.end); 30 | 31 | if (ref) { 32 | const name = Statement.delimName(ref.object.name, true); 33 | 34 | // Schema is based on a few things: 35 | // If it's a fully qualified path, use the schema path 36 | // Otherwise: 37 | // - if SQL naming is in use, then use the default schema 38 | // - if system naming is in use, then don't pass a library and the library list will be used 39 | const schema = ref.object.schema ? Statement.delimName(ref.object.schema, true) : naming === `sql` ? Statement.delimName(defaultSchema) : undefined; 40 | 41 | const possibleObjects = await Schemas.resolveObjects([{name, schema}], [`*LIB`]); 42 | 43 | let totalBlocks: string[] = []; 44 | 45 | if (possibleObjects.length > 0) { 46 | if (possibleObjects.length > 1) { 47 | totalBlocks.push( 48 | `-- Multiple objects found with the same name.` 49 | ); 50 | } 51 | 52 | for (const posObj of possibleObjects) { 53 | const newContent = await Schemas.generateSQL(posObj.schema, posObj.name, posObj.sqlType, true); 54 | totalBlocks.push(newContent); 55 | } 56 | 57 | const document = await workspace.openTextDocument({ content: totalBlocks.join(`\n\n`), language: `sql` }); 58 | 59 | return { 60 | uri: document.uri, 61 | range: document.lineAt(0).range, 62 | }; 63 | } 64 | 65 | } 66 | } 67 | } 68 | }); -------------------------------------------------------------------------------- /src/language/providers/statusProvider.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, languages, LanguageStatusSeverity } from "vscode"; 2 | import { SQLStatementChecker } from "../../connection/syntaxChecker"; 3 | import { getCheckerTimeout } from "./problemProvider"; 4 | import { useSystemNames } from "./logic/available"; 5 | 6 | export class Db2StatusProvider extends Disposable { 7 | private item = languages.createLanguageStatusItem(`sql`, {language: `sql`}); 8 | constructor() { 9 | super(() => this.item.dispose()); 10 | 11 | this.item.name = `SQL Language Status`; 12 | this.setState(false); 13 | } 14 | 15 | setState(hasJob: Boolean) { 16 | if (hasJob) { 17 | const checker = SQLStatementChecker.get(); 18 | const checkerTimeout = getCheckerTimeout() / 1000; 19 | this.item.text = `SQL assistance available. ${checker ? `Syntax checking enabled (every ${checkerTimeout}s after editing)` : `Syntax checking not available.`}`; 20 | this.item.detail = `You're connected to an IBM i. ${checker ? `You can use the advanced SQL language tooling.` : `Syntax checking not available. This means that the syntax checker did not install when connecting to this system.`}`; 21 | this.item.detail = [ 22 | `You're connected to an IBM i.`, 23 | checker ? `You can use the advanced SQL language tooling.` : `Syntax checking not available. This means that the syntax checker did not install when connecting to this system.`, 24 | (useSystemNames() ? `System names` : `SQL names`) + ` for columns will be used in the content assist.` 25 | ].join(` `); 26 | this.item.severity = checker ? LanguageStatusSeverity.Information : LanguageStatusSeverity.Warning; 27 | } else { 28 | this.item.text = `Basic SQL assistance available`; 29 | this.item.detail = `Connect to an IBM i to enable advanced SQL language tooling.`; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/language/sql/tests/tokens.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect, test } from 'vitest' 2 | import SQLTokeniser from '../tokens' 3 | 4 | // Edit an assertion and save to see HMR in action 5 | 6 | test('Basic tokens', () => { 7 | const tokeniser = new SQLTokeniser(); 8 | 9 | const tokens = tokeniser.tokenise(`select * from sample`); 10 | 11 | expect(tokens.length).toBe(4); 12 | }); 13 | 14 | test('Function and block test', () => { 15 | const tokeniser = new SQLTokeniser(); 16 | 17 | const tokens = tokeniser.tokenise(`select * from table(func()) x`); 18 | 19 | expect(tokens.length).toBe(10); 20 | }); 21 | 22 | test('Comment test', () => { 23 | const tokeniser = new SQLTokeniser(); 24 | 25 | const tokens = tokeniser.tokenise(`select * from table(func()) x -- Hello world`); 26 | 27 | expect(tokens.length).toBe(10); 28 | }); 29 | 30 | test('Comment token test', () => { 31 | const tokeniser = new SQLTokeniser(); 32 | tokeniser.storeComments = true; 33 | 34 | const tokens = tokeniser.tokenise([ 35 | `--hello: world!!!: coolness`, 36 | `select * from table(func()) x` 37 | ].join(`\n`)); 38 | 39 | expect(tokens.length).toBe(12); 40 | expect(tokens.some(t => t.value === `--hello: world!!!: coolness`)).toBeTruthy(); 41 | }); 42 | 43 | test('New line (\\n) and comments test', () => { 44 | const tokeniser = new SQLTokeniser(); 45 | 46 | const tokens = tokeniser.tokenise([ 47 | `select * --cool`, 48 | `from sample -- this table doesn't exist` 49 | ].join(`\n`)); 50 | 51 | expect(tokens.length).toBe(5); 52 | expect(tokens[2].type).toBe(`newline`); 53 | }); 54 | 55 | test('New line (\\r\\n) and comments test', () => { 56 | const tokeniser = new SQLTokeniser(); 57 | 58 | const tokens = tokeniser.tokenise([ 59 | `select * --cool`, 60 | `from sample-- this table doesn't exist` 61 | ].join(`\r\n`)); 62 | 63 | expect(tokens.length).toBe(5); 64 | expect (tokens[2].type).toBe(`newline`); 65 | }); 66 | 67 | test(`Delimited names`, () => { 68 | const tokeniser = new SQLTokeniser(); 69 | const line = `CREATE TABLE "TestDelimiters"."Delimited Table" ("Delimited Column" INTEGER DEFAULT NULL, CONSTRAINT "TestDelimiters"."Delimited Key" PRIMARY KEY ("Delimited Column"));`; 70 | 71 | const tokens = tokeniser.tokenise(line); 72 | 73 | expect(tokens.length).toBe(22); 74 | 75 | expect (tokens[2].type).toBe(`sqlName`); 76 | expect (tokens[2].value).toBe(`"TestDelimiters"`); 77 | 78 | expect (tokens[3].type).toBe(`dot`); 79 | 80 | expect (tokens[4].type).toBe(`sqlName`); 81 | expect (tokens[4].value).toBe(`"Delimited Table"`); 82 | }); 83 | 84 | test(`Block comments`, () => { 85 | const lines = [ 86 | `/*%METADATA */`, 87 | `/* %TEXT */`, 88 | `/*%EMETADATA */`, 89 | ``, 90 | `Create Trigger ORD701_Insert_order`, 91 | `After Insert on order`, 92 | `Referencing New As N`, 93 | ``, 94 | `For Each Row`, 95 | `Program Name ORD701`, 96 | `set option sqlPath = *LIBL`, 97 | `Begin`, 98 | ``, 99 | ` Update Customer set culastord = n.ordate`, 100 | ` where cuid = N.orcuid;`, 101 | `End`, 102 | ].join(` `); 103 | 104 | const tokeniser = new SQLTokeniser(); 105 | const tokens = tokeniser.tokenise(lines); 106 | 107 | expect(tokens[0].type).toBe(`statementType`) 108 | expect(tokens[0].value).toBe(`Create`) 109 | expect(lines.substring(tokens[0].range.start, tokens[0].range.end)).toBe(`Create`) 110 | }); 111 | 112 | test('For in data-type (issue #315)', () => { 113 | const tokeniser = new SQLTokeniser(); 114 | 115 | const tokens = tokeniser.tokenise([ 116 | `select cast(x'01' as char(1) for bit data) as something,`, 117 | `case when 1=1 then 'makes sense' else 'what?' end as something_else`, 118 | `from sysibm.sysdummy1;` 119 | ].join(`\n`)); 120 | 121 | expect(tokens.length).toBe(35); 122 | expect(tokens[9].type).toBe(`word`); 123 | expect(tokens[9].value.toLowerCase()).toBe(`for`); 124 | }); -------------------------------------------------------------------------------- /src/language/sql/types.ts: -------------------------------------------------------------------------------- 1 | import Statement from "./statement"; 2 | 3 | export enum StatementType { 4 | Unknown = "Unknown", 5 | Create = "Create", 6 | Close = "Close", 7 | Insert = "Insert", 8 | Select = "Select", 9 | With = "With", 10 | Update = "Update", 11 | Delete = "Delete", 12 | Declare = "Declare", 13 | Begin = "Begin", 14 | Drop = "Drop", 15 | End = "End", 16 | Else = "Else", 17 | Elseif = "Elseif", 18 | Call = "Call", 19 | Alter = "Alter", 20 | Fetch = "Fetch", 21 | For = "For", 22 | Get = "Get", 23 | Goto = "Goto", 24 | If = "If", 25 | Include = "Include", 26 | Iterate = "Iterate", 27 | Leave = "Leave", 28 | Loop = "Loop", 29 | Merge = "Merge", 30 | Open = "Open", 31 | Pipe = "Pipe", 32 | Repeat = "Repeat", 33 | Resignal = "Resignal", 34 | Return = "Return", 35 | Signal = "Signal", 36 | Set = "Set", 37 | While = "While" 38 | } 39 | 40 | export const StatementTypeWord = { 41 | 'CREATE': StatementType.Create, 42 | 'SELECT': StatementType.Select, 43 | 'WITH': StatementType.With, 44 | 'INSERT': StatementType.Insert, 45 | 'UPDATE': StatementType.Update, 46 | 'DELETE': StatementType.Delete, 47 | 'DECLARE': StatementType.Declare, 48 | 'DROP': StatementType.Drop, 49 | 'END': StatementType.End, 50 | 'ELSE': StatementType.Else, 51 | 'ELSEIF': StatementType.Elseif, 52 | 'CALL': StatementType.Call, 53 | 'BEGIN': StatementType.Begin, 54 | 'ALTER': StatementType.Alter, 55 | 'FOR': StatementType.For, 56 | 'FETCH': StatementType.Fetch, 57 | 'GET': StatementType.Get, 58 | 'GOTO': StatementType.Goto, 59 | 'IF': StatementType.If, 60 | 'INCLUDE': StatementType.Include, 61 | 'ITERATE': StatementType.Iterate, 62 | 'LEAVE': StatementType.Leave, 63 | 'LOOP': StatementType.Loop, 64 | 'MERGE': StatementType.Merge, 65 | 'PIPE': StatementType.Pipe, 66 | 'REPEAT': StatementType.Repeat, 67 | 'RESIGNAL': StatementType.Resignal, 68 | 'RETURN': StatementType.Return, 69 | 'SIGNAL': StatementType.Signal, 70 | 'SET': StatementType.Set, 71 | 'WHILE': StatementType.While, 72 | }; 73 | 74 | export enum ClauseType { 75 | Unknown = "Unknown", 76 | From = "From", 77 | Into = "Into", 78 | Where = "Where", 79 | Having = "Having", 80 | Group = "Group", 81 | Limit = "Limit", 82 | Offset = "Offset", 83 | Order = "Order" 84 | } 85 | 86 | export const ClauseTypeWord = { 87 | 'FROM': ClauseType.From, 88 | 'INTO': ClauseType.Into, 89 | 'WHERE': ClauseType.Where, 90 | 'HAVING': ClauseType.Having, 91 | 'GROUP': ClauseType.Group, 92 | 'LIMIT': ClauseType.Limit, 93 | 'OFFSET': ClauseType.Offset, 94 | 'ORDER': ClauseType.Order 95 | } 96 | 97 | export interface CTEReference { 98 | name: string; 99 | columns: string[]; 100 | statement: Statement 101 | }; 102 | 103 | export interface IRange { 104 | start: number; 105 | end: number; 106 | } 107 | 108 | export interface Token { 109 | value?: string; 110 | block?: Token[]; 111 | type: string; 112 | range: IRange; 113 | } 114 | 115 | export interface QualifiedObject { 116 | schema?: string; 117 | name?: string; 118 | system?: string; 119 | } 120 | 121 | export interface ObjectRef { 122 | tokens: Token[], 123 | object: QualifiedObject; 124 | alias?: string; 125 | 126 | isUDTF?: boolean; 127 | fromLateral?: boolean; 128 | 129 | /** only used within create statements */ 130 | createType?: string; 131 | } 132 | 133 | export interface StatementGroup { 134 | range: IRange, 135 | statements: Statement[] 136 | } 137 | 138 | export interface Definition extends ObjectRef { 139 | range: IRange; 140 | children: Definition[]; 141 | } 142 | 143 | export interface ParsedEmbeddedStatement { 144 | changed: boolean; 145 | content: string; 146 | parameterCount: number; 147 | } 148 | 149 | export interface CallableReference { 150 | tokens: Token[], 151 | parentRef: ObjectRef 152 | }; -------------------------------------------------------------------------------- /src/notebooks/IBMiSerializer.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { TextDecoder, TextEncoder } from 'util'; 3 | import { IBMiController } from './Controller'; 4 | import { notebookFromSqlUri } from './logic/openAsNotebook'; 5 | import { Cell, CodeCell, MarkdownCell, Notebook, Output } from './jupyter'; 6 | import { exportNotebookAsHtml } from './logic/export'; 7 | 8 | interface RawNotebookCell { 9 | language: string; 10 | value: string; 11 | kind: vscode.NotebookCellKind; 12 | } 13 | 14 | let newNotebookCount = 1; 15 | 16 | export function notebookInit() { 17 | const openBlankNotebook = vscode.commands.registerCommand(`vscode-db2i.notebook.open`, () => { 18 | vscode.workspace.openNotebookDocument( 19 | `db2i-notebook`, 20 | {cells: []} 21 | ) 22 | .then(doc => { 23 | vscode.window.showNotebookDocument(doc); 24 | }); 25 | }); 26 | 27 | return [ 28 | vscode.workspace.registerNotebookSerializer(`db2i-notebook`, new IBMiSerializer()), 29 | new IBMiController(), 30 | notebookFromSqlUri, 31 | openBlankNotebook, 32 | exportNotebookAsHtml 33 | ]; 34 | } 35 | 36 | export class IBMiSerializer implements vscode.NotebookSerializer { 37 | async deserializeNotebook( 38 | content: Uint8Array, 39 | _token: vscode.CancellationToken 40 | ): Promise { 41 | let contents = new TextDecoder().decode(content); 42 | 43 | let asNb: Notebook; 44 | try { 45 | asNb = JSON.parse(contents); 46 | } catch { 47 | asNb = undefined; 48 | } 49 | 50 | if (!asNb) { 51 | return new vscode.NotebookData([]); 52 | } 53 | 54 | const cells: vscode.NotebookCellData[] = asNb.cells.map((cell: Cell): vscode.NotebookCellData => { 55 | if (cell.cell_type === `markdown`) { 56 | return new vscode.NotebookCellData( 57 | vscode.NotebookCellKind.Markup, 58 | (typeof cell.source === `string` ? cell.source : cell.source.join(`\n`)), 59 | `markdown` 60 | ); 61 | } else { 62 | const language = cell.metadata && cell.metadata.tags && cell.metadata.tags[0] ? cell.metadata.tags[0] : `sql`; 63 | const newCell = new vscode.NotebookCellData( 64 | vscode.NotebookCellKind.Code, 65 | (typeof cell.source === `string` ? cell.source : cell.source.join(`\n`)), 66 | language 67 | ); 68 | 69 | if ('outputs' in cell) { 70 | newCell.outputs = cell.outputs.map((output): vscode.NotebookCellOutput => { 71 | switch (output.output_type) { 72 | case `display_data`: 73 | const items = Object.keys(output.data).map(mime => { 74 | if (typeof output.data[mime] === `string`) { 75 | return new vscode.NotebookCellOutputItem(Buffer.from(output.data[mime] as string), mime); 76 | } else { 77 | return new vscode.NotebookCellOutputItem(Buffer.from((output.data[mime] as string[]).join(`\n`)), mime); 78 | } 79 | }); 80 | 81 | return new vscode.NotebookCellOutput(items); 82 | } 83 | 84 | }); 85 | } 86 | 87 | return newCell; 88 | } 89 | }); 90 | 91 | return new vscode.NotebookData(cells); 92 | } 93 | 94 | async serializeNotebook( 95 | data: vscode.NotebookData, 96 | _token: vscode.CancellationToken 97 | ): Promise { 98 | const newNotebook: Notebook = { 99 | cells: data.cells.map(nbCellToJupyterCell), 100 | metadata: { 101 | kernelspec: { 102 | display_name: `DB2 for IBM i`, 103 | name: `db2i`, 104 | }, 105 | language_info: { 106 | name: `sql`, 107 | file_extension: `sql` 108 | }, 109 | }, 110 | nbformat: 4, 111 | nbformat_minor: 0 112 | } 113 | 114 | 115 | 116 | return new TextEncoder().encode(JSON.stringify(newNotebook)); 117 | } 118 | } 119 | 120 | function nbCellToJupyterCell(cell: vscode.NotebookCellData): Cell { 121 | if (cell.kind === vscode.NotebookCellKind.Code) { 122 | const codeBase: CodeCell = { 123 | cell_type: `code`, 124 | execution_count: null, 125 | metadata: {tags: [cell.languageId]}, 126 | outputs: cell.outputs.map((output): Output => { 127 | return { 128 | output_type: `display_data`, 129 | data: { 130 | [output.items[0].mime]: output.items[0].data.toString() 131 | }, 132 | metadata: {} 133 | } 134 | }), 135 | source: cell.value, 136 | }; 137 | 138 | return codeBase; 139 | 140 | } else { 141 | const mdBase: MarkdownCell = { 142 | cell_type: `markdown`, 143 | metadata: {}, 144 | source: cell.value, 145 | }; 146 | 147 | return mdBase; 148 | 149 | } 150 | } -------------------------------------------------------------------------------- /src/notebooks/contributes.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributes": { 3 | "notebooks": [ 4 | { 5 | "id": "db2i-notebook", 6 | "type": "db2i-notebook", 7 | "displayName": "IBM i Notebook", 8 | "selector": [ 9 | { 10 | "filenamePattern": "*.inb" 11 | } 12 | ] 13 | } 14 | ], 15 | "keybindings": [ 16 | { 17 | "command": "notebook.cell.execute", 18 | "key": "ctrl+r", 19 | "mac": "cmd+r", 20 | "when": "editorLangId == sql && resourceExtname == .inb" 21 | } 22 | ], 23 | "commands": [ 24 | { 25 | "command": "vscode-db2i.notebook.open", 26 | "title": "New Notebook", 27 | "category": "IBM i Notebooks", 28 | "enablement": "code-for-ibmi:connected == true", 29 | "icon": "$(notebook)" 30 | }, 31 | { 32 | "command": "vscode-db2i.notebook.fromSqlUri", 33 | "title": "Open as Notebook", 34 | "category": "IBM i Notebooks", 35 | "icon": "$(notebook)" 36 | }, 37 | { 38 | "command": "vscode-db2i.notebook.exportAsHtml", 39 | "title": "Export", 40 | "category": "IBM i Notebooks", 41 | "icon": "$(save)" 42 | } 43 | ], 44 | "menus": { 45 | "notebook/toolbar": [ 46 | { 47 | "command": "vscode-db2i.notebook.exportAsHtml", 48 | "when": "code-for-ibmi:connected == true && resourceExtname == .inb", 49 | "group": "navigation" 50 | } 51 | ], 52 | "commandPalette": [ 53 | { 54 | "command": "vscode-db2i.notebook.fromSqlUri", 55 | "when": "never" 56 | } 57 | ] 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/notebooks/logic/chart.ts: -------------------------------------------------------------------------------- 1 | import { ChartJsType, chartJsTypes } from "./chartJs"; 2 | 3 | export const chartTypes = [...chartJsTypes]; 4 | 5 | export interface ChartDetail { 6 | type?: ChartJsType; 7 | title?: string; 8 | y?: string; 9 | } 10 | 11 | export interface ChartData { 12 | labels: string[]; 13 | tooltips?: string[]; 14 | datasets: Dataset[]; 15 | } 16 | 17 | export interface Dataset { 18 | label: string; 19 | data: unknown[]; 20 | backgroundColor?: string; 21 | borderColor?: string; 22 | borderWidth?: number; 23 | } 24 | 25 | export type GeneratorFunction = (id: number, detail: ChartDetail, columns: string[], rows: any[]) => T; 26 | 27 | export function generateChart(id: number, detail: ChartDetail, columns: string[], rows: any[], gen: GeneratorFunction) { 28 | if (rows.length === 1) { 29 | const labels = columns; 30 | const data = Object.values(rows[0]); 31 | return gen(id, detail, labels, [{ data, label: `Data` }]); 32 | 33 | } else if (rows.length > 1) { 34 | const datasets: Dataset[] = []; 35 | const keys = Object.keys(rows[0]); 36 | 37 | let labels = []; 38 | 39 | const labelIndex = keys.findIndex(key => String(key).toUpperCase() === `LABEL`); 40 | // We only continue if we can find a label for each row 41 | if (labelIndex >= 0) { 42 | 43 | // Look through all the keys 44 | for (let i = 0; i < keys.length; i++) { 45 | if (i === labelIndex) { 46 | // If this column matches the label, set the labels based on the rows of this column 47 | labels = rows.map(row => row[columns[i]]); 48 | } else { 49 | // We only want to add columns that are numbers, so we ignore string columns 50 | if ([`bigint`, `number`].includes(typeof rows[0][columns[i]])) { 51 | const newSet = { 52 | label: columns[i], 53 | tooltips: [], 54 | data: rows.map(row => row[columns[i]]), 55 | }; 56 | 57 | // If we have a description column, we add it to the dataset 58 | const setDescriptionsColumn = columns.findIndex(col => col.toUpperCase().startsWith(columns[i].toUpperCase() + `_D`)); 59 | 60 | if (setDescriptionsColumn >= 0 && typeof rows[0][columns[setDescriptionsColumn]] === `string`) { 61 | newSet.tooltips = rows.map(row => row[columns[setDescriptionsColumn]].replaceAll(`\\n`, `\n`)); 62 | } 63 | 64 | datasets.push(newSet); 65 | } 66 | } 67 | } 68 | 69 | if (datasets.length === 0) { 70 | throw new Error(`No dataset columns found in the result set.`); 71 | } 72 | 73 | return gen(id, detail, labels, datasets); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/notebooks/logic/chartJs.ts: -------------------------------------------------------------------------------- 1 | 2 | import chartjs from 'chart.js/dist/chart.umd.js'; 3 | import { ChartData, ChartDetail, Dataset } from './chart'; 4 | 5 | export type ChartJsType = `bar` | `line` | `doughnut` | `pie` | `polarArea` | `radar`; 6 | export const chartJsTypes: ChartJsType[] = [`bar`, `line`, `doughnut`, `pie`, `polarArea`, `radar`]; 7 | 8 | export function generateChartHTMLCell(id: number, detail: ChartDetail, labels: string[], datasets: Dataset[]): string { 9 | const bodies = generateChartHTMLEmbedded(id, detail, labels, datasets, `message`); 10 | 11 | return /*html*/` 12 | 13 | 14 | 17 | 18 | 19 |
20 | ${bodies.html} 21 |
22 | 23 | `; 24 | } 25 | 26 | export function generateChartHTMLEmbedded(id: number, detail: ChartDetail, labels: string[], datasets: Dataset[], loadEvent: string = `load`): { html: string, script: string } { 27 | const chartData: ChartData = { 28 | labels, 29 | datasets, 30 | }; 31 | 32 | const hasYaxis = detail.type === `bar` || detail.type === `line`; 33 | 34 | const script = /*javascript*/ ` 35 | if (!window.ibmicharts) { 36 | window.ibmicharts = {}; 37 | } 38 | 39 | window.addEventListener('${loadEvent}', (event) => { 40 | const theChart = window.ibmicharts['myChart${id}']; 41 | if (!theChart) { 42 | const chartEle = document.getElementById('myChart${id}'); 43 | if (chartEle) { 44 | const maxHeight = window.innerHeight * 0.8; 45 | const maxWidth = window.innerWidth * 0.8; 46 | const targetSize = Math.min(maxHeight, maxWidth); 47 | chartEle.style.maxHeight = targetSize + 'px'; 48 | chartEle.style.maxWidth = targetSize + 'px'; 49 | try { 50 | window.ibmicharts['myChart${id}'] = new Chart(chartEle.getContext('2d'), { 51 | type: '${detail.type}', 52 | data: ${JSON.stringify(chartData)}, 53 | options: { 54 | animation: { 55 | duration: 0 56 | }, 57 | plugins: { 58 | title: { 59 | display: ${detail.title ? `true` : `false`}, 60 | text: '${detail.title || `undefined`}', 61 | position: 'top' 62 | }, 63 | tooltip: { 64 | callbacks: { 65 | afterLabel: function(context) { 66 | const nodeIndex = context.dataIndex; 67 | if (context.dataset.tooltips && context.dataset.tooltips[nodeIndex]) { 68 | return '\\n' + context.dataset.tooltips[nodeIndex]; 69 | } 70 | } 71 | } 72 | } 73 | }, 74 | scales: { 75 | y: { 76 | display: ${JSON.stringify(hasYaxis)}, 77 | title: { 78 | display: ${detail.y ? `true` : `false`}, 79 | text: ${JSON.stringify(detail.y)} 80 | } 81 | } 82 | } 83 | }, 84 | }); 85 | } catch (e) { 86 | console.error(e); 87 | document.getElementById('errorText${id}').innerText = 'Failed to render chart. Log appended to Dev Console.'; 88 | } 89 | } 90 | } 91 | }); 92 | `; 93 | 94 | const html = /*html*/` 95 | 96 |

97 | `; 98 | 99 | return { html, script }; 100 | } -------------------------------------------------------------------------------- /src/notebooks/logic/openAsNotebook.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { commands, Uri, workspace, NotebookCellData, NotebookCellKind, window } from "vscode"; 3 | import Document from "../../language/sql/document"; 4 | 5 | export const notebookFromSqlUri = commands.registerCommand(`vscode-db2i.notebook.fromSqlUri`, async (sqlUri?: Uri) => { 6 | if (sqlUri) { 7 | const fsPath = sqlUri.fsPath; 8 | const pathDetail = path.parse(fsPath); 9 | 10 | const sqlDoc = await workspace.openTextDocument(sqlUri); 11 | const content = sqlDoc.getText(); 12 | 13 | try { 14 | const sqlDocument = new Document(content); 15 | const statements = sqlDocument.getStatementGroups().map(g => content.substring(g.range.start, g.range.end)); 16 | 17 | notebookFromStatements(statements) 18 | 19 | } catch (e) { 20 | window.showWarningMessage(`Failed to parse SQL file: ${e.message}`); 21 | } 22 | } 23 | }); 24 | 25 | export function notebookFromStatements(statements?: string[]) { 26 | if (statements) { 27 | workspace.openNotebookDocument( 28 | `db2i-notebook`, 29 | {cells: statements.map(s => { 30 | if (s.startsWith(`##`)) { 31 | return new NotebookCellData(NotebookCellKind.Markup, s.substring(2).trim(), `markdown`) 32 | } else { 33 | return new NotebookCellData(NotebookCellKind.Code, s, `sql`) 34 | } 35 | })} 36 | ) 37 | .then(doc => { 38 | window.showNotebookDocument(doc); 39 | }); 40 | } 41 | }; -------------------------------------------------------------------------------- /src/notebooks/logic/statement.ts: -------------------------------------------------------------------------------- 1 | import { ChartDetail, chartTypes } from "./chart"; 2 | import { ChartJsType, chartJsTypes } from "./chartJs"; 3 | 4 | export interface StatementSettings { 5 | chart?: ChartJsType; 6 | title?: string; 7 | y?: string; 8 | hideStatement?: string; 9 | [key: string]: string 10 | }; 11 | 12 | export function getStatementDetail(content: string, eol: string) { 13 | let chartDetail: ChartDetail = {}; 14 | let settings: StatementSettings = {}; 15 | 16 | // Strip out starting comments 17 | if (content.startsWith(`--`)) { 18 | const lines = content.split(eol); 19 | const firstNonCommentLine = lines.findIndex(line => !line.startsWith(`--`)); 20 | 21 | const startingComments = lines.slice(0, firstNonCommentLine).map(line => line.substring(2).trim()); 22 | content = lines.slice(firstNonCommentLine).join(eol); 23 | 24 | for (let comment of startingComments) { 25 | const sep = comment.indexOf(`:`); 26 | const key = comment.substring(0, sep).trim(); 27 | const value = comment.substring(sep + 1).trim(); 28 | settings[key] = value; 29 | } 30 | 31 | // Chart settings defined by comments 32 | if (settings[`chart`] && chartJsTypes.includes(settings[`chart`])) { 33 | chartDetail.type = settings[`chart`]; 34 | } 35 | 36 | if (settings[`title`]) { 37 | chartDetail.title = settings[`title`]; 38 | } 39 | 40 | if (settings[`y`]) { 41 | chartDetail.y = settings[`y`]; 42 | } 43 | } 44 | 45 | // Remove trailing semicolon. The Service Component doesn't like it. 46 | if (content.endsWith(`;`)) { 47 | content = content.substring(0, content.length - 1); 48 | } 49 | 50 | // Perhaps the chart type is defined by the statement prefix 51 | const chartType: ChartJsType | undefined = chartTypes.find(type => content.startsWith(`${type}:`)); 52 | if (chartType) { 53 | chartDetail.type = chartType; 54 | content = content.substring(chartType.length + 1); 55 | } 56 | return { chartDetail, content, settings }; 57 | } -------------------------------------------------------------------------------- /src/testing/databasePerformance.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItemKind } from "vscode"; 2 | import { TestSuite } from "."; 3 | import Database from "../database/schemas"; 4 | import Statement from "../database/statement"; 5 | import { 6 | CompletionType 7 | } from "../language/providers/completionProvider"; 8 | 9 | const performance = require(`perf_hooks`).performance; 10 | const forSchema = Statement.noQuotes( 11 | Statement.delimName(`sample`, true) 12 | ); 13 | const sqlTypes: { [index: string]: CompletionType } = { 14 | tables: { 15 | order: `b`, 16 | label: `table`, 17 | type: `tables`, 18 | icon: CompletionItemKind.File, 19 | }, 20 | views: { 21 | order: `c`, 22 | label: `view`, 23 | type: `views`, 24 | icon: CompletionItemKind.Interface, 25 | }, 26 | aliases: { 27 | order: `d`, 28 | label: `alias`, 29 | type: `aliases`, 30 | icon: CompletionItemKind.Reference, 31 | }, 32 | functions: { 33 | order: `e`, 34 | label: `function`, 35 | type: `functions`, 36 | icon: CompletionItemKind.Method, 37 | }, 38 | }; 39 | 40 | 41 | export const DatabasePerformanceSuite: TestSuite = { 42 | name: `Database object query performance tests`, 43 | tests: [ 44 | { 45 | name: `time async get objects`, 46 | test: async () => { 47 | const start = performance.now(); 48 | const promises = Object.entries(sqlTypes).map(([_, value]) => Database.getObjects(forSchema, [value.type])); 49 | const results = await Promise.all(promises); 50 | const end = performance.now(); 51 | console.log(`time get objects: ${end - start}ms`); 52 | }, 53 | }, 54 | { 55 | name: `time one shot get objects`, 56 | test: async () => { 57 | const start = performance.now(); 58 | const data = await Database.getObjects(forSchema, [ 59 | `tables`, 60 | `views`, 61 | `aliases`, 62 | `functions`, 63 | ]); 64 | const end = performance.now(); 65 | console.log(`time get objects: ${end - start}ms`); 66 | }, 67 | } 68 | ], 69 | }; 70 | -------------------------------------------------------------------------------- /src/testing/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { env } from "process"; 3 | import { TestSuitesTreeProvider } from "./testCasesTree"; 4 | import { getInstance } from "../base"; 5 | import { ManagerSuite } from "./manager"; 6 | import { JobsSuite } from "./jobs"; 7 | import { DatabaseSuite } from "./database"; 8 | import { DatabasePerformanceSuite } from "./databasePerformance"; 9 | import { SelfCodesTestSuite } from "./selfCodes"; 10 | 11 | const suites : TestSuite[] = [ 12 | JobsSuite, 13 | ManagerSuite, 14 | DatabaseSuite, 15 | SelfCodesTestSuite 16 | // DatabasePerformanceSuite 17 | ] 18 | 19 | export type TestSuite = { 20 | name: string 21 | tests: TestCase[] 22 | } 23 | 24 | export interface TestCase { 25 | name: string, 26 | status?: "running" | "failed" | "pass" 27 | failure?: string 28 | test: () => Promise 29 | } 30 | 31 | let testSuitesTreeProvider: TestSuitesTreeProvider; 32 | export function initialiseTestSuite(context: vscode.ExtensionContext) { 33 | if (env['db2_testing'] === `true`) { 34 | const instance = getInstance(); 35 | 36 | vscode.commands.executeCommand(`setContext`, `vscode-db2i:testing`, true); 37 | instance.subscribe(context, `disconnected`, `db2i-resetTests`, resetTests); 38 | 39 | testSuitesTreeProvider = new TestSuitesTreeProvider(suites); 40 | 41 | context.subscriptions.push( 42 | vscode.window.registerTreeDataProvider("testingView-db2i", testSuitesTreeProvider), 43 | vscode.commands.registerCommand(`vscode-db2i.testing.specific`, (suiteName: string, testName: string) => { 44 | if (suiteName && testName) { 45 | const suite = suites.find(suite => suite.name === suiteName); 46 | 47 | if (suite) { 48 | const testCase = suite.tests.find(testCase => testCase.name === testName); 49 | 50 | if (testCase) { 51 | runTest(testCase); 52 | } 53 | } 54 | } 55 | }) 56 | ); 57 | 58 | const specificTests = env[`db2_specific`] === "true"; 59 | if (specificTests) 60 | return async () => {}; 61 | else 62 | return runTests; 63 | 64 | } 65 | } 66 | 67 | async function runTests() { 68 | for (const suite of suites) { 69 | console.log(`Running suite ${suite.name} (${suite.tests.length})`); 70 | console.log(); 71 | for (const test of suite.tests) { 72 | await runTest(test); 73 | } 74 | } 75 | } 76 | 77 | async function runTest(test: TestCase) { 78 | console.log(`\tRunning ${test.name}`); 79 | test.status = "running"; 80 | testSuitesTreeProvider.refresh(); 81 | 82 | try { 83 | await test.test(); 84 | test.status = "pass"; 85 | } 86 | 87 | catch (error: any){ 88 | console.log(error); 89 | test.status = "failed"; 90 | test.failure = error.message; 91 | } 92 | 93 | finally { 94 | testSuitesTreeProvider.refresh(); 95 | } 96 | } 97 | 98 | function resetTests(){ 99 | suites.flatMap(ts => ts.tests).forEach(tc => { 100 | tc.status = undefined; 101 | tc.failure = undefined; 102 | }); 103 | } -------------------------------------------------------------------------------- /src/testing/manager.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { TestSuite } from "."; 3 | import { JobManager } from "../config"; 4 | import { ServerComponent } from "../connection/serverComponent"; 5 | 6 | export const ManagerSuite: TestSuite = { 7 | name: `Job manager tests`, 8 | tests: [ 9 | {name: `Backend check`, test: async () => { 10 | const backendInstalled = await ServerComponent.initialise(); 11 | 12 | // To run these tests, we need the backend server. If this test fails. Don't bother 13 | assert.strictEqual(backendInstalled, true); 14 | await JobManager.endAll(); 15 | }}, 16 | 17 | {name: `Adding a job`, test: async () => { 18 | assert.strictEqual(ServerComponent.isInstalled(), true); 19 | 20 | // Ensure we have a blank manager first 21 | await JobManager.endAll(); 22 | assert.strictEqual(JobManager.getRunningJobs().length, 0); 23 | assert.strictEqual(JobManager.selectedJob, -1); 24 | 25 | // Add a new job 26 | await JobManager.newJob(); 27 | 28 | // Check the job exists 29 | assert.strictEqual(JobManager.getRunningJobs().length, 1); 30 | assert.strictEqual(JobManager.selectedJob, 0); 31 | 32 | // Check the job is really real 33 | const selected = JobManager.getSelection(); 34 | assert.notStrictEqual(selected, undefined); 35 | assert.notStrictEqual(selected.job.id, undefined); 36 | assert.strictEqual(selected.job.getStatus(), "ready"); 37 | 38 | // Close the job and see things go away 39 | JobManager.closeJob(JobManager.selectedJob); 40 | assert.strictEqual(JobManager.getRunningJobs().length, 0); 41 | assert.strictEqual(JobManager.selectedJob, -1); 42 | 43 | const badSelected = JobManager.getSelection(); 44 | assert.strictEqual(badSelected, undefined); 45 | }}, 46 | 47 | {name: `End all jobs`, test: async () => { 48 | assert.strictEqual(ServerComponent.isInstalled(), true); 49 | 50 | // Ensure we have a blank manager first 51 | await JobManager.endAll(); 52 | assert.strictEqual(JobManager.getRunningJobs().length, 0); 53 | assert.strictEqual(JobManager.selectedJob, -1); 54 | 55 | await JobManager.newJob(); 56 | await JobManager.newJob(); 57 | 58 | // Check the job exists 59 | assert.strictEqual(JobManager.getRunningJobs().length, 2); 60 | assert.strictEqual(JobManager.selectedJob, 1); 61 | 62 | // End the jobs 63 | await JobManager.endAll(); 64 | assert.strictEqual(JobManager.getRunningJobs().length, 0); 65 | assert.strictEqual(JobManager.selectedJob, -1); 66 | }}, 67 | 68 | {name: `Set selected by name`, test: async () => { 69 | assert.strictEqual(ServerComponent.isInstalled(), true); 70 | 71 | // Ensure we have a blank manager first 72 | await JobManager.endAll(); 73 | assert.strictEqual(JobManager.getRunningJobs().length, 0); 74 | assert.strictEqual(JobManager.selectedJob, -1); 75 | 76 | await JobManager.newJob(); 77 | await JobManager.newJob(); 78 | 79 | const runningJobs = JobManager.getRunningJobs(); 80 | 81 | // Check the job exists 82 | assert.strictEqual(runningJobs.length, 2); 83 | 84 | // Returns false due to bad name 85 | assert.strictEqual(JobManager.setSelection(`badName`), undefined); 86 | 87 | assert.notStrictEqual(JobManager.setSelection(runningJobs[0].name), undefined); 88 | 89 | assert.strictEqual(JobManager.getSelection().name, runningJobs[0].name); 90 | 91 | await JobManager.endAll(); 92 | }} 93 | ] 94 | } -------------------------------------------------------------------------------- /src/testing/selfCodes.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { TestSuite } from "."; 3 | import { ServerComponent } from "../connection/serverComponent"; 4 | import { testSelfCodes } from "../views/jobManager/selfCodes/selfCodesTest"; 5 | import { getInstance } from "../base"; 6 | 7 | export const SelfCodesTestSuite: TestSuite = { 8 | name: `Self Codes Tests`, 9 | tests: [ 10 | {name: `Backend check`, test: async () => { 11 | const backendInstalled = await ServerComponent.initialise(); 12 | 13 | // To run these tests, we need the backend server. If this test fails. Don't bother 14 | assert.strictEqual(backendInstalled, true); 15 | try { 16 | const selfTestSchema = [ 17 | `CREATE SCHEMA SELFTEST;`, 18 | ``, 19 | `CREATE OR REPLACE TABLE SELFTEST.MYTBL (C1 INT, C2 VARCHAR(100), C3 TIMESTAMP, C4 DATE);`, 20 | ``, 21 | `CREATE OR REPLACE TABLE SELFTEST.MYTBL2 (C1 INT, C2 VARCHAR(100), C3 TIMESTAMP, C4 DATE);`, 22 | ``, 23 | `INSERT INTO SELFTEST.MYTBL VALUES (0, 'ADAM', CURRENT TIMESTAMP, CURRENT DATE);`, 24 | ``, 25 | `INSERT INTO SELFTEST.MYTBL VALUES (1, 'LIAM', CURRENT TIMESTAMP + 1 SECOND, CURRENT DATE + 1 DAY);`, 26 | ``, 27 | `INSERT INTO SELFTEST.MYTBL VALUES (2, 'RYAN', CURRENT TIMESTAMP + 2 SECOND, CURRENT DATE + 2 DAY);`, 28 | ``, 29 | `INSERT INTO SELFTEST.MYTBL VALUES (3, NULL, CURRENT TIMESTAMP + 2 SECOND, CURRENT DATE + 2 DAY);`, 30 | ``, 31 | `INSERT INTO SELFTEST.MYTBL2 VALUES (0, 'TIM', CURRENT TIMESTAMP, CURRENT DATE);`, 32 | ``, 33 | `INSERT INTO SELFTEST.MYTBL2 VALUES (1, 'SCOTT', CURRENT TIMESTAMP + 1 SECOND, CURRENT DATE + 1 DAY);`, 34 | ``, 35 | `INSERT INTO SELFTEST.MYTBL2 VALUES (2, 'JESSIE', CURRENT TIMESTAMP + 2 SECOND, CURRENT DATE + 2 DAY);` 36 | ] 37 | await getInstance().getContent().runSQL(selfTestSchema.join(`\n`)); 38 | 39 | } catch (e) { 40 | console.log(`Possible fail`); 41 | } 42 | }}, 43 | ...testSelfCodes()] 44 | } -------------------------------------------------------------------------------- /src/testing/testCasesTree.ts: -------------------------------------------------------------------------------- 1 | import vscode from "vscode"; 2 | import { TestCase, TestSuite } from "."; 3 | 4 | export class TestSuitesTreeProvider implements vscode.TreeDataProvider{ 5 | private readonly emitter: vscode.EventEmitter = new vscode.EventEmitter(); 6 | readonly onDidChangeTreeData: vscode.Event = this.emitter.event; 7 | 8 | constructor(readonly testSuites: TestSuite[]) { 9 | 10 | } 11 | 12 | refresh(element?: TestSuiteItem) { 13 | this.emitter.fire(element); 14 | } 15 | 16 | getTreeItem(element: vscode.TreeItem): vscode.TreeItem | Thenable { 17 | return element; 18 | } 19 | 20 | getChildren(element?: TestSuiteItem): vscode.ProviderResult { 21 | if (element) { 22 | return element.getChilren(); 23 | } 24 | else { 25 | return this.testSuites.sort((ts1, ts2) => ts1.name.localeCompare(ts2.name)).map(ts => new TestSuiteItem(ts)); 26 | } 27 | } 28 | } 29 | 30 | class TestSuiteItem extends vscode.TreeItem { 31 | constructor(readonly testSuite: TestSuite) { 32 | super(testSuite.name, vscode.TreeItemCollapsibleState.Expanded); 33 | this.description = `${this.testSuite.tests.filter(tc => tc.status === "pass").length}/${this.testSuite.tests.length}`; 34 | 35 | let color; 36 | if (this.testSuite.tests.some(tc => tc.status === "failed")) { 37 | color = "testing.iconFailed"; 38 | } 39 | else if (this.testSuite.tests.some(tc => !tc.status)) { 40 | color = "testing.iconQueued"; 41 | } 42 | else { 43 | color = "testing.iconPassed"; 44 | } 45 | this.iconPath = new vscode.ThemeIcon("beaker", new vscode.ThemeColor(color)); 46 | } 47 | 48 | getChilren() { 49 | return this.testSuite.tests.map(tc => new TestCaseItem(this.label as string, tc)); 50 | } 51 | } 52 | 53 | class TestCaseItem extends vscode.TreeItem { 54 | constructor(suiteName: string, readonly testCase: TestCase) { 55 | super(testCase.name, vscode.TreeItemCollapsibleState.None); 56 | let icon; 57 | let color; 58 | switch (testCase.status) { 59 | case "running": 60 | color = "testing.runAction"; 61 | icon = "gear~spin"; 62 | break; 63 | case "failed": 64 | color = "testing.iconFailed"; 65 | icon = "close"; 66 | break; 67 | case "pass": 68 | color = "testing.iconPassed"; 69 | icon = "pass"; 70 | break; 71 | default: 72 | color = "testing.iconQueued"; 73 | icon = "watch"; 74 | } 75 | 76 | this.iconPath = new vscode.ThemeIcon(icon, new vscode.ThemeColor(color)); 77 | 78 | if (testCase.failure) 79 | this.tooltip = new vscode.MarkdownString(['```', testCase.failure, '```'].join(`\n`)); 80 | 81 | if (testCase.status !== `running`) { 82 | this.command = { 83 | command: `vscode-db2i.testing.specific`, 84 | arguments: [suiteName, testCase.name], 85 | title: `Re-run test` 86 | }; 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // https://www.ibm.com/docs/en/i/7.4?topic=views-syscolumns2 2 | export interface TableColumn { 3 | TABLE_SCHEMA: string, 4 | TABLE_NAME: string, 5 | COLUMN_NAME: string, 6 | SYSTEM_COLUMN_NAME: string, 7 | CONSTRAINT_NAME?: string, 8 | DATA_TYPE: string, 9 | CHARACTER_MAXIMUM_LENGTH?: number, 10 | NUMERIC_SCALE?: number, 11 | NUMERIC_PRECISION?: number, 12 | IS_NULLABLE: "Y" | "N", 13 | HAS_DEFAULT: "Y" | "N", 14 | COLUMN_DEFAULT?: string, 15 | COLUMN_TEXT: string, 16 | IS_IDENTITY: "YES" | "NO", 17 | } 18 | 19 | // https://www.ibm.com/docs/en/i/7.4?topic=views-sysparms 20 | export interface SQLParm { 21 | SPECIFIC_SCHEMA: string, 22 | SPECIFIC_NAME: string, 23 | PARAMETER_NAME: string, 24 | PARAMETER_MODE: "IN" | "OUT" | "INOUT", 25 | DATA_TYPE: string, 26 | CHARACTER_MAXIMUM_LENGTH?: number, 27 | NUMERIC_SCALE?: number, 28 | NUMERIC_PRECISION?: number, 29 | IS_NULLABLE: "YES" | "NO", 30 | DEFAULT?: string, 31 | LONG_COMMENT?: string, 32 | ORDINAL_POSITION: number, 33 | ROW_TYPE: "P" | "R", 34 | } 35 | 36 | export interface ResolvedSqlObject { 37 | schema: string; 38 | name: string; 39 | sqlType: string; 40 | } 41 | 42 | export interface BasicSQLObject { 43 | type: string; 44 | tableType: string; 45 | schema: string; 46 | name: string; 47 | specificName: string; 48 | text: string; 49 | system: { 50 | schema: string; 51 | name: string; 52 | } 53 | basedOn: { 54 | schema: string; 55 | name: string; 56 | } 57 | } 58 | 59 | export interface CPYFOptions { 60 | toLib: string; 61 | toFile: string; 62 | fromMbr: string; 63 | toMbr: string; 64 | mbrOpt: string; 65 | crtFile: string; 66 | outFmt: string 67 | } -------------------------------------------------------------------------------- /src/uriHandler.ts: -------------------------------------------------------------------------------- 1 | import { commands, env, Selection, Uri, UriHandler, window, workspace } from "vscode"; 2 | import querystring from "querystring"; 3 | import Document from "./language/sql/document"; 4 | import { ServerComponent } from "./connection/serverComponent"; 5 | import { remoteAssistIsEnabled } from "./language/providers/logic/available"; 6 | 7 | export class Db2iUriHandler implements UriHandler { 8 | handleUri(uri: Uri) { 9 | const path = uri.path; 10 | 11 | switch (path) { 12 | case '/sql': 13 | const queryData = querystring.parse(uri.query); 14 | const content = String(queryData.content).trim(); 15 | 16 | if (content) { 17 | const asLower = content.toLowerCase(); 18 | const isValid = asLower.startsWith(`select `) || asLower.startsWith(`with `); 19 | 20 | if (isValid) { 21 | const run = queryData.run === `true`; 22 | 23 | if (run && remoteAssistIsEnabled()) { 24 | commands.executeCommand(`vscode-db2i.runEditorStatement`, { 25 | content, 26 | qualifier: `statement`, 27 | open: true, 28 | }); 29 | } else { 30 | workspace.openTextDocument({ language: `sql`, content }).then(textDoc => { 31 | window.showTextDocument(textDoc); 32 | }); 33 | } 34 | } else { 35 | window.showErrorMessage(`Only SELECT or WITH statements are supported.`); 36 | } 37 | } 38 | 39 | break; 40 | } 41 | } 42 | } 43 | 44 | export const getStatementUri = commands.registerCommand(`vscode-db2i.getStatementUri`, async () => { 45 | const editor = window.activeTextEditor; 46 | 47 | if (editor) { 48 | const content = editor.document.getText(); 49 | 50 | const sqlDocument = new Document(content); 51 | const cursor = editor.document.offsetAt(editor.selection.active); 52 | 53 | const currentStmt = sqlDocument.getGroupByOffset(cursor); 54 | 55 | if (currentStmt) { 56 | const stmtContent = content.substring(currentStmt.range.start, currentStmt.range.end); 57 | editor.selection = new Selection(editor.document.positionAt(currentStmt.range.start), editor.document.positionAt(currentStmt.range.end)); 58 | const uri = `vscode://halcyontechltd.vscode-db2i/sql?content=${encodeURIComponent(stmtContent)}`; 59 | 60 | env.clipboard.writeText(uri); 61 | window.showInformationMessage(`Statement URI copied to clipboard.`); 62 | } 63 | } 64 | }); -------------------------------------------------------------------------------- /src/views/examples/contributes.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributes": { 3 | "views": { 4 | "db2-explorer": [ 5 | { 6 | "id": "exampleBrowser", 7 | "name": "Examples", 8 | "visibility": "collapsed", 9 | "when": "code-for-ibmi:connected == true" 10 | } 11 | ] 12 | }, 13 | "commands": [ 14 | { 15 | "command": "vscode-db2i.examples.setFilter", 16 | "title": "Set filter", 17 | "category": "Db2 for i (Examples)", 18 | "icon": "$(filter)" 19 | }, 20 | { 21 | "command": "vscode-db2i.examples.clearFilter", 22 | "title": "Clear filter", 23 | "category": "Db2 for i (Examples)", 24 | "icon": "$(clear-all)" 25 | } 26 | ], 27 | "menus": { 28 | "view/title": [ 29 | { 30 | "command": "vscode-db2i.examples.setFilter", 31 | "group": "navigation", 32 | "when": "view == exampleBrowser" 33 | }, 34 | { 35 | "command": "vscode-db2i.examples.clearFilter", 36 | "group": "navigation", 37 | "when": "view == exampleBrowser" 38 | } 39 | ] 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/views/examples/exampleBrowser.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventEmitter, ExtensionContext, MarkdownString, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, Uri, commands, window, workspace } from "vscode"; 2 | import { Examples, SQLExample, ServiceInfoLabel } from "."; 3 | import { getServiceInfo } from "../../database/serviceInfo"; 4 | import { notebookFromStatements } from "../../notebooks/logic/openAsNotebook"; 5 | import { osDetail } from "../../config"; 6 | 7 | export const openExampleCommand = `vscode-db2i.examples.open`; 8 | 9 | export class ExampleBrowser implements TreeDataProvider { 10 | private _onDidChangeTreeData: EventEmitter = new EventEmitter(); 11 | readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; 12 | 13 | private currentFilter: string | undefined; 14 | 15 | constructor(context: ExtensionContext) { 16 | context.subscriptions.push( 17 | commands.registerCommand(openExampleCommand, (example: SQLExample) => { 18 | if (example) { 19 | if (example.isNotebook) { 20 | notebookFromStatements(example.content); 21 | } else { 22 | workspace.openTextDocument({ 23 | content: example.content.join(`\n`), 24 | language: `sql` 25 | }).then(doc => { 26 | window.showTextDocument(doc); 27 | }); 28 | } 29 | } 30 | }), 31 | 32 | commands.registerCommand(`vscode-db2i.examples.setFilter`, async () => { 33 | this.currentFilter = await window.showInputBox({ 34 | title: `Example Filter`, 35 | prompt: `Enter filter criteria`, 36 | value: this.currentFilter, 37 | }); 38 | 39 | this.refresh(); 40 | }), 41 | 42 | commands.registerCommand(`vscode-db2i.examples.clearFilter`, async () => { 43 | this.currentFilter = undefined; 44 | this.refresh(); 45 | }), 46 | 47 | commands.registerCommand("vscode-db2i.examples.reload", () => { 48 | delete Examples[ServiceInfoLabel]; 49 | this.refresh(); 50 | }) 51 | ); 52 | } 53 | 54 | refresh() { 55 | this._onDidChangeTreeData.fire(); 56 | } 57 | 58 | getTreeItem(element: any): TreeItem | Thenable { 59 | return element; 60 | } 61 | 62 | async getChildren(element?: ExampleGroupItem): Promise { 63 | if (element) { 64 | return element.getChildren(); 65 | } 66 | else { 67 | // Unlike the bulk of the examples which are defined in views/examples/index.ts, the services examples are retrieved dynamically 68 | if (!Examples[ServiceInfoLabel]) { 69 | Examples[ServiceInfoLabel] = await getServiceInfo(); 70 | } 71 | 72 | if (this.currentFilter) { 73 | // If there is a filter, then show all examples that include this criteria 74 | const upperFilter = this.currentFilter.toUpperCase(); 75 | return Object.values(Examples) 76 | .flatMap(examples => examples.filter(exampleWorksForOnOS)) 77 | .filter(example => example.name.toUpperCase().includes(upperFilter) || example.content.some(line => line.toUpperCase().includes(upperFilter))) 78 | .sort(sort) 79 | .map(example => new SQLExampleItem(example)); 80 | } 81 | else { 82 | return Object.entries(Examples) 83 | .sort(([name1], [name2]) => sort(name1, name2)) 84 | .map(([name, examples]) => new ExampleGroupItem(name, examples)); 85 | } 86 | } 87 | } 88 | } 89 | 90 | class ExampleGroupItem extends TreeItem { 91 | constructor(name: string, private group: SQLExample[]) { 92 | super(name, TreeItemCollapsibleState.Collapsed); 93 | this.iconPath = ThemeIcon.Folder; 94 | } 95 | 96 | getChildren(): SQLExampleItem[] { 97 | return this.group 98 | .filter(example => exampleWorksForOnOS(example)) 99 | .sort(sort) 100 | .map(example => new SQLExampleItem(example)); 101 | } 102 | } 103 | 104 | class SQLExampleItem extends TreeItem { 105 | constructor(example: SQLExample) { 106 | super(example.name, TreeItemCollapsibleState.None); 107 | this.iconPath = ThemeIcon.File; 108 | this.resourceUri = Uri.parse('_.sql'); 109 | this.tooltip = new MarkdownString(['```sql', example.content.join(`\n`), '```'].join(`\n`)); 110 | 111 | this.command = { 112 | command: openExampleCommand, 113 | title: `Open example`, 114 | arguments: [example] 115 | }; 116 | } 117 | } 118 | 119 | function exampleWorksForOnOS(example: SQLExample): boolean { 120 | 121 | if (osDetail) { 122 | const myOsVersion = osDetail.getVersion(); 123 | 124 | // If this example has specific system requirements defined 125 | if (example.requirements && 126 | example.requirements[myOsVersion] && 127 | osDetail.getDb2Level() < example.requirements[myOsVersion]) { 128 | return false; 129 | } 130 | } 131 | 132 | return true; 133 | } 134 | 135 | function sort(string1: string | SQLExample, string2: string | SQLExample) { 136 | string1 = typeof string1 === "string" ? string1 : string1.name; 137 | string2 = typeof string2 === "string" ? string2 : string2.name; 138 | return string1.localeCompare(string2); 139 | } -------------------------------------------------------------------------------- /src/views/html.ts: -------------------------------------------------------------------------------- 1 | 2 | export function getHeader(options: {withCollapsed?: boolean} = {}): string { 3 | return /*html*/` 4 | 5 | 6 | 159 | `; 160 | } 161 | 162 | export const escapeHTML = str => str.replace(/[&<>'"]/g, 163 | tag => ({ 164 | '&': '&', 165 | '<': '<', 166 | '>': '>', 167 | "'": ''', 168 | '"': '"' 169 | }[tag])); -------------------------------------------------------------------------------- /src/views/jobManager/ConfigManager.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, ThemeIcon, TreeItem, TreeItemCollapsibleState, commands, window } from "vscode"; 2 | import Configuration from "../../configuration"; 3 | import { SQLJobItem } from "./jobManagerView"; 4 | import { JobManager } from "../../config"; 5 | import { editJobUi } from "./editJob"; 6 | import { JDBCOptions } from "@ibm/mapepire-js/dist/src/types"; 7 | 8 | interface JobConfigs { 9 | [name: string]: JDBCOptions 10 | }; 11 | 12 | export class ConfigManager { 13 | static refresh() { 14 | commands.executeCommand(`vscode-db2i.jobManager.refresh`); 15 | } 16 | 17 | static initialiseSaveCommands(): Disposable[] { 18 | return [ 19 | commands.registerCommand(`vscode-db2i.jobManager.newConfig`, async (jobNode: SQLJobItem) => { 20 | if (jobNode && jobNode.label) { 21 | const id = jobNode.label as string; 22 | let selected = JobManager.getJob(id); 23 | 24 | if (selected) { 25 | window.showInputBox({ 26 | value: selected.name, 27 | title: `New Config`, 28 | prompt: `New name for config` 29 | }).then(async newName => { 30 | if (newName) { 31 | await this.storeConfig(newName, selected.job.options); 32 | selected.name = newName; 33 | this.refresh(); 34 | } 35 | }) 36 | } 37 | } 38 | }), 39 | 40 | commands.registerCommand(`vscode-db2i.jobManager.startJobFromConfig`, async (name: string) => { 41 | const options = this.getConfig(name); 42 | 43 | if (options) { 44 | commands.executeCommand(`vscode-db2i.jobManager.newJob`, options, name); 45 | } 46 | }), 47 | 48 | commands.registerCommand(`vscode-db2i.jobManager.editConfig`, (configNode: SavedConfig) => { 49 | if (configNode && configNode.name) { 50 | const options = this.getConfig(configNode.name); 51 | 52 | if (options) { 53 | editJobUi(options, configNode.name).then(newOptions => { 54 | if (newOptions) { 55 | this.storeConfig(configNode.name, options); 56 | } 57 | }) 58 | } 59 | } 60 | }), 61 | 62 | commands.registerCommand(`vscode-db2i.jobManager.deleteConfig`, async (configNode: SavedConfig) => { 63 | if (configNode && configNode.name) { 64 | await this.deleteConfig(configNode.name); 65 | this.refresh(); 66 | } 67 | }), 68 | 69 | // Currently not accessible from the UI 70 | commands.registerCommand(`vscode-db2i.jobManager.setAsStartupConfig`, async (configNode: SavedConfig) => { 71 | if (configNode && configNode.name) { 72 | window.showWarningMessage(`This will attempt to startup ${configNode.name} automatically when you connect to any system. Do you want to continue?`, `Absolutely`, `Cancel`).then(chosen => { 73 | if (chosen === `Absolutely`) { 74 | Configuration.set(`alwaysStartSQLJob`, configNode.name); 75 | } 76 | }) 77 | } 78 | }), 79 | ] 80 | } 81 | 82 | static hasSavedItems() { 83 | const saved = ConfigManager.getSavedConfigs(); 84 | const names = Object.keys(saved); 85 | return names.length > 0; 86 | } 87 | 88 | static getConfigTreeItems(): SavedConfig[] { 89 | // TODO: add profiles in here? 90 | 91 | const saved = ConfigManager.getSavedConfigs(); 92 | const names = Object.keys(saved); 93 | return names.map(name => new SavedConfig(name)); 94 | } 95 | 96 | private static getSavedConfigs() { 97 | return Configuration.get(`jobConfigs`); 98 | } 99 | 100 | static getConfig(name: string): JDBCOptions | undefined { 101 | const configs = this.getSavedConfigs(); // Returns a proxy 102 | return Object.assign({}, configs[name]); 103 | } 104 | 105 | private static storeConfig(name: string, options: JDBCOptions) { 106 | let configs: JobConfigs = { 107 | ...ConfigManager.getSavedConfigs(), 108 | [name]: options 109 | }; 110 | 111 | return Configuration.set(`jobConfigs`, configs); 112 | } 113 | 114 | private static deleteConfig(name: string) { 115 | let configs = ConfigManager.getSavedConfigs(); 116 | 117 | configs[name] = undefined; 118 | 119 | return Configuration.set(`jobConfigs`, configs); 120 | }; 121 | } 122 | 123 | export class ConfigGroup extends TreeItem { 124 | static contextValue = `configGroup`; 125 | constructor() { 126 | super(`Saved Configuration`, TreeItemCollapsibleState.Expanded); 127 | 128 | this.iconPath = new ThemeIcon(`gear`); 129 | this.contextValue = ConfigGroup.contextValue; 130 | } 131 | } 132 | 133 | class SavedConfig extends TreeItem { 134 | constructor(public name: string) { 135 | super(name, TreeItemCollapsibleState.None); 136 | 137 | this.iconPath = new ThemeIcon(`add`); 138 | this.tooltip = `Click to start` 139 | this.command = { 140 | command: `vscode-db2i.jobManager.startJobFromConfig`, 141 | title: `Start Job from config`, 142 | arguments: [name] 143 | }; 144 | 145 | this.contextValue = `savedConfig`; 146 | } 147 | } -------------------------------------------------------------------------------- /src/views/jobManager/editJob/index.ts: -------------------------------------------------------------------------------- 1 | import { getBase } from "../../../base"; 2 | import { JDBCOptions } from "@ibm/mapepire-js/dist/src/types"; 3 | import { ComplexTab } from "@halcyontech/vscode-ibmi-types/webviews/CustomUI"; 4 | 5 | import getPerfTab from "./perfTab"; 6 | import getFormatTab from "./formatTab"; 7 | import getSystemTab from "./systemTab"; 8 | import getSortTab from "./sortTab"; 9 | import getOtherTab from "./otherTab"; 10 | 11 | export function formatDescription(text: string): string { 12 | return text 13 | .trim() // Remove leading and trailing whitespace 14 | .replace(/\n/g, "
") // Replace line breaks with
tags 15 | .replace(/\s/g, " "); // Replace spaces with non-breaking spaces 16 | } 17 | 18 | export async function editJobUi( 19 | options: JDBCOptions, 20 | jobName?: string 21 | ): Promise { 22 | const base = getBase(); 23 | const ui = base.customUI(); 24 | 25 | const syspropsTab = getSystemTab(options); 26 | const otherpropsTab = getOtherTab(options); 27 | const formatpropsTab = getFormatTab(options); 28 | const performancepropsTab = getPerfTab(options); 29 | const sortPropsTab = getSortTab(options); 30 | 31 | let tabs: ComplexTab[] = [ 32 | { label: `System`, fields: syspropsTab.fields }, 33 | { label: `Format`, fields: formatpropsTab.fields }, 34 | { label: `Performance`, fields: performancepropsTab.fields }, 35 | { label: `Sort`, fields: sortPropsTab.fields }, 36 | { label: `Other`, fields: otherpropsTab.fields }, 37 | ]; 38 | 39 | ui.addComplexTabs(tabs).addButtons( 40 | { id: `apply`, label: `Apply changes` }, 41 | { id: `cancel`, label: `Cancel` } 42 | ); 43 | 44 | const page = await ui.loadPage<{ [key: string]: string }>( 45 | `Edit ${jobName} options` 46 | ); 47 | 48 | if (page && page.data) { 49 | page.panel.dispose(); 50 | 51 | if (page.data.buttons === `apply`) { 52 | const keys = Object.keys(page.data); 53 | 54 | // We need to play with some of the form data to put it back into JDBCOptions 55 | for (const key of keys) { 56 | switch (key) { 57 | case `libraries`: 58 | options.libraries = page.data[key].split(`,`).map((v) => v.trim()); 59 | break; 60 | case `buttons`: 61 | // Do nothing with buttons 62 | break; 63 | 64 | default: 65 | // Handle of true/false values back into boolean types 66 | switch (page.data[key]) { 67 | case `true`: options[key] = true; break; 68 | case `false`: options[key] = false; break; 69 | default: options[key] = page.data[key]; break; 70 | } 71 | } 72 | } 73 | 74 | return options; 75 | } 76 | } 77 | 78 | return; 79 | } 80 | -------------------------------------------------------------------------------- /src/views/jobManager/editJob/sortTab.ts: -------------------------------------------------------------------------------- 1 | import { getBase } from "../../../base"; 2 | import { JDBCOptions } from "@ibm/mapepire-js/dist/src/types"; 3 | 4 | export default function getSortTab(options: JDBCOptions) { 5 | const base = getBase(); 6 | const tab = base.customUI(); 7 | 8 | tab 9 | .addParagraph( 10 | `Sort properties specify how the system performs stores and performs sorts.` 11 | ) 12 | .addSelect( 13 | `sort`, 14 | `Sort`, 15 | [ 16 | { 17 | value: `hex`, 18 | text: `Sort using hex values`, 19 | description: `Hex`, 20 | selected: options["sort"] === `hex`, 21 | }, 22 | { 23 | value: `language`, 24 | text: `Sort using language values`, 25 | description: `Language`, 26 | selected: options["sort"] === `language`, 27 | }, 28 | { 29 | value: `table`, 30 | text: `base the sort on the sort sequence table set in the [sort table] property`, 31 | description: `Table`, 32 | selected: options["sort"] === `table`, 33 | }, 34 | ], 35 | `Specifies how the system sorts records before sending them to the client.` 36 | ) 37 | .addInput( 38 | `sort language`, 39 | `Sort language`, 40 | `Specifies a 3-character language id to use for selection of a sort sequence. This property has no effect unless the "sort" property is set to "language".`, 41 | { default: options["sort language"] || `ENU` } 42 | ) 43 | .addSelect(`sort weight`, `Sort weight`, [ 44 | { 45 | value: `shared`, 46 | text: `uppercase and lowercase characters sort as the same character`, 47 | description: `Shared`, 48 | selected: options["sort weight"] === `shared`, 49 | }, 50 | { 51 | value: `unique`, 52 | text: `uppercase and lowercase characters sort as different characters`, 53 | description: `Unique`, 54 | selected: options["sort weight"] === `unique`, 55 | }, 56 | ]) 57 | .addInput( 58 | `sort table`, 59 | `Sort table`, 60 | `Specifies the library and file name of a sort sequence table stored on the system. This property has no effect unless the "sort" property is set to "table"`, 61 | { default: options["sort table"] || `` } 62 | ); 63 | 64 | return tab; 65 | } -------------------------------------------------------------------------------- /src/views/jobManager/jobLog.ts: -------------------------------------------------------------------------------- 1 | import { ViewColumn, window } from "vscode"; 2 | import { JobInfo } from "../../connection/manager"; 3 | import { escapeHTML, getHeader } from "../html"; 4 | import { JobLogEntry } from "../../connection/types"; 5 | import { JobManager } from "../../config"; 6 | 7 | 8 | export async function displayJobLog(selected: JobInfo) { 9 | const jobLog = await selected.job.getJobLog(); 10 | 11 | if (jobLog.data.length > 0) { 12 | const panel = window.createWebviewPanel(`tab`, selected.job.id, {viewColumn: ViewColumn.Active}, {enableScripts: true}); 13 | panel.webview.html = generatePage(jobLog.data); 14 | panel.reveal(); 15 | } else { 16 | window.showInformationMessage(`No messages in job log for ${selected.job.id}`); 17 | } 18 | } 19 | 20 | function generatePage(rows: JobLogEntry[]) { 21 | return /*html*/ ` 22 | 23 | 24 | 25 | ${getHeader()} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ${rows.map(row => { 41 | return ` 42 | 45 | 48 | 51 | 54 | 57 | 60 | ` 61 | }).join(``)} 62 | 63 |
SentTypeSeverityMessage IDMessageSecond Level Text
43 | ${row.MESSAGE_TIMESTAMP} 44 | 46 | ${row.MESSAGE_TYPE} 47 | 49 | ${row.SEVERITY} 50 | 52 | ${row.MESSAGE_ID} 53 | 55 | ${escapeHTML(row.MESSAGE_TEXT || ``)} 56 | 58 | ${escapeHTML(row.MESSAGE_SECOND_LEVEL_TEXT || ``)} 59 |
64 | 65 | 66 | `; 67 | } 68 | -------------------------------------------------------------------------------- /src/views/jobManager/selfCodes/contributes.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributes": { 3 | "configuration": [ 4 | { 5 | "id": "vscode-db2i.self", 6 | "title": "SQL Error Logging Facility (SELF)", 7 | "properties": { 8 | "vscode-db2i.jobSelfViewAutoRefresh": { 9 | "type": "boolean", 10 | "title": "Auto-refresh SELF Codes view", 11 | "description": "Enable auto-refresh for SELF Codes view when connecting to a system", 12 | "default": false 13 | } 14 | } 15 | } 16 | ], 17 | "views": { 18 | "ibmi-panel": [ 19 | { 20 | "type": "tree", 21 | "id": "vscode-db2i.self.nodes", 22 | "name": "SQL Error Logging Facility (SELF)", 23 | "when": "code-for-ibmi:connected && vscode-db2i:SELFSupported && vscode-db2i:jobManager == true", 24 | "visibility": "collapsed" 25 | } 26 | ] 27 | }, 28 | "viewsWelcome": [ 29 | { 30 | "view": "vscode-db2i.self.nodes", 31 | "contents": "🛠️ SELF Codes will appear here. You can set the SELF log level on specific jobs, or you can set the default for new jobs in the User Settings.\n\n[Set Default for New Jobs](command:vscode-db2i.jobManager.defaultSettings)\n\n[Learn about SELF](command:vscode-db2i.self.help)" 32 | } 33 | ], 34 | "commands": [ 35 | { 36 | "command": "vscode-db2i.self.refresh", 37 | "title": "Refresh SELF Codes View", 38 | "category": "Db2 for i", 39 | "icon": "$(refresh)" 40 | }, 41 | { 42 | "command": "vscode-db2i.self.enableAutoRefresh", 43 | "title": "Enable Auto Refresh", 44 | "category": "Db2 for i", 45 | "icon": "$(sync)" 46 | }, 47 | { 48 | "command": "vscode-db2i.self.disableAutoRefresh", 49 | "title": "Disable Auto Refresh", 50 | "category": "Db2 for i", 51 | "icon": "$(sync-ignored)" 52 | }, 53 | { 54 | "command": "vscode-db2i.self.enableSelectedJobOnly", 55 | "title": "Show Selected Job Only", 56 | "category": "Db2 for i", 57 | "icon": "$(filter)" 58 | }, 59 | { 60 | "command": "vscode-db2i.self.disableSelectedJobOnly", 61 | "title": "Show errors for user (all jobs)", 62 | "category": "Db2 for i", 63 | "icon": "$(account)" 64 | }, 65 | { 66 | "command": "vscode-db2i.self.reset", 67 | "title": "Reset SELF Codes View", 68 | "category": "Db2 for i", 69 | "icon": "$(trash)" 70 | }, 71 | { 72 | "command": "vscode-db2i.self.copySqlStatement", 73 | "title": "Copy SQL statement", 74 | "category": "Db2 for i", 75 | "icon": "$(pencil)" 76 | }, 77 | { 78 | "command": "vscode-db2i.self.displayDetails", 79 | "title": "Display SELF Code Details", 80 | "category": "Db2 for i", 81 | "icon": "$(info)" 82 | }, 83 | { 84 | "command": "vscode-db2i.self.explainSelf", 85 | "title": "Explain SELF Code with continue", 86 | "category": "Db2 for i", 87 | "icon": "$(debug-alt)", 88 | "enablement": "vscode-db2i:continueExtensionActive" 89 | }, 90 | { 91 | "command": "vscode-db2i.self.help", 92 | "title": "Open SELF Documentation", 93 | "category": "Db2 for i", 94 | "icon": "$(question)" 95 | } 96 | ], 97 | "menus": { 98 | "view/title": [ 99 | { 100 | "command": "vscode-db2i.self.refresh", 101 | "group": "navigation@1", 102 | "when": "view == vscode-db2i.self.nodes && vscode-db2i:jobManager.hasJob" 103 | }, 104 | { 105 | "command": "vscode-db2i.self.enableAutoRefresh", 106 | "group": "navigation@2", 107 | "when": "view == vscode-db2i.self.nodes && vscode-db2i.self.autoRefresh == false" 108 | }, 109 | { 110 | "command": "vscode-db2i.self.disableAutoRefresh", 111 | "group": "navigation@2", 112 | "when": "view == vscode-db2i.self.nodes && vscode-db2i.self.autoRefresh == true" 113 | }, 114 | { 115 | "command": "vscode-db2i.self.enableSelectedJobOnly", 116 | "group": "navigation@2", 117 | "when": "view == vscode-db2i.self.nodes && vscode-db2i.self.specificJob == false" 118 | }, 119 | { 120 | "command": "vscode-db2i.self.disableSelectedJobOnly", 121 | "group": "navigation@2", 122 | "when": "view == vscode-db2i.self.nodes && vscode-db2i.self.specificJob == true" 123 | }, 124 | { 125 | "command": "vscode-db2i.self.reset", 126 | "group": "navigation@3", 127 | "when": "view == vscode-db2i.self.nodes && vscode-db2i:jobManager.hasJob" 128 | }, 129 | { 130 | "command": "vscode-db2i.self.help", 131 | "group": "navigation@4", 132 | "when": "view == vscode-db2i.self.nodes" 133 | } 134 | ], 135 | "view/item/context": [ 136 | { 137 | "command": "vscode-db2i.self.copySqlStatement", 138 | "when": "view == vscode-db2i.self.nodes && viewItem == selfCodeNode", 139 | "group": "navigation" 140 | }, 141 | { 142 | "command": "vscode-db2i.self.displayDetails", 143 | "when": "view == vscode-db2i.self.nodes && viewItem == selfCodeNode", 144 | "group": "navigation" 145 | }, 146 | { 147 | "command": "vscode-db2i.self.explainSelf", 148 | "when": "view == vscode-db2i.self.nodes && viewItem == selfCodeNode && vscode-db2i:continueExtensionActive", 149 | "group": "navigation" 150 | } 151 | ] 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /src/views/jobManager/selfCodes/nodes.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface SelfIleStackFrame { 3 | ORD: number; 4 | TYPE: string; 5 | LIB: string; 6 | PGM: string; 7 | MODULE: string; 8 | PROC: string; 9 | STMT: string; 10 | ACTGRP: string; 11 | } 12 | 13 | export interface InitialStackData { 14 | initial_stack: SelfIleStackFrame[]; 15 | } 16 | 17 | export interface SelfCodeNode { 18 | JOB_NAME: string, 19 | USER_NAME: string; 20 | REASON_CODE: string; 21 | LOGGED_TIME: string; 22 | LOGGED_SQLSTATE: string; 23 | LOGGED_SQLCODE: number; 24 | MATCHES: number; 25 | STMTTEXT: string; 26 | MESSAGE_TEXT: string; 27 | MESSAGE_SECOND_LEVEL_TEXT: string; 28 | 29 | PROGRAM_LIBRARY: string; 30 | PROGRAM_NAME: string; 31 | PROGRAM_TYPE: "*PGM"|"*SRVPGM"; 32 | MODULE_NAME: string; 33 | CLIENT_APPLNAME: string 34 | CLIENT_PROGRAMID: string; 35 | INITIAL_STACK: InitialStackData; 36 | } 37 | 38 | export type SelfValue = "*ALL" | "*ERROR" | "*WARNING" | "*NONE"; 39 | 40 | export interface SelfCodeObject { 41 | code: SelfValue; 42 | message: string; 43 | } 44 | 45 | export const selfCodesMap: SelfCodeObject[] = [ 46 | {code: `*ALL`, message: undefined}, 47 | {code: `*ERROR`, message: undefined}, 48 | {code: `*WARNING`, message: undefined}, 49 | {code: `*NONE`, message: undefined} 50 | ]; -------------------------------------------------------------------------------- /src/views/jobManager/selfCodes/selfCodesBrowser.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { JobManager } from '../../../config'; 3 | import { SelfCodeObject, SelfValue } from './nodes'; 4 | 5 | export class SelfCodesQuickPickItem implements vscode.QuickPickItem { 6 | label: SelfValue; 7 | description?: string; 8 | detail?: string; 9 | 10 | constructor(object: SelfCodeObject) { 11 | this.label = object.code; 12 | this.description = object.message; 13 | } 14 | } -------------------------------------------------------------------------------- /src/views/jobManager/selfCodes/selfCodesTest.ts: -------------------------------------------------------------------------------- 1 | import { OldSQLJob } from "../../../connection/sqlJob"; 2 | import { TestCase } from "../../../testing"; 3 | import assert from "assert"; 4 | import { SelfValue } from "./nodes"; 5 | 6 | export const selfCodeTests = [ 7 | { 8 | name: "Trigger an error", 9 | code: "*ERROR", 10 | sql: "SELECT * from qsys2.noexist", 11 | }, 12 | { 13 | name: "Tigger a warning", 14 | code: "*WARNING", 15 | sql: "SELECT LEFT(C2, 1001) FROM SELFTEST.MYTBL" 16 | } 17 | ]; 18 | 19 | export function testSelfCodes(): TestCase[] { 20 | let tests: TestCase[] = []; 21 | const content = `SELECT job_name, matches FROM qsys2.sql_error_log where job_name = ?`; 22 | let before: string; 23 | let after: string; 24 | for (const test of selfCodeTests) { 25 | const testCase: TestCase = { 26 | name: `Self code Error for test ${test.name}`, 27 | test: async () => { 28 | let newJob = new OldSQLJob(); 29 | await newJob.connect(); 30 | await newJob.setSelfState(test.code as SelfValue); 31 | try { 32 | await newJob.query(test.sql).execute(); 33 | } catch (e) {} 34 | let result = await newJob.query(content, {parameters: [newJob.id]}).execute(); 35 | assert(result.data[0]['MATCHES'] >= 1); 36 | 37 | newJob.close(); 38 | }, 39 | }; 40 | tests.push(testCase); 41 | } 42 | return tests; 43 | } 44 | -------------------------------------------------------------------------------- /src/views/jobManager/statusBar.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownString, StatusBarAlignment, ThemeColor, languages, window } from "vscode"; 2 | import { ServerComponent } from "../../connection/serverComponent"; 3 | import { JobManager } from "../../config"; 4 | import { getInstance } from "../../base"; 5 | import Statement from "../../database/statement"; 6 | 7 | const item = window.createStatusBarItem(`sqlJob`, StatusBarAlignment.Left); 8 | 9 | export async function updateStatusBar(options: {newJob?: boolean, canceling?: boolean, jobIsBusy?: boolean, executing?: boolean} = {}) { 10 | const instance = getInstance(); 11 | const connection = instance.getConnection(); 12 | 13 | if (connection && ServerComponent.isInstalled()) { 14 | const selected = JobManager.getSelection(); 15 | 16 | let text; 17 | let backgroundColour: ThemeColor|undefined = undefined; 18 | let toolTipItems: string[] = []; 19 | 20 | if (options.executing) { 21 | text = `$(sync~spin) Executing...`; 22 | } else 23 | if (options.canceling) { 24 | text = `$(sync~spin) Canceling...`; 25 | } else 26 | if (options.jobIsBusy) { 27 | text = `🙁 Job is busy`; 28 | } else 29 | if (options.newJob) { 30 | text = `$(sync~spin) Spinning up job...`; 31 | } else 32 | if (selected) { 33 | text = `$(database) ${selected.name}`; 34 | 35 | const job = selected.job; 36 | 37 | if (job.getNaming() === `sql`) { 38 | toolTipItems.push(`SQL Naming.\n\nCurrent schema: \`${Statement.delimName(await job.getCurrentSchema())}\``); 39 | } else { 40 | toolTipItems.push([ 41 | `System Naming.`, 42 | ``, 43 | `Configured user library list for job:`, 44 | ``, 45 | ...job.options.libraries.map((lib, i) => `${i+1}. \`${lib}\``) 46 | ].join(`\n`)); 47 | } 48 | 49 | if (selected.job.underCommitControl()) { 50 | const pendingsTracts = await selected.job.getPendingTransactions(); 51 | if (pendingsTracts > 0) { 52 | backgroundColour = new ThemeColor('statusBarItem.warningBackground'); 53 | text = `$(pencil) ${selected.name}`; 54 | 55 | toolTipItems.push( 56 | `$(warning) Pending Transaction`, 57 | `[$(save) Commit](command:vscode-db2i.jobManager.jobCommit) / [$(discard) Rollback](command:vscode-db2i.jobManager.jobRollback)` 58 | ); 59 | } 60 | } 61 | 62 | toolTipItems.push(`[$(info) View Job Log](command:vscode-db2i.jobManager.viewJobLog)`); 63 | toolTipItems.push(`[$(edit) Edit Connection Settings](command:vscode-db2i.jobManager.editJobProps)`); 64 | toolTipItems.push(`[$(bracket-error) Edit SELF codes](command:vscode-db2i.jobManager.editSelfCodes)`); 65 | } else { 66 | text = `$(database) No job active`; 67 | toolTipItems.push(`[Start Job](command:vscode-db2i.jobManager.newJob)`); 68 | } 69 | 70 | if (toolTipItems.length > 0) { 71 | const toolTip = new MarkdownString(toolTipItems.join(`\n\n---\n\n`), true); 72 | toolTip.isTrusted = true; 73 | item.tooltip = toolTip; 74 | } 75 | 76 | item.text = text; 77 | item.backgroundColor = backgroundColour; 78 | 79 | item.show(); 80 | } else { 81 | item.hide(); 82 | } 83 | } -------------------------------------------------------------------------------- /src/views/queryHistoryView/contributes.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributes": { 3 | "views": { 4 | "db2-explorer": [ 5 | { 6 | "id": "queryHistory", 7 | "name": "Statement History", 8 | "visibility": "visible", 9 | "when": "code-for-ibmi:connected == true && vscode-db2i:jobManager.hasJob" 10 | } 11 | ] 12 | }, 13 | "viewsWelcome": [ 14 | { 15 | "view": "queryHistory", 16 | "contents": "Statement history will appear here." 17 | } 18 | ], 19 | "commands": [ 20 | { 21 | "command": "vscode-db2i.queryHistory.remove", 22 | "title": "Remove statement from history", 23 | "category": "Db2 for i", 24 | "icon": "$(trash)" 25 | }, 26 | { 27 | "command": "vscode-db2i.queryHistory.clear", 28 | "title": "Clear statement history", 29 | "category": "Db2 for i", 30 | "icon": "$(trash)" 31 | }, 32 | { 33 | "command": "vscode-db2i.queryHistory.find", 34 | "title": "Search query history", 35 | "category": "Db2 for i", 36 | "icon": "$(search)" 37 | }, 38 | { 39 | "command": "vscode-db2i.queryHistory.toggleStar", 40 | "title": "Star statement", 41 | "category": "Db2 for i", 42 | "icon": "$(star)" 43 | } 44 | ], 45 | "menus": { 46 | "commandPalette": [ 47 | { 48 | "command": "vscode-db2i.queryHistory.remove", 49 | "when": "never" 50 | }, 51 | { 52 | "command": "vscode-db2i.queryHistory.find", 53 | "when": "never" 54 | }, 55 | { 56 | "command": "vscode-db2i.queryHistory.toggleStar", 57 | "when": "never" 58 | } 59 | ], 60 | "view/title": [ 61 | { 62 | "command": "vscode-db2i.queryHistory.clear", 63 | "group": "navigation", 64 | "when": "view == queryHistory" 65 | }, 66 | { 67 | "command": "vscode-db2i.queryHistory.find", 68 | "group": "navigation", 69 | "when": "view == queryHistory" 70 | } 71 | ], 72 | "view/item/context": [ 73 | { 74 | "command": "vscode-db2i.queryHistory.remove", 75 | "when": "view == queryHistory && viewItem == query", 76 | "group": "inline" 77 | }, 78 | { 79 | "command": "vscode-db2i.queryHistory.toggleStar", 80 | "when": "view == queryHistory && viewItem == query", 81 | "group": "inline" 82 | } 83 | ] 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/views/results/codegen.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect, test } from 'vitest' 2 | import { columnToRpgDefinition, columnToRpgFieldName, queryResultToRpgDs } from './codegen'; 3 | import { QueryResult } from '@ibm/mapepire-js'; 4 | 5 | test('Column to RPG symbol', () => { 6 | let name; 7 | 8 | name = columnToRpgFieldName({display_size: 0, label: 'änderungs- benutzer ', name: 'ANDBEN', type: 'CHAR', precision: 10, scale: 0}, 'Label'); 9 | expect(name).toBe('anderungs_benutzer'); 10 | 11 | name = columnToRpgFieldName({display_size: 0, label: 'änderungs- benutzer ', name: 'ANDBEN', type: 'CHAR', precision: 10, scale: 0}, 'Name'); 12 | expect(name).toBe('andben'); 13 | 14 | name = columnToRpgFieldName({display_size: 0, label: 'Cust.number....:', name: 'CUSNUM', type: 'CHAR', precision: 10, scale: 0}, 'Label'); 15 | expect(name).toBe('cust_number'); 16 | 17 | name = columnToRpgFieldName({display_size: 0, label: 'Cust. name.... : ', name: 'CUSNAM', type: 'CHAR', precision: 10, scale: 0}, 'Label'); 18 | expect(name).toBe('cust_name'); 19 | 20 | name = columnToRpgFieldName({display_size: 0, label: 'Country:', name: 'C1', type: 'CHAR', precision: 10, scale: 0}, 'Label'); 21 | expect(name).toBe('country'); 22 | 23 | name = columnToRpgFieldName({display_size: 0, label: 'På bærtur', name: 'PB1', type: 'CHAR', precision: 10, scale: 0}, 'Label'); 24 | expect(name).toBe('paa_baertur'); 25 | 26 | name = columnToRpgFieldName({display_size: 0, label: 'öäß', name: 'ABCD', type: 'CHAR', precision: 10, scale: 0}, 'Label'); 27 | expect(name).toBe('oas'); 28 | 29 | name = columnToRpgFieldName({display_size: 0, label: '', name: '0001', type: 'INTEGER', precision: 0, scale: 0}, 'Name'); 30 | expect(name).toBe('col0001'); 31 | }); 32 | 33 | test('Column to RPG definition', () => { 34 | let rpgdef; 35 | 36 | rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'NUMERIC', precision: 11, scale: 0}); 37 | expect(rpgdef).toBe('zoned(11)'); 38 | 39 | rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'DECIMAL', precision: 13, scale: 2}); 40 | expect(rpgdef).toBe('packed(13 : 2)'); 41 | 42 | rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'VARCHAR', precision: 60, scale: 0}); 43 | expect(rpgdef).toBe('varchar(60)'); 44 | 45 | rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'CHAR', precision: 10, scale: 0}); 46 | expect(rpgdef).toBe('char(10)'); 47 | 48 | rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'DATE', precision: 0, scale: 0}); 49 | expect(rpgdef).toBe('date'); 50 | 51 | rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'TIME', precision: 0, scale: 0}); 52 | expect(rpgdef).toBe('time'); 53 | 54 | rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'TIMESTAMP', precision: 0, scale: 0}); 55 | expect(rpgdef).toBe('timestamp'); 56 | 57 | rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'SMALLINT', precision: 0, scale: 0}); 58 | expect(rpgdef).toBe('int(5)'); 59 | 60 | rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'INTEGER', precision: 0, scale: 0}); 61 | expect(rpgdef).toBe('int(10)'); 62 | 63 | rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'BIGINT', precision: 0, scale: 0}); 64 | expect(rpgdef).toBe('int(20)'); 65 | 66 | rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'BOOLEAN', precision: 0, scale: 0}); 67 | expect(rpgdef).toBe('ind'); 68 | 69 | rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'SOME_UNKNOWN_TYPE', precision: 0, scale: 0}); 70 | expect(rpgdef).toBe('// type:SOME_UNKNOWN_TYPE precision:0 scale:0'); 71 | }); 72 | 73 | test('QueryResult to RPG data structure', () => { 74 | const queryResult: QueryResult = { 75 | metadata: { 76 | column_count: 3, 77 | columns: [ 78 | { 79 | display_size: 0, 80 | label: 'id', 81 | name: 'id', 82 | type: 'INTEGER', 83 | precision: 0, 84 | scale: 0 85 | }, 86 | { 87 | display_size: 0, 88 | label: 'name', 89 | name: 'name', 90 | type: 'VARCHAR', 91 | precision: 80, 92 | scale: 0 93 | }, 94 | { 95 | display_size: 0, 96 | label: 'salary', 97 | name: 'salary', 98 | type: 'DECIMAL', 99 | precision: 13, 100 | scale: 2 101 | }, 102 | ] 103 | }, 104 | is_done: true, 105 | has_results: true, 106 | update_count: 0, 107 | data: [], 108 | id: '', 109 | success: true, 110 | sql_rc: 0, 111 | sql_state: '', 112 | execution_time: 0 113 | }; 114 | const ds = queryResultToRpgDs(queryResult); 115 | const lines = ds.split('\n').filter(l => l !== ''); 116 | expect(lines.length).toBe(5); 117 | expect(lines.at(0)).toBe('dcl-ds row_t qualified template;'); 118 | expect(lines.at(1).trim()).toBe('id int(10);'); 119 | expect(lines.at(2).trim()).toBe('name varchar(80);'); 120 | expect(lines.at(3).trim()).toBe('salary packed(13 : 2);'); 121 | expect(lines.at(4)).toBe('end-ds;'); 122 | }); 123 | 124 | -------------------------------------------------------------------------------- /src/views/results/codegen.ts: -------------------------------------------------------------------------------- 1 | import { ColumnMetaData, QueryResult } from "@ibm/mapepire-js"; 2 | 3 | export function queryResultToRpgDs(result: QueryResult, source: string = 'Name') : string { 4 | let content = `dcl-ds row_t qualified template;\n`; 5 | for (let i = 0; i < result.metadata.column_count; i++) { 6 | const name = columnToRpgFieldName(result.metadata.columns[i], source); 7 | content += ` ${name} ${columnToRpgDefinition(result.metadata.columns[i])};\n`; 8 | } 9 | content += `end-ds;\n`; 10 | return content; 11 | } 12 | 13 | export function columnToRpgFieldName(column: ColumnMetaData, source: string = 'Name') : string { 14 | let name = source === 'Label' ? column.label.toLowerCase().trim() : column.name.toLowerCase().trim(); 15 | name = name.replace(/\u00fc/g, "u") // ü -> u 16 | .replace(/\u00e4/g, "a") // ä -> a 17 | .replace(/\u00f6/g, "o") // ö -> o 18 | .replace(/\u00df/g, "s") // sharp s/Eszett -> s 19 | .replace(/\u00e6/g, "ae") // æ -> ae 20 | .replace(/\u00f8/g, "oe") // ø -> oe 21 | .replace(/\u00e5/g, "aa") // å -> aa 22 | .replace(/[ .:]+$/g, "") // remove trailing space, "." and ":" 23 | .replace(/[.]/g, "_") // "." between words to underscore 24 | .replace(/\s+/g, "_") // remaining whitespaces to underscore 25 | .replace(/[^a-zA-Z0-9_]/g, "") // remove non-alphanumeric chars 26 | .replace(/\_+/i, "_") // replace multiple underscores with single underscore 27 | .trim(); 28 | if (!isNaN(+name.charAt(0))) { 29 | name = `col` + name; 30 | } 31 | return name; 32 | } 33 | 34 | export function columnToRpgDefinition(column: ColumnMetaData) : string { 35 | switch (column.type) { 36 | case `NUMERIC`: 37 | return `zoned(${column.precision}${column.scale > 0 ? ' : ' + column.scale : ''})`; 38 | case `DECIMAL`: 39 | return `packed(${column.precision}${column.scale > 0 ? ' : ' + column.scale : ''})`; 40 | case `CHAR`: 41 | return `char(${column.precision})`; 42 | case `VARCHAR`: 43 | return `varchar(${column.precision})`; 44 | case `DATE`: 45 | return `date`; 46 | case `TIME`: 47 | return `time`; 48 | case `TIMESTAMP`: 49 | return `timestamp`; 50 | case `SMALLINT`: 51 | return `int(5)`; 52 | case `INTEGER`: 53 | return `int(10)`; 54 | case `BIGINT`: 55 | return `int(20)`; 56 | case `BOOLEAN`: 57 | return `ind`; 58 | default: 59 | return `// type:${column.type} precision:${column.precision} scale:${column.scale}`; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/views/results/explain/advice.ts: -------------------------------------------------------------------------------- 1 | import { ContextType, ExplainTree } from "./nodes"; 2 | 3 | export function generateSqlForAdvisedIndexes(explainTree: ExplainTree): string { 4 | let script: string[] = []; 5 | // Get the advised indexes and generate SQL for each 6 | explainTree.getContextObjects([ContextType.ADVISED_INDEX]).forEach(ai => { 7 | let tableSchema = ai.properties[1].value; 8 | let tableName = ai.properties[2].value; 9 | // Index type is either BINARY RADIX or EVI 10 | let type = (ai.properties[3].value as string).startsWith(`E`) ? ` ENCODED VECTOR ` : ` `; 11 | // Number of distinct values (only required for EVI type indexes, otherwise will be empty or 0) 12 | let distinctValues = (ai.properties[4]?.value as number); 13 | let keyColumns = ai.properties[5].value; 14 | let sortSeqSchema = ai.properties[6]; 15 | let sortSeqTable = ai.properties[7]; 16 | let sql: string = ``; 17 | // If sort sequence is specified, add a comment to indicate the connection settings that should be used when creating the index 18 | if (sortSeqSchema?.value != `*N` && sortSeqTable?.value != `*HEX`) { 19 | sql += `-- Use these connection properties when creating this index\n`; 20 | sql += `-- ${sortSeqSchema.title}: ${sortSeqSchema.value}\n`; 21 | sql += `-- ${sortSeqTable.title}: ${sortSeqTable.value}\n`; 22 | } 23 | sql += `CREATE${type}INDEX ${tableSchema}.${tableName}_IDX ON ${tableSchema}.${tableName} (${keyColumns})`; 24 | if (!isNaN(distinctValues) && distinctValues > 0) { 25 | sql += ` WITH ${distinctValues} VALUES`; 26 | } 27 | script.push(sql); 28 | }); 29 | 30 | return `-- Visual Explain - Advised Indexes\n\n` + script.join(`;\n\n`); 31 | } -------------------------------------------------------------------------------- /src/views/results/explain/doveNodeView.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { CancellationToken, Event, EventEmitter, ProviderResult, TreeView, TreeDataProvider, TreeItem, TreeItemCollapsibleState, commands } from "vscode"; 3 | import { ExplainNode, ExplainProperty, Highlighting, RecordType, NodeHighlights } from "./nodes"; 4 | import { toDoveTreeDecorationProviderUri } from "./doveTreeDecorationProvider"; 5 | import * as crypto from "crypto"; 6 | 7 | type EventType = PropertyNode | undefined | null | void; 8 | 9 | export class DoveNodeView implements TreeDataProvider { 10 | private _onDidChangeTreeData: EventEmitter = new EventEmitter(); 11 | readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; 12 | 13 | private currentNode: ExplainNode; 14 | private propertyNodes: PropertyNode[]; 15 | 16 | private treeView: TreeView; 17 | 18 | private defaultTitle: string; 19 | 20 | constructor() { 21 | this.treeView = vscode.window.createTreeView(`vscode-db2i.dove.node`, { treeDataProvider: this, showCollapseAll: true }); 22 | this.defaultTitle = this.treeView.title; 23 | } 24 | 25 | public getTreeView(): TreeView { 26 | return this.treeView; 27 | } 28 | 29 | setNode(node: ExplainNode, title?: string) { 30 | // If we have a current node in the view and it has a context defined, reset it before processing the new node 31 | if (this.currentNode?.nodeContext) { 32 | commands.executeCommand(`setContext`, this.currentNode.nodeContext, false); 33 | } 34 | this.currentNode = node; 35 | this.treeView.title = title || this.defaultTitle; 36 | this.propertyNodes = []; 37 | let currentSection: PropertySection = null; 38 | for (let property of node.props) { 39 | let node = property.type === RecordType.HEADING ? new PropertySection(property) : new PropertyNode(property); 40 | // If the property node is a new section, add it to the top level node list and set as the current section 41 | if (node instanceof PropertySection) { 42 | // If this is the first section, but the tree already has nodes, add a blank node to space things out a bit 43 | if (currentSection === null && this.propertyNodes.length > 0) { 44 | this.propertyNodes.push(new PropertyNode()); 45 | } 46 | // We expect that we are now processing a new section, so add a blank property to current section to finish it off 47 | if (currentSection && node != currentSection) { 48 | currentSection.addProperty(new PropertyNode()); 49 | } 50 | this.propertyNodes.push(node); 51 | currentSection = node; 52 | } else if (currentSection) { 53 | currentSection.addProperty(node); 54 | } else { 55 | this.propertyNodes.push(node); 56 | } 57 | } 58 | this._onDidChangeTreeData.fire(); 59 | // Ensure that the tree is positioned such that the first element is visible 60 | this.treeView.reveal(this.propertyNodes[0], { select: false }); 61 | // Show the detail view and if the explain node has a context defined, set it 62 | this.setContext(true); 63 | } 64 | getNode(): ExplainNode { 65 | return this.currentNode; 66 | } 67 | 68 | private setContext(enable: boolean): void { 69 | commands.executeCommand(`setContext`, `vscode-db2i:explainingNode`, enable); 70 | if (this.currentNode?.nodeContext) { 71 | commands.executeCommand(`setContext`, this.currentNode.nodeContext, enable); 72 | } 73 | } 74 | 75 | close() { 76 | // Hide the detail view and if the explain node has a context defined, reset it 77 | this.setContext(false); 78 | } 79 | 80 | getTreeItem(element: PropertyNode): PropertyNode | Thenable { 81 | return element; 82 | } 83 | 84 | getChildren(element?: PropertyNode): ProviderResult { 85 | if (element) { 86 | return element instanceof PropertySection ? element.getProperties() : []; 87 | } 88 | return this.propertyNodes; 89 | } 90 | 91 | getParent?(element: any) { 92 | throw new Error("Method not implemented."); 93 | } 94 | resolveTreeItem?(item: TreeItem, element: any, token: CancellationToken): ProviderResult { 95 | throw new Error("Method not implemented."); 96 | } 97 | } 98 | 99 | export class PropertyNode extends TreeItem { 100 | constructor(property?: ExplainProperty) { 101 | super(property?.title || ``); 102 | // Initialize the tooltip to an empty string, otherwise 'Loading...' is displayed 103 | this.tooltip = ``; 104 | if (property?.value) { 105 | this.description = String(property.value || ``); 106 | this.tooltip = this.description; 107 | this.contextValue = `propertyNode`; 108 | } 109 | } 110 | } 111 | 112 | class PropertySection extends PropertyNode { 113 | propertyNodes: PropertyNode[] = []; 114 | constructor(property: ExplainProperty) { 115 | super(property); 116 | // Random ID so that when switching between result nodes the expansion state of its attribute sections are not applied to another nodes attribute sections with the same title 117 | this.id = crypto.randomUUID(); 118 | this.collapsibleState = TreeItemCollapsibleState.Expanded; 119 | // Visually differentiate section headings from the rest of the attributes via node highlighting 120 | this.resourceUri = toDoveTreeDecorationProviderUri(new NodeHighlights().set(Highlighting.ATTRIBUTE_SECTION_HEADING)); 121 | } 122 | addProperty(p: PropertyNode) { 123 | this.propertyNodes.push(p); 124 | } 125 | getProperties(): PropertyNode[] { 126 | return this.propertyNodes; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/views/results/explain/doveTreeDecorationProvider.ts: -------------------------------------------------------------------------------- 1 | import { window, Event, EventEmitter, TreeItem, ThemeColor, FileDecorationProvider, FileDecoration, Uri, Disposable } from "vscode"; 2 | import { NodeHighlights, Highlighting } from "./nodes"; 3 | 4 | /** 5 | * The Uri scheme for VE node highlights 6 | */ 7 | const doveUriScheme = "db2i.dove"; 8 | 9 | /** 10 | * Generates a {@link DoveTreeDecorationProvider} compatible Uri. The Uri scheme is set to {@link doveUriScheme}. 11 | */ 12 | export function toDoveTreeDecorationProviderUri(highlights: NodeHighlights): Uri { 13 | return highlights.formatValue != 0 ? Uri.parse(doveUriScheme + ":" + highlights.formatValue, false) : null; 14 | } 15 | 16 | /** 17 | * Provides tree node decorations specific to Db2 for i Visual Explain. 18 | */ 19 | export class DoveTreeDecorationProvider implements FileDecorationProvider { 20 | private disposables: Array = []; 21 | 22 | readonly _onDidChangeFileDecorations: EventEmitter = new EventEmitter(); 23 | readonly onDidChangeFileDecorations: Event = this._onDidChangeFileDecorations.event; 24 | 25 | constructor() { 26 | this.disposables = []; 27 | this.disposables.push(window.registerFileDecorationProvider(this)); 28 | } 29 | 30 | async updateTreeItems(treeItem: TreeItem): Promise { 31 | this._onDidChangeFileDecorations.fire(treeItem.resourceUri); 32 | } 33 | 34 | /** 35 | * @inheritdoc 36 | * Provides tree node decorations specific to Db2 for i Visual Explain. 37 | */ 38 | async provideFileDecoration(uri: Uri): Promise { 39 | // Only decorate tree items tagged with the VE scheme 40 | if (uri?.scheme === doveUriScheme) { 41 | // The Uri path should simply be a number that represents the highlight attributes 42 | const value: number = Number(uri.fsPath); 43 | if (!isNaN(value) && value > 0) { 44 | const nodeHighlights = new NodeHighlights(value); 45 | let color: ThemeColor; 46 | let badge: string; 47 | let tooltip: string; 48 | // For attribute section headings, only the color needs to be applied, which is not controlled by the highlight preferences 49 | if (nodeHighlights.isSet(Highlighting.ATTRIBUTE_SECTION_HEADING)) { 50 | color = Highlighting.Colors[Highlighting.ATTRIBUTE_SECTION_HEADING]; 51 | } else { 52 | color = nodeHighlights.getPriorityColor(); 53 | badge = String(nodeHighlights.getCount()); // The number of highlights set for the node 54 | tooltip = "\n" + nodeHighlights.getNames().map(h => "🔥 " + Highlighting.Descriptions[Highlighting[h]]).join("\n"); 55 | } 56 | return { 57 | color: color, 58 | badge: badge, 59 | tooltip: tooltip, 60 | } 61 | } 62 | } 63 | return null; 64 | } 65 | 66 | dispose() { 67 | this.disposables.forEach((d) => d.dispose()); 68 | } 69 | } -------------------------------------------------------------------------------- /src/views/schemaBrowser/copyUI.ts: -------------------------------------------------------------------------------- 1 | import { getBase, getInstance, loadBase } from "../../base"; 2 | 3 | export function getCopyUi() { 4 | return getBase()!.customUI() 5 | .addInput('toFile', 'To File', 'Name', { 6 | minlength: 1, 7 | maxlength: 10 8 | }) 9 | .addInput('toLib', 'Library', 'Name', { 10 | default: '*LIBL', 11 | minlength: 1, 12 | maxlength: 10 13 | }) 14 | .addInput('fromMbr', 'From member', 'Name, generic*, *FIRST, *ALL', { 15 | default: '*FIRST' 16 | }) 17 | .addInput('toMbr', 'To member or label', 'Name, *FIRST, *FROMMBR, *ALL', { 18 | default: '*FIRST' 19 | }) 20 | .addSelect('mbrOpt', 'Replace or add records', [ 21 | { text: '*NONE', description: '*NONE', value: '*NONE' }, 22 | { text: '*ADD', description: '*ADD', value: '*ADD' }, 23 | { text: '*REPLACE', description: '*REPLACE', value: '*REPLACE' }, 24 | { text: '*UPDADD', description: '*UPDADD', value: '*UPDADD' }, 25 | ]) 26 | .addSelect('crtFile', 'Create file', [ 27 | { text: '*NO', description: '*NO', value: '*NO' }, 28 | { text: '*YES', description: '*YES', value: '*YES' }, 29 | ]) 30 | .addSelect('outFmt', 'Print format', [ 31 | { text: '*CHAR', description: '*CHAR', value: '*CHAR' }, 32 | { text: '*HEX', description: '*HEX', value: '*HEX' }, 33 | ]) 34 | .addButtons( 35 | { id: 'copy', label: 'Copy', requiresValidation: true }, 36 | { id: 'cancel', label: 'Cancel' } 37 | ); 38 | } -------------------------------------------------------------------------------- /src/views/types/ColumnTreeItem.ts: -------------------------------------------------------------------------------- 1 | 2 | import vscode from "vscode"; 3 | import Statement from "../../database/statement"; 4 | import { TableColumn } from "../../types"; 5 | 6 | export default class ColumnTreeItem extends vscode.TreeItem { 7 | schema: string; 8 | table: string; 9 | name: string; 10 | 11 | constructor(schema: string, table: string, data: TableColumn) { 12 | super(Statement.prettyName(data.COLUMN_NAME), vscode.TreeItemCollapsibleState.None); 13 | 14 | this.contextValue = `column`; 15 | this.schema = schema; 16 | this.table = table; 17 | this.name = data.COLUMN_NAME; 18 | 19 | let detail, length; 20 | if ([`DECIMAL`, `ZONED`].includes(data.DATA_TYPE)) { 21 | length = data.NUMERIC_PRECISION || null; 22 | detail = `${data.DATA_TYPE}${length ? `(${length}${data.NUMERIC_PRECISION ? `, ${data.NUMERIC_SCALE}` : ``})` : ``}` 23 | } else { 24 | length = data.CHARACTER_MAXIMUM_LENGTH || null; 25 | detail = `${data.DATA_TYPE}${length ? `(${length})` : ``}` 26 | } 27 | 28 | const descriptionParts = [ 29 | detail, 30 | data.IS_IDENTITY === `YES` ? `Identity` : ``, 31 | data.IS_NULLABLE === `Y` ? `nullable` : ``, 32 | data.HAS_DEFAULT === `Y` ? `${data.COLUMN_DEFAULT} def.` : ``, 33 | data.COLUMN_TEXT, 34 | ] 35 | 36 | this.description = descriptionParts.filter(part => part && part !== ``).join(`, `); 37 | 38 | this.iconPath = new vscode.ThemeIcon(data.CONSTRAINT_NAME ? `key` : `symbol-field`); 39 | } 40 | } -------------------------------------------------------------------------------- /src/views/types/ParmTreeItem.ts: -------------------------------------------------------------------------------- 1 | 2 | import vscode from "vscode"; 3 | import Statement from "../../database/statement"; 4 | import { SQLParm } from "../../types"; 5 | 6 | const icons = { 7 | IN: `arrow-right`, 8 | OUT: `arrow-left`, 9 | INOUT: `arrow-both`, 10 | } 11 | 12 | export default class ParmTreeItem extends vscode.TreeItem { 13 | schema: string; 14 | routine: string; 15 | name: string; 16 | 17 | constructor(schema: string, routine: string, data: SQLParm) { 18 | super(Statement.prettyName(data.PARAMETER_NAME), vscode.TreeItemCollapsibleState.None); 19 | 20 | this.contextValue = `parameter`; 21 | this.schema = schema; 22 | this.routine = routine; 23 | this.name = data.PARAMETER_NAME; 24 | 25 | let detail, length; 26 | if ([`DECIMAL`, `ZONED`].includes(data.DATA_TYPE)) { 27 | length = data.NUMERIC_PRECISION || null; 28 | detail = `${data.DATA_TYPE}${length ? `(${length}${data.NUMERIC_PRECISION ? `, ${data.NUMERIC_SCALE}` : ``})` : ``}` 29 | } else { 30 | length = data.CHARACTER_MAXIMUM_LENGTH || null; 31 | detail = `${data.DATA_TYPE}${length ? `(${length})` : ``}` 32 | } 33 | 34 | const descriptionParts = [ 35 | data.PARAMETER_MODE, 36 | detail, 37 | data.IS_NULLABLE === `YES` ? `nullable` : ``, 38 | data.DEFAULT, 39 | data.LONG_COMMENT, 40 | ] 41 | 42 | this.description = descriptionParts.filter(part => part && part !== ``).join(`, `); 43 | 44 | this.iconPath = new vscode.ThemeIcon(icons[data.PARAMETER_MODE]); 45 | } 46 | } -------------------------------------------------------------------------------- /src/views/types/function.ts: -------------------------------------------------------------------------------- 1 | 2 | import Function from "../../database/callable"; 3 | import ParmTreeItem from "./ParmTreeItem"; 4 | 5 | export async function getChildren (schema: string, specificName: string): Promise { 6 | const signatures = await Function.getSignaturesFor(schema, [specificName]); 7 | const allParms = signatures.map(signature => signature.parms).flat(); 8 | const removedDupes = allParms.filter((parm, index) => { 9 | return allParms.findIndex(p => p.PARAMETER_NAME === parm.PARAMETER_NAME && p.DATA_TYPE === p.DATA_TYPE) === index; 10 | }); 11 | 12 | return removedDupes.map(parm => new ParmTreeItem(schema, specificName, parm)); 13 | } -------------------------------------------------------------------------------- /src/views/types/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as Table from "./table"; 3 | import * as Procedure from "./procedure"; 4 | import * as View from "./view"; 5 | import * as Function from "./function"; 6 | 7 | export default { 8 | table: Table, 9 | procedure: Procedure, 10 | view: View, 11 | function: Function 12 | } -------------------------------------------------------------------------------- /src/views/types/procedure.ts: -------------------------------------------------------------------------------- 1 | 2 | import Procedure from "../../database/callable"; 3 | import ParmTreeItem from "./ParmTreeItem"; 4 | 5 | export async function getChildren(schema: string, specificName: string): Promise { 6 | const signatures = await Procedure.getSignaturesFor(schema, [specificName]); 7 | const allParms = signatures.map(signature => signature.parms).flat(); 8 | const removedDupes = allParms.filter((parm, index) => { 9 | return allParms.findIndex(p => p.PARAMETER_NAME === parm.PARAMETER_NAME && p.DATA_TYPE === p.DATA_TYPE) === index; 10 | }); 11 | 12 | return removedDupes.map(parm => new ParmTreeItem(schema, specificName, parm)); 13 | } -------------------------------------------------------------------------------- /src/views/types/table.ts: -------------------------------------------------------------------------------- 1 | 2 | import Table from "../../database/table"; 3 | import ColumnTreeItem from "./ColumnTreeItem"; 4 | 5 | export async function getChildren(schema: string, name: string): Promise { 6 | const columns = await Table.getItems(schema, name); 7 | 8 | return columns.map(column => new ColumnTreeItem(schema, name, column)); 9 | } -------------------------------------------------------------------------------- /src/views/types/view.ts: -------------------------------------------------------------------------------- 1 | 2 | import View from "../../database/view"; 3 | import ColumnTreeItem from "./ColumnTreeItem"; 4 | 5 | export async function getChildren (schema: string, name: string): Promise { 6 | const columns = await View.getColumns(schema, name); 7 | 8 | return columns.map(column => new ColumnTreeItem(schema, name, column)); 9 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2019", 5 | "checkJs": true, /* Typecheck .js files. */ 6 | "esModuleInterop": true, 7 | "lib": [ 8 | "ES2019" 9 | ], 10 | "outDir": "./dist", 11 | }, 12 | "exclude": [ 13 | "node_modules" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require(`path`); 6 | const fs = require(`fs`); 7 | const webpack = require(`webpack`); 8 | 9 | const npm_runner = process.env[`npm_lifecycle_script`]; 10 | const isProduction = (npm_runner && npm_runner.includes(`production`)); 11 | 12 | console.log(`Is production build: ${isProduction}`); 13 | // @ts-ignore 14 | const packageVer = require(`./package.json`).version; 15 | 16 | let exclude = undefined; 17 | 18 | if (isProduction) { 19 | exclude = path.resolve(__dirname, `src`, `testing`) 20 | } 21 | 22 | // We need to hack our chart.js copy and remove the hardcoded exports for our build. 23 | const chartJsPackagePath = path.resolve(__dirname, `node_modules`, `chart.js`, `package.json`); 24 | let chartJsPackage = JSON.parse(fs.readFileSync(chartJsPackagePath, `utf8`)); 25 | delete chartJsPackage.exports; 26 | fs.writeFileSync(chartJsPackagePath, JSON.stringify(chartJsPackage, null, 2), `utf8`); 27 | 28 | /**@type {import('webpack').Configuration}*/ 29 | const config = { 30 | target: `node`, // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 31 | 32 | entry: `./src/extension.ts`, // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 33 | output: { 34 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 35 | path: path.resolve(__dirname, `dist`), 36 | filename: `extension.js`, 37 | libraryTarget: `commonjs2`, 38 | devtoolModuleFilenameTemplate: `../[resource-path]`, 39 | }, 40 | plugins: [ 41 | new webpack.DefinePlugin({ 42 | 'process.env': { 43 | DEV: JSON.stringify(!isProduction), 44 | DB2I_VERSION: JSON.stringify(packageVer) 45 | } 46 | }), 47 | ], 48 | devtool: `source-map`, 49 | externals: { 50 | vscode: `commonjs vscode` // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 51 | }, 52 | resolve: { 53 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 54 | extensions: [`.ts`, `.js`, `.svg`], 55 | }, 56 | module: { 57 | rules: [ 58 | { 59 | test: /\.umd\.(js)$/i, 60 | include: path.resolve(__dirname, `node_modules`, `chart.js`, `dist`, `chart.umd.js`), 61 | type: `asset/source`, 62 | 63 | }, 64 | { 65 | test: /\.(ts|tsx)$/i, 66 | exclude: /node_modules/, 67 | use: [ 68 | { 69 | loader: `esbuild-loader`, 70 | options: { 71 | // JavaScript version to transpile to 72 | target: 'node18' 73 | } 74 | } 75 | ] 76 | }, 77 | { 78 | test: /\.ts$/, 79 | exclude 80 | }, 81 | { 82 | test: /\.ts$/, 83 | exclude: path.resolve(__dirname, `src`, `dcs.ts`) 84 | }, 85 | ] 86 | } 87 | }; 88 | 89 | module.exports = config; 90 | --------------------------------------------------------------------------------