├── .babelrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── connection.schema.json ├── docs └── assets │ └── img │ ├── activitybar.png │ ├── addconnection.png │ └── command_palette_add_new.png ├── icons ├── active.png ├── default.png ├── inactive.png └── sqltools.png ├── package-lock.json ├── package.json ├── src ├── constants.ts ├── extension.ts └── ls │ ├── driver.ts │ ├── irisdb.ts │ ├── keywords.ts │ ├── plugin.ts │ └── queries.ts ├── test ├── docker-compose.yml └── project.code-workspace ├── tsconfig.json ├── ui.schema.json └── webpack.config.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | require.resolve('@babel/preset-env'), 5 | { 6 | targets: { 7 | node: '8', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - "docs/**" 9 | - ".vscode/**" 10 | - ".github/**" 11 | - "*.md" 12 | - "**/*.md" 13 | pull_request: 14 | branches: 15 | - master 16 | release: 17 | types: 18 | - released 19 | jobs: 20 | build: 21 | timeout-minutes: 10 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest] 26 | outputs: 27 | name: ${{ steps.set-version.outputs.name }} 28 | version: ${{ steps.set-version.outputs.version }} 29 | taggedbranch: ${{ steps.find-branch.outputs.taggedbranch }} 30 | steps: 31 | - uses: actions/checkout@v3 32 | - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 33 | - name: Find which branch the release tag points at 34 | id: find-branch 35 | if: github.event_name == 'release' 36 | shell: bash 37 | run: | 38 | git fetch --depth=1 origin +refs/heads/*:refs/heads/* 39 | set -x 40 | TAGGEDBRANCH=$(git for-each-ref --points-at=${{github.sha}} --format='%(refname:lstrip=2)' refs/heads/) 41 | echo "taggedbranch=$TAGGEDBRANCH" >> $GITHUB_OUTPUT 42 | - name: Set an output 43 | id: set-version 44 | if: runner.os == 'Linux' 45 | shell: bash 46 | run: | 47 | set -x 48 | VERSION=$(jq -r '.version' package.json | cut -d- -f1) 49 | [ $GITHUB_EVENT_NAME == 'release' ] && VERSION=${{ github.event.release.tag_name }} && VERSION=${VERSION/v/} 50 | CHANGELOG=$(cat CHANGELOG.md | sed -n "/## \[${VERSION}\]/,/## /p" | sed '/^$/d;1d;$d') 51 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" 52 | echo "changelog=$CHANGELOG" >> $GITHUB_OUTPUT 53 | git tag -l | cat 54 | [ $GITHUB_EVENT_NAME == 'push' ] && VERSION+=-beta && VERSION+=.$(($(git tag -l "v$VERSION.*" | sort -nt. -k4 2>/dev/null | tail -1 | cut -d. -f4)+1)) 55 | [ $GITHUB_EVENT_NAME == 'pull_request' ] && VERSION+=-dev.${{ github.event.pull_request.number }} 56 | echo "version=$VERSION" >> $GITHUB_OUTPUT 57 | NAME=$(jq -r '.name' package.json)-$VERSION 58 | echo "name=$NAME" >> $GITHUB_OUTPUT 59 | tmp=$(mktemp) 60 | jq --arg version "$VERSION" '.version = $version' package.json > "$tmp" && mv "$tmp" package.json 61 | mkdir dist 62 | echo $VERSION > .version 63 | echo $NAME > .name 64 | - name: Use Node.js 65 | uses: actions/setup-node@v3 66 | with: 67 | node-version: 20 68 | - run: npm install 69 | - name: lint 70 | if: runner.os == 'Linux' 71 | run: npm run lint 72 | - run: npm run compile 73 | - name: Build package 74 | if: runner.os == 'Linux' 75 | run: | 76 | npx vsce package -o ${{ steps.set-version.outputs.name }}.vsix 77 | - uses: actions/upload-artifact@v4.4.2 78 | if: (runner.os == 'Linux') && (github.event_name != 'release') 79 | with: 80 | name: ${{ steps.set-version.outputs.name }}.vsix 81 | path: ${{ steps.set-version.outputs.name }}.vsix 82 | beta: 83 | if: (github.event_name == 'push') 84 | runs-on: ubuntu-latest 85 | needs: build 86 | steps: 87 | - uses: actions/download-artifact@v4.1.7 88 | with: 89 | name: ${{ needs.build.outputs.name }}.vsix 90 | - name: Create Release 91 | id: create_release 92 | uses: softprops/action-gh-release@v1 93 | with: 94 | tag_name: v${{ needs.build.outputs.version }} 95 | name: v${{ needs.build.outputs.version }} 96 | prerelease: ${{ github.event_name != 'release' }} 97 | token: ${{ secrets.GITHUB_TOKEN }} 98 | files: ${{ needs.build.outputs.name }}.vsix 99 | publish: 100 | needs: build 101 | if: github.event_name == 'release' && needs.build.outputs.taggedbranch == 'master' 102 | runs-on: ubuntu-latest 103 | steps: 104 | - uses: actions/checkout@v3 105 | with: 106 | ref: master 107 | token: ${{ secrets.TOKEN }} 108 | - name: Use Node.js 109 | uses: actions/setup-node@v3 110 | with: 111 | node-version: 20 112 | - name: Prepare build 113 | id: set-version 114 | shell: bash 115 | run: | 116 | VERSION=${{ needs.build.outputs.version }} 117 | NEXT_VERSION=`echo $VERSION | awk -F. '/[0-9]+\./{$NF++;print}' OFS=.` 118 | tmp=$(mktemp) 119 | git config --global user.name 'ProjectBot' 120 | git config --global user.email 'bot@users.noreply.github.com' 121 | jq --arg version "${NEXT_VERSION}-SNAPSHOT" '.version = $version' package.json > "$tmp" && mv "$tmp" package.json 122 | git add package.json 123 | git commit -m 'auto bump version with release [skip ci]' 124 | jq --arg version "$VERSION" '.version = $version' package.json > "$tmp" && mv "$tmp" package.json 125 | jq 'del(.enableProposedApi,.enabledApiProposals)' package.json > "$tmp" && mv "$tmp" package.json 126 | npm install 127 | git push 128 | - name: Build package 129 | run: | 130 | npx vsce package -o ${{ needs.build.outputs.name }}.vsix 131 | - name: Upload Release Asset 132 | id: upload-release-asset 133 | uses: softprops/action-gh-release@v1 134 | with: 135 | tag_name: ${{ github.event.release.tag_name }} 136 | files: ${{ needs.build.outputs.name }}.vsix 137 | token: ${{ secrets.GITHUB_TOKEN }} 138 | - name: Publish to VSCode Marketplace 139 | shell: bash 140 | run: | 141 | [ -n "${{ secrets.VSCE_TOKEN }}" ] && \ 142 | npx vsce publish --packagePath ${{ needs.build.outputs.name }}.vsix -p ${{ secrets.VSCE_TOKEN }} || true 143 | - name: Publish to Open VSX Registry 144 | shell: bash 145 | run: | 146 | [ -n "${{ secrets.OVSX_TOKEN }}" ] && \ 147 | npx ovsx publish ${{ needs.build.outputs.name }}.vsix --pat ${{ secrets.OVSX_TOKEN }} || true 148 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | out/ 4 | dist/ 5 | *.vsix -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run driver extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": [ 9 | "${workspaceFolder}/test/project.code-workspace", 10 | "--extensionDevelopmentPath=${workspaceFolder}" 11 | ], 12 | "sourceMaps": true, 13 | "outFiles": [ 14 | "${workspaceFolder}/dist/**/*.js" 15 | ], 16 | "preLaunchTask": "npm: webpack" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "sqltools.connections": [ 12 | { 13 | "namespace": "USER", 14 | "connectionMethod": "Server and Port", 15 | "showSystem": false, 16 | "previewLimit": 50, 17 | "server": "localhost", 18 | "port": 52773, 19 | "askForPassword": false, 20 | "driver": "InterSystems IRIS", 21 | "name": "iris", 22 | "username": "_SYSTEM", 23 | "password": "SYS" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /.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": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | ** 2 | !dist/**/*.js 3 | !icons 4 | !docs 5 | !README.md 6 | !CHANGELOG.md 7 | !LICENSE 8 | !package.json 9 | !ui.schema.json 10 | !connection.schema.json 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.2.0] 05-Feb-2025 4 | - Enhancements 5 | - Support use of Server Manager connection definitions (#60) 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 InterSystems Community 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 | # SQLTools Driver for InterSystems IRIS 2 | 3 | ## Overview 4 | 5 | As a complete, cloud-first data platform, InterSystems IRIS eliminates the need to integrate multiple technologies, resulting in less code, fewer system resources, less maintenance, and higher ROI. It provides high performance [database management, interoperability, and analytics capabilities](https://www.intersystems.com/products/intersystems-iris/#technology), all built-in from the ground up to speed and simplify your most demanding data-intensive applications. 6 | 7 | This SQL driver offers a handy way to test out queries when you're coding in VS Code. It provides a nice "scratchpad" to test out syntax before putting the query into your ObjectScript, Java, or Python code, all without leaving the VS Code environment. Look elsewhere for a fully-functional SQL client. 8 | 9 | Try out the [SQL QuickStart](https://gettingstarted.intersystems.com/language-quickstarts/sql-quickstart/) and explore more [getting started exercises](https://gettingstarted.intersystems.com). 10 | 11 | ## Installation 12 | 13 | - [Install SQLTools in VS Code from the Marketplace](https://marketplace.visualstudio.com/items?itemName=mtxr.sqltools) 14 | - Install the InterSystems extension to SQLTools 15 | - Install a published version from within VS Code 16 | - Click on the Extensions icon in your Activity pane 17 | - Search for SQLTools 18 | - Find InterSystems and click on Install 19 | - Or install a beta version 20 | - [Go to the GitHub releases page](https://github.com/intersystems-community/sqltools-intersystems-driver/releases) 21 | - Expand Assets triangle for the latest version 22 | - Download the file ending in `.vsix` 23 | - Click on the Extensions icon in the Activity pane 24 | - In the Extensions pane, at the top right, click the "..." menu and select "Install from VSIX..." 25 | 26 | ## Configuration 27 | 28 | - Click the SQLTools icon in the Activity pane (left side of VS Code) 29 | ![SQLTools icon in Activity pane](https://raw.githubusercontent.com/intersystems-community/sqltools-intersystems-driver/master/docs/assets/img/activitybar.png) 30 | - If you have no previous database connections, you will see an "Add new connection" button. Click that. 31 | ![Add connection button](https://raw.githubusercontent.com/intersystems-community/sqltools-intersystems-driver/master/docs/assets/img/addconnection.png) 32 | - If you already have other connections defined, you won't see the button. Instead, open the command palette (Ctrl/Cmd+Shift+P) and run "SQLTools Management: Add New Connection" ![Add connection from command palette](https://raw.githubusercontent.com/intersystems-community/sqltools-intersystems-driver/master/docs/assets/img/command_palette_add_new.png) 33 | - Click InterSystems IRIS. 34 | - Fill out connection information: 35 | - Namespace to work in. 36 | - Connect using: "Server Definition" or "Server and Port" 37 | - For "Server Definition", provide in "Server name" the name of a server configured using the InterSystems Server Manager extension. These server specs are stored in the `intersystems.servers` settings object. 38 | - For "Server and Port", provide (as applicable) "Webserver address", "Webserver port", "Path prefix (for shared webserver)", "Use HTTPS", "Username", "Ask for password?" and "Password". 39 | - Test the connection. 40 | - Save the connection. 41 | 42 | ## Use 43 | 44 | With a connection defined, you can now write SQL, browse tables, etc. 45 | 46 | ### To write raw SQL 47 | - Click the "New SQL file" icon 48 | - Write your SQL statement 49 | - Click "Run on active connection" 50 | - Or, select the SQL statement text, and execute it 51 | - either right-click and select "Run selected query" from the contentual menu 52 | - or type Command-E, Command-E (Mac) 53 | - or type Ctrl-E, Ctrl-E (Windows) 54 | 55 | ### To browse tables 56 | 57 | - Click on your connection 58 | - Click on "Tables" 59 | - Right-click on the table of interest 60 | - From the contextual menu, select "Show table records" 61 | -------------------------------------------------------------------------------- /connection.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "type": "object", 4 | "definitions": { 5 | "namespace": { 6 | "title": "Namespace", 7 | "type": "string", 8 | "minLength": 1, 9 | "default": "USER" 10 | }, 11 | "username": { 12 | "title": "Username", 13 | "type": "string", 14 | "minLength": 1 15 | }, 16 | "askForPassword": { 17 | "title": "Ask for password?", 18 | "type": "boolean", 19 | "default": false 20 | }, 21 | "password": { 22 | "title": "Password", 23 | "type": "string", 24 | "minLength": 1 25 | } 26 | }, 27 | "properties": { 28 | "namespace": { "$ref": "#/definitions/namespace" }, 29 | "connectionMethod": { 30 | "title": "Connect using", 31 | "type": "string", 32 | "minLength": 1, 33 | "enum": ["Server Definition", "Server and Port"], 34 | "default": "Server Definition" 35 | }, 36 | "showSystem": { 37 | "title": "Show system items?", 38 | "type": "boolean", 39 | "default": false 40 | }, 41 | "filter": { 42 | "title": "Filter", 43 | "type": "string", 44 | "examples": [ 45 | "'Ens*", 46 | "'HS*" 47 | ], 48 | "description": "* 0 or more characters, _ any one character, ' NOT pattern" 49 | } 50 | }, 51 | "dependencies": { 52 | "connectionMethod": { 53 | "oneOf": [ 54 | { 55 | "properties": { 56 | "connectionMethod": { 57 | "enum": ["Server and Port"] 58 | }, 59 | "server": { 60 | "title": "Webserver address", 61 | "type": "string", 62 | "minLength": 1, 63 | "default": "localhost" 64 | }, 65 | "port": { 66 | "title": "Webserver port", 67 | "minimum": 1, 68 | "default": 52773, 69 | "type": "integer" 70 | }, 71 | "pathPrefix": { 72 | "title": "Path prefix (for shared webserver)", 73 | "type": "string", 74 | "pattern": "^(|\/.*[^\/])$", 75 | "default": "" 76 | }, 77 | "https": { 78 | "title": "Use HTTPS?", 79 | "default": false, 80 | "type": "boolean" 81 | }, 82 | "username": { "$ref": "#/definitions/username" }, 83 | "askForPassword": { "$ref": "#/definitions/askForPassword" } 84 | }, 85 | "required": ["server", "port", "username"] 86 | }, 87 | { 88 | "properties": { 89 | "connectionMethod": { 90 | "enum": ["Server Definition"] 91 | }, 92 | "serverName": { 93 | "title": "Server name", 94 | "type": "string", 95 | "minLength": 1, 96 | "description": "Name of a server definition configured using InterSystems Server Manager" 97 | } 98 | }, 99 | "required": ["serverName"] 100 | } 101 | ] 102 | }, 103 | "askForPassword": { 104 | "oneOf": [ 105 | { "properties": { "askForPassword": { "enum": [true] } } }, 106 | { 107 | "properties": { 108 | "askForPassword": { "enum": [false] }, 109 | "password": { "$ref": "#/definitions/password" } 110 | }, 111 | "required": ["password"] 112 | } 113 | ] 114 | } 115 | }, 116 | "required": [ 117 | "connectionMethod", "namespace" 118 | ] 119 | } -------------------------------------------------------------------------------- /docs/assets/img/activitybar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/sqltools-intersystems-driver/1f0918ddc940d0e13bcbddc7d49d950c080c3c87/docs/assets/img/activitybar.png -------------------------------------------------------------------------------- /docs/assets/img/addconnection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/sqltools-intersystems-driver/1f0918ddc940d0e13bcbddc7d49d950c080c3c87/docs/assets/img/addconnection.png -------------------------------------------------------------------------------- /docs/assets/img/command_palette_add_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/sqltools-intersystems-driver/1f0918ddc940d0e13bcbddc7d49d950c080c3c87/docs/assets/img/command_palette_add_new.png -------------------------------------------------------------------------------- /icons/active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/sqltools-intersystems-driver/1f0918ddc940d0e13bcbddc7d49d950c080c3c87/icons/active.png -------------------------------------------------------------------------------- /icons/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/sqltools-intersystems-driver/1f0918ddc940d0e13bcbddc7d49d950c080c3c87/icons/default.png -------------------------------------------------------------------------------- /icons/inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/sqltools-intersystems-driver/1f0918ddc940d0e13bcbddc7d49d950c080c3c87/icons/inactive.png -------------------------------------------------------------------------------- /icons/sqltools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/sqltools-intersystems-driver/1f0918ddc940d0e13bcbddc7d49d950c080c3c87/icons/sqltools.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqltools-intersystems-driver", 3 | "displayName": "SQLTools InterSystems IRIS", 4 | "description": "SQLTools Driver for InterSystems IRIS", 5 | "version": "0.2.0-SNAPSHOT", 6 | "engines": { 7 | "vscode": "^1.93.0" 8 | }, 9 | "publisher": "intersystems-community", 10 | "contributors": [ 11 | { 12 | "name": "Dmitry Maslennikov", 13 | "email": "mrdaimor@gmail.com" 14 | }, 15 | { 16 | "name": "John Murray", 17 | "email": "johnm@georgejames.com" 18 | } 19 | ], 20 | "license": "MIT", 21 | "homepage": "https://github.com/intersystems-community/sqltools-intersystems-driver#readme", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/intersystems-community/sqltools-intersystems-driver.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/intersystems-community/sqltools-intersystems-driver/issues" 28 | }, 29 | "galleryBanner": { 30 | "color": "#2E2A90", 31 | "theme": "dark" 32 | }, 33 | "icon": "icons/sqltools.png", 34 | "scripts": { 35 | "clean": "rimraf -rf out dist *.vsix", 36 | "vscode:prepublish": "webpack --mode production", 37 | "webpack": "webpack --mode development", 38 | "webpack-dev": "webpack --mode development --watch", 39 | "test-compile": "tsc -p ./", 40 | "compile": "tsc -p ./", 41 | "watch": "tsc -watch -p ./", 42 | "package": "vsce package -o sqltools-intersystems-driver.vsix", 43 | "lint": "", 44 | "test": "" 45 | }, 46 | "keywords": [ 47 | "intersystems-iris-driver", 48 | "intersystems", 49 | "sqltools-driver", 50 | "sql" 51 | ], 52 | "categories": [ 53 | "Programming Languages", 54 | "Snippets", 55 | "Formatters", 56 | "Other" 57 | ], 58 | "extensionDependencies": [ 59 | "mtxr.sqltools" 60 | ], 61 | "activationEvents": [ 62 | "onStartupFinished", 63 | "onLanguage:sql", 64 | "onCommand:sqltools.*" 65 | ], 66 | "contributes": { 67 | "configuration": { 68 | "title": "SQLTools InterSystems IRIS Driver", 69 | "properties": { 70 | "sqltools-intersystems-driver.resultSetRowLimit": { 71 | "description": "Maximum number of rows returned by any query, including metadata queries. Use 0 for no limit, which may cause timeouts or other failures. Setting is applied at connection time and is ignored by server versions earlier than 2023.1.", 72 | "type": "integer", 73 | "default": 1000, 74 | "minimum": 0 75 | } 76 | } 77 | } 78 | }, 79 | "main": "./dist/extension.js", 80 | "ls": "./dist/ls/plugin.js", 81 | "dependencies": { 82 | "@babel/core": "^7.14.3", 83 | "@sqltools/base-driver": "^0.1.10", 84 | "@sqltools/types": "^0.1.5", 85 | "@types/request": "^2.48.5", 86 | "@types/request-promise": "^4.1.47", 87 | "request": "^2.88.2", 88 | "request-promise": "^4.2.6", 89 | "uuid": "^8.3.2" 90 | }, 91 | "devDependencies": { 92 | "@babel/preset-env": "^7.14.2", 93 | "@intersystems-community/intersystems-servermanager": "^3.8.0", 94 | "@types/lodash": "^4.17.15", 95 | "@types/node": "^14.17.0", 96 | "@types/vscode": "^1.93.0", 97 | "@vscode/vsce": "^2.19.0", 98 | "lodash": "^4.17.21", 99 | "rimraf": "^3.0.2", 100 | "ts-loader": "^9.2.1", 101 | "typescript": "^4.9.5", 102 | "webpack": "^5.94.0", 103 | "webpack-cli": "^4.7.0" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { IDriverAlias } from '@sqltools/types'; 2 | 3 | /** 4 | * Aliases for yout driver. EG: PostgreSQL, PG, postgres can all resolve to your driver 5 | */ 6 | export const DRIVER_ALIASES: IDriverAlias[] = [ 7 | { displayName: 'InterSystems IRIS', value: 'InterSystems IRIS'}, 8 | ]; 9 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { IExtension, IExtensionPlugin, IDriverExtensionApi } from '@sqltools/types'; 3 | import { ExtensionContext } from 'vscode'; 4 | import { DRIVER_ALIASES } from './constants'; 5 | const { publisher, name } = require('../package.json'); 6 | // import { workspace } from 'vscode'; 7 | // import { Uri } from 'vscode'; 8 | // import path from 'path'; 9 | import * as serverManager from "@intersystems-community/intersystems-servermanager"; 10 | 11 | const smExtensionId = "intersystems-community.servermanager"; 12 | let serverManagerApi: serverManager.ServerManagerAPI; 13 | 14 | /** Map of the intersystems.server connection specs we have resolved via the API to that extension */ 15 | const resolvedConnSpecs = new Map(); 16 | 17 | const driverName = 'InterSystems IRIS Driver'; 18 | 19 | export async function activate(extContext: ExtensionContext): Promise { 20 | const sqltools = vscode.extensions.getExtension('mtxr.sqltools'); 21 | if (!sqltools) { 22 | throw new Error('SQLTools not installed'); 23 | } 24 | await sqltools.activate(); 25 | 26 | const api = sqltools.exports; 27 | 28 | const extensionId = `${publisher}.${name}`; 29 | const plugin: IExtensionPlugin = { 30 | extensionId, 31 | name: `${driverName} Plugin`, 32 | type: 'driver', 33 | async register(extension) { 34 | // register ext part here 35 | extension.resourcesMap().set(`driver/${DRIVER_ALIASES[0].value}/icons`, { 36 | active: extContext.asAbsolutePath('icons/active.png'), 37 | default: extContext.asAbsolutePath('icons/default.png'), 38 | inactive: extContext.asAbsolutePath('icons/inactive.png'), 39 | }); 40 | DRIVER_ALIASES.forEach(({ value }) => { 41 | extension.resourcesMap().set(`driver/${value}/extension-id`, extensionId); 42 | extension.resourcesMap().set(`driver/${value}/connection-schema`, extContext.asAbsolutePath('connection.schema.json')); 43 | extension.resourcesMap().set(`driver/${value}/ui-schema`, extContext.asAbsolutePath('ui.schema.json')); 44 | }); 45 | await extension.client.sendRequest("ls/RegisterPlugin", { 46 | path: extContext.asAbsolutePath("dist/ls/plugin.js"), 47 | }); 48 | } 49 | }; 50 | api.registerPlugin(plugin); 51 | return { 52 | driverName, 53 | parseBeforeSaveConnection: ({ connInfo }) => { 54 | /** 55 | * This hook is called before saving the connection using the assistant 56 | * so you can do any transformations before saving it 57 | */ 58 | if (connInfo.connectionMethod === 'Server Definition') { 59 | // Transform to a connectString property 60 | connInfo.connectString = `${connInfo.serverName}:${connInfo.namespace}`; 61 | connInfo.serverName = undefined; 62 | connInfo.namespace = undefined; 63 | // Remove properties carried over from 'Server and Port' type connection 64 | connInfo.server = undefined; 65 | connInfo.port = undefined; 66 | connInfo.pathPrefix = undefined; 67 | connInfo.https = undefined; 68 | connInfo.askForPassword = undefined; 69 | connInfo.username = undefined; 70 | connInfo.password = undefined; 71 | 72 | } 73 | return connInfo; 74 | }, 75 | parseBeforeEditConnection: ({ connInfo }) => { 76 | /** 77 | * This hook is called before editing the connection using the assistant 78 | * so you can do any transformations before editing it. 79 | * EG: absolute file path transformation, string manipulation etc 80 | * Below is the exmaple for SQLite, where we use relative path to save, 81 | * but we transform to asolute before editing 82 | */ 83 | if (connInfo.connectionMethod === 'Server Definition') { 84 | const connParts = connInfo.connectString.split(':'); 85 | connInfo.serverName = connParts[0]; 86 | connInfo.namespace = connParts[1]; 87 | } 88 | return connInfo; 89 | }, 90 | resolveConnection: async ({ connInfo }) => { 91 | /** 92 | * This hook is called after a connection definition has been fetched 93 | * from settings and is about to be used to connect. 94 | */ 95 | if (connInfo.connectionMethod === 'Server Definition') { 96 | const connParts = connInfo.connectString.split(':'); 97 | const serverName = connParts[0]; 98 | const namespace = connParts[1]; 99 | let connSpec = resolvedConnSpecs.get(serverName) 100 | if (!connSpec) { 101 | 102 | if (!serverManagerApi) { 103 | 104 | // Get api for servermanager extension 105 | const smExt = vscode.extensions.getExtension(smExtensionId); 106 | if (!smExt) { 107 | throw new Error("Server Manager extension not found"); 108 | } 109 | if (!smExt.isActive) await smExt.activate(); 110 | serverManagerApi = smExt.exports; 111 | } 112 | connSpec = await serverManagerApi.getServerSpec(serverName); 113 | if (!connSpec) { 114 | throw new Error(`Failed to fetch definition of server '${serverName}'`) 115 | } 116 | const isUnauthenticated = (username?: string): boolean => { 117 | return username && (username == "" || username.toLowerCase() == "unknownuser"); 118 | } 119 | const resolvePassword = async (serverSpec): Promise => { 120 | if ( 121 | // Connection isn't unauthenticated 122 | (!isUnauthenticated(serverSpec.username)) && 123 | // A password is missing 124 | typeof serverSpec.password == "undefined" 125 | ) { 126 | const scopes = [serverSpec.name, serverSpec.username || ""]; 127 | 128 | // Handle Server Manager extension version < 3.8.0 129 | const account = serverManagerApi.getAccount ? serverManagerApi.getAccount(serverSpec) : undefined; 130 | 131 | let session = await vscode.authentication.getSession(serverManager.AUTHENTICATION_PROVIDER, scopes, { 132 | silent: true, 133 | account, 134 | }); 135 | if (!session) { 136 | session = await vscode.authentication.getSession(serverManager.AUTHENTICATION_PROVIDER, scopes, { 137 | createIfNone: true, 138 | account, 139 | }); 140 | } 141 | if (session) { 142 | // If original spec lacked username use the one obtained by the authprovider 143 | serverSpec.username = serverSpec.username || session.scopes[1]; 144 | serverSpec.password = session.accessToken; 145 | } 146 | } 147 | } 148 | 149 | await resolvePassword(connSpec); 150 | resolvedConnSpecs.set(serverName, connSpec); 151 | } 152 | const resultSetRowLimit = vscode.workspace.getConfiguration('sqltools-intersystems-driver').get('resultSetRowLimit'); 153 | connInfo = { ...connInfo, 154 | https: connSpec.webServer.scheme === 'https', 155 | server: connSpec.webServer.host, 156 | port: connSpec.webServer.port, 157 | pathPrefix: connSpec.webServer.pathPrefix || '', 158 | username: connSpec.username, 159 | password: connSpec.password, 160 | namespace, 161 | resultSetRowLimit, 162 | } 163 | } 164 | return connInfo; 165 | }, 166 | driverAliases: DRIVER_ALIASES, 167 | } 168 | } 169 | 170 | export function deactivate() {} 171 | -------------------------------------------------------------------------------- /src/ls/driver.ts: -------------------------------------------------------------------------------- 1 | import AbstractDriver from '@sqltools/base-driver'; 2 | import queries from './queries'; 3 | import { IConnectionDriver, MConnectionExplorer, NSDatabase, ContextValue, Arg0 } from '@sqltools/types'; 4 | import { v4 as generateId } from 'uuid'; 5 | import IRISdb, { IRISDirect, IQueries } from './irisdb'; 6 | import keywordsCompletion from './keywords'; 7 | import zipObject from 'lodash/zipObject'; 8 | 9 | const toBool = (v: any) => v && (v.toString() === '1' || v.toString().toLowerCase() === 'true' || v.toString().toLowerCase() === 'yes'); 10 | 11 | type DriverOptions = any; 12 | 13 | export default class IRISDriver extends AbstractDriver implements IConnectionDriver { 14 | 15 | queries: IQueries = queries; 16 | private showSystem = false; 17 | private filter = ""; 18 | private resultSetRowLimit; 19 | 20 | public async open() { 21 | if (this.connection) { 22 | return this.connection; 23 | } 24 | 25 | const { namespace } = this.credentials; 26 | let config: IRISDirect; 27 | this.showSystem = this.credentials.showSystem || false; 28 | this.filter = this.credentials.filter || ""; 29 | 30 | let { https, server: host, port, pathPrefix, username, password, resultSetRowLimit } = this.credentials; 31 | config = { 32 | https, 33 | host, 34 | port, 35 | pathPrefix, 36 | namespace, 37 | username, 38 | password 39 | }; 40 | this.resultSetRowLimit = resultSetRowLimit; 41 | 42 | const irisdb = new IRISdb(config, resultSetRowLimit); 43 | return irisdb.open() 44 | .then(() => { 45 | this.connection = Promise.resolve(irisdb); 46 | return this.connection; 47 | }); 48 | } 49 | 50 | public async close() { 51 | if (!this.connection) return Promise.resolve(); 52 | 53 | await (await this.connection).close(); 54 | this.connection = null; 55 | } 56 | 57 | private splitQueries(queries: string): string[] { 58 | return queries.split(/;\s*(\n|$)/gm).filter(query => query.trim().length); 59 | } 60 | 61 | // Handle duplicate column names by appending counter 62 | private getColumnNames(columns: { name: string, type: string }[]): string[] { 63 | return columns.reduce((names, { name }) => { 64 | const count = names.filter((n) => n === name).length; 65 | return names.concat(count > 0 ? `${name} (${count})` : name); 66 | }, []); 67 | } 68 | 69 | // Modify to take account of deduplicated column names 70 | private mapRows(rows: any[], columns: string[]): any[] { 71 | return rows.map((r) => zipObject(columns, r)); 72 | } 73 | 74 | public query: (typeof AbstractDriver)['prototype']['query'] = async (queries, opt = {}) => { 75 | const irisdb = await this.open(); 76 | const listQueries = this.splitQueries(queries.toString()); 77 | const queriesResults = await Promise.all(listQueries.map(query => irisdb.query(query, []))); 78 | const resultsAgg: NSDatabase.IResult[] = []; 79 | queriesResults.forEach(queryResult => { 80 | if (irisdb.apiVersion < 6) { 81 | resultsAgg.push({ 82 | cols: queryResult.content.length ? Object.keys(queryResult.content[0]) : [], 83 | connId: this.getId(), 84 | messages: [{ date: new Date(), message: `Query ok with ${queryResult.content.length} results` }], 85 | results: queryResult.content, 86 | query: queries.toString(), 87 | requestId: opt.requestId, 88 | resultId: generateId(), 89 | }); 90 | } 91 | else { 92 | const cols = this.getColumnNames(queryResult[0].columns || []); 93 | resultsAgg.push({ 94 | cols, 95 | connId: this.getId(), 96 | messages: [{ date: new Date(), message: `Query ok with ${queryResult[0]?.content.length ?? 'no'} results (resultSetRowLimit = ${this.resultSetRowLimit})` }], 97 | results: this.mapRows(queryResult[0]?.content, cols), 98 | query: queries.toString(), 99 | requestId: opt.requestId, 100 | resultId: generateId(), 101 | }); 102 | } 103 | }); 104 | 105 | return resultsAgg; 106 | } 107 | 108 | /** if you need a different way to test your connection, you can set it here. 109 | * Otherwise by default we open and close the connection only 110 | */ 111 | public async testConnection() { 112 | await this.open(); 113 | await this.query('SELECT 1', {}); 114 | } 115 | 116 | /** 117 | * This method is a helper to generate the connection explorer tree. 118 | * it gets the child items based on current item 119 | */ 120 | public async getChildrenForItem({ item, parent }: Arg0) { 121 | switch (item.type) { 122 | case ContextValue.CONNECTION: 123 | case ContextValue.CONNECTED_CONNECTION: 124 | return [ 125 | { label: 'Tables', type: ContextValue.RESOURCE_GROUP, iconId: 'folder', childType: ContextValue.TABLE }, 126 | { label: 'Views', type: ContextValue.RESOURCE_GROUP, iconId: 'folder', childType: ContextValue.VIEW }, 127 | { label: 'Procedures', type: ContextValue.RESOURCE_GROUP, iconId: 'folder', childType: ContextValue.FUNCTION }, 128 | ]; 129 | case ContextValue.RESOURCE_GROUP: 130 | return this.getSchemas({ item, parent }); 131 | case ContextValue.SCHEMA: 132 | return this.getChildrenForSchema({ item, parent }); 133 | case ContextValue.TABLE: 134 | case ContextValue.VIEW: 135 | return this.getColumns(item as NSDatabase.ITable); 136 | case ContextValue.FUNCTION: 137 | return []; 138 | } 139 | return []; 140 | } 141 | 142 | private async getSchemas({ item }: Arg0) { 143 | item['showSystem'] = this.showSystem; 144 | item['filter'] = this.filter; 145 | 146 | switch (item.childType) { 147 | case ContextValue.TABLE: 148 | return this.queryResults(this.queries.fetchTableSchemas(item as NSDatabase.IDatabase)); 149 | case ContextValue.VIEW: 150 | return this.queryResults(this.queries.fetchViewSchemas(item as NSDatabase.IDatabase)); 151 | case ContextValue.FUNCTION: 152 | return this.queryResults(this.queries.fetchFunctionSchemas(item as NSDatabase.IDatabase)); 153 | } 154 | return []; 155 | } 156 | 157 | private async getChildrenForSchema({ item }: Arg0) { 158 | item['showSystem'] = this.showSystem; 159 | item['filter'] = this.filter; 160 | 161 | switch (item.childType) { 162 | case ContextValue.TABLE: 163 | return this.queryResults(this.queries.fetchTables(item as NSDatabase.ISchema)); 164 | case ContextValue.VIEW: 165 | return this.queryResults(this.queries.fetchViews(item as NSDatabase.ISchema)); 166 | case ContextValue.FUNCTION: 167 | return this.queryResults(this.queries.fetchFunctions(item as NSDatabase.ISchema)).then(r => r.map(t => { 168 | t.childType = ContextValue.NO_CHILD; 169 | t["snippet"] = "Testing"; 170 | return t; 171 | })); 172 | } 173 | return []; 174 | } 175 | 176 | /** 177 | * This method is a helper for intellisense and quick picks. 178 | */ 179 | public async searchItems(itemType: ContextValue, search: string, extraParams: any = {}): Promise { 180 | switch (itemType) { 181 | case ContextValue.DATABASE: 182 | // Syntatically, a schema in IRIS SQL resembles a database in other databases. 183 | // That's the simplest way to adapt IRIS SQL to the generic Hue parser vscode-sqltools uses. 184 | return this.queryResults(this.queries.searchEverything({ search, showSystem: this.showSystem, filter: this.filter })); 185 | case ContextValue.TABLE: 186 | case ContextValue.FUNCTION: 187 | case ContextValue.VIEW: 188 | const searchParams = { search, showSystem: this.showSystem, filter: this.filter, itemType, ...extraParams }; 189 | if (extraParams['database']) { 190 | searchParams['schema'] = extraParams['database']; 191 | } 192 | return this.queryResults(this.queries.searchEverything(searchParams)).then(r => r 193 | .filter(r => r.type === itemType) 194 | .map(t => { 195 | t.isView = toBool(t.isView); 196 | return t; 197 | })); 198 | case ContextValue.COLUMN: 199 | return this.queryResults(this.queries.searchColumns({ search, ...extraParams })); 200 | } 201 | return []; 202 | } 203 | 204 | private async getColumns(parent: NSDatabase.ITable): Promise { 205 | const results = await this.queryResults(this.queries.fetchColumns(parent)); 206 | return results.map(col => ({ 207 | ...col, 208 | iconName: col.isPk ? 'pk' : (col.isFk ? 'fk' : null), 209 | childType: ContextValue.NO_CHILD, 210 | table: parent 211 | })); 212 | } 213 | 214 | public getStaticCompletions: IConnectionDriver['getStaticCompletions'] = async () => { 215 | return keywordsCompletion; 216 | } 217 | 218 | public async getInsertQuery({item, columns}): Promise { 219 | let insertQuery = `INSERT INTO ${item.schema}.${item.label} (${columns.map((col) => col.label).join(', ')}) VALUES (`; 220 | columns.forEach((col, index) => { 221 | insertQuery = insertQuery.concat(`'\${${index + 1}:${col.label}:${col.dataType}}', `); 222 | }); 223 | return insertQuery; 224 | } 225 | 226 | } 227 | -------------------------------------------------------------------------------- /src/ls/irisdb.ts: -------------------------------------------------------------------------------- 1 | import * as httpModule from "http"; 2 | import * as httpsModule from "https"; 3 | import requestPromise from "request-promise"; 4 | import { QueryBuilder, NSDatabase, IBaseQueries } from "@sqltools/types"; 5 | 6 | export class IRISDirect { 7 | https?: boolean; 8 | host: string; 9 | port: number; 10 | pathPrefix?: string; 11 | namespace: string; 12 | username?: string; 13 | password?: string; 14 | } 15 | 16 | export interface IQueries extends IBaseQueries { 17 | fetchTableSchemas?: QueryBuilder; 18 | fetchViewSchemas?: QueryBuilder; 19 | fetchFunctionSchemas?: QueryBuilder; 20 | 21 | fetchViews: QueryBuilder; 22 | 23 | searchEverything: QueryBuilder<{ search: string, limit?: number }, NSDatabase.ITable>; 24 | } 25 | 26 | export default class IRISdb { 27 | 28 | private config: IRISDirect; 29 | private resultSetRowLimit: number; 30 | private cookies: string[] = []; 31 | private _apiVersion = 1; 32 | 33 | public get apiVersion() { 34 | return this._apiVersion; 35 | } 36 | 37 | public constructor(config: IRISDirect, resultSetRowLimit: number) { 38 | this.config = config; 39 | this.config.namespace = this.config.namespace.toUpperCase(); 40 | this.resultSetRowLimit = resultSetRowLimit; 41 | } 42 | 43 | public updateCookies(newCookies: string[]): void { 44 | const cookies = this.cookies; 45 | newCookies.forEach(cookie => { 46 | const [cookieName] = cookie.split("="); 47 | const index = cookies.findIndex(el => el.startsWith(cookieName)); 48 | if (index >= 0) { 49 | cookies[index] = cookie; 50 | } else { 51 | cookies.push(cookie); 52 | } 53 | }); 54 | this.cookies = cookies; 55 | } 56 | 57 | public async request( 58 | minVersion: number, 59 | method: string, 60 | path?: string, 61 | body?: any, 62 | params?: any, 63 | headers?: any 64 | ): Promise { 65 | const { https, host, port, pathPrefix, username, password } = this.config; 66 | if (minVersion > this._apiVersion) { 67 | return Promise.reject(`${path} not supported by API version ${this._apiVersion}`); 68 | } 69 | if (minVersion && minVersion > 0) { 70 | path = `v${this._apiVersion}/${path}`; 71 | } 72 | 73 | headers = { 74 | ...headers, 75 | Accept: "application/json", 76 | }; 77 | const buildParams = (): string => { 78 | if (!params) { 79 | return ""; 80 | } 81 | const result = []; 82 | Object.keys(params).forEach(key => { 83 | const value = params[key]; 84 | if (typeof value === "boolean") { 85 | result.push(`${key}=${value ? "1" : "0"}`); 86 | } else if (value && value !== "") { 87 | result.push(`${key}=${value}`); 88 | } 89 | }); 90 | return result.length ? "?" + result.join("&") : ""; 91 | }; 92 | method = method.toUpperCase(); 93 | if (["PUT", "POST"].includes(method) && !headers["Content-Type"]) { 94 | headers["Content-Type"] = "application/json"; 95 | } 96 | headers["Cache-Control"] = "no-cache"; 97 | 98 | const proto = https ? "https" : "http"; 99 | const agent = new (https ? httpsModule : httpModule).Agent({ 100 | keepAlive: true, 101 | maxSockets: 10, 102 | rejectUnauthorized: https, 103 | }); 104 | path = encodeURI(`${pathPrefix || ""}/api/atelier/${path || ""}${buildParams()}`); 105 | 106 | const cookies = this.cookies; 107 | let auth; 108 | if (cookies.length || method === "HEAD") { 109 | auth = Promise.resolve(cookies); 110 | } else if (!cookies.length) { 111 | auth = this.request(0, "HEAD"); 112 | } 113 | 114 | return auth.then(cookie => 115 | requestPromise({ 116 | agent, 117 | auth: { user: username, pass: password, sendImmediately: true }, 118 | body: ["PUT", "POST"].includes(method) ? body : null, 119 | headers: { 120 | ...headers, 121 | Cookie: cookie, 122 | }, 123 | json: true, 124 | method, 125 | resolveWithFullResponse: true, 126 | simple: true, 127 | uri: `${proto}://${host}:${port}${path}`, 128 | }) 129 | // .catch(error => error.error) 130 | .then(response => { 131 | this.updateCookies(response.headers["set-cookie"]) 132 | return response; 133 | }) 134 | .then(response => { 135 | if (method === "HEAD") { 136 | return this.cookies; 137 | } 138 | const data = response.body; 139 | /// deconde encoded content 140 | if (data.result && data.result.enc && data.result.content) { 141 | data.result.enc = false; 142 | data.result.content = Buffer.from(data.result.content.join(""), "base64"); 143 | } 144 | if (data.console) { 145 | // outputConsole(data.console); 146 | } 147 | if (data.result.status && data.result.status !== "") { 148 | // outputChannel.appendLine(data.result.status); 149 | throw new Error(data.result.status); 150 | } 151 | if (data.status.summary) { 152 | throw new Error(data.status.summary); 153 | } else if (data.result.status) { 154 | throw new Error(data.result.status); 155 | } else { 156 | return data; 157 | } 158 | }) 159 | .catch(error => { 160 | console.log('Error', error); 161 | throw error; 162 | }) 163 | ); 164 | } 165 | 166 | public async open() { 167 | const { namespace } = this.config; 168 | return this.request(0, "GET").then(info => { 169 | if (info && info.result && info.result.content && info.result.content.api > 0) { 170 | const data = info.result.content; 171 | if (!data.namespaces.includes(namespace)) { 172 | throw { 173 | code: "WrongNamespace", 174 | message: `This server does not have specified namespace '${namespace}'.\n 175 | You must select one of the following: ${data.namespaces.join(", ")}.`, 176 | }; 177 | } 178 | this._apiVersion = data.api; 179 | return info; 180 | } 181 | }); 182 | } 183 | 184 | public async close() { 185 | } 186 | 187 | public async query(query: string, parameters: string[]): Promise { 188 | console.log('SQL: ' + query); 189 | return this.request(1, "POST", `${this.config.namespace}/action/query`, { 190 | parameters, 191 | query, 192 | }, this._apiVersion >= 6 ? { positional: true , max: this.resultSetRowLimit > 0 ? this.resultSetRowLimit : undefined } : {}).then(data => data.result) 193 | } 194 | 195 | 196 | 197 | } -------------------------------------------------------------------------------- /src/ls/keywords.ts: -------------------------------------------------------------------------------- 1 | import { NSDatabase } from '@sqltools/types'; 2 | 3 | const keywordsArr = [ 4 | '%AFTERHAVING', 5 | '%ALLINDEX', 6 | '%ALPHAUP', 7 | '%ALTER', 8 | '%BEGTRANS', 9 | '%CHECKPRIV', 10 | '%CLASSNAME', 11 | '%CLASSPARAMETER', 12 | '%DBUGFULL', 13 | '%DELDATA', 14 | '%DESCRIPTION', 15 | '%EXACT', 16 | '%EXTERNAL', 17 | '%FILE', 18 | '%FIRSTTABLE', 19 | '%FLATTEN', 20 | '%FOREACH', 21 | '%FULL', 22 | '%ID', 23 | '%IDADDED', 24 | '%IGNOREINDEX', 25 | '%IGNOREINDICES', 26 | '%INLIST', 27 | '%INORDER', 28 | '%INTERNAL', 29 | '%INTEXT', 30 | '%INTRANS', 31 | '%INTRANSACTION', 32 | '%KEY', 33 | '%MATCHES', 34 | '%MCODE', 35 | '%MERGE', 36 | '%MINUS', 37 | '%MVR', 38 | '%NOCHECK', 39 | '%NODELDATA', 40 | '%NOFLATTEN', 41 | '%NOFPLAN', 42 | '%NOINDEX', 43 | '%NOLOCK', 44 | '%NOMERGE', 45 | '%NOPARALLEL', 46 | '%NOREDUCE', 47 | '%NORUNTIME', 48 | '%NOSVSO', 49 | '%NOTOPOPT', 50 | '%NOTRIGGER', 51 | '%NOUNIONOROPT', 52 | '%NUMROWS', 53 | '%ODBCIN', 54 | '%ODBCOUT', 55 | '%PARALLEL', 56 | '%PLUS', 57 | '%PROFILE', 58 | '%PROFILE_ALL', 59 | '%PUBLICROWID', 60 | '%ROUTINE', 61 | '%ROWCOUNT', 62 | '%RUNTIMEIN', 63 | '%RUNTIMEOUT', 64 | '%STARTSWITH', 65 | '%STARTTABLE', 66 | '%SQLSTRING', 67 | '%SQLUPPER', 68 | '%STRING', 69 | '%TABLENAME', 70 | '%TRUNCATE', 71 | '%UPPER', 72 | '%VALUE', 73 | '%VID', 74 | 'ABSOLUTE', 75 | 'ADD', 76 | 'ALL', 77 | 'ALLOCATE', 78 | 'ALTER', 79 | 'AND', 80 | 'ANY', 81 | 'ARE', 82 | 'AS', 83 | 'ASC', 84 | 'ASSERTION', 85 | 'AT', 86 | 'AUTHORIZATION', 87 | 'AVG', 88 | 'BEGIN', 89 | 'BETWEEN', 90 | 'BIT', 91 | 'BIT_LENGTH', 92 | 'BOTH', 93 | 'BY', 94 | 'CASCADE', 95 | 'CASE', 96 | 'CAST |', 97 | 'CHAR', 98 | 'CHARACTER', 99 | 'CHARACTER_LENGTH', 100 | 'CHAR_LENGTH', 101 | 'CHECK', 102 | 'CLOSE', 103 | 'COALESCE', 104 | 'COLLATE', 105 | 'COMMIT', 106 | 'CONNECT', 107 | 'CONNECTION', 108 | 'CONSTRAINT', 109 | 'CONSTRAINTS', 110 | 'CONTINUE', 111 | 'CONVERT', 112 | 'CORRESPONDING', 113 | 'COUNT', 114 | 'CREATE', 115 | 'CROSS', 116 | 'CURRENT', 117 | 'CURRENT_DATE', 118 | 'CURRENT_TIME', 119 | 'CURRENT_TIMESTAMP', 120 | 'CURRENT_USER', 121 | 'CURSOR', 122 | 'DATE', 123 | 'DEALLOCATE', 124 | 'DEC', 125 | 'DECIMAL', 126 | 'DECLARE', 127 | 'DEFAULT', 128 | 'DEFERRABLE', 129 | 'DEFERRED', 130 | 'DELETE', 131 | 'DESC', 132 | 'DESCRIBE', 133 | 'DESCRIPTOR', 134 | 'DIAGNOSTICS', 135 | 'DISCONNECT', 136 | 'DISTINCT', 137 | 'DOMAIN', 138 | 'DOUBLE', 139 | 'DROP', 140 | 'ELSE', 141 | 'END', 142 | 'ENDEXEC', 143 | 'ESCAPE', 144 | 'EXCEPT', 145 | 'EXCEPTION', 146 | 'EXEC', 147 | 'EXECUTE', 148 | 'EXISTS', 149 | 'EXTERNAL', 150 | 'EXTRACT', 151 | 'FALSE', 152 | 'FETCH', 153 | 'FIRST', 154 | 'FLOAT', 155 | 'FOR', 156 | 'FOREIGN', 157 | 'FOUND', 158 | 'FROM', 159 | 'FULL', 160 | 'GET', 161 | 'GLOBAL', 162 | 'GO', 163 | 'GOTO', 164 | 'GRANT', 165 | 'GROUP', 166 | 'HAVING', 167 | 'HOUR', 168 | 'IDENTITY', 169 | 'IMMEDIATE', 170 | 'IN', 171 | 'INDICATOR', 172 | 'INITIALLY', 173 | 'INNER', 174 | 'INPUT', 175 | 'INSENSITIVE', 176 | 'INSERT', 177 | 'INT', 178 | 'INTEGER', 179 | 'INTERSECT', 180 | 'INTERVAL', 181 | 'INTO', 182 | 'IS', 183 | 'ISOLATION', 184 | 'JOIN', 185 | 'LANGUAGE', 186 | 'LAST', 187 | 'LEADING', 188 | 'LEFT', 189 | 'LEVEL', 190 | 'LIKE', 191 | 'LOCAL', 192 | 'LOWER', 193 | 'MATCH', 194 | 'MAX', 195 | 'MIN', 196 | 'MINUTE', 197 | 'MODULE', 198 | 'NAMES', 199 | 'NATIONAL', 200 | 'NATURAL', 201 | 'NCHAR', 202 | 'NEXT', 203 | 'NO', 204 | 'NOT', 205 | 'NULL', 206 | 'NULLIF', 207 | 'NUMERIC', 208 | 'OCTET_LENGTH', 209 | 'OF', 210 | 'ON', 211 | 'ONLY', 212 | 'OPEN', 213 | 'OPTION', 214 | 'OR', 215 | 'OUTER', 216 | 'OUTPUT', 217 | 'OVERLAPS', 218 | 'PAD', 219 | 'PARTIAL', 220 | 'PREPARE', 221 | 'PRESERVE', 222 | 'PRIMARY', 223 | 'PRIOR', 224 | 'PRIVILEGES', 225 | 'PROCEDURE', 226 | 'PUBLIC', 227 | 'READ', 228 | 'REAL', 229 | 'REFERENCES', 230 | 'RELATIVE', 231 | 'RESTRICT', 232 | 'REVOKE', 233 | 'RIGHT', 234 | 'ROLE', 235 | 'ROLLBACK', 236 | 'ROWS', 237 | 'SCHEMA', 238 | 'SCROLL', 239 | 'SECOND', 240 | 'SECTION', 241 | 'SELECT', 242 | 'SESSION_USER', 243 | 'SET', 244 | 'SHARD', 245 | 'SMALLINT', 246 | 'SOME', 247 | 'SPACE', 248 | 'SQLERROR', 249 | 'SQLSTATE', 250 | 'STATISTICS', 251 | 'SUBSTRING', 252 | 'SUM', 253 | 'SYSDATE', 254 | 'SYSTEM_USER', 255 | 'TABLE', 256 | 'TEMPORARY', 257 | 'THEN', 258 | 'TIME', 259 | 'TIMEZONE_HOUR', 260 | 'TIMEZONE_MINUTE', 261 | 'TO', 262 | 'TOP', 263 | 'TRAILING', 264 | 'TRANSACTION', 265 | 'TRIM', 266 | 'TRUE', 267 | 'UNION', 268 | 'UNIQUE', 269 | 'UPDATE', 270 | 'UPPER', 271 | 'USER', 272 | 'USING', 273 | 'VALUES', 274 | 'VARCHAR', 275 | 'VARYING', 276 | 'WHEN', 277 | 'WHENEVER', 278 | 'WHERE', 279 | 'WITH', 280 | 'WORK', 281 | 'WRITE', 282 | ]; 283 | 284 | const keywordsCompletion: { [w: string]: NSDatabase.IStaticCompletion } = keywordsArr.reduce((agg, word) => { 285 | agg[word] = { 286 | label: word, 287 | detail: word, 288 | filterText: word, 289 | sortText: (['SELECT', 'CREATE', 'UPDATE', 'DELETE', 'FROM', 'INSERT', 'INTO'].includes(word) ? '2:' : '99:') + word, 290 | documentation: { 291 | value: `\`\`\`yaml\nWORD: ${word}\n\`\`\``, 292 | kind: 'markdown' 293 | } 294 | }; 295 | return agg; 296 | }, {}); 297 | 298 | export default keywordsCompletion; -------------------------------------------------------------------------------- /src/ls/plugin.ts: -------------------------------------------------------------------------------- 1 | import { ILanguageServerPlugin } from '@sqltools/types'; 2 | import IRISDriver from './driver'; 3 | import { DRIVER_ALIASES } from './../constants'; 4 | 5 | const InterSystemsPlugin: ILanguageServerPlugin = { 6 | register(server) { 7 | DRIVER_ALIASES.forEach(({ value }) => { 8 | server.getContext().drivers.set(value, IRISDriver as any); 9 | }); 10 | } 11 | } 12 | 13 | export default InterSystemsPlugin; 14 | -------------------------------------------------------------------------------- /src/ls/queries.ts: -------------------------------------------------------------------------------- 1 | import { ContextValue, NSDatabase, QueryBuilder } from '@sqltools/types'; 2 | import queryFactory from '@sqltools/base-driver/dist/lib/factory'; 3 | import { IQueries } from './irisdb'; 4 | 5 | const Functions = {}; 6 | Functions[ContextValue.TABLE] = "%SQL_MANAGER.TablesTree"; 7 | Functions[ContextValue.VIEW] = "%SQL_MANAGER.ViewsTree"; 8 | Functions[ContextValue.FUNCTION] = "%SQL_MANAGER.ProceduresTree"; 9 | 10 | const ValueColumn = {}; 11 | ValueColumn[ContextValue.TABLE] = "TABLE_NAME"; 12 | ValueColumn[ContextValue.VIEW] = "VIEW_NAME"; 13 | ValueColumn[ContextValue.FUNCTION] = "PROCEDURE_NAME"; 14 | 15 | interface ISchema extends NSDatabase.ISchema { 16 | showSystem: boolean; 17 | filter: string; 18 | } 19 | 20 | const describeTable: IQueries['describeTable'] = queryFactory` 21 | SELECT * FROM INFORMATION_SCHEMA.COLUMNS 22 | WHERE 23 | TABLE_NAME = '${p => p.label}' 24 | AND TABLE_SCHEMA = '${p => p.schema}' 25 | `; 26 | 27 | const fetchColumns: IQueries['fetchColumns'] = queryFactory` 28 | SELECT 29 | COLUMN_NAME AS label, 30 | '${ContextValue.COLUMN}' as type, 31 | TABLE_NAME AS "table", 32 | DATA_TYPE AS "dataType", 33 | UPPER(DATA_TYPE || ( 34 | CASE WHEN CHARACTER_MAXIMUM_LENGTH > 0 THEN ( 35 | '(' || CHARACTER_MAXIMUM_LENGTH || ')' 36 | ) ELSE '' END 37 | )) AS "detail", 38 | CHARACTER_MAXIMUM_LENGTH AS size, 39 | TABLE_SCHEMA AS "schema", 40 | COLUMN_DEFAULT AS "defaultValue", 41 | IS_NULLABLE AS "isNullable" 42 | FROM 43 | INFORMATION_SCHEMA.COLUMNS 44 | WHERE 45 | TABLE_SCHEMA = '${p => p.schema}' 46 | AND TABLE_NAME = '${p => p.label}' 47 | ORDER BY 48 | TABLE_NAME, 49 | ORDINAL_POSITION 50 | `; 51 | 52 | const searchColumns: IQueries['searchColumns'] = queryFactory` 53 | SELECT COLUMN_NAME AS label, 54 | '${ContextValue.COLUMN}' as type, 55 | TABLE_NAME AS "table", 56 | TABLE_SCHEMA AS "schema", 57 | DATA_TYPE AS "dataType", 58 | IS_NULLABLE AS "isNullable" 59 | FROM 60 | INFORMATION_SCHEMA.COLUMNS 61 | WHERE 1 = 1 62 | ${ 63 | p => p.search 64 | ? `AND ( 65 | LOWER(TABLE_NAME || '.' || COLUMN_NAME) LIKE '%${p.search.toLowerCase()}%' 66 | OR LOWER(COLUMN_NAME) LIKE '%${p.search.toLowerCase()}%' 67 | )` 68 | : '' 69 | } 70 | ${ 71 | p => p.tables ? 'AND (' + 72 | p.tables.map(t => `TABLE_NAME = '${t.label}' and TABLE_SCHEMA = '${t.database}'`).join(' OR ') 73 | + ")" : '' 74 | } 75 | ORDER BY 76 | TABLE_NAME, 77 | ORDINAL_POSITION 78 | `; 79 | 80 | const treeFunctionFilter = function(p: { [key: string]: any }): string { 81 | if (p.schema || p.search) { 82 | let filter = ", '"; 83 | if (p.schema) { 84 | filter += p.schema + "."; 85 | } 86 | if (p.search) { 87 | filter += p.search; 88 | } 89 | filter += "*'"; 90 | return filter; 91 | } 92 | return ""; 93 | } 94 | 95 | const fetchRecords: IQueries['fetchRecords'] = queryFactory` 96 | SELECT * FROM ( 97 | SELECT TOP ALL * 98 | FROM ${p => p.table.schema}.${p => (p.table.label || p.table)} 99 | ) 100 | WHERE %vid BETWEEN ${p => (p.offset || 0) + 1} AND ${p => ((p.offset || 0) + (p.limit || 50))} 101 | `; 102 | 103 | const countRecords: IQueries['countRecords'] = queryFactory` 104 | SELECT count(1) AS total 105 | FROM ${p => p.table.schema}.${p => (p.table.label || p.table)} 106 | `; 107 | 108 | const fetchAnyItems = (type: ContextValue): QueryBuilder => queryFactory` 109 | SELECT 110 | ${p => p.schema || p.search && p.search.includes('.') 111 | ? ` 112 | ${ValueColumn[type]} AS label, 113 | SCHEMA_NAME AS "schema", 114 | SCHEMA_NAME || '.' || ${ValueColumn[type]} AS "snippet", 115 | '${type}' AS "type", 116 | ${type == ContextValue.VIEW ? `'TRUE'` : 'NULL'} AS isView, 117 | '0:' || ${ValueColumn[type]} AS sortText 118 | ` 119 | : ` 120 | DISTINCT BY(SCHEMA_NAME) 121 | %EXACT(SCHEMA_NAME) AS label, 122 | %EXACT(SCHEMA_NAME) AS "schema", 123 | '${ContextValue.SCHEMA}' AS "type", 124 | '0:' || SCHEMA_NAME AS sortText 125 | ` 126 | } 127 | FROM ${Functions[type]}(${p => p.showSystem ? 1 : 0}${p => treeFunctionFilter(p)}) 128 | ORDER BY 129 | ${p => p.schema || p.search && p.search.includes('.') ? ValueColumn[type] : 'SCHEMA_NAME'} 130 | `; 131 | 132 | const fetchTables = fetchAnyItems(ContextValue.TABLE); 133 | const fetchViews = fetchAnyItems(ContextValue.VIEW); 134 | const fetchFunctions = fetchAnyItems(ContextValue.FUNCTION); 135 | 136 | const searchHelper = (p: { [key: string]: any }, type: ContextValue) => ` 137 | SELECT 138 | ${p.schema || p.search && p.search.includes('.') 139 | ? ` 140 | ${ValueColumn[type]} AS label, 141 | SCHEMA_NAME AS "schema", 142 | '${type}' AS "type", 143 | ${type == ContextValue.VIEW ? '1' : '0'} AS isView, 144 | '0:' || ${ValueColumn[type]} AS sortText 145 | ` 146 | : ` 147 | DISTINCT BY(SCHEMA_NAME) 148 | %EXACT(SCHEMA_NAME) AS label, 149 | %EXACT(SCHEMA_NAME) AS "schema", 150 | '${ContextValue.SCHEMA}' AS "type", 151 | '0:' || SCHEMA_NAME AS sortText 152 | ` 153 | } 154 | FROM ${Functions[type]}(${p.showSystem ? 1 : 0}${treeFunctionFilter(p)}) 155 | `; 156 | 157 | const searchTables: IQueries['searchTables'] = queryFactory` 158 | ${p => searchHelper(p, ContextValue.TABLE)} 159 | ORDER BY sortText 160 | `; 161 | 162 | const searchEverything: IQueries['searchEverything'] = queryFactory` 163 | ${p => searchHelper(p, ContextValue.TABLE)} 164 | UNION 165 | ${p => searchHelper(p, ContextValue.VIEW)} 166 | UNION 167 | ${p => searchHelper(p, ContextValue.FUNCTION)} 168 | ORDER BY sortText 169 | `; 170 | 171 | 172 | const fetchTypedSchemas = (type: ContextValue): IQueries['fetchSchemas'] => queryFactory` 173 | SELECT 174 | DISTINCT BY(SCHEMA_NAME) 175 | %EXACT(SCHEMA_NAME) AS label, 176 | %EXACT(SCHEMA_NAME) AS "schema", 177 | '${ContextValue.SCHEMA}' as "type", 178 | '${type}' as "childType", 179 | 'folder' as iconId 180 | FROM ${Functions[type]} (${p => p.showSystem ? 1 : 0}, '${p => (p.filter && p.filter != "") ? `${p.filter.replace("'", "''")}` : "*"}') 181 | `; 182 | 183 | const fetchTableSchemas = fetchTypedSchemas(ContextValue.TABLE); 184 | const fetchViewSchemas = fetchTypedSchemas(ContextValue.VIEW); 185 | const fetchFunctionSchemas = fetchTypedSchemas(ContextValue.FUNCTION); 186 | 187 | export default { 188 | describeTable, 189 | countRecords, 190 | fetchColumns, 191 | fetchRecords, 192 | fetchTables, 193 | fetchFunctions, 194 | fetchViews, 195 | searchTables, 196 | searchEverything, 197 | searchColumns, 198 | fetchTableSchemas, 199 | fetchViewSchemas, 200 | fetchFunctionSchemas, 201 | } 202 | -------------------------------------------------------------------------------- /test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | cache16: 4 | image: daimor/intersystems-cache:2016.2 5 | ports: 6 | - 57772:57772 7 | volumes: 8 | - ~/cache.key:/usr/cachesys/mgr/cache.key 9 | cache18: 10 | image: daimor/intersystems-cache:2018.1 11 | ports: 12 | - 57773:57772 13 | volumes: 14 | - ~/cache.key:/usr/cachesys/mgr/cache.key 15 | iris: 16 | image: intersystemsdc/iris-community:preview 17 | ports: 18 | - 52773:52773 19 | command: 20 | - -a 21 | - iris session iris -U %SYS '##class(Security.Users).UnExpireUserPasswords("*")' 22 | -------------------------------------------------------------------------------- /test/project.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".", 5 | "name": "root" 6 | } 7 | ], 8 | "settings": { 9 | "sqltools.useNodeRuntime": true, 10 | "sqltools.format": { 11 | "reservedWordCase": "upper", 12 | "linesBetweenQueries": "preserve" 13 | }, 14 | "sqltools.debug": { 15 | "namespaces": "*" 16 | }, 17 | "sqltools.connections": [ 18 | { 19 | "askForPassword": false, 20 | "connectionMethod": "Server and Port", 21 | "driver": "InterSystems IRIS", 22 | "name": "Caché 2016.2", 23 | "namespace": "SAMPLES", 24 | "password": "SYS", 25 | "port": 57772, 26 | "previewLimit": 50, 27 | "server": "localhost", 28 | "showSystem": false, 29 | "username": "_SYSTEM" 30 | }, 31 | { 32 | "askForPassword": false, 33 | "connectionMethod": "Server and Port", 34 | "driver": "InterSystems IRIS", 35 | "name": "Caché 2018.1", 36 | "namespace": "SAMPLES", 37 | "password": "SYS", 38 | "port": 57773, 39 | "previewLimit": 50, 40 | "server": "localhost", 41 | "showSystem": false, 42 | "username": "_SYSTEM" 43 | }, 44 | { 45 | "namespace": "USER", 46 | "connectionMethod": "Server and Port", 47 | "showSystem": false, 48 | "filter": "'Ens*", 49 | "previewLimit": 50, 50 | "server": "localhost", 51 | "port": 52773, 52 | "https": false, 53 | "askForPassword": false, 54 | "driver": "InterSystems IRIS", 55 | "name": "InterSystems IRIS", 56 | "password": "SYS", 57 | "username": "_SYSTEM" 58 | } 59 | ] 60 | } 61 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "noUnusedLocals": true, 6 | "noUnusedParameters": true, 7 | "experimentalDecorators": true, 8 | "lib": ["es6"], 9 | "module": "commonjs", 10 | "noEmit": false, 11 | "removeComments": true, 12 | "resolveJsonModule": true, 13 | "sourceMap": true, 14 | "skipLibCheck": true, 15 | "declaration": false, 16 | "target": "es2017", 17 | "outDir": "out" 18 | }, 19 | "include": [ 20 | "src/**/*" 21 | ], 22 | "exclude": [ 23 | "node_modules" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /ui.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "ui:order": ["database"], 3 | "database": { "ui:widget": "file" } 4 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 10 | 11 | entry: { 12 | extension: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 13 | "ls/plugin": './src/ls/plugin.ts' 14 | }, 15 | output: { 16 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 17 | path: path.resolve(__dirname, 'dist'), 18 | // filename: 'extension.js', 19 | chunkFilename: '[id].js', 20 | libraryTarget: 'commonjs2', 21 | devtoolModuleFilenameTemplate: '../[resource-path]' 22 | }, 23 | devtool: 'source-map', 24 | externals: { 25 | 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/ 26 | }, 27 | resolve: { 28 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 29 | extensions: ['.ts', '.js'] 30 | }, 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.ts$/, 35 | exclude: /node_modules/, 36 | use: [ 37 | { 38 | loader: 'ts-loader' 39 | } 40 | ] 41 | } 42 | ] 43 | } 44 | }; 45 | module.exports = config; --------------------------------------------------------------------------------